@krapwoo/ds-viewer 0.14.2 → 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.
Files changed (37) hide show
  1. package/README.md +249 -3
  2. package/config/index.ts +3 -0
  3. package/dist/cli/config.js +9 -1
  4. package/dist/cli/doctor.js +452 -19
  5. package/dist/cli/explain.js +26 -0
  6. package/dist/cli/staticPage.js +99 -10
  7. package/dist/cli/tokenDefinition.js +125 -0
  8. package/dist/native/catalog/catalogNavigation.js +27 -11
  9. package/dist/native/catalog/pageApi.js +557 -26
  10. package/dist/native/catalog/tokenDefinition.js +205 -0
  11. package/native/catalog/CatalogSearchInput.tsx +18 -3
  12. package/native/catalog/CatalogShell.tsx +318 -46
  13. package/native/catalog/CatalogSidebar.tsx +37 -1
  14. package/native/catalog/FamilyPage.tsx +567 -137
  15. package/native/catalog/GroupPage.tsx +230 -0
  16. package/native/catalog/JumpRow.tsx +81 -0
  17. package/native/catalog/ReferenceDetails.tsx +144 -16
  18. package/native/catalog/SectionBlock.tsx +49 -5
  19. package/native/catalog/SourceReferencePicker.tsx +180 -0
  20. package/native/catalog/StructuredTokenPage.tsx +592 -0
  21. package/native/catalog/Swatch.tsx +29 -2
  22. package/native/catalog/TokenLayouts.tsx +30 -9
  23. package/native/catalog/catalogNavigation.ts +30 -10
  24. package/native/catalog/catalogReferences.ts +212 -0
  25. package/native/catalog/familyModel.ts +10 -4
  26. package/native/catalog/index.ts +22 -0
  27. package/native/catalog/pageApi.ts +598 -23
  28. package/native/catalog/tokenDefinition.ts +430 -0
  29. package/native/catalog/types.ts +77 -5
  30. package/package.json +1 -1
  31. package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +1 -0
  32. package/starter-kit/components/Avatar/Avatar.catalog.tsx +1 -0
  33. package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +1 -0
  34. package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +1 -0
  35. package/starter-kit/components/Divider/Divider.catalog.tsx +1 -0
  36. package/starter-kit/components/Pill/Pill.catalog.tsx +1 -0
  37. 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`).
@@ -230,7 +230,8 @@ Each catalog shows **one page at a time**: a persistent, searchable sidebar and
230
230
  On web the page is kept in the URL fragment (`#Button`), so refresh, deep links, and back/forward
231
231
  work. Desktop and laptop screens only. `SectionBlock` renders one page: breadcrumb, title,
232
232
  description, previous/next, then the specimens, then always-visible
233
- **Guidance** and **Quick reference** (source path, accessibility), then **Props** in their own box
233
+ **Guidance** and **Quick reference** (source path, accessibility, and — when the page has any —
234
+ **Composition** and the derived **"also used by"** inverse of it), then **Props** in their own box
234
235
  (two columns, filled across first, when there are 4+ props and room). Token pages show only
235
236
  Quick reference with the source path. Guidance, Quick reference, and — only for a page with
236
237
  confirmed `composedOf` entries — Composition lay out from the reference card's own measured inner
@@ -238,7 +239,12 @@ width, not viewport width alone: three columns side by side only when there's co
238
239
  *and* the card is wide enough for all three (Composition gets its own column only then; otherwise
239
240
  it stays nested under Quick reference, as it always has), two when there's room for Guidance and
240
241
  Quick reference side by side, or one stacked column — in full document order, nothing dropped —
241
- when the card is too narrow even for two. The specimens use one of four layouts:
242
+ when the card is too narrow even for two. A Composition name becomes a real link whenever it
243
+ resolves to a real catalog address (a page, a family member, or either one's alias — the same
244
+ resolution a family member's own "Uses" line already uses); an unresolvable name stays honest plain
245
+ text. "Also used by" is derived, never authored, the ordinary-page counterpart of a family member's
246
+ own line of the same name (see "Family pages" below): nothing renders when nothing else uses the
247
+ page. The specimens use one of four layouts:
242
248
  - **Grid** — two props that combine freely, from an explicit `comparison` (e.g. Button's
243
249
  Variant × State).
244
250
  - **Grouped rows** — one row per variant holding that variant's own configurations (e.g.
@@ -260,6 +266,44 @@ card narrower than its minimum scrolls inside itself rather than squeezing its c
260
266
  (`itemsFill`) slots default to `wide`. The catalog's own look comes from `native/catalog/tokens.ts`,
261
267
  never from the host app's tokens.
262
268
 
269
+ ### Source reference
270
+
271
+ A page's `navigation` field is `'browse'` (the default — omit it) or `'reference'`:
272
+
273
+ ```tsx
274
+ export default defineCatalogPage({
275
+ id: 'ButtonSource', description: 'The native declaration behind Button.',
276
+ navigation: 'reference',
277
+ // ...
278
+ });
279
+ ```
280
+
281
+ A `'reference'` page keeps every one of its addresses — its own id, its aliases, and (on a family
282
+ page) every member's own id/alias — fully routable; `navigation` only ever removes it from the
283
+ sidebar, the pager, and the hash fallback's own browse sequence. A family page's members inherit
284
+ their own page's `navigation`; there's no separate per-member field. The moment any page in the
285
+ catalog opts in, the sidebar grows a quiet **Source reference** entry: a catalog-wide search over
286
+ every registered page's and member's own id, title, detected component export, alias, and source
287
+ path — never a page-local member search, and never a second, hand-maintained registry (it's
288
+ derived from the same `SectionDef[]`/`FamilyDef` every other part of the viewer already reads). A
289
+ legacy catalog with no `'reference'` page keeps its present sidebar and page set, unchanged.
290
+ Selecting a result opens its real page (and, for a member, focuses it directly) and offers a real
291
+ `Back to <origin>` return link to wherever that selection actually started from — any page, not
292
+ only another reference destination. Every other already-true reference fact (a Composition name,
293
+ a derived "also used by" user, a family member's own `related`/anatomy name) becomes the same kind
294
+ of real, canonical, focused-return link once it resolves. Only loading a reference page's own URL
295
+ directly, with no recorded origin, offers `Browse catalog` to the first ordinary browsing page
296
+ instead of inventing one that was never visited; a catalog with no browsable page at all offers
297
+ neither.
298
+
299
+ Resolution for the Source reference picker, and for every Composition/"also used by"/`related`/
300
+ anatomy name across the whole catalog (see "Browsing the catalog" above and "Family pages" below),
301
+ shares one rule: an exact registered page id, member id, or alias always wins; only when none of
302
+ those claim the name does a unique, detected component export resolve it instead — letting a name
303
+ find an ordinary page whose own id differs from its component (e.g. a `PillButton` page whose
304
+ component export is `Button`). A component name shared by more than one page or member is
305
+ ambiguous and is never guessed; an unknown name stays plain, honest text rather than a dead link.
306
+
263
307
  ## Adding a component (the recipe)
264
308
 
265
309
  Copy the shape of an existing pair, e.g. `starter-kit/components/Button/` or `starter-kit/components/Badge/`:
@@ -273,6 +317,8 @@ a11y, and its content — `id`/`path`/`props` are derived, never authored), and
273
317
  `src/ds/components/index.ts` the same way its siblings are. `SectionBlock` shows the
274
318
  specimens first, then Guidance, Quick reference, and Props. Give it whichever of these fields
275
319
  actually apply:
320
+ - `notes: string[]` — page-level notes, listed as a "Notes" entry in the page's Quick reference
321
+ (for facts about the source that aren't part of the description, guidance or accessibility).
276
322
  - `comparison: { rowLabel, columnLabel, rows, columns, cells, size? }` — when two props genuinely
277
323
  combine. Author every row × column cell with a real instance, or an `unavailableReason` when that
278
324
  combination does not exist. Never multiply `variants` by `states` to fill it. Keep the column axis
@@ -449,6 +495,13 @@ path. For anything beyond a single list of tokens, use the token-page layouts be
449
495
  title, not the id — so "Control heights" sorts under "C", not under whatever the id starts with.
450
496
  - **Point token pages at their token file.** A token page has no component, so its Source would be
451
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.
452
505
  - **Coverage only counts the component's own options.** `doctor`'s `option-not-covered` checks
453
506
  string-literal unions declared in the component's own folder or a token folder. A union
454
507
  declared in another component's folder, such as a shared `IconName`, is that component's
@@ -486,6 +539,9 @@ export default defineCatalogPage({
486
539
  });
487
540
  ```
488
541
 
542
+ Every family page shows a one-line "Family page." explainer under its description, so a reader
543
+ knows its parts are separate components, unlike variants.
544
+
489
545
  - **`whole`** is a member id, and must equal the page's own `id`. It renders first, with no header
490
546
  of its own (the page title already names it) and no number.
491
547
  - **`sections`** (`{ label, kind: 'blocks' | 'list', inWhole, members }`) group the rest: `blocks`
@@ -557,6 +613,10 @@ export default defineCatalogPage({
557
613
  for behavior a web catalog can't demonstrate (a timer, a native gesture). A member's own
558
614
  `notShown` works the same way, under its specimens. Like `aliases`, the page-level field only
559
615
  takes effect on a page with `members`.
616
+ - **Notes and API.** A member's notes, "Uses …" line, guidance, accessibility, props and
617
+ reference excerpt open in one shared side panel (a modal dialog on the right, full width at
618
+ 768px and narrower) from a "Notes and API" link under the member — Esc, the × button or a click outside closes
619
+ it and returns focus to the link; a member with nothing to show has no link.
560
620
  - **`apiLabel`** (default `'API'`) renames the disclosure's reference-excerpt part for a host app
561
621
  that generates one (e.g. `'Swift API'`) — never hardcode a specific language's wording. Also
562
622
  only takes effect on a page with `members`.
@@ -564,6 +624,74 @@ export default defineCatalogPage({
564
624
  and at least one specimen, the same as a normal page's component — `doctor` checks each one.
565
625
  See `viewer-host/viewer-pages/FamilyPage.catalog.tsx` in this repo for a complete, worked example.
566
626
 
627
+ ### Group pages
628
+
629
+ A page can also document several unrelated-but-adjacent components on one page — e.g. two SwiftUI
630
+ button styles used together on one screen, or the handful of request-card bodies a switch statement
631
+ picks between — with no single "whole" that owns the others. Give it `members` with **no `whole`
632
+ and no `sections`** (both omitted, not just empty):
633
+
634
+ ```tsx
635
+ export default defineCatalogPage({
636
+ id: 'OnboardingButtons', group: 'Components', description: 'Two separate button styles used together on onboarding screens.',
637
+ members: [
638
+ { id: 'OnboardingPrimaryButtonStyle', source: 'Onboarding.swift:172', fidelity: 'replicated', relationship: 'Style', description: '...', whenToUse: '...', a11y: '...', variants: { items: [/* ... */] } },
639
+ { id: 'OnboardingSecondaryButtonStyle', source: 'Onboarding.swift:298', fidelity: 'replicated', relationship: 'Style', description: '...', whenToUse: '...', a11y: '...', variants: { items: [/* ... */] } },
640
+ ],
641
+ });
642
+ ```
643
+
644
+ Every group page shows a one-line "Group page." explainer under its description, so a reader knows
645
+ the members are separate components with their own APIs, unlike variants.
646
+
647
+ **Group vs. family:** reach for a family when the members genuinely compose one thing (a view and
648
+ its own subviews) — the page then opens with that thing's own specimens, before its parts. Reach
649
+ for a group when the members are independently meaningful and would otherwise each get their own
650
+ tiny page with nothing to compare them against; a group page shows every member's own section with
651
+ no shared "whole" and no Compare block (layout B) — just the page head, a jump row, then each
652
+ top-level member's own section in authored order.
653
+
654
+ - **Shape.** `sections` and page-level `anatomy` are never used on a group page — `doctor`'s
655
+ `family-invalid` rule reports either one if present. Every member keeps its full `CatalogMember`
656
+ shape (the same fields a family member has).
657
+ - **Top-level vs. nested.** A member whose `parent` names another member *on the same page* is one
658
+ of that member's own parts, nested underneath it; a member with no `parent`, or whose `parent`
659
+ names a member defined on a *different* page (see "External parts" below), is **top-level**. The
660
+ jump row lists exactly the top-level members, in authored order, each as a chip linking to its
661
+ own address; its muted qualifier is "Part N of `<Family>`" when the member is an external part,
662
+ "+k parts" when it has its own nested parts on this page, or nothing.
663
+ - **Embedded families.** A top-level member with parts of its own on the page may carry its own
664
+ `anatomy` (`CatalogMember.anatomy` — the same `{ picture, groups }` shape a family's page-level
665
+ `anatomy` has, but scoped to that one member's own descendants). Its parts number independently of
666
+ every other top-level member's: `1`, `2`, … among siblings, then `1.1`, `1.2`, … for children —
667
+ the same rule a family uses under its own whole, just re-anchored per top-level member instead of
668
+ once per page. `doctor`'s `group-anatomy-member` rule reports an embedded anatomy item naming
669
+ anything other than that member's own descendant.
670
+ - **External parts.** A family's own `inWhole` section may list a member id that is defined on a
671
+ group page instead of its own `members`, as long as that member's own `parent` equals the
672
+ family's whole — an *external part*. The family's outline numbers it exactly like an own part (the
673
+ same number shows on both pages); on the family page its row shows that number, its name, and a
674
+ "Details on `<group title>`" link to the member's own address, with no specimens or duplicated
675
+ description. On the group page, that member's own section heading shows "Part N of `<Family>`",
676
+ linking back to the family page. An id that no page defines at all is reported by
677
+ `family-invalid` ("not a member of this page"); an id that is defined but doesn't resolve to
678
+ exactly one group-page member with a matching parent (defined on several pages, on a page that
679
+ isn't a group page, or with a different `parent`) is reported by `doctor`'s
680
+ `family-external-part` rule. Listing the same external id in two sections, or twice in one, is a
681
+ `family-invalid` "in more than one section" error.
682
+ - **Routing, search, aliases.** A group page's own id and `aliases` resolve to the page, exactly
683
+ like a family's; every member id and alias on a group page resolves to that page with that member
684
+ focused, exactly like a family member's does (a member id or alias equal to its own page's id
685
+ could never be addressed, so `family-invalid` reports it). The sidebar, pager and search list the group page
686
+ once (its own title) — members are reachable by address and search, never as separate sidebar
687
+ rows, the same convention a family's own members already follow.
688
+ - No whole, no Compare block, no `relatedPages`/`composedOf`-at-the-page-level concept of its own
689
+ (a non-empty `relatedPages` on a group page is reported by `family-invalid` as unused) —
690
+ everything else (fidelity badges, `whenToUse`/`a11y`, `composedOf`'s own "Uses …" line, derived
691
+ "Also used by", `notShown`, `apiLabel`) works exactly the same as it does for a family member, see
692
+ above. See `viewer-host/viewer-pages/GroupPage.catalog.tsx` in this repo for a worked example,
693
+ including an embedded family and an external part of a small fixture family page.
694
+
567
695
  ### Token pages
568
696
 
569
697
  A token page shows each token at its real value with its role. Pick the layout by the token's
@@ -614,6 +742,107 @@ shape:
614
742
  <MotionSpecimen kind="spring" spring={UNIT_SPRING_CONFIG} valueRange="unit" />
615
743
  ```
616
744
 
745
+ ### `tokenDefinition`
746
+
747
+ An opt-in, structured alternative to `render`/`tokenGallery`/`tokenSections` for a token page's
748
+ *real entries* — name, value, provenance, source — instead of a hand-built preview. Omitting it
749
+ leaves every existing page exactly as it was; nothing here changes unless a page opts in.
750
+
751
+ ```tsx
752
+ import { defineCatalogPage, type TokenDefinition } from '@krapwoo/ds-viewer';
753
+
754
+ const COLORS: TokenDefinition = {
755
+ kind: 'colors',
756
+ sections: [
757
+ {
758
+ id: 'semantic', title: 'Semantic',
759
+ entries: [
760
+ { id: 'brand', name: 'Brand', kind: 'colors', value: { provenance: 'named', resolution: { status: 'resolved', value: { color: DS_SEMANTIC.brand } } } },
761
+ ],
762
+ },
763
+ ],
764
+ };
765
+
766
+ export default defineCatalogPage({ group: 'Tokens', description: 'Semantic and palette colors.', tokenDefinition: COLORS });
767
+ ```
768
+
769
+ - **Eight kinds**, each with one fixed canonical title: `colors` → Colors, `typography` →
770
+ Typography, `spacing` → Spacing, `radius` → Radius, `dimensions` → Dimensions, `shadows` →
771
+ Shadows, `opacity` → Opacity, `motion` → Motion. Omit the page's own `title` to get it for free;
772
+ an explicit `title` must match (`doctor`'s `token-definition-title-conflict`, below).
773
+ - **The runtime import is the value authority, not `tokens.json`.** An entry's `value` is whatever
774
+ real literal/import you give it at the call site — this is not a reader of your build's
775
+ `tokens.json`/token-scan output. That's also why a static check (`doctor`) can only ever read an
776
+ entry's literal metadata, never its real value — see "Static honesty" below.
777
+ - **Provenance, resolution and `named` are three independent things**, and every entry shows its
778
+ own provenance as a plain muted caption (never a badge/pill): `'named'` ("Named token") is a real
779
+ exported design-system token; `'inline'` ("Inline value") is a one-off literal with no shared
780
+ name; `'platform'` ("Platform-provided") means the real value comes from the OS at runtime, not a
781
+ literal this catalog can read; `'unresolved-finding'` ("Unresolved finding") is a real,
782
+ source-backed literal/expression found in code with no shipped design token behind it yet.
783
+ `resolution` is separate: `{ status: 'resolved', value }` when there's a real sample to draw, or
784
+ `{ status: 'unresolved', reason, expression? }` with the real reason (and, when one exists, the
785
+ real source expression) a human would read at the call site — never a fabricated sample or an
786
+ invented classification. `named: true` says a `'platform'`/`'unresolved-finding'` value is *also*
787
+ exposed through a real named token (shown as "Platform-provided · named token"); omit it for
788
+ `'named'`/`'inline'` provenance, which already say so by themselves. When every entry in a
789
+ section shares the same provenance/named pair, it's shown once for the whole section instead of
790
+ repeating on every entry.
791
+ - **Appearances and scoped ids.** A color entry's `appearance: 'light' | 'dark'` renders an
792
+ independent sample — not a global color-scheme context; omit it for a color with one appearance.
793
+ An entry's `id` only needs to be unique *within its own section* — the same id may recur in a
794
+ different section of the same page (e.g. two scoped "Palette" cards), which is the same
795
+ `TokenSection.id` precedent token galleries already use.
796
+ - **Groups inside a section.** A section may add `groups: [{ id, title, desc?, entries }]` — each
797
+ renders under its own sub-heading, after the section's own entries, in the same grid the section
798
+ itself uses. A group id is unique within its own section; an entry id is unique within its own
799
+ group, a separate scope from the section's own entries and every other group. The section-level
800
+ provenance consolidation (above) considers the section's own entries *and* every one of its
801
+ groups together, so a shared provenance/named pair is still shown only once for the whole section.
802
+ - **The category jump row.** A page with more than one section opens with one chip per section —
803
+ its title plus how many entries it holds (its own, plus every group's) — so "Attention" might
804
+ read "Attention, 3 values". Each chip scrolls that section into view; a single-section page shows
805
+ no jump row, since there's nothing to jump between.
806
+ - **Light/dark pairing.** Within one entries list (a section's own, or one group's), two `colors`
807
+ entries sharing the same `name` — one `appearance: 'light'`, the other `'dark'` — render as a
808
+ single swatch: a split chip (light half left, dark half right), the bare shared name once, and
809
+ `<light value> · <dark value>` as the value line. An entry with no matching opposite appearance
810
+ (or a third same-named entry once both appearances are already paired) renders exactly as any
811
+ other unpaired entry.
812
+ - **Motion's `native-expression` kind is a real limit, not a gap:** a SwiftUI
813
+ `.spring(duration:bounce:)`/`.spring(response:dampingFraction:)` (or any other conditional/
814
+ symbolic value) is shown as its own real text, never converted into a guessed
815
+ stiffness/damping/mass spring — that conversion genuinely isn't supported.
816
+ - **Conflicts.** `tokenDefinition` replaces, and so conflicts with, an explicit `render`,
817
+ `tokenGallery`, `tokenSections`, or `members` (a family page) on the same page — remove one of
818
+ them. These, and an explicit `title` that disagrees with the kind's canonical title, are always
819
+ `doctor` errors (`token-definition-conflict`, `token-definition-title-conflict`), independent of
820
+ `doctor.strict`. A duplicate section id, a duplicate group id within a section, or a duplicate
821
+ entry id within one section or one group, is `token-definition-duplicate-id` (also always an
822
+ error) — the same id may still recur in a different section/group/the section's own entries.
823
+ - **One page per kind.** Two or more pages declaring the same literal `tokenDefinition.kind` each
824
+ get a `token-definition-duplicate-kind` error naming the other page(s) — a page with no
825
+ `tokenDefinition` is never involved. Give one of them its own kind, or merge them into one page.
826
+ - **Aliases.** A `tokenDefinition` page's own `aliases` resolve to the page, the same way a family
827
+ page's `aliases` already do. A section's own `aliases` resolve to the page too, with that section
828
+ scrolled into view. Every one of these joins the catalog's existing alias-collision check
829
+ (`family-invalid`) — an alias must still name exactly one thing, catalog-wide.
830
+ - **Static honesty.** `npx ds-viewer doctor` reads a `tokenDefinition`'s `kind` and each section's
831
+ `id`/`title`/`desc`/`wide`/entry `id`s — and, the same way, each group's own `id`/`title`/entry
832
+ `id`s — the same literal-only way it reads every other page field — it never imports or executes a
833
+ page file, so it can never see a real `value`. When a section's (or a group's) own `entries` isn't
834
+ a literal array (an imported/computed list), or contains an entry whose own `id` isn't a literal,
835
+ that list's ids are read on a best-effort basis and the summary lists it as a section with
836
+ runtime-only entries (not statically validated). That is the normal case when values are imported
837
+ from your own token modules, so it is informational: it never fails `--ci` and `doctor.strict`
838
+ does not promote it. Literal sections still get every check. A `tokenDefinition` whose own
839
+ `kind`/`sections` metadata isn't literal at all makes the whole page not statically checkable —
840
+ reported only as the existing `page-not-checkable` warning, never a separate
841
+ `tokenDefinition`-specific one.
842
+ - **Migration.** A legacy `render`/`tokenGallery`/`tokenSections` token page keeps working
843
+ unchanged — `tokenDefinition` is additive, never required. To convert one, remove `render`/
844
+ `tokenSections` and author the same data as `tokenDefinition`'s entries instead.
845
+
617
846
  ## Keeping the catalog current
618
847
 
619
848
  ### Binding examples to props
@@ -683,6 +912,23 @@ Button
683
912
  non-`inWhole` section, naming every one of them so you can act without opening the file. Give
684
913
  each its own catalog page and name it in the family's `relatedPages` list instead; the section
685
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.
686
932
 
687
933
  The catalog also warns at render time when a page under-declares the width its examples need.
688
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 },