@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 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 astryx init # resolves to the CLI you just installed
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? Use the scoped package directly — `npx @astryxdesign/cli <cmd>` (or `pnpm dlx`/`bunx @astryxdesign/cli`). Bare `npx astryx` only works once `@astryxdesign/cli` is a dependency; before that npm resolves it to an unrelated package. See [XDS CLI](#xds-cli).
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` to run without installing; bare `npx astryx` only resolves once the CLI is a dependency.
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
- > Prefix these with your runner: `npx astryx …` / `pnpm exec astryx …` once the CLI is installed, or `npx @astryxdesign/cli …` to run without installing.
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
- * `npx @astryxdesign/cli docs icons`) or a rendered node.
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
- * `npx @astryxdesign/cli docs icons`) or a rendered node.
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 arrange left-to-right in equal-width columns
32
- * using CSS Grid. Each child occupies one equal column.
33
- * - `'horizontal-labels'` — CSS Grid with labels to the left of inputs.
34
- * Collapses to vertical when the container is narrow (≤480px).
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;;;;;;;;;;OAUG;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"}
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 arrange left-to-right, wrapping when needed.
6
- * - `'horizontal-labels'` — Fields stack vertically but labels sit to the left
7
- * of their inputs (settings/admin panel pattern).
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 arrange left-to-right, wrapping when needed.
23
- * - `'horizontal-labels'` — Fields stack vertically but labels sit to the left
24
- * of their inputs (settings/admin panel pattern).
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":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAOZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAGL,KAAK,yBAAyB,EAC/B,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAC;AAEtC,OAAO,KAAK,EAAC,WAAW,EAAE,kBAAkB,EAAC,MAAM,UAAU,CAAC;AAwB9D,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EAIjB,qBAAqB,EACrB,iBAAiB,EAIjB,qBAAqB,EACtB,MAAM,SAAS,CAAC;AA0RjB,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjD,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAC5C,SAAS,CAAC,WAAW,CAAC,EACtB,UAAU,CACX;IACC,yEAAyE;IACzE,MAAM,EAAE,iBAAiB,CAAC;IAC1B,gCAAgC;IAChC,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC1C,kCAAkC;IAClC,QAAQ,EAAE,CACR,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,EACzC,UAAU,EAAE,qBAAqB,EACjC,KAAK,EAAE,MAAM,KACV,IAAI,CAAC;IACV,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qDAAqD;IACrD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oDAAoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACjC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC,yDAAyD;IACzD,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACvC,yBAAyB;IACzB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,yBAAyB,CAAC;IAClD;;;OAGG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,yCAAyC;IACzC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,6BAA6B;IAC7B,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACzC;;;OAGG;IACH,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,qBAAqB,CAAC;CACpC;AAsBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EAAE,UAAU,EAClB,OAAO,EACP,QAAQ,EACR,KAAK,EAAE,cAAc,EACrB,aAAoB,EACpB,WAAW,EAAE,oBAAoB,EACjC,YAAoB,EACpB,QAAe,EACf,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,SAAS,EACT,OAAO,EACP,MAAM,EACN,MAAM,EACN,aAA0B,EAC1B,SAAS,EACT,cAAmB,EACnB,oBAAoB,EACpB,gBAA6C,EAC7C,sBAAsB,EAAE,+BAA+B,EACvD,UAAU,EACV,qBAAqB,EACrB,UAAU,EACV,WAAW,EACX,GAAG,EACH,SAAS,EACT,IAAI,EAAE,QAAQ,EACd,aAAa,EAAE,MAAM,EACrB,MAAM,EACN,SAAS,EACT,KAAK,EACL,UAAU,EAAE,kBAAkB,GAC/B,EAAE,gBAAgB,qBAmiBlB;yBArkBe,WAAW"}
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();
@@ -89,7 +89,7 @@ const styles = {
89
89
  kMwMTN: "x1heor9g",
90
90
  k5JduY: "x1s928wv",
91
91
  kwXMNM: "x1j6awrg",
92
- kv0HGH: "x1xsm0q9",
92
+ kv0HGH: "xramuyw",
93
93
  $$css: true
94
94
  }
95
95
  };
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
- .x1xsm0q9:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#)::after{inset:-14px}
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` — bare `npx astryx` resolves to an
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. Bare \`npx astryx\` resolves to an
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.081ccea",
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.081ccea",
796
- "@astryxdesign/cli": "0.7.2-canary.081ccea",
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 one-line next-step both postinstall nudges print. Kept here so layer 1
149
- * (core) and layer 2 (cli) can never word it differently.
150
- *
151
- * Scoped package form (`@astryxdesign/cli`) — always resolves to us, even before
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 SETUP_NUDGE =
156
- '\nNext step: run `npx @astryxdesign/cli init` to finish setup and install the Astryx agent prompt.\n\n';
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
@@ -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 `npx @astryxdesign/cli init`.
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
- process.stdout.write(contract.SETUP_NUDGE);
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 `npx @astryxdesign/cli docs icons` for valid semantic names.',
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
- * `npx @astryxdesign/cli docs icons`) or a rendered node.
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 `npx @astryxdesign/cli docs icons` for valid semantic names.',
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
- * `npx @astryxdesign/cli docs icons`) or a rendered node.
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 arranges left-to-right with equal flex-grow, and horizontal-labels uses CSS Grid with labels to the left of inputs (collapses to vertical on narrow viewports <=480px).',
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 从上到下堆叠,horizontal 从左到右排列且等比弹性增长,horizontal-labels 使用 CSS Grid 将标签放在输入框左侧(在窄视口 <=480px 时折叠为垂直布局)。',
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, horizontal arranges left-to-right w/ equal flex-grow, horizontal-labels uses CSS Grid w/ labels left of inputs (collapses <=480px).',
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 arrange left-to-right in equal-width columns
86
- * using CSS Grid. Each child occupies one equal column.
87
- * - `'horizontal-labels'` — CSS Grid with labels to the left of inputs.
88
- * Collapses to vertical when the container is narrow (≤480px).
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 arrange left-to-right, wrapping when needed.
24
- * - `'horizontal-labels'` — Fields stack vertically but labels sit to the left
25
- * of their inputs (settings/admin panel pattern).
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';
@@ -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 \`npx @astryxdesign/cli docs tokens\` for the
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 `npx @astryxdesign/cli docs tokens` for the set.',
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 `npx @astryxdesign/cli docs tokens` for the set.',
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) => {
@@ -190,7 +190,7 @@ const styles = stylex.create({
190
190
  '::after': {
191
191
  content: '""',
192
192
  position: 'absolute',
193
- inset: '-14px',
193
+ inset: '-4px',
194
194
  },
195
195
  },
196
196
  });
@@ -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
+ }
@@ -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
+ });