@crewlethq/ui 0.49.1 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +14 -0
  2. package/dist/ContrastBadge/ContrastBadge.d.ts +25 -3
  3. package/dist/ContrastBadge/index.d.ts +1 -1
  4. package/dist/ContrastBadge/index.js +1 -1
  5. package/dist/EditingStatus/EditingStatus.d.ts +15 -1
  6. package/dist/EditingStatus/index.js +3 -1
  7. package/dist/InlineEditor/index.js +4 -2
  8. package/dist/LogoSlot/index.js +2 -2
  9. package/dist/RichEditor/index.js +1 -1
  10. package/dist/SetupWizard/SetupWizard.d.ts +77 -0
  11. package/dist/SetupWizard/index.d.ts +3 -0
  12. package/dist/SetupWizard/index.js +11 -0
  13. package/dist/SetupWizard-Y5BTXZX5.css +197 -0
  14. package/dist/Stepper/Stepper.d.ts +60 -11
  15. package/dist/Stepper/index.d.ts +1 -1
  16. package/dist/Stepper/index.js +1 -1
  17. package/dist/Stepper-QEJI5RGX.css +355 -0
  18. package/dist/{chunk-ITI2Z44C.js → chunk-BEAXJARF.js} +1 -1
  19. package/dist/{chunk-7PCDKRNB.js → chunk-JWNYI7UD.js} +1 -1
  20. package/dist/chunk-KKHCUHWS.js +107 -0
  21. package/dist/chunk-LDGQIDIZ.js +129 -0
  22. package/dist/{chunk-MULOZYHL.js → chunk-MMOXWAUG.js} +16 -4
  23. package/dist/{chunk-B235ANDO.js → chunk-NSSOWQCJ.js} +20 -5
  24. package/dist/{chunk-7P336RA5.js → chunk-QGRA2RHC.js} +1 -1
  25. package/dist/generated/index.d.ts +1 -0
  26. package/dist/generated/index.js +24 -23
  27. package/dist/index.js +24 -23
  28. package/dist/styles.css +391 -12
  29. package/package.json +3 -3
  30. package/dist/Stepper-HW4JM3YH.css +0 -176
  31. package/dist/chunk-5QWIJSQM.js +0 -73
  32. package/dist/{Editor-ZYDXHGJE.js → Editor-MLGK3KOG.js} +1 -1
  33. /package/dist/{chunk-3WMOEGEL.js → chunk-HNQ2EIMR.js} +0 -0
package/README.md CHANGED
@@ -204,6 +204,20 @@ places:
204
204
  - A surface that pans or zooms dispatches `LAYER_REPOSITION_EVENT` on its host
205
205
  when the content beneath it moves, and every panel anchored inside follows.
206
206
 
207
+ ## Changes in 0.50.0
208
+
209
+ The pieces of a setup a person walks step by step, first used by the brand setup.
210
+
211
+ **`SetupWizard` is new**: a page of its own for a setup walked step by step. The header is a `PageHeader` title with the trail above it (`trail`) and the draft's status (`status`) and the way out (`actions`) beside it. The rail is a `nav` named by `stepsLabel`, holding a vertical `Stepper` of `steps` with `current`, `reached` and `onSelect`. The content is a `section` named by the step's `heading`, with a `lead` sentence and the step's fields (`children`) in one column of at most 640px. The optional `aside` is a complementary landmark named by `asideLabel` (required with it), held in view beside the content. The `footer` holds the step's actions at the bottom of the view, above the device's safe area. From 70rem of the wizard's own width the rail, the content and the aside stand side by side (240px, the content, 320px); under that, as in a 1024px window beside the application's rail, the aside moves under the content. Under the phone breakpoint the rail becomes a row of steps under "Step 3 of 6 · Colours" (`labels.progress`) and the aside folds into a disclosure (`labels.asideToggle`, "Preview"). Focus moves to the step's heading when the step changes, and the step's content and the aside start one heading level under it.
212
+
213
+ **`Stepper` takes `orientation`, a per-step `status` and `description`, and `onSelect`.** `orientation="vertical"` draws a column of rows: a 20px mark per step (the check, the dot, the warning glyph, or the step's number) on a hairline that joins them, the label beside it, and the description and the status words under it, with the current row lifted as the sidebar's is. A step's `status` (`done`, `current`, `upcoming`, `optional` or `attention`) draws it where its position does not say it all: an optional step says "Optional" (`optionalLabel`), and one that needs attention carries the warning glyph on the warning tint and says "Needs attention" (`attentionLabel`); in a row the words are said rather than shown. The current step is always drawn current, and `status: 'current'` on any other step is refused. With `onSelect`, every visited step (the current one, every step before it, and every step up to `reached`) is a button, the current one carrying `aria-current="step"`, and a step not reached yet stays words rather than a disabled button. The horizontal row draws exactly as before; its step-still-to-come class is now `crewlet-stepper__step--upcoming` (it was `--later`).
214
+
215
+ **`ContrastBadge` takes `advisory`**: under its scale's AA floor the badge reads "Low" in the warning tone with the warning glyph (`crewlet-contrast-badge--low`), rather than "AA large text" or "Fails" in the danger tone, for a pair a person is told about and may keep. A pair that blocks leaves it off and keeps today's behaviour. `labels.levels` now takes any of the level words on its own (`ContrastBadgeLabelsInput`), and the drawn level is typed `ContrastBadgeLevel`.
216
+
217
+ **`EditingStatus` takes `savedAt`**: with nothing unsaved and no save in flight, the status says how long ago the last save landed (`labels.saved`, "Saved 2m ago"; "Draft · Saved 2m ago" with `labels.editing` "Draft"). The time follows the shared clock and is hidden from the live region, which says "Saved" (`labels.savedSpoken`) once rather than every minute.
218
+
219
+ **`@crewlethq/tokens` exports `nearestContrastShade(hex, ground, min)`**: the colour nearest `hex` that clears `min` against `ground`, keeping its hue and moving only its OKLab lightness, or `null` when no shade can. `#00baba` on `#f6f7fb` gives `#007f7f` at 4.5:1. It is the one implementation a "Use #007f7f (4.51:1)" suggestion is computed with; see the tokens README.
220
+
207
221
  ## Changes in 0.49.1
208
222
 
209
223
  **`@crewlethq/tokens` keeps its palettes and its theme registry in modules of their own.** The `themes` object (both palettes, every value) was generated into the same module as `themeRegistry`, `themeGroups`, `DEFAULT_THEME_ID` and `isThemeId`, and a bundler keeps a module whole in the chunk that first needs it: an application that reads the registry at boot (a theme preference) carried every palette value in its entry as soon as any later screen imported `themes` (since 0.47.0, `LogoSlot`), about 8 KB raw and 2 KB gzipped on every first paint of the engine dashboard. `themes` now comes from a module of its own (`dist/palettes.js`), so a consumer of the registry never carries it and the palettes land in the chunk that uses them. The package's exports are unchanged: `import { themes, themeRegistry } from '@crewlethq/tokens'` works as before.
@@ -9,6 +9,11 @@ export type ContrastScale = 'body' | 'large';
9
9
  * 3:1 but not 4.5:1), or under every floor its scale has.
10
10
  */
11
11
  export type ContrastLevel = 'aaa' | 'aa' | 'aa-large' | 'fail';
12
+ /**
13
+ * What the badge draws: a level, or `low`, which an ADVISORY badge draws for
14
+ * a pair under its scale's AA floor in place of `aa-large` or `fail`.
15
+ */
16
+ export type ContrastBadgeLevel = ContrastLevel | 'low';
12
17
  /**
13
18
  * The level a measured ratio reaches, on the floors `@crewlethq/tokens`
14
19
  * exports. The ratio is compared UNROUNDED: a pair that passes only once
@@ -27,7 +32,7 @@ export declare function contrastRatio(foreground: string, background: string): n
27
32
  */
28
33
  export declare function formatContrastRatio(ratio: number): string;
29
34
  export interface ContrastBadgeLabels {
30
- levels: Record<ContrastLevel, string>;
35
+ levels: Record<ContrastBadgeLevel, string>;
31
36
  /** Said for a pair that cannot be measured (a colour not set yet). */
32
37
  unmeasured: string;
33
38
  /** The ratio, said in words: "4.49 to 1". */
@@ -35,6 +40,10 @@ export interface ContrastBadgeLabels {
35
40
  /** The letters the sample draws the pair with. */
36
41
  sample: string;
37
42
  }
43
+ /** The words a caller may replace: any of them, and any of the level words. */
44
+ export type ContrastBadgeLabelsInput = Partial<Omit<ContrastBadgeLabels, 'levels'>> & {
45
+ levels?: Partial<Record<ContrastBadgeLevel, string>> | undefined;
46
+ };
38
47
  export interface ContrastBadgeProps {
39
48
  /** The colour read: text, a logo, an accent. `#rrggbb`. */
40
49
  foreground: string;
@@ -44,8 +53,16 @@ export interface ContrastBadgeProps {
44
53
  label?: string | undefined;
45
54
  /** What it is read as: body text (the default), or large text and marks. */
46
55
  scale?: ContrastScale | undefined;
56
+ /**
57
+ * The pair is a warning and never a block: under its scale's AA floor it
58
+ * reads "Low" in the warning tone, rather than "AA large text" or "Fails"
59
+ * in the danger tone. For a pair a person is told about and allowed to
60
+ * keep, such as a brand's primary on its light ground; a pair that blocks
61
+ * (a brand's text on its light ground) leaves it off and keeps "Fails".
62
+ */
63
+ advisory?: boolean | undefined;
47
64
  className?: string | undefined;
48
- labels?: Partial<ContrastBadgeLabels> | undefined;
65
+ labels?: ContrastBadgeLabelsInput | undefined;
49
66
  }
50
67
  /**
51
68
  * Whether one colour reads on another: a sample of the pair, the measured
@@ -56,8 +73,13 @@ export interface ContrastBadgeProps {
56
73
  * every theme to, and the floors from its `WCAG_CONTRAST`, so the badge an
57
74
  * administrator reads and the gate a theme passes never disagree.
58
75
  *
76
+ * BLOCKING OR ADVISORY. By default a pair under every floor "Fails" in the
77
+ * danger tone. An `advisory` pair is one a person may keep: under its AA
78
+ * floor it reads "Low" in the warning tone with the warning glyph, so a
79
+ * screen of pairs tells the one that blocks from the ones that only warn.
80
+ *
59
81
  * THE LEVEL IS A WORD AND A GLYPH, never the tint alone, and the ratio is
60
82
  * said as "4.49 to 1" rather than read out as punctuation. The sample is
61
83
  * decoration: it shows the pair, and the words say how it measures.
62
84
  */
63
- export declare function ContrastBadge({ foreground, background, label, scale, className, labels }: ContrastBadgeProps): import("react").JSX.Element;
85
+ export declare function ContrastBadge({ foreground, background, label, scale, advisory, className, labels, }: ContrastBadgeProps): import("react").JSX.Element;
@@ -1,3 +1,3 @@
1
1
  import './ContrastBadge.css';
2
2
  export { ContrastBadge, contrastLevel, contrastRatio, formatContrastRatio } from './ContrastBadge.js';
3
- export type { ContrastBadgeLabels, ContrastBadgeProps, ContrastLevel, ContrastScale } from './ContrastBadge.js';
3
+ export type { ContrastBadgeLabels, ContrastBadgeLabelsInput, ContrastBadgeLevel, ContrastBadgeProps, ContrastLevel, ContrastScale, } from './ContrastBadge.js';
@@ -1,4 +1,4 @@
1
- export { ContrastBadge, contrastLevel, contrastRatio, formatContrastRatio } from '../chunk-B235ANDO.js';
1
+ export { ContrastBadge, contrastLevel, contrastRatio, formatContrastRatio } from '../chunk-NSSOWQCJ.js';
2
2
  import '../chunk-KYIAFNU7.js';
3
3
  import '../chunk-UZMNR366.js';
4
4
  import '../chunk-WIATRXEJ.js';
@@ -1,14 +1,28 @@
1
+ import { type TimeValue } from '../RelativeTime/index.js';
1
2
  /** Every string the status draws. */
2
3
  export interface EditingStatusLabels {
3
4
  editing: string;
4
5
  unsaved: string;
5
6
  saving: string;
7
+ /** Shown once the last save landed, with how long ago: "Saved 2 min ago". */
8
+ saved: (ago: string) => string;
9
+ /**
10
+ * Said in its place: the time is shown and not announced, so a screen
11
+ * reader hears "Saved" when the save lands rather than every minute after.
12
+ */
13
+ savedSpoken: string;
6
14
  }
7
15
  export interface EditingStatusProps {
8
16
  /** Whether anything differs from what was opened. */
9
17
  dirty: boolean;
10
18
  /** Whether a save is in flight. */
11
19
  saving?: boolean | undefined;
20
+ /**
21
+ * When the last save landed. Given, a status with nothing unsaved and no
22
+ * save in flight says how long ago: a draft that keeps itself, as a setup
23
+ * wizard's does ("Draft · Saved 2 min ago" with `labels.editing` "Draft").
24
+ */
25
+ savedAt?: TimeValue;
12
26
  labels?: Partial<EditingStatusLabels> | undefined;
13
27
  className?: string | undefined;
14
28
  }
@@ -22,4 +36,4 @@ export interface EditingStatusProps {
22
36
  * from saved to unsaved without being interrupted. On a narrow bar the words
23
37
  * fold away and the dot stays, named by the same words.
24
38
  */
25
- export declare function EditingStatus({ dirty, saving, labels, className }: EditingStatusProps): import("react").JSX.Element;
39
+ export declare function EditingStatus({ dirty, saving, savedAt, labels, className }: EditingStatusProps): import("react").JSX.Element;
@@ -1,3 +1,5 @@
1
- export { EditingStatus } from '../chunk-MULOZYHL.js';
1
+ export { EditingStatus } from '../chunk-MMOXWAUG.js';
2
+ import '../chunk-3CZ7XAVY.js';
3
+ import '../chunk-EUCC37WY.js';
2
4
  import '../chunk-BZFWDIV6.js';
3
5
  import '../chunk-ODEQTPFM.js';
@@ -1,5 +1,7 @@
1
- export { INLINE_EDITOR_LABELS, InlineEditor } from '../chunk-7P336RA5.js';
2
- import '../chunk-MULOZYHL.js';
1
+ export { INLINE_EDITOR_LABELS, InlineEditor } from '../chunk-QGRA2RHC.js';
2
+ import '../chunk-MMOXWAUG.js';
3
+ import '../chunk-3CZ7XAVY.js';
4
+ import '../chunk-EUCC37WY.js';
3
5
  import '../chunk-SMBKXWML.js';
4
6
  import '../chunk-OMV5W53A.js';
5
7
  import '../chunk-OG6CL3EB.js';
@@ -1,6 +1,6 @@
1
- export { LogoSlot } from '../chunk-ITI2Z44C.js';
1
+ export { LogoSlot } from '../chunk-BEAXJARF.js';
2
2
  import '../chunk-THDGBD6Y.js';
3
- import '../chunk-B235ANDO.js';
3
+ import '../chunk-NSSOWQCJ.js';
4
4
  import '../chunk-KYIAFNU7.js';
5
5
  import '../chunk-UZMNR366.js';
6
6
  import '../chunk-WIATRXEJ.js';
@@ -1,4 +1,4 @@
1
- export { RichEditor, preloadRichEditor } from '../chunk-7PCDKRNB.js';
1
+ export { RichEditor, preloadRichEditor } from '../chunk-JWNYI7UD.js';
2
2
  export { RICH_EDITOR_LABELS, richEditorLabels } from '../chunk-CSQTDGTS.js';
3
3
  import '../chunk-BAKPMH3S.js';
4
4
  import '../chunk-SZZRIHGT.js';
@@ -0,0 +1,77 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ import { type StepperStep } from '../Stepper/index.js';
3
+ import { type HeadingLevel } from '../utils/headingLevel.js';
4
+ export interface SetupWizardLabels {
5
+ /** Where the reader is, above the steps on a phone, before the step's name: "Step 3 of 6". */
6
+ progress: (position: number, total: number) => string;
7
+ /** The disclosure the aside folds into on a phone: "Preview". */
8
+ asideToggle: string;
9
+ }
10
+ export interface SetupWizardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'onSelect'> {
11
+ /** What is being set up: "Set up your brand". The page's heading. */
12
+ title: ReactNode;
13
+ /**
14
+ * The title's heading level. It follows the surrounding level by default,
15
+ * as `PageHeader` does; the step's heading is one level under it, and the
16
+ * step's own content and the aside start one level under that.
17
+ */
18
+ headingLevel?: HeadingLevel | undefined;
19
+ /** Where the page sits, above the title: a `Breadcrumb` "Company › Brand › Set up". */
20
+ trail?: ReactNode;
21
+ /** The draft's state, beside the actions: an `EditingStatus` "Draft saved 2 min ago". */
22
+ status?: ReactNode;
23
+ /** What leaves the wizard: Exit. */
24
+ actions?: ReactNode;
25
+ /** Every step, in order, as `Stepper` takes them (with a status and a description each where it has one). */
26
+ steps: readonly StepperStep[];
27
+ /** The step shown, by its id. */
28
+ current: string;
29
+ /** The furthest step visited, by its id, so the steps up to it stay buttons after walking back. `current` by default. */
30
+ reached?: string | undefined;
31
+ /** Called with a visited step's id when the reader picks it in the rail. Left out, the rail is words only. */
32
+ onSelect?: ((id: string) => void) | undefined;
33
+ /** What the steps are, for the navigation landmark and the list: "Setup steps". */
34
+ stepsLabel: string;
35
+ /** The step's heading: "Colours". */
36
+ heading: ReactNode;
37
+ /** One sentence under the heading saying why the step matters. */
38
+ lead?: ReactNode;
39
+ /** The step's content: its fields. */
40
+ children: ReactNode;
41
+ /** Beside the content on a wide page, under it on a narrower one, folded into a disclosure on a phone: a live preview. */
42
+ aside?: ReactNode;
43
+ /** What the aside is, for its complementary landmark: "Preview of your draft". Required with `aside`. */
44
+ asideLabel?: string | undefined;
45
+ /** The step's actions, held at the bottom of the view: Back, Skip for now, Continue. */
46
+ footer: ReactNode;
47
+ labels?: Partial<SetupWizardLabels> | undefined;
48
+ }
49
+ /**
50
+ * A setup a person walks step by step on a page of its own: a brand, an
51
+ * integration, a company.
52
+ *
53
+ * FIVE PARTS, EACH A LANDMARK OR A HEADING. The header is the page's title
54
+ * (a `PageHeader`) with the trail above it and the draft's status and the way
55
+ * out beside it. The rail is a `nav` holding a vertical `Stepper`, with every
56
+ * visited step a button. The content is a `section` named by the step's
57
+ * heading, with a lead sentence and the step's fields in one column of at
58
+ * most 640px. The aside is a complementary landmark named by `asideLabel`,
59
+ * held in view beside the content. The footer holds the step's actions at the
60
+ * bottom of the view, above the device's safe area.
61
+ *
62
+ * THREE SHAPES. On a wide page the rail, the content and the aside stand side
63
+ * by side (240px, the content, 320px). Where the wizard has under 70rem of its
64
+ * own width, which is a 1024px window beside the application's rail, the aside
65
+ * moves under the content and the rail stays: a container query, because the
66
+ * room is the page's and not the window's, as in `AsideLayout`. Under the
67
+ * phone breakpoint the rail becomes a row of steps above the content, under a
68
+ * line that says "Step 3 of 6 · Colours", and the aside folds into a
69
+ * disclosure, so the fields come first. Which element a phone draws is decided
70
+ * in `useMediaMatch`, the one subscription for that question; how it is laid
71
+ * out is decided in the stylesheet, at the same breakpoint token.
72
+ *
73
+ * FOCUS FOLLOWS THE STEP. When the step changes, focus moves to its heading,
74
+ * so a keyboard or screen reader user starts on the new step rather than on a
75
+ * Continue button that now belongs to it; the first render takes no focus.
76
+ */
77
+ export declare function SetupWizard({ title, headingLevel, trail, status, actions, steps, current, reached, onSelect, stepsLabel, heading, lead, children, aside, asideLabel, footer, labels, className, ...rest }: SetupWizardProps): import("react").JSX.Element;
@@ -0,0 +1,3 @@
1
+ import './SetupWizard.css';
2
+ export { SetupWizard } from './SetupWizard.js';
3
+ export type { SetupWizardLabels, SetupWizardProps } from './SetupWizard.js';
@@ -0,0 +1,11 @@
1
+ export { SetupWizard } from '../chunk-KKHCUHWS.js';
2
+ import '../chunk-LDGQIDIZ.js';
3
+ import '../chunk-DKB73KZI.js';
4
+ import '../chunk-WIATRXEJ.js';
5
+ import '../chunk-OGXHZHRX.js';
6
+ import '../chunk-FZVJDJFN.js';
7
+ import '../chunk-BHHIIK3M.js';
8
+ import '../chunk-PQADV6P6.js';
9
+ import '../chunk-RTPQDNW3.js';
10
+ import '../chunk-BZFWDIV6.js';
11
+ import '../chunk-ODEQTPFM.js';
@@ -0,0 +1,197 @@
1
+ /*
2
+ * SetupWizard: a setup walked step by step on a page of its own
3
+ * (SetupWizard.tsx).
4
+ *
5
+ * THE GRID. The header across the top, the footer across the bottom, and
6
+ * between them the rail (240px), the content (one column, at most 640px) and,
7
+ * from 70rem of the wizard's own width, the aside (320px). Under 70rem the
8
+ * aside takes the row under the content and the rail runs down beside both.
9
+ * Under the phone breakpoint every part is one column, in reading order.
10
+ */
11
+
12
+ .crewlet-setup-wizard {
13
+ container-type: inline-size;
14
+ min-width: 0;
15
+ }
16
+
17
+ .crewlet-setup-wizard__frame {
18
+ display: grid;
19
+ grid-template-columns: 240px minmax(0, 1fr);
20
+ grid-template-areas:
21
+ 'head head'
22
+ 'rail content'
23
+ 'rail aside'
24
+ 'foot foot';
25
+ grid-template-rows: auto auto 1fr auto;
26
+ column-gap: var(--spacing-8);
27
+ row-gap: var(--spacing-6);
28
+ min-width: 0;
29
+ }
30
+
31
+ .crewlet-setup-wizard__head {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: var(--spacing-2);
35
+ grid-area: head;
36
+ min-width: 0;
37
+ }
38
+
39
+ .crewlet-setup-wizard__status {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ }
43
+
44
+ /*
45
+ * THE RAIL stays in view while the content scrolls past it, as a column of
46
+ * the steps a reader can go back to.
47
+ */
48
+ .crewlet-setup-wizard__rail {
49
+ position: sticky;
50
+ top: var(--spacing-5);
51
+ align-self: start;
52
+ grid-area: rail;
53
+ min-width: 0;
54
+ }
55
+
56
+ .crewlet-setup-wizard__content {
57
+ display: flex;
58
+ flex-direction: column;
59
+ grid-area: content;
60
+ box-sizing: border-box;
61
+ width: 100%;
62
+ max-width: 640px;
63
+ min-width: 0;
64
+ }
65
+
66
+ .crewlet-setup-wizard__heading {
67
+ margin: 0;
68
+ color: var(--color-text-primary);
69
+ font-family: var(--font-family-sans);
70
+ font-size: var(--font-size-lg);
71
+ font-weight: var(--font-weight-semibold);
72
+ line-height: var(--font-line-height-tight);
73
+ }
74
+
75
+ /*
76
+ * The heading takes focus when the step changes. It keeps the browser's own
77
+ * focus treatment: after a press nothing is drawn, and after a key the ring
78
+ * shows where the reader now is.
79
+ */
80
+
81
+ .crewlet-setup-wizard__lead {
82
+ margin: var(--spacing-2) 0 0;
83
+ max-width: var(--size-measure);
84
+ color: var(--color-text-secondary);
85
+ font-family: var(--font-family-sans);
86
+ font-size: var(--font-size-sm);
87
+ line-height: var(--font-line-height-body);
88
+ }
89
+
90
+ .crewlet-setup-wizard__body {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: var(--spacing-6);
94
+ margin-top: var(--spacing-6);
95
+ min-width: 0;
96
+ }
97
+
98
+ /* UNDER THE CONTENT, the aside is set off by one hairline, as AsideLayout's is. */
99
+ .crewlet-setup-wizard__aside {
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: var(--spacing-4);
103
+ grid-area: aside;
104
+ box-sizing: border-box;
105
+ max-width: 640px;
106
+ min-width: 0;
107
+ padding-top: var(--spacing-6);
108
+ border-top: 1px solid var(--color-border-default);
109
+ }
110
+
111
+ /*
112
+ * THE FOOTER holds the step's actions at the bottom of the view while the
113
+ * content scrolls under it, on the sheet's own ground with a hairline above,
114
+ * and clears the device's safe area where it has one.
115
+ */
116
+ .crewlet-setup-wizard__footer {
117
+ position: sticky;
118
+ bottom: 0;
119
+ z-index: var(--z-index-sticky);
120
+ display: flex;
121
+ flex-wrap: wrap;
122
+ align-items: center;
123
+ justify-content: flex-end;
124
+ gap: var(--spacing-2);
125
+ grid-area: foot;
126
+ padding: var(--spacing-3) 0 calc(var(--spacing-3) + env(safe-area-inset-bottom, var(--spacing-0)));
127
+ border-top: 1px solid var(--color-border-default);
128
+ background: var(--color-surface-background);
129
+ }
130
+
131
+ /* BESIDE THE CONTENT, from 70rem of the wizard's own width: the aside holds in view. */
132
+ @container (min-width: 70rem) {
133
+ .crewlet-setup-wizard__frame {
134
+ grid-template-columns: 240px minmax(0, 1fr) 320px;
135
+ grid-template-areas:
136
+ 'head head head'
137
+ 'rail content aside'
138
+ 'foot foot foot';
139
+ grid-template-rows: auto 1fr auto;
140
+ }
141
+
142
+ .crewlet-setup-wizard__aside {
143
+ position: sticky;
144
+ top: var(--spacing-5);
145
+ align-self: start;
146
+ padding-top: 0;
147
+ border-top: 0;
148
+ }
149
+ }
150
+
151
+ /*
152
+ * ON A PHONE, one column in reading order: where the reader is, the step, the
153
+ * folded preview, and the actions. The rail is a row of steps under the
154
+ * position line, and scrolls away with the page rather than covering it.
155
+ */
156
+ @media (width < 640px) {
157
+ .crewlet-setup-wizard__frame {
158
+ grid-template-columns: minmax(0, 1fr);
159
+ grid-template-areas:
160
+ 'head'
161
+ 'rail'
162
+ 'content'
163
+ 'aside'
164
+ 'foot';
165
+ grid-template-rows: auto;
166
+ row-gap: var(--spacing-5);
167
+ }
168
+
169
+ .crewlet-setup-wizard__rail {
170
+ position: static;
171
+ display: flex;
172
+ flex-direction: column;
173
+ gap: var(--spacing-2);
174
+ }
175
+
176
+ .crewlet-setup-wizard__preview {
177
+ grid-area: aside;
178
+ min-width: 0;
179
+ }
180
+
181
+ .crewlet-setup-wizard__preview .crewlet-setup-wizard__aside {
182
+ padding-top: 0;
183
+ border-top: 0;
184
+ }
185
+ }
186
+
187
+ .crewlet-setup-wizard__position {
188
+ margin: 0;
189
+ color: var(--color-text-secondary);
190
+ font-family: var(--font-family-sans);
191
+ font-size: var(--font-size-xs);
192
+ font-weight: var(--font-weight-medium);
193
+ }
194
+
195
+ .crewlet-setup-wizard__position-step {
196
+ color: var(--color-text-primary);
197
+ }
@@ -12,6 +12,16 @@ import type { Tone } from '../utils/tone.js';
12
12
  * but `aria-current`, which nobody sees.
13
13
  */
14
14
  export type StepperTone = Exclude<Tone, 'neutral'>;
15
+ /**
16
+ * Where one step stands. `done`, `current` and `upcoming` are what a step's
17
+ * position against `current` already says; `optional` is a step that may be
18
+ * skipped and has not been completed, drawn quietly with the word "Optional";
19
+ * `attention` is a step with something to look at again (a low-contrast pair,
20
+ * a refused file), drawn with a warning glyph and the words "Needs attention".
21
+ */
22
+ export type StepperStatus = 'done' | 'current' | 'upcoming' | 'optional' | 'attention';
23
+ /** How the steps are laid out: a row of pills, or a column of rows with a mark each. */
24
+ export type StepperOrientation = 'horizontal' | 'vertical';
15
25
  /** One step of the sequence. */
16
26
  export interface StepperStep {
17
27
  /** What tells two steps apart, unique within the stepper: `execute`, `review`. */
@@ -21,15 +31,26 @@ export interface StepperStep {
21
31
  * round 7 of 25", "Context 0.8s".
22
32
  */
23
33
  label: ReactNode;
34
+ /**
35
+ * Where the step stands, when its position does not say it all. Left out,
36
+ * a step before `current` is `done` and one after it is `upcoming`. The
37
+ * current step is always drawn current: `attention` or `optional` on it adds
38
+ * the warning glyph or the word "Optional" to it, and `current` on any other
39
+ * step is refused, since only `current` names the step under way.
40
+ */
41
+ status?: StepperStatus | undefined;
42
+ /** A line under the label, in the vertical orientation only: "Name and tagline". */
43
+ description?: ReactNode;
24
44
  }
25
- export interface StepperProps extends Omit<HTMLAttributes<HTMLOListElement>, 'children'> {
45
+ export interface StepperProps extends Omit<HTMLAttributes<HTMLOListElement>, 'children' | 'onSelect'> {
26
46
  /** Every step, in order. */
27
47
  steps: readonly StepperStep[];
28
48
  /**
29
49
  * The step under way, by its id. Every step before it is done and every
30
- * step after it is still to come. `null` once EVERY step is done, which is
31
- * how a finished run is drawn: a turn's history in a task's timeline reads
32
- * "Execute ✓ — Review ✓" with nothing current.
50
+ * step after it is still to come, unless a step's `status` says otherwise.
51
+ * `null` once EVERY step is done, which is how a finished run is drawn: a
52
+ * turn's history in a task's timeline reads "Execute ✓, Review ✓" with
53
+ * nothing current.
33
54
  */
34
55
  current: string | null;
35
56
  /**
@@ -47,32 +68,60 @@ export interface StepperProps extends Omit<HTMLAttributes<HTMLOListElement>, 'ch
47
68
  * motion preference.
48
69
  */
49
70
  pulse?: boolean | undefined;
71
+ /** A row of pills (the default), or a column of rows, as a setup wizard's rail draws it. */
72
+ orientation?: StepperOrientation | undefined;
73
+ /**
74
+ * Called with a step's id when a reader picks it. Given, every VISITED step
75
+ * (the current one, every step before it, and every step up to `reached`)
76
+ * is a button; a step not visited yet stays words, reachable only by
77
+ * whatever moves the sequence on, never a disabled button that is a tab stop
78
+ * with nothing behind it.
79
+ */
80
+ onSelect?: ((id: string) => void) | undefined;
81
+ /**
82
+ * The furthest step a reader has visited, by its id, for a sequence they
83
+ * can walk back through and forward again: on step 2 after reaching step 5,
84
+ * steps 3 to 5 are still buttons. `current` by default.
85
+ */
86
+ reached?: string | undefined;
50
87
  /**
51
88
  * Said before a finished step's words, where the check glyph stands for a
52
89
  * reader who sees it: "Done Context".
53
90
  */
54
91
  doneLabel?: string | undefined;
92
+ /** Said after an optional step's words, and shown under them in the vertical orientation. "Optional" by default. */
93
+ optionalLabel?: string | undefined;
94
+ /** Said after the words of a step that needs attention, and shown under them vertically. "Needs attention" by default. */
95
+ attentionLabel?: string | undefined;
55
96
  }
56
97
  /**
57
- * Where a sequence has got to: the phases of a turn, the stages of a run.
98
+ * Where a sequence has got to: the phases of a turn, the stages of a run, the
99
+ * steps of a setup a person walks themselves.
58
100
  *
59
101
  * AN ORDERED LIST, because the order IS the information: a step's position
60
102
  * says whether it is behind the current one or ahead of it, and a list says
61
103
  * how many there are before a reader has heard any of them. The step under
62
104
  * way carries `aria-current="step"`, the one value ARIA gives this exact
63
105
  * meaning, and it is on exactly one step while the sequence is running and on
64
- * none once it has finished.
106
+ * none once it has finished. When the steps are buttons it is on the button,
107
+ * which is what a reader focuses and so what tells them.
65
108
  *
66
- * THREE STATES, EACH DRAWN TWO WAYS, so colour is never the only carrier. A
109
+ * EVERY STATE IS DRAWN TWO WAYS, so colour is never the only carrier. A
67
110
  * finished step is the secondary ink with a check, and says "Done" before its
68
111
  * words to a screen reader, where the check stands for a reader who sees it.
69
112
  * The current step is its tone's soft tint under the tone's ink with the
70
- * tone's dot, and is `aria-current`. A step still to come is the tertiary ink
71
- * and nothing else. The hairline round the chips, and the short rule between
72
- * two of them, are the design's quiet furniture.
113
+ * tone's dot. A step still to come is the tertiary ink and nothing else; an
114
+ * optional one says "Optional", and one that needs attention carries the
115
+ * warning glyph on the warning tint and says "Needs attention".
116
+ *
117
+ * TWO ORIENTATIONS. The row is the approved design's pills joined by a short
118
+ * rule. The column is a setup wizard's rail: a 20px mark per step (the check,
119
+ * the dot, the warning glyph or the step's number) on a hairline that joins
120
+ * them, the label beside it, and a description and the status words under
121
+ * the label.
73
122
  *
74
123
  * THE TONE IS DRAWN, NEVER SPOKEN: a reader hears which step is current, not
75
124
  * that it is in the warning hue. A current step whose state is anything but
76
125
  * ordinary work says so in its own words ("Review · sent back").
77
126
  */
78
- export declare function Stepper({ steps, current, label, tone, pulse, doneLabel, className, ...rest }: StepperProps): import("react").JSX.Element | null;
127
+ export declare function Stepper({ steps, current, label, tone, pulse, orientation, onSelect, reached, doneLabel, optionalLabel, attentionLabel, className, ...rest }: StepperProps): import("react").JSX.Element | null;
@@ -1,3 +1,3 @@
1
1
  import './Stepper.css';
2
2
  export { Stepper } from './Stepper.js';
3
- export type { StepperProps, StepperStep, StepperTone } from './Stepper.js';
3
+ export type { StepperOrientation, StepperProps, StepperStatus, StepperStep, StepperTone } from './Stepper.js';
@@ -1,4 +1,4 @@
1
- export { Stepper } from '../chunk-5QWIJSQM.js';
1
+ export { Stepper } from '../chunk-LDGQIDIZ.js';
2
2
  import '../chunk-WIATRXEJ.js';
3
3
  import '../chunk-BZFWDIV6.js';
4
4
  import '../chunk-ODEQTPFM.js';