@krapwoo/ds-viewer 0.1.0 → 0.2.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 (159) hide show
  1. package/README.md +100 -78
  2. package/dist/cli/config.js +26 -1
  3. package/dist/cli/dev.js +50 -5
  4. package/dist/cli/glob.js +8 -0
  5. package/dist/cli/init.js +148 -18
  6. package/dist/cli/main.js +102 -3
  7. package/dist/cli/props.js +166 -18
  8. package/dist/cli/sync.js +17 -1
  9. package/dist/cli/workspace.js +49 -21
  10. package/native/catalog/CatalogShell.tsx +6 -1
  11. package/native/catalog/CatalogSidebar.tsx +98 -5
  12. package/native/catalog/TokenRow.tsx +3 -2
  13. package/native/catalog/index.ts +6 -5
  14. package/native/catalog/logoLayout.ts +12 -0
  15. package/native/catalog/pageApi.ts +10 -1
  16. package/native/catalog/tokens.ts +5 -0
  17. package/package.json +11 -7
  18. package/starter-kit/WHEN_TO_USE.md +124 -0
  19. package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
  20. package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
  21. package/starter-kit/components/AnimatedChevron/index.ts +2 -0
  22. package/starter-kit/components/Avatar/Avatar.catalog.tsx +72 -0
  23. package/starter-kit/components/Avatar/Avatar.tsx +74 -0
  24. package/starter-kit/components/Avatar/index.ts +2 -0
  25. package/starter-kit/components/Badge/Badge.catalog.tsx +96 -0
  26. package/starter-kit/components/Badge/Badge.tsx +56 -0
  27. package/starter-kit/components/Badge/Badge.types.ts +18 -0
  28. package/starter-kit/components/Badge/index.ts +2 -0
  29. package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
  30. package/starter-kit/components/Banner/Banner.tsx +257 -0
  31. package/starter-kit/components/Banner/Banner.types.ts +38 -0
  32. package/starter-kit/components/Banner/index.ts +2 -0
  33. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
  34. package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
  35. package/starter-kit/components/BottomSheet/index.ts +2 -0
  36. package/starter-kit/components/Button/Button.catalog.tsx +113 -0
  37. package/starter-kit/components/Button/Button.tsx +173 -0
  38. package/starter-kit/components/Button/Button.types.ts +52 -0
  39. package/starter-kit/components/Button/index.ts +2 -0
  40. package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
  41. package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
  42. package/starter-kit/components/ButtonGroup/index.ts +2 -0
  43. package/starter-kit/components/Card/Card.catalog.tsx +68 -0
  44. package/starter-kit/components/Card/Card.tsx +133 -0
  45. package/starter-kit/components/Card/index.ts +2 -0
  46. package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
  47. package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
  48. package/starter-kit/components/Checkbox/index.ts +2 -0
  49. package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
  50. package/starter-kit/components/Dialog/Dialog.tsx +82 -0
  51. package/starter-kit/components/Dialog/index.ts +2 -0
  52. package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
  53. package/starter-kit/components/Divider/Divider.tsx +20 -0
  54. package/starter-kit/components/Divider/index.ts +2 -0
  55. package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
  56. package/starter-kit/components/Dock/Dock.tsx +58 -0
  57. package/starter-kit/components/Dock/index.ts +2 -0
  58. package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
  59. package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
  60. package/starter-kit/components/Dropdown/index.ts +2 -0
  61. package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
  62. package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
  63. package/starter-kit/components/EmptyState/index.ts +2 -0
  64. package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
  65. package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
  66. package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
  67. package/starter-kit/components/FieldContainer/index.ts +2 -0
  68. package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
  69. package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
  70. package/starter-kit/components/InputClearButton/index.ts +2 -0
  71. package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
  72. package/starter-kit/components/InputField/InputField.tsx +418 -0
  73. package/starter-kit/components/InputField/index.ts +2 -0
  74. package/starter-kit/components/List/List.catalog.tsx +30 -0
  75. package/starter-kit/components/List/List.tsx +36 -0
  76. package/starter-kit/components/List/index.ts +2 -0
  77. package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
  78. package/starter-kit/components/ListItem/ListItem.tsx +165 -0
  79. package/starter-kit/components/ListItem/index.ts +2 -0
  80. package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
  81. package/starter-kit/components/Loading/Loading.tsx +121 -0
  82. package/starter-kit/components/Loading/index.ts +2 -0
  83. package/starter-kit/components/Pill/Pill.catalog.tsx +93 -0
  84. package/starter-kit/components/Pill/Pill.tsx +181 -0
  85. package/starter-kit/components/Pill/Pill.types.ts +30 -0
  86. package/starter-kit/components/Pill/index.ts +2 -0
  87. package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
  88. package/starter-kit/components/PillRow/PillRow.tsx +140 -0
  89. package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
  90. package/starter-kit/components/PillRow/index.ts +2 -0
  91. package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
  92. package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
  93. package/starter-kit/components/ProgressDots/index.ts +2 -0
  94. package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
  95. package/starter-kit/components/Radio/Radio.tsx +114 -0
  96. package/starter-kit/components/Radio/index.ts +2 -0
  97. package/starter-kit/components/Radio/usePressScale.ts +33 -0
  98. package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
  99. package/starter-kit/components/SearchField/SearchField.tsx +98 -0
  100. package/starter-kit/components/SearchField/index.ts +2 -0
  101. package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
  102. package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
  103. package/starter-kit/components/SectionHeader/index.ts +2 -0
  104. package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
  105. package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
  106. package/starter-kit/components/SegmentedToggle/index.ts +2 -0
  107. package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
  108. package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
  109. package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
  110. package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
  111. package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
  112. package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
  113. package/starter-kit/components/Shimmer/index.ts +2 -0
  114. package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
  115. package/starter-kit/components/Surface/Surface.tsx +45 -0
  116. package/starter-kit/components/Surface/index.ts +2 -0
  117. package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
  118. package/starter-kit/components/Switch/Switch.tsx +129 -0
  119. package/starter-kit/components/Switch/index.ts +2 -0
  120. package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
  121. package/starter-kit/components/TextArea/TextArea.tsx +99 -0
  122. package/starter-kit/components/TextArea/index.ts +2 -0
  123. package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
  124. package/starter-kit/components/Toast/Toast.tsx +144 -0
  125. package/starter-kit/components/Toast/index.ts +2 -0
  126. package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
  127. package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
  128. package/starter-kit/components/Tooltip/index.ts +2 -0
  129. package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
  130. package/starter-kit/components/TopNav/TopNav.tsx +77 -0
  131. package/starter-kit/components/TopNav/index.ts +2 -0
  132. package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
  133. package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
  134. package/starter-kit/components/UnderlineTabs/index.ts +2 -0
  135. package/starter-kit/components/index.ts +36 -0
  136. package/starter-kit/components/statusColors.ts +27 -0
  137. package/starter-kit/icons/Icon.native.tsx +90 -0
  138. package/starter-kit/icons/index.ts +10 -0
  139. package/starter-kit/icons/paths.ts +623 -0
  140. package/starter-kit/icons/types.ts +69 -0
  141. package/starter-kit/pages/Colors.catalog.tsx +55 -0
  142. package/starter-kit/pages/Font.catalog.tsx +45 -0
  143. package/starter-kit/pages/Icons.catalog.tsx +51 -0
  144. package/starter-kit/pages/Manifest.catalog.tsx +24 -0
  145. package/starter-kit/pages/Motion.catalog.tsx +101 -0
  146. package/starter-kit/pages/Radius.catalog.tsx +35 -0
  147. package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
  148. package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
  149. package/starter-kit/pages/Shadow.catalog.tsx +34 -0
  150. package/starter-kit/pages/Spacing.catalog.tsx +12 -0
  151. package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
  152. package/starter-kit/pages/Typography.catalog.tsx +17 -0
  153. package/starter-kit/tokens/index.ts +50 -0
  154. package/starter-kit/tokens/motion.ts +90 -0
  155. package/starter-kit/tokens/palette.ts +111 -0
  156. package/starter-kit/tokens/scales.ts +106 -0
  157. package/starter-kit/tokens/semantic.ts +98 -0
  158. package/starter-kit/tokens/shadow.ts +36 -0
  159. package/starter-kit/tokens/typography.ts +72 -0
@@ -0,0 +1,247 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { View, Text, ScrollView, StyleSheet } from 'react-native';
3
+ import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
4
+ import { Avatar } from '../components/Avatar';
5
+ import { Badge } from '../components/Badge';
6
+ import { Button } from '../components/Button';
7
+ import { Dock } from '../components/Dock';
8
+ import { Dropdown } from '../components/Dropdown';
9
+ import { EmptyState } from '../components/EmptyState';
10
+ import { List } from '../components/List';
11
+ import { ListItem } from '../components/ListItem';
12
+ import { Loading } from '../components/Loading';
13
+ import { PillRow, type PillRowItem } from '../components/PillRow';
14
+ import { SearchField } from '../components/SearchField';
15
+ import { SectionHeader } from '../components/SectionHeader';
16
+ import { SegmentedToggle } from '../components/SegmentedToggle';
17
+ import { Surface } from '../components/Surface';
18
+ import { Toast } from '../components/Toast';
19
+ import { Tooltip } from '../components/Tooltip';
20
+ import { TopNav } from '../components/TopNav';
21
+ import { UnderlineTabs } from '../components/UnderlineTabs';
22
+ import { Icon } from '../icons/Icon.native';
23
+ import { DS_ICON_SIZE, DS_MOTION_DURATION, DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../tokens';
24
+
25
+ const styles = StyleSheet.create({
26
+ // Saved trips recipe — fills PhoneFrame edge-to-edge (like dropdownFrameContent), with a
27
+ // fixed TopNav header and Dock footer around a scrollable middle, the same fixed-header/
28
+ // scrollable-body/fixed-footer shape a real screen would use.
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 },
34
+ savedTripsScrollContent: { padding: DS_SPACING[800], gap: DS_SPACING[600] },
35
+ // Shared by SavedTrips' small inline rows — the "Updating arrival times…" loading row and each
36
+ // list row's trailing badge+button cluster (identical layout, one key).
37
+ savedTripsInlineRow: { flexDirection: 'row', alignItems: 'center', gap: DS_SPACING[300] },
38
+ savedTripsLoadingText: { ...DS_TYPOGRAPHY.bodyXs, color: DS_SEMANTIC.text.muted },
39
+ // Floats over the top of the screen — same top/left/right inset Toast's own catalog demo uses —
40
+ // instead of sitting inline in the flex flow and pushing the Dock down. A column with a small
41
+ // gap, since back-to-back removals stack one toast per removal (newest on top).
42
+ savedTripsToastOverlay: { position: 'absolute', top: DS_SPACING[800], left: DS_SPACING[600], right: DS_SPACING[600], zIndex: 20, gap: DS_SPACING[300] },
43
+ // Map mode's stand-in — a real map isn't in scope for this recipe, just enough to show the
44
+ // SegmentedToggle actually switches the screen's content, not just its own thumb.
45
+ savedTripsMapPlaceholder: { alignItems: 'center', justifyContent: 'center', gap: DS_SPACING[400], paddingVertical: DS_SPACING[2400] },
46
+ savedTripsMapPlaceholderText: { ...DS_TYPOGRAPHY.bodySm, color: DS_SEMANTIC.text.muted },
47
+ });
48
+
49
+ const SAVED_TRIPS_INITIAL = [
50
+ { id: 'uptown', title: 'Uptown & The Bronx', subtitle: '4 min · Subway', iconName: 'subway' as const, badgeVariant: 'positive' as const, badgeLabel: 'On time' },
51
+ { id: 'downtown', title: 'Downtown & Brooklyn', subtitle: '12 min · Train', iconName: 'train' as const, badgeVariant: 'warning' as const, badgeLabel: 'Delayed' },
52
+ { id: 'airport', title: 'Airport Express', subtitle: '22 min · Train', iconName: 'train' as const, badgeVariant: 'neutral' as const, badgeLabel: 'Scheduled' },
53
+ ];
54
+ const SAVED_TRIPS_SORT_OPTIONS = [
55
+ { value: 'arrival', label: 'Arrival time' },
56
+ { value: 'distance', label: 'Distance' },
57
+ { value: 'name', label: 'Name' },
58
+ ];
59
+ // How long the "Trip removed" toast stays up before auto-dismissing, same as a real snackbar —
60
+ // matches Toast's own doc comment ("expected to go away on its own or via its own action").
61
+ const SAVED_TRIPS_TOAST_MS = 3000;
62
+
63
+ function SavedTripsDemo() {
64
+ const [tab, setTab] = useState('all');
65
+ const [viewMode, setViewMode] = useState('list');
66
+ const [query, setQuery] = useState('');
67
+ const [sortBy, setSortBy] = useState('arrival');
68
+ const [pills, setPills] = useState<PillRowItem[]>([
69
+ { id: 'subway', label: 'Subway', variant: 'selected', iconName: 'subway' },
70
+ { id: 'train', label: 'Train', variant: 'not_selected', iconName: 'train' },
71
+ { id: 'ferry', label: 'Ferry', variant: 'not_selected', iconName: 'ferry' },
72
+ ]);
73
+ const [showFilterTip, setShowFilterTip] = useState(false);
74
+ const [trips, setTrips] = useState(SAVED_TRIPS_INITIAL);
75
+ // Toasts only appear right after a real action (removing a trip) and auto-dismiss — they never
76
+ // sit statically in the initial render, since that's not what Toast is for. Each removal gets its
77
+ // OWN stacked toast on its own 3s clock (newest on top), so removing a second trip while the
78
+ // first toast is still up never cuts the first one short — the earlier single-toast version
79
+ // reused one `visible` boolean, which couldn't restart its timer for a back-to-back removal.
80
+ // `visible: false` plays Toast's own exit animation; the entry is dropped from the array only
81
+ // after that animation has had time to run.
82
+ const [toasts, setToasts] = useState<
83
+ Array<{ key: number; trip: (typeof SAVED_TRIPS_INITIAL)[number]; visible: boolean }>
84
+ >([]);
85
+ const nextToastKey = useRef(0);
86
+ const toastTimers = useRef<Array<ReturnType<typeof setTimeout>>>([]);
87
+ // Timers are scheduled per-removal (in event handlers, not an effect), so unmount cleanup is one
88
+ // sweep here rather than per-timer effect returns.
89
+ useEffect(() => () => toastTimers.current.forEach(clearTimeout), []);
90
+
91
+ const hideToast = (key: number) => {
92
+ setToasts((prev) => prev.map((t) => (t.key === key ? { ...t, visible: false } : t)));
93
+ // Drop the entry once Toast's exit animation (it exits at DS_MOTION_DURATION.fast) has played.
94
+ toastTimers.current.push(
95
+ setTimeout(() => setToasts((prev) => prev.filter((t) => t.key !== key)), DS_MOTION_DURATION.fast),
96
+ );
97
+ };
98
+ const removeTrip = (id: string) => {
99
+ const removed = trips.find((t) => t.id === id);
100
+ if (!removed) return;
101
+ setTrips((prev) => prev.filter((t) => t.id !== id));
102
+ const key = nextToastKey.current++;
103
+ setToasts((prev) => [{ key, trip: removed, visible: true }, ...prev]);
104
+ toastTimers.current.push(setTimeout(() => hideToast(key), SAVED_TRIPS_TOAST_MS));
105
+ };
106
+ const undoRemove = (key: number, trip: (typeof SAVED_TRIPS_INITIAL)[number]) => {
107
+ setTrips((prev) => [...prev, trip]);
108
+ hideToast(key);
109
+ };
110
+
111
+ return (
112
+ <PhoneFrame>
113
+ <View style={styles.savedTripsScreen}>
114
+ <TopNav
115
+ title="Saved trips"
116
+ trailing={
117
+ <Tooltip visible={showFilterTip} label="Filter by mode" placement="bottom" align="right">
118
+ <Button
119
+ variant="secondary"
120
+ size="small"
121
+ showIcon
122
+ showLabel={false}
123
+ iconName="menu"
124
+ accessibilityLabel="Filters"
125
+ onPress={() => setShowFilterTip((v) => !v)}
126
+ />
127
+ </Tooltip>
128
+ }
129
+ />
130
+ <Surface tone="muted">
131
+ <ScrollView style={styles.savedTripsScroll} contentContainerStyle={styles.savedTripsScrollContent}>
132
+ <SearchField value={query} onChangeText={setQuery} placeholder="Search stations" />
133
+ <SegmentedToggle
134
+ value={viewMode}
135
+ onChange={setViewMode}
136
+ options={[
137
+ { value: 'list', label: 'List', iconName: 'menu' },
138
+ { value: 'map', label: 'Map', iconName: 'map' },
139
+ ]}
140
+ />
141
+ {viewMode === 'map' ? (
142
+ <View style={styles.savedTripsMapPlaceholder}>
143
+ <Icon name="map" size={DS_ICON_SIZE.xl} color={DS_SEMANTIC.text.muted} />
144
+ <Text style={styles.savedTripsMapPlaceholderText}>Map view</Text>
145
+ </View>
146
+ ) : (
147
+ <>
148
+ <UnderlineTabs
149
+ value={tab}
150
+ onChange={setTab}
151
+ options={[
152
+ { value: 'all', label: 'All' },
153
+ { value: 'nearby', label: 'Nearby' },
154
+ { value: 'favorites', label: 'Favorites' },
155
+ ]}
156
+ />
157
+ {tab === 'favorites' ? (
158
+ <EmptyState
159
+ iconName="waypoints"
160
+ title="No favorites yet"
161
+ description="Star a trip to see it here."
162
+ action={{ label: 'Browse trips', onPress: () => setTab('all') }}
163
+ secondaryAction={{ label: 'Not now', onPress: () => {} }}
164
+ />
165
+ ) : (
166
+ <>
167
+ <PillRow
168
+ pills={pills.map((p) => ({ ...p, onPress: () => setPills((prev) => prev.map((x) => ({ ...x, variant: x.id === p.id ? 'selected' : 'not_selected' }))) }))}
169
+ showAddPill={false}
170
+ />
171
+ <View style={styles.savedTripsInlineRow}>
172
+ <Loading size={14} />
173
+ <Text style={styles.savedTripsLoadingText}>Updating arrival times…</Text>
174
+ </View>
175
+ {/* Dropdown's own `label` prop, not a hand-rolled Text above it — the component
176
+ already owns the labeled-field pattern (PillRow, which has no label prop, is
177
+ the case where an external label is legitimately the only option). */}
178
+ <Dropdown label="Sort by" value={sortBy} onChange={setSortBy} options={SAVED_TRIPS_SORT_OPTIONS} />
179
+ <SectionHeader title="Nearby stations" />
180
+ {trips.length > 0 ? (
181
+ <List>
182
+ {trips.map((trip) => (
183
+ <ListItem
184
+ key={trip.id}
185
+ title={trip.title}
186
+ subtitle={trip.subtitle}
187
+ leading={<Avatar iconName={trip.iconName} size={40} />}
188
+ trailing={
189
+ <View style={styles.savedTripsInlineRow}>
190
+ <Badge variant={trip.badgeVariant} label={trip.badgeLabel} />
191
+ <Button
192
+ variant="ghost"
193
+ size="small"
194
+ showIcon
195
+ showLabel={false}
196
+ iconName="clear"
197
+ accessibilityLabel={`Remove ${trip.title}`}
198
+ onPress={() => removeTrip(trip.id)}
199
+ />
200
+ </View>
201
+ }
202
+ />
203
+ ))}
204
+ </List>
205
+ ) : (
206
+ <EmptyState
207
+ iconName="waypoints"
208
+ title="No nearby trips"
209
+ description="Removed trips you save will show up here again."
210
+ />
211
+ )}
212
+ </>
213
+ )}
214
+ </>
215
+ )}
216
+ </ScrollView>
217
+ </Surface>
218
+ {/* Absolutely positioned over the top of the screen (not inline in the flex flow — a real
219
+ toast floats over content, it doesn't push the Dock down) — same placement Toast's own
220
+ catalog demo uses. One Toast per pending removal, stacked newest-on-top; each entry
221
+ stays in the array (with `visible: false`) through its own exit animation instead of
222
+ being yanked out of the tree mid-motion. */}
223
+ <View style={styles.savedTripsToastOverlay} pointerEvents="box-none">
224
+ {toasts.map((t) => (
225
+ <Toast
226
+ key={t.key}
227
+ visible={t.visible}
228
+ message={`${t.trip.title} removed`}
229
+ action={{ label: 'Undo', onPress: () => undoRemove(t.key, t.trip) }}
230
+ />
231
+ ))}
232
+ </View>
233
+ <Dock>
234
+ <Button label="Plan a new trip" onPress={() => {}} />
235
+ </Dock>
236
+ </View>
237
+ </PhoneFrame>
238
+ );
239
+ }
240
+
241
+ export default defineCatalogPage({
242
+ group: 'Recipes',
243
+ 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
+ tokenGallery: true,
245
+ fullWidthLabel: 'Preview',
246
+ render: () => <SavedTripsDemo />,
247
+ });
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { defineCatalogPage, TokenRow } from '@krapwoo/ds-viewer';
4
+ import { DS_RADIUS, DS_SEMANTIC, DS_SHADOW, DS_SHADOW_USE, DS_SPACING, DS_TYPOGRAPHY, type ShadowToken } from '../tokens';
5
+
6
+ const styles = StyleSheet.create({
7
+ tokenStack: { gap: DS_SPACING[800] },
8
+ previewRow: { flexDirection: 'row', alignItems: 'center', gap: DS_SPACING[600] },
9
+ previewLabel: { minWidth: 64, ...DS_TYPOGRAPHY.labelSm, color: DS_SEMANTIC.text.regular },
10
+ shadowBox: { width: 56, height: 40, borderRadius: DS_RADIUS.medium, backgroundColor: DS_SEMANTIC.surface.white },
11
+ });
12
+
13
+ function ShadowGallery() {
14
+ const steps = Object.keys(DS_SHADOW) as ShadowToken[];
15
+ return (
16
+ <View style={styles.tokenStack}>
17
+ {steps.map((step, i) => (
18
+ <TokenRow key={step} use={DS_SHADOW_USE[step]} last={i === steps.length - 1}>
19
+ <View style={styles.previewRow}>
20
+ <Text style={styles.previewLabel}>{step}</Text>
21
+ <View style={[styles.shadowBox, DS_SHADOW[step]]} />
22
+ </View>
23
+ </TokenRow>
24
+ ))}
25
+ </View>
26
+ );
27
+ }
28
+
29
+ export default defineCatalogPage({
30
+ group: 'Tokens',
31
+ description: 'Elevation tokens as ready-to-spread React Native style objects (shadowColor/Offset/Opacity/Radius + Android elevation).',
32
+ tokenGallery: true,
33
+ render: () => <ShadowGallery />,
34
+ });
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { defineCatalogPage, SpacingScaleGallery } from '@krapwoo/ds-viewer';
3
+ import { DS_SPACING, DS_SPACING_STEPS, DS_SPACING_USE } from '../tokens';
4
+
5
+ export default defineCatalogPage({
6
+ group: 'Tokens',
7
+ description: 'The spacing scale (token number ÷ 50 = px), each step with a grounded "when to use this" note from DS_SPACING_USE. Reference steps directly — DS_SPACING[800], never a raw 16 — the key union is typo-safe by itself.',
8
+ tokenGallery: true,
9
+ render: () => (
10
+ <SpacingScaleGallery steps={DS_SPACING_STEPS} values={DS_SPACING} useNotes={DS_SPACING_USE} />
11
+ ),
12
+ });
@@ -0,0 +1,48 @@
1
+ import React, { useState } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
4
+ import { Button } from '../components/Button';
5
+ import { Card } from '../components/Card';
6
+ import { Divider } from '../components/Divider';
7
+ import { InputField } from '../components/InputField';
8
+ import { DS_SPACING } from '../tokens';
9
+
10
+ const styles = StyleSheet.create({
11
+ // Trip planner recipe — fills PhoneFrame edge-to-edge (`flex:1` + `alignSelf:'stretch'` override
12
+ // PhoneFrame's own `alignItems/justifyContent:'center'`).
13
+ recipeFrameContent: { flex: 1, alignSelf: 'stretch', padding: DS_SPACING[800] },
14
+ recipeFields: { gap: DS_SPACING[600] },
15
+ recipeDivider: { marginVertical: DS_SPACING[600] },
16
+ recipeActions: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
17
+ });
18
+
19
+ function TripPlannerFormDemo() {
20
+ const [from, setFrom] = useState('Current location');
21
+ const [to, setTo] = useState('');
22
+ return (
23
+ <PhoneFrame>
24
+ <View style={styles.recipeFrameContent}>
25
+ <Card>
26
+ <View style={styles.recipeFields}>
27
+ <InputField label="From" value={from} onChangeText={setFrom} editable />
28
+ <InputField label="To" value={to} onChangeText={setTo} editable placeholder="Where to?" />
29
+ </View>
30
+ <Divider style={styles.recipeDivider} />
31
+ <View style={styles.recipeActions}>
32
+ <Button variant="tertiary" size="medium" label="Add stop" onPress={() => {}} />
33
+ {/* trim() so a whitespace-only "destination" can't enable Continue. */}
34
+ <Button variant="primary" size="medium" label="Continue" onPress={() => {}} disabled={!to.trim()} />
35
+ </View>
36
+ </Card>
37
+ </View>
38
+ </PhoneFrame>
39
+ );
40
+ }
41
+
42
+ export default defineCatalogPage({
43
+ group: 'Recipes',
44
+ 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
+ tokenGallery: true,
46
+ fullWidthLabel: 'Preview',
47
+ render: () => <TripPlannerFormDemo />,
48
+ });
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { defineCatalogPage, TypeScaleGallery } from '@krapwoo/ds-viewer';
3
+ import { DS_TYPOGRAPHY, DS_TYPOGRAPHY_USE, type TypographyToken } from '../tokens';
4
+
5
+ export default defineCatalogPage({
6
+ group: 'Tokens',
7
+ description: 'The type scale, rendered at its real sizes with a grounded "when to use this" note from DS_TYPOGRAPHY_USE per token — label* are semibold UI labels, body* regular reading text, emphasis*/title/display headlines.',
8
+ tokenGallery: true,
9
+ render: () => (
10
+ <TypeScaleGallery
11
+ steps={Object.keys(DS_TYPOGRAPHY) as TypographyToken[]}
12
+ sampleStyle={(name) => DS_TYPOGRAPHY[name]}
13
+ meta={(name) => `${DS_TYPOGRAPHY[name].fontSize}/${DS_TYPOGRAPHY[name].fontWeight}`}
14
+ useNotes={DS_TYPOGRAPHY_USE}
15
+ />
16
+ ),
17
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Design tokens — the single source of truth for the native component tree.
3
+ *
4
+ * Nothing here is platform-specific: values are plain hexes, numbers, and font-weight strings. The
5
+ * RN components import these directly. Kept platform-neutral on purpose so a future non-native
6
+ * platform (web, desktop, …) can consume the same values — see the repo README.
7
+ */
8
+ export {
9
+ PALETTE_STEPS,
10
+ DS_PALETTE,
11
+ paletteTokenForColor,
12
+ colorValueLabel,
13
+ } from './palette';
14
+ export type { PaletteName, PaletteStep } from './palette';
15
+
16
+ export { DS_SEMANTIC } from './semantic';
17
+
18
+ export {
19
+ DS_SPACING,
20
+ DS_SPACING_STEPS,
21
+ DS_SPACING_USE,
22
+ DS_RADIUS,
23
+ DS_RADIUS_STEPS,
24
+ DS_RADIUS_USE,
25
+ DS_ICON_SIZE,
26
+ DS_ICON_SIZE_STEPS,
27
+ DS_A11Y_MIN_TOUCH_TARGET,
28
+ } from './scales';
29
+ export type { SpacingStep, RadiusStep, IconSizeStep } from './scales';
30
+
31
+ export { DS_FONT_WEIGHT, DS_FONT_WEIGHT_USE, DS_TYPOGRAPHY, DS_TYPOGRAPHY_USE } from './typography';
32
+ export type { FontWeightName, TypographyToken } from './typography';
33
+
34
+ export { DS_SHADOW, DS_SHADOW_USE } from './shadow';
35
+ export type { ShadowToken } from './shadow';
36
+
37
+ export {
38
+ DS_MOTION_DURATION,
39
+ DS_MOTION_DURATION_STEPS,
40
+ DS_MOTION_DURATION_USE,
41
+ DS_MOTION_EASING,
42
+ DS_MOTION_EASING_STEPS,
43
+ DS_MOTION_EASING_USE,
44
+ DS_MOTION_SPRING,
45
+ DS_MOTION_SPRING_USE,
46
+ DS_MOTION_LOOP_DURATION,
47
+ DS_MOTION_LOOP_DURATION_STEPS,
48
+ DS_MOTION_LOOP_DURATION_USE,
49
+ } from './motion';
50
+ export type { MotionDurationStep, MotionEasingStep, MotionSpringConfig, MotionLoopDurationStep } from './motion';
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Motion tokens — duration and easing curves for transitions/animations.
3
+ *
4
+ * Easing curves are plain cubic-bezier tuples (`[x1, y1, x2, y2]`, matching CSS `cubic-bezier()` /
5
+ * RN `Easing.bezier()`) rather than `Easing` function objects, so this file stays free of any
6
+ * `react-native` import — the same platform-neutral shape every other token file follows. Convert
7
+ * at the call site: `Easing.bezier(...DS_MOTION_EASING.standard)`.
8
+ */
9
+ export const DS_MOTION_DURATION_STEPS = ['fast', 'base', 'slow'] as const;
10
+ export type MotionDurationStep = (typeof DS_MOTION_DURATION_STEPS)[number];
11
+
12
+ export const DS_MOTION_DURATION: Record<MotionDurationStep, number> = {
13
+ fast: 150,
14
+ base: 240,
15
+ slow: 400,
16
+ };
17
+
18
+ export const DS_MOTION_EASING_STEPS = ['standard', 'decelerate', 'accelerate'] as const;
19
+ export type MotionEasingStep = (typeof DS_MOTION_EASING_STEPS)[number];
20
+
21
+ export const DS_MOTION_EASING: Record<MotionEasingStep, readonly [number, number, number, number]> = {
22
+ standard: [0.4, 0.0, 0.2, 1],
23
+ decelerate: [0.0, 0.0, 0.2, 1],
24
+ accelerate: [0.4, 0.0, 1, 1],
25
+ };
26
+
27
+ /**
28
+ * When to reach for each duration/easing step — grounded in this template's own real animations
29
+ * (AnimatedChevron's 240ms rotate, CollapsibleSwap's 240ms default) where a value already exists;
30
+ * `fast`/`accelerate`/`decelerate` are the standard Material-motion counterparts alongside it, for
31
+ * the short taps and exits this template doesn't yet have a named constant for.
32
+ */
33
+ export const DS_MOTION_DURATION_USE: Record<MotionDurationStep, string> = {
34
+ fast: 'A quick, barely-there transition — a pressed-state feedback flash, a small icon swap.',
35
+ base: "The default — AnimatedChevron's rotate and CollapsibleSwap's expand/collapse both already run at 240ms. Reach for this first.",
36
+ slow: 'A deliberate, noticeable transition — a sheet or panel entering/exiting the screen.',
37
+ };
38
+
39
+ export const DS_MOTION_EASING_USE: Record<MotionEasingStep, string> = {
40
+ standard: 'The default for most transitions — eases in and out symmetrically, for a move that starts and ends in the same place (a toggle, a swap).',
41
+ decelerate: 'Fast start, gentle stop — for something entering the screen (a sheet sliding up, a toast appearing).',
42
+ accelerate: 'Gentle start, fast finish — for something leaving the screen (a sheet dismissing, a toast exiting).',
43
+ };
44
+
45
+ /** Spring physics config, shaped for `Animated.spring(value, DS_MOTION_SPRING)` (or Reanimated's
46
+ * `withSpring`). Grounded in the metro-native app this template was extracted from, where this exact
47
+ * config (there, `SHEET_SPRING_CONFIG`) drives its BottomSheet's snap-point transitions — a
48
+ * near-critically-damped spring (high damping relative to stiffness, `overshootClamping` on) that
49
+ * settles quickly with no bounce, unlike a playful/bouncy spring. */
50
+ export interface MotionSpringConfig {
51
+ stiffness: number;
52
+ damping: number;
53
+ mass: number;
54
+ overshootClamping: boolean;
55
+ restDisplacementThreshold: number;
56
+ restSpeedThreshold: number;
57
+ }
58
+
59
+ export const DS_MOTION_SPRING: MotionSpringConfig = {
60
+ stiffness: 1000,
61
+ damping: 500,
62
+ mass: 3,
63
+ overshootClamping: true,
64
+ restDisplacementThreshold: 1,
65
+ restSpeedThreshold: 1,
66
+ };
67
+
68
+ export const DS_MOTION_SPRING_USE =
69
+ 'A snappy, near-critically-damped spring for programmatic snap-point transitions — e.g. a bottom sheet or panel settling into position after a drag, rather than a fixed-duration timing curve.';
70
+
71
+ /**
72
+ * Durations for continuous, indeterminate loops (a spinner's fill→empty cycle, a shimmer's pulse) —
73
+ * a distinct category from `DS_MOTION_DURATION`'s one-shot transitions, and generally slower: a loop
74
+ * has to read as calm and ongoing rather than a quick state change. Grounded in this template's own
75
+ * two existing loops: Loading's circle/linear fill-cycle (`duration` prop, default 1200) and
76
+ * Shimmer's shared pulse clock (`PULSE_CYCLE_MS`, 1800 — deliberately slower and calmer than the
77
+ * spinner, since a shimmer stands in for content that just hasn't loaded yet, not an active process).
78
+ */
79
+ export const DS_MOTION_LOOP_DURATION_STEPS = ['spinner', 'pulse'] as const;
80
+ export type MotionLoopDurationStep = (typeof DS_MOTION_LOOP_DURATION_STEPS)[number];
81
+
82
+ export const DS_MOTION_LOOP_DURATION: Record<MotionLoopDurationStep, number> = {
83
+ spinner: 1200,
84
+ pulse: 1800,
85
+ };
86
+
87
+ export const DS_MOTION_LOOP_DURATION_USE: Record<MotionLoopDurationStep, string> = {
88
+ spinner: "An active, ongoing process the user is waiting on — Loading's own default for both its circle and linear variants.",
89
+ pulse: "Content that hasn't loaded yet, not an active process — deliberately slower/calmer than spinner. Shimmer's shared pulse clock runs at this rate so every visible shimmer stays in lockstep.",
90
+ };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Raw color palette — the base scale every semantic token is built from.
3
+ *
4
+ * Each hue steps 0 (lightest) → 800 (darkest). These are RAW values; app code should almost
5
+ * always reach for a semantic token (see semantic.ts) instead of a palette step directly, so a
6
+ * rebrand only touches this file + semantic.ts. To rebrand: swap these hexes for your own scale
7
+ * (keep the 0–800 shape) and the whole system re-themes.
8
+ */
9
+ export const PALETTE_STEPS = [0, 100, 200, 300, 400, 500, 600, 700, 800] as const;
10
+ export type PaletteStep = (typeof PALETTE_STEPS)[number];
11
+
12
+ export const DS_PALETTE = {
13
+ grey: {
14
+ 0: '#FFFFFF',
15
+ 100: '#F5F5F5',
16
+ 200: '#E5E5E5',
17
+ 300: '#D4D4D4',
18
+ 400: '#A3A3A3',
19
+ 500: '#737373',
20
+ 600: '#404040',
21
+ 700: '#171717',
22
+ 800: '#000000',
23
+ },
24
+ green: {
25
+ 0: '#F0FFF4',
26
+ 100: '#C6F6D5',
27
+ 200: '#9AE6B4',
28
+ 300: '#68D391',
29
+ 400: '#48BB78',
30
+ 500: '#38A169',
31
+ 600: '#2F855A',
32
+ 700: '#276749',
33
+ 800: '#1C4532',
34
+ },
35
+ blue: {
36
+ 0: '#EBF8FF',
37
+ 100: '#BEE3F8',
38
+ 200: '#90CDF4',
39
+ 300: '#63B3ED',
40
+ 400: '#4299E1',
41
+ 500: '#3182CE',
42
+ 600: '#2B6CB0',
43
+ 700: '#2C5282',
44
+ 800: '#1A365D',
45
+ },
46
+ red: {
47
+ 0: '#FFF5F5',
48
+ 100: '#FED7D7',
49
+ 200: '#FEB2B2',
50
+ 300: '#FC8181',
51
+ 400: '#F56565',
52
+ 500: '#E53E3E',
53
+ 600: '#C53030',
54
+ 700: '#9B2C2C',
55
+ 800: '#63171B',
56
+ },
57
+ yellow: {
58
+ 0: '#FFF4EB',
59
+ 100: '#FFE3C7',
60
+ 200: '#FFD099',
61
+ 300: '#FFB966',
62
+ 400: '#FFA333',
63
+ 500: '#FF8C00',
64
+ 600: '#D67600',
65
+ 700: '#A35A00',
66
+ 800: '#703E00',
67
+ },
68
+ purple: {
69
+ 0: '#FAF5FF',
70
+ 100: '#E9D8FD',
71
+ 200: '#D6BCFA',
72
+ 300: '#B794F4',
73
+ 400: '#9F7AEA',
74
+ 500: '#805AD5',
75
+ 600: '#6B46C1',
76
+ 700: '#553C9A',
77
+ 800: '#322659',
78
+ },
79
+ } as const;
80
+
81
+ export type PaletteName = keyof typeof DS_PALETTE;
82
+
83
+ function normalizeHex(color: string): string | null {
84
+ const trimmed = color.trim();
85
+ if (!trimmed.startsWith('#')) return null;
86
+ let hex = trimmed.toUpperCase();
87
+ if (hex.length === 4) {
88
+ hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
89
+ }
90
+ return hex;
91
+ }
92
+
93
+ /** Resolves a hex color to a palette token name (e.g. `green-600`), or null if not in DS_PALETTE. */
94
+ export function paletteTokenForColor(color: string): string | null {
95
+ const target = normalizeHex(color);
96
+ if (!target) return null;
97
+
98
+ for (const name of Object.keys(DS_PALETTE) as PaletteName[]) {
99
+ for (const step of PALETTE_STEPS) {
100
+ if (normalizeHex(DS_PALETTE[name][step]) === target) {
101
+ return `${name}-${step}`;
102
+ }
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ /** Palette token when mapped; otherwise the raw value (hex / rgba). Used by the catalog. */
109
+ export function colorValueLabel(color: string): string {
110
+ return paletteTokenForColor(color) ?? color;
111
+ }