@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,77 @@
1
+ import React, { isValidElement, type ReactNode } from 'react';
2
+ import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY, DS_A11Y_MIN_TOUCH_TARGET } from '../../tokens';
4
+ import { Button, type ButtonProps } from '../Button';
5
+
6
+ // Both slots reserve this width whether populated or empty, so the title stays centered
7
+ // regardless of which side(s) actually have content.
8
+ const SLOT_SIZE = DS_A11Y_MIN_TOUCH_TARGET;
9
+
10
+ /** Dev-time consistency check (same policy as ButtonGroup's `checkConsistency`) — a nav bar's own
11
+ * leading/trailing actions read as one quiet, uniform family (back arrow, close, overflow menu),
12
+ * never as a prominent CTA. Warns (never throws) if a slot's Button doesn't follow that: `small`
13
+ * size, and not `ghost` (which reads as an inline text action, not a nav icon — see Button's own
14
+ * variant-choice doc). */
15
+ function checkSlotButton(node: ReactNode, slotName: 'leading' | 'trailing'): void {
16
+ if (!isValidElement(node) || node.type !== Button) return;
17
+ const props = node.props as ButtonProps;
18
+ if (props.size && props.size !== 'small') {
19
+ console.warn(`[TopNav] ${slotName} button uses size="${props.size}" — TopNav slot buttons should use size="small".`);
20
+ }
21
+ if (props.variant === 'ghost') {
22
+ console.warn(
23
+ `[TopNav] ${slotName} button uses variant="ghost" — that reads as an inline text action, not a nav icon. Use variant="secondary" (TopNav's default) instead.`,
24
+ );
25
+ }
26
+ }
27
+
28
+ export interface TopNavProps {
29
+ /** Centered title text. Ignored when `center` is set. */
30
+ title?: string;
31
+ /** Custom content overriding the centered title — e.g. a search field or segmented toggle. */
32
+ center?: ReactNode;
33
+ /** Leading slot — typically a back/close icon Button. Keep it `size="small"` and
34
+ * `variant="secondary"` (TopNav's default) unless there's a specific reason to deviate — never
35
+ * `ghost`, which reads as an inline text action rather than a nav icon. */
36
+ leading?: ReactNode;
37
+ /** Trailing slot — typically an action icon Button. Same size/variant guidance as `leading`. */
38
+ trailing?: ReactNode;
39
+ style?: StyleProp<ViewStyle>;
40
+ }
41
+
42
+ /** A screen's top bar: fixed-width leading/trailing slots flanking a centered title (or custom
43
+ * `center` content). Slots reserve their layout space even when empty, so the title stays
44
+ * centered whether one, both, or neither side has content. */
45
+ export function TopNav({ title, center, leading, trailing, style }: TopNavProps) {
46
+ checkSlotButton(leading, 'leading');
47
+ checkSlotButton(trailing, 'trailing');
48
+ return (
49
+ <View style={[styles.row, style]}>
50
+ <View style={styles.slot}>{leading}</View>
51
+ <View style={styles.center}>
52
+ {center ?? (title != null && (
53
+ <Text style={styles.title} numberOfLines={1} accessibilityRole="header">
54
+ {title}
55
+ </Text>
56
+ ))}
57
+ </View>
58
+ <View style={[styles.slot, styles.slotTrailing]}>{trailing}</View>
59
+ </View>
60
+ );
61
+ }
62
+
63
+ const styles = StyleSheet.create({
64
+ row: {
65
+ width: '100%',
66
+ flexDirection: 'row',
67
+ alignItems: 'center',
68
+ paddingHorizontal: DS_SPACING[800],
69
+ paddingTop: DS_SPACING[800],
70
+ paddingBottom: DS_SPACING[400],
71
+ backgroundColor: DS_SEMANTIC.surface.white,
72
+ },
73
+ slot: { width: SLOT_SIZE, height: SLOT_SIZE, alignItems: 'flex-start', justifyContent: 'center' },
74
+ slotTrailing: { alignItems: 'flex-end' },
75
+ center: { flex: 1, alignItems: 'center' },
76
+ title: { ...DS_TYPOGRAPHY.labelMd, color: DS_SEMANTIC.text.regular },
77
+ });
@@ -0,0 +1,2 @@
1
+ export { TopNav } from './TopNav';
2
+ export type { TopNavProps } from './TopNav';
@@ -0,0 +1,30 @@
1
+ import React, { useState } from 'react';
2
+ import { defineCatalogPage } from '@krapwoo/ds-viewer';
3
+ import { UnderlineTabs } from './UnderlineTabs';
4
+
5
+ function UnderlineTabsDemo() {
6
+ const [value, setValue] = useState('all');
7
+ return (
8
+ <UnderlineTabs
9
+ value={value}
10
+ onChange={setValue}
11
+ options={[
12
+ { value: 'all', label: 'All' },
13
+ // Leading-icon tab — the layout every other option (icon-less) would otherwise hide.
14
+ { value: 'nearby', label: 'Nearby', iconName: 'pin' },
15
+ { value: 'favorites', label: 'Favorites', badge: 2 },
16
+ ]}
17
+ />
18
+ );
19
+ }
20
+
21
+ export default defineCatalogPage({
22
+ component: 'UnderlineTabs',
23
+ group: 'Selection',
24
+ // Phone width plus a small phone, where long labels truncate.
25
+ previewWidths: [402, 320],
26
+ 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
+ whenToUse: "Quiet, secondary navigation within a screen that already has a clear primary focus. For a prominent, primary choice, use SegmentedToggle.",
28
+ a11y: 'Each tab is a Pressable label; the underline is a visual indicator only, so selection is also conveyed by the active label weight.',
29
+ render: () => <UnderlineTabsDemo />,
30
+ });
@@ -0,0 +1,156 @@
1
+ import React, { useState } from 'react';
2
+ import { View, Text, Pressable, Animated, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { DS_SEMANTIC, DS_SPACING, DS_RADIUS, DS_TYPOGRAPHY, DS_ICON_SIZE, DS_A11Y_MIN_TOUCH_TARGET } from '../../tokens';
4
+ import { Icon } from '../../icons/Icon.native';
5
+ import type { IconName } from '../../icons';
6
+ import { useSlideAnim } from '../SegmentedToggle/useSlideAnim';
7
+
8
+ export interface UnderlineTabOption {
9
+ value: string;
10
+ label: string;
11
+ /** Optional leading icon (16px) shown before the label. */
12
+ iconName?: IconName;
13
+ /** Optional count shown in a circular badge after the label (hidden when 0/undefined). */
14
+ badge?: number;
15
+ }
16
+
17
+ export interface UnderlineTabsProps {
18
+ /** The tabs. */
19
+ options: UnderlineTabOption[];
20
+ /** The currently-selected option `value`. */
21
+ value: string;
22
+ /** Selection handler. */
23
+ onChange: (value: string) => void;
24
+ style?: StyleProp<ViewStyle>;
25
+ }
26
+
27
+ const INDICATOR_HEIGHT = 2;
28
+
29
+ /**
30
+ * Low-prominence tab switcher — left-aligned text labels over a hairline rule, with a thin underline
31
+ * indicator that slides to the selected tab. A quieter stand-in for SegmentedToggle: same
32
+ * options/value/onChange API, but no filled track or lifted thumb. Two or more options.
33
+ */
34
+ export function UnderlineTabs({ options, value, onChange, style }: UnderlineTabsProps) {
35
+ // -1 when `value` matches no option (e.g. stale state after the options list changed). The
36
+ // indicator is hidden entirely in that case — clamping to tab 0 would underline a tab whose own
37
+ // label doesn't render as selected, a visibly contradictory state.
38
+ const matchedIndex = options.findIndex(o => o.value === value);
39
+ const hasSelection = matchedIndex !== -1;
40
+ const selectedIndex = Math.max(0, matchedIndex);
41
+ // Per-tab measured geometry (x offset + width within the row), so the indicator can size and slide to
42
+ // each variable-width label.
43
+ const [layouts, setLayouts] = useState<Record<number, { x: number; width: number }>>({});
44
+ // Which tab currently holds keyboard focus (null = none).
45
+ const [focusedValue, setFocusedValue] = useState<string | null>(null);
46
+ // translateX + width animate together; width can't use the native driver, so neither does.
47
+ const anim = useSlideAnim(selectedIndex, false);
48
+ const measured = options.every((_, i) => layouts[i] != null);
49
+
50
+ // Unlike SegmentedToggle's analytically-computed thumb position, this indicator slides to each
51
+ // tab's real measured `x` (from onLayout) — already RTL-correct as-is, since onLayout reports actual
52
+ // post-mirroring screen position, not an index-based LTR assumption.
53
+ const indices = options.map((_, i) => i);
54
+ const indicatorX = measured
55
+ ? anim.interpolate({ inputRange: indices, outputRange: indices.map(i => layouts[i].x) })
56
+ : 0;
57
+ const indicatorW = measured
58
+ ? anim.interpolate({ inputRange: indices, outputRange: indices.map(i => layouts[i].width) })
59
+ : 0;
60
+
61
+ return (
62
+ <View style={[styles.row, style]} accessibilityRole="tablist">
63
+ {options.map((opt, i) => {
64
+ const selected = opt.value === value;
65
+ const color = selected ? DS_SEMANTIC.text.regular : DS_SEMANTIC.text.muted;
66
+ return (
67
+ <Pressable
68
+ key={opt.value}
69
+ onPress={() => onChange(opt.value)}
70
+ onLayout={e => {
71
+ const { x, width } = e.nativeEvent.layout;
72
+ setLayouts(prev =>
73
+ prev[i] && prev[i].x === x && prev[i].width === width ? prev : { ...prev, [i]: { x, width } },
74
+ );
75
+ }}
76
+ onFocus={() => setFocusedValue(opt.value)}
77
+ onBlur={() => setFocusedValue(prev => (prev === opt.value ? null : prev))}
78
+ accessibilityRole="tab"
79
+ accessibilityLabel={opt.badge != null && opt.badge > 0 ? `${opt.label}, ${opt.badge}` : opt.label}
80
+ accessibilityState={{ selected }}
81
+ // Keyboard focus shows the same highlight as a press (same policy as SegmentedToggle/ListItem).
82
+ style={({ pressed }) => [styles.tab, (pressed || focusedValue === opt.value) && styles.tabPressed]}
83
+ >
84
+ {opt.iconName && <Icon name={opt.iconName} size={DS_ICON_SIZE.sm} color={color} />}
85
+ <Text style={[styles.label, { color }]}>{opt.label}</Text>
86
+ {opt.badge != null && opt.badge > 0 && (
87
+ <View style={[styles.badge, { backgroundColor: color }]}>
88
+ <Text style={[styles.badgeText, { color: DS_SEMANTIC.surface.white }]}>{opt.badge}</Text>
89
+ </View>
90
+ )}
91
+ </Pressable>
92
+ );
93
+ })}
94
+ {measured && hasSelection && (
95
+ <Animated.View
96
+ pointerEvents="none"
97
+ style={[styles.indicator, { width: indicatorW, transform: [{ translateX: indicatorX }] }]}
98
+ />
99
+ )}
100
+ </View>
101
+ );
102
+ }
103
+
104
+ const styles = StyleSheet.create({
105
+ row: {
106
+ flexDirection: 'row',
107
+ alignItems: 'flex-end',
108
+ gap: DS_SPACING[800],
109
+ borderBottomWidth: StyleSheet.hairlineWidth,
110
+ borderBottomColor: DS_SEMANTIC.element.divider,
111
+ },
112
+ tab: {
113
+ flexDirection: 'row',
114
+ alignItems: 'center',
115
+ gap: DS_SPACING[200],
116
+ paddingTop: DS_SPACING[400],
117
+ paddingBottom: DS_SPACING[600],
118
+ // A fixed floor (not a spacing token — this is an accessibility guideline number, not a design
119
+ // choice) so the tab's compact visual padding still guarantees a touch target.
120
+ minHeight: DS_A11Y_MIN_TOUCH_TARGET,
121
+ },
122
+ tabPressed: {
123
+ backgroundColor: DS_SEMANTIC.interaction.pressed,
124
+ borderRadius: DS_RADIUS.small,
125
+ },
126
+ label: {
127
+ // Matches the section-header label size (labelXs) it stands in for.
128
+ ...DS_TYPOGRAPHY.labelXs,
129
+ // Pin the line box to the badge height (18) so a tab is the same height with or without a count
130
+ // badge — otherwise the taller badged tab grows the row and shifts the divider/content below it.
131
+ lineHeight: 18,
132
+ },
133
+ // The sliding underline, sat on the hairline rule beneath the selected tab.
134
+ indicator: {
135
+ position: 'absolute',
136
+ left: 0,
137
+ bottom: -StyleSheet.hairlineWidth,
138
+ height: INDICATOR_HEIGHT,
139
+ borderRadius: INDICATOR_HEIGHT / 2,
140
+ backgroundColor: DS_SEMANTIC.text.regular,
141
+ },
142
+ // Fixed-diameter circle so the count badge stays round. 18 doesn't match a DS_SPACING/DS_ICON_SIZE
143
+ // step (16/20 are the neighbors) — sized to comfortably fit a 1-2 digit count at labelXs.
144
+ badge: {
145
+ width: 18,
146
+ height: 18,
147
+ borderRadius: DS_RADIUS.round,
148
+ alignItems: 'center',
149
+ justifyContent: 'center',
150
+ },
151
+ badgeText: {
152
+ ...DS_TYPOGRAPHY.labelXs,
153
+ // Pinned to the badge's own 18px diameter (above) so the count sits centered in the circle.
154
+ lineHeight: 18,
155
+ },
156
+ });
@@ -0,0 +1,2 @@
1
+ export { UnderlineTabs } from './UnderlineTabs';
2
+ export type { UnderlineTabsProps, UnderlineTabOption } from './UnderlineTabs';
@@ -0,0 +1,36 @@
1
+ /** Barrel for the React Native component tree. */
2
+ export * from './AnimatedChevron';
3
+ export * from './Avatar';
4
+ export * from './Badge';
5
+ export * from './Banner';
6
+ export * from './BottomSheet';
7
+ export * from './Button';
8
+ export * from './ButtonGroup';
9
+ export * from './Card';
10
+ export * from './Checkbox';
11
+ export * from './Dialog';
12
+ export * from './Divider';
13
+ export * from './Dock';
14
+ export * from './Dropdown';
15
+ export * from './EmptyState';
16
+ export * from './FieldContainer';
17
+ export * from './InputClearButton';
18
+ export * from './InputField';
19
+ export * from './List';
20
+ export * from './ListItem';
21
+ export * from './Loading';
22
+ export * from './Pill';
23
+ export * from './PillRow';
24
+ export * from './ProgressDots';
25
+ export * from './Radio';
26
+ export * from './SearchField';
27
+ export * from './SectionHeader';
28
+ export * from './SegmentedToggle';
29
+ export * from './Shimmer';
30
+ export * from './Surface';
31
+ export * from './Switch';
32
+ export * from './TextArea';
33
+ export * from './Toast';
34
+ export * from './Tooltip';
35
+ export * from './TopNav';
36
+ export * from './UnderlineTabs';
@@ -0,0 +1,27 @@
1
+ import { DS_SEMANTIC } from '../tokens';
2
+
3
+ /**
4
+ * The one shared status→color lookup behind every status-tinted component (Badge, Banner): a light
5
+ * tinted fill (`shade`) paired with a saturated foreground (`emphasis`), so a badge and a banner of
6
+ * the same status always read as the same color identity. Previously each component maintained its
7
+ * own identical copy of this map — one place to change now keeps them matched by construction.
8
+ * (Toast's variant map lives separately: its variant names differ (`success`/`informational`) and it
9
+ * bundles per-variant icons, so it's a different shape, not a third copy of this one.)
10
+ */
11
+ export type StatusVariant = 'neutral' | 'info' | 'positive' | 'warning' | 'negative';
12
+
13
+ export const STATUS_BG: Record<StatusVariant, string> = {
14
+ neutral: DS_SEMANTIC.shade.neutral,
15
+ info: DS_SEMANTIC.shade.info,
16
+ positive: DS_SEMANTIC.shade.positive,
17
+ warning: DS_SEMANTIC.shade.warning,
18
+ negative: DS_SEMANTIC.shade.negative,
19
+ };
20
+
21
+ export const STATUS_FG: Record<StatusVariant, string> = {
22
+ neutral: DS_SEMANTIC.emphasis.neutral,
23
+ info: DS_SEMANTIC.emphasis.info,
24
+ positive: DS_SEMANTIC.emphasis.positive,
25
+ warning: DS_SEMANTIC.emphasis.warning,
26
+ negative: DS_SEMANTIC.emphasis.negative,
27
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * react-native-svg renderer for the shared icon data (`./paths`).
3
+ *
4
+ * Reads `ICON_PATHS[name]` and maps each primitive to the matching RN-SVG element. Stroke icons
5
+ * get round caps/joins in the Lucide style; fill icons render solid. Per-primitive `fill`/`stroke`
6
+ * overrides (used by the transit icons) win over the def-level mode.
7
+ */
8
+ import React from 'react';
9
+ import Svg, { Path, Rect, Circle, Line, Polyline, Polygon } from 'react-native-svg';
10
+ import { DS_SEMANTIC, DS_ICON_SIZE } from '../tokens';
11
+ import { ICON_PATHS, type IconPrimitive } from './paths';
12
+ import type { IconRenderProps } from './types';
13
+
14
+ export type IconProps = IconRenderProps & { accessibilityLabel?: string };
15
+
16
+ /** Resolve a primitive's `'color' | 'none'` sentinel against the active icon color. */
17
+ function resolvePaint(value: 'color' | 'none' | undefined, color: string): string | undefined {
18
+ if (value === undefined) return undefined;
19
+ return value === 'color' ? color : 'none';
20
+ }
21
+
22
+ export function Icon({
23
+ name,
24
+ size = DS_ICON_SIZE.sm,
25
+ color = DS_SEMANTIC.text.regular,
26
+ strokeWidth,
27
+ accessibilityLabel,
28
+ }: IconProps) {
29
+ const def = ICON_PATHS[name];
30
+ if (!def) return null;
31
+
32
+ const isStroke = def.mode === 'stroke';
33
+ const defStrokeWidth = strokeWidth ?? def.strokeWidth ?? 2;
34
+
35
+ return (
36
+ <Svg
37
+ width={size}
38
+ height={size}
39
+ viewBox={def.viewBox}
40
+ fill="none"
41
+ accessibilityLabel={accessibilityLabel}
42
+ >
43
+ {def.primitives.map((p, i) => {
44
+ // Per-primitive override wins; otherwise fall back to the def mode.
45
+ const fill = resolvePaint(p.fill, color) ?? (isStroke ? 'none' : color);
46
+ const stroke = resolvePaint(p.stroke, color) ?? (isStroke ? color : undefined);
47
+ const hasStroke = stroke !== undefined && stroke !== 'none';
48
+
49
+ const common: Record<string, unknown> = { fill, fillRule: p.fillRule };
50
+ if (hasStroke) {
51
+ common.stroke = stroke;
52
+ common.strokeWidth = p.strokeWidth ?? defStrokeWidth;
53
+ // Round caps/joins are the Lucide house style; only apply for stroke-mode icons.
54
+ if (isStroke) {
55
+ common.strokeLinecap = 'round';
56
+ common.strokeLinejoin = 'round';
57
+ }
58
+ }
59
+ // A `transform` string (not `translateX`/`translateY` props) — react-native-svg's web
60
+ // shim only folds `translateX`/`translateY` into the SVG `transform` attribute, it does not
61
+ // strip the original props first, so they would otherwise also reach the DOM unchanged and
62
+ // React would warn "does not recognize the `translateX` prop on a DOM element".
63
+ if (p.translateX !== undefined || p.translateY !== undefined) {
64
+ common.transform = `translate(${p.translateX ?? 0}, ${p.translateY ?? 0})`;
65
+ }
66
+
67
+ return renderPrimitive(p, common, i);
68
+ })}
69
+ </Svg>
70
+ );
71
+ }
72
+
73
+ function renderPrimitive(p: IconPrimitive, common: Record<string, unknown>, key: number): React.ReactElement | null {
74
+ switch (p.tag) {
75
+ case 'path':
76
+ return <Path key={key} {...common} d={p.d} />;
77
+ case 'rect':
78
+ return <Rect key={key} {...common} x={p.x} y={p.y} width={p.width} height={p.height} rx={p.rx} ry={p.ry} />;
79
+ case 'circle':
80
+ return <Circle key={key} {...common} cx={p.cx} cy={p.cy} r={p.r} />;
81
+ case 'line':
82
+ return <Line key={key} {...common} x1={p.x1} y1={p.y1} x2={p.x2} y2={p.y2} />;
83
+ case 'polyline':
84
+ return <Polyline key={key} {...common} points={p.points} />;
85
+ case 'polygon':
86
+ return <Polygon key={key} {...common} points={p.points} />;
87
+ default:
88
+ return null;
89
+ }
90
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared icon layer barrel.
3
+ *
4
+ * Exposes the platform-neutral data + types only. Consumers import the renderer explicitly as
5
+ * `../icons/Icon.native` (the only renderer today — see the repo README for how a future platform
6
+ * port would add its own `Icon.<platform>.tsx` alongside it without touching this data).
7
+ */
8
+ export { ICON_PATHS } from './paths';
9
+ export type { IconPrimitive, IconDef } from './paths';
10
+ export type { IconName, IconRenderProps } from './types';