@duro-app/ui 0.38.1 → 0.40.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/components/ActionBar/styles.css.d.ts +2 -2
- package/dist/components/ActionBar/styles.css.d.ts.map +1 -1
- package/dist/components/Button/styles.css.d.ts +4 -3
- package/dist/components/Button/styles.css.d.ts.map +1 -1
- package/dist/components/Card/styles.css.d.ts +2 -2
- package/dist/components/Card/styles.css.d.ts.map +1 -1
- package/dist/components/Checkbox/styles.css.d.ts +2 -2
- package/dist/components/Checkbox/styles.css.d.ts.map +1 -1
- package/dist/components/ColorMode/styles.css.d.ts +2 -2
- package/dist/components/ColorMode/styles.css.d.ts.map +1 -1
- package/dist/components/Combobox/styles.css.d.ts +2 -2
- package/dist/components/Combobox/styles.css.d.ts.map +1 -1
- package/dist/components/DetailPanel/styles.css.d.ts +11 -10
- package/dist/components/DetailPanel/styles.css.d.ts.map +1 -1
- package/dist/components/Dialog/styles.css.d.ts +5 -5
- package/dist/components/Dialog/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/styles.css.d.ts +13 -13
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Input/styles.css.d.ts +2 -2
- package/dist/components/Input/styles.css.d.ts.map +1 -1
- package/dist/components/InputGroup/styles.css.d.ts +4 -4
- package/dist/components/InputGroup/styles.css.d.ts.map +1 -1
- package/dist/components/LinkButton/styles.css.d.ts +2 -2
- package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
- package/dist/components/Menu/styles.css.d.ts +2 -2
- package/dist/components/Menu/styles.css.d.ts.map +1 -1
- package/dist/components/RadioGroup/styles.css.d.ts +2 -2
- package/dist/components/RadioGroup/styles.css.d.ts.map +1 -1
- package/dist/components/ScrollArea/styles.css.d.ts +3 -3
- package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
- package/dist/components/Select/styles.css.d.ts +2 -2
- package/dist/components/Select/styles.css.d.ts.map +1 -1
- package/dist/components/SideNav/styles.css.d.ts +6 -6
- package/dist/components/SideNav/styles.css.d.ts.map +1 -1
- package/dist/components/Switch/styles.css.d.ts +4 -4
- package/dist/components/Switch/styles.css.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +2 -2
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/components/Tabs/TabsContext.d.ts +2 -0
- package/dist/components/Tabs/TabsContext.d.ts.map +1 -1
- package/dist/components/Tabs/styles.css.d.ts +33 -2
- package/dist/components/Tabs/styles.css.d.ts.map +1 -1
- package/dist/components/Tabs/useTabsRoot.d.ts.map +1 -1
- package/dist/components/TagGroup/styles.css.d.ts +2 -2
- package/dist/components/TagGroup/styles.css.d.ts.map +1 -1
- package/dist/components/Textarea/styles.css.d.ts +2 -2
- package/dist/components/Textarea/styles.css.d.ts.map +1 -1
- package/dist/components/Toast/styles.css.d.ts +1 -1
- package/dist/components/Toast/styles.css.d.ts.map +1 -1
- package/dist/components/Toggle/styles.css.d.ts +2 -2
- package/dist/components/Toggle/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +2197 -2118
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar/styles.css.ts +3 -2
- package/src/components/Button/styles.css.ts +13 -3
- package/src/components/Card/styles.css.ts +9 -3
- package/src/components/Checkbox/styles.css.ts +3 -2
- package/src/components/ColorMode/styles.css.ts +3 -2
- package/src/components/Combobox/styles.css.ts +3 -2
- package/src/components/DetailPanel/styles.css.ts +15 -15
- package/src/components/Dialog/styles.css.ts +6 -5
- package/src/components/Drawer/styles.css.ts +14 -13
- package/src/components/Input/styles.css.ts +3 -2
- package/src/components/InputGroup/styles.css.ts +5 -4
- package/src/components/LinkButton/styles.css.ts +3 -2
- package/src/components/Menu/styles.css.ts +3 -2
- package/src/components/RadioGroup/styles.css.ts +3 -2
- package/src/components/ScrollArea/styles.css.ts +4 -3
- package/src/components/Select/styles.css.ts +3 -2
- package/src/components/SideNav/styles.css.ts +7 -6
- package/src/components/Switch/styles.css.ts +5 -4
- package/src/components/Table/styles.css.ts +3 -2
- package/src/components/Tabs/Tabs.tsx +52 -3
- package/src/components/Tabs/TabsContext.ts +6 -0
- package/src/components/Tabs/styles.css.ts +41 -2
- package/src/components/Tabs/useTabsRoot.ts +12 -2
- package/src/components/TagGroup/styles.css.ts +3 -2
- package/src/components/Textarea/styles.css.ts +3 -2
- package/src/components/Toast/styles.css.ts +2 -1
- package/src/components/Toggle/styles.css.ts +3 -2
- package/src/global-reset.css +19 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type ReactNode, useRef, useId, useEffect} from 'react'
|
|
1
|
+
import {type ReactNode, useRef, useId, useEffect, useLayoutEffect, useState} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {styles} from './styles.css'
|
|
4
4
|
import {TabsContext, useTabs} from './TabsContext'
|
|
@@ -39,8 +39,44 @@ interface ListProps {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function List({children}: ListProps) {
|
|
42
|
-
const {orientation, activeValue, onSelect, tabsRef, orderRef} = useTabs()
|
|
42
|
+
const {orientation, activeValue, onSelect, tabsRef, orderRef, setIndicatorActive} = useTabs()
|
|
43
43
|
const listRef = useRef<HTMLDivElement>(null)
|
|
44
|
+
const [indicator, setIndicator] = useState<{offset: number; size: number} | null>(null)
|
|
45
|
+
|
|
46
|
+
// Measure the active tab and drive the sliding indicator. useLayoutEffect so
|
|
47
|
+
// the first position is set before paint (the indicator element mounts already
|
|
48
|
+
// in place — no slide-in from zero; only later tab changes animate). Observes
|
|
49
|
+
// the active tab (its own size, e.g. a changing "(3)" count) and the list
|
|
50
|
+
// (container resize / reflow). A non-active sibling resizing while you're on
|
|
51
|
+
// another tab is the one case not caught; it self-corrects on the next
|
|
52
|
+
// selection or resize.
|
|
53
|
+
useLayoutEffect(() => {
|
|
54
|
+
const list = listRef.current
|
|
55
|
+
if (!list || !activeValue) {
|
|
56
|
+
setIndicator(null)
|
|
57
|
+
setIndicatorActive(false)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
const el = list.querySelector<HTMLElement>(`[data-tab-value="${CSS.escape(activeValue)}"]`)
|
|
61
|
+
if (!el) {
|
|
62
|
+
setIndicator(null)
|
|
63
|
+
setIndicatorActive(false)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const measure = () => {
|
|
67
|
+
setIndicator(
|
|
68
|
+
orientation === 'horizontal'
|
|
69
|
+
? {offset: el.offsetLeft, size: el.offsetWidth}
|
|
70
|
+
: {offset: el.offsetTop, size: el.offsetHeight},
|
|
71
|
+
)
|
|
72
|
+
setIndicatorActive(true)
|
|
73
|
+
}
|
|
74
|
+
measure()
|
|
75
|
+
const ro = new ResizeObserver(measure)
|
|
76
|
+
ro.observe(el)
|
|
77
|
+
ro.observe(list)
|
|
78
|
+
return () => ro.disconnect()
|
|
79
|
+
}, [activeValue, orientation, setIndicatorActive])
|
|
44
80
|
|
|
45
81
|
useEffect(() => {
|
|
46
82
|
const el = listRef.current
|
|
@@ -128,6 +164,18 @@ function List({children}: ListProps) {
|
|
|
128
164
|
style={[styles.list, orientation === 'vertical' && styles.listVertical]}
|
|
129
165
|
>
|
|
130
166
|
{children}
|
|
167
|
+
{indicator && (
|
|
168
|
+
<html.div
|
|
169
|
+
aria-hidden
|
|
170
|
+
style={[
|
|
171
|
+
styles.indicator,
|
|
172
|
+
orientation === 'horizontal' ? styles.indicatorHorizontal : styles.indicatorVertical,
|
|
173
|
+
orientation === 'horizontal'
|
|
174
|
+
? styles.indicatorOffsetH(indicator.offset, indicator.size)
|
|
175
|
+
: styles.indicatorOffsetV(indicator.offset, indicator.size),
|
|
176
|
+
]}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
131
179
|
</html.div>
|
|
132
180
|
)
|
|
133
181
|
}
|
|
@@ -141,7 +189,7 @@ interface TabProps {
|
|
|
141
189
|
}
|
|
142
190
|
|
|
143
191
|
function Tab({value, disabled = false, children}: TabProps) {
|
|
144
|
-
const {activeValue, onSelect, orientation, registerTab} = useTabs()
|
|
192
|
+
const {activeValue, onSelect, orientation, registerTab, indicatorActive} = useTabs()
|
|
145
193
|
const isActive = activeValue === value
|
|
146
194
|
const tabId = useId()
|
|
147
195
|
const panelId = `${tabId}-panel`
|
|
@@ -172,6 +220,7 @@ function Tab({value, disabled = false, children}: TabProps) {
|
|
|
172
220
|
orientation === 'vertical' && styles.tabVertical,
|
|
173
221
|
isActive &&
|
|
174
222
|
(orientation === 'vertical' ? styles.tabActiveVertical : styles.tabActiveHorizontal),
|
|
223
|
+
isActive && indicatorActive && styles.tabIndicatorActive,
|
|
175
224
|
disabled && styles.tabDisabled,
|
|
176
225
|
]}
|
|
177
226
|
>
|
|
@@ -9,6 +9,12 @@ export interface TabsContextValue {
|
|
|
9
9
|
registerTab: (value: string, disabled: boolean) => () => void
|
|
10
10
|
tabsRef: React.RefObject<Map<string, boolean>>
|
|
11
11
|
orderRef: React.RefObject<string[]>
|
|
12
|
+
// True once the List has measured the active tab and is driving a sliding
|
|
13
|
+
// indicator. Tabs suppress their own accent border while it's true so the
|
|
14
|
+
// single sliding bar is the only underline; it stays false without JS
|
|
15
|
+
// (SSR / no-hydration), where the per-tab border is the graceful fallback.
|
|
16
|
+
indicatorActive: boolean
|
|
17
|
+
setIndicatorActive: (active: boolean) => void
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
export const TabsContext = createContext<TabsContextValue | null>(null)
|
|
@@ -2,6 +2,7 @@ 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
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
5
6
|
|
|
6
7
|
export const styles = css.create({
|
|
7
8
|
root: {
|
|
@@ -18,6 +19,8 @@ export const styles = css.create({
|
|
|
18
19
|
borderBottomStyle: 'solid',
|
|
19
20
|
borderBottomColor: colors.border,
|
|
20
21
|
gap: spacing.xs,
|
|
22
|
+
// Positioning context for the sliding indicator.
|
|
23
|
+
position: 'relative',
|
|
21
24
|
},
|
|
22
25
|
listVertical: {
|
|
23
26
|
flexDirection: 'column',
|
|
@@ -49,8 +52,8 @@ export const styles = css.create({
|
|
|
49
52
|
borderBottomColor: 'transparent',
|
|
50
53
|
cursor: 'pointer',
|
|
51
54
|
transitionProperty: 'color, border-color',
|
|
52
|
-
transitionDuration:
|
|
53
|
-
transitionTimingFunction:
|
|
55
|
+
transitionDuration: duration.fast,
|
|
56
|
+
transitionTimingFunction: easing.standard,
|
|
54
57
|
outlineWidth: {
|
|
55
58
|
default: 0,
|
|
56
59
|
':focus-visible': 2,
|
|
@@ -83,6 +86,42 @@ export const styles = css.create({
|
|
|
83
86
|
color: colors.text,
|
|
84
87
|
borderRightColor: colors.accent,
|
|
85
88
|
},
|
|
89
|
+
// Applied to the active tab once JS drives the sliding indicator: drop the
|
|
90
|
+
// per-tab accent border so the single sliding bar is the only underline
|
|
91
|
+
// (avoids a double line and an instant-pop that would undercut the slide).
|
|
92
|
+
tabIndicatorActive: {
|
|
93
|
+
borderBottomColor: 'transparent',
|
|
94
|
+
borderRightColor: 'transparent',
|
|
95
|
+
},
|
|
96
|
+
// --- Sliding active indicator (positioned by measuring the active tab) ---
|
|
97
|
+
indicator: {
|
|
98
|
+
position: 'absolute',
|
|
99
|
+
backgroundColor: colors.accent,
|
|
100
|
+
pointerEvents: 'none',
|
|
101
|
+
transitionProperty: 'transform, width, height',
|
|
102
|
+
transitionDuration: duration.base,
|
|
103
|
+
transitionTimingFunction: easing.easeOut,
|
|
104
|
+
},
|
|
105
|
+
indicatorHorizontal: {
|
|
106
|
+
bottom: -1,
|
|
107
|
+
left: 0,
|
|
108
|
+
height: 2,
|
|
109
|
+
},
|
|
110
|
+
indicatorVertical: {
|
|
111
|
+
right: -1,
|
|
112
|
+
top: 0,
|
|
113
|
+
width: 2,
|
|
114
|
+
},
|
|
115
|
+
// Dynamic transform/size from the measured active tab (StyleX dynamic style:
|
|
116
|
+
// simple identifier params only).
|
|
117
|
+
indicatorOffsetH: (offset: number, size: number) => ({
|
|
118
|
+
transform: `translateX(${offset}px)`,
|
|
119
|
+
width: size,
|
|
120
|
+
}),
|
|
121
|
+
indicatorOffsetV: (offset: number, size: number) => ({
|
|
122
|
+
transform: `translateY(${offset}px)`,
|
|
123
|
+
height: size,
|
|
124
|
+
}),
|
|
86
125
|
tabDisabled: {
|
|
87
126
|
opacity: 0.5,
|
|
88
127
|
cursor: 'not-allowed',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useCallback, useRef} from 'react'
|
|
1
|
+
import {useCallback, useRef, useState} from 'react'
|
|
2
2
|
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
3
3
|
import type {Orientation, TabsContextValue} from './TabsContext'
|
|
4
4
|
|
|
@@ -26,6 +26,7 @@ export function useTabsRoot({
|
|
|
26
26
|
)
|
|
27
27
|
const tabsRef = useRef(new Map<string, boolean>())
|
|
28
28
|
const orderRef = useRef<string[]>([])
|
|
29
|
+
const [indicatorActive, setIndicatorActive] = useState(false)
|
|
29
30
|
|
|
30
31
|
const registerTab = useCallback((value: string, disabled: boolean) => {
|
|
31
32
|
tabsRef.current.set(value, disabled)
|
|
@@ -38,5 +39,14 @@ export function useTabsRoot({
|
|
|
38
39
|
}
|
|
39
40
|
}, [])
|
|
40
41
|
|
|
41
|
-
return {
|
|
42
|
+
return {
|
|
43
|
+
activeValue,
|
|
44
|
+
onSelect,
|
|
45
|
+
orientation,
|
|
46
|
+
registerTab,
|
|
47
|
+
tabsRef,
|
|
48
|
+
orderRef,
|
|
49
|
+
indicatorActive,
|
|
50
|
+
setIndicatorActive,
|
|
51
|
+
}
|
|
42
52
|
}
|
|
@@ -2,6 +2,7 @@ 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
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
5
6
|
|
|
6
7
|
export const styles = css.create({
|
|
7
8
|
root: {
|
|
@@ -48,8 +49,8 @@ export const styles = css.create({
|
|
|
48
49
|
paddingRight: spacing.sm,
|
|
49
50
|
minHeight: 40,
|
|
50
51
|
transitionProperty: 'border-color',
|
|
51
|
-
transitionDuration:
|
|
52
|
-
transitionTimingFunction:
|
|
52
|
+
transitionDuration: duration.fast,
|
|
53
|
+
transitionTimingFunction: easing.standard,
|
|
53
54
|
outlineWidth: {
|
|
54
55
|
default: 0,
|
|
55
56
|
':focus': 2,
|
|
@@ -2,6 +2,7 @@ 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
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
5
6
|
|
|
6
7
|
export const styles = css.create({
|
|
7
8
|
base: {
|
|
@@ -20,8 +21,8 @@ export const styles = css.create({
|
|
|
20
21
|
borderRadius: radii.sm,
|
|
21
22
|
resize: 'vertical' as const,
|
|
22
23
|
transitionProperty: 'border-color',
|
|
23
|
-
transitionDuration:
|
|
24
|
-
transitionTimingFunction:
|
|
24
|
+
transitionDuration: duration.fast,
|
|
25
|
+
transitionTimingFunction: easing.standard,
|
|
25
26
|
outlineWidth: {
|
|
26
27
|
default: 0,
|
|
27
28
|
':focus-visible': 2,
|
|
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
|
3
3
|
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
5
|
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
|
+
import {easing} from '@duro-app/tokens/tokens/motion.css'
|
|
6
7
|
|
|
7
8
|
const enter = css.keyframes({
|
|
8
9
|
from: {opacity: 0, transform: 'translateY(10px)'},
|
|
@@ -51,7 +52,7 @@ export const styles = css.create({
|
|
|
51
52
|
'@media (prefers-reduced-motion: reduce)': 'none',
|
|
52
53
|
},
|
|
53
54
|
animationDuration: '160ms',
|
|
54
|
-
animationTimingFunction:
|
|
55
|
+
animationTimingFunction: easing.easeOut,
|
|
55
56
|
},
|
|
56
57
|
success: {borderLeftColor: colors.successBorder},
|
|
57
58
|
error: {borderLeftColor: colors.errorBorder},
|
|
@@ -2,6 +2,7 @@ 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
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
5
6
|
|
|
6
7
|
export const styles = css.create({
|
|
7
8
|
base: {
|
|
@@ -14,8 +15,8 @@ export const styles = css.create({
|
|
|
14
15
|
fontFamily: typography.fontFamily,
|
|
15
16
|
fontWeight: typography.fontWeightMedium,
|
|
16
17
|
transitionProperty: 'background-color, border-color, color, opacity',
|
|
17
|
-
transitionDuration:
|
|
18
|
-
transitionTimingFunction:
|
|
18
|
+
transitionDuration: duration.fast,
|
|
19
|
+
transitionTimingFunction: easing.standard,
|
|
19
20
|
outlineWidth: {default: 0, ':focus-visible': 2},
|
|
20
21
|
outlineStyle: 'solid',
|
|
21
22
|
outlineColor: colors.accent,
|
package/src/global-reset.css
CHANGED
|
@@ -33,3 +33,22 @@
|
|
|
33
33
|
text-decoration: none;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* Reduced-motion safety net. Neutralizes animation/transition timing globally
|
|
39
|
+
* for users who prefer reduced motion, covering every component (dialogs,
|
|
40
|
+
* drawers, detail panels, spinners, …) without touching their per-component
|
|
41
|
+
* tuned animations. Uses 0.01ms (not 0) so animationend/transitionend still
|
|
42
|
+
* fire and any state-driven mount/unmount logic completes. Kept outside
|
|
43
|
+
* `@layer reset` so its `!important` wins regardless of cascade layers.
|
|
44
|
+
*/
|
|
45
|
+
@media (prefers-reduced-motion: reduce) {
|
|
46
|
+
*,
|
|
47
|
+
*::before,
|
|
48
|
+
*::after {
|
|
49
|
+
animation-duration: 0.01ms !important;
|
|
50
|
+
animation-iteration-count: 1 !important;
|
|
51
|
+
transition-duration: 0.01ms !important;
|
|
52
|
+
scroll-behavior: auto !important;
|
|
53
|
+
}
|
|
54
|
+
}
|