@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 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. */
@@ -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 },
@@ -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 }),
@@ -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 just `component`
387
- * (design §4: role/relationship are authored prose `doctor` never needs for its missing-
388
- * composition comparison). Undefined — not an empty array — for anything that isn't itself a
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
- entries.push({ component });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krapwoo/ds-viewer",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A browsable, always-current catalog for an Expo / React Native design system.",
@@ -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' },