@astryxdesign/core 0.7.6-canary.eecd9c1 → 0.7.6-canary.f73623d

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.
Files changed (182) hide show
  1. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  2. package/dist/DropdownMenu/DropdownMenu.js +28 -19
  3. package/dist/Outline/Outline.d.ts.map +1 -1
  4. package/dist/Outline/Outline.js +27 -0
  5. package/dist/Popover/usePopover.d.ts.map +1 -1
  6. package/dist/Popover/usePopover.js +10 -3
  7. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  8. package/dist/RadioList/RadioListItem.js +5 -2
  9. package/dist/Table/Table.d.ts.map +1 -1
  10. package/dist/Table/Table.js +36 -10
  11. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  12. package/dist/Table/plugins/sortable/useTableSortable.js +46 -58
  13. package/dist/ToggleButton/ToggleButtonGroup.d.ts +7 -1
  14. package/dist/ToggleButton/ToggleButtonGroup.d.ts.map +1 -1
  15. package/dist/ToggleButton/ToggleButtonGroup.js +16 -2
  16. package/dist/Token/Token.d.ts.map +1 -1
  17. package/dist/Token/Token.js +37 -5
  18. package/dist/Token/TokenLink.d.ts.map +1 -1
  19. package/dist/Token/TokenLink.js +14 -2
  20. package/dist/i18n/generated-locales/af-ZA.generated.d.ts.map +1 -1
  21. package/dist/i18n/generated-locales/af-ZA.generated.js +0 -5
  22. package/dist/i18n/generated-locales/ar-SA.generated.d.ts.map +1 -1
  23. package/dist/i18n/generated-locales/ar-SA.generated.js +0 -5
  24. package/dist/i18n/generated-locales/ca-ES.generated.d.ts.map +1 -1
  25. package/dist/i18n/generated-locales/ca-ES.generated.js +0 -5
  26. package/dist/i18n/generated-locales/cs-CZ.generated.d.ts.map +1 -1
  27. package/dist/i18n/generated-locales/cs-CZ.generated.js +0 -5
  28. package/dist/i18n/generated-locales/da-DK.generated.d.ts.map +1 -1
  29. package/dist/i18n/generated-locales/da-DK.generated.js +0 -5
  30. package/dist/i18n/generated-locales/de-DE.generated.d.ts.map +1 -1
  31. package/dist/i18n/generated-locales/de-DE.generated.js +0 -5
  32. package/dist/i18n/generated-locales/el-GR.generated.d.ts.map +1 -1
  33. package/dist/i18n/generated-locales/el-GR.generated.js +0 -5
  34. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  35. package/dist/i18n/generated-locales/en.generated.js +1 -5
  36. package/dist/i18n/generated-locales/es-ES.generated.d.ts.map +1 -1
  37. package/dist/i18n/generated-locales/es-ES.generated.js +0 -5
  38. package/dist/i18n/generated-locales/fi-FI.generated.d.ts.map +1 -1
  39. package/dist/i18n/generated-locales/fi-FI.generated.js +0 -5
  40. package/dist/i18n/generated-locales/fr-FR.generated.d.ts.map +1 -1
  41. package/dist/i18n/generated-locales/fr-FR.generated.js +0 -5
  42. package/dist/i18n/generated-locales/he-IL.generated.d.ts.map +1 -1
  43. package/dist/i18n/generated-locales/he-IL.generated.js +0 -5
  44. package/dist/i18n/generated-locales/hu-HU.generated.d.ts.map +1 -1
  45. package/dist/i18n/generated-locales/hu-HU.generated.js +0 -5
  46. package/dist/i18n/generated-locales/it-IT.generated.d.ts.map +1 -1
  47. package/dist/i18n/generated-locales/it-IT.generated.js +0 -5
  48. package/dist/i18n/generated-locales/ja-JP.generated.d.ts.map +1 -1
  49. package/dist/i18n/generated-locales/ja-JP.generated.js +0 -5
  50. package/dist/i18n/generated-locales/ko-KR.generated.d.ts.map +1 -1
  51. package/dist/i18n/generated-locales/ko-KR.generated.js +0 -5
  52. package/dist/i18n/generated-locales/nl-NL.generated.d.ts.map +1 -1
  53. package/dist/i18n/generated-locales/nl-NL.generated.js +0 -5
  54. package/dist/i18n/generated-locales/no-NO.generated.d.ts.map +1 -1
  55. package/dist/i18n/generated-locales/no-NO.generated.js +0 -5
  56. package/dist/i18n/generated-locales/pl-PL.generated.d.ts.map +1 -1
  57. package/dist/i18n/generated-locales/pl-PL.generated.js +0 -5
  58. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  59. package/dist/i18n/generated-locales/pseudo.generated.js +1 -5
  60. package/dist/i18n/generated-locales/pt-BR.generated.d.ts.map +1 -1
  61. package/dist/i18n/generated-locales/pt-BR.generated.js +0 -5
  62. package/dist/i18n/generated-locales/pt-PT.generated.d.ts.map +1 -1
  63. package/dist/i18n/generated-locales/pt-PT.generated.js +0 -5
  64. package/dist/i18n/generated-locales/ro-RO.generated.d.ts.map +1 -1
  65. package/dist/i18n/generated-locales/ro-RO.generated.js +0 -5
  66. package/dist/i18n/generated-locales/ru-RU.generated.d.ts.map +1 -1
  67. package/dist/i18n/generated-locales/ru-RU.generated.js +0 -5
  68. package/dist/i18n/generated-locales/sr-SP.generated.d.ts.map +1 -1
  69. package/dist/i18n/generated-locales/sr-SP.generated.js +0 -5
  70. package/dist/i18n/generated-locales/sv-SE.generated.d.ts.map +1 -1
  71. package/dist/i18n/generated-locales/sv-SE.generated.js +0 -5
  72. package/dist/i18n/generated-locales/tr-TR.generated.d.ts.map +1 -1
  73. package/dist/i18n/generated-locales/tr-TR.generated.js +0 -5
  74. package/dist/i18n/generated-locales/uk-UA.generated.d.ts.map +1 -1
  75. package/dist/i18n/generated-locales/uk-UA.generated.js +0 -5
  76. package/dist/i18n/generated-locales/vi-VN.generated.d.ts.map +1 -1
  77. package/dist/i18n/generated-locales/vi-VN.generated.js +0 -5
  78. package/dist/i18n/generated-locales/zh-CN.generated.d.ts.map +1 -1
  79. package/dist/i18n/generated-locales/zh-CN.generated.js +0 -5
  80. package/dist/i18n/generated-locales/zh-TW.generated.d.ts.map +1 -1
  81. package/dist/i18n/generated-locales/zh-TW.generated.js +0 -5
  82. package/locales/af-ZA.json +0 -21
  83. package/locales/ar-SA.json +0 -21
  84. package/locales/ca-ES.json +0 -21
  85. package/locales/cs-CZ.json +0 -21
  86. package/locales/da-DK.json +0 -21
  87. package/locales/de-DE.json +0 -21
  88. package/locales/el-GR.json +0 -21
  89. package/locales/en.json +3 -19
  90. package/locales/es-ES.json +0 -21
  91. package/locales/fi-FI.json +0 -21
  92. package/locales/fr-FR.json +0 -21
  93. package/locales/he-IL.json +0 -21
  94. package/locales/hu-HU.json +0 -21
  95. package/locales/it-IT.json +0 -21
  96. package/locales/ja-JP.json +0 -21
  97. package/locales/ko-KR.json +0 -21
  98. package/locales/nl-NL.json +0 -21
  99. package/locales/no-NO.json +0 -21
  100. package/locales/pl-PL.json +0 -21
  101. package/locales/pseudo.json +2 -14
  102. package/locales/pt-BR.json +0 -21
  103. package/locales/pt-PT.json +0 -21
  104. package/locales/ro-RO.json +0 -21
  105. package/locales/ru-RU.json +0 -21
  106. package/locales/sr-SP.json +0 -21
  107. package/locales/sv-SE.json +0 -21
  108. package/locales/tr-TR.json +0 -21
  109. package/locales/uk-UA.json +0 -21
  110. package/locales/vi-VN.json +0 -21
  111. package/locales/zh-CN.json +0 -21
  112. package/locales/zh-TW.json +0 -21
  113. package/package.json +4 -4
  114. package/src/Chat/ChatLayout.spec.md +41 -36
  115. package/src/Chat/ChatLayoutScrollButton.spec.md +87 -103
  116. package/src/Chat/ChatSendButton.spec.md +78 -67
  117. package/src/ContextMenu/ContextMenu.spec.md +84 -56
  118. package/src/ContextMenu/ContextMenuItem.spec.md +132 -0
  119. package/src/DropdownMenu/DropdownMenu.spec.md +198 -218
  120. package/src/DropdownMenu/DropdownMenu.test.tsx +117 -0
  121. package/src/DropdownMenu/DropdownMenu.tsx +33 -18
  122. package/src/DropdownMenu/DropdownMenuCheckboxItem.spec.md +192 -0
  123. package/src/DropdownMenu/DropdownMenuDivider.spec.md +25 -31
  124. package/src/DropdownMenu/DropdownMenuGroup.spec.md +145 -0
  125. package/src/DropdownMenu/DropdownMenuItem.spec.md +48 -42
  126. package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +34 -37
  127. package/src/DropdownMenu/DropdownMenuRadioItem.spec.md +54 -53
  128. package/src/DropdownMenu/DropdownMenuSubMenu.spec.md +47 -41
  129. package/src/EmptyState/EmptyState.spec.md +35 -29
  130. package/src/Outline/Outline.spec.md +165 -69
  131. package/src/Outline/Outline.test.tsx +67 -2
  132. package/src/Outline/Outline.tsx +23 -0
  133. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +3 -4
  134. package/src/Popover/Popover.test.tsx +44 -0
  135. package/src/Popover/usePopover.tsx +10 -3
  136. package/src/RadioList/RadioList.test.tsx +56 -0
  137. package/src/RadioList/RadioListItem.tsx +5 -2
  138. package/src/Table/Table.spec.md +1 -1
  139. package/src/Table/Table.test.tsx +32 -0
  140. package/src/Table/Table.tsx +43 -11
  141. package/src/Table/__tests__/Table.a11y.states.ts +1 -5
  142. package/src/Table/plugins/sortable/useTableSortable.test.tsx +73 -144
  143. package/src/Table/plugins/sortable/useTableSortable.tsx +44 -73
  144. package/src/Table/plugins/sortable/useTableSortableState.test.tsx +9 -21
  145. package/src/ToggleButton/ToggleButton.test.tsx +27 -0
  146. package/src/ToggleButton/ToggleButtonGroup.tsx +20 -1
  147. package/src/Token/Token.doc.mjs +2 -2
  148. package/src/Token/Token.spec.md +41 -51
  149. package/src/Token/Token.test.tsx +67 -0
  150. package/src/Token/Token.tsx +60 -5
  151. package/src/Token/TokenLink.tsx +21 -7
  152. package/src/i18n/generated-locales/af-ZA.generated.ts +0 -7
  153. package/src/i18n/generated-locales/ar-SA.generated.ts +0 -6
  154. package/src/i18n/generated-locales/ca-ES.generated.ts +0 -7
  155. package/src/i18n/generated-locales/cs-CZ.generated.ts +0 -6
  156. package/src/i18n/generated-locales/da-DK.generated.ts +0 -6
  157. package/src/i18n/generated-locales/de-DE.generated.ts +0 -6
  158. package/src/i18n/generated-locales/el-GR.generated.ts +0 -7
  159. package/src/i18n/generated-locales/en.generated.ts +1 -6
  160. package/src/i18n/generated-locales/es-ES.generated.ts +0 -7
  161. package/src/i18n/generated-locales/fi-FI.generated.ts +0 -6
  162. package/src/i18n/generated-locales/fr-FR.generated.ts +0 -7
  163. package/src/i18n/generated-locales/he-IL.generated.ts +0 -6
  164. package/src/i18n/generated-locales/hu-HU.generated.ts +0 -6
  165. package/src/i18n/generated-locales/it-IT.generated.ts +0 -7
  166. package/src/i18n/generated-locales/ja-JP.generated.ts +0 -6
  167. package/src/i18n/generated-locales/ko-KR.generated.ts +0 -6
  168. package/src/i18n/generated-locales/nl-NL.generated.ts +0 -6
  169. package/src/i18n/generated-locales/no-NO.generated.ts +0 -6
  170. package/src/i18n/generated-locales/pl-PL.generated.ts +0 -7
  171. package/src/i18n/generated-locales/pseudo.generated.ts +1 -6
  172. package/src/i18n/generated-locales/pt-BR.generated.ts +0 -7
  173. package/src/i18n/generated-locales/pt-PT.generated.ts +0 -7
  174. package/src/i18n/generated-locales/ro-RO.generated.ts +0 -6
  175. package/src/i18n/generated-locales/ru-RU.generated.ts +0 -7
  176. package/src/i18n/generated-locales/sr-SP.generated.ts +0 -6
  177. package/src/i18n/generated-locales/sv-SE.generated.ts +0 -6
  178. package/src/i18n/generated-locales/tr-TR.generated.ts +0 -7
  179. package/src/i18n/generated-locales/uk-UA.generated.ts +0 -7
  180. package/src/i18n/generated-locales/vi-VN.generated.ts +0 -6
  181. package/src/i18n/generated-locales/zh-CN.generated.ts +0 -6
  182. package/src/i18n/generated-locales/zh-TW.generated.ts +0 -6
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  schema_version: 3
3
- template_version: 6
3
+ template_version: 7
4
4
  kind: component
5
5
  id: component:ChatSendButton
6
- authority: draft
6
+ authority: current
7
7
  archive_reason: null
8
8
  superseded_by: null
9
- approved_by: null
10
- approved_at: null
9
+ approved_by: cixzhang
10
+ approved_at: 2026-10-31
11
11
  owners: [cixzhang]
12
12
  review_triggers: [public-api, behavior, theming, accessibility, testing]
13
13
  verified_by:
@@ -39,18 +39,17 @@ system_specs: [spec:AST-002, spec:AST-020, spec:AST-029, spec:AST-032]
39
39
 
40
40
  ## Contract at a glance
41
41
 
42
- | Area | Contract |
43
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | Public contract | The exported control selects send or stop presentation from props over ChatComposer context, accepts `sm` or `md`, supports icon overrides, and extends the rendered Button with supported Button DOM, event, styling, and ref inputs. |
45
- | Behavior | Send is disabled from explicit or composer readiness, stop remains enabled, state-specific actions run before a composed generic click handler, and current send artwork resolves from an explicit override before the shared fallback. |
46
- | End-user impact | People receive a translated, state-appropriate send or stop action whose availability follows composer readiness. |
47
- | Builder impact | Builders can use the context defaults or supply every state and action explicitly for standalone composition. |
48
- | Compatibility/readiness | This draft records current or remediated observable behavior. It adds no public prop, default, target, or compatibility promise. Consumers that used `onClick` to replace sending migrate that logic to `onSend`; implementation of the AST-032 component-slot requirement remains pending. |
49
- | Review checks | Reject lost send/stop routing, consumer events that replace the state action, a disabled stop action, unlabelled icon-only output, dropped root inputs, or send fallback drift from the observed explicit-then-shared order. |
50
- | Governing rules | `spec:AST-032/FR2-FR3` (current requirement, implementation pending with its owner); `architecture:public-component-api/INV5-INV8`; `architecture:interaction-modality`; `architecture:component-test-sufficiency`; `spec:AST-020`; `spec:AST-029`. |
42
+ | Area | Contract |
43
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | Public contract | The exported control selects send or stop presentation from props over ChatComposer context, accepts `sm` or `md`, supports icon overrides, and extends the rendered Button with supported Button DOM, event, styling, and ref inputs. |
45
+ | Behavior | Send is disabled from explicit or composer readiness, stop remains enabled, state-specific actions run before a composed generic click handler, and current send artwork resolves from an explicit override before the shared fallback. |
46
+ | End-user impact | People receive a translated, state-appropriate send or stop action whose availability follows composer readiness. |
47
+ | Builder impact | None. |
48
+ | Compatibility/readiness | Records shipped behavior; no public change. One implementation gap, listed under Current-state impact. |
49
+ | Review checks | Reject lost send/stop routing, consumer events that replace the state action, a disabled stop action, unlabelled icon-only output, dropped root inputs, or a lost explicit-first send icon or `arrowUp` fallback. |
50
+ | Governing rules | `spec:AST-032` FR2–FR3; `architecture:public-component-api` INV5–INV8; `architecture:interaction-modality`; `architecture:component-test-sufficiency`; `spec:AST-020`; `spec:AST-029`. |
51
51
 
52
- This table is a review projection. The draft body below records checkable current
53
- or remediated behavior and does not create product policy.
52
+ This table is a review projection; the body below is authoritative.
54
53
 
55
54
  ## Intent
56
55
 
@@ -60,13 +59,21 @@ icon, styling, and ref seams available for standalone composition.
60
59
 
61
60
  ## Compatibility and migration
62
61
 
63
- - Released default preserved: no public default changes are made by this audit.
64
- - Compatibility class: patch-compatible event-composition repair plus observational documentation and evidence coverage.
65
- - Controlled/uncontrolled behavior: not applicable; explicit props take precedence over context-derived values.
66
- - Migration decision: consumers that previously used `onClick` to replace sending should move that logic to `onSend`, because the repaired component now runs the state action and then the accepted generic click.
62
+ - Released default preserved: `yes`
63
+ - Compatibility class: records shipped behavior; no public change
64
+ - Controlled/uncontrolled behavior: not applicable; explicit props take
65
+ precedence over context-derived values
66
+ - Migration decision: none
67
67
 
68
68
  Consumer migration instructions belong in consumer docs and release notes.
69
69
 
70
+ ## Current-state impact
71
+
72
+ - **The send icon has no component-slot stage.** Send artwork resolves from
73
+ `sendIcon`, then the shared `arrowUp`. `spec:AST-032` FR2–FR3 put a
74
+ component-icon slot stage in that resolution; ChatSendButton does not offer
75
+ it yet (FR3).
76
+
70
77
  ## Ownership boundary
71
78
 
72
79
  **Owns**
@@ -90,34 +97,34 @@ Consumer migration instructions belong in consumer docs and release notes.
90
97
  Consumer syntax remains in `ChatSendButton.doc.mjs`. This inventory closes the
91
98
  observable semantic surface without duplicating the prop table.
92
99
 
93
- | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
94
- | ------------ | -------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------- | ----------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------- |
95
- | Action state | send; stop | Selects the name, Button variant, icon role, disabled rule, and state action. | Every render. | Composer `isStopShown`, otherwise send. | `component:ChatSendButton` | Observed released surface | Values outside the boolean prop type are unsupported. |
96
- | Availability | enabled; disabled | Prevents send activation when content is not ready. Stop intentionally ignores it. | Disabled applies only to send. | Explicit `isDisabled`, otherwise inverse composer canSend. | component and ChatComposer context | Observed released surface | Values outside the boolean prop type are unsupported. |
97
- | Action | state callback; composer fallback; composed generic click | Runs the state action and then an accepted consumer click handler. | Enabled send and stop activation. | `onSend` or submit current value; `onStop` or context stop. | component, context, and caller | Remediated observable path | With no state callback or matching context action, only a supplied generic click can run. |
98
- | Send icon | explicit content; shared fallback | Selects artwork for the send state. | Send state. | `sendIcon`, then shared `arrowUp`. | component and caller | Observed released surface | Unsupported React content follows the shared Button rendering boundary. |
99
- | Stop icon | explicit content; shared fallback | Selects artwork for the stop state. | Stop state. | `stopIcon`, then shared `stop`. | component and caller | Observed released surface | Unsupported React content follows the shared Button rendering boundary. |
100
- | Size | `sm`; `md` | Selects the delegated Button size while preserving circular treatment. | Both states. | `md`. | `component:ChatSendButton` | Observed released surface | Values outside the public union are rejected by TypeScript. |
101
- | Root surface | ref; supported Button DOM/data/ARIA/events; style/class/xstyle | Extends and styles the rendered Button without replacing component-owned action logic. | Every render. | Omitted. | `architecture:public-component-api` | Remediated observable path | Unsupported BaseProps omissions remain unsupported; owned semantics win accidental clashes. |
100
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
101
+ | ------------ | -------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------- | ----------------------------------- | --------- | ------------------------------------------------------------------------------------------- |
102
+ | Action state | send; stop | Selects the name, Button variant, icon role, disabled rule, and state action. | Every render. | Composer `isStopShown`, otherwise send. | `component:ChatSendButton` | Stable | Values outside the boolean prop type are unsupported. |
103
+ | Availability | enabled; disabled | Prevents send activation when content is not ready. Stop intentionally ignores it. | Disabled applies only to send. | Explicit `isDisabled`, otherwise inverse composer canSend. | component and ChatComposer context | Stable | Values outside the boolean prop type are unsupported. |
104
+ | Action | state callback; composer fallback; composed generic click | Runs the state action and then an accepted consumer click handler. | Enabled send and stop activation. | `onSend` or submit current value; `onStop` or context stop. | component, context, and caller | Stable | With no state callback or matching context action, only a supplied generic click can run. |
105
+ | Send icon | explicit content; shared fallback | Selects artwork for the send state. | Send state. | `sendIcon`, then shared `arrowUp`. | component and caller | Stable | Unsupported React content follows the shared Button rendering boundary. |
106
+ | Stop icon | explicit content; shared fallback | Selects artwork for the stop state. | Stop state. | `stopIcon`, then shared `stop`. | component and caller | Stable | Unsupported React content follows the shared Button rendering boundary. |
107
+ | Size | `sm`; `md` | Selects the delegated Button size while preserving circular treatment. | Both states. | `md`. | `component:ChatSendButton` | Stable | Values outside the public union are rejected by TypeScript. |
108
+ | Root surface | ref; supported Button DOM/data/ARIA/events; style/class/xstyle | Extends and styles the rendered Button without replacing component-owned action logic. | Every render. | Omitted. | `architecture:public-component-api` | Stable | Unsupported BaseProps omissions remain unsupported; owned semantics win accidental clashes. |
102
109
 
103
110
  ## Behavioral and layout contract
104
111
 
105
- | ID | Candidate invariant | Basis | Draft review state |
106
- | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
107
- | FR1 | The send state MUST render a primary icon-only Button named by the send translation, and it MUST be disabled when explicit or context-derived readiness is false. | Current implementation, docs, tests, `spec:AST-020`, and shared Button behavior | Verified current behavior |
108
- | FR2 | The stop state MUST render a secondary icon-only Button named by the stop translation, remain enabled regardless of send readiness, and invoke explicit or context stop behavior. | Current implementation, docs, and tests | Verified current behavior; the missing-handler public boundary is owned by `component:ChatComposer/OQ4` |
109
- | FR3 | Send icon resolution currently prefers `sendIcon` and otherwise resolves shared `arrowUp`. `spec:AST-032/FR2-FR3` separately owns a current requirement to add a component-icon slot; its implementation remains pending with that owner. | Current implementation, docs, tests, and current owner requirement in `spec:AST-032` | Verified shipped two-stage behavior; the required AST-032 stage is not described as shipped |
110
- | FR4 | Stop icon resolution currently prefers `stopIcon` and otherwise resolves the shared `stop` icon. | Current implementation, docs, and tests | Verified shipped behavior; this draft does not invent a stop component slot |
111
- | FR5 | Explicit state, disabled, callback, icon, and size props MUST win over their context or component fallback. A generic accepted click handler MUST compose after the selected state action rather than replace it. | Current implementation after audit repair plus `architecture:public-component-api/INV5-INV7` and API contributor guide | Settled objective repair with red-before-green focused tests |
112
- | FR6 | Supported Button DOM, data, ARIA, className, style, xstyle, and ref inputs MUST reach or compose on the rendered Button while component-owned label, state, and activation behavior remain intact. | Current implementation plus `architecture:public-component-api/INV5-INV8` | Verify through source, type checks, and focused tests |
113
- | FR7 | The component MUST paint through the shared Button on the co-located `chat-send-button` target and preserve its circular component treatment. | Current implementation, Chat consumer docs, and `architecture:component-theming-surface` | Verified current target and composition |
112
+ | ID | Invariant | Basis |
113
+ | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
114
+ | FR1 | The send state MUST render a primary icon-only Button named by the send translation, and it MUST be disabled when explicit or context-derived readiness is false. | Current implementation, docs, tests, `spec:AST-020`, and shared Button behavior |
115
+ | FR2 | The stop state MUST render a secondary icon-only Button named by the stop translation, remain enabled regardless of send readiness, and invoke explicit or context stop behavior. | Current implementation, docs, and tests |
116
+ | FR3 | Send icon resolution prefers `sendIcon` and otherwise resolves the shared `arrowUp`. Implementation gap: the component-icon slot stage `spec:AST-032` FR2–FR3 require is not offered (Current-state impact). | Current implementation, docs, tests, and current owner requirement in `spec:AST-032` |
117
+ | FR4 | Stop icon resolution prefers `stopIcon` and otherwise resolves the shared `stop` icon. | Current implementation, docs, and tests |
118
+ | FR5 | Explicit state, disabled, callback, icon, and size props MUST win over their context or component fallback. A generic accepted click handler MUST compose after the selected state action rather than replace it. | Current source and tests; `architecture:public-component-api` INV5–INV7 |
119
+ | FR6 | Supported Button DOM, data, ARIA, className, style, xstyle, and ref inputs MUST reach or compose on the rendered Button while component-owned label, state, and activation behavior remain intact. | Current implementation plus `architecture:public-component-api/INV5-INV8` |
120
+ | FR7 | The component MUST paint through the shared Button on the co-located `chat-send-button` target and preserve its circular component treatment. | Current implementation, Chat consumer docs, and `architecture:component-theming-surface` |
114
121
 
115
122
  ### Allowed variation
116
123
 
117
124
  - **AV1 — Context or standalone composition.** A builder may rely on ChatComposer
118
125
  context or provide all state, action, and disabled values explicitly.
119
126
  - **AV2 — Artwork.** Explicit send and stop content and shared fallback artwork may
120
- vary within the observed icon-resolution behavior.
127
+ vary within the icon resolution FR3 and FR4 state.
121
128
  - **AV3 — Consumer styling.** Supported styling inputs may extend the Button while
122
129
  component-owned state and activation behavior remain.
123
130
 
@@ -141,7 +148,9 @@ observable semantic surface without duplicating the prop table.
141
148
  - **ORD3 — Resolve availability.** Stop remains enabled. Send uses explicit
142
149
  `isDisabled`, then inverse composer readiness, then disabled outside context.
143
150
  - **ORD4 — Resolve artwork.** In send, explicit `sendIcon` wins, then shared
144
- `arrowUp`; in stop, explicit `stopIcon` wins, then shared `stop`.
151
+ `arrowUp`; in stop, explicit `stopIcon` wins, then shared `stop`. The
152
+ component-icon slot stage `spec:AST-032` FR2 requires is not offered yet
153
+ (FR3).
145
154
  - **ORD5 — Compose root.** Apply component-owned Button semantics and the
146
155
  `chat-send-button` target while preserving supported consumer root inputs.
147
156
 
@@ -163,14 +172,15 @@ observable semantic surface without duplicating the prop table.
163
172
 
164
173
  ## Design relationships
165
174
 
166
- | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
167
- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------ | ------------------ |
168
- | Action button | Circular compact Button carrying the current send or stop action. | Current source and consumer docs; exact tuning stays in code | Primary/secondary action | FR1, FR2, FR7 |
169
- | Send artwork | Upward send glyph unless explicit content wins; AST-032 currently requires a component slot whose implementation is pending. | Current source and docs; current requirement in `spec:AST-032/FR2-FR3` | Action cue | FR3 |
170
- | Stop artwork | Shared stop glyph unless explicit content wins. | Current source and consumer docs | Action cue | FR4 |
175
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
176
+ | ---------------- | ----------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------ | ------------------ |
177
+ | Action button | Circular compact Button carrying the current send or stop action. | Current source and consumer docs; exact tuning stays in code | Primary/secondary action | FR1, FR2, FR7 |
178
+ | Send artwork | Upward send glyph unless explicit content wins (FR3's gap). | Current source and docs; `spec:AST-032` FR2–FR3 | Action cue | FR3 |
179
+ | Stop artwork | Shared stop glyph unless explicit content wins. | Current source and consumer docs | Action cue | FR4 |
171
180
 
172
- This observational draft does not select new dimensions, variants, icon artwork,
173
- color roles, or visual prominence.
181
+ This record selects no new dimensions, variants, icon artwork, color roles, or
182
+ visual prominence. No anatomy-theming map is recorded yet; the block is
183
+ optional during migration, so the map is deferred.
174
184
 
175
185
  The Chat family consumer doc catalogs `chat-send-button`. Button owns the
176
186
  underlying painting and interaction-state styling; the component target is
@@ -183,43 +193,44 @@ co-located on that painting element.
183
193
  - `architecture:public-component-api` owns BaseProps reachability, style/ref
184
194
  composition, and deliberate event composition.
185
195
  - `architecture:icon-resolution-and-component-slots` owns the shared slot model;
186
- `spec:AST-032/FR2-FR3` currently requires that stage in send resolution, and
187
- its implementation remains pending with the owner. This draft does not describe
188
- the unshipped stage as current behavior.
196
+ `spec:AST-032` FR2–FR3 put a component-slot stage in send resolution, which is
197
+ not shipped (Current-state impact).
189
198
  - `architecture:component-theming-surface` owns the qualification and placement
190
199
  of the co-located `chat-send-button` target.
191
200
  - `architecture:interaction-modality` and `spec:AST-020` own pointer, keyboard,
192
201
  focus, and accessibility parity inherited through Button.
193
202
  - `architecture:component-test-sufficiency` owns public-seam regression coverage.
194
- - `spec:AST-029` owns this Night Watch observational backfill and prevents it from
195
- settling new product meaning.
203
+ - `spec:AST-029` owns the audit evidence inventory this record's verification
204
+ map follows.
196
205
 
197
206
  ## Verification map
198
207
 
199
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
200
- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ |
201
- | FR1, FR2, AR1-AR4 | `ChatSendButton.test.tsx`, `ChatSendButton.a11y.chromium.spec.ts`, and `ChatComposer.test.tsx` | Enabled/disabled send, stop with and without handler, context stop, keyboard/pointer, light/dark interaction states | Label, disabled, native activation, state routing, or interaction paint drift fails focused or exact-head browser evidence. | `audit:ChatSendButton/accessibility` |
202
- | FR3 | `ChatSendButton.test.tsx`; current AST-032 owner requirement and its pending implementation | Explicit and shared-fallback send icon | Losing the observed two-stage precedence fails focused tests; the AST-032 owner verifies the required component-slot stage. | `audit:ChatSendButton/theming` |
203
- | FR4 | `ChatSendButton.test.tsx` | Explicit and fallback stop icon | Losing explicit precedence or registry resolution fails focused assertions. | `audit:ChatSendButton/behavior` |
204
- | FR5, FR6 | Red-before-green callback composition, ordering, size, root passthrough, disabled-click, and keyboard tests; types and lint | Send, stop, `sm`/`md`, ref/class/data/ARIA, Enter/Space | Consumer events replacing state actions, cancellation reordering, lost root inputs, or modality drift fails focused tests. | `audit:ChatSendButton/api` |
205
- | FR7 | `themingTargets.test.ts`, exact-head browser receipts, and source inspection | Send and stop themes | Target removal, movement off paint, or undocumented target drift fails tests or browser evidence. | `audit:ChatSendButton/theming` |
206
- | RTL relation | RTL shared-fixture route plus refreshed verified-N/A source receipts for ChatSendButton and ChatComposer | Send and stop | Directional artwork, physical positioning, or transform invalidates the checked-in receipts. | `audit:ChatSendButton/i18n-rtl` |
207
- | Browser matrix | `ChatSendButton.a11y.chromium.spec.ts` exact-head 10-sensor receipts | Light/dark send and stop rest/hover/focus/press, disabled rest/hover, `sm`/`md`, 320px coarse pointer, D7 contrast pairs | A stale build, decoy selector, missing state, changed disabled paint, absent interaction pixels, failed contrast, or geometry miss fails closed. | `audit:ChatSendButton/visual` |
208
- | Documentation and surface | Consumer docs, export checks, Storybook, and `check:knowledge` | Props, public subpath, states, draft contract | Missing or stale docs, exports, relationships, or required draft structure fails validation. | `audit:ChatSendButton/docs` |
208
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
209
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ |
210
+ | FR1, FR2, AR1-AR4 | `ChatSendButton.test.tsx`, `ChatSendButton.a11y.chromium.spec.ts`, and `ChatComposer.test.tsx` | Enabled/disabled send, stop with and without handler, context stop, keyboard/pointer, light/dark interaction states | Label, disabled, native activation, state routing, or interaction paint drift fails focused or exact-head browser evidence. | `audit:ChatSendButton/accessibility` |
211
+ | FR3 | `ChatSendButton.test.tsx` | Explicit and shared-fallback send icon | Losing explicit-first precedence or the `arrowUp` fallback fails focused tests; the `spec:AST-032` slot stage is not tested here. | `audit:ChatSendButton/theming` |
212
+ | FR4 | `ChatSendButton.test.tsx` | Explicit and fallback stop icon | Losing explicit precedence or registry resolution fails focused assertions. | `audit:ChatSendButton/behavior` |
213
+ | FR5, FR6 | `ChatSendButton.test.tsx` callback composition, ordering, size, root passthrough, disabled-click, and keyboard tests | Send, stop, `sm`/`md`, ref/class/data/ARIA, Enter/Space | Consumer events replacing state actions, cancellation reordering, lost root inputs, or modality drift fails focused tests. | `audit:ChatSendButton/api` |
214
+ | FR7 | `themingTargets.test.ts` and `ChatSendButton.a11y.chromium.spec.ts` | Send and stop themes | Target removal, movement off paint, or undocumented target drift fails tests or browser evidence. | `audit:ChatSendButton/theming` |
215
+ | RTL relation | RTL shared-fixture route plus refreshed verified-N/A source receipts for ChatSendButton and ChatComposer | Send and stop | Directional artwork, physical positioning, or transform invalidates the checked-in receipts. | `audit:ChatSendButton/i18n-rtl` |
216
+ | Browser matrix | `ChatSendButton.a11y.chromium.spec.ts` browser states | Light/dark send and stop rest/hover/focus/press, disabled rest/hover, `sm`/`md`, 320px coarse pointer, D7 contrast pairs | A stale build, decoy selector, missing state, changed disabled paint, absent interaction pixels, failed contrast, or geometry miss fails closed. | `audit:ChatSendButton/visual` |
217
+ | Documentation and surface | Consumer docs, export checks, Storybook, and `check:knowledge` | Props, public subpath, states, contract | Missing or stale docs, exports, relationships, or required structure fails validation. | `audit:ChatSendButton/docs` |
218
+
219
+ Read from source, with no focused test yet:
220
+
221
+ - FR5: explicit props winning over ChatComposer context.
222
+ - FR6: `style` and `xstyle` reaching the root.
209
223
 
210
224
  ## Decision log
211
225
 
212
- None. This draft records current facts, one objective event-composition repair,
213
- and one current AST-032 requirement whose implementation remains pending with its
214
- owner. It introduces no component-local public API, default, compatibility,
215
- ownership, or subjective visual decision.
226
+ No component-local decision is recorded.
216
227
 
217
228
  ## Open questions
218
229
 
219
230
  - **Cross-reference: stop without handler.** `component:ChatComposer/OQ4` owns
220
231
  the unresolved policy for a visible stop action without an explicit or context
221
- `onStop`. This component draft records and tests the current enabled no-op
222
- branch without duplicating that question.
232
+ `onStop`. This record covers today's enabled no-op branch without
233
+ duplicating that question.
223
234
 
224
235
  ## Content boundary
225
236
 
@@ -1,21 +1,20 @@
1
1
  ---
2
2
  schema_version: 3
3
- template_version: 3
3
+ template_version: 7
4
4
  kind: component
5
5
  id: component:ContextMenu
6
- authority: draft
6
+ authority: current
7
7
  archive_reason: null
8
8
  superseded_by: null
9
- approved_by: null
10
- approved_at: null
9
+ approved_by: cixzhang
10
+ approved_at: 2026-10-31
11
11
  owners: [cixzhang]
12
12
  review_triggers: [accessibility, behavior, public-api, theming]
13
13
  verified_by:
14
14
  [
15
15
  packages/core/src/ContextMenu/ContextMenu.test.tsx,
16
- packages/core/src/DropdownMenu/DropdownMenu.test.tsx,
17
- packages/core/src/BottomSheet/BottomSheet.test.tsx,
18
- packages/core/src/List/List.test.tsx,
16
+ packages/core/src/ContextMenu/ContextMenu.triggerProps.test.tsx,
17
+ packages/core/src/theme/themingTargets.test.ts,
19
18
  scripts/check-knowledge.mjs,
20
19
  ]
21
20
  modules: []
@@ -29,29 +28,39 @@ architecture:
29
28
  architecture:public-component-api,
30
29
  ]
31
30
  contributing: []
32
- system_specs: []
31
+ system_specs: [spec:AST-094, spec:AST-107]
33
32
  ---
34
33
 
35
34
  # ContextMenu component contract
36
35
 
36
+ ## Contract at a glance
37
+
38
+ | Area | Contract |
39
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | Public contract | None changes. ContextMenu takes caller `children` as the Trigger area, data-driven `items` or compound `menuContent`, `label`, `presentation`, `isDisabled`, and `triggerAs`. |
41
+ | Behavior | Right-click, Shift+F10 or the Menu key, and a touch long press open a menu at the cursor (or the trigger's bottom-left from the keyboard), or a bottom sheet; rows follow DropdownMenu's rows and press model. |
42
+ | End-user impact | People reach a region's actions by mouse, keyboard, or a held finger; a sub-menu drills in on a phone. Under a finger the menu does not grow from where it was opened or lift its trigger, and a scroll that starts outside leaves it open. |
43
+ | Builder impact | Builders wrap any region, including an inline reference in prose (`triggerAs="span"`). |
44
+ | Compatibility/readiness | Records shipped behavior; no API or DOM change; the released default under a finger moves as `spec:AST-094` FR25 lists, with `presentation="popover"` keeping it. The touch grow, pan dismissal, and trigger lift (FR5) are listed under Current-state impact. |
45
+ | Review checks | Reject a trigger that gains a role or `aria-haspopup`, a menu that bypasses the shared Escape stack, a `context-menu` target moved off the painted surface, or a second open-state owner. |
46
+ | Governing rules | `spec:AST-107` FR8; `spec:AST-094` FR26–FR35; `module:DropdownMenu/useMenuPress`; `family:overlay-dismissal`; `architecture:component-theming-surface`; `architecture:layer-runtime`. |
47
+
48
+ This table is a review projection; the body below is authoritative.
49
+
37
50
  ## Intent
38
51
 
39
52
  ContextMenu presents actions for a caller-provided region in either a
40
- cursor-positioned pointer menu or a BottomSheet-hosted touch action list. The
41
- record describes current consumer anatomy, presentation-specific ownership, and
42
- theming reachability.
53
+ cursor-positioned pointer menu or a BottomSheet-hosted touch action list.
43
54
 
44
55
  ## Compatibility and migration
45
56
 
46
- - Released default: `popover`. Under a finger, `spec:AST-094` FR33's lift
47
- becomes the default, and `presentation="popover"` keeps the released menu
48
- (`spec:AST-094` FR25).
49
- - Compatibility class: compatible behavior correction; public API, DOM shape,
50
- styling, and targets are preserved; the default under a finger changes as
51
- above
57
+ - Released default preserved: `no` under a finger: the omitted `presentation`
58
+ follows `spec:AST-094` FR31–FR33 (FR5); `presentation="popover"` keeps the
59
+ released menu (`spec:AST-094` FR25)
60
+ - Compatibility class: records shipped behavior; no public prop, export, or
61
+ DOM change
52
62
  - Controlled/uncontrolled behavior: internally managed open state
53
- - Migration decision: `spec:AST-094` FR25 (a caller that needs the released
54
- menu under a finger passes `presentation="popover"`)
63
+ - Migration decision: `spec:AST-094` FR25
55
64
 
56
65
  Consumer migration instructions belong in consumer docs and release notes.
57
66
 
@@ -59,17 +68,18 @@ Consumer migration instructions belong in consumer docs and release notes.
59
68
 
60
69
  **Owns**
61
70
 
62
- - The Trigger area's context-menu invocation wiring, through the public
63
- headless context menu (FR10).
71
+ - The Trigger area's context-menu invocation wiring (FR10).
64
72
  - The alternative Pointer menu surface and Touch menu surface and their shared
65
- current `context-menu` target.
66
- - Cursor-anchor placement across the two presentation paths; open state is the
67
- factory's (FR10).
73
+ `context-menu` target.
74
+ - Cursor-anchor placement across the two presentation paths, and open state
75
+ (FR10).
68
76
 
69
77
  **Does not own / non-goals**
70
78
 
71
79
  - Caller-provided Trigger area content.
72
- - Pointer action-row presentation — delegated to `component:DropdownMenu`.
80
+ - Pointer action-row presentation — delegated to `component:DropdownMenu`; the
81
+ `ContextMenuItem`, `ContextMenuDivider`, and `ContextMenuGroup` exports are
82
+ DropdownMenu's components under these names (`component:ContextMenuItem`).
73
83
  - The Touch sheet frame — delegated to `component:BottomSheet`.
74
84
  - Data-driven Touch action-list and row presentation — delegated to
75
85
  `component:List`.
@@ -78,7 +88,7 @@ Consumer migration instructions belong in consumer docs and release notes.
78
88
  ## Public concepts
79
89
 
80
90
  Consumer props, content modes, and presentation policy remain documented in
81
- `ContextMenu.doc.mjs`. One component-local concept is added by DEC-1:
91
+ `ContextMenu.doc.mjs`. DEC-1 adds one component-local concept:
82
92
 
83
93
  | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
84
94
  | --------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------- | ----------------------- | --------- | -------------------------------- |
@@ -86,18 +96,18 @@ Consumer props, content modes, and presentation policy remain documented in
86
96
 
87
97
  ## Behavioral and layout contract
88
98
 
89
- | ID | Candidate invariant | Basis | Draft review state |
90
- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------- |
91
- | FR1 | Every render contains a caller-provided Trigger area. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests | Current behavior; verified |
92
- | FR2 | The current `context-menu` target remains on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | Current source, docs, and tests | Current inventory; verified |
93
- | FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | Current delegation; verified |
94
- | FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | Current branches; verified |
95
- | FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. Under a finger the press follows `spec:AST-094` FR26–FR28 and FR30, and the menu's grow, dismissal, long-press lift and outside press follow FR31–FR33 and FR35 (pending implementation); a mouse press outside is unchanged. Right-click, Shift+F10, long-press invocation and keyboard navigation keep their existing behavior. | `module:DropdownMenu/useMenuPress`; `ContextMenu.test.tsx` press model suite | Current module projection; verified in jsdom |
96
- | FR6 | With `triggerAs="span"` the Trigger area MUST be an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box. | `component:ContextMenu/DEC-1` | Accepted; verified in jsdom |
97
- | FR7 | Sub-menu rows follow `component:DropdownMenu` FR12 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom; owner decision pending |
98
- | FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in DropdownMenu FR6; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. | Current source and tests; shared `useListFocus` and `useTypeahead` suites | Proposed; verified in jsdom; owner decision pending |
99
- | FR9 | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface. | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case | Current family projection; verified in jsdom |
100
- | FR10 | ContextMenu is built on the public headless context menu (`spec:AST-107` FR8): its invocation, open state, and cleanup are the factory's, with no behavior of its own beside the factory's. | Owner decision, 2026-10-26; `spec:AST-107` FR8 | Decided |
99
+ | ID | Invariant | Basis |
100
+ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
101
+ | FR1 | Every render contains a caller-provided Trigger area; with `isDisabled` no invocation opens a menu and the native context menu is left alone. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests |
102
+ | FR2 | The `context-menu` target stays on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | `architecture:component-theming-surface`; current source |
103
+ | FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs |
104
+ | FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests |
105
+ | FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. The long press that opened the menu does not close it as it lifts; a tap outside closes it, and a mouse press outside closes it at the press. Under a finger the menu MUST grow out of where it was opened and shrink back (`spec:AST-094` FR31), MUST close as a pan that starts outside it begins (FR32), MUST hand the held finger that opened it to the menu as the opening press and, on a block trigger, lift the trigger above the page with the menu beside it, and MUST treat a pen's press outside as FR32 does (FR33). Implementation gap: it keeps the layer's own entrance and exit, a scroll that starts outside leaves it open, the opening finger is not handed to the menu so it cannot slide to a row, no trigger lift exists, and a pen press outside closes it at the press. Right-click, Shift+F10, long-press invocation, and keyboard navigation are as FR1 and FR8 state. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR33 |
106
+ | FR6 | With `triggerAs="span"` the Trigger area is an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box. | DEC-1 |
107
+ | FR7 | Sub-menu rows follow `component:DropdownMenu` FR11 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. A sub-menu with a header or footer shows its rows alone here (`component:DropdownMenu` FR20). | `component:DropdownMenu/DEC-4` |
108
+ | FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in `component:DropdownMenu` FR12; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. Arrow keys and typeahead skip disabled rows, shipped behavior that is not a requirement until `component:DropdownMenu` OQ1 is ruled. | Current source and tests |
109
+ | FR9 | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface. | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case |
110
+ | FR10 | ContextMenu MUST be built on the public headless context menu (`spec:AST-107` FR8), with its invocation, open state, and cleanup the factory's and no behavior of its own beside the factory's. Implementation gap: no headless context menu ships; ContextMenu owns its invocation, open state, outside-press listener, and cleanup itself. | `spec:AST-107` FR8 |
101
111
 
102
112
  ### Allowed variation
103
113
 
@@ -122,13 +132,18 @@ Consumer props, content modes, and presentation policy remain documented in
122
132
 
123
133
  ### Transformation and precedence order
124
134
 
125
- - Presentation resolution, cursor positioning, long-press handling, item
126
- rendering, and styling keep their current precedence.
135
+ - **ORD1 — Cursor anchor.** A pointer invocation anchors at the pointer,
136
+ kept inside the trigger at its end edges; a keyboard invocation anchors at
137
+ the trigger's bottom-left.
127
138
 
128
139
  ### Performance and resources
129
140
 
130
- - The component introduces no separate loading, listener, measurement, or render
131
- constraint beyond the owners named above.
141
+ - **PR1 — Bounded resources.** While open, ContextMenu adds a document (and
142
+ shadow-root) mousedown listener for outside dismissal and removes it on
143
+ close, and the
144
+ pointer menu observes its size and content to decide whether it scrolls. With
145
+ `presentation="adaptive"` it subscribes to the compact-touch query while
146
+ mounted.
132
147
 
133
148
  ## Accessibility contract
134
149
 
@@ -140,7 +155,7 @@ stack; while a pointer is held in the pointer menu, the highlight is DOM focus p
140
155
 
141
156
  - **AR1 — An inline trigger keeps its semantics.** `triggerAs` changes the
142
157
  Trigger area's element and box only; it carries no role or `aria-haspopup`
143
- (menus-15) in any form, and the menu keeps its own name (`label`).
158
+ in any form, and the menu keeps its own name (`label`).
144
159
 
145
160
  ## Design relationships
146
161
 
@@ -196,10 +211,10 @@ anchor is positioning infrastructure rather than consumer anatomy.
196
211
  - `architecture:interaction-modality` owns the shared right-click, keyboard,
197
212
  long-press, pointer, touch, and adaptive-presentation boundaries.
198
213
  - `architecture:layer-runtime` owns the current context-mode `useLayer` host and
199
- cursor-anchor positioning. The factory closes the menu on an outside press
200
- (`spec:AST-107` FR8; FR10), and ContextMenu joins shared Escape ordering through
214
+ cursor-anchor positioning. ContextMenu closes the menu on an outside press itself (FR10), and joins shared Escape ordering through
201
215
  `useLayerDismissal`; touch hosting delegates
202
216
  to BottomSheet.
217
+ - `spec:AST-094` owns the touch press, lift, grow, and dismissal rules (FR5).
203
218
  - `family:overlay-dismissal` owns shared Escape and platform-close ordering.
204
219
  ContextMenu's pointer presentation is a shared-stack adopter; the touch
205
220
  presentation delegates dismissal to BottomSheet, which is also a shared-stack
@@ -207,17 +222,30 @@ anchor is positioning infrastructure rather than consumer anatomy.
207
222
 
208
223
  ## Verification map
209
224
 
210
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
211
- | ------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
212
- | FR1, FR4 | `ContextMenu.test.tsx` pointer, long-press, bottom-sheet, adaptive, compound, and drill-in suites | Pointer data/compound and touch data/compound | Collapsing presentation-specific parts or content owners fails structure, role, or behavior tests. | `audit:ContextMenu/anatomy` |
213
- | FR2 | ContextMenu target tests, source inspection, and theming target inventories | Pointer and touch menu surfaces | Moving `context-menu` to the trigger or removing it from a painted branch fails evidence. | `audit:ContextMenu/theming` |
214
- | FR3 | ContextMenu, DropdownMenu, BottomSheet, and List owner tests | Pointer rows, touch frame, touch list/rows | A composed part loses its owner target or is documented as a new ContextMenu target. | `audit:ContextMenu/theming` |
215
- | Layer relationships | `ContextMenu.test.tsx` nested-layer case and current layer/dismissal architecture records | Outside click, Escape, nested context layers, sheet | A Pointer menu bypassing the shared stack or dismissing with its nested child fails. | `audit:ContextMenu/behavior` |
216
- | FR5 | `ContextMenu.test.tsx` press model case; `module:DropdownMenu/useMenuPress` map | Finger slide across rows, stray click | A row acting on the press row or the stray click fails. | `audit:ContextMenu/behavior` |
217
- | FR6 | `ContextMenu.test.tsx` keyboard case; shared `useListFocus` and `useTypeahead` suites | wrap at both ends | Clamping arrows fail. | `audit:ContextMenu/behavior` |
218
- | FR10 | ContextMenu tests over `createContextMenu` and a source check | a right click, a long press, Shift+F10; open, close, unmount while open | A context request that bypasses the factory, open state ContextMenu keeps on its own, or a listener left after unmount fails | `audit:ContextMenu/behavior` |
219
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:ContextMenu/theming` |
220
- | FR6, AR1 | `ContextMenu.test.tsx` "inline trigger (triggerAs)" suite | `span` in a paragraph, default `div` | A block trigger inside prose, or a lost invocation path, fails. | `audit:ContextMenu/behavior` |
225
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
226
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
227
+ | FR1, FR4 | `ContextMenu.test.tsx` rendering, invocation, bottom-sheet, adaptive, compound, and disabled cases ("renders menu with role=\"menu\"", "opens menu on right-click", "opens from a keyboard-invoked contextmenu (Shift+F10 / Menu key)", "opens on touch long-press", "opens the real menu content in a BottomSheet when requested", "uses the BottomSheet for adaptive presentation on compact touch", "renders menuContent as menu items", "does not open when isDisabled is true") | Right-click, keyboard, long press; pointer and sheet; data and compound; disabled | A missing invocation path, menu role, sheet, or compound interior, or a disabled trigger that opens, fails. Evidence gap: no test asserts that a disabled trigger leaves the native context menu alone. | `audit:ContextMenu/anatomy` |
228
+ | FR2 | `themingTargets.test.ts` | Rendered targets | A rendered class absent from the documented targets fails. Evidence gap: no test asserts `context-menu` on either painted surface or its absence from the trigger. | `audit:ContextMenu/theming` |
229
+ | FR3 | `ContextMenu.test.tsx` destructive-variant, section, divider, selectable, and alias cases | Shared rows through ContextMenu | A row that loses DropdownMenu's semantics or variant fails. Evidence gap: no test asserts the BottomSheet and List delegation beyond rendering. | `audit:ContextMenu/theming` |
230
+ | FR5 | `ContextMenu.test.tsx` "acts on the row under a finger release, once, and swallows the stray click" and outside-dismissal cases; `ContextMenuLongPress.a11y.chromium.spec.ts` | Finger release; long press lift; tap, mouse, pen, and scroll outside | A row acting on the press row, a stray click that acts, a long press that closes its own menu, or a tap or mouse press outside that leaves it open fails. Evidence gap: no test asserts the FR31 grow, the opening-finger handoff, or the FR33 lift; the scroll-outside and pen cases assert the current gaps. | `audit:ContextMenu/behavior` |
231
+ | FR6, AR1, ORD1 | `ContextMenu.test.tsx` "inline trigger (triggerAs)" and cursor-anchor cases; "does not put aria-haspopup on the role-less trigger wrapper (menus-15)"; `ContextMenu.triggerProps.test.tsx`; `ContextMenuCursorAnchor.a11y.browser.spec.ts` | `span` in a paragraph; default `div`; pointer and keyboard anchors | A block trigger in prose, a trigger with a role or `aria-haspopup`, or an anchor outside the trigger fails. | `audit:ContextMenu/behavior` |
232
+ | FR7 | `ContextMenu.test.tsx` "drills into nested data items inside the BottomSheet", "renders a submenu from a nested items array and keyboard-reaches an item after it", and "shows a sub-menu with a header or footer as its rows alone, and says why in development" | Sheet drill-in; pointer sub-menu; regions under ContextMenu | A missing sub-menu, an unreachable row after it, or a sub-menu dialog under ContextMenu fails. Evidence gap: no test here asserts the pointer drill-in on a phone. | `audit:ContextMenu/behavior` |
233
+ | FR8 | `ContextMenu.test.tsx` "wraps arrow navigation at the ends", typeahead cases, and "skips aria-disabled menuitemradio and menuitemcheckbox items during arrow navigation" | Wrap; typeahead; disabled rows | A clamped arrow, a missing typeahead match, or a disabled row landed on fails. Evidence gap: no test asserts Page keys or auto-repeat. | `audit:ContextMenu/behavior` |
234
+ | FR9 | `ContextMenu.test.tsx` "lets a nested layer consume Escape before the context menu", "closes on Escape even when opened without auto-focus", and "ignores Escape during IME composition" | Nested layer; Escape; composition | A context menu that closes with its nested child, or on a composition Escape, fails. | `audit:ContextMenu/behavior` |
235
+ | FR10, PR1 | `ContextMenu.test.tsx` invocation and handler-composition cases ("composes the consumer context-menu handler with menu opening", "keeps component-owned opening when a consumer prevents default", "composes consumer touch handlers with long-press detection") | Right click, long press, keyboard | A context request that does not open the menu, or a dropped consumer handler, fails. Evidence gap: no test asserts listener cleanup on unmount; nothing tests a headless factory, which does not ship (the FR10 gap). | `audit:ContextMenu/behavior` |
236
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and target | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:ContextMenu/theming` |
237
+
238
+ ## Current-state impact
239
+
240
+ - **No headless factory (FR10).** No headless context menu ships, so ContextMenu
241
+ owns its invocation, open state, and outside-press listener itself, against
242
+ `spec:AST-107` FR8.
243
+ - **Touch grow, pan dismissal, handoff, and lift (FR5).** Under a finger the
244
+ menu keeps the layer's own entrance and exit instead of growing from where it
245
+ was opened, a scroll that starts outside leaves it open, the held finger that
246
+ opened it cannot slide to a row, and a block trigger is not lifted; a pen
247
+ press outside closes it at the press. All are against `spec:AST-094` FR31,
248
+ FR32, and FR33.
221
249
 
222
250
  ## Decision log
223
251