Skip to content

Commit 7f7908f

Browse files
committed
feat(homepage): click-driven step sections with sticky visuals and scroll focus
Sync and Admin demos now keep every step's content persistently on screen (no sliding scenes). The companion visual stays sticky on the right while scrolling; scroll position auto-advances the focused step (auto click-through), and clicking any step block or pipeline stage focuses it directly. Remove the redundant sync pill selector and the tour control.
1 parent ed4732d commit 7f7908f

8 files changed

Lines changed: 384 additions & 440 deletions

File tree

src/components/Icon.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export type IconName =
1010
| "check"
1111
| "chevron-down"
1212
| "chevron-right"
13-
| "chevron-up"
1413
| "cloud"
1514
| "download"
1615
| "clock"
@@ -27,7 +26,6 @@ export type IconName =
2726
| "menu"
2827
| "more"
2928
| "play"
30-
| "pause"
3129
| "person"
3230
| "plus"
3331
| "refresh"
@@ -136,12 +134,6 @@ export function Icon({
136134
<path d="m9 6 6 6-6 6" />
137135
</svg>
138136
);
139-
case "chevron-up":
140-
return (
141-
<svg {...common}>
142-
<path d="m6 15 6-6 6 6" />
143-
</svg>
144-
);
145137
case "cloud":
146138
return (
147139
<svg {...common}>
@@ -232,12 +224,6 @@ export function Icon({
232224
<circle cx="19" cy="12" r="1" fill="currentColor" />
233225
</svg>
234226
);
235-
case "pause":
236-
return (
237-
<svg {...common}>
238-
<path d="M8 5.5v13M16 5.5v13" />
239-
</svg>
240-
);
241227
case "play":
242228
return (
243229
<svg {...common}>

src/homepage/FlowDemo.tsx

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
22
import { Collector } from "../components/Collector";
33
import { ContributorHome } from "../components/ContributorHome";
44
import { TopBar } from "../components/TopBar";
5-
import { Icon } from "../components/Icon";
5+
import { Icon, type IconName } from "../components/Icon";
66
import { DocLinks } from "./DocLinks";
77
import { projectFields } from "../data/schemaFixtures";
88
import type { Observation, Project } from "../types";
@@ -83,18 +83,31 @@ const initialSampleObservations: Observation[] = [
8383
},
8484
];
8585

86+
/** The input types the schema supports; each chip jumps the preview phone
87+
* to the matching question so visitors see the flow is not media-only. */
88+
const FIELD_TYPES: { icon: IconName; label: string; fieldKey: string }[] = [
89+
{ icon: "file", label: "Short text", fieldKey: "site_code" },
90+
{ icon: "check", label: "Single choice", fieldKey: "building_type" },
91+
{ icon: "info", label: "Tri-state", fieldKey: "building_occupancy" },
92+
{ icon: "more", label: "Multi-select", fieldKey: "visible_features" },
93+
{ icon: "clock", label: "Date", fieldKey: "observed_date" },
94+
{ icon: "sliders", label: "Number", fieldKey: "people_count" },
95+
{ icon: "camera", label: "Photo & audio", fieldKey: "site_photos" },
96+
{ icon: "mic", label: "Field notes", fieldKey: "notes" },
97+
];
98+
8699
const TAB_NARRATION: Record<ContributorTab, { title: string; body: string }> = {
87100
home: {
88101
title: "Field Home",
89102
body: "Shows active survey guidance, offline sync status, and a single dominant action to begin collecting.",
90103
},
91104
flow: {
92105
title: "Guided Step Flow",
93-
body: "One clear question per screen with large touch targets for gloves and sunlight, native inputs, and keyboard-safe viewports.",
106+
body: "One clear question per screen: short text, single choice, tri-state, multi-select, dates, and counts use native inputs with large touch targets for gloves and sunlight.",
94107
},
95108
media: {
96109
title: "Raw Media Capture",
97-
body: "Stores photos and audio as unmodified original files with SHA-256 integrity hashes, bypassing compression.",
110+
body: "Photos, audio, and field notes join every other input type — originals stay unmodified with SHA-256 integrity hashes, never recompressed.",
98111
},
99112
};
100113

@@ -174,6 +187,7 @@ export function FlowDemo() {
174187
const [phase, setPhase] = useState<"home" | "collecting">("home");
175188
const [activeTab, setActiveTab] = useState<ContributorTab>("home");
176189
const [collectorStep, setCollectorStep] = useState<number>(0);
190+
const [jumpKey, setJumpKey] = useState<string | undefined>(undefined);
177191
const [draft, setDraft] = useState<Record<string, unknown>>({});
178192
const [observation, setObservation] = useState<Observation | null>(null);
179193
const [seedObservations, setSeedObservations] = useState<Observation[]>(
@@ -194,6 +208,7 @@ export function FlowDemo() {
194208
setPhase("home");
195209
setActiveTab("home");
196210
setCollectorStep(0);
211+
setJumpKey(undefined);
197212
};
198213

199214
const handleTabClick = (tab: ContributorTab) => {
@@ -211,6 +226,13 @@ export function FlowDemo() {
211226
}
212227
};
213228

229+
const jumpToField = (fieldKey: string) => {
230+
setPhase("collecting");
231+
setActiveTab("flow");
232+
setCollectorStep(-1);
233+
setJumpKey(fieldKey);
234+
};
235+
214236
const triggerSyncAnimation = () => {
215237
timersRef.current.forEach(clearTimeout);
216238
timersRef.current = [];
@@ -283,6 +305,25 @@ export function FlowDemo() {
283305
</button>
284306
</div>
285307

308+
{activeTab === "flow" && (
309+
<div className="hp-field-types">
310+
<span className="hp-field-types-label">Input types</span>
311+
<div className="hp-field-types-row">
312+
{FIELD_TYPES.map((type) => (
313+
<button
314+
key={type.fieldKey}
315+
type="button"
316+
className="hp-field-type-chip"
317+
onClick={() => jumpToField(type.fieldKey)}
318+
>
319+
<Icon name={type.icon} size={13} />
320+
{type.label}
321+
</button>
322+
))}
323+
</div>
324+
</div>
325+
)}
326+
286327
<div className="hp-story" aria-live="polite">
287328
<span className="hp-story-kicker">Contributor Surface</span>
288329
<h3>{narrative.title}</h3>
@@ -361,11 +402,15 @@ export function FlowDemo() {
361402
) : (
362403
<div className="main-shell">
363404
<Collector
364-
key={`${round}-${collectorStep}`}
405+
key={`${round}-${collectorStep}-${jumpKey ?? ""}`}
365406
project={demoProject}
366-
initialStepIndex={activeTab === "media" ? 0 : collectorStep}
407+
initialStepIndex={
408+
activeTab === "media" || jumpKey ? 0 : collectorStep
409+
}
367410
initialFieldKey={
368-
activeTab === "media" ? "site_photos" : undefined
411+
activeTab === "media"
412+
? "site_photos"
413+
: (jumpKey ?? undefined)
369414
}
370415
draft={draft}
371416
lastSavedAt={null}

src/homepage/HomepageApp.tsx

Lines changed: 60 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,32 @@ import { DocLinks, DOCS } from "./DocLinks";
1111
import { Icon, type IconName } from "../components/Icon";
1212
import { CollectBrand } from "../components/CollectBrand";
1313
import { useSectionSpy } from "./useSectionSpy";
14-
import { TourControl, type TourSection } from "./TourControl";
14+
import { useScrollFocus } from "./useScrollFocus";
1515

1616
const GITHUB_URL = "https://github.com/gbrlpzz/collect";
1717

18-
const TOUR_SECTIONS: TourSection[] = [
19-
{ id: "collection", label: "Field collection" },
20-
{ id: "guarantees", label: "Guarantees" },
21-
{ id: "sync", label: "Sync engine" },
22-
{ id: "admin", label: "Setup & fleet" },
23-
{ id: "integrity", label: "Data integrity" },
24-
{ id: "data", label: "Dataset export" },
25-
{ id: "preview", label: "Request access" },
26-
];
18+
const ADMIN_SCENES = [
19+
{
20+
tab: "setup",
21+
kicker: "Form Schema",
22+
title: "Immutable schema versioning",
23+
body: "Published field definitions are locked. Schema updates create a new version without altering historical observations.",
24+
},
25+
{
26+
tab: "contributors",
27+
kicker: "Fleet Readiness",
28+
title: "Real-time fleet sync & progress monitoring",
29+
body: "Track incoming observations, unsynced device queues, and contributor attention reliability scores across all field teams in real time.",
30+
},
31+
{
32+
tab: "export",
33+
kicker: "Checkpoint Export",
34+
title: "Verified publication checkpoints",
35+
body: "Review received observations, monitor contributor attention, and export self-contained research archives once all devices report complete.",
36+
},
37+
] as const;
38+
39+
type AdminSceneTab = (typeof ADMIN_SCENES)[number]["tab"];
2740

2841
function TopBar({ activeSection }: { activeSection: string }) {
2942
return (
@@ -263,9 +276,6 @@ function DifferentiationSummary() {
263276

264277
export function HomepageApp() {
265278
const [draftEmail, setDraftEmail] = useState("");
266-
const [adminTab, setAdminTab] = useState<"setup" | "contributors" | "export">(
267-
"setup",
268-
);
269279

270280
const captureEmail = (email: string) => {
271281
setDraftEmail(email);
@@ -279,12 +289,21 @@ export function HomepageApp() {
279289
});
280290
};
281291

282-
const activeSection = useSectionSpy(TOUR_SECTIONS.map((s) => s.id));
283-
const tourSection = useSectionSpy(
284-
TOUR_SECTIONS.map((s) => s.id),
285-
140,
286-
true,
287-
);
292+
const activeSection = useSectionSpy([
293+
"collection",
294+
"guarantees",
295+
"sync",
296+
"admin",
297+
"integrity",
298+
"data",
299+
"preview",
300+
]);
301+
const {
302+
ref: adminStepsRef,
303+
active: adminStep,
304+
activate: activateAdminStep,
305+
} = useScrollFocus<HTMLDivElement>(".hp-step");
306+
const adminTab: AdminSceneTab = ADMIN_SCENES[adminStep].tab;
288307

289308
return (
290309
<div className="hp-shell">
@@ -324,7 +343,7 @@ export function HomepageApp() {
324343
aria-labelledby="admin-title"
325344
>
326345
<div className="hp-section-inner">
327-
<div className="hp-flow-layout">
346+
<div className="hp-flow-layout" ref={adminStepsRef}>
328347
<div className="hp-flow-copy">
329348
<div className="section-heading">
330349
<p className="eyebrow">Setup & Operations</p>
@@ -341,70 +360,31 @@ export function HomepageApp() {
341360
<DocLinks files={["flows.md", "spec.md"]} />
342361
</div>
343362

344-
<div className="hp-admin-tab-selector">
345-
<button
346-
type="button"
347-
className={`hp-admin-step-btn ${adminTab === "setup" ? "active" : ""}`}
348-
onClick={() => setAdminTab("setup")}
349-
>
350-
Form Schema
351-
</button>
352-
<button
353-
type="button"
354-
className={`hp-admin-step-btn ${adminTab === "contributors" ? "active" : ""}`}
355-
onClick={() => setAdminTab("contributors")}
356-
>
357-
Fleet Readiness
358-
</button>
359-
<button
360-
type="button"
361-
className={`hp-admin-step-btn ${adminTab === "export" ? "active" : ""}`}
362-
onClick={() => setAdminTab("export")}
363+
{ADMIN_SCENES.map((scene, i) => (
364+
<div
365+
key={scene.tab}
366+
className="hp-step"
367+
data-active={adminStep === i}
368+
aria-current={adminStep === i ? "step" : undefined}
369+
role="button"
370+
tabIndex={0}
371+
onClick={() => activateAdminStep(i)}
372+
onKeyDown={(e) => {
373+
if (e.key === "Enter" || e.key === " ") {
374+
e.preventDefault();
375+
activateAdminStep(i);
376+
}
377+
}}
363378
>
364-
Checkpoint Export
365-
</button>
366-
</div>
367-
368-
<div className="hp-story" aria-live="polite">
369-
<span className="hp-story-kicker">Administrator Console</span>
370-
{adminTab === "setup" && (
371-
<>
372-
<h3>Immutable schema versioning</h3>
373-
<p>
374-
Published field definitions are locked. Schema updates
375-
create a new version without altering historical
376-
observations.
377-
</p>
378-
</>
379-
)}
380-
{adminTab === "contributors" && (
381-
<>
382-
<h3>Real-time fleet sync & progress monitoring</h3>
383-
<p>
384-
Track incoming observations, unsynced device queues, and
385-
contributor attention reliability scores across all
386-
field teams in real time.
387-
</p>
388-
</>
389-
)}
390-
{adminTab === "export" && (
391-
<>
392-
<h3>Verified publication checkpoints</h3>
393-
<p>
394-
Review received observations, monitor contributor
395-
attention, and export self-contained research archives
396-
once all devices report complete.
397-
</p>
398-
</>
399-
)}
400-
</div>
379+
<p className="hp-step-kicker">{scene.kicker}</p>
380+
<h3>{scene.title}</h3>
381+
<p>{scene.body}</p>
382+
</div>
383+
))}
401384
</div>
402385

403386
<div className="hp-flow-visual">
404-
<AdminWalkthrough
405-
initialTab={adminTab}
406-
onTabChange={setAdminTab}
407-
/>
387+
<AdminWalkthrough initialTab={adminTab} />
408388
</div>
409389
</div>
410390
</div>
@@ -675,7 +655,6 @@ export function HomepageApp() {
675655
</p>
676656
</div>
677657
</footer>
678-
<TourControl sections={TOUR_SECTIONS} activeId={tourSection} />
679658
<Analytics />
680659
</div>
681660
);

0 commit comments

Comments
 (0)