@krapwoo/ds-viewer 0.1.1 → 0.3.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 (173) hide show
  1. package/README.md +182 -78
  2. package/dist/cli/agentsFile.js +31 -0
  3. package/dist/cli/config.js +26 -1
  4. package/dist/cli/dev.js +50 -5
  5. package/dist/cli/doctor.js +492 -0
  6. package/dist/cli/explain.js +151 -0
  7. package/dist/cli/githubAction.js +68 -0
  8. package/dist/cli/glob.js +8 -0
  9. package/dist/cli/init.js +152 -18
  10. package/dist/cli/main.js +115 -4
  11. package/dist/cli/pageIndex.js +27 -1
  12. package/dist/cli/props.js +170 -21
  13. package/dist/cli/staticPage.js +356 -0
  14. package/dist/cli/sync.js +28 -5
  15. package/dist/cli/workspace.js +49 -21
  16. package/dist/native/catalog/comparison.js +283 -0
  17. package/dist/native/catalog/tokens.js +119 -0
  18. package/dist/native/catalog/types.js +1 -0
  19. package/native/catalog/CatalogShell.tsx +6 -1
  20. package/native/catalog/CatalogSidebar.tsx +98 -5
  21. package/native/catalog/ComparisonGrid.tsx +9 -7
  22. package/native/catalog/SpacingScaleGallery.tsx +1 -1
  23. package/native/catalog/TokenRow.tsx +3 -2
  24. package/native/catalog/catalogNavigation.ts +1 -1
  25. package/native/catalog/comparison.ts +63 -5
  26. package/native/catalog/index.ts +8 -6
  27. package/native/catalog/logoLayout.ts +12 -0
  28. package/native/catalog/pageApi.ts +21 -3
  29. package/native/catalog/tokens.ts +5 -0
  30. package/native/catalog/types.ts +9 -6
  31. package/package.json +12 -6
  32. package/starter-kit/WHEN_TO_USE.md +124 -0
  33. package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
  34. package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
  35. package/starter-kit/components/AnimatedChevron/index.ts +2 -0
  36. package/starter-kit/components/Avatar/Avatar.catalog.tsx +54 -0
  37. package/starter-kit/components/Avatar/Avatar.tsx +74 -0
  38. package/starter-kit/components/Avatar/index.ts +2 -0
  39. package/starter-kit/components/Badge/Badge.catalog.tsx +88 -0
  40. package/starter-kit/components/Badge/Badge.tsx +56 -0
  41. package/starter-kit/components/Badge/Badge.types.ts +18 -0
  42. package/starter-kit/components/Badge/index.ts +2 -0
  43. package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
  44. package/starter-kit/components/Banner/Banner.tsx +257 -0
  45. package/starter-kit/components/Banner/Banner.types.ts +38 -0
  46. package/starter-kit/components/Banner/index.ts +2 -0
  47. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
  48. package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
  49. package/starter-kit/components/BottomSheet/index.ts +2 -0
  50. package/starter-kit/components/Button/Button.catalog.tsx +99 -0
  51. package/starter-kit/components/Button/Button.tsx +173 -0
  52. package/starter-kit/components/Button/Button.types.ts +52 -0
  53. package/starter-kit/components/Button/index.ts +2 -0
  54. package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
  55. package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
  56. package/starter-kit/components/ButtonGroup/index.ts +2 -0
  57. package/starter-kit/components/Card/Card.catalog.tsx +68 -0
  58. package/starter-kit/components/Card/Card.tsx +133 -0
  59. package/starter-kit/components/Card/index.ts +2 -0
  60. package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
  61. package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
  62. package/starter-kit/components/Checkbox/index.ts +2 -0
  63. package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
  64. package/starter-kit/components/Dialog/Dialog.tsx +82 -0
  65. package/starter-kit/components/Dialog/index.ts +2 -0
  66. package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
  67. package/starter-kit/components/Divider/Divider.tsx +20 -0
  68. package/starter-kit/components/Divider/index.ts +2 -0
  69. package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
  70. package/starter-kit/components/Dock/Dock.tsx +58 -0
  71. package/starter-kit/components/Dock/index.ts +2 -0
  72. package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
  73. package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
  74. package/starter-kit/components/Dropdown/index.ts +2 -0
  75. package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
  76. package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
  77. package/starter-kit/components/EmptyState/index.ts +2 -0
  78. package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
  79. package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
  80. package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
  81. package/starter-kit/components/FieldContainer/index.ts +2 -0
  82. package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
  83. package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
  84. package/starter-kit/components/InputClearButton/index.ts +2 -0
  85. package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
  86. package/starter-kit/components/InputField/InputField.tsx +418 -0
  87. package/starter-kit/components/InputField/index.ts +2 -0
  88. package/starter-kit/components/List/List.catalog.tsx +30 -0
  89. package/starter-kit/components/List/List.tsx +36 -0
  90. package/starter-kit/components/List/index.ts +2 -0
  91. package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
  92. package/starter-kit/components/ListItem/ListItem.tsx +165 -0
  93. package/starter-kit/components/ListItem/index.ts +2 -0
  94. package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
  95. package/starter-kit/components/Loading/Loading.tsx +121 -0
  96. package/starter-kit/components/Loading/index.ts +2 -0
  97. package/starter-kit/components/Pill/Pill.catalog.tsx +79 -0
  98. package/starter-kit/components/Pill/Pill.tsx +181 -0
  99. package/starter-kit/components/Pill/Pill.types.ts +30 -0
  100. package/starter-kit/components/Pill/index.ts +2 -0
  101. package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
  102. package/starter-kit/components/PillRow/PillRow.tsx +140 -0
  103. package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
  104. package/starter-kit/components/PillRow/index.ts +2 -0
  105. package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
  106. package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
  107. package/starter-kit/components/ProgressDots/index.ts +2 -0
  108. package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
  109. package/starter-kit/components/Radio/Radio.tsx +114 -0
  110. package/starter-kit/components/Radio/index.ts +2 -0
  111. package/starter-kit/components/Radio/usePressScale.ts +33 -0
  112. package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
  113. package/starter-kit/components/SearchField/SearchField.tsx +98 -0
  114. package/starter-kit/components/SearchField/index.ts +2 -0
  115. package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
  116. package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
  117. package/starter-kit/components/SectionHeader/index.ts +2 -0
  118. package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
  119. package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
  120. package/starter-kit/components/SegmentedToggle/index.ts +2 -0
  121. package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
  122. package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
  123. package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
  124. package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
  125. package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
  126. package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
  127. package/starter-kit/components/Shimmer/index.ts +2 -0
  128. package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
  129. package/starter-kit/components/Surface/Surface.tsx +45 -0
  130. package/starter-kit/components/Surface/index.ts +2 -0
  131. package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
  132. package/starter-kit/components/Switch/Switch.tsx +129 -0
  133. package/starter-kit/components/Switch/index.ts +2 -0
  134. package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
  135. package/starter-kit/components/TextArea/TextArea.tsx +99 -0
  136. package/starter-kit/components/TextArea/index.ts +2 -0
  137. package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
  138. package/starter-kit/components/Toast/Toast.tsx +144 -0
  139. package/starter-kit/components/Toast/index.ts +2 -0
  140. package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
  141. package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
  142. package/starter-kit/components/Tooltip/index.ts +2 -0
  143. package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
  144. package/starter-kit/components/TopNav/TopNav.tsx +77 -0
  145. package/starter-kit/components/TopNav/index.ts +2 -0
  146. package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
  147. package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
  148. package/starter-kit/components/UnderlineTabs/index.ts +2 -0
  149. package/starter-kit/components/index.ts +36 -0
  150. package/starter-kit/components/statusColors.ts +27 -0
  151. package/starter-kit/icons/Icon.native.tsx +90 -0
  152. package/starter-kit/icons/index.ts +10 -0
  153. package/starter-kit/icons/paths.ts +623 -0
  154. package/starter-kit/icons/types.ts +69 -0
  155. package/starter-kit/pages/Colors.catalog.tsx +55 -0
  156. package/starter-kit/pages/Font.catalog.tsx +45 -0
  157. package/starter-kit/pages/Icons.catalog.tsx +51 -0
  158. package/starter-kit/pages/Manifest.catalog.tsx +24 -0
  159. package/starter-kit/pages/Motion.catalog.tsx +101 -0
  160. package/starter-kit/pages/Radius.catalog.tsx +35 -0
  161. package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
  162. package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
  163. package/starter-kit/pages/Shadow.catalog.tsx +34 -0
  164. package/starter-kit/pages/Spacing.catalog.tsx +12 -0
  165. package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
  166. package/starter-kit/pages/Typography.catalog.tsx +17 -0
  167. package/starter-kit/tokens/index.ts +50 -0
  168. package/starter-kit/tokens/motion.ts +90 -0
  169. package/starter-kit/tokens/palette.ts +111 -0
  170. package/starter-kit/tokens/scales.ts +106 -0
  171. package/starter-kit/tokens/semantic.ts +98 -0
  172. package/starter-kit/tokens/shadow.ts +36 -0
  173. package/starter-kit/tokens/typography.ts +72 -0
@@ -0,0 +1,124 @@
1
+ # When to use X vs Y
2
+
3
+ Disambiguation for components whose names or looks overlap, so a new screen reaches for the right
4
+ one on the first try instead of the closest-looking one. Each entry names the deciding question —
5
+ answer it and the choice falls out. For the full prop/variant/state list per component, read
6
+ `native/catalog/manifest.ts`'s `buildComponentManifest()` output (rendered live at the catalog's
7
+ "Manifest" page) rather than guessing from a component's name alone.
8
+
9
+ ## InputField vs SearchField vs Dropdown
10
+
11
+ All three render on the same field chrome (`FieldContainer`) and can look identical at rest. The
12
+ deciding question: **what happens when the user taps in?**
13
+
14
+ - **InputField** — a labeled field with a *floating label* (the label shrinks to a caption once
15
+ active/filled). Use for a named piece of data with a fixed identity across the interaction — "To",
16
+ "From", "Arrive by", a person's name. Supports a live `editable` TextInput mode *or* a tappable
17
+ picker mode (`active` forced from outside, e.g. by a Dropdown/BottomSheet you're driving).
18
+ - **SearchField** — a single-purpose live text input with a permanent leading search icon and no
19
+ floating label (there's nothing to float to — "search" *is* the label, implied by the icon). Use
20
+ for free-text filtering/searching, never for a named field with a fixed value.
21
+ - **Dropdown** — not a text input at all. A label-above-value trigger that opens a `BottomSheet`
22
+ picker with a fixed option list. Use when the answer is one of a small, known set of choices
23
+ (a direction, a filter value) — never for free text.
24
+
25
+ Rule of thumb: known short list → **Dropdown**. Free-text filter/search → **SearchField**. Anything
26
+ else the user types or picks, with its own persistent label → **InputField**.
27
+
28
+ ## Button vs Pill
29
+
30
+ Both are tappable, labeled, and can carry an icon.
31
+
32
+ - **Button** — an *action*. Something happens when you tap it (save, confirm, navigate, submit).
33
+ Five weights (`primary`/`secondary`/`tertiary`/`white`/`ghost`) express visual priority among
34
+ multiple actions on the same screen.
35
+ - **Pill** — a *selection toggle*, not an action. Tapping it flips a `selected`/`not_selected`
36
+ state that persists (a filter chip, a saved-place shortcut). If tapping it doesn't leave something
37
+ visibly "on," it should be a Button, not a Pill.
38
+
39
+ ## Toast vs Banner
40
+
41
+ Both show a status message with an icon and optional action, and both support the same five
42
+ semantic variants.
43
+
44
+ - **Toast** — *transient* and *self-contained*. Appears on top of the current screen, unrelated to
45
+ the content beneath it, and is expected to go away on its own or via its own action ("Undo",
46
+ "Retry"). Owns no layout space — it floats.
47
+ - **Banner** — *persistent* and *in-flow*. Lives inside the screen's own layout (top of a list,
48
+ inside a card stack) for as long as the condition it describes is true ("Fare increase", "Line
49
+ suspended"). Never auto-dismisses; the condition or an explicit user action clears it.
50
+
51
+ If the message is about something happening *right now, because of something the user just did* →
52
+ Toast. If it's a standing fact about the current screen's data → Banner.
53
+
54
+ ## Badge vs Pill vs a Toast/Banner variant
55
+
56
+ All three can render the same five semantic colors, which makes them easy to reach for
57
+ interchangeably. They aren't interchangeable:
58
+
59
+ - **Badge** — *read-only status label*. Not tappable. Use inline next to other content ("On time"
60
+ next to a train row, "Suspended" next to a line name).
61
+ - **Pill** — *tappable selection*. Use it when the same-looking chip needs to respond to a tap and
62
+ hold a selected state.
63
+ - **Toast/Banner variant** — the color describes a *message about the whole screen or a transient
64
+ event*, not a single inline data point. If you're tagging one row's status, that's a Badge; if
65
+ you're telling the user something happened, that's a Toast or Banner.
66
+
67
+ ## Dialog vs BottomSheet
68
+
69
+ Both are overlay containers with a backdrop.
70
+
71
+ - **Dialog** — center-screen, fades and scales in. Use for a short, focused decision that
72
+ interrupts the flow (confirm/cancel, a single form). Named `Dialog` (not `Modal`) specifically to
73
+ avoid shadowing React Native's own built-in `Modal`.
74
+ - **BottomSheet** — bottom-anchored, slides up, height driven by content up to 90% of the screen.
75
+ Use for anything longer, browsable, or that benefits from a `TopNav`/`Dock` header-footer
76
+ structure (an option picker, a detail view, a multi-field form). If the content would need its
77
+ own internal scrolling, it's a BottomSheet, not a Dialog.
78
+
79
+ ## Card vs ListItem (inside a List)
80
+
81
+ Both hold a leading element, a title, and secondary text.
82
+
83
+ - **ListItem** (inside **List**) — a *row in a set of peers* — settings, a menu, search results.
84
+ Rows are visually light (no individual shadow/border) because the *List* container supplies the
85
+ shared surface and the dividers between them.
86
+ - **Card** — a *standalone, self-contained unit* with its own shadow and rounded surface. Use when
87
+ the content doesn't belong to a homogeneous list — a single trip summary, a promo, one dashboard
88
+ tile sitting alongside other, differently-shaped content.
89
+
90
+ If you're about to render more than one of the same shape in a vertical stack with dividers between
91
+ them, that's `List`/`ListItem`, not a stack of `Card`s.
92
+
93
+ ## SegmentedToggle vs UnderlineTabs
94
+
95
+ Same `options`/`value`/`onChange` API, same job (switch between a small set of views) — the
96
+ difference is purely visual weight.
97
+
98
+ - **SegmentedToggle** — a filled, recessed track with a sliding white thumb. Heavier visual weight;
99
+ use when the choice is a primary, prominent control on the screen (e.g. Map/List view).
100
+ - **UnderlineTabs** — left-aligned labels over a hairline rule with a sliding underline. Quieter;
101
+ use for secondary navigation within a screen that already has a clear primary focus.
102
+
103
+ ## Checkbox vs Switch vs Radio
104
+
105
+ All three are binary-ish controls; the difference is what the value means and how many can be true
106
+ at once.
107
+
108
+ - **Checkbox** — an independent on/off fact about *this one item* ("Remember this trip"). Any number
109
+ of checkboxes on a screen can be checked at once, independently.
110
+ - **Switch** — an on/off *setting* that takes effect immediately (no separate "Save" step). Reach
111
+ for Switch specifically when toggling it *does something right away*; reach for Checkbox when it's
112
+ just recording a fact that a future action (a form submit) will act on.
113
+ - **Radio** — one selection from a *mutually exclusive* set (only one can be selected across a group
114
+ of Radios sharing one consumer-held value). If checking one should un-check another, it's Radio,
115
+ never Checkbox.
116
+
117
+ ## EmptyState vs Banner
118
+
119
+ - **EmptyState** — fills the *entire* content area of a screen or section because there is
120
+ *nothing else to show* (no results, no saved items, first run). Centered, icon-led, optionally one
121
+ action.
122
+ - **Banner** — sits *alongside* other real content, describing a condition about that content or the
123
+ screen in general. If the screen would otherwise be blank, use EmptyState; if it has content and
124
+ you're adding a note above/around it, use Banner.
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { defineCatalogPage } from '@krapwoo/ds-viewer';
3
+ import { AnimatedChevron } from './AnimatedChevron';
4
+ import { DS_SEMANTIC } from '../../tokens';
5
+
6
+ export default defineCatalogPage({
7
+ component: 'AnimatedChevron',
8
+ group: 'Sub-Parts',
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
+ 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
+ variants: {
12
+ items: [
13
+ { key: 'collapsed', name: 'Collapsed', props: { expanded: false }, node: <AnimatedChevron expanded={false} /> },
14
+ { key: 'expanded', name: 'Expanded', props: { expanded: true }, node: <AnimatedChevron expanded={true} /> },
15
+ ],
16
+ },
17
+ states: {
18
+ items: [
19
+ { key: 'size', name: 'Custom size', node: <AnimatedChevron expanded={false} size={28} /> },
20
+ { key: 'color', name: 'Custom color', node: <AnimatedChevron expanded={true} color={DS_SEMANTIC.emphasis.info} /> },
21
+ ],
22
+ },
23
+ });
@@ -0,0 +1,46 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, Easing } from 'react-native';
3
+ import { Icon } from '../../icons/Icon.native';
4
+ import { DS_MOTION_DURATION, DS_MOTION_EASING } from '../../tokens';
5
+
6
+ export interface AnimatedChevronProps {
7
+ /** When true the chevron points up; when false it points down. */
8
+ expanded: boolean;
9
+ /** Icon size in px. */
10
+ size?: number;
11
+ /** Icon color. */
12
+ color?: string;
13
+ /** Morph duration in ms. Defaults to DS_MOTION_DURATION.base (240). */
14
+ duration?: number;
15
+ }
16
+
17
+ /**
18
+ * A chevron that morphs between down (collapsed) and up (expanded) by flipping vertically in place
19
+ * (`scaleY` 1 → 0 → -1) rather than rotating through a sideways-pointing intermediate angle — a
20
+ * plain chevron-down glyph mirrored on its own vertical centre (`scaleY: -1`) is exactly chevron-up,
21
+ * so no second icon or path swap is needed. Uses the core `Animated` API with `useNativeDriver:
22
+ * true` (transform-only), so it's smooth on the New Architecture in dev and release.
23
+ */
24
+ export function AnimatedChevron({ expanded, size = 16, color, duration = DS_MOTION_DURATION.base }: AnimatedChevronProps) {
25
+ const progress = useRef(new Animated.Value(expanded ? 1 : 0)).current;
26
+
27
+ useEffect(() => {
28
+ const anim = Animated.timing(progress, {
29
+ toValue: expanded ? 1 : 0,
30
+ duration,
31
+ // `standard` — an in-place morph (a swap), per DS_MOTION_EASING_USE.
32
+ easing: Easing.bezier(...DS_MOTION_EASING.standard),
33
+ useNativeDriver: true,
34
+ });
35
+ anim.start();
36
+ return () => anim.stop();
37
+ }, [expanded, duration, progress]);
38
+
39
+ const scaleY = progress.interpolate({ inputRange: [0, 1], outputRange: [1, -1] });
40
+
41
+ return (
42
+ <Animated.View style={{ transform: [{ scaleY }] }}>
43
+ <Icon name="chevron-down" size={size} color={color} />
44
+ </Animated.View>
45
+ );
46
+ }
@@ -0,0 +1,2 @@
1
+ export { AnimatedChevron } from './AnimatedChevron';
2
+ export type { AnimatedChevronProps } from './AnimatedChevron';
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { defineCatalogPage, grid } from '@krapwoo/ds-viewer';
3
+ import { Avatar } from './Avatar';
4
+ import { DS_SEMANTIC } from '../../tokens';
5
+
6
+ // Avatar: content Kind × Size (Avatar's `size` is continuous; 24 / 40 default / 64 is the sweep).
7
+ // Neither axis names a real prop — "Kind" is which of 3 optional props is set, not one enum, and
8
+ // `size` has no fixed option set — so both stay unbound. An unbound axis is fully valid; `doctor`
9
+ // only ever reports how many examples are unbound as information, never as an error or a warning.
10
+ const AVATAR_COMPARISON = grid(
11
+ 'Kind',
12
+ 'Size',
13
+ [
14
+ { key: 'image', label: 'Image' },
15
+ { key: 'icon', label: 'Icon' },
16
+ { key: 'initials', label: 'Initials fallback' },
17
+ ],
18
+ [
19
+ { key: 'small', label: 'Small · 24' },
20
+ { key: 'medium', label: 'Medium · 40 (default)' },
21
+ { key: 'large', label: 'Large · 64' },
22
+ ],
23
+ (row, column) => {
24
+ const size = column === 'small' ? 24 : column === 'large' ? 64 : 40;
25
+ if (row === 'image') return <Avatar imageUrl="https://i.pravatar.cc/100" accessibilityLabel="Jordan Lee" size={size} />;
26
+ if (row === 'icon') return <Avatar iconName="users" accessibilityLabel="Guest" size={size} />;
27
+ return <Avatar initials="JL" accessibilityLabel="Jordan Lee" size={size} />;
28
+ },
29
+ 'compact',
30
+ );
31
+
32
+ export default defineCatalogPage({
33
+ component: 'Avatar',
34
+ group: 'Surfaces',
35
+ specimenSize: 'compact',
36
+ comparison: AVATAR_COMPARISON,
37
+ description: 'A circular image, or an initials fallback on a solid fill when there\'s no image (or it fails to load).',
38
+ a11y: 'Renders with accessibilityRole="image"; pass accessibilityLabel for a meaningful name, otherwise it falls back to the initials text.',
39
+ variants: {
40
+ items: [
41
+ { key: 'image', name: 'Image', node: <Avatar imageUrl="https://i.pravatar.cc/100" accessibilityLabel="Jordan Lee" /> },
42
+ { key: 'icon', name: 'Icon', node: <Avatar iconName="users" accessibilityLabel="Guest" /> },
43
+ { key: 'initials', name: 'Initials fallback', node: <Avatar initials="JL" accessibilityLabel="Jordan Lee" /> },
44
+ ],
45
+ },
46
+ states: {
47
+ items: [
48
+ { key: 'small', name: 'Small', node: <Avatar initials="JL" size={24} /> },
49
+ { key: 'medium', name: 'Medium (default)', node: <Avatar initials="JL" size={40} /> },
50
+ { key: 'large', name: 'Large', node: <Avatar initials="JL" size={64} /> },
51
+ { key: 'custom-color', name: 'Custom colour', node: <Avatar initials="AC" backgroundColor={DS_SEMANTIC.emphasis.info} /> },
52
+ ],
53
+ },
54
+ });
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import { View, Text, Image, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { DS_SEMANTIC, DS_FONT_WEIGHT } from '../../tokens';
4
+ import { Icon } from '../../icons/Icon.native';
5
+ import type { IconName } from '../../icons';
6
+
7
+ export interface AvatarProps {
8
+ /** Remote image URL. Takes precedence over `iconName`/`initials`; falls back to them when omitted
9
+ * or the image fails to load. */
10
+ imageUrl?: string;
11
+ /** Icon shown instead of initials — e.g. for a generic/anonymous avatar. Takes precedence over
12
+ * `initials` when there's no image. */
13
+ iconName?: IconName;
14
+ /** Shown when there's no image or icon — the first 1-2 characters are used, uppercased. */
15
+ initials?: string;
16
+ /** Diameter in px. @default 40 */
17
+ size?: number;
18
+ /** Fill colour behind the icon/initials. Defaults to a neutral muted surface (with dark
19
+ * foreground); passing a custom colour switches the icon/initials to light (inverse) text,
20
+ * assuming a saturated/dark fill — match that pairing if you override this. */
21
+ backgroundColor?: string;
22
+ accessibilityLabel?: string;
23
+ style?: StyleProp<ViewStyle>;
24
+ }
25
+
26
+ /** A circular image, icon, or initials fallback on a solid fill — in that order of precedence. */
27
+ export function Avatar({
28
+ imageUrl,
29
+ iconName,
30
+ initials,
31
+ size = 40,
32
+ backgroundColor,
33
+ accessibilityLabel,
34
+ style,
35
+ }: AvatarProps) {
36
+ const [imageFailed, setImageFailed] = React.useState(false);
37
+ const showImage = !!imageUrl && !imageFailed;
38
+ const showIcon = !showImage && !!iconName;
39
+ const dimension = { width: size, height: size, borderRadius: size / 2 };
40
+ // The default fill (surface.muted) is light, so its icon/initials need dark text; a caller-supplied
41
+ // backgroundColor is assumed saturated/dark (the existing convention — see the "Custom colour"
42
+ // catalog example), so it keeps the light inverse text that pairing needs.
43
+ const contentColor = backgroundColor ? DS_SEMANTIC.text.inverse : DS_SEMANTIC.text.regular;
44
+
45
+ return (
46
+ <View
47
+ style={[styles.circle, dimension, !showImage && { backgroundColor: backgroundColor ?? DS_SEMANTIC.surface.muted }, style]}
48
+ accessible
49
+ accessibilityRole="image"
50
+ accessibilityLabel={accessibilityLabel ?? initials}
51
+ >
52
+ {showImage ? (
53
+ <Image source={{ uri: imageUrl }} style={dimension} onError={() => setImageFailed(true)} />
54
+ ) : showIcon ? (
55
+ <Icon name={iconName} size={Math.round(size * 0.5)} color={contentColor} />
56
+ ) : (
57
+ <Text style={[styles.initials, { fontSize: Math.round(size * 0.4), color: contentColor }]} numberOfLines={1}>
58
+ {(initials ?? '?').slice(0, 2).toUpperCase()}
59
+ </Text>
60
+ )}
61
+ </View>
62
+ );
63
+ }
64
+
65
+ const styles = StyleSheet.create({
66
+ circle: {
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ overflow: 'hidden',
70
+ },
71
+ initials: {
72
+ fontWeight: DS_FONT_WEIGHT.semibold,
73
+ },
74
+ });
@@ -0,0 +1,2 @@
1
+ export { Avatar } from './Avatar';
2
+ export type { AvatarProps } from './Avatar';
@@ -0,0 +1,88 @@
1
+ import React from 'react';
2
+ import { defineCatalogPage, grid } from '@krapwoo/ds-viewer';
3
+ import { Badge } from './Badge';
4
+ import type { BadgeVariant } from './Badge.types';
5
+
6
+ // Badge: Tone × Icon layout. One icon per tone, matching the Variants examples.
7
+ const BADGE_TONES: { key: BadgeVariant; label: string; text: string; icon: React.ComponentProps<typeof Badge>['leadingIcon'] }[] = [
8
+ { key: 'neutral', label: 'Neutral', text: 'Local', icon: 'pin' },
9
+ { key: 'info', label: 'Info', text: 'Notice', icon: 'info-circle' },
10
+ { key: 'positive', label: 'Positive', text: 'On time', icon: 'circle-check' },
11
+ { key: 'warning', label: 'Warning', text: 'Delayed', icon: 'triangle-alert' },
12
+ { key: 'negative', label: 'Negative', text: 'Suspended', icon: 'circle-slash' },
13
+ ];
14
+ // `rows` is bound to Badge's own `variant` prop (design §4) — written as its own literal array
15
+ // (not `BADGE_TONES.map(...)`) so `doctor`'s static reader can check it without evaluating a
16
+ // function call; `BADGE_TONES` above still drives the real rendering in `cell` below.
17
+ const BADGE_ROWS = {
18
+ prop: 'variant',
19
+ items: [
20
+ { key: 'neutral', label: 'Neutral' },
21
+ { key: 'info', label: 'Info' },
22
+ { key: 'positive', label: 'Positive' },
23
+ { key: 'warning', label: 'Warning' },
24
+ { key: 'negative', label: 'Negative' },
25
+ ],
26
+ } as const;
27
+ const BADGE_COMPARISON = grid(
28
+ 'Tone',
29
+ 'Icon',
30
+ BADGE_ROWS,
31
+ [
32
+ { key: 'label-only', label: 'Label only' },
33
+ { key: 'leading-icon', label: 'Leading icon' },
34
+ { key: 'trailing-icon', label: 'Trailing icon' },
35
+ { key: 'icon-only', label: 'Icon-only' },
36
+ ],
37
+ (row, column) => {
38
+ const tone = BADGE_TONES.find((t) => t.key === row)!;
39
+ if (column === 'icon-only') return <Badge variant={tone.key} leadingIcon={tone.icon} accessibilityLabel={tone.text} />;
40
+ return (
41
+ <Badge
42
+ variant={tone.key}
43
+ label={tone.text}
44
+ leadingIcon={column === 'leading-icon' ? tone.icon : undefined}
45
+ trailingIcon={column === 'trailing-icon' ? tone.icon : undefined}
46
+ />
47
+ );
48
+ },
49
+ 'compact',
50
+ );
51
+
52
+ export default defineCatalogPage({
53
+ component: 'Badge',
54
+ group: 'Surfaces',
55
+ specimenSize: 'compact',
56
+ comparison: BADGE_COMPARISON,
57
+ description: 'A small status chip — five semantic variants, with optional leading/trailing icons or icon-only.',
58
+ whenToUse: "Read-only and inline, not tappable — for one row's data point. For a tappable chip with a selected state, use Pill; for a message about the whole screen, use Toast or Banner.",
59
+ a11y: 'A plain View with text; the label carries the meaning, so avoid encoding status by color alone.',
60
+ variants: {
61
+ items: [
62
+ { key: 'neutral', name: 'Neutral', props: { variant: 'neutral' }, node: <Badge variant="neutral" label="Local" /> },
63
+ { key: 'info', name: 'Info', props: { variant: 'info' }, node: <Badge variant="info" label="Notice" leadingIcon="info-circle" /> },
64
+ { key: 'positive', name: 'Positive', props: { variant: 'positive' }, node: <Badge variant="positive" label="On time" leadingIcon="circle-check" /> },
65
+ { key: 'warning', name: 'Warning', props: { variant: 'warning' }, node: <Badge variant="warning" label="Delayed" leadingIcon="triangle-alert" /> },
66
+ { key: 'negative', name: 'Negative', props: { variant: 'negative' }, node: <Badge variant="negative" label="Suspended" leadingIcon="circle-slash" /> },
67
+ ],
68
+ },
69
+ states: {
70
+ items: [
71
+ {
72
+ key: 'leading-icon',
73
+ name: 'Leading icon',
74
+ node: <Badge variant="positive" label="On time" leadingIcon="circle-check" />,
75
+ },
76
+ {
77
+ key: 'trailing-icon',
78
+ name: 'Trailing icon',
79
+ node: <Badge variant="positive" label="On time" trailingIcon="circle-check" />,
80
+ },
81
+ {
82
+ key: 'icon-only',
83
+ name: 'Icon-only',
84
+ node: <Badge variant="info" leadingIcon="info-circle" accessibilityLabel="Notice" />,
85
+ },
86
+ ],
87
+ },
88
+ });
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { DS_RADIUS, DS_SPACING, DS_TYPOGRAPHY, DS_ICON_SIZE } from '../../tokens';
4
+ import { Icon } from '../../icons/Icon.native';
5
+ import { STATUS_BG, STATUS_FG } from '../statusColors';
6
+ import type { BadgeProps } from './Badge.types';
7
+
8
+ /**
9
+ * A small status pill. Composable: label-only, icon-only (a single icon, no label), or a label with
10
+ * a leading and/or trailing icon. Text + icons sit on the variant background in the variant's own
11
+ * emphasis colour.
12
+ */
13
+ export function Badge({ variant = 'neutral', label, leadingIcon, trailingIcon, accessibilityLabel, style }: BadgeProps) {
14
+ const hasLabel = label != null && label !== '';
15
+ // Dev-console check (same policy as ButtonGroup's checkConsistency): the docs require an
16
+ // accessible name for an icon-only badge, but the type can't enforce it — warn instead of
17
+ // silently rendering an unnameable status glyph.
18
+ if (__DEV__ && !hasLabel && !accessibilityLabel) {
19
+ console.warn('[Badge] Icon-only badge (no `label`) needs an `accessibilityLabel` — without one it has no accessible name.');
20
+ }
21
+ const fg = STATUS_FG[variant];
22
+
23
+ return (
24
+ <View
25
+ style={[styles.badge, !hasLabel && styles.iconOnly, { backgroundColor: STATUS_BG[variant] }, style]}
26
+ accessibilityRole="text"
27
+ accessibilityLabel={accessibilityLabel}
28
+ >
29
+ {leadingIcon ? <Icon name={leadingIcon} size={DS_ICON_SIZE.xs} color={fg} /> : null}
30
+ {hasLabel ? (
31
+ <Text style={[styles.label, { color: fg }]} numberOfLines={1}>
32
+ {label}
33
+ </Text>
34
+ ) : null}
35
+ {trailingIcon ? <Icon name={trailingIcon} size={DS_ICON_SIZE.xs} color={fg} /> : null}
36
+ </View>
37
+ );
38
+ }
39
+
40
+ const styles = StyleSheet.create({
41
+ badge: {
42
+ flexDirection: 'row',
43
+ alignItems: 'center',
44
+ gap: DS_SPACING[100],
45
+ paddingVertical: DS_SPACING[200],
46
+ paddingHorizontal: DS_SPACING[400],
47
+ borderRadius: DS_RADIUS.round,
48
+ alignSelf: 'flex-start',
49
+ },
50
+ iconOnly: {
51
+ paddingHorizontal: DS_SPACING[200],
52
+ },
53
+ label: {
54
+ ...DS_TYPOGRAPHY.labelXs,
55
+ },
56
+ });
@@ -0,0 +1,18 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ import type { IconName } from '../../icons';
3
+
4
+ export type BadgeVariant = 'neutral' | 'info' | 'positive' | 'warning' | 'negative';
5
+
6
+ export interface BadgeProps {
7
+ /** Semantic color scheme. */
8
+ variant?: BadgeVariant;
9
+ /** Label text. Omit (with a single icon) for an icon-only chip. */
10
+ label?: string;
11
+ /** Icon before the label. */
12
+ leadingIcon?: IconName;
13
+ /** Icon after the label. */
14
+ trailingIcon?: IconName;
15
+ /** Accessible name — required for an icon-only badge (no `label`) so it can be announced. */
16
+ accessibilityLabel?: string;
17
+ style?: StyleProp<ViewStyle>;
18
+ }
@@ -0,0 +1,2 @@
1
+ export { Badge } from './Badge';
2
+ export type { BadgeProps, BadgeVariant } from './Badge.types';
@@ -0,0 +1,87 @@
1
+ import React from 'react';
2
+ import { defineCatalogPage } from '@krapwoo/ds-viewer';
3
+ import { Banner } from './Banner';
4
+
5
+ export default defineCatalogPage({
6
+ component: 'Banner',
7
+ group: 'Surfaces',
8
+ description: 'An inline callout for status/announcements — five semantic variants, optional collapsible body, inline link, and action button.',
9
+ whenToUse: "Persistent and in-flow, describing a standing condition about the screen's content. For a transient, self-contained event notification, use Toast instead.",
10
+ a11y: 'When onPress/action is set the header/button are Pressables; the collapsible header toggles the description with a chevron affordance.',
11
+ variants: {
12
+ itemsFill: true,
13
+ items: [
14
+ { key: 'neutral', name: 'Neutral', props: { variant: 'neutral' }, node: <Banner variant="neutral" title="Schedule notice" description="Holiday schedule in effect Monday." /> },
15
+ { key: 'info', name: 'Info', props: { variant: 'info' }, node: <Banner variant="info" title="Weekend service change" description="The 6 runs express in both directions this weekend." /> },
16
+ { key: 'positive', name: 'Positive', props: { variant: 'positive' }, node: <Banner variant="positive" title="Service restored" description="All lines are running on a normal schedule." /> },
17
+ { key: 'warning', name: 'Warning', props: { variant: 'warning' }, node: <Banner variant="warning" title="Delays" description="Signal problems near 14 St." /> },
18
+ { key: 'negative', name: 'Negative', props: { variant: 'negative' }, node: <Banner variant="negative" title="Line suspended" description="No service between 96 St and 137 St until further notice." /> },
19
+ ],
20
+ },
21
+ states: {
22
+ itemsFill: true,
23
+ items: [
24
+ {
25
+ key: 'tappable',
26
+ name: 'Tappable',
27
+ node: (
28
+ <Banner
29
+ variant="neutral"
30
+ title="Alert preferences"
31
+ description="Tap to manage which alerts you receive."
32
+ onPress={() => {}}
33
+ />
34
+ ),
35
+ },
36
+ {
37
+ key: 'collapsible-expanded',
38
+ name: 'Collapsible (expanded)',
39
+ node: (
40
+ <Banner
41
+ variant="neutral"
42
+ title="Trip details"
43
+ description="Board at the front car for a faster transfer at Union Sq."
44
+ collapsible
45
+ />
46
+ ),
47
+ },
48
+ {
49
+ key: 'collapsible-collapsed',
50
+ name: 'Collapsible (collapsed)',
51
+ node: (
52
+ <Banner
53
+ variant="neutral"
54
+ title="Trip details"
55
+ description="Board at the front car for a faster transfer at Union Sq."
56
+ collapsible
57
+ defaultExpanded={false}
58
+ />
59
+ ),
60
+ },
61
+ {
62
+ key: 'action',
63
+ name: 'With action',
64
+ node: (
65
+ <Banner
66
+ variant="info"
67
+ title="App update available"
68
+ description="Version 4.2 adds live bus tracking."
69
+ action={{ label: 'Update', onPress: () => {} }}
70
+ />
71
+ ),
72
+ },
73
+ {
74
+ key: 'link',
75
+ name: 'With link',
76
+ node: (
77
+ <Banner
78
+ variant="neutral"
79
+ title="Fare increase"
80
+ description="New fares start March 1."
81
+ link={{ label: 'Learn more', onPress: () => {} }}
82
+ />
83
+ ),
84
+ },
85
+ ],
86
+ },
87
+ });