@astryxdesign/cli 0.5.2-canary.e4f8e4e → 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/foundation/discovery/__fixtures__/component-accessibility-overlay.doc.d.mts +16 -1
- package/foundation/discovery/__fixtures__/component-accessibility-overlay.doc.mjs +17 -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 +6 -2
- package/foundation/discovery/component-loader.test.mjs +36 -1
- package/foundation/discovery/componentDocOverlay.test.mjs +29 -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
|
|
|
@@ -33,7 +33,13 @@ export namespace docs {
|
|
|
33
33
|
}
|
|
34
34
|
let props: never[];
|
|
35
35
|
}
|
|
36
|
-
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 };
|
|
37
43
|
export namespace usage_1 {
|
|
38
44
|
let description_1: string;
|
|
39
45
|
export { description_1 as description };
|
|
@@ -42,3 +48,12 @@ export namespace docsDense {
|
|
|
42
48
|
let props_1: never[];
|
|
43
49
|
export { props_1 as props };
|
|
44
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
|
+
}
|
|
@@ -34,15 +34,30 @@ export const docs = {
|
|
|
34
34
|
],
|
|
35
35
|
anatomy: [
|
|
36
36
|
{
|
|
37
|
-
name: '
|
|
37
|
+
name: 'Track',
|
|
38
38
|
required: true,
|
|
39
|
-
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.',
|
|
40
45
|
},
|
|
41
46
|
],
|
|
42
47
|
},
|
|
43
48
|
props: [],
|
|
44
49
|
};
|
|
45
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
|
+
|
|
46
61
|
export const docsDense = {
|
|
47
62
|
usage: {
|
|
48
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,7 +194,8 @@ function overlayComponentDoc(docs, translation) {
|
|
|
194
194
|
});
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
-
/** Preserve structured
|
|
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
|
*/
|
|
@@ -210,6 +211,9 @@ function overlayComponentDoc(docs, translation) {
|
|
|
210
211
|
baseUsage?.accessibilityThemeCoverage !== undefined
|
|
211
212
|
? {accessibilityThemeCoverage: baseUsage.accessibilityThemeCoverage}
|
|
212
213
|
: null),
|
|
214
|
+
...(translatedUsage.anatomy === undefined && baseUsage?.anatomy !== undefined
|
|
215
|
+
? {anatomy: baseUsage.anatomy}
|
|
216
|
+
: null),
|
|
213
217
|
};
|
|
214
218
|
};
|
|
215
219
|
|
|
@@ -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});
|
|
@@ -127,6 +127,33 @@ describe('the reported symptom', () => {
|
|
|
127
127
|
'Decorative track — Not part of the contrast audit.',
|
|
128
128
|
);
|
|
129
129
|
expect(dense.usage.bestPractices).toBeUndefined();
|
|
130
|
-
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);
|
|
131
158
|
});
|
|
132
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": {
|