@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/dist/cli/main.js
CHANGED
|
@@ -1,14 +1,113 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { resolveConfig } from "./config.js";
|
|
4
|
-
import { initExistingProject } from "./init.js";
|
|
6
|
+
import { initExistingProject, initNewProject, promptInitMode } from "./init.js";
|
|
5
7
|
import { dev } from "./dev.js";
|
|
6
8
|
import { sync } from "./sync.js";
|
|
9
|
+
const USAGE = `Usage: ds-viewer <command>
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
init Set up the viewer in this project: config, catalog pages, and an npm script
|
|
13
|
+
--new start from the starter kit (new project)
|
|
14
|
+
--existing detect this project's own components/tokens (existing project)
|
|
15
|
+
--kit-root DIR where to copy the starter kit (--new only; default src/ds)
|
|
16
|
+
--yes accept the detected components and tokens without asking (--existing only)
|
|
17
|
+
sync Regenerate the component, props, token, and page data from source
|
|
18
|
+
dev Sync, then serve the catalog on a local web address and keep it current as files change
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-h, --help Show this help
|
|
22
|
+
-v, --version Show the installed version`;
|
|
23
|
+
/** Walks up from this file (cli/ in source, dist/cli/ when installed) to the package's own package.json. */
|
|
24
|
+
function packageVersion() {
|
|
25
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
while (true) {
|
|
27
|
+
const candidate = path.join(dir, 'package.json');
|
|
28
|
+
if (existsSync(candidate)) {
|
|
29
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
30
|
+
if (pkg.name === '@krapwoo/ds-viewer' && pkg.version)
|
|
31
|
+
return pkg.version;
|
|
32
|
+
}
|
|
33
|
+
const parent = path.dirname(dir);
|
|
34
|
+
if (parent === dir)
|
|
35
|
+
return 'unknown';
|
|
36
|
+
dir = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** `--kit-root`'s raw CLI value, normalized to a project-root-relative, forward-slash path — or an
|
|
40
|
+
* error when the value can't be made into one. Without this: an absolute value was silently
|
|
41
|
+
* relocated *under* the project root by `path.join` (which never does `path.resolve`'s "a later
|
|
42
|
+
* absolute segment replaces everything before it" — it just concatenates), a trailing slash
|
|
43
|
+
* produced a doubled-slash glob (`src/ds//components/*`), and a quote broke the generated
|
|
44
|
+
* config's TypeScript string literal (Minor finding, Fable's implementation review,
|
|
45
|
+
* cli/main.ts:59-60 & cli/init.ts:231-233,257). */
|
|
46
|
+
export function parseKitRoot(projectRoot, rawValue) {
|
|
47
|
+
if (rawValue.includes("'") || rawValue.includes('"')) {
|
|
48
|
+
return { error: `--kit-root cannot contain a quote (got "${rawValue}")` };
|
|
49
|
+
}
|
|
50
|
+
const relative = path.relative(projectRoot, path.resolve(projectRoot, rawValue));
|
|
51
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
52
|
+
return { error: `--kit-root must stay inside the project root (got "${rawValue}")` };
|
|
53
|
+
}
|
|
54
|
+
return { value: relative.split(path.sep).join('/') };
|
|
55
|
+
}
|
|
56
|
+
/** Parses `init`'s flags and dispatches to `initNewProject`/`initExistingProject` — pulled out of
|
|
57
|
+
* `main` so the dispatch (which flags pick which project path, `--yes`'s 0.1-compatible default,
|
|
58
|
+
* `--kit-root`'s normalization) is unit-testable with an injected installer, never a real `expo
|
|
59
|
+
* install` (Minor finding, Fable's implementation review: "No CLI test covers `--yes` defaulting
|
|
60
|
+
* to existing, `--kit-root` parsing, or `--new` dispatch"). */
|
|
61
|
+
export async function runInit(projectRoot, rest, options = {}) {
|
|
62
|
+
const kitRootIndex = rest.indexOf('--kit-root');
|
|
63
|
+
// Validated, not just indexed: a bare trailing `--kit-root` (no value) or `--kit-root --yes`
|
|
64
|
+
// (the next token is itself a flag) would otherwise silently pass `undefined`/`'--yes'` as the
|
|
65
|
+
// kit root. Both are treated as "no value given", the same as omitting the flag entirely.
|
|
66
|
+
const kitRootValue = kitRootIndex !== -1 ? rest[kitRootIndex + 1] : undefined;
|
|
67
|
+
const kitRootRaw = kitRootValue && !kitRootValue.startsWith('--') ? kitRootValue : undefined;
|
|
68
|
+
let kitRoot;
|
|
69
|
+
if (kitRootRaw !== undefined) {
|
|
70
|
+
const parsed = parseKitRoot(projectRoot, kitRootRaw);
|
|
71
|
+
if (parsed.error)
|
|
72
|
+
return { messages: [parsed.error], written: [], exitCode: 1 };
|
|
73
|
+
kitRoot = parsed.value;
|
|
74
|
+
}
|
|
75
|
+
let mode = rest.includes('--new') ? 'new' : rest.includes('--existing') ? 'existing' : undefined;
|
|
76
|
+
// 0.1 had only one `init` path (today's `initExistingProject`) and no mode question at all —
|
|
77
|
+
// `--yes` alone must keep working exactly as it did for a 0.1 user or script, so `--yes` with
|
|
78
|
+
// neither `--new` nor `--existing` defaults to `existing`, the same path 0.1's `--yes` always
|
|
79
|
+
// ran, instead of prompting (which would hang/exit 1 on non-interactive stdin and break them).
|
|
80
|
+
if (!mode && rest.includes('--yes'))
|
|
81
|
+
mode = 'existing';
|
|
82
|
+
if (!mode)
|
|
83
|
+
mode = await (options.promptMode ?? promptInitMode)();
|
|
84
|
+
if (!mode) {
|
|
85
|
+
const isTTY = options.isTTY ?? Boolean(process.stdin.isTTY);
|
|
86
|
+
// A non-interactive stdin (CI, piped npx) never got to ask the question at all — but a TTY
|
|
87
|
+
// whose answer `promptInitMode` simply couldn't parse did ask, so it needs its own message,
|
|
88
|
+
// not the "non-interactive" one (Minor finding, Fable's implementation review).
|
|
89
|
+
const message = isTTY
|
|
90
|
+
? 'Could not parse that answer. Specify --new or --existing directly.'
|
|
91
|
+
: 'Specify --new or --existing (non-interactive stdin cannot be asked).';
|
|
92
|
+
return { messages: [`${message}\n\n${USAGE}`], written: [], exitCode: 1 };
|
|
93
|
+
}
|
|
94
|
+
return mode === 'new'
|
|
95
|
+
? initNewProject(projectRoot, { kitRoot, installer: options.installer })
|
|
96
|
+
: initExistingProject(projectRoot, { yes: rest.includes('--yes'), confirm: options.confirm });
|
|
97
|
+
}
|
|
7
98
|
async function main() {
|
|
8
99
|
const [command, ...rest] = process.argv.slice(2);
|
|
9
100
|
const projectRoot = process.cwd();
|
|
101
|
+
if (command === undefined || command === 'help' || command === '--help' || command === '-h') {
|
|
102
|
+
console.log(USAGE);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (command === '--version' || command === '-v') {
|
|
106
|
+
console.log(packageVersion());
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
10
109
|
if (command === 'init') {
|
|
11
|
-
const result = await
|
|
110
|
+
const result = await runInit(projectRoot, rest);
|
|
12
111
|
for (const message of result.messages)
|
|
13
112
|
console.log(message);
|
|
14
113
|
for (const file of result.written)
|
|
@@ -28,7 +127,7 @@ async function main() {
|
|
|
28
127
|
await dev(config);
|
|
29
128
|
return;
|
|
30
129
|
}
|
|
31
|
-
console.error(`Unknown command "${command
|
|
130
|
+
console.error(`Unknown command "${command}".\n\n${USAGE}`);
|
|
32
131
|
process.exitCode = 1;
|
|
33
132
|
}
|
|
34
133
|
main().catch((error) => {
|
package/dist/cli/props.js
CHANGED
|
@@ -52,7 +52,7 @@ export function createComponentReader(entryFiles, options = {}) {
|
|
|
52
52
|
allowJs: false,
|
|
53
53
|
noEmit: true,
|
|
54
54
|
baseUrl: host.baseUrl,
|
|
55
|
-
paths: { ...host.paths, ...options.paths },
|
|
55
|
+
paths: { ...options.fallbackPaths, ...host.paths, ...options.paths },
|
|
56
56
|
// `pathsBasePath` is @internal: it tells the compiler which folder `paths` are relative to when
|
|
57
57
|
// the host tsconfig has `paths` but no `baseUrl` (the TypeScript 6 idiom).
|
|
58
58
|
...(host.pathsBasePath ? { pathsBasePath: host.pathsBasePath } : {}),
|
|
@@ -156,33 +156,23 @@ function readComponentRecord(checker, name, type, declaration, optionRoots) {
|
|
|
156
156
|
const propsType = signature && signature.parameters.length > 0 ? checker.getTypeOfSymbolAtLocation(signature.parameters[0], declaration) : undefined;
|
|
157
157
|
const implementationDefaults = defaultsFromImplementation(declaration);
|
|
158
158
|
const props = [];
|
|
159
|
-
const inheritedFrom = new Set();
|
|
160
159
|
if (propsType) {
|
|
161
160
|
const branches = propsType.isUnion() ? propsType.types : [propsType];
|
|
162
161
|
for (const merged of mergeUnionProps(checker, branches)) {
|
|
163
162
|
const { symbol: prop, declaration: propDeclaration, branchCount } = merged;
|
|
164
163
|
const propFile = propDeclaration?.getSourceFile().fileName ?? '';
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// (e.g. react-native's `TextInput`) are still reported as before.
|
|
170
|
-
if (!/[/\\]@types[/\\]react[/\\]/.test(propFile) || /[/\\]@types[/\\]react-native[/\\]/.test(propFile)) {
|
|
171
|
-
inheritedFrom.add(path.basename(path.dirname(propFile)));
|
|
172
|
-
}
|
|
164
|
+
// A prop declared in `node_modules` (e.g. `value` from react-native's `TextInputProps`) is
|
|
165
|
+
// never listed individually — `inheritedFrom` below (design §3) summarises the whole
|
|
166
|
+
// extended type as one row instead.
|
|
167
|
+
if (propFile.includes('node_modules'))
|
|
173
168
|
continue;
|
|
174
|
-
}
|
|
175
169
|
const propType = propDeclaration ? checker.getTypeOfSymbolAtLocation(prop, propDeclaration) : checker.getAnyType();
|
|
176
|
-
// Under `strict`, an optional prop's type includes `| undefined` (that's how TypeScript
|
|
177
|
-
// represents `label?: string` when read via a symbol) — print its non-nullable type instead,
|
|
178
|
-
// keeping any alias name, so the Props table reads "string" rather than "string | undefined".
|
|
179
|
-
const displayType = prop.flags & ts.SymbolFlags.Optional ? checker.getNonNullableType(propType) : propType;
|
|
180
170
|
const jsDocDefault = prop.getJsDocTags().find((tag) => tag.name === 'default');
|
|
181
|
-
const baseDesc = ts.displayPartsToString(prop.getDocumentationComment(checker));
|
|
171
|
+
const baseDesc = joinWrappedLines(ts.displayPartsToString(prop.getDocumentationComment(checker)));
|
|
182
172
|
const isPartial = branchCount < branches.length;
|
|
183
173
|
props.push({
|
|
184
174
|
name: prop.getName(),
|
|
185
|
-
type: checker
|
|
175
|
+
type: displayTypeString(checker, prop, propType),
|
|
186
176
|
// A prop present in only some branches is never required overall — the caller can always
|
|
187
177
|
// pick a branch that omits it — regardless of whether it's required within the branch(es)
|
|
188
178
|
// it does appear in (that's `merged.requiredInEvery`, checked only when `!isPartial`).
|
|
@@ -193,7 +183,165 @@ function readComponentRecord(checker, name, type, declaration, optionRoots) {
|
|
|
193
183
|
});
|
|
194
184
|
}
|
|
195
185
|
}
|
|
196
|
-
|
|
186
|
+
const inheritedFrom = propsType ? nodeModulesHeritageNames(checker, propsType.isUnion() ? propsType.types : [propsType]) : [];
|
|
187
|
+
return { name, file: path.relative(process.cwd(), sourceFile.fileName), props, inheritedFrom };
|
|
188
|
+
}
|
|
189
|
+
/** Design §3 — "Type column: keeps alias names, not expanded unions." An optional prop's type
|
|
190
|
+
* (as read via its symbol) includes `undefined` — printed as `"T | undefined"` when `T` has no
|
|
191
|
+
* alias of its own (e.g. `label?: string`), but when `T` is itself an alias whose own definition
|
|
192
|
+
* already includes `undefined` (react-native's `StyleProp<T>`, React's `ReactNode`), the type as
|
|
193
|
+
* declared is simply `T` with no added union member, and `checker.typeToString` already prints
|
|
194
|
+
* just the alias name (e.g. `"StyleProp<ViewStyle>"`) — no `| undefined` to strip. Dropping a
|
|
195
|
+
* trailing/leading `undefined` segment as *text*, rather than calling `checker.getNonNullableType`
|
|
196
|
+
* on the type and re-printing that, is what keeps the alias in both cases: `getNonNullableType`
|
|
197
|
+
* rebuilds a fresh, unaliased union once it has to remove a member nested inside an alias's own
|
|
198
|
+
* definition (`StyleProp`/`ReactNode`), even though the printed string never needed that member
|
|
199
|
+
* removed in the first place. */
|
|
200
|
+
function displayTypeString(checker, prop, propType) {
|
|
201
|
+
const printed = checker.typeToString(propType, undefined, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
|
|
202
|
+
if (!(prop.flags & ts.SymbolFlags.Optional))
|
|
203
|
+
return printed;
|
|
204
|
+
const kept = splitTopLevelUnion(printed).filter((part) => part !== 'undefined' && part !== 'null');
|
|
205
|
+
if (kept.length === 0)
|
|
206
|
+
return printed;
|
|
207
|
+
// A single remaining part that is itself wrapped in one outer pair of parentheses (e.g. the
|
|
208
|
+
// `(() => void)` TypeScript prints around an arrow type that was a union member) no longer needs
|
|
209
|
+
// those parentheses once it is the only part left — `(t: string) => void`, not `((t: string) =>
|
|
210
|
+
// void)`.
|
|
211
|
+
if (kept.length === 1)
|
|
212
|
+
return unwrapOuterParens(kept[0]);
|
|
213
|
+
return kept.join(' | ');
|
|
214
|
+
}
|
|
215
|
+
/** Splits a printed type's top-level ` | ` union members — skipping any `|` nested inside
|
|
216
|
+
* `<...>`, `(...)`, `{...}`, or `[...]` (e.g. the one inside `RecursiveArray<Falsy | ViewStyle>`)
|
|
217
|
+
* — so `displayTypeString` can drop a top-level `undefined`/`null` member without disturbing a
|
|
218
|
+
* union that appears only as part of a deeper type argument. An arrow type's `=>` prints a `>`
|
|
219
|
+
* that is not a closing bracket, so it must not decrement `depth`: skip it whenever the previous
|
|
220
|
+
* character was `=` (otherwise `(() => void) | undefined` goes to depth -1 at the arrow and the
|
|
221
|
+
* top-level ` | ` before `undefined` is read as nested, never splitting it off). */
|
|
222
|
+
function splitTopLevelUnion(typeString) {
|
|
223
|
+
const parts = [];
|
|
224
|
+
let depth = 0;
|
|
225
|
+
let start = 0;
|
|
226
|
+
for (let i = 0; i < typeString.length; i++) {
|
|
227
|
+
const ch = typeString[i];
|
|
228
|
+
if (ch === '<' || ch === '(' || ch === '{' || ch === '[')
|
|
229
|
+
depth += 1;
|
|
230
|
+
else if (ch === '>' || ch === ')' || ch === '}' || ch === ']') {
|
|
231
|
+
if (ch === '>' && typeString[i - 1] === '=')
|
|
232
|
+
continue;
|
|
233
|
+
depth -= 1;
|
|
234
|
+
}
|
|
235
|
+
else if (depth === 0 && typeString.startsWith(' | ', i)) {
|
|
236
|
+
parts.push(typeString.slice(start, i));
|
|
237
|
+
start = i + 3;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
parts.push(typeString.slice(start));
|
|
241
|
+
return parts;
|
|
242
|
+
}
|
|
243
|
+
/** Strips one outer `(...)` pair from `part` when it wraps the whole string — not just any
|
|
244
|
+
* leading `(`, e.g. `(a) => (b)` has balanced outer parens only around `a`, not around the whole
|
|
245
|
+
* expression, so this walks depth to confirm the first `(` closes at the very last character. */
|
|
246
|
+
function unwrapOuterParens(part) {
|
|
247
|
+
if (part[0] !== '(' || part[part.length - 1] !== ')')
|
|
248
|
+
return part;
|
|
249
|
+
let depth = 0;
|
|
250
|
+
for (let i = 0; i < part.length; i++) {
|
|
251
|
+
const ch = part[i];
|
|
252
|
+
if (ch === '(')
|
|
253
|
+
depth += 1;
|
|
254
|
+
else if (ch === ')') {
|
|
255
|
+
depth -= 1;
|
|
256
|
+
if (depth === 0)
|
|
257
|
+
return i === part.length - 1 ? part.slice(1, -1) : part;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return part;
|
|
261
|
+
}
|
|
262
|
+
/** Design §3 — "Inherited props declared in `node_modules` ... are summarised as one row: 'plus
|
|
263
|
+
* all TextInput props'." Reads each branch's own `extends` heritage clause(s) — not the flattened
|
|
264
|
+
* property list `mergeUnionProps` builds for the table above — because a `node_modules` interface
|
|
265
|
+
* like react-native's `TextInputProps` itself extends several more `node_modules` interfaces
|
|
266
|
+
* (`ViewProps`, `TouchableWithoutFeedbackProps`, ...), each declared in its own file; grouping by
|
|
267
|
+
* every inherited *property*'s own declaration file (the previous approach) reported one noisy
|
|
268
|
+
* entry per ancestor file instead of the one name the component's own Props type actually names.
|
|
269
|
+
* A `forwardRef` component's props type is `OwnProps & RefAttributes<T>` (an intersection, not an
|
|
270
|
+
* interface) — `heritageInterfaceDeclarations` unwraps that to reach `OwnProps`'s own declaration. */
|
|
271
|
+
function nodeModulesHeritageNames(checker, branches) {
|
|
272
|
+
const names = new Set();
|
|
273
|
+
for (const branch of branches) {
|
|
274
|
+
for (const declaration of heritageInterfaceDeclarations(branch)) {
|
|
275
|
+
const declarationFile = declaration.getSourceFile().fileName;
|
|
276
|
+
if (declarationFile.includes('node_modules')) {
|
|
277
|
+
// `declaration` was pulled in directly by the branch itself (e.g. `type Props =
|
|
278
|
+
// TextInputProps & {...}`), not named in a *local* interface's own `extends` clause —
|
|
279
|
+
// report its own name. Walking its `heritageClauses` below (as the `extends
|
|
280
|
+
// TextInputProps` case does) would instead report TextInputProps's own ancestors, e.g.
|
|
281
|
+
// "View"/"TouchableWithoutFeedback".
|
|
282
|
+
if (!isReactOnlyFile(declarationFile))
|
|
283
|
+
names.add(declaration.name.getText().replace(/Props$/, ''));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
for (const clause of declaration.heritageClauses ?? []) {
|
|
287
|
+
for (const typeNode of clause.types) {
|
|
288
|
+
// `getTypeAtLocation` on the whole `ExpressionWithTypeArguments` node (not just its
|
|
289
|
+
// `.expression`) is required for `resolveUtilityTypeSource` below to see `Omit`/`Pick`'s
|
|
290
|
+
// actual instantiated type arguments — on `.expression` alone they come back as the
|
|
291
|
+
// utility type's own unbound generic parameters ("T", "K"), not `TextInputProps`.
|
|
292
|
+
const heritageType = resolveUtilityTypeSource(checker.getTypeAtLocation(typeNode));
|
|
293
|
+
const heritageDeclaration = (heritageType.aliasSymbol ?? heritageType.symbol)?.declarations?.[0];
|
|
294
|
+
if (!heritageDeclaration)
|
|
295
|
+
continue;
|
|
296
|
+
const file = heritageDeclaration.getSourceFile().fileName;
|
|
297
|
+
if (!file.includes('node_modules'))
|
|
298
|
+
continue;
|
|
299
|
+
// `@types/react` itself (e.g. `Attributes`, reached through a `forwardRef` component's
|
|
300
|
+
// own `RefAttributes<T>`) is not a meaningful "inherited from" fact — every component
|
|
301
|
+
// already uses React. A host-type package's own heritage (e.g. react-native's `View`,
|
|
302
|
+
// reached through `TextInputProps`) is still reported as before.
|
|
303
|
+
if (isReactOnlyFile(file))
|
|
304
|
+
continue;
|
|
305
|
+
const rawName = (heritageType.aliasSymbol ?? heritageType.symbol)?.getName() ?? typeNode.expression.getText();
|
|
306
|
+
names.add(rawName.replace(/Props$/, ''));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return [...names];
|
|
312
|
+
}
|
|
313
|
+
/** True for a declaration file under `@types/react` that is not also under `@types/react-native`
|
|
314
|
+
* (react-native's own `@types` package path contains `react` as a substring). */
|
|
315
|
+
function isReactOnlyFile(file) {
|
|
316
|
+
return /[/\\]@types[/\\]react[/\\]/.test(file) && !/[/\\]@types[/\\]react-native[/\\]/.test(file);
|
|
317
|
+
}
|
|
318
|
+
/** `Omit<T, K>`/`Pick<T, K>` of a node_modules type prints its own alias symbol ("Omit"/"Pick")
|
|
319
|
+
* rather than `T`'s name — resolve to `T` (the type's first alias type argument) instead, so
|
|
320
|
+
* `extends Omit<TextInputProps, 'value'>` reports "TextInput", not "Omit" (Fable's review,
|
|
321
|
+
* cli/props.ts:270-301). */
|
|
322
|
+
function resolveUtilityTypeSource(type) {
|
|
323
|
+
const aliasName = type.aliasSymbol?.getName();
|
|
324
|
+
if ((aliasName === 'Omit' || aliasName === 'Pick') && type.aliasTypeArguments?.[0]) {
|
|
325
|
+
return type.aliasTypeArguments[0];
|
|
326
|
+
}
|
|
327
|
+
return type;
|
|
328
|
+
}
|
|
329
|
+
/** Every `InterfaceDeclaration` backing `type` — itself if `type` is one, or (recursing) each
|
|
330
|
+
* constituent of an intersection, so a `forwardRef` component's `OwnProps & RefAttributes<T>`
|
|
331
|
+
* still reaches `OwnProps`'s own heritage clause. */
|
|
332
|
+
function heritageInterfaceDeclarations(type) {
|
|
333
|
+
if (type.isIntersection())
|
|
334
|
+
return type.types.flatMap(heritageInterfaceDeclarations);
|
|
335
|
+
return (type.symbol?.declarations ?? []).filter(ts.isInterfaceDeclaration);
|
|
336
|
+
}
|
|
337
|
+
/** Design §3, finding 4: a JSDoc paragraph hard-wrapped in source (`getDocumentationComment`
|
|
338
|
+
* preserves each line break verbatim) must read as running text — join lines within a paragraph
|
|
339
|
+
* with a single space, but keep a blank-line paragraph break as its own line break. */
|
|
340
|
+
function joinWrappedLines(text) {
|
|
341
|
+
return text
|
|
342
|
+
.split(/\n{2,}/)
|
|
343
|
+
.map((paragraph) => paragraph.replace(/\s*\n\s*/g, ' ').trim())
|
|
344
|
+
.join('\n\n');
|
|
197
345
|
}
|
|
198
346
|
/**
|
|
199
347
|
* Design §3 — "Unions of object types (e.g. `CardProps` as two intersections) are merged into one
|
package/dist/cli/sync.js
CHANGED
|
@@ -33,7 +33,23 @@ export function sync(config, resolveOptions) {
|
|
|
33
33
|
const components = [];
|
|
34
34
|
const componentRefs = [];
|
|
35
35
|
const seenNames = new Set();
|
|
36
|
-
|
|
36
|
+
// A component entry file can live outside `config.projectRoot` (e.g. kit-host's sibling
|
|
37
|
+
// `../starter-kit/`) — `createComponentReader`'s own host-tsconfig lookup walks up from the
|
|
38
|
+
// entry file, never reaching `config.projectRoot`'s own node_modules. This fallback (lowest
|
|
39
|
+
// priority — a real host `paths`, or `resolveOptions.paths`, still wins) is what lets such a
|
|
40
|
+
// file still resolve `react`/`react-native` against the project that actually declares them.
|
|
41
|
+
// `@types/*` is listed BEFORE the plain package deliberately (confirmed by spike,
|
|
42
|
+
// `spikes/README.md` "sync-fallback-paths"): once `paths` redirects a bare specifier to an
|
|
43
|
+
// on-disk folder, TypeScript resolves it there and never falls through to a second entry just
|
|
44
|
+
// because the first has no types — the reverse order left `react`/`react-native` both
|
|
45
|
+
// resolving to "implicitly any", because the plain package folder matched first. Only computed
|
|
46
|
+
// when at least one entry file actually lies outside `config.projectRoot` — normal projects
|
|
47
|
+
// (every entry file under projectRoot) keep standard TypeScript resolution untouched.
|
|
48
|
+
const hasEntryOutsideProjectRoot = componentEntryFiles.some((file) => !file.startsWith(`${config.projectRoot}${path.sep}`));
|
|
49
|
+
const fallbackPaths = hasEntryOutsideProjectRoot
|
|
50
|
+
? { '*': [path.join(config.projectRoot, 'node_modules', '@types', '*'), path.join(config.projectRoot, 'node_modules', '*')] }
|
|
51
|
+
: undefined;
|
|
52
|
+
const readComponent = createComponentReader(componentEntryFiles, { ...resolveOptions, optionRoots, fallbackPaths });
|
|
37
53
|
for (const entryFile of componentEntryFiles) {
|
|
38
54
|
try {
|
|
39
55
|
for (const record of readComponent(entryFile)) {
|
package/dist/cli/workspace.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
3
|
+
import { globBaseFolder } from "./glob.js";
|
|
4
|
+
function metroConfigJs(extraWatchFolders) {
|
|
5
|
+
return `const path = require('path');
|
|
4
6
|
const fs = require('fs');
|
|
5
7
|
const workspace = __dirname;
|
|
6
8
|
const projectRoot = path.resolve(workspace, '..');
|
|
@@ -16,7 +18,18 @@ if (typeof config !== 'object') {
|
|
|
16
18
|
config.projectRoot = workspace;
|
|
17
19
|
// Expo sets the server root to the folder the config was built for; bundle URLs must resolve from the workspace.
|
|
18
20
|
config.server = { ...(config.server || {}), unstable_serverRoot: workspace };
|
|
19
|
-
|
|
21
|
+
// Every configured components/tokens/pages glob's base folder — not just projectRoot itself — so a
|
|
22
|
+
// host whose kit lives in a sibling folder (e.g. kit-host/ds-viewer.config.ts reaching
|
|
23
|
+
// ../starter-kit/components) still gets its files watched and bundled. Resolved from projectRoot at
|
|
24
|
+
// runtime, not baked in as an absolute path, so the generated file stays portable across machines.
|
|
25
|
+
const EXTRA_WATCH_FOLDERS = ${JSON.stringify(extraWatchFolders)};
|
|
26
|
+
const extraWatchFolders = EXTRA_WATCH_FOLDERS;
|
|
27
|
+
// A configured components/tokens/pages glob can point at a folder that doesn't exist yet (e.g. a
|
|
28
|
+
// standalone-pages folder before its first file is added) — Metro refuses to watch a nonexistent
|
|
29
|
+
// folder, which otherwise makes every single bundle request fail with HTTP 500. Filtered here, at
|
|
30
|
+
// workspace-generation time, not above at config-resolution time, because a folder created after
|
|
31
|
+
// \`dev\` starts should start being watched on its own next run, same as every other glob folder.
|
|
32
|
+
config.watchFolders = Array.from(new Set([...(config.watchFolders || []), projectRoot, ...extraWatchFolders.map((f) => path.resolve(projectRoot, f)).filter((f) => fs.existsSync(f))]));
|
|
20
33
|
config.resolver.nodeModulesPaths = [path.join(projectRoot, 'node_modules')];
|
|
21
34
|
// A standalone page can read generated data with \`import { components, tokens } from '@krapwoo/ds-viewer/generated'\`
|
|
22
35
|
// (design §3) — redirect that one specifier to this workspace's own generated/index.ts.
|
|
@@ -31,17 +44,28 @@ config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
|
31
44
|
};
|
|
32
45
|
module.exports = config;
|
|
33
46
|
`;
|
|
47
|
+
}
|
|
34
48
|
/** Decided here, not inside the viewer: Metro resolves `react-native-safe-area-context` statically
|
|
35
49
|
* at bundle time, so a runtime try/catch `require` cannot skip it when the optional peer is
|
|
36
50
|
* missing. Importing and wrapping must instead be baked into the generated entry (design §1
|
|
37
51
|
* "Three parts", Viewer row). */
|
|
38
|
-
function entryTsx(name, hasSafeArea, groupOrder) {
|
|
52
|
+
function entryTsx(name, hasSafeArea, groupOrder, logoRequirePath) {
|
|
39
53
|
const safeAreaImport = hasSafeArea ? "import { SafeAreaProvider } from 'react-native-safe-area-context';\n" : '';
|
|
40
|
-
|
|
54
|
+
// A literal `require(...)` with a path computed at generation time: Metro resolves every import
|
|
55
|
+
// statically, so the logo's actual (arbitrary, user-configured) location must already be baked
|
|
56
|
+
// into this generated file, the same reasoning `hasSafeArea` above already follows for the
|
|
57
|
+
// optional SafeAreaProvider import.
|
|
58
|
+
// `JSON.stringify`, not a plain single-quoted template literal: `logoRequirePath` carries the
|
|
59
|
+
// user's own logo file name, which may contain a `'` (e.g. `assets/it's.png`) — a bare
|
|
60
|
+
// `'${logoRequirePath}'` breaks the generated file's syntax the moment it does (Minor finding,
|
|
61
|
+
// Fable's implementation review, cli/workspace.ts:65).
|
|
62
|
+
const logoImport = logoRequirePath ? `const logoSource = require(${JSON.stringify(logoRequirePath)});\n` : '';
|
|
63
|
+
const logoProp = logoRequirePath ? ' logoImageSource={logoSource}' : '';
|
|
64
|
+
const viewer = `<CatalogShell appName={${JSON.stringify(name)}} title="Component Catalog" groups={groups} sections={sections}${logoProp} />`;
|
|
41
65
|
const root = hasSafeArea ? `<SafeAreaProvider>${viewer}</SafeAreaProvider>` : viewer;
|
|
42
66
|
return `import { registerRootComponent } from 'expo';
|
|
43
67
|
import { CatalogShell, buildCatalogSections } from '@krapwoo/ds-viewer';
|
|
44
|
-
${safeAreaImport}import pages from './generated/pages';
|
|
68
|
+
${safeAreaImport}${logoImport}import pages from './generated/pages';
|
|
45
69
|
import components from './generated/components.json';
|
|
46
70
|
|
|
47
71
|
function App() {
|
|
@@ -52,25 +76,22 @@ function App() {
|
|
|
52
76
|
registerRootComponent(App);
|
|
53
77
|
`;
|
|
54
78
|
}
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
function
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
/** Every configured components/tokens/pages glob's base folder, deduplicated and sorted — shared by
|
|
80
|
+
* `projectIncludeGlobs` (tsconfig `include`, below) and `metroConfigJs`'s extra watch folders
|
|
81
|
+
* (above). Kept as the raw relative folder (e.g. `"../starter-kit/components"`), not yet turned
|
|
82
|
+
* into either a tsconfig glob or an absolute path — each caller does that its own way. */
|
|
83
|
+
function configGlobFolders(config) {
|
|
84
|
+
const folders = new Set();
|
|
85
|
+
for (const pattern of [...config.components, ...(config.tokens ?? []), ...(config.pages ?? [])]) {
|
|
86
|
+
folders.add(globBaseFolder(pattern));
|
|
87
|
+
}
|
|
88
|
+
return [...folders].sort();
|
|
64
89
|
}
|
|
65
90
|
/** Folders the workspace's own `tsconfig.json` needs to type-check — derived from the config's own
|
|
66
91
|
* globs (the spike used `../src/**\/*`), instead of `../**\/*`, which would pull in the whole
|
|
67
92
|
* project, including `node_modules`. */
|
|
68
93
|
function projectIncludeGlobs(config) {
|
|
69
|
-
|
|
70
|
-
for (const pattern of [...config.components, ...(config.tokens ?? []), ...(config.pages ?? [])]) {
|
|
71
|
-
folders.add(globBaseFolder(pattern));
|
|
72
|
-
}
|
|
73
|
-
return [...folders].sort().map((folder) => `../${folder}/**/*`);
|
|
94
|
+
return configGlobFolders(config).map((folder) => `../${folder}/**/*`);
|
|
74
95
|
}
|
|
75
96
|
/** Writes the self-contained Expo web workspace at `.ds-viewer/` (design §2 "The preview
|
|
76
97
|
* workspace and `npx ds-viewer dev`"). Rewritten freely on every `dev` run — nothing here is
|
|
@@ -88,12 +109,19 @@ export function writeWorkspace(config) {
|
|
|
88
109
|
if (existsSync(projectTsconfig))
|
|
89
110
|
workspaceTsconfig.extends = '../tsconfig.json';
|
|
90
111
|
writeFileSync(path.join(workspace, 'tsconfig.json'), `${JSON.stringify(workspaceTsconfig, null, 2)}\n`);
|
|
91
|
-
writeFileSync(path.join(workspace, 'metro.config.js'),
|
|
112
|
+
writeFileSync(path.join(workspace, 'metro.config.js'), metroConfigJs(configGlobFolders(config)));
|
|
92
113
|
const projectBabelConfig = path.join(config.projectRoot, 'babel.config.js');
|
|
93
114
|
if (existsSync(projectBabelConfig)) {
|
|
94
115
|
writeFileSync(path.join(workspace, 'babel.config.js'), "module.exports = require('../babel.config.js');\n");
|
|
95
116
|
}
|
|
96
117
|
const hasSafeArea = existsSync(path.join(config.projectRoot, 'node_modules', 'react-native-safe-area-context', 'package.json'));
|
|
97
|
-
|
|
118
|
+
// `config.logo` is already validated by `cli/config.ts`'s `resolveConfig` (Step 3 above) — by the
|
|
119
|
+
// time `writeWorkspace` sees it, it's either a real, existing file or undefined. Computed relative
|
|
120
|
+
// to `workspace` (one level deeper than `projectRoot`), with forward slashes so the generated
|
|
121
|
+
// `require()` string is portable across machines.
|
|
122
|
+
const logoRequirePath = config.logo
|
|
123
|
+
? path.relative(workspace, path.resolve(config.projectRoot, config.logo)).split(path.sep).join('/')
|
|
124
|
+
: undefined;
|
|
125
|
+
writeFileSync(path.join(workspace, 'entry.tsx'), entryTsx(config.name, hasSafeArea, config.groupOrder ?? [], logoRequirePath));
|
|
98
126
|
return workspace;
|
|
99
127
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { AccessibilityInfo, Platform, ScrollView, StyleSheet, Text, View, findNodeHandle } from 'react-native';
|
|
3
|
+
import type { ImageSourcePropType } from 'react-native';
|
|
3
4
|
import { CATALOG_COLOR, CATALOG_LAYOUT, CATALOG_MAX_CONTENT_WIDTH, CATALOG_SPACE, CATALOG_TYPE } from './tokens';
|
|
4
5
|
import { CatalogSidebar } from './CatalogSidebar';
|
|
5
6
|
import { SectionBlock } from './SectionBlock';
|
|
@@ -43,6 +44,7 @@ function resetMainScroll(scrollView: ScrollView | null): void {
|
|
|
43
44
|
*/
|
|
44
45
|
export function CatalogShell<TId extends string>({
|
|
45
46
|
appName,
|
|
47
|
+
logoImageSource,
|
|
46
48
|
title,
|
|
47
49
|
groups,
|
|
48
50
|
sections,
|
|
@@ -50,6 +52,9 @@ export function CatalogShell<TId extends string>({
|
|
|
50
52
|
}: {
|
|
51
53
|
/** Short product/app name — the sidebar logo and the breadcrumb root. */
|
|
52
54
|
appName: string;
|
|
55
|
+
/** Optional logo image, validated and bundled by `cli/workspace.ts` from the project's `logo`
|
|
56
|
+
* config field. See `CatalogSidebar`'s own doc comment for the approved layout rule. */
|
|
57
|
+
logoImageSource?: ImageSourcePropType;
|
|
53
58
|
/** What this catalog is (e.g. "Component Catalog") — the sidebar caption. */
|
|
54
59
|
title: string;
|
|
55
60
|
groups: NavGroup<TId>[];
|
|
@@ -122,7 +127,7 @@ export function CatalogShell<TId extends string>({
|
|
|
122
127
|
|
|
123
128
|
return (
|
|
124
129
|
<View style={styles.root}>
|
|
125
|
-
<CatalogSidebar logo={appName} caption={title} groups={groups} active={active} onPress={select} />
|
|
130
|
+
<CatalogSidebar logo={appName} logoImageSource={logoImageSource} caption={title} groups={groups} active={active} onPress={select} />
|
|
126
131
|
<ScrollView ref={scrollRef} style={styles.main} contentContainerStyle={styles.mainContent}>
|
|
127
132
|
{activeDef ? (
|
|
128
133
|
<SectionBlock
|