@duro-app/ui 0.35.3 → 0.36.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 (33) hide show
  1. package/dist/components/ColorMode/ColorMode.d.ts +61 -0
  2. package/dist/components/ColorMode/ColorMode.d.ts.map +1 -0
  3. package/dist/components/ColorMode/ColorMode.meta.d.ts +3 -0
  4. package/dist/components/ColorMode/ColorMode.meta.d.ts.map +1 -0
  5. package/dist/components/ColorMode/ColorMode.stories.d.ts +12 -0
  6. package/dist/components/ColorMode/ColorMode.stories.d.ts.map +1 -0
  7. package/dist/components/ColorMode/index.d.ts +2 -0
  8. package/dist/components/ColorMode/index.d.ts.map +1 -0
  9. package/dist/components/ColorMode/styles.css.d.ts +24 -0
  10. package/dist/components/ColorMode/styles.css.d.ts.map +1 -0
  11. package/dist/components/Icon/Icon.d.ts +1 -1
  12. package/dist/components/Icon/Icon.d.ts.map +1 -1
  13. package/dist/components/Select/Select.d.ts.map +1 -1
  14. package/dist/components/Select/Select.stories.d.ts +1 -0
  15. package/dist/components/Select/Select.stories.d.ts.map +1 -1
  16. package/dist/components/Select/styles.css.d.ts +10 -4
  17. package/dist/components/Select/styles.css.d.ts.map +1 -1
  18. package/dist/index.css +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2436 -2197
  22. package/dist/index.js.map +1 -1
  23. package/package.json +2 -2
  24. package/src/components/ColorMode/ColorMode.meta.ts +40 -0
  25. package/src/components/ColorMode/ColorMode.stories.tsx +87 -0
  26. package/src/components/ColorMode/ColorMode.tsx +205 -0
  27. package/src/components/ColorMode/index.ts +6 -0
  28. package/src/components/ColorMode/styles.css.ts +32 -0
  29. package/src/components/Icon/Icon.tsx +110 -0
  30. package/src/components/Select/Select.stories.tsx +59 -7
  31. package/src/components/Select/Select.tsx +43 -5
  32. package/src/components/Select/styles.css.ts +16 -4
  33. package/src/index.ts +6 -0
@@ -1,9 +1,11 @@
1
- import {type ReactNode, useRef, useId, useEffect} from 'react'
1
+ import {type ReactNode, useRef, useId, useEffect, useLayoutEffect, useState} from 'react'
2
+ import {createPortal} from 'react-dom'
2
3
  import {html} from 'react-strict-dom'
3
4
  import {styles} from './styles.css'
4
5
  import {SelectContext, useSelect} from './SelectContext'
5
6
  import {useSelectRoot} from './useSelectRoot'
6
7
  import {useFieldContext} from '../Field/FieldContext'
8
+ import {usePortalMount} from '../ThemeProvider/ThemeProvider'
7
9
 
8
10
  // --- Root ---
9
11
  interface RootProps {
@@ -108,22 +110,58 @@ function Icon({children}: {children?: ReactNode}) {
108
110
  }
109
111
 
110
112
  // --- Popup ---
113
+ // Rendered into the ThemeProvider portal mount and positioned with viewport
114
+ // coordinates (measured off the trigger) so it escapes ancestor
115
+ // `overflow: hidden` / `transform` containing blocks (notably Dialog/Drawer).
116
+ // The listbox stays mounted while closed (display: none) so Items can register
117
+ // their labels from DOM text before the popup is ever opened.
111
118
  function Popup({children}: {children: ReactNode}) {
112
- const {open, close, listboxId} = useSelect()
113
-
114
- return (
119
+ const {open, close, listboxId, triggerRef} = useSelect()
120
+ const mount = usePortalMount()
121
+ const [coords, setCoords] = useState<{top: number; left: number; width: number} | null>(null)
122
+
123
+ // Measure the trigger when the popup opens, and re-measure on scroll/resize so
124
+ // the dropdown follows it. Capture-phase scroll catches scroll on any ancestor
125
+ // (e.g. a Dialog body).
126
+ useLayoutEffect(() => {
127
+ if (!open) return
128
+ const update = () => {
129
+ const el = triggerRef.current
130
+ if (!el) return
131
+ const rect = el.getBoundingClientRect()
132
+ // `xs` token = 4px gap below the trigger.
133
+ setCoords({top: rect.bottom + 4, left: rect.left, width: rect.width})
134
+ }
135
+ update()
136
+ window.addEventListener('resize', update)
137
+ window.addEventListener('scroll', update, true)
138
+ return () => {
139
+ window.removeEventListener('resize', update)
140
+ window.removeEventListener('scroll', update, true)
141
+ }
142
+ }, [open, triggerRef])
143
+
144
+ const node = (
115
145
  <>
116
146
  {open && <html.div style={styles.backdrop} onClick={close} />}
117
147
  <html.div
118
148
  id={listboxId}
119
149
  role="listbox"
120
150
  aria-hidden={!open}
121
- style={[styles.popup, !open && styles.hidden]}
151
+ style={[
152
+ styles.popup,
153
+ (!open || !coords) && styles.hidden,
154
+ coords && styles.popupPosition(coords.top, coords.left, coords.width),
155
+ ]}
122
156
  >
123
157
  {children}
124
158
  </html.div>
125
159
  </>
126
160
  )
161
+
162
+ // SSR / pre-mount fallback: render inline (hidden under overflow parents but
163
+ // won't crash). Once the mount exists, portal into it.
164
+ return mount ? createPortal(node, mount) : node
127
165
  }
128
166
 
129
167
  // --- Item ---
@@ -51,12 +51,15 @@ export const styles = css.create({
51
51
  right: 0,
52
52
  bottom: 0,
53
53
  zIndex: 49,
54
+ // The portal mount is pointer-events: none, so re-enable here to catch
55
+ // outside clicks that close the popup.
56
+ pointerEvents: 'auto',
54
57
  },
55
58
  popup: {
56
- position: 'absolute',
57
- top: '100%',
58
- left: 0,
59
- marginTop: spacing.xs,
59
+ // Position is `fixed` and top/left/minWidth are applied inline by Popup
60
+ // (computed from the trigger's bounding rect) so the dropdown escapes any
61
+ // ancestor with `overflow: hidden` / `transform` (notably inside a Dialog).
62
+ position: 'fixed',
60
63
  backgroundColor: colors.bgCard,
61
64
  borderWidth: 1,
62
65
  borderStyle: 'solid',
@@ -66,8 +69,17 @@ export const styles = css.create({
66
69
  paddingTop: spacing.xs,
67
70
  paddingBottom: spacing.xs,
68
71
  minWidth: 120,
72
+ maxHeight: 280,
73
+ overflowY: 'auto',
69
74
  zIndex: 50,
75
+ // Re-enable pointer events: the portal mount is pointer-events: none.
76
+ pointerEvents: 'auto',
70
77
  },
78
+ popupPosition: (top: number, left: number, minWidth: number) => ({
79
+ top,
80
+ left,
81
+ minWidth,
82
+ }),
71
83
  item: {
72
84
  display: 'flex',
73
85
  alignItems: 'center',
package/src/index.ts CHANGED
@@ -62,6 +62,12 @@ export {
62
62
  usePortalMount,
63
63
  type ThemeName,
64
64
  } from './components/ThemeProvider/ThemeProvider'
65
+ export {
66
+ ColorModeProvider,
67
+ ColorModeToggle,
68
+ useColorMode,
69
+ type ColorModePreference,
70
+ } from './components/ColorMode'
65
71
  export {Text, type TextVariant, type TextColor} from './components/Text/Text'
66
72
  export {Toggle, type ToggleSize} from './components/Toggle/Toggle'
67
73
  export {ToggleGroup} from './components/ToggleGroup/ToggleGroup'