@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,250 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 3
4
+ kind: component
5
+ id: component:Typeahead
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]
13
+ verified_by:
14
+ [
15
+ packages/core/src/Typeahead/Typeahead.test.tsx,
16
+ packages/core/src/Typeahead/TypeaheadItem.test.tsx,
17
+ packages/core/src/Field/Field.test.tsx,
18
+ packages/core/src/Token/Token.test.tsx,
19
+ packages/core/src/Icon/Icon.test.tsx,
20
+ scripts/check-knowledge.mjs,
21
+ ]
22
+ families: [family:overlay-dismissal]
23
+ design_specs: []
24
+ architecture:
25
+ [architecture:component-theming-surface, architecture:layer-runtime]
26
+ contributing: []
27
+ system_specs: []
28
+ ---
29
+
30
+ # Typeahead component contract
31
+
32
+ ## Intent
33
+
34
+ Typeahead renders a single-selection search control with an optional standalone
35
+ Field shell and delegates its combobox engine and popup results to BaseTypeahead. This draft records current
36
+ consumer anatomy, target reachability, and the shipped difference between the
37
+ stable result row and its default TypeaheadItem content without changing runtime
38
+ behavior, styling, targets, or public API.
39
+
40
+ ## Compatibility and migration
41
+
42
+ - Released default preserved: `yes`
43
+ - Compatibility class: additive documentation only; runtime, DOM, styling,
44
+ targets, and public API remain unchanged
45
+ - Controlled/uncontrolled behavior: unchanged
46
+ - Migration decision: none
47
+
48
+ Consumer migration instructions belong in consumer docs and release notes.
49
+
50
+ ## Ownership boundary
51
+
52
+ **Owns**
53
+
54
+ - The painted Input surface and its current `typeahead` target.
55
+ - The Dropdown and Empty state rendered by BaseTypeahead and their current
56
+ `typeahead-dropdown` and `typeahead-empty-state` targets.
57
+ - Default item content rendered by TypeaheadItem and its current
58
+ `typeahead-item` target.
59
+ - The stable outer Result row, Result group heading, and Selected result state,
60
+ which currently have no Typeahead target.
61
+
62
+ **Does not own / non-goals**
63
+
64
+ - Standalone field-shell presentation — rendered by Field and themed through
65
+ Field's current `field` target; omitted when Typeahead is inside InputGroup.
66
+ - The Selected token — rendered by Token and themed through Token's current
67
+ `token` target.
68
+ - The Clear button — rendered by Field's InputClearButton and themed through
69
+ Field's current `input-clear-button` target.
70
+ - General Icon presentation for an Icon-rendered start icon.
71
+ - Arbitrary ReactNode start or item content supplied by the caller.
72
+ - A target/state for the outer Result row, Result group heading, or selected-row
73
+ concept; none exists on current `main`.
74
+ - Shared layer lifecycle, positioning, and dismissal behavior.
75
+
76
+ ## Public concepts
77
+
78
+ No new public concept is introduced. Consumer props and usage remain documented
79
+ in `Typeahead.doc.mjs`; BaseTypeahead and TypeaheadItem retain their existing
80
+ public component documentation.
81
+
82
+ ## Behavioral and layout contract
83
+
84
+ | ID | Candidate invariant | Basis | Draft review state |
85
+ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------- |
86
+ | FR1 | The current standalone presentation contains a Field and painted Input surface; the supported InputGroup path renders the same Input surface without a Field. The surface contains an editable input when unselected or editing, and a Selected token plus collapsed input when selected and not editing. | Current source, docs, and tests | Verified current behavior; no normalization |
87
+ | FR2 | Input surface, Dropdown, Empty state, and Default item content carry the four current Typeahead targets documented in `Typeahead.doc.mjs`. | Current source, docs, and tests | Verified current behavior; no target change |
88
+ | FR3 | Every result, whether default or caller-rendered, sits inside the same stable outer Result row that owns option semantics, highlight, selected styling, pointer interaction, and keyboard indexing. | Current source and tests | Verified current behavior; no new behavior decided |
89
+ | FR4 | `typeahead-item` reaches only the standard TypeaheadItem label/supporting-content branch inside a Result row. A custom `renderItem` branch or caller-provided `item.element` does not receive that target, and no branch gives the outer Result row a Typeahead target. | Current source and owner tests | Verified current asymmetry; no normalization |
90
+ | FR5 | Result group heading and Selected result state are stable visible concepts but no current Typeahead target reaches them. The selected check uses a generic Icon, which does not expose a Typeahead-specific selected-row selector. | Current source | Verified factual reachability gaps |
91
+ | FR6 | When present, Field, Selected token, Clear button, and Icon-rendered start icon retain their Field, Token, and Icon theming owners; arbitrary ReactNode start and item content stays caller-owned. | Current composition and owners | Verified current behavior; no target change |
92
+
93
+ ### Allowed variation
94
+
95
+ - Standalone Typeahead renders Field; the supported InputGroup path omits it.
96
+ - The Input surface shows an editable input while empty or editing and a Token
97
+ while a selected value is not being edited. This shipped asymmetry is recorded,
98
+ not normalized into a new shared part or target.
99
+ - A Result row may contain standard TypeaheadItem content, custom `renderItem`
100
+ content, or a caller-provided `item.element`. Only the standard TypeaheadItem
101
+ label/supporting-content branch carries `typeahead-item`.
102
+ - Results may be ungrouped or grouped. Group wrappers and headings do not gain a
103
+ target from that variation.
104
+ - Semantic names and icon component types render through Icon. Arbitrary ReactNode
105
+ start content renders directly and stays caller-owned.
106
+
107
+ ### Representative states
108
+
109
+ | State | Required invariant | Allowed variation |
110
+ | ----------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
111
+ | Standalone or InputGroup | Field is present only standalone; Input surface anatomy and targets remain the same in both paths. | InputGroup owns grouped layout and labeling support. |
112
+ | Empty or editing | Editable input remains inside the `typeahead` Input surface. | Query, loading state, and popup visibility may vary. |
113
+ | Selected and not editing | Token owns the selected-value presentation while the input is collapsed. | Clear button may be present according to `hasClear` and disabled state. |
114
+ | Results with default renderer | Outer Result row remains untargeted; inner TypeaheadItem carries `typeahead-item`. | Item label, icon/avatar, and description may vary. |
115
+ | Results with custom renderer | Outer Result row remains untargeted; custom content receives no `typeahead-item`. | Caller owns the inner result content. |
116
+ | Grouped and selected results | Group heading and selected-row concept remain untargeted by Typeahead. | A generic Icon paints the selected check. |
117
+ | Empty completed search | Empty state carries `typeahead-empty-state` inside the Dropdown. | Consumer-supplied empty text may vary. |
118
+
119
+ ### Transformation and precedence order
120
+
121
+ - No new query, search-result, selection, edit-mode, or styling precedence rule is
122
+ introduced.
123
+
124
+ ### Performance and resources
125
+
126
+ - No new performance or resource rule is introduced.
127
+
128
+ ## Accessibility contract
129
+
130
+ This draft does not change or extend Typeahead's existing field, combobox,
131
+ listbox, option, live-region, focus, or dismissal behavior.
132
+
133
+ ## Design relationships
134
+
135
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
136
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------ | ----------------- | ------------------ |
137
+ | Field | Provides the label and optional supporting content outside InputGroup; omitted inside InputGroup. | Field component | Supporting | FR1, FR6 |
138
+ | Input surface | Paints the control around the editable input or selected token. | Current source and public docs | Prominent | FR1, FR2 |
139
+ | Icon-rendered start icon | Presents a semantic or component icon through Icon. | Icon component | Supporting | FR6 |
140
+ | Caller-rendered start content | Presents arbitrary caller content directly in the input surface's start slot. | Caller-supplied content | Context-dependent | FR6 |
141
+ | Selected token | Presents the selected value outside edit mode through Token. | Token component | Prominent | FR1, FR6 |
142
+ | Clear button | Removes the selected value through the shared field clear action. | Field component | Supporting | FR6 |
143
+ | Dropdown | Paints the anchored listbox surface containing results or Empty state. | Current source and public docs | Prominent | FR2 |
144
+ | Empty state | Presents the no-results message after a completed empty search. | Current source and public docs | Prominent | FR2 |
145
+ | Result row | Owns option semantics, interaction, highlight, selection styling, and keyboard index for every result. | Current source and tests | Prominent | FR3, FR4 |
146
+ | Default item content | Presents the standard TypeaheadItem label and optional supporting content inside a Result row. | TypeaheadItem component | Prominent | FR2, FR4 |
147
+ | Caller-rendered item content | Presents custom `renderItem` or `item.element` content inside the stable Result row. | Caller-supplied content | Context-dependent | FR3, FR4, FR6 |
148
+ | Result group heading | Labels a visible group of Result rows. | Current source | Supporting | FR5 |
149
+ | Selected result state | Presents selected row weight and a trailing check without a Typeahead-specific target or target state. | Current source | Supporting | FR5 |
150
+
151
+ The current target placement is asymmetric: `typeahead-item` is on default inner
152
+ TypeaheadItem content, while the stable outer Result row owns interaction and state
153
+ for both default and custom content. Group headings and the selected-result concept
154
+ are also untargeted. This record preserves those facts and does not move, add, or
155
+ normalize targets.
156
+
157
+ ### Theming anatomy
158
+
159
+ <!-- anatomy-theming:v1 -->
160
+
161
+ ```json
162
+ {
163
+ "Field": {"delegatesTo": {"owner": "component:Field", "target": "field"}},
164
+ "Input surface": {"target": "typeahead"},
165
+ "Icon-rendered start icon": {
166
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
167
+ },
168
+ "Caller-rendered start content": {
169
+ "none": {
170
+ "reason": "intentional: Arbitrary ReactNode content is caller-owned and receives no Typeahead target."
171
+ }
172
+ },
173
+ "Selected token": {
174
+ "delegatesTo": {"owner": "component:Token", "target": "token"}
175
+ },
176
+ "Clear button": {
177
+ "delegatesTo": {
178
+ "owner": "component:Field",
179
+ "target": "input-clear-button"
180
+ }
181
+ },
182
+ "Dropdown": {"target": "typeahead-dropdown"},
183
+ "Empty state": {"target": "typeahead-empty-state"},
184
+ "Result row": {
185
+ "none": {
186
+ "reason": "reachability-gap: The stable outer option row owns interaction and state but has no current Typeahead target."
187
+ }
188
+ },
189
+ "Default item content": {"target": "typeahead-item"},
190
+ "Caller-rendered item content": {
191
+ "none": {
192
+ "reason": "intentional: Custom renderItem and item.element content is caller-owned and does not receive the standard TypeaheadItem target."
193
+ }
194
+ },
195
+ "Result group heading": {
196
+ "none": {
197
+ "reason": "reachability-gap: No current public target reaches the visible result-group heading."
198
+ }
199
+ },
200
+ "Selected result state": {
201
+ "none": {
202
+ "reason": "reachability-gap: No current Typeahead target or target state reaches the outer selected row and its selection concept."
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ ## Family and system relationships
209
+
210
+ - `architecture:component-theming-surface` owns anatomy qualification, local
211
+ target mapping, delegation, and factual `none` dispositions.
212
+ - `architecture:layer-runtime` owns the current Popover host, positioning, native
213
+ light-dismiss, and visibility reconciliation used by the Dropdown.
214
+ - `family:overlay-dismissal` owns shared Escape and platform-close ordering;
215
+ Typeahead participates through its composed Popover owner.
216
+ - Field, Token, and Icon retain ownership of their delegated targets and
217
+ presentation.
218
+
219
+ ## Verification map
220
+
221
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
222
+ | ------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------------------- |
223
+ | FR1, FR6 | `Typeahead.test.tsx`, owner tests, and Typeahead composition source | Standalone/InputGroup, empty, editing, selected, clear, start icon | A documented composed part disappears, gains the wrong owner, or misreports Field presence. | `audit:Typeahead/anatomy` |
224
+ | FR2 | Source inspection, target inventories, empty-state test, and `TypeaheadItem.test.tsx` | Input, open Dropdown, empty search, default item | A current target is missed, invented, or documented on an element that does not carry it. | `audit:Typeahead/theming` |
225
+ | FR3, FR4, FR5 | BaseTypeahead source and result/group/selection tests | Default/custom result, grouped result, selected row | The record hides the outer/inner asymmetry or claims reachability that current code lacks. | `audit:Typeahead/theming` |
226
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target inventory | Missing, extra, prefixed, stale, or multiply assigned mappings fail repository validation. | `audit:Typeahead/theming` |
227
+
228
+ Existing tests directly assert Empty state and TypeaheadItem target presence,
229
+ selected-value Token behavior, result semantics, selection state, and standalone
230
+ Field content. Source inspection confirms InputGroup's Field omission and, with
231
+ public target inventories, provides the current Input surface and Dropdown target
232
+ evidence. No test is represented here as proof of a target it does not directly
233
+ assert.
234
+
235
+ ## Decision log
236
+
237
+ None. This draft records current facts and introduces no component-local design,
238
+ API, theming, or layer-system decision.
239
+
240
+ ## Open questions
241
+
242
+ - **OQ1 — Should the stable Result row, Result group heading, or Selected result
243
+ state gain a public Typeahead target?** (`human-api`) Their current lack of
244
+ reachability is an audit gap, not settled intent. This draft does not normalize
245
+ the existing outer-row/default-content asymmetry.
246
+
247
+ ## Content boundary
248
+
249
+ This file does not duplicate consumer prop tables, examples, search algorithms,
250
+ implementation steps, or shared layer and theming rules. It links to their owners.
@@ -0,0 +1,35 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useAdaptivePresentation.ts
7
+ * @input Uses the shared SSR-safe media-query hook
8
+ * @output Resolves popover/bottom-sheet presentation policy
9
+ * @position Internal overlay policy shared across component families
10
+ */
11
+
12
+ import {useMediaQuery} from './useMediaQuery';
13
+
14
+ export type AdaptivePresentation = 'popover' | 'bottom-sheet' | 'adaptive';
15
+ export type ResolvedAdaptivePresentation = Exclude<
16
+ AdaptivePresentation,
17
+ 'adaptive'
18
+ >;
19
+
20
+ // Width keeps desktop-class tablets and large touch displays anchored by
21
+ // default. Pointer capability, rather than `hover: none`, avoids excluding
22
+ // touch browsers that also report hover capability.
23
+ export const COMPACT_TOUCH_PRESENTATION_QUERY =
24
+ '(max-width: 768px) and (pointer: coarse)';
25
+
26
+ export function useAdaptivePresentation(
27
+ presentation: AdaptivePresentation,
28
+ ): ResolvedAdaptivePresentation {
29
+ const isCompactTouch = useMediaQuery(COMPACT_TOUCH_PRESENTATION_QUERY);
30
+ return presentation === 'adaptive'
31
+ ? isCompactTouch
32
+ ? 'bottom-sheet'
33
+ : 'popover'
34
+ : presentation;
35
+ }
@@ -0,0 +1,45 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useFocusReturnVisibility.ts
7
+ * @input Uses the shared interaction-modality tracker
8
+ * @output Preserves returned focus while suppressing pointer-only focus rings
9
+ * @position Internal focus-return policy for adaptive overlays
10
+ */
11
+
12
+ import {useCallback, useEffect, useState} from 'react';
13
+ import {
14
+ getInteractionModality,
15
+ trackInteractionModality,
16
+ } from '../utils/interactionModality';
17
+
18
+ export function useFocusReturnVisibility() {
19
+ const [isFocusRingSuppressed, setIsFocusRingSuppressed] = useState(false);
20
+
21
+ useEffect(() => {
22
+ trackInteractionModality();
23
+ }, []);
24
+
25
+ const prepareFocusReturn = useCallback(() => {
26
+ setIsFocusRingSuppressed(getInteractionModality() === 'pointer');
27
+ }, []);
28
+
29
+ const resetFocusReturn = useCallback(() => {
30
+ setIsFocusRingSuppressed(false);
31
+ }, []);
32
+
33
+ const onFocusReturnTargetFocus = useCallback(() => {
34
+ if (getInteractionModality() === 'keyboard') {
35
+ setIsFocusRingSuppressed(false);
36
+ }
37
+ }, []);
38
+
39
+ return {
40
+ isFocusRingSuppressed,
41
+ onFocusReturnTargetFocus,
42
+ prepareFocusReturn,
43
+ resetFocusReturn,
44
+ };
45
+ }
@@ -193,6 +193,46 @@ describe('useFocusTrap tabbable model (infra-8)', () => {
193
193
  expect(screen.getByTestId('first')).toHaveFocus();
194
194
  });
195
195
 
196
+ it('moves from a programmatic container focus target into the tabbable cycle', () => {
197
+ render(
198
+ <Trap>
199
+ <button type="button" data-testid="first">
200
+ First
201
+ </button>
202
+ <button type="button" data-testid="last">
203
+ Last
204
+ </button>
205
+ </Trap>,
206
+ );
207
+ const trap = screen.getByTestId('trap');
208
+ trap.tabIndex = -1;
209
+ trap.focus();
210
+
211
+ fireEvent.keyDown(document, {key: 'Tab'});
212
+
213
+ expect(screen.getByTestId('first')).toHaveFocus();
214
+ });
215
+
216
+ it('moves backward from a programmatic container focus target to the last tabbable control', () => {
217
+ render(
218
+ <Trap>
219
+ <button type="button" data-testid="first">
220
+ First
221
+ </button>
222
+ <button type="button" data-testid="last">
223
+ Last
224
+ </button>
225
+ </Trap>,
226
+ );
227
+ const trap = screen.getByTestId('trap');
228
+ trap.tabIndex = -1;
229
+ trap.focus();
230
+
231
+ fireEvent.keyDown(document, {key: 'Tab', shiftKey: true});
232
+
233
+ expect(screen.getByTestId('last')).toHaveFocus();
234
+ });
235
+
196
236
  it('keeps a programmatic focus target when the trap has no tabbable controls', () => {
197
237
  render(
198
238
  <Trap>
@@ -375,6 +375,19 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
375
375
  const first = focusable[0];
376
376
  const last = focusable[focusable.length - 1];
377
377
 
378
+ if (document.activeElement === container) {
379
+ // Programmatic initial focus can sit on the trap container itself
380
+ // (for example a dialog panel with no content controls). Move into
381
+ // the tabbable cycle instead of letting Tab escape to the page.
382
+ event.preventDefault();
383
+ if (event.shiftKey) {
384
+ last.focus();
385
+ } else {
386
+ first.focus();
387
+ }
388
+ return;
389
+ }
390
+
378
391
  if (event.shiftKey) {
379
392
  // Shift+Tab: if on first element, wrap to last
380
393
  if (document.activeElement === first) {
@@ -3,8 +3,10 @@
3
3
  /* eslint-disable @typescript-eslint/no-require-imports */
4
4
  /**
5
5
  * @file Guards `theming.targets` against the real `themeProps()` call sites (#3741).
6
- * @input Component sources (*.tsx/*.ts) and their `{Name}.doc.mjs` files.
7
- * @output Vitest failures naming each undocumented class / visual prop.
6
+ * @input Core/Lab component sources (*.tsx/*.ts), their `{Name}.doc.mjs`
7
+ * files, and the Lab promotion-candidate manifest.
8
+ * @output Vitest failures naming each undocumented class / visual prop on the
9
+ * stable Core surface or a capability-participating Lab component.
8
10
  * @position Sibling of derivedVarRegistry.test.ts, which already validates the
9
11
  * OTHER fields of the same `theming` block (`vars`, `derived`). `targets` was
10
12
  * the one field with no machine check, so it drifted — twice (#3652, #3680).
@@ -22,6 +24,11 @@
22
24
  * `visualProps` or `states`. Docs may list MORE than the source passes —
23
25
  * components forward props they don't themselves reflect (Timestamp passes
24
26
  * `{format}` but documents `type`/`color`/`format`), and that is intentional.
27
+ *
28
+ * Lab is capability-based: a component participates when it already declares
29
+ * `theming.targets` or enters the existing promotion-candidate manifest. Other
30
+ * Lab components remain free to iterate with runtime hooks that are not yet a
31
+ * documented public theming promise.
25
32
  */
26
33
 
27
34
  import {describe, it, expect} from 'vitest';
@@ -30,7 +37,23 @@ import {join, relative} from 'node:path';
30
37
  import ts from 'typescript';
31
38
  import {stableClassName} from '../naming';
32
39
 
33
- const SRC_DIR = join(__dirname, '..');
40
+ const CORE_SRC_DIR = join(__dirname, '..');
41
+ const LAB_SRC_DIR = join(__dirname, '../../../lab/src');
42
+ const LAB_PROMOTION_MANIFEST = join(
43
+ __dirname,
44
+ '../../../../internal/lab-readiness/manifest.mjs',
45
+ );
46
+
47
+ interface LabPromotionCandidate {
48
+ sourceDir: string;
49
+ }
50
+
51
+ const {CANDIDATES: labPromotionCandidates} = require(
52
+ LAB_PROMOTION_MANIFEST,
53
+ ) as {CANDIDATES: LabPromotionCandidate[]};
54
+ const LAB_PROMOTION_DIRS = new Set(
55
+ labPromotionCandidates.map(candidate => candidate.sourceDir),
56
+ );
34
57
 
35
58
  // ---------------------------------------------------------------------------
36
59
  // Source scanning: find themeProps() call sites via the TypeScript AST
@@ -314,6 +337,7 @@ type DocBlock = {theming?: {targets?: DocTarget[]}};
314
337
  type ComponentDocModule = {docs?: DocBlock; docsZh?: DocBlock};
315
338
 
316
339
  interface ComponentInfo {
340
+ packageName: 'core' | 'lab';
317
341
  dir: string;
318
342
  sites: ThemeTargetSite[];
319
343
  /** The doc blocks that carry theming.targets, by the key they live under. */
@@ -328,7 +352,10 @@ interface ComponentInfo {
328
352
  * Reads the file as text rather than requiring it: this runs for directories
329
353
  * that have no doc of their own, so most candidates are misses.
330
354
  */
331
- function docFilesDocumenting(classNames: Set<string>): string[] {
355
+ function docFilesDocumenting(
356
+ srcDir: string,
357
+ classNames: Set<string>,
358
+ ): string[] {
332
359
  const matches: string[] = [];
333
360
  const walk = (dir: string): void => {
334
361
  for (const entry of readdirSync(dir, {withFileTypes: true})) {
@@ -346,18 +373,22 @@ function docFilesDocumenting(classNames: Set<string>): string[] {
346
373
  }
347
374
  }
348
375
  };
349
- walk(SRC_DIR);
376
+ walk(srcDir);
350
377
  return matches;
351
378
  }
352
379
 
353
- function discoverComponents(): ComponentInfo[] {
380
+ function discoverComponents(
381
+ srcDir: string,
382
+ packageName: ComponentInfo['packageName'],
383
+ requiredDirs: ReadonlySet<string> = new Set(),
384
+ ): ComponentInfo[] {
354
385
  const results: ComponentInfo[] = [];
355
- const dirs = readdirSync(SRC_DIR, {withFileTypes: true})
386
+ const dirs = readdirSync(srcDir, {withFileTypes: true})
356
387
  .filter(d => d.isDirectory())
357
388
  .map(d => d.name);
358
389
 
359
390
  for (const dir of dirs) {
360
- const dirPath = join(SRC_DIR, dir);
391
+ const dirPath = join(srcDir, dir);
361
392
  const dirEntries = readdirSync(dirPath);
362
393
 
363
394
  const sourceFiles = dirEntries.filter(
@@ -389,10 +420,7 @@ function discoverComponents(): ComponentInfo[] {
389
420
  // doc file that CI never checks. (Same guard as derivedVarRegistry.test.ts.)
390
421
  const docFiles = dirEntries.includes(`${dir}.doc.mjs`)
391
422
  ? [join(dirPath, `${dir}.doc.mjs`)]
392
- : docFilesDocumenting(new Set(sites.map(s => s.className)));
393
- if (docFiles.length === 0) {
394
- continue;
395
- }
423
+ : docFilesDocumenting(srcDir, new Set(sites.map(s => s.className)));
396
424
 
397
425
  const docBlocks: ComponentInfo['docBlocks'] = [];
398
426
  for (const docFile of docFiles) {
@@ -404,18 +432,23 @@ function discoverComponents(): ComponentInfo[] {
404
432
  }
405
433
  for (const key of ['docs', 'docsZh'] as const) {
406
434
  const targets = mod[key]?.theming?.targets;
407
- // Only blocks that already document a theming surface are held to it —
408
- // a doc with no theming block at all is a separate (documentation) gap.
435
+ // A normal Core or Lab component enrolls by declaring a theming
436
+ // surface. Promotion candidates are also enrolled here so a missing
437
+ // block is a failure rather than a silently skipped readiness gap.
409
438
  if (targets != null) {
410
- docBlocks.push({key, file: relative(SRC_DIR, docFile), targets});
439
+ docBlocks.push({key, file: relative(srcDir, docFile), targets});
411
440
  }
412
441
  }
413
442
  }
414
- if (docBlocks.length === 0) {
443
+ const participates =
444
+ packageName === 'core'
445
+ ? docBlocks.length > 0
446
+ : requiredDirs.has(dir) || docBlocks.length > 0;
447
+ if (!participates) {
415
448
  continue;
416
449
  }
417
450
 
418
- results.push({dir, sites, docBlocks});
451
+ results.push({packageName, dir, sites, docBlocks});
419
452
  }
420
453
  return results;
421
454
  }
@@ -425,24 +458,57 @@ function discoverComponents(): ComponentInfo[] {
425
458
  // ---------------------------------------------------------------------------
426
459
 
427
460
  describe('theming.targets matches the themeProps() call sites', () => {
428
- const components = discoverComponents();
461
+ const components = [
462
+ ...discoverComponents(CORE_SRC_DIR, 'core'),
463
+ ...discoverComponents(LAB_SRC_DIR, 'lab', LAB_PROMOTION_DIRS),
464
+ ];
465
+
466
+ it('finds participating Core and Lab components', () => {
467
+ // A refactor that renames themeProps or drops the Lab package from this
468
+ // inventory must not silently disable either side of the guard.
469
+ expect(components.some(component => component.packageName === 'core')).toBe(
470
+ true,
471
+ );
472
+ expect(components.some(component => component.packageName === 'lab')).toBe(
473
+ true,
474
+ );
475
+ });
429
476
 
430
- it('finds components to check', () => {
431
- // A refactor that renames themeProps must not silently disable this file.
432
- expect(components.length).toBeGreaterThan(0);
477
+ it('enrolls every Lab promotion candidate and explicit theming capability', () => {
478
+ const labDirs = new Set(
479
+ components
480
+ .filter(component => component.packageName === 'lab')
481
+ .map(component => component.dir),
482
+ );
483
+ expect([...LAB_PROMOTION_DIRS].filter(dir => !labDirs.has(dir))).toEqual(
484
+ [],
485
+ );
486
+ expect(LAB_PROMOTION_DIRS.has('CircularProgress')).toBe(false);
487
+ expect(labDirs.has('CircularProgress')).toBe(true);
488
+ expect(labDirs.has('Schedule')).toBe(false);
433
489
  });
434
490
 
435
- for (const {dir, sites, docBlocks} of components) {
491
+ for (const {packageName, dir, sites, docBlocks} of components) {
492
+ const componentLabel = `${packageName}/${dir}`;
436
493
  const renderedClasses = [...new Set(sites.map(s => s.className))].sort();
437
494
 
495
+ it(`${componentLabel}: participating components declare theming metadata`, () => {
496
+ expect(
497
+ docBlocks,
498
+ `${componentLabel} participates in the public theming contract but has ` +
499
+ `no loadable .doc.mjs theming.targets. Lab components participate ` +
500
+ `when they declare theming.targets or enter the promotion manifest.`,
501
+ ).not.toHaveLength(0);
502
+ });
503
+
438
504
  for (const {key, file, targets} of docBlocks) {
439
505
  const documented = new Set(targets.map(t => t.className));
440
506
 
441
- it(`${dir} (${file} ${key}): every rendered class is documented`, () => {
507
+ it(`${componentLabel} (${file} ${key}): every rendered class is documented`, () => {
442
508
  const undocumented = renderedClasses.filter(c => !documented.has(c));
443
509
  expect(
444
510
  undocumented,
445
- `${dir} renders ${undocumented.length} astryx-* class(es) that ` +
511
+ `${componentLabel} renders ${undocumented.length} astryx-* class(es) that ` +
446
512
  `${file} ${key}.theming.targets does not document: ` +
447
513
  `${undocumented.join(', ')}. An undocumented class is an ` +
448
514
  `unthemeable element — theme authors and codegen read targets[] ` +
@@ -450,7 +516,7 @@ describe('theming.targets matches the themeProps() call sites', () => {
450
516
  ).toEqual([]);
451
517
  });
452
518
 
453
- it(`${dir} (${file} ${key}): every visual prop passed to themeProps is documented`, () => {
519
+ it(`${componentLabel} (${file} ${key}): every visual prop passed to themeProps is documented`, () => {
454
520
  const missing: string[] = [];
455
521
  for (const site of sites) {
456
522
  const target = targets.find(t => t.className === site.className);