fix: improve light theme onboarding controls
Make the onboarding tour theme-aware across the app themes, add the first-run theme picker, and keep fake media previews on the dark media surface. Update light-mode secondary controls in onboarding, settings, dropdowns, toolbar controls, and duplicate actions so they no longer render as dark buttons on subtle-light.
This commit is contained in:
@@ -43,10 +43,10 @@ export function OnboardingOverlay() {
|
||||
const isLast = onboardingStep >= STEPS.length - 1;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/65 backdrop-blur-sm">
|
||||
<div className="flex max-h-[min(85vh,760px)] w-[min(90vw,720px)] flex-col rounded-lg border border-white/10 bg-[#07080f] shadow-2xl shadow-black/60">
|
||||
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/65 backdrop-blur-sm light-theme:bg-black/40">
|
||||
<div className="flex max-h-[min(85vh,760px)] w-[min(90vw,720px)] flex-col rounded-lg border border-white/10 bg-gray-950 shadow-2xl shadow-black/60 light-theme:border-gray-300/70">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-white/[0.07] px-7 py-4">
|
||||
<div className="flex items-center justify-between border-b border-white/[0.07] px-7 py-4 light-theme:border-gray-300/70">
|
||||
<div>
|
||||
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">
|
||||
Step {onboardingStep + 1} of {STEPS.length}
|
||||
@@ -59,7 +59,11 @@ export function OnboardingOverlay() {
|
||||
key={s.id}
|
||||
aria-label={s.title}
|
||||
className={`h-1.5 rounded-full transition-all ${
|
||||
i === onboardingStep ? "w-5 bg-white/70" : i < onboardingStep ? "w-1.5 bg-white/35" : "w-1.5 bg-white/15"
|
||||
i === onboardingStep
|
||||
? "w-5 bg-white/70 light-theme:bg-gray-700"
|
||||
: i < onboardingStep
|
||||
? "w-1.5 bg-white/35 light-theme:bg-gray-400"
|
||||
: "w-1.5 bg-white/15 light-theme:bg-gray-300"
|
||||
}`}
|
||||
onClick={() => setOnboardingStep(i)}
|
||||
/>
|
||||
@@ -83,23 +87,23 @@ export function OnboardingOverlay() {
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between border-t border-white/[0.07] px-7 py-4">
|
||||
<div className="flex items-center justify-between border-t border-white/[0.07] px-7 py-4 light-theme:border-gray-300/70">
|
||||
<button
|
||||
className="rounded-md border border-transparent px-3 py-1.5 text-xs text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-gray-200"
|
||||
className="rounded-md border border-transparent px-3 py-1.5 text-xs text-gray-500 transition-colors hover:bg-white/[0.06] hover:text-gray-200 light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white light-theme:hover:bg-gray-800 light-theme:hover:text-white"
|
||||
onClick={completeOnboarding}
|
||||
>
|
||||
Skip tour
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
|
||||
className="rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-45 light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white light-theme:hover:bg-gray-800 light-theme:hover:text-white"
|
||||
onClick={() => setOnboardingStep(onboardingStep - 1)}
|
||||
disabled={isFirst}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md border border-emerald-400/35 bg-emerald-500/15 px-4 py-1.5 text-xs text-emerald-200 transition-colors hover:bg-emerald-500/25"
|
||||
className="rounded-md border border-emerald-400/35 bg-emerald-500/15 px-4 py-1.5 text-xs text-emerald-200 transition-colors hover:bg-emerald-500/25 light-theme:border-emerald-600/50 light-theme:bg-emerald-100 light-theme:text-emerald-700 light-theme:hover:bg-emerald-200"
|
||||
onClick={() => (isLast ? completeOnboarding() : setOnboardingStep(onboardingStep + 1))}
|
||||
>
|
||||
{isLast ? "Finish" : "Next"}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function StepAddLibrary() {
|
||||
added, renamed, or removed. Nothing is moved or copied.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 flex items-center justify-between gap-6 border-y border-white/[0.05] py-4">
|
||||
<div className="mt-5 flex items-center justify-between gap-6 border-y border-white/[0.05] py-4 light-theme:border-gray-300/70">
|
||||
<div className="min-w-0">
|
||||
{folders.length > 0 ? (
|
||||
<>
|
||||
@@ -49,10 +49,10 @@ export function StepAddLibrary() {
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{addError ? <p className="mt-1.5 text-xs text-amber-300/90">{addError}</p> : null}
|
||||
{addError ? <p className="mt-1.5 text-xs text-amber-300/90 light-theme:text-amber-700">{addError}</p> : null}
|
||||
</div>
|
||||
<button
|
||||
className="shrink-0 rounded-md border border-emerald-400/35 bg-emerald-500/15 px-3 py-1.5 text-xs text-emerald-200 transition-colors hover:bg-emerald-500/25 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
className="shrink-0 rounded-md border border-emerald-400/35 bg-emerald-500/15 px-3 py-1.5 text-xs text-emerald-200 transition-colors hover:bg-emerald-500/25 disabled:cursor-not-allowed disabled:opacity-45 light-theme:border-emerald-600/50 light-theme:bg-emerald-100 light-theme:text-emerald-700 light-theme:hover:bg-emerald-200"
|
||||
onClick={() => void handlePick()}
|
||||
disabled={adding}
|
||||
>
|
||||
@@ -64,7 +64,7 @@ export function StepAddLibrary() {
|
||||
As indexing runs, the gallery fills in roughly like this — tiles appear immediately and sharpen
|
||||
as thumbnails are generated:
|
||||
</p>
|
||||
<div className="mt-3 grid grid-cols-6 gap-1.5">
|
||||
<div className="media-dark-surface mt-3 grid grid-cols-6 gap-1.5">
|
||||
<FakeTile index={0} />
|
||||
<FakeTile index={1} favorite />
|
||||
<FakeTile index={2} duration="0:42" />
|
||||
|
||||
@@ -37,18 +37,18 @@ export function StepAiFeatures() {
|
||||
</p>
|
||||
|
||||
<h4 className="mt-6 text-[12px] font-semibold uppercase tracking-[0.08em] text-gray-400">AI tagging — optional</h4>
|
||||
<div className="mt-1 divide-y divide-white/[0.05]">
|
||||
<div className="mt-1 divide-y divide-white/[0.05] light-theme:divide-gray-300/70">
|
||||
<div className="py-4">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-white">Automatic tags for every image</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-gray-500">
|
||||
The WD tagger model (~1.3 GB download) labels images so you can search with{" "}
|
||||
<code className="rounded bg-white/[0.07] px-1 py-0.5 text-[11px] text-gray-200">/t</code> — tags look like:
|
||||
<code className="rounded bg-gray-900 px-1 py-0.5 text-[11px] text-gray-200 light-theme:bg-gray-800 light-theme:text-gray-100">/t</code> — tags look like:
|
||||
</p>
|
||||
<span className="mt-2 flex flex-wrap gap-1.5">
|
||||
{FAKE_TAGS.map((tag) => (
|
||||
<span key={tag} className="rounded-md border border-white/10 bg-white/[0.04] px-2 py-0.5 text-[11px] text-gray-400">
|
||||
<span key={tag} className="rounded-md border border-white/10 bg-gray-900/50 px-2 py-0.5 text-[11px] text-gray-400 light-theme:border-gray-300/70 light-theme:bg-gray-900 light-theme:text-gray-600">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
@@ -56,12 +56,12 @@ export function StepAiFeatures() {
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
{taggerReady ? (
|
||||
<span className="inline-flex rounded-md border border-emerald-400/25 bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-300">
|
||||
<span className="inline-flex rounded-md border border-emerald-400/25 bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-300 light-theme:border-emerald-600/40 light-theme:bg-emerald-100 light-theme:text-emerald-700">
|
||||
Installed
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
className="rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
|
||||
className="rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-45 light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white light-theme:hover:bg-gray-800 light-theme:hover:text-white"
|
||||
onClick={() => void prepareTaggerModel()}
|
||||
disabled={taggerModelPreparing}
|
||||
>
|
||||
@@ -84,7 +84,7 @@ export function StepAiFeatures() {
|
||||
</div>
|
||||
) : null}
|
||||
{!taggerModelPreparing && taggerModelError ? (
|
||||
<p className="mt-2 text-xs leading-relaxed text-amber-300/90">
|
||||
<p className="mt-2 text-xs leading-relaxed text-amber-300/90 light-theme:text-amber-700">
|
||||
Download failed: {taggerModelError}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -92,11 +92,11 @@ export function StepAiFeatures() {
|
||||
</div>
|
||||
|
||||
<h4 className="mt-6 text-[12px] font-semibold uppercase tracking-[0.08em] text-gray-400">Semantic search & similarity — built in</h4>
|
||||
<div className="mt-1 divide-y divide-white/[0.05]">
|
||||
<div className="mt-1 divide-y divide-white/[0.05] light-theme:divide-gray-300/70">
|
||||
<div className="py-4">
|
||||
<p className="text-sm text-white">Search by meaning, find look-alikes</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-gray-500">
|
||||
Powers <code className="rounded bg-white/[0.07] px-1 py-0.5 text-[11px] text-gray-200">/s</code> search,
|
||||
Powers <code className="rounded bg-gray-900 px-1 py-0.5 text-[11px] text-gray-200 light-theme:bg-gray-800 light-theme:text-gray-100">/s</code> search,
|
||||
"find similar", and the Explore view, so it's part of the standard pipeline: the CLIP model
|
||||
(~580 MB) downloads automatically the first time embeddings run. Nothing to do — you'll see it
|
||||
in the background-tasks bar.
|
||||
|
||||
@@ -37,14 +37,14 @@ export function StepGalleryPreview() {
|
||||
carry your favorites, star ratings, and video durations; hover for filename and quick actions.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 grid grid-cols-4 gap-1.5">
|
||||
<div className="media-dark-surface mt-5 grid grid-cols-4 gap-1.5">
|
||||
{TILE_PROPS.map((props, i) => (
|
||||
<FakeTile key={i} index={i} loaded={i < revealed} {...props} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-start justify-between gap-4">
|
||||
<div className="divide-y divide-white/[0.05] text-xs leading-relaxed text-gray-500">
|
||||
<div className="divide-y divide-white/[0.05] text-xs leading-relaxed text-gray-500 light-theme:divide-gray-300/70">
|
||||
<p className="pb-2.5">
|
||||
<span className="text-gray-300">Click any tile</span> to open the lightbox — keyboard navigation,
|
||||
zoom, inline tag editing, ratings, and a full video player.
|
||||
|
||||
@@ -49,7 +49,7 @@ export function StepPipeline() {
|
||||
</p>
|
||||
|
||||
{/* Fake BackgroundTasks slim bar */}
|
||||
<div className="mt-3 rounded-lg border border-white/[0.07] bg-white/[0.02] px-4 py-3">
|
||||
<div className="mt-3 rounded-lg border border-white/[0.07] bg-gray-900/30 px-4 py-3 light-theme:border-gray-300/70 light-theme:bg-gray-900">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="relative flex h-1.5 w-1.5 shrink-0">
|
||||
{!finished ? (
|
||||
@@ -57,7 +57,7 @@ export function StepPipeline() {
|
||||
) : null}
|
||||
<span className={`relative inline-flex h-1.5 w-1.5 rounded-full ${finished ? "bg-emerald-400" : "bg-blue-400"}`} />
|
||||
</span>
|
||||
<span className="text-[13px] font-medium text-white/60">Holiday Photos</span>
|
||||
<span className="text-[13px] font-medium text-white/60 light-theme:text-gray-500">Holiday Photos</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{STAGES.map((stage, i) => (
|
||||
<FakeStageTag
|
||||
@@ -72,7 +72,7 @@ export function StepPipeline() {
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-start justify-between gap-4">
|
||||
<div className="divide-y divide-white/[0.05] text-xs leading-relaxed text-gray-500">
|
||||
<div className="divide-y divide-white/[0.05] text-xs leading-relaxed text-gray-500 light-theme:divide-gray-300/70">
|
||||
<p className="pb-2.5">
|
||||
<span className="text-gray-300">It's all interruptible.</span> Close the app whenever you like —
|
||||
the queue picks up where it left off next launch.
|
||||
|
||||
@@ -64,12 +64,12 @@ export function StepSearchDemo() {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-sm leading-relaxed text-gray-300">
|
||||
One search bar, three modes — picked by prefix. No prefix searches filenames, <code className="rounded bg-white/[0.07] px-1 py-0.5 text-[11px] text-gray-200">/s</code> searches
|
||||
by meaning, <code className="rounded bg-white/[0.07] px-1 py-0.5 text-[11px] text-gray-200">/t</code> searches tags.
|
||||
One search bar, three modes — picked by prefix. No prefix searches filenames, <code className="rounded bg-gray-900 px-1 py-0.5 text-[11px] text-gray-200 light-theme:bg-gray-800 light-theme:text-gray-100">/s</code> searches
|
||||
by meaning, <code className="rounded bg-gray-900 px-1 py-0.5 text-[11px] text-gray-200 light-theme:bg-gray-800 light-theme:text-gray-100">/t</code> searches tags.
|
||||
</p>
|
||||
|
||||
{/* Mock search bar */}
|
||||
<div className="mt-5 flex items-center gap-2.5 rounded-lg border border-white/10 bg-white/[0.04] px-3.5 py-2.5">
|
||||
<div className="mt-5 flex items-center gap-2.5 rounded-lg border border-white/10 bg-gray-900/50 px-3.5 py-2.5 light-theme:border-gray-300/70 light-theme:bg-gray-900">
|
||||
<svg className="h-4 w-4 shrink-0 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||
</svg>
|
||||
@@ -77,7 +77,7 @@ export function StepSearchDemo() {
|
||||
{demo.query.slice(0, typed)}
|
||||
{!finished ? <span className="animate-pulse text-gray-500">|</span> : null}
|
||||
</span>
|
||||
<span className="ml-auto shrink-0 rounded-md border border-sky-400/25 bg-sky-500/10 px-2 py-0.5 text-[11px] font-medium text-sky-300">
|
||||
<span className="ml-auto shrink-0 rounded-md border border-sky-400/25 bg-sky-500/10 px-2 py-0.5 text-[11px] font-medium text-sky-300 light-theme:border-sky-600/40 light-theme:bg-sky-100 light-theme:text-sky-700">
|
||||
{demo.mode}
|
||||
</span>
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@ export function StepSearchDemo() {
|
||||
demo's visible state. */}
|
||||
<div
|
||||
key={demoIndex}
|
||||
className={`mt-3 flex flex-wrap justify-center gap-1.5 transition-opacity duration-300 ${fullyTyped ? "opacity-100" : "opacity-0"}`}
|
||||
className={`media-dark-surface mt-3 flex flex-wrap justify-center gap-1.5 transition-opacity duration-300 ${fullyTyped ? "opacity-100" : "opacity-0"}`}
|
||||
>
|
||||
{demo.results.map((src, i) => (
|
||||
<div key={i} className="aspect-square w-36 overflow-hidden rounded-xl bg-white/[0.04]">
|
||||
|
||||
@@ -24,9 +24,9 @@ export function StepUpdates() {
|
||||
</p>
|
||||
|
||||
{/* Mini app-window mockup: the lit mark shown in a real title bar. */}
|
||||
<div className="mt-3 overflow-hidden rounded-lg border border-white/10 bg-gray-950 shadow-lg">
|
||||
<div className="flex items-center gap-2 border-b border-white/[0.06] px-3 py-2">
|
||||
<div className="relative flex h-6 w-6 items-center justify-center rounded-md bg-white/[0.08] text-gray-300">
|
||||
<div className="mt-3 overflow-hidden rounded-lg border border-white/10 bg-gray-950 shadow-lg light-theme:border-gray-300/70">
|
||||
<div className="flex items-center gap-2 border-b border-white/[0.06] px-3 py-2 light-theme:border-gray-300/70">
|
||||
<div className="relative flex h-6 w-6 items-center justify-center rounded-md bg-gray-900 text-gray-300 light-theme:bg-gray-800">
|
||||
<span className="pointer-events-none absolute left-1/2 top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-400/30 blur-[3px]" />
|
||||
<span className="pointer-events-none absolute left-1/2 top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-400/55 animate-ping" />
|
||||
<PhokusMark className="relative h-[18px] w-[18px]" dotClassName="fill-amber-400" />
|
||||
@@ -48,16 +48,16 @@ export function StepUpdates() {
|
||||
|
||||
{/* Ghosted window body, just enough to read as the app. */}
|
||||
<div className="flex h-[72px] bg-gray-900/30">
|
||||
<div className="w-14 shrink-0 border-r border-white/[0.05] p-2.5">
|
||||
<div className="h-1.5 w-full rounded bg-white/[0.07]" />
|
||||
<div className="mt-2 h-1.5 w-3/4 rounded bg-white/[0.04]" />
|
||||
<div className="mt-2 h-1.5 w-3/4 rounded bg-white/[0.04]" />
|
||||
<div className="w-14 shrink-0 border-r border-white/[0.05] p-2.5 light-theme:border-gray-300/70">
|
||||
<div className="h-1.5 w-full rounded bg-gray-800" />
|
||||
<div className="mt-2 h-1.5 w-3/4 rounded bg-gray-900" />
|
||||
<div className="mt-2 h-1.5 w-3/4 rounded bg-gray-900" />
|
||||
</div>
|
||||
<div className="flex-1 p-2.5">
|
||||
<div className="h-2 w-20 rounded bg-white/[0.06]" />
|
||||
<div className="h-2 w-20 rounded bg-gray-800" />
|
||||
<div className="mt-2.5 grid grid-cols-5 gap-1.5">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="h-7 rounded bg-white/[0.04]" />
|
||||
<div key={i} className="h-7 rounded bg-gray-900" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ function ExplorePreview() {
|
||||
{ size: 16, x: 70, y: 44, i: 6 },
|
||||
];
|
||||
return (
|
||||
<div className="relative h-[72px] w-32 overflow-hidden rounded-lg border border-white/[0.07] bg-white/[0.02]">
|
||||
<div className="media-dark-surface relative h-[72px] w-32 overflow-hidden rounded-lg border border-white/[0.07] bg-white/[0.02]">
|
||||
{blobs.map((blob, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
@@ -36,7 +36,7 @@ function ExplorePreview() {
|
||||
|
||||
function TimelinePreview() {
|
||||
return (
|
||||
<div className="flex h-[72px] w-32 flex-col justify-center gap-2 rounded-lg border border-white/[0.07] bg-white/[0.02] px-3">
|
||||
<div className="media-dark-surface flex h-[72px] w-32 flex-col justify-center gap-2 rounded-lg border border-white/[0.07] bg-white/[0.02] px-3">
|
||||
{[2024, 2023].map((year, row) => (
|
||||
<div key={year} className="flex items-center gap-1.5">
|
||||
<span className="w-7 text-[9px] tabular-nums text-gray-600">{year}</span>
|
||||
@@ -51,7 +51,7 @@ function TimelinePreview() {
|
||||
|
||||
function DuplicatesPreview() {
|
||||
return (
|
||||
<div className="flex h-[72px] w-32 items-center justify-center gap-1.5 rounded-lg border border-white/[0.07] bg-white/[0.02]">
|
||||
<div className="media-dark-surface flex h-[72px] w-32 items-center justify-center gap-1.5 rounded-lg border border-white/[0.07] bg-white/[0.02]">
|
||||
<div className="w-10">
|
||||
<FakeTile index={3} className="rounded-md" />
|
||||
</div>
|
||||
@@ -69,7 +69,7 @@ export function StepViews() {
|
||||
<p className="text-sm leading-relaxed text-gray-300">
|
||||
Beyond the main grid, the sidebar switches between three more ways to look at your library:
|
||||
</p>
|
||||
<div className="mt-3 divide-y divide-white/[0.05]">
|
||||
<div className="mt-3 divide-y divide-white/[0.05] light-theme:divide-gray-300/70">
|
||||
<ViewRow
|
||||
title="Explore"
|
||||
description="A visual cluster map and tag cloud — browse by theme instead of folder, and jump into any cluster."
|
||||
|
||||
@@ -1,6 +1,52 @@
|
||||
import { useGalleryStore } from "../../store";
|
||||
import { AppTheme, useGalleryStore } from "../../store";
|
||||
import { FakeProgressBar, formatBytes } from "./fakes";
|
||||
|
||||
const THEME_OPTIONS: {
|
||||
value: AppTheme;
|
||||
name: string;
|
||||
colors: {
|
||||
background: string;
|
||||
surface: string;
|
||||
text: string;
|
||||
muted: string;
|
||||
accent: string;
|
||||
};
|
||||
}[] = [
|
||||
{
|
||||
value: "phokus",
|
||||
name: "Phokus",
|
||||
colors: {
|
||||
background: "#030712",
|
||||
surface: "#111827",
|
||||
text: "#f9fafb",
|
||||
muted: "#6b7280",
|
||||
accent: "#10b981",
|
||||
},
|
||||
},
|
||||
{
|
||||
value: "conventional-dark",
|
||||
name: "Conventional Dark",
|
||||
colors: {
|
||||
background: "#1f1f1f",
|
||||
surface: "#303030",
|
||||
text: "#a3a3a3",
|
||||
muted: "#666666",
|
||||
accent: "#10b981",
|
||||
},
|
||||
},
|
||||
{
|
||||
value: "subtle-light",
|
||||
name: "Subtle Light",
|
||||
colors: {
|
||||
background: "#e9e7e2",
|
||||
surface: "#dedbd4",
|
||||
text: "#18202c",
|
||||
muted: "#817b72",
|
||||
accent: "#059669",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function FfmpegStatusRow() {
|
||||
const ffmpegStatus = useGalleryStore((s) => s.ffmpegStatus);
|
||||
const ffmpegProgress = useGalleryStore((s) => s.ffmpegProgress);
|
||||
@@ -10,7 +56,7 @@ export function FfmpegStatusRow() {
|
||||
if (ffmpegStatus === "installed") {
|
||||
return (
|
||||
<div className="flex items-center gap-2.5 py-3">
|
||||
<svg className="h-4 w-4 shrink-0 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<svg className="h-4 w-4 shrink-0 text-emerald-400 light-theme:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
@@ -27,14 +73,14 @@ export function FfmpegStatusRow() {
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-white">Media engine download failed</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-amber-300/90">{ffmpegError}</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-amber-300/90 light-theme:text-amber-700">{ffmpegError}</p>
|
||||
<p className="mt-1.5 text-xs leading-relaxed text-gray-500">
|
||||
You can keep going — photos work without it. Video thumbnails and metadata will start
|
||||
automatically once the download succeeds.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="shrink-0 rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white"
|
||||
className="shrink-0 rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white light-theme:hover:bg-gray-800 light-theme:hover:text-white"
|
||||
onClick={() => void retryFfmpegDownload()}
|
||||
>
|
||||
Retry download
|
||||
@@ -71,6 +117,9 @@ export function FfmpegStatusRow() {
|
||||
}
|
||||
|
||||
export function StepWelcome() {
|
||||
const theme = useGalleryStore((s) => s.theme);
|
||||
const setTheme = useGalleryStore((s) => s.setTheme);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="text-sm leading-relaxed text-gray-300">
|
||||
@@ -83,8 +132,40 @@ export function StepWelcome() {
|
||||
from Settings.
|
||||
</p>
|
||||
|
||||
<h4 className="mt-7 text-[12px] font-semibold uppercase tracking-[0.08em] text-gray-400">Choose your look</h4>
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
{THEME_OPTIONS.map((option) => {
|
||||
const selected = option.value === theme;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`rounded-md border p-2 text-left transition-colors ${
|
||||
selected
|
||||
? "border-emerald-400/35 bg-emerald-500/15 text-emerald-200 ring-1 ring-emerald-400/40 light-theme:border-emerald-600/50 light-theme:bg-emerald-100 light-theme:text-emerald-700 light-theme:hover:bg-emerald-200"
|
||||
: "border-white/10 bg-white/[0.055] text-gray-300 hover:bg-white/10 hover:text-white light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white light-theme:hover:bg-gray-800 light-theme:hover:text-white"
|
||||
}`}
|
||||
onClick={() => setTheme(option.value)}
|
||||
>
|
||||
<span
|
||||
className="block overflow-hidden rounded border border-black/20 p-1.5"
|
||||
style={{ backgroundColor: option.colors.background }}
|
||||
>
|
||||
<span className="mb-1 block h-2 w-8 rounded-sm" style={{ backgroundColor: option.colors.accent }} />
|
||||
<span className="block rounded-sm p-1.5" style={{ backgroundColor: option.colors.surface }}>
|
||||
<span className="block h-1.5 w-9 rounded-sm" style={{ backgroundColor: option.colors.text }} />
|
||||
<span className="mt-1 block h-1.5 w-14 rounded-sm" style={{ backgroundColor: option.colors.muted }} />
|
||||
<span className="mt-1 block h-1.5 w-10 rounded-sm" style={{ backgroundColor: option.colors.muted }} />
|
||||
</span>
|
||||
</span>
|
||||
<span className="mt-2 block text-[11px] font-medium">{option.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h4 className="mt-7 text-[12px] font-semibold uppercase tracking-[0.08em] text-gray-400">First-time setup</h4>
|
||||
<div className="mt-1 divide-y divide-white/[0.05]">
|
||||
<div className="mt-1 divide-y divide-white/[0.05] light-theme:divide-gray-300/70">
|
||||
<FfmpegStatusRow />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,7 @@ export function FakeTile({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={`group relative aspect-square overflow-hidden rounded-xl bg-white/[0.04] ${className}`}>
|
||||
<div className={`media-dark-surface group relative aspect-square overflow-hidden rounded-xl bg-white/[0.04] ${className}`}>
|
||||
{loaded ? (
|
||||
<img src={fakeImage(index)} alt="" loading="lazy" className="absolute inset-0 h-full w-full object-cover" />
|
||||
) : (
|
||||
@@ -116,16 +116,16 @@ export function FakeTile({
|
||||
export function FakeStageTag({ label, state }: { label: string; state: "active" | "done" | "waiting" }) {
|
||||
const className =
|
||||
state === "active"
|
||||
? "bg-white/5 text-gray-300"
|
||||
? "bg-gray-900 text-gray-300 light-theme:bg-gray-900 light-theme:text-gray-300"
|
||||
: state === "done"
|
||||
? "bg-emerald-500/10 text-emerald-400"
|
||||
: "bg-white/4 text-gray-600";
|
||||
? "bg-emerald-500/10 text-emerald-400 light-theme:bg-emerald-100 light-theme:text-emerald-700"
|
||||
: "bg-gray-900/60 text-gray-600 light-theme:bg-gray-800 light-theme:text-gray-500";
|
||||
return <span className={`rounded-md px-2 py-0.5 text-[11px] ${className}`}>{label}</span>;
|
||||
}
|
||||
|
||||
export function FakeProgressBar({ fraction, className = "" }: { fraction: number | null; className?: string }) {
|
||||
return (
|
||||
<div className={`h-px overflow-hidden rounded-full bg-white/8 ${className}`}>
|
||||
<div className={`h-px overflow-hidden rounded-full bg-gray-200/60 light-theme:bg-gray-300 ${className}`}>
|
||||
{fraction === null ? (
|
||||
<div className="h-full w-full animate-pulse bg-blue-500/40" />
|
||||
) : (
|
||||
@@ -143,7 +143,7 @@ export function ReplayButton({ onClick, label = "Replay" }: { onClick: () => voi
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-200"
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-200 light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white light-theme:hover:bg-gray-800 light-theme:hover:text-white"
|
||||
>
|
||||
<svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12a7.5 7.5 0 0012.9 5.3M19.5 12A7.5 7.5 0 006.6 6.7M4.5 6.5v3h3M19.5 17.5v-3h-3" />
|
||||
|
||||
Reference in New Issue
Block a user