@astryxdesign/core 0.4.3 → 0.4.4-canary.0f4d45f

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 (200) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +79 -45
  3. package/dist/Banner/Banner.d.ts +3 -0
  4. package/dist/Banner/Banner.d.ts.map +1 -1
  5. package/dist/Banner/Banner.js +13 -3
  6. package/dist/BottomSheet/BottomSheet.d.ts +66 -0
  7. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
  8. package/dist/BottomSheet/BottomSheet.js +392 -0
  9. package/dist/BottomSheet/BottomSheetPanel.d.ts +60 -0
  10. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
  11. package/dist/BottomSheet/BottomSheetPanel.js +398 -0
  12. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
  13. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
  14. package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
  15. package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
  16. package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
  17. package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
  18. package/dist/BottomSheet/index.d.ts +11 -0
  19. package/dist/BottomSheet/index.d.ts.map +1 -0
  20. package/dist/BottomSheet/index.js +12 -0
  21. package/dist/BottomSheet/snapOffsets.d.ts +72 -0
  22. package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
  23. package/dist/BottomSheet/snapOffsets.js +130 -0
  24. package/dist/BottomSheet/useMobileKeyboard.d.ts +35 -0
  25. package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
  26. package/dist/BottomSheet/useMobileKeyboard.js +373 -0
  27. package/dist/BottomSheet/useSheetGestures.d.ts +180 -0
  28. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
  29. package/dist/BottomSheet/useSheetGestures.js +989 -0
  30. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  31. package/dist/Chat/ChatComposerInput.js +3 -4
  32. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  33. package/dist/ComplexSelector/ComplexSelector.js +12 -6
  34. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  35. package/dist/ContextMenu/ContextMenu.js +4 -2
  36. package/dist/DateInput/DateInput.d.ts.map +1 -1
  37. package/dist/DateInput/DateInput.js +9 -1
  38. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  39. package/dist/DateTimeInput/DateTimeInput.js +31 -1
  40. package/dist/Dialog/Dialog.d.ts.map +1 -1
  41. package/dist/Dialog/Dialog.js +2 -1
  42. package/dist/Field/InputClearButton.d.ts +2 -1
  43. package/dist/Field/InputClearButton.d.ts.map +1 -1
  44. package/dist/Field/InputClearButton.js +6 -1
  45. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  46. package/dist/Lightbox/Lightbox.js +9 -3
  47. package/dist/MobileNav/MobileNav.d.ts +12 -0
  48. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  49. package/dist/MobileNav/MobileNav.js +102 -30
  50. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  51. package/dist/MultiSelector/MultiSelector.js +21 -7
  52. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  53. package/dist/NumberInput/NumberInput.js +9 -1
  54. package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
  55. package/dist/Selector/Selector.d.ts.map +1 -1
  56. package/dist/Selector/Selector.js +14 -3
  57. package/dist/StatusDot/StatusDot.d.ts +43 -3
  58. package/dist/StatusDot/StatusDot.d.ts.map +1 -1
  59. package/dist/StatusDot/StatusDot.js +42 -5
  60. package/dist/Switch/Switch.d.ts.map +1 -1
  61. package/dist/Switch/Switch.js +5 -1
  62. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  63. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  64. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  65. package/dist/TimeInput/TimeInput.js +8 -1
  66. package/dist/Tokenizer/Tokenizer.js +11 -5
  67. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  68. package/dist/Tooltip/useTooltip.js +4 -1
  69. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  70. package/dist/Typeahead/BaseTypeahead.js +5 -3
  71. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  72. package/dist/Typeahead/Typeahead.js +10 -1
  73. package/dist/astryx.css +21 -0
  74. package/dist/hooks/index.d.ts +8 -1
  75. package/dist/hooks/index.d.ts.map +1 -1
  76. package/dist/hooks/index.js +8 -1
  77. package/dist/hooks/useFocusTrap.d.ts +0 -11
  78. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  79. package/dist/hooks/useFocusTrap.js +1 -11
  80. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  81. package/dist/hooks/useInputStatusIcon.js +1 -0
  82. package/dist/index.d.ts +1 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +1 -0
  85. package/dist/theme/defineTheme.d.ts +22 -4
  86. package/dist/theme/defineTheme.d.ts.map +1 -1
  87. package/dist/theme/expandColorScale.d.ts +17 -2
  88. package/dist/theme/expandColorScale.d.ts.map +1 -1
  89. package/dist/theme/expandColorScale.js +63 -33
  90. package/dist/utils/ime.d.ts +5 -0
  91. package/dist/utils/ime.d.ts.map +1 -0
  92. package/dist/utils/ime.js +63 -0
  93. package/dist/utils/index.d.ts +1 -0
  94. package/dist/utils/index.d.ts.map +1 -1
  95. package/dist/utils/index.js +2 -1
  96. package/locales/af-ZA.json +958 -0
  97. package/locales/ar-SA.json +998 -0
  98. package/locales/ca-ES.json +974 -0
  99. package/locales/cs-CZ.json +990 -0
  100. package/locales/da-DK.json +966 -0
  101. package/locales/de-DE.json +966 -0
  102. package/locales/el-GR.json +990 -0
  103. package/locales/en.json +48 -0
  104. package/locales/es-ES.json +978 -0
  105. package/locales/fi-FI.json +990 -0
  106. package/locales/fr-FR.json +928 -0
  107. package/locales/he-IL.json +990 -0
  108. package/locales/hu-HU.json +990 -0
  109. package/locales/it-IT.json +978 -0
  110. package/locales/ja-JP.json +998 -0
  111. package/locales/ko-KR.json +990 -0
  112. package/locales/nl-NL.json +954 -0
  113. package/locales/no-NO.json +978 -0
  114. package/locales/pl-PL.json +982 -0
  115. package/locales/pseudo.json +36 -0
  116. package/locales/pt-BR.json +982 -0
  117. package/locales/pt-PT.json +982 -0
  118. package/locales/ro-RO.json +982 -0
  119. package/locales/ru-RU.json +990 -0
  120. package/locales/sr-SP.json +990 -0
  121. package/locales/sv-SE.json +986 -0
  122. package/locales/tr-TR.json +990 -0
  123. package/locales/uk-UA.json +990 -0
  124. package/locales/vi-VN.json +986 -0
  125. package/locales/zh-CN.json +998 -0
  126. package/locales/zh-TW.json +998 -0
  127. package/package.json +9 -8
  128. package/src/Banner/Banner.doc.mjs +3 -3
  129. package/src/Banner/Banner.test.tsx +40 -0
  130. package/src/Banner/Banner.tsx +27 -2
  131. package/src/BottomSheet/BottomSheet.doc.mjs +266 -0
  132. package/src/BottomSheet/BottomSheet.test.tsx +2114 -0
  133. package/src/BottomSheet/BottomSheet.tsx +545 -0
  134. package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
  135. package/src/BottomSheet/BottomSheetPanel.tsx +547 -0
  136. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
  137. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
  138. package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
  139. package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
  140. package/src/BottomSheet/index.ts +15 -0
  141. package/src/BottomSheet/snapOffsets.test.ts +187 -0
  142. package/src/BottomSheet/snapOffsets.ts +159 -0
  143. package/src/BottomSheet/useMobileKeyboard.ts +499 -0
  144. package/src/BottomSheet/useSheetGestures.test.ts +1059 -0
  145. package/src/BottomSheet/useSheetGestures.ts +1459 -0
  146. package/src/Chat/ChatComposerInput.tsx +3 -4
  147. package/src/ComplexSelector/ComplexSelector.tsx +12 -5
  148. package/src/ContextMenu/ContextMenu.tsx +4 -2
  149. package/src/DateInput/DateInput.test.tsx +21 -0
  150. package/src/DateInput/DateInput.tsx +9 -1
  151. package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
  152. package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
  153. package/src/DateTimeInput/DateTimeInput.tsx +39 -2
  154. package/src/Dialog/Dialog.tsx +2 -1
  155. package/src/Field/Field.doc.mjs +1 -0
  156. package/src/Field/InputClearButton.test.tsx +25 -0
  157. package/src/Field/InputClearButton.tsx +4 -1
  158. package/src/FileInput/FileInput.test.tsx +53 -9
  159. package/src/Lightbox/Lightbox.test.tsx +49 -28
  160. package/src/Lightbox/Lightbox.tsx +7 -3
  161. package/src/MobileNav/MobileNav.tsx +128 -37
  162. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
  163. package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
  164. package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
  165. package/src/MultiSelector/MultiSelector.test.tsx +195 -51
  166. package/src/MultiSelector/MultiSelector.tsx +22 -8
  167. package/src/NumberInput/NumberInput.test.tsx +18 -0
  168. package/src/NumberInput/NumberInput.tsx +9 -1
  169. package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
  170. package/src/Selector/Selector.test.tsx +85 -26
  171. package/src/Selector/Selector.tsx +13 -4
  172. package/src/StatusDot/StatusDot.doc.mjs +13 -0
  173. package/src/StatusDot/StatusDot.test.tsx +115 -8
  174. package/src/StatusDot/StatusDot.tsx +74 -9
  175. package/src/Switch/Switch.test.tsx +16 -0
  176. package/src/Switch/Switch.tsx +7 -0
  177. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
  178. package/src/TimeInput/TimeInput.test.tsx +23 -0
  179. package/src/TimeInput/TimeInput.tsx +8 -0
  180. package/src/Tokenizer/Tokenizer.test.tsx +50 -32
  181. package/src/Tokenizer/Tokenizer.tsx +5 -5
  182. package/src/Tooltip/useTooltip.tsx +4 -1
  183. package/src/Typeahead/BaseTypeahead.tsx +10 -3
  184. package/src/Typeahead/Typeahead.test.tsx +116 -7
  185. package/src/Typeahead/Typeahead.tsx +10 -1
  186. package/src/__tests__/stubMatchMedia.test.ts +79 -0
  187. package/src/__tests__/stubMatchMedia.ts +95 -0
  188. package/src/hooks/index.ts +9 -5
  189. package/src/hooks/useFocusTrap.ts +1 -14
  190. package/src/hooks/useInputStatusIcon.test.tsx +19 -0
  191. package/src/hooks/useInputStatusIcon.tsx +7 -0
  192. package/src/index.ts +1 -0
  193. package/src/theme/defineTheme.ts +22 -4
  194. package/src/theme/expandColorScale.test.ts +138 -0
  195. package/src/theme/expandColorScale.ts +89 -44
  196. package/src/utils/ime.test.ts +32 -0
  197. package/src/utils/ime.ts +69 -0
  198. package/src/utils/index.ts +2 -0
  199. package/dist/astryx.umd.js +0 -147
  200. package/dist/astryx.umd.js.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.4.3",
3
+ "version": "0.4.4-canary.0f4d45f",
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",
@@ -27,8 +27,6 @@
27
27
  "type": "module",
28
28
  "main": "./dist/index.js",
29
29
  "types": "./dist/index.d.ts",
30
- "unpkg": "./dist/astryx.umd.js",
31
- "jsdelivr": "./dist/astryx.umd.js",
32
30
  "sideEffects": [
33
31
  "**/*.stylex.ts",
34
32
  "**/*.stylex.js",
@@ -49,7 +47,6 @@
49
47
  "types": "./src/astryx.css.d.ts",
50
48
  "default": "./dist/astryx.css"
51
49
  },
52
- "./astryx.umd.js": "./dist/astryx.umd.js",
53
50
  "./tailwind-theme.css": {
54
51
  "types": "./src/tailwind-theme.css.d.ts",
55
52
  "default": "./src/tailwind-theme.css"
@@ -122,6 +119,11 @@
122
119
  "types": "./dist/Blockquote/index.d.ts",
123
120
  "default": "./dist/Blockquote/index.js"
124
121
  },
122
+ "./BottomSheet": {
123
+ "source": "./src/BottomSheet/index.ts",
124
+ "types": "./dist/BottomSheet/index.d.ts",
125
+ "default": "./dist/BottomSheet/index.js"
126
+ },
125
127
  "./Breadcrumbs": {
126
128
  "source": "./src/Breadcrumbs/index.ts",
127
129
  "types": "./dist/Breadcrumbs/index.d.ts",
@@ -654,6 +656,7 @@
654
656
  "react-dom": ">=19.0.0"
655
657
  },
656
658
  "devDependencies": {
659
+ "@astryxdesign/cli": "0.4.4-canary.0f4d45f",
657
660
  "@babel/cli": "^8.0.4",
658
661
  "@babel/core": "^7.29.7",
659
662
  "@babel/preset-react": "^8.0.1",
@@ -662,19 +665,17 @@
662
665
  "@testing-library/dom": "^10.0.0",
663
666
  "@testing-library/jest-dom": "^6.6.0",
664
667
  "@testing-library/react": "^16.3.2",
665
- "rimraf": "^6.0.1",
666
- "@astryxdesign/cli": "0.4.3"
668
+ "rimraf": "^6.0.1"
667
669
  },
668
670
  "dependencies": {
669
671
  "intl-messageformat": "^11.2.9"
670
672
  },
671
673
  "scripts": {
672
674
  "postinstall": "node scripts/postinstall.mjs",
673
- "build": "rimraf dist && pnpm build:i18n && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && pnpm build:umd && node ../../scripts/check-no-dev-jsx.mjs && node ../../scripts/check-fully-specified.mjs",
675
+ "build": "rimraf dist && pnpm build:i18n && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && node ../../scripts/check-no-dev-jsx.mjs && node ../../scripts/check-fully-specified.mjs",
674
676
  "build:i18n": "node scripts/build-pseudo-locale.mjs",
675
677
  "build:esm": "babel src --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts\" --config-file ./babel.config.json",
676
678
  "build:css": "node ../../scripts/build-css.mjs",
677
- "build:umd": "node ../../scripts/build-umd.mjs",
678
679
  "dev": "babel src --watch --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css*\" --config-file ./babel.config.json",
679
680
  "test": "vitest run --root ../..",
680
681
  "test:watch": "vitest --root ../..",
@@ -70,7 +70,7 @@ export const docs = {
70
70
  name: 'endContent',
71
71
  type: 'ReactNode',
72
72
  description:
73
- 'Action content rendered in the header area, end-aligned. Typically a button or link.',
73
+ 'Action content rendered in the header area, end-aligned. Wraps to its own row below the text when the header is too narrow to hold both.',
74
74
  slotElements: [
75
75
  {__element: 'Icon', props: {icon: 'chevronDown', size: 'sm'}},
76
76
  {__element: 'Badge', props: {label: '3'}},
@@ -164,7 +164,7 @@ export const docsZh = {
164
164
  {name: 'icon', type: 'ReactNode', description: '覆盖默认的状态图标。'},
165
165
  {name: 'isDismissable', type: 'boolean', description: '横幅是否可被用户关闭。', default: 'false'},
166
166
  {name: 'onDismiss', type: '() => void', description: '点击关闭按钮时调用;无论是否提供此回调,横幅都会自动隐藏。'},
167
- {name: 'endContent', type: 'ReactNode', description: '渲染在头部区域末端对齐的操作内容,通常是按钮或链接。'},
167
+ {name: 'endContent', type: 'ReactNode', description: '渲染在头部区域末端对齐的操作内容,通常是按钮或链接。头部过窄时会整体换行到文本下方,自成一行。'},
168
168
  {name: 'container', type: "'card' | 'section'", description: '视觉变体:card 带圆角;section 无圆角全宽,适用于页面级场景。', default: "'card'"},
169
169
  {name: 'elevation', type: "'none' | 'low' | 'med' | 'high'", description: '静止阴影深度。用于悬浮于内容之上的浮动横幅;none 为默认内联横幅。', default: "'none'"},
170
170
  {name: 'children', type: 'ReactNode', description: '渲染在彩色头部下方卡片背景区域的内容。'},
@@ -238,7 +238,7 @@ export const docsDense = {
238
238
  icon: 'override default status icon',
239
239
  isDismissable: 'user can dismiss banner',
240
240
  onDismiss: 'dismiss callback; banner self-hides regardless',
241
- endContent: 'end-aligned action in header, typically button/link',
241
+ endContent: 'end-aligned action in header, typically button/link; wraps to its own row when the header is too narrow',
242
242
  container: 'card=border-radius; section=full-width no radius for page-level',
243
243
  elevation: 'resting shadow depth: none|low|med|high; raise for a floating banner',
244
244
  children: 'content in card-bg area below colored header',
@@ -523,4 +523,44 @@ describe('Banner', () => {
523
523
  expect(header.children[1].children).toHaveLength(1);
524
524
  });
525
525
  });
526
+
527
+ // jsdom does no flex layout, so these read the declarations that produce the
528
+ // wrap. The rendered result is verified in Chromium at 320/375/480/768.
529
+ describe('narrow-viewport wrapping', () => {
530
+ const renderBanner = (endContent?: React.ReactNode) => {
531
+ const {container} = render(
532
+ <Banner
533
+ status="warning"
534
+ title="A compute node is required"
535
+ endContent={endContent}
536
+ />,
537
+ );
538
+ const header = container.firstElementChild!.firstElementChild!;
539
+ return {
540
+ header,
541
+ textColumn: screen.getByText('A compute node is required')
542
+ .parentElement!,
543
+ };
544
+ };
545
+
546
+ it('lets the header wrap so the end area can take its own row', () => {
547
+ const {header} = renderBanner(<button type="button">Retry</button>);
548
+ expect(getComputedStyle(header).flexWrap).toBe('wrap');
549
+ });
550
+
551
+ it('gives the text column a wrap threshold when endContent is present', () => {
552
+ const {textColumn} = renderBanner(<button type="button">Retry</button>);
553
+ expect(getComputedStyle(textColumn).flexBasis).toBe('8rem');
554
+ });
555
+
556
+ it('leaves the text column free to shrink when there is no endContent', () => {
557
+ const {textColumn} = renderBanner();
558
+ expect(getComputedStyle(textColumn).flexBasis).not.toBe('8rem');
559
+ });
560
+
561
+ it('leaves it free for an endContent that renders nothing', () => {
562
+ const {textColumn} = renderBanner(false);
563
+ expect(getComputedStyle(textColumn).flexBasis).not.toBe('8rem');
564
+ });
565
+ });
526
566
  });
@@ -116,6 +116,9 @@ export interface BannerProps extends BaseProps<HTMLDivElement> {
116
116
  * Action button rendered in the header area (end-aligned).
117
117
  * Typically an Button with a secondary or ghost variant.
118
118
  *
119
+ * When the header is too narrow to hold the actions and a readable text
120
+ * column, the whole end area wraps to its own row below the text.
121
+ *
119
122
  * @example
120
123
  * ```
121
124
  * endContent={<Button label="Retry" variant="ghost" onClick={handleRetry} />}
@@ -205,7 +208,11 @@ const styles = stylex.create({
205
208
  header: {
206
209
  display: 'flex',
207
210
  alignItems: 'flex-start',
208
- gap: spacingVars['--spacing-2'],
211
+ flexWrap: 'wrap',
212
+ columnGap: spacingVars['--spacing-2'],
213
+ // The end area carries a -4px block margin (see endArea), so the wrapped
214
+ // row reads as one step of spacing rather than two.
215
+ rowGap: spacingVars['--spacing-3'],
209
216
  paddingBlock: spacingVars['--spacing-3'],
210
217
  paddingInline: spacingVars['--spacing-4'],
211
218
  },
@@ -231,6 +238,15 @@ const styles = stylex.create({
231
238
  flex: 1,
232
239
  minWidth: 0,
233
240
  },
241
+ // The wrap threshold, applied only when there is `endContent` to wrap. Flex
242
+ // breaks a line when the items no longer fit at their base size, so this
243
+ // basis is what moves the end area to its own row instead of letting it hold
244
+ // the header and squeeze the title down to one word per line. In rem so it
245
+ // tracks the user's font size. The dismiss and expand controls alone are
246
+ // narrow enough never to need it.
247
+ headerContentWithEndContent: {
248
+ flexBasis: '8rem',
249
+ },
234
250
  title: {
235
251
  margin: 0,
236
252
  fontFamily: 'inherit',
@@ -260,8 +276,13 @@ const styles = stylex.create({
260
276
  endArea: {
261
277
  display: 'flex',
262
278
  alignItems: 'center',
279
+ flexWrap: 'wrap',
280
+ justifyContent: 'flex-end',
263
281
  gap: spacingVars['--spacing-2'],
264
282
  flexShrink: 0,
283
+ // Bounded so a group of actions wider than the row wraps within itself
284
+ // rather than pushing the banner past the viewport (WCAG 1.4.10).
285
+ maxWidth: '100%',
265
286
  marginInlineStart: 'auto',
266
287
  marginBlock: `calc(-1 * (${spacingVars['--spacing-3']} - ${spacingVars['--spacing-2']}))`,
267
288
  },
@@ -537,7 +558,11 @@ export function Banner({
537
558
  />
538
559
  ) : null}
539
560
  </div>
540
- <div {...stylex.props(styles.headerContent)}>
561
+ <div
562
+ {...stylex.props(
563
+ styles.headerContent,
564
+ isRenderable(endContent) && styles.headerContentWithEndContent,
565
+ )}>
541
566
  <div {...stylex.props(styles.title)}>{title}</div>
542
567
  {isRenderable(description) && (
543
568
  <div {...stylex.props(styles.description)}>{description}</div>
@@ -0,0 +1,266 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
3
+
4
+ export const docs = {
5
+ name: 'BottomSheet',
6
+ displayName: 'Bottom Sheet',
7
+ group: 'BottomSheet',
8
+ category: 'Overlay',
9
+ keywords: [
10
+ 'bottom sheet',
11
+ 'sheet',
12
+ 'mobile',
13
+ 'touch',
14
+ 'drag',
15
+ 'swipe',
16
+ 'dismiss',
17
+ 'grab handle',
18
+ 'dialog',
19
+ 'overlay',
20
+ 'modal',
21
+ 'form',
22
+ 'mobile keyboard',
23
+ 'visual viewport',
24
+ ],
25
+ playground: {
26
+ overlay: true,
27
+ defaults: {
28
+ isOpen: false,
29
+ label: 'Filters',
30
+ height: 'hug',
31
+ children: {
32
+ __element: 'Section',
33
+ props: {padding: 4},
34
+ children: {
35
+ __element: 'VStack',
36
+ props: {gap: 2},
37
+ children: [
38
+ {
39
+ __element: 'Heading',
40
+ props: {level: 3},
41
+ children: 'Filters',
42
+ },
43
+ {
44
+ __element: 'Text',
45
+ props: {type: 'body'},
46
+ children: 'Adjust the properties below, then open the preview.',
47
+ },
48
+ ],
49
+ },
50
+ },
51
+ },
52
+ },
53
+ theming: {
54
+ targets: [{className: 'astryx-bottom-sheet', visualProps: []}],
55
+ },
56
+ description:
57
+ "A mobile touch sheet that rises from the bottom edge, with animated entrance and exit, a grab handle, drag-to-resize snap points, and purpose-controlled dismissal. A standalone sheet owns a native <dialog>; inside BottomSheetSwitcher it renders a panel in the switcher's shared dialog. In both modes, ref and shared DOM props target the visual panel <div>.",
58
+ props: [
59
+ {
60
+ name: 'isOpen',
61
+ type: 'boolean',
62
+ description:
63
+ 'Whether a standalone sheet is open. Fully controlled; pair with onOpenChange. Omit inside BottomSheetSwitcher.',
64
+ },
65
+ {
66
+ name: 'onOpenChange',
67
+ type: '(isOpen: boolean) => void',
68
+ description:
69
+ 'For a standalone sheet, called when it requests an open-state change. Automatic calls follow purpose: info dismisses on Escape, scrim click, or swipe; form dismisses on Escape only; required never dismisses implicitly. Omit inside BottomSheetSwitcher.',
70
+ },
71
+ {
72
+ name: 'purpose',
73
+ type: "'required' | 'form' | 'info'",
74
+ description:
75
+ "Controls implicit dismissal behavior, matching Dialog. info allows Escape, scrim click, and swipe-to-dismiss. form protects entered data by blocking scrim click and swipe while allowing Escape. required blocks every implicit dismissal path and uses role='alertdialog'. Explicit controls may still update the controlled state. Works for standalone and BottomSheetSwitcher-managed sheets.",
76
+ default: "'info'",
77
+ },
78
+ {
79
+ name: 'sheetId',
80
+ type: 'string',
81
+ description:
82
+ 'Unique ID for this sheet inside BottomSheetSwitcher. The switcher opens it when activeSheet matches. Omit isOpen and onOpenChange when sheetId is used.',
83
+ },
84
+ {
85
+ name: 'label',
86
+ type: 'string',
87
+ description:
88
+ 'Accessible label for the sheet. Required; the sheet has no built-in heading to derive a name from.',
89
+ required: true,
90
+ },
91
+ {
92
+ name: 'children',
93
+ type: 'ReactNode',
94
+ description:
95
+ "Sheet content, rendered below the grab handle in a scrollable area. If it includes a text-entry control that can bring up the mobile keyboard, use height='tall' and keep the sheet fully expanded while editing.",
96
+ required: true,
97
+ },
98
+ {
99
+ name: 'height',
100
+ type: "'hug' | 'capped' | 'tall' | number | string",
101
+ description:
102
+ "How tall the sheet is. Named budgets: 'hug' fits its content up to 92% of the viewport, 'capped' is a scrolling mid-height panel (~62%), and 'tall' is a pinned near-full panel (~92%) for content that streams in. Or pass a number (px) / CSS length for a custom budget. The user can drag between snap points regardless. On shorter viewports the sheet fills the available height. Only a fully expanded 'tall' sheet provides mobile-keyboard accommodation: it stays put and scrolls each focused control above the keyboard. Hug, Capped, numeric and CSS-length heights never do, and a Tall sheet stops doing it the moment the user drags it to a shorter detent — resuming when they drag it back. Outside that state the sheet neither moves nor adds keyboard scroll space, and the browser's own focus reveal is left in place; on iOS that reveal can shift the whole page.",
103
+ default: "'capped'",
104
+ },
105
+ {
106
+ name: 'hasScrim',
107
+ type: 'boolean',
108
+ description:
109
+ "For a standalone BottomSheet, whether to render a scrim, the semi-transparent overlay that covers and blocks the background. true (default) uses showModal(): top layer, focus trap, ::backdrop scrim, body scroll lock, and tap-scrim-to-dismiss when purpose='info', with the background inert. false uses show() with no scrim, leaving the page behind interactive and scrollable. For a multi-step flow, configure hasScrim on BottomSheetSwitcher instead; it owns one shared dialog across every child.",
110
+ default: 'true',
111
+ },
112
+ ],
113
+ usage: {
114
+ description:
115
+ 'A mobile touch surface for filters, actions, forms, and detail views that should rise from the bottom of the viewport; use BottomSheetSwitcher for multi-step flows.',
116
+ bestPractices: [
117
+ {
118
+ guidance: true,
119
+ description:
120
+ 'Use for mobile-first surfaces (filters, share sheets, quick actions) where the content should rise from the bottom edge.',
121
+ },
122
+ {
123
+ guidance: true,
124
+ description:
125
+ "Pick the starting height that fits the content: 'hug' for short bounded content, 'capped' for lists, and 'tall' for forms or streaming/resizing content.",
126
+ },
127
+ {
128
+ guidance: true,
129
+ description:
130
+ "Use purpose='form' to protect entered data from scrim clicks and swipes while keeping Escape available; reserve purpose='required' for flows that must end through an explicit action.",
131
+ },
132
+ {
133
+ guidance: false,
134
+ description:
135
+ "Don't make the sheet content overly long. Consider breaking it into steps and using Bottom Sheet Switcher.",
136
+ },
137
+ ],
138
+ },
139
+ examples: [
140
+ {
141
+ label: 'Basic',
142
+ code: `const [isOpen, setIsOpen] = useState(false);
143
+ <BottomSheet
144
+ isOpen={isOpen}
145
+ onOpenChange={setIsOpen}
146
+ label="Filters">
147
+ <FilterControls />
148
+ </BottomSheet>`,
149
+ },
150
+ {
151
+ label: 'Tall sheet (a list)',
152
+ code: `const [isOpen, setIsOpen] = useState(false);
153
+ <BottomSheet
154
+ isOpen={isOpen}
155
+ onOpenChange={setIsOpen}
156
+ label="Nearby places"
157
+ height="tall">
158
+ <PlaceList />
159
+ </BottomSheet>`,
160
+ },
161
+ {
162
+ label: 'Hug height (fits content)',
163
+ code: `const [isOpen, setIsOpen] = useState(false);
164
+ <BottomSheet
165
+ isOpen={isOpen}
166
+ onOpenChange={setIsOpen}
167
+ label="Share"
168
+ height="hug">
169
+ <ShareActions />
170
+ </BottomSheet>`,
171
+ },
172
+ {
173
+ label: 'Long scrolling content',
174
+ code: `const [isOpen, setIsOpen] = useState(false);
175
+ <BottomSheet
176
+ isOpen={isOpen}
177
+ onOpenChange={setIsOpen}
178
+ label="Release notes"
179
+ height="hug">
180
+ <ReleaseNotes />
181
+ </BottomSheet>`,
182
+ },
183
+ {
184
+ label: 'Multi-step flow (one sheet at a time)',
185
+ code: `const [activeSheet, setActiveSheet] = useState(null);
186
+ <>
187
+ <Button label="Start" onClick={() => setActiveSheet('details')} />
188
+ <BottomSheetSwitcher
189
+ activeSheet={activeSheet}
190
+ onActiveSheetChange={setActiveSheet}>
191
+ <BottomSheet sheetId="details" label="Details">
192
+ <Button label="Continue" onClick={() => setActiveSheet('confirm')} />
193
+ </BottomSheet>
194
+ <BottomSheet sheetId="confirm" label="Confirm">
195
+ <Button label="Back" onClick={() => setActiveSheet('details')} />
196
+ </BottomSheet>
197
+ </BottomSheetSwitcher>
198
+ </>`,
199
+ },
200
+ {
201
+ label: 'Mobile keyboard',
202
+ code: `const [isOpen, setIsOpen] = useState(false);
203
+ <BottomSheet
204
+ isOpen={isOpen}
205
+ onOpenChange={setIsOpen}
206
+ label="Add a comment"
207
+ height="tall">
208
+ <LongCommentForm />
209
+ </BottomSheet>`,
210
+ },
211
+ {
212
+ label: 'No scrim',
213
+ code: `const [isOpen, setIsOpen] = useState(true);
214
+ <BottomSheet
215
+ isOpen={isOpen}
216
+ onOpenChange={setIsOpen}
217
+ label="Nearby places"
218
+ hasScrim={false}>
219
+ <PlaceList />
220
+ </BottomSheet>`,
221
+ },
222
+ {
223
+ label: 'Protect form input',
224
+ code: `const [isOpen, setIsOpen] = useState(false);
225
+ <BottomSheet
226
+ isOpen={isOpen}
227
+ onOpenChange={setIsOpen}
228
+ purpose="form"
229
+ label="Edit profile">
230
+ <ProfileForm onSave={() => setIsOpen(false)} />
231
+ </BottomSheet>`,
232
+ },
233
+ ],
234
+ };
235
+
236
+ /** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
237
+ export const docsDense = {
238
+ description:
239
+ 'mobile touch sheet rising from the bottom edge (native <dialog>): grab handle, transform-based drag-to-resize snap points, scrolling area resizes to the snapped visible height on release (the peek detent keeps the full height and slides instead), Dialog-aligned dismissal purpose (info/form/required), purpose-gated swipe-to-dismiss, fully-expanded Tall visual-viewport mobile-keyboard handling, named height scale, modal (default) or non-modal (hasScrim={false}) presentation',
240
+ usage: {
241
+ description:
242
+ 'Mobile touch surface for filters, actions, forms, and detail views that should rise from the bottom of the viewport; use BottomSheetSwitcher for multi-step flows.',
243
+ bestPractices: [
244
+ {
245
+ guidance: true,
246
+ description:
247
+ 'Use for mobile-first surfaces (filters, share sheets, quick actions) where the content should rise from the bottom edge.',
248
+ },
249
+ {
250
+ guidance: true,
251
+ description:
252
+ "Pick the starting height that fits the content: 'hug' for short bounded content, 'capped' for lists, and 'tall' for forms or streaming/resizing content.",
253
+ },
254
+ {
255
+ guidance: true,
256
+ description:
257
+ "Use purpose='form' to protect entered data from scrim clicks and swipes while keeping Escape available; reserve purpose='required' for flows that must end through an explicit action.",
258
+ },
259
+ {
260
+ guidance: false,
261
+ description:
262
+ "Don't make the sheet content overly long. Consider breaking it into steps and using Bottom Sheet Switcher.",
263
+ },
264
+ ],
265
+ },
266
+ };