@krapwoo/ds-viewer 0.4.6 → 0.4.7

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 (34) hide show
  1. package/README.md +100 -15
  2. package/dist/cli/doctor.js +137 -0
  3. package/dist/cli/explain.js +44 -6
  4. package/dist/cli/props.js +80 -1
  5. package/dist/cli/staticPage.js +228 -6
  6. package/dist/native/catalog/comparison.js +29 -4
  7. package/native/catalog/BoundedOverlayViewport.tsx +112 -0
  8. package/native/catalog/CatalogShell.tsx +22 -0
  9. package/native/catalog/ComparisonGrid.tsx +2 -2
  10. package/native/catalog/ComparisonGroups.tsx +2 -2
  11. package/native/catalog/ComparisonList.tsx +2 -2
  12. package/native/catalog/PhoneFrame.tsx +6 -0
  13. package/native/catalog/ReferenceDetails.tsx +75 -20
  14. package/native/catalog/SectionBlock.tsx +66 -4
  15. package/native/catalog/SpecimenSurface.tsx +18 -10
  16. package/native/catalog/comparison.ts +39 -5
  17. package/native/catalog/index.ts +4 -0
  18. package/native/catalog/overlayViewport.ts +156 -0
  19. package/native/catalog/pageApi.ts +6 -0
  20. package/native/catalog/specimenSurfaceStyle.ts +70 -1
  21. package/native/catalog/types.ts +42 -12
  22. package/package.json +1 -1
  23. package/starter-kit/components/Banner/Banner.catalog.tsx +3 -0
  24. package/starter-kit/components/Button/Button.catalog.tsx +3 -0
  25. package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +6 -0
  26. package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +5 -0
  27. package/starter-kit/components/InputField/InputField.catalog.tsx +3 -0
  28. package/starter-kit/components/List/List.catalog.tsx +3 -0
  29. package/starter-kit/components/Pill/Pill.catalog.tsx +3 -0
  30. package/starter-kit/components/PillRow/PillRow.catalog.tsx +3 -0
  31. package/starter-kit/components/SearchField/SearchField.catalog.tsx +4 -0
  32. package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +3 -0
  33. package/starter-kit/components/TextArea/TextArea.catalog.tsx +3 -0
  34. package/starter-kit/components/Toast/Toast.catalog.tsx +7 -0
@@ -1,4 +1,4 @@
1
- import type { SpecimenSurfaceKind } from './types.ts';
1
+ import type { SpecimenAlign, SpecimenSurfaceKind } from './types.ts';
2
2
 
3
3
  /**
4
4
  * The example wrapper's width policy inside a cell, kept free of any `react-native` import so
@@ -16,6 +16,75 @@ export const SPECIMEN_SURFACE_WIDTH_STYLE = {
16
16
  alignSelf: 'stretch',
17
17
  } as const;
18
18
 
19
+ /**
20
+ * Centers a non-`fill` specimen horizontally inside its (now full-width) wrapper — unchanged,
21
+ * bounded (non-`fill`) specimens stay centered by default. See `SpecimenSurface.tsx`'s
22
+ * `SpecimenContent`, the one place every presentation shape (list/grouped/grid) wraps a specimen
23
+ * before handing it to its cell.
24
+ */
25
+ export const SPECIMEN_CONTENT_CENTER_STYLE = {
26
+ ...SPECIMEN_SURFACE_WIDTH_STYLE,
27
+ alignItems: 'center',
28
+ } as const;
29
+
30
+ /**
31
+ * A `fill` specimen's content stretches instead of centering: `alignItems: 'center'` on this
32
+ * wrapper previously applied to `fill` specimens too, which collapses an auto-sized, already
33
+ * full-width child (e.g. NextTrainInfo's row) down to its own intrinsic content width instead of
34
+ * leaving it at the wrapper's full width — `alignItems: 'center'` resolves a child with no definite
35
+ * cross-axis size to its content size, the opposite of what `fill` promises. A `fill` specimen
36
+ * whose own size is capped below the wrapper's width (e.g. a phone-frame preview's `maxWidth`) is
37
+ * expected to center itself (`alignSelf: 'center'`, see `PhoneFrame.tsx`) rather than lean on this
38
+ * wrapper's `alignItems` — the same way `alignItems: 'stretch'` already behaves like `flex-start`,
39
+ * not an actual stretch, for any child with a definite cross-axis size (a set width/maxWidth) per
40
+ * the flexbox spec. An explicit `align` (see `specimenContentAlignItems`) still overrides this.
41
+ */
42
+ export const SPECIMEN_CONTENT_FILL_STYLE = {
43
+ ...SPECIMEN_SURFACE_WIDTH_STYLE,
44
+ alignItems: 'stretch',
45
+ } as const;
46
+
47
+ const ALIGN_ITEMS_BY_SPECIMEN_ALIGN: Record<SpecimenAlign, 'flex-start' | 'center' | 'flex-end'> = {
48
+ start: 'flex-start',
49
+ center: 'center',
50
+ end: 'flex-end',
51
+ };
52
+
53
+ /** The `SpecimenContent` wrapper's `alignItems`: an explicit `align` (`SpecimenPresentation.align`)
54
+ * always wins over `fill`. Omitted, `fill` decides the established default — `'stretch'` (preserve
55
+ * a full-width auto-sized child) when true, `'center'` (unchanged) otherwise. */
56
+ export function specimenContentAlignItems(fill: boolean | undefined, align: SpecimenAlign | undefined): 'flex-start' | 'center' | 'flex-end' | 'stretch' {
57
+ if (align !== undefined) return ALIGN_ITEMS_BY_SPECIMEN_ALIGN[align];
58
+ return fill ? 'stretch' : 'center';
59
+ }
60
+
61
+ /** A non-fill specimen's horizontal position, as `justifyContent` on `SpecimenContent`'s own row
62
+ * main axis rather than `alignItems` on a column: `alignItems` only sets a child's *default*
63
+ * `alignSelf`, which the specimen's own root can — and does, e.g. Badge's `alignSelf:
64
+ * 'flex-start'` — override outright, leaning the whole grid left regardless of the wrapper's
65
+ * intent. `justifyContent` positions along the main axis directly and cannot be overridden by a
66
+ * child's `alignSelf`, which only ever targets the cross axis (vertical, on a row). Reuses
67
+ * `ALIGN_ITEMS_BY_SPECIMEN_ALIGN`'s mapping — `flex-start`/`center`/`flex-end` are valid values
68
+ * for both `alignItems` and `justifyContent`. */
69
+ export function specimenContentJustifyContent(align: SpecimenAlign | undefined): 'flex-start' | 'center' | 'flex-end' {
70
+ return align !== undefined ? ALIGN_ITEMS_BY_SPECIMEN_ALIGN[align] : 'center';
71
+ }
72
+
73
+ /** The non-fill `SpecimenContent` wrapper's own style: still full-width (`SPECIMEN_SURFACE_WIDTH_STYLE`,
74
+ * so a percentage-width child keeps resolving against it), but a row main axis so
75
+ * `justifyContent` — not `alignItems` — decides horizontal placement (see
76
+ * `specimenContentJustifyContent`). Vertical cross-axis stays centered, as it already did. Only
77
+ * for non-`fill` specimens; `fill` keeps the existing column + `alignItems` stretch behavior
78
+ * (`specimenContentAlignItems`), unchanged. */
79
+ export function specimenContentBoundedStyle(align: SpecimenAlign | undefined) {
80
+ return {
81
+ ...SPECIMEN_SURFACE_WIDTH_STYLE,
82
+ flexDirection: 'row' as const,
83
+ justifyContent: specimenContentJustifyContent(align),
84
+ alignItems: 'center' as const,
85
+ };
86
+ }
87
+
19
88
  /** Each example's cell turns gray only when its example would vanish against white (see
20
89
  * `specimenFill.ts`). A page can force a fill with `specimenSurface`. */
21
90
  export const DEFAULT_SPECIMEN_SURFACE: SpecimenSurfaceKind = 'auto';
@@ -12,21 +12,39 @@ export interface PropDef {
12
12
  desc: string;
13
13
  }
14
14
 
15
+ /** How a specimen's content is cross-axis aligned inside its (already full-width) wrapper —
16
+ * `start`/`center`/`end` map to `flex-start`/`center`/`flex-end`. Omit to inherit the established
17
+ * default: `center` when `fill` is false/omitted (unchanged), or the wrapper's own full-width
18
+ * stretch when `fill` is true — a capped-width `fill` specimen (e.g. a phone-frame preview) that
19
+ * still wants centering sets `align: 'center'` explicitly rather than relying on `fill` alone; see
20
+ * `specimenContentAlignItems` in `specimenSurfaceStyle.ts`. */
21
+ export type SpecimenAlign = 'start' | 'center' | 'end';
22
+
23
+ /** Per-specimen presentation, layered under whatever a slot or page already decides: this one
24
+ * specimen's own background (`surface`, overriding the slot's/page's `specimenSurface` — omit to
25
+ * inherit it), whether its wrapper stretches to the row's full width instead of shrinking to its
26
+ * own content width (`fill`, for a single wide-format instance, e.g. a `fullWidth` Button, sitting
27
+ * among otherwise compact, centered siblings — independent of a slot's own `itemsFill`, which
28
+ * applies to every item uniformly), and how its content cross-axis aligns inside that wrapper
29
+ * (`align` — see `SpecimenAlign`). Shared by `VariantExample` (list/grouped items) and
30
+ * `ComparisonCell` (grid cells) so the same knobs mean the same thing in every presentation shape
31
+ * ds-viewer renders a specimen in; an explicit `surface` here always bypasses `'auto'`'s own fill
32
+ * heuristic (`specimenFill.ts`), the same way a slot's/page's own explicit surface already does. */
33
+ export interface SpecimenPresentation {
34
+ /** @default false */
35
+ fill?: boolean;
36
+ surface?: SpecimenSurfaceKind;
37
+ align?: SpecimenAlign;
38
+ }
39
+
15
40
  /** One individual example inside a variant/state cluster — e.g. one Button instance. */
16
- export interface VariantExample {
41
+ export interface VariantExample extends SpecimenPresentation {
17
42
  /** React key; keep stable and unique within its group. */
18
43
  key: string;
19
44
  /** Shown as a small caption under this item — the actual variant/state value it demonstrates
20
45
  * (e.g. "Primary", "Icon-only"), not a generic label like "Example 1". */
21
46
  name: string;
22
47
  node: React.ReactNode;
23
- /** Stretch *this item's* wrapper to the row's full width, instead of shrinking to its own content
24
- * width — for a single wide-format instance (e.g. a `fullWidth` Button) sitting among otherwise
25
- * compact, centered siblings in the same slot. Without this, a `fullWidth`/stretch-based prop on
26
- * the instance itself has nothing to stretch into — its wrapper still shrinks to content, so the
27
- * instance renders at its natural size regardless of the prop. Independent of the slot's own
28
- * `itemsFill` (which applies to every item uniformly); this is a per-item override. @default false */
29
- fill?: boolean;
30
48
  /** Which real prop value(s) this instance demonstrates, e.g. `{ variant: 'primary' }` or
31
49
  * `{ size: 'large', disabled: true }` — mirrors the actual props passed to `node`. Optional and
32
50
  * additive: SectionBlock only cross-checks a section's enum props against this metadata once at
@@ -99,14 +117,13 @@ export interface ComparisonAxisItem {
99
117
  export type GridAxis = readonly ComparisonAxisItem[] | { prop: string; items: readonly ComparisonAxisItem[] };
100
118
 
101
119
  /** One authored row × column specimen. Provide exactly one of `node` (a real instance of the
102
- * documented component) or `unavailableReason` (the combination genuinely does not exist). */
103
- export interface ComparisonCell {
120
+ * documented component) or `unavailableReason` (the combination genuinely does not exist). See
121
+ * `SpecimenPresentation` for `fill`/`surface`. */
122
+ export interface ComparisonCell extends SpecimenPresentation {
104
123
  rowKey: string;
105
124
  columnKey: string;
106
125
  node?: React.ReactNode;
107
126
  unavailableReason?: string;
108
- /** Stretch this specimen to the cell width. @default false */
109
- fill?: boolean;
110
127
  }
111
128
 
112
129
  /** A grid of two props that combine freely. Every row × column pair is authored — never inferred
@@ -222,6 +239,13 @@ export interface SectionDef<TId extends string = string> {
222
239
  /** Widths for this page's `render()` preview, e.g. `[402, 320]` to add a small-phone example.
223
240
  * Defaults to the catalog's default (CatalogShell `defaultPreviewWidths`), else `[402]`. */
224
241
  previewWidths?: PreviewWidths;
242
+ /** Presentation for this page's numeric-width component Preview (guided intelligence,
243
+ * "Collapsible Preview table"): `'frames'` (default, unchanged) — one borderless-card frame per
244
+ * width, side by side. `'table'` — the widths share one `ComparisonList`-style card, each with
245
+ * its own muted caption strip above its live body; still an independent, stateful instance per
246
+ * width. Has no effect on a `'full'` preview width (catalog chrome) or a token gallery — those
247
+ * always render as `'frames'` regardless of this field. */
248
+ previewLayout?: 'frames' | 'table';
225
249
  /** Remove specific parts of the page — for sections with no meaningful states, props, or
226
250
  * accessibility story (e.g. a framework's own building-block pages). `variants` removes the
227
251
  * first specimen block (grid, Variants list, or Preview); `states` removes the States /
@@ -237,6 +261,12 @@ export interface SectionDef<TId extends string = string> {
237
261
  * Quick reference. See `ComposedOfEntry`. Omit when the component has no meaningful composition
238
262
  * to disclose (most leaf components). */
239
263
  composedOf?: ComposedOfEntry[];
264
+ /** Explicit author declaration (guided intelligence design §3) that this page's examples are
265
+ * intentionally static despite the documented component having a controlled state/event pair —
266
+ * suppresses `cli/doctor.ts`'s `missing-working-preview` advisory. `reason` is a short, bounded
267
+ * sentence explaining why (not free-form prose) — e.g. "Selection is already demonstrated by X's
268
+ * working preview." Omit when no such declaration applies; the advisory then runs normally. */
269
+ intentionalStaticPreview?: { reason: string };
240
270
  }
241
271
 
242
272
  /** 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.6",
3
+ "version": "0.4.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A browsable, always-current catalog for an Expo / React Native design system.",
@@ -5,6 +5,9 @@ import { Banner } from './Banner';
5
5
  export default defineCatalogPage({
6
6
  component: 'Banner',
7
7
  group: 'Surfaces',
8
+ composedOf: [
9
+ { component: 'AnimatedChevron', role: "The expand/collapse chevron in the header of a collapsible banner.", relationship: 'built-in' },
10
+ ],
8
11
  description: 'An inline callout for status/announcements — five semantic variants, optional collapsible body, inline link, and action button.',
9
12
  whenToUse: "Persistent and in-flow, describing a standing condition about the screen's content. For a transient, self-contained event notification, use Toast instead.",
10
13
  a11y: 'When onPress/action is set the header/button are Pressables; the collapsible header toggles the description with a chevron affordance.',
@@ -56,6 +56,9 @@ const BUTTON_COMPARISON = grid(
56
56
  export default defineCatalogPage({
57
57
  component: 'Button',
58
58
  group: 'Actions',
59
+ composedOf: [
60
+ { component: 'Loading', role: "The spinner that replaces the label and icon while loading is true.", relationship: 'built-in' },
61
+ ],
59
62
  comparison: BUTTON_COMPARISON,
60
63
  description:
61
64
  'The primary tap target. Five visual weights, three sizes, optional leading/trailing icon, plus loading and icon-only modes.',
@@ -39,6 +39,12 @@ function DropdownDemo() {
39
39
  export default defineCatalogPage({
40
40
  component: 'Dropdown',
41
41
  group: 'Inputs',
42
+ composedOf: [
43
+ { component: 'FieldContainer', role: "The field frame around the label, value and chevron (the trigger).", relationship: 'built-in' },
44
+ { component: 'BottomSheet', role: "The sheet listing the options while the dropdown is open.", relationship: 'built-in' },
45
+ { component: 'TopNav', role: "The sheet's header, titled with the field's label.", relationship: 'built-in' },
46
+ { component: 'Button', role: "The icon-only Close button in the sheet header.", relationship: 'built-in' },
47
+ ],
42
48
  description: 'A labelled field that opens a BottomSheet picker on tap — the "bottom sheet picker" interaction: tap the trigger, pick an option from the sheet, it closes.',
43
49
  whenToUse: 'The answer is one of a small, known set of choices — never free text. For free-text filtering, use SearchField; for a named field with a fixed identity, use InputField.',
44
50
  a11y: 'The trigger is a Pressable (via FieldContainer) with accessibilityRole="button". Options render as accessibilityRole="radio" inside a "radiogroup", with accessibilityState.checked reflecting the current selection.',
@@ -6,6 +6,11 @@ import { DS_SEMANTIC } from '../../tokens';
6
6
  export default defineCatalogPage({
7
7
  component: 'EmptyState',
8
8
  group: 'Layout',
9
+ composedOf: [
10
+ { component: 'Avatar', role: "The icon circle above the title (iconName, default 'users').", relationship: 'built-in' },
11
+ { component: 'ButtonGroup', role: "Stacks the action buttons when action is set.", relationship: 'built-in' },
12
+ { component: 'Button', role: "The action button, plus a tertiary secondaryAction button when set.", relationship: 'built-in' },
13
+ ],
9
14
  description: 'A centred placeholder for a screen or section with nothing to show yet — no results, no saved items, a first-run state.',
10
15
  whenToUse: "Fills the entire content area because there's nothing else to show. For a note that sits alongside other real content, use Banner instead.",
11
16
  a11y: 'A plain View with text; the action(s) render as real Buttons inside a ButtonGroup, which carry their own accessibility.',
@@ -18,6 +18,9 @@ function InputFieldDemo() {
18
18
  export default defineCatalogPage({
19
19
  component: 'InputField',
20
20
  group: 'Inputs',
21
+ composedOf: [
22
+ { component: 'InputClearButton', role: "The clear (\u00d7) button, shown while the field has a value and is focused (or active, when not editable).", relationship: 'built-in' },
23
+ ],
21
24
  description: 'A floating-label field. Resting: a centred, body-sized label with no border. Active (focused, or picker with active set) or filled: the label floats to a small top caption, row 2 shows the value/input, and a border fades in while active.',
22
25
  whenToUse: 'A named field with a fixed identity ("To", "Arrive by") across the interaction. For free-text search with no floating label, use SearchField; for one of a small known set of choices, use Dropdown.',
23
26
  a11y: 'The editable mode is a live TextInput; the picker mode is a Pressable row. Either mode shows a clear button while active (focused, or active for a picker) with a value set. Provide a meaningful label.',
@@ -9,6 +9,9 @@ import { DS_SEMANTIC } from '../../tokens';
9
9
  export default defineCatalogPage({
10
10
  component: 'List',
11
11
  group: 'Layout',
12
+ composedOf: [
13
+ { component: 'Divider', role: "The hairline between consecutive items (none after the last).", relationship: 'built-in' },
14
+ ],
12
15
  description: 'Stacks ListItem rows on a rounded white surface, with a Divider automatically inserted between each consecutive pair — never after the last.',
13
16
  a11y: 'A plain View; each ListItem child carries its own accessibility.',
14
17
  variants: {
@@ -41,6 +41,9 @@ const PILL_COMPARISON = grid(
41
41
  export default defineCatalogPage({
42
42
  component: 'Pill',
43
43
  group: 'Actions',
44
+ composedOf: [
45
+ { component: 'Loading', role: "The spinner shown instead of the label and icon while loading is true.", relationship: 'built-in' },
46
+ ],
44
47
  specimenSize: 'compact',
45
48
  comparison: PILL_COMPARISON,
46
49
  description: 'A compact selectable chip — selected/unselected states with an optional leading icon.',
@@ -5,6 +5,9 @@ import { PillRow } from './PillRow';
5
5
  export default defineCatalogPage({
6
6
  component: 'PillRow',
7
7
  group: 'Actions',
8
+ composedOf: [
9
+ { component: 'Pill', role: "Each item in pills, plus the trailing icon-only Edit pill when shown.", relationship: 'built-in' },
10
+ ],
8
11
  description: 'A horizontally-scrolling row of Pill chips, with an optional trailing icon-only add pill — keeps the selected pill scrolled into view automatically.',
9
12
  whenToUse: 'A scrollable row of selection chips (e.g. saved-place shortcuts) — for a fixed row of action buttons instead, use ButtonGroup.',
10
13
  a11y: 'The row itself carries no accessibility role; each Pill (including the add pill) keeps its own accessibilityLabel/role from the Pill component.',
@@ -10,6 +10,10 @@ function SearchFieldDemo() {
10
10
  export default defineCatalogPage({
11
11
  component: 'SearchField',
12
12
  group: 'Inputs',
13
+ composedOf: [
14
+ { component: 'FieldContainer', role: "The field frame around the search icon, input and clear button.", relationship: 'built-in' },
15
+ { component: 'InputClearButton', role: "The clear (\u00d7) button, shown while focused with text.", relationship: 'built-in' },
16
+ ],
13
17
  description: 'Single-line search input on the shared field chrome — leading search icon, a border that darkens on focus, and a clear button while active with text.',
14
18
  whenToUse: 'Free-text filtering/searching only — never a named field with a fixed value. For that, use InputField; for a small known set of choices, use Dropdown.',
15
19
  a11y: 'Forwards a ref to the underlying TextInput; all TextInputProps pass through, so pass accessibilityLabel/placeholder as needed.',
@@ -5,6 +5,9 @@ import { SectionHeader } from './SectionHeader';
5
5
  export default defineCatalogPage({
6
6
  component: 'SectionHeader',
7
7
  group: 'Layout',
8
+ composedOf: [
9
+ { component: 'Button', role: "The trailing button, rendered when trailingButtonLabel is set.", relationship: 'built-in' },
10
+ ],
8
11
  description: 'An uppercase muted section label with an optional inline icon and a right-aligned ghost button.',
9
12
  whenToUse: "A label above a group of related rows within a screen (e.g. above a List) — for the screen's own top bar, use TopNav.",
10
13
  a11y: 'Title renders as plain Text — no heading role. A tappable labelIcon (onPress set) becomes accessibilityRole="button" with accessibilityLabel falling back to the section\'s own title; a non-interactive labelIcon has no accessibility node of its own. The trailing button is a real Button, so it carries Button\'s own accessibility for free.',
@@ -10,6 +10,9 @@ function TextAreaDemo() {
10
10
  export default defineCatalogPage({
11
11
  component: 'TextArea',
12
12
  group: 'Inputs',
13
+ composedOf: [
14
+ { component: 'FieldContainer', role: "The field frame around the multi-line input.", relationship: 'built-in' },
15
+ ],
13
16
  description: 'A multi-line input that grows with its content from a minimum height.',
14
17
  a11y: 'A multiline TextInput; pass accessibilityLabel via inputProps when there is no visible label beside it.',
15
18
  variants: {
@@ -32,6 +32,13 @@ export default defineCatalogPage({
32
32
  description: 'A transient confirmation bar. Without a variant it renders the dark surface with inverse text; a variant shifts to a pastel status color.',
33
33
  whenToUse: "Transient and self-contained, floating over the screen, expected to go away on its own or via its own action. For a persistent, in-flow message about a standing condition, use Banner.",
34
34
  a11y: 'Rendered with accessibilityRole="alert" so screen readers announce it when it appears.',
35
+ // Confirmed from Toast.tsx's own JSX, not inferred from its imports: it renders a Button itself
36
+ // (not a caller-supplied child) whenever `action` is set. Toast.tsx also renders an Icon for its
37
+ // status glyph, but Icon has no catalog page/known component name of its own to confirm against
38
+ // yet, so that entry is left out rather than logging an unknown-component warning.
39
+ composedOf: [
40
+ { component: 'Button', role: 'Renders the optional action button when action is set.', relationship: 'built-in' },
41
+ ],
35
42
  variants: {
36
43
  itemsFill: true,
37
44
  items: [