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:
2026-06-17 22:07:35 +01:00
parent 9047c8053a
commit c97fec2eb3
15 changed files with 176 additions and 91 deletions
+3 -3
View File
@@ -193,7 +193,7 @@ export function DuplicateFinder() {
{/* Batch select — only shown when there are groups and nothing is selected yet */} {/* Batch select — only shown when there are groups and nothing is selected yet */}
{hasResults && selectedCount === 0 && !deleting && ( {hasResults && selectedCount === 0 && !deleting && (
<button <button
className="rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs text-gray-400 transition-colors hover:bg-white/[0.07] hover:text-white" className="rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs text-gray-400 transition-colors hover:bg-white/[0.07] 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={selectKeepFirstAllGroups} onClick={selectKeepFirstAllGroups}
title={`Mark ${totalDuplicateImages} duplicate${totalDuplicateImages === 1 ? "" : "s"} for deletion across all groups (keeps first in each)`} title={`Mark ${totalDuplicateImages} duplicate${totalDuplicateImages === 1 ? "" : "s"} for deletion across all groups (keeps first in each)`}
> >
@@ -204,7 +204,7 @@ export function DuplicateFinder() {
<> <>
<span className="text-[11px] text-white/40">{selectedCount} marked for deletion</span> <span className="text-[11px] text-white/40">{selectedCount} marked for deletion</span>
<button <button
className="rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs text-gray-400 transition-colors hover:bg-white/[0.07] hover:text-white disabled:opacity-40" className="rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs text-gray-400 transition-colors hover:bg-white/[0.07] hover:text-white disabled:opacity-40 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={clearDuplicateSelection} onClick={clearDuplicateSelection}
disabled={deleting} disabled={deleting}
> >
@@ -220,7 +220,7 @@ export function DuplicateFinder() {
</> </>
) : null} ) : null}
<button <button
className="rounded-lg 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-40" className="rounded-lg 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-40 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={() => { setDeleteResult(null); void scanDuplicates(selectedFolderId); }} onClick={() => { setDeleteResult(null); void scanDuplicates(selectedFolderId); }}
disabled={duplicateScanning} disabled={duplicateScanning}
> >
+5 -5
View File
@@ -38,8 +38,8 @@ export function FolderScopeDropdown() {
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
className={`flex max-w-56 items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs transition-colors ${ className={`flex max-w-56 items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs transition-colors ${
open open
? "border-white/15 bg-white/8 text-white" ? "border-white/15 bg-white/8 text-white light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white"
: "border-white/8 bg-transparent text-gray-400 hover:border-white/15 hover:text-gray-200" : "border-white/8 bg-transparent text-gray-400 hover:border-white/15 hover:text-gray-200 light-theme:border-gray-700/40 light-theme:text-gray-600 light-theme:hover:border-gray-700 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
title="Change folder scope" title="Change folder scope"
> >
@@ -55,10 +55,10 @@ export function FolderScopeDropdown() {
</svg> </svg>
</button> </button>
{open ? ( {open ? (
<div className="absolute right-0 top-full z-30 mt-1.5 max-h-80 min-w-52 overflow-y-auto rounded-xl border border-white/10 bg-gray-950/98 p-1 shadow-2xl backdrop-blur"> <div className="absolute right-0 top-full z-30 mt-1.5 max-h-80 min-w-52 overflow-y-auto rounded-xl border border-white/10 bg-gray-950/98 p-1 shadow-2xl backdrop-blur light-theme:border-gray-700/50">
<button <button
className={`flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors ${ className={`flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors ${
selectedFolderId === null ? "bg-white/6 text-white" : "text-gray-400 hover:bg-white/5 hover:text-white" selectedFolderId === null ? "bg-white/6 text-white light-theme:bg-gray-900 light-theme:text-white" : "text-gray-400 hover:bg-white/5 hover:text-white light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
onClick={() => select(null)} onClick={() => select(null)}
> >
@@ -75,7 +75,7 @@ export function FolderScopeDropdown() {
<button <button
key={folder.id} key={folder.id}
className={`flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors ${ className={`flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2 text-left text-sm transition-colors ${
active ? "bg-white/6 text-white" : "text-gray-400 hover:bg-white/5 hover:text-white" active ? "bg-white/6 text-white light-theme:bg-gray-900 light-theme:text-white" : "text-gray-400 hover:bg-white/5 hover:text-white light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
onClick={() => select(folder.id)} onClick={() => select(folder.id)}
> >
+12 -12
View File
@@ -90,7 +90,7 @@ function ScopeButton({ scope, current, onSelect, children }: {
className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${ className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${
active active
? "border-emerald-400/35 bg-emerald-500/15 text-emerald-200 light-theme:border-emerald-600/50 light-theme:bg-emerald-100 light-theme:text-emerald-700" ? "border-emerald-400/35 bg-emerald-500/15 text-emerald-200 light-theme:border-emerald-600/50 light-theme:bg-emerald-100 light-theme:text-emerald-700"
: "border-transparent text-gray-500 hover:bg-white/[0.06] hover:text-gray-200 light-theme:text-gray-600 light-theme:hover:bg-black/[0.06] light-theme:hover:text-gray-900" : "border-transparent text-gray-500 hover:bg-white/[0.06] hover:text-gray-200 light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
onClick={() => onSelect(scope)} onClick={() => onSelect(scope)}
> >
@@ -112,7 +112,7 @@ function TaggerAccelerationButton({ acceleration, current, onSelect, children }:
className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${ className={`rounded-md border px-3 py-1.5 text-xs transition-colors ${
active active
? "border-emerald-400/35 bg-emerald-500/15 text-emerald-200 light-theme:border-emerald-600/50 light-theme:bg-emerald-100 light-theme:text-emerald-700" ? "border-emerald-400/35 bg-emerald-500/15 text-emerald-200 light-theme:border-emerald-600/50 light-theme:bg-emerald-100 light-theme:text-emerald-700"
: "border-transparent text-gray-500 hover:bg-white/[0.06] hover:text-gray-200 light-theme:text-gray-600 light-theme:hover:bg-black/[0.06] light-theme:hover:text-gray-900" : "border-transparent text-gray-500 hover:bg-white/[0.06] hover:text-gray-200 light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
onClick={() => onSelect(acceleration)} onClick={() => onSelect(acceleration)}
> >
@@ -371,7 +371,7 @@ export function SettingsModal() {
{taggerReady ? ( {taggerReady ? (
<> <>
<button <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 light-theme:border-gray-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" 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 probeTaggerRuntime()} onClick={() => void probeTaggerRuntime()}
disabled={taggerRuntimeChecking} disabled={taggerRuntimeChecking}
> >
@@ -387,7 +387,7 @@ export function SettingsModal() {
</> </>
) : ( ) : (
<button <button
className="relative overflow-hidden 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-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" className="relative overflow-hidden 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()} onClick={() => void prepareTaggerModel()}
disabled={taggerModelPreparing} disabled={taggerModelPreparing}
> >
@@ -536,14 +536,14 @@ export function SettingsModal() {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
className="rounded-md border border-white/10 bg-white/[0.045] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/[0.075] hover:text-white disabled:opacity-40 disabled:cursor-not-allowed light-theme:border-gray-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" className="rounded-md border border-white/10 bg-white/[0.045] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/[0.075] hover:text-white disabled:opacity-40 disabled:cursor-not-allowed 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={() => setTaggingQueueFolderIds(folders.map((folder) => folder.id))} onClick={() => setTaggingQueueFolderIds(folders.map((folder) => folder.id))}
disabled={taggingQueueScope === "all" || folders.length === 0} disabled={taggingQueueScope === "all" || folders.length === 0}
> >
Select all Select all
</button> </button>
<button <button
className="rounded-md border border-white/10 bg-white/[0.045] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/[0.075] hover:text-white disabled:opacity-40 disabled:cursor-not-allowed light-theme:border-gray-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" className="rounded-md border border-white/10 bg-white/[0.045] px-2.5 py-1 text-[11px] text-gray-400 transition-colors hover:bg-white/[0.075] hover:text-white disabled:opacity-40 disabled:cursor-not-allowed 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={() => setTaggingQueueFolderIds([])} onClick={() => setTaggingQueueFolderIds([])}
disabled={taggingQueueScope === "all" || taggingQueueFolderIds.length === 0} disabled={taggingQueueScope === "all" || taggingQueueFolderIds.length === 0}
> >
@@ -582,7 +582,7 @@ export function SettingsModal() {
<SettingsItem label="Queue tagging jobs" description="Generate missing AI tags for the current target. Results flow back into the library as the background worker finishes."> <SettingsItem label="Queue tagging jobs" description="Generate missing AI tags for the current target. Results flow back into the library as the background worker finishes.">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <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 light-theme:border-gray-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" 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={() => runQueueAction("queue")} onClick={() => runQueueAction("queue")}
disabled={!taggerReady || taggerQueueing || taggerClearing || (taggingQueueScope === "selected" && taggingQueueFolderIds.length === 0)} disabled={!taggerReady || taggerQueueing || taggerClearing || (taggingQueueScope === "selected" && taggingQueueFolderIds.length === 0)}
> >
@@ -649,7 +649,7 @@ export function SettingsModal() {
</button> </button>
) : ( ) : (
<button <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 light-theme:border-gray-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" 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 checkForUpdates()} onClick={() => void checkForUpdates()}
disabled={updateStatus === "checking" || updateStatus === "downloading" || updateStatus === "installing"} disabled={updateStatus === "checking" || updateStatus === "downloading" || updateStatus === "installing"}
> >
@@ -666,7 +666,7 @@ export function SettingsModal() {
description="Replay the guided first-run tour — library setup, the background pipeline, search modes, and AI features." description="Replay the guided first-run tour — library setup, the background pipeline, search modes, and AI features."
> >
<button <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 light-theme:border-gray-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" 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 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={() => { onClick={() => {
setSettingsOpen(false); setSettingsOpen(false);
openOnboarding(); openOnboarding();
@@ -683,7 +683,7 @@ export function SettingsModal() {
description="Open the folder in Explorer to inspect or back up the database, thumbnails, and models." description="Open the folder in Explorer to inspect or back up the database, thumbnails, and models."
> >
<button <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 light-theme:border-gray-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" 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={() => { onClick={() => {
setOpeningDataFolder(true); setOpeningDataFolder(true);
void openAppDataFolder().finally(() => setOpeningDataFolder(false)); void openAppDataFolder().finally(() => setOpeningDataFolder(false));
@@ -748,7 +748,7 @@ export function SettingsModal() {
} }
> >
<button <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 light-theme:border-gray-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" 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={() => { onClick={() => {
setVacuuming(true); setVacuuming(true);
setVacuumResult(null); setVacuumResult(null);
@@ -809,7 +809,7 @@ export function SettingsModal() {
} }
> >
<button <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 light-theme:border-gray-300/70 light-theme:bg-gray-100 light-theme:text-gray-700 light-theme:hover:bg-gray-200 light-theme:hover:text-gray-900" 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={() => { onClick={() => {
setCleaningThumbnails(true); setCleaningThumbnails(true);
cleanupOrphanedThumbnails() cleanupOrphanedThumbnails()
+6 -6
View File
@@ -49,9 +49,9 @@ export function ThemedDropdown({
onClick={() => setOpen((currentOpen) => !currentOpen)} onClick={() => setOpen((currentOpen) => !currentOpen)}
className={`flex items-center justify-between gap-2 rounded-md border transition-colors ${ className={`flex items-center justify-between gap-2 rounded-md border transition-colors ${
compact compact
? "border-white/10 bg-white/[0.04] px-1.5 py-0.5 text-[10px] font-medium" ? "border-white/10 bg-white/[0.04] px-1.5 py-0.5 text-[10px] font-medium light-theme:border-gray-700/50 light-theme:bg-gray-900"
: "min-w-40 border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs" : "min-w-40 border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs light-theme:border-gray-700/50 light-theme:bg-gray-900"
} ${open ? "border-white/20 text-white" : "text-gray-400 hover:border-white/15 hover:text-gray-200"}`} } ${open ? "border-white/20 text-white light-theme:border-gray-700 light-theme:text-white" : "text-gray-400 hover:border-white/15 hover:text-gray-200 light-theme:text-white light-theme:hover:border-gray-700 light-theme:hover:bg-gray-800 light-theme:hover:text-white"}`}
> >
<span>{current?.label}</span> <span>{current?.label}</span>
<svg <svg
@@ -68,7 +68,7 @@ export function ThemedDropdown({
<div <div
role="listbox" role="listbox"
aria-label={ariaLabel} aria-label={ariaLabel}
className={`absolute top-full z-50 mt-1.5 min-w-full rounded-xl border border-white/10 bg-gray-950/98 p-1 shadow-2xl shadow-black/30 backdrop-blur-xl ${ className={`absolute top-full z-50 mt-1.5 min-w-full rounded-xl border border-white/10 bg-gray-950/98 p-1 shadow-2xl shadow-black/30 backdrop-blur-xl light-theme:border-gray-700/50 ${
align === "right" ? "right-0" : "left-0" align === "right" ? "right-0" : "left-0"
}`} }`}
> >
@@ -82,8 +82,8 @@ export function ThemedDropdown({
aria-selected={selected} aria-selected={selected}
className={`flex w-full items-center justify-between gap-4 whitespace-nowrap rounded-lg px-3 py-2 text-left text-xs transition-colors ${ className={`flex w-full items-center justify-between gap-4 whitespace-nowrap rounded-lg px-3 py-2 text-left text-xs transition-colors ${
selected selected
? "bg-white/[0.08] text-white" ? "bg-white/[0.08] text-white light-theme:bg-gray-900 light-theme:text-white"
: "text-gray-400 hover:bg-white/[0.055] hover:text-gray-200" : "text-gray-400 hover:bg-white/[0.055] hover:text-gray-200 light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
onClick={() => { onClick={() => {
onChange(option.value); onChange(option.value);
+11 -11
View File
@@ -55,8 +55,8 @@ function SortDropdown({
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs transition-colors ${ className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs transition-colors ${
open open
? "border-white/15 bg-white/8 text-white" ? "border-white/15 bg-white/8 text-white light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white"
: "border-white/8 bg-transparent text-gray-400 hover:border-white/15 hover:text-gray-200" : "border-white/8 bg-transparent text-gray-400 hover:border-white/15 hover:text-gray-200 light-theme:border-gray-700/40 light-theme:text-gray-600 light-theme:hover:border-gray-700 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
> >
<span>{current?.label ?? "Sort"}</span> <span>{current?.label ?? "Sort"}</span>
@@ -68,14 +68,14 @@ function SortDropdown({
</svg> </svg>
</button> </button>
{open ? ( {open ? (
<div className="absolute right-0 top-full z-30 mt-1.5 min-w-44 rounded-xl border border-white/10 bg-gray-950/98 p-1 shadow-2xl backdrop-blur"> <div className="absolute right-0 top-full z-30 mt-1.5 min-w-44 rounded-xl border border-white/10 bg-gray-950/98 p-1 shadow-2xl backdrop-blur light-theme:border-gray-700/50">
{options.map((option) => ( {options.map((option) => (
<button <button
key={option.value} key={option.value}
className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition-colors ${ className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition-colors ${
option.value === value option.value === value
? "bg-white/6 text-white" ? "bg-white/6 text-white light-theme:bg-gray-900 light-theme:text-white"
: "text-gray-400 hover:bg-white/5 hover:text-white" : "text-gray-400 hover:bg-white/5 hover:text-white light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
onClick={() => { onChange(option.value); setOpen(false); }} onClick={() => { onChange(option.value); setOpen(false); }}
> >
@@ -106,14 +106,14 @@ function FilterPill({
}) { }) {
const activeClass = const activeClass =
variant === "amber" variant === "amber"
? "bg-amber-500/15 text-amber-300 border border-amber-500/30" ? "bg-amber-500/15 text-amber-300 border border-amber-500/30 light-theme:bg-amber-100 light-theme:text-amber-700 light-theme:border-amber-500/50"
: "bg-white/10 text-white"; : "bg-white/10 text-white light-theme:bg-gray-900 light-theme:text-white";
return ( return (
<button <button
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all duration-150 ${ className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all duration-150 ${
active active
? activeClass ? activeClass
: "text-gray-500 hover:text-gray-200 hover:bg-white/5" : "text-gray-500 hover:text-gray-200 hover:bg-white/5 light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
onClick={onClick} onClick={onClick}
> >
@@ -419,7 +419,7 @@ export function Toolbar() {
<div className="h-4 w-px bg-white/10 shrink-0" /> <div className="h-4 w-px bg-white/10 shrink-0" />
{/* Zoom */} {/* Zoom */}
<div className="flex items-center rounded-lg border border-white/8 overflow-hidden"> <div className="flex items-center rounded-lg border border-white/8 overflow-hidden light-theme:border-gray-700/40">
{(["compact", "comfortable", "detail"] as const).map((preset, i) => ( {(["compact", "comfortable", "detail"] as const).map((preset, i) => (
<button <button
key={preset} key={preset}
@@ -427,8 +427,8 @@ export function Toolbar() {
i > 0 ? "border-l border-white/8" : "" i > 0 ? "border-l border-white/8" : ""
} ${ } ${
zoomPreset === preset zoomPreset === preset
? "bg-white/10 text-white" ? "bg-white/10 text-white light-theme:bg-gray-900 light-theme:text-white"
: "text-gray-500 hover:text-gray-200 hover:bg-white/5" : "text-gray-500 hover:text-gray-200 hover:bg-white/5 light-theme:text-gray-600 light-theme:hover:bg-gray-900 light-theme:hover:text-white"
}`} }`}
title={`${tileSize}px tiles`} title={`${tileSize}px tiles`}
onClick={() => setZoomPreset(preset)} onClick={() => setZoomPreset(preset)}
@@ -43,10 +43,10 @@ export function OnboardingOverlay() {
const isLast = onboardingStep >= STEPS.length - 1; const isLast = onboardingStep >= STEPS.length - 1;
return ( return (
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/65 backdrop-blur-sm"> <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-[#07080f] shadow-2xl shadow-black/60"> <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 */} {/* 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> <div>
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600"> <p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">
Step {onboardingStep + 1} of {STEPS.length} Step {onboardingStep + 1} of {STEPS.length}
@@ -59,7 +59,11 @@ export function OnboardingOverlay() {
key={s.id} key={s.id}
aria-label={s.title} aria-label={s.title}
className={`h-1.5 rounded-full transition-all ${ 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)} onClick={() => setOnboardingStep(i)}
/> />
@@ -83,23 +87,23 @@ export function OnboardingOverlay() {
</div> </div>
{/* Footer */} {/* 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 <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} onClick={completeOnboarding}
> >
Skip tour Skip tour
</button> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <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)} onClick={() => setOnboardingStep(onboardingStep - 1)}
disabled={isFirst} disabled={isFirst}
> >
Back Back
</button> </button>
<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))} onClick={() => (isLast ? completeOnboarding() : setOnboardingStep(onboardingStep + 1))}
> >
{isLast ? "Finish" : "Next"} {isLast ? "Finish" : "Next"}
+4 -4
View File
@@ -30,7 +30,7 @@ export function StepAddLibrary() {
added, renamed, or removed. Nothing is moved or copied. added, renamed, or removed. Nothing is moved or copied.
</p> </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"> <div className="min-w-0">
{folders.length > 0 ? ( {folders.length > 0 ? (
<> <>
@@ -49,10 +49,10 @@ export function StepAddLibrary() {
</p> </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> </div>
<button <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()} onClick={() => void handlePick()}
disabled={adding} 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 indexing runs, the gallery fills in roughly like this tiles appear immediately and sharpen
as thumbnails are generated: as thumbnails are generated:
</p> </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={0} />
<FakeTile index={1} favorite /> <FakeTile index={1} favorite />
<FakeTile index={2} duration="0:42" /> <FakeTile index={2} duration="0:42" />
+8 -8
View File
@@ -37,18 +37,18 @@ export function StepAiFeatures() {
</p> </p>
<h4 className="mt-6 text-[12px] font-semibold uppercase tracking-[0.08em] text-gray-400">AI tagging — optional</h4> <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="py-4">
<div className="flex items-start justify-between gap-6"> <div className="flex items-start justify-between gap-6">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm text-white">Automatic tags for every image</p> <p className="text-sm text-white">Automatic tags for every image</p>
<p className="mt-1 text-xs leading-relaxed text-gray-500"> <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{" "} 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> </p>
<span className="mt-2 flex flex-wrap gap-1.5"> <span className="mt-2 flex flex-wrap gap-1.5">
{FAKE_TAGS.map((tag) => ( {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} {tag}
</span> </span>
))} ))}
@@ -56,12 +56,12 @@ export function StepAiFeatures() {
</div> </div>
<div className="shrink-0"> <div className="shrink-0">
{taggerReady ? ( {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 Installed
</span> </span>
) : ( ) : (
<button <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()} onClick={() => void prepareTaggerModel()}
disabled={taggerModelPreparing} disabled={taggerModelPreparing}
> >
@@ -84,7 +84,7 @@ export function StepAiFeatures() {
</div> </div>
) : null} ) : null}
{!taggerModelPreparing && taggerModelError ? ( {!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} Download failed: {taggerModelError}
</p> </p>
) : null} ) : null}
@@ -92,11 +92,11 @@ export function StepAiFeatures() {
</div> </div>
<h4 className="mt-6 text-[12px] font-semibold uppercase tracking-[0.08em] text-gray-400">Semantic search & similarity — built in</h4> <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"> <div className="py-4">
<p className="text-sm text-white">Search by meaning, find look-alikes</p> <p className="text-sm text-white">Search by meaning, find look-alikes</p>
<p className="mt-1 text-xs leading-relaxed text-gray-500"> <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 "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 (~580 MB) downloads automatically the first time embeddings run. Nothing to do you'll see it
in the background-tasks bar. 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. carry your favorites, star ratings, and video durations; hover for filename and quick actions.
</p> </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) => ( {TILE_PROPS.map((props, i) => (
<FakeTile key={i} index={i} loaded={i < revealed} {...props} /> <FakeTile key={i} index={i} loaded={i < revealed} {...props} />
))} ))}
</div> </div>
<div className="mt-6 flex items-start justify-between gap-4"> <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"> <p className="pb-2.5">
<span className="text-gray-300">Click any tile</span> to open the lightbox keyboard navigation, <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. zoom, inline tag editing, ratings, and a full video player.
+3 -3
View File
@@ -49,7 +49,7 @@ export function StepPipeline() {
</p> </p>
{/* Fake BackgroundTasks slim bar */} {/* 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"> <div className="flex items-center gap-3">
<span className="relative flex h-1.5 w-1.5 shrink-0"> <span className="relative flex h-1.5 w-1.5 shrink-0">
{!finished ? ( {!finished ? (
@@ -57,7 +57,7 @@ export function StepPipeline() {
) : null} ) : null}
<span className={`relative inline-flex h-1.5 w-1.5 rounded-full ${finished ? "bg-emerald-400" : "bg-blue-400"}`} /> <span className={`relative inline-flex h-1.5 w-1.5 rounded-full ${finished ? "bg-emerald-400" : "bg-blue-400"}`} />
</span> </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"> <div className="flex items-center gap-1.5">
{STAGES.map((stage, i) => ( {STAGES.map((stage, i) => (
<FakeStageTag <FakeStageTag
@@ -72,7 +72,7 @@ export function StepPipeline() {
</div> </div>
<div className="mt-6 flex items-start justify-between gap-4"> <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"> <p className="pb-2.5">
<span className="text-gray-300">It's all interruptible.</span> Close the app whenever you like <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. the queue picks up where it left off next launch.
+5 -5
View File
@@ -64,12 +64,12 @@ export function StepSearchDemo() {
return ( return (
<div> <div>
<p className="text-sm leading-relaxed text-gray-300"> <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 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-white/[0.07] px-1 py-0.5 text-[11px] text-gray-200">/t</code> searches tags. 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> </p>
{/* Mock search bar */} {/* 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}> <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" /> <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> </svg>
@@ -77,7 +77,7 @@ export function StepSearchDemo() {
{demo.query.slice(0, typed)} {demo.query.slice(0, typed)}
{!finished ? <span className="animate-pulse text-gray-500">|</span> : null} {!finished ? <span className="animate-pulse text-gray-500">|</span> : null}
</span> </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} {demo.mode}
</span> </span>
</div> </div>
@@ -90,7 +90,7 @@ export function StepSearchDemo() {
demo's visible state. */} demo's visible state. */}
<div <div
key={demoIndex} 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) => ( {demo.results.map((src, i) => (
<div key={i} className="aspect-square w-36 overflow-hidden rounded-xl bg-white/[0.04]"> <div key={i} className="aspect-square w-36 overflow-hidden rounded-xl bg-white/[0.04]">
+9 -9
View File
@@ -24,9 +24,9 @@ export function StepUpdates() {
</p> </p>
{/* Mini app-window mockup: the lit mark shown in a real title bar. */} {/* 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="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"> <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-white/[0.08] text-gray-300"> <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.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" /> <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" /> <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. */} {/* Ghosted window body, just enough to read as the app. */}
<div className="flex h-[72px] bg-gray-900/30"> <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="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-white/[0.07]" /> <div className="h-1.5 w-full rounded bg-gray-800" />
<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-gray-900" />
<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-gray-900" />
</div> </div>
<div className="flex-1 p-2.5"> <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"> <div className="mt-2.5 grid grid-cols-5 gap-1.5">
{Array.from({ length: 5 }).map((_, i) => ( {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>
</div> </div>
+4 -4
View File
@@ -22,7 +22,7 @@ function ExplorePreview() {
{ size: 16, x: 70, y: 44, i: 6 }, { size: 16, x: 70, y: 44, i: 6 },
]; ];
return ( 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) => ( {blobs.map((blob, idx) => (
<div <div
key={idx} key={idx}
@@ -36,7 +36,7 @@ function ExplorePreview() {
function TimelinePreview() { function TimelinePreview() {
return ( 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) => ( {[2024, 2023].map((year, row) => (
<div key={year} className="flex items-center gap-1.5"> <div key={year} className="flex items-center gap-1.5">
<span className="w-7 text-[9px] tabular-nums text-gray-600">{year}</span> <span className="w-7 text-[9px] tabular-nums text-gray-600">{year}</span>
@@ -51,7 +51,7 @@ function TimelinePreview() {
function DuplicatesPreview() { function DuplicatesPreview() {
return ( 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"> <div className="w-10">
<FakeTile index={3} className="rounded-md" /> <FakeTile index={3} className="rounded-md" />
</div> </div>
@@ -69,7 +69,7 @@ export function StepViews() {
<p className="text-sm leading-relaxed text-gray-300"> <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: Beyond the main grid, the sidebar switches between three more ways to look at your library:
</p> </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 <ViewRow
title="Explore" title="Explore"
description="A visual cluster map and tag cloud — browse by theme instead of folder, and jump into any cluster." description="A visual cluster map and tag cloud — browse by theme instead of folder, and jump into any cluster."
+86 -5
View File
@@ -1,6 +1,52 @@
import { useGalleryStore } from "../../store"; import { AppTheme, useGalleryStore } from "../../store";
import { FakeProgressBar, formatBytes } from "./fakes"; 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() { export function FfmpegStatusRow() {
const ffmpegStatus = useGalleryStore((s) => s.ffmpegStatus); const ffmpegStatus = useGalleryStore((s) => s.ffmpegStatus);
const ffmpegProgress = useGalleryStore((s) => s.ffmpegProgress); const ffmpegProgress = useGalleryStore((s) => s.ffmpegProgress);
@@ -10,7 +56,7 @@ export function FfmpegStatusRow() {
if (ffmpegStatus === "installed") { if (ffmpegStatus === "installed") {
return ( return (
<div className="flex items-center gap-2.5 py-3"> <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" /> <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> </svg>
<div> <div>
@@ -27,14 +73,14 @@ export function FfmpegStatusRow() {
<div className="flex items-start justify-between gap-6"> <div className="flex items-start justify-between gap-6">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm text-white">Media engine download failed</p> <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"> <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 You can keep going photos work without it. Video thumbnails and metadata will start
automatically once the download succeeds. automatically once the download succeeds.
</p> </p>
</div> </div>
<button <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()} onClick={() => void retryFfmpegDownload()}
> >
Retry download Retry download
@@ -71,6 +117,9 @@ export function FfmpegStatusRow() {
} }
export function StepWelcome() { export function StepWelcome() {
const theme = useGalleryStore((s) => s.theme);
const setTheme = useGalleryStore((s) => s.setTheme);
return ( return (
<div> <div>
<p className="text-sm leading-relaxed text-gray-300"> <p className="text-sm leading-relaxed text-gray-300">
@@ -83,8 +132,40 @@ export function StepWelcome() {
from Settings. from Settings.
</p> </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> <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 /> <FfmpegStatusRow />
</div> </div>
</div> </div>
+6 -6
View File
@@ -82,7 +82,7 @@ export function FakeTile({
className?: string; className?: string;
}) { }) {
return ( 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 ? ( {loaded ? (
<img src={fakeImage(index)} alt="" loading="lazy" className="absolute inset-0 h-full w-full object-cover" /> <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" }) { export function FakeStageTag({ label, state }: { label: string; state: "active" | "done" | "waiting" }) {
const className = const className =
state === "active" 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" : state === "done"
? "bg-emerald-500/10 text-emerald-400" ? "bg-emerald-500/10 text-emerald-400 light-theme:bg-emerald-100 light-theme:text-emerald-700"
: "bg-white/4 text-gray-600"; : "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>; 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 }) { export function FakeProgressBar({ fraction, className = "" }: { fraction: number | null; className?: string }) {
return ( 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 ? ( {fraction === null ? (
<div className="h-full w-full animate-pulse bg-blue-500/40" /> <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 <button
type="button" type="button"
onClick={onClick} 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}> <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" /> <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" />