@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
@@ -17,7 +17,6 @@ import type React from 'react';
17
17
  * content width is a 1280px viewport minus the 264px sidebar and 32px side padding. */
18
18
  export const MATRIX_LAYOUT = {
19
19
  rowHeaderWidth: 120,
20
- columnMaxWidth: 402,
21
20
  cellPadding: 16,
22
21
  rowMinHeight: 150,
23
22
  laptopContentWidth: 952,
@@ -43,8 +42,9 @@ export const REFERENCE_MIN_COLUMN_WIDTH = 280;
43
42
  * use). */
44
43
  export const REFERENCE_COLUMN_GAP = 32;
45
44
 
46
- /** Smallest column or cell width per specimen size. Wide specimens are fixed at phone width. */
47
- export const COLUMN_MIN_WIDTH: Record<SpecimenSize, number> = { compact: 160, regular: 240, wide: 402 };
45
+ /** Smallest column or cell width per specimen size. `wide` is phone width as a minimum, not a
46
+ * maximum: cells share their container equally above it. */
47
+ export const COLUMN_MIN_WIDTH: Record<SpecimenSize, number> = { compact: 160, regular: 240, wide: 402, page: 512 };
48
48
 
49
49
  /** `Array.isArray`'s built-in type guard narrows to a mutable `any[]`, which a `readonly T[]`
50
50
  * union member is never assignable to — so the `else` branch of `Array.isArray(axis) ? ... :
@@ -106,12 +106,11 @@ export function gridColumnLimit(size: SpecimenSize): number {
106
106
  return Math.floor((MATRIX_LAYOUT.laptopContentWidth - MATRIX_LAYOUT.rowHeaderWidth) / COLUMN_MIN_WIDTH[size]);
107
107
  }
108
108
 
109
- export function gridWidthBounds(columnCount: number, size: SpecimenSize): { minWidth: number; maxWidth: number } {
110
- const n = Math.max(0, columnCount);
111
- return {
112
- minWidth: MATRIX_LAYOUT.rowHeaderWidth + n * COLUMN_MIN_WIDTH[size],
113
- maxWidth: MATRIX_LAYOUT.rowHeaderWidth + n * MATRIX_LAYOUT.columnMaxWidth,
114
- };
109
+ /** The narrowest a grid card may be: the row header plus every column at its minimum. A card wider
110
+ * than its container scrolls inside itself; above this width the columns share the container
111
+ * equally, because the phone-width cap belongs to the specimen inside a cell, not to the column. */
112
+ export function gridMinWidth(columnCount: number, size: SpecimenSize): number {
113
+ return MATRIX_LAYOUT.rowHeaderWidth + Math.max(0, columnCount) * COLUMN_MIN_WIDTH[size];
115
114
  }
116
115
 
117
116
  export interface ListGeometry {
@@ -123,7 +122,8 @@ export interface ListGeometry {
123
122
  }
124
123
 
125
124
  /** Balanced wrapping for a one-axis list inside a single bordered card (1px outer border). Each
126
- * cell's 1px divider sits inside its own width, so no cell is ever wider than 402px. Uses as few
125
+ * cell's 1px divider sits inside its own width. Cells share the container equally — phone width
126
+ * caps the specimen inside a cell, not the cell. Uses as few
127
127
  * rows as fit, then spreads items evenly across them so a short last row leaves as little empty
128
128
  * space as possible. `fillers` blank cells complete the last row. `columnCap` (from
129
129
  * `VariantSlot.maxColumns`) only ever lowers the natural, width-based column count — a narrow
@@ -139,7 +139,7 @@ export function listGeometry(itemCount: number, availableWidth: number, size: Sp
139
139
  const rows = Math.ceil(itemCount / maxColumns);
140
140
  const columns = Math.ceil(itemCount / rows);
141
141
  const fitted = Math.floor(inner / columns);
142
- const cellWidth = size === 'wide' ? MATRIX_LAYOUT.columnMaxWidth : Math.max(min, Math.min(MATRIX_LAYOUT.columnMaxWidth, fitted));
142
+ const cellWidth = Math.max(min, fitted);
143
143
  return {
144
144
  columns,
145
145
  rows,
@@ -253,7 +253,7 @@ export type PresentationBlock =
253
253
  | { kind: 'grid'; title: string; size: SpecimenSize; comparison: ComparisonDef }
254
254
  | { kind: 'grouped'; title: string; size: SpecimenSize; groups: ListGroup[] }
255
255
  | { kind: 'list'; title: string; size: SpecimenSize; items: ListItem[]; maxColumns?: 1 | 2 | 3 | 4 | 5 }
256
- | { kind: 'preview'; title: string; widths: PreviewWidths }
256
+ | { kind: 'preview'; title: string; widths: PreviewWidths; layout?: 'frames' | 'table' }
257
257
  | { kind: 'tokenSections'; title: string; sections: TokenSection[]; columns: 1 | 2 | 3 }
258
258
  | { kind: 'empty'; title: string; message: string };
259
259
 
@@ -278,10 +278,11 @@ function slotColumnCap(slot: VariantSlot): { maxColumns?: 1 | 2 | 3 | 4 | 5 } {
278
278
  return slot.itemsFill ? { maxColumns: FILLED_SLOT_MAX_COLUMNS } : {};
279
279
  }
280
280
 
281
- function slotItems(slot: VariantSlot, from: 'variants' | 'states'): ListItem[] {
281
+ function slotItems(slot: VariantSlot, from: 'variants' | 'states', fillByDefault = false): ListItem[] {
282
282
  // Nullish, not `||`: an item's own explicit `fill: false` must override an inherited
283
283
  // `itemsFill: true`, which `false || slot.itemsFill` would silently discard.
284
- return slot.items.map((item) => ({ key: item.key, slot: from, label: item.name, node: item.node, fill: item.fill ?? slot.itemsFill, surface: item.surface, align: item.align }));
284
+ // Page-width examples (layouts wider than a phone) fill their cell unless they say otherwise.
285
+ return slot.items.map((item) => ({ key: item.key, slot: from, label: item.name, node: item.node, fill: item.fill ?? slot.itemsFill ?? (fillByDefault || undefined), surface: item.surface, align: item.align }));
285
286
  }
286
287
 
287
288
  /** Preview widths, each capped at phone width. 'full' is kept for catalog chrome and token pages. */
@@ -350,11 +351,11 @@ export function presentationBlocks<TId extends string>(def: SectionDef<TId>, opt
350
351
  kind: 'list',
351
352
  title: 'Variants',
352
353
  size: slotSize(def, def.variants),
353
- items: slotItems(def.variants, 'variants'),
354
+ items: slotItems(def.variants, 'variants', def.specimenSize === 'page'),
354
355
  ...slotColumnCap(def.variants),
355
356
  });
356
357
  } else if (def.render) {
357
- blocks.push({ kind: 'preview', title: 'Preview', widths: previewWidths(def.previewWidths, options.defaultPreviewWidths) });
358
+ blocks.push({ kind: 'preview', title: 'Preview', widths: previewWidths(def.previewWidths, options.defaultPreviewWidths), ...(def.previewLayout ? { layout: def.previewLayout } : {}) });
358
359
  }
359
360
  }
360
361
 
@@ -364,7 +365,7 @@ export function presentationBlocks<TId extends string>(def: SectionDef<TId>, opt
364
365
  kind: 'list',
365
366
  title: secondary ? 'Other configurations' : 'States / configurations',
366
367
  size: slotSize(def, states),
367
- items: slotItems(states, 'states'),
368
+ items: slotItems(states, 'states', def.specimenSize === 'page'),
368
369
  ...slotColumnCap(states),
369
370
  });
370
371
  }
@@ -377,9 +378,11 @@ export function presentationBlocks<TId extends string>(def: SectionDef<TId>, opt
377
378
 
378
379
  export interface PlacementBlock {
379
380
  kind: PresentationBlock['kind'];
380
- /** Rendered width of the block's card (lists hug their columns; everything else fills). */
381
- width?: number;
382
- /** Measured height of the whole block, label included, while stacked. */
381
+ /** The narrowest this block renders without its cells dropping below their minimum, from
382
+ * `blockMinWidth`. A block that can only fill a whole row (a 'full' preview, token sections)
383
+ * reports `Infinity` and never shares one. */
384
+ minWidth?: number;
385
+ /** Measured height of the whole block, label included, while stacked at the full width. */
383
386
  height: number;
384
387
  /** For a list: item count, specimen size, and any explicit column cap, so its side-by-side
385
388
  * height can be predicted from the same geometry the rendered list uses. */
@@ -388,20 +391,61 @@ export interface PlacementBlock {
388
391
  maxColumns?: 1 | 2 | 3 | 4 | 5;
389
392
  }
390
393
 
391
- /** Side by side or stacked, for a page with exactly two blocks. Tries both and keeps the shorter;
392
- * stays stacked unless side by side saves at least PLACEMENT_MIN_SAVING. Only a list can move
393
- * beside a first block that hugs its content, and only when its cells keep their minimum width. */
394
+ /** The narrowest a block renders at. Every card fills the column it is given, so this is a floor,
395
+ * not the width it will take: below it a grid scrolls inside itself and a list would push its
396
+ * cells under their minimum. `Infinity` means the block can only have a whole row. */
397
+ /** What a preview card adds around its frames: `padding` (CATALOG_SPACE.xl) and a 1px border on
398
+ * each side. `blockMinWidth` must add it, or a preview is offered a half-row that fits its frame
399
+ * only after the card has taken its share, and the frame silently renders under phone width.
400
+ * SectionBlock's `previewCard` style is built from these, so the two cannot drift. */
401
+ export const PREVIEW_CARD_CHROME = { padding: 24, border: 1 } as const;
402
+ /** The same for a `previewLayout: 'table'` card: no outer padding, a 1px border, and each cell
403
+ * carries `cellPadding` on both sides of its frame. */
404
+ export const TABLE_PREVIEW_CHROME = { padding: MATRIX_LAYOUT.cellPadding, border: 1 } as const;
405
+ /** Total horizontal chrome for one preview frame in either card. */
406
+ export function previewChrome(layout: 'frames' | 'table' | undefined): number {
407
+ const chrome = layout === 'table' ? TABLE_PREVIEW_CHROME : PREVIEW_CARD_CHROME;
408
+ return 2 * chrome.padding + 2 * chrome.border;
409
+ }
410
+
411
+ export function blockMinWidth(block: PresentationBlock): number {
412
+ switch (block.kind) {
413
+ case 'grid':
414
+ return gridMinWidth(axisItems(block.comparison.columns).length, block.size);
415
+ case 'grouped':
416
+ return MATRIX_LAYOUT.rowHeaderWidth + Math.max(1, ...block.groups.map((g) => g.items.length)) * COLUMN_MIN_WIDTH[block.size] + 2;
417
+ case 'list':
418
+ // One column: a list always wraps further rather than going below its size's minimum.
419
+ return COLUMN_MIN_WIDTH[block.size] + 2;
420
+ case 'preview':
421
+ // A 'full' preview is the row. Otherwise the *widest* frame sets the minimum: the frames wrap,
422
+ // so each one sits on its own row and `maxWidth: '100%'` would squeeze any frame wider than
423
+ // the card. The card's own chrome counts too — a frame keeps its width only if the row also
424
+ // covers the padding and border around it.
425
+ return block.widths === 'full' ? Infinity : Math.max(...block.widths) + previewChrome(block.layout);
426
+ default:
427
+ return Infinity;
428
+ }
429
+ }
430
+
431
+ /** The height a block takes at `width`, from the height measured while stacked at `available`.
432
+ * Only a list reflows: its rows change with the width, and every row is the same height. */
433
+ function heightAtWidth(block: PlacementBlock, width: number, available: number): number {
434
+ if (block.kind !== 'list' || !block.itemCount || !block.size) return block.height;
435
+ const stackedRows = listGeometry(block.itemCount, available, block.size, block.maxColumns).rows;
436
+ const rows = listGeometry(block.itemCount, width, block.size, block.maxColumns).rows;
437
+ return (block.height / Math.max(1, stackedRows)) * rows;
438
+ }
439
+
440
+ /** Side by side or stacked, for a page with exactly two blocks. Side by side splits the row into
441
+ * two equal columns and each block fills its own, so it is only possible when both blocks keep
442
+ * their minimum width in half a row; it is only taken when it saves at least PLACEMENT_MIN_SAVING
443
+ * against stacking, measuring each block at the width it would actually have. */
394
444
  export function choosePlacement({ available, gap, first, second }: { available: number; gap: number; first: PlacementBlock; second: PlacementBlock }): 'side' | 'stacked' {
395
- if (second.kind !== 'list' || !second.itemCount || !second.size) return 'stacked';
396
- const firstWidth = first.width ?? available;
397
- if (firstWidth > available * 0.7) return 'stacked';
398
- const room = available - firstWidth - gap;
399
- const minCell = second.size === 'wide' ? MATRIX_LAYOUT.columnMaxWidth : COLUMN_MIN_WIDTH[second.size];
400
- if (room < minCell + 2) return 'stacked';
401
- const stackedRows = listGeometry(second.itemCount, available, second.size, second.maxColumns).rows;
402
- const sideRows = listGeometry(second.itemCount, room, second.size, second.maxColumns).rows;
403
- const rowHeight = second.height / Math.max(1, stackedRows);
404
- const sideHeight = Math.max(first.height, sideRows * rowHeight);
445
+ const half = Math.floor((available - gap) / 2);
446
+ if (half <= 0) return 'stacked';
447
+ if ((first.minWidth ?? Infinity) > half || (second.minWidth ?? Infinity) > half) return 'stacked';
448
+ const sideHeight = Math.max(heightAtWidth(first, half, available), heightAtWidth(second, half, available));
405
449
  const stackedHeight = first.height + gap + second.height;
406
450
  return sideHeight <= stackedHeight - PLACEMENT_MIN_SAVING ? 'side' : 'stacked';
407
451
  }
@@ -31,6 +31,9 @@ export { TokenSections, TokenGrid, TokenTile } from './TokenLayouts';
31
31
  export { DividedStack } from './DividedStack';
32
32
  export { Swatch } from './Swatch';
33
33
  export { PhoneFrame } from './PhoneFrame';
34
+ export { PhoneScreen } from './PhoneScreen';
35
+ export { OverlayDemo } from './OverlayDemo';
36
+ export { CatalogButton } from './CatalogButton';
34
37
  export { BoundedOverlayViewport } from './BoundedOverlayViewport';
35
38
  export type { BoundedOverlayViewportProps } from './BoundedOverlayViewport';
36
39
  export { comparisonCellItemKey } from './overlayViewport';
@@ -47,7 +50,7 @@ export type {
47
50
  } from './types';
48
51
  export { UpdatePanel } from './UpdatePanel';
49
52
  export { UPDATE_PAGE_ID } from './catalogNavigation';
50
- export type { UpdateNotice } from './types';
53
+ export type { UpdateNotice, VersionStatus, AutoCheckSource } from './types';
51
54
  export type { ListGroup, ListItem } from './comparison';
52
55
  export {
53
56
  CATALOG_TYPE,
@@ -0,0 +1,8 @@
1
+ /** What `OverlayDemo` shows for a given open state. Kept free of runtime imports for `node --test`. */
2
+ export function overlayTrigger(
3
+ open: boolean,
4
+ options: { triggerLabel: string | ((open: boolean) => string); keepTrigger?: boolean },
5
+ ): { visible: boolean; label: string } {
6
+ const label = typeof options.triggerLabel === 'function' ? options.triggerLabel(open) : options.triggerLabel;
7
+ return { visible: !open || Boolean(options.keepTrigger), label };
8
+ }
@@ -37,6 +37,8 @@ export interface CatalogPageInput<TId extends string = string> {
37
37
  composedOf?: ComposedOfEntry[];
38
38
  /** See `SectionDef.intentionalStaticPreview`. */
39
39
  intentionalStaticPreview?: { reason: string };
40
+ /** See `SectionDef.internalProps`. */
41
+ internalProps?: Record<string, string>;
40
42
  /** Export name this page documents; defaults to `id` for component pages. */
41
43
  component?: string;
42
44
  /** Sidebar group label. */
@@ -175,6 +177,7 @@ export function buildCatalogSections<TId extends string>(
175
177
  previewLayout: page.previewLayout,
176
178
  hide: component ? page.hide : { ...page.hide, props: true },
177
179
  composedOf: page.composedOf,
180
+ internalProps: page.internalProps,
178
181
  intentionalStaticPreview: page.intentionalStaticPreview,
179
182
  };
180
183
  });
@@ -142,3 +142,14 @@ export function visibleBoxes(root: LayoutBox): { left: number; right: number; pa
142
142
  walk(root, true);
143
143
  return out;
144
144
  }
145
+
146
+ /** The widest a filled example gets: a phone's content width (402 minus 16 px padding each side).
147
+ * Fields, dropdowns and full-width buttons fill a phone's width in real use, so a wider cell would
148
+ * misrepresent them. Examples that aren't filled keep their own width. */
149
+ export const FILLED_CONTENT_MAX_WIDTH = 370;
150
+
151
+ /** The wrapper style for a filled example: full width up to `FILLED_CONTENT_MAX_WIDTH`, centred.
152
+ * `capped: false` (page-width examples) fills the whole cell. */
153
+ export function filledContentStyle(capped = true): { width: '100%'; maxWidth?: number; alignSelf: 'center' } {
154
+ return capped ? { width: '100%', maxWidth: FILLED_CONTENT_MAX_WIDTH, alignSelf: 'center' } : { width: '100%', alignSelf: 'center' };
155
+ }
@@ -11,15 +11,17 @@
11
11
  * `textMuted` stays #666666 because the references' #777777 fails WCAG AA contrast.
12
12
  */
13
13
 
14
+ /** Owner decision 2026-10-11: only 10, 12, 14, 16, 18, 24, 28, 32 and 40. Named roles share a
15
+ * step (tableHeader and panelHeading are 12, brand is 18). */
14
16
  export const CATALOG_TYPE = {
15
17
  xs: 10,
16
- tableHeader: 11,
18
+ tableHeader: 12,
17
19
  sm: 12,
18
- panelHeading: 13,
20
+ panelHeading: 12,
19
21
  md: 14,
20
22
  lg: 16,
21
- brand: 17,
22
- xl: 20,
23
+ brand: 18,
24
+ xl: 18,
23
25
  '2xl': 24,
24
26
  pageTitle: 28,
25
27
  '3xl': 32,
@@ -82,8 +84,8 @@ export const CATALOG_TYPE_USE: Record<keyof typeof CATALOG_TYPE, string> = {
82
84
  md: 'Default body text: descriptions, nav links, grid row headers, quick-reference rows, the filter field.',
83
85
  lg: 'Pager arrows and the filter field\'s clear (×) glyph.',
84
86
  brand: 'The app name at the top of the sidebar.',
85
- xl: 'Reserved — no current consumer.',
86
- '2xl': 'Reserved — no current consumer.',
87
+ xl: "The major-update banner's close (×) glyph.",
88
+ '2xl': "The update page's version numbers and the arrow between them.",
87
89
  pageTitle: 'The selected page\'s title.',
88
90
  '3xl': 'Reserved — no current consumer.',
89
91
  '4xl': 'Reserved — no current consumer.',
@@ -75,9 +75,10 @@ export interface VariantSlot {
75
75
  items: VariantExample[];
76
76
  }
77
77
 
78
- /** Width class for a page's specimens: compact (min 160px), regular (min 240px), or wide (fixed
79
- * 402px, phone width). Sets grid column and list cell widths. */
80
- export type SpecimenSize = 'compact' | 'regular' | 'wide';
78
+ /** Width class for a page's specimens: compact (min 160px), regular (min 240px), wide (min 402px,
79
+ * phone width), or page (min 512px) for components wider than a phone, such as a whole catalog
80
+ * page. Each is a minimum: cells share the row they are given. Sets grid column and cell widths. */
81
+ export type SpecimenSize = 'compact' | 'regular' | 'wide' | 'page';
81
82
 
82
83
  /** Widths for a component preview: one or more phone widths (each capped at 402px), or 'full' for
83
84
  * content that is not a phone component (catalog chrome, token galleries). */
@@ -267,6 +268,12 @@ export interface SectionDef<TId extends string = string> {
267
268
  * sentence explaining why (not free-form prose) — e.g. "Selection is already demonstrated by X's
268
269
  * working preview." Omit when no such declaration applies; the advisory then runs normally. */
269
270
  intentionalStaticPreview?: { reason: string };
271
+ /** Props the reader never chooses: plumbing a parent passes (a layout size, an internal surface
272
+ * kind). `cli/doctor.ts`'s `option-not-covered` advisory then skips them, instead of asking for
273
+ * an example of every option of a prop nobody picks by hand. Each entry needs a short reason,
274
+ * so the exemption is a visible declaration in the page rather than a silent gap — write an
275
+ * example instead whenever the reader really does choose the value. */
276
+ internalProps?: Record<string, string>;
270
277
  }
271
278
 
272
279
  /** A labeled group of section ids in the sidebar (e.g. "Components" vs "Tokens"). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krapwoo/ds-viewer",
3
- "version": "0.4.9",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A browsable, always-current catalog for an Expo / React Native design system.",
@@ -1,6 +1,6 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import { View, Text, StyleSheet } from 'react-native';
3
- import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
3
+ import { defineCatalogPage, OverlayDemo } from '@krapwoo/ds-viewer';
4
4
  import { Button } from '../Button';
5
5
  import { Dock } from '../Dock';
6
6
  import { TopNav } from '../TopNav';
@@ -17,53 +17,43 @@ const styles = StyleSheet.create({
17
17
  });
18
18
 
19
19
  function BottomSheetDemo() {
20
- const [visible, setVisible] = useState(false);
21
20
  return (
22
- <PhoneFrame>
23
- {!visible && <Button label="Open sheet" onPress={() => setVisible(true)} />}
24
- <BottomSheet
25
- visible={visible}
26
- onDismiss={() => setVisible(false)}
27
- header={
28
- <TopNav
29
- title="Trip details"
30
- trailing={
31
- <Button
32
- variant="secondary"
33
- size="small"
34
- showIcon
35
- showLabel={false}
36
- iconName="clear"
37
- accessibilityLabel="Close"
38
- onPress={() => setVisible(false)}
39
- />
40
- }
41
- />
42
- }
43
- footer={
44
- <Dock>
45
- <Button label="Confirm" onPress={() => setVisible(false)} />
46
- </Dock>
47
- }
48
- >
49
- <View style={styles.sheetContent}>
50
- <Text style={styles.cardTitle}>Uptown & The Bronx</Text>
51
- <Text style={styles.cardBody}>Next train in 4 min · every 6–8 min.</Text>
52
- {/* Long enough to overflow the phone frame's fixed height — demonstrates the Dock footer's
53
- `elevated` shadow turning on automatically once this content area actually scrolls. */}
54
- <Text style={styles.cardBody}>Board at the front car for a faster transfer at Union Sq.</Text>
55
- <Text style={styles.cardBody}>
56
- This line runs express between 96 St and 168 St during rush hours, skipping local stops in
57
- between. Weekend service runs local along the full route, with some stations closed for
58
- planned maintenance.
59
- </Text>
60
- <Text style={styles.cardBody}>
61
- Elevators are available at 168 St, 137 St, and 125 St — check the map for accessible
62
- entrances before you travel.
63
- </Text>
64
- </View>
65
- </BottomSheet>
66
- </PhoneFrame>
21
+ <OverlayDemo triggerLabel="Open sheet">
22
+ {({ open, close }) => (
23
+ <BottomSheet
24
+ visible={open}
25
+ onDismiss={close}
26
+ header={
27
+ <TopNav
28
+ title="Trip details"
29
+ trailing={<Button variant="secondary" size="small" showIcon showLabel={false} iconName="clear" accessibilityLabel="Close" onPress={close} />}
30
+ />
31
+ }
32
+ footer={
33
+ <Dock>
34
+ <Button label="Confirm" onPress={close} />
35
+ </Dock>
36
+ }
37
+ >
38
+ <View style={styles.sheetContent}>
39
+ <Text style={styles.cardTitle}>Uptown & The Bronx</Text>
40
+ <Text style={styles.cardBody}>Next train in 4 min · every 6–8 min.</Text>
41
+ {/* Long enough to overflow the phone frame's fixed height — demonstrates the Dock footer's
42
+ `elevated` shadow turning on automatically once this content area actually scrolls. */}
43
+ <Text style={styles.cardBody}>Board at the front car for a faster transfer at Union Sq.</Text>
44
+ <Text style={styles.cardBody}>
45
+ This line runs express between 96 St and 168 St during rush hours, skipping local stops in
46
+ between. Weekend service runs local along the full route, with some stations closed for
47
+ planned maintenance.
48
+ </Text>
49
+ <Text style={styles.cardBody}>
50
+ Elevators are available at 168 St, 137 St, and 125 St — check the map for accessible
51
+ entrances before you travel.
52
+ </Text>
53
+ </View>
54
+ </BottomSheet>
55
+ )}
56
+ </OverlayDemo>
67
57
  );
68
58
  }
69
59
 
@@ -1,6 +1,6 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import { View, Text, StyleSheet } from 'react-native';
3
- import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
3
+ import { defineCatalogPage, OverlayDemo } from '@krapwoo/ds-viewer';
4
4
  import { Button } from '../Button';
5
5
  import { ButtonGroup } from '../ButtonGroup';
6
6
  import { Dialog } from './Dialog';
@@ -13,21 +13,21 @@ const styles = StyleSheet.create({
13
13
  });
14
14
 
15
15
  function DialogDemo() {
16
- const [visible, setVisible] = useState(false);
17
16
  return (
18
- <PhoneFrame>
19
- {!visible && <Button label="Open dialog" onPress={() => setVisible(true)} />}
20
- <Dialog visible={visible} onDismiss={() => setVisible(false)}>
21
- <Text style={styles.cardTitle}>Delete this trip?</Text>
22
- <Text style={styles.cardBody}>This can't be undone.</Text>
23
- <View style={styles.dialogActions}>
24
- <ButtonGroup>
25
- <Button label="Cancel" variant="tertiary" onPress={() => setVisible(false)} />
26
- <Button label="Delete" onPress={() => setVisible(false)} />
27
- </ButtonGroup>
28
- </View>
29
- </Dialog>
30
- </PhoneFrame>
17
+ <OverlayDemo triggerLabel="Open dialog">
18
+ {({ open, close }) => (
19
+ <Dialog visible={open} onDismiss={close}>
20
+ <Text style={styles.cardTitle}>Delete this trip?</Text>
21
+ <Text style={styles.cardBody}>This can't be undone.</Text>
22
+ <View style={styles.dialogActions}>
23
+ <ButtonGroup>
24
+ <Button label="Cancel" variant="tertiary" onPress={close} />
25
+ <Button label="Delete" onPress={close} />
26
+ </ButtonGroup>
27
+ </View>
28
+ </Dialog>
29
+ )}
30
+ </OverlayDemo>
31
31
  );
32
32
  }
33
33
 
@@ -1,7 +1,6 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
2
  import { View, StyleSheet } from 'react-native';
3
- import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
4
- import { Button } from '../Button';
3
+ import { defineCatalogPage, OverlayDemo } from '@krapwoo/ds-viewer';
5
4
  import { Toast } from './Toast';
6
5
  import { DS_SPACING } from '../../tokens';
7
6
 
@@ -15,14 +14,14 @@ const styles = StyleSheet.create({
15
14
  // pattern) so this demo actually exercises the slide-in-from-top/fade animation, not just the two
16
15
  // static end states.
17
16
  function ToastDemo() {
18
- const [visible, setVisible] = useState(true);
19
17
  return (
20
- <PhoneFrame>
21
- <Button label={visible ? 'Hide toast' : 'Show toast'} onPress={() => setVisible((v) => !v)} />
22
- <View style={styles.toastDemoOverlay} pointerEvents="box-none">
23
- <Toast message="Trip saved" variant="success" visible={visible} />
24
- </View>
25
- </PhoneFrame>
18
+ <OverlayDemo triggerLabel={(open) => (open ? 'Hide toast' : 'Show toast')} keepTrigger initiallyOpen>
19
+ {({ open }) => (
20
+ <View style={styles.toastDemoOverlay} pointerEvents="box-none">
21
+ <Toast message="Trip saved" variant="success" visible={open} />
22
+ </View>
23
+ )}
24
+ </OverlayDemo>
26
25
  );
27
26
  }
28
27
 
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react';
2
2
  import { View, Text, StyleSheet } from 'react-native';
3
- import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
3
+ import { defineCatalogPage, OverlayDemo } from '@krapwoo/ds-viewer';
4
4
  import { Banner } from '../components/Banner';
5
5
  import { BottomSheet } from '../components/BottomSheet';
6
6
  import { Button } from '../components/Button';
@@ -51,7 +51,6 @@ const REPORT_ISSUE_TYPE_PILLS: PillRowItem[] = [
51
51
  ];
52
52
 
53
53
  function ReportIssueDemo() {
54
- const [visible, setVisible] = useState(false);
55
54
  const [confirmDiscard, setConfirmDiscard] = useState(false);
56
55
  const [issueType, setIssueType] = useState('delay');
57
56
  const [line, setLine] = useState('4');
@@ -60,17 +59,19 @@ function ReportIssueDemo() {
60
59
  const [visibility, setVisibility] = useState('public');
61
60
  const [notify, setNotify] = useState(true);
62
61
 
63
- const requestClose = () => setConfirmDiscard(true);
64
- const discard = () => {
65
- setConfirmDiscard(false);
66
- setVisible(false);
67
- };
68
-
69
62
  return (
70
- <PhoneFrame>
71
- {!visible && <Button label="Report an issue" onPress={() => setVisible(true)} />}
63
+ <OverlayDemo triggerLabel="Report an issue">
64
+ {({ open, close }) => {
65
+ // Closing asks first; discarding confirms and closes the sheet.
66
+ const requestClose = () => setConfirmDiscard(true);
67
+ const discard = () => {
68
+ setConfirmDiscard(false);
69
+ close();
70
+ };
71
+ return (
72
+ <>
72
73
  <BottomSheet
73
- visible={visible}
74
+ visible={open}
74
75
  onDismiss={requestClose}
75
76
  header={
76
77
  <TopNav
@@ -82,7 +83,7 @@ function ReportIssueDemo() {
82
83
  }
83
84
  footer={
84
85
  <Dock>
85
- <Button label="Submit report" onPress={() => setVisible(false)} />
86
+ <Button label="Submit report" onPress={close} />
86
87
  </Dock>
87
88
  }
88
89
  >
@@ -135,7 +136,10 @@ function ReportIssueDemo() {
135
136
  </ButtonGroup>
136
137
  </View>
137
138
  </Dialog>
138
- </PhoneFrame>
139
+ </>
140
+ );
141
+ }}
142
+ </OverlayDemo>
139
143
  );
140
144
  }
141
145