@krapwoo/ds-viewer 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -78
- package/dist/cli/agentsFile.js +31 -0
- package/dist/cli/config.js +26 -1
- package/dist/cli/dev.js +50 -5
- package/dist/cli/doctor.js +492 -0
- package/dist/cli/explain.js +151 -0
- package/dist/cli/githubAction.js +68 -0
- package/dist/cli/glob.js +8 -0
- package/dist/cli/init.js +152 -18
- package/dist/cli/main.js +115 -4
- package/dist/cli/pageIndex.js +27 -1
- package/dist/cli/props.js +170 -21
- package/dist/cli/staticPage.js +356 -0
- package/dist/cli/sync.js +28 -5
- package/dist/cli/workspace.js +49 -21
- package/dist/native/catalog/comparison.js +283 -0
- package/dist/native/catalog/tokens.js +119 -0
- package/dist/native/catalog/types.js +1 -0
- package/native/catalog/CatalogShell.tsx +6 -1
- package/native/catalog/CatalogSidebar.tsx +98 -5
- package/native/catalog/ComparisonGrid.tsx +9 -7
- package/native/catalog/SpacingScaleGallery.tsx +1 -1
- package/native/catalog/TokenRow.tsx +3 -2
- package/native/catalog/catalogNavigation.ts +1 -1
- package/native/catalog/comparison.ts +63 -5
- package/native/catalog/index.ts +8 -6
- package/native/catalog/logoLayout.ts +12 -0
- package/native/catalog/pageApi.ts +21 -3
- package/native/catalog/tokens.ts +5 -0
- package/native/catalog/types.ts +9 -6
- package/package.json +12 -6
- 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 +54 -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 +88 -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 +99 -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 +79 -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/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
|
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/** Approved geometry. `cellPadding` equals CATALOG_SPACE.lg (asserted in tests). The laptop
|
|
2
|
+
* content width is a 1280px viewport minus the 264px sidebar and 32px side padding. */
|
|
3
|
+
export const MATRIX_LAYOUT = {
|
|
4
|
+
rowHeaderWidth: 120,
|
|
5
|
+
columnMaxWidth: 402,
|
|
6
|
+
cellPadding: 16,
|
|
7
|
+
rowMinHeight: 150,
|
|
8
|
+
laptopContentWidth: 952,
|
|
9
|
+
};
|
|
10
|
+
/** Component previews never render wider than a phone. */
|
|
11
|
+
export const PREVIEW_MAX_WIDTH = 402;
|
|
12
|
+
/** Two blocks share a row only when that saves at least this much page height. */
|
|
13
|
+
export const PLACEMENT_MIN_SAVING = 120;
|
|
14
|
+
/** A Props column is never narrower than this; below it the Props box uses one column. */
|
|
15
|
+
export const PROPS_MIN_COLUMN_WIDTH = 360;
|
|
16
|
+
/** Gap between the two Props columns (CATALOG_SPACE['2xl']). */
|
|
17
|
+
export const PROPS_COLUMN_GAP = 32;
|
|
18
|
+
/** Smallest column or cell width per specimen size. Wide specimens are fixed at phone width. */
|
|
19
|
+
export const COLUMN_MIN_WIDTH = { compact: 160, regular: 240, wide: 402 };
|
|
20
|
+
/** `Array.isArray`'s built-in type guard narrows to a mutable `any[]`, which a `readonly T[]`
|
|
21
|
+
* union member is never assignable to — so the `else` branch of `Array.isArray(axis) ? ... :
|
|
22
|
+
* ...` keeps `axis`'s full union type instead of narrowing away the array member. This explicit
|
|
23
|
+
* predicate sidesteps that (never caught before Task 4: this file had never been typechecked by
|
|
24
|
+
* `tsc` until `cli/doctor.ts`'s import pulled it into the root `tsconfig.json`'s program). */
|
|
25
|
+
function isBoundAxis(axis) {
|
|
26
|
+
return !Array.isArray(axis);
|
|
27
|
+
}
|
|
28
|
+
/** A `GridAxis`'s own items, whether it's a plain array or bound to a prop — every reader of
|
|
29
|
+
* `ComparisonDef.rows`/`columns` goes through this (never `Array.isArray` directly), so a future
|
|
30
|
+
* third axis shape only needs to change this one function. */
|
|
31
|
+
export function axisItems(axis) {
|
|
32
|
+
return isBoundAxis(axis) ? [...axis.items] : [...axis];
|
|
33
|
+
}
|
|
34
|
+
/** The prop name a `GridAxis` is bound to, or undefined for a plain, unbound axis. */
|
|
35
|
+
export function axisProp(axis) {
|
|
36
|
+
return isBoundAxis(axis) ? axis.prop : undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Shared page-authoring helper (design §1 "Page shape", §4 "Binding examples to props"). Every
|
|
40
|
+
* grid comparison in the starter kit builds its `ComparisonDef` with this instead of its own local
|
|
41
|
+
* copy, so `cli/staticPage.ts`'s static reader has exactly one call shape to recognize anywhere a
|
|
42
|
+
* page's `comparison` field is produced by a function call rather than written as a plain object
|
|
43
|
+
* literal. `rows`/`columns` accept either a plain item array (unbound) or `{ prop, items }` (bound)
|
|
44
|
+
* — the cell grid itself is unaffected either way, since cells are always keyed by each axis's own
|
|
45
|
+
* items regardless of whether that axis names a prop. Lives here, beside `axisItems`/`axisProp`,
|
|
46
|
+
* rather than in its own `grid.ts` — every other file in this module has only *type-only* relative
|
|
47
|
+
* imports; a bare, no-extension value import would be the first of its kind in `native/catalog`.
|
|
48
|
+
*
|
|
49
|
+
* Each cell is a real instance with exactly the props its row and column name. Never build these by
|
|
50
|
+
* multiplying `variants` with `states` — those are pre-rendered nodes and cannot be combined.
|
|
51
|
+
*/
|
|
52
|
+
export function grid(rowLabel, columnLabel, rows, columns, cell, size) {
|
|
53
|
+
const rowItems = axisItems(rows);
|
|
54
|
+
const columnItems = axisItems(columns);
|
|
55
|
+
return {
|
|
56
|
+
rowLabel,
|
|
57
|
+
columnLabel,
|
|
58
|
+
rows,
|
|
59
|
+
columns,
|
|
60
|
+
cells: rowItems.flatMap((row) => columnItems.map((column) => ({ rowKey: row.key, columnKey: column.key, node: cell(row.key, column.key) }))),
|
|
61
|
+
size,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** How many grid columns fit beside the row header on a 1280px laptop without scrolling. */
|
|
65
|
+
export function gridColumnLimit(size) {
|
|
66
|
+
return Math.floor((MATRIX_LAYOUT.laptopContentWidth - MATRIX_LAYOUT.rowHeaderWidth) / COLUMN_MIN_WIDTH[size]);
|
|
67
|
+
}
|
|
68
|
+
export function gridWidthBounds(columnCount, size) {
|
|
69
|
+
const n = Math.max(0, columnCount);
|
|
70
|
+
return {
|
|
71
|
+
minWidth: MATRIX_LAYOUT.rowHeaderWidth + n * COLUMN_MIN_WIDTH[size],
|
|
72
|
+
maxWidth: MATRIX_LAYOUT.rowHeaderWidth + n * MATRIX_LAYOUT.columnMaxWidth,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Balanced wrapping for a one-axis list inside a single bordered card (1px outer border). Each
|
|
76
|
+
* cell's 1px divider sits inside its own width, so no cell is ever wider than 402px. Uses as few
|
|
77
|
+
* rows as fit, then spreads items evenly across them so a short last row leaves as little empty
|
|
78
|
+
* space as possible. `fillers` blank cells complete the last row. */
|
|
79
|
+
export function listGeometry(itemCount, availableWidth, size) {
|
|
80
|
+
if (itemCount <= 0)
|
|
81
|
+
return { columns: 0, rows: 0, fillers: 0, cellWidth: 0, containerWidth: 0 };
|
|
82
|
+
const inner = Math.max(0, availableWidth - 2);
|
|
83
|
+
const min = COLUMN_MIN_WIDTH[size];
|
|
84
|
+
const maxColumns = Math.max(1, Math.min(itemCount, Math.floor(inner / min)));
|
|
85
|
+
const rows = Math.ceil(itemCount / maxColumns);
|
|
86
|
+
const columns = Math.ceil(itemCount / rows);
|
|
87
|
+
const fitted = Math.floor(inner / columns);
|
|
88
|
+
const cellWidth = size === 'wide' ? MATRIX_LAYOUT.columnMaxWidth : Math.max(min, Math.min(MATRIX_LAYOUT.columnMaxWidth, fitted));
|
|
89
|
+
return {
|
|
90
|
+
columns,
|
|
91
|
+
rows,
|
|
92
|
+
fillers: rows * columns - itemCount,
|
|
93
|
+
cellWidth,
|
|
94
|
+
containerWidth: columns * cellWidth + 2,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function cellKey(rowKey, columnKey) {
|
|
98
|
+
return `${rowKey}\u0000${columnKey}`;
|
|
99
|
+
}
|
|
100
|
+
export function indexCells(def) {
|
|
101
|
+
const index = new Map();
|
|
102
|
+
for (const cell of def.cells) {
|
|
103
|
+
const key = cellKey(cell.rowKey, cell.columnKey);
|
|
104
|
+
if (!index.has(key))
|
|
105
|
+
index.set(key, cell);
|
|
106
|
+
}
|
|
107
|
+
return index;
|
|
108
|
+
}
|
|
109
|
+
/** Human-readable problems in a stable order: axis duplicates, column fit, each cell in
|
|
110
|
+
* declaration order, then missing coordinates in row-major order. Empty when valid. */
|
|
111
|
+
export function validateComparison(def) {
|
|
112
|
+
const rowItems = axisItems(def.rows);
|
|
113
|
+
const columnItems = axisItems(def.columns);
|
|
114
|
+
if (rowItems.length === 0 || columnItems.length === 0) {
|
|
115
|
+
return ['Comparison needs at least one row and one column.'];
|
|
116
|
+
}
|
|
117
|
+
const issues = [];
|
|
118
|
+
const rowKeys = new Set();
|
|
119
|
+
for (const row of rowItems) {
|
|
120
|
+
if (rowKeys.has(row.key))
|
|
121
|
+
issues.push(`Duplicate row key "${row.key}".`);
|
|
122
|
+
rowKeys.add(row.key);
|
|
123
|
+
}
|
|
124
|
+
const columnKeys = new Set();
|
|
125
|
+
for (const column of columnItems) {
|
|
126
|
+
if (columnKeys.has(column.key))
|
|
127
|
+
issues.push(`Duplicate column key "${column.key}".`);
|
|
128
|
+
columnKeys.add(column.key);
|
|
129
|
+
}
|
|
130
|
+
const size = def.size ?? 'regular';
|
|
131
|
+
const limit = gridColumnLimit(size);
|
|
132
|
+
if (columnKeys.size > limit) {
|
|
133
|
+
issues.push(`Grid has ${columnKeys.size} columns; at most ${limit} ${size} columns fit a 1280px laptop. Swap the axes or use two lists.`);
|
|
134
|
+
}
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
for (const cell of def.cells) {
|
|
137
|
+
const key = cellKey(cell.rowKey, cell.columnKey);
|
|
138
|
+
if (seen.has(key)) {
|
|
139
|
+
issues.push(`Duplicate cell for row "${cell.rowKey}" and column "${cell.columnKey}".`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
seen.add(key);
|
|
143
|
+
if (!rowKeys.has(cell.rowKey)) {
|
|
144
|
+
issues.push(`Cell references undeclared row "${cell.rowKey}".`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!columnKeys.has(cell.columnKey)) {
|
|
148
|
+
issues.push(`Cell references undeclared column "${cell.columnKey}".`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const hasNode = cell.node !== undefined;
|
|
152
|
+
const hasReason = typeof cell.unavailableReason === 'string' && cell.unavailableReason.length > 0;
|
|
153
|
+
if (hasNode === hasReason) {
|
|
154
|
+
issues.push(`Cell for row "${cell.rowKey}" and column "${cell.columnKey}" must provide exactly one of node or unavailableReason.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const rowKey of rowKeys) {
|
|
158
|
+
for (const columnKey of columnKeys) {
|
|
159
|
+
if (!seen.has(cellKey(rowKey, columnKey))) {
|
|
160
|
+
issues.push(`Missing cell for row "${rowKey}" and column "${columnKey}".`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return issues;
|
|
165
|
+
}
|
|
166
|
+
/** `states` minus items whose key is already a grid row or column. */
|
|
167
|
+
export function remainingStates(states, comparison) {
|
|
168
|
+
if (!states)
|
|
169
|
+
return undefined;
|
|
170
|
+
if (!comparison)
|
|
171
|
+
return states;
|
|
172
|
+
const covered = new Set([...axisItems(comparison.rows).map((row) => row.key), ...axisItems(comparison.columns).map((column) => column.key)]);
|
|
173
|
+
const items = states.items.filter((item) => !covered.has(item.key));
|
|
174
|
+
return items.length > 0 ? { ...states, items } : undefined;
|
|
175
|
+
}
|
|
176
|
+
/** Specimen size for a one-axis slot: the section's explicit size, else wide for full-width
|
|
177
|
+
* (itemsFill) slots, else regular. */
|
|
178
|
+
export function slotSize(def, slot) {
|
|
179
|
+
return def.specimenSize ?? (slot.itemsFill ? 'wide' : 'regular');
|
|
180
|
+
}
|
|
181
|
+
function slotItems(slot) {
|
|
182
|
+
return slot.items.map((item) => ({ key: item.key, label: item.name, node: item.node, fill: item.fill || slot.itemsFill }));
|
|
183
|
+
}
|
|
184
|
+
/** Preview widths, each capped at phone width. 'full' is kept for catalog chrome and token pages. */
|
|
185
|
+
function previewWidths(requested, fallback) {
|
|
186
|
+
const widths = requested ?? fallback ?? [PREVIEW_MAX_WIDTH];
|
|
187
|
+
return widths === 'full' ? 'full' : widths.map((w) => Math.min(w, PREVIEW_MAX_WIDTH));
|
|
188
|
+
}
|
|
189
|
+
/** Grouped rows when at least one state names a variant through `group`. Every variant gets a row:
|
|
190
|
+
* its grouped states, or the variant's own example when it has none. States without a matching
|
|
191
|
+
* group are returned as leftovers for "Other configurations". */
|
|
192
|
+
function groupStates(variants, states) {
|
|
193
|
+
const variantKeys = new Set(variants.items.map((v) => v.key));
|
|
194
|
+
if (!states.items.some((s) => s.group !== undefined && variantKeys.has(s.group)))
|
|
195
|
+
return undefined;
|
|
196
|
+
const fill = (item, slot) => item.fill || slot.itemsFill;
|
|
197
|
+
const groups = variants.items.map((variant) => {
|
|
198
|
+
const own = states.items.filter((s) => s.group === variant.key);
|
|
199
|
+
const items = own.length > 0
|
|
200
|
+
? own.map((s) => ({ key: s.key, label: s.name, node: s.node, fill: fill(s, states) }))
|
|
201
|
+
: [{ key: variant.key, label: variant.name, node: variant.node, fill: fill(variant, variants) }];
|
|
202
|
+
return { key: variant.key, label: variant.name, items };
|
|
203
|
+
});
|
|
204
|
+
const rest = states.items.filter((s) => s.group === undefined || !variantKeys.has(s.group));
|
|
205
|
+
for (const s of rest) {
|
|
206
|
+
if (s.group !== undefined && globalThis.__DEV__) {
|
|
207
|
+
console.warn(`[Catalog] state "${s.key}" names group "${s.group}", which matches no variant key; it is shown under "Other configurations".`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { groups, leftovers: rest.length > 0 ? { ...states, items: rest } : undefined };
|
|
211
|
+
}
|
|
212
|
+
/** The visual blocks of one page, in reading order. Reference details are not included. A page
|
|
213
|
+
* never shows an empty block next to a real one; with nothing documented at all it shows one
|
|
214
|
+
* "No examples documented." block. */
|
|
215
|
+
export function presentationBlocks(def, options = {}) {
|
|
216
|
+
if (def.tokenGallery) {
|
|
217
|
+
const title = def.fullWidthLabel ?? 'Tokens';
|
|
218
|
+
return [def.render ? { kind: 'preview', title, widths: 'full' } : { kind: 'empty', title, message: 'Nothing to preview.' }];
|
|
219
|
+
}
|
|
220
|
+
const hide = def.hide ?? {};
|
|
221
|
+
const blocks = [];
|
|
222
|
+
let states = hide.states ? undefined : remainingStates(def.states, def.comparison);
|
|
223
|
+
if (!hide.variants) {
|
|
224
|
+
const grouped = !def.comparison && def.variants && states ? groupStates(def.variants, states) : undefined;
|
|
225
|
+
if (def.comparison) {
|
|
226
|
+
blocks.push({
|
|
227
|
+
kind: 'grid',
|
|
228
|
+
title: `${def.comparison.rowLabel} × ${def.comparison.columnLabel}`,
|
|
229
|
+
size: def.comparison.size ?? def.specimenSize ?? 'regular',
|
|
230
|
+
comparison: def.comparison,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else if (grouped && def.variants) {
|
|
234
|
+
// Grouped rows place several cells beside a row header, so full-width (itemsFill) slots do
|
|
235
|
+
// not force 402px cells here; a cell's own `fill` still stretches its specimen.
|
|
236
|
+
blocks.push({ kind: 'grouped', title: 'Variant × configuration', size: def.specimenSize ?? 'regular', groups: grouped.groups });
|
|
237
|
+
states = grouped.leftovers;
|
|
238
|
+
}
|
|
239
|
+
else if (def.variants) {
|
|
240
|
+
blocks.push({ kind: 'list', title: 'Variants', size: slotSize(def, def.variants), items: slotItems(def.variants) });
|
|
241
|
+
}
|
|
242
|
+
else if (def.render) {
|
|
243
|
+
blocks.push({ kind: 'preview', title: 'Preview', widths: previewWidths(def.previewWidths, options.defaultPreviewWidths) });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (states) {
|
|
247
|
+
const secondary = blocks.length > 0 && blocks[0].kind !== 'list';
|
|
248
|
+
blocks.push({
|
|
249
|
+
kind: 'list',
|
|
250
|
+
title: secondary ? 'Other configurations' : 'States / configurations',
|
|
251
|
+
size: slotSize(def, states),
|
|
252
|
+
items: slotItems(states),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (blocks.length === 0 && !hide.variants && !hide.states) {
|
|
256
|
+
blocks.push({ kind: 'empty', title: 'Examples', message: 'No examples documented.' });
|
|
257
|
+
}
|
|
258
|
+
return blocks;
|
|
259
|
+
}
|
|
260
|
+
/** Side by side or stacked, for a page with exactly two blocks. Tries both and keeps the shorter;
|
|
261
|
+
* stays stacked unless side by side saves at least PLACEMENT_MIN_SAVING. Only a list can move
|
|
262
|
+
* beside a first block that hugs its content, and only when its cells keep their minimum width. */
|
|
263
|
+
export function choosePlacement({ available, gap, first, second }) {
|
|
264
|
+
if (second.kind !== 'list' || !second.itemCount || !second.size)
|
|
265
|
+
return 'stacked';
|
|
266
|
+
const firstWidth = first.width ?? available;
|
|
267
|
+
if (firstWidth > available * 0.7)
|
|
268
|
+
return 'stacked';
|
|
269
|
+
const room = available - firstWidth - gap;
|
|
270
|
+
const minCell = second.size === 'wide' ? MATRIX_LAYOUT.columnMaxWidth : COLUMN_MIN_WIDTH[second.size];
|
|
271
|
+
if (room < minCell + 2)
|
|
272
|
+
return 'stacked';
|
|
273
|
+
const stackedRows = listGeometry(second.itemCount, available, second.size).rows;
|
|
274
|
+
const sideRows = listGeometry(second.itemCount, room, second.size).rows;
|
|
275
|
+
const rowHeight = second.height / Math.max(1, stackedRows);
|
|
276
|
+
const sideHeight = Math.max(first.height, sideRows * rowHeight);
|
|
277
|
+
const stackedHeight = first.height + gap + second.height;
|
|
278
|
+
return sideHeight <= stackedHeight - PLACEMENT_MIN_SAVING ? 'side' : 'stacked';
|
|
279
|
+
}
|
|
280
|
+
/** Props box columns: two, filled across first, for 4+ props when each column keeps its minimum. */
|
|
281
|
+
export function propsColumns(count, width) {
|
|
282
|
+
return count >= 4 && width >= 2 * PROPS_MIN_COLUMN_WIDTH + PROPS_COLUMN_GAP ? 2 : 1;
|
|
283
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-only design tokens — the visual system of the catalog interface itself.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately independent of any host app's design-system tokens (DS_SEMANTIC / DS_PALETTE /
|
|
5
|
+
* DS_SPACING): the catalog documents a design system, it does not consume one. Keeping its chrome
|
|
6
|
+
* on its own tokens means a change to the host app's tokens never restyles the catalog, and this
|
|
7
|
+
* folder can be copied into another app's repo unchanged.
|
|
8
|
+
*
|
|
9
|
+
* Values match the approved Studio Matrix references
|
|
10
|
+
* (docs/design/2026-10-06-catalog-studio-matrix-*.html), with one deliberate exception:
|
|
11
|
+
* `textMuted` stays #666666 because the references' #777777 fails WCAG AA contrast.
|
|
12
|
+
*/
|
|
13
|
+
export const CATALOG_TYPE = {
|
|
14
|
+
xs: 10,
|
|
15
|
+
tableHeader: 11,
|
|
16
|
+
sm: 12,
|
|
17
|
+
panelHeading: 13,
|
|
18
|
+
md: 14,
|
|
19
|
+
lg: 16,
|
|
20
|
+
brand: 17,
|
|
21
|
+
xl: 20,
|
|
22
|
+
'2xl': 24,
|
|
23
|
+
pageTitle: 28,
|
|
24
|
+
'3xl': 32,
|
|
25
|
+
'4xl': 40,
|
|
26
|
+
};
|
|
27
|
+
export const CATALOG_SPACE = {
|
|
28
|
+
xs: 4,
|
|
29
|
+
sm: 8,
|
|
30
|
+
md: 12,
|
|
31
|
+
lg: 16,
|
|
32
|
+
xl: 24,
|
|
33
|
+
'2xl': 32,
|
|
34
|
+
'3xl': 40,
|
|
35
|
+
};
|
|
36
|
+
export const CATALOG_RADIUS = {
|
|
37
|
+
sm: 8,
|
|
38
|
+
control: 9,
|
|
39
|
+
md: 12,
|
|
40
|
+
card: 14,
|
|
41
|
+
};
|
|
42
|
+
/** Exact chrome measurements from the approved references that are not spacing-scale steps. */
|
|
43
|
+
export const CATALOG_LAYOUT = {
|
|
44
|
+
sidebarWidth: 264,
|
|
45
|
+
sidebarPaddingTop: 24,
|
|
46
|
+
sidebarPaddingX: 18,
|
|
47
|
+
mainPaddingTop: 28,
|
|
48
|
+
mainPaddingX: 32,
|
|
49
|
+
controlSize: 44,
|
|
50
|
+
focusRingWidth: 3,
|
|
51
|
+
navItemPaddingY: 8,
|
|
52
|
+
navItemPaddingX: 10,
|
|
53
|
+
panelPadding: 20,
|
|
54
|
+
factPaddingY: 9,
|
|
55
|
+
blockLabelGap: 14,
|
|
56
|
+
blockGap: 28,
|
|
57
|
+
/** Sidebar logo header (design `docs/design/2026-10-08-ds-viewer-sidebar-logo-approved.html`,
|
|
58
|
+
* "C · Adaptive", approved 2026-10-08) — see `native/catalog/logoLayout.ts`. */
|
|
59
|
+
logoMark: 32,
|
|
60
|
+
logoWordmarkHeight: 28,
|
|
61
|
+
logoCaptionGap: 6,
|
|
62
|
+
};
|
|
63
|
+
/** Max width of the main content column. */
|
|
64
|
+
export const CATALOG_MAX_CONTENT_WIDTH = 1200;
|
|
65
|
+
/** When to reach for each catalog-chrome type size. Rendered in the framework catalog's Type
|
|
66
|
+
* Scale page. */
|
|
67
|
+
export const CATALOG_TYPE_USE = {
|
|
68
|
+
xs: 'Sidebar group labels (uppercase); a prop\'s type annotation.',
|
|
69
|
+
tableHeader: 'Grid column headers and list cell captions (uppercase).',
|
|
70
|
+
sm: 'Breadcrumb, catalog caption, block labels, prop names and descriptions.',
|
|
71
|
+
panelHeading: 'Reference-panel headings: Guidance, Quick reference, Props (uppercase).',
|
|
72
|
+
md: 'Default body text: descriptions, nav links, grid row headers, quick-reference rows, the filter field.',
|
|
73
|
+
lg: 'Pager arrows and the filter field\'s clear (×) glyph.',
|
|
74
|
+
brand: 'The app name at the top of the sidebar.',
|
|
75
|
+
xl: 'Reserved — no current consumer.',
|
|
76
|
+
'2xl': 'Reserved — no current consumer.',
|
|
77
|
+
pageTitle: 'The selected page\'s title.',
|
|
78
|
+
'3xl': 'Reserved — no current consumer.',
|
|
79
|
+
'4xl': 'Reserved — no current consumer.',
|
|
80
|
+
};
|
|
81
|
+
/** When to reach for each catalog-chrome spacing step. Rendered in the framework catalog's
|
|
82
|
+
* Spacing page. */
|
|
83
|
+
export const CATALOG_SPACE_USE = {
|
|
84
|
+
xs: 'Tight gap — e.g. between a token\'s rendered value and its use-note.',
|
|
85
|
+
sm: 'Small gap — below the filter field; between a preview frame\'s label and its demo.',
|
|
86
|
+
md: 'Medium gap — between a card\'s contents; token-row divider padding.',
|
|
87
|
+
lg: 'Grid and list cell padding (16px); row gap in scale galleries.',
|
|
88
|
+
xl: 'Preview-card padding; gap between preview frames.',
|
|
89
|
+
'2xl': 'Gap between the reference panel\'s Guidance and Quick reference columns.',
|
|
90
|
+
'3xl': 'Bottom padding of the main column.',
|
|
91
|
+
};
|
|
92
|
+
export const CATALOG_COLOR = {
|
|
93
|
+
text: '#181818',
|
|
94
|
+
// #666666, not the references' #777777: #777 is 4.14–4.48:1 on this file's surfaces (WCAG AA
|
|
95
|
+
// needs 4.5:1); #666 is 5.3–5.7:1.
|
|
96
|
+
textMuted: '#666666',
|
|
97
|
+
/** Dividers inside grids, lists, props tables, token rows. */
|
|
98
|
+
border: '#e4e4e4',
|
|
99
|
+
/** Sidebar edge, reference card, filter field. */
|
|
100
|
+
borderHairline: '#dddddd',
|
|
101
|
+
/** Grid, list, and preview cards; pager buttons. */
|
|
102
|
+
borderStrong: '#d7d7d7',
|
|
103
|
+
/** Quick-reference row dividers. */
|
|
104
|
+
borderSubtle: '#eeeeee',
|
|
105
|
+
surface: '#ffffff',
|
|
106
|
+
/** Table headers, row headers, cell captions, filter field. */
|
|
107
|
+
surfaceMuted: '#fafafa',
|
|
108
|
+
/** Hover and pressed feedback on nav rows and buttons. */
|
|
109
|
+
surfacePressed: '#f1f3f8',
|
|
110
|
+
pageBackground: '#f6f6f4',
|
|
111
|
+
chip: '#eeeeee',
|
|
112
|
+
/** Active nav label, prop types, scale bars. */
|
|
113
|
+
accent: '#174dc6',
|
|
114
|
+
/** Active nav row background. */
|
|
115
|
+
accentSubtle: '#e9efff',
|
|
116
|
+
/** 3px keyboard focus ring on every catalog control. */
|
|
117
|
+
focusRing: '#c9d7ff',
|
|
118
|
+
code: 'Menlo',
|
|
119
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|