@astryxdesign/core 0.5.2-canary.2da7a39 → 0.5.2-canary.3828a31

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.
@@ -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;AAyJD;;;;;;;;;;;;;;;;;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"}
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"}
@@ -69,11 +69,11 @@ const styles = {
69
69
  $$css: true
70
70
  },
71
71
  fadeStart: {
72
- kX1K2I: "x1ujl4f5",
72
+ kX1K2I: "x1ujl4f5 x17xqekc",
73
73
  $$css: true
74
74
  },
75
75
  fadeEnd: {
76
- kX1K2I: "xtdd0ob",
76
+ kX1K2I: "xtdd0ob x8ulde1",
77
77
  $$css: true
78
78
  },
79
79
  fadeBoth: {
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.2da7a39",
3
+ "version": "0.5.2-canary.3828a31",
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.2da7a39",
664
+ "@astryxdesign/cli": "0.5.2-canary.3828a31",
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: `linear-gradient(to right, transparent 0%, rgba(0,0,0,0.3) 2px, black ${spacingVars['--spacing-1']})`,
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: `linear-gradient(to left, transparent 0%, rgba(0,0,0,0.3) 2px, black ${spacingVars['--spacing-1']})`,
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%)`,
@@ -1,5 +1,74 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Dialog',
7
+ required: true,
8
+ description: 'Modal surface that contains the command palette.',
9
+ },
10
+ {
11
+ name: 'Input',
12
+ required: true,
13
+ description:
14
+ 'Search region containing the query field and its supporting visuals.',
15
+ },
16
+ {
17
+ name: 'Search glyph',
18
+ required: true,
19
+ description: 'Search symbol rendered by Icon in the default Input.',
20
+ },
21
+ {
22
+ name: 'Query field',
23
+ required: true,
24
+ description: 'Native text field used to enter a search query.',
25
+ },
26
+ {
27
+ name: 'Loading spinner',
28
+ required: false,
29
+ description:
30
+ 'Spinner shown in the default Input while a search is pending.',
31
+ },
32
+ {
33
+ name: 'List',
34
+ required: true,
35
+ description:
36
+ 'Scrollable listbox containing the current results or Empty state.',
37
+ },
38
+ {
39
+ name: 'Item',
40
+ required: false,
41
+ description: 'Selectable command result rendered inside the List.',
42
+ },
43
+ {
44
+ name: 'Group',
45
+ required: false,
46
+ description: 'Optional collection of Items that share a heading.',
47
+ },
48
+ {
49
+ name: 'Group heading',
50
+ required: false,
51
+ description: 'Visible heading rendered for a Group.',
52
+ },
53
+ {
54
+ name: 'Empty',
55
+ required: false,
56
+ description: 'Message shown when the current result set is empty.',
57
+ },
58
+ {
59
+ name: 'Footer',
60
+ required: false,
61
+ description:
62
+ 'Footer region for default keyboard guidance or caller-provided content.',
63
+ },
64
+ {
65
+ name: 'Keyboard shortcut',
66
+ required: false,
67
+ description:
68
+ 'Painted key badges rendered by Kbd in the default Footer guidance.',
69
+ },
70
+ ];
71
+
3
72
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
73
  export const docs = {
5
74
  name: 'CommandPalette',
@@ -152,6 +221,7 @@ export const docs = {
152
221
  ],
153
222
  },
154
223
  usage: {
224
+ anatomy,
155
225
  description: 'CommandPalette is a searchable dialog for quick access to commands, navigation, and actions. Use it as a keyboard-driven launcher powered by SearchSource for filtering and selection.',
156
226
  bestPractices: [
157
227
  { guidance: true, description: 'Provide a searchSource with bootstrap results so users see useful options before typing.' },
@@ -165,6 +235,7 @@ export const docs = {
165
235
  /** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
166
236
  export const docsZh = {
167
237
  usage: {
238
+ anatomy,
168
239
  description: 'CommandPalette is a searchable dialog for quick access to commands, navigation, and actions. Use it as a keyboard-driven launcher powered by SearchSource for filtering and selection.',
169
240
  bestPractices: [
170
241
  { guidance: true, description: 'Provide a searchSource with bootstrap results so users see useful options before typing.' },
@@ -180,6 +251,7 @@ export const docsDense = {
180
251
  description:
181
252
  'searchSource-driven command palette dialog; filtering, keyboard nav, grouping, selection; same SearchSource interface as Typeahead',
182
253
  usage: {
254
+ anatomy,
183
255
  description: 'CommandPalette is a searchable dialog for quick access to commands, navigation, and actions. Use it as a keyboard-driven launcher powered by SearchSource for filtering and selection.',
184
256
  bestPractices: [
185
257
  { guidance: true, description: 'Provide a searchSource with bootstrap results so users see useful options before typing.' },
@@ -0,0 +1,223 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 3
4
+ kind: component
5
+ id: component:CommandPalette
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers: [behavior, theming, accessibility]
13
+ verified_by:
14
+ [
15
+ packages/core/src/CommandPalette/CommandPalette.test.tsx,
16
+ packages/core/src/CommandPalette/CommandPaletteInput.test.tsx,
17
+ packages/core/src/CommandPalette/CommandPaletteList.test.tsx,
18
+ packages/core/src/CommandPalette/CommandPaletteItem.test.tsx,
19
+ packages/core/src/CommandPalette/CommandPaletteGroup.test.tsx,
20
+ packages/core/src/CommandPalette/CommandPaletteFooter.test.tsx,
21
+ packages/core/src/theme/themingTargets.test.ts,
22
+ scripts/check-knowledge.mjs,
23
+ ]
24
+ families: [family:overlay-dismissal]
25
+ design_specs: []
26
+ architecture:
27
+ [
28
+ architecture:component-theming-surface,
29
+ architecture:interaction-modality,
30
+ architecture:public-component-api,
31
+ ]
32
+ contributing: []
33
+ system_specs: []
34
+ ---
35
+
36
+ # CommandPalette component contract
37
+
38
+ ## Intent
39
+
40
+ CommandPalette presents searchable commands inside a Dialog. This draft records
41
+ its current aggregate consumer anatomy, target ownership, and delegated stable
42
+ parts without changing runtime behavior, styling, targets, or public API.
43
+
44
+ ## Compatibility and migration
45
+
46
+ - Released default preserved: `yes`
47
+ - Compatibility class: additive documentation only; runtime, DOM, styling,
48
+ targets, aliases, and public API remain unchanged
49
+ - Controlled/uncontrolled behavior: unchanged
50
+ - Migration decision: none
51
+
52
+ Consumer migration instructions belong in consumer docs and release notes.
53
+
54
+ ## Ownership boundary
55
+
56
+ **Owns**
57
+
58
+ - Search orchestration and the current Input, List, Item, Group, Group heading,
59
+ Empty, and Footer parts and targets.
60
+ - The native Query field rendered inside the default Input.
61
+
62
+ **Does not own / non-goals**
63
+
64
+ - Dialog's surface, Icon's glyph, Spinner's loading indicator, or Kbd's shortcut
65
+ badges — owned by their respective components.
66
+ - Caller-provided replacement Input, Footer, item content, or trailing input
67
+ content — owned by the product callsite.
68
+ - A `command-palette` root target — no such current target exists, and this
69
+ factual backfill does not invent one.
70
+ - Shared layer hosting, positioning, or lifecycle rules beyond the current
71
+ records linked below.
72
+
73
+ ## Public concepts
74
+
75
+ No new public concept is introduced. Consumer props, slots, and usage remain
76
+ documented in `CommandPalette.doc.mjs` and its subcomponent docs.
77
+
78
+ ## Behavioral and layout contract
79
+
80
+ | ID | Candidate invariant | Basis | Draft review state |
81
+ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------- |
82
+ | FR1 | The current default render places Input and Footer around a List inside a delegated Dialog; the List contains Items, optional Groups and Group headings, or Empty according to the current results. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
83
+ | FR2 | Input, List, Item, Group, Group heading, Empty, and Footer carry the seven current `command-palette-*` targets documented below; no `command-palette` root target exists. | Current source and target docs | Verified current inventory; focused placement coverage is partial |
84
+ | FR3 | The default Input delegates its Search glyph to Icon and pending Loading spinner to Spinner; the default Footer delegates keyboard shortcuts to Kbd; the containing surface delegates to Dialog. | Current source and component docs | Verified current composition; no ownership change |
85
+ | FR4 | Query field is a distinct native text field inside Input and currently has no separate public target. The `command-palette-input` target is on the surrounding search region, not the native field. | Current source and target docs | Verified current reachability; long-term theming intent remains unsettled |
86
+
87
+ ### Allowed variation
88
+
89
+ - **AV1 — Results.** Item count, grouping, selected content, and empty state may
90
+ vary without changing the aggregate anatomy.
91
+ - **AV2 — Slots.** Caller-provided Input and Footer content may replace the
92
+ defaults without becoming CommandPalette-owned subparts.
93
+ - **AV3 — Delegated rendering.** Dialog, Icon, Spinner, and Kbd may change their
94
+ internal element shape while preserving their own public contracts.
95
+
96
+ ### Representative states
97
+
98
+ | State | Required invariant | Allowed variation |
99
+ | ---------------------- | ------------------------------------------------------- | ----------------------------------------------- |
100
+ | Ungrouped results | List contains Item instances. | Item count and caller-rendered content. |
101
+ | Grouped results | List contains Group, Group heading, and Item instances. | Group names, count, ordering, and item content. |
102
+ | Empty bootstrap/search | List contains Empty instead of Items. | Caller-provided empty content. |
103
+ | Pending search | Default Input may contain Loading spinner. | Spinner is absent when no search is pending. |
104
+ | Default slots | Input and Footer render their documented defaults. | Footer shortcut text and translated labels. |
105
+ | Caller-replaced slots | The slot content replaces the corresponding default. | Replacement structure remains caller-owned. |
106
+
107
+ ### Transformation and precedence order
108
+
109
+ - No new search, selection, layout, or styling precedence rule is introduced.
110
+
111
+ ### Performance and resources
112
+
113
+ - No new performance or resource rule is introduced.
114
+
115
+ ## Accessibility contract
116
+
117
+ This draft does not change or extend CommandPalette's current Dialog, combobox,
118
+ listbox, option, announcement, or keyboard behavior.
119
+
120
+ ## Design relationships
121
+
122
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
123
+ | ----------------- | ----------------------------------------------------------------------- | ------------------------------ | -------------- | ------------------ |
124
+ | Dialog | Presents the containing modal surface. | `component:Dialog` | Supporting | FR1, FR3 |
125
+ | Input | Paints the search region around the query field and supporting visuals. | Current source and public docs | Prominent | FR1, FR2, FR4 |
126
+ | Search glyph | Presents the search symbol in the default Input. | `component:Icon` | Supporting | FR3 |
127
+ | Query field | Accepts the native text query inside Input. | Current source and public docs | Prominent | FR1, FR4 |
128
+ | Loading spinner | Indicates a pending search in the default Input. | `component:Spinner` | Supporting | FR3 |
129
+ | List | Presents the current result collection or Empty state. | Current source and public docs | Supporting | FR1, FR2 |
130
+ | Item | Presents one selectable command result. | Current source and public docs | Prominent | FR1, FR2 |
131
+ | Group | Arranges related Items together. | Current source and public docs | Supporting | FR1, FR2 |
132
+ | Group heading | Labels one Group visually. | Current source and public docs | Supporting | FR1, FR2 |
133
+ | Empty | Presents the no-results or no-query message. | Current source and public docs | Prominent | FR1, FR2 |
134
+ | Footer | Presents default guidance or caller-provided footer content. | Current source and public docs | Supporting | FR1, FR2 |
135
+ | Keyboard shortcut | Presents one shortcut as painted key badges in the default Footer. | `component:Kbd` | Supporting | FR3 |
136
+
137
+ The native Query field is separate from the Input region that carries
138
+ `command-palette-input`. Its current lack of a direct target is observed
139
+ reachability, not a decision that it must remain unthemeable.
140
+
141
+ ### Theming anatomy
142
+
143
+ <!-- anatomy-theming:v1 -->
144
+
145
+ ```json
146
+ {
147
+ "Dialog": {
148
+ "delegatesTo": {"owner": "component:Dialog", "target": "dialog"}
149
+ },
150
+ "Input": {"target": "command-palette-input"},
151
+ "Search glyph": {
152
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
153
+ },
154
+ "Query field": {
155
+ "none": {
156
+ "reason": "unsettled: No current public target is applied directly to the native query field; future exposure still needs an owner decision"
157
+ }
158
+ },
159
+ "Loading spinner": {
160
+ "delegatesTo": {"owner": "component:Spinner", "target": "spinner"}
161
+ },
162
+ "List": {"target": "command-palette-list"},
163
+ "Item": {"target": "command-palette-item"},
164
+ "Group": {"target": "command-palette-group"},
165
+ "Group heading": {"target": "command-palette-group-heading"},
166
+ "Empty": {"target": "command-palette-empty"},
167
+ "Footer": {"target": "command-palette-footer"},
168
+ "Keyboard shortcut": {
169
+ "delegatesTo": {"owner": "component:Kbd", "target": "kbd"}
170
+ }
171
+ }
172
+ ```
173
+
174
+ The seven local targets are current public seams. The delegated parts retain
175
+ their existing component owners. The Query field `none` disposition records a
176
+ current audit gap and does not authorize a new target.
177
+
178
+ ## Family and system relationships
179
+
180
+ - `architecture:component-theming-surface` owns anatomy qualification, target
181
+ mapping, delegation, and factual `none` dispositions.
182
+ - `architecture:public-component-api` owns the stable props and composition
183
+ surface; this documentation adds no API.
184
+ - `architecture:interaction-modality` owns shared keyboard and pointer modality;
185
+ this draft does not redefine focus ownership or input behavior.
186
+ - `family:overlay-dismissal` identifies CommandPalette as a current member and
187
+ records its component-local Escape handling as an adoption gap.
188
+ - No layer-runtime record is linked because no current record with that scope is
189
+ present in this checkout.
190
+
191
+ ## Verification map
192
+
193
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
194
+ | ------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
195
+ | FR1 | CommandPalette root and subcomponent render suites plus source inspection | Default, grouped, empty, pending, and replaced slots | Removing the asserted Dialog, Query field, List, Item, Group, Group heading, Empty, or Footer content fails existing role, content, or slot assertions; Search glyph, Loading spinner, and Keyboard shortcut presence remain source-inspected only. | `audit:CommandPalette/anatomy` |
196
+ | FR2 | Source inspection, `themingTargets.test.ts`, and `CommandPaletteGroup.test.tsx` | Seven local targets; Group and Group heading placement | Removing a current target fails the global inventory; moving Group or Group heading fails focused class assertions. | `audit:CommandPalette/theming` |
197
+ | FR3 | Source inspection plus Dialog, Icon, Spinner, and Kbd public target metadata | Default surface, Input visuals, and Footer shortcuts | Existing focused tests do not assert the composed Icon, Spinner, or Kbd instances; changing delegated ownership requires this map and the delegated component metadata to change. | `audit:CommandPalette/theming` |
198
+ | FR4 | `CommandPaletteInput.test.tsx` and source inspection | Native query field with idle and pending Input | Removing the field fails combobox tests; adding a field target requires an explicit map update. | `audit:CommandPalette/anatomy` |
199
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and seven current local targets | Missing, extra, prefixed, stale, or alias-backed mappings fail repository validation. | `audit:CommandPalette/theming` |
200
+
201
+ Focused target-placement assertions currently cover Group and Group heading.
202
+ Input, List, Item, Empty, and Footer placement rely on source inspection plus the
203
+ global target inventory. Existing tests assert default Footer text and pending
204
+ announcements, but do not assert that Icon, Spinner, or Kbd renders.
205
+
206
+ ## Decision log
207
+
208
+ None. This draft records current facts and introduces no component-local design,
209
+ layer, API, or theming decision.
210
+
211
+ ## Open questions
212
+
213
+ - **OQ1 — Should Query field gain a stable public theming target?** (`human-api`)
214
+ Its current lack of direct reachability is an audit gap, not settled intent.
215
+ - **OQ2 — Should focused tests pin the default Search glyph, pending Loading
216
+ spinner, and default Footer Keyboard shortcuts?** (`checkable`) Their presence
217
+ is currently source-inspected rather than asserted.
218
+
219
+ ## Content boundary
220
+
221
+ This file does not duplicate consumer prop tables/examples, shared layer or
222
+ modality rules, current audit results, or implementation steps. It links to
223
+ their owners.
@@ -1,5 +1,24 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Message box',
7
+ required: true,
8
+ description: 'Painted container for the validation feedback.',
9
+ },
10
+ {
11
+ name: 'Detached icon',
12
+ required: false,
13
+ description: 'Leading status glyph shown only by the detached variant.',
14
+ },
15
+ {
16
+ name: 'Message text',
17
+ required: true,
18
+ description: 'Text describing the validation status.',
19
+ },
20
+ ];
21
+
3
22
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
23
  export const docs = {
5
24
  name: 'FieldStatus',
@@ -61,6 +80,7 @@ export const docs = {
61
80
  'Use FieldStatus for general alerts or page-level notices; use Banner or Toast instead.',
62
81
  },
63
82
  ],
83
+ anatomy,
64
84
  },
65
85
  };
66
86
 
@@ -86,6 +106,7 @@ export const docsDense = {
86
106
  'Use FieldStatus for general alerts or page-level notices; use Banner or Toast instead.',
87
107
  },
88
108
  ],
109
+ anatomy,
89
110
  },
90
111
  propDescriptions: {
91
112
  type: 'error/warning/success status tone',