@duro-app/ui 3.5.0 → 3.7.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.
- package/dist/{Table-P-UHkEqJ.js → Table-Cgr5VDEZ.js} +430 -417
- package/dist/Table-Cgr5VDEZ.js.map +1 -0
- package/dist/components/Input/Input.d.ts +6 -1
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/Input/Input.stories.d.ts +1 -0
- package/dist/components/Input/Input.stories.d.ts.map +1 -1
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/Menu.meta.d.ts.map +1 -1
- package/dist/components/Menu/Menu.stories.d.ts +1 -0
- package/dist/components/Menu/Menu.stories.d.ts.map +1 -1
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Textarea/Textarea.d.ts +6 -1
- package/dist/components/Textarea/Textarea.d.ts.map +1 -1
- package/dist/components/Textarea/Textarea.stories.d.ts +1 -0
- package/dist/components/Textarea/Textarea.stories.d.ts.map +1 -1
- package/dist/components/Tree/Tree.d.ts +35 -0
- package/dist/components/Tree/Tree.d.ts.map +1 -0
- package/dist/components/Tree/Tree.meta.d.ts +3 -0
- package/dist/components/Tree/Tree.meta.d.ts.map +1 -0
- package/dist/components/Tree/Tree.stories.d.ts +9 -0
- package/dist/components/Tree/Tree.stories.d.ts.map +1 -0
- package/dist/components/Tree/TreeContext.d.ts +18 -0
- package/dist/components/Tree/TreeContext.d.ts.map +1 -0
- package/dist/components/Tree/styles.css.d.ts +84 -0
- package/dist/components/Tree/styles.css.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2742 -2439
- package/dist/index.js.map +1 -1
- package/dist/shared/devWarnOnce.d.ts +3 -0
- package/dist/shared/devWarnOnce.d.ts.map +1 -0
- package/dist/table.js +1 -1
- package/package.json +2 -2
- package/src/components/Input/Input.stories.tsx +16 -0
- package/src/components/Input/Input.tsx +11 -0
- package/src/components/Menu/Menu.meta.ts +8 -5
- package/src/components/Menu/Menu.stories.tsx +27 -1
- package/src/components/Menu/Menu.tsx +17 -0
- package/src/components/Table/Table.tsx +2 -13
- package/src/components/Textarea/Textarea.stories.tsx +16 -0
- package/src/components/Textarea/Textarea.tsx +11 -0
- package/src/components/Tree/Tree.meta.ts +41 -0
- package/src/components/Tree/Tree.stories.tsx +151 -0
- package/src/components/Tree/Tree.tsx +304 -0
- package/src/components/Tree/TreeContext.ts +26 -0
- package/src/components/Tree/styles.css.ts +96 -0
- package/src/index.ts +1 -0
- package/src/shared/devWarnOnce.ts +15 -0
- package/dist/Table-P-UHkEqJ.js.map +0 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import {html} from 'react-strict-dom'
|
|
13
|
+
import {styles} from './styles.css'
|
|
14
|
+
import {TreeContext, TreeLevelContext, useTree} from './TreeContext'
|
|
15
|
+
|
|
16
|
+
/* Tree — a hierarchy of items (the WAI-ARIA tree pattern): single selection,
|
|
17
|
+
* expandable branches, one tab stop (roving tabindex), and the keyboard of a
|
|
18
|
+
* file explorer — ↑/↓ move between visible items, → opens a branch then goes
|
|
19
|
+
* to its first child, ← closes it then goes to the parent, Home/End, Enter
|
|
20
|
+
* or Space select, and typing jumps to the next item whose label starts with
|
|
21
|
+
* the typed letters. */
|
|
22
|
+
|
|
23
|
+
// --- Root ---
|
|
24
|
+
|
|
25
|
+
interface RootProps {
|
|
26
|
+
children: ReactNode
|
|
27
|
+
/** accessible name of the tree (or give `aria-labelledby`) */
|
|
28
|
+
'aria-label'?: string
|
|
29
|
+
'aria-labelledby'?: string
|
|
30
|
+
/** the selected item (controlled) */
|
|
31
|
+
value?: string | null
|
|
32
|
+
defaultValue?: string | null
|
|
33
|
+
onValueChange?: (value: string) => void
|
|
34
|
+
/** the open branches (controlled) */
|
|
35
|
+
expanded?: ReadonlyArray<string>
|
|
36
|
+
defaultExpanded?: ReadonlyArray<string>
|
|
37
|
+
onExpandedChange?: (expanded: string[]) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TYPEAHEAD_RESET_MS = 500
|
|
41
|
+
|
|
42
|
+
function Root({
|
|
43
|
+
children,
|
|
44
|
+
'aria-label': ariaLabel,
|
|
45
|
+
'aria-labelledby': ariaLabelledBy,
|
|
46
|
+
value,
|
|
47
|
+
defaultValue = null,
|
|
48
|
+
onValueChange,
|
|
49
|
+
expanded,
|
|
50
|
+
defaultExpanded = [],
|
|
51
|
+
onExpandedChange,
|
|
52
|
+
}: RootProps) {
|
|
53
|
+
const [selectedInner, setSelectedInner] = useState<string | null>(defaultValue)
|
|
54
|
+
const selectedValue = value !== undefined ? value : selectedInner
|
|
55
|
+
const [expandedInner, setExpandedInner] = useState<ReadonlyArray<string>>(defaultExpanded)
|
|
56
|
+
const open = expanded ?? expandedInner
|
|
57
|
+
const [focusValue, setFocusValue] = useState<string | null>(selectedValue)
|
|
58
|
+
// the ring shows on the focused ROW (the item wraps its children, so a
|
|
59
|
+
// ring on it would circle the whole branch), only for keyboard focus —
|
|
60
|
+
// the tree's own :focus-visible
|
|
61
|
+
const [keyboardFocus, setKeyboardFocus] = useState(false)
|
|
62
|
+
const rootRef = useRef<HTMLUListElement>(null)
|
|
63
|
+
|
|
64
|
+
const onSelect = useCallback(
|
|
65
|
+
(v: string) => {
|
|
66
|
+
if (value === undefined) setSelectedInner(v)
|
|
67
|
+
onValueChange?.(v)
|
|
68
|
+
},
|
|
69
|
+
[value, onValueChange],
|
|
70
|
+
)
|
|
71
|
+
const setExpanded = useCallback(
|
|
72
|
+
(v: string, isOpen: boolean) => {
|
|
73
|
+
const next = isOpen ? [...new Set([...open, v])] : open.filter((x) => x !== v)
|
|
74
|
+
if (next.length === open.length && next.every((x, i) => x === open[i])) return
|
|
75
|
+
if (expanded === undefined) setExpandedInner(next)
|
|
76
|
+
onExpandedChange?.(next)
|
|
77
|
+
},
|
|
78
|
+
[open, expanded, onExpandedChange],
|
|
79
|
+
)
|
|
80
|
+
const isExpanded = useCallback((v: string) => open.includes(v), [open])
|
|
81
|
+
const toggle = useCallback((v: string) => setExpanded(v, !open.includes(v)), [open, setExpanded])
|
|
82
|
+
|
|
83
|
+
// Exactly one item is tabbable: the focus holder, else the selection, else
|
|
84
|
+
// the first item — re-checked when an item appears, closes or goes away.
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
const root = rootRef.current
|
|
87
|
+
if (!root) return
|
|
88
|
+
const tabbable = root.querySelector('[role="treeitem"][tabindex="0"]')
|
|
89
|
+
if (tabbable) return
|
|
90
|
+
const first = root.querySelector<HTMLElement>('[role="treeitem"]')
|
|
91
|
+
const next = first?.dataset.treeValue ?? null
|
|
92
|
+
if (next !== focusValue) setFocusValue(next)
|
|
93
|
+
}, [focusValue, open, children])
|
|
94
|
+
|
|
95
|
+
// Keyboard: DOM order over the rendered items IS the visible order (a
|
|
96
|
+
// closed branch renders no children), so navigation reads it directly.
|
|
97
|
+
const typeahead = useRef({text: '', at: 0})
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const root = rootRef.current
|
|
100
|
+
if (!root) return
|
|
101
|
+
const rootEl = root
|
|
102
|
+
const items = () => Array.from(rootEl.querySelectorAll<HTMLElement>('[role="treeitem"]'))
|
|
103
|
+
const focusItem = (el: HTMLElement | null | undefined) => {
|
|
104
|
+
if (!el) return
|
|
105
|
+
el.focus()
|
|
106
|
+
}
|
|
107
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
108
|
+
const current = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
|
|
109
|
+
if (!current || !rootEl.contains(current)) return
|
|
110
|
+
const list = items()
|
|
111
|
+
const i = list.indexOf(current)
|
|
112
|
+
const val = current.dataset.treeValue ?? ''
|
|
113
|
+
const isOpen = current.getAttribute('aria-expanded')
|
|
114
|
+
switch (e.key) {
|
|
115
|
+
case 'ArrowDown':
|
|
116
|
+
e.preventDefault()
|
|
117
|
+
focusItem(list[i + 1])
|
|
118
|
+
return
|
|
119
|
+
case 'ArrowUp':
|
|
120
|
+
e.preventDefault()
|
|
121
|
+
focusItem(list[i - 1])
|
|
122
|
+
return
|
|
123
|
+
case 'ArrowRight':
|
|
124
|
+
e.preventDefault()
|
|
125
|
+
if (isOpen === 'false') setExpanded(val, true)
|
|
126
|
+
else if (isOpen === 'true') focusItem(list[i + 1])
|
|
127
|
+
return
|
|
128
|
+
case 'ArrowLeft': {
|
|
129
|
+
e.preventDefault()
|
|
130
|
+
if (isOpen === 'true') {
|
|
131
|
+
setExpanded(val, false)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
const parent = current.parentElement?.closest<HTMLElement>('[role="treeitem"]')
|
|
135
|
+
if (parent && rootEl.contains(parent)) focusItem(parent)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
case 'Home':
|
|
139
|
+
e.preventDefault()
|
|
140
|
+
focusItem(list[0])
|
|
141
|
+
return
|
|
142
|
+
case 'End':
|
|
143
|
+
e.preventDefault()
|
|
144
|
+
focusItem(list[list.length - 1])
|
|
145
|
+
return
|
|
146
|
+
case 'Enter':
|
|
147
|
+
case ' ':
|
|
148
|
+
e.preventDefault()
|
|
149
|
+
onSelect(val)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
// typeahead: a printable character, no modifier
|
|
153
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
154
|
+
const now = Date.now()
|
|
155
|
+
const t = typeahead.current
|
|
156
|
+
t.text = now - t.at > TYPEAHEAD_RESET_MS ? e.key : t.text + e.key
|
|
157
|
+
t.at = now
|
|
158
|
+
// the same letter typed again cycles through the items it starts;
|
|
159
|
+
// a real prefix ("fi") narrows, and may match the current item
|
|
160
|
+
const typed = t.text.toLocaleLowerCase()
|
|
161
|
+
const repeat = [...typed].every((c) => c === typed[0])
|
|
162
|
+
const needle = repeat ? typed[0] : typed
|
|
163
|
+
const start = repeat ? i + 1 : i
|
|
164
|
+
for (let k = 0; k < list.length; k++) {
|
|
165
|
+
const el = list[(start + k) % list.length]
|
|
166
|
+
if ((el.dataset.treeText ?? '').toLocaleLowerCase().startsWith(needle)) {
|
|
167
|
+
focusItem(el)
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// the roving tab stop follows focus, however it moved (keys or pointer)
|
|
174
|
+
function onFocusIn(e: FocusEvent) {
|
|
175
|
+
const item = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
|
|
176
|
+
const v = item?.dataset.treeValue
|
|
177
|
+
if (v) setFocusValue(v)
|
|
178
|
+
setKeyboardFocus(item?.matches(':focus-visible') ?? false)
|
|
179
|
+
}
|
|
180
|
+
function onFocusOut(e: FocusEvent) {
|
|
181
|
+
if (!rootEl.contains(e.relatedTarget as Node | null)) setKeyboardFocus(false)
|
|
182
|
+
}
|
|
183
|
+
const onPointerDown = () => setKeyboardFocus(false)
|
|
184
|
+
const onKey = () => setKeyboardFocus(true)
|
|
185
|
+
root.addEventListener('keydown', onKeyDown)
|
|
186
|
+
root.addEventListener('keydown', onKey)
|
|
187
|
+
root.addEventListener('focusin', onFocusIn)
|
|
188
|
+
root.addEventListener('focusout', onFocusOut)
|
|
189
|
+
root.addEventListener('pointerdown', onPointerDown)
|
|
190
|
+
return () => {
|
|
191
|
+
root.removeEventListener('keydown', onKeyDown)
|
|
192
|
+
root.removeEventListener('keydown', onKey)
|
|
193
|
+
root.removeEventListener('focusin', onFocusIn)
|
|
194
|
+
root.removeEventListener('focusout', onFocusOut)
|
|
195
|
+
root.removeEventListener('pointerdown', onPointerDown)
|
|
196
|
+
}
|
|
197
|
+
}, [onSelect, setExpanded])
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<TreeContext.Provider
|
|
201
|
+
value={{
|
|
202
|
+
selectedValue,
|
|
203
|
+
onSelect,
|
|
204
|
+
isExpanded,
|
|
205
|
+
toggle,
|
|
206
|
+
setExpanded,
|
|
207
|
+
focusValue,
|
|
208
|
+
setFocusValue,
|
|
209
|
+
keyboardFocus,
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<TreeLevelContext.Provider value={1}>
|
|
213
|
+
<html.ul
|
|
214
|
+
ref={rootRef}
|
|
215
|
+
role="tree"
|
|
216
|
+
aria-label={ariaLabel}
|
|
217
|
+
aria-labelledby={ariaLabelledBy}
|
|
218
|
+
style={styles.root}
|
|
219
|
+
>
|
|
220
|
+
{children}
|
|
221
|
+
</html.ul>
|
|
222
|
+
</TreeLevelContext.Provider>
|
|
223
|
+
</TreeContext.Provider>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- Item ---
|
|
228
|
+
|
|
229
|
+
interface ItemProps {
|
|
230
|
+
/** unique within the tree */
|
|
231
|
+
value: string
|
|
232
|
+
/** what the row shows */
|
|
233
|
+
label: ReactNode
|
|
234
|
+
/** plain text for typeahead when `label` isn't a string */
|
|
235
|
+
textValue?: string
|
|
236
|
+
/** nested items: the branch's children (none = a leaf) */
|
|
237
|
+
children?: ReactNode
|
|
238
|
+
/** trailing content on the row (a count, a status) */
|
|
239
|
+
meta?: ReactNode
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function Item({value, label, textValue, children, meta}: ItemProps) {
|
|
243
|
+
const {selectedValue, onSelect, isExpanded, toggle, focusValue, keyboardFocus} = useTree()
|
|
244
|
+
const level = useContext(TreeLevelContext)
|
|
245
|
+
const hasChildren = Children.count(children) > 0
|
|
246
|
+
const open = hasChildren && isExpanded(value)
|
|
247
|
+
const selected = selectedValue === value
|
|
248
|
+
const text = textValue ?? (typeof label === 'string' ? label : '')
|
|
249
|
+
// named by its own label only: a branch's content includes its children,
|
|
250
|
+
// and a name computed from content would read the whole subtree
|
|
251
|
+
const labelId = useId()
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<html.li
|
|
255
|
+
role="treeitem"
|
|
256
|
+
aria-labelledby={labelId}
|
|
257
|
+
aria-level={level}
|
|
258
|
+
aria-expanded={hasChildren ? open : undefined}
|
|
259
|
+
aria-selected={selected}
|
|
260
|
+
tabIndex={focusValue === value ? 0 : -1}
|
|
261
|
+
data-tree-value={value}
|
|
262
|
+
data-tree-text={text}
|
|
263
|
+
style={styles.item}
|
|
264
|
+
>
|
|
265
|
+
<html.div
|
|
266
|
+
onClick={() => onSelect(value)}
|
|
267
|
+
style={[
|
|
268
|
+
styles.row,
|
|
269
|
+
styles.indent(level),
|
|
270
|
+
selected && styles.rowSelected,
|
|
271
|
+
keyboardFocus && focusValue === value && styles.rowFocused,
|
|
272
|
+
]}
|
|
273
|
+
>
|
|
274
|
+
<html.span
|
|
275
|
+
aria-hidden
|
|
276
|
+
onClick={(e: {stopPropagation: () => void}) => {
|
|
277
|
+
if (!hasChildren) return
|
|
278
|
+
e.stopPropagation()
|
|
279
|
+
toggle(value)
|
|
280
|
+
}}
|
|
281
|
+
style={[styles.chevron, open && styles.chevronOpen, !hasChildren && styles.chevronLeaf]}
|
|
282
|
+
>
|
|
283
|
+
▸
|
|
284
|
+
</html.span>
|
|
285
|
+
<html.span id={labelId} style={styles.label}>
|
|
286
|
+
{label}
|
|
287
|
+
</html.span>
|
|
288
|
+
{meta != null ? <html.span style={styles.meta}>{meta}</html.span> : null}
|
|
289
|
+
</html.div>
|
|
290
|
+
{open ? (
|
|
291
|
+
<TreeLevelContext.Provider value={level + 1}>
|
|
292
|
+
<html.ul role="group" style={styles.group}>
|
|
293
|
+
{children}
|
|
294
|
+
</html.ul>
|
|
295
|
+
</TreeLevelContext.Provider>
|
|
296
|
+
) : null}
|
|
297
|
+
</html.li>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export const Tree = {
|
|
302
|
+
Root,
|
|
303
|
+
Item,
|
|
304
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {createContext, useContext} from 'react'
|
|
2
|
+
|
|
3
|
+
export interface TreeContextValue {
|
|
4
|
+
/** the selected item's value (single selection), or null */
|
|
5
|
+
selectedValue: string | null
|
|
6
|
+
onSelect: (value: string) => void
|
|
7
|
+
isExpanded: (value: string) => boolean
|
|
8
|
+
toggle: (value: string) => void
|
|
9
|
+
setExpanded: (value: string, expanded: boolean) => void
|
|
10
|
+
/** the item holding the tree's single tab stop (roving tabindex) */
|
|
11
|
+
focusValue: string | null
|
|
12
|
+
setFocusValue: (value: string) => void
|
|
13
|
+
/** the focus holder is focused from the keyboard (draws its ring) */
|
|
14
|
+
keyboardFocus: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const TreeContext = createContext<TreeContextValue | null>(null)
|
|
18
|
+
|
|
19
|
+
export function useTree() {
|
|
20
|
+
const ctx = useContext(TreeContext)
|
|
21
|
+
if (!ctx) throw new Error('Tree compound components must be used within Tree.Root')
|
|
22
|
+
return ctx
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The nesting depth of the items rendered here (1 = top level). */
|
|
26
|
+
export const TreeLevelContext = createContext(1)
|
|
@@ -0,0 +1,96 @@
|
|
|
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, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
6
|
+
|
|
7
|
+
export const styles = css.create({
|
|
8
|
+
root: {
|
|
9
|
+
display: 'flex',
|
|
10
|
+
flexDirection: 'column',
|
|
11
|
+
margin: 0,
|
|
12
|
+
padding: 0,
|
|
13
|
+
listStyleType: 'none',
|
|
14
|
+
},
|
|
15
|
+
// The `role="group"` holding a branch's children: purely structural.
|
|
16
|
+
group: {
|
|
17
|
+
display: 'flex',
|
|
18
|
+
flexDirection: 'column',
|
|
19
|
+
margin: 0,
|
|
20
|
+
padding: 0,
|
|
21
|
+
listStyleType: 'none',
|
|
22
|
+
},
|
|
23
|
+
// The treeitem carries focus; its row draws the ring (the item also wraps
|
|
24
|
+
// its children, so a ring on it would circle the whole branch).
|
|
25
|
+
item: {
|
|
26
|
+
display: 'flex',
|
|
27
|
+
flexDirection: 'column',
|
|
28
|
+
outlineWidth: 0,
|
|
29
|
+
outlineStyle: 'none',
|
|
30
|
+
},
|
|
31
|
+
row: {
|
|
32
|
+
display: 'flex',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
gap: spacing.xs,
|
|
35
|
+
paddingTop: '5px',
|
|
36
|
+
paddingBottom: '5px',
|
|
37
|
+
paddingRight: spacing.sm,
|
|
38
|
+
fontFamily: typography.fontFamily,
|
|
39
|
+
fontSize: typography.fontSizeSm,
|
|
40
|
+
color: colors.text,
|
|
41
|
+
borderRadius: radii.sm,
|
|
42
|
+
cursor: 'pointer',
|
|
43
|
+
backgroundColor: {
|
|
44
|
+
default: 'transparent',
|
|
45
|
+
':hover': colors.bgCardHover,
|
|
46
|
+
},
|
|
47
|
+
transitionProperty: 'background-color',
|
|
48
|
+
transitionDuration: duration.fast,
|
|
49
|
+
transitionTimingFunction: easing.standard,
|
|
50
|
+
},
|
|
51
|
+
// one indent step per level below the top
|
|
52
|
+
indent: (level: number) => ({
|
|
53
|
+
paddingLeft: `calc(${spacing.sm} + ${Math.max(0, level - 1)} * ${spacing.lg})`,
|
|
54
|
+
}),
|
|
55
|
+
rowFocused: {
|
|
56
|
+
outlineWidth: 2,
|
|
57
|
+
outlineStyle: 'solid',
|
|
58
|
+
outlineColor: colors.accent,
|
|
59
|
+
outlineOffset: -2,
|
|
60
|
+
},
|
|
61
|
+
rowSelected: {
|
|
62
|
+
color: colors.accent,
|
|
63
|
+
fontWeight: typography.fontWeightMedium,
|
|
64
|
+
backgroundColor: colors.bgCardHover,
|
|
65
|
+
},
|
|
66
|
+
chevron: {
|
|
67
|
+
display: 'inline-flex',
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
justifyContent: 'center',
|
|
70
|
+
width: spacing.md,
|
|
71
|
+
flexShrink: 0,
|
|
72
|
+
color: colors.textMuted,
|
|
73
|
+
transitionProperty: 'transform',
|
|
74
|
+
transitionDuration: duration.fast,
|
|
75
|
+
transitionTimingFunction: easing.standard,
|
|
76
|
+
},
|
|
77
|
+
chevronOpen: {
|
|
78
|
+
transform: 'rotate(90deg)',
|
|
79
|
+
},
|
|
80
|
+
// a leaf keeps the chevron's width (labels align) but shows nothing
|
|
81
|
+
chevronLeaf: {
|
|
82
|
+
visibility: 'hidden',
|
|
83
|
+
},
|
|
84
|
+
label: {
|
|
85
|
+
flexGrow: 1,
|
|
86
|
+
minWidth: 0,
|
|
87
|
+
overflow: 'hidden',
|
|
88
|
+
textOverflow: 'ellipsis',
|
|
89
|
+
whiteSpace: 'nowrap',
|
|
90
|
+
},
|
|
91
|
+
meta: {
|
|
92
|
+
flexShrink: 0,
|
|
93
|
+
fontSize: typography.fontSizeXs,
|
|
94
|
+
color: colors.textMuted,
|
|
95
|
+
},
|
|
96
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -74,6 +74,7 @@ export {TagGroup} from './components/TagGroup/TagGroup'
|
|
|
74
74
|
// from there gives the same object with those attached.
|
|
75
75
|
export {Table, type TableVariant, type TableSize} from './components/Table/Table'
|
|
76
76
|
export {Tabs} from './components/Tabs/Tabs'
|
|
77
|
+
export {Tree} from './components/Tree/Tree'
|
|
77
78
|
export {Textarea, type TextareaVariant} from './components/Textarea/Textarea'
|
|
78
79
|
export {
|
|
79
80
|
ThemeProvider,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// --- Dev-only warn registry ---
|
|
2
|
+
//
|
|
3
|
+
// One-shot per warning code per process: long-running dev sessions
|
|
4
|
+
// shouldn't drown the console on every re-render of a misused component.
|
|
5
|
+
const IS_PROD = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production'
|
|
6
|
+
const warned = new Set<string>()
|
|
7
|
+
|
|
8
|
+
/** Warn once, outside production, as `[duro-app/ui <scope>] <message>`. */
|
|
9
|
+
export function devWarnOnce(scope: string, code: string, message: string): void {
|
|
10
|
+
const key = `${scope}:${code}`
|
|
11
|
+
if (IS_PROD || warned.has(key)) return
|
|
12
|
+
warned.add(key)
|
|
13
|
+
|
|
14
|
+
console.warn(`[duro-app/ui ${scope}] ${message}`)
|
|
15
|
+
}
|