@astryxdesign/cli 0.5.2-canary.e4f7677 → 0.5.2-canary.edc0cfc
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/api/theme/targets/targets.test.mjs +15 -0
- package/assets/templates/blocks/components/Table/TableRowStatusTable.doc.mjs +3 -3
- package/assets/templates/blocks/components/Table/TableRowStatusTable.tsx +4 -4
- package/authoring/doctypes/base/type.ts +91 -1
- package/authoring/doctypes/component/component.doc.mjs +7 -1
- package/authoring/index.d.ts +7 -0
- package/foundation/discovery/__fixtures__/component-accessibility-overlay.doc.d.mts +26 -1
- package/foundation/discovery/__fixtures__/component-accessibility-overlay.doc.mjs +31 -2
- package/foundation/discovery/__fixtures__/component-anatomy-override.doc.d.mts +38 -0
- package/foundation/discovery/__fixtures__/component-anatomy-override.doc.mjs +45 -0
- package/foundation/discovery/component-loader.mjs +17 -6
- package/foundation/discovery/component-loader.test.mjs +36 -1
- package/foundation/discovery/componentDocOverlay.test.mjs +35 -2
- package/foundation/discovery/theming-targets.d.mts +8 -3
- package/foundation/discovery/theming-targets.mjs +58 -6
- package/foundation/discovery/theming-targets.test.mjs +49 -2
- package/package.json +9 -9
|
@@ -39,6 +39,21 @@ describe('themeTargets (api/theme/targets)', () => {
|
|
|
39
39
|
]);
|
|
40
40
|
}, 60_000);
|
|
41
41
|
|
|
42
|
+
it.each(['table-header', 'table-body', 'table-footer'])(
|
|
43
|
+
'%s appears once under the Table owner',
|
|
44
|
+
async target => {
|
|
45
|
+
const {data} = await themeTargets('Table');
|
|
46
|
+
const matches = data.targets.filter(entry => entry.key === target);
|
|
47
|
+
expect(data.componentCount).toBe(1);
|
|
48
|
+
expect(matches).toHaveLength(1);
|
|
49
|
+
expect(matches[0]).toMatchObject({
|
|
50
|
+
key: target,
|
|
51
|
+
component: 'Table',
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
60_000,
|
|
55
|
+
);
|
|
56
|
+
|
|
42
57
|
// Half the system's keys contain "button" (chat-send-button, toggle-button,
|
|
43
58
|
// …). A component name has to mean the component, or `theme targets Button`
|
|
44
59
|
// answers a different question than `component Button` and the two views
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
export const doc = {
|
|
5
5
|
type: 'block',
|
|
6
6
|
exampleFor: 'useTableRowStatus',
|
|
7
|
-
name: 'useTableRowStatus - Status
|
|
8
|
-
displayName: 'useTableRowStatus - Status
|
|
7
|
+
name: 'useTableRowStatus - Status Icons',
|
|
8
|
+
displayName: 'useTableRowStatus - Status Icons',
|
|
9
9
|
description:
|
|
10
|
-
'A job table using useTableRowStatus
|
|
10
|
+
'A job table using useTableRowStatus. Semantic outcomes use the themed success, warning, and error icons; a palette-only queued state keeps the neutral dot.',
|
|
11
11
|
isReady: true,
|
|
12
12
|
aspectRatio: 16 / 9,
|
|
13
13
|
componentsUsed: ['Table'],
|
|
@@ -34,13 +34,13 @@ const columns: TableColumn<Job>[] = [
|
|
|
34
34
|
function jobStatus(job: Job): TableRowStatus | null {
|
|
35
35
|
switch (job.state) {
|
|
36
36
|
case 'failed':
|
|
37
|
-
return {color: 'error',
|
|
37
|
+
return {color: 'error', label: 'Failed'};
|
|
38
38
|
case 'running':
|
|
39
|
-
return {color: 'warning',
|
|
39
|
+
return {color: 'warning', label: 'Running'};
|
|
40
40
|
case 'queued':
|
|
41
41
|
return {color: 'gray', label: 'Queued'};
|
|
42
|
-
|
|
43
|
-
return
|
|
42
|
+
case 'succeeded':
|
|
43
|
+
return {color: 'success', label: 'Succeeded'};
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -64,8 +64,96 @@ export interface ComponentBestPractice {
|
|
|
64
64
|
export interface ComponentAccessibilityRequirement {
|
|
65
65
|
/** Short scannable label, e.g. `"Accessible name"` or `"Loading"`. */
|
|
66
66
|
name: string;
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* The accessibility contract consumers must preserve. Write at about a
|
|
69
|
+
* grade-7 reading level with short sentences, common words, and active
|
|
70
|
+
* voice. For color contrast, put the ratio in `requirement`; name the exact
|
|
71
|
+
* foreground, background, state, and any overlay in `description`; explain
|
|
72
|
+
* exceptions in plain language; and include enough detail for a human or
|
|
73
|
+
* agent to reproduce the check.
|
|
74
|
+
*/
|
|
68
75
|
description: string;
|
|
76
|
+
/** Groups related requirements in the docsite Accessibility tab. */
|
|
77
|
+
category?: 'Color contrast' | 'Keyboard' | 'Semantics' | 'Content';
|
|
78
|
+
/** Relevant WCAG success criterion, e.g. `"1.4.3 Contrast (Minimum)"`. */
|
|
79
|
+
criterion?: string;
|
|
80
|
+
/** Short threshold or rule, e.g. `"4.5:1"`, `"3:1"`, or `"Exempt"`. */
|
|
81
|
+
requirement?: string;
|
|
82
|
+
/** Component states covered by this requirement. */
|
|
83
|
+
states?: string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ComponentAccessibilityThemeStatus = 'Pass' | 'Fail' | 'Not tested';
|
|
87
|
+
|
|
88
|
+
export type ComponentAccessibilityThemeApplicability =
|
|
89
|
+
'Required' | 'Conditional' | 'Supplemental' | 'Decorative';
|
|
90
|
+
|
|
91
|
+
export interface ComponentAccessibilityThemeMeasurement {
|
|
92
|
+
/** Column heading, e.g. `"Rest"` or `"Spinner"`. */
|
|
93
|
+
label: string;
|
|
94
|
+
/** Display value, e.g. `"15.13:1"`. */
|
|
95
|
+
value: string;
|
|
96
|
+
/** Optional supporting detail shown below the value, such as a worst case. */
|
|
97
|
+
detail?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Whether this measurement is required for every use, required only in some
|
|
100
|
+
* contexts, shown as a supplemental cue, or decorative. Each theme declares
|
|
101
|
+
* this intent, informed by the component contract. Never infer it from the
|
|
102
|
+
* measured ratio. Non-required measurements do not determine row status.
|
|
103
|
+
*/
|
|
104
|
+
applicability?: ComponentAccessibilityThemeApplicability;
|
|
105
|
+
/** Rendered foreground and background colors used for this measurement. */
|
|
106
|
+
colorPair?: {
|
|
107
|
+
foreground: string;
|
|
108
|
+
background: string;
|
|
109
|
+
};
|
|
110
|
+
/** Optional per-variant results shown from a compact details trigger. */
|
|
111
|
+
breakdown?: Array<{
|
|
112
|
+
label: string;
|
|
113
|
+
value: string;
|
|
114
|
+
detail?: string;
|
|
115
|
+
colorPair: {
|
|
116
|
+
foreground: string;
|
|
117
|
+
background: string;
|
|
118
|
+
};
|
|
119
|
+
status?: 'Pass' | 'Fail';
|
|
120
|
+
}>;
|
|
121
|
+
/** Mark a failed measurement so the docsite can emphasize it. */
|
|
122
|
+
status?: 'Pass' | 'Fail';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ComponentAccessibilityThemeResult {
|
|
126
|
+
/** Row heading, usually a component variant. */
|
|
127
|
+
name: string;
|
|
128
|
+
/** Measurements shown between the row heading and status. */
|
|
129
|
+
measurements: ComponentAccessibilityThemeMeasurement[];
|
|
130
|
+
/** Overall result for the row. */
|
|
131
|
+
status: ComponentAccessibilityThemeStatus;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface ComponentAccessibilityThemeMode {
|
|
135
|
+
/** Theme mode covered by these results. */
|
|
136
|
+
mode: 'Light' | 'Dark';
|
|
137
|
+
/** Detailed results for the component in this mode. */
|
|
138
|
+
results: ComponentAccessibilityThemeResult[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ComponentAccessibilityThemeTable {
|
|
142
|
+
/** Optional heading for one complete group of measurements. */
|
|
143
|
+
title?: string;
|
|
144
|
+
/** Explains the scope of this measurement group. */
|
|
145
|
+
description?: string;
|
|
146
|
+
/** Detailed results separated by theme mode. */
|
|
147
|
+
modes: ComponentAccessibilityThemeMode[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ComponentAccessibilityThemeCoverage {
|
|
151
|
+
/** Display name of the audited theme. */
|
|
152
|
+
theme: string;
|
|
153
|
+
/** Complete groups of measurements for this theme and component. */
|
|
154
|
+
tables: ComponentAccessibilityThemeTable[];
|
|
155
|
+
/** Theme visuals intentionally excluded from measurement, with a reason. */
|
|
156
|
+
notMeasured?: string[];
|
|
69
157
|
}
|
|
70
158
|
|
|
71
159
|
/**
|
|
@@ -462,6 +550,8 @@ export interface UsageDoc {
|
|
|
462
550
|
/** Accessibility requirements specific to this component and its supported
|
|
463
551
|
* content combinations. Generic audit procedure stays in the wiki rubric. */
|
|
464
552
|
accessibility?: ComponentAccessibilityRequirement[];
|
|
553
|
+
/** Verified color-accessibility coverage for bundled themes. */
|
|
554
|
+
accessibilityThemeCoverage?: ComponentAccessibilityThemeCoverage[];
|
|
465
555
|
/** Structural/visual anatomy of the component. Each entry describes one
|
|
466
556
|
* element that makes up the component (icon slot, label, container, etc.).
|
|
467
557
|
* Order entries in the visual reading order (leading → trailing, top → bottom). */
|
|
@@ -132,7 +132,13 @@ export const doc = {
|
|
|
132
132
|
name: 'usage.accessibility',
|
|
133
133
|
type: 'ComponentAccessibilityRequirement[]',
|
|
134
134
|
description:
|
|
135
|
-
'Component-specific
|
|
135
|
+
'Component-specific requirements rendered in the shared Accessibility tab. Write at about a grade-7 reading level with short sentences, common words, and active voice. For color contrast, put the ratio in `requirement`; name the exact foreground, background, state, and any overlay in `description`; explain exceptions in plain language; and give a human or agent enough detail to reproduce the check. Keep repository audit procedures in the wiki rubric.',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'usage.accessibilityThemeCoverage',
|
|
139
|
+
type: 'ComponentAccessibilityThemeCoverage[]',
|
|
140
|
+
description:
|
|
141
|
+
'Verified per-theme accessibility measurements rendered in the shared Accessibility tab. Record light and dark mode separately, include rendered color pairs, and mark failed measurements. Put visuals excluded from the audit in `notMeasured` with a short reason; do not add them as table measurements. Each theme declares `applicability` for measured values, informed by the component contract and never inferred from the ratio: `Conditional` is required only in some contexts, `Supplemental` adds another meaningful cue, and `Decorative` has no required meaning. These values do not change row status. Provide a complete breakdown when one cell summarizes multiple combinations, and protect derived values with an automated audit.',
|
|
136
142
|
},
|
|
137
143
|
{
|
|
138
144
|
name: 'usage.anatomy',
|
package/authoring/index.d.ts
CHANGED
|
@@ -68,6 +68,13 @@ export type {
|
|
|
68
68
|
ComponentExampleDoc,
|
|
69
69
|
ComponentAnatomyElement,
|
|
70
70
|
ComponentAccessibilityRequirement,
|
|
71
|
+
ComponentAccessibilityThemeStatus,
|
|
72
|
+
ComponentAccessibilityThemeApplicability,
|
|
73
|
+
ComponentAccessibilityThemeMeasurement,
|
|
74
|
+
ComponentAccessibilityThemeResult,
|
|
75
|
+
ComponentAccessibilityThemeMode,
|
|
76
|
+
ComponentAccessibilityThemeTable,
|
|
77
|
+
ComponentAccessibilityThemeCoverage,
|
|
71
78
|
ComponentBestPractice,
|
|
72
79
|
ComponentSlotElement,
|
|
73
80
|
ComponentPlaygroundConfig,
|
|
@@ -15,6 +15,16 @@ export namespace docs {
|
|
|
15
15
|
name: string;
|
|
16
16
|
description: string;
|
|
17
17
|
}[];
|
|
18
|
+
let accessibilityThemeCoverage: {
|
|
19
|
+
theme: string;
|
|
20
|
+
tables: {
|
|
21
|
+
modes: {
|
|
22
|
+
mode: string;
|
|
23
|
+
results: never[];
|
|
24
|
+
}[];
|
|
25
|
+
}[];
|
|
26
|
+
notMeasured: string[];
|
|
27
|
+
}[];
|
|
18
28
|
let anatomy: {
|
|
19
29
|
name: string;
|
|
20
30
|
required: boolean;
|
|
@@ -23,7 +33,13 @@ export namespace docs {
|
|
|
23
33
|
}
|
|
24
34
|
let props: never[];
|
|
25
35
|
}
|
|
26
|
-
export namespace
|
|
36
|
+
export namespace docsZh {
|
|
37
|
+
let name_1: string;
|
|
38
|
+
export { name_1 as name };
|
|
39
|
+
let displayName_1: string;
|
|
40
|
+
export { displayName_1 as displayName };
|
|
41
|
+
let category_1: string;
|
|
42
|
+
export { category_1 as category };
|
|
27
43
|
export namespace usage_1 {
|
|
28
44
|
let description_1: string;
|
|
29
45
|
export { description_1 as description };
|
|
@@ -32,3 +48,12 @@ export namespace docsDense {
|
|
|
32
48
|
let props_1: never[];
|
|
33
49
|
export { props_1 as props };
|
|
34
50
|
}
|
|
51
|
+
export namespace docsDense {
|
|
52
|
+
export namespace usage_2 {
|
|
53
|
+
let description_2: string;
|
|
54
|
+
export { description_2 as description };
|
|
55
|
+
}
|
|
56
|
+
export { usage_2 as usage };
|
|
57
|
+
let props_2: never[];
|
|
58
|
+
export { props_2 as props };
|
|
59
|
+
}
|
|
@@ -18,17 +18,46 @@ export const docs = {
|
|
|
18
18
|
description: 'Provide an accessible name.',
|
|
19
19
|
},
|
|
20
20
|
],
|
|
21
|
+
accessibilityThemeCoverage: [
|
|
22
|
+
{
|
|
23
|
+
theme: 'Fixture',
|
|
24
|
+
tables: [
|
|
25
|
+
{
|
|
26
|
+
modes: [
|
|
27
|
+
{mode: 'Light', results: []},
|
|
28
|
+
{mode: 'Dark', results: []},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
notMeasured: ['Decorative track — Not part of the contrast audit.'],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
21
35
|
anatomy: [
|
|
22
36
|
{
|
|
23
|
-
name: '
|
|
37
|
+
name: 'Track',
|
|
24
38
|
required: true,
|
|
25
|
-
description: '
|
|
39
|
+
description: 'The rail that shows the current on/off state.',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Thumb',
|
|
43
|
+
required: true,
|
|
44
|
+
description: 'The control that moves along the track.',
|
|
26
45
|
},
|
|
27
46
|
],
|
|
28
47
|
},
|
|
29
48
|
props: [],
|
|
30
49
|
};
|
|
31
50
|
|
|
51
|
+
export const docsZh = {
|
|
52
|
+
name: 'AccessibilityOverlayFixture',
|
|
53
|
+
displayName: 'Accessibility Overlay Fixture',
|
|
54
|
+
category: 'Test',
|
|
55
|
+
usage: {
|
|
56
|
+
description: 'Translated full-doc description.',
|
|
57
|
+
},
|
|
58
|
+
props: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
32
61
|
export const docsDense = {
|
|
33
62
|
usage: {
|
|
34
63
|
description: 'Dense description.',
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
export namespace docs {
|
|
5
|
+
let name: string;
|
|
6
|
+
let displayName: string;
|
|
7
|
+
let category: string;
|
|
8
|
+
namespace usage {
|
|
9
|
+
let description: string;
|
|
10
|
+
let anatomy: {
|
|
11
|
+
name: string;
|
|
12
|
+
required: boolean;
|
|
13
|
+
description: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
let props: never[];
|
|
17
|
+
}
|
|
18
|
+
export namespace docsZh {
|
|
19
|
+
let name_1: string;
|
|
20
|
+
export { name_1 as name };
|
|
21
|
+
let displayName_1: string;
|
|
22
|
+
export { displayName_1 as displayName };
|
|
23
|
+
let category_1: string;
|
|
24
|
+
export { category_1 as category };
|
|
25
|
+
export namespace usage_1 {
|
|
26
|
+
let description_1: string;
|
|
27
|
+
export { description_1 as description };
|
|
28
|
+
let anatomy_1: {
|
|
29
|
+
name: string;
|
|
30
|
+
required: boolean;
|
|
31
|
+
description: string;
|
|
32
|
+
}[];
|
|
33
|
+
export { anatomy_1 as anatomy };
|
|
34
|
+
}
|
|
35
|
+
export { usage_1 as usage };
|
|
36
|
+
let props_1: never[];
|
|
37
|
+
export { props_1 as props };
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
export const docs = {
|
|
4
|
+
name: 'AnatomyOverrideFixture',
|
|
5
|
+
displayName: 'Anatomy Override Fixture',
|
|
6
|
+
category: 'Test',
|
|
7
|
+
usage: {
|
|
8
|
+
description: 'A multiline field for longer text.',
|
|
9
|
+
anatomy: [
|
|
10
|
+
{
|
|
11
|
+
name: 'Field',
|
|
12
|
+
required: true,
|
|
13
|
+
description: 'The multiline text entry area.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'Resize handle',
|
|
17
|
+
required: false,
|
|
18
|
+
description: 'Lets the user change the field height.',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
props: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const docsZh = {
|
|
26
|
+
name: 'AnatomyOverrideFixture',
|
|
27
|
+
displayName: 'Anatomy Override Fixture',
|
|
28
|
+
category: 'Test',
|
|
29
|
+
usage: {
|
|
30
|
+
description: '用于输入较长文本的多行字段。',
|
|
31
|
+
anatomy: [
|
|
32
|
+
{
|
|
33
|
+
name: '文本区域',
|
|
34
|
+
required: true,
|
|
35
|
+
description: '用于输入多行文本的区域。',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: '调整大小控件',
|
|
39
|
+
required: false,
|
|
40
|
+
description: '允许用户调整字段高度。',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
props: [],
|
|
45
|
+
};
|
|
@@ -51,7 +51,7 @@ export async function loadComponentDoc(
|
|
|
51
51
|
/** @type {any} */
|
|
52
52
|
const translation = mod[translationKey];
|
|
53
53
|
if (translation.props || translation.components?.some((/** @type {any} */ c) => c.props)) {
|
|
54
|
-
return translation;
|
|
54
|
+
return overlayComponentDoc(docs, translation);
|
|
55
55
|
}
|
|
56
56
|
return mergeTranslation(docs, translation);
|
|
57
57
|
}
|
|
@@ -194,16 +194,27 @@ function overlayComponentDoc(docs, translation) {
|
|
|
194
194
|
});
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
-
/** Preserve
|
|
197
|
+
/** Preserve canonical structured guidance added after legacy full-doc translations,
|
|
198
|
+
* without changing established translated prose behavior.
|
|
198
199
|
* @param {any} baseUsage
|
|
199
200
|
* @param {any} translatedUsage
|
|
200
201
|
*/
|
|
201
202
|
const mergeUsage = (baseUsage, translatedUsage) => {
|
|
202
203
|
if (!translatedUsage) return baseUsage;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
return {
|
|
205
|
+
...translatedUsage,
|
|
206
|
+
...(translatedUsage.accessibility === undefined &&
|
|
207
|
+
baseUsage?.accessibility !== undefined
|
|
208
|
+
? {accessibility: baseUsage.accessibility}
|
|
209
|
+
: null),
|
|
210
|
+
...(translatedUsage.accessibilityThemeCoverage === undefined &&
|
|
211
|
+
baseUsage?.accessibilityThemeCoverage !== undefined
|
|
212
|
+
? {accessibilityThemeCoverage: baseUsage.accessibilityThemeCoverage}
|
|
213
|
+
: null),
|
|
214
|
+
...(translatedUsage.anatomy === undefined && baseUsage?.anatomy !== undefined
|
|
215
|
+
? {anatomy: baseUsage.anatomy}
|
|
216
|
+
: null),
|
|
217
|
+
};
|
|
207
218
|
};
|
|
208
219
|
|
|
209
220
|
const merged = {...docs, ...translation, usage: mergeUsage(docs.usage, translation.usage)};
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {describe, it, expect} from 'vitest';
|
|
12
|
-
import
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import {loadComponentDoc, loadDocs, mergeTranslation} from './component-loader.mjs';
|
|
13
14
|
|
|
14
15
|
/** Minimal HookDoc-shaped fixture with params + returns. */
|
|
15
16
|
function hookDocs() {
|
|
@@ -105,3 +106,37 @@ describe('mergeTranslation — component prop descriptions (regression)', () =>
|
|
|
105
106
|
expect(merged.props.find(p => p.name === 'disabled').description).toBe('Disabled state.');
|
|
106
107
|
});
|
|
107
108
|
});
|
|
109
|
+
|
|
110
|
+
describe('loadComponentDoc — full localized doc overlays', () => {
|
|
111
|
+
it('inherits canonical anatomy when docsZh omits it and matches loadDocs', async () => {
|
|
112
|
+
const docPath = path.join(
|
|
113
|
+
import.meta.dirname,
|
|
114
|
+
'__fixtures__',
|
|
115
|
+
'component-accessibility-overlay.doc.mjs',
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const direct = await loadComponentDoc(docPath, {zh: true});
|
|
119
|
+
const cli = await loadDocs(docPath, {zh: true});
|
|
120
|
+
|
|
121
|
+
expect(direct).toEqual(cli);
|
|
122
|
+
expect(direct.usage.description).toBe('Translated full-doc description.');
|
|
123
|
+
expect(direct.usage.anatomy).toEqual((await loadComponentDoc(docPath)).usage.anatomy);
|
|
124
|
+
expect(direct.usage.bestPractices).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('keeps explicit localized anatomy and matches loadDocs', async () => {
|
|
128
|
+
const docPath = path.join(
|
|
129
|
+
import.meta.dirname,
|
|
130
|
+
'__fixtures__',
|
|
131
|
+
'component-anatomy-override.doc.mjs',
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const direct = await loadComponentDoc(docPath, {zh: true});
|
|
135
|
+
const cli = await loadDocs(docPath, {zh: true});
|
|
136
|
+
const canonical = await loadComponentDoc(docPath);
|
|
137
|
+
|
|
138
|
+
expect(direct).toEqual(cli);
|
|
139
|
+
expect(direct.usage.anatomy).not.toEqual(canonical.usage.anatomy);
|
|
140
|
+
expect(direct.usage.anatomy[0].name).toBe('文本区域');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -90,7 +90,7 @@ describe('translated component docs never drop a prop', () => {
|
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
describe('
|
|
93
|
+
describe('full component-doc overlays', () => {
|
|
94
94
|
it('astryx component Button --zh still lists isInterruptible and isIconOnly', async () => {
|
|
95
95
|
const docPath = path.join(CORE_SRC, 'Button', 'Button.doc.mjs');
|
|
96
96
|
const zh = await loadDocs(docPath, {zh: true});
|
|
@@ -120,7 +120,40 @@ describe('the reported symptom', () => {
|
|
|
120
120
|
|
|
121
121
|
expect(english.usage.accessibility.length).toBeGreaterThan(0);
|
|
122
122
|
expect(dense.usage.accessibility).toEqual(english.usage.accessibility);
|
|
123
|
+
expect(dense.usage.accessibilityThemeCoverage).toEqual(
|
|
124
|
+
english.usage.accessibilityThemeCoverage,
|
|
125
|
+
);
|
|
126
|
+
expect(dense.usage.accessibilityThemeCoverage[0].notMeasured).toContain(
|
|
127
|
+
'Decorative track — Not part of the contrast audit.',
|
|
128
|
+
);
|
|
123
129
|
expect(dense.usage.bestPractices).toBeUndefined();
|
|
124
|
-
expect(dense.usage.anatomy).
|
|
130
|
+
expect(dense.usage.anatomy).toEqual(english.usage.anatomy);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('inherits canonical anatomy when a full docsZh usage omits it', async () => {
|
|
134
|
+
const docPath = path.join(
|
|
135
|
+
import.meta.dirname,
|
|
136
|
+
'__fixtures__',
|
|
137
|
+
'component-accessibility-overlay.doc.mjs',
|
|
138
|
+
);
|
|
139
|
+
const english = await loadDocs(docPath);
|
|
140
|
+
const zh = await loadDocs(docPath, {zh: true});
|
|
141
|
+
|
|
142
|
+
expect(zh.usage.description).toBe('Translated full-doc description.');
|
|
143
|
+
expect(zh.usage.anatomy).toEqual(english.usage.anatomy);
|
|
144
|
+
expect(zh.usage.bestPractices).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('keeps explicit localized anatomy from a full docsZh usage', async () => {
|
|
148
|
+
const docPath = path.join(
|
|
149
|
+
import.meta.dirname,
|
|
150
|
+
'__fixtures__',
|
|
151
|
+
'component-anatomy-override.doc.mjs',
|
|
152
|
+
);
|
|
153
|
+
const mod = await import(pathToFileURL(docPath).href);
|
|
154
|
+
const zh = await loadDocs(docPath, {zh: true});
|
|
155
|
+
|
|
156
|
+
expect(zh.usage.anatomy).toEqual(mod.docsZh.usage.anatomy);
|
|
157
|
+
expect(zh.usage.anatomy).not.toEqual(mod.docs.usage.anatomy);
|
|
125
158
|
});
|
|
126
159
|
});
|
|
@@ -3,16 +3,21 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Every theming target declared under a core `src` directory, sorted by key
|
|
6
|
-
* then component.
|
|
7
|
-
*
|
|
6
|
+
* then component. When both a parent doc and one of its `subComponentOf`
|
|
7
|
+
* children declare the same class, the parent is the canonical discovery owner;
|
|
8
|
+
* the child keeps its direct docs but does not add a second listing row. Shared
|
|
9
|
+
* targets declared by unrelated components remain separate rows.
|
|
8
10
|
*
|
|
9
11
|
* Unreadable docs are skipped rather than fatal — a single malformed doc must
|
|
10
12
|
* not take out theme validation or the listing.
|
|
11
13
|
*
|
|
12
14
|
* @param {string} coreSrc - absolute path to `<core>/src`
|
|
15
|
+
* @param {{includeDeprecated?: boolean}} [options] - preserve the CLI's full listing by default; ownership checks can request active targets only
|
|
13
16
|
* @returns {Promise<ThemingTarget[]>}
|
|
14
17
|
*/
|
|
15
|
-
export function collectThemingTargets(coreSrc: string
|
|
18
|
+
export function collectThemingTargets(coreSrc: string, { includeDeprecated }?: {
|
|
19
|
+
includeDeprecated?: boolean;
|
|
20
|
+
}): Promise<ThemingTarget[]>;
|
|
16
21
|
/**
|
|
17
22
|
* One public custom property a theme may set on a component's target.
|
|
18
23
|
* @typedef {object} ThemingVar
|
|
@@ -45,19 +45,25 @@ function targetKey(className) {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Every theming target declared under a core `src` directory, sorted by key
|
|
48
|
-
* then component.
|
|
49
|
-
*
|
|
48
|
+
* then component. When both a parent doc and one of its `subComponentOf`
|
|
49
|
+
* children declare the same class, the parent is the canonical discovery owner;
|
|
50
|
+
* the child keeps its direct docs but does not add a second listing row. Shared
|
|
51
|
+
* targets declared by unrelated components remain separate rows.
|
|
50
52
|
*
|
|
51
53
|
* Unreadable docs are skipped rather than fatal — a single malformed doc must
|
|
52
54
|
* not take out theme validation or the listing.
|
|
53
55
|
*
|
|
54
56
|
* @param {string} coreSrc - absolute path to `<core>/src`
|
|
57
|
+
* @param {{includeDeprecated?: boolean}} [options] - preserve the CLI's full listing by default; ownership checks can request active targets only
|
|
55
58
|
* @returns {Promise<ThemingTarget[]>}
|
|
56
59
|
*/
|
|
57
|
-
export async function collectThemingTargets(
|
|
60
|
+
export async function collectThemingTargets(
|
|
61
|
+
coreSrc,
|
|
62
|
+
{includeDeprecated = true} = {},
|
|
63
|
+
) {
|
|
58
64
|
if (!coreSrc || !fs.existsSync(coreSrc)) return [];
|
|
59
65
|
|
|
60
|
-
/** @type {ThemingTarget
|
|
66
|
+
/** @type {Array<ThemingTarget & {parent: string|null}>} */
|
|
61
67
|
const targets = [];
|
|
62
68
|
|
|
63
69
|
/** @param {string} dir */
|
|
@@ -85,6 +91,7 @@ export async function collectThemingTargets(coreSrc) {
|
|
|
85
91
|
: path.basename(path.dirname(full));
|
|
86
92
|
|
|
87
93
|
for (const target of doc?.theming?.targets || []) {
|
|
94
|
+
if (!includeDeprecated && target?.deprecatedFor != null) continue;
|
|
88
95
|
const className = target?.className;
|
|
89
96
|
if (typeof className !== 'string') continue;
|
|
90
97
|
const key = targetKey(className);
|
|
@@ -93,6 +100,8 @@ export async function collectThemingTargets(coreSrc) {
|
|
|
93
100
|
key,
|
|
94
101
|
className,
|
|
95
102
|
component,
|
|
103
|
+
parent:
|
|
104
|
+
typeof doc?.subComponentOf === 'string' ? doc.subComponentOf : null,
|
|
96
105
|
props: stringList(target.visualProps),
|
|
97
106
|
states: stringList(target.states),
|
|
98
107
|
});
|
|
@@ -102,10 +111,53 @@ export async function collectThemingTargets(coreSrc) {
|
|
|
102
111
|
|
|
103
112
|
await scan(coreSrc);
|
|
104
113
|
|
|
105
|
-
targets
|
|
114
|
+
const canonical = canonicalizeParentTargets(targets);
|
|
115
|
+
canonical.sort(
|
|
106
116
|
(a, b) => a.key.localeCompare(b.key) || a.component.localeCompare(b.component),
|
|
107
117
|
);
|
|
108
|
-
return
|
|
118
|
+
return canonical;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Collapse only an explicit parent/child duplicate. A child target is removed
|
|
123
|
+
* when its `subComponentOf` parent declares that same class exactly once; its
|
|
124
|
+
* props and states are merged into the parent's row so no capability is lost.
|
|
125
|
+
*
|
|
126
|
+
* Unrelated components sharing a class remain separate. An ambiguous parent
|
|
127
|
+
* declaration also remains untouched rather than guessing which row is
|
|
128
|
+
* canonical.
|
|
129
|
+
*
|
|
130
|
+
* @param {Array<ThemingTarget & {parent: string|null}>} targets
|
|
131
|
+
* @returns {ThemingTarget[]}
|
|
132
|
+
*/
|
|
133
|
+
function canonicalizeParentTargets(targets) {
|
|
134
|
+
/** @type {Map<string, Array<ThemingTarget & {parent: string|null}>>} */
|
|
135
|
+
const rootsByComponentAndClass = new Map();
|
|
136
|
+
for (const target of targets) {
|
|
137
|
+
if (target.parent != null) continue;
|
|
138
|
+
const identity = `${target.component}\0${target.className}`;
|
|
139
|
+
const roots = rootsByComponentAndClass.get(identity) ?? [];
|
|
140
|
+
roots.push(target);
|
|
141
|
+
rootsByComponentAndClass.set(identity, roots);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @type {Set<ThemingTarget & {parent: string|null}>} */
|
|
145
|
+
const duplicates = new Set();
|
|
146
|
+
for (const target of targets) {
|
|
147
|
+
if (target.parent == null) continue;
|
|
148
|
+
const roots =
|
|
149
|
+
rootsByComponentAndClass.get(`${target.parent}\0${target.className}`) ?? [];
|
|
150
|
+
if (roots.length !== 1) continue;
|
|
151
|
+
|
|
152
|
+
const canonical = roots[0];
|
|
153
|
+
canonical.props = [...new Set([...canonical.props, ...target.props])];
|
|
154
|
+
canonical.states = [...new Set([...canonical.states, ...target.states])];
|
|
155
|
+
duplicates.add(target);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return targets
|
|
159
|
+
.filter(target => !duplicates.has(target))
|
|
160
|
+
.map(({parent: _parent, ...target}) => target);
|
|
109
161
|
}
|
|
110
162
|
|
|
111
163
|
/**
|
|
@@ -41,6 +41,10 @@ const coreSrc = path.join(coreDir, 'src');
|
|
|
41
41
|
|
|
42
42
|
/** @type {Promise<import('./theming-targets.mjs').ThemingTarget[]>} */
|
|
43
43
|
const enumerated = collectThemingTargets(coreSrc);
|
|
44
|
+
/** @type {Promise<import('./theming-targets.mjs').ThemingTarget[]>} */
|
|
45
|
+
const activeEnumerated = collectThemingTargets(coreSrc, {
|
|
46
|
+
includeDeprecated: false,
|
|
47
|
+
});
|
|
44
48
|
|
|
45
49
|
describe('collectThemingTargets', () => {
|
|
46
50
|
it('enumerates the whole surface, not a handful', async () => {
|
|
@@ -56,6 +60,20 @@ describe('collectThemingTargets', () => {
|
|
|
56
60
|
}
|
|
57
61
|
});
|
|
58
62
|
|
|
63
|
+
it('can limit ownership checks to active targets', async () => {
|
|
64
|
+
const all = await enumerated;
|
|
65
|
+
const active = await activeEnumerated;
|
|
66
|
+
expect(
|
|
67
|
+
all.some(t => t.component === 'CodeBlock' && t.key === 'codeblock'),
|
|
68
|
+
).toBe(true);
|
|
69
|
+
expect(
|
|
70
|
+
active.some(t => t.component === 'CodeBlock' && t.key === 'codeblock'),
|
|
71
|
+
).toBe(false);
|
|
72
|
+
expect(
|
|
73
|
+
active.some(t => t.component === 'CodeBlock' && t.key === 'code-block'),
|
|
74
|
+
).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
59
77
|
it('carries the props and states a target reflects', async () => {
|
|
60
78
|
const targets = await enumerated;
|
|
61
79
|
expect(targets.find(t => t.key === 'switch-thumb')).toEqual({
|
|
@@ -67,6 +85,34 @@ describe('collectThemingTargets', () => {
|
|
|
67
85
|
});
|
|
68
86
|
});
|
|
69
87
|
|
|
88
|
+
it.each([
|
|
89
|
+
['TableHeader', 'table-header'],
|
|
90
|
+
['TableBody', 'table-body'],
|
|
91
|
+
['TableFooter', 'table-footer'],
|
|
92
|
+
])('keeps %s theming metadata available in its direct doc', async (name, key) => {
|
|
93
|
+
const doc = await loadComponentDoc(
|
|
94
|
+
path.join(coreSrc, 'Table', `${name}.doc.mjs`),
|
|
95
|
+
);
|
|
96
|
+
expect(doc.subComponentOf).toBe('Table');
|
|
97
|
+
expect(doc.theming.targets).toContainEqual({className: `astryx-${key}`});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it.each(['table-header', 'table-body', 'table-footer'])(
|
|
101
|
+
'enumerates %s once under its canonical Table owner',
|
|
102
|
+
async key => {
|
|
103
|
+
const matches = (await enumerated).filter(target => target.key === key);
|
|
104
|
+
expect(matches).toEqual([
|
|
105
|
+
{
|
|
106
|
+
key,
|
|
107
|
+
className: `astryx-${key}`,
|
|
108
|
+
component: 'Table',
|
|
109
|
+
props: [],
|
|
110
|
+
states: [],
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
70
116
|
it('is sorted by key, so a diff of two runs is readable', async () => {
|
|
71
117
|
const keys = (await enumerated).map(t => t.key);
|
|
72
118
|
expect(keys).toEqual([...keys].sort((a, b) => a.localeCompare(b)));
|
|
@@ -78,9 +124,10 @@ describe('collectThemingTargets', () => {
|
|
|
78
124
|
it('collapses to the override keys, merging the components that share one', async () => {
|
|
79
125
|
const byKey = targetsByKey(await enumerated);
|
|
80
126
|
expect(byKey['switch']).toEqual(['size', 'checked', 'disabled']);
|
|
81
|
-
// `radio` is documented by
|
|
127
|
+
// `radio` is documented by two unrelated owners. Parent/child
|
|
128
|
+
// canonicalization must not collapse a shared target across families.
|
|
82
129
|
const radio = (await enumerated).filter(t => t.key === 'radio');
|
|
83
|
-
expect(radio.
|
|
130
|
+
expect(radio.map(t => t.component)).toEqual(['Indicator', 'RadioList']);
|
|
84
131
|
for (const t of radio) {
|
|
85
132
|
for (const name of [...t.props, ...t.states]) {
|
|
86
133
|
expect(byKey['radio']).toContain(name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.5.2-canary.
|
|
3
|
+
"version": "0.5.2-canary.edc0cfc",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -87,10 +87,10 @@
|
|
|
87
87
|
"zod": "^4.4.3"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"@astryxdesign/charts": "0.5.2-canary.
|
|
91
|
-
"@astryxdesign/core": "0.5.2-canary.
|
|
92
|
-
"@astryxdesign/lab": "0.5.2-canary.
|
|
93
|
-
"@astryxdesign/theme-neutral": "0.5.2-canary.
|
|
90
|
+
"@astryxdesign/charts": "0.5.2-canary.edc0cfc",
|
|
91
|
+
"@astryxdesign/core": "0.5.2-canary.edc0cfc",
|
|
92
|
+
"@astryxdesign/lab": "0.5.2-canary.edc0cfc",
|
|
93
|
+
"@astryxdesign/theme-neutral": "0.5.2-canary.edc0cfc",
|
|
94
94
|
"gpt-tokenizer": "^3.4.0"
|
|
95
95
|
},
|
|
96
96
|
"peerDependenciesMeta": {
|
|
@@ -108,10 +108,10 @@
|
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
|
-
"@astryxdesign/charts": "0.5.2-canary.
|
|
112
|
-
"@astryxdesign/core": "0.5.2-canary.
|
|
113
|
-
"@astryxdesign/lab": "0.5.2-canary.
|
|
114
|
-
"@astryxdesign/theme-neutral": "0.5.2-canary.
|
|
111
|
+
"@astryxdesign/charts": "0.5.2-canary.edc0cfc",
|
|
112
|
+
"@astryxdesign/core": "0.5.2-canary.edc0cfc",
|
|
113
|
+
"@astryxdesign/lab": "0.5.2-canary.edc0cfc",
|
|
114
|
+
"@astryxdesign/theme-neutral": "0.5.2-canary.edc0cfc",
|
|
115
115
|
"gpt-tokenizer": "^3.4.0"
|
|
116
116
|
},
|
|
117
117
|
"scripts": {
|