@krapwoo/ds-viewer 0.1.0 → 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.
- package/README.md +100 -78
- package/dist/cli/config.js +26 -1
- package/dist/cli/dev.js +50 -5
- package/dist/cli/glob.js +8 -0
- package/dist/cli/init.js +148 -18
- package/dist/cli/main.js +102 -3
- package/dist/cli/props.js +166 -18
- package/dist/cli/sync.js +17 -1
- package/dist/cli/workspace.js +49 -21
- package/native/catalog/CatalogShell.tsx +6 -1
- package/native/catalog/CatalogSidebar.tsx +98 -5
- package/native/catalog/TokenRow.tsx +3 -2
- package/native/catalog/index.ts +6 -5
- package/native/catalog/logoLayout.ts +12 -0
- package/native/catalog/pageApi.ts +10 -1
- package/native/catalog/tokens.ts +5 -0
- package/package.json +11 -7
- package/starter-kit/WHEN_TO_USE.md +124 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
- package/starter-kit/components/AnimatedChevron/index.ts +2 -0
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +72 -0
- package/starter-kit/components/Avatar/Avatar.tsx +74 -0
- package/starter-kit/components/Avatar/index.ts +2 -0
- package/starter-kit/components/Badge/Badge.catalog.tsx +96 -0
- package/starter-kit/components/Badge/Badge.tsx +56 -0
- package/starter-kit/components/Badge/Badge.types.ts +18 -0
- package/starter-kit/components/Badge/index.ts +2 -0
- package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
- package/starter-kit/components/Banner/Banner.tsx +257 -0
- package/starter-kit/components/Banner/Banner.types.ts +38 -0
- package/starter-kit/components/Banner/index.ts +2 -0
- package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
- package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
- package/starter-kit/components/BottomSheet/index.ts +2 -0
- package/starter-kit/components/Button/Button.catalog.tsx +113 -0
- package/starter-kit/components/Button/Button.tsx +173 -0
- package/starter-kit/components/Button/Button.types.ts +52 -0
- package/starter-kit/components/Button/index.ts +2 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
- package/starter-kit/components/ButtonGroup/index.ts +2 -0
- package/starter-kit/components/Card/Card.catalog.tsx +68 -0
- package/starter-kit/components/Card/Card.tsx +133 -0
- package/starter-kit/components/Card/index.ts +2 -0
- package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
- package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
- package/starter-kit/components/Checkbox/index.ts +2 -0
- package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
- package/starter-kit/components/Dialog/Dialog.tsx +82 -0
- package/starter-kit/components/Dialog/index.ts +2 -0
- package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
- package/starter-kit/components/Divider/Divider.tsx +20 -0
- package/starter-kit/components/Divider/index.ts +2 -0
- package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
- package/starter-kit/components/Dock/Dock.tsx +58 -0
- package/starter-kit/components/Dock/index.ts +2 -0
- package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
- package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
- package/starter-kit/components/Dropdown/index.ts +2 -0
- package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
- package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
- package/starter-kit/components/EmptyState/index.ts +2 -0
- package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
- package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
- package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
- package/starter-kit/components/FieldContainer/index.ts +2 -0
- package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
- package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
- package/starter-kit/components/InputClearButton/index.ts +2 -0
- package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
- package/starter-kit/components/InputField/InputField.tsx +418 -0
- package/starter-kit/components/InputField/index.ts +2 -0
- package/starter-kit/components/List/List.catalog.tsx +30 -0
- package/starter-kit/components/List/List.tsx +36 -0
- package/starter-kit/components/List/index.ts +2 -0
- package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
- package/starter-kit/components/ListItem/ListItem.tsx +165 -0
- package/starter-kit/components/ListItem/index.ts +2 -0
- package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
- package/starter-kit/components/Loading/Loading.tsx +121 -0
- package/starter-kit/components/Loading/index.ts +2 -0
- package/starter-kit/components/Pill/Pill.catalog.tsx +93 -0
- package/starter-kit/components/Pill/Pill.tsx +181 -0
- package/starter-kit/components/Pill/Pill.types.ts +30 -0
- package/starter-kit/components/Pill/index.ts +2 -0
- package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
- package/starter-kit/components/PillRow/PillRow.tsx +140 -0
- package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
- package/starter-kit/components/PillRow/index.ts +2 -0
- package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
- package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
- package/starter-kit/components/ProgressDots/index.ts +2 -0
- package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
- package/starter-kit/components/Radio/Radio.tsx +114 -0
- package/starter-kit/components/Radio/index.ts +2 -0
- package/starter-kit/components/Radio/usePressScale.ts +33 -0
- package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
- package/starter-kit/components/SearchField/SearchField.tsx +98 -0
- package/starter-kit/components/SearchField/index.ts +2 -0
- package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
- package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
- package/starter-kit/components/SectionHeader/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
- package/starter-kit/components/SegmentedToggle/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
- package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
- package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
- package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
- package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
- package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
- package/starter-kit/components/Shimmer/index.ts +2 -0
- package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
- package/starter-kit/components/Surface/Surface.tsx +45 -0
- package/starter-kit/components/Surface/index.ts +2 -0
- package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
- package/starter-kit/components/Switch/Switch.tsx +129 -0
- package/starter-kit/components/Switch/index.ts +2 -0
- package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
- package/starter-kit/components/TextArea/TextArea.tsx +99 -0
- package/starter-kit/components/TextArea/index.ts +2 -0
- package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
- package/starter-kit/components/Toast/Toast.tsx +144 -0
- package/starter-kit/components/Toast/index.ts +2 -0
- package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
- package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
- package/starter-kit/components/Tooltip/index.ts +2 -0
- package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
- package/starter-kit/components/TopNav/TopNav.tsx +77 -0
- package/starter-kit/components/TopNav/index.ts +2 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
- package/starter-kit/components/UnderlineTabs/index.ts +2 -0
- package/starter-kit/components/index.ts +36 -0
- package/starter-kit/components/statusColors.ts +27 -0
- package/starter-kit/icons/Icon.native.tsx +90 -0
- package/starter-kit/icons/index.ts +10 -0
- package/starter-kit/icons/paths.ts +623 -0
- package/starter-kit/icons/types.ts +69 -0
- package/starter-kit/pages/Colors.catalog.tsx +55 -0
- package/starter-kit/pages/Font.catalog.tsx +45 -0
- package/starter-kit/pages/Icons.catalog.tsx +51 -0
- package/starter-kit/pages/Manifest.catalog.tsx +24 -0
- package/starter-kit/pages/Motion.catalog.tsx +101 -0
- package/starter-kit/pages/Radius.catalog.tsx +35 -0
- package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
- package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
- package/starter-kit/pages/Shadow.catalog.tsx +34 -0
- package/starter-kit/pages/Spacing.catalog.tsx +12 -0
- package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
- package/starter-kit/pages/Typography.catalog.tsx +17 -0
- package/starter-kit/tokens/index.ts +50 -0
- package/starter-kit/tokens/motion.ts +90 -0
- package/starter-kit/tokens/palette.ts +111 -0
- package/starter-kit/tokens/scales.ts +106 -0
- package/starter-kit/tokens/semantic.ts +98 -0
- package/starter-kit/tokens/shadow.ts +36 -0
- 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
|
-
├──
|
|
11
|
-
│ ├──
|
|
12
|
-
│ ├──
|
|
13
|
-
│ ├──
|
|
14
|
-
│
|
|
15
|
-
│ ├──
|
|
16
|
-
│ └──
|
|
17
|
-
├──
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
65
|
-
`
|
|
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 +
|
|
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 '
|
|
92
|
-
import { Badge } from '
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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. `
|
|
150
|
-
- `<Name>.tsx` — `StyleSheet.create`, import tokens from
|
|
151
|
-
|
|
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 `
|
|
156
|
-
|
|
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 (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
195
|
-
|
|
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`,
|
|
200
|
-
platform-neutral — no React Native or DOM imports. Only
|
|
201
|
-
`Icon.native.tsx` renderer are RN-specific. When you need this
|
|
202
|
-
(web, desktop via Tauri/Electron, etc.), that's a translation of
|
|
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.
|
package/dist/cli/config.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
49
|
-
assertLocalInstall(
|
|
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
|
|
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 },
|
|
70
|
-
...(existsSync(config.configPath) ? [watch(config.configPath,
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
...
|
|
122
|
-
|
|
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
|