@duro-app/ui 5.11.0 → 5.12.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 (157) hide show
  1. package/dist/Table-ColfpgKV.js.map +1 -1
  2. package/dist/compiler-runtime-BuugS4D0.js.map +1 -1
  3. package/dist/components/Alert/Alert.d.ts.map +1 -1
  4. package/dist/components/Button/Button.d.ts +2 -1
  5. package/dist/components/Button/Button.d.ts.map +1 -1
  6. package/dist/components/Button/styles.css.d.ts +1 -0
  7. package/dist/components/Button/styles.css.d.ts.map +1 -1
  8. package/dist/components/Callout/Callout.d.ts.map +1 -1
  9. package/dist/components/Callout/styles.css.d.ts +3 -0
  10. package/dist/components/Callout/styles.css.d.ts.map +1 -1
  11. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
  12. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -1
  13. package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
  14. package/dist/components/Dialog/Dialog.d.ts +2 -17
  15. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  16. package/dist/components/Dialog/Dialog.stories.d.ts +4 -0
  17. package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
  18. package/dist/components/Dialog/native.css.d.ts +82 -0
  19. package/dist/components/Dialog/native.css.d.ts.map +1 -0
  20. package/dist/components/Dialog/parts.css.d.ts +82 -0
  21. package/dist/components/Dialog/parts.css.d.ts.map +1 -0
  22. package/dist/components/Dialog/parts.d.ts +35 -0
  23. package/dist/components/Dialog/parts.d.ts.map +1 -0
  24. package/dist/components/Dialog/styles.css.d.ts +1 -74
  25. package/dist/components/Dialog/styles.css.d.ts.map +1 -1
  26. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  27. package/dist/components/Drawer/styles.css.d.ts +2 -0
  28. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  29. package/dist/components/Grid/styles.css.d.ts +1 -0
  30. package/dist/components/Grid/styles.css.d.ts.map +1 -1
  31. package/dist/components/Icon/Icon.d.ts +3 -9
  32. package/dist/components/Icon/Icon.d.ts.map +1 -1
  33. package/dist/components/Icon/geometry.d.ts +60 -0
  34. package/dist/components/Icon/geometry.d.ts.map +1 -0
  35. package/dist/components/Icon/types.d.ts +15 -0
  36. package/dist/components/Icon/types.d.ts.map +1 -0
  37. package/dist/components/Input/Input.d.ts +5 -1
  38. package/dist/components/Input/Input.d.ts.map +1 -1
  39. package/dist/components/Input/styles.css.d.ts +1 -0
  40. package/dist/components/Input/styles.css.d.ts.map +1 -1
  41. package/dist/components/LinkButton/styles.css.d.ts +1 -0
  42. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  43. package/dist/components/Meter/styles.css.d.ts +1 -0
  44. package/dist/components/Meter/styles.css.d.ts.map +1 -1
  45. package/dist/components/PageShell/styles.css.d.ts +2 -0
  46. package/dist/components/PageShell/styles.css.d.ts.map +1 -1
  47. package/dist/components/RadioGroup/RadioGroup.d.ts +1 -17
  48. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  49. package/dist/components/RadioGroup/RadioGroup.stories.d.ts +3 -0
  50. package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -1
  51. package/dist/components/RadioGroup/group.d.ts +31 -0
  52. package/dist/components/RadioGroup/group.d.ts.map +1 -0
  53. package/dist/components/RadioGroup/styles.css.d.ts +10 -0
  54. package/dist/components/RadioGroup/styles.css.d.ts.map +1 -1
  55. package/dist/components/ScrollArea/styles.css.d.ts +1 -0
  56. package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
  57. package/dist/components/Spinner/Spinner.d.ts +2 -6
  58. package/dist/components/Spinner/Spinner.d.ts.map +1 -1
  59. package/dist/components/Spinner/Spinner.types.d.ts +6 -0
  60. package/dist/components/Spinner/Spinner.types.d.ts.map +1 -0
  61. package/dist/components/Spinner/ring.css.d.ts +39 -0
  62. package/dist/components/Spinner/ring.css.d.ts.map +1 -0
  63. package/dist/components/Spinner/styles.css.d.ts +1 -28
  64. package/dist/components/Spinner/styles.css.d.ts.map +1 -1
  65. package/dist/components/StatusIcon/StatusIcon.d.ts.map +1 -1
  66. package/dist/components/StatusIcon/styles.css.d.ts +3 -0
  67. package/dist/components/StatusIcon/styles.css.d.ts.map +1 -1
  68. package/dist/components/Switch/Switch.d.ts +2 -18
  69. package/dist/components/Switch/Switch.d.ts.map +1 -1
  70. package/dist/components/Switch/Switch.types.d.ts +18 -0
  71. package/dist/components/Switch/Switch.types.d.ts.map +1 -0
  72. package/dist/components/TableOfContents/styles.css.d.ts +1 -0
  73. package/dist/components/TableOfContents/styles.css.d.ts.map +1 -1
  74. package/dist/components/Textarea/styles.css.d.ts +1 -0
  75. package/dist/components/Textarea/styles.css.d.ts.map +1 -1
  76. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  77. package/dist/components/Toggle/styles.css.d.ts +11 -2
  78. package/dist/components/Toggle/styles.css.d.ts.map +1 -1
  79. package/dist/components/VirtualTable/styles.css.d.ts +6 -0
  80. package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
  81. package/dist/index.css +1 -1
  82. package/dist/index.js +4299 -3566
  83. package/dist/index.js.map +1 -1
  84. package/dist/shared/portal/hostRegistry.d.ts +33 -0
  85. package/dist/shared/portal/hostRegistry.d.ts.map +1 -0
  86. package/dist/shared/useOverlayOpenState.d.ts +40 -0
  87. package/dist/shared/useOverlayOpenState.d.ts.map +1 -0
  88. package/dist/table.js +141 -135
  89. package/dist/table.js.map +1 -1
  90. package/package.json +34 -4
  91. package/src/components/Alert/Alert.tsx +14 -2
  92. package/src/components/Button/Button.tsx +3 -2
  93. package/src/components/Button/styles.css.ts +2 -0
  94. package/src/components/Callout/Callout.tsx +17 -3
  95. package/src/components/Callout/styles.css.ts +5 -0
  96. package/src/components/Checkbox/Checkbox.native.tsx +67 -0
  97. package/src/components/Checkbox/Checkbox.tsx +1 -50
  98. package/src/components/ConfirmDialog/ConfirmDialog.tsx +3 -0
  99. package/src/components/DetailPanel/DetailPanel.tsx +9 -45
  100. package/src/components/Dialog/Dialog.native.tsx +296 -0
  101. package/src/components/Dialog/Dialog.stories.tsx +71 -0
  102. package/src/components/Dialog/Dialog.tsx +41 -130
  103. package/src/components/Dialog/native.css.ts +78 -0
  104. package/src/components/Dialog/parts.css.ts +101 -0
  105. package/src/components/Dialog/parts.tsx +61 -0
  106. package/src/components/Dialog/styles.css.ts +2 -89
  107. package/src/components/Drawer/Drawer.tsx +20 -49
  108. package/src/components/Drawer/styles.css.ts +4 -0
  109. package/src/components/Grid/styles.css.ts +2 -0
  110. package/src/components/Icon/Icon.native.tsx +54 -0
  111. package/src/components/Icon/Icon.tsx +19 -430
  112. package/src/components/Icon/geometry.ts +456 -0
  113. package/src/components/Icon/types.ts +74 -0
  114. package/src/components/Input/Input.tsx +8 -0
  115. package/src/components/Input/styles.css.ts +2 -0
  116. package/src/components/LinkButton/styles.css.ts +2 -0
  117. package/src/components/Meter/styles.css.ts +2 -0
  118. package/src/components/PageShell/styles.css.ts +4 -0
  119. package/src/components/RadioGroup/RadioGroup.meta.ts +1 -1
  120. package/src/components/RadioGroup/RadioGroup.native.tsx +48 -0
  121. package/src/components/RadioGroup/RadioGroup.stories.tsx +64 -1
  122. package/src/components/RadioGroup/RadioGroup.tsx +8 -83
  123. package/src/components/RadioGroup/group.tsx +85 -0
  124. package/src/components/RadioGroup/styles.css.ts +13 -0
  125. package/src/components/ScrollArea/styles.css.ts +2 -0
  126. package/src/components/Spinner/Spinner.native.tsx +37 -0
  127. package/src/components/Spinner/Spinner.tsx +6 -15
  128. package/src/components/Spinner/Spinner.types.ts +6 -0
  129. package/src/components/Spinner/ring.css.ts +45 -0
  130. package/src/components/Spinner/styles.css.ts +2 -32
  131. package/src/components/StatusIcon/StatusIcon.tsx +13 -2
  132. package/src/components/StatusIcon/styles.css.ts +5 -0
  133. package/src/components/Switch/Switch.native.tsx +45 -0
  134. package/src/components/Switch/Switch.tsx +22 -44
  135. package/src/components/Switch/Switch.types.ts +18 -0
  136. package/src/components/TableOfContents/styles.css.ts +2 -0
  137. package/src/components/Textarea/styles.css.ts +2 -0
  138. package/src/components/ThemeProvider/ThemeProvider.native.tsx +70 -10
  139. package/src/components/Toggle/Toggle.tsx +2 -0
  140. package/src/components/Toggle/styles.css.ts +14 -2
  141. package/src/components/VirtualTable/styles.css.ts +12 -0
  142. package/src/index.native.ts +28 -1
  143. package/src/native-guard/globals.d.ts +10 -0
  144. package/src/native-guard/react-dom.d.ts +3 -0
  145. package/src/shared/accessibilityFocus.native.ts +23 -0
  146. package/src/shared/portal/OverlayModal.native.tsx +51 -0
  147. package/src/shared/portal/PortalHost.native.tsx +102 -0
  148. package/src/shared/portal/hostRegistry.ts +76 -0
  149. package/src/shared/portal/themeScope.native.ts +18 -0
  150. package/src/shared/safeArea.native.ts +22 -0
  151. package/src/shared/useOverlayOpenState.ts +128 -0
  152. package/dist/components/ThemeProvider/ThemeProvider.native.d.ts +0 -10
  153. package/dist/components/ThemeProvider/ThemeProvider.native.d.ts.map +0 -1
  154. package/dist/index.native.d.ts +0 -33
  155. package/dist/index.native.d.ts.map +0 -1
  156. package/dist/platform.native.d.ts +0 -2
  157. package/dist/platform.native.d.ts.map +0 -1
@@ -1,30 +1,32 @@
1
1
  import {createPortal} from 'react-dom'
2
- import {
3
- type ReactNode,
4
- type RefObject,
5
- createContext,
6
- useContext,
7
- useState,
8
- useEffect,
9
- useCallback,
10
- useId,
11
- useRef,
12
- } from 'react'
2
+ import {type ReactNode, type RefObject, useEffect, useCallback, useId, useRef} from 'react'
13
3
  import {html} from 'react-strict-dom'
14
- import {DURATION_MS, type DurationToken} from '@duro-app/tokens/keys'
4
+ import type {DurationToken} from '@duro-app/tokens/keys'
15
5
  import {styles} from './styles.css'
6
+ import {partStyles} from './parts.css'
7
+ import {
8
+ Body,
9
+ Description,
10
+ DialogContext,
11
+ Footer,
12
+ Header,
13
+ Title,
14
+ useDialog,
15
+ type DialogSize,
16
+ } from './parts'
16
17
  import {usePortalMount} from '../ThemeProvider/ThemeProvider'
17
18
  import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
18
19
  import {ModalContext} from '../../shared/ModalContext'
19
20
  import {devWarnOnce} from '../../shared/devWarnOnce'
21
+ import {useOverlayOpenState} from '../../shared/useOverlayOpenState'
20
22
 
21
23
  // --- Types ---
22
24
 
25
+ export type {DialogSize}
26
+
23
27
  const FOCUSABLE =
24
28
  'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
25
29
 
26
- export type DialogSize = 'sm' | 'md' | 'lg'
27
-
28
30
  const closeDurationMap = {
29
31
  instant: styles.closeDurationInstant,
30
32
  minimal: styles.closeDurationMinimal,
@@ -35,30 +37,6 @@ const closeDurationMap = {
35
37
  slow: styles.closeDurationSlow,
36
38
  } as const satisfies Record<DurationToken, unknown>
37
39
 
38
- // --- Context ---
39
-
40
- interface DialogContextValue {
41
- open: boolean
42
- closing: boolean
43
- closeDuration: DurationToken
44
- dismissable: boolean
45
- requestOpen: () => void
46
- requestClose: () => void
47
- titleId: string
48
- descriptionId: string
49
- popupRef: RefObject<HTMLDivElement | null>
50
- /** Dialog.Close parts currently rendered (for the closeOnEscape dev warning). */
51
- registerClose: () => () => void
52
- }
53
-
54
- const DialogContext = createContext<DialogContextValue | null>(null)
55
-
56
- function useDialog() {
57
- const ctx = useContext(DialogContext)
58
- if (!ctx) throw new Error('Dialog compound components must be used within Dialog.Root')
59
- return ctx
60
- }
61
-
62
40
  // --- Root ---
63
41
 
64
42
  interface RootProps {
@@ -93,55 +71,28 @@ function Root({
93
71
  initialFocus,
94
72
  closeAnimationDuration = 'fast',
95
73
  }: RootProps) {
96
- const isControlled = controlledOpen !== undefined
97
- const [internalOpen, setInternalOpen] = useState(defaultOpen)
98
- const [closing, setClosing] = useState(false)
99
- const closingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
74
+ const {
75
+ open: isOpen,
76
+ present,
77
+ closing,
78
+ requestOpen,
79
+ requestClose,
80
+ } = useOverlayOpenState({
81
+ open: controlledOpen,
82
+ defaultOpen,
83
+ onOpenChange,
84
+ exitDuration: closeAnimationDuration,
85
+ closeNotice: 'after-exit',
86
+ })
100
87
  const popupRef = useRef<HTMLDivElement>(null)
101
88
 
102
- const isOpen = isControlled ? controlledOpen : internalOpen
103
-
104
89
  const titleId = useId()
105
90
  const descriptionId = useId()
106
91
 
107
- const requestOpen = useCallback(() => {
108
- if (closingTimerRef.current) {
109
- clearTimeout(closingTimerRef.current)
110
- closingTimerRef.current = null
111
- }
112
- setClosing(false)
113
- if (!isControlled) setInternalOpen(true)
114
- onOpenChange?.(true)
115
- }, [isControlled, onOpenChange])
116
-
117
- const requestClose = useCallback(() => {
118
- setClosing(true)
119
- closingTimerRef.current = setTimeout(() => {
120
- setClosing(false)
121
- closingTimerRef.current = null
122
- if (!isControlled) setInternalOpen(false)
123
- onOpenChange?.(false)
124
- }, DURATION_MS[closeAnimationDuration])
125
- }, [isControlled, onOpenChange, closeAnimationDuration])
126
-
127
- // If controlled open goes false externally, trigger close animation
128
- useEffect(() => {
129
- if (isControlled && !controlledOpen && !closing) {
130
- // Already closed, nothing to animate
131
- }
132
- }, [isControlled, controlledOpen, closing])
133
-
134
- // Clean up timer on unmount
135
- useEffect(() => {
136
- return () => {
137
- if (closingTimerRef.current) clearTimeout(closingTimerRef.current)
138
- }
139
- }, [])
140
-
141
92
  // Handle Escape key. A layer inside the dialog (a Menu, a Select) that
142
93
  // already handled it marks the event, and the dialog stays open.
143
94
  useEffect(() => {
144
- if (!isOpen && !closing) return
95
+ if (!present) return
145
96
  if (!closeOnEscape) return
146
97
 
147
98
  function onKeyDown(e: KeyboardEvent) {
@@ -151,7 +102,7 @@ function Root({
151
102
  }
152
103
  document.addEventListener('keydown', onKeyDown)
153
104
  return () => document.removeEventListener('keydown', onKeyDown)
154
- }, [isOpen, closing, closeOnEscape, requestClose])
105
+ }, [present, closeOnEscape, requestClose])
155
106
 
156
107
  // Focus: into the dialog on open (initialFocus, else the first focusable
157
108
  // element, else the dialog itself), back to what had it on close. The
@@ -180,29 +131,29 @@ function Root({
180
131
  'Dialog.Root has closeOnEscape={false} and no Dialog.Close: a keyboard user cannot leave this dialog. Render a Dialog.Close, or keep Escape.',
181
132
  )
182
133
  }
183
- } else if (!isOpen && !closing && wasOpenRef.current) {
134
+ } else if (!present && wasOpenRef.current) {
184
135
  const target = previousFocusRef.current
185
136
  previousFocusRef.current = null
186
137
  if (target?.isConnected) target.focus({preventScroll: true})
187
138
  }
188
- wasOpenRef.current = isOpen || closing
189
- }, [isOpen, closing, initialFocus, closeOnEscape])
139
+ wasOpenRef.current = present
140
+ }, [isOpen, present, initialFocus, closeOnEscape])
190
141
 
191
142
  // Lock body scroll when open
192
143
  useEffect(() => {
193
- if (isOpen || closing) {
144
+ if (present) {
194
145
  const prev = document.body.style.overflow
195
146
  document.body.style.overflow = 'hidden'
196
147
  return () => {
197
148
  document.body.style.overflow = prev
198
149
  }
199
150
  }
200
- }, [isOpen, closing])
151
+ }, [present])
201
152
 
202
153
  return (
203
154
  <DialogContext.Provider
204
155
  value={{
205
- open: isOpen || closing,
156
+ open: present,
206
157
  closing,
207
158
  closeDuration: closeAnimationDuration,
208
159
  dismissable,
@@ -229,7 +180,7 @@ function Trigger({children}: TriggerProps) {
229
180
  const {requestOpen} = useDialog()
230
181
 
231
182
  return (
232
- <html.div onClick={requestOpen} style={styles.inlineWrapper}>
183
+ <html.div onClick={requestOpen} style={partStyles.inlineWrapper}>
233
184
  {children}
234
185
  </html.div>
235
186
  )
@@ -288,7 +239,7 @@ function Portal({children, size = 'md'}: PortalProps) {
288
239
  aria-describedby={descriptionId}
289
240
  style={[
290
241
  styles.popup,
291
- styles[size],
242
+ partStyles[size],
292
243
  closing && styles.popupClosing,
293
244
  closing && closeDurationMap[closeDuration],
294
245
  !closing && styles.popupOpen,
@@ -310,46 +261,6 @@ function Portal({children, size = 'md'}: PortalProps) {
310
261
  return mount ? createPortal(node, mount) : node
311
262
  }
312
263
 
313
- // --- Header ---
314
-
315
- function Header({children}: {children: ReactNode}) {
316
- return <html.div style={styles.header}>{children}</html.div>
317
- }
318
-
319
- // --- Title ---
320
-
321
- function Title({children}: {children: ReactNode}) {
322
- const {titleId} = useDialog()
323
- return (
324
- <html.h2 id={titleId} style={styles.title}>
325
- {children}
326
- </html.h2>
327
- )
328
- }
329
-
330
- // --- Description ---
331
-
332
- function Description({children}: {children: ReactNode}) {
333
- const {descriptionId} = useDialog()
334
- return (
335
- <html.p id={descriptionId} style={styles.description}>
336
- {children}
337
- </html.p>
338
- )
339
- }
340
-
341
- // --- Body ---
342
-
343
- function Body({children}: {children: ReactNode}) {
344
- return <html.div style={styles.body}>{children}</html.div>
345
- }
346
-
347
- // --- Footer ---
348
-
349
- function Footer({children}: {children: ReactNode}) {
350
- return <html.div style={styles.footer}>{children}</html.div>
351
- }
352
-
353
264
  // --- Close ---
354
265
 
355
266
  interface CloseProps {
@@ -363,14 +274,14 @@ function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
363
274
 
364
275
  if (children) {
365
276
  return (
366
- <html.div onClick={requestClose} style={styles.inlineWrapper}>
277
+ <html.div onClick={requestClose} style={partStyles.inlineWrapper}>
367
278
  {children}
368
279
  </html.div>
369
280
  )
370
281
  }
371
282
 
372
283
  return (
373
- <html.button onClick={requestClose} aria-label={ariaLabel} style={styles.closeButton}>
284
+ <html.button onClick={requestClose} aria-label={ariaLabel} style={partStyles.closeButton}>
374
285
  <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
375
286
  <path
376
287
  d="M4 4l8 8M12 4l-8 8"
@@ -0,0 +1,78 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {shadows} from '@duro-app/tokens/tokens/shadows.css'
5
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
6
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
7
+
8
+ /**
9
+ * Native Dialog surfaces. RSD has no CSS animations on native, so enter and
10
+ * exit are transitions between a hidden and a shown state (polyfilled with
11
+ * Animated).
12
+ */
13
+ export const nativeStyles = css.create({
14
+ fill: {
15
+ position: 'absolute',
16
+ top: 0,
17
+ left: 0,
18
+ right: 0,
19
+ bottom: 0,
20
+ },
21
+ backdrop: {
22
+ backgroundColor: colors.scrim,
23
+ transitionProperty: 'opacity',
24
+ transitionDuration: duration.base,
25
+ transitionTimingFunction: easing.easeOut,
26
+ },
27
+ // Padding around the panel inside the RN viewport view.
28
+ inset: {
29
+ display: 'flex',
30
+ padding: spacing.lg,
31
+ width: '100%',
32
+ // Native has no border-box reset (the web's global-reset.css); declare it.
33
+ boxSizing: 'border-box',
34
+ alignItems: 'center',
35
+ },
36
+ popup: {
37
+ backgroundColor: colors.bgCard,
38
+ borderRadius: radii.lg,
39
+ boxShadow: shadows.lg,
40
+ width: '100%',
41
+ // Native has no border-box reset (the web's global-reset.css); declare it.
42
+ boxSizing: 'border-box',
43
+ display: 'flex',
44
+ flexDirection: 'column',
45
+ overflow: 'hidden',
46
+ borderWidth: borders.hairline,
47
+ borderStyle: 'solid',
48
+ borderColor: colors.border,
49
+ transitionProperty: 'opacity, transform',
50
+ transitionDuration: duration.base,
51
+ transitionTimingFunction: easing.easeOut,
52
+ },
53
+ // The Body's padding inside its ScrollView (no flex: it sizes to content).
54
+ bodyContent: {
55
+ paddingLeft: spacing.lg,
56
+ paddingRight: spacing.lg,
57
+ paddingBottom: spacing.lg,
58
+ },
59
+ // 85% of the window, as the web caps the panel at 85vh: a percentage
60
+ // max-height has no definite parent to resolve against here. The Body
61
+ // (a ScrollView) yields.
62
+ maxHeight: (height: number) => ({maxHeight: height}),
63
+ shown: {
64
+ opacity: 1,
65
+ transform: 'scale(1)',
66
+ },
67
+ hidden: {
68
+ opacity: 0,
69
+ transform: 'scale(0.97)',
70
+ },
71
+ exitInstant: {transitionDuration: duration.instant},
72
+ exitMinimal: {transitionDuration: duration.minimal},
73
+ exitQuick: {transitionDuration: duration.quick},
74
+ exitFast: {transitionDuration: duration.fast},
75
+ exitBrisk: {transitionDuration: duration.brisk},
76
+ exitBase: {transitionDuration: duration.base},
77
+ exitSlow: {transitionDuration: duration.slow},
78
+ })
@@ -0,0 +1,101 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {duration} from '@duro-app/tokens/tokens/motion.css'
6
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
7
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
8
+
9
+ /** Styles of the Dialog parts both platforms draw (no animations: native has none). */
10
+ export const partStyles = css.create({
11
+ // --- Sizes ---
12
+ sm: {maxWidth: sizes.dialogSm},
13
+ md: {maxWidth: sizes.dialogMd},
14
+ lg: {maxWidth: sizes.dialogLg},
15
+
16
+ // --- Header ---
17
+ header: {
18
+ // Header, Description and Footer keep their size; only the Body yields.
19
+ flexShrink: 0,
20
+ display: 'flex',
21
+ alignItems: 'flex-start',
22
+ justifyContent: 'space-between',
23
+ paddingTop: spacing.lg,
24
+ paddingLeft: spacing.lg,
25
+ paddingRight: spacing.lg,
26
+ paddingBottom: spacing.md,
27
+ gap: spacing.md,
28
+ },
29
+
30
+ // --- Title ---
31
+ title: {
32
+ fontSize: typography.fontSizeLg,
33
+ fontWeight: typography.fontWeightSemibold,
34
+ lineHeight: typography.lineHeight,
35
+ color: colors.text,
36
+ margin: 0,
37
+ },
38
+
39
+ // --- Description ---
40
+ description: {
41
+ flexShrink: 0,
42
+ fontSize: typography.fontSizeSm,
43
+ lineHeight: typography.lineHeight,
44
+ color: colors.textMuted,
45
+ margin: 0,
46
+ paddingLeft: spacing.lg,
47
+ paddingRight: spacing.lg,
48
+ paddingBottom: spacing.md,
49
+ },
50
+
51
+ // --- Body ---
52
+ body: {
53
+ paddingLeft: spacing.lg,
54
+ paddingRight: spacing.lg,
55
+ paddingBottom: spacing.lg,
56
+ overflowY: 'auto',
57
+ flex: 1,
58
+ },
59
+
60
+ // --- Footer ---
61
+ footer: {
62
+ flexShrink: 0,
63
+ display: 'flex',
64
+ alignItems: 'center',
65
+ justifyContent: 'flex-end',
66
+ gap: spacing.sm,
67
+ paddingTop: spacing.md,
68
+ paddingLeft: spacing.lg,
69
+ paddingRight: spacing.lg,
70
+ paddingBottom: spacing.lg,
71
+ borderTopWidth: borders.hairline,
72
+ borderTopStyle: 'solid',
73
+ borderTopColor: colors.border,
74
+ },
75
+
76
+ // --- Close button ---
77
+ closeButton: {
78
+ display: 'flex',
79
+ alignItems: 'center',
80
+ justifyContent: 'center',
81
+ width: sizes.iconButton,
82
+ height: sizes.iconButton,
83
+ borderRadius: radii.sm,
84
+ borderWidth: 0,
85
+ backgroundColor: {
86
+ default: 'transparent',
87
+ ':hover': colors.bgCardHover,
88
+ },
89
+ color: colors.textMuted,
90
+ cursor: 'pointer',
91
+ padding: 0,
92
+ flexShrink: 0,
93
+ transitionProperty: 'background-color',
94
+ transitionDuration: duration.fast,
95
+ },
96
+
97
+ // --- Inline wrapper (for Trigger / Close children) ---
98
+ inlineWrapper: {
99
+ display: 'inline-flex',
100
+ },
101
+ })
@@ -0,0 +1,61 @@
1
+ import {createContext, type ReactNode, type RefObject, useContext} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import type {DurationToken} from '@duro-app/tokens/keys'
4
+ import {partStyles} from './parts.css'
5
+
6
+ export type DialogSize = 'sm' | 'md' | 'lg'
7
+
8
+ export interface DialogContextValue {
9
+ /** Rendered: open, or playing the exit animation. */
10
+ open: boolean
11
+ closing: boolean
12
+ closeDuration: DurationToken
13
+ dismissable: boolean
14
+ requestOpen: () => void
15
+ requestClose: () => void
16
+ titleId: string
17
+ descriptionId: string
18
+ popupRef: RefObject<HTMLDivElement | null>
19
+ /** Dialog.Close parts currently rendered (for the closeOnEscape dev warning). */
20
+ registerClose: () => () => void
21
+ }
22
+
23
+ export const DialogContext = createContext<DialogContextValue | null>(null)
24
+
25
+ export function useDialog() {
26
+ const ctx = useContext(DialogContext)
27
+ if (!ctx) throw new Error('Dialog compound components must be used within Dialog.Root')
28
+ return ctx
29
+ }
30
+
31
+ // The parts below are pure react-strict-dom, drawn the same on both platforms.
32
+
33
+ export function Header({children}: {children: ReactNode}) {
34
+ return <html.div style={partStyles.header}>{children}</html.div>
35
+ }
36
+
37
+ export function Title({children}: {children: ReactNode}) {
38
+ const {titleId} = useDialog()
39
+ return (
40
+ <html.h2 id={titleId} style={partStyles.title}>
41
+ {children}
42
+ </html.h2>
43
+ )
44
+ }
45
+
46
+ export function Description({children}: {children: ReactNode}) {
47
+ const {descriptionId} = useDialog()
48
+ return (
49
+ <html.p id={descriptionId} style={partStyles.description}>
50
+ {children}
51
+ </html.p>
52
+ )
53
+ }
54
+
55
+ export function Body({children}: {children: ReactNode}) {
56
+ return <html.div style={partStyles.body}>{children}</html.div>
57
+ }
58
+
59
+ export function Footer({children}: {children: ReactNode}) {
60
+ return <html.div style={partStyles.footer}>{children}</html.div>
61
+ }
@@ -1,10 +1,8 @@
1
1
  import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
- import {typography} from '@duro-app/tokens/tokens/typography.css'
5
4
  import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
5
  import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
7
- import {sizes} from '@duro-app/tokens/tokens/sizes.css'
8
6
  import {borders} from '@duro-app/tokens/tokens/borders.css'
9
7
  import {layers} from '@duro-app/tokens/tokens/layers.css'
10
8
 
@@ -60,6 +58,8 @@ export const styles = css.create({
60
58
  borderRadius: radii.lg,
61
59
  boxShadow: shadows.lg,
62
60
  width: '100%',
61
+ // Native has no border-box reset (the web's global-reset.css); declare it.
62
+ boxSizing: 'border-box',
63
63
  maxHeight: '85vh',
64
64
  display: 'flex',
65
65
  flexDirection: 'column',
@@ -115,93 +115,6 @@ export const styles = css.create({
115
115
  closeDurationBase: {animationDuration: duration.base},
116
116
  closeDurationSlow: {animationDuration: duration.slow},
117
117
 
118
- // --- Sizes ---
119
- sm: {maxWidth: sizes.dialogSm},
120
- md: {maxWidth: sizes.dialogMd},
121
- lg: {maxWidth: sizes.dialogLg},
122
-
123
- // --- Header ---
124
- header: {
125
- display: 'flex',
126
- alignItems: 'flex-start',
127
- justifyContent: 'space-between',
128
- paddingTop: spacing.lg,
129
- paddingLeft: spacing.lg,
130
- paddingRight: spacing.lg,
131
- paddingBottom: spacing.md,
132
- gap: spacing.md,
133
- },
134
-
135
- // --- Title ---
136
- title: {
137
- fontSize: typography.fontSizeLg,
138
- fontWeight: typography.fontWeightSemibold,
139
- lineHeight: typography.lineHeight,
140
- color: colors.text,
141
- margin: 0,
142
- },
143
-
144
- // --- Description ---
145
- description: {
146
- fontSize: typography.fontSizeSm,
147
- lineHeight: typography.lineHeight,
148
- color: colors.textMuted,
149
- margin: 0,
150
- paddingLeft: spacing.lg,
151
- paddingRight: spacing.lg,
152
- paddingBottom: spacing.md,
153
- },
154
-
155
- // --- Body ---
156
- body: {
157
- paddingLeft: spacing.lg,
158
- paddingRight: spacing.lg,
159
- paddingBottom: spacing.lg,
160
- overflowY: 'auto',
161
- flex: 1,
162
- },
163
-
164
- // --- Footer ---
165
- footer: {
166
- display: 'flex',
167
- alignItems: 'center',
168
- justifyContent: 'flex-end',
169
- gap: spacing.sm,
170
- paddingTop: spacing.md,
171
- paddingLeft: spacing.lg,
172
- paddingRight: spacing.lg,
173
- paddingBottom: spacing.lg,
174
- borderTopWidth: borders.hairline,
175
- borderTopStyle: 'solid',
176
- borderTopColor: colors.border,
177
- },
178
-
179
- // --- Close button ---
180
- closeButton: {
181
- display: 'flex',
182
- alignItems: 'center',
183
- justifyContent: 'center',
184
- width: sizes.iconButton,
185
- height: sizes.iconButton,
186
- borderRadius: radii.sm,
187
- borderWidth: 0,
188
- backgroundColor: {
189
- default: 'transparent',
190
- ':hover': colors.bgCardHover,
191
- },
192
- color: colors.textMuted,
193
- cursor: 'pointer',
194
- padding: 0,
195
- flexShrink: 0,
196
- transitionProperty: 'background-color',
197
- transitionDuration: duration.fast,
198
- },
199
-
200
- // --- Inline wrapper (for Trigger / Close children) ---
201
- inlineWrapper: {
202
- display: 'inline-flex',
203
- },
204
-
205
118
  // --- Reduced motion ---
206
119
  reducedMotion: {
207
120
  animationDuration: duration.minimal,