@astryxdesign/core 0.7.2-canary.64e1dde → 0.7.2-canary.6c792d8

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.
@@ -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"}
@@ -105,10 +105,10 @@ export function FormLayout({
105
105
  value: contextValue,
106
106
  children: /*#__PURE__*/_jsx("div", {
107
107
  ref: ref,
108
+ ...props,
108
109
  ...mergeProps(themeProps('form-layout', {
109
110
  direction
110
111
  }), stylex.props(styles.base, direction === 'horizontal' && styles.horizontal, direction === 'horizontal-labels' && styles.horizontalLabels, xstyle), className, style),
111
- ...props,
112
112
  children: children
113
113
  })
114
114
  });
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.2-canary.64e1dde",
3
+ "version": "0.7.2-canary.6c792d8",
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.64e1dde",
796
- "@astryxdesign/cli": "0.7.2-canary.64e1dde",
795
+ "@astryxdesign/a11y-spec": "0.7.2-canary.6c792d8",
796
+ "@astryxdesign/cli": "0.7.2-canary.6c792d8",
797
797
  "@babel/cli": "^7.29.7",
798
798
  "@babel/core": "^7.29.7",
799
799
  "@babel/preset-react": "^7.29.7",
@@ -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.
@@ -71,6 +71,21 @@ describe('FormLayout', () => {
71
71
  expect(el).toHaveAttribute('role', 'group');
72
72
  });
73
73
 
74
+ it('keeps component-owned direction metadata over a colliding pass-through', () => {
75
+ render(
76
+ <FormLayout
77
+ direction="horizontal"
78
+ data-direction="vertical"
79
+ data-testid="layout">
80
+ content
81
+ </FormLayout>,
82
+ );
83
+ expect(screen.getByTestId('layout')).toHaveAttribute(
84
+ 'data-direction',
85
+ 'horizontal',
86
+ );
87
+ });
88
+
74
89
  // ─── Direction modes ────────────────────────────────────────────────────
75
90
 
76
91
  it('defaults to vertical direction', () => {
@@ -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
  */
@@ -153,6 +154,7 @@ export function FormLayout({
153
154
  <FormLayoutContext value={contextValue}>
154
155
  <div
155
156
  ref={ref}
157
+ {...props}
156
158
  {...mergeProps(
157
159
  themeProps('form-layout', {direction}),
158
160
  stylex.props(
@@ -163,8 +165,7 @@ export function FormLayout({
163
165
  ),
164
166
  className,
165
167
  style,
166
- )}
167
- {...props}>
168
+ )}>
168
169
  {children}
169
170
  </div>
170
171
  </FormLayoutContext>
@@ -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';
@@ -24,7 +24,12 @@ verified_by:
24
24
  packages/core/src/theme/themingTargets.test.ts,
25
25
  scripts/check-knowledge.mjs,
26
26
  ]
27
- modules: [module:Table/useTableRowStatus, module:Table/useTableColumnResize]
27
+ modules:
28
+ [
29
+ module:Table/useTableActivation,
30
+ module:Table/useTableRowStatus,
31
+ module:Table/useTableColumnResize,
32
+ ]
28
33
  families: []
29
34
  design_specs: []
30
35
  architecture:
@@ -0,0 +1,254 @@
1
+ ---
2
+ schema_version: 3
3
+ template_version: 4
4
+ kind: module
5
+ id: module:Table/useTableActivation
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:
13
+ [public-api, behavior, layout, theming, accessibility, interaction]
14
+ verified_by:
15
+ [
16
+ packages/core/src/Table/plugins/activation/useTableActivation.test.tsx,
17
+ packages/core/src/Table/__tests__/TableActivation.a11y.chromium.spec.ts,
18
+ packages/core/src/Table/Table.perf.test.tsx,
19
+ scripts/check-knowledge.mjs,
20
+ ]
21
+ parent_component: component:Table
22
+ references:
23
+ [
24
+ architecture:component-theming-surface,
25
+ architecture:interaction-modality,
26
+ architecture:public-component-api,
27
+ spec:AST-002,
28
+ spec:AST-009,
29
+ ]
30
+ ---
31
+
32
+ # useTableActivation module contract
33
+
34
+ ## Contract at a glance
35
+
36
+ | Area | Contract |
37
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
38
+ | Public contract | Candidate `useTableActivation<T>` hook returning one `TablePlugin<T>` from controlled `activeKey`, `onActivate`, and optional row eligibility. V1 has one press interaction model and consumes Table's canonical resolved row key rather than introducing a second extractor. |
39
+ | Behavior | Eligible rows request activation from a primary pointer click or Enter/Space after earlier row handlers run. Cancellation, nested or portalled controls, repeated keys, and competing whole-row owners do not trigger duplicate activation; native row semantics remain intact. |
40
+ | End-user impact | People can keep one current row visually anchored while it drives adjacent detail or preview content, without losing cell controls or other Table behavior. |
41
+ | Builder impact | Builders own the active key and response to activation. The plugin owns row-state reflection and one whole-row activation path; it does not own routing, detail content, or selection. |
42
+ | Compatibility/readiness | Additive, unreleased module. This draft is not implementation authority. Canonical row identity, competing activation-owner enforcement, ineligible active-state treatment, and active-state paint require owner decisions before promotion. |
43
+ | Review checks | Reject selection coupling, a second row-key extractor, duplicate keys activating multiple rows, implicit navigation, button/link roles on `<tr>`, default `aria-current`, hover activation, non-primary or repeated activation, missing Enter/Space metadata, clobbered or uncancellable handlers, activation from nested/portalled controls or an in-row selection gesture, active paint that fails on pinned cells, and plugin identity churn for stable inputs. |
44
+ | Governing rules | `component:Table` FR9-FR12 and ORD1-ORD4 own plugin composition; `architecture:interaction-modality` owns shared input-modality rules; `architecture:public-component-api` and `spec:AST-002` own public caller burden; `spec:AST-009` owns accessibility-evidence escalation. |
45
+
46
+ This table is a review projection; the body below is authoritative.
47
+
48
+ ## Intent
49
+
50
+ `useTableActivation` gives a data-driven Table one controlled current product-row state
51
+ for master-detail, preview, and inspector layouts. It consumes identity through the
52
+ parent dependency in OQ6, requests activation, reflects active state, composes with
53
+ existing row behavior, and keeps state visually coherent through pinned cells.
54
+
55
+ Activation means “this row currently drives adjacent product state.” It is not a
56
+ selected-set operation, a navigation promise, or focus ownership. Those distinctions
57
+ are part of the module contract rather than left to each product implementation.
58
+
59
+ ## Compatibility and migration
60
+
61
+ - Released default preserved: `yes`; Table behavior remains unchanged unless the new plugin is supplied.
62
+ - Compatibility class: additive public hook, types, documentation, and examples.
63
+ - Migration decision: `module:Table/useTableActivation/DEC-1` after the remaining identity, ownership, eligibility, and active-presentation decisions are approved.
64
+
65
+ Consumer migration instructions belong in consumer docs and release notes.
66
+
67
+ ## Ownership boundary
68
+
69
+ **Owns**
70
+
71
+ - The public `useTableActivation` hook, its configuration, and the controlled active-row contract.
72
+ - Consumption of Table's canonical resolved row key, row eligibility, activation request boundaries, and active-state reflection.
73
+ - Composition with existing row handlers and first-party Table plugins, including existing-handler-first cancellation through `preventDefault`.
74
+ - Suppression of accidental activation from nested or portalled interactive content, non-primary clicks, repeated key presses, and active text selection.
75
+ - Primary-pointer and Enter/Space parity while preserving native row semantics, prior focus ownership, and visible focus.
76
+ - One whole-row activation owner; selection, expansion, tree controls, and other non-owning row behavior may compose without duplicate activation.
77
+ - Active-row paint, pinned-cell coherence, accessibility, performance, and evidence.
78
+
79
+ **Does not own / non-goals**
80
+
81
+ - Aggregate `TablePlugin` protocol, transform order, failure isolation, or plugin-array identity — owned by `component:Table`.
82
+ - Selection state, checkbox semantics, or `aria-selected` — owned by `useTableSelection` and its caller.
83
+ - Routing, URLs, link semantics, or `aria-current="page"` — owned by a navigation surface, not activation.
84
+ - Adjacent detail content, preview loading, prefetching, or focus movement — owned by the product or another module.
85
+ - Cell controls. Buttons, links, inputs, editable content, and portalled controls remain independently operable and do not activate their row as a side effect.
86
+ - Hover-driven activation. Hover may remain a visual enhancement, but v1 never changes controlled active state from pointer movement alone.
87
+ - Children-mode rows. The plugin pipeline applies to data-driven rows only.
88
+ - A new public theme target. The active row remains part of the existing `table-row` anatomy unless a separate theming decision proves another owner is necessary.
89
+
90
+ ## Public API and concepts
91
+
92
+ The candidate public shape is:
93
+
94
+ ```ts
95
+ export interface UseTableActivationConfig<T extends Record<string, unknown>> {
96
+ activeKey: string | number | null;
97
+ onActivate: (item: T, key: string | number) => void;
98
+ getIsItemActivatable?: (item: T) => boolean;
99
+ }
100
+
101
+ export function useTableActivation<T extends Record<string, unknown>>(
102
+ config: UseTableActivationConfig<T>,
103
+ ): TablePlugin<T>;
104
+ ```
105
+
106
+ Consumer docs own final signatures, defaults, imports, and examples after promotion.
107
+
108
+ | Concept | Closed values or states | Meaning | Default | Owner | Stability |
109
+ | -------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | --------------------------------- | ------------------------------ |
110
+ | Active key | Stable unique `string`, `number`, or `null` | Matches Table's one validated canonical row key; `null` means no active row. | Controlled `null` | Caller through this module | Candidate |
111
+ | Row eligibility | Product row eligible or inert; synthetic group header inert | Distinguishes product activation from grouping controls before applying caller eligibility. | Every product row eligible; group headers excluded | `module:Table/useTableActivation` | Parent distinction pending OQ7 |
112
+ | Activation request | `(item, key)` callback | Asks the caller to update controlled state; the plugin never mutates `activeKey`. | No implicit state update | Caller through this module | Candidate |
113
+ | Activation inputs | Primary pointer click, Enter, or Space | One press interaction model requests activation; hover and auxiliary clicks never change active state. | All three paths enabled | `module:Table/useTableActivation` | Candidate |
114
+ | Active presentation | Inactive or active | Reflects controlled state on the existing row anatomy and through pinned cells. | Inactive | `module:Table/useTableActivation` | Unsettled |
115
+ | Selection/navigation | Independent | Activation neither changes selection nor claims a route, destination, or navigation role. | Unchanged | Their respective owners | Required |
116
+
117
+ ## Behavioral contract
118
+
119
+ | ID | Candidate invariant | Basis | Draft review state |
120
+ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------ |
121
+ | FR1 | One mounted hook instance returns one stable `TablePlugin`, and each mounted row keeps a stable composed activation handler while controlled keys or current callbacks change. | `component:Table` FR12 and PR1; existing stable-identity plugin evidence | Settled candidate |
122
+ | FR2 | `activeKey` is fully controlled. Activation depends on the unapproved parent contract in OQ6: one explicit stable unique `idKey`, its resolved key and validity available to activation, and duplicate-key rows inert in all builds with one deduplicated development warning. Current Table derives keys but does not validate them or pass them to row transforms. The candidate compares `activeKey` with the resolved key using strict equality and emits that same key: a property-name `idKey` resolves `String(item[idKey])`, so `"42"` matches and `42` does not; a function preserves its returned string or number. Without `idKey`, activation is inert with one deduplicated development warning, not keyed by row index. No second extractor or activation-side coercion is added. | One row identity across parent and module | Parent API decision required |
123
+ | FR3 | An eligible row whose validated canonical key equals `activeKey` exposes a stable `data-active` state and component-owned active presentation; all other rows remove both. No default `aria-current` or `aria-selected` is inferred. | Activation/selection/navigation boundary | Settled candidate |
124
+ | FR4 | An ineligible product row contributes no activation handler, keyboard affordance, active paint, or active-state attribute even if its key equals the controlled key, subject to OQ5. Synthetic `useTableGroupedRows` headers are always ineligible for product activation, including when an eligibility predicate returns true; their existing group toggle and disclosure button remain independent. Safe product/header distinction is an OQ7 parent dependency; grouped composition is not admitted until that boundary is enforceable. | Disabled interaction boundary | Verify whether controlled active state should remain visible |
125
+ | FR5 | A primary pointer click, Enter, or Space calls `onActivate(item, key)` once. The module runs each earlier accumulated handler first, preserves its side effects, and stops before activation when that handler calls `preventDefault`; it never replaces a handler already present when its transform runs. | Parent sequential plugin contract; proven interaction composition | Settled candidate |
126
+ | FR6 | An event originating in a nested button, link, input, select, textarea, editable surface, equivalent interactive role, or portalled control outside the row does not activate the row. Text selection suppresses activation only when the non-collapsed selection associated with that gesture intersects the activated row. | Accidental-activation prevention | Settled candidate |
127
+ | FR7 | Each eligible row is a sequential focus stop by default and activates on Enter or Space while retaining native `<tr>` semantics and visible focus. An earlier plugin's explicit `tabIndex`, including `-1` for roving focus, remains authoritative; a programmatically focused row still activates. The row advertises `Enter Space` through composed `aria-keyshortcuts` metadata. Promotion evidence covers representative large unvirtualized tables because the default sequential path grows with mounted eligible rows. | Proven keyboard model; WCAG 2.2 SC 2.1.1, 2.4.7, and 2.4.11 | Settled candidate |
128
+ | FR8 | Only a primary-button click activates; auxiliary clicks remain available to earlier handlers. Keyboard auto-repeat never activates. Repeated Space remains default-prevented so holding it cannot scroll the page, while repeated Enter is ignored without claiming a browser default. Pointer hover never changes `activeKey` in v1. | Proven press boundary; shared modality rules | Settled candidate |
129
+ | FR9 | Activation is independent from selection, expansion, tree controls, context menus, sorting, and navigation. Non-owning plugins retain their handlers and controlled state. A product row has at most one whole-row activation owner; expansion configured with `hasRowClickExpansion` and future navigation are competing owners. Synthetic `useTableGroupedRows` headers remain grouping-owned and ineligible under FR4. OQ7 requires parent ownership enforcement and product/header distinction; plugin insertion order must not decide whether a group toggle also calls product `onActivate`. | Module boundary, single ownership, and plugin composition | Parent enforcement decision required |
130
+ | FR10 | Active presentation uses a component-owned fallback on existing row anatomy. OQ3 owns the unresolved paint precedence across striping, hover, focus, selection's row wash, `hasRowHighlight`, and active state; no selected-plus-active combination is implementation-ready without that decision. Each admitted combination defines one effective fill/overlay for both normal and pinned cells and a distinguishable active cue independent of colour. `hasRowHighlight=false` continues to disable selection-owned wash, not selection semantics; whether activation replaces the current detail-panel background-reclamation use case remains an OQ3 decision. No theme-authored `active` state is implied. | Current private row overlay and theming-surface boundaries | Human visual decision |
131
+ | FR11 | Children mode, empty state, and rows excluded by upstream transforms remain unchanged. Any module-owned resource usage is bounded by mounted rows, fully released when the owning row or Table detaches, and produces no activation or global side effect after release. | Observable resource limits; implementation freedom | Settled candidate |
132
+ | FR12 | For valid unique canonical keys, changing only `activeKey` does not replace the plugin array, row handlers, columns, or product data and does not React-rerender unrelated rows. At most the old active and new active mounted rows change their reflected state/presentation. | Bounded-update performance requirement and parent row memoisation | Settled candidate |
133
+
134
+ ### Transformation and precedence order
135
+
136
+ - **ORD1 — Eligibility before state.** Distinguish product rows from synthetic grouped headers through the approved OQ7 parent contract, then resolve product eligibility. Group headers remain ineligible regardless of the caller predicate; ineligible rows return prior accumulated row props unchanged.
137
+ - **ORD2 — Controlled state.** The OQ6 dependency supplies one validated canonical key. Compare it with `activeKey` using FR2's strict, type-preserving equality and emit the same key; never derive a second identity or infer active state from focus or a prior event. Missing explicit identity or duplicate-key rows are inert.
138
+ - **ORD3 — Existing handler.** Run the accumulated handler for that event before the module's request. A handler that calls `preventDefault` cancels activation without discarding its own side effects.
139
+ - **ORD4 — Event boundary.** Nested interactive descendants, portalled events outside the row, and a non-collapsed selection intersecting the activated row suppress the module request without suppressing descendant behavior; an unrelated selection elsewhere does not.
140
+ - **ORD5 — Accepted press.** After the prior handler and boundary guards, accept only a primary click or non-repeated Enter/Space. Repeated Space prevents scrolling without activating; hover and auxiliary clicks never activate.
141
+ - **ORD6 — Presentation.** Apply inactive or active state after eligibility while preserving prior classes, styles, refs, attributes, and children.
142
+ - **ORD7 — Parent pipeline.** Relative order with non-owning plugins remains the order supplied by `component:Table`; this module creates no private second pipeline. Competing whole-row owners follow the OQ7 parent enforcement outcome.
143
+
144
+ ### Performance and resources
145
+
146
+ - **PR1 — Constant module overhead.** Canonical-key comparison, state reflection, and handler guards add O(1) module-owned work per relevant row update. The module invokes `getIsItemActivatable` at most once when resolving that row update; caller callback complexity is outside the module's guarantee and is measured separately by call count.
147
+ - **PR2 — Stable public identities.** The returned plugin object, Table's resolved plugin array, and mounted row activation handlers stay referentially stable across `activeKey`, callback, and eligibility-function changes while still observing their current values.
148
+ - **PR3 — Bounded active update.** For valid unique keys, changing only `activeKey` changes reflected state/presentation on at most the old active and new active mounted rows and does not React-rerender unrelated rows.
149
+ - **PR4 — Bounded lifetime.** Module-owned retained resources scale no faster than mounted rows, are released when their owning row/Table detaches, and leave no post-release callback or global side effect. Equivalent implementations that meet those observable bounds remain valid.
150
+
151
+ ## Accessibility contract
152
+
153
+ - **AR1 — Native table semantics.** Body rows retain their native row role; the module does not claim button, link, option, or gridcell semantics it cannot fulfil.
154
+ - **AR2 — Keyboard parity.** Eligible rows are focusable by default, preserve an earlier roving-focus `tabIndex`, activate on Enter/Space, compose `aria-keyshortcuts="Enter Space"`, and keep visible unobscured focus without replacing native row semantics (WCAG 2.2 SC 2.1.1, 2.4.7, and 2.4.11).
155
+ - **AR3 — Nested and portalled controls.** Interactive descendants and portalled controls remain reachable and operable without activating the row (WCAG 2.2 SC 2.1.1).
156
+ - **AR4 — State honesty.** The module does not emit `aria-selected` or default `aria-current`; active presentation alone does not claim selection or navigation semantics.
157
+ - **AR5 — Non-colour state.** If active state communicates current position required to use an adjacent view, its focus/current treatment cannot rely on colour alone (WCAG 2.2 SC 1.4.1). The exact representation is blocked on OQ3.
158
+ - **AR6 — No hover-only activation.** Hover may enhance row paint, but it never changes active state; pointer, touch, and keyboard users share the press-owned activation outcome (WCAG 2.2 SC 2.1.1 and `architecture:interaction-modality` INV4).
159
+ - **AR7 — Proportionate evidence.** Unit, DOM, accessibility-tree, axe, and real-browser keyboard evidence prove static semantics, nested-control isolation, focus, and reflected state. Real AT is added only if an approved outcome depends on announcement, browse/virtual-cursor behavior, or a recorded AT/browser divergence, as required by `spec:AST-009`.
160
+
161
+ ## Design relationships
162
+
163
+ | Anatomy or state | Design requirement | Representation authority | Module contract |
164
+ | -------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------ |
165
+ | Activatable row | Communicates availability without replacing native table structure or cell controls. | This module plus `architecture:interaction-modality` | FR5-FR9, AR1-AR3 |
166
+ | Active row | Remains visually traceable through normal and pinned cells. | Component-owned fallback; public theme-state admission remains OQ3 | FR3, FR10, AR4-AR5 |
167
+ | Focused row | Shows keyboard focus independently from controlled active state. | Shared focus rules and existing row anatomy | FR2, FR7, AR2, AR5 |
168
+ | Nested cell control | Keeps its own interaction, focus, and semantic contract. | The nested component | FR6, FR9, AR3 |
169
+ | Selection/navigation | Never inferred from activation. | Selection or navigation owner | FR2, FR3, FR9, AR4 |
170
+
171
+ The default contract adds no public theme target or theme-authored `active` state. The module owns a component fallback and must propagate its effective overlay to pinned cells. OQ3 may admit an `active` state on the existing `table-row` target only if it defines the fallback, guaranteed paint properties, state precedence, and pinned-overlay projection required by `architecture:component-theming-surface`. A separate direct target still requires another theming decision.
172
+
173
+ ## Parent and system relationships
174
+
175
+ - `component:Table` owns the aggregate `TablePlugin` protocol, canonical ordering, sequential transforms, failure isolation, and stable plugin-array identity. Its current contract does not promise validated canonical identity or key delivery to activation. OQ6 requires a separately approved parent addition for explicit stable unique `idKey` validation, resolved-key equality, and duplicate-key inertness; OQ7 requires a separately approved ownership and product/header distinction contract. This module neither supplies nor approves those missing parent contracts.
176
+ - `architecture:interaction-modality` owns cross-component keyboard/pointer modality rules; this module owns only their row-activation projection.
177
+ - `architecture:public-component-api` and `spec:AST-002` own additive API discipline and caller burden. The module exposes controlled product state rather than routing or data-fetching dependencies.
178
+ - `spec:AST-009` owns accessibility-evidence escalation. This module uses lower-layer semantic and browser evidence unless an approved outcome depends on assistive-technology behavior.
179
+ - `useTableSelection`, tree, expansion, context-menu, sticky-column, and future navigation modules retain their independent state and semantics.
180
+
181
+ ## Verification map
182
+
183
+ | Contract | Verification | Representative states | Mutation or failure expectation |
184
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
185
+ | FR1-FR4, FR11-FR12, PR1-PR4 | Public plugin/handler identity, controlled render/mutation/callback counts, invalid-key fallback, and resource-lifetime tests | Active, inactive, `null`, eligible, ineligible, missing `idKey`, property-key `"42"` versus numeric `42`, function-returned numeric keys, duplicate resolved keys, grouped headers, callback/key changes | Plugin/handlers churn, duplicate-key rows activate, unrelated rows rerender, more than two valid-key rows mutate, callbacks overrun, or resources leak. |
186
+ | FR5-FR9, ORD1-ORD7 | Unit/browser interaction tests with earlier handlers and selection, grouped-row headers, control-only expansion, `hasRowClickExpansion`, tree, sticky, context-menu, navigation-owner, and custom plugins | Primary/auxiliary pointer, Enter/Space, auto-repeat, prior `tabIndex`, grouped headers and product rows in both plugin orders, eligibility override, nested/portalled controls, in-row/unrelated text selection, cancellation | Handlers are replaced, callbacks fire twice, auxiliary/repeated/hover input activates, Space repeat scrolls, earlier focus ownership is lost, unrelated selection blocks activation, nested/portalled controls activate rows, or two whole-row owners fire. |
187
+ | FR10, AR5-AR6 | Real-browser screenshots in light/dark, striped, hover, pinned-column, keyboard-focus, touch emulation, and forced-colors states | OQ3's complete selection/activation/`hasRowHighlight` matrix, selection-first and activation-first updates, focus/hover, light/dark, and normal/pinned cells | Component fallback or approved theme state disappears under pinned cells, relies on colour alone, loses focus indication, or hover changes active state. |
188
+ | AR1-AR7 | Unit/DOM/accessibility-tree tests, axe, and real-browser keyboard automation; real AT only for an adopted AT-dependent outcome | Native traversal in representative small and large unvirtualized tables, default/roving/programmatic focus, Enter/Space metadata and activation, nested link/button/input, portalled control, state update | Row semantics are replaced, Enter/Space is undiscoverable or unavailable, focus order becomes incoherent at representative scale, earlier focus ownership is lost, nested controls activate, or selection/navigation semantics are falsely exposed. |
189
+ | Plugin composition | Focused integration matrix plus existing Table selection/tree/expansion/sticky/context-menu suites | Every supported first-party combination and custom row handler order | Enabling activation breaks another plugin, changes column order, or loses accumulated row props. |
190
+ | Public API and documentation | Public-subpath compile fixture including required explicit `idKey` and parent-supplied validated canonical row key, generated inventory, `.doc.mjs`, Storybook, and one recipe | Generic row type, controlled update, eligibility, duplicate keys, primary click, Enter/Space | A second extractor or mode switch appears, duplicate behavior is undefined, the hook/types are unavailable, or docs imply hover activation. |
191
+ | Performance | Extend `Table.perf.test.tsx` with plugin/handler identity, render and DOM-mutation counts, eligibility-callback counts, stable no-op, and adjacent valid-key changes | Large representative data, same key, adjacent key, callbacks changed | One key change replaces the plugin path, rerenders unrelated rows, mutates more than old/new rows, over-invokes eligibility, or exceeds budgets. |
192
+ | Structural ownership | `pnpm check:knowledge` | Draft module and parent backlink | A missing, orphaned, misnamed, wrong-kind, or duplicate module record passes validation. |
193
+
194
+ ## Decision log
195
+
196
+ ### DEC-1 — Activation is controlled and independent
197
+
198
+ **Reference:** `module:Table/useTableActivation/DEC-1`
199
+ **Decider:** pending owner approval
200
+
201
+ Activation represents one caller-owned current product row. It depends on OQ6's parent validation and key projection, not a validation guarantee in current Table. The candidate requires explicit stable unique `idKey`, compares the resolved key with `activeKey` as specified in FR2, and emits the same key; missing identity and duplicate-key rows remain inert. Grouped headers do not become product rows under an eligibility override. The module never owns product state, selection, routing, focus, detail content, or prefetching.
202
+
203
+ Rejected: a second activation-specific key extractor, deriving activation from selection or focus, storing an internal fallback key, or combining row navigation and activation into one hook.
204
+
205
+ ### DEC-2 — Preserve native row semantics
206
+
207
+ **Reference:** `module:Table/useTableActivation/DEC-2`
208
+ **Decider:** pending owner approval
209
+
210
+ Whole-row convenience keeps native semantic table-row structure. Eligible rows are focusable by default, preserve an earlier roving-focus `tabIndex`, advertise Enter/Space through `aria-keyshortcuts`, and activate from primary click, Enter, or Space without claiming button or link semantics.
211
+
212
+ Rejected: `role="button"` or `role="link"` on `<tr>`, pointer-only activation, and
213
+ implicit `aria-selected` or `aria-current`.
214
+
215
+ ### DEC-3 — Active state uses existing row anatomy
216
+
217
+ **Reference:** `module:Table/useTableActivation/DEC-3`
218
+ **Decider:** pending owner approval
219
+
220
+ The module reflects active state through existing row anatomy, with one effective fill/overlay reaching normal and pinned cells. OQ3 owns the exact representation and precedence, including selection's wash and `hasRowHighlight`; neither selected-plus-active precedence nor a replacement for the detail-panel background-reclamation path is implied. A second target is not justified. Theme-authored `active` state is admitted only if OQ3 defines fallback, properties, precedence, and pinned projection.
221
+
222
+ Rejected: styling only transparent row backgrounds that disappear under pinned cells, implying that the current `table-row` target already accepts an undeclared `active` state, and adding a public `table-row-activation` target without an independent theme-author need.
223
+
224
+ ### DEC-4 — Press owns activation and earlier handlers may cancel
225
+
226
+ **Reference:** `module:Table/useTableActivation/DEC-4`
227
+ **Decider:** pending owner approval
228
+
229
+ The module has one press interaction model. It runs the accumulated handler first, treats `preventDefault` as cancellation, and then accepts only a primary click or non-repeated Enter/Space outside nested or portalled controls and intersecting text selection. Repeated Space prevents page scrolling without activation. A product row has at most one whole-row activation owner; grouped headers remain grouping-owned and ineligible. OQ7 owns the missing parent distinction and enforcement contract.
230
+
231
+ Rejected: hover activation, replacing earlier handlers, activating after cancellation, and activating from auxiliary clicks or key auto-repeat.
232
+
233
+ ## Open questions
234
+
235
+ - **OQ3 — How is required current state communicated?** (`human-design`) Decide the component-owned fallback, focus-versus-active precedence, non-colour cue, and any admitted `active` state on the existing `table-row` target. The decision must specify every state in the matrix below, including whether selected-plus-active uses selection-dominant fill with a distinct active cue or active-dominant fill with a persistent selection cue. `hasRowHighlight=false` removes selection wash while preserving selection semantics; it is not an activation disable flag. Decide whether an active row driving detail content replaces the current background-reclamation use case or needs another explicit caller choice. Each outcome must define the same effective paint on normal and pinned cells, including selection/active changes in either order. No active or selected-plus-active paint is admitted while this question remains open. An opt-in caller-supplied `aria-current` value, if warranted for genuine current-item sets, is a separate decision; default selection/navigation semantics remain forbidden.
236
+
237
+ | Selection | Activation | `hasRowHighlight` | Required outcome before admission |
238
+ | ---------- | ---------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
239
+ | Unselected | Inactive | Either | Existing striping, hover, and focus treatment. |
240
+ | Selected | Inactive | `true` | Existing selection wash and selection semantics on normal and pinned cells. |
241
+ | Selected | Inactive | `false` | No selection-owned wash; selection semantics remain. |
242
+ | Unselected | Active | Either | OQ3 defines active fill, non-colour cue, and focus/hover precedence independently of the selection flag. |
243
+ | Selected | Active | `true` | OQ3 chooses selection-versus-active fill precedence and preserves distinguishable selection, active state, and focus on normal and pinned cells. |
244
+ | Selected | Active | `false` | OQ3 defines active paint without reintroducing selection wash; selection semantics remain. |
245
+
246
+ - **OQ5 — Should an ineligible product row matching `activeKey` remain visibly active?** (`human-api`) Decide whether caller eligibility governs interaction only or both interaction and controlled presentation for real product rows. This decision does not relax FR4's exclusion of synthetic grouped headers.
247
+ - **OQ6 — How does Table expose validated canonical row identity?** (`human-api`) Approve the missing parent-owned validation and key-delivery contract. Current Table resolves an index when `idKey` is omitted, `String(item[idKey])` for a property name, or the function's returned key; it does not validate uniqueness or deliver the key to `transformBodyRow`. The activation candidate requires explicit stable unique identity, FR2's strict resolved-key equality and same-key callback, inert missing/duplicate identity in all builds, and deduplicated development diagnostics. The approved parent record must own these additions and their compatibility; this module adds no activation-specific extractor or coercion.
248
+ - **OQ7 — How does Table enforce whole-row ownership and product eligibility?** (`human-api`) Approve the missing parent contract for distinguishing caller product rows from synthetic `useTableGroupedRows` headers and enforcing FR4's default header ineligibility. Group headers keep their grouping-owned click and disclosure-button paths; `hasRowClickExpansion` and future navigation compete with activation on product rows. Define deterministic rejection or neutralisation, development diagnostics, and production fallback for every admitted combination without making plugin insertion order select an owner. Grouping integration remains unsupported until its distinction and ownership boundary are enforceable; key prefixes or private marker inspection are not caller obligations.
249
+
250
+ ## Content boundary
251
+
252
+ This record does not duplicate consumer signatures/examples, the parent Table plugin
253
+ protocol or ordering, shared input-modality rules, current usage audits, implementation
254
+ steps, or navigation/selection/prefetch contracts. It links to their canonical owners.