@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.
- package/README.md +74 -8
- package/dist/cli/doctor.js +92 -5
- package/dist/cli/explain.js +11 -20
- package/dist/cli/main.js +11 -2
- package/dist/cli/render.js +338 -0
- package/dist/cli/staticPage.js +27 -4
- package/dist/native/catalog/comparison.js +69 -30
- package/dist/native/catalog/tokens.js +8 -6
- package/native/catalog/CatalogButton.tsx +47 -0
- package/native/catalog/CatalogShell.tsx +29 -11
- package/native/catalog/CatalogSidebar.tsx +18 -6
- package/native/catalog/ComparisonGrid.tsx +9 -8
- package/native/catalog/ComparisonGroups.tsx +6 -5
- package/native/catalog/ComparisonList.tsx +12 -3
- package/native/catalog/OverlayDemo.tsx +52 -0
- package/native/catalog/PhoneScreen.tsx +51 -0
- package/native/catalog/SectionBlock.tsx +16 -11
- package/native/catalog/SpecimenSurface.tsx +4 -4
- package/native/catalog/TokenLayouts.tsx +14 -3
- package/native/catalog/UpdatePanel.tsx +4 -4
- package/native/catalog/comparison.ts +77 -33
- package/native/catalog/index.ts +4 -1
- package/native/catalog/overlayDemoState.ts +8 -0
- package/native/catalog/pageApi.ts +3 -0
- package/native/catalog/specimenSurfaceStyle.ts +11 -0
- package/native/catalog/tokens.ts +8 -6
- package/native/catalog/types.ts +10 -3
- package/package.json +1 -1
- package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +38 -48
- package/starter-kit/components/Dialog/Dialog.catalog.tsx +16 -16
- package/starter-kit/components/Toast/Toast.catalog.tsx +9 -10
- package/starter-kit/pages/ReportIssue.catalog.tsx +17 -13
- 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: {
|
|
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
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
<
|
|
133
|
-
<
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
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: '
|
|
139
|
-
{ value: '
|
|
168
|
+
{ value: 'all', label: 'All' },
|
|
169
|
+
{ value: 'nearby', label: 'Nearby' },
|
|
170
|
+
{ value: 'favorites', label: 'Favorites' },
|
|
140
171
|
]}
|
|
141
172
|
/>
|
|
142
|
-
{
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
{
|
|
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
|
|
162
|
-
description="
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
}
|