@krapwoo/ds-viewer 0.4.9 → 0.6.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 (33) hide show
  1. package/README.md +74 -8
  2. package/dist/cli/doctor.js +92 -5
  3. package/dist/cli/explain.js +11 -20
  4. package/dist/cli/main.js +11 -2
  5. package/dist/cli/render.js +338 -0
  6. package/dist/cli/staticPage.js +27 -4
  7. package/dist/native/catalog/comparison.js +69 -30
  8. package/dist/native/catalog/tokens.js +8 -6
  9. package/native/catalog/CatalogButton.tsx +47 -0
  10. package/native/catalog/CatalogShell.tsx +29 -11
  11. package/native/catalog/CatalogSidebar.tsx +18 -6
  12. package/native/catalog/ComparisonGrid.tsx +9 -8
  13. package/native/catalog/ComparisonGroups.tsx +6 -5
  14. package/native/catalog/ComparisonList.tsx +12 -3
  15. package/native/catalog/OverlayDemo.tsx +52 -0
  16. package/native/catalog/PhoneScreen.tsx +51 -0
  17. package/native/catalog/SectionBlock.tsx +16 -11
  18. package/native/catalog/SpecimenSurface.tsx +4 -4
  19. package/native/catalog/TokenLayouts.tsx +14 -3
  20. package/native/catalog/UpdatePanel.tsx +4 -4
  21. package/native/catalog/comparison.ts +77 -33
  22. package/native/catalog/index.ts +4 -1
  23. package/native/catalog/overlayDemoState.ts +8 -0
  24. package/native/catalog/pageApi.ts +3 -0
  25. package/native/catalog/specimenSurfaceStyle.ts +11 -0
  26. package/native/catalog/tokens.ts +8 -6
  27. package/native/catalog/types.ts +10 -3
  28. package/package.json +1 -1
  29. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +38 -48
  30. package/starter-kit/components/Dialog/Dialog.catalog.tsx +16 -16
  31. package/starter-kit/components/Toast/Toast.catalog.tsx +9 -10
  32. package/starter-kit/pages/ReportIssue.catalog.tsx +17 -13
  33. package/starter-kit/pages/SavedTrips.catalog.tsx +114 -119
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
2
  import { View, Text, ScrollView, StyleSheet } from 'react-native';
3
- import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
3
+ import { defineCatalogPage, PhoneFrame, PhoneScreen } from '@krapwoo/ds-viewer';
4
4
  import { Avatar } from '../components/Avatar';
5
5
  import { Badge } from '../components/Badge';
6
6
  import { Button } from '../components/Button';
@@ -23,13 +23,6 @@ import { Icon } from '../icons/Icon.native';
23
23
  import { DS_ICON_SIZE, DS_MOTION_DURATION, DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../tokens';
24
24
 
25
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
- // Takes the space between the TopNav and the Dock and scrolls (`minHeight: 0` lets a flex child
31
- // shrink below its content on the web).
32
- savedTripsScroll: { flex: 1, minHeight: 0 },
33
26
  // The muted area between TopNav and Dock takes the remaining height, so the list scrolls there.
34
27
  savedTripsBody: { flex: 1, minHeight: 0 },
35
28
  savedTripsScrollContent: { padding: DS_SPACING[800], gap: DS_SPACING[600] },
@@ -40,7 +33,7 @@ const styles = StyleSheet.create({
40
33
  // Floats over the top of the screen — same top/left/right inset Toast's own catalog demo uses —
41
34
  // instead of sitting inline in the flex flow and pushing the Dock down. A column with a small
42
35
  // gap, since back-to-back removals stack one toast per removal (newest on top).
43
- savedTripsToastOverlay: { position: 'absolute', top: DS_SPACING[800], left: DS_SPACING[600], right: DS_SPACING[600], zIndex: 20, gap: DS_SPACING[300] },
36
+ savedTripsToastOverlay: { marginTop: DS_SPACING[800], marginHorizontal: DS_SPACING[600], gap: DS_SPACING[300] },
44
37
  // Map mode's stand-in — a real map isn't in scope for this recipe, just enough to show the
45
38
  // SegmentedToggle actually switches the screen's content, not just its own thumb.
46
39
  savedTripsMapPlaceholder: { alignItems: 'center', justifyContent: 'center', gap: DS_SPACING[400], paddingVertical: DS_SPACING[2400] },
@@ -111,130 +104,132 @@ function SavedTripsDemo() {
111
104
 
112
105
  return (
113
106
  <PhoneFrame>
114
- <View style={styles.savedTripsScreen}>
115
- <TopNav
116
- title="Saved trips"
117
- trailing={
118
- <Tooltip visible={showFilterTip} label="Filter by mode" placement="bottom" align="right">
119
- <Button
120
- variant="secondary"
121
- size="small"
122
- showIcon
123
- showLabel={false}
124
- iconName="menu"
125
- accessibilityLabel="Filters"
126
- onPress={() => setShowFilterTip((v) => !v)}
107
+ <PhoneScreen
108
+ header={
109
+ <TopNav
110
+ title="Saved trips"
111
+ trailing={
112
+ <Tooltip visible={showFilterTip} label="Filter by mode" placement="bottom" align="right">
113
+ <Button
114
+ variant="secondary"
115
+ size="small"
116
+ showIcon
117
+ showLabel={false}
118
+ iconName="menu"
119
+ accessibilityLabel="Filters"
120
+ onPress={() => setShowFilterTip((v) => !v)}
121
+ />
122
+ </Tooltip>
123
+ }
124
+ />
125
+ }
126
+ bodyWrapper={(body) => <Surface tone="muted" style={styles.savedTripsBody}>{body}</Surface>}
127
+ contentContainerStyle={styles.savedTripsScrollContent}
128
+ // Toasts float over the top of the screen instead of pushing the Dock down. One per
129
+ // pending removal, newest on top; each stays mounted (visible: false) through its exit.
130
+ floating={
131
+ <View style={styles.savedTripsToastOverlay} pointerEvents="box-none">
132
+ {toasts.map((t) => (
133
+ <Toast
134
+ key={t.key}
135
+ visible={t.visible}
136
+ message={`${t.trip.title} removed`}
137
+ action={{ label: 'Undo', onPress: () => undoRemove(t.key, t.trip) }}
127
138
  />
128
- </Tooltip>
129
- }
139
+ ))}
140
+ </View>
141
+ }
142
+ footer={
143
+ <Dock>
144
+ <Button label="Plan a new trip" onPress={() => {}} />
145
+ </Dock>
146
+ }
147
+ >
148
+ <SearchField value={query} onChangeText={setQuery} placeholder="Search stations" />
149
+ <SegmentedToggle
150
+ value={viewMode}
151
+ onChange={setViewMode}
152
+ options={[
153
+ { value: 'list', label: 'List', iconName: 'menu' },
154
+ { value: 'map', label: 'Map', iconName: 'map' },
155
+ ]}
130
156
  />
131
- <Surface tone="muted" style={styles.savedTripsBody}>
132
- <ScrollView style={styles.savedTripsScroll} contentContainerStyle={styles.savedTripsScrollContent}>
133
- <SearchField value={query} onChangeText={setQuery} placeholder="Search stations" />
134
- <SegmentedToggle
135
- value={viewMode}
136
- onChange={setViewMode}
157
+ {viewMode === 'map' ? (
158
+ <View style={styles.savedTripsMapPlaceholder}>
159
+ <Icon name="map" size={DS_ICON_SIZE.xl} color={DS_SEMANTIC.text.muted} />
160
+ <Text style={styles.savedTripsMapPlaceholderText}>Map view</Text>
161
+ </View>
162
+ ) : (
163
+ <>
164
+ <UnderlineTabs
165
+ value={tab}
166
+ onChange={setTab}
137
167
  options={[
138
- { value: 'list', label: 'List', iconName: 'menu' },
139
- { value: 'map', label: 'Map', iconName: 'map' },
168
+ { value: 'all', label: 'All' },
169
+ { value: 'nearby', label: 'Nearby' },
170
+ { value: 'favorites', label: 'Favorites' },
140
171
  ]}
141
172
  />
142
- {viewMode === 'map' ? (
143
- <View style={styles.savedTripsMapPlaceholder}>
144
- <Icon name="map" size={DS_ICON_SIZE.xl} color={DS_SEMANTIC.text.muted} />
145
- <Text style={styles.savedTripsMapPlaceholderText}>Map view</Text>
146
- </View>
173
+ {tab === 'favorites' ? (
174
+ <EmptyState
175
+ iconName="waypoints"
176
+ title="No favorites yet"
177
+ description="Star a trip to see it here."
178
+ action={{ label: 'Browse trips', onPress: () => setTab('all') }}
179
+ secondaryAction={{ label: 'Not now', onPress: () => {} }}
180
+ />
147
181
  ) : (
148
182
  <>
149
- <UnderlineTabs
150
- value={tab}
151
- onChange={setTab}
152
- options={[
153
- { value: 'all', label: 'All' },
154
- { value: 'nearby', label: 'Nearby' },
155
- { value: 'favorites', label: 'Favorites' },
156
- ]}
183
+ <PillRow
184
+ pills={pills.map((p) => ({ ...p, onPress: () => setPills((prev) => prev.map((x) => ({ ...x, variant: x.id === p.id ? 'selected' : 'not_selected' }))) }))}
185
+ showAddPill={false}
157
186
  />
158
- {tab === 'favorites' ? (
187
+ <View style={styles.savedTripsInlineRow}>
188
+ <Loading size={14} />
189
+ <Text style={styles.savedTripsLoadingText}>Updating arrival times…</Text>
190
+ </View>
191
+ {/* Dropdown's own `label` prop, not a hand-rolled Text above it — the component
192
+ already owns the labeled-field pattern (PillRow, which has no label prop, is
193
+ the case where an external label is legitimately the only option). */}
194
+ <Dropdown label="Sort by" value={sortBy} onChange={setSortBy} options={SAVED_TRIPS_SORT_OPTIONS} />
195
+ <SectionHeader title="Nearby stations" />
196
+ {trips.length > 0 ? (
197
+ <List>
198
+ {trips.map((trip) => (
199
+ <ListItem
200
+ key={trip.id}
201
+ title={trip.title}
202
+ subtitle={trip.subtitle}
203
+ leading={<Avatar iconName={trip.iconName} size={40} />}
204
+ trailing={
205
+ <View style={styles.savedTripsInlineRow}>
206
+ <Badge variant={trip.badgeVariant} label={trip.badgeLabel} />
207
+ <Button
208
+ variant="ghost"
209
+ size="small"
210
+ showIcon
211
+ showLabel={false}
212
+ iconName="clear"
213
+ accessibilityLabel={`Remove ${trip.title}`}
214
+ onPress={() => removeTrip(trip.id)}
215
+ />
216
+ </View>
217
+ }
218
+ />
219
+ ))}
220
+ </List>
221
+ ) : (
159
222
  <EmptyState
160
223
  iconName="waypoints"
161
- title="No favorites yet"
162
- description="Star a trip to see it here."
163
- action={{ label: 'Browse trips', onPress: () => setTab('all') }}
164
- secondaryAction={{ label: 'Not now', onPress: () => {} }}
224
+ title="No nearby trips"
225
+ description="Removed trips you save will show up here again."
165
226
  />
166
- ) : (
167
- <>
168
- <PillRow
169
- pills={pills.map((p) => ({ ...p, onPress: () => setPills((prev) => prev.map((x) => ({ ...x, variant: x.id === p.id ? 'selected' : 'not_selected' }))) }))}
170
- showAddPill={false}
171
- />
172
- <View style={styles.savedTripsInlineRow}>
173
- <Loading size={14} />
174
- <Text style={styles.savedTripsLoadingText}>Updating arrival times…</Text>
175
- </View>
176
- {/* Dropdown's own `label` prop, not a hand-rolled Text above it — the component
177
- already owns the labeled-field pattern (PillRow, which has no label prop, is
178
- the case where an external label is legitimately the only option). */}
179
- <Dropdown label="Sort by" value={sortBy} onChange={setSortBy} options={SAVED_TRIPS_SORT_OPTIONS} />
180
- <SectionHeader title="Nearby stations" />
181
- {trips.length > 0 ? (
182
- <List>
183
- {trips.map((trip) => (
184
- <ListItem
185
- key={trip.id}
186
- title={trip.title}
187
- subtitle={trip.subtitle}
188
- leading={<Avatar iconName={trip.iconName} size={40} />}
189
- trailing={
190
- <View style={styles.savedTripsInlineRow}>
191
- <Badge variant={trip.badgeVariant} label={trip.badgeLabel} />
192
- <Button
193
- variant="ghost"
194
- size="small"
195
- showIcon
196
- showLabel={false}
197
- iconName="clear"
198
- accessibilityLabel={`Remove ${trip.title}`}
199
- onPress={() => removeTrip(trip.id)}
200
- />
201
- </View>
202
- }
203
- />
204
- ))}
205
- </List>
206
- ) : (
207
- <EmptyState
208
- iconName="waypoints"
209
- title="No nearby trips"
210
- description="Removed trips you save will show up here again."
211
- />
212
- )}
213
- </>
214
227
  )}
215
228
  </>
216
229
  )}
217
- </ScrollView>
218
- </Surface>
219
- {/* Absolutely positioned over the top of the screen (not inline in the flex flow — a real
220
- toast floats over content, it doesn't push the Dock down) — same placement Toast's own
221
- catalog demo uses. One Toast per pending removal, stacked newest-on-top; each entry
222
- stays in the array (with `visible: false`) through its own exit animation instead of
223
- being yanked out of the tree mid-motion. */}
224
- <View style={styles.savedTripsToastOverlay} pointerEvents="box-none">
225
- {toasts.map((t) => (
226
- <Toast
227
- key={t.key}
228
- visible={t.visible}
229
- message={`${t.trip.title} removed`}
230
- action={{ label: 'Undo', onPress: () => undoRemove(t.key, t.trip) }}
231
- />
232
- ))}
233
- </View>
234
- <Dock>
235
- <Button label="Plan a new trip" onPress={() => {}} />
236
- </Dock>
237
- </View>
230
+ </>
231
+ )}
232
+ </PhoneScreen>
238
233
  </PhoneFrame>
239
234
  );
240
235
  }