feat(website): redesign the mobile experience
Replace the stacked desktop layout with a phone-first product flow featuring an image-led hero, inline privacy proof, compact semantic search, swipeable feature cards with live pagination, expandable technical details, and touch-friendly navigation and calls to action. Add the standalone Phokus aperture SVG asset.
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="Phokus">
|
||||||
|
<title>Phokus</title>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Phokus app mark — a camera iris.
|
||||||
|
The hexagon in the middle is the lens OPENING; each blade edge sweeps from a
|
||||||
|
corner of the opening out to the rim, all leaning the same way (the pinwheel).
|
||||||
|
|
||||||
|
Tuning knobs (edit and re-open in any browser/Figma):
|
||||||
|
* opening roundness -> the "52" radius in the opening <path> arcs.
|
||||||
|
smaller (toward 30) = rounder/softer hole; larger (toward 90) = flatter, sharper.
|
||||||
|
* blade curve -> the "110" radius in each blade <path>.
|
||||||
|
smaller = more pronounced curve; larger = straighter blade.
|
||||||
|
* blade sweep amount -> the +40deg baked into the blade endpoint (70.71,-84.27).
|
||||||
|
* curve DIRECTION -> the final flag in each "A r r 0 0 X" command (0<->1 flips the bow).
|
||||||
|
* weight -> stroke-width on the <g>.
|
||||||
|
* color -> stroke on the <g>; swap "#e9e9ec" for currentColor to inherit.
|
||||||
|
A brand-accent focal dot is provided at the bottom (commented out).
|
||||||
|
-->
|
||||||
|
|
||||||
|
<rect x="8" y="8" width="240" height="240" rx="52" fill="#0e0f14"/>
|
||||||
|
|
||||||
|
<g transform="translate(128 128)" fill="none" stroke="#e9e9ec" stroke-width="9"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
|
||||||
|
<!-- outer rim -->
|
||||||
|
<circle r="110"/>
|
||||||
|
|
||||||
|
<!-- lens opening: soft, arc-rounded hexagon -->
|
||||||
|
<path d="M0,-46 A52,52 0 0 0 39.84,-23 A52,52 0 0 0 39.84,23 A52,52 0 0 0 0,46 A52,52 0 0 0 -39.84,23 A52,52 0 0 0 -39.84,-23 A52,52 0 0 0 0,-46 Z"/>
|
||||||
|
|
||||||
|
<!-- blades: one curved edge, swept six times -->
|
||||||
|
<g transform="rotate(0)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(60)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(120)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(180)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(240)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
<g transform="rotate(300)"><path d="M0,-46 A110,110 0 0 1 70.71,-84.27"/></g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- optional brand focal point -->
|
||||||
|
<!-- <circle cx="128" cy="128" r="9" fill="#e6a23c"/> -->
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
+311
-37
@@ -1,3 +1,4 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
import { PhokusMark } from "./components/PhokusMark";
|
import { PhokusMark } from "./components/PhokusMark";
|
||||||
// Screenshots are 2560×1600 masters; imagetools downscales + transcodes to
|
// Screenshots are 2560×1600 masters; imagetools downscales + transcodes to
|
||||||
// AVIF/WebP at build (see the Shot helper). `as=picture` must be the last query
|
// AVIF/WebP at build (see the Shot helper). `as=picture` must be the last query
|
||||||
@@ -58,10 +59,10 @@ function Shot({
|
|||||||
function Header() {
|
function Header() {
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-edge bg-canvas/70 backdrop-blur-md">
|
<header className="sticky top-0 z-50 border-b border-edge bg-canvas/70 backdrop-blur-md">
|
||||||
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-6">
|
<div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-5 lg:h-16 lg:px-6">
|
||||||
<a href="#top" className="flex items-center gap-2.5 text-ink">
|
<a href="#top" className="flex items-center gap-2.5 text-ink">
|
||||||
<PhokusMark className="h-6 w-6 text-amber" />
|
<PhokusMark className="h-5 w-5 text-amber lg:h-6 lg:w-6" />
|
||||||
<span className="font-display text-lg font-semibold tracking-tight">Phokus</span>
|
<span className="font-display font-semibold tracking-tight lg:text-lg">Phokus</span>
|
||||||
</a>
|
</a>
|
||||||
<nav className="hidden items-center gap-8 text-sm text-muted md:flex">
|
<nav className="hidden items-center gap-8 text-sm text-muted md:flex">
|
||||||
<a href="#search" className="transition-colors hover:text-ink">Search</a>
|
<a href="#search" className="transition-colors hover:text-ink">Search</a>
|
||||||
@@ -71,21 +72,133 @@ function Header() {
|
|||||||
</nav>
|
</nav>
|
||||||
<a
|
<a
|
||||||
href={DOWNLOAD_URL}
|
href={DOWNLOAD_URL}
|
||||||
className="rounded-lg border border-edge-strong px-4 py-2 text-sm font-medium text-ink transition-colors hover:bg-surface"
|
className="hidden rounded-lg border border-edge-strong px-4 py-2 text-sm font-medium text-ink transition-colors hover:bg-surface md:block"
|
||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href={REPO_URL}
|
||||||
|
aria-label="View Phokus source on GitHub"
|
||||||
|
className="grid h-9 w-9 place-items-center rounded-full border border-edge-strong text-muted transition-colors hover:bg-surface hover:text-ink md:hidden"
|
||||||
|
>
|
||||||
|
<GitHubIcon className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MobileChapterNav() {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
aria-label="Page sections"
|
||||||
|
className="sticky top-14 z-40 flex gap-1 overflow-x-auto border-b border-edge bg-canvas/90 px-4 py-2 backdrop-blur-md lg:hidden"
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
["Search", "#search"],
|
||||||
|
["Features", "#features"],
|
||||||
|
["Details", "#tech"],
|
||||||
|
["Download", "#download"],
|
||||||
|
].map(([label, href]) => (
|
||||||
|
<a
|
||||||
|
key={href}
|
||||||
|
href={href}
|
||||||
|
className="shrink-0 rounded-full px-3 py-1.5 text-xs font-medium text-muted transition-colors hover:bg-surface-2 hover:text-ink"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileChapter({
|
||||||
|
number,
|
||||||
|
label,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
number: string;
|
||||||
|
label: string;
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<div className="flex items-center gap-3 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-faint">
|
||||||
|
<span className="font-mono text-amber">{number}</span>
|
||||||
|
<span className="h-px w-8 bg-edge-strong" />
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<h2 className="mt-3 max-w-[20rem] font-display text-[1.75rem] font-semibold leading-[1.08] tracking-tight text-ink">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Hero() {
|
function Hero() {
|
||||||
return (
|
return (
|
||||||
<section id="top" className="relative overflow-hidden lg:flex lg:min-h-[86vh] lg:items-center">
|
<section id="top" className="relative overflow-hidden">
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<div className="relative h-[34svh] min-h-64 overflow-hidden border-b border-edge">
|
||||||
|
<Shot
|
||||||
|
pic={heroShot}
|
||||||
|
alt="The Phokus gallery showing a dense library of landscape photos."
|
||||||
|
className="absolute inset-0 h-full w-full object-cover object-[67%_top]"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-canvas/20 via-transparent to-canvas" />
|
||||||
|
<div className="absolute inset-x-5 top-5 flex items-center justify-between">
|
||||||
|
<span className="rounded-full border border-white/10 bg-black/45 px-3 py-1.5 text-[0.68rem] font-medium text-white/80 backdrop-blur">
|
||||||
|
Your folders. Your machine.
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1.5 text-[0.68rem] text-white/60">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-amber" />
|
||||||
|
Windows
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative -mt-7 px-5 pb-12">
|
||||||
|
<h1 className="max-w-[21rem] font-display text-[2.45rem] font-semibold leading-[0.98] tracking-[-0.035em] text-ink">
|
||||||
|
See everything.
|
||||||
|
<br />
|
||||||
|
Find anything.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 max-w-[22rem] text-[0.95rem] leading-6 text-muted">
|
||||||
|
Search and curate the images and videos already on your computer. Everything stays local.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<a
|
||||||
|
href={DOWNLOAD_URL}
|
||||||
|
className="flex min-h-12 items-center justify-between rounded-xl bg-amber px-5 text-sm font-semibold text-canvas transition-colors hover:bg-amber-soft"
|
||||||
|
>
|
||||||
|
Download for Windows
|
||||||
|
<span aria-hidden="true">↓</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 grid grid-cols-3 divide-x divide-edge border-y border-edge py-4 text-center">
|
||||||
|
<span className="px-2 text-[0.68rem] leading-4 text-faint">
|
||||||
|
<strong className="block font-medium text-ink">Local</strong>
|
||||||
|
no uploads
|
||||||
|
</span>
|
||||||
|
<span className="px-2 text-[0.68rem] leading-4 text-faint">
|
||||||
|
<strong className="block font-medium text-ink">Private</strong>
|
||||||
|
no account
|
||||||
|
</span>
|
||||||
|
<span className="px-2 text-[0.68rem] leading-4 text-faint">
|
||||||
|
<strong className="block font-medium text-ink">Offline</strong>
|
||||||
|
after setup
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative hidden min-h-[86vh] items-center lg:flex">
|
||||||
{/* Product leads: the gallery dominates the right and bleeds off the frame,
|
{/* Product leads: the gallery dominates the right and bleeds off the frame,
|
||||||
cropped so the workspace appears to continue beyond the viewport. */}
|
cropped so the workspace appears to continue beyond the viewport. */}
|
||||||
<div className="pointer-events-none absolute inset-y-0 right-0 hidden w-[62%] lg:block">
|
<div className="pointer-events-none absolute inset-y-0 right-0 w-[62%]">
|
||||||
<Shot
|
<Shot
|
||||||
pic={heroShot}
|
pic={heroShot}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -97,13 +210,13 @@ function Hero() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pointer-events-none absolute -left-24 top-8 -z-10 h-96 w-96 rounded-full bg-amber/[0.06] blur-3xl" />
|
<div className="pointer-events-none absolute -left-24 top-8 -z-10 h-96 w-96 rounded-full bg-amber/[0.06] blur-3xl" />
|
||||||
|
|
||||||
<div className="relative mx-auto w-full max-w-7xl px-6 py-16 sm:py-20 lg:py-24">
|
<div className="relative mx-auto w-full max-w-7xl px-6 py-24">
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<p className="mb-6 inline-flex items-center gap-2 rounded-full border border-edge bg-surface/80 px-3 py-1 text-xs font-medium text-muted backdrop-blur">
|
<p className="mb-6 inline-flex items-center gap-2 rounded-full border border-edge bg-surface/80 px-3 py-1 text-xs font-medium text-muted backdrop-blur">
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-amber" />
|
<span className="h-1.5 w-1.5 rounded-full bg-amber" />
|
||||||
Local-first · Windows desktop
|
Local-first · Windows desktop
|
||||||
</p>
|
</p>
|
||||||
<h1 className="font-display text-[2.5rem] font-semibold leading-[1.06] tracking-tight text-ink sm:text-6xl sm:leading-[1.04]">
|
<h1 className="font-display text-6xl font-semibold leading-[1.04] tracking-tight text-ink">
|
||||||
Your local media library,
|
Your local media library,
|
||||||
<br />
|
<br />
|
||||||
made searchable.
|
made searchable.
|
||||||
@@ -132,14 +245,6 @@ function Hero() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile/tablet: product sits below the copy, still bleeding off the right. */}
|
|
||||||
<div className="-mr-6 mt-2 overflow-hidden rounded-l-xl border-y border-l border-edge lg:hidden">
|
|
||||||
<Shot
|
|
||||||
pic={heroShot}
|
|
||||||
alt="The Phokus gallery: a virtualized grid of landscape photos with star ratings, a folder sidebar, filter tabs, and a search bar."
|
|
||||||
className="block w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -161,9 +266,9 @@ function LocalFirst() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<section id="privacy" className="relative overflow-hidden border-y border-edge bg-surface">
|
<section id="privacy" className="relative hidden overflow-hidden border-y border-edge bg-surface lg:block">
|
||||||
<EdgeMark side="left" />
|
<EdgeMark side="left" />
|
||||||
<div className="relative mx-auto max-w-7xl px-6 py-20 sm:py-28">
|
<div className="relative mx-auto max-w-7xl px-6 py-28">
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<SectionEyebrow label="Local-first" />
|
<SectionEyebrow label="Local-first" />
|
||||||
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
@@ -200,7 +305,31 @@ function SearchSection() {
|
|||||||
return (
|
return (
|
||||||
<section id="search" className="relative overflow-hidden">
|
<section id="search" className="relative overflow-hidden">
|
||||||
<EdgeMark side="right" />
|
<EdgeMark side="right" />
|
||||||
<div className="relative mx-auto max-w-7xl px-6 py-20 sm:py-28">
|
<div className="relative mx-auto max-w-7xl px-5 py-12 lg:px-6 lg:py-28">
|
||||||
|
<MobileChapter number="02" label="Semantic search" title="Describe the image. Skip the filename.">
|
||||||
|
<div className="mt-6 flex min-h-11 items-center gap-3 rounded-xl border border-edge-strong bg-surface px-4 shadow-lg shadow-black/20">
|
||||||
|
<span className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-xs text-amber">/s</span>
|
||||||
|
<span className="text-sm text-ink">seaside</span>
|
||||||
|
<span className="ml-auto text-xs text-faint">200 matches</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative mt-3 h-80 overflow-hidden rounded-2xl border border-edge bg-surface shadow-2xl shadow-black/40">
|
||||||
|
<Shot
|
||||||
|
pic={semanticShot}
|
||||||
|
alt="Coastal photos returned for a semantic search for seaside."
|
||||||
|
className="h-full w-full object-cover object-[65%_center]"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-black/80 to-transparent" />
|
||||||
|
<p className="absolute bottom-4 left-4 right-4 text-xs leading-5 text-white/70">
|
||||||
|
Coastlines, waves, shells and dunes, matched by visual meaning.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="mt-5 text-sm leading-6 text-muted">
|
||||||
|
Search by mood, place, subject, or visual idea. CLIP runs on-device, so it still works
|
||||||
|
offline and needs no manual tags.
|
||||||
|
</p>
|
||||||
|
</MobileChapter>
|
||||||
|
|
||||||
|
<div className="hidden lg:block">
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<SectionEyebrow label="Search by meaning" />
|
<SectionEyebrow label="Search by meaning" />
|
||||||
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
@@ -223,6 +352,131 @@ function SearchSection() {
|
|||||||
className="block w-full"
|
className="block w-full"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileFeatureDeck() {
|
||||||
|
const [activeCard, setActiveCard] = useState(0);
|
||||||
|
const cardRefs = useRef<Array<HTMLElement | null>>([]);
|
||||||
|
|
||||||
|
function updateActiveCard(event: React.UIEvent<HTMLDivElement>) {
|
||||||
|
const deckCenter = event.currentTarget.scrollLeft + event.currentTarget.clientWidth / 2;
|
||||||
|
let nearestIndex = 0;
|
||||||
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
cardRefs.current.forEach((card, index) => {
|
||||||
|
if (!card) return;
|
||||||
|
const cardCenter = card.offsetLeft + card.offsetWidth / 2;
|
||||||
|
const distance = Math.abs(cardCenter - deckCenter);
|
||||||
|
if (distance < nearestDistance) {
|
||||||
|
nearestDistance = distance;
|
||||||
|
nearestIndex = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setActiveCard(nearestIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCard(index: number) {
|
||||||
|
cardRefs.current[index]?.scrollIntoView({
|
||||||
|
behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="features" className="overflow-hidden border-y border-edge bg-surface py-12 lg:hidden">
|
||||||
|
<div className="px-5">
|
||||||
|
<MobileChapter number="03" label="More than search" title="Three ways to move through a library.">
|
||||||
|
<p className="mt-4 text-sm leading-6 text-muted">
|
||||||
|
Swipe through the core workflows. Each one stays focused on the media, not app chrome.
|
||||||
|
</p>
|
||||||
|
</MobileChapter>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onScroll={updateActiveCard}
|
||||||
|
className="mt-7 flex snap-x snap-mandatory gap-3 overflow-x-auto px-5 pb-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
|
>
|
||||||
|
<article
|
||||||
|
ref={(node) => { cardRefs.current[0] = node; }}
|
||||||
|
className="w-[78vw] max-w-[19rem] shrink-0 snap-center overflow-hidden rounded-2xl border border-edge bg-canvas"
|
||||||
|
>
|
||||||
|
<div className="h-64 overflow-hidden">
|
||||||
|
<Shot
|
||||||
|
pic={exploreShot}
|
||||||
|
alt="Visual clusters arranged across the Explore canvas."
|
||||||
|
className="h-full w-full object-cover object-[54%_center]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<span className="font-mono text-[0.65rem] text-amber">EXPLORE</span>
|
||||||
|
<h3 className="mt-2 font-display text-xl font-semibold text-ink">See visual clusters.</h3>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-muted">
|
||||||
|
Wander through related shots, then switch to a chronological timeline.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article
|
||||||
|
ref={(node) => { cardRefs.current[1] = node; }}
|
||||||
|
className="w-[78vw] max-w-[19rem] shrink-0 snap-center overflow-hidden rounded-2xl border border-edge bg-canvas"
|
||||||
|
>
|
||||||
|
<div className="h-64 overflow-hidden">
|
||||||
|
<Shot
|
||||||
|
pic={lightboxShot}
|
||||||
|
alt="A waterfall open in the Phokus lightbox."
|
||||||
|
className="h-full w-full object-cover object-[37%_center]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<span className="font-mono text-[0.65rem] text-amber">CURATE</span>
|
||||||
|
<h3 className="mt-2 font-display text-xl font-semibold text-ink">Review without leaving.</h3>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-muted">
|
||||||
|
Rate, favourite, tag, zoom, play video, and find similar images in place.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article
|
||||||
|
ref={(node) => { cardRefs.current[2] = node; }}
|
||||||
|
className="w-[78vw] max-w-[19rem] shrink-0 snap-center overflow-hidden rounded-2xl border border-edge bg-canvas"
|
||||||
|
>
|
||||||
|
<div className="h-64 overflow-hidden">
|
||||||
|
<Shot
|
||||||
|
pic={duplicatesShot}
|
||||||
|
alt="Duplicate image pairs selected for safe cleanup."
|
||||||
|
className="h-full w-full object-cover object-[24%_top]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<span className="font-mono text-[0.65rem] text-amber">CLEANUP</span>
|
||||||
|
<h3 className="mt-2 font-display text-xl font-semibold text-ink">Compare exact pairs.</h3>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-muted">
|
||||||
|
Confirm byte-identical files and choose what to remove. Nothing is automatic.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 px-5 text-[0.68rem] text-faint">
|
||||||
|
{[0, 1, 2].map((index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
type="button"
|
||||||
|
aria-label={`Show feature ${index + 1}`}
|
||||||
|
aria-current={activeCard === index ? "true" : undefined}
|
||||||
|
onClick={() => showCard(index)}
|
||||||
|
className={`h-1.5 rounded-full transition-[width,background-color] ${
|
||||||
|
activeCard === index ? "w-8 bg-amber" : "w-1.5 bg-edge-strong"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<span className="ml-1">Swipe</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -271,9 +525,9 @@ function EdgeMark({ side }: { side: "left" | "right" }) {
|
|||||||
|
|
||||||
function ExploreSection() {
|
function ExploreSection() {
|
||||||
return (
|
return (
|
||||||
<section id="explore" className="relative overflow-hidden border-t border-edge bg-surface">
|
<section id="explore" className="relative hidden overflow-hidden border-t border-edge bg-surface lg:block">
|
||||||
<EdgeMark side="left" />
|
<EdgeMark side="left" />
|
||||||
<div className="relative mx-auto max-w-7xl px-6 py-20 sm:py-28">
|
<div className="relative mx-auto max-w-7xl px-5 py-16 lg:px-6 lg:py-28">
|
||||||
<div className="grid items-center gap-10 lg:grid-cols-12">
|
<div className="grid items-center gap-10 lg:grid-cols-12">
|
||||||
<div className="lg:col-span-5">
|
<div className="lg:col-span-5">
|
||||||
<SectionEyebrow label="Explore & timeline" />
|
<SectionEyebrow label="Explore & timeline" />
|
||||||
@@ -308,9 +562,9 @@ function ExploreSection() {
|
|||||||
|
|
||||||
function CurateSection() {
|
function CurateSection() {
|
||||||
return (
|
return (
|
||||||
<section id="curate" className="relative overflow-hidden">
|
<section id="curate" className="relative hidden overflow-hidden lg:block">
|
||||||
<EdgeMark side="right" />
|
<EdgeMark side="right" />
|
||||||
<div className="relative mx-auto max-w-7xl px-6 py-20 sm:py-28">
|
<div className="relative mx-auto max-w-7xl px-5 py-16 lg:px-6 lg:py-28">
|
||||||
<div className="grid items-center gap-10 lg:grid-cols-12">
|
<div className="grid items-center gap-10 lg:grid-cols-12">
|
||||||
<figure className="order-2 overflow-hidden rounded-xl border border-edge bg-surface shadow-2xl shadow-black/50 lg:order-1 lg:col-span-7">
|
<figure className="order-2 overflow-hidden rounded-xl border border-edge bg-surface shadow-2xl shadow-black/50 lg:order-1 lg:col-span-7">
|
||||||
<Shot
|
<Shot
|
||||||
@@ -345,9 +599,9 @@ function CurateSection() {
|
|||||||
|
|
||||||
function CleanupSection() {
|
function CleanupSection() {
|
||||||
return (
|
return (
|
||||||
<section id="cleanup" className="relative overflow-hidden border-t border-edge bg-surface">
|
<section id="cleanup" className="relative hidden overflow-hidden border-t border-edge bg-surface lg:block">
|
||||||
<EdgeMark side="left" />
|
<EdgeMark side="left" />
|
||||||
<div className="relative mx-auto max-w-7xl px-6 py-20 sm:py-28">
|
<div className="relative mx-auto max-w-7xl px-5 py-16 lg:px-6 lg:py-28">
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<SectionEyebrow label="Clean up safely" />
|
<SectionEyebrow label="Clean up safely" />
|
||||||
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
@@ -381,7 +635,23 @@ function TechFactsSection() {
|
|||||||
{ k: "License & build", v: "MIT, open source. The installer is currently unsigned — see the note below." },
|
{ k: "License & build", v: "MIT, open source. The installer is currently unsigned — see the note below." },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<section id="tech" className="relative mx-auto max-w-7xl px-6 py-20 sm:py-28">
|
<section id="tech" className="relative mx-auto max-w-7xl px-5 py-12 lg:px-6 lg:py-28">
|
||||||
|
<MobileChapter number="04" label="Technical details" title="The practical bits.">
|
||||||
|
<div className="mt-6 divide-y divide-edge border-y border-edge">
|
||||||
|
{facts.map((fact, index) => (
|
||||||
|
<details key={fact.k} className="group py-1">
|
||||||
|
<summary className="flex min-h-12 cursor-pointer list-none items-center gap-3 py-2 text-sm font-medium text-ink">
|
||||||
|
<span className="font-mono text-[0.68rem] text-faint">0{index + 1}</span>
|
||||||
|
{fact.k}
|
||||||
|
<span className="ml-auto text-lg font-light text-amber transition-transform group-open:rotate-45">+</span>
|
||||||
|
</summary>
|
||||||
|
<p className="pb-5 pl-8 text-sm leading-6 text-muted">{fact.v}</p>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</MobileChapter>
|
||||||
|
|
||||||
|
<div className="hidden lg:block">
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<SectionEyebrow label="The technical facts" />
|
<SectionEyebrow label="The technical facts" />
|
||||||
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
<h2 className="mt-4 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
||||||
@@ -396,6 +666,7 @@ function TechFactsSection() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -404,30 +675,31 @@ function DownloadSection() {
|
|||||||
return (
|
return (
|
||||||
<section id="download" className="relative overflow-hidden border-t border-edge bg-surface">
|
<section id="download" className="relative overflow-hidden border-t border-edge bg-surface">
|
||||||
<EdgeMark side="right" />
|
<EdgeMark side="right" />
|
||||||
<div className="relative mx-auto max-w-3xl px-6 py-20 text-center sm:py-28">
|
<div className="relative mx-auto max-w-3xl px-5 py-12 text-left lg:px-6 lg:py-28 lg:text-center">
|
||||||
<PhokusMark className="mx-auto h-12 w-12 text-ink" dotClassName="fill-amber" />
|
<PhokusMark className="h-10 w-10 text-ink lg:mx-auto lg:h-12 lg:w-12" dotClassName="fill-amber" />
|
||||||
<h2 className="mt-6 font-display text-3xl font-semibold tracking-tight text-ink sm:text-4xl">
|
<h2 className="mt-6 max-w-xs font-display text-[2rem] font-semibold leading-tight tracking-tight text-ink lg:mx-auto lg:max-w-none lg:text-4xl">
|
||||||
Point Phokus at a folder.
|
Point Phokus at a folder.
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-lg leading-relaxed text-muted">
|
<p className="mt-4 max-w-md text-base leading-7 text-muted lg:mx-auto lg:text-lg lg:leading-relaxed">
|
||||||
Free and open source. Your library is indexed on your machine — the download is just the app.
|
Turn a folder full of files into a library you can actually explore.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
<div className="mt-8 grid gap-3 lg:flex lg:flex-wrap lg:justify-center">
|
||||||
<a
|
<a
|
||||||
href={DOWNLOAD_URL}
|
href={DOWNLOAD_URL}
|
||||||
className="rounded-lg bg-amber px-6 py-3 text-sm font-medium text-canvas transition-colors hover:bg-amber-soft"
|
className="flex min-h-12 items-center justify-between rounded-xl bg-amber px-5 text-sm font-semibold text-canvas transition-colors hover:bg-amber-soft lg:min-h-0 lg:justify-center lg:rounded-lg lg:px-6 lg:py-3 lg:font-medium"
|
||||||
>
|
>
|
||||||
Download for Windows
|
Download for Windows
|
||||||
|
<span className="lg:hidden" aria-hidden="true">↓</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={REPO_URL}
|
href={REPO_URL}
|
||||||
className="inline-flex items-center gap-2 rounded-lg border border-edge-strong px-6 py-3 text-sm font-medium text-ink transition-colors hover:bg-surface-2"
|
className="flex min-h-12 items-center justify-between rounded-xl border border-edge-strong px-5 text-sm font-medium text-ink transition-colors hover:bg-surface-2 lg:min-h-0 lg:justify-center lg:gap-2 lg:rounded-lg lg:px-6 lg:py-3"
|
||||||
>
|
>
|
||||||
<GitHubIcon className="h-4 w-4" />
|
|
||||||
View source
|
View source
|
||||||
|
<GitHubIcon className="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p className="mx-auto mt-10 max-w-xl rounded-lg border border-edge bg-canvas/60 p-4 text-left text-sm leading-relaxed text-muted">
|
<p className="mt-8 max-w-xl border-l border-amber/50 pl-4 text-left text-xs leading-5 text-muted lg:mx-auto lg:mt-10 lg:rounded-lg lg:border lg:border-edge lg:bg-canvas/60 lg:p-4 lg:text-sm lg:leading-relaxed">
|
||||||
<span className="font-medium text-ink">Heads up:</span> this build isn't code-signed yet, so
|
<span className="font-medium text-ink">Heads up:</span> this build isn't code-signed yet, so
|
||||||
Windows SmartScreen will warn that the publisher is unrecognised — choose{" "}
|
Windows SmartScreen will warn that the publisher is unrecognised — choose{" "}
|
||||||
<span className="text-ink">More info → Run anyway</span>. An NVIDIA CUDA build is on the
|
<span className="text-ink">More info → Run anyway</span>. An NVIDIA CUDA build is on the
|
||||||
@@ -441,7 +713,7 @@ function DownloadSection() {
|
|||||||
function Footer() {
|
function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer>
|
<footer>
|
||||||
<div className="mx-auto flex max-w-7xl flex-col gap-4 px-6 py-10 text-sm text-faint sm:flex-row sm:items-center sm:justify-between">
|
<div className="mx-auto flex max-w-7xl flex-col gap-5 px-5 py-8 text-xs text-faint lg:flex-row lg:items-center lg:justify-between lg:px-6 lg:py-10 lg:text-sm">
|
||||||
<div className="flex items-center gap-2 text-muted">
|
<div className="flex items-center gap-2 text-muted">
|
||||||
<PhokusMark className="h-5 w-5 text-amber" />
|
<PhokusMark className="h-5 w-5 text-amber" />
|
||||||
<span className="font-display font-medium text-ink">Phokus</span>
|
<span className="font-display font-medium text-ink">Phokus</span>
|
||||||
@@ -462,10 +734,12 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header />
|
<Header />
|
||||||
|
<MobileChapterNav />
|
||||||
<main>
|
<main>
|
||||||
<Hero />
|
<Hero />
|
||||||
<LocalFirst />
|
<LocalFirst />
|
||||||
<SearchSection />
|
<SearchSection />
|
||||||
|
<MobileFeatureDeck />
|
||||||
<ExploreSection />
|
<ExploreSection />
|
||||||
<CurateSection />
|
<CurateSection />
|
||||||
<CleanupSection />
|
<CleanupSection />
|
||||||
|
|||||||
@@ -47,11 +47,18 @@ body {
|
|||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:focus-visible {
|
a:focus-visible,
|
||||||
|
summary:focus-visible {
|
||||||
outline: 2px solid var(--color-amber);
|
outline: 2px solid var(--color-amber);
|
||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 63.999rem) {
|
||||||
|
section[id] {
|
||||||
|
scroll-margin-top: 6.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
html {
|
html {
|
||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user