@krapwoo/ds-viewer 0.4.2 → 0.4.4
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 +111 -1
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/explain.js +29 -8
- package/dist/cli/props.js +20 -5
- package/dist/cli/staticPage.js +42 -6
- package/dist/cli/sync.js +2 -1
- package/dist/native/catalog/catalogNavigation.js +4 -2
- package/dist/native/catalog/comparison.js +22 -6
- package/dist/native/catalog/tokenLayout.js +26 -0
- package/native/catalog/CatalogShell.tsx +3 -1
- package/native/catalog/CatalogSidebar.tsx +7 -4
- package/native/catalog/ComparisonGrid.tsx +18 -5
- package/native/catalog/ComparisonGroups.tsx +18 -5
- package/native/catalog/ComparisonList.tsx +22 -6
- package/native/catalog/MotionSpecimen.tsx +152 -0
- package/native/catalog/ReferenceDetails.tsx +15 -0
- package/native/catalog/SectionBlock.tsx +50 -21
- package/native/catalog/SpecimenSurface.tsx +46 -0
- package/native/catalog/TokenLayouts.tsx +92 -0
- package/native/catalog/TokenRow.tsx +30 -5
- package/native/catalog/catalogNavigation.ts +7 -2
- package/native/catalog/comparison.ts +28 -9
- package/native/catalog/index.ts +8 -1
- package/native/catalog/motionValueDomain.ts +37 -0
- package/native/catalog/pageApi.ts +38 -3
- package/native/catalog/specimenSurfaceStyle.ts +15 -0
- package/native/catalog/tokenLayout.ts +29 -0
- package/native/catalog/types.ts +62 -0
- package/package.json +6 -1
- package/starter-kit/pages/Motion.catalog.tsx +48 -20
package/README.md
CHANGED
|
@@ -96,6 +96,12 @@ npm run check:catalog # the same headless check CI runs — every page, zero
|
|
|
96
96
|
documenting the viewer framework's own pieces (`CatalogShell`, `SectionBlock`, `ComparisonGrid`,
|
|
97
97
|
…), shown under a "Viewer" group alongside the starter kit's own pages.
|
|
98
98
|
|
|
99
|
+
## Contributing
|
|
100
|
+
|
|
101
|
+
Found a bug or an improvement while using ds-viewer in your app? Open an issue or a pull request:
|
|
102
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) explains how to try a change in your own app before opening
|
|
103
|
+
one, and what every pull request needs.
|
|
104
|
+
|
|
99
105
|
## Picking the right component
|
|
100
106
|
|
|
101
107
|
Several components look alike but solve different problems (InputField vs. SearchField vs.
|
|
@@ -184,8 +190,28 @@ actually apply:
|
|
|
184
190
|
within what fits a 1280px laptop: 5 compact, 3 regular, or 2 wide columns. `states` items whose
|
|
185
191
|
key matches a row or column key are not repeated below the grid.
|
|
186
192
|
- `specimenSize: 'compact' | 'regular' | 'wide'` — the page's specimen width class.
|
|
193
|
+
- `specimenSurface: 'neutral' | 'white' | 'dark' | 'transparent'` — the catalog-owned background
|
|
194
|
+
behind every live specimen on this page (list/grouped/grid cells, and a non-token `render()`
|
|
195
|
+
preview). Defaults to `'neutral'` (a light gray stage, visibly distinct from the surrounding
|
|
196
|
+
white card) — reach for `'dark'` to check a low-contrast component against a dark background, or
|
|
197
|
+
`'transparent'` when the surface itself would be misleading (e.g. a component whose own
|
|
198
|
+
elevation/shadow needs to read against the page, not a chip). Token galleries and sections always
|
|
199
|
+
keep their current, unstaged presentation regardless of this setting.
|
|
200
|
+
- `variants`/`states`' own `maxColumns: 1 | 2 | 3 | 4 | 5` — caps how many columns that slot's list
|
|
201
|
+
ever wraps into (e.g. a 3-item size scale that should always read as one row of exactly 3, never
|
|
202
|
+
more just because a laptop is wide). A narrow window still drops below the cap when it has to —
|
|
203
|
+
`maxColumns` only ever lowers the natural, width-based column count, never raises it.
|
|
187
204
|
- `group` on a `states` item — the `variants` key it belongs to (a size that only exists for one
|
|
188
205
|
variant). Grouped states render one row per variant; ungrouped ones go to "Other configurations".
|
|
206
|
+
- `composedOf: [{ component, role, relationship }]` — what this component is built from or
|
|
207
|
+
composed with, shown as a "Composition" subsection in Quick reference. `relationship` is
|
|
208
|
+
`'built-in'` for a component always present inside this one (e.g. Toast is built from a Banner),
|
|
209
|
+
`'slot'` for an optional caller-supplied child (e.g. a `leadingIcon` prop), or `'related'` for a
|
|
210
|
+
component commonly used alongside this one without either containing the other. `component` should be
|
|
211
|
+
a real component or page id: while you browse the catalog in development, an unknown name logs a
|
|
212
|
+
`[Catalog]` warning in the browser console. `doctor` doesn't check it, and it never fails a
|
|
213
|
+
build — e.g.
|
|
214
|
+
`composedOf: [{ component: 'Icon', role: 'Leading glyph', relationship: 'slot' }]`.
|
|
189
215
|
- `previewWidths: [402, 320]` — extra preview widths for a `render()` page (each capped at 402).
|
|
190
216
|
`CatalogShell`'s `defaultPreviewWidths="full"` keeps a catalog's previews full width.
|
|
191
217
|
- `variants: { desc?, align?, itemsFill?, items: [{ key, name, node }] }` — one item per prop enum
|
|
@@ -204,7 +230,91 @@ renders in a Preview card (phone width for components). For a token-gallery sect
|
|
|
204
230
|
data, not a component — see `ColorsGallery`/`SpacingGallery`/`TypographyGallery`), set
|
|
205
231
|
`tokenGallery: true` instead of `props`/`a11y`/`states` — `SectionBlock` then renders `render()`'s
|
|
206
232
|
output full width under a "Tokens" label and shows only a Quick reference card with the source
|
|
207
|
-
path.
|
|
233
|
+
path. For anything beyond a single list of tokens, use the token-page layouts below.
|
|
234
|
+
|
|
235
|
+
### Organising the catalog
|
|
236
|
+
|
|
237
|
+
- **`group`/`groupOrder` accept any semantic category you want** — not just `Tokens` and
|
|
238
|
+
`Components`. A page's `group` is a plain string shown as its sidebar section label;
|
|
239
|
+
`groupOrder` (e.g. `['Tokens', 'Components', 'Patterns', 'Recipes', 'Experiences']`) only
|
|
240
|
+
controls which order those sections appear in, top to bottom — any group not listed sorts
|
|
241
|
+
alphabetically after the ones that are. This starter kit's own two groups (`Tokens`, then
|
|
242
|
+
`Components`) are one valid shape, not the only one: a larger catalog might add `Patterns`
|
|
243
|
+
(reusable combinations like a settings row), `Recipes` (worked end-to-end screens), or
|
|
244
|
+
`Experiences` (full flows) alongside them. Pages sort A–Z inside each group regardless of how
|
|
245
|
+
many groups you have. Avoid splitting `Components` into finer categories such as "Actions" or
|
|
246
|
+
"Status" purely to browse by — that makes readers guess where a component lives; search and A–Z
|
|
247
|
+
already find it. A distinct semantic group (Tokens vs. Components vs. Patterns) is a different
|
|
248
|
+
decision from that, since it changes what the reader expects to find there.
|
|
249
|
+
- **Wide components at 3 columns.** Give block-level components (Banner, Card, Toast,
|
|
250
|
+
InputField) `specimenSize: 'regular'`, so Variants and States sit 3 across on a laptop
|
|
251
|
+
(about 316px each). `'wide'` (2 columns, phone width) is only for pages where the full phone
|
|
252
|
+
width is what you're checking, such as truncation or full-screen sheets. 4 columns is too narrow
|
|
253
|
+
for these components.
|
|
254
|
+
- **Mark OS components.** When a component is, or contains, a native platform control rather than
|
|
255
|
+
a custom one (a native date picker, iOS Liquid Glass), set `osComponent: 'full'` or
|
|
256
|
+
`'partial'`. The page shows an "OS component" or "Partly OS component" badge under its title;
|
|
257
|
+
say in `description` which part is native and how it differs on web, where the native part
|
|
258
|
+
usually can't render. Custom replacements styled after a platform control (a custom switch or
|
|
259
|
+
spinner) are not OS components.
|
|
260
|
+
- **Readable titles.** The page id is its address (`#ControlHeights`) and comes from the file
|
|
261
|
+
name; set `title: 'Control heights'` for the heading, sidebar and search. Pages still sort by id.
|
|
262
|
+
- **Point token pages at their token file.** A token page has no component, so its Source would be
|
|
263
|
+
the page file; set `source: 'src/design-system/tokens.ts'` (project-relative) instead.
|
|
264
|
+
- **Coverage only counts the component's own options.** `doctor`'s `option-not-covered` checks
|
|
265
|
+
string-literal unions declared in the component's own folder or a token folder. A union
|
|
266
|
+
declared in another component's folder, such as a shared `IconName`, is that component's
|
|
267
|
+
options, so a Banner page never needs one example per icon.
|
|
268
|
+
|
|
269
|
+
### Token pages
|
|
270
|
+
|
|
271
|
+
A token page shows each token at its real value with its role. Pick the layout by the token's
|
|
272
|
+
shape:
|
|
273
|
+
|
|
274
|
+
| Token shape | Layout | Example |
|
|
275
|
+
|---|---|---|
|
|
276
|
+
| Small, self-contained preview (radius, shadow, control height, a single colour) | `TokenGrid` of `TokenTile`s: equal tiles in as many columns as fit | Radius, Shadow, ControlHeights |
|
|
277
|
+
| Narrow sample with a long note (icon size, spacing) | `TokenRow` with `notePlacement="right"`: one line per token | IconSizes |
|
|
278
|
+
| Several kinds of token on one page | `tokenSections`: one titled card per kind | Colors (semantic vs palette) |
|
|
279
|
+
| A scale with families (type) | `tokenSections` with `tokenColumns: 2` or `3`, one section per family | Typography (label, body, emphasis…) |
|
|
280
|
+
| A duration/easing or spring value | `MotionSpecimen` next to the token's value | Motion (duration, easing, spring) |
|
|
281
|
+
|
|
282
|
+
- **`tokenSections: [{ title, desc?, wide?, render }]`** replaces `render`. Each section is its
|
|
283
|
+
own titled card. Keep different kinds of token in different sections, never mixed: semantic roles
|
|
284
|
+
apart from raw palette ramps, each type family apart.
|
|
285
|
+
- **`tokenColumns: 1 | 2 | 3`** places sections side by side. Use 2–3 for narrow sections (type
|
|
286
|
+
families); the page drops a column rather than squeeze a section under 280px. `wide: true`
|
|
287
|
+
gives a section its own full row, for wide content such as palette ramps.
|
|
288
|
+
- **Show each token's role, not its category.** The note says where or why the token is used, in
|
|
289
|
+
that order of preference: the token file's own comments, then a usage scan of the code (name the
|
|
290
|
+
main components and how many files use it, and say so when a token is unused). Never invent
|
|
291
|
+
advice, and never write a generic category such as "Regular reading text" when the name and
|
|
292
|
+
value already say it. If there's nothing grounded to say, leave the note out.
|
|
293
|
+
- **Label each sample with its name and real value** (`bodyMd · 16/22 · 400`, `medium · 12px`).
|
|
294
|
+
Use realistic sample text from the product, short enough to fit its column at the largest size.
|
|
295
|
+
- **`MotionSpecimen`** demonstrates a duration/easing or spring token as a real, bounded
|
|
296
|
+
start-to-end slide — a dot travels a fixed track once when it mounts, with a named Replay button
|
|
297
|
+
to play it again. Give it `kind: 'timing'` with `duration` (and optionally a real `Easing`
|
|
298
|
+
function, e.g. `Easing.bezier(...DS_MOTION_EASING.standard)`) for a duration/easing token, or
|
|
299
|
+
`kind: 'spring'` with a `spring` config shaped for `Animated.spring` (e.g. `DS_MOTION_SPRING`) for
|
|
300
|
+
a spring token. It respects the platform's reduce-motion preference automatically — with it on,
|
|
301
|
+
every play (including the initial one) shows the end state immediately instead of animating, so
|
|
302
|
+
it never needs its own per-page reduce-motion handling:
|
|
303
|
+
```tsx
|
|
304
|
+
<MotionSpecimen kind="timing" duration={DS_MOTION_DURATION.base} easing={Easing.bezier(...DS_MOTION_EASING.standard)} />
|
|
305
|
+
<MotionSpecimen kind="spring" spring={DS_MOTION_SPRING} />
|
|
306
|
+
```
|
|
307
|
+
A spring's `valueRange` says which domain its `spring` config's rest thresholds are scaled for,
|
|
308
|
+
so the specimen animates the same domain the real config does rather than always normalizing to
|
|
309
|
+
0–1. `'distance'` (default) is for a config tuned for pixel-space points, e.g. a `BottomSheet`'s
|
|
310
|
+
snap-point spring — the `Animated.Value` animates directly from `0` to the track's pixel
|
|
311
|
+
distance. `'unit'` is for a config whose production value is a normalized 0-to-1 progress or
|
|
312
|
+
blend factor — the `Animated.Value` animates `0` to `1` and is interpolated to the pixel
|
|
313
|
+
distance; `'distance'`'s pixel-scale thresholds would otherwise end a unit-valued spring before
|
|
314
|
+
it ever moves:
|
|
315
|
+
```tsx
|
|
316
|
+
<MotionSpecimen kind="spring" spring={UNIT_SPRING_CONFIG} valueRange="unit" />
|
|
317
|
+
```
|
|
208
318
|
|
|
209
319
|
## Keeping the catalog current
|
|
210
320
|
|
package/dist/cli/doctor.js
CHANGED
|
@@ -55,7 +55,7 @@ function checkComponentExportRemoved(pages, components) {
|
|
|
55
55
|
* `description` — in which case the viewer shows the exact same "No examples documented." an
|
|
56
56
|
* undocumented component would. */
|
|
57
57
|
function pageHasExamples(page) {
|
|
58
|
-
return Boolean(page.comparison) || Boolean(page.variantsItems?.length) || Boolean(page.statesItems?.length) || page.hasRender === true;
|
|
58
|
+
return Boolean(page.comparison) || Boolean(page.variantsItems?.length) || Boolean(page.statesItems?.length) || page.hasRender === true || Boolean(page.tokenSectionTitles?.length);
|
|
59
59
|
}
|
|
60
60
|
function checkComponentNoExamples(pages, components) {
|
|
61
61
|
const byComponent = new Map();
|
package/dist/cli/explain.js
CHANGED
|
@@ -4,16 +4,15 @@ import { discoverAllPageFiles, resolvePageId } from "./pageIndex.js";
|
|
|
4
4
|
import { resolveGlob } from "./glob.js";
|
|
5
5
|
import { axisItems, choosePlacement, gridColumnLimit, listGeometry, presentationBlocks, MATRIX_LAYOUT, } from "../native/catalog/comparison.js";
|
|
6
6
|
import { CATALOG_LAYOUT } from "../native/catalog/tokens.js";
|
|
7
|
+
import { sectionColumns } from "../native/catalog/tokenLayout.js";
|
|
7
8
|
function placeholderItem(item) {
|
|
8
9
|
return { key: item.key, name: item.name, node: null, group: item.group };
|
|
9
10
|
}
|
|
10
11
|
/** A throwaway `SectionDef` built from a `StaticPage` (Task 3) — every specimen's `node` is `null`
|
|
11
12
|
* (design §4: specimen nodes are never evaluated), since none of the layout functions below ever
|
|
12
13
|
* look at it; only the counts/keys/sizes they do look at are carried over. Every field Task 3
|
|
13
|
-
* reads is carried through here, including `previewWidths`/`fullWidthLabel`/`hide`/`itemsFill
|
|
14
|
-
*
|
|
15
|
-
* viewer for any page that set one of them (25 kit pages set `itemsFill`; every `kit-host`
|
|
16
|
-
* Viewer page sets `hide`). */
|
|
14
|
+
* reads is carried through here, including `previewWidths`/`fullWidthLabel`/`hide`/`itemsFill`/
|
|
15
|
+
* `maxColumns` — dropping any of them makes `explain` report different geometry than the viewer. */
|
|
17
16
|
function toSectionDef(page) {
|
|
18
17
|
const comparison = page.comparison && {
|
|
19
18
|
rowLabel: page.comparison.rowLabel ?? '',
|
|
@@ -31,10 +30,12 @@ function toSectionDef(page) {
|
|
|
31
30
|
fullWidthLabel: page.fullWidthLabel,
|
|
32
31
|
specimenSize: page.specimenSize,
|
|
33
32
|
previewWidths: page.previewWidths,
|
|
34
|
-
variants: page.variantsItems ? { itemsFill: page.variantsItemsFill, items: page.variantsItems.map(placeholderItem) } : undefined,
|
|
35
|
-
states: page.statesItems ? { itemsFill: page.statesItemsFill, items: page.statesItems.map(placeholderItem) } : undefined,
|
|
33
|
+
variants: page.variantsItems ? { itemsFill: page.variantsItemsFill, maxColumns: page.variantsMaxColumns, items: page.variantsItems.map(placeholderItem) } : undefined,
|
|
34
|
+
states: page.statesItems ? { itemsFill: page.statesItemsFill, maxColumns: page.statesMaxColumns, items: page.statesItems.map(placeholderItem) } : undefined,
|
|
36
35
|
comparison,
|
|
37
36
|
render: page.hasRender ? () => null : undefined,
|
|
37
|
+
tokenSections: page.tokenSectionTitles?.map((title, i) => ({ title, wide: page.tokenSectionWide?.[i] || undefined, render: () => null })),
|
|
38
|
+
tokenColumns: page.tokenColumns === 2 || page.tokenColumns === 3 ? page.tokenColumns : undefined,
|
|
38
39
|
hide: (page.hideVariants || page.hideStates) ? { variants: page.hideVariants, states: page.hideStates } : undefined,
|
|
39
40
|
};
|
|
40
41
|
}
|
|
@@ -50,7 +51,7 @@ function explainBlock(block) {
|
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
if (block.kind === 'list') {
|
|
53
|
-
const geometry = listGeometry(block.items.length, MATRIX_LAYOUT.laptopContentWidth, block.size);
|
|
54
|
+
const geometry = listGeometry(block.items.length, MATRIX_LAYOUT.laptopContentWidth, block.size, block.maxColumns);
|
|
54
55
|
return {
|
|
55
56
|
kind: 'list', title: block.title,
|
|
56
57
|
reason: `${block.items.length} items (${block.size}) → ${geometry.columns} columns × ${geometry.rows} row${geometry.rows === 1 ? '' : 's'}` +
|
|
@@ -64,6 +65,19 @@ function explainBlock(block) {
|
|
|
64
65
|
const widths = block.widths === 'full' ? 'full width' : `${block.widths.join('/')}px`;
|
|
65
66
|
return { kind: 'preview', title: block.title, reason: `render() content at ${widths}.` };
|
|
66
67
|
}
|
|
68
|
+
if (block.kind === 'tokenSections') {
|
|
69
|
+
const n = block.sections.length;
|
|
70
|
+
const titles = block.sections.every((s) => s.title) ? ` (${block.sections.map((s) => s.title).join(', ')})` : '';
|
|
71
|
+
const wide = block.sections.filter((s) => s.wide).length;
|
|
72
|
+
const narrow = n - wide;
|
|
73
|
+
const fit = sectionColumns(MATRIX_LAYOUT.laptopContentWidth, block.columns);
|
|
74
|
+
const parts = [];
|
|
75
|
+
if (narrow > 0)
|
|
76
|
+
parts.push(narrow > 1 && fit > 1 ? `${Math.min(fit, narrow)} side by side on a 1280px laptop` : 'one per row');
|
|
77
|
+
if (wide > 0)
|
|
78
|
+
parts.push(`${wide} full width`);
|
|
79
|
+
return { kind: 'tokenSections', title: 'Token sections', reason: `${n} section${n === 1 ? '' : 's'}${titles}: ${parts.join('; ')}.` };
|
|
80
|
+
}
|
|
67
81
|
return { kind: 'empty', title: block.title, reason: block.message };
|
|
68
82
|
}
|
|
69
83
|
/** Mirrors `native/catalog/SectionBlock.tsx`'s own `blockWidth` helper: a list block's width is its
|
|
@@ -73,7 +87,14 @@ function explainBlock(block) {
|
|
|
73
87
|
* computed uniformly here for either role, since setting it on `second` is simply unused, not wrong. */
|
|
74
88
|
function toPlacementBlock(block, height, available) {
|
|
75
89
|
if (block.kind === 'list') {
|
|
76
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
kind: 'list',
|
|
92
|
+
height,
|
|
93
|
+
itemCount: block.items.length,
|
|
94
|
+
size: block.size,
|
|
95
|
+
maxColumns: block.maxColumns,
|
|
96
|
+
width: listGeometry(block.items.length, available, block.size, block.maxColumns).containerWidth,
|
|
97
|
+
};
|
|
77
98
|
}
|
|
78
99
|
return { kind: block.kind, height };
|
|
79
100
|
}
|
package/dist/cli/props.js
CHANGED
|
@@ -97,7 +97,7 @@ export function createComponentReader(entryFiles, options = {}) {
|
|
|
97
97
|
const type = checker.getTypeOfSymbolAtLocation(resolved, declaration);
|
|
98
98
|
if (!isComponentType(checker, type))
|
|
99
99
|
continue;
|
|
100
|
-
records.push(readComponentRecord(checker, exportSymbol.getName(), type, declaration, options.optionRoots ?? []));
|
|
100
|
+
records.push(readComponentRecord(checker, exportSymbol.getName(), type, declaration, options.optionRoots ?? [], options.componentRoots ?? []));
|
|
101
101
|
}
|
|
102
102
|
return records.sort((a, b) => a.name.localeCompare(b.name));
|
|
103
103
|
};
|
|
@@ -151,7 +151,7 @@ function isComponentType(checker, type) {
|
|
|
151
151
|
return true;
|
|
152
152
|
return COMPONENT_TYPE_HINT.test(checker.typeToString(type));
|
|
153
153
|
}
|
|
154
|
-
function readComponentRecord(checker, name, type, declaration, optionRoots) {
|
|
154
|
+
function readComponentRecord(checker, name, type, declaration, optionRoots, componentRoots = []) {
|
|
155
155
|
const sourceFile = declaration.getSourceFile();
|
|
156
156
|
const signature = type.getCallSignatures()[0];
|
|
157
157
|
const propsType = signature && signature.parameters.length > 0 ? checker.getTypeOfSymbolAtLocation(signature.parameters[0], declaration) : undefined;
|
|
@@ -180,7 +180,7 @@ function readComponentRecord(checker, name, type, declaration, optionRoots) {
|
|
|
180
180
|
required: !isPartial && merged.requiredInEvery,
|
|
181
181
|
default: implementationDefaults.get(prop.getName()) ?? (jsDocDefault ? ts.displayPartsToString(jsDocDefault.text) : undefined),
|
|
182
182
|
desc: isPartial ? `${baseDesc} Only with some variants of this prop's type.`.trim() : baseDesc,
|
|
183
|
-
options: readOptions(checker, propType, propFile, optionRoots),
|
|
183
|
+
options: readOptions(checker, propType, propFile, optionRoots, componentRoots),
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
}
|
|
@@ -383,9 +383,10 @@ function mergeUnionProps(checker, branches) {
|
|
|
383
383
|
* 2 or more string literals, AND that union is declared inside one of `optionRoots` — checked
|
|
384
384
|
* against the union's alias symbol's declaration file (a named type like `Variant`), or the prop's
|
|
385
385
|
* own declaration file for an inline union with no alias. A single string literal is never
|
|
386
|
-
* options; an out-of-root union (e.g. a 44-member `IconName` from `icons/`) is skipped
|
|
386
|
+
* options; an out-of-root union (e.g. a 44-member `IconName` from `icons/`) is skipped, and so is a
|
|
387
|
+
* union declared in a different component's folder under a component root (a sibling `Icon/`).
|
|
387
388
|
*/
|
|
388
|
-
function readOptions(checker, propType, propDeclarationFile, optionRoots) {
|
|
389
|
+
function readOptions(checker, propType, propDeclarationFile, optionRoots, componentRoots = []) {
|
|
389
390
|
if (optionRoots.length === 0)
|
|
390
391
|
return undefined;
|
|
391
392
|
const nonNullable = checker.getNonNullableType(propType);
|
|
@@ -397,8 +398,22 @@ function readOptions(checker, propType, propDeclarationFile, optionRoots) {
|
|
|
397
398
|
const isLocal = optionRoots.some((root) => declarationFile === root || declarationFile.startsWith(`${root}${path.sep}`));
|
|
398
399
|
if (!isLocal)
|
|
399
400
|
return undefined;
|
|
401
|
+
if (declarationFile !== propDeclarationFile && isInOtherComponent(declarationFile, propDeclarationFile, componentRoots))
|
|
402
|
+
return undefined;
|
|
400
403
|
return nonNullable.types.map((t) => t.value);
|
|
401
404
|
}
|
|
405
|
+
/** True when `file` sits in a different component subfolder than `ownFile` under the same root. */
|
|
406
|
+
function isInOtherComponent(file, ownFile, componentRoots) {
|
|
407
|
+
const componentOf = (target, root) => {
|
|
408
|
+
const rel = path.relative(root, target);
|
|
409
|
+
return rel.startsWith('..') || path.isAbsolute(rel) ? undefined : rel.split(path.sep)[0];
|
|
410
|
+
};
|
|
411
|
+
return componentRoots.some((root) => {
|
|
412
|
+
const theirs = componentOf(file, root);
|
|
413
|
+
const ours = componentOf(ownFile, root);
|
|
414
|
+
return theirs !== undefined && ours !== undefined && theirs !== ours && path.extname(theirs) === '';
|
|
415
|
+
});
|
|
416
|
+
}
|
|
402
417
|
/** Finds the real implementation function behind a `React.memo(Impl)` / `forwardRef(function
|
|
403
418
|
* Name(...) {})` wrapper and reads each destructured parameter's default initializer as source
|
|
404
419
|
* text (design §3: "shown as source text"). Resolving the *value* of a non-literal default
|
package/dist/cli/staticPage.js
CHANGED
|
@@ -71,6 +71,28 @@ function readStaticPage(file) {
|
|
|
71
71
|
page.specimenSize = stringLiteral(resolve(specimenSizeProp.initializer, consts, new Set()));
|
|
72
72
|
if (tokenGalleryProp)
|
|
73
73
|
page.tokenGallery = resolve(tokenGalleryProp.initializer, consts, new Set()).kind === ts.SyntaxKind.TrueKeyword;
|
|
74
|
+
const tokenSectionsProp = findProp(pageObject, 'tokenSections');
|
|
75
|
+
if (tokenSectionsProp) {
|
|
76
|
+
const list = resolve(tokenSectionsProp.initializer, consts, new Set());
|
|
77
|
+
const items = ts.isArrayLiteralExpression(list) ? list.elements.map((element) => resolve(element, consts, new Set())) : [list];
|
|
78
|
+
const field = (item, name) => (ts.isObjectLiteralExpression(item) ? findProp(item, name) : undefined);
|
|
79
|
+
page.tokenSectionTitles = items.map((item) => {
|
|
80
|
+
const title = field(item, 'title');
|
|
81
|
+
return (title && stringLiteral(resolve(title.initializer, consts, new Set()))) ?? '';
|
|
82
|
+
});
|
|
83
|
+
page.tokenSectionWide = items.map((item) => {
|
|
84
|
+
const wide = field(item, 'wide');
|
|
85
|
+
return wide ? resolve(wide.initializer, consts, new Set()).kind === ts.SyntaxKind.TrueKeyword : false;
|
|
86
|
+
});
|
|
87
|
+
if (page.tokenSectionTitles.length > 0)
|
|
88
|
+
page.tokenGallery = true;
|
|
89
|
+
}
|
|
90
|
+
const tokenColumnsProp = findProp(pageObject, 'tokenColumns');
|
|
91
|
+
if (tokenColumnsProp) {
|
|
92
|
+
const value = resolve(tokenColumnsProp.initializer, consts, new Set());
|
|
93
|
+
if (ts.isNumericLiteral(value))
|
|
94
|
+
page.tokenColumns = Number(value.text);
|
|
95
|
+
}
|
|
74
96
|
page.hasWhenToUse = findProp(pageObject, 'whenToUse') !== undefined;
|
|
75
97
|
page.hasA11y = findProp(pageObject, 'a11y') !== undefined;
|
|
76
98
|
page.hasRender = findProp(pageObject, 'render') !== undefined;
|
|
@@ -124,6 +146,7 @@ function readStaticPage(file) {
|
|
|
124
146
|
else {
|
|
125
147
|
page.variantsItems = variants.items;
|
|
126
148
|
page.variantsItemsFill = variants.itemsFill;
|
|
149
|
+
page.variantsMaxColumns = variants.maxColumns;
|
|
127
150
|
}
|
|
128
151
|
const states = readSlotItems(findProp(pageObject, 'states'), consts);
|
|
129
152
|
if (!states.checkable)
|
|
@@ -131,6 +154,7 @@ function readStaticPage(file) {
|
|
|
131
154
|
else {
|
|
132
155
|
page.statesItems = states.items;
|
|
133
156
|
page.statesItemsFill = states.itemsFill;
|
|
157
|
+
page.statesMaxColumns = states.maxColumns;
|
|
134
158
|
}
|
|
135
159
|
return page;
|
|
136
160
|
}
|
|
@@ -335,11 +359,12 @@ function readLiteralProps(node, consts) {
|
|
|
335
359
|
}
|
|
336
360
|
return result;
|
|
337
361
|
}
|
|
338
|
-
/** A `variants`/`states` field: `{ itemsFill?, items: [...] }`, `items` read by
|
|
339
|
-
* Distinguishes "field absent" (always checkable, everything undefined) from
|
|
340
|
-
* not literal" (not checkable). `itemsFill` is read only if present and
|
|
341
|
-
* `previewWidths`/`fullWidthLabel`, it isn't in
|
|
342
|
-
* non-literal `itemsFill` is simply left unset
|
|
362
|
+
/** A `variants`/`states` field: `{ itemsFill?, maxColumns?, items: [...] }`, `items` read by
|
|
363
|
+
* `readListItems`. Distinguishes "field absent" (always checkable, everything undefined) from
|
|
364
|
+
* "field present but not literal" (not checkable). `itemsFill` is read only if present and
|
|
365
|
+
* literal — like `previewWidths`/`fullWidthLabel`, it isn't in the original literal-checkable
|
|
366
|
+
* list, so a non-literal `itemsFill` is simply left unset. `maxColumns` does affect the geometry
|
|
367
|
+
* `explain` reports, so it must be a literal integer from 1–5 or the page is not checkable. */
|
|
343
368
|
function readSlotItems(prop, consts) {
|
|
344
369
|
if (!prop)
|
|
345
370
|
return { checkable: true };
|
|
@@ -352,5 +377,16 @@ function readSlotItems(prop, consts) {
|
|
|
352
377
|
return { checkable: false };
|
|
353
378
|
const itemsFillProp = ts.isObjectLiteralExpression(resolved) ? findProp(resolved, 'itemsFill') : undefined;
|
|
354
379
|
const itemsFill = itemsFillProp ? resolve(itemsFillProp.initializer, consts, new Set()).kind === ts.SyntaxKind.TrueKeyword : undefined;
|
|
355
|
-
|
|
380
|
+
const maxColumnsProp = ts.isObjectLiteralExpression(resolved) ? findProp(resolved, 'maxColumns') : undefined;
|
|
381
|
+
let maxColumns;
|
|
382
|
+
if (maxColumnsProp) {
|
|
383
|
+
const value = resolve(maxColumnsProp.initializer, consts, new Set());
|
|
384
|
+
if (!ts.isNumericLiteral(value))
|
|
385
|
+
return { checkable: false };
|
|
386
|
+
const parsed = Number(value.text);
|
|
387
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 5)
|
|
388
|
+
return { checkable: false };
|
|
389
|
+
maxColumns = parsed;
|
|
390
|
+
}
|
|
391
|
+
return { items, itemsFill, maxColumns, checkable: true };
|
|
356
392
|
}
|
package/dist/cli/sync.js
CHANGED
|
@@ -49,7 +49,8 @@ export function sync(config, resolveOptions) {
|
|
|
49
49
|
const fallbackPaths = hasEntryOutsideProjectRoot
|
|
50
50
|
? { '*': [path.join(config.projectRoot, 'node_modules', '@types', '*'), path.join(config.projectRoot, 'node_modules', '*')] }
|
|
51
51
|
: undefined;
|
|
52
|
-
const
|
|
52
|
+
const componentRoots = [...new Set(config.components.map((pattern) => path.join(config.projectRoot, globBaseFolder(pattern))))];
|
|
53
|
+
const readComponent = createComponentReader(componentEntryFiles, { ...resolveOptions, optionRoots, componentRoots, fallbackPaths });
|
|
53
54
|
const warnings = [];
|
|
54
55
|
for (const entryFile of componentEntryFiles) {
|
|
55
56
|
try {
|
|
@@ -34,12 +34,14 @@ export function neighbors(order, id) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
/** Sidebar filter: case-insensitive substring match, sorted ids, empty groups removed. */
|
|
37
|
-
export function filterGroups(groups, query
|
|
37
|
+
export function filterGroups(groups, query,
|
|
38
|
+
/** Display titles by id; a page matches on its id or its title. */
|
|
39
|
+
labels) {
|
|
38
40
|
const q = query.trim().toLowerCase();
|
|
39
41
|
return groups
|
|
40
42
|
.map((group) => ({
|
|
41
43
|
label: group.label,
|
|
42
|
-
ids: sortIds(q ? group.ids.filter((id) => id.toLowerCase().includes(q)) : group.ids),
|
|
44
|
+
ids: sortIds(q ? group.ids.filter((id) => id.toLowerCase().includes(q) || (labels?.get(id)?.toLowerCase().includes(q) ?? false)) : group.ids),
|
|
43
45
|
}))
|
|
44
46
|
.filter((group) => group.ids.length > 0);
|
|
45
47
|
}
|
|
@@ -75,13 +75,18 @@ export function gridWidthBounds(columnCount, size) {
|
|
|
75
75
|
/** Balanced wrapping for a one-axis list inside a single bordered card (1px outer border). Each
|
|
76
76
|
* cell's 1px divider sits inside its own width, so no cell is ever wider than 402px. Uses as few
|
|
77
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
|
-
|
|
78
|
+
* space as possible. `fillers` blank cells complete the last row. `columnCap` (from
|
|
79
|
+
* `VariantSlot.maxColumns`) only ever lowers the natural, width-based column count — a narrow
|
|
80
|
+
* container still drops below it when it has to, so the cap can never widen a row past what the
|
|
81
|
+
* available width actually fits. */
|
|
82
|
+
export function listGeometry(itemCount, availableWidth, size, columnCap) {
|
|
80
83
|
if (itemCount <= 0)
|
|
81
84
|
return { columns: 0, rows: 0, fillers: 0, cellWidth: 0, containerWidth: 0 };
|
|
82
85
|
const inner = Math.max(0, availableWidth - 2);
|
|
83
86
|
const min = COLUMN_MIN_WIDTH[size];
|
|
84
|
-
const
|
|
87
|
+
const widthLimit = Math.floor(inner / min);
|
|
88
|
+
const limit = columnCap !== undefined ? Math.min(widthLimit, columnCap) : widthLimit;
|
|
89
|
+
const maxColumns = Math.max(1, Math.min(itemCount, limit));
|
|
85
90
|
const rows = Math.ceil(itemCount / maxColumns);
|
|
86
91
|
const columns = Math.ceil(itemCount / rows);
|
|
87
92
|
const fitted = Math.floor(inner / columns);
|
|
@@ -213,6 +218,10 @@ function groupStates(variants, states) {
|
|
|
213
218
|
* never shows an empty block next to a real one; with nothing documented at all it shows one
|
|
214
219
|
* "No examples documented." block. */
|
|
215
220
|
export function presentationBlocks(def, options = {}) {
|
|
221
|
+
if (def.tokenSections && def.tokenSections.length > 0) {
|
|
222
|
+
// Each section carries its own label, so the block itself has none.
|
|
223
|
+
return [{ kind: 'tokenSections', title: '', sections: def.tokenSections, columns: def.tokenColumns ?? 1 }];
|
|
224
|
+
}
|
|
216
225
|
if (def.tokenGallery) {
|
|
217
226
|
const title = def.fullWidthLabel ?? 'Tokens';
|
|
218
227
|
return [def.render ? { kind: 'preview', title, widths: 'full' } : { kind: 'empty', title, message: 'Nothing to preview.' }];
|
|
@@ -237,7 +246,13 @@ export function presentationBlocks(def, options = {}) {
|
|
|
237
246
|
states = grouped.leftovers;
|
|
238
247
|
}
|
|
239
248
|
else if (def.variants) {
|
|
240
|
-
blocks.push({
|
|
249
|
+
blocks.push({
|
|
250
|
+
kind: 'list',
|
|
251
|
+
title: 'Variants',
|
|
252
|
+
size: slotSize(def, def.variants),
|
|
253
|
+
items: slotItems(def.variants),
|
|
254
|
+
...(def.variants.maxColumns !== undefined ? { maxColumns: def.variants.maxColumns } : {}),
|
|
255
|
+
});
|
|
241
256
|
}
|
|
242
257
|
else if (def.render) {
|
|
243
258
|
blocks.push({ kind: 'preview', title: 'Preview', widths: previewWidths(def.previewWidths, options.defaultPreviewWidths) });
|
|
@@ -250,6 +265,7 @@ export function presentationBlocks(def, options = {}) {
|
|
|
250
265
|
title: secondary ? 'Other configurations' : 'States / configurations',
|
|
251
266
|
size: slotSize(def, states),
|
|
252
267
|
items: slotItems(states),
|
|
268
|
+
...(states.maxColumns !== undefined ? { maxColumns: states.maxColumns } : {}),
|
|
253
269
|
});
|
|
254
270
|
}
|
|
255
271
|
if (blocks.length === 0 && !hide.variants && !hide.states) {
|
|
@@ -270,8 +286,8 @@ export function choosePlacement({ available, gap, first, second }) {
|
|
|
270
286
|
const minCell = second.size === 'wide' ? MATRIX_LAYOUT.columnMaxWidth : COLUMN_MIN_WIDTH[second.size];
|
|
271
287
|
if (room < minCell + 2)
|
|
272
288
|
return 'stacked';
|
|
273
|
-
const stackedRows = listGeometry(second.itemCount, available, second.size).rows;
|
|
274
|
-
const sideRows = listGeometry(second.itemCount, room, second.size).rows;
|
|
289
|
+
const stackedRows = listGeometry(second.itemCount, available, second.size, second.maxColumns).rows;
|
|
290
|
+
const sideRows = listGeometry(second.itemCount, room, second.size, second.maxColumns).rows;
|
|
275
291
|
const rowHeight = second.height / Math.max(1, stackedRows);
|
|
276
292
|
const sideHeight = Math.max(first.height, sideRows * rowHeight);
|
|
277
293
|
const stackedHeight = first.height + gap + second.height;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure layout maths for token pages — no React or React Native runtime imports, so it is unit
|
|
3
|
+
* tested directly (`__tests__/tokenLayout.test.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Token pages lay out in two levels:
|
|
6
|
+
* - sections (one titled card per kind of token, e.g. "Semantic" vs "Palette") flow in up to
|
|
7
|
+
* `tokenColumns` columns, dropping to fewer rather than squeezing a section under its minimum;
|
|
8
|
+
* - inside a section, `TokenGrid` tiles as many equal tiles per row as keep their minimum width.
|
|
9
|
+
*/
|
|
10
|
+
/** Narrowest a side-by-side token section may get before the page drops a column. */
|
|
11
|
+
export const TOKEN_SECTION_MIN_WIDTH = 280;
|
|
12
|
+
/** Gap between side-by-side sections and between tiles. */
|
|
13
|
+
export const TOKEN_LAYOUT_GAP = 20;
|
|
14
|
+
/** How many section columns fit in `available` px, at most `requested`, never below 1. */
|
|
15
|
+
export function sectionColumns(available, requested) {
|
|
16
|
+
const fit = Math.floor((available + TOKEN_LAYOUT_GAP) / (TOKEN_SECTION_MIN_WIDTH + TOKEN_LAYOUT_GAP));
|
|
17
|
+
return Math.max(1, Math.min(requested, fit));
|
|
18
|
+
}
|
|
19
|
+
/** Equal tiles per row for a `TokenGrid`: as many as keep `minTileWidth` (capped by `maxColumns`),
|
|
20
|
+
* sized so the row is filled exactly. */
|
|
21
|
+
export function tileGeometry(available, minTileWidth, maxColumns = Infinity) {
|
|
22
|
+
const fit = Math.floor((available + TOKEN_LAYOUT_GAP) / (minTileWidth + TOKEN_LAYOUT_GAP));
|
|
23
|
+
const columns = Math.max(1, Math.min(maxColumns, fit));
|
|
24
|
+
const tileWidth = columns === 1 ? available : (available - TOKEN_LAYOUT_GAP * (columns - 1)) / columns;
|
|
25
|
+
return { columns, tileWidth };
|
|
26
|
+
}
|
|
@@ -136,6 +136,7 @@ export function CatalogShell<TId extends string>({
|
|
|
136
136
|
updateEndpoint?: { baseUrl: string; secret: string };
|
|
137
137
|
}) {
|
|
138
138
|
const sectionsById = useMemo(() => new Map(sections.map((def) => [def.id, def])), [sections]);
|
|
139
|
+
const labels = useMemo(() => new Map(sections.filter((def) => def.title).map((def) => [def.id as string, def.title as string])), [sections]);
|
|
139
140
|
const order = useMemo(() => orderedIds(groups, new Set(sectionsById.keys())), [groups, sectionsById]);
|
|
140
141
|
const [active, setActive] = useState<ShellPageId<TId> | undefined>(() =>
|
|
141
142
|
isWeb() ? resolveActiveFromHash(window.location.hash, order) : order[0],
|
|
@@ -215,7 +216,7 @@ export function CatalogShell<TId extends string>({
|
|
|
215
216
|
|
|
216
217
|
return (
|
|
217
218
|
<View style={styles.root}>
|
|
218
|
-
<CatalogSidebar logo={appName} logoImageSource={logoImageSource} caption={title} groups={groups} active={active} onPress={select} footer={footer} />
|
|
219
|
+
<CatalogSidebar logo={appName} logoImageSource={logoImageSource} caption={title} groups={groups} labels={labels} active={active} onPress={select} footer={footer} />
|
|
219
220
|
<ScrollView ref={scrollRef} style={styles.main} contentContainerStyle={styles.mainContent}>
|
|
220
221
|
{active === UPDATE_PAGE_ID ? (
|
|
221
222
|
<UpdatePanel update={update ?? null} endpoint={updateEndpoint} appName={appName} headingRef={headingRef} />
|
|
@@ -233,6 +234,7 @@ export function CatalogShell<TId extends string>({
|
|
|
233
234
|
previousId: neighbors(order, activeDef.id).previous,
|
|
234
235
|
nextId: neighbors(order, activeDef.id).next,
|
|
235
236
|
onNavigate: select,
|
|
237
|
+
labelOf: (id) => labels.get(id) ?? id,
|
|
236
238
|
}}
|
|
237
239
|
headingRef={headingRef}
|
|
238
240
|
defaultPreviewWidths={defaultPreviewWidths}
|
|
@@ -13,7 +13,7 @@ function webLinkProps(id: string, active: boolean) {
|
|
|
13
13
|
|
|
14
14
|
/** One nav link. Hover via onHoverIn/onHoverOut (react-native-web fires them; native never does);
|
|
15
15
|
* keyboard focus draws the catalog focus ring because react-native-web removes the browser's. */
|
|
16
|
-
function NavItem<TId extends string>({ id, active, onPress }: { id: TId; active: boolean; onPress: () => void }) {
|
|
16
|
+
function NavItem<TId extends string>({ id, label, active, onPress }: { id: TId; label: string; active: boolean; onPress: () => void }) {
|
|
17
17
|
const [hovered, setHovered] = useState(false);
|
|
18
18
|
const [focused, setFocused] = useState(false);
|
|
19
19
|
return (
|
|
@@ -38,7 +38,7 @@ function NavItem<TId extends string>({ id, active, onPress }: { id: TId; active:
|
|
|
38
38
|
focused && styles.focusRing,
|
|
39
39
|
]}
|
|
40
40
|
>
|
|
41
|
-
<Text style={[styles.label, active && styles.labelActive]}>{
|
|
41
|
+
<Text style={[styles.label, active && styles.labelActive]}>{label}</Text>
|
|
42
42
|
</Pressable>
|
|
43
43
|
);
|
|
44
44
|
}
|
|
@@ -79,6 +79,7 @@ export function CatalogSidebar<TId extends string>({
|
|
|
79
79
|
logoImageSource,
|
|
80
80
|
caption,
|
|
81
81
|
groups,
|
|
82
|
+
labels,
|
|
82
83
|
active,
|
|
83
84
|
onPress,
|
|
84
85
|
footer,
|
|
@@ -91,6 +92,8 @@ export function CatalogSidebar<TId extends string>({
|
|
|
91
92
|
logoImageSource?: ImageSourcePropType;
|
|
92
93
|
caption: string;
|
|
93
94
|
groups: NavGroup<TId>[];
|
|
95
|
+
/** Display titles by page id (`SectionDef.title`); pages without one show their id. */
|
|
96
|
+
labels?: ReadonlyMap<string, string>;
|
|
94
97
|
active: string | undefined;
|
|
95
98
|
onPress: (id: TId) => void;
|
|
96
99
|
/** The quiet update-notice link below the nav list (design §5's footer line), or `undefined`
|
|
@@ -112,7 +115,7 @@ export function CatalogSidebar<TId extends string>({
|
|
|
112
115
|
const [loadedAspect, setLoadedAspect] = useState<number | undefined>(undefined);
|
|
113
116
|
const [logoFailed, setLogoFailed] = useState(false);
|
|
114
117
|
const [nameWrapped, setNameWrapped] = useState(false);
|
|
115
|
-
const filtered = filterGroups(groups, query);
|
|
118
|
+
const filtered = filterGroups(groups, query, labels);
|
|
116
119
|
|
|
117
120
|
const logoAspect = staticAspect ?? loadedAspect;
|
|
118
121
|
const attemptingLogo = Boolean(logoImageSource) && !logoFailed;
|
|
@@ -191,7 +194,7 @@ export function CatalogSidebar<TId extends string>({
|
|
|
191
194
|
<View key={group.label}>
|
|
192
195
|
<Text style={styles.groupLabel}>{group.label}</Text>
|
|
193
196
|
{group.ids.map((id) => (
|
|
194
|
-
<NavItem key={id} id={id} active={active === id} onPress={() => onPress(id)} />
|
|
197
|
+
<NavItem key={id} id={id} label={labels?.get(id) ?? id} active={active === id} onPress={() => onPress(id)} />
|
|
195
198
|
))}
|
|
196
199
|
</View>
|
|
197
200
|
))}
|
|
@@ -2,14 +2,26 @@ import React from 'react';
|
|
|
2
2
|
import { View, Text, ScrollView, StyleSheet } from 'react-native';
|
|
3
3
|
import { CATALOG_COLOR, CATALOG_RADIUS, CATALOG_TYPE } from './tokens';
|
|
4
4
|
import { COLUMN_MIN_WIDTH, MATRIX_LAYOUT, axisItems, cellKey, gridWidthBounds, indexCells, validateComparison } from './comparison';
|
|
5
|
-
import
|
|
5
|
+
import { SpecimenSurface } from './SpecimenSurface';
|
|
6
|
+
import type { ComparisonDef, SpecimenSize, SpecimenSurfaceKind } from './types';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* A grid of two props that combine freely, rendered as an accessible table inside one card.
|
|
9
10
|
* Columns grow with the window up to 402px and never shrink below the specimen size's minimum;
|
|
10
|
-
* a grid wider than its container scrolls horizontally inside the card, never the page.
|
|
11
|
+
* a grid wider than its container scrolls horizontally inside the card, never the page. `surface`
|
|
12
|
+
* is the page's `specimenSurface` behind each cell's example.
|
|
11
13
|
*/
|
|
12
|
-
export function ComparisonGrid({
|
|
14
|
+
export function ComparisonGrid({
|
|
15
|
+
def,
|
|
16
|
+
size,
|
|
17
|
+
sectionId,
|
|
18
|
+
surface = 'neutral',
|
|
19
|
+
}: {
|
|
20
|
+
def: ComparisonDef;
|
|
21
|
+
size: SpecimenSize;
|
|
22
|
+
sectionId: string;
|
|
23
|
+
surface?: SpecimenSurfaceKind;
|
|
24
|
+
}) {
|
|
13
25
|
if (__DEV__) {
|
|
14
26
|
for (const issue of validateComparison({ ...def, size })) console.warn(`[Catalog] ${sectionId}: ${issue}`);
|
|
15
27
|
}
|
|
@@ -53,7 +65,9 @@ export function ComparisonGrid({ def, size, sectionId }: { def: ComparisonDef; s
|
|
|
53
65
|
return (
|
|
54
66
|
<View key={c.key} role="cell" style={[styles.cell, column, styles.bodyRow, styles.specimenCell, ci === lastColumn && styles.lastColumn]}>
|
|
55
67
|
{cell?.node !== undefined ? (
|
|
56
|
-
<
|
|
68
|
+
<SpecimenSurface surface={surface} style={cell.fill && styles.fill}>
|
|
69
|
+
{cell.node}
|
|
70
|
+
</SpecimenSurface>
|
|
57
71
|
) : (
|
|
58
72
|
<Text style={styles.unavailable}>
|
|
59
73
|
{cell?.unavailableReason ? `Not supported — ${cell.unavailableReason}` : 'Missing example'}
|
|
@@ -88,7 +102,6 @@ const styles = StyleSheet.create({
|
|
|
88
102
|
headerCell: { backgroundColor: CATALOG_COLOR.surfaceMuted, justifyContent: 'center' },
|
|
89
103
|
bodyRow: { minHeight: MATRIX_LAYOUT.rowMinHeight },
|
|
90
104
|
specimenCell: { alignItems: 'center', justifyContent: 'center' },
|
|
91
|
-
center: { alignItems: 'center' },
|
|
92
105
|
fill: { alignSelf: 'stretch' },
|
|
93
106
|
headerText: { fontSize: CATALOG_TYPE.tableHeader, fontWeight: '800', letterSpacing: 0.44, color: CATALOG_COLOR.text },
|
|
94
107
|
rowHeaderText: { fontSize: CATALOG_TYPE.md, fontWeight: '700', color: CATALOG_COLOR.text },
|