@krapwoo/ds-viewer 0.4.7 → 0.4.9

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 (62) hide show
  1. package/README.md +21 -6
  2. package/dist/cli/dev.js +66 -37
  3. package/dist/cli/doctor.js +4 -2
  4. package/dist/cli/endpoint.js +33 -0
  5. package/dist/cli/init.js +5 -8
  6. package/dist/native/catalog/comparison.js +17 -8
  7. package/native/catalog/CatalogShell.tsx +5 -1
  8. package/native/catalog/ComparisonGrid.tsx +5 -1
  9. package/native/catalog/ComparisonGroups.tsx +7 -1
  10. package/native/catalog/ComparisonList.tsx +7 -1
  11. package/native/catalog/PhoneFrame.tsx +108 -24
  12. package/native/catalog/ReferenceDetails.tsx +8 -0
  13. package/native/catalog/SectionBlock.tsx +10 -4
  14. package/native/catalog/SpecimenAddress.tsx +15 -0
  15. package/native/catalog/SpecimenSurface.tsx +66 -4
  16. package/native/catalog/UpdatePanel.tsx +114 -103
  17. package/native/catalog/comparison.ts +21 -8
  18. package/native/catalog/deviceFrame.ts +14 -0
  19. package/native/catalog/overlayViewport.ts +4 -2
  20. package/native/catalog/specimenSurfaceStyle.ts +54 -0
  21. package/native/catalog/updatePanelState.ts +68 -1
  22. package/package.json +5 -4
  23. package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +1 -1
  24. package/starter-kit/components/Avatar/Avatar.catalog.tsx +1 -1
  25. package/starter-kit/components/Badge/Badge.catalog.tsx +1 -1
  26. package/starter-kit/components/Banner/Banner.catalog.tsx +2 -1
  27. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +5 -1
  28. package/starter-kit/components/Button/Button.catalog.tsx +1 -1
  29. package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +8 -1
  30. package/starter-kit/components/Card/Card.catalog.tsx +2 -1
  31. package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +2 -2
  32. package/starter-kit/components/Dialog/Dialog.catalog.tsx +1 -1
  33. package/starter-kit/components/Divider/Divider.catalog.tsx +2 -1
  34. package/starter-kit/components/Dock/Dock.catalog.tsx +4 -1
  35. package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +1 -1
  36. package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +2 -1
  37. package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +5 -1
  38. package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +1 -1
  39. package/starter-kit/components/InputField/InputField.catalog.tsx +3 -1
  40. package/starter-kit/components/List/List.catalog.tsx +2 -1
  41. package/starter-kit/components/ListItem/ListItem.catalog.tsx +7 -1
  42. package/starter-kit/components/Loading/Loading.catalog.tsx +2 -1
  43. package/starter-kit/components/Pill/Pill.catalog.tsx +1 -1
  44. package/starter-kit/components/PillRow/PillRow.catalog.tsx +1 -1
  45. package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +1 -1
  46. package/starter-kit/components/Radio/Radio.catalog.tsx +2 -2
  47. package/starter-kit/components/SearchField/SearchField.catalog.tsx +2 -1
  48. package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +2 -1
  49. package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +1 -3
  50. package/starter-kit/components/Shimmer/Breathing.catalog.tsx +2 -1
  51. package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +2 -1
  52. package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +5 -1
  53. package/starter-kit/components/Surface/Surface.catalog.tsx +2 -1
  54. package/starter-kit/components/Switch/Switch.catalog.tsx +2 -2
  55. package/starter-kit/components/TextArea/TextArea.catalog.tsx +2 -1
  56. package/starter-kit/components/Toast/Toast.catalog.tsx +2 -1
  57. package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +2 -1
  58. package/starter-kit/components/TopNav/TopNav.catalog.tsx +4 -1
  59. package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +1 -3
  60. package/starter-kit/pages/ReportIssue.catalog.tsx +18 -1
  61. package/starter-kit/pages/SavedTrips.catalog.tsx +27 -6
  62. package/starter-kit/pages/TripPlannerForm.catalog.tsx +7 -1
@@ -32,7 +32,7 @@ export type UpdateStatusView =
32
32
  | { phase: 'updating'; steps: { label: string; state: 'done' | 'now' | 'todo' }[] }
33
33
  | { phase: 'restarting' }
34
34
  | { phase: 'success'; doctorSummary: string; files: string[]; latest: string }
35
- | { phase: 'failure'; log: string; failedStep: string };
35
+ | { phase: 'failure'; log: string; failedStep: string; step?: 'install' | 'migrate' | 'doctor' | 'other' };
36
36
 
37
37
  export type UpdatePhase = 'checking' | 'ready' | 'dirty' | 'offline' | 'updating' | 'restarting' | 'success' | 'failure';
38
38
 
@@ -144,3 +144,70 @@ export function bumpLabel(current: string, latest: string, breaking: boolean): '
144
144
  if (!a || !b) return 'Minor';
145
145
  return a[0] === b[0] && a[1] === b[1] ? 'Patch' : 'Minor';
146
146
  }
147
+
148
+ export interface ResultNote {
149
+ tone: 'ok' | 'err';
150
+ title: string;
151
+ description: string;
152
+ detail: string;
153
+ /** The terminal fallback, shown as code inside `detail` (failures only). */
154
+ command?: string;
155
+ /** At most two words. `dismiss` clears the note; `retry` runs the whole update again; `resume`
156
+ * continues from the failed migrate or doctor step. */
157
+ action: { label: string; kind: 'dismiss' | 'retry' | 'resume' };
158
+ hasLog: boolean;
159
+ }
160
+
161
+ /** The note shown at the top of the update page after an update finished (approved mockup
162
+ * 2026-10-11): a header, a description, one detail line, and a bottom-left button. */
163
+ export function resultNote(
164
+ status: Extract<UpdateStatusView, { phase: 'success' | 'failure' }>,
165
+ current: string,
166
+ latest?: string,
167
+ ): ResultNote {
168
+ if (status.phase === 'success') {
169
+ return {
170
+ tone: 'ok',
171
+ title: `Updated to ${status.latest}`,
172
+ description: "The changes aren't committed yet, so review them in your editor.",
173
+ // Doctor's own summary line continues after an em dash with component counts; the note keeps
174
+ // only the error and warning counts.
175
+ detail: `Changed: ${status.files.join(', ') || 'nothing'} · Doctor: ${status.doctorSummary.split(' — ')[0]}`,
176
+ action: { label: 'Got it', kind: 'dismiss' },
177
+ hasLog: false,
178
+ };
179
+ }
180
+ const failure = (title: string, description: string, command: string, action: ResultNote['action']): ResultNote => ({
181
+ tone: 'err', title, description, detail: `You can also run ${command} in your terminal.`, command, action, hasLog: true,
182
+ });
183
+ switch (status.step) {
184
+ case 'migrate':
185
+ return failure(
186
+ 'The update installed, but migrating failed',
187
+ `Your package is on ${latest ?? 'the new version'}, but your files still match ${current}.`,
188
+ `npx ds-viewer migrate --from ${current}`,
189
+ { label: 'Finish migration', kind: 'resume' },
190
+ );
191
+ case 'doctor':
192
+ return failure(
193
+ 'The update installed, but doctor failed to run',
194
+ 'The update itself is done. Only the page check is missing.',
195
+ 'npx ds-viewer doctor',
196
+ { label: 'Rerun doctor', kind: 'resume' },
197
+ );
198
+ case 'install':
199
+ return failure(
200
+ 'The update stopped while installing',
201
+ 'Nothing was migrated. Retrying starts again from the download.',
202
+ 'npx ds-viewer update',
203
+ { label: 'Retry update', kind: 'retry' },
204
+ );
205
+ default:
206
+ return failure(
207
+ 'The update stopped',
208
+ 'Something unexpected went wrong. Retrying starts again from the download.',
209
+ 'npx ds-viewer update',
210
+ { label: 'Retry update', kind: 'retry' },
211
+ );
212
+ }
213
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krapwoo/ds-viewer",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A browsable, always-current catalog for an Expo / React Native design system.",
@@ -65,9 +65,10 @@
65
65
  "test": "node --experimental-strip-types --test 'cli/**/*.test.ts' 'config/**/*.test.ts' 'native/catalog/__tests__/*.test.ts'",
66
66
  "typecheck": "tsc -p tsconfig.json --noEmit",
67
67
  "kit:dev": "npm run build && cd kit-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js dev",
68
- "check:catalog": "npm run build && node scripts/checkCatalogConsole.mjs kit-host",
69
- "check:doctor": "npm run build && cd kit-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js doctor --ci",
70
- "check:types": "tsc --noEmit -p kit-host",
68
+ "viewer:dev": "npm run build && cd viewer-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js dev",
69
+ "check:catalog": "npm run build && node scripts/checkCatalogConsole.mjs kit-host && node scripts/checkCatalogConsole.mjs viewer-host",
70
+ "check:doctor": "npm run build && cd kit-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js doctor --ci && cd ../viewer-host && node node_modules/@krapwoo/ds-viewer/dist/cli/main.js doctor --ci",
71
+ "check:types": "tsc --noEmit -p kit-host && tsc --noEmit -p viewer-host",
71
72
  "release": "node scripts/release.mjs"
72
73
  }
73
74
  }
@@ -5,7 +5,7 @@ import { DS_SEMANTIC } from '../../tokens';
5
5
 
6
6
  export default defineCatalogPage({
7
7
  component: 'AnimatedChevron',
8
- group: 'Sub-Parts',
8
+ group: 'Components',
9
9
  description: 'A chevron that morphs between down (collapsed) and up (expanded) — flipping vertically in place instead of rotating through a sideways-pointing angle, and instead of swapping icons. The morph Banner\'s own collapsible header uses for its disclosure indicator. Not commonly used on its own; exported for building a custom disclosure/accordion toggle.',
10
10
  a11y: 'Purely decorative — no accessibility role of its own. Wrap it in an accessible parent (the way Banner\'s own header Pressable does) if the toggle needs to be announced.',
11
11
  variants: {
@@ -31,7 +31,7 @@ const AVATAR_COMPARISON = grid(
31
31
 
32
32
  export default defineCatalogPage({
33
33
  component: 'Avatar',
34
- group: 'Surfaces',
34
+ group: 'Components',
35
35
  specimenSize: 'compact',
36
36
  comparison: AVATAR_COMPARISON,
37
37
  description: 'A circular image, or an initials fallback on a solid fill when there\'s no image (or it fails to load).',
@@ -51,7 +51,7 @@ const BADGE_COMPARISON = grid(
51
51
 
52
52
  export default defineCatalogPage({
53
53
  component: 'Badge',
54
- group: 'Surfaces',
54
+ group: 'Components',
55
55
  specimenSize: 'compact',
56
56
  comparison: BADGE_COMPARISON,
57
57
  description: 'A small status chip — five semantic variants, with optional leading/trailing icons or icon-only.',
@@ -4,7 +4,8 @@ import { Banner } from './Banner';
4
4
 
5
5
  export default defineCatalogPage({
6
6
  component: 'Banner',
7
- group: 'Surfaces',
7
+ group: 'Components',
8
+ specimenSize: 'regular',
8
9
  composedOf: [
9
10
  { component: 'AnimatedChevron', role: "The expand/collapse chevron in the header of a collapsible banner.", relationship: 'built-in' },
10
11
  ],
@@ -69,7 +69,11 @@ function BottomSheetDemo() {
69
69
 
70
70
  export default defineCatalogPage({
71
71
  component: 'BottomSheet',
72
- group: 'Navigation',
72
+ group: 'Components',
73
+ composedOf: [
74
+ { component: 'TopNav', role: "The header slot, typically a TopNav with a close or back action.", relationship: 'slot' },
75
+ { component: 'Dock', role: "The footer slot, typically a Dock with the sheet's primary actions; a Dock footer gets its shadow once the content scrolls.", relationship: 'slot' },
76
+ ],
73
77
  description: 'A sheet that slides up over a dismissible backdrop. Height is driven by its content (not fixed snap points) up to 90% of the available height, then the content area scrolls. Compose header with TopNav and footer with Dock.',
74
78
  whenToUse: 'For longer, browsable content, or anything that benefits from a TopNav/Dock header-footer structure. For a short, focused decision that interrupts the flow, use Dialog.',
75
79
  a11y: 'The backdrop is a Pressable with accessibilityRole="button" and accessibilityLabel="Dismiss"; header/content/footer carry their own accessibility (e.g. TopNav\'s title as accessibilityRole="header").',
@@ -55,7 +55,7 @@ const BUTTON_COMPARISON = grid(
55
55
 
56
56
  export default defineCatalogPage({
57
57
  component: 'Button',
58
- group: 'Actions',
58
+ group: 'Components',
59
59
  composedOf: [
60
60
  { component: 'Loading', role: "The spinner that replaces the label and icon while loading is true.", relationship: 'built-in' },
61
61
  ],
@@ -5,7 +5,11 @@ import { Button } from '../Button';
5
5
 
6
6
  export default defineCatalogPage({
7
7
  component: 'ButtonGroup',
8
- group: 'Actions',
8
+ group: 'Components',
9
+ composedOf: [
10
+ { component: 'Button', role: "Its children: up to two Buttons side by side, or three stacked.", relationship: 'slot' },
11
+ ],
12
+ specimenSize: 'regular',
9
13
  description: 'Groups Button elements in one of two layouts: horizontal (up to two, auto-width, trailing-aligned) or vertical (up to three, stretched full width — the same layout Dock\'s own button area uses). Every button in a group should read as one family — the same variant tier (primary/secondary/tertiary/white, never ghost), the same size, and either all icon+label or all label-only, never a mix. Dev-console warns if two or more children disagree.',
10
14
  a11y: 'A plain View; each Button child carries its own accessibility role and label.',
11
15
  variants: {
@@ -15,6 +19,9 @@ export default defineCatalogPage({
15
19
  key: 'horizontal',
16
20
  name: 'Horizontal',
17
21
  props: { variant: 'horizontal' },
22
+ // Shown at its own width, centred: stretched, its trailing alignment pushes it to the
23
+ // cell's edge, which reads as a misplaced example rather than as trailing alignment.
24
+ fill: false,
18
25
  node: (
19
26
  <ButtonGroup variant="horizontal">
20
27
  <Button label="Cancel" variant="tertiary" onPress={() => {}} />
@@ -11,7 +11,8 @@ const styles = StyleSheet.create({
11
11
 
12
12
  export default defineCatalogPage({
13
13
  component: 'Card',
14
- group: 'Surfaces',
14
+ group: 'Components',
15
+ specimenSize: 'regular',
15
16
  description: 'The primary content surface — a rounded white card with a soft resting shadow. Pass children, or onPress to make the whole card a button.',
16
17
  whenToUse: 'A standalone, self-contained unit with its own shadow. For a row in a homogeneous stack of peers (with dividers between them), use ListItem inside a List instead.',
17
18
  a11y: 'When onPress is set, renders a Pressable with accessibilityRole="button" and a visible focus ring; a plain card is a non-interactive View.',
@@ -9,8 +9,8 @@ function CheckboxDemo() {
9
9
 
10
10
  export default defineCatalogPage({
11
11
  component: 'Checkbox',
12
- group: 'Controls',
13
- specimenSize: 'compact',
12
+ group: 'Components',
13
+ specimenSize: 'regular',
14
14
  description: 'A square selection control — the box fills with the accent colour and a checkmark when checked.',
15
15
  whenToUse: 'An independent on/off fact about this one item — any number can be checked at once. For a setting that takes effect immediately, use Switch; for one-of-many exclusive selection, use Radio.',
16
16
  a11y: 'Renders a Pressable with accessibilityRole="checkbox" and accessibilityState.checked; the optional label doubles as its accessibilityLabel.',
@@ -33,7 +33,7 @@ function DialogDemo() {
33
33
 
34
34
  export default defineCatalogPage({
35
35
  component: 'Dialog',
36
- group: 'Overlays',
36
+ group: 'Components',
37
37
  description: 'A card centred on screen over a dismissible backdrop — fades and scales in, distinct from BottomSheet\'s bottom-anchored slide. Named Dialog (not Modal) to avoid shadowing React Native\'s own built-in Modal.',
38
38
  whenToUse: 'A short, focused decision that interrupts the flow (confirm/cancel, a single form). For anything longer, browsable, or that needs its own internal scrolling, use BottomSheet.',
39
39
  a11y: 'The backdrop is a Pressable with accessibilityRole="button" and accessibilityLabel="Dismiss"; content you pass as children carries its own accessibility.',
@@ -11,7 +11,8 @@ const styles = StyleSheet.create({
11
11
 
12
12
  export default defineCatalogPage({
13
13
  component: 'Divider',
14
- group: 'Layout',
14
+ group: 'Components',
15
+ specimenSize: 'regular',
15
16
  description: 'A 1px hairline separator at the divider token colour.',
16
17
  a11y: 'A plain, non-interactive View — purely decorative.',
17
18
  // No `variant` prop and no other real configuration exists on Divider — a single hairline is the
@@ -5,7 +5,10 @@ import { Dock } from './Dock';
5
5
 
6
6
  export default defineCatalogPage({
7
7
  component: 'Dock',
8
- group: 'Navigation',
8
+ group: 'Components',
9
+ composedOf: [
10
+ { component: 'Button', role: "Its children: up to three full-width Buttons, stacked.", relationship: 'slot' },
11
+ ],
9
12
  description: 'Pinned to the bottom of the screen, above the home indicator — holds up to three full-width Buttons stacked vertically, with an optional small caption area above them.',
10
13
  a11y: 'A plain View; each Button child carries its own accessibility role and label.',
11
14
  // No `variant` prop exists on Dock — its "Variants" column just shows the one default look,
@@ -38,7 +38,7 @@ function DropdownDemo() {
38
38
 
39
39
  export default defineCatalogPage({
40
40
  component: 'Dropdown',
41
- group: 'Inputs',
41
+ group: 'Components',
42
42
  composedOf: [
43
43
  { component: 'FieldContainer', role: "The field frame around the label, value and chevron (the trigger).", relationship: 'built-in' },
44
44
  { component: 'BottomSheet', role: "The sheet listing the options while the dropdown is open.", relationship: 'built-in' },
@@ -5,7 +5,8 @@ import { DS_SEMANTIC } from '../../tokens';
5
5
 
6
6
  export default defineCatalogPage({
7
7
  component: 'EmptyState',
8
- group: 'Layout',
8
+ group: 'Components',
9
+ specimenSize: 'regular',
9
10
  composedOf: [
10
11
  { component: 'Avatar', role: "The icon circle above the title (iconName, default 'users').", relationship: 'built-in' },
11
12
  { component: 'ButtonGroup', role: "Stacks the action buttons when action is set.", relationship: 'built-in' },
@@ -12,7 +12,11 @@ const styles = StyleSheet.create({
12
12
 
13
13
  export default defineCatalogPage({
14
14
  component: 'FieldContainer',
15
- group: 'Sub-Parts',
15
+ group: 'Components',
16
+ composedOf: [
17
+ { component: 'Surface', role: "Reads the surrounding Surface tone: a recessed fill on white, opaque white inside a muted Surface.", relationship: 'related' },
18
+ ],
19
+ specimenSize: 'regular',
16
20
  description: 'The shared field chrome — white surface, medium radius, a 1px border that darkens on focus and mutes when disabled — behind InputField, TextArea, Dropdown, and SearchField, so all four share one visually-consistent field look instead of each re-implementing it.',
17
21
  a11y: 'Renders a plain View by default. Passing onPress (without disabled) makes it a Pressable with accessibilityRole="button" and the given accessibilityLabel — a consumer building a real editable field (InputField\'s editable mode) relies on its own inner TextInput for accessibility instead.',
18
22
  variants: {
@@ -4,7 +4,7 @@ import { InputClearButton } from './InputClearButton';
4
4
 
5
5
  export default defineCatalogPage({
6
6
  component: 'InputClearButton',
7
- group: 'Sub-Parts',
7
+ group: 'Components',
8
8
  description: 'The clear (×) button InputField and SearchField both show once a field is active and holds a value — a filled circle-x icon sized to reach the 44pt touch target via hitSlop, not visual size.',
9
9
  a11y: 'A Pressable with accessibilityRole="button" and the given (or default "Clear field") accessibilityLabel; hitSlop of 10 on every side pads its 24×24 visual size out to the 44pt minimum.',
10
10
  variants: {
@@ -17,9 +17,11 @@ function InputFieldDemo() {
17
17
 
18
18
  export default defineCatalogPage({
19
19
  component: 'InputField',
20
- group: 'Inputs',
20
+ group: 'Components',
21
+ specimenSize: 'regular',
21
22
  composedOf: [
22
23
  { component: 'InputClearButton', role: "The clear (\u00d7) button, shown while the field has a value and is focused (or active, when not editable).", relationship: 'built-in' },
24
+ { component: 'Surface', role: 'Reads the surrounding Surface tone so the field stays visible on a muted Surface.', relationship: 'related' },
23
25
  ],
24
26
  description: 'A floating-label field. Resting: a centred, body-sized label with no border. Active (focused, or picker with active set) or filled: the label floats to a small top caption, row 2 shows the value/input, and a border fades in while active.',
25
27
  whenToUse: 'A named field with a fixed identity ("To", "Arrive by") across the interaction. For free-text search with no floating label, use SearchField; for one of a small known set of choices, use Dropdown.',
@@ -8,7 +8,8 @@ import { DS_SEMANTIC } from '../../tokens';
8
8
 
9
9
  export default defineCatalogPage({
10
10
  component: 'List',
11
- group: 'Layout',
11
+ group: 'Components',
12
+ specimenSize: 'regular',
12
13
  composedOf: [
13
14
  { component: 'Divider', role: "The hairline between consecutive items (none after the last).", relationship: 'built-in' },
14
15
  ],
@@ -9,7 +9,13 @@ import { DS_SEMANTIC } from '../../tokens';
9
9
 
10
10
  export default defineCatalogPage({
11
11
  component: 'ListItem',
12
- group: 'Layout',
12
+ group: 'Components',
13
+ composedOf: [
14
+ { component: 'Avatar', role: "The leading slot, typically an Avatar (or an icon).", relationship: 'slot' },
15
+ { component: 'Switch', role: "A trailing-slot option, e.g. a settings toggle.", relationship: 'slot' },
16
+ { component: 'Button', role: "A trailing-slot option, e.g. an Accept action.", relationship: 'slot' },
17
+ ],
18
+ specimenSize: 'regular',
13
19
  description: 'A single row: optional leading/trailing slots flanking a title (+ optional subtitle/footer). Stack several inside a List for a settings screen, menu, or search-results list.',
14
20
  whenToUse: "A row in a set of visually-light peers sharing a List's own surface and dividers. For a standalone unit with its own shadow, use Card instead.",
15
21
  a11y: 'A tappable row (onPress set) renders as a Pressable with accessibilityRole="button" and an accessibilityLabel built from title + subtitle; a plain row is a non-interactive View.',
@@ -5,7 +5,8 @@ import { DS_SEMANTIC } from '../../tokens';
5
5
 
6
6
  export default defineCatalogPage({
7
7
  component: 'Loading',
8
- group: 'Feedback',
8
+ group: 'Components',
9
+ specimenSize: 'regular',
9
10
  description: 'An indeterminate loader — the shape fills up, empties out, then fills again, seamlessly. Two variants: circle (used internally by Button and Pill for their own loading states) and linear.',
10
11
  a11y: 'Exposed to assistive tech as accessibilityRole="progressbar" with accessibilityLabel="Loading" — no extra wiring needed at the call site.',
11
12
  // One example per `variant` enum value — `circle` is the default, so it comes first.
@@ -40,7 +40,7 @@ const PILL_COMPARISON = grid(
40
40
 
41
41
  export default defineCatalogPage({
42
42
  component: 'Pill',
43
- group: 'Actions',
43
+ group: 'Components',
44
44
  composedOf: [
45
45
  { component: 'Loading', role: "The spinner shown instead of the label and icon while loading is true.", relationship: 'built-in' },
46
46
  ],
@@ -4,7 +4,7 @@ import { PillRow } from './PillRow';
4
4
 
5
5
  export default defineCatalogPage({
6
6
  component: 'PillRow',
7
- group: 'Actions',
7
+ group: 'Components',
8
8
  composedOf: [
9
9
  { component: 'Pill', role: "Each item in pills, plus the trailing icon-only Edit pill when shown.", relationship: 'built-in' },
10
10
  ],
@@ -4,7 +4,7 @@ import { ProgressDots } from './ProgressDots';
4
4
 
5
5
  export default defineCatalogPage({
6
6
  component: 'ProgressDots',
7
- group: 'Feedback',
7
+ group: 'Components',
8
8
  description: 'A row of dots for a stepped flow; the active dot widens into a pill.',
9
9
  a11y: 'Decorative progress indicator; convey the "step X of Y" position in text for screen readers.',
10
10
  // No `variant` prop exists on ProgressDots — its "Variants" column just shows the one default
@@ -20,8 +20,8 @@ function RadioGroupDemo() {
20
20
 
21
21
  export default defineCatalogPage({
22
22
  component: 'Radio',
23
- group: 'Controls',
24
- specimenSize: 'compact',
23
+ group: 'Components',
24
+ specimenSize: 'regular',
25
25
  description: 'A single circular selection control — a filled dot appears in the ring when selected. A group of mutually-exclusive Radios is just multiple instances sharing one selected value in the consumer.',
26
26
  whenToUse: 'One selection from a mutually-exclusive set — checking one should un-check another. For an independent on/off fact, use Checkbox; for a setting that takes effect immediately, use Switch.',
27
27
  a11y: 'Renders a Pressable with accessibilityRole="radio" and accessibilityState.selected; the optional label doubles as its accessibilityLabel.',
@@ -9,7 +9,8 @@ function SearchFieldDemo() {
9
9
 
10
10
  export default defineCatalogPage({
11
11
  component: 'SearchField',
12
- group: 'Inputs',
12
+ group: 'Components',
13
+ specimenSize: 'regular',
13
14
  composedOf: [
14
15
  { component: 'FieldContainer', role: "The field frame around the search icon, input and clear button.", relationship: 'built-in' },
15
16
  { component: 'InputClearButton', role: "The clear (\u00d7) button, shown while focused with text.", relationship: 'built-in' },
@@ -4,7 +4,8 @@ import { SectionHeader } from './SectionHeader';
4
4
 
5
5
  export default defineCatalogPage({
6
6
  component: 'SectionHeader',
7
- group: 'Layout',
7
+ group: 'Components',
8
+ specimenSize: 'regular',
8
9
  composedOf: [
9
10
  { component: 'Button', role: "The trailing button, rendered when trailingButtonLabel is set.", relationship: 'built-in' },
10
11
  ],
@@ -21,9 +21,7 @@ function SegmentedToggleDemo() {
21
21
 
22
22
  export default defineCatalogPage({
23
23
  component: 'SegmentedToggle',
24
- group: 'Selection',
25
- // Phone width plus a small phone, where long labels truncate.
26
- previewWidths: [402, 320],
24
+ group: 'Components',
27
25
  description: 'A row of mutually-exclusive options on a recessed track, with a white thumb that slides to the selected segment. Two or more options.',
28
26
  whenToUse: 'A filled, heavier-weight control for a primary, prominent choice on the screen. For quieter secondary navigation, use UnderlineTabs.',
29
27
  a11y: 'The row is accessibilityRole="tablist"; each segment is a "tab" with accessibilityState.selected reflecting the current value.',
@@ -10,7 +10,8 @@ const styles = StyleSheet.create({
10
10
 
11
11
  export default defineCatalogPage({
12
12
  component: 'Breathing',
13
- group: 'Feedback',
13
+ group: 'Components',
14
+ specimenSize: 'regular',
14
15
  description:
15
16
  "The shared animated primitive behind Shimmer — a solid block that breathes between a low and high colour on one shared, module-level clock, so every Breathing/Shimmer instance on screen stays phase-locked instead of drifting out of sync. Exported so a custom composite skeleton can reuse the same pulse directly instead of going through Shimmer's fixed text/circle/container shapes.",
16
17
  a11y: 'Renders a plain Animated.View with no role by default — pass accessibilityRole="progressbar" and accessibilityLabel yourself, the same way Shimmer does internally for its own standalone (non-grouped) blocks.',
@@ -11,7 +11,8 @@ const styles = StyleSheet.create({
11
11
 
12
12
  export default defineCatalogPage({
13
13
  component: 'Shimmer',
14
- group: 'Feedback',
14
+ group: 'Components',
15
+ specimenSize: 'regular',
15
16
  description: 'Loading placeholders that breathe while content loads. `text` stands in for one line of text (an element with 3 lines gets 3 stacked `text` Shimmers, not one tall one), `circle` is always for a circular element, and `container` covers everything else (Card, Banner, image, …) — sized to match the real element it replaces, not an arbitrary block. When several stand in for one element, wrap them in `SkeletonGroup`.',
16
17
  whenToUse: 'A composite skeleton (several Shimmers standing in for one element) → wrap them in SkeletonGroup so it announces "Loading" once, not once per block. A single lone Shimmer needs no wrapper — it announces on its own.',
17
18
  a11y: 'A standalone Shimmer is exposed as accessibilityRole="progressbar" + accessibilityLabel="Loading". Inside a SkeletonGroup, each block goes silent and the group carries one busy "Loading" announcement for the whole skeleton — so a screen reader says "Loading" once, not once per block.',
@@ -11,7 +11,11 @@ const styles = StyleSheet.create({
11
11
 
12
12
  export default defineCatalogPage({
13
13
  component: 'SkeletonGroup',
14
- group: 'Sub-Parts',
14
+ group: 'Components',
15
+ composedOf: [
16
+ { component: 'Shimmer', role: 'Its children: the stacked Shimmers that stand in for one element, announced once as a single Loading region.', relationship: 'slot' },
17
+ ],
18
+ specimenSize: 'regular',
15
19
  description: 'An accessibility wrapper for a composite skeleton — several Shimmers standing in for one real element (e.g. a list row: an avatar + a two-line label). It announces the whole thing as one "Loading" region and silences the individual blocks, so a screen reader says "Loading" once instead of once per Shimmer. Adds no layout of its own — pass your own flexDirection/gap via style.',
16
20
  whenToUse: 'Wrap 2+ Shimmers that together stand in for one element. A single lone Shimmer already announces on its own and needs no wrapper.',
17
21
  a11y: 'Carries accessibilityRole="progressbar" + accessibilityLabel (default "Loading") + accessibilityState={{ busy: true }} for the whole region; provides a context that makes every descendant Shimmer drop its own announcement. Net effect: one "Loading" announcement per skeleton, not one per block.',
@@ -11,7 +11,8 @@ const styles = StyleSheet.create({
11
11
 
12
12
  export default defineCatalogPage({
13
13
  component: 'Surface',
14
- group: 'Surfaces',
14
+ group: 'Components',
15
+ specimenSize: 'regular',
15
16
  description:
16
17
  "Declares \"this subtree's background is tone\" — renders a plain View filled with the matching token (surface.white or surface.main) and provides that tone to descendants via context, so a tone-aware component inside (e.g. FieldContainer/InputField) can automatically pick a fill that contrasts with it instead of assuming white.",
17
18
  whenToUse:
@@ -14,8 +14,8 @@ function SwitchLabelDemo() {
14
14
 
15
15
  export default defineCatalogPage({
16
16
  component: 'Switch',
17
- group: 'Controls',
18
- specimenSize: 'compact',
17
+ group: 'Components',
18
+ specimenSize: 'regular',
19
19
  description: 'A boolean on/off toggle. The thumb slides and the track crossfades colour, sharing SegmentedToggle/UnderlineTabs\' own slide-animation hook for a consistent motion feel.',
20
20
  whenToUse: 'A setting that takes effect immediately, no separate save step. For recording a fact a future action (like a form submit) will act on, use Checkbox; for one-of-many exclusive selection, use Radio.',
21
21
  a11y: 'Renders a Pressable with accessibilityRole="switch" and accessibilityState.checked — pass accessibilityLabel to say what it controls.',
@@ -9,7 +9,8 @@ function TextAreaDemo() {
9
9
 
10
10
  export default defineCatalogPage({
11
11
  component: 'TextArea',
12
- group: 'Inputs',
12
+ group: 'Components',
13
+ specimenSize: 'regular',
13
14
  composedOf: [
14
15
  { component: 'FieldContainer', role: "The field frame around the multi-line input.", relationship: 'built-in' },
15
16
  ],
@@ -28,7 +28,8 @@ function ToastDemo() {
28
28
 
29
29
  export default defineCatalogPage({
30
30
  component: 'Toast',
31
- group: 'Feedback',
31
+ group: 'Components',
32
+ specimenSize: 'regular',
32
33
  description: 'A transient confirmation bar. Without a variant it renders the dark surface with inverse text; a variant shifts to a pastel status color.',
33
34
  whenToUse: "Transient and self-contained, floating over the screen, expected to go away on its own or via its own action. For a persistent, in-flow message about a standing condition, use Banner.",
34
35
  a11y: 'Rendered with accessibilityRole="alert" so screen readers announce it when it appears.',
@@ -31,7 +31,8 @@ function TooltipDemo() {
31
31
 
32
32
  export default defineCatalogPage({
33
33
  component: 'Tooltip',
34
- group: 'Overlays',
34
+ group: 'Components',
35
+ specimenSize: 'regular',
35
36
  description: 'A small floating label anchored above or below its wrapped trigger. Fully controlled — drive `visible` from the trigger\'s own onLongPress/onPressIn, since mobile has no hover.',
36
37
  a11y: 'The bubble is a plain, non-interactive View; the trigger you wrap it around carries its own accessibility.',
37
38
  variants: {
@@ -11,7 +11,10 @@ const styles = StyleSheet.create({
11
11
 
12
12
  export default defineCatalogPage({
13
13
  component: 'TopNav',
14
- group: 'Navigation',
14
+ group: 'Components',
15
+ composedOf: [
16
+ { component: 'Button', role: "The leading and trailing slots, typically small icon Buttons (back, close, an action).", relationship: 'slot' },
17
+ ],
15
18
  description: 'A screen\'s top bar — fixed-width leading/trailing slots flanking a centered title (or custom center content). Slots reserve their layout space even when empty, so the title stays centered no matter which sides are populated.',
16
19
  a11y: 'The title renders with accessibilityRole="header". Slot content (typically icon-only Buttons) carries its own accessibilityLabel.',
17
20
  // No `variant` prop exists on TopNav — its "Variants" column just shows the one default look,
@@ -20,9 +20,7 @@ function UnderlineTabsDemo() {
20
20
 
21
21
  export default defineCatalogPage({
22
22
  component: 'UnderlineTabs',
23
- group: 'Selection',
24
- // Phone width plus a small phone, where long labels truncate.
25
- previewWidths: [402, 320],
23
+ group: 'Components',
26
24
  description: 'A quieter tab switcher — left-aligned labels over a hairline rule, with a sliding underline indicator. Same options/value/onChange API as SegmentedToggle.',
27
25
  whenToUse: "Quiet, secondary navigation within a screen that already has a clear primary focus. For a prominent, primary choice, use SegmentedToggle.",
28
26
  a11y: 'Each tab is a Pressable label; the underline is a visual indicator only, so selection is also conveyed by the active label weight.',
@@ -140,7 +140,24 @@ function ReportIssueDemo() {
140
140
  }
141
141
 
142
142
  export default defineCatalogPage({
143
- group: 'Recipes',
143
+ group: 'Patterns',
144
+ composedOf: [
145
+ { component: 'BottomSheet', role: "Holds the whole report flow, opened from the Report an issue button.", relationship: 'built-in' },
146
+ { component: 'TopNav', role: "The sheet header with a Close button.", relationship: 'built-in' },
147
+ { component: 'ProgressDots', role: "The step indicator.", relationship: 'built-in' },
148
+ { component: 'Banner', role: "The Delays reported near 14 St service alert.", relationship: 'built-in' },
149
+ { component: 'PillRow', role: "The issue-type and line pickers.", relationship: 'built-in' },
150
+ { component: 'TextArea', role: "Free-text details.", relationship: 'built-in' },
151
+ { component: 'Checkbox', role: "Marks the report urgent.", relationship: 'built-in' },
152
+ { component: 'Radio', role: "Chooses between sharing publicly and reporting anonymously.", relationship: 'built-in' },
153
+ { component: 'Switch', role: "Notify me when resolved.", relationship: 'built-in' },
154
+ { component: 'SkeletonGroup', role: "Groups the loading lines for Recent reports near you into one loading region.", relationship: 'built-in' },
155
+ { component: 'Shimmer', role: "The loading lines themselves.", relationship: 'built-in' },
156
+ { component: 'Dock', role: "The sheet footer holding Submit report.", relationship: 'built-in' },
157
+ { component: 'Dialog', role: "Confirms discarding the report on close.", relationship: 'built-in' },
158
+ { component: 'ButtonGroup', role: "The dialog's Keep editing and Discard buttons.", relationship: 'built-in' },
159
+ { component: 'Button', role: "Report an issue, Close, Submit report, and the dialog's actions.", relationship: 'built-in' },
160
+ ],
144
161
  description: 'A composed real screen — a "report an issue" flow opened from a BottomSheet, combining a step indicator, a service-alert banner, a Pill-based issue-type and line picker (a data choice, not a view switch, so Pill rather than SegmentedToggle), free-text and toggle inputs, and a loading skeleton for recent reports. Dismissing with unsaved changes opens a confirmation Dialog.',
145
162
  tokenGallery: true,
146
163
  fullWidthLabel: 'Preview',