@krapwoo/ds-viewer 0.1.1 → 0.2.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 (159) hide show
  1. package/README.md +100 -78
  2. package/dist/cli/config.js +26 -1
  3. package/dist/cli/dev.js +50 -5
  4. package/dist/cli/glob.js +8 -0
  5. package/dist/cli/init.js +148 -18
  6. package/dist/cli/main.js +66 -4
  7. package/dist/cli/props.js +166 -18
  8. package/dist/cli/sync.js +17 -1
  9. package/dist/cli/workspace.js +49 -21
  10. package/native/catalog/CatalogShell.tsx +6 -1
  11. package/native/catalog/CatalogSidebar.tsx +98 -5
  12. package/native/catalog/TokenRow.tsx +3 -2
  13. package/native/catalog/index.ts +6 -5
  14. package/native/catalog/logoLayout.ts +12 -0
  15. package/native/catalog/pageApi.ts +10 -1
  16. package/native/catalog/tokens.ts +5 -0
  17. package/package.json +8 -6
  18. package/starter-kit/WHEN_TO_USE.md +124 -0
  19. package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
  20. package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
  21. package/starter-kit/components/AnimatedChevron/index.ts +2 -0
  22. package/starter-kit/components/Avatar/Avatar.catalog.tsx +72 -0
  23. package/starter-kit/components/Avatar/Avatar.tsx +74 -0
  24. package/starter-kit/components/Avatar/index.ts +2 -0
  25. package/starter-kit/components/Badge/Badge.catalog.tsx +96 -0
  26. package/starter-kit/components/Badge/Badge.tsx +56 -0
  27. package/starter-kit/components/Badge/Badge.types.ts +18 -0
  28. package/starter-kit/components/Badge/index.ts +2 -0
  29. package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
  30. package/starter-kit/components/Banner/Banner.tsx +257 -0
  31. package/starter-kit/components/Banner/Banner.types.ts +38 -0
  32. package/starter-kit/components/Banner/index.ts +2 -0
  33. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
  34. package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
  35. package/starter-kit/components/BottomSheet/index.ts +2 -0
  36. package/starter-kit/components/Button/Button.catalog.tsx +113 -0
  37. package/starter-kit/components/Button/Button.tsx +173 -0
  38. package/starter-kit/components/Button/Button.types.ts +52 -0
  39. package/starter-kit/components/Button/index.ts +2 -0
  40. package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
  41. package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
  42. package/starter-kit/components/ButtonGroup/index.ts +2 -0
  43. package/starter-kit/components/Card/Card.catalog.tsx +68 -0
  44. package/starter-kit/components/Card/Card.tsx +133 -0
  45. package/starter-kit/components/Card/index.ts +2 -0
  46. package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
  47. package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
  48. package/starter-kit/components/Checkbox/index.ts +2 -0
  49. package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
  50. package/starter-kit/components/Dialog/Dialog.tsx +82 -0
  51. package/starter-kit/components/Dialog/index.ts +2 -0
  52. package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
  53. package/starter-kit/components/Divider/Divider.tsx +20 -0
  54. package/starter-kit/components/Divider/index.ts +2 -0
  55. package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
  56. package/starter-kit/components/Dock/Dock.tsx +58 -0
  57. package/starter-kit/components/Dock/index.ts +2 -0
  58. package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
  59. package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
  60. package/starter-kit/components/Dropdown/index.ts +2 -0
  61. package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
  62. package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
  63. package/starter-kit/components/EmptyState/index.ts +2 -0
  64. package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
  65. package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
  66. package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
  67. package/starter-kit/components/FieldContainer/index.ts +2 -0
  68. package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
  69. package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
  70. package/starter-kit/components/InputClearButton/index.ts +2 -0
  71. package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
  72. package/starter-kit/components/InputField/InputField.tsx +418 -0
  73. package/starter-kit/components/InputField/index.ts +2 -0
  74. package/starter-kit/components/List/List.catalog.tsx +30 -0
  75. package/starter-kit/components/List/List.tsx +36 -0
  76. package/starter-kit/components/List/index.ts +2 -0
  77. package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
  78. package/starter-kit/components/ListItem/ListItem.tsx +165 -0
  79. package/starter-kit/components/ListItem/index.ts +2 -0
  80. package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
  81. package/starter-kit/components/Loading/Loading.tsx +121 -0
  82. package/starter-kit/components/Loading/index.ts +2 -0
  83. package/starter-kit/components/Pill/Pill.catalog.tsx +93 -0
  84. package/starter-kit/components/Pill/Pill.tsx +181 -0
  85. package/starter-kit/components/Pill/Pill.types.ts +30 -0
  86. package/starter-kit/components/Pill/index.ts +2 -0
  87. package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
  88. package/starter-kit/components/PillRow/PillRow.tsx +140 -0
  89. package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
  90. package/starter-kit/components/PillRow/index.ts +2 -0
  91. package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
  92. package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
  93. package/starter-kit/components/ProgressDots/index.ts +2 -0
  94. package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
  95. package/starter-kit/components/Radio/Radio.tsx +114 -0
  96. package/starter-kit/components/Radio/index.ts +2 -0
  97. package/starter-kit/components/Radio/usePressScale.ts +33 -0
  98. package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
  99. package/starter-kit/components/SearchField/SearchField.tsx +98 -0
  100. package/starter-kit/components/SearchField/index.ts +2 -0
  101. package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
  102. package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
  103. package/starter-kit/components/SectionHeader/index.ts +2 -0
  104. package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
  105. package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
  106. package/starter-kit/components/SegmentedToggle/index.ts +2 -0
  107. package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
  108. package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
  109. package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
  110. package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
  111. package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
  112. package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
  113. package/starter-kit/components/Shimmer/index.ts +2 -0
  114. package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
  115. package/starter-kit/components/Surface/Surface.tsx +45 -0
  116. package/starter-kit/components/Surface/index.ts +2 -0
  117. package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
  118. package/starter-kit/components/Switch/Switch.tsx +129 -0
  119. package/starter-kit/components/Switch/index.ts +2 -0
  120. package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
  121. package/starter-kit/components/TextArea/TextArea.tsx +99 -0
  122. package/starter-kit/components/TextArea/index.ts +2 -0
  123. package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
  124. package/starter-kit/components/Toast/Toast.tsx +144 -0
  125. package/starter-kit/components/Toast/index.ts +2 -0
  126. package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
  127. package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
  128. package/starter-kit/components/Tooltip/index.ts +2 -0
  129. package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
  130. package/starter-kit/components/TopNav/TopNav.tsx +77 -0
  131. package/starter-kit/components/TopNav/index.ts +2 -0
  132. package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
  133. package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
  134. package/starter-kit/components/UnderlineTabs/index.ts +2 -0
  135. package/starter-kit/components/index.ts +36 -0
  136. package/starter-kit/components/statusColors.ts +27 -0
  137. package/starter-kit/icons/Icon.native.tsx +90 -0
  138. package/starter-kit/icons/index.ts +10 -0
  139. package/starter-kit/icons/paths.ts +623 -0
  140. package/starter-kit/icons/types.ts +69 -0
  141. package/starter-kit/pages/Colors.catalog.tsx +55 -0
  142. package/starter-kit/pages/Font.catalog.tsx +45 -0
  143. package/starter-kit/pages/Icons.catalog.tsx +51 -0
  144. package/starter-kit/pages/Manifest.catalog.tsx +24 -0
  145. package/starter-kit/pages/Motion.catalog.tsx +101 -0
  146. package/starter-kit/pages/Radius.catalog.tsx +35 -0
  147. package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
  148. package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
  149. package/starter-kit/pages/Shadow.catalog.tsx +34 -0
  150. package/starter-kit/pages/Spacing.catalog.tsx +12 -0
  151. package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
  152. package/starter-kit/pages/Typography.catalog.tsx +17 -0
  153. package/starter-kit/tokens/index.ts +50 -0
  154. package/starter-kit/tokens/motion.ts +90 -0
  155. package/starter-kit/tokens/palette.ts +111 -0
  156. package/starter-kit/tokens/scales.ts +106 -0
  157. package/starter-kit/tokens/semantic.ts +98 -0
  158. package/starter-kit/tokens/shadow.ts +36 -0
  159. package/starter-kit/tokens/typography.ts +72 -0
package/README.md CHANGED
@@ -7,63 +7,100 @@ 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
49
64
  ```
50
65
 
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`).
66
+ `ds-viewer.config.ts` fields: `name`, `logo` (optional — a path to an image asset, shown in the
67
+ sidebar beside the name, or in place of it for a wide wordmark; validated at `sync`/`dev` time
68
+ and bundled into the preview workspace), `components`, `exclude`, `tokens`, `pages`, `groupOrder`, `starterKit` (new-project only —
69
+ records the kit's version), `updateCheck`, `doctor` — see `config/index.ts`'s `DsViewerConfig` for
70
+ each field's exact meaning. Write a page with `defineCatalogPage()` from `@krapwoo/ds-viewer` (same
71
+ shape as a `SectionDef`, minus `id`/`path`/`props`, plus `component`, `group`, and `propNotes`).
72
+
73
+ `logo`'s own shape decides how it renders, not a separate setting: a roughly square or modestly
74
+ wide image appears as a small mark beside the name; one more than twice as wide as it is tall
75
+ replaces the name entirely as a wordmark. A missing file just warns and falls back to the name
76
+ only. Use PNG if your project transforms SVG imports into components (Metro would otherwise try to
77
+ bundle the logo as a React component, not an image).
55
78
 
56
79
  The package ships `native/catalog/` as TypeScript source, not pre-built JavaScript — your own
57
80
  project's `tsc`/Metro compiles it under your own compiler options, the same as any other local file.
58
81
 
82
+ ## Developing this repository
83
+
84
+ This repository views its own starter kit the same way a real user would: `kit-host/` is a small
85
+ Expo project that depends on `@krapwoo/ds-viewer` via `file:..` and points its `ds-viewer.config.ts`
86
+ at `../starter-kit/`. From the repo root:
87
+
88
+ ```bash
89
+ npm run kit:dev # builds the CLI, then opens the starter kit's own catalog
90
+ npm run check:catalog # the same headless check CI runs — every page, zero console errors
91
+ ```
92
+
93
+ `kit-host/` also hosts `viewer-pages/` — standalone catalog pages (not shipped in the package)
94
+ documenting the viewer framework's own pieces (`CatalogShell`, `SectionBlock`, `ComparisonGrid`,
95
+ …), shown under a "Viewer" group alongside the starter kit's own pages.
96
+
59
97
  ## Picking the right component
60
98
 
61
99
  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)**
100
+ Dropdown, Toast vs. Banner, Dialog vs. BottomSheet, …). See **[WHEN_TO_USE.md](./starter-kit/WHEN_TO_USE.md)**
63
101
  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.
102
+ component's exact props as structured data (not prose), see `@krapwoo/ds-viewer/generated` (built
103
+ live by `sync` from source), also rendered live at the catalog's "Manifest" page.
67
104
 
68
105
  ## The one rule
69
106
 
@@ -72,24 +109,24 @@ rebrand a two-file edit. Read tokens directly: `import { DS_SEMANTIC } from '...
72
109
 
73
110
  ## Peer dependencies
74
111
 
75
- `react`, `react-native` assumed, plus: `react-native-svg` (icons + LoadingCircle),
112
+ `react`, `react-native` assumed, plus: `react-native-svg` (icons + Loading's circle variant),
76
113
  `react-native-safe-area-context` (catalog shell only — not required by the components themselves).
77
114
 
78
115
  ## Rebranding (make it yours)
79
116
 
80
- 1. **Palette** — edit `tokens/palette.ts`: swap the six hue scales for your brand's (keep the 0–800
117
+ 1. **Palette** — edit `starter-kit/tokens/palette.ts`: swap the six hue scales for your brand's (keep the 0–800
81
118
  shape).
82
- 2. **Semantic** — in `tokens/semantic.ts`, re-point any role you want to shift (e.g. make `emphasis.info`
119
+ 2. **Semantic** — in `starter-kit/tokens/semantic.ts`, re-point any role you want to shift (e.g. make `emphasis.info`
83
120
  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.
121
+ 3. **Type** — adjust `starter-kit/tokens/typography.ts` (sizes/weights) and set your font family at the app root.
85
122
 
86
123
  Nothing else needs touching — components reference roles, not values.
87
124
 
88
125
  ## Using the components
89
126
 
90
127
  ```tsx
91
- import { Button } from '@ds/native/components/Button';
92
- import { Badge } from '@ds/native/components/Badge';
128
+ import { Button } from './src/ds/components/Button'; // after `init --new` (default --kit-root)
129
+ import { Badge } from './src/ds/components/Badge';
93
130
 
94
131
  <Button label="Save" variant="primary" onPress={save} />
95
132
  <Badge variant="positive" label="On time" leadingIcon="check" />
@@ -98,17 +135,10 @@ import { Badge } from '@ds/native/components/Badge';
98
135
  ## Browsing the catalog
99
136
 
100
137
  `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.
138
+ it has no runnable app of its own and no hand-assembled example anymore. A page is just a
139
+ `*.catalog.tsx` file next to its component (or a standalone one for tokens/recipes), written with
140
+ `defineCatalogPage()`; `ds-viewer dev` discovers them, generates their props tables from your real
141
+ component source, and serves the whole thing as one app you never have to add a route for.
112
142
 
113
143
  Each catalog shows **one page at a time**: a persistent, searchable sidebar and the selected page.
114
144
  On web the page is kept in the URL fragment (`#Button`), so refresh, deep links, and back/forward
@@ -133,27 +163,17 @@ Grid columns and list cells are at most 402px wide with 16px padding. A page's `
133
163
  slots default to `wide`. The catalog's own look comes from `native/catalog/tokens.ts`, never from
134
164
  the host app's tokens.
135
165
 
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
166
  ## Adding a component (the recipe)
148
167
 
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`.
168
+ Copy the shape of an existing pair, e.g. `starter-kit/components/Button/` or `starter-kit/components/Badge/`:
169
+ - `<Name>.tsx` — `StyleSheet.create`, import tokens from `../../tokens`, icons from
170
+ `../../icons/Icon.native`.
152
171
  - `<Name>.types.ts` — export `<Name>Props` (+ any `<Name>Variant`).
153
172
  - `index.ts` — re-export.
154
173
 
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
174
+ Then add a `<Name>.catalog.tsx` file next to it, written with `defineCatalogPage()` (description,
175
+ a11y, and its content — `id`/`path`/`props` are derived, never authored), and export it from
176
+ `src/ds/components/index.ts` the same way its siblings are. `SectionBlock` shows the
157
177
  specimens first, then Guidance, Quick reference, and Props. Give it whichever of these fields
158
178
  actually apply:
159
179
  - `comparison: { rowLabel, columnLabel, rows, columns, cells, size? }` — when two props genuinely
@@ -186,20 +206,22 @@ path.
186
206
 
187
207
  ## What's included
188
208
 
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.
209
+ Tokens · Icons (45) · and generic components:
210
+ AnimatedChevron · Avatar · Badge · Banner · BottomSheet · Button · ButtonGroup · Card · Checkbox ·
211
+ Dialog · Divider · Dock · Dropdown · EmptyState · FieldContainer · InputClearButton · InputField ·
212
+ List · ListItem · Loading · Pill · PillRow · ProgressDots · Radio · SearchField · SectionHeader ·
213
+ SegmentedToggle · Shimmer · Surface · Switch · TextArea · Toast · Tooltip · TopNav · UnderlineTabs.
193
214
 
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.
215
+ Deliberately **not** included: any domain-specific component. Everything here depends only on
216
+ generic navigation/gesture stacks (`BottomSheet`, `Dialog`, `Dock`, `TopNav` are included) — what's
217
+ left out is app content, not app chrome.
196
218
 
197
219
  ## Porting to another platform later
198
220
 
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
221
+ The token layer (`starter-kit/tokens/`) and icon data (`starter-kit/icons/paths.ts`,
222
+ `starter-kit/icons/types.ts`) are already platform-neutral — no React Native or DOM imports. Only
223
+ `starter-kit/components/` and the `Icon.native.tsx` renderer are RN-specific. When you need this
224
+ design system on another platform (web, desktop via Tauri/Electron, etc.), that's a translation of
225
+ `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
226
  template and later removed to keep the template single-platform until it's actually needed; ask
205
227
  for that port again when you're ready rather than maintaining an unused second tree in the meantime.
@@ -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
  }
package/dist/cli/glob.js CHANGED
@@ -30,6 +30,14 @@ function walk(root, segments, consumed) {
30
30
  }
31
31
  }
32
32
  const [segment, ...rest] = segments;
33
+ // `readdirSync` never returns "." or ".." as entries, so without this a config glob that climbs
34
+ // out of its own root (e.g. kit-host's `../starter-kit/components/*/index.ts`, Task 5) would
35
+ // regex-match against real directory entries and silently resolve to nothing. `path.join` below
36
+ // already collapses "." and ".." correctly once they're part of `consumed` — this just lets them
37
+ // reach it instead of being treated as a literal name to search for.
38
+ if (segment === '..' || segment === '.') {
39
+ return walk(root, rest, [...consumed, segment]);
40
+ }
33
41
  const dir = path.join(root, ...consumed);
34
42
  let entries;
35
43
  try {
package/dist/cli/init.js CHANGED
@@ -1,6 +1,7 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createInterface } from 'node:readline/promises';
4
+ import { execFileSync } from 'node:child_process';
4
5
  import { preflight } from "./preflight.js";
5
6
  import { detectComponentFolders, detectTokenFiles } from "./detect.js";
6
7
  async function promptConfirm(message) {
@@ -15,25 +16,28 @@ async function promptConfirm(message) {
15
16
  rl.close();
16
17
  return /^y(es)?$/i.test(answer.trim());
17
18
  }
18
- /** This package's own version — used as the `^x.y.z` devDependency version written below, since a
19
- * project run via `npx @krapwoo/ds-viewer init` has no local copy to read its version from yet.
20
- * Walks up from this file's own directory looking for the `package.json` named
21
- * `@krapwoo/ds-viewer`, rather than a fixed relative path: this file sits one level under the
22
- * repo root as `cli/init.ts` (how the test below runs it) but two levels under it once compiled,
23
- * as `dist/cli/init.js` — a single `../..` would be wrong for one of the two. */
24
- function ownVersion() {
19
+ /** This package's own installed root — needed both for `ownVersion()` below and (new in 0.2) to
20
+ * find the bundled `starter-kit/` to copy from. Walks up from this file's own directory looking
21
+ * for the `package.json` named `@krapwoo/ds-viewer`, rather than a fixed relative path: this file
22
+ * sits one level under the repo root as `cli/init.ts` (how tests run it) but two levels under it
23
+ * once compiled, as `dist/cli/init.js` — a single `../..` would be wrong for one of the two. */
24
+ function packageRoot() {
25
25
  let dir = import.meta.dirname;
26
26
  for (let depth = 0; depth < 5; depth += 1) {
27
27
  const candidate = path.join(dir, 'package.json');
28
28
  if (existsSync(candidate)) {
29
29
  const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
30
30
  if (pkg.name === '@krapwoo/ds-viewer')
31
- return pkg.version;
31
+ return dir;
32
32
  }
33
33
  dir = path.dirname(dir);
34
34
  }
35
35
  throw new Error("Could not find @krapwoo/ds-viewer's own package.json to read its version.");
36
36
  }
37
+ function ownVersion() {
38
+ const pkg = JSON.parse(readFileSync(path.join(packageRoot(), 'package.json'), 'utf8'));
39
+ return pkg.version;
40
+ }
37
41
  /** Detects the existing indent of a JSON file's first indented line, so rewriting it (e.g.
38
42
  * `package.json` below) doesn't reformat the user's own indentation style. Falls back to two
39
43
  * spaces when nothing indented is found (e.g. a minified `package.json`). */
@@ -85,6 +89,25 @@ export async function initExistingProject(projectRoot, options = {}) {
85
89
  written.push(draftPath);
86
90
  }
87
91
  }
92
+ written.push(...ensureSharedProjectFiles(projectRoot));
93
+ const routerWarnings = componentFolders
94
+ .filter((folder) => folder.relativePath.split(path.sep)[0] === 'app')
95
+ .map((folder) => `Warning: ${folder.relativePath.split(path.sep).join('/')} is inside an Expo Router "app/" directory; its .catalog.tsx file will become a route.`);
96
+ const messages = [
97
+ ...warnings.map((issue) => `Warning: ${issue.package} not found. Run: ${issue.installCommand}`),
98
+ ...routerWarnings,
99
+ // "with catalog pages", not "with examples": this counts any existing `<Name>.catalog.tsx`,
100
+ // including init's own drafts, which explicitly say "Needs examples" — not real coverage yet.
101
+ `${componentFolders.length} components · ${withExamplesCount} with catalog pages`,
102
+ 'Next: npm install, then npm run ds-viewer dev',
103
+ ];
104
+ return { messages, written };
105
+ }
106
+ /** Design §2, "Both paths then write what is missing": `ds-viewer.config.ts` is written by each
107
+ * path separately (its contents differ), but the package script, the devDependency entry, and the
108
+ * `.gitignore` entry are identical either way. */
109
+ function ensureSharedProjectFiles(projectRoot) {
110
+ const written = [];
88
111
  const packageJsonPath = path.join(projectRoot, 'package.json');
89
112
  const packageJsonRaw = readFileSync(packageJsonPath, 'utf8');
90
113
  const packageJson = JSON.parse(packageJsonRaw);
@@ -106,26 +129,133 @@ export async function initExistingProject(projectRoot, options = {}) {
106
129
  const gitignorePath = path.join(projectRoot, '.gitignore');
107
130
  const gitignoreEntry = '.ds-viewer/';
108
131
  const existingGitignore = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
109
- // `\r\n` line endings leave a trailing `\r` on each split line, which never equals the entry —
110
- // trim it per line so a CRLF .gitignore is recognized as already having the entry on a rerun.
111
132
  if (!existingGitignore.split('\n').some((line) => line.replace(/\r$/, '') === gitignoreEntry)) {
112
133
  const separator = existingGitignore === '' || existingGitignore.endsWith('\n') ? '' : '\n';
113
134
  writeFileSync(gitignorePath, `${existingGitignore}${separator}${gitignoreEntry}\n`);
114
135
  written.push(gitignorePath);
115
136
  }
116
- const routerWarnings = componentFolders
117
- .filter((folder) => folder.relativePath.split(path.sep)[0] === 'app')
118
- .map((folder) => `Warning: ${folder.relativePath.split(path.sep).join('/')} is inside an Expo Router "app/" directory; its .catalog.tsx file will become a route.`);
137
+ return written;
138
+ }
139
+ const KIT_REQUIRED_PACKAGES = ['react-native-svg', 'react-native-safe-area-context'];
140
+ /** The starter kit's own intended sidebar order (the same list kit-host/ds-viewer.config.ts
141
+ * declares, minus "Viewer" — that group only holds kit-host's own framework-docs pages under
142
+ * `viewer-pages/`, which `init --new` never copies). Written into a fresh project's config so its
143
+ * sidebar isn't left alphabetical (controller end-to-end finding E3). */
144
+ const KIT_GROUP_ORDER = [
145
+ 'Actions', 'Surfaces', 'Inputs', 'Controls', 'Selection', 'Feedback', 'Navigation',
146
+ 'Overlays', 'Layout', 'Sub-Parts', 'Recipes', 'Tokens', 'Reference',
147
+ ];
148
+ /** Default `installer` — a real `npx expo install`, run only for whichever of
149
+ * `KIT_REQUIRED_PACKAGES` the project doesn't already depend on. Test-injected in every unit test
150
+ * (this plan's Global Constraints: never run a real `expo install` in tests). */
151
+ function defaultInstaller(projectRoot, missingPackages) {
152
+ if (missingPackages.length === 0)
153
+ return;
154
+ // Same `shell: process.platform === 'win32'` reasoning as `dev.ts`'s existing `spawn('npx', ...)`
155
+ // call: Windows resolves `npx` via `npx.cmd`, which `execFileSync` only finds through a shell.
156
+ execFileSync('npx', ['expo', 'install', ...missingPackages], { cwd: projectRoot, stdio: 'inherit', shell: process.platform === 'win32' });
157
+ }
158
+ /** Copies every file under `srcDir` into `destDir`, skipping any destination file that already
159
+ * exists (design's "never overwrites user files" rule — Global Constraints) and reporting, not
160
+ * throwing on, one unreadable source file (Error-handling rule). Also reports, rather than
161
+ * throwing on, an unreadable nested folder — the same `readdirSync` call recurses, so wrapping it
162
+ * here covers every depth — so files already copied before it are still returned, and skips a
163
+ * symlinked entry (neither a plain file nor a directory `copyKitIfMissing` should recurse into)
164
+ * instead of copying through it (Minor finding, Fable's implementation review,
165
+ * cli/init.ts:205-224). Returns every path it wrote. */
166
+ export function copyKitIfMissing(srcDir, destDir) {
167
+ const written = [];
168
+ mkdirSync(destDir, { recursive: true });
169
+ let entries;
170
+ try {
171
+ entries = readdirSync(srcDir, { withFileTypes: true });
172
+ }
173
+ catch (error) {
174
+ console.warn(`Could not read ${srcDir}: ${error.message}`);
175
+ return written;
176
+ }
177
+ for (const entry of entries) {
178
+ if (entry.isSymbolicLink())
179
+ continue;
180
+ const srcPath = path.join(srcDir, entry.name);
181
+ const destPath = path.join(destDir, entry.name);
182
+ if (entry.isDirectory()) {
183
+ written.push(...copyKitIfMissing(srcPath, destPath));
184
+ continue;
185
+ }
186
+ if (existsSync(destPath))
187
+ continue;
188
+ try {
189
+ copyFileSync(srcPath, destPath);
190
+ written.push(destPath);
191
+ }
192
+ catch (error) {
193
+ console.warn(`Could not copy ${path.relative(srcDir, srcPath)}: ${error.message}`);
194
+ }
195
+ }
196
+ return written;
197
+ }
198
+ function newProjectConfigContents(kitRoot, starterKitVersion) {
199
+ return `import { defineConfig } from '@krapwoo/ds-viewer/config';
200
+
201
+ export default defineConfig({
202
+ name: 'My App',
203
+ components: ['${kitRoot}/components/*/index.ts'],
204
+ tokens: ['${kitRoot}/tokens/index.ts'],
205
+ pages: ['${kitRoot}/pages/*.catalog.tsx'],
206
+ groupOrder: ${JSON.stringify(KIT_GROUP_ORDER)},
207
+ starterKit: { version: '${starterKitVersion}' },
208
+ });
209
+ `;
210
+ }
211
+ /** The new-project half of `init` (design §2's "new project" column) — existing-project `init` is
212
+ * `initExistingProject`, 0.1 scope. No confirmation prompt here: unlike the existing-project path,
213
+ * there is nothing to detect or confirm — the kit root is the only user choice, and it's a flag. */
214
+ export async function initNewProject(projectRoot, options = {}) {
215
+ const { errors, warnings } = preflight(projectRoot);
216
+ if (errors.length > 0) {
217
+ return { messages: errors.map((issue) => `Missing ${issue.package}. Run: ${issue.installCommand}`), written: [], exitCode: 1 };
218
+ }
219
+ const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
220
+ const installedPackages = { ...packageJson.dependencies, ...packageJson.devDependencies };
221
+ const missingKitPackages = KIT_REQUIRED_PACKAGES.filter((name) => !installedPackages[name]);
222
+ await (options.installer ?? defaultInstaller)(projectRoot, missingKitPackages);
223
+ const kitRoot = options.kitRoot ?? 'src/ds';
224
+ const written = copyKitIfMissing(path.join(packageRoot(), 'starter-kit'), path.join(projectRoot, kitRoot));
225
+ // Read before `ensureSharedProjectFiles`/the config write below add their own entries — this is
226
+ // only true when `copyKitIfMissing` itself wrote nothing, i.e. a rerun found every kit file
227
+ // already present (controller end-to-end finding E2: a second `init --new` claimed to have
228
+ // copied the kit even though nothing changed on disk).
229
+ const copiedKitFiles = written.length > 0;
230
+ const configPath = path.join(projectRoot, 'ds-viewer.config.ts');
231
+ if (!existsSync(configPath)) {
232
+ writeFileSync(configPath, newProjectConfigContents(kitRoot, ownVersion()));
233
+ written.push(configPath);
234
+ }
235
+ written.push(...ensureSharedProjectFiles(projectRoot));
119
236
  const messages = [
120
237
  ...warnings.map((issue) => `Warning: ${issue.package} not found. Run: ${issue.installCommand}`),
121
- ...routerWarnings,
122
- // "with catalog pages", not "with examples": this counts any existing `<Name>.catalog.tsx`,
123
- // including init's own drafts, which explicitly say "Needs examples" — not real coverage yet.
124
- `${componentFolders.length} components · ${withExamplesCount} with catalog pages`,
238
+ ...(missingKitPackages.length > 0 ? [`Installed ${missingKitPackages.join(', ')}.`] : []),
239
+ copiedKitFiles ? `Copied the starter kit into ${kitRoot}/.` : `Starter kit already present in ${kitRoot}/ — nothing copied.`,
125
240
  'Next: npm install, then npm run ds-viewer dev',
126
241
  ];
127
242
  return { messages, written };
128
243
  }
244
+ /** The one new-vs-existing question (design §2's "One question") — `--new`/`--existing` (wired in
245
+ * `cli/main.ts`) skip it entirely; this is only reached with neither flag. */
246
+ export async function promptInitMode() {
247
+ if (!process.stdin.isTTY)
248
+ return undefined;
249
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
250
+ const answer = await rl.question('New or existing project? [new/existing] ');
251
+ rl.close();
252
+ const normalized = answer.trim().toLowerCase();
253
+ if (normalized === 'new' || normalized === 'n')
254
+ return 'new';
255
+ if (normalized === 'existing' || normalized === 'e')
256
+ return 'existing';
257
+ return undefined;
258
+ }
129
259
  function configFileContents(componentFolders, tokenFiles) {
130
260
  // Emits both `index.ts` and `index.tsx` globs for every detected parent folder whenever any
131
261
  // detected folder uses `.tsx` (`detect.ts` accepts either extension — Important: a project using