@astryxdesign/core 0.4.3 → 0.4.4-canary.2022d94

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 (156) 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/Lightbox/Lightbox.d.ts.map +1 -1
  43. package/dist/Lightbox/Lightbox.js +9 -3
  44. package/dist/MobileNav/MobileNav.d.ts +12 -0
  45. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  46. package/dist/MobileNav/MobileNav.js +102 -30
  47. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  48. package/dist/MultiSelector/MultiSelector.js +21 -7
  49. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  50. package/dist/NumberInput/NumberInput.js +9 -1
  51. package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
  52. package/dist/Selector/Selector.d.ts.map +1 -1
  53. package/dist/Selector/Selector.js +14 -3
  54. package/dist/Switch/Switch.d.ts.map +1 -1
  55. package/dist/Switch/Switch.js +5 -1
  56. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  57. package/dist/TimeInput/TimeInput.js +8 -1
  58. package/dist/Tokenizer/Tokenizer.js +11 -5
  59. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  60. package/dist/Tooltip/useTooltip.js +4 -1
  61. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  62. package/dist/Typeahead/BaseTypeahead.js +5 -3
  63. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  64. package/dist/Typeahead/Typeahead.js +10 -1
  65. package/dist/astryx.css +21 -0
  66. package/dist/hooks/index.d.ts +8 -1
  67. package/dist/hooks/index.d.ts.map +1 -1
  68. package/dist/hooks/index.js +8 -1
  69. package/dist/hooks/useFocusTrap.d.ts +0 -11
  70. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  71. package/dist/hooks/useFocusTrap.js +1 -11
  72. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  73. package/dist/hooks/useInputStatusIcon.js +1 -0
  74. package/dist/index.d.ts +1 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -0
  77. package/dist/theme/defineTheme.d.ts +22 -4
  78. package/dist/theme/defineTheme.d.ts.map +1 -1
  79. package/dist/theme/expandColorScale.d.ts +17 -2
  80. package/dist/theme/expandColorScale.d.ts.map +1 -1
  81. package/dist/theme/expandColorScale.js +63 -33
  82. package/dist/utils/ime.d.ts +5 -0
  83. package/dist/utils/ime.d.ts.map +1 -0
  84. package/dist/utils/ime.js +63 -0
  85. package/dist/utils/index.d.ts +1 -0
  86. package/dist/utils/index.d.ts.map +1 -1
  87. package/dist/utils/index.js +2 -1
  88. package/locales/en.json +48 -0
  89. package/locales/pseudo.json +36 -0
  90. package/package.json +9 -8
  91. package/src/Banner/Banner.doc.mjs +3 -3
  92. package/src/Banner/Banner.test.tsx +40 -0
  93. package/src/Banner/Banner.tsx +27 -2
  94. package/src/BottomSheet/BottomSheet.doc.mjs +266 -0
  95. package/src/BottomSheet/BottomSheet.test.tsx +2114 -0
  96. package/src/BottomSheet/BottomSheet.tsx +545 -0
  97. package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
  98. package/src/BottomSheet/BottomSheetPanel.tsx +547 -0
  99. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
  100. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
  101. package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
  102. package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
  103. package/src/BottomSheet/index.ts +15 -0
  104. package/src/BottomSheet/snapOffsets.test.ts +187 -0
  105. package/src/BottomSheet/snapOffsets.ts +159 -0
  106. package/src/BottomSheet/useMobileKeyboard.ts +499 -0
  107. package/src/BottomSheet/useSheetGestures.test.ts +1059 -0
  108. package/src/BottomSheet/useSheetGestures.ts +1459 -0
  109. package/src/Chat/ChatComposerInput.tsx +3 -4
  110. package/src/ComplexSelector/ComplexSelector.tsx +12 -5
  111. package/src/ContextMenu/ContextMenu.tsx +4 -2
  112. package/src/DateInput/DateInput.test.tsx +21 -0
  113. package/src/DateInput/DateInput.tsx +9 -1
  114. package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
  115. package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
  116. package/src/DateTimeInput/DateTimeInput.tsx +39 -2
  117. package/src/Dialog/Dialog.tsx +2 -1
  118. package/src/FileInput/FileInput.test.tsx +53 -9
  119. package/src/Lightbox/Lightbox.test.tsx +49 -28
  120. package/src/Lightbox/Lightbox.tsx +7 -3
  121. package/src/MobileNav/MobileNav.tsx +128 -37
  122. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
  123. package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
  124. package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
  125. package/src/MultiSelector/MultiSelector.test.tsx +195 -51
  126. package/src/MultiSelector/MultiSelector.tsx +22 -8
  127. package/src/NumberInput/NumberInput.test.tsx +18 -0
  128. package/src/NumberInput/NumberInput.tsx +9 -1
  129. package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
  130. package/src/Selector/Selector.test.tsx +85 -26
  131. package/src/Selector/Selector.tsx +13 -4
  132. package/src/Switch/Switch.test.tsx +16 -0
  133. package/src/Switch/Switch.tsx +7 -0
  134. package/src/TimeInput/TimeInput.test.tsx +23 -0
  135. package/src/TimeInput/TimeInput.tsx +8 -0
  136. package/src/Tokenizer/Tokenizer.test.tsx +50 -32
  137. package/src/Tokenizer/Tokenizer.tsx +5 -5
  138. package/src/Tooltip/useTooltip.tsx +4 -1
  139. package/src/Typeahead/BaseTypeahead.tsx +10 -3
  140. package/src/Typeahead/Typeahead.test.tsx +116 -7
  141. package/src/Typeahead/Typeahead.tsx +10 -1
  142. package/src/__tests__/stubMatchMedia.test.ts +79 -0
  143. package/src/__tests__/stubMatchMedia.ts +95 -0
  144. package/src/hooks/index.ts +9 -5
  145. package/src/hooks/useFocusTrap.ts +1 -14
  146. package/src/hooks/useInputStatusIcon.test.tsx +19 -0
  147. package/src/hooks/useInputStatusIcon.tsx +7 -0
  148. package/src/index.ts +1 -0
  149. package/src/theme/defineTheme.ts +22 -4
  150. package/src/theme/expandColorScale.test.ts +138 -0
  151. package/src/theme/expandColorScale.ts +89 -44
  152. package/src/utils/ime.test.ts +32 -0
  153. package/src/utils/ime.ts +69 -0
  154. package/src/utils/index.ts +2 -0
  155. package/dist/astryx.umd.js +0 -147
  156. package/dist/astryx.umd.js.map +0 -7
@@ -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
+ };