@astryxdesign/core 0.5.2-canary.e4f7677 → 0.5.2-canary.e4f8e4e
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/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +2 -2
- package/dist/astryx.css +2 -0
- package/package.json +2 -2
- package/src/Button/Button.doc.mjs +56 -0
- package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
- package/src/Carousel/Carousel.test.tsx +83 -0
- package/src/Carousel/Carousel.tsx +8 -2
- package/src/IconButton/IconButton.doc.mjs +38 -0
- package/src/ProgressBar/ProgressBar.spec.md +208 -0
- package/src/SegmentedControl/SegmentedControl.doc.mjs +56 -0
- package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
- package/src/theme/themingTargets.test.ts +91 -25
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/Carousel/Carousel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAKhD;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;OAIG;IACH,aAAa,IAAI,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACtC,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/Carousel/Carousel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAKhD;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;OAIG;IACH,aAAa,IAAI,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACtC,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA+JD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,SAAS,EACT,QAAQ,EACR,GAAO,EACP,UAAiB,EACjB,WAAkB,EAClB,OAAe,EACf,OAAe,EACf,OAAO,EACP,YAAY,EAAE,kBAAkB,EAChC,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,SAAS,EACb,EAAE,aAAa,+BAgYf;yBAhZe,QAAQ"}
|
package/dist/astryx.css
CHANGED
|
@@ -1317,7 +1317,9 @@
|
|
|
1317
1317
|
.x16khyan:is(:disabled,[aria-disabled="true"]):not(#\#):not(#\#):not(#\#){cursor:default}
|
|
1318
1318
|
.x1y40yee:popover-open:not(#\#):not(#\#):not(#\#){display:flex}
|
|
1319
1319
|
.x1sik18w:not(:first-child):not(#\#):not(#\#):not(#\#){margin-inline-start:var(--_avatar-group-overlap)}
|
|
1320
|
+
.x17xqekc:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){mask-image:linear-gradient(to left,transparent 0%,rgba(0,0,0,.3) 2px,black var(--spacing-1))}
|
|
1320
1321
|
.x1nwey30:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){mask-image:linear-gradient(to left,transparent var(--_tab-strip-bleed),black calc(var(--_tab-strip-bleed) + var(--spacing-8)))}
|
|
1322
|
+
.x8ulde1:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){mask-image:linear-gradient(to right,transparent 0%,rgba(0,0,0,.3) 2px,black var(--spacing-1))}
|
|
1321
1323
|
.x1rxa4r8:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){mask-image:linear-gradient(to right,transparent var(--_tab-strip-bleed),black calc(var(--_tab-strip-bleed) + var(--spacing-8)))}
|
|
1322
1324
|
.x1euntei:focus-within:not(#\#):not(#\#):not(#\#){opacity:1}
|
|
1323
1325
|
.x25t5g8:focus-visible:not(#\#):not(#\#):not(#\#){opacity:1}
|
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.e4f8e4e",
|
|
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.e4f8e4e",
|
|
665
665
|
"@babel/cli": "^8.0.4",
|
|
666
666
|
"@babel/core": "^7.29.7",
|
|
667
667
|
"@babel/preset-react": "^8.0.1",
|
|
@@ -23,6 +23,62 @@ export const docs = {
|
|
|
23
23
|
{guidance: false, description: 'Use the destructive variant without a confirmation step for irreversible actions like deleting data.'},
|
|
24
24
|
{guidance: false, description: 'Use a button for navigation. If it only takes the user to another page, use a link instead. Buttons are for actions like saving, deleting, or submitting.'},
|
|
25
25
|
],
|
|
26
|
+
accessibility: [
|
|
27
|
+
{
|
|
28
|
+
name: 'Text label',
|
|
29
|
+
category: 'Color contrast',
|
|
30
|
+
criterion: '1.4.3 Contrast (Minimum)',
|
|
31
|
+
requirement: '4.5:1',
|
|
32
|
+
states: ['Rest', 'Hover', 'Pointer down'],
|
|
33
|
+
description:
|
|
34
|
+
'Button text must have at least 4.5:1 contrast with the button background in every state. For Hover and Pointer down, measure the final background after the overlay is applied.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Essential icon or spinner arc',
|
|
38
|
+
category: 'Color contrast',
|
|
39
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
40
|
+
requirement: '3:1',
|
|
41
|
+
states: ['Icon only', 'Loading'],
|
|
42
|
+
description:
|
|
43
|
+
'An icon used instead of text must have at least 3:1 contrast with the button background. The moving spinner arc must also meet 3:1. An icon beside a visible label does not need its own check.',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'Badge text',
|
|
47
|
+
category: 'Color contrast',
|
|
48
|
+
criterion: '1.4.3 Contrast (Minimum)',
|
|
49
|
+
requirement: '4.5:1',
|
|
50
|
+
states: ['Rest', 'Hover', 'Pointer down'],
|
|
51
|
+
description:
|
|
52
|
+
'Badge text inside a button must have at least 4.5:1 contrast with the Badge background. Check all 14 built-in Badge colors in Rest, Hover, and Pointer down on page and surface backgrounds. This covers 336 pairs per mode. Check custom end content separately.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Visible control boundary',
|
|
56
|
+
category: 'Color contrast',
|
|
57
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
58
|
+
requirement: '3:1 if needed',
|
|
59
|
+
states: ['Rest'],
|
|
60
|
+
description:
|
|
61
|
+
'The button edge needs 3:1 contrast only when users need it to see the control. A text-only button can rely on its label.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'Keyboard focus indicator',
|
|
65
|
+
category: 'Color contrast',
|
|
66
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
67
|
+
requirement: '3:1',
|
|
68
|
+
states: ['Focus visible'],
|
|
69
|
+
description:
|
|
70
|
+
'The focus outline needs at least 3:1 contrast with the area around the button. Check every style. Destructive buttons use a red outline.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Disabled appearance',
|
|
74
|
+
category: 'Color contrast',
|
|
75
|
+
criterion: '1.4.3 and 1.4.11 exceptions',
|
|
76
|
+
requirement: 'Not required',
|
|
77
|
+
states: ['Disabled'],
|
|
78
|
+
description:
|
|
79
|
+
'Disabled controls do not need to meet these contrast ratios.',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
26
82
|
anatomy: [
|
|
27
83
|
{name: 'Icon', required: false, description: 'A leading icon that reinforces the label, like a trash icon on a Delete button.'},
|
|
28
84
|
{name: 'Label', required: true, description: 'The visible text describing the action. Also used as the accessible name.'},
|
|
@@ -39,6 +39,53 @@ export const docs = {
|
|
|
39
39
|
usage: {
|
|
40
40
|
description:
|
|
41
41
|
'ButtonGroup joins related actions into a single connected control. Use it when multiple buttons represent related choices or operations that belong together visually, like copy/cut/paste, or undo/redo.',
|
|
42
|
+
accessibility: [
|
|
43
|
+
{
|
|
44
|
+
name: 'Text label',
|
|
45
|
+
category: 'Color contrast',
|
|
46
|
+
criterion: '1.4.3 Contrast (Minimum)',
|
|
47
|
+
requirement: '4.5:1',
|
|
48
|
+
states: ['Rest', 'Hover', 'Pointer down'],
|
|
49
|
+
description:
|
|
50
|
+
'Text in each button must have at least 4.5:1 contrast with its background in every state. For Hover and Pointer down, measure the final background after the overlay is applied.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Essential icon or spinner arc',
|
|
54
|
+
category: 'Color contrast',
|
|
55
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
56
|
+
requirement: '3:1',
|
|
57
|
+
states: ['Icon only', 'Loading'],
|
|
58
|
+
description:
|
|
59
|
+
'An icon used instead of text must have at least 3:1 contrast with the button background. The moving spinner arc must also meet 3:1. An icon beside a visible label does not need its own check.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'Visible control boundary',
|
|
63
|
+
category: 'Color contrast',
|
|
64
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
65
|
+
requirement: '3:1 if needed',
|
|
66
|
+
states: ['Rest'],
|
|
67
|
+
description:
|
|
68
|
+
'Some groups need a divider or edge to show each button. That divider or edge must have at least 3:1 contrast.',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'Keyboard focus indicator',
|
|
72
|
+
category: 'Color contrast',
|
|
73
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
74
|
+
requirement: '3:1',
|
|
75
|
+
states: ['Focus visible'],
|
|
76
|
+
description:
|
|
77
|
+
'The focus outline must have at least 3:1 contrast with the area around it.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'Disabled appearance',
|
|
81
|
+
category: 'Color contrast',
|
|
82
|
+
criterion: '1.4.3 and 1.4.11 exceptions',
|
|
83
|
+
requirement: 'Not required',
|
|
84
|
+
states: ['Disabled'],
|
|
85
|
+
description:
|
|
86
|
+
'Disabled controls do not need to meet these contrast ratios.',
|
|
87
|
+
},
|
|
88
|
+
],
|
|
42
89
|
bestPractices: [
|
|
43
90
|
{guidance: true, description: 'Group buttons that perform related actions on the same object, like copy, cut, paste on selected text.'},
|
|
44
91
|
{guidance: true, description: 'Use the same variant for all buttons in a group so they look like a single connected unit.'},
|
|
@@ -850,4 +850,87 @@ describe('Carousel', () => {
|
|
|
850
850
|
expect(scroller).not.toHaveAttribute('data-padding');
|
|
851
851
|
});
|
|
852
852
|
});
|
|
853
|
+
|
|
854
|
+
describe('edge fade', () => {
|
|
855
|
+
function getScroller() {
|
|
856
|
+
const region = screen.getByRole('region');
|
|
857
|
+
return region.firstElementChild as HTMLElement;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function makeOverflowing(el: HTMLElement, scrollLeft: number) {
|
|
861
|
+
Object.defineProperty(el, 'scrollWidth', {
|
|
862
|
+
value: 500,
|
|
863
|
+
configurable: true,
|
|
864
|
+
});
|
|
865
|
+
Object.defineProperty(el, 'clientWidth', {
|
|
866
|
+
value: 200,
|
|
867
|
+
configurable: true,
|
|
868
|
+
});
|
|
869
|
+
Object.defineProperty(el, 'scrollLeft', {
|
|
870
|
+
value: scrollLeft,
|
|
871
|
+
writable: true,
|
|
872
|
+
configurable: true,
|
|
873
|
+
});
|
|
874
|
+
fireEvent.scroll(el);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function injectedCss(): string {
|
|
878
|
+
let out = '';
|
|
879
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
880
|
+
try {
|
|
881
|
+
for (const rule of Array.from(sheet.cssRules)) {
|
|
882
|
+
out += rule.cssText + '\n';
|
|
883
|
+
}
|
|
884
|
+
} catch {
|
|
885
|
+
// Ignore cross-origin sheets.
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return out;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
it('fades the physical left edge when only the start overflows (LTR)', () => {
|
|
892
|
+
render(
|
|
893
|
+
<Carousel aria-label="Fade">
|
|
894
|
+
<div>Item 1</div>
|
|
895
|
+
<div>Item 2</div>
|
|
896
|
+
</Carousel>,
|
|
897
|
+
);
|
|
898
|
+
const scroller = getScroller();
|
|
899
|
+
makeOverflowing(scroller, 300);
|
|
900
|
+
expect(getComputedStyle(scroller).maskImage).toContain(
|
|
901
|
+
'linear-gradient(to right',
|
|
902
|
+
);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('fades the physical right edge when only the end overflows (LTR)', () => {
|
|
906
|
+
render(
|
|
907
|
+
<Carousel aria-label="Fade">
|
|
908
|
+
<div>Item 1</div>
|
|
909
|
+
<div>Item 2</div>
|
|
910
|
+
</Carousel>,
|
|
911
|
+
);
|
|
912
|
+
const scroller = getScroller();
|
|
913
|
+
makeOverflowing(scroller, 0);
|
|
914
|
+
expect(getComputedStyle(scroller).maskImage).toContain(
|
|
915
|
+
'linear-gradient(to left',
|
|
916
|
+
);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('mirrors both single-edge fade gradients under RTL', () => {
|
|
920
|
+
render(
|
|
921
|
+
<Carousel aria-label="Fade">
|
|
922
|
+
<div>Item 1</div>
|
|
923
|
+
<div>Item 2</div>
|
|
924
|
+
</Carousel>,
|
|
925
|
+
);
|
|
926
|
+
const css = injectedCss();
|
|
927
|
+
expect(css).toMatch(
|
|
928
|
+
/:is\(\[dir="rtl"\][^)]*\)[^{]*\{\s*mask-image:\s*linear-gradient\(to left/,
|
|
929
|
+
);
|
|
930
|
+
expect(css).toMatch(
|
|
931
|
+
/:is\(\[dir="rtl"\][^)]*\)[^{]*\{\s*mask-image:\s*linear-gradient\(to right/,
|
|
932
|
+
);
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
853
936
|
});
|
|
@@ -178,10 +178,16 @@ const styles = stylex.create({
|
|
|
178
178
|
maskImage: 'none',
|
|
179
179
|
},
|
|
180
180
|
fadeStart: {
|
|
181
|
-
maskImage:
|
|
181
|
+
maskImage: {
|
|
182
|
+
default: `linear-gradient(to right, transparent 0%, rgba(0,0,0,0.3) 2px, black ${spacingVars['--spacing-1']})`,
|
|
183
|
+
':is([dir="rtl"] *)': `linear-gradient(to left, transparent 0%, rgba(0,0,0,0.3) 2px, black ${spacingVars['--spacing-1']})`,
|
|
184
|
+
},
|
|
182
185
|
},
|
|
183
186
|
fadeEnd: {
|
|
184
|
-
maskImage:
|
|
187
|
+
maskImage: {
|
|
188
|
+
default: `linear-gradient(to left, transparent 0%, rgba(0,0,0,0.3) 2px, black ${spacingVars['--spacing-1']})`,
|
|
189
|
+
':is([dir="rtl"] *)': `linear-gradient(to right, transparent 0%, rgba(0,0,0,0.3) 2px, black ${spacingVars['--spacing-1']})`,
|
|
190
|
+
},
|
|
185
191
|
},
|
|
186
192
|
fadeBoth: {
|
|
187
193
|
maskImage: `linear-gradient(to right, transparent 0%, rgba(0,0,0,0.3) 2px, black ${spacingVars['--spacing-1']}, black calc(100% - ${spacingVars['--spacing-1']}), rgba(0,0,0,0.3) calc(100% - 2px), transparent 100%)`,
|
|
@@ -74,6 +74,44 @@ export const docs = {
|
|
|
74
74
|
|
|
75
75
|
usage: {
|
|
76
76
|
description: 'A button that shows only an icon with no visible text. Use IconButton in toolbars, table rows, and compact UI where space is tight and the icon is universally understood.',
|
|
77
|
+
accessibility: [
|
|
78
|
+
{
|
|
79
|
+
name: 'Essential icon or spinner arc',
|
|
80
|
+
category: 'Color contrast',
|
|
81
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
82
|
+
requirement: '3:1',
|
|
83
|
+
states: ['Rest', 'Hover', 'Pointer down', 'Loading'],
|
|
84
|
+
description:
|
|
85
|
+
'IconButton has no visible label. Its icon must have at least 3:1 contrast with the button background in Rest, Hover, and Pointer down. The moving spinner arc must also meet 3:1 while loading.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'Visible control boundary',
|
|
89
|
+
category: 'Color contrast',
|
|
90
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
91
|
+
requirement: '3:1 if needed',
|
|
92
|
+
states: ['Rest'],
|
|
93
|
+
description:
|
|
94
|
+
'The button edge needs 3:1 contrast only when users need it to see the control.',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'Keyboard focus indicator',
|
|
98
|
+
category: 'Color contrast',
|
|
99
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
100
|
+
requirement: '3:1',
|
|
101
|
+
states: ['Focus visible'],
|
|
102
|
+
description:
|
|
103
|
+
'The focus outline must have at least 3:1 contrast with the area around the button. Check the red outline on destructive buttons too.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'Disabled appearance',
|
|
107
|
+
category: 'Color contrast',
|
|
108
|
+
criterion: '1.4.3 and 1.4.11 exceptions',
|
|
109
|
+
requirement: 'Not required',
|
|
110
|
+
states: ['Disabled'],
|
|
111
|
+
description:
|
|
112
|
+
'Disabled controls do not need to meet these contrast ratios.',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
77
115
|
bestPractices: [
|
|
78
116
|
{ guidance: true, description: 'Make the aria-label specific: a trash icon labeled "Delete conversation" is clearer than just "Delete" for screen readers.' },
|
|
79
117
|
{ guidance: true, description: 'Add a tooltip: even a gear icon can mean Settings, Preferences, or Configure.' },
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 1
|
|
3
|
+
template_version: 1
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:ProgressBar
|
|
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: [public-api, behavior, theming, accessibility]
|
|
13
|
+
verified_by: [packages/core/src/ProgressBar/ProgressBar.test.tsx]
|
|
14
|
+
families: []
|
|
15
|
+
design_specs: []
|
|
16
|
+
architecture:
|
|
17
|
+
[
|
|
18
|
+
architecture:public-component-api,
|
|
19
|
+
architecture:component-theming-surface,
|
|
20
|
+
architecture:theme-authoring-contract,
|
|
21
|
+
architecture:theme-tokens,
|
|
22
|
+
]
|
|
23
|
+
contributing: []
|
|
24
|
+
system_specs: [spec:AST-002/DEC-1, spec:AST-002/DEC-2]
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# ProgressBar component contract
|
|
28
|
+
|
|
29
|
+
## Intent
|
|
30
|
+
|
|
31
|
+
ProgressBar communicates the progress of an operation. A determinate bar must be
|
|
32
|
+
a sufficient standalone visual: people must be able to distinguish completed
|
|
33
|
+
from remaining progress without depending on visible text rendered elsewhere.
|
|
34
|
+
External text may supplement the bar, but it is not a prerequisite for the bar
|
|
35
|
+
to be correct.
|
|
36
|
+
|
|
37
|
+
This draft is intentionally limited to that visual-completeness requirement and
|
|
38
|
+
the resulting public-API boundary. It does not choose a visual treatment.
|
|
39
|
+
|
|
40
|
+
## Compatibility and migration
|
|
41
|
+
|
|
42
|
+
- Released default preserved: `yes`; this draft changes no runtime behavior.
|
|
43
|
+
- Compatibility class: documentation-only draft; no public API is added, removed,
|
|
44
|
+
or changed.
|
|
45
|
+
- Controlled/uncontrolled behavior: unchanged.
|
|
46
|
+
- Migration decision: `component:ProgressBar/DEC-1`.
|
|
47
|
+
|
|
48
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
49
|
+
|
|
50
|
+
## Ownership boundary
|
|
51
|
+
|
|
52
|
+
**Owns**
|
|
53
|
+
|
|
54
|
+
- A sufficient standalone visual distinction between completed and remaining
|
|
55
|
+
progress for every determinate presentation the component provides.
|
|
56
|
+
- Internal resolution of that visual treatment when the caller supplies ordinary
|
|
57
|
+
progress state.
|
|
58
|
+
|
|
59
|
+
**Does not own / non-goals**
|
|
60
|
+
|
|
61
|
+
- External visible labels, values, or descriptions — owned by the product
|
|
62
|
+
callsite and supplementary to the bar.
|
|
63
|
+
- The exact standalone contrast treatment — still a human design decision.
|
|
64
|
+
- Theme token definitions — owned by `architecture:theme-tokens`.
|
|
65
|
+
- Public API that makes visual correctness depend on caller-declared external
|
|
66
|
+
content — governed by `architecture:public-component-api` and `spec:AST-002`.
|
|
67
|
+
|
|
68
|
+
## Public concepts
|
|
69
|
+
|
|
70
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
71
|
+
| ------------------- | ---------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------- | ------------------------------- | ----------------------- | ------------------ | ------------------------------ |
|
|
72
|
+
| Progress mode | `determinate`, `indeterminate` | Whether completed progress is known | All variants | Determinate | `component:ProgressBar` | Stable current API | Existing prop handling applies |
|
|
73
|
+
| Built-in value text | Shown or hidden | Optional visible text supplement generated by ProgressBar | Determinate only | Hidden | `component:ProgressBar` | Stable current API | Ignored when indeterminate |
|
|
74
|
+
| Visual treatment | One sufficient standalone treatment; exact form unresolved | How the graphic distinguishes completed from remaining progress | Determinate progress | Sufficient standalone treatment | `component:ProgressBar` | Draft decision | No incomplete public mode |
|
|
75
|
+
|
|
76
|
+
The current public props remain documented in `ProgressBar.doc.mjs`. This draft
|
|
77
|
+
does not turn nearby external content into a ProgressBar concept.
|
|
78
|
+
|
|
79
|
+
## Behavioral and layout contract
|
|
80
|
+
|
|
81
|
+
Draft requirements identify their basis so observed code is not mistaken for an
|
|
82
|
+
intentional decision.
|
|
83
|
+
|
|
84
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
85
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------------- |
|
|
86
|
+
| FR1 | Every determinate presentation ProgressBar provides MUST be a sufficient standalone visual for distinguishing completed from remaining progress. | `component:ProgressBar/DEC-1`; existing non-text contrast standard | Settled |
|
|
87
|
+
| FR2 | External visible text MAY supplement the bar but MUST NOT be required for the bar's visual correctness. | `component:ProgressBar/DEC-1` | Settled |
|
|
88
|
+
| FR3 | ProgressBar MUST NOT make visual correctness depend on caller-declared external content that it cannot verify or associate. | `component:ProgressBar/DEC-1`; `spec:AST-002` | Settled |
|
|
89
|
+
| FR4 | Supplementary text or graphics MUST NOT substitute for a sufficient distinction between completed and remaining progress. | `component:ProgressBar/DEC-1` | Settled |
|
|
90
|
+
| FR5 | Public API MAY be considered only after the component has a correct base behavior and a stable caller-owned distinction still remains. | `component:ProgressBar/DEC-1`; `spec:AST-002` | Settled admission gate |
|
|
91
|
+
|
|
92
|
+
### Observed current behavior
|
|
93
|
+
|
|
94
|
+
These observations describe `main`; they are not design approval from this
|
|
95
|
+
draft:
|
|
96
|
+
|
|
97
|
+
- Determinate progress renders a semantic-color fill over a muted track.
|
|
98
|
+
- `hasValueLabel` optionally renders formatted value text in the component.
|
|
99
|
+
- Callers may compose other visible text outside the component.
|
|
100
|
+
- The current public theming surface exposes root, fill, track, and mark targets;
|
|
101
|
+
variant state is reflected on the root, fill, and mark targets.
|
|
102
|
+
- Current unit tests cover value semantics, labels, variants, determinate and
|
|
103
|
+
indeterminate modes, disabled rendering, marks, and public target names. They
|
|
104
|
+
do not establish a sufficient standalone completed-versus-remaining visual
|
|
105
|
+
across shipped themes.
|
|
106
|
+
|
|
107
|
+
### Allowed variation
|
|
108
|
+
|
|
109
|
+
- **AV1 — Exact visual treatment.** Shape, token choice, and other rendering
|
|
110
|
+
details may vary if every resulting presentation satisfies FR1.
|
|
111
|
+
- **AV2 — Themes.** Themes may express their visual language through the current
|
|
112
|
+
theming system while preserving the standalone distinction.
|
|
113
|
+
|
|
114
|
+
### Representative states
|
|
115
|
+
|
|
116
|
+
| State | Required invariant | Allowed variation |
|
|
117
|
+
| -------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
118
|
+
| Determinate, partial progress | Completed and remaining progress are distinguishable from the graphic alone | Exact visual treatment and semantic variant |
|
|
119
|
+
| Determinate with built-in value text | The graphic still satisfies FR1; value text supplements it | Value formatting |
|
|
120
|
+
| Determinate with external visible text | The graphic still satisfies FR1 without relying on that text | Callsite composition |
|
|
121
|
+
| Indeterminate | Communicates ongoing activity without claiming a completed amount | Existing animation and theme variation; completed-versus-remaining distinction does not apply |
|
|
122
|
+
|
|
123
|
+
### Transformation and precedence order
|
|
124
|
+
|
|
125
|
+
- **ORD1 — Progress state before presentation.** Resolve determinate or
|
|
126
|
+
indeterminate behavior from component state, then render a treatment valid for
|
|
127
|
+
that mode. External content does not weaken the determinate visual requirement.
|
|
128
|
+
|
|
129
|
+
### Performance and resources
|
|
130
|
+
|
|
131
|
+
- **PR1 — No external-content inspection.** ProgressBar MUST NOT add DOM
|
|
132
|
+
observation, measurement, or relationship discovery to decide whether nearby
|
|
133
|
+
visible text makes a weaker bar acceptable.
|
|
134
|
+
|
|
135
|
+
## Accessibility contract
|
|
136
|
+
|
|
137
|
+
- **AR1 — Standalone non-text distinction.** A determinate bar MUST satisfy the
|
|
138
|
+
applicable non-text contrast requirement without relying on external visible
|
|
139
|
+
text.
|
|
140
|
+
- **AR2 — Programmatic semantics remain present.** The current required
|
|
141
|
+
accessible name and determinate value semantics remain independent of the
|
|
142
|
+
visible treatment.
|
|
143
|
+
- **AR3 — Supplementary text is not inferred.** ProgressBar MUST NOT claim that
|
|
144
|
+
external text is present, visible, equivalent, or correctly associated when it
|
|
145
|
+
cannot encode or verify those facts.
|
|
146
|
+
|
|
147
|
+
## Design relationships
|
|
148
|
+
|
|
149
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
150
|
+
| ------------------------------------ | ------------------------------------------- | ------------------------ | -------------- | ------------------ |
|
|
151
|
+
| Determinate fill and remaining track | Sufficient standalone distinction | Unsettled | Prominent | FR1, AR1 |
|
|
152
|
+
| Built-in or external visible value | Supplements rather than enables correctness | Prescribed | Supporting | FR2, AR3 |
|
|
153
|
+
|
|
154
|
+
The component implements design requirements without copying their rationale.
|
|
155
|
+
The standalone visual remains a human design decision; this contract does not
|
|
156
|
+
invent its form.
|
|
157
|
+
|
|
158
|
+
## Family and system relationships
|
|
159
|
+
|
|
160
|
+
- `architecture:public-component-api` and `spec:AST-002/DEC-1` govern whether a
|
|
161
|
+
caller-owned distinction justifies a public prop.
|
|
162
|
+
- `architecture:component-theming-surface` governs which stable visible parts and
|
|
163
|
+
states become public theme capabilities.
|
|
164
|
+
- `architecture:theme-authoring-contract` governs how themes override those
|
|
165
|
+
component capabilities without creating a second component contract.
|
|
166
|
+
- `architecture:theme-tokens` governs the semantic token vocabulary used by the
|
|
167
|
+
eventual treatment.
|
|
168
|
+
|
|
169
|
+
## Verification map
|
|
170
|
+
|
|
171
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
172
|
+
| ------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------- |
|
|
173
|
+
| FR1, AR1 | Real-browser visual and contrast evidence across shipped themes and color modes | Partial determinate progress for each semantic variant | A completed or remaining segment becomes indistinguishable without text | Future ProgressBar visual audit |
|
|
174
|
+
| FR2, FR3, AR3 | Public type and consumer-doc review | No value text, built-in value text, external composed text | A caller signal is credited with correctness the component cannot verify | Future ProgressBar API tests |
|
|
175
|
+
| AR2 | `ProgressBar.test.tsx` | Determinate, indeterminate, hidden label, custom value text | Accessible name or value semantics disappear when visuals change | Future ProgressBar accessibility audit |
|
|
176
|
+
| Theming | Theme-target metadata checks plus browser evidence | Shipped themes, light and dark modes | A theme override bypasses the standalone distinction | Future ProgressBar theming audit |
|
|
177
|
+
|
|
178
|
+
## Decision log
|
|
179
|
+
|
|
180
|
+
### DEC-1 — ProgressBar owns a sufficient standalone visual
|
|
181
|
+
|
|
182
|
+
**Reference:** `component:ProgressBar/DEC-1`
|
|
183
|
+
**Decider:** `cixzhang`, `2026-08-30`
|
|
184
|
+
|
|
185
|
+
ProgressBar must provide a sufficient standalone visual. Visible text inside or
|
|
186
|
+
outside the component may improve understanding, but the bar remains responsible
|
|
187
|
+
for communicating completed versus remaining progress without it.
|
|
188
|
+
|
|
189
|
+
Public API must not make that correctness depend on external content the
|
|
190
|
+
component cannot carry, associate, or verify. Solve the component behavior first.
|
|
191
|
+
Consider new API only when a stable caller-owned distinction still remains after
|
|
192
|
+
the base behavior is correct.
|
|
193
|
+
|
|
194
|
+
Rejected: weakening the bar based on a caller claim about nearby content. That
|
|
195
|
+
would move component correctness into an external condition the component cannot
|
|
196
|
+
verify.
|
|
197
|
+
|
|
198
|
+
## Open questions
|
|
199
|
+
|
|
200
|
+
- **OQ1 — What exact visual treatment gives every determinate presentation a
|
|
201
|
+
sufficient standalone completed-versus-remaining distinction across supported
|
|
202
|
+
variants, themes, and color modes?** (`human-design`)
|
|
203
|
+
|
|
204
|
+
## Content boundary
|
|
205
|
+
|
|
206
|
+
This file does not duplicate consumer prop tables or examples, current audit
|
|
207
|
+
results, implementation steps, exact visual values, or system theming rules. It
|
|
208
|
+
links to their owners.
|
|
@@ -98,6 +98,62 @@ export const docs = {
|
|
|
98
98
|
usage: {
|
|
99
99
|
description:
|
|
100
100
|
'A segmented button group that allows users to make a single selection from a small set of mutually exclusive options. Use SegmentedControl when all options should be visible at once and the selection controls a value or mode, not page navigation.',
|
|
101
|
+
accessibility: [
|
|
102
|
+
{
|
|
103
|
+
name: 'Text label',
|
|
104
|
+
category: 'Color contrast',
|
|
105
|
+
criterion: '1.4.3 Contrast (Minimum)',
|
|
106
|
+
requirement: '4.5:1',
|
|
107
|
+
states: ['Rest', 'Hover', 'Selected'],
|
|
108
|
+
description:
|
|
109
|
+
'Each label must have at least 4.5:1 contrast with its segment background. Check unselected, Hover, and selected colors as they appear on screen.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'Essential icon',
|
|
113
|
+
category: 'Color contrast',
|
|
114
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
115
|
+
requirement: '3:1',
|
|
116
|
+
states: ['Icon only'],
|
|
117
|
+
description:
|
|
118
|
+
'When a segment has no visible label, its icon must have at least 3:1 contrast with the segment background. An icon beside a visible label does not need its own check.',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'Selected state indicator',
|
|
122
|
+
category: 'Color contrast',
|
|
123
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
124
|
+
requirement: '3:1 if relied upon',
|
|
125
|
+
states: ['Selected'],
|
|
126
|
+
description:
|
|
127
|
+
'The selected background must reach 3:1 only when users need it to tell selected from unselected. Label color and weight also show selection.',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'Visible control boundary',
|
|
131
|
+
category: 'Color contrast',
|
|
132
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
133
|
+
requirement: '3:1 if needed',
|
|
134
|
+
states: ['Rest'],
|
|
135
|
+
description:
|
|
136
|
+
'The control edge or segment borders need at least 3:1 contrast when users need them to see the choices.',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'Keyboard focus indicator',
|
|
140
|
+
category: 'Color contrast',
|
|
141
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
142
|
+
requirement: '3:1',
|
|
143
|
+
states: ['Focus visible'],
|
|
144
|
+
description:
|
|
145
|
+
'The focus outline must have at least 3:1 contrast with the area around the segment. Check it on the track and selected background.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'Disabled appearance',
|
|
149
|
+
category: 'Color contrast',
|
|
150
|
+
criterion: '1.4.3 and 1.4.11 exceptions',
|
|
151
|
+
requirement: 'Not required',
|
|
152
|
+
states: ['Disabled'],
|
|
153
|
+
description:
|
|
154
|
+
'Disabled controls do not need to meet these contrast ratios.',
|
|
155
|
+
},
|
|
156
|
+
],
|
|
101
157
|
bestPractices: [
|
|
102
158
|
{guidance: true, description: 'Use for switching between 2–5 mutually exclusive views or modes where all options should be visible.'},
|
|
103
159
|
{guidance: true, description: 'Provide a descriptive label for the control to ensure the group is accessible to screen readers.'},
|
|
@@ -122,6 +122,62 @@ export const docs = {
|
|
|
122
122
|
usage: {
|
|
123
123
|
description:
|
|
124
124
|
'ToggleButton switches between selected and unselected states to represent a persistent on/off choice. Use it standalone for binary actions like bold, mute, or favorite, or inside a ToggleButtonGroup for single-select or multi-select toolbar controls.',
|
|
125
|
+
accessibility: [
|
|
126
|
+
{
|
|
127
|
+
name: 'Text label',
|
|
128
|
+
category: 'Color contrast',
|
|
129
|
+
criterion: '1.4.3 Contrast (Minimum)',
|
|
130
|
+
requirement: '4.5:1',
|
|
131
|
+
states: ['Unselected', 'Selected', 'Hover', 'Pointer down'],
|
|
132
|
+
description:
|
|
133
|
+
'The label must have at least 4.5:1 contrast with the button background when selected and unselected. For Hover and Pointer down, measure the final background after the overlay is applied.',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'Essential icon or spinner arc',
|
|
137
|
+
category: 'Color contrast',
|
|
138
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
139
|
+
requirement: '3:1',
|
|
140
|
+
states: ['Icon only', 'Loading'],
|
|
141
|
+
description:
|
|
142
|
+
'An icon-only ToggleButton must have at least 3:1 contrast between its icon and button background. The moving spinner arc must also meet 3:1. An icon beside a visible label does not need its own check.',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'Selected state indicator',
|
|
146
|
+
category: 'Color contrast',
|
|
147
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
148
|
+
requirement: '3:1 if relied upon',
|
|
149
|
+
states: ['Selected'],
|
|
150
|
+
description:
|
|
151
|
+
'The selected background must reach 3:1 only when users need it to tell selected from unselected. Label weight or a changed icon can also show selection.',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'Visible control boundary',
|
|
155
|
+
category: 'Color contrast',
|
|
156
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
157
|
+
requirement: '3:1 if needed',
|
|
158
|
+
states: ['Rest'],
|
|
159
|
+
description:
|
|
160
|
+
'The button edge needs 3:1 contrast only when users need it to see the control. A visible label or icon can show the control instead.',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'Keyboard focus indicator',
|
|
164
|
+
category: 'Color contrast',
|
|
165
|
+
criterion: '1.4.11 Non-text Contrast',
|
|
166
|
+
requirement: '3:1',
|
|
167
|
+
states: ['Focus visible'],
|
|
168
|
+
description:
|
|
169
|
+
'The focus outline must have at least 3:1 contrast with the area around the button. Check both selected and unselected states.',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'Disabled appearance',
|
|
173
|
+
category: 'Color contrast',
|
|
174
|
+
criterion: '1.4.3 and 1.4.11 exceptions',
|
|
175
|
+
requirement: 'Not required',
|
|
176
|
+
states: ['Disabled'],
|
|
177
|
+
description:
|
|
178
|
+
'Disabled controls do not need to meet these contrast ratios.',
|
|
179
|
+
},
|
|
180
|
+
],
|
|
125
181
|
bestPractices: [
|
|
126
182
|
{guidance: true, description: 'Use a filled or colored icon for the pressed state so users can see the current state at a glance: an outline star vs a solid star, for example.'},
|
|
127
183
|
{guidance: true, description: 'Keep the label identical between pressed and unpressed states. Let the visual treatment (icon, weight, background) communicate the change.'},
|
|
@@ -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);
|