- It's all interruptible. Close the app whenever you like —
- the queue picks up where it left off next launch.
-
-
- Per-folder control. Right-click a folder in the sidebar to
- pause its background work, and click the bar itself to expand a detailed per-folder view.
-
+
+
+
+ It's all interruptible. Close the app whenever you like —
+ the queue picks up where it left off next launch.
+
+
+ Per-folder control. Right-click a folder in the sidebar to
+ pause its background work, and click the bar itself to expand a detailed per-folder view.
+
+
+ {finished ? : null}
);
diff --git a/src/components/onboarding/StepSearchDemo.tsx b/src/components/onboarding/StepSearchDemo.tsx
index 926ba54..e2a4545 100644
--- a/src/components/onboarding/StepSearchDemo.tsx
+++ b/src/components/onboarding/StepSearchDemo.tsx
@@ -1,11 +1,11 @@
import { useEffect, useState } from "react";
-import { SEARCH_RESULTS } from "./fakes";
+import { ReplayButton, SEARCH_RESULTS } from "./fakes";
const DEMOS = [
{
query: "beach-day_042.jpg",
mode: "Filename",
- description: "Plain text matches file names — the default, instant search.",
+ description: "Plain text matches file names — the default, instant search. One name, one file.",
results: SEARCH_RESULTS.filename,
},
{
@@ -25,25 +25,39 @@ const DEMOS = [
const TYPE_MS = 55;
const HOLD_MS = 2600;
-/// A mock search bar that "types" each demo query, shows fake results, and
-/// cycles to the next mode.
+/// Types each demo query, shows matching results, advances through all three
+/// modes once, then stops on the last with a replay control.
export function StepSearchDemo() {
const [demoIndex, setDemoIndex] = useState(0);
const [typed, setTyped] = useState(0);
+ const [finished, setFinished] = useState(false);
const demo = DEMOS[demoIndex];
+ const isLastDemo = demoIndex === DEMOS.length - 1;
useEffect(() => {
+ if (finished) return;
if (typed < demo.query.length) {
const timer = setTimeout(() => setTyped((n) => n + 1), TYPE_MS);
return () => clearTimeout(timer);
}
+ // Fully typed: hold, then advance — or finish on the last demo.
const timer = setTimeout(() => {
- setDemoIndex((i) => (i + 1) % DEMOS.length);
- setTyped(0);
+ if (isLastDemo) {
+ setFinished(true);
+ } else {
+ setDemoIndex((i) => i + 1);
+ setTyped(0);
+ }
}, HOLD_MS);
return () => clearTimeout(timer);
- }, [typed, demo.query.length]);
+ }, [typed, demo.query.length, isLastDemo, finished]);
+
+ const replay = () => {
+ setDemoIndex(0);
+ setTyped(0);
+ setFinished(false);
+ };
const fullyTyped = typed >= demo.query.length;
@@ -61,23 +75,26 @@ export function StepSearchDemo() {
{demo.query.slice(0, typed)}
- |
+ {!finished ? | : null}
{demo.mode}
- {/* Fake results */}
-
+ {/* Results — fixed-width tiles, centered, so 1/2/3 results all look right */}
+
{demo.results.map((src, i) => (
-
+
))}
-
{demo.description}
+
+
{demo.description}
+ {finished ? : null}
+
);
}
diff --git a/src/components/onboarding/fakes.tsx b/src/components/onboarding/fakes.tsx
index c3e2896..ad43dba 100644
--- a/src/components/onboarding/fakes.tsx
+++ b/src/components/onboarding/fakes.tsx
@@ -3,6 +3,7 @@
// demo progress — so new users see the real UI's shapes before their own
// library exists.
import sunset from "../../assets/onboarding/sunset.webp";
+import sunset2 from "../../assets/onboarding/sunset2.webp";
import beach from "../../assets/onboarding/beach.webp";
import landscape1 from "../../assets/onboarding/landscape1.webp";
import landscape2 from "../../assets/onboarding/landscape2.webp";
@@ -37,11 +38,14 @@ export function fakeImage(index: number): string {
return FAKE_IMAGES[index % FAKE_IMAGES.length];
}
-// Curated result sets so each search demo returns thematically right images.
+// Result sets matched to what each query would genuinely return.
+// - filename: one exact file (a filename search hits a specific name)
+// - semantic "golden sunset over water": only the ocean-sunset stills
+// - tags "landscape": mountain valleys + the alpine lake (all landscapes)
export const SEARCH_RESULTS = {
- filename: [beach, dunes, sunset], // "beach-day_042.jpg"
- semantic: [sunset, alpinelake, beach], // "golden sunset over water"
- tags: [landscape1, landscape2, forest], // "landscape"
+ filename: [beach],
+ semantic: [sunset, sunset2],
+ tags: [landscape1, landscape2, alpinelake],
} as const;
// Abstract gradient blobs/ticks for the Explore & Timeline mini-previews,
@@ -133,6 +137,21 @@ export function FakeProgressBar({ fraction, className = "" }: { fraction: number
);
}
+export function ReplayButton({ onClick, label = "Replay" }: { onClick: () => void; label?: string }) {
+ return (
+
+ );
+}
+
export function formatBytes(bytes: number): string {
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;