@krapwoo/ds-viewer 0.4.6 → 0.4.8

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 (65) hide show
  1. package/README.md +121 -21
  2. package/dist/cli/doctor.js +141 -2
  3. package/dist/cli/explain.js +44 -6
  4. package/dist/cli/init.js +5 -8
  5. package/dist/cli/props.js +80 -1
  6. package/dist/cli/staticPage.js +228 -6
  7. package/dist/native/catalog/comparison.js +43 -9
  8. package/native/catalog/BoundedOverlayViewport.tsx +112 -0
  9. package/native/catalog/CatalogShell.tsx +26 -0
  10. package/native/catalog/ComparisonGrid.tsx +6 -2
  11. package/native/catalog/ComparisonGroups.tsx +8 -2
  12. package/native/catalog/ComparisonList.tsx +8 -2
  13. package/native/catalog/PhoneFrame.tsx +109 -19
  14. package/native/catalog/ReferenceDetails.tsx +83 -20
  15. package/native/catalog/SectionBlock.tsx +75 -7
  16. package/native/catalog/SpecimenAddress.tsx +15 -0
  17. package/native/catalog/SpecimenSurface.tsx +80 -10
  18. package/native/catalog/comparison.ts +57 -10
  19. package/native/catalog/deviceFrame.ts +14 -0
  20. package/native/catalog/index.ts +4 -0
  21. package/native/catalog/overlayViewport.ts +158 -0
  22. package/native/catalog/pageApi.ts +6 -0
  23. package/native/catalog/specimenSurfaceStyle.ts +124 -1
  24. package/native/catalog/types.ts +42 -12
  25. package/package.json +5 -4
  26. package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +1 -1
  27. package/starter-kit/components/Avatar/Avatar.catalog.tsx +1 -1
  28. package/starter-kit/components/Badge/Badge.catalog.tsx +1 -1
  29. package/starter-kit/components/Banner/Banner.catalog.tsx +5 -1
  30. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +5 -1
  31. package/starter-kit/components/Button/Button.catalog.tsx +4 -1
  32. package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +8 -1
  33. package/starter-kit/components/Card/Card.catalog.tsx +2 -1
  34. package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +2 -2
  35. package/starter-kit/components/Dialog/Dialog.catalog.tsx +1 -1
  36. package/starter-kit/components/Divider/Divider.catalog.tsx +2 -1
  37. package/starter-kit/components/Dock/Dock.catalog.tsx +4 -1
  38. package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +7 -1
  39. package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +7 -1
  40. package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +5 -1
  41. package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +1 -1
  42. package/starter-kit/components/InputField/InputField.catalog.tsx +6 -1
  43. package/starter-kit/components/List/List.catalog.tsx +5 -1
  44. package/starter-kit/components/ListItem/ListItem.catalog.tsx +7 -1
  45. package/starter-kit/components/Loading/Loading.catalog.tsx +2 -1
  46. package/starter-kit/components/Pill/Pill.catalog.tsx +4 -1
  47. package/starter-kit/components/PillRow/PillRow.catalog.tsx +4 -1
  48. package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +1 -1
  49. package/starter-kit/components/Radio/Radio.catalog.tsx +2 -2
  50. package/starter-kit/components/SearchField/SearchField.catalog.tsx +6 -1
  51. package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +5 -1
  52. package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +1 -3
  53. package/starter-kit/components/Shimmer/Breathing.catalog.tsx +2 -1
  54. package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +2 -1
  55. package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +5 -1
  56. package/starter-kit/components/Surface/Surface.catalog.tsx +2 -1
  57. package/starter-kit/components/Switch/Switch.catalog.tsx +2 -2
  58. package/starter-kit/components/TextArea/TextArea.catalog.tsx +5 -1
  59. package/starter-kit/components/Toast/Toast.catalog.tsx +9 -1
  60. package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +2 -1
  61. package/starter-kit/components/TopNav/TopNav.catalog.tsx +4 -1
  62. package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +1 -3
  63. package/starter-kit/pages/ReportIssue.catalog.tsx +18 -1
  64. package/starter-kit/pages/SavedTrips.catalog.tsx +27 -6
  65. package/starter-kit/pages/TripPlannerForm.catalog.tsx +7 -1
@@ -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,10 @@ const BUTTON_COMPARISON = grid(
55
55
 
56
56
  export default defineCatalogPage({
57
57
  component: 'Button',
58
- group: 'Actions',
58
+ group: 'Components',
59
+ composedOf: [
60
+ { component: 'Loading', role: "The spinner that replaces the label and icon while loading is true.", relationship: 'built-in' },
61
+ ],
59
62
  comparison: BUTTON_COMPARISON,
60
63
  description:
61
64
  'The primary tap target. Five visual weights, three sizes, optional leading/trailing icon, plus loading and icon-only modes.',
@@ -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,13 @@ function DropdownDemo() {
38
38
 
39
39
  export default defineCatalogPage({
40
40
  component: 'Dropdown',
41
- group: 'Inputs',
41
+ group: 'Components',
42
+ composedOf: [
43
+ { component: 'FieldContainer', role: "The field frame around the label, value and chevron (the trigger).", relationship: 'built-in' },
44
+ { component: 'BottomSheet', role: "The sheet listing the options while the dropdown is open.", relationship: 'built-in' },
45
+ { component: 'TopNav', role: "The sheet's header, titled with the field's label.", relationship: 'built-in' },
46
+ { component: 'Button', role: "The icon-only Close button in the sheet header.", relationship: 'built-in' },
47
+ ],
42
48
  description: 'A labelled field that opens a BottomSheet picker on tap — the "bottom sheet picker" interaction: tap the trigger, pick an option from the sheet, it closes.',
43
49
  whenToUse: 'The answer is one of a small, known set of choices — never free text. For free-text filtering, use SearchField; for a named field with a fixed identity, use InputField.',
44
50
  a11y: 'The trigger is a Pressable (via FieldContainer) with accessibilityRole="button". Options render as accessibilityRole="radio" inside a "radiogroup", with accessibilityState.checked reflecting the current selection.',
@@ -5,7 +5,13 @@ 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',
10
+ composedOf: [
11
+ { component: 'Avatar', role: "The icon circle above the title (iconName, default 'users').", relationship: 'built-in' },
12
+ { component: 'ButtonGroup', role: "Stacks the action buttons when action is set.", relationship: 'built-in' },
13
+ { component: 'Button', role: "The action button, plus a tertiary secondaryAction button when set.", relationship: 'built-in' },
14
+ ],
9
15
  description: 'A centred placeholder for a screen or section with nothing to show yet — no results, no saved items, a first-run state.',
10
16
  whenToUse: "Fills the entire content area because there's nothing else to show. For a note that sits alongside other real content, use Banner instead.",
11
17
  a11y: 'A plain View with text; the action(s) render as real Buttons inside a ButtonGroup, which carry their own accessibility.',
@@ -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,7 +17,12 @@ function InputFieldDemo() {
17
17
 
18
18
  export default defineCatalogPage({
19
19
  component: 'InputField',
20
- group: 'Inputs',
20
+ group: 'Components',
21
+ specimenSize: 'regular',
22
+ composedOf: [
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' },
25
+ ],
21
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.',
22
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.',
23
28
  a11y: 'The editable mode is a live TextInput; the picker mode is a Pressable row. Either mode shows a clear button while active (focused, or active for a picker) with a value set. Provide a meaningful label.',
@@ -8,7 +8,11 @@ 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',
13
+ composedOf: [
14
+ { component: 'Divider', role: "The hairline between consecutive items (none after the last).", relationship: 'built-in' },
15
+ ],
12
16
  description: 'Stacks ListItem rows on a rounded white surface, with a Divider automatically inserted between each consecutive pair — never after the last.',
13
17
  a11y: 'A plain View; each ListItem child carries its own accessibility.',
14
18
  variants: {
@@ -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,10 @@ const PILL_COMPARISON = grid(
40
40
 
41
41
  export default defineCatalogPage({
42
42
  component: 'Pill',
43
- group: 'Actions',
43
+ group: 'Components',
44
+ composedOf: [
45
+ { component: 'Loading', role: "The spinner shown instead of the label and icon while loading is true.", relationship: 'built-in' },
46
+ ],
44
47
  specimenSize: 'compact',
45
48
  comparison: PILL_COMPARISON,
46
49
  description: 'A compact selectable chip — selected/unselected states with an optional leading icon.',
@@ -4,7 +4,10 @@ import { PillRow } from './PillRow';
4
4
 
5
5
  export default defineCatalogPage({
6
6
  component: 'PillRow',
7
- group: 'Actions',
7
+ group: 'Components',
8
+ composedOf: [
9
+ { component: 'Pill', role: "Each item in pills, plus the trailing icon-only Edit pill when shown.", relationship: 'built-in' },
10
+ ],
8
11
  description: 'A horizontally-scrolling row of Pill chips, with an optional trailing icon-only add pill — keeps the selected pill scrolled into view automatically.',
9
12
  whenToUse: 'A scrollable row of selection chips (e.g. saved-place shortcuts) — for a fixed row of action buttons instead, use ButtonGroup.',
10
13
  a11y: 'The row itself carries no accessibility role; each Pill (including the add pill) keeps its own accessibilityLabel/role from the Pill component.',
@@ -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,12 @@ function SearchFieldDemo() {
9
9
 
10
10
  export default defineCatalogPage({
11
11
  component: 'SearchField',
12
- group: 'Inputs',
12
+ group: 'Components',
13
+ specimenSize: 'regular',
14
+ composedOf: [
15
+ { component: 'FieldContainer', role: "The field frame around the search icon, input and clear button.", relationship: 'built-in' },
16
+ { component: 'InputClearButton', role: "The clear (\u00d7) button, shown while focused with text.", relationship: 'built-in' },
17
+ ],
13
18
  description: 'Single-line search input on the shared field chrome — leading search icon, a border that darkens on focus, and a clear button while active with text.',
14
19
  whenToUse: 'Free-text filtering/searching only — never a named field with a fixed value. For that, use InputField; for a small known set of choices, use Dropdown.',
15
20
  a11y: 'Forwards a ref to the underlying TextInput; all TextInputProps pass through, so pass accessibilityLabel/placeholder as needed.',
@@ -4,7 +4,11 @@ import { SectionHeader } from './SectionHeader';
4
4
 
5
5
  export default defineCatalogPage({
6
6
  component: 'SectionHeader',
7
- group: 'Layout',
7
+ group: 'Components',
8
+ specimenSize: 'regular',
9
+ composedOf: [
10
+ { component: 'Button', role: "The trailing button, rendered when trailingButtonLabel is set.", relationship: 'built-in' },
11
+ ],
8
12
  description: 'An uppercase muted section label with an optional inline icon and a right-aligned ghost button.',
9
13
  whenToUse: "A label above a group of related rows within a screen (e.g. above a List) — for the screen's own top bar, use TopNav.",
10
14
  a11y: 'Title renders as plain Text — no heading role. A tappable labelIcon (onPress set) becomes accessibilityRole="button" with accessibilityLabel falling back to the section\'s own title; a non-interactive labelIcon has no accessibility node of its own. The trailing button is a real Button, so it carries Button\'s own accessibility for free.',
@@ -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,11 @@ function TextAreaDemo() {
9
9
 
10
10
  export default defineCatalogPage({
11
11
  component: 'TextArea',
12
- group: 'Inputs',
12
+ group: 'Components',
13
+ specimenSize: 'regular',
14
+ composedOf: [
15
+ { component: 'FieldContainer', role: "The field frame around the multi-line input.", relationship: 'built-in' },
16
+ ],
13
17
  description: 'A multi-line input that grows with its content from a minimum height.',
14
18
  a11y: 'A multiline TextInput; pass accessibilityLabel via inputProps when there is no visible label beside it.',
15
19
  variants: {
@@ -28,10 +28,18 @@ 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.',
36
+ // Confirmed from Toast.tsx's own JSX, not inferred from its imports: it renders a Button itself
37
+ // (not a caller-supplied child) whenever `action` is set. Toast.tsx also renders an Icon for its
38
+ // status glyph, but Icon has no catalog page/known component name of its own to confirm against
39
+ // yet, so that entry is left out rather than logging an unknown-component warning.
40
+ composedOf: [
41
+ { component: 'Button', role: 'Renders the optional action button when action is set.', relationship: 'built-in' },
42
+ ],
35
43
  variants: {
36
44
  itemsFill: true,
37
45
  items: [
@@ -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',
@@ -27,10 +27,11 @@ const styles = StyleSheet.create({
27
27
  // fixed TopNav header and Dock footer around a scrollable middle, the same fixed-header/
28
28
  // scrollable-body/fixed-footer shape a real screen would use.
29
29
  savedTripsScreen: { flex: 1, alignSelf: 'stretch' },
30
- // A fixed cap (not `flex: 1`) — sidesteps a web-only flexbox quirk where a `flex: 1` ScrollView
31
- // nested inside PhoneFrame's own fixed height doesn't actually shrink to the space left after
32
- // TopNav/Dock, and grows PhoneFrame itself past its intended 480px instead of scrolling.
33
- savedTripsScroll: { maxHeight: 320 },
30
+ // Takes the space between the TopNav and the Dock and scrolls (`minHeight: 0` lets a flex child
31
+ // shrink below its content on the web).
32
+ savedTripsScroll: { flex: 1, minHeight: 0 },
33
+ // The muted area between TopNav and Dock takes the remaining height, so the list scrolls there.
34
+ savedTripsBody: { flex: 1, minHeight: 0 },
34
35
  savedTripsScrollContent: { padding: DS_SPACING[800], gap: DS_SPACING[600] },
35
36
  // Shared by SavedTrips' small inline rows — the "Updating arrival times…" loading row and each
36
37
  // list row's trailing badge+button cluster (identical layout, one key).
@@ -127,7 +128,7 @@ function SavedTripsDemo() {
127
128
  </Tooltip>
128
129
  }
129
130
  />
130
- <Surface tone="muted">
131
+ <Surface tone="muted" style={styles.savedTripsBody}>
131
132
  <ScrollView style={styles.savedTripsScroll} contentContainerStyle={styles.savedTripsScrollContent}>
132
133
  <SearchField value={query} onChangeText={setQuery} placeholder="Search stations" />
133
134
  <SegmentedToggle
@@ -239,7 +240,27 @@ function SavedTripsDemo() {
239
240
  }
240
241
 
241
242
  export default defineCatalogPage({
242
- group: 'Recipes',
243
+ group: 'Patterns',
244
+ composedOf: [
245
+ { component: 'TopNav', role: "The screen header, titled Saved trips, with a Filters button.", relationship: 'built-in' },
246
+ { component: 'Tooltip', role: "The Filter by mode hint on the Filters button.", relationship: 'built-in' },
247
+ { component: 'Surface', role: "The muted background behind the search, toggles and list.", relationship: 'built-in' },
248
+ { component: 'SearchField', role: "Searches stations.", relationship: 'built-in' },
249
+ { component: 'SegmentedToggle', role: "Switches between List and Map views.", relationship: 'built-in' },
250
+ { component: 'UnderlineTabs', role: "The All, Nearby and Favorites tabs.", relationship: 'built-in' },
251
+ { component: 'PillRow', role: "Mode filter pills.", relationship: 'built-in' },
252
+ { component: 'Loading', role: "The small spinner beside Updating arrival times.", relationship: 'built-in' },
253
+ { component: 'Dropdown', role: "The Sort by picker.", relationship: 'built-in' },
254
+ { component: 'SectionHeader', role: "The Nearby stations heading.", relationship: 'built-in' },
255
+ { component: 'List', role: "Holds the saved-trip rows with dividers.", relationship: 'built-in' },
256
+ { component: 'ListItem', role: "Each saved trip row.", relationship: 'built-in' },
257
+ { component: 'Avatar', role: "Each row's mode icon.", relationship: 'built-in' },
258
+ { component: 'Badge', role: "Each row's status, e.g. delays.", relationship: 'built-in' },
259
+ { component: 'Button', role: "The Filters button, each row's remove button, and Plan a new trip.", relationship: 'built-in' },
260
+ { component: 'EmptyState', role: "Shown instead of the list on an empty tab (Favorites, or after removing every row).", relationship: 'built-in' },
261
+ { component: 'Toast', role: "Confirms a removed trip, with Undo.", relationship: 'built-in' },
262
+ { component: 'Dock', role: "The bottom bar holding Plan a new trip.", relationship: 'built-in' },
263
+ ],
243
264
  description: 'A composed real screen — a saved-trips list with a search bar, tab switcher, mode filter pills, a sort Dropdown, and a bottom action bar. Switch to the "Favorites" tab (or remove every row) to see the EmptyState alternative to the list; tap the header icon to see its Tooltip; tap a row\'s trailing × to see the Toast, which only appears after that real action and auto-dismisses (or Undo) — remove two rows back-to-back and each removal gets its own stacked toast on its own clock.',
244
265
  tokenGallery: true,
245
266
  fullWidthLabel: 'Preview',
@@ -40,7 +40,13 @@ function TripPlannerFormDemo() {
40
40
  }
41
41
 
42
42
  export default defineCatalogPage({
43
- group: 'Recipes',
43
+ group: 'Patterns',
44
+ composedOf: [
45
+ { component: 'Card', role: "Frames the whole form.", relationship: 'built-in' },
46
+ { component: 'InputField', role: "The From and To fields.", relationship: 'built-in' },
47
+ { component: 'Divider', role: "Separates the fields from the actions.", relationship: 'built-in' },
48
+ { component: 'Button', role: "Add stop (tertiary) and Continue (primary, disabled until To is filled).", relationship: 'built-in' },
49
+ ],
44
50
  description: 'A composed real screen — not one component in isolation — showing how Card, InputField, Divider, and Button actually fit together: a From/To trip form with a secondary "Add stop" action and a primary "Continue" that\'s disabled until a destination is entered.',
45
51
  tokenGallery: true,
46
52
  fullWidthLabel: 'Preview',