Skip to content

Commit a09631c

Browse files
committed
Onboarding: polish + design system + docs
- New "Soft sheets" subsection in `docs/design-system.md` § Component patterns: documents the `--sheet-*` tokens (added in M1), when to use a soft sheet vs `ModalDialog`, and points at `OnboardingWizard.svelte` as the canonical implementation. - `lib/ui/CLAUDE.md` opens with an "Not part of this module: soft sheets" note explaining the wizard isn't a `ModalDialog` variant and cross-references the design-system entry. - `docs/architecture.md` AI row clarifies the wizard owns first-launch consent now; the AI module only tracks runtime states. - `accessibility.spec.ts` extended with a wizard scan (re-entry path, axe over each step). Per-FDA-branch banner coverage stays in tier-3 Vitest.
1 parent 7d081d2 commit a09631c

4 files changed

Lines changed: 112 additions & 1 deletion

File tree

apps/desktop/src/lib/ui/CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ Reusable UI components used across the entire desktop app.
1919
| `ToggleGroup.svelte` | Generic segmented-control primitive: tabs ARIA shape or Ark toggle-group ARIA shape |
2020
| `toast/` | Centralized toast notification system: store, container, item |
2121

22+
## Not part of this module: soft sheets
23+
24+
`OnboardingWizard.svelte` (in `$lib/onboarding/`) is the canonical soft-sheet implementation: ~90% viewport coverage,
25+
frosted backdrop, no drag / Escape / × button, body owns the close gesture. It's NOT a `ModalDialog` variant — sheets
26+
break almost every `ModalDialog` constraint (full-bleed sizing, no title bar, no Escape, no draggable). Adding sheet
27+
variants to `ModalDialog` would dilute its contract; sheets get their own shell, their own `--sheet-*` design tokens
28+
(see [`docs/design-system.md`](../../../../docs/design-system.md) § "Soft sheets"), and their own focus-trap. They still
29+
plug into the same dialog registry (`'onboarding'`) so MCP tracking works through the same id-based surface.
30+
31+
Reach for a sheet when you have a multi-step flow the user must commit to. Reach for `ModalDialog` for everything else.
32+
2233
## ModalDialog
2334

2435
Props:

apps/desktop/test/e2e-playwright/accessibility.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,77 @@ for (const mode of ['light', 'dark'] as const) {
409409
}
410410
})
411411

412+
test(`Onboarding wizard (re-entry)`, async ({ tauriPage }) => {
413+
// The wizard is mounted via the same re-entry path the menu / palette use.
414+
// On macOS the E2E fixture grants FDA so re-entry shows step 1 (already-granted variant);
415+
// on Linux re-entry lands directly on step 2 (the Linux skip-step-1 path).
416+
// Step 3 is reached by clicking "One more optional setup step" on step 2.
417+
// Per-FDA-branch banner coverage stays in tier-3 Vitest (`StepAi.a11y.test.ts`).
418+
await ensureAppReady(tauriPage)
419+
420+
const WIZARD_SELECTOR = '[data-dialog-id="onboarding"]'
421+
422+
await dispatchMenuCommand(tauriPage, 'cmdr.openOnboarding')
423+
await tauriPage.waitForSelector(WIZARD_SELECTOR, 3000)
424+
425+
// Scan the wizard at its opening step (step 1 on macOS, step 2 on Linux).
426+
const { all: openingViolations } = await runAxeAudit(tauriPage, `Onboarding wizard opening (${mode})`, WIZARD_SELECTOR)
427+
expect(openingViolations, `Violations on wizard opening step (${mode})`).toHaveLength(0)
428+
429+
const isMac = process.platform === 'darwin'
430+
if (isMac) {
431+
// Advance step 1 (already-granted) → step 2 so we can scan it too.
432+
await tauriPage.evaluate(`(function() {
433+
var btn = document.querySelector('${WIZARD_SELECTOR} .primary-slot button');
434+
if (btn) btn.click();
435+
})()`)
436+
await expect
437+
.poll(async () => {
438+
return tauriPage.evaluate<number | null>(`(function() {
439+
var dots = document.querySelectorAll('${WIZARD_SELECTOR} .step-dot');
440+
for (var i = 0; i < dots.length; i++) {
441+
if (dots[i].getAttribute('aria-current') === 'step') return i + 1;
442+
}
443+
return null;
444+
})()`)
445+
}, { timeout: 3000 })
446+
.toBe(2)
447+
const { all: step2Violations } = await runAxeAudit(tauriPage, `Onboarding wizard step 2 (${mode})`, WIZARD_SELECTOR)
448+
expect(step2Violations, `Violations on wizard step 2 (${mode})`).toHaveLength(0)
449+
}
450+
451+
// Advance step 2 → step 3 via the "One more optional setup step" button (primary slot, last).
452+
await tauriPage.evaluate(`(function() {
453+
var btns = document.querySelectorAll('${WIZARD_SELECTOR} .primary-slot button');
454+
for (var i = 0; i < btns.length; i++) {
455+
if ((btns[i].textContent || '').indexOf('One more optional') !== -1) {
456+
btns[i].click();
457+
return;
458+
}
459+
}
460+
})()`)
461+
await expect
462+
.poll(async () => {
463+
return tauriPage.evaluate<number | null>(`(function() {
464+
var dots = document.querySelectorAll('${WIZARD_SELECTOR} .step-dot');
465+
for (var i = 0; i < dots.length; i++) {
466+
if (dots[i].getAttribute('aria-current') === 'step') return i + 1;
467+
}
468+
return null;
469+
})()`)
470+
}, { timeout: 3000 })
471+
.toBe(3)
472+
const { all: step3Violations } = await runAxeAudit(tauriPage, `Onboarding wizard step 3 (${mode})`, WIZARD_SELECTOR)
473+
expect(step3Violations, `Violations on wizard step 3 (${mode})`).toHaveLength(0)
474+
475+
// Finish so the wizard doesn't leak into the next test (the safety net would otherwise fire).
476+
await tauriPage.evaluate(`(function() {
477+
var btns = document.querySelectorAll('${WIZARD_SELECTOR} .primary-slot button');
478+
if (btns.length > 0) btns[btns.length - 1].click();
479+
})()`)
480+
await expect.poll(async () => !(await tauriPage.isVisible(WIZARD_SELECTOR)), { timeout: 3000 }).toBeTruthy()
481+
})
482+
412483
test(`File viewer with text file`, async ({ tauriPage }) => {
413484
await ensureAppReady(tauriPage)
414485

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ All under `apps/desktop/src/lib/`.
3232
| `logging/` | Unified logging: LogTape config, batching bridge to Rust, verbose toggle |
3333
| `error-reporter/` | Error report dialog (Flow A preview), auto-send toast (Flow B), shared `error-report-flow` entry point |
3434
| `crash-reporter/` | Frontend half of the crash pipeline: detects the persisted crash file on next launch and offers to send it |
35-
| `ai/` | Local LLM features (folder suggestions), download flow |
35+
| `ai/` | Local LLM features (folder suggestions), download flow. Runtime states only (`downloading`, `installing`, `ready`, `starting`); first-launch consent is owned by `lib/onboarding/` |
3636
| `indexing/` | Drive index state, events, priority triggers, scan status overlay |
3737
| `query-ui/` | Shared filter-and-act-on primitives consumed by Search and (M7+) Selection: `QueryBar`, `ModeChips`, `AiPromptStrip`, `FilterChips` (size/modified/scope/pattern), `QueryResults` (path pills + row menus), `EmptyState`, `recent-items/` footer + popover with adapter pattern, `createQueryFilterState()` factory, shared keyboard contract |
3838
| `search/` | Whole-drive file search dialog (first `query-ui` consumer): thin orchestrator + Search-only extras (scope, system-dir exclusions, AI label/pattern), snapshot store + virtual `search-results` volume, "Open in pane" handoff, MCP `open_search_dialog` tool, index lifecycle |

docs/design-system.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,35 @@ All dialogs use `ModalDialog.svelte`.
488488
Overlay: `background: rgba(0,0,0, 0.4)` in light mode, `rgba(0,0,0, 0.6)` in dark mode (higher opacity needed for
489489
contrast against dark chrome).
490490

491+
### Soft sheets (app)
492+
493+
Soft sheets cover ~90% of the viewport over the running app and host multi-step flows the user must commit to (consent,
494+
setup, onboarding). The canonical implementation is `OnboardingWizard.svelte`. Unlike `ModalDialog`, sheets have no
495+
title bar, no drag, no Escape close, no × button: the body owns the close gesture (Next / Finish / Allow / Deny). The
496+
sheet is centered, lifted off the canvas with a frosted backdrop, and sized via the `--sheet-*` tokens below.
497+
498+
| Token | Value | Role |
499+
| ------------------------- | --------------------------- | -------------------------------------------------------------------------------------------- |
500+
| `--sheet-width-fraction` | `90vw` | Sheet width target. Pair with `min(var(--sheet-max-width), var(--sheet-width-fraction))`. |
501+
| `--sheet-height-fraction` | `90vh` | Sheet height target. Pair with `min(var(--sheet-max-height), var(--sheet-height-fraction))`. |
502+
| `--sheet-max-width` | `1200px` | Hard cap so the sheet stays readable on ultra-wide displays. |
503+
| `--sheet-max-height` | `900px` | Hard cap so the sheet stays compact on 4K+ vertical setups. |
504+
| `--sheet-radius` | `var(--radius-lg)` (8px) | Matches macOS sheet convention. |
505+
| `--sheet-backdrop-blur` | `10px` | Frosted-glass amount. GPU-composited; the sheet is the only consumer today. |
506+
| `--sheet-backdrop-color` | `var(--color-overlay-heavy)` | Dim layer behind the sheet. Resolves to `rgba(0,0,0,0.6)` in both themes (heavier than `ModalDialog`'s scrim because sheets sit over the full app, not a centered cluster). |
507+
508+
**When to use a sheet vs `ModalDialog`:**
509+
510+
| Use a sheet for | Use `ModalDialog` for |
511+
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
512+
| Multi-step flows the user must commit to (onboarding, paid licensing later) | Single-decision confirmations (delete, discard, overwrite) |
513+
| Content that needs > 480px width (provider picker grids, comparison tables) | Short prose + a two-button choice |
514+
| Flows where Escape-to-dismiss would lose meaningful state | Flows where Escape-to-dismiss is safe (re-openable, idempotent) |
515+
| First-launch consent (user must choose, can't cancel) | Any everyday dialog (drag, blur, focus trap all come from the prim.) |
516+
517+
Sheets are heavier: they own their own backdrop, focus trap, MCP dialog-registry entry (`'onboarding'`), and footer
518+
chrome. Only use one when the contract genuinely needs it.
519+
491520
### Inputs (app)
492521

493522
```css

0 commit comments

Comments
 (0)