@astryxdesign/core 0.7.2-canary.081ccea → 0.7.2-canary.085e145
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/README.md +4 -4
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/FormLayout/FormLayout.d.ts +5 -4
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayoutContext.d.ts +3 -3
- package/dist/FormLayout/FormLayoutContext.js +3 -3
- package/dist/Grid/Grid.js +1 -1
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +5 -0
- package/dist/Token/Token.js +1 -1
- package/dist/astryx.css +1 -1
- package/docs.mjs +4 -4
- package/package.json +3 -3
- package/scripts/agent-doc-state.mjs +29 -7
- package/scripts/postinstall.mjs +43 -2
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
- package/src/FormLayout/FormLayout.doc.mjs +6 -3
- package/src/FormLayout/FormLayout.spec.md +192 -0
- package/src/FormLayout/FormLayout.tsx +5 -4
- package/src/FormLayout/FormLayoutContext.ts +3 -3
- package/src/Grid/Grid.test.tsx +22 -0
- package/src/Grid/Grid.tsx +2 -2
- package/src/Indicator/Indicator.doc.mjs +3 -3
- package/src/PowerSearch/PowerSearch.test.tsx +62 -0
- package/src/PowerSearch/PowerSearch.tsx +9 -0
- package/src/Token/Token.tsx +1 -1
- package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +86 -0
- package/src/postinstall.test.mjs +33 -0
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ Core UI components, theme system, and utilities for the Astryx design system. Fo
|
|
|
6
6
|
>
|
|
7
7
|
> ```bash
|
|
8
8
|
> npm install -D @astryxdesign/cli # or: pnpm add -D / yarn add -D / bun add -d
|
|
9
|
-
> npx
|
|
9
|
+
> npx @astryxdesign/cli init # the CLI you just installed
|
|
10
10
|
> ```
|
|
11
11
|
>
|
|
12
|
-
> `init` writes the Astryx component index into your `AGENTS.md`/`CLAUDE.md` so your agent discovers components, templates, and design tokens instead of guessing. Need a single command without installing?
|
|
12
|
+
> `init` writes the Astryx component index into your `AGENTS.md`/`CLAUDE.md` so your agent discovers components, templates, and design tokens instead of guessing. Need a single command without installing? The same scoped command works: `npx @astryxdesign/cli <cmd>`. Always use the scoped name: the unscoped `astryx` resolves to an unrelated npm package until `@astryxdesign/cli` is a dependency. See [XDS CLI](#xds-cli).
|
|
13
13
|
|
|
14
14
|
## Component Docs
|
|
15
15
|
|
|
@@ -21,7 +21,7 @@ npx @astryxdesign/cli component Button # full docs for a component
|
|
|
21
21
|
npx @astryxdesign/cli component --list # list all components
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
> Use the scoped `@astryxdesign/cli
|
|
24
|
+
> Use the scoped `@astryxdesign/cli`: it runs with or without installing, while the unscoped `astryx` resolves to an unrelated npm package until the CLI is a dependency.
|
|
25
25
|
|
|
26
26
|
## Page Layouts
|
|
27
27
|
|
|
@@ -57,7 +57,7 @@ astryx discover # discover external Astryx packages
|
|
|
57
57
|
astryx gap-report # report a missing capability
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
>
|
|
60
|
+
> Run these as `npx @astryxdesign/cli …`, or `pnpm exec astryx …` (or your package manager's equivalent) once the CLI is installed.
|
|
61
61
|
|
|
62
62
|
## Related Packages
|
|
63
63
|
|
|
@@ -35,7 +35,7 @@ export interface DropdownMenuCheckboxItemProps extends Omit<BaseProps, 'onChange
|
|
|
35
35
|
description?: ReactNode;
|
|
36
36
|
/**
|
|
37
37
|
* Icon to display before the label. Accepts a semantic icon name (see
|
|
38
|
-
* `
|
|
38
|
+
* `astryx docs icons`) or a rendered node.
|
|
39
39
|
*/
|
|
40
40
|
icon?: ReactNode | IconType;
|
|
41
41
|
/**
|
|
@@ -35,7 +35,7 @@ export interface DropdownMenuRadioItemProps extends Omit<BaseProps, 'role' | 'ar
|
|
|
35
35
|
description?: ReactNode;
|
|
36
36
|
/**
|
|
37
37
|
* Icon to display before the label. Accepts a semantic icon name (see
|
|
38
|
-
* `
|
|
38
|
+
* `astryx docs icons`) or a rendered node.
|
|
39
39
|
*/
|
|
40
40
|
icon?: ReactNode | IconType;
|
|
41
41
|
/**
|
|
@@ -28,10 +28,11 @@ export interface FormLayoutProps extends BaseProps<HTMLDivElement> {
|
|
|
28
28
|
* Direction of field arrangement.
|
|
29
29
|
*
|
|
30
30
|
* - `'vertical'` — Fields stack top-to-bottom (default). Most common.
|
|
31
|
-
* - `'horizontal'` — Fields
|
|
32
|
-
*
|
|
33
|
-
* - `'horizontal-labels'` — CSS Grid with labels
|
|
34
|
-
* Collapses to vertical when the
|
|
31
|
+
* - `'horizontal'` — Fields occupy one `1fr` CSS Grid track per child in
|
|
32
|
+
* logical inline order.
|
|
33
|
+
* - `'horizontal-labels'` — CSS Grid with labels beside their inputs on the
|
|
34
|
+
* inline-start side. Collapses to vertical when the viewport is narrow
|
|
35
|
+
* (≤480px).
|
|
35
36
|
*
|
|
36
37
|
* @default 'vertical'
|
|
37
38
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormLayout.d.ts","sourceRoot":"","sources":["../../src/FormLayout/FormLayout.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAG5C,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AA2C7B,MAAM,WAAW,eAAgB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAChE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB
|
|
1
|
+
{"version":3,"file":"FormLayout.d.ts","sourceRoot":"","sources":["../../src/FormLayout/FormLayout.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAG5C,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AA2C7B,MAAM,WAAW,eAAgB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAChE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAEhC;;;;;;;;;;;;;;;;;OAiBG;IACH,kBAAkB,CAAC,EAAE,eAAe,CAAC;CACtC;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,SAAsB,EACtB,kBAAkB,EAClB,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,eAAe,+BA0BjB;yBAnCe,UAAU"}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Direction of form field arrangement.
|
|
3
3
|
*
|
|
4
4
|
* - `'vertical'` — Fields stack top-to-bottom (default). Most common.
|
|
5
|
-
* - `'horizontal'` — Fields
|
|
6
|
-
* - `'horizontal-labels'` — Fields stack vertically
|
|
7
|
-
*
|
|
5
|
+
* - `'horizontal'` — Fields occupy one `1fr` grid track per child in logical inline order.
|
|
6
|
+
* - `'horizontal-labels'` — Fields stack vertically while labels sit beside
|
|
7
|
+
* their inputs on the inline-start side (settings/admin panel pattern).
|
|
8
8
|
*/
|
|
9
9
|
export type FormLayoutDirection = 'vertical' | 'horizontal' | 'horizontal-labels';
|
|
10
10
|
/**
|
|
@@ -19,9 +19,9 @@ import { createLayerScopedContext as createContext } from "../Layer/layerScopedC
|
|
|
19
19
|
* Direction of form field arrangement.
|
|
20
20
|
*
|
|
21
21
|
* - `'vertical'` — Fields stack top-to-bottom (default). Most common.
|
|
22
|
-
* - `'horizontal'` — Fields
|
|
23
|
-
* - `'horizontal-labels'` — Fields stack vertically
|
|
24
|
-
*
|
|
22
|
+
* - `'horizontal'` — Fields occupy one `1fr` grid track per child in logical inline order.
|
|
23
|
+
* - `'horizontal-labels'` — Fields stack vertically while labels sit beside
|
|
24
|
+
* their inputs on the inline-start side (settings/admin panel pattern).
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
/**
|
package/dist/Grid/Grid.js
CHANGED
|
@@ -368,6 +368,7 @@ export function Grid({
|
|
|
368
368
|
const columnsVariant = typeof columns === 'number' ? columns : typeof columns === 'object' ? undefined : undefined;
|
|
369
369
|
return /*#__PURE__*/_jsx("div", {
|
|
370
370
|
ref: ref,
|
|
371
|
+
...props,
|
|
371
372
|
...mergeProps(themeProps('grid', {
|
|
372
373
|
columns: columnsVariant,
|
|
373
374
|
gap,
|
|
@@ -377,7 +378,6 @@ export function Grid({
|
|
|
377
378
|
...style,
|
|
378
379
|
...inlineStyle
|
|
379
380
|
}),
|
|
380
|
-
...props,
|
|
381
381
|
children: children
|
|
382
382
|
});
|
|
383
383
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PowerSearch.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearch.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"PowerSearch.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearch.tsx"],"names":[],"mappings":"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"}
|
|
@@ -661,6 +661,11 @@ export function PowerSearch({
|
|
|
661
661
|
size: size,
|
|
662
662
|
icon: tokenIcon,
|
|
663
663
|
endContent: valueContent,
|
|
664
|
+
onMouseDown: handleClick ? e => {
|
|
665
|
+
// Keep a token press from entering Tokenizer's input-focus
|
|
666
|
+
// redirect. Click still edits/removes; Tab entry is unchanged.
|
|
667
|
+
e.preventDefault();
|
|
668
|
+
} : undefined,
|
|
664
669
|
onClick: handleClick ? e => {
|
|
665
670
|
e.stopPropagation();
|
|
666
671
|
handleClick();
|
package/dist/Token/Token.js
CHANGED
package/dist/astryx.css
CHANGED
|
@@ -2077,7 +2077,7 @@
|
|
|
2077
2077
|
.x1bev2v5[data-astryx-press="on"]:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#){background-image:none}
|
|
2078
2078
|
.x524tya[data-astryx-press="fading"]:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#){background-image:none}
|
|
2079
2079
|
@starting-style{.x1sopeo2.x1sopeo2:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#){grid-template-rows:0fr}}
|
|
2080
|
-
.
|
|
2080
|
+
.xramuyw:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{inset:-4px}
|
|
2081
2081
|
.xarstr8:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{inset:0}
|
|
2082
2082
|
.xxx281p:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::before{inset:0}
|
|
2083
2083
|
.xht581f:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{inset:var(--_input-clear-hit-inset)}
|
package/docs.mjs
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* ==========================================================================
|
|
14
14
|
* AI AGENT: do NOT guess component APIs or hand-write CSS. Run:
|
|
15
15
|
* npx @astryxdesign/cli init
|
|
16
|
-
* Use the SCOPED name `@astryxdesign/cli` —
|
|
17
|
-
* unrelated package until the CLI is a dependency. Then, per component:
|
|
16
|
+
* Use the SCOPED name `@astryxdesign/cli` — the unscoped `astryx` resolves to
|
|
17
|
+
* an unrelated npm package until the CLI is a dependency. Then, per component:
|
|
18
18
|
* npx @astryxdesign/cli component <Name>
|
|
19
19
|
* npx @astryxdesign/cli component --list
|
|
20
20
|
* ==========================================================================
|
|
@@ -27,8 +27,8 @@ const BANNER = `
|
|
|
27
27
|
|
|
28
28
|
npx @astryxdesign/cli init
|
|
29
29
|
|
|
30
|
-
(Use the scoped name @astryxdesign/cli.
|
|
31
|
-
unrelated package until the CLI is installed as a dependency.)
|
|
30
|
+
(Use the scoped name @astryxdesign/cli. The unscoped name astryx resolves
|
|
31
|
+
to an unrelated npm package until the CLI is installed as a dependency.)
|
|
32
32
|
|
|
33
33
|
init writes the full component guide, page templates, design rules & tokens
|
|
34
34
|
into your AGENTS.md / CLAUDE.md. Then, for any component:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.2-canary.
|
|
3
|
+
"version": "0.7.2-canary.085e145",
|
|
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",
|
|
@@ -792,8 +792,8 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
-
"@astryxdesign/a11y-spec": "0.7.2-canary.
|
|
796
|
-
"@astryxdesign/cli": "0.7.2-canary.
|
|
795
|
+
"@astryxdesign/a11y-spec": "0.7.2-canary.085e145",
|
|
796
|
+
"@astryxdesign/cli": "0.7.2-canary.085e145",
|
|
797
797
|
"@babel/cli": "^7.29.7",
|
|
798
798
|
"@babel/core": "^7.29.7",
|
|
799
799
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -145,15 +145,37 @@ function findRepositoryRoot(start) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
|
-
* The
|
|
149
|
-
* (
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* the CLI is installed. Bare `npx astryx` would fetch an unrelated look-alike
|
|
148
|
+
* The invocation the setup nudge falls back to when the CLI's picker
|
|
149
|
+
* (`getCliInvocation`) cannot be loaded. Scoped package form
|
|
150
|
+
* (`@astryxdesign/cli`) — always resolves to us, even before the CLI is
|
|
151
|
+
* installed. npx with the unscoped name would fetch an unrelated look-alike
|
|
153
152
|
* package (see PR #4151).
|
|
154
153
|
*/
|
|
155
|
-
export const
|
|
156
|
-
|
|
154
|
+
export const SETUP_INVOCATION = 'npx @astryxdesign/cli';
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The one-line next-step every setup layer prints. Kept here so layer 1
|
|
158
|
+
* (core), layer 2 (cli postinstall) and layer 3 (each command) can never word
|
|
159
|
+
* it differently. Each layer passes the stem from the CLI's picker when it can
|
|
160
|
+
* load it.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} [invocation] the CLI invocation stem, e.g. `pnpm exec astryx`
|
|
163
|
+
* @param {{before?: string | null}} [options] `before`: a command to run
|
|
164
|
+
* first, such as the one that adds the CLI to a project that has none
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
export function setupNudge(
|
|
168
|
+
invocation = SETUP_INVOCATION,
|
|
169
|
+
{before = null} = {},
|
|
170
|
+
) {
|
|
171
|
+
const run = before
|
|
172
|
+
? `\`${before}\`, then \`${invocation} init\``
|
|
173
|
+
: `\`${invocation} init\``;
|
|
174
|
+
return `\nNext step: run ${run} to finish setup and install the Astryx agent prompt.\n\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The nudge with the fallback invocation. */
|
|
178
|
+
export const SETUP_NUDGE = setupNudge();
|
|
157
179
|
|
|
158
180
|
/**
|
|
159
181
|
* Pure decision: should a postinstall print the setup nudge? Shared by BOTH
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @file @astryxdesign/core postinstall — nudge to run `
|
|
4
|
+
* @file @astryxdesign/core postinstall — nudge to run `astryx init`, prefixed
|
|
5
|
+
* the way the project's installed CLI prints commands.
|
|
5
6
|
*
|
|
6
7
|
* Enforcement layer 1 of making `astryx init` foolproof: when the design system
|
|
7
8
|
* is installed and the project hasn't run init yet, print a one-line next-step
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
* during npx's fetch, and once set up.
|
|
20
21
|
*/
|
|
21
22
|
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
22
25
|
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
23
26
|
|
|
24
27
|
const HERE = fileURLToPath(import.meta.url);
|
|
@@ -37,6 +40,39 @@ async function loadContract() {
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The stem the project's installed CLI would print, from its picker
|
|
45
|
+
* (`getCliInvocation`). Core cannot depend on the CLI, so this looks for an
|
|
46
|
+
* installed copy from the project upward and imports the picker by path; with
|
|
47
|
+
* no CLI there, or one too old to have the picker, it returns undefined and
|
|
48
|
+
* the contract's scoped fallback is printed. At install time no integration is
|
|
49
|
+
* loaded, so this is the package-manager guess.
|
|
50
|
+
* @param {string} root
|
|
51
|
+
* @returns {Promise<string | undefined>}
|
|
52
|
+
*/
|
|
53
|
+
export async function cliInvocation(root) {
|
|
54
|
+
try {
|
|
55
|
+
let dir = path.resolve(root);
|
|
56
|
+
for (;;) {
|
|
57
|
+
const picker = path.join(
|
|
58
|
+
dir,
|
|
59
|
+
'node_modules/@astryxdesign/cli/foundation/env/package-manager.mjs',
|
|
60
|
+
);
|
|
61
|
+
if (fs.existsSync(picker)) {
|
|
62
|
+
const {getCliInvocation} = await import(pathToFileURL(picker).href);
|
|
63
|
+
return typeof getCliInvocation === 'function'
|
|
64
|
+
? getCliInvocation(root)
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
const parent = path.dirname(dir);
|
|
68
|
+
if (parent === dir) return undefined;
|
|
69
|
+
dir = parent;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
async function main() {
|
|
41
77
|
const contract = await loadContract();
|
|
42
78
|
if (!contract) return;
|
|
@@ -48,7 +84,12 @@ async function main() {
|
|
|
48
84
|
isSetUp: contract.isAstryxPromptInReach(root),
|
|
49
85
|
})
|
|
50
86
|
) {
|
|
51
|
-
|
|
87
|
+
const invocation = await cliInvocation(root);
|
|
88
|
+
process.stdout.write(
|
|
89
|
+
typeof contract.setupNudge === 'function'
|
|
90
|
+
? contract.setupNudge(invocation)
|
|
91
|
+
: contract.SETUP_NUDGE,
|
|
92
|
+
);
|
|
52
93
|
}
|
|
53
94
|
}
|
|
54
95
|
|
|
@@ -51,7 +51,7 @@ export const docs = {
|
|
|
51
51
|
name: 'icon',
|
|
52
52
|
type: 'IconType',
|
|
53
53
|
description:
|
|
54
|
-
'Icon to display before the label. See `
|
|
54
|
+
'Icon to display before the label. See `astryx docs icons` for valid semantic names.',
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
name: 'value',
|
|
@@ -98,7 +98,7 @@ export interface DropdownMenuCheckboxItemProps extends Omit<
|
|
|
98
98
|
description?: ReactNode;
|
|
99
99
|
/**
|
|
100
100
|
* Icon to display before the label. Accepts a semantic icon name (see
|
|
101
|
-
* `
|
|
101
|
+
* `astryx docs icons`) or a rendered node.
|
|
102
102
|
*/
|
|
103
103
|
icon?: ReactNode | IconType;
|
|
104
104
|
/**
|
|
@@ -43,7 +43,7 @@ export const docs = {
|
|
|
43
43
|
name: 'icon',
|
|
44
44
|
type: 'IconType',
|
|
45
45
|
description:
|
|
46
|
-
'Icon to display before the label. See `
|
|
46
|
+
'Icon to display before the label. See `astryx docs icons` for valid semantic names.',
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
name: 'isDisabled',
|
|
@@ -93,7 +93,7 @@ export interface DropdownMenuRadioItemProps extends Omit<
|
|
|
93
93
|
description?: ReactNode;
|
|
94
94
|
/**
|
|
95
95
|
* Icon to display before the label. Accepts a semantic icon name (see
|
|
96
|
-
* `
|
|
96
|
+
* `astryx docs icons`) or a rendered node.
|
|
97
97
|
*/
|
|
98
98
|
icon?: ReactNode | IconType;
|
|
99
99
|
/**
|
|
@@ -13,7 +13,7 @@ export const docs = {
|
|
|
13
13
|
name: 'direction',
|
|
14
14
|
type: "'vertical' | 'horizontal' | 'horizontal-labels'",
|
|
15
15
|
description:
|
|
16
|
-
'Controls field arrangement. Vertical stacks top-to-bottom, horizontal
|
|
16
|
+
'Controls field arrangement. Vertical stacks top-to-bottom, horizontal uses one fractional grid track per field in logical inline order, and horizontal-labels uses CSS Grid with labels beside inputs on the inline-start side (collapsing to vertical when the viewport is <=480px).',
|
|
17
17
|
default: "'vertical'",
|
|
18
18
|
},
|
|
19
19
|
{
|
|
@@ -51,6 +51,7 @@ export const docs = {
|
|
|
51
51
|
{ guidance: false, description: 'Nest horizontal-labels inside another FormLayout. It uses CSS Grid and needs to be the outermost container.' },
|
|
52
52
|
],
|
|
53
53
|
anatomy: [
|
|
54
|
+
{name: 'Layout container', required: true, description: 'Owns field arrangement, spacing, direction, and form-level optionality.'},
|
|
54
55
|
{name: 'Form title', required: false, description: 'Heading that describes the purpose of the form.'},
|
|
55
56
|
{name: 'Fields', required: true, description: 'Input components with labels for collecting user data.'},
|
|
56
57
|
{name: 'Footer', required: false, description: 'Contains confirmation buttons such as Submit or Cancel.'},
|
|
@@ -67,7 +68,7 @@ export const docsZh = {
|
|
|
67
68
|
name: 'direction',
|
|
68
69
|
type: "'vertical' | 'horizontal' | 'horizontal-labels'",
|
|
69
70
|
description:
|
|
70
|
-
'控制字段排列方式。vertical
|
|
71
|
+
'控制字段排列方式。vertical 从上到下堆叠;horizontal 为每个字段使用一个 1fr 网格轨道,并按逻辑行内方向排列;horizontal-labels 使用 CSS Grid,将标签放在输入框的行内起始侧(当视口宽度 <=480px 时折叠为垂直布局)。',
|
|
71
72
|
default: "'vertical'",
|
|
72
73
|
},
|
|
73
74
|
{
|
|
@@ -105,6 +106,7 @@ export const docsZh = {
|
|
|
105
106
|
{ guidance: false, description: 'Nest horizontal-labels inside another FormLayout. It uses CSS Grid and needs to be the outermost container.' },
|
|
106
107
|
],
|
|
107
108
|
anatomy: [
|
|
109
|
+
{name: 'Layout container', required: true, description: 'Owns field arrangement, spacing, direction, and form-level optionality.'},
|
|
108
110
|
{name: 'Form title', required: false, description: 'Heading that describes the purpose of the form.'},
|
|
109
111
|
{name: 'Fields', required: true, description: 'Input components with labels for collecting user data.'},
|
|
110
112
|
{name: 'Footer', required: false, description: 'Contains confirmation buttons such as Submit or Cancel.'},
|
|
@@ -127,13 +129,14 @@ export const docsDense = {
|
|
|
127
129
|
{ guidance: false, description: 'Nest horizontal-labels inside another FormLayout. It uses CSS Grid and needs to be the outermost container.' },
|
|
128
130
|
],
|
|
129
131
|
anatomy: [
|
|
132
|
+
{name: 'Layout container', required: true, description: 'Owns field arrangement, spacing, direction, and form-level optionality.'},
|
|
130
133
|
{name: 'Form title', required: false, description: 'Heading that describes the purpose of the form.'},
|
|
131
134
|
{name: 'Fields', required: true, description: 'Input components with labels for collecting user data.'},
|
|
132
135
|
{name: 'Footer', required: false, description: 'Contains confirmation buttons such as Submit or Cancel.'},
|
|
133
136
|
],
|
|
134
137
|
},
|
|
135
138
|
propDescriptions: {
|
|
136
|
-
direction: 'Field arrangement. Vertical stacks top-to-bottom
|
|
139
|
+
direction: 'Field arrangement. Vertical stacks top-to-bottom; horizontal uses one fractional grid track per field in logical inline order; horizontal-labels places labels on the inline-start side and collapses when the viewport is <=480px.',
|
|
137
140
|
defaultOptionality: 'State the form treats as default, so only the exception is marked. "optional" → only isRequired fields show an indicator; "required" → only isOptional fields do. Under "required" unmarked fields also expose aria-required (aria only, not native required). Unset = per-field behavior.',
|
|
138
141
|
children: 'Form fields to arrange. Accepts Astryx inputs + Field-wrapped custom controls.',
|
|
139
142
|
xstyle: 'StyleX styles for layout customization. Must be stylex.create() value.',
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 7
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:FormLayout
|
|
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, layout, theming, accessibility]
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/FormLayout/FormLayout.test.tsx,
|
|
16
|
+
apps/storybook/stories/FormLayout.stories.tsx,
|
|
17
|
+
packages/core/src/theme/themingTargets.test.ts,
|
|
18
|
+
packages/core/src/docPropLiterals.test.ts,
|
|
19
|
+
packages/core/src/docPropReferences.test.ts,
|
|
20
|
+
]
|
|
21
|
+
modules: []
|
|
22
|
+
families: [family:input-fields]
|
|
23
|
+
design_specs: []
|
|
24
|
+
architecture:
|
|
25
|
+
[
|
|
26
|
+
architecture:public-component-api,
|
|
27
|
+
architecture:component-theming-surface,
|
|
28
|
+
architecture:component-test-sufficiency,
|
|
29
|
+
]
|
|
30
|
+
contributing: []
|
|
31
|
+
system_specs: []
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# FormLayout component contract
|
|
35
|
+
|
|
36
|
+
## Contract at a glance
|
|
37
|
+
|
|
38
|
+
| Area | Contract |
|
|
39
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| Public contract | `FormLayout` arranges caller-owned fields through `vertical`, `horizontal`, and `horizontal-labels` directions, provides a form-level optionality default, exports its context and supporting types, and accepts root `BaseProps`. |
|
|
41
|
+
| Behavior | Vertical stacks children; horizontal creates one fractional grid track per child in logical inline order; horizontal-labels pairs Field labels and controls, then collapses below a 481px viewport. |
|
|
42
|
+
| End-user impact | People encounter predictable field spacing and order, and assistive technology receives required-state information even when the form-wide visual convention leaves the common case unmarked. |
|
|
43
|
+
| Builder impact | Builders supply the fields and any semantic `<form>`, title, or footer; nested layouts replace the outer direction and optionality context for their descendants. |
|
|
44
|
+
| Compatibility/readiness | This draft records released behavior. It changes no public API, default, or runtime behavior. |
|
|
45
|
+
| Review checks | Reject lost root props, physical inline ordering, changed collapse boundaries, caller-overridable direction reflection, lost nested-context shadowing, or a visible optionality convention that disagrees with ARIA. Narrow label/control grouping remains open in OQ1. |
|
|
46
|
+
| Governing rules | Established public FormLayout interface; `family:input-fields` outer-arrangement ownership; `architecture:public-component-api/INV5–INV9`; `architecture:component-theming-surface/INV2–INV6`. |
|
|
47
|
+
|
|
48
|
+
This table is a review projection; the body below is authoritative.
|
|
49
|
+
|
|
50
|
+
## Intent
|
|
51
|
+
|
|
52
|
+
`FormLayout` owns the outer spatial relationship among caller-owned form fields. It does not create form submission, field state, labels, controls, or action regions.
|
|
53
|
+
|
|
54
|
+
## Compatibility and migration
|
|
55
|
+
|
|
56
|
+
- Released default preserved: yes; direction defaults to `vertical`, and optionality remains field-owned when `defaultOptionality` is unset.
|
|
57
|
+
- Compatibility class: observational only; this draft changes no public prop, default, or behavior.
|
|
58
|
+
- Controlled/uncontrolled behavior: not applicable; FormLayout owns no form value or submission state.
|
|
59
|
+
- Migration decision: none.
|
|
60
|
+
|
|
61
|
+
## Ownership boundary
|
|
62
|
+
|
|
63
|
+
**Owns**
|
|
64
|
+
|
|
65
|
+
- Outer field arrangement, spacing, direction, and the viewport collapse for horizontal labels.
|
|
66
|
+
- Form-level optionality context and nested-layout shadowing.
|
|
67
|
+
- The root DOM element, root prop composition, and the `form-layout` theme target.
|
|
68
|
+
|
|
69
|
+
**Does not own / non-goals**
|
|
70
|
+
|
|
71
|
+
- Form semantics, submission, validation, or action placement — owned by the product callsite and native `<form>`.
|
|
72
|
+
- Field labels, controls, status, and internal geometry — owned by `component:Field`, each input, and `family:input-fields`.
|
|
73
|
+
- The content or visual contract of caller-supplied titles and footers — owned by their rendered components or the callsite.
|
|
74
|
+
|
|
75
|
+
## Public concepts
|
|
76
|
+
|
|
77
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
78
|
+
| ------------------- | ----------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------- | ---------------------- | --------- | ---------------------- |
|
|
79
|
+
| direction | `vertical`; `horizontal`; `horizontal-labels` | Selects the outer arrangement and provides that value to descendants. | All layouts. | `vertical` | `component:FormLayout` | released | Type-constrained |
|
|
80
|
+
| default optionality | unset; `optional`; `required` | Marks only exceptions while controls preserve resolved required state. | All directions; the nearest nested layout shadows the outer. | unset / per-field | `component:FormLayout` | released | Type-constrained |
|
|
81
|
+
| root composition | DOM/data/ARIA/events; ref; class/style/`xstyle` | Extends the root layout container without replacing owned reflection. | All directions. | no caller decoration | `component:FormLayout` | released | Shared merge contract |
|
|
82
|
+
| layout context | direction plus optionality | Lets Field and inputs adapt their layout and required-state projection. | Descendants in the same layer scope. | vertical / unset | `component:FormLayout` | released | Nearest provider wins |
|
|
83
|
+
|
|
84
|
+
## Behavioral and layout contract
|
|
85
|
+
|
|
86
|
+
Draft requirements identify their basis so observed code is not mistaken for an intentional decision.
|
|
87
|
+
|
|
88
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
89
|
+
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
90
|
+
| FR1 | Vertical direction stacks children with the shared spacing token. | Established public interface; current source and stories | settled observation |
|
|
91
|
+
| FR2 | Horizontal direction places every direct child in one `1fr` grid track and follows the document's logical inline order without a component-owned wrap rule. | Established public interface; current source and stories | settled observation |
|
|
92
|
+
| FR3 | Horizontal-labels direction lets Field labels and control groups participate in a two-column grid, with the label on the inline-start side; at a viewport width of 480px or below it becomes one vertical flex column. Whether the collapsed label/control gap distinguishes each pair strongly enough remains open in OQ1. | Established public interface; Field composition and browser fixture | wide behavior settled; narrow grouping human decision |
|
|
93
|
+
| FR4 | A nested FormLayout replaces the outer direction and optionality context for its descendants. | Existing context and unit tests | settled observation |
|
|
94
|
+
| FR5 | `defaultOptionality` suppresses only the visible marker that restates the form default; descendant controls still expose resolved `aria-required`, while native `required` remains tied to explicit field intent. | Existing tests; `family:input-fields` ownership | settled observation |
|
|
95
|
+
| FR6 | Root ref, DOM/data/ARIA/event props, `xstyle`, class name, and inline style reach or compose on the root without replacing `data-direction` or the canonical target. | `architecture:public-component-api/INV5–INV8` | settled observation |
|
|
96
|
+
|
|
97
|
+
### Allowed variation
|
|
98
|
+
|
|
99
|
+
- **AV1 — Caller content.** Any caller-owned field, heading, explanatory content, or action region may be a child; FormLayout does not inspect or rewrite it.
|
|
100
|
+
- **AV2 — Nested arrangement.** Builders may nest vertical and horizontal layouts; each provider owns only its descendant scope.
|
|
101
|
+
- **AV3 — Root styling.** Caller styling may change the container's presentation while preserving the public direction and child contracts.
|
|
102
|
+
|
|
103
|
+
### Representative states
|
|
104
|
+
|
|
105
|
+
| State | Required invariant | Allowed variation |
|
|
106
|
+
| -------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
107
|
+
| vertical | One block-axis stack with tokenized gaps. | Any caller-owned children. |
|
|
108
|
+
| horizontal | One `1fr` grid track per child in logical inline order. | Child count and content. |
|
|
109
|
+
| horizontal labels / wide | Label/control pairs share consistent two-column tracks. | Label and control content. |
|
|
110
|
+
| horizontal labels / narrow | One vertical column at 480px or below with no component-owned overflow. | Label/control grouping remains OQ1; caller-owned content may wrap. |
|
|
111
|
+
| optional by default | Only explicitly required fields show the exception marker. | Field and input type. |
|
|
112
|
+
| required by default | Only explicitly optional fields show the exception marker. | Field and input type. |
|
|
113
|
+
| nested | Inner direction and optionality replace outer values. | Any supported inner direction/default combination. |
|
|
114
|
+
|
|
115
|
+
### Transformation and precedence order
|
|
116
|
+
|
|
117
|
+
- **ORD1 — Context.** Explicit props resolve against `vertical` and unset defaults, then the nearest provider exposes the resolved values to descendants.
|
|
118
|
+
- **ORD2 — Root props.** FormLayout-owned target and direction reflection compose with StyleX and caller styling; caller DOM props reach the same root without replacing owned reflection.
|
|
119
|
+
- **ORD3 — Optionality.** The layout default establishes the unmarked majority; a field-level `isRequired` or `isOptional` marks the exception and controls native validation only when explicitly requested.
|
|
120
|
+
|
|
121
|
+
### Performance and resources
|
|
122
|
+
|
|
123
|
+
- **PR1 — Stable context identity.** Descendants receive one memoized context value per direction/optionality pair; FormLayout owns no listener, observer, timer, or external resource.
|
|
124
|
+
|
|
125
|
+
## Accessibility contract
|
|
126
|
+
|
|
127
|
+
- **AR1 — Layout remains nonsemantic.** FormLayout renders a `<div>` and does not imply `<form>`, group, label, or submission semantics; callers provide those when their composition needs them.
|
|
128
|
+
- **AR2 — Required state remains perceivable.** A form-wide optionality convention may remove redundant visible markers, but descendant controls preserve the resolved required state for assistive technology.
|
|
129
|
+
- **AR3 — Logical order is preserved.** Every direction retains DOM order and logical inline flow; FormLayout does not create a separate keyboard order.
|
|
130
|
+
|
|
131
|
+
## Design relationships
|
|
132
|
+
|
|
133
|
+
No current component-local or shared design record prescribes a FormLayout-specific representation. Spacing uses the current token scale; exact tuning remains token-owned.
|
|
134
|
+
|
|
135
|
+
### Theming anatomy
|
|
136
|
+
|
|
137
|
+
<!-- anatomy-theming:v1 -->
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"Layout container": {"target": "form-layout"},
|
|
142
|
+
"Form title": {
|
|
143
|
+
"none": {
|
|
144
|
+
"reason": "intentional: A title is caller-owned content; FormLayout neither renders nor styles a title part."
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"Fields": {
|
|
148
|
+
"none": {
|
|
149
|
+
"reason": "intentional: Caller-owned Field and input components retain their own theming targets."
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"Footer": {
|
|
153
|
+
"none": {
|
|
154
|
+
"reason": "intentional: A footer is caller-owned content; FormLayout neither renders nor styles an action region."
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The root target reflects the closed `direction` axis. It owns the outer arrangement container and does not claim the visual parts rendered by child components.
|
|
161
|
+
|
|
162
|
+
## Family and system relationships
|
|
163
|
+
|
|
164
|
+
- `family:input-fields` identifies FormLayout as the owner of outer arrangement, spacing, direction, and form-level optionality.
|
|
165
|
+
- `component:Field` consumes horizontal-label direction and optionality while retaining label, control, description, and status ownership.
|
|
166
|
+
- `architecture:public-component-api` owns root prop, styling, event, ref, and released-type composition.
|
|
167
|
+
- `architecture:component-theming-surface` owns the root target's anatomy mapping and reflected direction axis.
|
|
168
|
+
- `architecture:component-test-sufficiency` owns bounded, mutation-sensitive evidence selection.
|
|
169
|
+
|
|
170
|
+
## Verification map
|
|
171
|
+
|
|
172
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
173
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
174
|
+
| FR1–FR4, ORD1 | `FormLayout.test.tsx`, exact-head `AuditMatrix` story-play guards, and independent rendered inspection | vertical, horizontal, horizontal-label wide/narrow, nested | Changed display, order, breakpoint, or provider shadowing fails; narrow grouping remains visible for OQ1. | `audit:FormLayout/behavior` |
|
|
175
|
+
| FR5, ORD3, AR2 | `FormLayout.test.tsx`, Field tests, and scoped browser accessibility audit | required/optional/unset defaults; explicit exceptions | A redundant marker returns, resolved `aria-required` disappears, or native validation changes. | `audit:FormLayout/accessibility` |
|
|
176
|
+
| FR6, ORD2 | root composition tests, BaseProps lint, theming-target tests, and exact-head AuditMatrix play | ref, DOM/data/ARIA/events, class/style/`xstyle`, every direction | A caller input drops, direction reflection is replaced, or the public target disappears. | `audit:FormLayout/api` |
|
|
177
|
+
| AR1, AR3 | DOM tests and exact-head browser/a11y evidence | plain root, native form wrapper, LTR/RTL | Layout invents semantics, changes DOM/keyboard order, or uses physical inline placement. | `audit:FormLayout/accessibility` |
|
|
178
|
+
| Theming anatomy | knowledge validation, consumer-doc projection, and theming-target tests | root target; caller-owned title, fields, and footer | The target loses its semantic owner or FormLayout claims a child component's painter. | `audit:FormLayout/theming` |
|
|
179
|
+
| Documentation | docs typecheck and prop literal/reference tests | all directions, breakpoint, optionality, root anatomy | Docs claim wrapping, physical order, flex growth, or another behavior the released code lacks. | `audit:FormLayout/docs` |
|
|
180
|
+
| RTL applicability | verified-N/A registry plus exact-head AuditMatrix logical-order assertion | horizontal and horizontal-labels in LTR/RTL | A physical side, directional glyph, transform, scroll/drag, overlay, or directional key path makes N/A stale. | `audit:FormLayout/i18n-rtl` |
|
|
181
|
+
|
|
182
|
+
## Decision log
|
|
183
|
+
|
|
184
|
+
No component-local decision is asserted. This draft records released behavior and its current shared owners.
|
|
185
|
+
|
|
186
|
+
## Open questions
|
|
187
|
+
|
|
188
|
+
- **OQ1 — At 480px and below, should each horizontal-label pair preserve a smaller intra-field label/control gap than the gap between adjacent fields?** (`human-design`)
|
|
189
|
+
|
|
190
|
+
## Content boundary
|
|
191
|
+
|
|
192
|
+
This draft records verified shipped behavior and the unresolved narrow grouping relationship. It does not create form semantics, prescribe caller content, approve a new API or target, or duplicate consumer prop tables and audit-run scores.
|
|
@@ -82,10 +82,11 @@ export interface FormLayoutProps extends BaseProps<HTMLDivElement> {
|
|
|
82
82
|
* Direction of field arrangement.
|
|
83
83
|
*
|
|
84
84
|
* - `'vertical'` — Fields stack top-to-bottom (default). Most common.
|
|
85
|
-
* - `'horizontal'` — Fields
|
|
86
|
-
*
|
|
87
|
-
* - `'horizontal-labels'` — CSS Grid with labels
|
|
88
|
-
* Collapses to vertical when the
|
|
85
|
+
* - `'horizontal'` — Fields occupy one `1fr` CSS Grid track per child in
|
|
86
|
+
* logical inline order.
|
|
87
|
+
* - `'horizontal-labels'` — CSS Grid with labels beside their inputs on the
|
|
88
|
+
* inline-start side. Collapses to vertical when the viewport is narrow
|
|
89
|
+
* (≤480px).
|
|
89
90
|
*
|
|
90
91
|
* @default 'vertical'
|
|
91
92
|
*/
|
|
@@ -20,9 +20,9 @@ import {createLayerScopedContext as createContext} from '../Layer/layerScopedCon
|
|
|
20
20
|
* Direction of form field arrangement.
|
|
21
21
|
*
|
|
22
22
|
* - `'vertical'` — Fields stack top-to-bottom (default). Most common.
|
|
23
|
-
* - `'horizontal'` — Fields
|
|
24
|
-
* - `'horizontal-labels'` — Fields stack vertically
|
|
25
|
-
*
|
|
23
|
+
* - `'horizontal'` — Fields occupy one `1fr` grid track per child in logical inline order.
|
|
24
|
+
* - `'horizontal-labels'` — Fields stack vertically while labels sit beside
|
|
25
|
+
* their inputs on the inline-start side (settings/admin panel pattern).
|
|
26
26
|
*/
|
|
27
27
|
export type FormLayoutDirection =
|
|
28
28
|
'vertical' | 'horizontal' | 'horizontal-labels';
|
package/src/Grid/Grid.test.tsx
CHANGED
|
@@ -308,6 +308,28 @@ describe('Grid', () => {
|
|
|
308
308
|
expect(grid).toHaveAttribute('aria-label', 'Product grid');
|
|
309
309
|
});
|
|
310
310
|
|
|
311
|
+
it('keeps component-owned theme metadata over colliding pass-throughs', () => {
|
|
312
|
+
render(
|
|
313
|
+
<Grid
|
|
314
|
+
columns={3}
|
|
315
|
+
gap={4}
|
|
316
|
+
align="end"
|
|
317
|
+
justify="center"
|
|
318
|
+
data-columns="9"
|
|
319
|
+
data-gap="10"
|
|
320
|
+
data-align="start"
|
|
321
|
+
data-justify="stretch"
|
|
322
|
+
data-testid="grid">
|
|
323
|
+
<div>Item</div>
|
|
324
|
+
</Grid>,
|
|
325
|
+
);
|
|
326
|
+
const grid = screen.getByTestId('grid');
|
|
327
|
+
expect(grid).toHaveAttribute('data-columns', '3');
|
|
328
|
+
expect(grid).toHaveAttribute('data-gap', '4');
|
|
329
|
+
expect(grid).toHaveAttribute('data-align', 'end');
|
|
330
|
+
expect(grid).toHaveAttribute('data-justify', 'center');
|
|
331
|
+
});
|
|
332
|
+
|
|
311
333
|
it('renders children correctly', () => {
|
|
312
334
|
render(
|
|
313
335
|
<Grid columns={3} data-testid="grid">
|
package/src/Grid/Grid.tsx
CHANGED
|
@@ -455,6 +455,7 @@ export function Grid({
|
|
|
455
455
|
return (
|
|
456
456
|
<div
|
|
457
457
|
ref={ref}
|
|
458
|
+
{...props}
|
|
458
459
|
{...mergeProps(
|
|
459
460
|
themeProps('grid', {columns: columnsVariant, gap, align, justify}),
|
|
460
461
|
stylex.props(
|
|
@@ -470,8 +471,7 @@ export function Grid({
|
|
|
470
471
|
),
|
|
471
472
|
className,
|
|
472
473
|
{...style, ...inlineStyle},
|
|
473
|
-
)}
|
|
474
|
-
{...props}>
|
|
474
|
+
)}>
|
|
475
475
|
{children}
|
|
476
476
|
</div>
|
|
477
477
|
);
|
|
@@ -171,7 +171,7 @@ defineTheme({
|
|
|
171
171
|
code: `// When the shape itself is wrong, hand the theme a component. It receives
|
|
172
172
|
// {state, size, isDisabled, children} and nothing else.
|
|
173
173
|
//
|
|
174
|
-
// Use theme tokens, never raw values — run \`
|
|
174
|
+
// Use theme tokens, never raw values — run \`astryx docs tokens\` for the
|
|
175
175
|
// full set. Color: --color-accent, --color-on-accent, --color-border,
|
|
176
176
|
// --color-border-emphasized, --color-background-surface,
|
|
177
177
|
// --color-background-muted. Radius: --radius-inner, --radius-full.
|
|
@@ -236,7 +236,7 @@ defineTheme({name: 'brand', indicators: {check: RadioIndicator}});`,
|
|
|
236
236
|
{
|
|
237
237
|
guidance: true,
|
|
238
238
|
description:
|
|
239
|
-
'Use theme tokens for every color, radius, and border width in a replacement. Run `
|
|
239
|
+
'Use theme tokens for every color, radius, and border width in a replacement. Run `astryx docs tokens` for the set.',
|
|
240
240
|
},
|
|
241
241
|
{
|
|
242
242
|
guidance: true,
|
|
@@ -297,7 +297,7 @@ export const docsDense = {
|
|
|
297
297
|
{
|
|
298
298
|
guidance: true,
|
|
299
299
|
description:
|
|
300
|
-
'Use theme tokens for every color, radius, and border width in a replacement. Run `
|
|
300
|
+
'Use theme tokens for every color, radius, and border width in a replacement. Run `astryx docs tokens` for the set.',
|
|
301
301
|
},
|
|
302
302
|
{
|
|
303
303
|
guidance: true,
|
|
@@ -552,6 +552,68 @@ describe('PowerSearch', () => {
|
|
|
552
552
|
});
|
|
553
553
|
});
|
|
554
554
|
|
|
555
|
+
describe('filter token focus', () => {
|
|
556
|
+
it('opens the editor with the mouse without opening search suggestions', async () => {
|
|
557
|
+
const user = userEvent.setup();
|
|
558
|
+
render(
|
|
559
|
+
<PowerSearch
|
|
560
|
+
config={config}
|
|
561
|
+
filters={[
|
|
562
|
+
{
|
|
563
|
+
field: 'status',
|
|
564
|
+
operator: 'is',
|
|
565
|
+
value: {type: 'enum', value: 'open'},
|
|
566
|
+
},
|
|
567
|
+
]}
|
|
568
|
+
onChange={() => {}}
|
|
569
|
+
/>,
|
|
570
|
+
);
|
|
571
|
+
const input = screen.getByRole('combobox', {name: 'Search'});
|
|
572
|
+
|
|
573
|
+
await user.pointer({
|
|
574
|
+
target: screen.getByText('Status: is'),
|
|
575
|
+
keys: '[MouseLeft>]',
|
|
576
|
+
});
|
|
577
|
+
expect(input).not.toHaveFocus();
|
|
578
|
+
expect(input).toHaveAttribute('aria-expanded', 'false');
|
|
579
|
+
expect(
|
|
580
|
+
screen.queryByRole('button', {name: 'Apply', hidden: true}),
|
|
581
|
+
).toBeNull();
|
|
582
|
+
|
|
583
|
+
await user.pointer({keys: '[/MouseLeft]'});
|
|
584
|
+
expect(
|
|
585
|
+
await screen.findByRole('button', {name: 'Apply', hidden: true}),
|
|
586
|
+
).toBeInTheDocument();
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('preserves search entry and token editing with the keyboard', async () => {
|
|
590
|
+
const user = userEvent.setup();
|
|
591
|
+
render(
|
|
592
|
+
<PowerSearch
|
|
593
|
+
config={config}
|
|
594
|
+
filters={[
|
|
595
|
+
{
|
|
596
|
+
field: 'status',
|
|
597
|
+
operator: 'is',
|
|
598
|
+
value: {type: 'enum', value: 'open'},
|
|
599
|
+
},
|
|
600
|
+
]}
|
|
601
|
+
onChange={() => {}}
|
|
602
|
+
/>,
|
|
603
|
+
);
|
|
604
|
+
await user.tab();
|
|
605
|
+
expect(screen.getByRole('combobox', {name: 'Search'})).toHaveFocus();
|
|
606
|
+
|
|
607
|
+
await user.tab({shift: true}); // Remove button
|
|
608
|
+
await user.tab({shift: true}); // Token label
|
|
609
|
+
expect(screen.getByRole('button', {name: 'Status: is'})).toHaveFocus();
|
|
610
|
+
await user.keyboard('{Enter}');
|
|
611
|
+
expect(
|
|
612
|
+
await screen.findByRole('button', {name: 'Apply', hidden: true}),
|
|
613
|
+
).toBeInTheDocument();
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
555
617
|
describe('PowerSearch statusVariant forwarding', () => {
|
|
556
618
|
it('defaults to attached (status renders with data-variant="attached")', () => {
|
|
557
619
|
const {container} = render(
|
|
@@ -900,6 +900,15 @@ export function PowerSearch({
|
|
|
900
900
|
size={size}
|
|
901
901
|
icon={tokenIcon}
|
|
902
902
|
endContent={valueContent}
|
|
903
|
+
onMouseDown={
|
|
904
|
+
handleClick
|
|
905
|
+
? e => {
|
|
906
|
+
// Keep a token press from entering Tokenizer's input-focus
|
|
907
|
+
// redirect. Click still edits/removes; Tab entry is unchanged.
|
|
908
|
+
e.preventDefault();
|
|
909
|
+
}
|
|
910
|
+
: undefined
|
|
911
|
+
}
|
|
903
912
|
onClick={
|
|
904
913
|
handleClick
|
|
905
914
|
? (e: React.MouseEvent) => {
|
package/src/Token/Token.tsx
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file TokenRemoveTarget.a11y.chromium.spec.ts
|
|
5
|
+
* @input The `Core/PowerSearch` Pre-set Filters story in a built Storybook and
|
|
6
|
+
* real mouse input, in both text directions.
|
|
7
|
+
* @output Real-engine proof that a Token's enlarged remove target stays on its
|
|
8
|
+
* X: a press on the end of the value beside it runs the token's own action,
|
|
9
|
+
* and a press just outside the visible X still removes.
|
|
10
|
+
* @position Run with `pnpm test:a11y-contract`. jsdom has no hit testing, so
|
|
11
|
+
* the remove target's reach is only observable here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {expect, test, type Page} from '@playwright/test';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_STORYBOOK_DIR,
|
|
17
|
+
serveStorybook,
|
|
18
|
+
type StaticServer,
|
|
19
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
20
|
+
|
|
21
|
+
let storybook: StaticServer;
|
|
22
|
+
|
|
23
|
+
test.beforeAll(async () => {
|
|
24
|
+
storybook = await serveStorybook(
|
|
25
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test.afterAll(async () => {
|
|
30
|
+
await storybook?.close();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const STORY = 'core-powersearch--with-preset-filters';
|
|
34
|
+
const REMOVE_BUTTONS = '#storybook-root button[aria-label^="Remove "]';
|
|
35
|
+
|
|
36
|
+
async function open(page: Page, direction: 'ltr' | 'rtl') {
|
|
37
|
+
await page.setViewportSize({width: 900, height: 600});
|
|
38
|
+
await page.goto(
|
|
39
|
+
`${storybook.origin}/iframe.html?id=${STORY}&viewMode=story&globals=direction:${direction}`,
|
|
40
|
+
);
|
|
41
|
+
await expect(page.locator(REMOVE_BUTTONS)).toHaveCount(2);
|
|
42
|
+
const remove = page.locator(REMOVE_BUTTONS).first();
|
|
43
|
+
const value = page
|
|
44
|
+
.locator('#storybook-root')
|
|
45
|
+
.getByText('Open', {exact: true})
|
|
46
|
+
.first();
|
|
47
|
+
const x = await remove.boundingBox();
|
|
48
|
+
const v = await value.boundingBox();
|
|
49
|
+
if (x == null || v == null) {
|
|
50
|
+
throw new Error('the Status token has no visible value or remove button');
|
|
51
|
+
}
|
|
52
|
+
return {remove, x, v};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const direction of ['ltr', 'rtl'] as const) {
|
|
56
|
+
test(`a press on the end of the value opens its editor (${direction})`, async ({
|
|
57
|
+
page,
|
|
58
|
+
}) => {
|
|
59
|
+
const {x, v} = await open(page, direction);
|
|
60
|
+
// Two pixels inside the value's inline-end edge, level with the X.
|
|
61
|
+
const px = direction === 'ltr' ? v.x + v.width - 2 : v.x + 2;
|
|
62
|
+
const py = x.y + x.height / 2;
|
|
63
|
+
// Precondition: the point is on the value, not on the visible X.
|
|
64
|
+
expect(px < x.x || px > x.x + x.width).toBe(true);
|
|
65
|
+
|
|
66
|
+
await page.mouse.click(px, py);
|
|
67
|
+
|
|
68
|
+
await expect(page.getByRole('combobox', {name: 'Field'})).toBeVisible();
|
|
69
|
+
await expect(page.locator(REMOVE_BUTTONS)).toHaveCount(2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test(`a press just outside the visible X still removes (${direction})`, async ({
|
|
73
|
+
page,
|
|
74
|
+
}) => {
|
|
75
|
+
const {x} = await open(page, direction);
|
|
76
|
+
// Two pixels past the X's inline-end edge: outside the 16px glyph, inside
|
|
77
|
+
// the enlarged target.
|
|
78
|
+
const px = direction === 'ltr' ? x.x + x.width + 2 : x.x - 2;
|
|
79
|
+
const py = x.y + x.height / 2;
|
|
80
|
+
|
|
81
|
+
await page.mouse.click(px, py);
|
|
82
|
+
|
|
83
|
+
await expect(page.locator(REMOVE_BUTTONS)).toHaveCount(1);
|
|
84
|
+
await expect(page.getByRole('combobox', {name: 'Field'})).toHaveCount(0);
|
|
85
|
+
});
|
|
86
|
+
}
|
package/src/postinstall.test.mjs
CHANGED
|
@@ -24,7 +24,9 @@ import {
|
|
|
24
24
|
AGENT_DOC_PATHS,
|
|
25
25
|
INIT_MARKERS,
|
|
26
26
|
SETUP_NUDGE,
|
|
27
|
+
setupNudge,
|
|
27
28
|
} from '../scripts/agent-doc-state.mjs';
|
|
29
|
+
import {cliInvocation} from '../scripts/postinstall.mjs';
|
|
28
30
|
// Dev-only, monorepo-relative: core must not depend on the CLI at runtime (the
|
|
29
31
|
// CLI peer-depends on core), but the test can read the source and the generator.
|
|
30
32
|
import * as cliLeaf from '../../cli/foundation/agent-docs/agent-doc-state.mjs';
|
|
@@ -95,6 +97,9 @@ describe('core postinstall — one authoritative source, no drift', () => {
|
|
|
95
97
|
expect(AGENT_DOC_PATHS).toEqual(cliLeaf.AGENT_DOC_PATHS);
|
|
96
98
|
expect(INIT_MARKERS).toEqual(cliLeaf.INIT_MARKERS);
|
|
97
99
|
expect(SETUP_NUDGE).toBe(cliLeaf.SETUP_NUDGE);
|
|
100
|
+
expect(setupNudge('pnpm exec astryx')).toBe(
|
|
101
|
+
cliLeaf.setupNudge('pnpm exec astryx'),
|
|
102
|
+
);
|
|
98
103
|
});
|
|
99
104
|
|
|
100
105
|
it('agrees with the CLI leaf on a real project', () => {
|
|
@@ -181,3 +186,31 @@ describe('core postinstall — shouldNudge', () => {
|
|
|
181
186
|
}
|
|
182
187
|
});
|
|
183
188
|
});
|
|
189
|
+
|
|
190
|
+
describe('core postinstall — the nudge names the command the CLI would print', () => {
|
|
191
|
+
it('words the nudge around the stem it is given', () => {
|
|
192
|
+
expect(setupNudge('pnpm exec astryx')).toContain(
|
|
193
|
+
'run `pnpm exec astryx init` to finish setup',
|
|
194
|
+
);
|
|
195
|
+
expect(SETUP_NUDGE).toContain('run `npx @astryxdesign/cli init`');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("asks the project's installed CLI for its stem", async () => {
|
|
199
|
+
write('package.json', '{"name":"app"}');
|
|
200
|
+
write('pnpm-lock.yaml', '');
|
|
201
|
+
write('node_modules/.bin/astryx', '');
|
|
202
|
+
fs.mkdirSync(path.join(tmp, 'node_modules/@astryxdesign'), {
|
|
203
|
+
recursive: true,
|
|
204
|
+
});
|
|
205
|
+
fs.symlinkSync(
|
|
206
|
+
path.resolve(import.meta.dirname, '../../cli'),
|
|
207
|
+
path.join(tmp, 'node_modules/@astryxdesign/cli'),
|
|
208
|
+
);
|
|
209
|
+
expect(await cliInvocation(tmp)).toBe('pnpm exec astryx');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('has no stem to offer when the CLI is not installed', async () => {
|
|
213
|
+
write('package.json', '{"name":"app"}');
|
|
214
|
+
expect(await cliInvocation(tmp)).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
});
|