@krapwoo/ds-viewer 0.1.1 → 0.3.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 (173) hide show
  1. package/README.md +182 -78
  2. package/dist/cli/agentsFile.js +31 -0
  3. package/dist/cli/config.js +26 -1
  4. package/dist/cli/dev.js +50 -5
  5. package/dist/cli/doctor.js +492 -0
  6. package/dist/cli/explain.js +151 -0
  7. package/dist/cli/githubAction.js +68 -0
  8. package/dist/cli/glob.js +8 -0
  9. package/dist/cli/init.js +152 -18
  10. package/dist/cli/main.js +115 -4
  11. package/dist/cli/pageIndex.js +27 -1
  12. package/dist/cli/props.js +170 -21
  13. package/dist/cli/staticPage.js +356 -0
  14. package/dist/cli/sync.js +28 -5
  15. package/dist/cli/workspace.js +49 -21
  16. package/dist/native/catalog/comparison.js +283 -0
  17. package/dist/native/catalog/tokens.js +119 -0
  18. package/dist/native/catalog/types.js +1 -0
  19. package/native/catalog/CatalogShell.tsx +6 -1
  20. package/native/catalog/CatalogSidebar.tsx +98 -5
  21. package/native/catalog/ComparisonGrid.tsx +9 -7
  22. package/native/catalog/SpacingScaleGallery.tsx +1 -1
  23. package/native/catalog/TokenRow.tsx +3 -2
  24. package/native/catalog/catalogNavigation.ts +1 -1
  25. package/native/catalog/comparison.ts +63 -5
  26. package/native/catalog/index.ts +8 -6
  27. package/native/catalog/logoLayout.ts +12 -0
  28. package/native/catalog/pageApi.ts +21 -3
  29. package/native/catalog/tokens.ts +5 -0
  30. package/native/catalog/types.ts +9 -6
  31. package/package.json +12 -6
  32. package/starter-kit/WHEN_TO_USE.md +124 -0
  33. package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
  34. package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
  35. package/starter-kit/components/AnimatedChevron/index.ts +2 -0
  36. package/starter-kit/components/Avatar/Avatar.catalog.tsx +54 -0
  37. package/starter-kit/components/Avatar/Avatar.tsx +74 -0
  38. package/starter-kit/components/Avatar/index.ts +2 -0
  39. package/starter-kit/components/Badge/Badge.catalog.tsx +88 -0
  40. package/starter-kit/components/Badge/Badge.tsx +56 -0
  41. package/starter-kit/components/Badge/Badge.types.ts +18 -0
  42. package/starter-kit/components/Badge/index.ts +2 -0
  43. package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
  44. package/starter-kit/components/Banner/Banner.tsx +257 -0
  45. package/starter-kit/components/Banner/Banner.types.ts +38 -0
  46. package/starter-kit/components/Banner/index.ts +2 -0
  47. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
  48. package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
  49. package/starter-kit/components/BottomSheet/index.ts +2 -0
  50. package/starter-kit/components/Button/Button.catalog.tsx +99 -0
  51. package/starter-kit/components/Button/Button.tsx +173 -0
  52. package/starter-kit/components/Button/Button.types.ts +52 -0
  53. package/starter-kit/components/Button/index.ts +2 -0
  54. package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
  55. package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
  56. package/starter-kit/components/ButtonGroup/index.ts +2 -0
  57. package/starter-kit/components/Card/Card.catalog.tsx +68 -0
  58. package/starter-kit/components/Card/Card.tsx +133 -0
  59. package/starter-kit/components/Card/index.ts +2 -0
  60. package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
  61. package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
  62. package/starter-kit/components/Checkbox/index.ts +2 -0
  63. package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
  64. package/starter-kit/components/Dialog/Dialog.tsx +82 -0
  65. package/starter-kit/components/Dialog/index.ts +2 -0
  66. package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
  67. package/starter-kit/components/Divider/Divider.tsx +20 -0
  68. package/starter-kit/components/Divider/index.ts +2 -0
  69. package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
  70. package/starter-kit/components/Dock/Dock.tsx +58 -0
  71. package/starter-kit/components/Dock/index.ts +2 -0
  72. package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
  73. package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
  74. package/starter-kit/components/Dropdown/index.ts +2 -0
  75. package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
  76. package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
  77. package/starter-kit/components/EmptyState/index.ts +2 -0
  78. package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
  79. package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
  80. package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
  81. package/starter-kit/components/FieldContainer/index.ts +2 -0
  82. package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
  83. package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
  84. package/starter-kit/components/InputClearButton/index.ts +2 -0
  85. package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
  86. package/starter-kit/components/InputField/InputField.tsx +418 -0
  87. package/starter-kit/components/InputField/index.ts +2 -0
  88. package/starter-kit/components/List/List.catalog.tsx +30 -0
  89. package/starter-kit/components/List/List.tsx +36 -0
  90. package/starter-kit/components/List/index.ts +2 -0
  91. package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
  92. package/starter-kit/components/ListItem/ListItem.tsx +165 -0
  93. package/starter-kit/components/ListItem/index.ts +2 -0
  94. package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
  95. package/starter-kit/components/Loading/Loading.tsx +121 -0
  96. package/starter-kit/components/Loading/index.ts +2 -0
  97. package/starter-kit/components/Pill/Pill.catalog.tsx +79 -0
  98. package/starter-kit/components/Pill/Pill.tsx +181 -0
  99. package/starter-kit/components/Pill/Pill.types.ts +30 -0
  100. package/starter-kit/components/Pill/index.ts +2 -0
  101. package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
  102. package/starter-kit/components/PillRow/PillRow.tsx +140 -0
  103. package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
  104. package/starter-kit/components/PillRow/index.ts +2 -0
  105. package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
  106. package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
  107. package/starter-kit/components/ProgressDots/index.ts +2 -0
  108. package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
  109. package/starter-kit/components/Radio/Radio.tsx +114 -0
  110. package/starter-kit/components/Radio/index.ts +2 -0
  111. package/starter-kit/components/Radio/usePressScale.ts +33 -0
  112. package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
  113. package/starter-kit/components/SearchField/SearchField.tsx +98 -0
  114. package/starter-kit/components/SearchField/index.ts +2 -0
  115. package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
  116. package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
  117. package/starter-kit/components/SectionHeader/index.ts +2 -0
  118. package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
  119. package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
  120. package/starter-kit/components/SegmentedToggle/index.ts +2 -0
  121. package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
  122. package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
  123. package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
  124. package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
  125. package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
  126. package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
  127. package/starter-kit/components/Shimmer/index.ts +2 -0
  128. package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
  129. package/starter-kit/components/Surface/Surface.tsx +45 -0
  130. package/starter-kit/components/Surface/index.ts +2 -0
  131. package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
  132. package/starter-kit/components/Switch/Switch.tsx +129 -0
  133. package/starter-kit/components/Switch/index.ts +2 -0
  134. package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
  135. package/starter-kit/components/TextArea/TextArea.tsx +99 -0
  136. package/starter-kit/components/TextArea/index.ts +2 -0
  137. package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
  138. package/starter-kit/components/Toast/Toast.tsx +144 -0
  139. package/starter-kit/components/Toast/index.ts +2 -0
  140. package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
  141. package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
  142. package/starter-kit/components/Tooltip/index.ts +2 -0
  143. package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
  144. package/starter-kit/components/TopNav/TopNav.tsx +77 -0
  145. package/starter-kit/components/TopNav/index.ts +2 -0
  146. package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
  147. package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
  148. package/starter-kit/components/UnderlineTabs/index.ts +2 -0
  149. package/starter-kit/components/index.ts +36 -0
  150. package/starter-kit/components/statusColors.ts +27 -0
  151. package/starter-kit/icons/Icon.native.tsx +90 -0
  152. package/starter-kit/icons/index.ts +10 -0
  153. package/starter-kit/icons/paths.ts +623 -0
  154. package/starter-kit/icons/types.ts +69 -0
  155. package/starter-kit/pages/Colors.catalog.tsx +55 -0
  156. package/starter-kit/pages/Font.catalog.tsx +45 -0
  157. package/starter-kit/pages/Icons.catalog.tsx +51 -0
  158. package/starter-kit/pages/Manifest.catalog.tsx +24 -0
  159. package/starter-kit/pages/Motion.catalog.tsx +101 -0
  160. package/starter-kit/pages/Radius.catalog.tsx +35 -0
  161. package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
  162. package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
  163. package/starter-kit/pages/Shadow.catalog.tsx +34 -0
  164. package/starter-kit/pages/Spacing.catalog.tsx +12 -0
  165. package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
  166. package/starter-kit/pages/Typography.catalog.tsx +17 -0
  167. package/starter-kit/tokens/index.ts +50 -0
  168. package/starter-kit/tokens/motion.ts +90 -0
  169. package/starter-kit/tokens/palette.ts +111 -0
  170. package/starter-kit/tokens/scales.ts +106 -0
  171. package/starter-kit/tokens/semantic.ts +98 -0
  172. package/starter-kit/tokens/shadow.ts +36 -0
  173. package/starter-kit/tokens/typography.ts +72 -0
package/README.md CHANGED
@@ -7,63 +7,102 @@ Drop it into a new project, rebrand the tokens, and build.
7
7
 
8
8
  ```
9
9
  design-system-template/
10
- ├── tokens/ ← ONE source of truth (colors, spacing, radius, type, shadow). Platform-neutral.
11
- │ ├── palette.ts raw color scale (0–800 per hue)
12
- │ ├── semantic.ts role tokens (surface / text / emphasis / shade / border …)
13
- │ ├── scales.ts spacing · radius · icon-size
14
- │ ├── typography.ts type scale + font weights
15
- │ ├── shadow.ts elevation (RN style objects)
16
- │ └── index.ts barrel
17
- ├── icons/ ← shared SVG path DATA + the native renderer
18
- │ ├── paths.ts platform-neutral icon geometry
19
- │ ├── types.ts IconName union
20
- │ ├── Icon.native.tsx react-native-svg renderer
21
- │ └── index.ts barrel (data + types only)
22
- ├── native/ ← React Native / Expo components + catalog
23
- │ ├── components/… one folder per component (Component.tsx, .types.ts, index.ts)
24
- │ └── catalog/ app-agnostic catalog framework (RN) + CatalogExample.tsx
25
- └── native-preview/ ← dev-only Expo shell for browsing the catalog in a browser (not part
26
- of the reusable template — see "Browsing the catalog" below)
10
+ ├── starter-kit/ ← the starter kit: copied, never imported, into a new project by `init --new`
11
+ │ ├── tokens/ ONE source of truth (colors, spacing, radius, type, shadow). Platform-neutral.
12
+ │ ├── icons/ shared SVG path data + the native (react-native-svg) renderer
13
+ │ ├── components/… one folder per component: Component.tsx, .types.ts, index.ts, and a
14
+ │ │ finished Component.catalog.tsx documenting it
15
+ │ ├── pages/ standalone pages (token galleries, Icons, recipes, Manifest)
16
+ │ └── WHEN_TO_USE.md the deciding question behind every pair of similar components
17
+ ├── native/catalog/ ← the viewer itself — app-agnostic, ships in the published package
18
+ └── kit-host/ ← this repo's own view of the starter kit; depends on this package via
19
+ `file:..`; `npm run kit:dev` opens it (not part of the published package —
20
+ see "Developing this repository" below)
27
21
  ```
28
22
 
29
23
  ## Using `@krapwoo/ds-viewer` in your own project
30
24
 
31
- This repository is also the source of the `@krapwoo/ds-viewer` npm package — a CLI and viewer
32
- for browsing *your own* app's components, not this template's. In an existing Expo app:
25
+ **New project:**
33
26
 
34
27
  ```bash
35
- npx @krapwoo/ds-viewer init
28
+ npx @krapwoo/ds-viewer init --new
36
29
  npm install
30
+ npm run ds-viewer dev
37
31
  ```
38
32
 
39
- `init` detects your component folders and token modules and, after you confirm the list it prints
40
- (`--yes` skips the prompt), writes `ds-viewer.config.ts`, a draft `<Export>.catalog.tsx` beside
41
- each detected component (marked "needs examples" — nothing is invented), a `ds-viewer` package
42
- script, a `@krapwoo/ds-viewer` devDependency entry, and a `.ds-viewer/` entry in `.gitignore`. The
43
- `npm install` above is what actually fetches that devDependency — `init` can only add the entry,
44
- not invoke your package manager. Then:
33
+ `init --new` checks Expo/Node/package requirements, installs `react-native-svg` and
34
+ `react-native-safe-area-context` if your project doesn't have them yet (via `expo install`), then
35
+ copies the starter kit above into `src/ds/` — pass `--kit-root <path>` to put it somewhere else.
36
+ Every component already has a finished example page; `ds-viewer dev` opens the catalog immediately
37
+ with no further authoring needed. Running `init --new` again only adds whatever is still missing —
38
+ it never overwrites a file you've already edited.
39
+
40
+ **Existing project:**
41
+
42
+ ```bash
43
+ npx @krapwoo/ds-viewer init --existing
44
+ npm install
45
+ ```
46
+
47
+ `init --existing` detects your component folders and token modules and, after you confirm the list
48
+ it prints (`--yes` skips the prompt), writes `ds-viewer.config.ts`, a draft
49
+ `<Export>.catalog.tsx` beside each detected component (marked "needs examples" — nothing is
50
+ invented), a `ds-viewer` package script, a `@krapwoo/ds-viewer` devDependency entry, and a
51
+ `.ds-viewer/` entry in `.gitignore`.
52
+
53
+ Running `npx @krapwoo/ds-viewer init` with neither `--new` nor `--existing` asks which one you
54
+ want, interactively. `init --yes` alone (no `--new`/`--existing`) skips that question and sets up
55
+ an existing project instead — the same default `--yes` always had in 0.1, so a script that already
56
+ calls `init --yes` keeps working unchanged. The `npm install` above is what actually fetches the
57
+ devDependency `init` added — `init` can only add the entry, not invoke your package manager.
58
+
59
+ Either way, then:
45
60
 
46
61
  ```bash
47
62
  npm run ds-viewer dev # opens the catalog in a browser, re-syncing on every file change
48
63
  npm run ds-viewer sync # regenerates component/token/page data without starting a server
64
+ npm run ds-viewer doctor # checks every page for drift and coverage gaps (--json, --ci)
65
+ npm run ds-viewer explain <Page> # prints why a page's specimens are laid out the way they are
49
66
  ```
50
67
 
51
- `ds-viewer.config.ts` fields in 0.1: `name`, `components`, `exclude`, `tokens`, `pages`,
52
- `groupOrder` — see `config/index.ts`'s `DsViewerConfig` for each field's exact meaning. Write a
53
- page with `defineCatalogPage()` from `@krapwoo/ds-viewer` (same shape as this template's own
54
- `SectionDef`, minus `id`/`path`/`props`, plus `component`, `group`, and `propNotes`).
68
+ `ds-viewer.config.ts` fields: `name`, `logo` (optional — a path to an image asset, shown in the
69
+ sidebar beside the name, or in place of it for a wide wordmark; validated at `sync`/`dev` time
70
+ and bundled into the preview workspace), `components`, `exclude`, `tokens`, `pages`, `groupOrder`, `starterKit` (new-project only —
71
+ records the kit's version), `updateCheck`, `doctor` — see `config/index.ts`'s `DsViewerConfig` for
72
+ each field's exact meaning. Write a page with `defineCatalogPage()` from `@krapwoo/ds-viewer` (same
73
+ shape as a `SectionDef`, minus `id`/`path`/`props`, plus `component`, `group`, and `propNotes`).
74
+
75
+ `logo`'s own shape decides how it renders, not a separate setting: a roughly square or modestly
76
+ wide image appears as a small mark beside the name; one more than twice as wide as it is tall
77
+ replaces the name entirely as a wordmark. A missing file just warns and falls back to the name
78
+ only. Use PNG if your project transforms SVG imports into components (Metro would otherwise try to
79
+ bundle the logo as a React component, not an image).
55
80
 
56
81
  The package ships `native/catalog/` as TypeScript source, not pre-built JavaScript — your own
57
82
  project's `tsc`/Metro compiles it under your own compiler options, the same as any other local file.
58
83
 
84
+ ## Developing this repository
85
+
86
+ This repository views its own starter kit the same way a real user would: `kit-host/` is a small
87
+ Expo project that depends on `@krapwoo/ds-viewer` via `file:..` and points its `ds-viewer.config.ts`
88
+ at `../starter-kit/`. From the repo root:
89
+
90
+ ```bash
91
+ npm run kit:dev # builds the CLI, then opens the starter kit's own catalog
92
+ npm run check:catalog # the same headless check CI runs — every page, zero console errors
93
+ ```
94
+
95
+ `kit-host/` also hosts `viewer-pages/` — standalone catalog pages (not shipped in the package)
96
+ documenting the viewer framework's own pieces (`CatalogShell`, `SectionBlock`, `ComparisonGrid`,
97
+ …), shown under a "Viewer" group alongside the starter kit's own pages.
98
+
59
99
  ## Picking the right component
60
100
 
61
101
  Several components look alike but solve different problems (InputField vs. SearchField vs.
62
- Dropdown, Toast vs. Banner, Dialog vs. BottomSheet, …). See **[WHEN_TO_USE.md](./WHEN_TO_USE.md)**
102
+ Dropdown, Toast vs. Banner, Dialog vs. BottomSheet, …). See **[WHEN_TO_USE.md](./starter-kit/WHEN_TO_USE.md)**
63
103
  for the deciding question behind each pair before reaching for the closest-looking one. For a
64
- component's exact props/variants/states as structured data (not prose), see
65
- `native/catalog/manifest.ts`'s `buildComponentManifest()`, also rendered live at the catalog's
66
- "Manifest" page.
104
+ component's exact props as structured data (not prose), see `@krapwoo/ds-viewer/generated` (built
105
+ live by `sync` from source), also rendered live at the catalog's "Manifest" page.
67
106
 
68
107
  ## The one rule
69
108
 
@@ -72,24 +111,24 @@ rebrand a two-file edit. Read tokens directly: `import { DS_SEMANTIC } from '...
72
111
 
73
112
  ## Peer dependencies
74
113
 
75
- `react`, `react-native` assumed, plus: `react-native-svg` (icons + LoadingCircle),
114
+ `react`, `react-native` assumed, plus: `react-native-svg` (icons + Loading's circle variant),
76
115
  `react-native-safe-area-context` (catalog shell only — not required by the components themselves).
77
116
 
78
117
  ## Rebranding (make it yours)
79
118
 
80
- 1. **Palette** — edit `tokens/palette.ts`: swap the six hue scales for your brand's (keep the 0–800
119
+ 1. **Palette** — edit `starter-kit/tokens/palette.ts`: swap the six hue scales for your brand's (keep the 0–800
81
120
  shape).
82
- 2. **Semantic** — in `tokens/semantic.ts`, re-point any role you want to shift (e.g. make `emphasis.info`
121
+ 2. **Semantic** — in `starter-kit/tokens/semantic.ts`, re-point any role you want to shift (e.g. make `emphasis.info`
83
122
  your brand blue). Every component re-themes automatically.
84
- 3. **Type** — adjust `tokens/typography.ts` (sizes/weights) and set your font family at the app root.
123
+ 3. **Type** — adjust `starter-kit/tokens/typography.ts` (sizes/weights) and set your font family at the app root.
85
124
 
86
125
  Nothing else needs touching — components reference roles, not values.
87
126
 
88
127
  ## Using the components
89
128
 
90
129
  ```tsx
91
- import { Button } from '@ds/native/components/Button';
92
- import { Badge } from '@ds/native/components/Badge';
130
+ import { Button } from './src/ds/components/Button'; // after `init --new` (default --kit-root)
131
+ import { Badge } from './src/ds/components/Badge';
93
132
 
94
133
  <Button label="Save" variant="primary" onPress={save} />
95
134
  <Badge variant="positive" label="On time" leadingIcon="check" />
@@ -98,17 +137,10 @@ import { Badge } from '@ds/native/components/Badge';
98
137
  ## Browsing the catalog
99
138
 
100
139
  `native/catalog/` is a framework-only package (`CatalogShell`, `SectionBlock`, `PropsTable`, …) —
101
- it has no runnable app of its own. It ships two worked examples, both built the same way (drop
102
- either behind a dev-only route in your Expo app):
103
-
104
- - `native/catalog/CatalogExample.tsx` — **"Native App DS Template"**: documents this template's own
105
- DS components (Button, Card, Banner, …).
106
- - `native/catalog/CatalogFrameworkExample.tsx` — **"Design System DS Catalog"**: documents the
107
- catalog framework's own eight pieces (`CatalogShell`, `CatalogSidebar`, `CatalogSearchInput`,
108
- `SectionBlock`, `PropsTable`, `VariantGroup`, `TokenRow`, `DividedStack`) plus its own
109
- Colors/Spacing/Type Scale token pages (`native/catalog/tokens.ts` — `CATALOG_*`, independent of
110
- the host app's DS tokens) — a catalog of the catalog tool itself, useful when you're extending the
111
- framework rather than the DS.
140
+ it has no runnable app of its own and no hand-assembled example anymore. A page is just a
141
+ `*.catalog.tsx` file next to its component (or a standalone one for tokens/recipes), written with
142
+ `defineCatalogPage()`; `ds-viewer dev` discovers them, generates their props tables from your real
143
+ component source, and serves the whole thing as one app you never have to add a route for.
112
144
 
113
145
  Each catalog shows **one page at a time**: a persistent, searchable sidebar and the selected page.
114
146
  On web the page is kept in the URL fragment (`#Button`), so refresh, deep links, and back/forward
@@ -133,27 +165,17 @@ Grid columns and list cells are at most 402px wide with 16px padding. A page's `
133
165
  slots default to `wide`. The catalog's own look comes from `native/catalog/tokens.ts`, never from
134
166
  the host app's tokens.
135
167
 
136
- ```tsx
137
- import { CatalogExample } from '@ds/native/catalog/CatalogExample';
138
- // or: import { CatalogFrameworkExample } from '@ds/native/catalog/CatalogFrameworkExample';
139
- // e.g. render one from a `?ds=1` dev route, mirroring the pattern this template's source project used.
140
- ```
141
-
142
- For a quick browser preview without a host app, see `native-preview/` — a minimal throwaway Expo
143
- shell (not part of the reusable template) that renders both via `expo start --web`:
144
- - `http://localhost:5181/` → CatalogExample ("Native App DS Template")
145
- - `http://localhost:5181/?catalog=framework` → CatalogFrameworkExample ("Design System DS Catalog")
146
-
147
168
  ## Adding a component (the recipe)
148
169
 
149
- Copy the shape of an existing pair, e.g. `native/components/Button/` or `native/components/Badge/`:
150
- - `<Name>.tsx` — `StyleSheet.create`, import tokens from `../../../tokens`, icons from
151
- `../../../icons/Icon.native`.
170
+ Copy the shape of an existing pair, e.g. `starter-kit/components/Button/` or `starter-kit/components/Badge/`:
171
+ - `<Name>.tsx` — `StyleSheet.create`, import tokens from `../../tokens`, icons from
172
+ `../../icons/Icon.native`.
152
173
  - `<Name>.types.ts` — export `<Name>Props` (+ any `<Name>Variant`).
153
174
  - `index.ts` — re-export.
154
175
 
155
- Then add a `SectionDef` for it (id, path, description, props, a11y, and its content) to
156
- `native/catalog/CatalogExample.tsx`, and to `native/components/index.ts`. `SectionBlock` shows the
176
+ Then add a `<Name>.catalog.tsx` file next to it, written with `defineCatalogPage()` (description,
177
+ a11y, and its content — `id`/`path`/`props` are derived, never authored), and export it from
178
+ `src/ds/components/index.ts` the same way its siblings are. `SectionBlock` shows the
157
179
  specimens first, then Guidance, Quick reference, and Props. Give it whichever of these fields
158
180
  actually apply:
159
181
  - `comparison: { rowLabel, columnLabel, rows, columns, cells, size? }` — when two props genuinely
@@ -184,22 +206,104 @@ data, not a component — see `ColorsGallery`/`SpacingGallery`/`TypographyGaller
184
206
  output full width under a "Tokens" label and shows only a Quick reference card with the source
185
207
  path.
186
208
 
209
+ ## Keeping the catalog current
210
+
211
+ ### Binding examples to props
212
+
213
+ A grid's row or column axis can name the real prop it demonstrates:
214
+
215
+ ```tsx
216
+ import { defineCatalogPage, grid } from '@krapwoo/ds-viewer';
217
+
218
+ comparison: grid(
219
+ 'Variant', 'State',
220
+ { prop: 'variant', items: [
221
+ { key: 'primary', label: 'Primary' },
222
+ { key: 'ghost', label: 'Ghost' },
223
+ ] },
224
+ [{ key: 'default', label: 'Default' }, { key: 'disabled', label: 'Disabled' }],
225
+ (row, column) => <Button variant={row} disabled={column === 'disabled'} onPress={() => {}} />,
226
+ ),
227
+ ```
228
+
229
+ Only a prop typed as a string-literal union (2 or more options) can be bound this way; each item's
230
+ `key` must then be a real option of that prop. An axis that doesn't name a single real prop (e.g.
231
+ "which of three optional props is set," or a continuous `number`) stays a plain array of
232
+ `{ key, label }` — just as valid, and never flagged by `doctor`. `variants`/`states` list items use
233
+ the existing `props` tag (`{ key: 'ghost', props: { variant: 'ghost' }, node: ... }`) the same way.
234
+ Binding is what lets `doctor` tell you precisely when a renamed or removed option leaves a stale
235
+ example behind, instead of you finding out by reading the diff.
236
+
237
+ ### `npx ds-viewer doctor`
238
+
239
+ Reads every page with the TypeScript compiler — it never imports a page file or runs app code.
240
+ Human output is grouped by page, each issue with a one-line fix:
241
+
242
+ ```
243
+ Button
244
+ [error] bound-option-removed: The row "outline" is bound to "variant", which no longer has that option.
245
+ Fix: Delete this row, or update its key to a current option of "variant".
246
+
247
+ 0 errors, 1 warning — 38 components (37 with examples), 2 unbound examples.
248
+ ```
249
+
250
+ - `--json` prints the same report as stable, versioned data: `{ version: 1, summary: { errors, warnings, components, withExamples, unboundExamples }, update: null, issues: [{ id, severity, page?, file?, line?, message, fix }] }`. `update` is always `null` in this release (filled in starting 0.4's update check); `file` is always relative to the project root. `line` is populated for a `page-parse-error` found by typechecking a page file (see below), and always `undefined` for every other issue id — threading a real position through every rule is future work.
251
+ - `--ci` exits 1 only when an error was found — a warning alone never fails CI, and `--ci` never makes a network call.
252
+ - `doctor.strict: true` in `ds-viewer.config.ts` promotes every warning to an error, for both the summary counts and `--ci`'s exit code.
253
+ - A page whose layout data (`specimenSize`, `group`, grid `rows`/`columns`, list items' `key`/`name`/`props`/`group`, `propNotes` keys) isn't a literal — written as something other than a literal value, array, object, or a same-file `const` reference — gets one "page not statically checkable" warning. A page file that fails to parse *or typecheck* is `page-parse-error` instead: every page file is also run through the TypeScript type checker (the same way a component file already is), so a genuine syntax or type error in a page is reported with its line.
254
+
255
+ ## Known gaps
256
+
257
+ - The viewer's own dev-mode console warnings only cover `duplicate-page-id` (design §4's "the same rules appear as development warnings naming the page" — scoped to that one rule in 0.3). Bound-axis drift (`bound-option-removed`, `bound-axis-prop-removed`) has no viewer-side warning yet; `npx ds-viewer doctor` (and its `--ci` workflow) is the only place that catches it today.
258
+
259
+ ### `npx ds-viewer explain <Page>`
260
+
261
+ Prints each layout decision with its reason, using the exact same pure functions the catalog itself
262
+ renders with:
263
+
264
+ ```
265
+ Button
266
+ Grid (Variant × State)
267
+ 5 rows × 3 columns (regular); 3 of 3 max regular columns used — fits a 1280px laptop.
268
+ ```
269
+
270
+ Side-by-side placement depends on a real measured height, so by default `explain` reports it as
271
+ "decided in the viewer." Pass `--heights <firstBlockPx>,<secondBlockPx>` to check that decision here
272
+ too, assuming a 1280px laptop (952px content width) the same way `doctor`'s own column-limit check
273
+ does. `--json` is available on `explain` as well.
274
+
275
+ ### The `AGENTS.md` section and GitHub Action
276
+
277
+ `npx @krapwoo/ds-viewer init` (either path) appends a short section to `AGENTS.md` between
278
+ `<!-- ds-viewer:start v1 -->`/`<!-- ds-viewer:end -->` markers — creating the file if it doesn't
279
+ exist — telling an AI working on the project to update a page alongside its component, bind
280
+ examples to props, never invent grid cells or groups, and run `doctor`/`explain` before finishing.
281
+ It also writes `.github/workflows/ds-viewer.yml`, a SHA-pinned Action that installs dependencies
282
+ (picking `npm ci`, `pnpm install --frozen-lockfile`, or `yarn install --immutable` from whichever
283
+ lockfile the project has at that moment) and then runs `npx ds-viewer doctor --ci` on every pull
284
+ request. Both are written once; re-running `init` adds
285
+ either one only if it's missing (an `AGENTS.md` without the start marker counts as missing the
286
+ section, even if the file already has other content) and never touches anything outside its own
287
+ markers or its own file.
288
+
187
289
  ## What's included
188
290
 
189
- Tokens · Icons (44) · and generic components:
190
- Button · Badge · Divider · LoadingCircle · Card · NestedCard · SectionHeader · Banner · Status ·
191
- FieldContainer · InputField · TextArea · InputClearButton · SearchField · Pill · PillRow ·
192
- SegmentedToggle · UnderlineTabs · Toast · ProgressDots · Shimmer · Collapsible · AnimatedChevron.
291
+ Tokens · Icons (45) · and generic components:
292
+ AnimatedChevron · Avatar · Badge · Banner · BottomSheet · Button · ButtonGroup · Card · Checkbox ·
293
+ Dialog · Divider · Dock · Dropdown · EmptyState · FieldContainer · InputClearButton · InputField ·
294
+ List · ListItem · Loading · Pill · PillRow · ProgressDots · Radio · SearchField · SectionHeader ·
295
+ SegmentedToggle · Shimmer · Surface · Switch · TextArea · Toast · Tooltip · TopNav · UnderlineTabs.
193
296
 
194
- Deliberately **not** included (app/overlay-specific, port per project): gesture bottom sheets, nav
195
- bars, modals, and any domain components. They depend on navigation/gesture stacks that vary by app.
297
+ Deliberately **not** included: any domain-specific component. Everything here depends only on
298
+ generic navigation/gesture stacks (`BottomSheet`, `Dialog`, `Dock`, `TopNav` are included) — what's
299
+ left out is app content, not app chrome.
196
300
 
197
301
  ## Porting to another platform later
198
302
 
199
- The token layer (`tokens/`) and icon data (`icons/paths.ts`, `icons/types.ts`) are already
200
- platform-neutral — no React Native or DOM imports. Only `native/` (components + catalog) and the
201
- `Icon.native.tsx` renderer are RN-specific. When you need this design system on another platform
202
- (web, desktop via Tauri/Electron, etc.), that's a translation of `native/` against those same
203
- tokens and icon data — a `web/` (or other) tree was built this way once already for this exact
303
+ The token layer (`starter-kit/tokens/`) and icon data (`starter-kit/icons/paths.ts`,
304
+ `starter-kit/icons/types.ts`) are already platform-neutral — no React Native or DOM imports. Only
305
+ `starter-kit/components/` and the `Icon.native.tsx` renderer are RN-specific. When you need this
306
+ design system on another platform (web, desktop via Tauri/Electron, etc.), that's a translation of
307
+ `starter-kit/components/` against those same tokens and icon data — a `web/` (or other) tree was built this way once already for this exact
204
308
  template and later removed to keep the template single-platform until it's actually needed; ask
205
309
  for that port again when you're ready rather than maintaining an unused second tree in the meantime.
@@ -0,0 +1,31 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ const START_MARKER = '<!-- ds-viewer:start v1 -->';
4
+ const END_MARKER = '<!-- ds-viewer:end -->';
5
+ // Design §4's four numbered lines, verbatim — no heading added: the design names only these four
6
+ // lines as the section's content.
7
+ const SECTION_BODY = [
8
+ "1. When you add or change a component's props or a token, update its \`*.catalog.tsx\` in the same change.",
9
+ "2. Bind examples to props (\`prop\` on grid axes, \`props\` on list items) so drift is detected.",
10
+ '3. Never invent grid cells or groups; author only combinations that exist.',
11
+ "4. Run \`npx ds-viewer doctor\` before finishing and fix every error. Use \`npx ds-viewer explain <Page>\` to check layout; see the README's page-authoring guide.",
12
+ ].join('\n');
13
+ /** Design §4 "AGENTS.md section", §1 Ownership, §2 step 3. Appends between the exact markers,
14
+ * creating the file when absent — never touches a byte outside them, and never runs at all once
15
+ * the start marker is already present, even if the rest of the file looks nothing like what this
16
+ * function would have written (design: "An AGENTS.md without the markers counts as missing the
17
+ * section" — the converse is also true: a file WITH the marker is never rewritten in 0.3). */
18
+ export function ensureAgentsFileSection(projectRoot) {
19
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
20
+ const existing = existsSync(agentsPath) ? readFileSync(agentsPath, 'utf8') : '';
21
+ if (existing.includes(START_MARKER))
22
+ return [];
23
+ // Minor finding (Fable's review): a CRLF `AGENTS.md` must not end up with mixed line endings
24
+ // after the append — the separator and the whole section below use whichever newline the
25
+ // existing file already uses.
26
+ const newline = existing.includes('\r\n') ? '\r\n' : '\n';
27
+ const separator = existing === '' ? '' : existing.endsWith(`${newline}${newline}`) ? '' : existing.endsWith(newline) ? newline : `${newline}${newline}`;
28
+ const body = newline === '\n' ? SECTION_BODY : SECTION_BODY.replace(/\n/g, newline);
29
+ writeFileSync(agentsPath, `${existing}${separator}${START_MARKER}${newline}${body}${newline}${END_MARKER}${newline}`);
30
+ return [agentsPath];
31
+ }
@@ -55,6 +55,28 @@ export function loadConfig(configPath) {
55
55
  };
56
56
  return { config, warnings };
57
57
  }
58
+ /** Design §1 Ownership table — a missing `logo` file is a warning, never a hard error: the
59
+ * sidebar simply falls back to the name-only header. Resolved relative to `projectRoot` with
60
+ * `path.resolve` — the same way `cli/workspace.ts` resolves `config.logo` to actually `require`
61
+ * it — not `path.join`, which nests an absolute `logo` value *under* `projectRoot` instead of
62
+ * using it directly, misreporting a real file as "not found" (Minor finding, Fable's
63
+ * implementation review, cli/config.ts:74 vs cli/workspace.ts:134). A `logo` that resolves
64
+ * outside `projectRoot` is also rejected here, with its own clear warning: `cli/workspace.ts`
65
+ * only watches folders under `projectRoot`, so letting it through would surface later as an
66
+ * unclear Metro bundling error instead. */
67
+ export function validateLogo(projectRoot, logo) {
68
+ if (!logo)
69
+ return {};
70
+ const resolved = path.resolve(projectRoot, logo);
71
+ if (!existsSync(resolved)) {
72
+ return { warning: `Logo not found: ${logo} — showing the name instead.` };
73
+ }
74
+ const relativeToRoot = path.relative(projectRoot, resolved);
75
+ if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
76
+ return { warning: `Logo is outside the project root: ${logo} — showing the name instead.` };
77
+ }
78
+ return { logo };
79
+ }
58
80
  /** Loads `<projectRoot>/ds-viewer.config.ts`, attaches where it came from, and prints any
59
81
  * "unknown field" warning so every CLI command that resolves a config reports them the same way. */
60
82
  export function resolveConfig(projectRoot) {
@@ -62,5 +84,8 @@ export function resolveConfig(projectRoot) {
62
84
  const { config, warnings } = loadConfig(configPath);
63
85
  for (const warning of warnings)
64
86
  console.warn(warning);
65
- return { ...config, projectRoot, configPath };
87
+ const { logo, warning: logoWarning } = validateLogo(projectRoot, config.logo);
88
+ if (logoWarning)
89
+ console.warn(logoWarning);
90
+ return { ...config, logo, projectRoot, configPath };
66
91
  }
package/dist/cli/dev.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, watch } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { spawn } from 'node:child_process';
4
+ import { resolveConfig } from "./config.js";
4
5
  import { sync } from "./sync.js";
5
6
  import { writeWorkspace } from "./workspace.js";
6
7
  import { findFreePort } from "./port.js";
@@ -40,19 +41,34 @@ export function watchTargetFolders(config) {
40
41
  const patterns = [...config.components, ...(config.tokens ?? []), ...(config.pages ?? [])];
41
42
  return [...new Set(patterns.map((pattern) => path.join(config.projectRoot, globBaseFolder(pattern))))].filter((folder) => existsSync(folder));
42
43
  }
44
+ /** Re-resolves `<projectRoot>/ds-viewer.config.ts` from disk, syncs from it, and rewrites the
45
+ * generated workspace (entry.tsx, metro.config.js, …) from the freshly-resolved values — unlike
46
+ * plain `sync(config)`, which only ever regenerates `.ds-viewer/generated/` from whatever
47
+ * `ResolvedConfig` it was already given. `dev`'s config-file watcher (below) calls this instead
48
+ * of `sync` directly: without it, a changed `name` or `logo` never reached the generated
49
+ * workspace until `dev` was restarted, because `writeWorkspace` was never called again with the
50
+ * new value (controller end-to-end finding E1). Returns the freshly-resolved config so `dev` can
51
+ * keep watching with it. */
52
+ export function reloadWorkspace(projectRoot) {
53
+ const config = resolveConfig(projectRoot);
54
+ sync(config);
55
+ writeWorkspace(config);
56
+ return config;
57
+ }
43
58
  /** Design §2 "`dev`": checks the local install, runs `sync`, writes the workspace, starts Expo
44
59
  * web on the first free port from 5181 bound to localhost only, and watches the project for
45
60
  * component/token/page changes to re-run `sync`. Metro's own HMR updates the rendered component
46
61
  * when its source changes, but not the generated props table or page list (Task 17 Step 4 relies
47
62
  * on this: editing `Button.tsx` must update its props table, not just hot-reload the component). */
48
- export async function dev(config) {
49
- assertLocalInstall(config.projectRoot);
63
+ export async function dev(initialConfig) {
64
+ assertLocalInstall(initialConfig.projectRoot);
65
+ let config = initialConfig;
50
66
  sync(config);
51
67
  const workspace = writeWorkspace(config);
52
68
  const port = await findFreePort(5181, 5199);
53
69
  console.log(`Starting the catalog at http://localhost:${port}`);
54
70
  let debounceTimer;
55
- const onChange = (_eventType, relativePath) => {
71
+ const onSourceChange = (_eventType, relativePath) => {
56
72
  if (relativePath && !isWatchedPath(relativePath))
57
73
  return;
58
74
  clearTimeout(debounceTimer);
@@ -65,9 +81,25 @@ export async function dev(config) {
65
81
  }
66
82
  }, 200);
67
83
  };
84
+ // A distinct debounce/handler from `onSourceChange` above: a config change needs
85
+ // `reloadWorkspace` (re-resolve, then rewrite the generated workspace too), not just `sync` with
86
+ // the config this closure already captured — otherwise a changed `name`/`logo` never reaches
87
+ // `entry.tsx`/`metro.config.js` until `dev` is restarted (controller end-to-end finding E1).
88
+ let configDebounceTimer;
89
+ const onConfigChange = () => {
90
+ clearTimeout(configDebounceTimer);
91
+ configDebounceTimer = setTimeout(() => {
92
+ try {
93
+ config = reloadWorkspace(config.projectRoot);
94
+ }
95
+ catch (error) {
96
+ console.warn(`Could not reload ds-viewer.config.ts after a change: ${error.message}`);
97
+ }
98
+ }, 200);
99
+ };
68
100
  const watchers = [
69
- ...watchTargetFolders(config).map((folder) => watch(folder, { recursive: true }, onChange)),
70
- ...(existsSync(config.configPath) ? [watch(config.configPath, onChange)] : []),
101
+ ...watchTargetFolders(config).map((folder) => watch(folder, { recursive: true }, onSourceChange)),
102
+ ...(existsSync(config.configPath) ? [watch(config.configPath, onConfigChange)] : []),
71
103
  ];
72
104
  for (const watcher of watchers) {
73
105
  watcher.on('error', (error) => console.warn(`File watcher error: ${error.message}`));
@@ -83,4 +115,17 @@ export async function dev(config) {
83
115
  watcher.close();
84
116
  process.exitCode = code ?? 0;
85
117
  });
118
+ // Node's default SIGTERM/SIGINT behavior exits this process without touching its children —
119
+ // left alone, a signal sent only to `dev`'s own pid (not its process group) would leave the
120
+ // spawned `expo start` running, still bound to its port (Important finding, Fable correction
121
+ // pass: this is exactly what would hang `check:catalog`'s driving script, Task 11, if `dev`
122
+ // didn't clean up on its own).
123
+ for (const signal of ['SIGTERM', 'SIGINT']) {
124
+ process.on(signal, () => {
125
+ for (const watcher of watchers)
126
+ watcher.close();
127
+ child.kill(signal);
128
+ process.exit(0);
129
+ });
130
+ }
86
131
  }