@astryxdesign/core 0.7.2 → 0.7.3-canary.02f5fe4

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 (237) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  2. package/dist/BottomSheet/BottomSheet.js +8 -4
  3. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
  5. package/dist/Calendar/Calendar.js +3 -2
  6. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  7. package/dist/CheckboxInput/CheckboxInput.js +19 -24
  8. package/dist/ClickableCard/ClickableCard.d.ts +2 -2
  9. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  10. package/dist/ClickableCard/ClickableCard.js +11 -9
  11. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  12. package/dist/CodeBlock/CodeBlock.js +11 -1
  13. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  14. package/dist/ComplexSelector/ComplexSelector.js +23 -5
  15. package/dist/DateInput/DateInput.d.ts +4 -4
  16. package/dist/DateInput/DateInput.d.ts.map +1 -1
  17. package/dist/DateInput/DateInput.js +27 -20
  18. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  19. package/dist/DateInput/NativeDateField.js +14 -8
  20. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  21. package/dist/DateInput/TouchDateField.js +14 -10
  22. package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
  23. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  24. package/dist/DateRangeInput/DateRangeInput.js +27 -18
  25. package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
  26. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  27. package/dist/DateTimeInput/DateTimeInput.js +25 -27
  28. package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
  29. package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
  30. package/dist/DateTimeInput/NativeDateSegment.js +5 -5
  31. package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
  32. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
  33. package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
  34. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  35. package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
  36. package/dist/Dialog/Dialog.d.ts.map +1 -1
  37. package/dist/Dialog/Dialog.js +9 -4
  38. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  39. package/dist/DropdownMenu/DropdownMenu.js +52 -10
  40. package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
  41. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  42. package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
  43. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
  44. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  45. package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
  46. package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
  47. package/dist/DropdownMenu/menuRegionStops.js +2 -2
  48. package/dist/FileInput/FileInput.d.ts.map +1 -1
  49. package/dist/FileInput/FileInput.js +14 -12
  50. package/dist/Link/Link.d.ts +2 -3
  51. package/dist/Link/Link.d.ts.map +1 -1
  52. package/dist/Link/Link.js +11 -19
  53. package/dist/Markdown/Markdown.d.ts.map +1 -1
  54. package/dist/Markdown/Markdown.js +48 -15
  55. package/dist/MultiSelector/MultiSelector.d.ts +4 -4
  56. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  57. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
  58. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
  59. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
  60. package/dist/MultiSelector/MultiSelector.js +25 -18
  61. package/dist/Pagination/Pagination.d.ts.map +1 -1
  62. package/dist/Pagination/Pagination.js +5 -2
  63. package/dist/Popover/Popover.d.ts.map +1 -1
  64. package/dist/Popover/Popover.js +8 -4
  65. package/dist/Selector/Selector.d.ts +4 -4
  66. package/dist/Selector/Selector.d.ts.map +1 -1
  67. package/dist/Selector/Selector.js +23 -14
  68. package/dist/Selector/SelectorOption.d.ts.map +1 -1
  69. package/dist/Selector/SelectorOption.js +1 -0
  70. package/dist/Slider/Slider.d.ts.map +1 -1
  71. package/dist/Slider/Slider.js +7 -14
  72. package/dist/Switch/Switch.d.ts.map +1 -1
  73. package/dist/Switch/Switch.js +25 -17
  74. package/dist/Switch/switch.markers.stylex.d.ts +2 -0
  75. package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
  76. package/dist/Switch/switch.markers.stylex.js +6 -0
  77. package/dist/TimeInput/TimeInput.d.ts +7 -6
  78. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  79. package/dist/TimeInput/TimeInput.js +37 -20
  80. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  81. package/dist/TimeInput/TouchTimeField.js +17 -6
  82. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  83. package/dist/Timestamp/Timestamp.js +41 -8
  84. package/dist/Token/Token.d.ts.map +1 -1
  85. package/dist/Token/Token.js +8 -3
  86. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  87. package/dist/Tooltip/Tooltip.js +36 -14
  88. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  89. package/dist/Tooltip/useTooltip.js +11 -2
  90. package/dist/TopNav/TopNav.d.ts.map +1 -1
  91. package/dist/TopNav/TopNav.js +2 -2
  92. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  93. package/dist/TopNav/TopNavHeading.js +1 -0
  94. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  95. package/dist/TreeList/TreeListItem.js +8 -3
  96. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  97. package/dist/Typeahead/Typeahead.js +14 -2
  98. package/dist/astryx.css +8 -2
  99. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  100. package/dist/hooks/useGridFocus.js +10 -1
  101. package/dist/hooks/useMenuPress.d.ts +11 -1
  102. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  103. package/dist/hooks/useMenuPress.js +43 -5
  104. package/dist/utils/disabledInteraction.d.ts +21 -0
  105. package/dist/utils/disabledInteraction.d.ts.map +1 -0
  106. package/dist/utils/disabledInteraction.js +33 -0
  107. package/dist/utils/timeParser.d.ts +4 -0
  108. package/dist/utils/timeParser.d.ts.map +1 -1
  109. package/dist/utils/timeParser.js +9 -2
  110. package/package.json +4 -4
  111. package/scripts/agent-doc-state.mjs +58 -9
  112. package/src/Banner/Banner.spec.md +37 -32
  113. package/src/BottomSheet/BottomSheet.spec.md +34 -54
  114. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  115. package/src/BottomSheet/BottomSheet.tsx +8 -4
  116. package/src/BottomSheet/BottomSheetSwitcher.spec.md +125 -58
  117. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
  118. package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
  119. package/src/Calendar/Calendar.test.tsx +68 -24
  120. package/src/Calendar/Calendar.tsx +2 -1
  121. package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
  122. package/src/CheckboxInput/CheckboxInput.tsx +16 -26
  123. package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
  124. package/src/CheckboxList/CheckboxList.test.tsx +9 -6
  125. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  126. package/src/ClickableCard/ClickableCard.test.tsx +5 -4
  127. package/src/ClickableCard/ClickableCard.tsx +9 -9
  128. package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
  129. package/src/CodeBlock/CodeBlock.tsx +11 -1
  130. package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
  131. package/src/ComplexSelector/ComplexSelector.tsx +21 -4
  132. package/src/ContextMenu/ContextMenu.test.tsx +41 -0
  133. package/src/DateInput/DateInput.doc.mjs +9 -8
  134. package/src/DateInput/DateInput.test.tsx +88 -13
  135. package/src/DateInput/DateInput.tsx +49 -21
  136. package/src/DateInput/DateInputTouch.test.tsx +22 -1
  137. package/src/DateInput/NativeDateField.test.tsx +29 -3
  138. package/src/DateInput/NativeDateField.tsx +20 -7
  139. package/src/DateInput/TouchDateField.tsx +24 -9
  140. package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
  141. package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
  142. package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
  143. package/src/DateRangeInput/DateRangeInput.tsx +40 -20
  144. package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
  145. package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
  146. package/src/DateTimeInput/DateTimeInput.tsx +42 -26
  147. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
  148. package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
  149. package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
  150. package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
  151. package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
  152. package/src/Dialog/Dialog.test.tsx +39 -0
  153. package/src/Dialog/Dialog.tsx +9 -4
  154. package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
  155. package/src/DropdownMenu/DropdownMenu.tsx +79 -8
  156. package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
  157. package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
  158. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
  159. package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
  160. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
  161. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
  162. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
  163. package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
  164. package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
  165. package/src/DropdownMenu/menuRegionStops.ts +2 -2
  166. package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
  167. package/src/FieldStatus/FieldStatus.spec.md +3 -2
  168. package/src/FileInput/FileInput.test.tsx +30 -7
  169. package/src/FileInput/FileInput.tsx +21 -11
  170. package/src/Link/Link.test.tsx +21 -7
  171. package/src/Link/Link.tsx +16 -24
  172. package/src/Link/__tests__/Link.a11y.states.ts +1 -1
  173. package/src/Markdown/Markdown.rerender.test.tsx +143 -0
  174. package/src/Markdown/Markdown.tsx +89 -35
  175. package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
  176. package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
  177. package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
  178. package/src/MultiSelector/MultiSelector.test.tsx +49 -7
  179. package/src/MultiSelector/MultiSelector.tsx +53 -29
  180. package/src/Pagination/Pagination.test.tsx +5 -1
  181. package/src/Pagination/Pagination.tsx +3 -1
  182. package/src/Popover/Popover.test.tsx +15 -0
  183. package/src/Popover/Popover.tsx +19 -8
  184. package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
  185. package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
  186. package/src/Selector/Selector.doc.mjs +4 -4
  187. package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
  188. package/src/Selector/Selector.spec.md +11 -11
  189. package/src/Selector/Selector.test.tsx +64 -6
  190. package/src/Selector/Selector.tsx +51 -21
  191. package/src/Selector/SelectorOption.tsx +4 -0
  192. package/src/SideNav/SideNav.test.tsx +35 -0
  193. package/src/Slider/Slider.spec.md +11 -11
  194. package/src/Slider/Slider.test.tsx +3 -3
  195. package/src/Slider/Slider.tsx +8 -15
  196. package/src/Switch/Switch.test.tsx +85 -8
  197. package/src/Switch/Switch.tsx +26 -16
  198. package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
  199. package/src/Switch/switch.markers.stylex.ts +4 -0
  200. package/src/Table/Table.spec.md +2 -0
  201. package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
  202. package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
  203. package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
  204. package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
  205. package/src/TimeInput/Presentation.test.tsx +23 -0
  206. package/src/TimeInput/TimeInput.doc.mjs +13 -10
  207. package/src/TimeInput/TimeInput.test.tsx +139 -7
  208. package/src/TimeInput/TimeInput.tsx +53 -24
  209. package/src/TimeInput/TouchTimeField.tsx +27 -5
  210. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  211. package/src/Timestamp/Timestamp.test.tsx +168 -0
  212. package/src/Timestamp/Timestamp.tsx +62 -11
  213. package/src/Token/Token.test.tsx +26 -4
  214. package/src/Token/Token.tsx +6 -2
  215. package/src/Tokenizer/Tokenizer.spec.md +28 -28
  216. package/src/Tooltip/Tooltip.test.tsx +51 -6
  217. package/src/Tooltip/Tooltip.tsx +41 -18
  218. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
  219. package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
  220. package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
  221. package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
  222. package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
  223. package/src/Tooltip/useTooltip.test.tsx +23 -0
  224. package/src/Tooltip/useTooltip.tsx +22 -9
  225. package/src/TopNav/TopNav.tsx +11 -1
  226. package/src/TopNav/TopNavHeading.tsx +1 -0
  227. package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
  228. package/src/TreeList/TreeList.test.tsx +28 -0
  229. package/src/TreeList/TreeListItem.tsx +6 -2
  230. package/src/Typeahead/Typeahead.test.tsx +89 -0
  231. package/src/Typeahead/Typeahead.tsx +15 -2
  232. package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
  233. package/src/hooks/useGridFocus.ts +16 -1
  234. package/src/hooks/useMenuPress.ts +45 -7
  235. package/src/utils/disabledInteraction.ts +40 -0
  236. package/src/utils/timeParser.test.ts +11 -0
  237. package/src/utils/timeParser.ts +9 -2
@@ -0,0 +1,33 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file disabledInteraction.ts
5
+ * @input An optional caller keydown handler
6
+ * @output Event props that keep a focusable, aria-disabled control inert
7
+ * @position Shared by controls that replace native `disabled` with
8
+ * `aria-disabled` under spec:AST-085 (FR1, FR2)
9
+ */
10
+
11
+ /**
12
+ * Event props for a disabled control that stays focusable. Native `disabled`
13
+ * dispatched no click at all; without it, Enter and Space still synthesize a
14
+ * click that native listeners on the element (a wrapping Popover's trigger)
15
+ * receive. Cancelling those keys keeps the click from existing, and the click
16
+ * handler cancels pointer clicks and keeps them from reaching ancestors. Other
17
+ * keys still reach the caller's handler.
18
+ */
19
+ export function disabledInteractionProps(onKeyDown) {
20
+ return {
21
+ onClick: event => {
22
+ event.preventDefault();
23
+ event.stopPropagation();
24
+ },
25
+ onKeyDown: event => {
26
+ if (event.key === 'Enter' || event.key === ' ') {
27
+ event.preventDefault();
28
+ } else {
29
+ onKeyDown?.(event);
30
+ }
31
+ }
32
+ };
33
+ }
@@ -70,6 +70,10 @@ export declare function isTimeInRange(time: ISOTimeString, min?: ISOTimeString,
70
70
  export declare function clampTime(time: ISOTimeString, min?: ISOTimeString, max?: ISOTimeString, includeSeconds?: boolean): ISOTimeString;
71
71
  /**
72
72
  * Adjusts a time by adding/subtracting minutes.
73
+ *
74
+ * `deltaMinutes` is a whole number of minutes. A fractional delta leaves the
75
+ * time unchanged rather than returning a value that is no longer a time —
76
+ * `HH:MM` has no place for a fraction of a minute.
73
77
  */
74
78
  export declare function adjustTime(time: ISOTimeString, deltaMinutes: number, includeSeconds?: boolean): ISOTimeString;
75
79
  //# sourceMappingURL=timeParser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"timeParser.d.ts","sourceRoot":"","sources":["../../src/utils/timeParser.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;CAAC,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAMvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA6B5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAUf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAqBR;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAmBR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,cAAc,GAAE,OAAe,GAC9B,aAAa,GAAG,IAAI,CA6HtB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,CAAC,EAAE,aAAa,GAAG,SAAS,EAC5B,CAAC,EAAE,aAAa,GAAG,SAAS,GAC3B,MAAM,CA4BR;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,GAClB,OAAO,CAQT;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAoBf;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,MAAM,EACpB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAwBf"}
1
+ {"version":3,"file":"timeParser.d.ts","sourceRoot":"","sources":["../../src/utils/timeParser.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;CAAC,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAMvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA6B5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAUf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAqBR;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAmBR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,cAAc,GAAE,OAAe,GAC9B,aAAa,GAAG,IAAI,CA6HtB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,CAAC,EAAE,aAAa,GAAG,SAAS,EAC5B,CAAC,EAAE,aAAa,GAAG,SAAS,GAC3B,MAAM,CA4BR;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,GAClB,OAAO,CAQT;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAoBf;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,MAAM,EACpB,cAAc,GAAE,OAAe,GAC9B,aAAa,CA2Bf"}
@@ -313,6 +313,10 @@ export function clampTime(time, min, max, includeSeconds = false) {
313
313
 
314
314
  /**
315
315
  * Adjusts a time by adding/subtracting minutes.
316
+ *
317
+ * `deltaMinutes` is a whole number of minutes. A fractional delta leaves the
318
+ * time unchanged rather than returning a value that is no longer a time —
319
+ * `HH:MM` has no place for a fraction of a minute.
316
320
  */
317
321
  export function adjustTime(time, deltaMinutes, includeSeconds = false) {
318
322
  const parsed = parseISOTime(time);
@@ -321,8 +325,11 @@ export function adjustTime(time, deltaMinutes, includeSeconds = false) {
321
325
  }
322
326
 
323
327
  // A non-finite delta would spin the wrap-around forever (-Infinity) or
324
- // produce "NaN:NaN" (NaN) — return the input unchanged instead.
325
- if (!Number.isFinite(deltaMinutes)) {
328
+ // produce "NaN:NaN" (NaN), and a fractional one is never rounded on the way
329
+ // out — formatISOTime pads "30.5" to two characters and leaves it, so the
330
+ // caller gets back a value that is no longer a time. Steps are whole
331
+ // minutes: return the input unchanged instead.
332
+ if (!Number.isInteger(deltaMinutes)) {
326
333
  return time;
327
334
  }
328
335
  let totalMinutes = parsed.hour * 60 + parsed.minute + deltaMinutes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.2",
3
+ "version": "0.7.3-canary.02f5fe4",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -792,6 +792,8 @@
792
792
  "react-dom": ">=19.0.0"
793
793
  },
794
794
  "devDependencies": {
795
+ "@astryxdesign/a11y-spec": "0.7.3-canary.02f5fe4",
796
+ "@astryxdesign/cli": "0.7.3-canary.02f5fe4",
795
797
  "@babel/cli": "^7.29.7",
796
798
  "@babel/core": "^7.29.7",
797
799
  "@babel/preset-react": "^7.29.7",
@@ -802,9 +804,7 @@
802
804
  "@testing-library/jest-dom": "^6.6.0",
803
805
  "@testing-library/react": "^16.3.2",
804
806
  "@types/babel__core": "^7.20.5",
805
- "tailwindcss": "^4.3.2",
806
- "@astryxdesign/a11y-spec": "0.0.0",
807
- "@astryxdesign/cli": "0.7.2"
807
+ "tailwindcss": "^4.3.2"
808
808
  },
809
809
  "dependencies": {
810
810
  "intl-messageformat": "^11.2.9"
@@ -39,10 +39,31 @@ import * as path from 'node:path';
39
39
  export const AGENTS_MD = 'AGENTS.md';
40
40
  export const CLAUDE_MD = 'CLAUDE.md';
41
41
  export const CLAUDE_DIR_MD = '.claude/CLAUDE.md'; // cross-platform literal
42
+ export const GEMINI_MD = 'GEMINI.md';
42
43
  export const CURSOR_RULES = '.cursorrules';
43
44
  export const HERMES_DOT_MD = '.hermes.md';
44
45
  export const HERMES_MD = 'HERMES.md';
45
46
 
47
+ /**
48
+ * The page workflow every Astryx agent text gives, written with the command
49
+ * name `astryx`: the project agent docs and the computer-level skill both
50
+ * render these lines, so the two never drift.
51
+ */
52
+ export const WORKFLOW_TITLE =
53
+ 'WORKFLOW — start every page from a template, never from scratch:';
54
+ export const WORKFLOW_STEPS = Object.freeze([
55
+ '1. `astryx build "<idea>"` — names the template to scaffold from and the parts it lacks.',
56
+ '2. `astryx template <name> <path>` — scaffold it; keep its frame, gap and padding; replace the content.',
57
+ '3. `astryx template <Block>` for parts it lacks; `astryx component <Name>` to read props before using one.',
58
+ ]);
59
+
60
+ /**
61
+ * The self-check's fix for a project with no StyleX or Tailwind compiler,
62
+ * and for an agent outside a project.
63
+ */
64
+ export const SELF_CHECK_CSS =
65
+ 'replace any raw <div>/<span>, imported .css/@apply, or hardcoded #hex/px with the component or a token';
66
+
46
67
  export const MARKER_START = '<!-- ASTRYX:START -->';
47
68
  export const MARKER_END = '<!-- ASTRYX:END -->';
48
69
  // Legacy markers — read during migration so the script finds existing XDS blocks
@@ -51,15 +72,18 @@ export const LEGACY_MARKER_END = '<!-- XDS:END -->';
51
72
 
52
73
  /**
53
74
  * The canonical set of EVERY location an --agent preset (or the default) can
54
- * write the Astryx block. SINGLE SOURCE OF TRUTH: discovery, removal, and the
55
- * `isAstryxInitialized` predicate all derive from this list, so "where init
56
- * writes" and "where we look" can never drift. (Explicit --agent-docs-path
57
- * targets are user-chosen and not enumerable here.)
75
+ * write a managed Astryx block — the full block, or the one-line import of
76
+ * AGENTS.md that the default writes to GEMINI.md. SINGLE
77
+ * SOURCE OF TRUTH: discovery, removal, and the `isAstryxInitialized` predicate
78
+ * all derive from this list, so "where init writes" and "where we look" can
79
+ * never drift. (Explicit --agent-docs-path targets are user-chosen and not
80
+ * enumerable here.)
58
81
  */
59
82
  export const AGENT_DOC_PATHS = [
60
83
  AGENTS_MD, // Codex / ChatGPT / Muse / generic
61
84
  CLAUDE_MD, // Claude Code (root)
62
85
  CLAUDE_DIR_MD, // Claude Code (.claude/CLAUDE.md)
86
+ GEMINI_MD, // Gemini CLI
63
87
  CURSOR_RULES, // Cursor
64
88
  HERMES_DOT_MD, // Hermes
65
89
  HERMES_MD, // Hermes
@@ -74,7 +98,7 @@ export const INIT_MARKERS = [MARKER_START, LEGACY_MARKER_START];
74
98
  /**
75
99
  * Find all existing agent doc files in a directory, across EVERY location any
76
100
  * preset can write (see {@link AGENT_DOC_PATHS}: AGENTS.md, CLAUDE.md,
77
- * .claude/CLAUDE.md, .cursorrules, .hermes.md, HERMES.md).
101
+ * .claude/CLAUDE.md, GEMINI.md, .cursorrules, .hermes.md, HERMES.md).
78
102
  * @param {string} targetDir
79
103
  * @returns {string[]} Relative paths of existing agent doc files
80
104
  */
@@ -82,10 +106,35 @@ export function discoverAgentDocs(targetDir) {
82
106
  return AGENT_DOC_PATHS.filter(p => fs.existsSync(path.join(targetDir, p)));
83
107
  }
84
108
 
109
+ /**
110
+ * Whether `content` holds an Astryx block. A managed block that only imports
111
+ * another file (the one-line `@./AGENTS.md` that init writes to GEMINI.md) does
112
+ * not count: it holds nothing once that file has lost its block. A malformed
113
+ * block still counts, so a half-written file reads as set up.
114
+ *
115
+ * @param {string} content
116
+ * @returns {boolean}
117
+ */
118
+ export function holdsAstryxBlock(content) {
119
+ for (const marker of INIT_MARKERS) {
120
+ let at = content.indexOf(marker);
121
+ while (at !== -1) {
122
+ const from = at + marker.length;
123
+ const end = content.indexOf('<!--', from);
124
+ const inner = (
125
+ end === -1 ? content.slice(from) : content.slice(from, end)
126
+ ).trim();
127
+ if (!/^@\S+$/.test(inner)) return true;
128
+ at = content.indexOf(marker, from);
129
+ }
130
+ }
131
+ return false;
132
+ }
133
+
85
134
  /**
86
135
  * Single source of truth for "is Astryx set up in this project?" — true when any
87
- * agent-doc file already carries the Astryx marker, i.e. `init` / `agent-docs`
88
- * has run. Reused by the init & upgrade commands, the per-command setup nudge
136
+ * agent-doc file already holds an Astryx block, i.e. `init` / `agent-docs` has
137
+ * run. Reused by the init & upgrade commands, the per-command setup nudge
89
138
  * (enforcement layer 3), and the cli postinstall nudge (layer 2). Core's
90
139
  * postinstall (separate package, layer 1) mirrors the same marker contract.
91
140
  *
@@ -96,7 +145,7 @@ export function isAstryxInitialized(targetDir = process.cwd()) {
96
145
  for (const rel of discoverAgentDocs(targetDir)) {
97
146
  try {
98
147
  const content = fs.readFileSync(path.join(targetDir, rel), 'utf-8');
99
- if (INIT_MARKERS.some(m => content.includes(m))) {
148
+ if (holdsAstryxBlock(content)) {
100
149
  return true;
101
150
  }
102
151
  } catch {
@@ -136,7 +185,7 @@ export function isAstryxPromptInReach(startDir = process.cwd()) {
136
185
  * @returns {string | null}
137
186
  */
138
187
  function findRepositoryRoot(start) {
139
- for (let dir = start; ; ) {
188
+ for (let dir = start; ;) {
140
189
  if (fs.existsSync(path.join(dir, '.git'))) return dir;
141
190
  const parent = path.dirname(dir);
142
191
  if (parent === dir) return null;
@@ -42,29 +42,29 @@ separately targetable surface. The status and content targets remain unchanged.
42
42
  - Released default preserved: `no` — Banner no longer derives a live-region role from visual status
43
43
  - Compatibility class: breaking accessibility-semantic correction; visual output, DOM structure, props, theming targets, and interaction behavior are unchanged
44
44
  - Controlled/uncontrolled behavior: unchanged
45
- - Migration decision: announce async outcomes in their transition handler with `useAnnounce`; add an explicit `role="status"` only for a persistent semantic mirror, and reserve `role="alert"` for genuinely urgent interruption
45
+ - Migration decision: announce async outcomes in their transition handler with `useAnnounce`; add an explicit `role="status"` only for a persistent semantic mirror, and reserve `role="alert"` for genuinely urgent interruption. Inside a mounted live container, `spec:AST-086` FR4 applies instead.
46
46
 
47
47
  Consumer migration instructions belong in consumer docs and release notes.
48
48
 
49
49
  ### DEP-0007 — Deprecate live-region roles derived from `status`
50
50
 
51
- | Field | Value |
52
- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
- | id | `DEP-0007` |
54
- | cleanup | `CLN-0015` |
55
- | package | `@astryxdesign/core` |
56
- | surface | Default ARIA role of `Banner`: `role="alert"` for `warning` and `error`, `role="status"` for `info` and `success`, derived from `status` when the caller passes no `role` |
57
- | old contract | Visual `status` selects the live-region role of the Banner root |
58
- | replacement | A caller that announces an async outcome calls `useAnnounce` from the transition that produced it. A caller whose Banner is a persistent semantic mirror passes `role="status"`; genuinely urgent content passes `role="alert"`. Both explicit roles already reach the Banner root |
59
- | direct authority | `component:Banner`. Owners: `cixzhang`, `imdreamrunner` |
60
- | warning | Consumer docs and the release notes name `DEP-0007` and the explicit `role` that keeps the released semantics. No runtime warning (`spec:AST-017` FR29) |
61
- | migration | Non-mechanical: a caller that announces an async outcome calls `useAnnounce` from the transition that produced it; a caller whose Banner is a persistent mirror or urgent content passes `role="status"` or `role="alert"` |
62
- | codemod | None: whether a Banner needs an announcement, and when, depends on the transition that renders it, which only the caller knows; adding the released role everywhere would restore the coupling this cleanup removes |
63
- | downstream | Maintained docs teach `useAnnounce` for async outcomes and explicit `role` for persistent mirrors |
64
- | transition | no prior transition |
65
- | state | `proposed` |
66
- | open decision | Banner FR and DEC entries that make `status` visual-only |
67
- | target plan | `CLN-0015` in the next minor release |
51
+ | Field | Value |
52
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | id | `DEP-0007` |
54
+ | cleanup | `CLN-0015` |
55
+ | package | `@astryxdesign/core` |
56
+ | surface | Default ARIA role of `Banner`: `role="alert"` for `warning` and `error`, `role="status"` for `info` and `success`, derived from `status` when the caller passes no `role` |
57
+ | old contract | Visual `status` selects the live-region role of the Banner root |
58
+ | replacement | A caller that announces an async outcome calls `useAnnounce` from the transition that produced it. A caller whose Banner is a persistent semantic mirror passes `role="status"`; genuinely urgent content passes `role="alert"`. Both explicit roles already reach the Banner root. Inside a mounted live container, `spec:AST-086` FR4 applies instead. |
59
+ | direct authority | `component:Banner`. Owners: `cixzhang`, `imdreamrunner` |
60
+ | warning | Consumer docs and the release notes name `DEP-0007` and the explicit `role` that keeps the released semantics. No runtime warning (`spec:AST-017` FR29) |
61
+ | migration | Non-mechanical: a caller that announces an async outcome calls `useAnnounce` from the transition that produced it; a caller whose Banner is a persistent mirror or urgent content passes `role="status"` or `role="alert"`. Inside a mounted live container, `spec:AST-086` FR4 applies instead. |
62
+ | codemod | None: whether a Banner needs an announcement, and when, depends on the transition that renders it, which only the caller knows; adding the released role everywhere would restore the coupling this cleanup removes |
63
+ | downstream | Maintained docs teach `useAnnounce` for async outcomes and explicit `role` for persistent mirrors. Inside a mounted live container, `spec:AST-086` FR4 applies instead. |
64
+ | transition | no prior transition |
65
+ | state | `proposed` |
66
+ | open decision | Banner FR and DEC entries that make `status` visual-only |
67
+ | target plan | `CLN-0015` in the next minor release |
68
68
 
69
69
  ### CLN-0015 — Banner renders no live-region role by default
70
70
 
@@ -105,16 +105,16 @@ prop vocabulary or extensibility rule changes.
105
105
  The requirements below describe the current implementation and its additive
106
106
  frame target contract.
107
107
 
108
- | ID | Invariant | Basis | Contract state |
109
- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------ |
110
- | FR1 | The current render places `banner` on the colored status surface, which is Banner's primary painted surface and reflects `container` and `status`. | Current source, public docs, and tests | Current behavior |
111
- | FR2 | The Banner frame receives the public ref, role, supported DOM props, `xstyle`, `className`, and `style`; it also paints the selected `boxShadow`. | Current source and tests | Current behavior |
112
- | FR3 | An elevated `card` frame also paints the whole-banner radius. A `section` frame remains square. A non-elevated frame does not receive the frame-radius style. | Current source and elevation tests | Current behavior |
113
- | FR4 | Card shape is split across surface owners: the status surface owns all corners without visible content or the top corners with visible content; the content surface owns the bottom corners. | Current source | Current behavior |
114
- | FR5 | Theme-authored `borderRadius` on `banner` currently writes the private `--_banner-radius` variable on the Status surface only. CSS inheritance does not carry that value upward to the frame or sideways to the Content surface, so both keep their fallback radius. | Current compiler output and CSS inheritance | Current reachability gap |
115
- | FR6 | The Banner frame exposes `banner-frame` and reflects `container` and `elevation`. The existing `banner` target remains on the Status surface rather than moving to the outermost DOM element. | Owner decision; current source and tests | Current behavior |
116
- | FR7 | `banner-frame` is additive. The existing `banner`, `banner-icon`, `banner-description`, and `banner-content` targets and their current axes remain unchanged. | Current target inventory; compatibility policy | Current behavior |
117
- | FR8 | `status` controls visual meaning only and MUST NOT derive `role`, `aria-live`, or an announcement. Standard DOM props MAY provide an explicit persistent role. Async outcomes MUST announce from the transition handler through `useAnnounce`; `role="alert"` is reserved for genuinely urgent interruptive content. | Accessibility Checklist §Announcements; owner decision | Current behavior |
108
+ | ID | Invariant | Basis | Contract state |
109
+ | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------ |
110
+ | FR1 | The current render places `banner` on the colored status surface, which is Banner's primary painted surface and reflects `container` and `status`. | Current source, public docs, and tests | Current behavior |
111
+ | FR2 | The Banner frame receives the public ref, role, supported DOM props, `xstyle`, `className`, and `style`; it also paints the selected `boxShadow`. | Current source and tests | Current behavior |
112
+ | FR3 | An elevated `card` frame also paints the whole-banner radius. A `section` frame remains square. A non-elevated frame does not receive the frame-radius style. | Current source and elevation tests | Current behavior |
113
+ | FR4 | Card shape is split across surface owners: the status surface owns all corners without visible content or the top corners with visible content; the content surface owns the bottom corners. | Current source | Current behavior |
114
+ | FR5 | Theme-authored `borderRadius` on `banner` currently writes the private `--_banner-radius` variable on the Status surface only. CSS inheritance does not carry that value upward to the frame or sideways to the Content surface, so both keep their fallback radius. | Current compiler output and CSS inheritance | Current reachability gap |
115
+ | FR6 | The Banner frame exposes `banner-frame` and reflects `container` and `elevation`. The existing `banner` target remains on the Status surface rather than moving to the outermost DOM element. | Owner decision; current source and tests | Current behavior |
116
+ | FR7 | `banner-frame` is additive. The existing `banner`, `banner-icon`, `banner-description`, and `banner-content` targets and their current axes remain unchanged. | Current target inventory; compatibility policy | Current behavior |
117
+ | FR8 | `status` controls visual meaning only and MUST NOT derive `role`, `aria-live`, or an announcement. Standard DOM props MAY provide an explicit persistent role, except on a Banner added inside a mounted live container (`spec:AST-086` FR4). Async outcomes MUST announce from the transition handler through `useAnnounce`, except inside a mounted live container, where `spec:AST-086` FR4 applies; `role="alert"` is reserved for genuinely urgent interruptive content. | Accessibility Checklist §Announcements; owner decision | Current behavior |
118
118
 
119
119
  ### Allowed variation
120
120
 
@@ -154,11 +154,15 @@ runtime work.
154
154
  `status` communicates visual category through color and icon; it does not choose
155
155
  announcement urgency. Banner renders no live-region role by default. A caller may
156
156
  provide `role="status"` through the standard DOM surface when the visible banner
157
- is intentionally a persistent semantic mirror. Async outcomes use `useAnnounce`
157
+ is intentionally a persistent semantic mirror, except on a Banner added inside a
158
+ mounted live container. Async outcomes use `useAnnounce`
158
159
  from the transition handler, which writes into a previously mounted live region;
160
+ inside a mounted live container, that container delivers them
161
+ (`spec:AST-086` FR4);
159
162
  a Banner mounted together with its message is not a reliable announcement.
160
163
  `role="alert"` remains available as an explicit native role, but is reserved for
161
- genuinely urgent content that should interrupt current speech.
164
+ genuinely urgent content that should interrupt current speech; it is not used on
165
+ a Banner added inside a mounted live container.
162
166
 
163
167
  Dismissal focus handoff, disclosure controls, and accessible control names remain
164
168
  unchanged.
@@ -247,8 +251,9 @@ placement and would imply that the target must remain on the outermost element.
247
251
  Banner status selects color and icon only. It does not assign `role="status"` or
248
252
  `role="alert"`. Async outcomes announce through `useAnnounce` from the transition
249
253
  handler so the message reaches a persistent live region rather than one mounted
250
- with its content. An explicit native role remains available through BaseProps for
251
- a persistent semantic mirror or the rare genuinely urgent alert.
254
+ with its content; inside a mounted live container, `spec:AST-086` FR4 applies. An explicit native role remains available through BaseProps for
255
+ a persistent semantic mirror or the rare genuinely urgent alert, except on a
256
+ Banner added inside a mounted live container.
252
257
 
253
258
  Rejected: deriving alert urgency from `warning` or `error`. Severity and urgency
254
259
  are independent, and a static error present on first paint is not an event that
@@ -3,11 +3,11 @@ schema_version: 3
3
3
  template_version: 3
4
4
  kind: component
5
5
  id: component:BottomSheet
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: imdreamrunner
10
+ approved_at: 2026-10-21
11
11
  owners: [cixzhang]
12
12
  review_triggers: [theming, layout, public-api]
13
13
  verified_by:
@@ -37,52 +37,24 @@ system_specs: [spec:AST-025]
37
37
  ## Intent
38
38
 
39
39
  BottomSheet presents caller-provided content in a panel that rises from the
40
- bottom edge. This draft records current consumer anatomy, keyboard-reachable
41
- scrolling, theming reachability, and the Content area's container padding.
40
+ bottom edge. This record covers consumer anatomy, the native dialog every sheet
41
+ renders, keyboard-reachable scrolling, theming reachability, and the Content
42
+ area's container padding. Where the shipped sheet does not yet meet a
43
+ requirement, the row carries a present-tense implementation gap note.
42
44
 
43
45
  ## Compatibility and migration
44
46
 
45
- - Released default preserved: `no` — with no `padding` prop and no theme
46
- padding, the Content area insets by `--spacing-4` on every logical edge
47
- - Compatibility class: breaking default, scheduled for a minor release. Content
48
- whose only child is a padded Section keeps its geometry because that Section
49
- escapes the inset; content that supplies its own inset is padded twice until
50
- it drops that inset or sets `padding={0}`
47
+ - Released default preserved: `yes`; with no `padding` prop and no theme
48
+ padding, the Content area insets by `--spacing-4` on every logical edge, and
49
+ `padding={0}` keeps an unpadded Content area
50
+ - Compatibility class: records the shipped anatomy, padded Content area, and
51
+ native dialog; no public prop, default, or export changes
51
52
  - Controlled/uncontrolled behavior: unchanged
52
- - Migration decision: `padding={0}` restores the released unpadded Content area
53
+ - Migration decision: none; the precedence rule under Transformation and
54
+ precedence order is the whole contract
53
55
 
54
56
  Consumer migration instructions belong in consumer docs and release notes.
55
57
 
56
- ### DEP-0009 — Deprecate the unpadded default Content area of `BottomSheet`
57
-
58
- | Field | Value |
59
- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
- | id | `DEP-0009` |
61
- | cleanup | `CLN-0018` |
62
- | package | `@astryxdesign/core` |
63
- | surface | Default inset of the `BottomSheet` Content area when no `padding` prop and no theme padding are set |
64
- | old contract | The Content area has no padding |
65
- | replacement | `padding={0}` keeps the unpadded Content area; `padding` and theme padding already resolve before the default |
66
- | direct authority | `component:BottomSheet` Compatibility and migration. Owners: `cixzhang` |
67
- | warning | Consumer docs and the release notes name `DEP-0009` and `padding={0}`. No runtime warning (`spec:AST-017` FR29) |
68
- | migration | Mechanical: add `padding={0}` to a BottomSheet that sets no padding, unless its only child is a padded Section or a Layout |
69
- | codemod | `astryx upgrade` adds `padding={0}` before any spread, skips sheets whose only child is a core Section or Layout, and marks a nested padding-less Layout for review |
70
- | downstream | In-repo sheets that supply their own inset pass `padding={0}`; consumer examples rely on the default inset |
71
- | transition | no prior transition |
72
- | state | `proposed` |
73
- | open decision | This record is draft and needs approval |
74
- | target plan | `CLN-0018` in the next minor release |
75
-
76
- ### CLN-0018 — BottomSheet pads its Content area by default
77
-
78
- | Field | Value |
79
- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
80
- | id | `CLN-0018` |
81
- | lifecycle | `DEP-0009` |
82
- | delta | With no `padding` prop and no theme padding, the Content area insets by `--spacing-4` on every logical edge and publishes that inset |
83
- | rollback | Restore the unpadded default from the final-patch baseline |
84
- | state | `pending` — lands in a declared minor with its `[breaking]` Changeset |
85
-
86
58
  ## Ownership boundary
87
59
 
88
60
  **Owns**
@@ -93,14 +65,19 @@ Consumer migration instructions belong in consumer docs and release notes.
93
65
  - The Content area's container padding, published per
94
66
  `architecture:container-padding`.
95
67
  - Standalone sheet presentation, including its optional native-dialog Scrim.
68
+ - The native dialog every sheet renders, standalone or switcher-managed
69
+ (`component:BottomSheetSwitcher` DEC-1). Implementation gap: a
70
+ switcher-managed sheet renders a panel inside the switcher's one shared
71
+ dialog and owns no dialog of its own.
96
72
 
97
73
  **Does not own / non-goals**
98
74
 
99
75
  - Caller-provided content rendered inside the Content area.
100
76
  - A separate target for Content area, Handle, or Scrim; none exists on current
101
77
  `main`.
102
- - A shared switcher dialog or scrim — owned by BottomSheetSwitcher when the sheet
103
- participates in that host.
78
+ - The presentation lifecycle of its dialog and the flow's scrim ownership when
79
+ the sheet participates in a BottomSheetSwitcher — owned by BottomSheetSwitcher
80
+ (`component:BottomSheetSwitcher` DEC-1).
104
81
  - Shared layer hosting or dismissal policy.
105
82
 
106
83
  ## Public concepts
@@ -110,12 +87,12 @@ in `BottomSheet.doc.mjs`.
110
87
 
111
88
  ## Behavioral and layout contract
112
89
 
113
- | ID | Candidate invariant | Basis | Draft review state |
114
- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -------------------------------------------------- |
115
- | FR1 | The current presented sheet contains one Sheet panel, one scrolling Content area, and one decorative Handle; a Scrim is present only in scrim-backed presentation. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
116
- | FR2 | The Sheet panel carries `bottom-sheet`; Content area, Handle, and Scrim carry no BottomSheet public target. | Current source, docs, and tests | Verified current behavior; no target change |
117
- | FR3 | Content renders in one real observed content box. Effective overflow, containment, and keyboard access come from `useScrollableArea`; the named overflowing body delegates forward Tab entry to a safe first sequential native link/button. | `spec:AST-025`; issue #5207; tests | Focus-time prototype for owner review |
118
- | FR4 | The Content area pads caller content by the sheet's container inset and publishes the applied inset to descendants as a container publisher. | `architecture:container-padding` | Admitted; implementation pending |
90
+ | ID | Invariant | Basis |
91
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
92
+ | FR1 | The current presented sheet contains one Sheet panel, one scrolling Content area, and one decorative Handle; a Scrim is present only in scrim-backed presentation. | Current source, docs, and tests |
93
+ | FR2 | The Sheet panel carries `bottom-sheet`; Content area, Handle, and Scrim carry no BottomSheet public target. | Current source, docs, and tests |
94
+ | FR3 | Content renders in one real observed content box. Effective overflow, containment, and keyboard access come from `useScrollableArea`; the named overflowing body delegates forward Tab entry to a safe first sequential native link/button. | `spec:AST-025`; tests |
95
+ | FR4 | The Content area pads caller content by the sheet's container inset and publishes the applied inset to descendants as a container publisher. | `architecture:container-padding` |
119
96
 
120
97
  ### Allowed variation
121
98
 
@@ -128,8 +105,10 @@ in `BottomSheet.doc.mjs`.
128
105
  - A standalone modal sheet renders Sheet panel, Content area, Handle, and Scrim.
129
106
  - A standalone non-modal sheet renders Sheet panel, Content area, and Handle
130
107
  without a Scrim.
131
- - A switcher-managed sheet renders the same panel anatomy while
132
- BottomSheetSwitcher owns the shared dialog and optional Scrim.
108
+ - A switcher-managed sheet renders the same panel anatomy in its own dialog while
109
+ BottomSheetSwitcher coordinates that dialog's lifecycle and the flow's one Scrim
110
+ (`component:BottomSheetSwitcher` DEC-1). Implementation gap: the panel
111
+ renders inside the switcher's one shared dialog.
133
112
 
134
113
  ### Transformation and precedence order
135
114
 
@@ -184,7 +163,7 @@ a decision that they must remain unthemeable.
184
163
  },
185
164
  "Scrim": {
186
165
  "none": {
187
- "reason": "reachability-gap: No BottomSheet public target reaches the native dialog backdrop or switcher-owned scrim."
166
+ "reason": "reachability-gap: No BottomSheet public target reaches the native dialog backdrop, standalone or switcher-coordinated."
188
167
  }
189
168
  }
190
169
  }
@@ -210,7 +189,8 @@ a decision that they must remain unthemeable.
210
189
  | ------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | --------------------------------- |
211
190
  | FR1 | `BottomSheet.test.tsx` render, content, Handle, and scrim behavior suites | Modal, non-modal, and switcher presentations | Removing a documented part fails existing content, structure, or dismissal assertions. | `audit:BottomSheet/anatomy` |
212
191
  | FR2 | Panel target assertion, source inspection, and theming target inventories | Sheet panel, Content area, Handle, and Scrim | Removing the panel target or documenting an unshipped child target fails evidence or inventory. | `audit:BottomSheet/theming` |
213
- | FR3, AR1–AR2 | `BottomSheetKeyboard.test.tsx`; `BottomSheetKeyboard.a11y.browser.spec.ts`; open Storybook stories | fitting/overflow, plain text, usable/unusable/dynamic descendants, focus | Missing/duplicate keyboard access, stale ownership, focus movement, or broken content-box layout fails. | `audit:BottomSheet/accessibility` | | Layout evidence | `BottomSheetPanel.test.tsx` | Floating Handle and scrolling Content area | Reordering or merging the stable parts fails existing panel structure and style assertions. | `audit:BottomSheet/anatomy` |
192
+ | FR3, AR1–AR2 | `BottomSheetKeyboard.test.tsx`; `BottomSheetKeyboard.a11y.browser.spec.ts`; open Storybook stories | fitting/overflow, plain text, usable/unusable/dynamic descendants, focus | Missing/duplicate keyboard access, stale ownership, focus movement, or broken content-box layout fails. | `audit:BottomSheet/accessibility` |
193
+ | Layout evidence | `BottomSheetPanel.test.tsx` | Floating Handle and scrolling Content area | Reordering or merging the stable parts fails existing panel structure and style assertions. | `audit:BottomSheet/anatomy` |
214
194
  | FR4 | `BottomSheetPanel.test.tsx` container padding tests; `derivedVarRegistry.test.ts` | default, `padding` prop, theme `padding`, lone padded Section child | The published inset differs from the applied padding, or a lone padded Section changes geometry. | `audit:BottomSheet/anatomy` |
215
195
  | 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:BottomSheet/theming` |
216
196
 
@@ -498,6 +498,35 @@ describe('BottomSheet', () => {
498
498
  expect(dialog).toHaveStyle({'--_sheet-scrim-opacity': '1'});
499
499
  });
500
500
 
501
+ it('stays open when a file picker inside it is dismissed, and still closes on its own close request', () => {
502
+ // A file input fires a bubbling `cancel` at itself when its picker is
503
+ // dismissed. It reaches the <dialog> like a close request but is not one.
504
+ const onOpenChange = vi.fn();
505
+ render(
506
+ <BottomSheet
507
+ isOpen
508
+ purpose="form"
509
+ onOpenChange={onOpenChange}
510
+ label="Upload">
511
+ <input type="file" aria-label="Picture" />
512
+ </BottomSheet>,
513
+ );
514
+
515
+ const fromPicker = new Event('cancel', {bubbles: true, cancelable: true});
516
+ fireEvent(screen.getByLabelText('Picture'), fromPicker);
517
+
518
+ expect(onOpenChange).not.toHaveBeenCalled();
519
+ expect(fromPicker.defaultPrevented).toBe(false);
520
+
521
+ fireEvent(
522
+ screen.getByRole('dialog'),
523
+ new Event('cancel', {cancelable: true}),
524
+ );
525
+
526
+ expect(onOpenChange).toHaveBeenCalledTimes(1);
527
+ expect(onOpenChange).toHaveBeenCalledWith(false);
528
+ });
529
+
501
530
  it('keeps a standalone modal sheet presented through its exit animation', () => {
502
531
  render(<ExitHarness />);
503
532
  const dialog = screen.getByRole('dialog', {name: 'Filters'});
@@ -361,12 +361,16 @@ function StandaloneBottomSheet({
361
361
  });
362
362
 
363
363
  // The native `cancel` event is a close request the stack never saw a press
364
- // for (Android back, the platform close watcher). Always preventDefault so
365
- // the browser cannot close the controlled <dialog>, then answer with the
366
- // stack's rules: only the top-most layer dismisses, and nothing dismisses
367
- // during an IME composition.
364
+ // for (Android back, the platform close watcher). One that bubbles up from
365
+ // inside is not: a file input fires it when its picker is dismissed, so it is
366
+ // left alone. Otherwise always preventDefault so the browser cannot close the
367
+ // controlled <dialog>, then answer with the stack's rules: only the top-most
368
+ // layer dismisses, and nothing dismisses during an IME composition.
368
369
  const handleCancel = useCallback(
369
370
  (event: React.SyntheticEvent<HTMLDialogElement>) => {
371
+ if (event.target !== event.currentTarget) {
372
+ return;
373
+ }
370
374
  event.preventDefault();
371
375
  if (shouldDismissOnCloseRequest()) {
372
376
  dismissOnEscape();