@astryxdesign/core 0.5.2-canary.c9c8564 → 0.5.2-canary.e03511b

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 (216) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +3 -1
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +4 -3
  4. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
  5. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
  7. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
  9. package/dist/Button/Button.d.ts +2 -1
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.js +11 -4
  12. package/dist/Carousel/Carousel.d.ts.map +1 -1
  13. package/dist/Carousel/Carousel.js +2 -2
  14. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  15. package/dist/Chat/ChatToolCalls.js +34 -17
  16. package/dist/ContextMenu/ContextMenu.d.ts +14 -3
  17. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  18. package/dist/ContextMenu/ContextMenu.js +149 -26
  19. package/dist/ContextMenu/index.d.ts +1 -0
  20. package/dist/ContextMenu/index.d.ts.map +1 -1
  21. package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
  22. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  23. package/dist/DropdownMenu/DropdownMenu.js +340 -26
  24. package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
  25. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  26. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  27. package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
  28. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
  29. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
  31. package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
  32. package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
  33. package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
  34. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
  35. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
  36. package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
  37. package/dist/DropdownMenu/index.d.ts +2 -1
  38. package/dist/DropdownMenu/index.d.ts.map +1 -1
  39. package/dist/DropdownMenu/menuWidth.d.ts +14 -0
  40. package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
  41. package/dist/DropdownMenu/menuWidth.js +35 -0
  42. package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
  43. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
  44. package/dist/DropdownMenu/useMenuOverflow.js +60 -0
  45. package/dist/MoreMenu/MoreMenu.d.ts +8 -2
  46. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  47. package/dist/MoreMenu/MoreMenu.js +2 -0
  48. package/dist/MultiSelector/MultiSelector.d.ts +11 -1
  49. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  50. package/dist/MultiSelector/MultiSelector.js +97 -68
  51. package/dist/MultiSelector/index.d.ts +1 -1
  52. package/dist/MultiSelector/index.d.ts.map +1 -1
  53. package/dist/Popover/Popover.d.ts +10 -6
  54. package/dist/Popover/Popover.d.ts.map +1 -1
  55. package/dist/Popover/Popover.js +141 -16
  56. package/dist/Popover/usePopover.d.ts +4 -0
  57. package/dist/Popover/usePopover.d.ts.map +1 -1
  58. package/dist/Popover/usePopover.js +52 -5
  59. package/dist/Selector/Selector.d.ts +10 -0
  60. package/dist/Selector/Selector.d.ts.map +1 -1
  61. package/dist/Selector/Selector.js +106 -80
  62. package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
  63. package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
  64. package/dist/Selector/SelectorBottomSheet.js +83 -0
  65. package/dist/Selector/index.d.ts +1 -1
  66. package/dist/Selector/index.d.ts.map +1 -1
  67. package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
  68. package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
  69. package/dist/Selector/selectorPresentation.stylex.js +21 -0
  70. package/dist/Selector/useSelectorPresentation.d.ts +32 -0
  71. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
  72. package/dist/Selector/useSelectorPresentation.js +90 -0
  73. package/dist/Toast/useToastGesture.d.ts.map +1 -1
  74. package/dist/Toast/useToastGesture.js +46 -9
  75. package/dist/astryx.css +19 -1
  76. package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
  77. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
  78. package/dist/hooks/useAdaptivePresentation.js +19 -0
  79. package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
  80. package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
  81. package/dist/hooks/useFocusReturnVisibility.js +35 -0
  82. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  83. package/dist/hooks/useFocusTrap.js +12 -0
  84. package/locales/en.json +20 -0
  85. package/locales/pseudo.json +15 -0
  86. package/package.json +2 -2
  87. package/src/BottomSheet/BottomSheet.doc.mjs +37 -0
  88. package/src/BottomSheet/BottomSheet.spec.md +177 -0
  89. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  90. package/src/BottomSheet/BottomSheet.tsx +7 -4
  91. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
  92. package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
  93. package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
  94. package/src/Button/Button.doc.mjs +56 -0
  95. package/src/Button/Button.test.tsx +10 -0
  96. package/src/Button/Button.tsx +13 -5
  97. package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
  98. package/src/Carousel/Carousel.test.tsx +83 -0
  99. package/src/Carousel/Carousel.tsx +8 -2
  100. package/src/Chat/ChatToolCalls.test.tsx +44 -0
  101. package/src/Chat/ChatToolCalls.tsx +36 -15
  102. package/src/CheckboxList/CheckboxList.doc.mjs +172 -31
  103. package/src/CheckboxList/CheckboxList.spec.md +214 -0
  104. package/src/CommandPalette/CommandPalette.doc.mjs +72 -0
  105. package/src/CommandPalette/CommandPalette.spec.md +223 -0
  106. package/src/ComplexSelector/ComplexSelector.doc.mjs +42 -0
  107. package/src/ComplexSelector/ComplexSelector.spec.md +189 -0
  108. package/src/ContextMenu/ContextMenu.doc.mjs +61 -1
  109. package/src/ContextMenu/ContextMenu.spec.md +206 -0
  110. package/src/ContextMenu/ContextMenu.test.tsx +131 -1
  111. package/src/ContextMenu/ContextMenu.tsx +199 -32
  112. package/src/ContextMenu/index.ts +1 -0
  113. package/src/Divider/Divider.doc.mjs +24 -0
  114. package/src/Divider/Divider.spec.md +150 -0
  115. package/src/DropdownMenu/DropdownMenu.doc.mjs +305 -40
  116. package/src/DropdownMenu/DropdownMenu.spec.md +245 -0
  117. package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
  118. package/src/DropdownMenu/DropdownMenu.tsx +459 -28
  119. package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
  120. package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
  121. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
  122. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
  123. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
  124. package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
  125. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
  126. package/src/DropdownMenu/index.ts +2 -0
  127. package/src/DropdownMenu/menuWidth.ts +51 -0
  128. package/src/DropdownMenu/useMenuOverflow.ts +77 -0
  129. package/src/FieldStatus/FieldStatus.doc.mjs +21 -0
  130. package/src/FieldStatus/FieldStatus.spec.md +212 -0
  131. package/src/Grid/Grid.doc.mjs +19 -0
  132. package/src/Grid/Grid.spec.md +161 -0
  133. package/src/Icon/Icon.doc.mjs +196 -48
  134. package/src/Icon/Icon.spec.md +191 -0
  135. package/src/IconButton/IconButton.doc.mjs +38 -0
  136. package/src/Kbd/Kbd.doc.mjs +18 -0
  137. package/src/Kbd/Kbd.spec.md +141 -0
  138. package/src/Layout/Layout.doc.mjs +32 -0
  139. package/src/Layout/Layout.spec.md +199 -0
  140. package/src/Lightbox/Lightbox.doc.mjs +149 -33
  141. package/src/Lightbox/Lightbox.spec.md +197 -0
  142. package/src/Markdown/Markdown.doc.mjs +60 -0
  143. package/src/Markdown/Markdown.spec.md +200 -0
  144. package/src/MobileNav/MobileNav.doc.mjs +140 -32
  145. package/src/MobileNav/MobileNav.spec.md +208 -0
  146. package/src/MoreMenu/MoreMenu.doc.mjs +59 -0
  147. package/src/MoreMenu/MoreMenu.spec.md +221 -0
  148. package/src/MoreMenu/MoreMenu.test.tsx +47 -0
  149. package/src/MoreMenu/MoreMenu.tsx +13 -1
  150. package/src/MultiSelector/MultiSelector.doc.mjs +128 -0
  151. package/src/MultiSelector/MultiSelector.spec.md +266 -0
  152. package/src/MultiSelector/MultiSelector.test.tsx +127 -0
  153. package/src/MultiSelector/MultiSelector.tsx +121 -73
  154. package/src/MultiSelector/index.ts +1 -0
  155. package/src/NavIcon/NavIcon.doc.mjs +17 -0
  156. package/src/NavIcon/NavIcon.spec.md +133 -0
  157. package/src/NavMenu/NavHeadingMenu.spec.md +205 -0
  158. package/src/NavMenu/NavMenu.doc.mjs +37 -0
  159. package/src/Outline/Outline.doc.mjs +35 -0
  160. package/src/Outline/Outline.spec.md +161 -0
  161. package/src/OverflowList/OverflowList.doc.mjs +24 -0
  162. package/src/OverflowList/OverflowList.spec.md +156 -0
  163. package/src/Pagination/Pagination.doc.mjs +74 -0
  164. package/src/Pagination/Pagination.spec.md +225 -0
  165. package/src/Popover/Popover.doc.mjs +11 -11
  166. package/src/Popover/Popover.test.tsx +397 -2
  167. package/src/Popover/Popover.tsx +228 -21
  168. package/src/Popover/usePopover.tsx +92 -15
  169. package/src/ProgressBar/ProgressBar.doc.mjs +198 -38
  170. package/src/ProgressBar/ProgressBar.spec.md +260 -0
  171. package/src/Resizable/Resizable.doc.mjs +59 -16
  172. package/src/Resizable/Resizable.spec.md +194 -0
  173. package/src/Section/Section.doc.mjs +17 -0
  174. package/src/Section/Section.spec.md +138 -0
  175. package/src/SegmentedControl/SegmentedControl.doc.mjs +83 -0
  176. package/src/SegmentedControl/SegmentedControl.spec.md +151 -0
  177. package/src/Selector/Selector.doc.mjs +12 -0
  178. package/src/Selector/Selector.spec.md +202 -0
  179. package/src/Selector/Selector.test.tsx +135 -0
  180. package/src/Selector/Selector.tsx +141 -82
  181. package/src/Selector/SelectorBottomSheet.tsx +97 -0
  182. package/src/Selector/index.ts +1 -0
  183. package/src/Selector/selectorPresentation.stylex.ts +20 -0
  184. package/src/Selector/useSelectorPresentation.ts +147 -0
  185. package/src/Skeleton/Skeleton.doc.mjs +12 -0
  186. package/src/Skeleton/Skeleton.spec.md +126 -0
  187. package/src/Slider/Slider.doc.mjs +184 -30
  188. package/src/Slider/Slider.spec.md +223 -0
  189. package/src/Stack/Stack.doc.mjs +22 -0
  190. package/src/Stack/Stack.spec.md +154 -0
  191. package/src/StatusDot/StatusDot.doc.mjs +17 -0
  192. package/src/StatusDot/StatusDot.spec.md +137 -0
  193. package/src/Switch/Switch.doc.mjs +40 -0
  194. package/src/Switch/Switch.spec.md +174 -0
  195. package/src/Table/Table.doc.mjs +102 -24
  196. package/src/Table/Table.spec.md +283 -0
  197. package/src/Text/Text.doc.mjs +190 -43
  198. package/src/Text/Text.spec.md +205 -0
  199. package/src/TextArea/TextArea.doc.mjs +255 -45
  200. package/src/TextArea/TextArea.spec.md +204 -0
  201. package/src/Toast/ToastViewport.test.tsx +118 -4
  202. package/src/Toast/useToastGesture.ts +61 -8
  203. package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
  204. package/src/Toolbar/Toolbar.doc.mjs +19 -0
  205. package/src/Toolbar/Toolbar.spec.md +173 -0
  206. package/src/Tooltip/Tooltip.doc.mjs +17 -0
  207. package/src/Tooltip/Tooltip.spec.md +141 -0
  208. package/src/TreeList/TreeList.doc.mjs +58 -0
  209. package/src/TreeList/TreeList.spec.md +220 -0
  210. package/src/Typeahead/Typeahead.doc.mjs +83 -1
  211. package/src/Typeahead/Typeahead.spec.md +250 -0
  212. package/src/hooks/useAdaptivePresentation.ts +35 -0
  213. package/src/hooks/useFocusReturnVisibility.ts +45 -0
  214. package/src/hooks/useFocusTrap.test.tsx +40 -0
  215. package/src/hooks/useFocusTrap.ts +13 -0
  216. package/src/theme/themingTargets.test.ts +91 -25
@@ -0,0 +1,151 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 3
4
+ kind: component
5
+ id: component:SegmentedControl
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: [theming, accessibility]
13
+ verified_by:
14
+ [
15
+ packages/core/src/SegmentedControl/SegmentedControl.test.tsx,
16
+ packages/core/src/theme/themingTargets.test.ts,
17
+ scripts/check-knowledge.mjs,
18
+ ]
19
+ families: []
20
+ design_specs: []
21
+ architecture: [architecture:component-theming-surface]
22
+ contributing: []
23
+ system_specs: []
24
+ ---
25
+
26
+ # SegmentedControl component contract
27
+
28
+ ## Intent
29
+
30
+ SegmentedControl presents a group of mutually exclusive choices through
31
+ SegmentedControlItem children. This draft records the current consumer anatomy
32
+ and theming ownership without changing selection behavior, targets, or public
33
+ API.
34
+
35
+ ## Compatibility and migration
36
+
37
+ - Released default preserved: `yes`
38
+ - Compatibility class: additive documentation only; runtime, DOM, styling,
39
+ targets, and public API remain unchanged
40
+ - Controlled/uncontrolled behavior: unchanged; selection remains controlled by
41
+ `value` and `onChange`
42
+ - Migration decision: none
43
+
44
+ Consumer migration instructions belong in consumer docs and release notes.
45
+
46
+ ## Ownership boundary
47
+
48
+ **Owns**
49
+
50
+ - The radiogroup control and its current `segmented-control` theming target.
51
+ - SegmentedControlItem's segment button and its current
52
+ `segmented-control-item` theming target.
53
+ - Rendering optional icon content and a visible label inside a segment.
54
+
55
+ **Does not own / non-goals**
56
+
57
+ - The artwork supplied through an item's `icon` prop — owned by the caller.
58
+ - A separate anatomy part or target for selected state; selection is state on the
59
+ segment target.
60
+ - A visible control-level label; the control's required `label` is its accessible
61
+ name and is not rendered as visual anatomy.
62
+
63
+ ## Public concepts
64
+
65
+ No new public concept is introduced. Consumer props and usage remain documented
66
+ in `SegmentedControl.doc.mjs` and `SegmentedControlItem.doc.mjs`.
67
+
68
+ ## Behavioral and layout contract
69
+
70
+ | ID | Candidate invariant | Basis | Draft review state |
71
+ | --- | ------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------- |
72
+ | FR1 | The radiogroup container carries the current `segmented-control` target and renders its supplied children. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
73
+ | FR2 | Each SegmentedControlItem renders one radio button carrying the current `segmented-control-item` target. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
74
+ | FR3 | A visible item label and optional icon render inside the item without separate public targets. | Current source and tests | Verified current behavior; no target change |
75
+ | FR4 | Selected and disabled remain reflected states on `segmented-control-item`, not standalone anatomy or targets. | Current source, docs, and tests | Verified current behavior; no target change |
76
+
77
+ ### Allowed variation
78
+
79
+ - A segment may render its visible label, an icon and label, or an icon while the
80
+ required label supplies the accessible name.
81
+
82
+ ### Representative states
83
+
84
+ - Selected, unselected, disabled, and enabled items retain the same anatomy.
85
+ Selection and disabled styling vary on the segment target.
86
+
87
+ ### Transformation and precedence order
88
+
89
+ - No new selection, focus, layout, or styling precedence rule is introduced.
90
+
91
+ ### Performance and resources
92
+
93
+ - No new performance or resource rule is introduced.
94
+
95
+ ## Accessibility contract
96
+
97
+ This draft does not change or extend the existing radiogroup, radio, accessible
98
+ name, roving focus, disabled-message, or selection behavior.
99
+
100
+ ## Design relationships
101
+
102
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
103
+ | ---------------- | ----------------------------------------------------------- | ------------------------------ | -------------- | ------------------ |
104
+ | Control | Presents the current grouped choice container. | Current source and public docs | Supporting | FR1 |
105
+ | Segment | Presents one mutually exclusive choice and its item states. | Current source and public docs | Prominent | FR2, FR4 |
106
+ | Label | Identifies a segment visually when not hidden. | Current source and public docs | Prominent | FR3 |
107
+ | Icon | Presents optional caller-supplied artwork within a segment. | Caller-supplied content | Supporting | FR3 |
108
+
109
+ Selection is a state of Segment, not a separate anatomy part.
110
+
111
+ ### Theming anatomy
112
+
113
+ <!-- anatomy-theming:v1 -->
114
+
115
+ ```json
116
+ {
117
+ "Control": {"target": "segmented-control"},
118
+ "Segment": {"target": "segmented-control-item"},
119
+ "Label": {"inherits": "segmented-control-item"},
120
+ "Icon": {"inherits": "segmented-control-item"}
121
+ }
122
+ ```
123
+
124
+ ## Family and system relationships
125
+
126
+ - `architecture:component-theming-surface` owns anatomy qualification, inherited
127
+ parts, and state-on-target rules.
128
+
129
+ ## Verification map
130
+
131
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
132
+ | ------------------- | ----------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------- |
133
+ | FR1, FR2 | `SegmentedControl.test.tsx` radiogroup and rendering suites | Control with multiple segment items | Removing the group or item semantics fails existing role and content assertions. | `audit:SegmentedControl/anatomy` |
134
+ | FR3 | `SegmentedControl.test.tsx` label and icon suites | Visible label, icon with label, icon-only item | Removing or changing current label/icon rendering fails existing DOM assertions. | `audit:SegmentedControl/anatomy` |
135
+ | FR4 | `SegmentedControl.test.tsx` selection and disabled suites | Selected, unselected, and disabled items | Breaking reflected item state fails current ARIA, class, or data-attribute assertions. | `audit:SegmentedControl/theming` |
136
+ | Target inventory | `packages/core/src/theme/themingTargets.test.ts` | Control and item targets | Runtime and documented target metadata drift fails target validation. | `audit:SegmentedControl/theming` |
137
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and both current targets | Missing, extra, prefixed, or stale mappings fail repository validation. | `audit:SegmentedControl/theming` |
138
+
139
+ ## Decision log
140
+
141
+ None. This draft records current facts and introduces no component-local design,
142
+ selection, accessibility, or API decision.
143
+
144
+ ## Open questions
145
+
146
+ None.
147
+
148
+ ## Content boundary
149
+
150
+ This file does not duplicate consumer prop tables, examples, selection or focus
151
+ mechanics, implementation steps, or system rules. It links to their owners.
@@ -194,6 +194,13 @@ export const docs = {
194
194
  'Which edge of the option row carries the selected mark. start reserves a mark column ahead of every label so they stay aligned, the way a native menu does; end is the house convention shared with Typeahead and CommandPalette.',
195
195
  default: "'end'",
196
196
  },
197
+ {
198
+ name: 'presentation',
199
+ type: "'popover' | 'bottom-sheet' | 'adaptive'",
200
+ description:
201
+ 'How the option list is presented. adaptive uses a bottom sheet on compact touch screens and an anchored popover otherwise.',
202
+ default: "'popover'",
203
+ },
197
204
  {
198
205
  name: 'width',
199
206
  type: 'SizeValue',
@@ -253,6 +260,11 @@ export const docs = {
253
260
  description:
254
261
  'Use variant="ghost" when a selector sits in a toolbar with ghost buttons. If validation status is needed there, prefer statusVariant="tooltip" so the toolbar height stays compact.',
255
262
  },
263
+ {
264
+ guidance: true,
265
+ description:
266
+ 'Use presentation="adaptive" when the selector should become a bottom sheet on compact touch screens.',
267
+ },
256
268
  {
257
269
  guidance: false,
258
270
  description:
@@ -0,0 +1,202 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 3
4
+ kind: component
5
+ id: component:Selector
6
+ authority: current
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: cixzhang
10
+ approved_at: 2026-08-30
11
+ owners: [cixzhang, imdreamrunner]
12
+ review_triggers: [public-api, behavior, layout, theming, accessibility]
13
+ verified_by:
14
+ [
15
+ packages/core/src/Selector/Selector.test.tsx,
16
+ packages/core/src/Selector/Selector.source-build.test.mjs,
17
+ ]
18
+ families: []
19
+ design_specs: []
20
+ architecture:
21
+ [
22
+ architecture:component-theming-surface,
23
+ architecture:icon-resolution-and-component-slots,
24
+ architecture:interaction-modality,
25
+ architecture:layer-runtime,
26
+ architecture:public-component-api,
27
+ ]
28
+ contributing: []
29
+ system_specs: [spec:AST-004/DEC-1]
30
+ ---
31
+
32
+ # Selector component contract
33
+
34
+ ## Intent
35
+
36
+ Selector lets a person choose one value from a moderate list while keeping the
37
+ trigger usable as a form field or compact toolbar control. It owns single-value
38
+ selection, option navigation, search within the supplied options, and the
39
+ connection between the closed trigger and its selection surface.
40
+
41
+ ## Compatibility and migration
42
+
43
+ - Released defaults and behavior remain unchanged by this record.
44
+ - `indicatorPosition` defaults to `end`; every option row currently reserves the
45
+ indicator column at the configured logical edge.
46
+ - `presentation` defaults to `popover`. `bottom-sheet` is an explicit modal
47
+ presentation, and `adaptive` selects it on compact coarse-pointer screens.
48
+ - `hasClear` changes the value contract to include `null`; that distinction is
49
+ already part of the public type.
50
+ - `spec:AST-004/DEC-1` accepts a future change that collapses an empty indicator
51
+ column. It is not shipped behavior and does not replace FR3 until implementation,
52
+ visual verification, and an update to this current contract land together.
53
+
54
+ ## Ownership boundary
55
+
56
+ **Owns**
57
+
58
+ - Choosing one value from supplied options.
59
+ - Trigger, listbox, option, search, empty, loading, and disabled behavior.
60
+ - Keyboard navigation and announcements for that selection flow.
61
+ - Selector-specific composition of shared Field, Layer, adaptive presentation,
62
+ and indicator behavior.
63
+
64
+ **Does not own / non-goals**
65
+
66
+ - Action or navigation menus; DropdownMenu owns those.
67
+ - Multi-value selection; MultiSelector owns it.
68
+ - Product-specific option content or filtering performed outside the supplied
69
+ option list.
70
+ - Family-wide input sizing, loading, end-lane, or disabled-reason policy. The
71
+ draft `family:input-fields` record is non-authoritative context only.
72
+
73
+ ## Public concepts
74
+
75
+ This table names semantic concepts reviewers need. Prop syntax, complete defaults,
76
+ and examples remain in `Selector.doc.mjs`.
77
+
78
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
79
+ | ---------------------- | -------------------------------------- | ------------------------------------------------------- | ----------------------------------------- | --------- | -------- | --------- | ------------------------------- |
80
+ | trigger variant | `input`, `ghost` | Form-field or toolbar presentation | All trigger states | `input` | Selector | released | TypeScript rejects other values |
81
+ | size | `sm`, `md`, `lg` | Trigger and option-row density | All presentations | `md` | Selector | released | TypeScript rejects other values |
82
+ | selected-mark position | `start`, `end` | Logical edge containing the reserved selection column | Every option row | `end` | Selector | released | TypeScript rejects other values |
83
+ | presentation | `popover`, `bottom-sheet`, `adaptive` | Anchored pointer surface or modal compact-touch surface | All trigger variants | `popover` | Selector | released | TypeScript rejects other values |
84
+ | popup semantics | `listbox`; modal dialog containing one | Semantics follow the active presentation | Popover; bottom sheet | `listbox` | Selector | released | No separate role prop is public |
85
+ | option-row state | `selected`, `disabled` | Stable theming state on each option row | Every rendered option | neither | Selector | released | Unknown states are not emitted |
86
+
87
+ ## Behavioral and layout contract
88
+
89
+ These requirements describe shipped behavior on current `main`.
90
+
91
+ | ID | Shipped invariant | Evidence |
92
+ | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
93
+ | FR1 | Selecting an enabled option updates the one selected value, closes the active presentation, and returns the trigger to its stable closed state. | Consumer docs and selection interaction tests |
94
+ | FR2 | Without explicit placement, a non-search popover aligns the selected row over the trigger and clamps it to the viewport. Search popovers and explicit placement use normal Layer positioning. | Consumer docs, implementation, and geometry tests |
95
+ | FR3 | Every option row reserves one selection-mark column at `indicatorPosition`, even when the default unchecked indicator draws nothing. The column has a minimum width and can grow for a larger themed replacement. | `itemMarkColumn`, indicator-position tests, and theme tests |
96
+ | FR4 | `popover` uses an anchored Popover. `bottom-sheet` uses a modal BottomSheet. `adaptive` resolves to the modal bottom sheet on compact coarse-pointer screens and Popover otherwise. | Presentation controller and adaptive-presentation tests |
97
+ | FR5 | While `isLoading` is true, the trigger exposes busy state and the listbox suppresses empty and no-results output. | Loading, empty-state, and announcement tests |
98
+
99
+ ### Allowed variation
100
+
101
+ - **AV1 — Indicator rendering.** A theme may replace the check indicator. The
102
+ replacement may draw in both selected and unselected states; the current row
103
+ still reserves its indicator column.
104
+ - **AV2 — Option content.** `renderOption` may replace visible option content,
105
+ while Selector keeps row role, selection, disabled state, navigation, and
106
+ theming state.
107
+ - **AV3 — Selected value content.** `renderValue` may replace the closed value
108
+ display without changing the placeholder or trigger's selection role.
109
+
110
+ ### Representative states
111
+
112
+ | State | Required invariant | Allowed variation |
113
+ | ------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------ |
114
+ | closed with no value | Label and placeholder identify the field | Consumer placeholder text |
115
+ | closed with a value | Selected option is represented in the trigger | Custom `renderValue` content |
116
+ | pointer / popover | Anchored surface exposes the listbox without modal-dialog semantics | Default or explicit placement |
117
+ | compact coarse pointer | BottomSheet exposes a modal dialog containing the listbox | Explicit `bottom-sheet` or resolved `adaptive` |
118
+ | searching | Visible options, keyboard navigation, and announced result count use one filter | Consumer search and empty text |
119
+ | loading | Trigger is busy; empty and no-results output is suppressed | Consumer loading duration |
120
+ | disabled with reason | Trigger remains focusable enough to expose the reason while activation stays blocked | Consumer reason text |
121
+ | selected/unselected rows | Row semantics and theming state are correct; both reserve the indicator column | Start/end position and themed indicator representation |
122
+
123
+ ### Transformation and precedence order
124
+
125
+ - **ORD1 — Option normalization precedes filtering and selection.** Strings and
126
+ option objects become one option shape before search, keyboard matching,
127
+ rendering, and value comparison.
128
+ - **ORD2 — Caller-selected content wins deliberately.** `startIcon` takes
129
+ precedence over a selected option's icon; explicit placement takes precedence
130
+ over selected-item overlay alignment.
131
+
132
+ ### Performance and resources
133
+
134
+ - **PR1 — Search does not add effect-driven result-count renders.** The next
135
+ filtered count is derived from the input change and announced once for that
136
+ query.
137
+ - **PR2 — Geometry work is scoped to the open popover.** Selected-item alignment
138
+ may read browser layout while opening; it does not impose document-wide or
139
+ persistent observation while closed.
140
+
141
+ ## Accessibility contract
142
+
143
+ - **AR1 — Semantics follow presentation.** The anchored presentation exposes a
144
+ listbox through a Popover and the trigger uses `aria-haspopup="listbox"`. The
145
+ touch presentation exposes a modal BottomSheet dialog containing the listbox
146
+ and the trigger uses `aria-haspopup="dialog"`.
147
+ - **AR2 — Focus follows the active interaction model.** The anchored presentation
148
+ keeps the combobox relationship at the trigger/search control. The modal touch
149
+ presentation moves focus into its search control or listbox and restores focus
150
+ to the trigger when it closes.
151
+ - **AR3 — Keyboard selection matches the visible set.** Arrow, Home/End,
152
+ typeahead, search, Enter, Escape, and Tab behavior operate on the options a
153
+ person can currently perceive.
154
+
155
+ ## Design relationships
156
+
157
+ No current design spec is linked.
158
+
159
+ | Anatomy or state | Current representation requirement | Representation authority | Hierarchy role | Component contract |
160
+ | -------------------------------- | --------------------------------------------------------------------------------------- | -------------------------- | ----------------------- | ------------------ |
161
+ | selected mark and option spacing | Every row reserves the indicator column, preserving label alignment and available width | existing shipped behavior | supporting | FR3 |
162
+ | input versus ghost trigger | none recorded | existing released behavior | form or toolbar control | Public concepts |
163
+
164
+ The approved but unimplemented replacement for the first row is owned by
165
+ `spec:AST-004/DEC-1`.
166
+
167
+ ## Family and system relationships
168
+
169
+ - The current architecture links in frontmatter own public API, theming, icon
170
+ resolution, interaction modality, and Layer behavior used by Selector.
171
+ - `spec:AST-004/DEC-1` owns the accepted future indicator-space change. Its
172
+ `accepted` phase is direction for implementation, not evidence that the runtime
173
+ has changed.
174
+ - `family:input-fields` remains a draft. It may inform future reconciliation, but
175
+ this current component contract neither backlinks to it in frontmatter nor
176
+ depends on it for present policy.
177
+
178
+ ## Verification map
179
+
180
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
181
+ | --------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------- |
182
+ | FR1, AR2, AR3 | `Selector.test.tsx` selection, focus, and keyboard suites | closed/open, search/non-search, disabled option | Removing selection wiring, focus movement, or keyboard behavior fails the named interaction tests | `audit:Selector/behavior` |
183
+ | FR2 | placement and selected-item geometry tests | default, explicit, RTL, transformed entry | Using the wrong positioning model fails the expected position-area or margin | `audit:Selector/behavior` |
184
+ | FR3, AV1 | `itemMarkColumn` source inspection plus indicator-position and replacement-indicator tests | start/end, selected/unselected, check/radio | Removing the wrapper fails row-structure tests; changing its reserved width requires source/layout review | `audit:Selector/design-rendered` |
185
+ | FR4, AR1, AR2 | presentation tests plus `aria-haspopup` source review | pointer, compact coarse pointer, search/non-search | Wrong Popover/dialog roles or focus destinations fail tests; `aria-haspopup` values require source/a11y review | `audit:Selector/accessibility` |
186
+ | FR5 | loading, empty-state, and live-region tests | empty options, unmatched search, loading | Empty/no-results output appears or is announced while loading | `audit:Selector/behavior` |
187
+ | source-build contract | `Selector.source-build.test.mjs` | package source compiled by consumer Babel | Moving evaluated StyleX values outside the supported source form fails compilation | `audit:Selector/code-health` |
188
+
189
+ ## Decision log
190
+
191
+ No component-local future decision is recorded here. Accepted unimplemented work
192
+ is owned by `spec:AST-004`.
193
+
194
+ ## Open questions
195
+
196
+ None.
197
+
198
+ ## Content boundary
199
+
200
+ This file does not copy the full prop reference, examples, current audit score,
201
+ family proposals, or future implementation steps. Those remain with their
202
+ existing owners.
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
+ import {readFileSync} from 'node:fs';
13
14
  import {
14
15
  act,
15
16
  render,
@@ -19,6 +20,7 @@ import {
19
20
  within,
20
21
  } from '@testing-library/react';
21
22
  import userEvent from '@testing-library/user-event';
23
+ import * as stylex from '@stylexjs/stylex';
22
24
  import {useState} from 'react';
23
25
  import type {ReactNode} from 'react';
24
26
  import {Selector} from './Selector';
@@ -35,6 +37,7 @@ import {defineTheme} from '../theme/defineTheme';
35
37
  import {Theme} from '../theme/Theme';
36
38
  import {generateThemeCSS} from '../theme/generateThemeRules';
37
39
  import {spacingVars} from '../theme/tokens.stylex';
40
+ import {selectorPresentationStyles} from './selectorPresentation.stylex';
38
41
 
39
42
  function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
40
43
  const {prose, component} = generateThemeCSS(theme);
@@ -45,6 +48,28 @@ function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
45
48
  beforeEach(() => {
46
49
  // The live regions are a document-level singleton; start each test clean.
47
50
  __resetLiveRegionsForTest();
51
+ __resetInteractionModalityForTest();
52
+ HTMLDialogElement.prototype.showModal = vi.fn(function (
53
+ this: HTMLDialogElement,
54
+ ) {
55
+ this.setAttribute('open', '');
56
+ });
57
+ HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
58
+ this.removeAttribute('open');
59
+ });
60
+ vi.stubGlobal(
61
+ 'matchMedia',
62
+ vi.fn().mockImplementation((query: string) => ({
63
+ matches: false,
64
+ media: query,
65
+ onchange: null,
66
+ addEventListener: vi.fn(),
67
+ removeEventListener: vi.fn(),
68
+ addListener: vi.fn(),
69
+ removeListener: vi.fn(),
70
+ dispatchEvent: vi.fn(),
71
+ })),
72
+ );
48
73
  HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
49
74
  this.setAttribute('popover-open', '');
50
75
  const event = new Event('toggle', {bubbles: false});
@@ -228,6 +253,116 @@ function mockSelectorRects({
228
253
  }
229
254
 
230
255
  describe('Selector', () => {
256
+ it('uses a bottom sheet and closes after a selection when requested', async () => {
257
+ const user = userEvent.setup();
258
+ const onChange = vi.fn();
259
+ render(
260
+ <Selector
261
+ label="Fruit"
262
+ options={OPTIONS}
263
+ onChange={onChange}
264
+ presentation="bottom-sheet"
265
+ />,
266
+ );
267
+
268
+ const trigger = screen.getByRole('combobox');
269
+ await user.click(trigger);
270
+
271
+ expect(
272
+ await screen.findByRole('dialog', {name: 'Fruit'}),
273
+ ).toBeInTheDocument();
274
+ expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
275
+
276
+ await user.click(screen.getByRole('option', {name: /Banana/}));
277
+ expect(onChange).toHaveBeenCalledWith('Banana');
278
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
279
+ });
280
+
281
+ it('restores touch focus without painting a trigger focus ring', async () => {
282
+ render(
283
+ <Selector
284
+ label="Fruit"
285
+ options={OPTIONS}
286
+ onChange={() => {}}
287
+ presentation="bottom-sheet"
288
+ />,
289
+ );
290
+
291
+ const trigger = screen.getByRole('combobox');
292
+ fireEvent.pointerDown(trigger, {pointerType: 'touch'});
293
+ fireEvent.click(trigger, {detail: 1});
294
+ const option = await screen.findByRole('option', {name: /Banana/});
295
+ fireEvent.pointerDown(option, {pointerType: 'touch'});
296
+ fireEvent.click(option, {detail: 1});
297
+
298
+ trigger.focus();
299
+ expect(trigger).toHaveFocus();
300
+ expect(trigger.parentElement).toHaveClass(
301
+ stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
302
+ );
303
+ });
304
+
305
+ it('uses content-hugging height without extra bottom content padding', () => {
306
+ const source = readFileSync(
307
+ 'packages/core/src/Selector/SelectorBottomSheet.tsx',
308
+ 'utf8',
309
+ );
310
+
311
+ expect(source).toContain('height="hug"');
312
+ expect(source).toContain('paddingBlockStart={4}');
313
+ expect(source).toContain('paddingBlockEnd={0}');
314
+ });
315
+
316
+ it('moves keyboard focus into a bottom-sheet listbox', async () => {
317
+ const user = userEvent.setup();
318
+ render(
319
+ <Selector label="Fruit" options={OPTIONS} presentation="bottom-sheet" />,
320
+ );
321
+
322
+ const trigger = screen.getByRole('combobox');
323
+ trigger.focus();
324
+ await user.keyboard('{Enter}');
325
+
326
+ await waitFor(() => expect(screen.getByRole('listbox')).toHaveFocus());
327
+ });
328
+
329
+ it('uses a bottom sheet for adaptive presentation on compact touch', async () => {
330
+ vi.stubGlobal(
331
+ 'matchMedia',
332
+ vi.fn().mockImplementation((query: string) => ({
333
+ matches: query === '(max-width: 768px) and (pointer: coarse)',
334
+ media: query,
335
+ onchange: null,
336
+ addEventListener: vi.fn(),
337
+ removeEventListener: vi.fn(),
338
+ addListener: vi.fn(),
339
+ removeListener: vi.fn(),
340
+ dispatchEvent: vi.fn(),
341
+ })),
342
+ );
343
+ const user = userEvent.setup();
344
+ render(
345
+ <Selector label="Fruit" options={OPTIONS} presentation="adaptive" />,
346
+ );
347
+
348
+ await user.click(screen.getByRole('combobox'));
349
+ expect(
350
+ await screen.findByRole('dialog', {name: 'Fruit'}),
351
+ ).toBeInTheDocument();
352
+ expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
353
+ });
354
+
355
+ it('keeps adaptive presentation anchored without compact touch', async () => {
356
+ const user = userEvent.setup();
357
+ render(
358
+ <Selector label="Fruit" options={OPTIONS} presentation="adaptive" />,
359
+ );
360
+
361
+ await user.click(screen.getByRole('combobox'));
362
+ expect(HTMLElement.prototype.showPopover).toHaveBeenCalledOnce();
363
+ expect(HTMLDialogElement.prototype.showModal).not.toHaveBeenCalled();
364
+ });
365
+
231
366
  it('renders with placeholder when no value', () => {
232
367
  render(<Selector label="Fruit" options={OPTIONS} placeholder="Pick one" />);
233
368
  expect(screen.getByRole('combobox')).toHaveTextContent('Pick one');