@astryxdesign/core 0.5.2-canary.17f22f4 → 0.5.2-canary.1e29ea2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.5.2-canary.17f22f4",
3
+ "version": "0.5.2-canary.1e29ea2",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -661,7 +661,7 @@
661
661
  "react-dom": ">=19.0.0"
662
662
  },
663
663
  "devDependencies": {
664
- "@astryxdesign/cli": "0.5.2-canary.17f22f4",
664
+ "@astryxdesign/cli": "0.5.2-canary.1e29ea2",
665
665
  "@babel/cli": "^8.0.4",
666
666
  "@babel/core": "^7.29.7",
667
667
  "@babel/preset-react": "^8.0.1",
@@ -1,5 +1,74 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Dialog',
7
+ required: true,
8
+ description: 'Modal surface that contains the command palette.',
9
+ },
10
+ {
11
+ name: 'Input',
12
+ required: true,
13
+ description:
14
+ 'Search region containing the query field and its supporting visuals.',
15
+ },
16
+ {
17
+ name: 'Search glyph',
18
+ required: true,
19
+ description: 'Search symbol rendered by Icon in the default Input.',
20
+ },
21
+ {
22
+ name: 'Query field',
23
+ required: true,
24
+ description: 'Native text field used to enter a search query.',
25
+ },
26
+ {
27
+ name: 'Loading spinner',
28
+ required: false,
29
+ description:
30
+ 'Spinner shown in the default Input while a search is pending.',
31
+ },
32
+ {
33
+ name: 'List',
34
+ required: true,
35
+ description:
36
+ 'Scrollable listbox containing the current results or Empty state.',
37
+ },
38
+ {
39
+ name: 'Item',
40
+ required: false,
41
+ description: 'Selectable command result rendered inside the List.',
42
+ },
43
+ {
44
+ name: 'Group',
45
+ required: false,
46
+ description: 'Optional collection of Items that share a heading.',
47
+ },
48
+ {
49
+ name: 'Group heading',
50
+ required: false,
51
+ description: 'Visible heading rendered for a Group.',
52
+ },
53
+ {
54
+ name: 'Empty',
55
+ required: false,
56
+ description: 'Message shown when the current result set is empty.',
57
+ },
58
+ {
59
+ name: 'Footer',
60
+ required: false,
61
+ description:
62
+ 'Footer region for default keyboard guidance or caller-provided content.',
63
+ },
64
+ {
65
+ name: 'Keyboard shortcut',
66
+ required: false,
67
+ description:
68
+ 'Painted key badges rendered by Kbd in the default Footer guidance.',
69
+ },
70
+ ];
71
+
3
72
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
73
  export const docs = {
5
74
  name: 'CommandPalette',
@@ -152,6 +221,7 @@ export const docs = {
152
221
  ],
153
222
  },
154
223
  usage: {
224
+ anatomy,
155
225
  description: 'CommandPalette is a searchable dialog for quick access to commands, navigation, and actions. Use it as a keyboard-driven launcher powered by SearchSource for filtering and selection.',
156
226
  bestPractices: [
157
227
  { guidance: true, description: 'Provide a searchSource with bootstrap results so users see useful options before typing.' },
@@ -165,6 +235,7 @@ export const docs = {
165
235
  /** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
166
236
  export const docsZh = {
167
237
  usage: {
238
+ anatomy,
168
239
  description: 'CommandPalette is a searchable dialog for quick access to commands, navigation, and actions. Use it as a keyboard-driven launcher powered by SearchSource for filtering and selection.',
169
240
  bestPractices: [
170
241
  { guidance: true, description: 'Provide a searchSource with bootstrap results so users see useful options before typing.' },
@@ -180,6 +251,7 @@ export const docsDense = {
180
251
  description:
181
252
  'searchSource-driven command palette dialog; filtering, keyboard nav, grouping, selection; same SearchSource interface as Typeahead',
182
253
  usage: {
254
+ anatomy,
183
255
  description: 'CommandPalette is a searchable dialog for quick access to commands, navigation, and actions. Use it as a keyboard-driven launcher powered by SearchSource for filtering and selection.',
184
256
  bestPractices: [
185
257
  { guidance: true, description: 'Provide a searchSource with bootstrap results so users see useful options before typing.' },
@@ -0,0 +1,223 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 2
4
+ kind: component
5
+ id: component:CommandPalette
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers: [behavior, theming, accessibility]
13
+ verified_by:
14
+ [
15
+ packages/core/src/CommandPalette/CommandPalette.test.tsx,
16
+ packages/core/src/CommandPalette/CommandPaletteInput.test.tsx,
17
+ packages/core/src/CommandPalette/CommandPaletteList.test.tsx,
18
+ packages/core/src/CommandPalette/CommandPaletteItem.test.tsx,
19
+ packages/core/src/CommandPalette/CommandPaletteGroup.test.tsx,
20
+ packages/core/src/CommandPalette/CommandPaletteFooter.test.tsx,
21
+ packages/core/src/theme/themingTargets.test.ts,
22
+ scripts/check-knowledge.mjs,
23
+ ]
24
+ families: [family:overlay-dismissal]
25
+ design_specs: []
26
+ architecture:
27
+ [
28
+ architecture:component-theming-surface,
29
+ architecture:interaction-modality,
30
+ architecture:public-component-api,
31
+ ]
32
+ contributing: []
33
+ system_specs: []
34
+ ---
35
+
36
+ # CommandPalette component contract
37
+
38
+ ## Intent
39
+
40
+ CommandPalette presents searchable commands inside a Dialog. This draft records
41
+ its current aggregate consumer anatomy, target ownership, and delegated stable
42
+ parts without changing runtime behavior, styling, targets, or public API.
43
+
44
+ ## Compatibility and migration
45
+
46
+ - Released default preserved: `yes`
47
+ - Compatibility class: additive documentation only; runtime, DOM, styling,
48
+ targets, aliases, and public API remain unchanged
49
+ - Controlled/uncontrolled behavior: unchanged
50
+ - Migration decision: none
51
+
52
+ Consumer migration instructions belong in consumer docs and release notes.
53
+
54
+ ## Ownership boundary
55
+
56
+ **Owns**
57
+
58
+ - Search orchestration and the current Input, List, Item, Group, Group heading,
59
+ Empty, and Footer parts and targets.
60
+ - The native Query field rendered inside the default Input.
61
+
62
+ **Does not own / non-goals**
63
+
64
+ - Dialog's surface, Icon's glyph, Spinner's loading indicator, or Kbd's shortcut
65
+ badges — owned by their respective components.
66
+ - Caller-provided replacement Input, Footer, item content, or trailing input
67
+ content — owned by the product callsite.
68
+ - A `command-palette` root target — no such current target exists, and this
69
+ factual backfill does not invent one.
70
+ - Shared layer hosting, positioning, or lifecycle rules beyond the current
71
+ records linked below.
72
+
73
+ ## Public concepts
74
+
75
+ No new public concept is introduced. Consumer props, slots, and usage remain
76
+ documented in `CommandPalette.doc.mjs` and its subcomponent docs.
77
+
78
+ ## Behavioral and layout contract
79
+
80
+ | ID | Candidate invariant | Basis | Draft review state |
81
+ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------- |
82
+ | FR1 | The current default render places Input and Footer around a List inside a delegated Dialog; the List contains Items, optional Groups and Group headings, or Empty according to the current results. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
83
+ | FR2 | Input, List, Item, Group, Group heading, Empty, and Footer carry the seven current `command-palette-*` targets documented below; no `command-palette` root target exists. | Current source and target docs | Verified current inventory; focused placement coverage is partial |
84
+ | FR3 | The default Input delegates its Search glyph to Icon and pending Loading spinner to Spinner; the default Footer delegates keyboard shortcuts to Kbd; the containing surface delegates to Dialog. | Current source and component docs | Verified current composition; no ownership change |
85
+ | FR4 | Query field is a distinct native text field inside Input and currently has no separate public target. The `command-palette-input` target is on the surrounding search region, not the native field. | Current source and target docs | Verified current reachability; long-term theming intent remains unsettled |
86
+
87
+ ### Allowed variation
88
+
89
+ - **AV1 — Results.** Item count, grouping, selected content, and empty state may
90
+ vary without changing the aggregate anatomy.
91
+ - **AV2 — Slots.** Caller-provided Input and Footer content may replace the
92
+ defaults without becoming CommandPalette-owned subparts.
93
+ - **AV3 — Delegated rendering.** Dialog, Icon, Spinner, and Kbd may change their
94
+ internal element shape while preserving their own public contracts.
95
+
96
+ ### Representative states
97
+
98
+ | State | Required invariant | Allowed variation |
99
+ | ---------------------- | ------------------------------------------------------- | ----------------------------------------------- |
100
+ | Ungrouped results | List contains Item instances. | Item count and caller-rendered content. |
101
+ | Grouped results | List contains Group, Group heading, and Item instances. | Group names, count, ordering, and item content. |
102
+ | Empty bootstrap/search | List contains Empty instead of Items. | Caller-provided empty content. |
103
+ | Pending search | Default Input may contain Loading spinner. | Spinner is absent when no search is pending. |
104
+ | Default slots | Input and Footer render their documented defaults. | Footer shortcut text and translated labels. |
105
+ | Caller-replaced slots | The slot content replaces the corresponding default. | Replacement structure remains caller-owned. |
106
+
107
+ ### Transformation and precedence order
108
+
109
+ - No new search, selection, layout, or styling precedence rule is introduced.
110
+
111
+ ### Performance and resources
112
+
113
+ - No new performance or resource rule is introduced.
114
+
115
+ ## Accessibility contract
116
+
117
+ This draft does not change or extend CommandPalette's current Dialog, combobox,
118
+ listbox, option, announcement, or keyboard behavior.
119
+
120
+ ## Design relationships
121
+
122
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
123
+ | ----------------- | ----------------------------------------------------------------------- | ------------------------------ | -------------- | ------------------ |
124
+ | Dialog | Presents the containing modal surface. | `component:Dialog` | Supporting | FR1, FR3 |
125
+ | Input | Paints the search region around the query field and supporting visuals. | Current source and public docs | Prominent | FR1, FR2, FR4 |
126
+ | Search glyph | Presents the search symbol in the default Input. | `component:Icon` | Supporting | FR3 |
127
+ | Query field | Accepts the native text query inside Input. | Current source and public docs | Prominent | FR1, FR4 |
128
+ | Loading spinner | Indicates a pending search in the default Input. | `component:Spinner` | Supporting | FR3 |
129
+ | List | Presents the current result collection or Empty state. | Current source and public docs | Supporting | FR1, FR2 |
130
+ | Item | Presents one selectable command result. | Current source and public docs | Prominent | FR1, FR2 |
131
+ | Group | Arranges related Items together. | Current source and public docs | Supporting | FR1, FR2 |
132
+ | Group heading | Labels one Group visually. | Current source and public docs | Supporting | FR1, FR2 |
133
+ | Empty | Presents the no-results or no-query message. | Current source and public docs | Prominent | FR1, FR2 |
134
+ | Footer | Presents default guidance or caller-provided footer content. | Current source and public docs | Supporting | FR1, FR2 |
135
+ | Keyboard shortcut | Presents one shortcut as painted key badges in the default Footer. | `component:Kbd` | Supporting | FR3 |
136
+
137
+ The native Query field is separate from the Input region that carries
138
+ `command-palette-input`. Its current lack of a direct target is observed
139
+ reachability, not a decision that it must remain unthemeable.
140
+
141
+ ### Theming anatomy
142
+
143
+ <!-- anatomy-theming:v1 -->
144
+
145
+ ```json
146
+ {
147
+ "Dialog": {
148
+ "delegatesTo": {"owner": "component:Dialog", "target": "dialog"}
149
+ },
150
+ "Input": {"target": "command-palette-input"},
151
+ "Search glyph": {
152
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
153
+ },
154
+ "Query field": {
155
+ "none": {
156
+ "reason": "No current public target is applied directly to the native query field"
157
+ }
158
+ },
159
+ "Loading spinner": {
160
+ "delegatesTo": {"owner": "component:Spinner", "target": "spinner"}
161
+ },
162
+ "List": {"target": "command-palette-list"},
163
+ "Item": {"target": "command-palette-item"},
164
+ "Group": {"target": "command-palette-group"},
165
+ "Group heading": {"target": "command-palette-group-heading"},
166
+ "Empty": {"target": "command-palette-empty"},
167
+ "Footer": {"target": "command-palette-footer"},
168
+ "Keyboard shortcut": {
169
+ "delegatesTo": {"owner": "component:Kbd", "target": "kbd"}
170
+ }
171
+ }
172
+ ```
173
+
174
+ The seven local targets are current public seams. The delegated parts retain
175
+ their existing component owners. The Query field `none` disposition records a
176
+ current audit gap and does not authorize a new target.
177
+
178
+ ## Family and system relationships
179
+
180
+ - `architecture:component-theming-surface` owns anatomy qualification, target
181
+ mapping, delegation, and factual `none` dispositions.
182
+ - `architecture:public-component-api` owns the stable props and composition
183
+ surface; this documentation adds no API.
184
+ - `architecture:interaction-modality` owns shared keyboard and pointer modality;
185
+ this draft does not redefine focus ownership or input behavior.
186
+ - `family:overlay-dismissal` identifies CommandPalette as a current member and
187
+ records its component-local Escape handling as an adoption gap.
188
+ - No layer-runtime record is linked because no current record with that scope is
189
+ present in this checkout.
190
+
191
+ ## Verification map
192
+
193
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
194
+ | ------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
195
+ | FR1 | CommandPalette root and subcomponent render suites plus source inspection | Default, grouped, empty, pending, and replaced slots | Removing the asserted Dialog, Query field, List, Item, Group, Group heading, Empty, or Footer content fails existing role, content, or slot assertions; Search glyph, Loading spinner, and Keyboard shortcut presence remain source-inspected only. | `audit:CommandPalette/anatomy` |
196
+ | FR2 | Source inspection, `themingTargets.test.ts`, and `CommandPaletteGroup.test.tsx` | Seven local targets; Group and Group heading placement | Removing a current target fails the global inventory; moving Group or Group heading fails focused class assertions. | `audit:CommandPalette/theming` |
197
+ | FR3 | Source inspection plus Dialog, Icon, Spinner, and Kbd public target metadata | Default surface, Input visuals, and Footer shortcuts | Existing focused tests do not assert the composed Icon, Spinner, or Kbd instances; changing delegated ownership requires this map and the delegated component metadata to change. | `audit:CommandPalette/theming` |
198
+ | FR4 | `CommandPaletteInput.test.tsx` and source inspection | Native query field with idle and pending Input | Removing the field fails combobox tests; adding a field target requires an explicit map update. | `audit:CommandPalette/anatomy` |
199
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and seven current local targets | Missing, extra, prefixed, stale, or alias-backed mappings fail repository validation. | `audit:CommandPalette/theming` |
200
+
201
+ Focused target-placement assertions currently cover Group and Group heading.
202
+ Input, List, Item, Empty, and Footer placement rely on source inspection plus the
203
+ global target inventory. Existing tests assert default Footer text and pending
204
+ announcements, but do not assert that Icon, Spinner, or Kbd renders.
205
+
206
+ ## Decision log
207
+
208
+ None. This draft records current facts and introduces no component-local design,
209
+ layer, API, or theming decision.
210
+
211
+ ## Open questions
212
+
213
+ - **OQ1 — Should Query field gain a stable public theming target?** (`human-api`)
214
+ Its current lack of direct reachability is an audit gap, not settled intent.
215
+ - **OQ2 — Should focused tests pin the default Search glyph, pending Loading
216
+ spinner, and default Footer Keyboard shortcuts?** (`checkable`) Their presence
217
+ is currently source-inspected rather than asserted.
218
+
219
+ ## Content boundary
220
+
221
+ This file does not duplicate consumer prop tables/examples, shared layer or
222
+ modality rules, current audit results, or implementation steps. It links to
223
+ their owners.
@@ -1,5 +1,24 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Message box',
7
+ required: true,
8
+ description: 'Painted container for the validation feedback.',
9
+ },
10
+ {
11
+ name: 'Detached icon',
12
+ required: false,
13
+ description: 'Leading status glyph shown only by the detached variant.',
14
+ },
15
+ {
16
+ name: 'Message text',
17
+ required: true,
18
+ description: 'Text describing the validation status.',
19
+ },
20
+ ];
21
+
3
22
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
23
  export const docs = {
5
24
  name: 'FieldStatus',
@@ -61,6 +80,7 @@ export const docs = {
61
80
  'Use FieldStatus for general alerts or page-level notices; use Banner or Toast instead.',
62
81
  },
63
82
  ],
83
+ anatomy,
64
84
  },
65
85
  };
66
86
 
@@ -86,6 +106,7 @@ export const docsDense = {
86
106
  'Use FieldStatus for general alerts or page-level notices; use Banner or Toast instead.',
87
107
  },
88
108
  ],
109
+ anatomy,
89
110
  },
90
111
  propDescriptions: {
91
112
  type: 'error/warning/success status tone',
@@ -0,0 +1,212 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 1
4
+ kind: component
5
+ id: component:FieldStatus
6
+ authority: current
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: cixzhang
10
+ approved_at: 2026-08-30
11
+ owners: [cixzhang, imdreamrunner]
12
+ review_triggers: [public-api, behavior, theming, accessibility]
13
+ verified_by:
14
+ [
15
+ packages/core/src/FieldStatus/FieldStatus.test.tsx,
16
+ scripts/check-knowledge.mjs,
17
+ ]
18
+ families: []
19
+ design_specs: []
20
+ architecture: [architecture:component-theming-surface]
21
+ contributing: []
22
+ system_specs: []
23
+ ---
24
+
25
+ # FieldStatus component contract
26
+
27
+ ## Intent
28
+
29
+ FieldStatus presents one validation message for a field or field-like control. It
30
+ owns the visible message treatment and announcement behavior for attached and
31
+ detached presentations; consumer usage remains documented in
32
+ `FieldStatus.doc.mjs`.
33
+
34
+ ## Compatibility and migration
35
+
36
+ - Released default preserved: `yes`
37
+ - Compatibility class: additive documentation only; runtime, DOM, styling, and
38
+ public API remain unchanged
39
+ - Controlled/uncontrolled behavior: not applicable
40
+ - Migration decision: direct `FieldStatus variant="tooltip"` support is rejected.
41
+ `tooltip` remains a field-family sentinel that MUST be handled before rendering
42
+ FieldStatus. Narrowing the currently permissive type/runtime path requires a
43
+ separate compatibility-aware implementation change.
44
+
45
+ Consumer migration instructions belong in consumer docs and release notes.
46
+
47
+ ## Ownership boundary
48
+
49
+ **Owns**
50
+
51
+ - The attached and detached validation-message presentations.
52
+ - Routing visible messages through the persistent announcement channels.
53
+ - The detached presentation's redundant visual status glyph.
54
+
55
+ **Does not own / non-goals**
56
+
57
+ - Whether a caller renders a status message, or which control it describes —
58
+ owned by the composing field component or product callsite.
59
+ - The bordered control's status treatment and on-field tooltip presentation —
60
+ owned by the composing field component.
61
+ - General alerts and page-level notices — owned by the corresponding feedback
62
+ components.
63
+
64
+ ## Public concepts
65
+
66
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
67
+ | ------------ | ----------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------- | ----------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------- |
68
+ | Status type | `error`, `warning`, `success` | Selects the message tone and announcement channel. | All rendered presentations. | Required | `component:FieldStatus` | Stable | TypeScript rejects unsupported values. |
69
+ | Presentation | `attached`, `detached` | Selects the message presentation. | All direct FieldStatus usage. The field family's `tooltip` sentinel is handled before FieldStatus renders. | `attached` | `component:FieldStatus` | Stable. | TypeScript rejects values absent from the extensible variant map; its current acceptance of `tooltip` is an implementation gap. |
70
+
71
+ The field family's `tooltip` status presentation suppresses FieldStatus and
72
+ surfaces the message from the control's on-field icon. `tooltip` is not a
73
+ FieldStatus presentation. The public extensible variant map currently permits a
74
+ direct `tooltip` value; that is an implementation gap, not supported behavior.
75
+
76
+ ## Behavioral and layout contract
77
+
78
+ Draft requirements identify their basis so observed code is not mistaken for an
79
+ intentional decision.
80
+
81
+ | ID | Candidate invariant | Basis | Draft review state |
82
+ | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------- |
83
+ | FR1 | FieldStatus MUST render the provided message in one visible message box when mounted. | Documented promise and current tests. | Settled intent. |
84
+ | FR2 | The attached presentation MUST render the message without a leading glyph; the detached presentation MUST render its status glyph before the message text. | Documented promise, current DOM, and current tests. | Settled intent. |
85
+ | FR3 | Status type MUST select distinct error, warning, or success treatment and MUST be reflected on the message-box target. | Current behavior and theming tests. | Settled intent. |
86
+ | FR4 | The detached glyph MUST reflect status type on its own target; the target MUST be absent from attached presentation. | Current behavior and target tests. | Settled intent. |
87
+ | FR5 | The root MUST continue to forward its ref, ID, supported DOM props, and consumer styling escape hatches. | Current behavior and tests. | Current compatibility behavior; verify before changing. |
88
+ | FR6 | Message and status-type updates MUST update the visible output without replacing the component contract. | Current behavior and tests. | Current compatibility behavior; verify before changing. |
89
+ | FR7 | Direct FieldStatus usage MUST support only `attached` and `detached`; the Field family MUST consume its `tooltip` sentinel before rendering FieldStatus. | Approved component boundary. | Settled intent; current type acceptance is a gap. |
90
+
91
+ ### Allowed variation
92
+
93
+ - **AV1 — Theme styling.** Theme and consumer overrides may change visual CSS
94
+ properties through the existing targets and styling escape hatches without
95
+ changing anatomy, ownership, or presentation behavior.
96
+ - **AV2 — Message length.** The message may wrap; the detached glyph remains
97
+ aligned with the first text line.
98
+
99
+ ### Representative states
100
+
101
+ | State | Required invariant | Allowed variation |
102
+ | ---------------- | -------------------------------------------------------------- | ----------------------------------------------------------- |
103
+ | Attached error | Message box and text render; no detached glyph renders. | Theme-controlled visual properties on `field-status`. |
104
+ | Detached warning | Message box, leading warning glyph, then message text render. | Theme-controlled visual properties on both current targets. |
105
+ | Detached success | Message box, leading success glyph, then message text render. | Message length and theme-controlled visual properties. |
106
+ | Empty message | The root remains mounted and renders the provided empty value. | No announcement channel is created. |
107
+
108
+ ### Transformation and precedence order
109
+
110
+ - **ORD1 — Presentation styling.** Resolve the default presentation, reflect
111
+ status and presentation through `themeProps`, apply component styles, then
112
+ merge `xstyle`, `className`, and inline `style` in their established order.
113
+ - **ORD2 — Detached content.** Render the decorative status glyph before the
114
+ message text, using the same status-to-glyph mapping as field controls.
115
+
116
+ ### Performance and resources
117
+
118
+ - **PR1 — Announcement work.** A message change schedules only the persistent
119
+ live-region announcement; FieldStatus owns no global event listeners,
120
+ observers, or measurements.
121
+
122
+ ## Accessibility contract
123
+
124
+ - **AR1 — Announcement channel.** Errors MUST announce through the persistent
125
+ assertive region; warnings and successes MUST announce through the persistent
126
+ polite region, including on first mount and when message or type changes.
127
+ - **AR2 — Visible description.** The rendered message box MUST remain available
128
+ to assistive technology for `aria-describedby` association and MUST NOT become
129
+ its own newly mounted live region.
130
+ - **AR3 — Non-color cue.** Detached status MUST include a leading status glyph so
131
+ status is not conveyed only by color or position.
132
+ - **AR4 — Redundant glyph.** The detached glyph MUST remain hidden from assistive
133
+ technology because the visible message already conveys and announces the
134
+ status.
135
+
136
+ ## Design relationships
137
+
138
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
139
+ | ---------------- | ---------------------------------------------------------- | ----------------------------------------------- | -------------- | ------------------ |
140
+ | Message box | Carries the visible validation treatment. | Current behavior and approved component intent. | Supporting | FR1, FR3 |
141
+ | Detached icon | Adds a non-color status cue only in detached presentation. | Current behavior and approved component intent. | Supporting | FR2, FR4, AR3 |
142
+ | Message text | Communicates the validation result in words. | Current behavior and approved component intent. | Prominent | FR1, AR1, AR2 |
143
+
144
+ Current behavior renders the detached icon before message text. The intended
145
+ public theming ownership is the exact map below; it does not add a target or
146
+ change either presentation.
147
+
148
+ ### Theming anatomy
149
+
150
+ <!-- anatomy-theming:v1 -->
151
+
152
+ ```json
153
+ {
154
+ "Message box": {"target": "field-status"},
155
+ "Detached icon": {"target": "field-status-icon"},
156
+ "Message text": {"inherits": "field-status"}
157
+ }
158
+ ```
159
+
160
+ ## Family and system relationships
161
+
162
+ - `architecture:component-theming-surface` owns the qualification and validation
163
+ rules for the two targets and inherited message text.
164
+ - Field-like controls compose FieldStatus but continue to own control rendering,
165
+ status placement decisions, and association with the control.
166
+
167
+ ## Verification map
168
+
169
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
170
+ | ------------------- | --------------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------- |
171
+ | FR1, FR2, AR3, AR4 | `FieldStatus.test.tsx` rendering and detached-icon suites | Attached and detached; all status types | Removing or reordering presentation parts fails DOM and accessibility assertions. | `audit:FieldStatus/anatomy` |
172
+ | FR3, FR4 | `FieldStatus.test.tsx` theme target suites | Attached/detached; error/warning/success | Removing a target or status reflection fails class/data-attribute assertions. | `audit:FieldStatus/theming` |
173
+ | FR5, FR6 | `FieldStatus.test.tsx` forwarding and update suites | Initial render and rerender | Breaking the released root or update behavior fails compatibility assertions. | `audit:FieldStatus/behavior` |
174
+ | AR1, AR2 | `FieldStatus.test.tsx` announcement suites | Mount, message update, type update, empty message | Reintroducing a local live region or misrouting severity fails announcement assertions. | `audit:FieldStatus/accessibility` |
175
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Consumer anatomy and current targets | Missing, extra, duplicated, prefixed, or stale mappings fail repository validation. | `audit:FieldStatus/theming` |
176
+
177
+ ## Decision log
178
+
179
+ ### DEC-1 — Presentation-specific anatomy remains explicit
180
+
181
+ **Reference:** `component:FieldStatus/DEC-1`
182
+ **Decider:** cixzhang, 2026-08-30
183
+
184
+ The detached icon is a separate anatomy part because only detached presentation
185
+ renders it and it owns a target distinct from the message box. Message text
186
+ inherits the message-box target rather than creating a redundant child target.
187
+
188
+ Rejected: collapsing the glyph into a generic message row, which would hide its
189
+ presentation condition and distinct theming owner.
190
+
191
+ ### DEC-2 — `tooltip` is not a FieldStatus presentation
192
+
193
+ **Reference:** `component:FieldStatus/DEC-2`
194
+ **Decider:** cixzhang, 2026-08-30
195
+
196
+ Direct FieldStatus usage supports only `attached` and `detached`. The Field
197
+ family may use `tooltip` as an internal presentation sentinel, but it MUST
198
+ consume that value before rendering FieldStatus and surface the message through
199
+ the control's on-field status treatment instead.
200
+
201
+ Rejected: defining direct `FieldStatus variant="tooltip"` rendering semantics.
202
+ That would expose a field-composition mechanism as a third FieldStatus
203
+ presentation and preserve currently unintentional output.
204
+
205
+ ## Open questions
206
+
207
+ None.
208
+
209
+ ## Content boundary
210
+
211
+ This file does not duplicate consumer prop examples, current audit results,
212
+ implementation steps, or family/system rules. It links to their owners.
@@ -1,5 +1,60 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Tree list',
7
+ required: true,
8
+ description: 'Container that presents the hierarchical tree.',
9
+ },
10
+ {
11
+ name: 'Header',
12
+ required: false,
13
+ description: 'Caller-provided content that visibly names the tree.',
14
+ },
15
+ {
16
+ name: 'Item',
17
+ required: true,
18
+ description: 'Painted row for one node in the hierarchy.',
19
+ },
20
+ {
21
+ name: 'Chevron',
22
+ required: false,
23
+ description:
24
+ 'Expand and collapse control rendered for an Item with children.',
25
+ },
26
+ {
27
+ name: 'Chevron glyph',
28
+ required: false,
29
+ description: 'Directional symbol rendered by Icon inside a Chevron.',
30
+ },
31
+ {
32
+ name: 'Item label',
33
+ required: true,
34
+ description: 'Primary content that identifies an Item.',
35
+ },
36
+ {
37
+ name: 'Item description',
38
+ required: false,
39
+ description: 'Secondary text rendered below an Item label.',
40
+ },
41
+ {
42
+ name: 'Start content',
43
+ required: false,
44
+ description: 'Caller-provided content rendered before an Item label.',
45
+ },
46
+ {
47
+ name: 'End content',
48
+ required: false,
49
+ description: 'Caller-provided content rendered after an Item label.',
50
+ },
51
+ {
52
+ name: 'Guide',
53
+ required: false,
54
+ description: 'Connector line that shows parent-child relationships.',
55
+ },
56
+ ];
57
+
3
58
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
59
 
5
60
  export const docs = {
@@ -79,6 +134,7 @@ export const docs = {
79
134
  },
80
135
  ],
81
136
  usage: {
137
+ anatomy,
82
138
  description:
83
139
  'An expandable tree structure for displaying hierarchical data with branch connector lines. Use it for file explorers, nested category browsers, or any interface that visualizes parent-child relationships.',
84
140
  bestPractices: [
@@ -158,6 +214,7 @@ export const docsZh = {
158
214
  },
159
215
  ],
160
216
  usage: {
217
+ anatomy,
161
218
  description:
162
219
  'An expandable tree structure for displaying hierarchical data with branch connector lines. Use it for file explorers, nested category browsers, or any interface that visualizes parent-child relationships.',
163
220
  bestPractices: [
@@ -175,6 +232,7 @@ export const docsDense = {
175
232
  description:
176
233
  'Data-driven tree list for hierarchical data w/ expand/collapse, branch lines, interactive items. Flat items array w/ recursive children, no composition, no cloneElement.',
177
234
  usage: {
235
+ anatomy,
178
236
  description:
179
237
  'An expandable tree structure for displaying hierarchical data with branch connector lines. Use it for file explorers, nested category browsers, or any interface that visualizes parent-child relationships.',
180
238
  bestPractices: [
@@ -0,0 +1,220 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 2
4
+ kind: component
5
+ id: component:TreeList
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers: [behavior, theming, accessibility]
13
+ verified_by:
14
+ [
15
+ packages/core/src/TreeList/TreeList.test.tsx,
16
+ packages/core/src/theme/themingTargets.test.ts,
17
+ scripts/check-knowledge.mjs,
18
+ ]
19
+ families: []
20
+ design_specs: []
21
+ architecture:
22
+ [
23
+ architecture:component-theming-surface,
24
+ architecture:interaction-modality,
25
+ architecture:public-component-api,
26
+ ]
27
+ contributing: []
28
+ system_specs: []
29
+ ---
30
+
31
+ # TreeList component contract
32
+
33
+ ## Intent
34
+
35
+ TreeList presents hierarchical data as expandable tree rows. This draft records
36
+ its current consumer anatomy, target ownership, delegated Icon glyph, and
37
+ unreached stable parts without changing runtime behavior, styling, targets, or
38
+ public API.
39
+
40
+ ## Compatibility and migration
41
+
42
+ - Released default preserved: `yes`
43
+ - Compatibility class: additive documentation only; runtime, DOM, styling,
44
+ targets, aliases, and public API remain unchanged
45
+ - Controlled/uncontrolled behavior: unchanged
46
+ - Migration decision: none
47
+
48
+ Consumer migration instructions belong in consumer docs and release notes.
49
+
50
+ ## Ownership boundary
51
+
52
+ **Owns**
53
+
54
+ - The Tree list, Item, Chevron, Item label, and Guide parts and their five current
55
+ targets.
56
+ - The stable Item description rendered below an Item label.
57
+ - Hierarchical placement, indentation, expansion, selection, and tree semantics.
58
+
59
+ **Does not own / non-goals**
60
+
61
+ - The Chevron glyph artwork and Icon target — owned by `component:Icon`.
62
+ - Header, Start content, and End content supplied through consumer slots — owned
63
+ by the product callsite.
64
+ - New targets for Item description or caller-provided slots — unresolved by this
65
+ factual backfill.
66
+
67
+ ## Public concepts
68
+
69
+ No new public concept is introduced. Consumer data, props, slots, and usage
70
+ remain documented in `TreeList.doc.mjs`.
71
+
72
+ ## Behavioral and layout contract
73
+
74
+ | ID | Candidate invariant | Basis | Draft review state |
75
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | ------------------------------------------------------------------------- |
76
+ | FR1 | The current render contains one Tree list, one Item and Item label per rendered node, optional Header, Chevron, Item description, Start content, End content, and Guide parts, and delegated Chevron glyphs. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
77
+ | FR2 | Tree list, Item, Chevron, Item label, and Guide carry the five current `tree-list*` targets documented below. | Current source and target docs | Verified current inventory; focused placement coverage is partial |
78
+ | FR3 | Chevron glyph delegates to Icon's `icon` target. Header, Start content, and End content are consumer-owned and have no TreeList target. | Current source and component docs | Verified current ownership; no target change |
79
+ | FR4 | Item description is a stable library-rendered part below Item label and currently has no public target. It does not inherit through `tree-list-item-label`; both spans are siblings inside the row content. | Current source and target docs | Verified current reachability; long-term theming intent remains unsettled |
80
+
81
+ ### Allowed variation
82
+
83
+ - **AV1 — Hierarchy.** Item count, depth, expansion, selection, density, and guide
84
+ visibility may vary without changing the anatomy ownership recorded here.
85
+ - **AV2 — Consumer content.** Header, Start content, End content, Item label
86
+ values, and descriptions may vary without making caller-owned slot content a
87
+ TreeList target.
88
+ - **AV3 — Delegated glyph.** Icon may change the Chevron glyph's internal element
89
+ shape while preserving its own public contract.
90
+
91
+ ### Representative states
92
+
93
+ | State | Required invariant | Allowed variation |
94
+ | ------------------------ | ----------------------------------------------------- | --------------------------------------------- |
95
+ | Empty tree | Tree list remains; no Item instances render. | Optional Header content. |
96
+ | Flat items | Each Item contains an Item label. | Description and consumer slots may be absent. |
97
+ | Expandable item | Item may contain Chevron and delegated Chevron glyph. | Expanded or collapsed state. |
98
+ | Nested `lineGuides` tree | Guide instances show parent-child relationships. | Count and position follow hierarchy. |
99
+ | `noGuides` tree | Guide instances are absent; indentation remains. | All other anatomy remains unchanged. |
100
+ | Described item | Item description renders below Item label. | Caller-provided description text. |
101
+
102
+ ### Transformation and precedence order
103
+
104
+ - No new hierarchy, focus, expansion, layout, or styling precedence rule is
105
+ introduced.
106
+
107
+ ### Performance and resources
108
+
109
+ - No new performance or resource rule is introduced.
110
+
111
+ ## Accessibility contract
112
+
113
+ This draft does not change or extend TreeList's current tree, treeitem, group,
114
+ roving-focus, expansion, selection, disabled, or activation behavior.
115
+
116
+ ## Design relationships
117
+
118
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
119
+ | ---------------- | -------------------------------------------------------------------- | ------------------------------ | ----------------- | ------------------ |
120
+ | Tree list | Groups the hierarchical tree and owns its density and guide variant. | Current source and public docs | Supporting | FR1, FR2 |
121
+ | Header | Visibly names the tree with caller-provided content. | Caller-supplied content | Prominent | FR1, FR3 |
122
+ | Item | Paints one row and reflects density, selection, and disabled state. | Current source and public docs | Prominent | FR1, FR2 |
123
+ | Chevron | Provides the expand and collapse control for an Item with children. | Current source and public docs | Supporting | FR1, FR2 |
124
+ | Chevron glyph | Presents the current directional symbol inside Chevron. | `component:Icon` | Supporting | FR1, FR3 |
125
+ | Item label | Presents the primary content that identifies an Item. | Current source and public docs | Prominent | FR1, FR2 |
126
+ | Item description | Presents stable secondary text below Item label. | Current source and public docs | Supporting | FR1, FR4 |
127
+ | Start content | Presents caller-provided content before Item label. | Caller-supplied content | Context-dependent | FR1, FR3 |
128
+ | End content | Presents caller-provided content after Item label. | Caller-supplied content | Context-dependent | FR1, FR3 |
129
+ | Guide | Paints a connector between related hierarchy levels. | Current source and public docs | Supporting | FR1, FR2 |
130
+
131
+ Item description is stable TreeList-rendered anatomy, not consumer-owned slot
132
+ structure. Its current lack of a target is observed reachability, not a decision
133
+ that it must remain unthemeable. Header, Start content, and End content remain
134
+ caller-owned even though TreeList positions their slot wrappers.
135
+
136
+ ### Theming anatomy
137
+
138
+ <!-- anatomy-theming:v1 -->
139
+
140
+ ```json
141
+ {
142
+ "Tree list": {"target": "tree-list"},
143
+ "Header": {
144
+ "none": {
145
+ "reason": "TreeList applies no public target to caller-provided Header content"
146
+ }
147
+ },
148
+ "Item": {"target": "tree-list-item"},
149
+ "Chevron": {"target": "tree-list-chevron"},
150
+ "Chevron glyph": {
151
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
152
+ },
153
+ "Item label": {"target": "tree-list-item-label"},
154
+ "Item description": {
155
+ "none": {
156
+ "reason": "No current public target is applied to the stable Item description"
157
+ }
158
+ },
159
+ "Start content": {
160
+ "none": {
161
+ "reason": "TreeList applies no public target to caller-provided Start content"
162
+ }
163
+ },
164
+ "End content": {
165
+ "none": {
166
+ "reason": "TreeList applies no public target to caller-provided End content"
167
+ }
168
+ },
169
+ "Guide": {"target": "tree-list-guide"}
170
+ }
171
+ ```
172
+
173
+ The five local targets are current public seams. The Chevron glyph retains Icon
174
+ ownership. Consumer-slot `none` dispositions are ownership boundaries; Item
175
+ description's `none` disposition records a current audit gap and does not
176
+ authorize a new target.
177
+
178
+ ## Family and system relationships
179
+
180
+ - `architecture:component-theming-surface` owns anatomy qualification, target
181
+ mapping, delegation, and factual `none` dispositions.
182
+ - `architecture:public-component-api` owns the stable data, props, and slot
183
+ surface; this documentation adds no API.
184
+ - `architecture:interaction-modality` owns shared keyboard and pointer modality;
185
+ TreeList continues to own its existing tree focus and activation behavior.
186
+ - TreeList has no current family contract, and no unresolved family or runtime
187
+ record identifier is included.
188
+
189
+ ## Verification map
190
+
191
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
192
+ | ------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
193
+ | FR1 | `TreeList.test.tsx` rendering, hierarchy, slot, and accessibility suites plus source inspection | Flat, nested, described, headed, and slotted items | Removing asserted Tree list, Header, Item, Chevron, Item label, Item description, Start content, End content, or Guide behavior fails existing role, content, hierarchy, or state assertions; Chevron glyph presence remains source-inspected only. | `audit:TreeList/anatomy` |
194
+ | FR2 | `TreeList.test.tsx` and `themingTargets.test.ts` | Five local targets and their documented states | Removing or moving any current local target fails focused class assertions or the global inventory. | `audit:TreeList/theming` |
195
+ | FR3 | Source inspection plus Icon public target metadata | Expandable rows and caller-provided slots | Existing focused tests do not assert the composed Icon instance; changing glyph or slot ownership requires this map and the relevant owner metadata to change. | `audit:TreeList/theming` |
196
+ | FR4 | `TreeList.test.tsx` description rendering test and source inspection | Item with description | Removing description fails content coverage; adding a target requires an explicit map update. | `audit:TreeList/anatomy` |
197
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and five current local targets | Missing, extra, prefixed, stale, or alias-backed mappings fail repository validation. | `audit:TreeList/theming` |
198
+
199
+ Focused target-placement assertions cover all five local targets: Tree list,
200
+ Item, Chevron, Item label, and Guide. Existing tests do not assert the Icon
201
+ instance that renders Chevron glyph.
202
+
203
+ ## Decision log
204
+
205
+ None. This draft records current facts and introduces no component-local design,
206
+ API, behavior, or theming decision.
207
+
208
+ ## Open questions
209
+
210
+ - **OQ1 — Should Item description gain a stable public theming target?**
211
+ (`human-api`) Its current lack of reachability is an audit gap, not settled
212
+ intent.
213
+ - **OQ2 — Should a focused test pin the delegated Chevron glyph's Icon
214
+ instance?** (`checkable`) Its presence is currently source-inspected rather
215
+ than asserted.
216
+
217
+ ## Content boundary
218
+
219
+ This file does not duplicate consumer prop tables/examples, shared modality
220
+ rules, current audit results, or implementation steps. It links to their owners.
@@ -3,8 +3,10 @@
3
3
  /* eslint-disable @typescript-eslint/no-require-imports */
4
4
  /**
5
5
  * @file Guards `theming.targets` against the real `themeProps()` call sites (#3741).
6
- * @input Component sources (*.tsx/*.ts) and their `{Name}.doc.mjs` files.
7
- * @output Vitest failures naming each undocumented class / visual prop.
6
+ * @input Core/Lab component sources (*.tsx/*.ts), their `{Name}.doc.mjs`
7
+ * files, and the Lab promotion-candidate manifest.
8
+ * @output Vitest failures naming each undocumented class / visual prop on the
9
+ * stable Core surface or a capability-participating Lab component.
8
10
  * @position Sibling of derivedVarRegistry.test.ts, which already validates the
9
11
  * OTHER fields of the same `theming` block (`vars`, `derived`). `targets` was
10
12
  * the one field with no machine check, so it drifted — twice (#3652, #3680).
@@ -22,6 +24,11 @@
22
24
  * `visualProps` or `states`. Docs may list MORE than the source passes —
23
25
  * components forward props they don't themselves reflect (Timestamp passes
24
26
  * `{format}` but documents `type`/`color`/`format`), and that is intentional.
27
+ *
28
+ * Lab is capability-based: a component participates when it already declares
29
+ * `theming.targets` or enters the existing promotion-candidate manifest. Other
30
+ * Lab components remain free to iterate with runtime hooks that are not yet a
31
+ * documented public theming promise.
25
32
  */
26
33
 
27
34
  import {describe, it, expect} from 'vitest';
@@ -30,7 +37,23 @@ import {join, relative} from 'node:path';
30
37
  import ts from 'typescript';
31
38
  import {stableClassName} from '../naming';
32
39
 
33
- const SRC_DIR = join(__dirname, '..');
40
+ const CORE_SRC_DIR = join(__dirname, '..');
41
+ const LAB_SRC_DIR = join(__dirname, '../../../lab/src');
42
+ const LAB_PROMOTION_MANIFEST = join(
43
+ __dirname,
44
+ '../../../../internal/lab-readiness/manifest.mjs',
45
+ );
46
+
47
+ interface LabPromotionCandidate {
48
+ sourceDir: string;
49
+ }
50
+
51
+ const {CANDIDATES: labPromotionCandidates} = require(
52
+ LAB_PROMOTION_MANIFEST,
53
+ ) as {CANDIDATES: LabPromotionCandidate[]};
54
+ const LAB_PROMOTION_DIRS = new Set(
55
+ labPromotionCandidates.map(candidate => candidate.sourceDir),
56
+ );
34
57
 
35
58
  // ---------------------------------------------------------------------------
36
59
  // Source scanning: find themeProps() call sites via the TypeScript AST
@@ -314,6 +337,7 @@ type DocBlock = {theming?: {targets?: DocTarget[]}};
314
337
  type ComponentDocModule = {docs?: DocBlock; docsZh?: DocBlock};
315
338
 
316
339
  interface ComponentInfo {
340
+ packageName: 'core' | 'lab';
317
341
  dir: string;
318
342
  sites: ThemeTargetSite[];
319
343
  /** The doc blocks that carry theming.targets, by the key they live under. */
@@ -328,7 +352,10 @@ interface ComponentInfo {
328
352
  * Reads the file as text rather than requiring it: this runs for directories
329
353
  * that have no doc of their own, so most candidates are misses.
330
354
  */
331
- function docFilesDocumenting(classNames: Set<string>): string[] {
355
+ function docFilesDocumenting(
356
+ srcDir: string,
357
+ classNames: Set<string>,
358
+ ): string[] {
332
359
  const matches: string[] = [];
333
360
  const walk = (dir: string): void => {
334
361
  for (const entry of readdirSync(dir, {withFileTypes: true})) {
@@ -346,18 +373,22 @@ function docFilesDocumenting(classNames: Set<string>): string[] {
346
373
  }
347
374
  }
348
375
  };
349
- walk(SRC_DIR);
376
+ walk(srcDir);
350
377
  return matches;
351
378
  }
352
379
 
353
- function discoverComponents(): ComponentInfo[] {
380
+ function discoverComponents(
381
+ srcDir: string,
382
+ packageName: ComponentInfo['packageName'],
383
+ requiredDirs: ReadonlySet<string> = new Set(),
384
+ ): ComponentInfo[] {
354
385
  const results: ComponentInfo[] = [];
355
- const dirs = readdirSync(SRC_DIR, {withFileTypes: true})
386
+ const dirs = readdirSync(srcDir, {withFileTypes: true})
356
387
  .filter(d => d.isDirectory())
357
388
  .map(d => d.name);
358
389
 
359
390
  for (const dir of dirs) {
360
- const dirPath = join(SRC_DIR, dir);
391
+ const dirPath = join(srcDir, dir);
361
392
  const dirEntries = readdirSync(dirPath);
362
393
 
363
394
  const sourceFiles = dirEntries.filter(
@@ -389,10 +420,7 @@ function discoverComponents(): ComponentInfo[] {
389
420
  // doc file that CI never checks. (Same guard as derivedVarRegistry.test.ts.)
390
421
  const docFiles = dirEntries.includes(`${dir}.doc.mjs`)
391
422
  ? [join(dirPath, `${dir}.doc.mjs`)]
392
- : docFilesDocumenting(new Set(sites.map(s => s.className)));
393
- if (docFiles.length === 0) {
394
- continue;
395
- }
423
+ : docFilesDocumenting(srcDir, new Set(sites.map(s => s.className)));
396
424
 
397
425
  const docBlocks: ComponentInfo['docBlocks'] = [];
398
426
  for (const docFile of docFiles) {
@@ -404,18 +432,23 @@ function discoverComponents(): ComponentInfo[] {
404
432
  }
405
433
  for (const key of ['docs', 'docsZh'] as const) {
406
434
  const targets = mod[key]?.theming?.targets;
407
- // Only blocks that already document a theming surface are held to it —
408
- // a doc with no theming block at all is a separate (documentation) gap.
435
+ // A normal Core or Lab component enrolls by declaring a theming
436
+ // surface. Promotion candidates are also enrolled here so a missing
437
+ // block is a failure rather than a silently skipped readiness gap.
409
438
  if (targets != null) {
410
- docBlocks.push({key, file: relative(SRC_DIR, docFile), targets});
439
+ docBlocks.push({key, file: relative(srcDir, docFile), targets});
411
440
  }
412
441
  }
413
442
  }
414
- if (docBlocks.length === 0) {
443
+ const participates =
444
+ packageName === 'core'
445
+ ? docBlocks.length > 0
446
+ : requiredDirs.has(dir) || docBlocks.length > 0;
447
+ if (!participates) {
415
448
  continue;
416
449
  }
417
450
 
418
- results.push({dir, sites, docBlocks});
451
+ results.push({packageName, dir, sites, docBlocks});
419
452
  }
420
453
  return results;
421
454
  }
@@ -425,24 +458,57 @@ function discoverComponents(): ComponentInfo[] {
425
458
  // ---------------------------------------------------------------------------
426
459
 
427
460
  describe('theming.targets matches the themeProps() call sites', () => {
428
- const components = discoverComponents();
461
+ const components = [
462
+ ...discoverComponents(CORE_SRC_DIR, 'core'),
463
+ ...discoverComponents(LAB_SRC_DIR, 'lab', LAB_PROMOTION_DIRS),
464
+ ];
465
+
466
+ it('finds participating Core and Lab components', () => {
467
+ // A refactor that renames themeProps or drops the Lab package from this
468
+ // inventory must not silently disable either side of the guard.
469
+ expect(components.some(component => component.packageName === 'core')).toBe(
470
+ true,
471
+ );
472
+ expect(components.some(component => component.packageName === 'lab')).toBe(
473
+ true,
474
+ );
475
+ });
429
476
 
430
- it('finds components to check', () => {
431
- // A refactor that renames themeProps must not silently disable this file.
432
- expect(components.length).toBeGreaterThan(0);
477
+ it('enrolls every Lab promotion candidate and explicit theming capability', () => {
478
+ const labDirs = new Set(
479
+ components
480
+ .filter(component => component.packageName === 'lab')
481
+ .map(component => component.dir),
482
+ );
483
+ expect([...LAB_PROMOTION_DIRS].filter(dir => !labDirs.has(dir))).toEqual(
484
+ [],
485
+ );
486
+ expect(LAB_PROMOTION_DIRS.has('CircularProgress')).toBe(false);
487
+ expect(labDirs.has('CircularProgress')).toBe(true);
488
+ expect(labDirs.has('Schedule')).toBe(false);
433
489
  });
434
490
 
435
- for (const {dir, sites, docBlocks} of components) {
491
+ for (const {packageName, dir, sites, docBlocks} of components) {
492
+ const componentLabel = `${packageName}/${dir}`;
436
493
  const renderedClasses = [...new Set(sites.map(s => s.className))].sort();
437
494
 
495
+ it(`${componentLabel}: participating components declare theming metadata`, () => {
496
+ expect(
497
+ docBlocks,
498
+ `${componentLabel} participates in the public theming contract but has ` +
499
+ `no loadable .doc.mjs theming.targets. Lab components participate ` +
500
+ `when they declare theming.targets or enter the promotion manifest.`,
501
+ ).not.toHaveLength(0);
502
+ });
503
+
438
504
  for (const {key, file, targets} of docBlocks) {
439
505
  const documented = new Set(targets.map(t => t.className));
440
506
 
441
- it(`${dir} (${file} ${key}): every rendered class is documented`, () => {
507
+ it(`${componentLabel} (${file} ${key}): every rendered class is documented`, () => {
442
508
  const undocumented = renderedClasses.filter(c => !documented.has(c));
443
509
  expect(
444
510
  undocumented,
445
- `${dir} renders ${undocumented.length} astryx-* class(es) that ` +
511
+ `${componentLabel} renders ${undocumented.length} astryx-* class(es) that ` +
446
512
  `${file} ${key}.theming.targets does not document: ` +
447
513
  `${undocumented.join(', ')}. An undocumented class is an ` +
448
514
  `unthemeable element — theme authors and codegen read targets[] ` +
@@ -450,7 +516,7 @@ describe('theming.targets matches the themeProps() call sites', () => {
450
516
  ).toEqual([]);
451
517
  });
452
518
 
453
- it(`${dir} (${file} ${key}): every visual prop passed to themeProps is documented`, () => {
519
+ it(`${componentLabel} (${file} ${key}): every visual prop passed to themeProps is documented`, () => {
454
520
  const missing: string[] = [];
455
521
  for (const site of sites) {
456
522
  const target = targets.find(t => t.className === site.className);