ajo-ui 0.1.0

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 (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +126 -0
  3. package/dist/accordion.js +130 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/calendar.js +2 -0
  6. package/dist/carousel.js +239 -0
  7. package/dist/chart.js +790 -0
  8. package/dist/checkbox-group.js +70 -0
  9. package/dist/checkbox.js +77 -0
  10. package/dist/chunks/bar-CVafh6C1.js +99 -0
  11. package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
  12. package/dist/chunks/collection-DtRB63U4.js +111 -0
  13. package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
  14. package/dist/chunks/menu-B45IyHHC.js +704 -0
  15. package/dist/chunks/native-BJdhd9XJ.js +20 -0
  16. package/dist/chunks/popup-C8Bb3l_g.js +459 -0
  17. package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
  18. package/dist/chunks/position-D6_i_SRn.js +434 -0
  19. package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
  20. package/dist/collapsible.js +106 -0
  21. package/dist/command.js +263 -0
  22. package/dist/context-menu.js +112 -0
  23. package/dist/data-table.js +5 -0
  24. package/dist/dialog.js +207 -0
  25. package/dist/direction.js +22 -0
  26. package/dist/drawer.js +139 -0
  27. package/dist/field.js +26 -0
  28. package/dist/index.js +38 -0
  29. package/dist/input-date.js +994 -0
  30. package/dist/input-group.js +52 -0
  31. package/dist/input-otp.js +179 -0
  32. package/dist/menu.js +2 -0
  33. package/dist/menubar.js +236 -0
  34. package/dist/message-scroller.js +446 -0
  35. package/dist/navigation-menu.js +330 -0
  36. package/dist/popover.js +307 -0
  37. package/dist/progress.js +39 -0
  38. package/dist/radio-group.js +107 -0
  39. package/dist/resizable.js +172 -0
  40. package/dist/select.js +961 -0
  41. package/dist/sidebar.js +343 -0
  42. package/dist/slider.js +259 -0
  43. package/dist/switch.js +53 -0
  44. package/dist/tabs.js +182 -0
  45. package/dist/toast.js +492 -0
  46. package/dist/toggle-group.js +111 -0
  47. package/dist/toggle.js +52 -0
  48. package/dist/toolbar.js +127 -0
  49. package/dist/tooltip.js +196 -0
  50. package/dist/utils.js +104 -0
  51. package/dist/virtual-list.js +2 -0
  52. package/package.json +250 -0
  53. package/src/accordion.tsx +261 -0
  54. package/src/availability.ts +261 -0
  55. package/src/avatar.tsx +99 -0
  56. package/src/bar.ts +156 -0
  57. package/src/calendar.tsx +1441 -0
  58. package/src/carousel.tsx +424 -0
  59. package/src/chart.tsx +1194 -0
  60. package/src/checkbox-group.tsx +132 -0
  61. package/src/checkbox.tsx +130 -0
  62. package/src/collapsible.tsx +188 -0
  63. package/src/collection.ts +154 -0
  64. package/src/command.tsx +511 -0
  65. package/src/context-menu.tsx +233 -0
  66. package/src/data-table-contract.ts +143 -0
  67. package/src/data-table-model.ts +760 -0
  68. package/src/data-table.tsx +475 -0
  69. package/src/dialog.tsx +393 -0
  70. package/src/direction.tsx +45 -0
  71. package/src/drawer.tsx +251 -0
  72. package/src/field.tsx +61 -0
  73. package/src/index.ts +37 -0
  74. package/src/input-date.tsx +1539 -0
  75. package/src/input-group.tsx +142 -0
  76. package/src/input-otp.tsx +324 -0
  77. package/src/menu-cluster.ts +124 -0
  78. package/src/menu.tsx +1095 -0
  79. package/src/menubar.tsx +459 -0
  80. package/src/message-scroller.tsx +732 -0
  81. package/src/native.ts +26 -0
  82. package/src/navigation-menu.tsx +578 -0
  83. package/src/popover.tsx +519 -0
  84. package/src/popup-surface.tsx +31 -0
  85. package/src/popup.ts +569 -0
  86. package/src/position.ts +523 -0
  87. package/src/progress.tsx +70 -0
  88. package/src/radio-group.tsx +186 -0
  89. package/src/resizable.tsx +310 -0
  90. package/src/segments.ts +922 -0
  91. package/src/select.tsx +1501 -0
  92. package/src/sidebar.tsx +683 -0
  93. package/src/slider.tsx +424 -0
  94. package/src/switch.tsx +104 -0
  95. package/src/tabs.tsx +314 -0
  96. package/src/toast.tsx +923 -0
  97. package/src/toggle-group.tsx +249 -0
  98. package/src/toggle.tsx +91 -0
  99. package/src/toolbar.tsx +212 -0
  100. package/src/tooltip.tsx +359 -0
  101. package/src/utils.ts +204 -0
  102. package/src/virtual-list.tsx +205 -0
  103. package/src/virtual.ts +385 -0
package/src/dialog.tsx ADDED
@@ -0,0 +1,393 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import { callHandler, callRef, controlled, id, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+ import { triggerAttrs } from './utils'
5
+ import type { FixedArgs, OmitArg } from './utils'
6
+
7
+ /** Arguments for the Dialog state provider and its wrapper host. */
8
+ export type DialogArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'onchange'> & {
9
+ /** Controlled open state. */
10
+ open?: boolean
11
+ /** Initial open state for uncontrolled usage. */
12
+ defaultOpen?: boolean
13
+ /** Use native modal behavior. Set false for modeless dialogs. */
14
+ modal?: boolean
15
+ /** Called whenever the dialog opens or closes. */
16
+ onOpenChange?: (open: boolean, event?: Event) => void
17
+ /** Additional CSS classes for the root wrapper. */
18
+ class?: string
19
+ }> & FixedArgs<'onchange'>
20
+
21
+ /** Arguments for the button that opens its nearest Dialog. */
22
+ export type DialogTriggerArgs = WithChildren<IntrinsicElements['button'] & {
23
+ /** Additional CSS classes. */
24
+ class?: string
25
+ }>
26
+
27
+ /** Arguments for a button that closes its nearest Dialog. */
28
+ export type DialogCloseArgs = WithChildren<IntrinsicElements['button'] & {
29
+ /** Additional CSS classes. */
30
+ class?: string
31
+ }>
32
+
33
+ /** Arguments for the native dialog panel and its dismissal hooks. */
34
+ export type DialogContentArgs = WithChildren<OmitArg<IntrinsicElements['dialog'], 'open'> & {
35
+ /** Called when Escape requests dialog close. Prevent default to keep it open. */
36
+ onEscapeKeyDown?: (event: KeyboardEvent) => void
37
+ /** Called when the native backdrop is clicked. Prevent default to keep it open. */
38
+ onPointerDownOutside?: (event: MouseEvent) => void
39
+ /** Additional CSS classes for the dialog panel. */
40
+ class?: string
41
+ }> & FixedArgs<'open'>
42
+
43
+ /** Shared arguments for structural sections inside a dialog panel. */
44
+ export type DialogSectionArgs = WithChildren<IntrinsicElements['div'] & {
45
+ /** Additional CSS classes. */
46
+ class?: string
47
+ }>
48
+
49
+ /** Arguments for the title and description area of a dialog. */
50
+ export type DialogHeaderArgs = DialogSectionArgs
51
+
52
+ /** Arguments for the action area at the end of a dialog. */
53
+ export type DialogFooterArgs = DialogSectionArgs
54
+
55
+ /** Arguments for the heading that labels DialogContent. */
56
+ export type DialogTitleArgs = WithChildren<IntrinsicElements['h2'] & {
57
+ /** Additional CSS classes. */
58
+ class?: string
59
+ }>
60
+
61
+ /** Arguments for the text that describes DialogContent. */
62
+ export type DialogDescriptionArgs = WithChildren<IntrinsicElements['p'] & {
63
+ /** Additional CSS classes. */
64
+ class?: string
65
+ }>
66
+
67
+ type DialogContextValue = {
68
+ close: (event?: Event) => void
69
+ contentId: string
70
+ descriptionId: string
71
+ modal: boolean
72
+ open: boolean
73
+ setContent: (element: HTMLDialogElement | null) => void
74
+ setOpen: (open: boolean, event?: Event) => void
75
+ setTrigger: (element: HTMLButtonElement | null) => void
76
+ titleId: string
77
+ triggerId: string
78
+ }
79
+
80
+ const DialogContext = context<DialogContextValue | null>(null)
81
+
82
+ const dialog = () => {
83
+ const value = DialogContext()
84
+ if (!value) throw new Error('Dialog component must be used within Dialog.')
85
+ return value
86
+ }
87
+
88
+ const outside = (element: HTMLDialogElement, event: MouseEvent) => {
89
+ const rect = element.getBoundingClientRect()
90
+ return event.clientX < rect.left ||
91
+ event.clientX > rect.right ||
92
+ event.clientY < rect.top ||
93
+ event.clientY > rect.bottom
94
+ }
95
+
96
+ const DialogRoot: Stateful<DialogArgs> = function* ({ defaultOpen, open }) {
97
+ const dialogId = id('dialog')
98
+ const watched = new WeakSet<HTMLDialogElement>()
99
+ let content: HTMLDialogElement | null = null
100
+ let modal = true
101
+ let onOpenChange: DialogArgs['onOpenChange']
102
+ let syncing = false
103
+ let trigger: HTMLButtonElement | null = null
104
+ const state = controlled<boolean>(this, {
105
+ fallback: Boolean(open ?? defaultOpen),
106
+ onChange: (next, event) => onOpenChange?.(next, event),
107
+ })
108
+ let current = state.value
109
+
110
+ const focusTrigger = () => queueMicrotask(() => trigger?.focus())
111
+
112
+ const sync = () => queueMicrotask(() => {
113
+ if (!content) return
114
+ syncing = true
115
+
116
+ try {
117
+ if (current && !content.open) {
118
+ if (modal) content.showModal()
119
+ else content.show()
120
+ } else if (!current && content.open) {
121
+ content.close()
122
+ }
123
+ } finally {
124
+ queueMicrotask(() => syncing = false)
125
+ }
126
+ })
127
+
128
+ const setOpen = (next: boolean, event?: Event) => {
129
+ if (next === current) return
130
+ state.set(next, event)
131
+ current = state.value
132
+ sync()
133
+ if (!next) focusTrigger()
134
+ }
135
+
136
+ const setContent = (element: HTMLDialogElement | null) => {
137
+ content = element
138
+ if (!element || watched.has(element)) return
139
+
140
+ watched.add(element)
141
+ element.addEventListener('close', event => {
142
+ if (syncing || current === false) return
143
+ state.accept(false, event)
144
+ current = state.value
145
+ focusTrigger()
146
+ }, { signal: this.signal })
147
+ }
148
+
149
+ for (const args of this) {
150
+ modal = args.modal !== false
151
+ onOpenChange = args.onOpenChange
152
+ current = state.sync(args.open == null ? undefined : Boolean(args.open))
153
+
154
+ DialogContext({
155
+ close: event => setOpen(false, event),
156
+ contentId: `${dialogId}-content`,
157
+ descriptionId: `${dialogId}-description`,
158
+ modal,
159
+ open: current,
160
+ setContent,
161
+ setOpen,
162
+ setTrigger: element => trigger = element,
163
+ titleId: `${dialogId}-title`,
164
+ triggerId: `${dialogId}-trigger`,
165
+ })
166
+
167
+ sync()
168
+ yield <>{args.children}</>
169
+ }
170
+ }
171
+
172
+
173
+ /** Unstyled root provider for a native dialog. */
174
+ const Dialog: Stateless<DialogArgs> = ({
175
+ children,
176
+ class: classes,
177
+ 'data-slot': slot = 'dialog',
178
+ defaultOpen,
179
+ modal,
180
+ onOpenChange,
181
+ open,
182
+ ...attrs
183
+ }) => (
184
+ <DialogRoot
185
+ {...rootAttrs(attrs)}
186
+ defaultOpen={defaultOpen}
187
+ modal={modal}
188
+ onOpenChange={onOpenChange}
189
+ open={open}
190
+ attr:class={classes}
191
+ attr:data-slot={slot}
192
+ >
193
+ {children}
194
+ </DialogRoot>
195
+ )
196
+
197
+ /** Unstyled button that opens the nearest Dialog. */
198
+ const DialogTrigger: Stateless<DialogTriggerArgs> = ({
199
+ children,
200
+ 'data-slot': slot = 'dialog-trigger',
201
+ disabled,
202
+ id,
203
+ ref,
204
+ type = 'button',
205
+ 'set:onclick': onClick,
206
+ ...attrs
207
+ }) => {
208
+ const ctx = dialog()
209
+
210
+ return (
211
+ <button
212
+ {...attrs}
213
+ {...triggerAttrs({
214
+ controls: ctx.contentId,
215
+ expanded: ctx.open,
216
+ id,
217
+ open: ctx.open,
218
+ ref,
219
+ setTrigger: ctx.setTrigger,
220
+ triggerId: ctx.triggerId,
221
+ })}
222
+ data-slot={slot}
223
+ disabled={disabled}
224
+ type={type}
225
+ set:onclick={(event: Event) => {
226
+ callHandler(onClick, event)
227
+ if (event.defaultPrevented || disabled) return
228
+ ctx.setOpen(true, event)
229
+ }}
230
+ >
231
+ {children}
232
+ </button>
233
+ )
234
+ }
235
+
236
+ /** Unstyled button that closes the nearest Dialog. */
237
+ const DialogClose: Stateless<DialogCloseArgs> = ({
238
+ children,
239
+ 'data-slot': slot = 'dialog-close',
240
+ disabled,
241
+ type = 'button',
242
+ 'set:onclick': onClick,
243
+ ...attrs
244
+ }) => {
245
+ const ctx = dialog()
246
+
247
+ return (
248
+ <button
249
+ {...attrs}
250
+ data-slot={slot}
251
+ disabled={disabled}
252
+ type={type}
253
+ set:onclick={(event: Event) => {
254
+ callHandler(onClick, event)
255
+ if (event.defaultPrevented || disabled) return
256
+ ctx.close(event)
257
+ }}
258
+ >
259
+ {children}
260
+ </button>
261
+ )
262
+ }
263
+
264
+ /** Unstyled native dialog panel. */
265
+ const DialogContent: Stateless<DialogContentArgs> = ({
266
+ children,
267
+ class: classes,
268
+ 'data-slot': slot = 'dialog-content',
269
+ onEscapeKeyDown,
270
+ onPointerDownOutside,
271
+ ref,
272
+ 'aria-describedby': describedBy,
273
+ 'aria-labelledby': labelledBy,
274
+ 'set:oncancel': onCancel,
275
+ 'set:onclick': onClick,
276
+ 'set:onkeydown': onKeydown,
277
+ ...attrs
278
+ }) => {
279
+ const ctx = dialog()
280
+ const reference = (element: HTMLDialogElement | null) => {
281
+ ctx.setContent(element)
282
+ callRef(ref, element)
283
+ }
284
+
285
+ return (
286
+ <dialog
287
+ {...attrs}
288
+ aria-describedby={describedBy ?? ctx.descriptionId}
289
+ aria-labelledby={labelledBy ?? ctx.titleId}
290
+ aria-modal={ctx.modal ? 'true' : undefined}
291
+ class={classes}
292
+ data-slot={slot}
293
+ data-state={ctx.open ? 'open' : 'closed'}
294
+ id={ctx.contentId}
295
+ ref={reference}
296
+ set:oncancel={(event: Event) => {
297
+ callHandler(onCancel, event)
298
+ if (event.defaultPrevented) return
299
+ if (event.cancelable) event.preventDefault()
300
+ ctx.close(event)
301
+ }}
302
+ set:onclick={(event: MouseEvent) => {
303
+ callHandler(onClick, event)
304
+ if (event.defaultPrevented) return
305
+ const target = event.currentTarget as HTMLDialogElement
306
+ if (event.target === target && outside(target, event)) {
307
+ onPointerDownOutside?.(event)
308
+ if (!event.defaultPrevented) ctx.close(event)
309
+ }
310
+ }}
311
+ set:onkeydown={(event: KeyboardEvent) => {
312
+ if (event.key === 'Escape') onEscapeKeyDown?.(event)
313
+ callHandler(onKeydown, event)
314
+ }}
315
+ >
316
+ {children}
317
+ {/* Portal outlet for top-layer UI that must stay interactive while
318
+ this dialog is modal (everything outside the dialog subtree is
319
+ inert). The Toaster re-homes its viewport here through a second
320
+ render root; `skip` keeps that root's DOM out of this tree's
321
+ reconciliation, the `key` keeps sibling churn in the dialog's
322
+ children from ever claiming this element (unkeyed matching
323
+ pairs by tag name alone), and display:contents keeps the
324
+ outlet out of the panel's layout. */}
325
+ <div data-slot="dialog-portal" key="dialog-portal" skip style="display:contents" />
326
+ </dialog>
327
+ )
328
+ }
329
+
330
+ /** Unstyled header area for dialog title and description. */
331
+ const DialogHeader: Stateless<DialogHeaderArgs> = ({
332
+ children,
333
+ 'data-slot': slot = 'dialog-header',
334
+ ...attrs
335
+ }) => (
336
+ <div {...attrs} data-slot={slot}>
337
+ {children}
338
+ </div>
339
+ )
340
+
341
+ /** Unstyled footer area for dialog actions. */
342
+ const DialogFooter: Stateless<DialogFooterArgs> = ({
343
+ children,
344
+ 'data-slot': slot = 'dialog-footer',
345
+ ...attrs
346
+ }) => (
347
+ <div {...attrs} data-slot={slot}>
348
+ {children}
349
+ </div>
350
+ )
351
+
352
+ /** Accessible title for DialogContent. */
353
+ const DialogTitle: Stateless<DialogTitleArgs> = ({
354
+ children,
355
+ 'data-slot': slot = 'dialog-title',
356
+ id,
357
+ ...attrs
358
+ }) => {
359
+ const ctx = DialogContext()
360
+
361
+ return (
362
+ <h2 {...attrs} data-slot={slot} id={id ?? ctx?.titleId}>
363
+ {children}
364
+ </h2>
365
+ )
366
+ }
367
+
368
+ /** Accessible description for DialogContent. */
369
+ const DialogDescription: Stateless<DialogDescriptionArgs> = ({
370
+ children,
371
+ 'data-slot': slot = 'dialog-description',
372
+ id,
373
+ ...attrs
374
+ }) => {
375
+ const ctx = DialogContext()
376
+
377
+ return (
378
+ <p {...attrs} data-slot={slot} id={id ?? ctx?.descriptionId}>
379
+ {children}
380
+ </p>
381
+ )
382
+ }
383
+
384
+ export {
385
+ Dialog,
386
+ DialogClose,
387
+ DialogContent,
388
+ DialogDescription,
389
+ DialogFooter,
390
+ DialogHeader,
391
+ DialogTitle,
392
+ DialogTrigger,
393
+ }
@@ -0,0 +1,45 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import type { OmitArg } from './utils'
3
+ import { statefulRootAttrs as rootAttrs } from 'ajo-cloves'
4
+ import { context } from 'ajo/context'
5
+
6
+ /** Logical text direction inherited by direction-aware components. */
7
+ export type Direction = 'ltr' | 'rtl'
8
+
9
+ /** Arguments for a provider that owns direction context and the host `dir`. */
10
+ export type DirectionProviderArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'dir'> & {
11
+ /** Text direction. */
12
+ dir?: Direction
13
+ }>
14
+
15
+ /** Direction inherited by direction-aware components. */
16
+ export const DirectionContext = context<Direction>('ltr')
17
+
18
+ type DirectionRootArgs = WithChildren<{
19
+ dir: Direction
20
+ }>
21
+
22
+ const DirectionRoot: Stateful<DirectionRootArgs> = function* () {
23
+ for (const { children, dir } of this) {
24
+ DirectionContext(dir)
25
+ yield <>{children}</>
26
+ }
27
+ }
28
+
29
+ /** Unstyled provider that sets direction context and an inherited HTML `dir` attribute. */
30
+ const DirectionProvider: Stateless<DirectionProviderArgs> = ({
31
+ children,
32
+ dir = 'ltr',
33
+ ...attrs
34
+ }) => (
35
+ <DirectionRoot
36
+ {...rootAttrs(attrs)}
37
+ dir={dir}
38
+ attr:data-slot="direction-provider"
39
+ attr:dir={dir}
40
+ >
41
+ {children}
42
+ </DirectionRoot>
43
+ )
44
+
45
+ export { DirectionProvider }
package/src/drawer.tsx ADDED
@@ -0,0 +1,251 @@
1
+ import type { Stateful, Stateless } from 'ajo'
2
+ import { callRef, move, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+ import { clx, withSlot } from './utils'
5
+ import {
6
+ Dialog,
7
+ DialogClose,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ DialogTrigger,
14
+ type DialogArgs,
15
+ type DialogCloseArgs,
16
+ type DialogContentArgs,
17
+ type DialogDescriptionArgs,
18
+ type DialogFooterArgs,
19
+ type DialogHeaderArgs,
20
+ type DialogTitleArgs,
21
+ type DialogTriggerArgs,
22
+ } from './dialog'
23
+
24
+ /** Viewport edge from which a Drawer panel enters. */
25
+ export type DrawerSide = 'top' | 'right' | 'bottom' | 'left'
26
+
27
+ /** Arguments for the Drawer state provider and entry edge. */
28
+ export type DrawerArgs = DialogArgs & {
29
+ /** Edge where the drawer enters from. */
30
+ side?: DrawerSide
31
+ }
32
+ /** Arguments for a button that opens its nearest Drawer. */
33
+ export type DrawerTriggerArgs = DialogTriggerArgs
34
+ /** Arguments for a button that closes its nearest Drawer. */
35
+ export type DrawerCloseArgs = DialogCloseArgs
36
+ /** Arguments for the Drawer panel, drag handle, and default close control. */
37
+ export type DrawerContentArgs = DialogContentArgs & {
38
+ closeClass?: string
39
+ /** Icon class for the default close button. */
40
+ closeIconClass?: string
41
+ /** Accessible label for the default close button. */
42
+ closeLabel?: string
43
+ handle?: boolean
44
+ handleClass?: string
45
+ /** Accessible label for the drag handle. */
46
+ handleLabel?: string
47
+ showCloseButton?: boolean
48
+ sideClass?: Partial<Record<DrawerSide, string>>
49
+ }
50
+ /** Arguments for the title and description area of a Drawer. */
51
+ export type DrawerHeaderArgs = DialogHeaderArgs
52
+ /** Arguments for the action area at the end of a Drawer. */
53
+ export type DrawerFooterArgs = DialogFooterArgs
54
+ /** Arguments for the heading that labels DrawerContent. */
55
+ export type DrawerTitleArgs = DialogTitleArgs
56
+ /** Arguments for the text that describes DrawerContent. */
57
+ export type DrawerDescriptionArgs = DialogDescriptionArgs
58
+
59
+ type DrawerContextValue = {
60
+ drag: ReturnType<typeof move>
61
+ side: DrawerSide
62
+ setPanel: (element: HTMLDialogElement | null) => void
63
+ }
64
+
65
+ const DrawerContext = context<DrawerContextValue>({
66
+ drag: {
67
+ start: () => false,
68
+ get active() {
69
+ return false
70
+ },
71
+ },
72
+ side: 'right',
73
+ setPanel: () => {},
74
+ })
75
+
76
+ const DrawerRoot: Stateful<DrawerArgs> = function* () {
77
+ let side: DrawerSide = 'right'
78
+ let offset = 0
79
+ let panel: HTMLDialogElement | null = null
80
+
81
+ const reset = () => {
82
+ if (!panel) return
83
+ panel.style.transform = ''
84
+ panel.style.transition = ''
85
+ panel.style.willChange = ''
86
+ }
87
+
88
+ const movePanel = (next: number) => {
89
+ if (!panel) return
90
+ panel.style.transition = 'none'
91
+ panel.style.transform = transform(next, side)
92
+ panel.style.willChange = 'transform'
93
+ }
94
+
95
+ const drag = move(this, {
96
+ onStart: () => {
97
+ offset = 0
98
+ },
99
+ onMove: data => {
100
+ const delta = side === 'left' || side === 'right' ? data.dx : data.dy
101
+ offset = Math.max(0, side === 'left' || side === 'top' ? -delta : delta)
102
+ movePanel(offset)
103
+ },
104
+ onEnd: data => {
105
+ reset()
106
+ if (!data.canceled && offset > 72 && panel?.open) panel.close()
107
+ },
108
+ })
109
+
110
+ for (const args of this) {
111
+ side = args.side ?? 'right'
112
+ DrawerContext({
113
+ drag,
114
+ side,
115
+ setPanel: element => panel = element,
116
+ })
117
+
118
+ yield (
119
+ <Dialog
120
+ data-slot="drawer-dialog"
121
+ defaultOpen={args.defaultOpen}
122
+ modal={args.modal}
123
+ onOpenChange={args.onOpenChange}
124
+ open={args.open}
125
+ >
126
+ {args.children}
127
+ </Dialog>
128
+ )
129
+ }
130
+ }
131
+
132
+
133
+ /** Unstyled edge panel provider built on native Dialog behavior. */
134
+ const Drawer: Stateless<DrawerArgs> = ({
135
+ children,
136
+ class: classes,
137
+ defaultOpen,
138
+ modal,
139
+ onOpenChange,
140
+ open,
141
+ side,
142
+ ...attrs
143
+ }) => (
144
+ <DrawerRoot
145
+ {...rootAttrs(attrs)}
146
+ defaultOpen={defaultOpen}
147
+ modal={modal}
148
+ onOpenChange={onOpenChange}
149
+ open={open}
150
+ side={side}
151
+ attr:class={classes}
152
+ attr:data-slot="drawer"
153
+ >
154
+ {children}
155
+ </DrawerRoot>
156
+ )
157
+
158
+ /** Unstyled button that opens the nearest Drawer. */
159
+ const DrawerTrigger: Stateless<DrawerTriggerArgs> = withSlot<DrawerTriggerArgs>(DialogTrigger, 'drawer-trigger')
160
+
161
+ /** Unstyled button that closes the nearest Drawer. */
162
+ const DrawerClose: Stateless<DrawerCloseArgs> = withSlot<DrawerCloseArgs>(DialogClose, 'drawer-close')
163
+
164
+ const transform = (offset: number, side: DrawerSide) => {
165
+ if (side === 'left') return `translateX(${-offset}px)`
166
+ if (side === 'right') return `translateX(${offset}px)`
167
+ if (side === 'top') return `translateY(${-offset}px)`
168
+ return `translateY(${offset}px)`
169
+ }
170
+
171
+ /** Unstyled native Drawer panel with optional drag and close controls. */
172
+ const DrawerContent: Stateless<DrawerContentArgs> = ({
173
+ children,
174
+ class: classes,
175
+ closeClass,
176
+ closeIconClass,
177
+ closeLabel = 'Close',
178
+ handle = false,
179
+ handleClass,
180
+ handleLabel = 'Drag to close',
181
+ ref,
182
+ showCloseButton = true,
183
+ sideClass,
184
+ ...attrs
185
+ }) => {
186
+ const drawer = DrawerContext()
187
+ const { side } = drawer
188
+ let panel: HTMLDialogElement | null = null
189
+
190
+ const reference = (element: HTMLDialogElement | null) => {
191
+ panel = element
192
+ drawer.setPanel(element)
193
+ callRef(ref, element)
194
+ }
195
+
196
+ return (
197
+ <DialogContent
198
+ {...attrs}
199
+ class={clx(sideClass?.[side], classes)}
200
+ data-side={side}
201
+ data-slot="drawer-content"
202
+ ref={reference}
203
+ >
204
+ {handle ? (
205
+ <div
206
+ aria-label={handleLabel}
207
+ class={handleClass}
208
+ data-slot="drawer-handle"
209
+ role="button"
210
+ tabIndex={0}
211
+ set:onkeydown={(event: KeyboardEvent) => {
212
+ if ((event.key === 'Enter' || event.key === ' ') && panel?.open) {
213
+ event.preventDefault()
214
+ panel.close()
215
+ }
216
+ }}
217
+ set:onpointerdown={(event: PointerEvent) => drawer.drag.start(event)}
218
+ />
219
+ ) : null}
220
+ {children}
221
+ {showCloseButton ? (
222
+ <DrawerClose aria-label={closeLabel} class={closeClass}>
223
+ <span aria-hidden="true" class={closeIconClass} data-slot="drawer-close-icon" />
224
+ </DrawerClose>
225
+ ) : null}
226
+ </DialogContent>
227
+ )
228
+ }
229
+
230
+ /** Unstyled header area for Drawer title and description. */
231
+ const DrawerHeader: Stateless<DrawerHeaderArgs> = withSlot<DrawerHeaderArgs>(DialogHeader, 'drawer-header')
232
+
233
+ /** Unstyled footer area for Drawer actions. */
234
+ const DrawerFooter: Stateless<DrawerFooterArgs> = withSlot<DrawerFooterArgs>(DialogFooter, 'drawer-footer')
235
+
236
+ /** Accessible heading for DrawerContent. */
237
+ const DrawerTitle: Stateless<DrawerTitleArgs> = withSlot<DrawerTitleArgs>(DialogTitle, 'drawer-title')
238
+
239
+ /** Accessible description for DrawerContent. */
240
+ const DrawerDescription: Stateless<DrawerDescriptionArgs> = withSlot<DrawerDescriptionArgs>(DialogDescription, 'drawer-description')
241
+
242
+ export {
243
+ Drawer,
244
+ DrawerClose,
245
+ DrawerContent,
246
+ DrawerDescription,
247
+ DrawerFooter,
248
+ DrawerHeader,
249
+ DrawerTitle,
250
+ DrawerTrigger,
251
+ }