@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 +2 -2
- package/src/CommandPalette/CommandPalette.doc.mjs +72 -0
- package/src/CommandPalette/CommandPalette.spec.md +223 -0
- package/src/FieldStatus/FieldStatus.doc.mjs +21 -0
- package/src/FieldStatus/FieldStatus.spec.md +212 -0
- package/src/TreeList/TreeList.doc.mjs +58 -0
- package/src/TreeList/TreeList.spec.md +220 -0
- package/src/theme/themingTargets.test.ts +91 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.5.2-canary.
|
|
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.
|
|
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
|
|
7
|
-
*
|
|
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
|
|
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(
|
|
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(
|
|
376
|
+
walk(srcDir);
|
|
350
377
|
return matches;
|
|
351
378
|
}
|
|
352
379
|
|
|
353
|
-
function discoverComponents(
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
408
|
-
//
|
|
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(
|
|
439
|
+
docBlocks.push({key, file: relative(srcDir, docFile), targets});
|
|
411
440
|
}
|
|
412
441
|
}
|
|
413
442
|
}
|
|
414
|
-
|
|
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 =
|
|
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('
|
|
431
|
-
|
|
432
|
-
|
|
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(`${
|
|
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
|
-
`${
|
|
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(`${
|
|
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);
|