diff --git a/app/globals.css b/app/globals.css index 2278181..92e4bc3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -657,7 +657,7 @@ button:disabled { .tutorial-examples { display: grid; gap: 12px; - grid-template-columns: 1fr 1fr; + grid-template-columns: repeat(3, minmax(0, 1fr)); } .tutorial-example-card { background: #f4f1e9; @@ -672,8 +672,12 @@ button:disabled { display: block; height: 160px; image-rendering: pixelated; + object-fit: contain; width: 100%; } +.tutorial-example-card .tutorial-yoke-preview { + image-rendering: auto; +} .tutorial-example-card > div { align-content: start; border-top: 1px solid var(--line); @@ -704,6 +708,29 @@ button:disabled { justify-self: start; margin-top: auto; } +.tutorial-example-card .tutorial-yoke-shortcut { + background: transparent; + border-color: transparent; + box-shadow: none; + color: var(--accent-deep); + font-size: 12px; + font-weight: 600; + margin-top: 7px; + min-height: 28px; + padding: 3px 6px; + text-decoration: underline; + text-underline-offset: 3px; +} +.tutorial-example-card .tutorial-yoke-shortcut:hover:not(:disabled) { + background: var(--accent-soft); + border-color: transparent; + box-shadow: none; + transform: none; +} +.tutorial-example-card .tutorial-yoke-shortcut:focus-visible { + outline: 2px solid var(--accent-deep); + outline-offset: 2px; +} .tutorial-limit-note { color: #943f32; font-size: 11px; @@ -717,6 +744,21 @@ button:disabled { position: fixed; z-index: 60; } +.tour-layer-guided { + pointer-events: auto; +} +.yoke-tour-editor { + background: var(--panel); + inset: 0; + overflow: auto; + padding: 18px; + position: fixed; + z-index: 46; +} +.yoke-tour-editor > h2 { + font-size: 16px; + margin: 0 0 12px; +} .tour-dim { background: rgba(31, 36, 33, 0.62); inset: 0; @@ -741,10 +783,18 @@ button:disabled { border-radius: 13px; box-shadow: 0 18px 48px rgba(28, 34, 30, 0.28); display: grid; + max-height: calc(100dvh - 24px); + overflow: auto; padding: 17px; pointer-events: auto; position: fixed; } +.tour-card.tour-card-centered { + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: min(350px, calc(100vw - 24px)); +} .tour-card-progress { align-items: center; color: var(--muted); @@ -5094,8 +5144,6 @@ select:focus { border-top: 0; } .tour-card { - max-height: calc(100vh - 24px); - overflow: auto; width: auto; } } diff --git a/components/chart-maker.tsx b/components/chart-maker.tsx index f8a050c..b3f6491 100644 --- a/components/chart-maker.tsx +++ b/components/chart-maker.tsx @@ -165,6 +165,7 @@ export function ChartMaker({ useState(null); const [knitModeOpen, setKnitModeOpen] = useState(false); const [newYokeOpen, setNewYokeOpen] = useState(false); + const [tourFileOpen, setTourFileOpen] = useState(false); const [patternImporterOpen, setPatternImporterOpen] = useState(false); const [dimensionDraft, setDimensionDraft] = useSyncedDraft( `${activeTabId}:${document.width}:${document.height}`, @@ -1178,10 +1179,12 @@ export function ChartMaker({ onImportPattern={() => setPatternImporterOpen(true)} onNewBlank={addTab} onNewYoke={() => setNewYokeOpen(true)} + tourOpen={tourFileOpen} tabsFull={tabs.length >= MAX_CHART_TABS} onNavigateAway={confirmLeavingEditor} /> = MAX_CHART_TABS} onOpenExample={openTutorialExample} /> diff --git a/components/export-tools.tsx b/components/export-tools.tsx index 3b7336e..f2dbd3a 100644 --- a/components/export-tools.tsx +++ b/components/export-tools.tsx @@ -30,6 +30,7 @@ type Props = { onNewBlank?: () => void; onNewYoke?: () => void; tabsFull?: boolean; + tourOpen?: boolean; onNavigateAway?: (event: { preventDefault(): void }) => void; }; @@ -210,6 +211,7 @@ export function ExportTools({ onNewBlank, onNewYoke, tabsFull, + tourOpen = false, onNavigateAway, }: Props) { const inputRef = useRef(null); @@ -354,7 +356,7 @@ export function ExportTools({ /> - {open ? ( + {open || tourOpen ? (
@@ -351,7 +464,9 @@ export function OnboardingTour({ tabsFull, onOpenExample }: Props) {
- {example.document.width} × {example.document.height} + {example.document.yoke + ? `Circular yoke · ${example.document.height} rounds` + : `${example.document.width} × ${example.document.height}`}

{example.title}

{example.description}

@@ -361,6 +476,14 @@ export function OnboardingTour({ tabsFull, onOpenExample }: Props) { > Open example + {example.id === "tidal-fish-yoke" ? ( + + ) : null}
))} @@ -375,60 +498,71 @@ export function OnboardingTour({ tabsFull, onOpenExample }: Props) { ) : null} {view === "tour" ? ( -
- {targetRect ? ( -
- ) : ( -
- )} -
+ {step.scene && step.scene !== "file" ? ( + + ) : null} +
-
- - {stepIndex + 1} of {steps.length} - - -
-

{step.eyebrow}

-

{step.title}

-

{step.body}

-
- -
- {stepIndex > 0 ? ( + {targetRect ? ( +
+ ) : ( +
+ )} +
+
+ + {stepIndex + 1} of {steps.length} + + +
+

{step.eyebrow}

+

{step.title}

+

{step.body}

+
+ +
+ {stepIndex > 0 ? ( + + ) : null} - ) : null} - +
-
-
-
+ +
+ ) : null} ); diff --git a/components/yoke-editor.tsx b/components/yoke-editor.tsx index a149d9c..34de0f0 100644 --- a/components/yoke-editor.tsx +++ b/components/yoke-editor.tsx @@ -225,6 +225,7 @@ export function YokeEditor({ />
diff --git a/components/yoke-plan-dialog.tsx b/components/yoke-plan-dialog.tsx index bc4e794..0d87a85 100644 --- a/components/yoke-plan-dialog.tsx +++ b/components/yoke-plan-dialog.tsx @@ -87,12 +87,14 @@ export function YokePlanDialog({ tabsFull, onCommit, onClose, + tourStage, }: { document?: ChartDocument; initialMeasurements?: YokeMeasurements; tabsFull: boolean; onCommit: (document: ChartDocument, newTab: boolean) => boolean; onClose: () => void; + tourStage?: "measurements" | "schedule" | "preview"; }) { const [base] = useState(() => document); const measurements = @@ -103,10 +105,11 @@ export function YokePlanDialog({ calculateYokeTargets(measurements), measurements.repeats, ); - const [pending, setPending] = useState<{ + const [draft, setPending] = useState<{ measurements: YokeMeasurements; events: IncreaseEvent[]; } | null>(null); + const pending = tourStage === "preview" ? { measurements, events } : draft; const [mode, setMode] = useState<"keep" | "scale">("keep"); const [shortRows, setShortRows] = useState(); const [error, setError] = useState(""); @@ -189,6 +192,7 @@ export function YokePlanDialog({

) : null}
-
- Neckline short rows +
+ Neckline short rows drafts(measurements)); - const [candidate, setCandidate] = useState(null); + const [calculated, setCandidate] = useState(null); const [schedule, setSchedule] = useState(scheduleDrafts(events)); const [increaseRounds, setIncreaseRounds] = useState(String(events.length)); const [automatic, setAutomatic] = useState(true); const [error, setError] = useState(""); const [roundingAccepted, setRoundingAccepted] = useState(false); - const rounding = useMemo( - () => - candidate - ? yokeRoundingReview(candidate.measurements, candidate.targets) - : null, - [candidate], - ); const source = JSON.stringify({ values, unit, @@ -75,6 +70,21 @@ export function YokePlanner({ automatic, increaseRounds, }); + const candidate = useMemo( + () => + calculated ?? + (tourShowSchedule + ? { measurements, targets: calculateYokeTargets(measurements), source } + : null), + [calculated, tourShowSchedule, measurements, source], + ); + const rounding = useMemo( + () => + candidate + ? yokeRoundingReview(candidate.measurements, candidate.targets) + : null, + [candidate], + ); const fields = yokeMeasurementFields(mode); const dirty = candidate !== null && candidate.source !== source; const number = (value: number) => value.toFixed(1); @@ -151,7 +161,10 @@ export function YokePlanner({

-
+
Measurement input