@krapwoo/ds-viewer 0.15.0 → 0.16.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 +25 -1
- package/config/index.ts +3 -0
- package/dist/cli/config.js +9 -1
- package/dist/cli/doctor.js +146 -2
- package/dist/cli/staticPage.js +9 -4
- package/native/catalog/pageApi.ts +3 -0
- package/package.json +1 -1
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +1 -0
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +1 -0
- package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +1 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +1 -0
- package/starter-kit/components/Divider/Divider.catalog.tsx +1 -0
- package/starter-kit/components/Pill/Pill.catalog.tsx +1 -0
- package/starter-kit/components/TopNav/TopNav.catalog.tsx +1 -0
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ identified.
|
|
|
132
132
|
|
|
133
133
|
`ds-viewer.config.ts` fields: `name`, `logo` (optional — a path to an image asset, shown in the
|
|
134
134
|
sidebar beside the name, or in place of it for a wide wordmark; validated at `sync`/`dev` time
|
|
135
|
-
and bundled into the preview workspace), `components`, `exclude`, `tokens`, `pages`, `groupOrder`, `starterKit` (new-project only —
|
|
135
|
+
and bundled into the preview workspace), `components`, `exclude`, `tokens`, `pages`, `groupOrder`, `patternGroups`, `starterKit` (new-project only —
|
|
136
136
|
records the kit's version), `updateCheck`, `doctor` — see `config/index.ts`'s `DsViewerConfig` for
|
|
137
137
|
each field's exact meaning. Write a page with `defineCatalogPage()` from `@krapwoo/ds-viewer` (same
|
|
138
138
|
shape as a `SectionDef`, minus `id`/`path`/`props`, plus `component`, `group`, and `propNotes`).
|
|
@@ -495,6 +495,13 @@ path. For anything beyond a single list of tokens, use the token-page layouts be
|
|
|
495
495
|
title, not the id — so "Control heights" sorts under "C", not under whatever the id starts with.
|
|
496
496
|
- **Point token pages at their token file.** A token page has no component, so its Source would be
|
|
497
497
|
the page file; set `source: 'src/design-system/tokens.ts'` (project-relative) instead.
|
|
498
|
+
- **Parts and empty pages.** `doctor` warns about a page that is only a built-in part of one other
|
|
499
|
+
page (`part-own-page`) and about a page that shows nothing (`nothing-to-show`); see the rule list
|
|
500
|
+
under `doctor`. To keep such a page on purpose, set `standalone: '<why>'`, for example
|
|
501
|
+
`standalone: 'Not a part of GitFileCard; it draws its own counts.'`. The reason must be non-blank
|
|
502
|
+
and is only a note in the source — the viewer ignores it. Family and group pages, token pages and
|
|
503
|
+
`navigation: 'reference'` pages are never checked. Both checks count `composedOf` written on a
|
|
504
|
+
family or group member as well as on a page.
|
|
498
505
|
- **Coverage only counts the component's own options.** `doctor`'s `option-not-covered` checks
|
|
499
506
|
string-literal unions declared in the component's own folder or a token folder. A union
|
|
500
507
|
declared in another component's folder, such as a shared `IconName`, is that component's
|
|
@@ -905,6 +912,23 @@ Button
|
|
|
905
912
|
non-`inWhole` section, naming every one of them so you can act without opening the file. Give
|
|
906
913
|
each its own catalog page and name it in the family's `relatedPages` list instead; the section
|
|
907
914
|
keeps working until you do.
|
|
915
|
+
- `part-own-page` — a standalone page for a component that exactly one other page declares in
|
|
916
|
+
`composedOf` as `relationship: 'built-in'`, and that page doesn't list it as a member. Fold it
|
|
917
|
+
into that page's family, or record why it isn't part of it with `standalone`. A component built
|
|
918
|
+
into two or more pages is exempt, and `'slot'`/`'related'` entries never count. Pages in a pattern
|
|
919
|
+
group (example screens built from components) never count as the whole: by default the group
|
|
920
|
+
named `Patterns`; set `patternGroups` in `ds-viewer.config.ts` to name your own (e.g.
|
|
921
|
+
`patternGroups: ['Screens']`).
|
|
922
|
+
- `nothing-to-show` — a plain page with no variants, states, `render` or comparison, and no
|
|
923
|
+
members or token content (`previewWidths` alone doesn't count; it only sizes `render`). The
|
|
924
|
+
message names the pages that list it in `composedOf` — on the page or on one of its members —
|
|
925
|
+
or says only pattern pages use it, or that nothing does. List it as a documentation-only part
|
|
926
|
+
of a page that uses it, set `navigation: 'reference'`, or add a `standalone` reason. A page
|
|
927
|
+
whose specimens can't be read literally is never reported. A component page with no specimens
|
|
928
|
+
usually also gets `component-no-examples` (unless the component is documented elsewhere, on
|
|
929
|
+
another page or as a family member); the two say different things (no examples vs. no reason to
|
|
930
|
+
be its own page) and clear together once it has a specimen.
|
|
931
|
+
- `standalone-reason-empty` — `standalone` is blank, so it records no reason and opts nothing out.
|
|
908
932
|
|
|
909
933
|
The catalog also warns at render time when a page under-declares the width its examples need.
|
|
910
934
|
A `PhoneFrame` given a cell narrower than the 375px device scales the whole phone down, and its
|
package/config/index.ts
CHANGED
|
@@ -16,6 +16,9 @@ export interface DsViewerConfig {
|
|
|
16
16
|
pages?: string[];
|
|
17
17
|
/** Sidebar group display order; groups not listed here follow alphabetically. */
|
|
18
18
|
groupOrder?: string[];
|
|
19
|
+
/** Sidebar groups whose pages are patterns: example screens built from components. `doctor`
|
|
20
|
+
* never treats a pattern page as the whole a component is a part of. @default ['Patterns'] */
|
|
21
|
+
patternGroups?: string[];
|
|
19
22
|
/** Starter-kit projects only. `root` is the folder `init --new` copied the kit into (default
|
|
20
23
|
* `src/ds`); absent on a config written before 0.4, in which case `cli/kitRoot.ts` infers it
|
|
21
24
|
* from the first `components` glob instead. */
|
package/dist/cli/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { defineConfig } from "../config/index.js";
|
|
5
5
|
const KNOWN_FIELDS = new Set([
|
|
6
|
-
'name', 'logo', 'components', 'exclude', 'tokens', 'pages', 'groupOrder', 'starterKit', 'updateCheck', 'doctor',
|
|
6
|
+
'name', 'logo', 'components', 'exclude', 'tokens', 'pages', 'groupOrder', 'patternGroups', 'starterKit', 'updateCheck', 'doctor',
|
|
7
7
|
]);
|
|
8
8
|
/**
|
|
9
9
|
* Loads `ds-viewer.config.ts` with a bundled TypeScript loader. This is a guard rail, not a
|
|
@@ -41,6 +41,13 @@ export function loadConfig(configPath) {
|
|
|
41
41
|
if (!KNOWN_FIELDS.has(key))
|
|
42
42
|
warnings.push(`Unknown config field "${key}" is ignored.`);
|
|
43
43
|
}
|
|
44
|
+
let patternGroups;
|
|
45
|
+
if (raw.patternGroups !== undefined) {
|
|
46
|
+
if (Array.isArray(raw.patternGroups) && raw.patternGroups.every((name) => typeof name === 'string'))
|
|
47
|
+
patternGroups = raw.patternGroups;
|
|
48
|
+
else
|
|
49
|
+
warnings.push('"patternGroups" must be an array of group names; using the default [\'Patterns\'].');
|
|
50
|
+
}
|
|
44
51
|
const config = {
|
|
45
52
|
name: raw.name ?? '',
|
|
46
53
|
logo: raw.logo,
|
|
@@ -49,6 +56,7 @@ export function loadConfig(configPath) {
|
|
|
49
56
|
tokens: raw.tokens ?? [],
|
|
50
57
|
pages: raw.pages ?? [],
|
|
51
58
|
groupOrder: raw.groupOrder ?? [],
|
|
59
|
+
patternGroups,
|
|
52
60
|
starterKit: raw.starterKit,
|
|
53
61
|
updateCheck: raw.updateCheck ?? true,
|
|
54
62
|
doctor: raw.doctor ?? { strict: false },
|
package/dist/cli/doctor.js
CHANGED
|
@@ -530,6 +530,146 @@ function checkMissingComposition(pages, components) {
|
|
|
530
530
|
}
|
|
531
531
|
return issues;
|
|
532
532
|
}
|
|
533
|
+
/** #66: a page is a family or group (it has `members`) — presence, so a page whose `members` could
|
|
534
|
+
* not be read is still never treated as a plain page. */
|
|
535
|
+
function hasMembers(page) {
|
|
536
|
+
return page.hasMembers === true || Boolean(page.members && page.members.length > 0);
|
|
537
|
+
}
|
|
538
|
+
/** #66: a token page — a `tokenDefinition`, a token gallery, or token sections, by presence. */
|
|
539
|
+
function isTokenPage(page) {
|
|
540
|
+
return page.hasTokenDefinition === true || page.tokenDefinition !== undefined || page.tokenGallery === true
|
|
541
|
+
|| page.hasExplicitTokenGallery === true || page.hasExplicitTokenSections === true || Boolean(page.tokenSectionTitles?.length);
|
|
542
|
+
}
|
|
543
|
+
/** #66: a non-blank `standalone` reason opts a page out of `part-own-page` and `nothing-to-show`. */
|
|
544
|
+
function hasStandaloneReason(page) {
|
|
545
|
+
return page.standalone !== undefined && page.standalone.trim() !== '';
|
|
546
|
+
}
|
|
547
|
+
/** #66: the pages the two structure rules consider — readable, plain (no members, not a token page),
|
|
548
|
+
* browse-listed, and without a recorded `standalone` reason. */
|
|
549
|
+
function isStructureCandidate(page) {
|
|
550
|
+
return page.checkable && !hasMembers(page) && !isTokenPage(page) && page.navigation !== 'reference' && !hasStandaloneReason(page);
|
|
551
|
+
}
|
|
552
|
+
/** #66: the default `patternGroups` — the starter kit's own group for example screens. */
|
|
553
|
+
export const DEFAULT_PATTERN_GROUPS = ['Patterns'];
|
|
554
|
+
/** #66: whether a page sits in a pattern group (case-insensitive). A pattern page uses components; it
|
|
555
|
+
* is never the whole they are parts of. */
|
|
556
|
+
function isPatternPage(page, patternGroups) {
|
|
557
|
+
const group = page.group?.trim().toLowerCase();
|
|
558
|
+
return group !== undefined && patternGroups.some((name) => name.trim().toLowerCase() === group);
|
|
559
|
+
}
|
|
560
|
+
/** #66 `standalone-reason-empty`: a `standalone` that is blank or whitespace records no reason, so it
|
|
561
|
+
* is not an opt-out. */
|
|
562
|
+
function checkStandaloneReason(pages) {
|
|
563
|
+
return pages
|
|
564
|
+
.filter((page) => page.standalone !== undefined && page.standalone.trim() === '')
|
|
565
|
+
.map((page) => issue({
|
|
566
|
+
id: 'standalone-reason-empty', severity: 'warning', ...pageRef(page),
|
|
567
|
+
message: '`standalone` is empty, so it does not opt this page out of the structure checks.',
|
|
568
|
+
fix: "Write the reason, e.g. standalone: 'Not a part of GitFileCard; it draws its own counts.', or remove the field.",
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
/** #66: every page that uses one of `names`, excluding `self`, deduplicated by host page (a page
|
|
572
|
+
* counts once however many of its entries name the component). A built-in entry anywhere on the
|
|
573
|
+
* host wins over a non-built-in one. */
|
|
574
|
+
function usesOf(pages, self, names) {
|
|
575
|
+
const byHost = new Map();
|
|
576
|
+
for (const host of pages) {
|
|
577
|
+
if (host === self)
|
|
578
|
+
continue;
|
|
579
|
+
const entries = [...(host.composedOf ?? []), ...(host.members ?? []).flatMap((member) => member.composedOf ?? [])];
|
|
580
|
+
for (const entry of entries) {
|
|
581
|
+
if (!names.has(entry.component))
|
|
582
|
+
continue;
|
|
583
|
+
const known = byHost.get(host);
|
|
584
|
+
if (!known || (known.relationship !== 'built-in' && entry.relationship === 'built-in'))
|
|
585
|
+
byHost.set(host, { host, relationship: entry.relationship });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return [...byHost.values()];
|
|
589
|
+
}
|
|
590
|
+
/** #66: the names a page answers to — its id and its component. */
|
|
591
|
+
function pageNames(page) {
|
|
592
|
+
return new Set([resolvePageId(page, page.file), ...(page.component ? [page.component] : [])]);
|
|
593
|
+
}
|
|
594
|
+
/** #66: whether some other page has composition this reader couldn't read — a page-level or
|
|
595
|
+
* member-level `composedOf`, or a `members` list that wasn't read at all — so "only" and "nothing
|
|
596
|
+
* uses it" can't be claimed outright. */
|
|
597
|
+
function hasUnreadableComposition(pages, self) {
|
|
598
|
+
return pages.some((other) => other !== self && ((other.hasComposedOf === true && other.composedOf === undefined)
|
|
599
|
+
|| (other.hasMembers === true && other.members === undefined)
|
|
600
|
+
|| Boolean(other.members?.some((member) => member.hasComposedOf === true && member.composedOf === undefined))));
|
|
601
|
+
}
|
|
602
|
+
const pageList = (pages) => [...new Set(pages.map((p) => `\`${resolvePageId(p, p.file)}\``))].join(', ');
|
|
603
|
+
/** #66 `part-own-page`: a candidate page whose component is declared `{ relationship: 'built-in' }`
|
|
604
|
+
* (at page or member level) by exactly one other non-pattern page, which doesn't already list it
|
|
605
|
+
* as a member (by member id, component or alias). Two or more users can't give it one owner, so it
|
|
606
|
+
* is exempt; `slot`/`related`/missing/non-literal relationships never count. Reports; never infers
|
|
607
|
+
* an owner or a relationship. */
|
|
608
|
+
function checkPartOwnPage(pages, patternGroups) {
|
|
609
|
+
const issues = [];
|
|
610
|
+
for (const page of pages) {
|
|
611
|
+
if (!isStructureCandidate(page))
|
|
612
|
+
continue;
|
|
613
|
+
const names = pageNames(page);
|
|
614
|
+
const owners = usesOf(pages, page, names)
|
|
615
|
+
.filter((use) => use.relationship === 'built-in' && !isPatternPage(use.host, patternGroups))
|
|
616
|
+
.map((use) => use.host);
|
|
617
|
+
if (owners.length !== 1)
|
|
618
|
+
continue;
|
|
619
|
+
const owner = owners[0];
|
|
620
|
+
if (owner.members?.some((member) => names.has(member.id) || (member.component !== undefined && names.has(member.component))
|
|
621
|
+
|| member.aliases?.some((alias) => names.has(alias))))
|
|
622
|
+
continue;
|
|
623
|
+
const ownerId = resolvePageId(owner, owner.file);
|
|
624
|
+
const id = resolvePageId(page, page.file);
|
|
625
|
+
const caveat = hasUnreadableComposition(pages, page) ? ' (Some pages\' `composedOf` could not be read, so another user may exist.)' : '';
|
|
626
|
+
issues.push(issue({
|
|
627
|
+
id: 'part-own-page', severity: 'warning', ...pageRef(page),
|
|
628
|
+
message: `\`${id}\` is a built-in part of \`${ownerId}\` only, but has its own page.${caveat}`,
|
|
629
|
+
fix: `List it as a part in ${ownerId}'s family (\`members\`/\`sections\`), or set \`standalone: '<reason>'\` if it is not really part of ${ownerId}.`,
|
|
630
|
+
}));
|
|
631
|
+
}
|
|
632
|
+
return issues;
|
|
633
|
+
}
|
|
634
|
+
/** #66 `nothing-to-show`: a candidate page with no variants, states, comparison or `render`. A page
|
|
635
|
+
* this reader could not fully read is never a candidate (`checkable`), so "nothing" is only claimed
|
|
636
|
+
* about a page that was read in full. `previewWidths` alone is not content: it only sizes `render`. */
|
|
637
|
+
function checkNothingToShow(pages, patternGroups) {
|
|
638
|
+
const issues = [];
|
|
639
|
+
for (const page of pages) {
|
|
640
|
+
if (!isStructureCandidate(page))
|
|
641
|
+
continue;
|
|
642
|
+
const hasSpecimens = Boolean(page.comparison) || Boolean(page.variantsItems?.length) || Boolean(page.statesItems?.length) || page.hasRender === true;
|
|
643
|
+
if (hasSpecimens)
|
|
644
|
+
continue;
|
|
645
|
+
const id = resolvePageId(page, page.file);
|
|
646
|
+
const hosts = usesOf(pages, page, pageNames(page)).map((use) => use.host);
|
|
647
|
+
const users = hosts.filter((host) => !isPatternPage(host, patternGroups));
|
|
648
|
+
const patternUsers = hosts.filter((host) => isPatternPage(host, patternGroups));
|
|
649
|
+
const unreadable = hasUnreadableComposition(pages, page);
|
|
650
|
+
const base = { id: 'nothing-to-show', severity: 'warning', ...pageRef(page) };
|
|
651
|
+
const otherFix = "Add a specimen, set `navigation: 'reference'`, or `standalone: '<reason>'`.";
|
|
652
|
+
if (users.length > 0) {
|
|
653
|
+
const alsoPatterns = patternUsers.length > 0 ? ` Pattern pages also use it (${pageList(patternUsers)}).` : '';
|
|
654
|
+
issues.push(issue({ ...base,
|
|
655
|
+
message: `\`${id}\` shows no specimens. It is used by ${pageList(users)}.${alsoPatterns}`,
|
|
656
|
+
fix: "List it on that page as a documentation-only part, or set `navigation: 'reference'` to keep it reachable through Source reference search without a browse entry, or `standalone: '<reason>'`.",
|
|
657
|
+
}));
|
|
658
|
+
}
|
|
659
|
+
else if (patternUsers.length > 0) {
|
|
660
|
+
issues.push(issue({ ...base, message: `\`${id}\` shows no specimens. Only pattern pages use it (${pageList(patternUsers)}).`, fix: otherFix }));
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
issues.push(issue({ ...base,
|
|
664
|
+
message: unreadable
|
|
665
|
+
? `\`${id}\` shows no specimens, and no page whose \`composedOf\` doctor could read uses it.`
|
|
666
|
+
: `\`${id}\` shows no specimens and no other page uses it.`,
|
|
667
|
+
fix: otherFix,
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return issues;
|
|
672
|
+
}
|
|
533
673
|
/** The viewer exports that frame an example as a phone (or another bounded document), so an
|
|
534
674
|
* overlay that portals to the document body opens inside it instead of over the catalog. */
|
|
535
675
|
const DEVICE_FRAME_EXPORTS = ['PhoneFrame', 'OverlayDemo', 'PhoneScreen', 'BoundedOverlayViewport'];
|
|
@@ -997,7 +1137,8 @@ function checkTokenDefinitionAliases(pages) {
|
|
|
997
1137
|
return issues;
|
|
998
1138
|
}
|
|
999
1139
|
/** Every page-scoped issue, from every rule above. */
|
|
1000
|
-
export function collectIssues(pages, components) {
|
|
1140
|
+
export function collectIssues(pages, components, options = {}) {
|
|
1141
|
+
const patternGroups = options.patternGroups ?? DEFAULT_PATTERN_GROUPS;
|
|
1001
1142
|
return [
|
|
1002
1143
|
...checkParseErrors(pages),
|
|
1003
1144
|
...checkNotCheckable(pages),
|
|
@@ -1009,6 +1150,9 @@ export function collectIssues(pages, components) {
|
|
|
1009
1150
|
...checkDegenerateGridAxis(pages),
|
|
1010
1151
|
...checkComponentPages(pages, components),
|
|
1011
1152
|
...checkMissingComposition(pages, components),
|
|
1153
|
+
...checkStandaloneReason(pages),
|
|
1154
|
+
...checkPartOwnPage(pages, patternGroups),
|
|
1155
|
+
...checkNothingToShow(pages, patternGroups),
|
|
1012
1156
|
...checkMissingWorkingPreview(pages, components),
|
|
1013
1157
|
...checkPreviewOnlyPages(pages),
|
|
1014
1158
|
...checkFamilyMembers(pages, components),
|
|
@@ -1155,7 +1299,7 @@ export function runDoctor(config) {
|
|
|
1155
1299
|
? { '*': [path.join(config.projectRoot, 'node_modules', '@types', '*'), path.join(config.projectRoot, 'node_modules', '*')] }
|
|
1156
1300
|
: undefined;
|
|
1157
1301
|
let issues = [
|
|
1158
|
-
...collectIssues(pages, components),
|
|
1302
|
+
...collectIssues(pages, components, { patternGroups: config.patternGroups }),
|
|
1159
1303
|
...checkPageTypeErrors(pages, { fallbackPaths, hostRoot: config.projectRoot }),
|
|
1160
1304
|
...checkOverlaysFramed(pages, components, config.projectRoot),
|
|
1161
1305
|
...collectGlobalIssues({ syncWarnings: syncResult.warnings, tokenFileCount }),
|
package/dist/cli/staticPage.js
CHANGED
|
@@ -255,6 +255,9 @@ function readStaticPage(file) {
|
|
|
255
255
|
page.checkable = false;
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
+
const standaloneProp = findProp(pageObject, 'standalone');
|
|
259
|
+
if (standaloneProp)
|
|
260
|
+
page.standalone = stringLiteral(resolve(standaloneProp.initializer, consts, new Set()));
|
|
258
261
|
const tokenDefinitionProp = findProp(pageObject, 'tokenDefinition');
|
|
259
262
|
page.hasTokenDefinition = tokenDefinitionProp !== undefined;
|
|
260
263
|
if (tokenDefinitionProp) {
|
|
@@ -344,6 +347,7 @@ function readFamilyMembers(node, consts, gridNames, sourceFile) {
|
|
|
344
347
|
parent,
|
|
345
348
|
aliases,
|
|
346
349
|
composedOf,
|
|
350
|
+
hasComposedOf: composedOfProp !== undefined,
|
|
347
351
|
hasWhenToUse: findProp(resolvedElement, 'whenToUse') !== undefined,
|
|
348
352
|
hasA11y: findProp(resolvedElement, 'a11y') !== undefined,
|
|
349
353
|
hasExamples: Boolean(variants.items?.length) || Boolean(states.items?.length) || Boolean(comparison),
|
|
@@ -383,9 +387,9 @@ function readSections(node, consts) {
|
|
|
383
387
|
}
|
|
384
388
|
return sections;
|
|
385
389
|
}
|
|
386
|
-
/** A literal array of `ComposedOfEntry`-shaped object literals, read down to
|
|
387
|
-
* (
|
|
388
|
-
*
|
|
390
|
+
/** A literal array of `ComposedOfEntry`-shaped object literals, read down to `component` and a
|
|
391
|
+
* literal `relationship` (a non-literal one is left off the entry; `role` is authored prose
|
|
392
|
+
* `doctor` never needs). Undefined — not an empty array — for anything that isn't itself a
|
|
389
393
|
* literal array, or whose element isn't an object literal with a literal `component`: `doctor`
|
|
390
394
|
* must never mistake "couldn't read this" for "author declared no composition here". */
|
|
391
395
|
function readComposedOf(node, consts) {
|
|
@@ -400,7 +404,8 @@ function readComposedOf(node, consts) {
|
|
|
400
404
|
const component = propertyStringLiteral(resolvedElement, 'component', consts);
|
|
401
405
|
if (component === undefined)
|
|
402
406
|
return undefined;
|
|
403
|
-
|
|
407
|
+
const relationship = propertyStringLiteral(resolvedElement, 'relationship', consts);
|
|
408
|
+
entries.push(relationship === undefined ? { component } : { component, relationship });
|
|
404
409
|
}
|
|
405
410
|
return entries;
|
|
406
411
|
}
|
|
@@ -24,6 +24,9 @@ export interface CatalogPageInput<TId extends string = string> {
|
|
|
24
24
|
/** Page-level notes, shown as a "Notes" entry in the page's Quick reference (the page-level
|
|
25
25
|
* counterpart of a family member's `notes`). */
|
|
26
26
|
notes?: string[];
|
|
27
|
+
/** Why this page stands on its own. Opts it out of `doctor`'s `part-own-page` and
|
|
28
|
+
* `nothing-to-show` checks and records the decision in source; the viewer does not render it. */
|
|
29
|
+
standalone?: string;
|
|
27
30
|
variants?: VariantSlot;
|
|
28
31
|
states?: VariantSlot;
|
|
29
32
|
render?: () => React.ReactNode;
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import { DS_SEMANTIC } from '../../tokens';
|
|
|
6
6
|
export default defineCatalogPage({
|
|
7
7
|
component: 'AnimatedChevron',
|
|
8
8
|
group: 'Components',
|
|
9
|
+
standalone: 'A reusable kit component in its own right; Banner is just the only kit component that uses it.',
|
|
9
10
|
whenToUse: 'An expand/collapse affordance that should animate its own state change — it flips vertically in place, so it never points sideways mid-transition. Where a chevron never changes state, a plain icon is enough.',
|
|
10
11
|
description: 'A chevron that morphs between down (collapsed) and up (expanded) — flipping vertically in place instead of rotating through a sideways-pointing angle, and instead of swapping icons. The morph Banner\'s own collapsible header uses for its disclosure indicator. Not commonly used on its own; exported for building a custom disclosure/accordion toggle.',
|
|
11
12
|
a11y: 'Purely decorative — no accessibility role of its own. Wrap it in an accessible parent (the way Banner\'s own header Pressable does) if the toggle needs to be announced.',
|
|
@@ -32,6 +32,7 @@ const AVATAR_COMPARISON = grid(
|
|
|
32
32
|
export default defineCatalogPage({
|
|
33
33
|
component: 'Avatar',
|
|
34
34
|
group: 'Components',
|
|
35
|
+
standalone: 'A reusable kit component in its own right; EmptyState is just the only kit component that uses it.',
|
|
35
36
|
whenToUse: 'A person or place\'s identity in a circle. Pass whatever you have: image, icon, or initials are tried in that order, so a missing image degrades to initials without leaving a gap.',
|
|
36
37
|
specimenSize: 'compact',
|
|
37
38
|
comparison: AVATAR_COMPARISON,
|
|
@@ -65,6 +65,7 @@ const dock = (close: () => void) => (
|
|
|
65
65
|
export default defineCatalogPage({
|
|
66
66
|
component: 'BottomSheet',
|
|
67
67
|
group: 'Components',
|
|
68
|
+
standalone: 'A reusable kit component in its own right; Dropdown is just the only kit component that uses it.',
|
|
68
69
|
// Every example is a phone. At the default size two blocks share the row and each cell falls to
|
|
69
70
|
// 270px, which squeezes the frame until the sheet's own text is barely readable — 'wide' keeps a
|
|
70
71
|
// 402px minimum, the width the sheet is actually used at.
|
|
@@ -6,6 +6,7 @@ import { Button } from '../Button';
|
|
|
6
6
|
export default defineCatalogPage({
|
|
7
7
|
component: 'ButtonGroup',
|
|
8
8
|
group: 'Components',
|
|
9
|
+
standalone: 'A reusable kit component in its own right; EmptyState is just the only kit component that uses it.',
|
|
9
10
|
whenToUse: 'Two or three Buttons that should read as one family — horizontal for a trailing-aligned pair (a dialog\'s Cancel and Confirm), vertical for up to three stretched full width. Keep one variant tier and one size across the group. For a screen\'s bottom action area, use Dock.',
|
|
10
11
|
composedOf: [
|
|
11
12
|
{ component: 'Button', role: "Its children: up to two Buttons side by side, or three stacked.", relationship: 'slot' },
|
|
@@ -12,6 +12,7 @@ const styles = StyleSheet.create({
|
|
|
12
12
|
export default defineCatalogPage({
|
|
13
13
|
component: 'Divider',
|
|
14
14
|
group: 'Components',
|
|
15
|
+
standalone: 'A reusable kit component in its own right; List is just the only kit component that uses it.',
|
|
15
16
|
whenToUse: 'A single hairline between two pieces of content you are laying out yourself. List already puts one between each pair of its rows, so adding your own there would double them.',
|
|
16
17
|
specimenSize: 'regular',
|
|
17
18
|
description: 'A 1px hairline separator at the divider token colour.',
|
|
@@ -41,6 +41,7 @@ const PILL_COMPARISON = grid(
|
|
|
41
41
|
export default defineCatalogPage({
|
|
42
42
|
component: 'Pill',
|
|
43
43
|
group: 'Components',
|
|
44
|
+
standalone: 'A reusable kit component in its own right; PillRow is just the only kit component that uses it.',
|
|
44
45
|
composedOf: [
|
|
45
46
|
{ component: 'Loading', role: "The spinner shown instead of the label and icon while loading is true.", relationship: 'built-in' },
|
|
46
47
|
],
|
|
@@ -12,6 +12,7 @@ const styles = StyleSheet.create({
|
|
|
12
12
|
export default defineCatalogPage({
|
|
13
13
|
component: 'TopNav',
|
|
14
14
|
group: 'Components',
|
|
15
|
+
standalone: 'A reusable kit component in its own right; Dropdown is just the only kit component that uses it.',
|
|
15
16
|
whenToUse: 'A screen\'s top bar, and BottomSheet\'s header slot. Its leading and trailing slots reserve their width even when empty, so the title stays centred whether one, both, or neither side has content. For actions at the bottom of a screen, use Dock.',
|
|
16
17
|
composedOf: [
|
|
17
18
|
{ component: 'Button', role: "The leading and trailing slots, typically small icon Buttons (back, close, an action).", relationship: 'slot' },
|