@duro-app/ui 5.1.0 → 5.2.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 (123) hide show
  1. package/dist/Table-C70zdSkM.js +1419 -0
  2. package/dist/Table-C70zdSkM.js.map +1 -0
  3. package/dist/components/Button/Button.d.ts +11 -2
  4. package/dist/components/Button/Button.d.ts.map +1 -1
  5. package/dist/components/Button/Button.stories.d.ts +1 -0
  6. package/dist/components/Button/Button.stories.d.ts.map +1 -1
  7. package/dist/components/Combobox/Combobox.d.ts +1 -1
  8. package/dist/components/Combobox/Combobox.d.ts.map +1 -1
  9. package/dist/components/Combobox/styles.css.d.ts +0 -64
  10. package/dist/components/Combobox/styles.css.d.ts.map +1 -1
  11. package/dist/components/Dialog/Dialog.d.ts +11 -3
  12. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  13. package/dist/components/Dialog/Dialog.stories.d.ts +4 -0
  14. package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
  15. package/dist/components/Dialog/styles.css.d.ts +1 -0
  16. package/dist/components/Dialog/styles.css.d.ts.map +1 -1
  17. package/dist/components/DragDrop/styles.css.d.ts +10 -2
  18. package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
  19. package/dist/components/Grid/Grid.d.ts +11 -2
  20. package/dist/components/Grid/Grid.d.ts.map +1 -1
  21. package/dist/components/Grid/Grid.stories.d.ts +2 -0
  22. package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
  23. package/dist/components/Input/Input.d.ts +6 -1
  24. package/dist/components/Input/Input.d.ts.map +1 -1
  25. package/dist/components/Input/Input.meta.d.ts.map +1 -1
  26. package/dist/components/Input/Input.stories.d.ts +1 -0
  27. package/dist/components/Input/Input.stories.d.ts.map +1 -1
  28. package/dist/components/Input/styles.css.d.ts +18 -0
  29. package/dist/components/Input/styles.css.d.ts.map +1 -1
  30. package/dist/components/InputGroup/InputGroup.d.ts +6 -1
  31. package/dist/components/InputGroup/InputGroup.d.ts.map +1 -1
  32. package/dist/components/InputGroup/InputGroup.stories.d.ts +1 -0
  33. package/dist/components/InputGroup/InputGroup.stories.d.ts.map +1 -1
  34. package/dist/components/Listbox/Listbox.d.ts +75 -0
  35. package/dist/components/Listbox/Listbox.d.ts.map +1 -0
  36. package/dist/components/Listbox/Listbox.meta.d.ts +3 -0
  37. package/dist/components/Listbox/Listbox.meta.d.ts.map +1 -0
  38. package/dist/components/Listbox/Listbox.stories.d.ts +7 -0
  39. package/dist/components/Listbox/Listbox.stories.d.ts.map +1 -0
  40. package/dist/components/Listbox/styles.css.d.ts +75 -0
  41. package/dist/components/Listbox/styles.css.d.ts.map +1 -0
  42. package/dist/components/Menu/Menu.d.ts +18 -5
  43. package/dist/components/Menu/Menu.d.ts.map +1 -1
  44. package/dist/components/Menu/Menu.meta.d.ts.map +1 -1
  45. package/dist/components/Menu/Menu.stories.d.ts +7 -0
  46. package/dist/components/Menu/Menu.stories.d.ts.map +1 -1
  47. package/dist/components/Menu/MenuContext.d.ts +3 -0
  48. package/dist/components/Menu/MenuContext.d.ts.map +1 -1
  49. package/dist/components/Menu/styles.css.d.ts +24 -15
  50. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  51. package/dist/components/Menu/useMenuRoot.d.ts +14 -1
  52. package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
  53. package/dist/components/TextLink/TextLink.d.ts +24 -0
  54. package/dist/components/TextLink/TextLink.d.ts.map +1 -0
  55. package/dist/components/TextLink/TextLink.meta.d.ts +3 -0
  56. package/dist/components/TextLink/TextLink.meta.d.ts.map +1 -0
  57. package/dist/components/TextLink/TextLink.stories.d.ts +9 -0
  58. package/dist/components/TextLink/TextLink.stories.d.ts.map +1 -0
  59. package/dist/components/TextLink/styles.css.d.ts +28 -0
  60. package/dist/components/TextLink/styles.css.d.ts.map +1 -0
  61. package/dist/components/ThemeProvider/ThemeProvider.stories.d.ts +1 -0
  62. package/dist/components/ThemeProvider/ThemeProvider.stories.d.ts.map +1 -1
  63. package/dist/components/VisuallyHidden/VisuallyHidden.d.ts +14 -0
  64. package/dist/components/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
  65. package/dist/components/VisuallyHidden/VisuallyHidden.meta.d.ts +3 -0
  66. package/dist/components/VisuallyHidden/VisuallyHidden.meta.d.ts.map +1 -0
  67. package/dist/components/VisuallyHidden/VisuallyHidden.stories.d.ts +7 -0
  68. package/dist/components/VisuallyHidden/VisuallyHidden.stories.d.ts.map +1 -0
  69. package/dist/docs/FlexGrow.stories.d.ts +7 -0
  70. package/dist/docs/FlexGrow.stories.d.ts.map +1 -0
  71. package/dist/index.css +1 -1
  72. package/dist/index.d.ts +5 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +2943 -2778
  75. package/dist/index.js.map +1 -1
  76. package/dist/index.native.d.ts +1 -0
  77. package/dist/index.native.d.ts.map +1 -1
  78. package/dist/styles/visually-hidden.css.d.ts +6 -0
  79. package/dist/styles/visually-hidden.css.d.ts.map +1 -1
  80. package/dist/table.js +1 -1
  81. package/package.json +2 -2
  82. package/src/components/Button/Button.stories.tsx +41 -0
  83. package/src/components/Button/Button.tsx +16 -1
  84. package/src/components/Combobox/Combobox.tsx +25 -79
  85. package/src/components/Combobox/styles.css.ts +0 -74
  86. package/src/components/Dialog/Dialog.meta.ts +1 -1
  87. package/src/components/Dialog/Dialog.stories.tsx +109 -3
  88. package/src/components/Dialog/Dialog.tsx +63 -9
  89. package/src/components/Dialog/styles.css.ts +2 -0
  90. package/src/components/DragDrop/styles.css.ts +16 -3
  91. package/src/components/Grid/Grid.meta.ts +1 -1
  92. package/src/components/Grid/Grid.stories.tsx +53 -1
  93. package/src/components/Grid/Grid.tsx +23 -11
  94. package/src/components/Input/Input.meta.ts +10 -3
  95. package/src/components/Input/Input.stories.tsx +52 -2
  96. package/src/components/Input/Input.tsx +12 -3
  97. package/src/components/Input/styles.css.ts +29 -0
  98. package/src/components/InputGroup/InputGroup.stories.tsx +20 -0
  99. package/src/components/InputGroup/InputGroup.tsx +20 -2
  100. package/src/components/Listbox/Listbox.meta.ts +52 -0
  101. package/src/components/Listbox/Listbox.stories.tsx +170 -0
  102. package/src/components/Listbox/Listbox.tsx +278 -0
  103. package/src/components/Listbox/styles.css.ts +89 -0
  104. package/src/components/Menu/Menu.meta.ts +2 -1
  105. package/src/components/Menu/Menu.stories.tsx +289 -8
  106. package/src/components/Menu/Menu.tsx +100 -29
  107. package/src/components/Menu/MenuContext.ts +3 -0
  108. package/src/components/Menu/styles.css.ts +31 -15
  109. package/src/components/Menu/useMenuRoot.ts +105 -48
  110. package/src/components/TextLink/TextLink.meta.ts +33 -0
  111. package/src/components/TextLink/TextLink.stories.tsx +63 -0
  112. package/src/components/TextLink/TextLink.tsx +46 -0
  113. package/src/components/TextLink/styles.css.ts +58 -0
  114. package/src/components/ThemeProvider/ThemeProvider.stories.tsx +50 -0
  115. package/src/components/VisuallyHidden/VisuallyHidden.meta.ts +19 -0
  116. package/src/components/VisuallyHidden/VisuallyHidden.stories.tsx +30 -0
  117. package/src/components/VisuallyHidden/VisuallyHidden.tsx +22 -0
  118. package/src/docs/FlexGrow.stories.tsx +83 -0
  119. package/src/index.native.ts +1 -0
  120. package/src/index.ts +5 -0
  121. package/src/styles/visually-hidden.css.ts +8 -0
  122. package/dist/Table-BkrgH0jb.js +0 -1199
  123. package/dist/Table-BkrgH0jb.js.map +0 -1
@@ -0,0 +1,278 @@
1
+ import {
2
+ type ReactNode,
3
+ type Ref,
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useLayoutEffect,
9
+ useRef,
10
+ useState,
11
+ } from 'react'
12
+ import {createPortal} from 'react-dom'
13
+ import {html} from 'react-strict-dom'
14
+ import {SPACING_PX} from '@duro-app/tokens/keys'
15
+ import {styles} from './styles.css'
16
+ import {usePortalMount} from '../ThemeProvider/ThemeProvider'
17
+ import {computePopoverPosition} from '../Popover/position'
18
+ import {usePopoverLayer} from '../Popover/PopoverLayerContext'
19
+
20
+ /**
21
+ * A headless-state popup list of options for an input that keeps focus: a
22
+ * Combobox's text field, or a rich-text editor's typeahead (mentions, slash
23
+ * commands). The anchor element owns focus and the keyboard; it moves the
24
+ * highlight by setting `highlightedId`, and wires itself to the list with
25
+ * `getAnchorProps()` (aria-controls / aria-expanded / aria-activedescendant).
26
+ * A press on an option never takes focus from the anchor.
27
+ */
28
+
29
+ export type ListboxMaxHeight = 'listMaxH' | 'listMaxHSm'
30
+
31
+ const maxHeightMap = {
32
+ listMaxH: styles.maxHeightListMaxH,
33
+ listMaxHSm: styles.maxHeightListMaxHSm,
34
+ } as const
35
+
36
+ /** Default option id: the listbox id plus the value, made safe for an id. */
37
+ export function defaultOptionId(listboxId: string, value: string): string {
38
+ return `${listboxId}-option-${value.replace(/[^\w-]/g, (ch) => `_${ch.charCodeAt(0).toString(16)}`)}`
39
+ }
40
+
41
+ export interface ListboxAnchorProps {
42
+ role?: 'combobox'
43
+ 'aria-controls': string | undefined
44
+ 'aria-expanded': boolean
45
+ 'aria-activedescendant': string | undefined
46
+ 'aria-autocomplete': 'list'
47
+ 'aria-haspopup': 'listbox'
48
+ }
49
+
50
+ /**
51
+ * ARIA for the element that keeps focus and drives the list. Spread it on an
52
+ * input, or set each attribute on a contenteditable root.
53
+ */
54
+ export function getAnchorProps({
55
+ id,
56
+ open,
57
+ highlightedId,
58
+ }: {
59
+ id: string
60
+ open: boolean
61
+ highlightedId: string | null | undefined
62
+ }): ListboxAnchorProps {
63
+ return {
64
+ 'aria-controls': open ? id : undefined,
65
+ 'aria-expanded': open,
66
+ 'aria-activedescendant': open && highlightedId ? highlightedId : undefined,
67
+ 'aria-autocomplete': 'list',
68
+ 'aria-haspopup': 'listbox',
69
+ }
70
+ }
71
+
72
+ interface ListboxContextValue {
73
+ id: string
74
+ highlightedId: string | null
75
+ onHighlight?: (id: string | null) => void
76
+ onSelect?: (value: string) => void
77
+ getOptionId: (value: string) => string
78
+ }
79
+
80
+ const ListboxContext = createContext<ListboxContextValue | null>(null)
81
+
82
+ function useListbox(part: string) {
83
+ const ctx = useContext(ListboxContext)
84
+ if (!ctx) throw new Error(`Listbox.${part} must be used within Listbox.Root`)
85
+ return ctx
86
+ }
87
+
88
+ // --- Root ---
89
+
90
+ interface RootProps {
91
+ /** DOM id of the list; pass the same to getAnchorProps. */
92
+ id: string
93
+ open: boolean
94
+ /** The rect the list is placed against: the input, or the editor's caret. */
95
+ anchor: () => DOMRect | null | undefined
96
+ /** Give the list the anchor's width (a Combobox under its input). */
97
+ matchAnchorWidth?: boolean
98
+ /** Id of the highlighted option, controlled by the anchor's keyboard handling. */
99
+ highlightedId: string | null
100
+ /** Pointer hover asks to move the highlight. */
101
+ onHighlight?: (id: string | null) => void
102
+ /** A press on an option. */
103
+ onSelect?: (value: string) => void
104
+ /** Option value → DOM id. Default: defaultOptionId(id, value). */
105
+ getOptionId?: (value: string) => string
106
+ /** Accessible name of the list. */
107
+ 'aria-label'?: string
108
+ /** Size-token key capping the list's height; longer lists scroll. Default: listMaxH. */
109
+ maxHeight?: ListboxMaxHeight
110
+ children: ReactNode
111
+ }
112
+
113
+ function Root({
114
+ id,
115
+ open,
116
+ anchor,
117
+ matchAnchorWidth = false,
118
+ highlightedId,
119
+ onHighlight,
120
+ onSelect,
121
+ getOptionId,
122
+ 'aria-label': ariaLabel,
123
+ maxHeight = 'listMaxH',
124
+ children,
125
+ }: RootProps) {
126
+ const mount = usePortalMount()
127
+ const listRef = useRef<HTMLDivElement>(null)
128
+ const [place, setPlace] = useState<{top: number; left: number; width: number} | null>(null)
129
+ const anchorRef = useRef(anchor)
130
+ anchorRef.current = anchor
131
+
132
+ useLayoutEffect(() => {
133
+ if (!open) {
134
+ setPlace(null)
135
+ return
136
+ }
137
+ const update = () => {
138
+ const rect = anchorRef.current()
139
+ if (!rect) return
140
+ const list = listRef.current
141
+ const next = computePopoverPosition({
142
+ anchor: {top: rect.top, left: rect.left, width: rect.width, height: rect.height},
143
+ popup: {
144
+ width: matchAnchorWidth ? rect.width : (list?.offsetWidth ?? 0),
145
+ height: list?.offsetHeight ?? 0,
146
+ },
147
+ viewport: {width: window.innerWidth, height: window.innerHeight},
148
+ side: 'bottom',
149
+ align: 'start',
150
+ offset: SPACING_PX.xs,
151
+ })
152
+ setPlace((prev) =>
153
+ prev && prev.top === next.top && prev.left === next.left && prev.width === rect.width
154
+ ? prev
155
+ : {top: next.top, left: next.left, width: rect.width},
156
+ )
157
+ }
158
+ update()
159
+ // A second pass once the list has its size, so the flip sees its height.
160
+ const frame = requestAnimationFrame(update)
161
+ window.addEventListener('resize', update)
162
+ window.addEventListener('scroll', update, true)
163
+ return () => {
164
+ cancelAnimationFrame(frame)
165
+ window.removeEventListener('resize', update)
166
+ window.removeEventListener('scroll', update, true)
167
+ }
168
+ }, [open, matchAnchorWidth])
169
+
170
+ // A press inside the list must not move focus off the anchor.
171
+ useEffect(() => {
172
+ const list = listRef.current
173
+ if (!list || !open) return
174
+ const keep = (e: MouseEvent) => e.preventDefault()
175
+ list.addEventListener('mousedown', keep)
176
+ return () => list.removeEventListener('mousedown', keep)
177
+ }, [open, place])
178
+
179
+ // Keep the highlighted option in view.
180
+ useEffect(() => {
181
+ if (!open || !highlightedId) return
182
+ const list = listRef.current
183
+ const option = list?.querySelector<HTMLElement>(`[id="${CSS.escape(highlightedId)}"]`)
184
+ option?.scrollIntoView?.({block: 'nearest'})
185
+ }, [open, highlightedId])
186
+
187
+ usePopoverLayer(listRef, {active: open && place !== null})
188
+
189
+ const optionId = useCallback(
190
+ (value: string) => (getOptionId ? getOptionId(value) : defaultOptionId(id, value)),
191
+ [getOptionId, id],
192
+ )
193
+
194
+ if (!open || !place || typeof document === 'undefined') return null
195
+
196
+ const node = (
197
+ <ListboxContext.Provider
198
+ value={{id, highlightedId, onHighlight, onSelect, getOptionId: optionId}}
199
+ >
200
+ <html.div
201
+ ref={listRef}
202
+ id={id}
203
+ role="listbox"
204
+ aria-label={ariaLabel}
205
+ style={[
206
+ styles.popup,
207
+ maxHeightMap[maxHeight],
208
+ styles.position(place.top, place.left),
209
+ matchAnchorWidth ? styles.width(place.width) : styles.ownWidth,
210
+ ]}
211
+ >
212
+ {children}
213
+ </html.div>
214
+ </ListboxContext.Provider>
215
+ )
216
+ return mount ? createPortal(node, mount) : node
217
+ }
218
+
219
+ // --- Option ---
220
+
221
+ interface OptionProps {
222
+ value: string
223
+ /** DOM id. Default: Root's getOptionId(value). */
224
+ id?: string
225
+ /** The committed choice (aria-selected). */
226
+ selected?: boolean
227
+ disabled?: boolean
228
+ children: ReactNode
229
+ ref?: Ref<HTMLDivElement>
230
+ }
231
+
232
+ function Option({
233
+ value,
234
+ id: idProp,
235
+ selected = false,
236
+ disabled = false,
237
+ children,
238
+ ref,
239
+ }: OptionProps) {
240
+ const {highlightedId, onHighlight, onSelect, getOptionId} = useListbox('Option')
241
+ const id = idProp ?? getOptionId(value)
242
+ const highlighted = highlightedId === id
243
+
244
+ return (
245
+ <html.div
246
+ ref={ref}
247
+ id={id}
248
+ role="option"
249
+ aria-selected={selected}
250
+ aria-disabled={disabled || undefined}
251
+ onClick={disabled ? undefined : () => onSelect?.(value)}
252
+ onPointerEnter={disabled ? undefined : () => onHighlight?.(id)}
253
+ style={[
254
+ styles.option,
255
+ selected && styles.optionSelected,
256
+ highlighted && styles.optionHighlighted,
257
+ disabled && styles.optionDisabled,
258
+ ]}
259
+ >
260
+ {children}
261
+ </html.div>
262
+ )
263
+ }
264
+
265
+ // --- Empty ---
266
+
267
+ /** Shown in place of options when nothing matches. */
268
+ function Empty({children}: {children: ReactNode}) {
269
+ useListbox('Empty')
270
+ return <html.div style={styles.empty}>{children}</html.div>
271
+ }
272
+
273
+ export const Listbox = {
274
+ Root,
275
+ Option,
276
+ Empty,
277
+ getAnchorProps,
278
+ }
@@ -0,0 +1,89 @@
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 {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
+ import {duration} from '@duro-app/tokens/tokens/motion.css'
7
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
8
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
9
+
10
+ export const styles = css.create({
11
+ popup: {
12
+ // Fixed, with top/left (and width) applied inline from the anchor's rect,
13
+ // so the list escapes any ancestor with overflow: hidden or a transform.
14
+ position: 'fixed',
15
+ top: 0,
16
+ left: 0,
17
+ backgroundColor: colors.bgCard,
18
+ borderWidth: borders.hairline,
19
+ borderStyle: 'solid',
20
+ borderColor: colors.border,
21
+ borderRadius: radii.sm,
22
+ boxShadow: shadows.md,
23
+ paddingTop: spacing.xs,
24
+ paddingBottom: spacing.xs,
25
+ overflowY: 'auto',
26
+ // Above Dialog/Drawer (1000/1001): they portal into the same mount.
27
+ zIndex: 1050,
28
+ // The portal mount is pointer-events: none; the list takes them back.
29
+ pointerEvents: 'auto',
30
+ },
31
+ // A list that does not take the anchor's width (an editor typeahead).
32
+ ownWidth: {
33
+ minWidth: sizes.popupMinW,
34
+ },
35
+ maxHeightListMaxH: {
36
+ maxHeight: sizes.listMaxH,
37
+ },
38
+ maxHeightListMaxHSm: {
39
+ maxHeight: sizes.listMaxHSm,
40
+ },
41
+ position: (top: number, left: number) => ({
42
+ top,
43
+ left,
44
+ }),
45
+ width: (width: number) => ({
46
+ width,
47
+ }),
48
+ option: {
49
+ display: 'flex',
50
+ alignItems: 'center',
51
+ paddingTop: spacing.sm,
52
+ paddingBottom: spacing.sm,
53
+ paddingLeft: spacing.md,
54
+ paddingRight: spacing.md,
55
+ fontSize: typography.fontSizeSm,
56
+ fontFamily: typography.fontFamily,
57
+ color: colors.text,
58
+ cursor: 'pointer',
59
+ backgroundColor: 'transparent',
60
+ transitionProperty: 'background-color',
61
+ transitionDuration: duration.fast,
62
+ },
63
+ optionSelected: {
64
+ color: colors.accent,
65
+ fontWeight: typography.fontWeightMedium,
66
+ },
67
+ // The fill is the highlight; the transparent outline is what forced-colors
68
+ // mode paints, where fills are dropped.
69
+ optionHighlighted: {
70
+ backgroundColor: colors.bgCardHover,
71
+ outlineStyle: 'solid',
72
+ outlineWidth: borders.focusRing,
73
+ outlineColor: 'transparent',
74
+ outlineOffset: `calc(-1 * ${borders.focusRing})`,
75
+ },
76
+ optionDisabled: {
77
+ color: colors.textMuted,
78
+ cursor: 'default',
79
+ },
80
+ empty: {
81
+ paddingTop: spacing.sm,
82
+ paddingBottom: spacing.sm,
83
+ paddingLeft: spacing.md,
84
+ paddingRight: spacing.md,
85
+ fontSize: typography.fontSizeSm,
86
+ fontFamily: typography.fontFamily,
87
+ color: colors.textMuted,
88
+ },
89
+ })
@@ -2,11 +2,12 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button.',
5
+ 'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button; an icon-only trigger takes aria-label, and variant="ghost" suits toolbars. The popup is portalled and placed against the trigger (never clipped by a sticky or overflow ancestor), capped at sizes.listMaxH with scrolling, and takes focus on open (WAI-ARIA menu button); Escape closes only the menu, not an enclosing Popover or Dialog.',
6
6
  whenToUse: [
7
7
  'Context menu or "more actions" dropdown',
8
8
  'Navigation links in a dropdown',
9
9
  'Overflow menu for toolbar actions',
10
+ 'A toolbar dropdown (align, insert) in an editor — Menu.Trigger variant="ghost"',
10
11
  ],
11
12
  whenNotToUse: [
12
13
  'Picking a value — use Select',