@duro-app/ui 5.5.0 → 5.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-DYN-bp0e.js → Table-Dj7PXqO_.js} +405 -387
- package/dist/Table-Dj7PXqO_.js.map +1 -0
- package/dist/components/Button/Button.d.ts +2 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/styles.css.d.ts +1 -0
- package/dist/components/Button/styles.css.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +14 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
- package/dist/components/DragDrop/DragDrop.d.ts +95 -14
- package/dist/components/DragDrop/DragDrop.d.ts.map +1 -1
- package/dist/components/DragDrop/DragDrop.meta.d.ts.map +1 -1
- package/dist/components/DragDrop/DragDropBoard.stories.d.ts +69 -0
- package/dist/components/DragDrop/DragDropBoard.stories.d.ts.map +1 -0
- package/dist/components/DragDrop/styles.css.d.ts +80 -3
- package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
- package/dist/components/LinkButton/styles.css.d.ts +1 -0
- package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
- package/dist/components/Menu/Menu.d.ts +4 -1
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/styles.css.d.ts +14 -0
- package/dist/components/Menu/styles.css.d.ts.map +1 -1
- package/dist/components/Meter/Meter.d.ts +17 -0
- package/dist/components/Meter/Meter.d.ts.map +1 -0
- package/dist/components/Meter/Meter.meta.d.ts +3 -0
- package/dist/components/Meter/Meter.meta.d.ts.map +1 -0
- package/dist/components/Meter/Meter.stories.d.ts +12 -0
- package/dist/components/Meter/Meter.stories.d.ts.map +1 -0
- package/dist/components/Meter/styles.css.d.ts +34 -0
- package/dist/components/Meter/styles.css.d.ts.map +1 -0
- package/dist/components/Popover/Popover.d.ts +14 -2
- package/dist/components/Popover/Popover.d.ts.map +1 -1
- package/dist/components/Popover/Popover.meta.d.ts.map +1 -1
- package/dist/components/Popover/PopoverLayerContext.d.ts +3 -0
- package/dist/components/Popover/PopoverLayerContext.d.ts.map +1 -1
- package/dist/components/Popover/PopoverRaised.stories.d.ts +28 -0
- package/dist/components/Popover/PopoverRaised.stories.d.ts.map +1 -0
- package/dist/components/Popover/styles.css.d.ts +15 -0
- package/dist/components/Popover/styles.css.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts +4 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/styles.css.d.ts +9 -0
- package/dist/components/Select/styles.css.d.ts.map +1 -1
- package/dist/components/Timeline/Timeline.d.ts +71 -0
- package/dist/components/Timeline/Timeline.d.ts.map +1 -0
- package/dist/components/Timeline/Timeline.meta.d.ts +3 -0
- package/dist/components/Timeline/Timeline.meta.d.ts.map +1 -0
- package/dist/components/Timeline/Timeline.stories.d.ts +26 -0
- package/dist/components/Timeline/Timeline.stories.d.ts.map +1 -0
- package/dist/components/Timeline/styles.css.d.ts +158 -0
- package/dist/components/Timeline/styles.css.d.ts.map +1 -0
- package/dist/components/Toast/Toast.meta.d.ts.map +1 -1
- package/dist/components/Toast/Toast.stories.d.ts +9 -0
- package/dist/components/Toast/Toast.stories.d.ts.map +1 -1
- package/dist/components/Toast/ToastProvider.d.ts +11 -5
- package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
- package/dist/components/Toast/index.d.ts +1 -1
- package/dist/components/Toast/index.d.ts.map +1 -1
- package/dist/components/Toast/styles.css.d.ts +6 -0
- package/dist/components/Toast/styles.css.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.d.ts +2 -1
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/styles.css.d.ts +1 -0
- package/dist/components/Toggle/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4992 -3845
- package/dist/index.js.map +1 -1
- package/dist/shared/ModalContext.d.ts +7 -0
- package/dist/shared/ModalContext.d.ts.map +1 -1
- package/dist/shared/exports.typecheck.d.ts +9 -0
- package/dist/shared/exports.typecheck.d.ts.map +1 -0
- package/dist/shared/types.d.ts +2 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/table.js +1 -1
- package/package.json +2 -2
- package/src/components/Button/Button.tsx +2 -1
- package/src/components/Button/styles.css.ts +3 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +172 -14
- package/src/components/DragDrop/DragDrop.meta.ts +22 -21
- package/src/components/DragDrop/DragDrop.tsx +1147 -246
- package/src/components/DragDrop/DragDropBoard.stories.tsx +668 -0
- package/src/components/DragDrop/styles.css.ts +74 -4
- package/src/components/LinkButton/styles.css.ts +4 -0
- package/src/components/Menu/Menu.meta.ts +1 -1
- package/src/components/Menu/Menu.tsx +17 -2
- package/src/components/Menu/styles.css.ts +18 -0
- package/src/components/Meter/Meter.meta.ts +24 -0
- package/src/components/Meter/Meter.stories.tsx +64 -0
- package/src/components/Meter/Meter.tsx +55 -0
- package/src/components/Meter/styles.css.ts +30 -0
- package/src/components/Popover/Popover.meta.ts +2 -1
- package/src/components/Popover/Popover.tsx +29 -4
- package/src/components/Popover/PopoverLayerContext.ts +3 -0
- package/src/components/Popover/PopoverRaised.stories.tsx +334 -0
- package/src/components/Popover/styles.css.ts +21 -0
- package/src/components/Select/Select.meta.ts +1 -1
- package/src/components/Select/Select.tsx +5 -1
- package/src/components/Select/styles.css.ts +12 -0
- package/src/components/Timeline/Timeline.meta.ts +46 -0
- package/src/components/Timeline/Timeline.stories.tsx +238 -0
- package/src/components/Timeline/Timeline.tsx +332 -0
- package/src/components/Timeline/styles.css.ts +162 -0
- package/src/components/Toast/Toast.meta.ts +2 -1
- package/src/components/Toast/Toast.stories.tsx +84 -0
- package/src/components/Toast/ToastProvider.tsx +49 -11
- package/src/components/Toast/index.ts +7 -1
- package/src/components/Toast/styles.css.ts +2 -0
- package/src/components/Toggle/Toggle.tsx +2 -1
- package/src/components/Toggle/styles.css.ts +2 -0
- package/src/index.ts +18 -0
- package/src/shared/ModalContext.ts +8 -1
- package/src/shared/exports.typecheck.ts +16 -0
- package/src/shared/types.ts +3 -0
- package/dist/Table-DYN-bp0e.js.map +0 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {microSpacing, radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
5
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
6
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
7
|
+
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
8
|
+
|
|
9
|
+
export const styles = css.create({
|
|
10
|
+
// Two columns — row labels, date track — with the axis as the first row.
|
|
11
|
+
root: {
|
|
12
|
+
display: 'grid',
|
|
13
|
+
gridTemplateColumns: `${sizes.timelineLabelW} 1fr`,
|
|
14
|
+
rowGap: spacing.xs,
|
|
15
|
+
color: colors.text,
|
|
16
|
+
},
|
|
17
|
+
heading: {
|
|
18
|
+
display: 'flex',
|
|
19
|
+
alignItems: 'flex-end',
|
|
20
|
+
fontSize: typography.fontSizeXs,
|
|
21
|
+
color: colors.textMuted,
|
|
22
|
+
},
|
|
23
|
+
axis: {
|
|
24
|
+
position: 'relative',
|
|
25
|
+
height: sizes.chip,
|
|
26
|
+
fontSize: typography.fontSizeXs,
|
|
27
|
+
color: colors.textMuted,
|
|
28
|
+
whiteSpace: 'nowrap',
|
|
29
|
+
},
|
|
30
|
+
tick: {
|
|
31
|
+
position: 'absolute',
|
|
32
|
+
top: 0,
|
|
33
|
+
paddingLeft: spacing.xs,
|
|
34
|
+
borderLeftWidth: borders.hairline,
|
|
35
|
+
borderLeftStyle: 'solid',
|
|
36
|
+
borderLeftColor: colors.border,
|
|
37
|
+
},
|
|
38
|
+
todayLabel: {
|
|
39
|
+
position: 'absolute',
|
|
40
|
+
bottom: 0,
|
|
41
|
+
transform: 'translateX(-50%)',
|
|
42
|
+
color: colors.errorText,
|
|
43
|
+
fontWeight: typography.fontWeightMedium,
|
|
44
|
+
},
|
|
45
|
+
left: (percent: number) => ({left: `${percent}%`}),
|
|
46
|
+
width: (percent: number) => ({width: `${percent}%`}),
|
|
47
|
+
label: {
|
|
48
|
+
display: 'flex',
|
|
49
|
+
flexDirection: 'column',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
minWidth: 0,
|
|
52
|
+
paddingRight: spacing.sm,
|
|
53
|
+
},
|
|
54
|
+
labelText: {
|
|
55
|
+
fontSize: typography.fontSizeSm,
|
|
56
|
+
fontWeight: typography.fontWeightMedium,
|
|
57
|
+
whiteSpace: 'nowrap',
|
|
58
|
+
overflow: 'hidden',
|
|
59
|
+
textOverflow: 'ellipsis',
|
|
60
|
+
},
|
|
61
|
+
labelDescription: {
|
|
62
|
+
fontSize: typography.fontSizeXs,
|
|
63
|
+
color: colors.textMuted,
|
|
64
|
+
whiteSpace: 'nowrap',
|
|
65
|
+
},
|
|
66
|
+
track: {
|
|
67
|
+
position: 'relative',
|
|
68
|
+
height: sizes.controlMd,
|
|
69
|
+
},
|
|
70
|
+
today: {
|
|
71
|
+
position: 'absolute',
|
|
72
|
+
top: 0,
|
|
73
|
+
bottom: 0,
|
|
74
|
+
borderLeftWidth: borders.strong,
|
|
75
|
+
borderLeftStyle: 'solid',
|
|
76
|
+
borderLeftColor: colors.error,
|
|
77
|
+
pointerEvents: 'none',
|
|
78
|
+
zIndex: 2,
|
|
79
|
+
},
|
|
80
|
+
bar: {
|
|
81
|
+
position: 'absolute',
|
|
82
|
+
top: '50%',
|
|
83
|
+
transform: 'translateY(-50%)',
|
|
84
|
+
boxSizing: 'border-box',
|
|
85
|
+
height: sizes.controlSm,
|
|
86
|
+
},
|
|
87
|
+
face: {
|
|
88
|
+
position: 'relative',
|
|
89
|
+
display: 'flex',
|
|
90
|
+
alignItems: 'center',
|
|
91
|
+
boxSizing: 'border-box',
|
|
92
|
+
width: '100%',
|
|
93
|
+
height: '100%',
|
|
94
|
+
padding: 0,
|
|
95
|
+
overflow: 'hidden',
|
|
96
|
+
borderRadius: radii.xs,
|
|
97
|
+
borderWidth: borders.hairline,
|
|
98
|
+
borderStyle: 'solid',
|
|
99
|
+
borderColor: {default: colors.border, ':hover': colors.accent},
|
|
100
|
+
backgroundColor: colors.bg,
|
|
101
|
+
color: colors.text,
|
|
102
|
+
cursor: 'pointer',
|
|
103
|
+
textAlign: 'left',
|
|
104
|
+
transitionProperty: 'box-shadow, border-color',
|
|
105
|
+
transitionDuration: duration.fast,
|
|
106
|
+
transitionTimingFunction: easing.standard,
|
|
107
|
+
outlineStyle: {default: 'none', ':focus-visible': 'solid'},
|
|
108
|
+
outlineWidth: borders.focusRing,
|
|
109
|
+
outlineColor: colors.accent,
|
|
110
|
+
outlineOffset: borders.focusOffset,
|
|
111
|
+
},
|
|
112
|
+
faceSelected: {
|
|
113
|
+
borderColor: colors.accent,
|
|
114
|
+
boxShadow: `0 0 0 ${microSpacing.px1} ${colors.accent}`,
|
|
115
|
+
},
|
|
116
|
+
barPlanned: {
|
|
117
|
+
borderStyle: 'dashed',
|
|
118
|
+
},
|
|
119
|
+
barDone: {
|
|
120
|
+
backgroundColor: colors.successBg,
|
|
121
|
+
},
|
|
122
|
+
fill: {
|
|
123
|
+
position: 'absolute',
|
|
124
|
+
left: 0,
|
|
125
|
+
top: 0,
|
|
126
|
+
bottom: 0,
|
|
127
|
+
backgroundColor: colors.infoBg,
|
|
128
|
+
borderRightWidth: borders.strong,
|
|
129
|
+
borderRightStyle: 'solid',
|
|
130
|
+
borderRightColor: colors.accent,
|
|
131
|
+
},
|
|
132
|
+
fillDone: {
|
|
133
|
+
backgroundColor: colors.successBg,
|
|
134
|
+
borderRightColor: colors.success,
|
|
135
|
+
},
|
|
136
|
+
barText: {
|
|
137
|
+
position: 'relative',
|
|
138
|
+
paddingLeft: spacing.sm,
|
|
139
|
+
fontSize: typography.fontSizeXs,
|
|
140
|
+
fontVariantNumeric: 'tabular-nums',
|
|
141
|
+
whiteSpace: 'nowrap',
|
|
142
|
+
},
|
|
143
|
+
// The edges: keyboard sliders, drawn only while focused.
|
|
144
|
+
edge: {
|
|
145
|
+
position: 'absolute',
|
|
146
|
+
top: 0,
|
|
147
|
+
bottom: 0,
|
|
148
|
+
width: spacing.sm,
|
|
149
|
+
borderRadius: radii.xs,
|
|
150
|
+
outlineStyle: {default: 'none', ':focus-visible': 'solid'},
|
|
151
|
+
outlineWidth: borders.focusRing,
|
|
152
|
+
outlineColor: colors.accent,
|
|
153
|
+
backgroundColor: {default: 'transparent', ':focus-visible': colors.accent},
|
|
154
|
+
zIndex: 3,
|
|
155
|
+
},
|
|
156
|
+
edgeStart: {
|
|
157
|
+
left: `calc(-1 * ${spacing.xs})`,
|
|
158
|
+
},
|
|
159
|
+
edgeEnd: {
|
|
160
|
+
right: `calc(-1 * ${spacing.xs})`,
|
|
161
|
+
},
|
|
162
|
+
})
|
|
@@ -2,11 +2,12 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs.',
|
|
5
|
+
'Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs. Variants: success, info, warning, error. The countdown pauses while a toast is hovered or holds focus, and every toast has a close button (`dismissLabel` on the provider names it).',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Confirming an action succeeded (e.g. "Grant revoked")',
|
|
8
8
|
'Reporting an action failed, with the reason',
|
|
9
9
|
'Reversible actions that benefit from an inline Undo',
|
|
10
|
+
'A soft limit crossed but allowed (variant warning), e.g. a column over its WIP limit',
|
|
10
11
|
],
|
|
11
12
|
whenNotToUse: [
|
|
12
13
|
'In-form validation errors — use Field.Error or an inline Alert',
|
|
@@ -47,6 +47,31 @@ function Triggers() {
|
|
|
47
47
|
>
|
|
48
48
|
Show with undo
|
|
49
49
|
</html.button>
|
|
50
|
+
<html.button
|
|
51
|
+
style={s.btn}
|
|
52
|
+
onClick={() =>
|
|
53
|
+
toast({
|
|
54
|
+
variant: 'warning',
|
|
55
|
+
message: 'In progress is over its WIP limit (4/3)',
|
|
56
|
+
duration: 0,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
Show warning
|
|
61
|
+
</html.button>
|
|
62
|
+
<html.button
|
|
63
|
+
style={s.btn}
|
|
64
|
+
onClick={() =>
|
|
65
|
+
toast({
|
|
66
|
+
variant: 'success',
|
|
67
|
+
message: 'Moved to Done',
|
|
68
|
+
duration: 600,
|
|
69
|
+
action: {label: 'Undo', onClick: () => {}},
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
Show short
|
|
74
|
+
</html.button>
|
|
50
75
|
</html.div>
|
|
51
76
|
)
|
|
52
77
|
}
|
|
@@ -87,3 +112,62 @@ export const Dismiss: Story = {
|
|
|
87
112
|
await waitFor(() => expect(canvas.queryByRole('status')).not.toBeInTheDocument())
|
|
88
113
|
},
|
|
89
114
|
}
|
|
115
|
+
|
|
116
|
+
/** The WIP-limit shape: a warning, announced politely, with its own tone. */
|
|
117
|
+
export const Warning: Story = {
|
|
118
|
+
play: async ({canvas}) => {
|
|
119
|
+
await userEvent.click(canvas.getByText('Show warning'))
|
|
120
|
+
const toast = await waitFor(() => canvas.getByRole('status'))
|
|
121
|
+
await expect(toast).toHaveTextContent('In progress is over its WIP limit (4/3)')
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
126
|
+
|
|
127
|
+
/** The countdown stops while the pointer is on the toast and resumes with
|
|
128
|
+
* what was left when it leaves. */
|
|
129
|
+
export const PausesOnHover: Story = {
|
|
130
|
+
play: async ({canvas}) => {
|
|
131
|
+
await userEvent.click(canvas.getByText('Show short'))
|
|
132
|
+
const toast = await waitFor(() => canvas.getByRole('status'))
|
|
133
|
+
await userEvent.hover(toast)
|
|
134
|
+
await wait(1000)
|
|
135
|
+
await expect(canvas.getByRole('status')).toHaveTextContent('Moved to Done')
|
|
136
|
+
await userEvent.unhover(toast)
|
|
137
|
+
await waitFor(() => expect(canvas.queryByRole('status')).not.toBeInTheDocument(), {
|
|
138
|
+
timeout: 2000,
|
|
139
|
+
})
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Focus inside the toast (its Undo, its close button) holds it too. */
|
|
144
|
+
export const PausesOnFocus: Story = {
|
|
145
|
+
play: async ({canvas}) => {
|
|
146
|
+
await userEvent.click(canvas.getByText('Show short'))
|
|
147
|
+
await waitFor(() => canvas.getByRole('status'))
|
|
148
|
+
canvas.getByRole('button', {name: 'Undo'}).focus()
|
|
149
|
+
await wait(1000)
|
|
150
|
+
// Moving between the toast's own buttons keeps it paused.
|
|
151
|
+
canvas.getByRole('button', {name: 'Dismiss'}).focus()
|
|
152
|
+
await wait(300)
|
|
153
|
+
await expect(canvas.getByRole('status')).toHaveTextContent('Moved to Done')
|
|
154
|
+
canvas.getByText('Show short').focus()
|
|
155
|
+
await waitFor(() => expect(canvas.queryByText('Moved to Done')).not.toBeInTheDocument(), {
|
|
156
|
+
timeout: 2000,
|
|
157
|
+
})
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The close button's name comes from the app (dismissLabel). */
|
|
162
|
+
export const LocalisedClose: Story = {
|
|
163
|
+
render: () => (
|
|
164
|
+
<ToastProvider dismissLabel="Fermer">
|
|
165
|
+
<Triggers />
|
|
166
|
+
</ToastProvider>
|
|
167
|
+
),
|
|
168
|
+
play: async ({canvas}) => {
|
|
169
|
+
await userEvent.click(canvas.getByText('Show warning'))
|
|
170
|
+
await userEvent.click(await canvas.findByRole('button', {name: 'Fermer'}))
|
|
171
|
+
await waitFor(() => expect(canvas.queryByRole('status')).not.toBeInTheDocument())
|
|
172
|
+
},
|
|
173
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
useEffect,
|
|
6
6
|
useMemo,
|
|
7
7
|
useRef,
|
|
8
|
+
useState,
|
|
8
9
|
useSyncExternalStore,
|
|
9
10
|
type ReactNode,
|
|
10
11
|
} from 'react'
|
|
@@ -14,7 +15,7 @@ import {Icon, type IconName} from '../Icon'
|
|
|
14
15
|
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
15
16
|
import {styles} from './styles.css'
|
|
16
17
|
|
|
17
|
-
export type ToastVariant = 'success' | 'error' | 'info'
|
|
18
|
+
export type ToastVariant = 'success' | 'error' | 'info' | 'warning'
|
|
18
19
|
|
|
19
20
|
export interface ToastAction {
|
|
20
21
|
label: string
|
|
@@ -22,12 +23,14 @@ export interface ToastAction {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface ToastOptions {
|
|
25
|
-
/** Visual + semantic tone. Errors announce assertively and
|
|
26
|
+
/** Visual + semantic tone. Errors announce assertively; errors and
|
|
27
|
+
* warnings linger longer. */
|
|
26
28
|
variant?: ToastVariant
|
|
27
29
|
message: ReactNode
|
|
28
30
|
/** Optional single action, e.g. Undo. Runs, then dismisses the toast. */
|
|
29
31
|
action?: ToastAction
|
|
30
|
-
/** Auto-dismiss delay in ms. `0` (or negative) keeps it until dismissed.
|
|
32
|
+
/** Auto-dismiss delay in ms. `0` (or negative) keeps it until dismissed.
|
|
33
|
+
* The countdown pauses while the toast is hovered or holds focus. */
|
|
31
34
|
duration?: number
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -56,6 +59,7 @@ const DEFAULT_DURATION: Record<ToastVariant, number> = {
|
|
|
56
59
|
success: 5000,
|
|
57
60
|
info: 5000,
|
|
58
61
|
error: 8000,
|
|
62
|
+
warning: 8000,
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
function createToastStore(): ToastStore {
|
|
@@ -92,11 +96,13 @@ const variantIcon: Record<ToastVariant, IconName> = {
|
|
|
92
96
|
success: 'check-circle-filled',
|
|
93
97
|
error: 'x-circle-filled',
|
|
94
98
|
info: 'info-circle-filled',
|
|
99
|
+
warning: 'alert-triangle-filled',
|
|
95
100
|
}
|
|
96
101
|
const variantIconStyle = {
|
|
97
102
|
success: styles.iconSuccess,
|
|
98
103
|
error: styles.iconError,
|
|
99
104
|
info: styles.iconInfo,
|
|
105
|
+
warning: styles.iconWarning,
|
|
100
106
|
} as const
|
|
101
107
|
|
|
102
108
|
const ToastContext = createContext<ToastContextValue | null>(null)
|
|
@@ -107,20 +113,45 @@ export function useToast(): ToastContextValue {
|
|
|
107
113
|
return ctx
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
function ToastItem({
|
|
116
|
+
function ToastItem({
|
|
117
|
+
entry,
|
|
118
|
+
onDismiss,
|
|
119
|
+
dismissLabel,
|
|
120
|
+
}: {
|
|
121
|
+
entry: ToastEntry
|
|
122
|
+
onDismiss: (id: string) => void
|
|
123
|
+
dismissLabel: string
|
|
124
|
+
}) {
|
|
111
125
|
const {id, variant, message, action, duration} = entry
|
|
126
|
+
const [hovered, setHovered] = useState(false)
|
|
127
|
+
const [focused, setFocused] = useState(false)
|
|
128
|
+
const paused = hovered || focused
|
|
129
|
+
// What is left of the countdown; a pause keeps it, a resume restarts the
|
|
130
|
+
// timer with what was left (WCAG 2.2.1: the reader controls the timing).
|
|
131
|
+
const remaining = useRef(duration)
|
|
112
132
|
|
|
113
133
|
useEffect(() => {
|
|
114
|
-
if (duration <= 0) return
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
if (duration <= 0 || paused) return
|
|
135
|
+
const started = Date.now()
|
|
136
|
+
const timer = setTimeout(() => onDismiss(id), Math.max(remaining.current, 0))
|
|
137
|
+
return () => {
|
|
138
|
+
clearTimeout(timer)
|
|
139
|
+
remaining.current -= Date.now() - started
|
|
140
|
+
}
|
|
141
|
+
}, [id, duration, paused, onDismiss])
|
|
118
142
|
|
|
119
143
|
return (
|
|
120
144
|
<html.div
|
|
121
145
|
role={variant === 'error' ? 'alert' : 'status'}
|
|
122
146
|
aria-live={variant === 'error' ? 'assertive' : 'polite'}
|
|
123
147
|
style={[styles.toast, styles[variant]]}
|
|
148
|
+
onPointerEnter={() => setHovered(true)}
|
|
149
|
+
onPointerLeave={() => setHovered(false)}
|
|
150
|
+
onFocus={() => setFocused(true)}
|
|
151
|
+
onBlur={(e: {currentTarget: HTMLElement; relatedTarget: EventTarget | null}) => {
|
|
152
|
+
// Focus moving between the toast's own buttons keeps it paused.
|
|
153
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setFocused(false)
|
|
154
|
+
}}
|
|
124
155
|
>
|
|
125
156
|
<html.div style={[styles.iconWrap, variantIconStyle[variant]]}>
|
|
126
157
|
<Icon name={variantIcon[variant]} size="md" />
|
|
@@ -142,7 +173,7 @@ function ToastItem({entry, onDismiss}: {entry: ToastEntry; onDismiss: (id: strin
|
|
|
142
173
|
</html.div>
|
|
143
174
|
<html.button
|
|
144
175
|
type="button"
|
|
145
|
-
aria-label=
|
|
176
|
+
aria-label={dismissLabel}
|
|
146
177
|
style={styles.closeBtn}
|
|
147
178
|
onClick={() => onDismiss(id)}
|
|
148
179
|
>
|
|
@@ -152,7 +183,14 @@ function ToastItem({entry, onDismiss}: {entry: ToastEntry; onDismiss: (id: strin
|
|
|
152
183
|
)
|
|
153
184
|
}
|
|
154
185
|
|
|
155
|
-
export
|
|
186
|
+
export interface ToastProviderProps {
|
|
187
|
+
/** Accessible name of each toast's close button, e.g. from the app's
|
|
188
|
+
* message catalog. */
|
|
189
|
+
dismissLabel?: string
|
|
190
|
+
children: ReactNode
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function ToastProvider({dismissLabel = 'Dismiss', children}: ToastProviderProps) {
|
|
156
194
|
const storeRef = useRef<ToastStore>(null)
|
|
157
195
|
if (storeRef.current === null) {
|
|
158
196
|
storeRef.current = createToastStore()
|
|
@@ -175,7 +213,7 @@ export function ToastProvider({children}: {children: ReactNode}) {
|
|
|
175
213
|
const region = (
|
|
176
214
|
<html.div role="region" aria-label="Notifications" style={styles.region}>
|
|
177
215
|
{entries.map((entry) => (
|
|
178
|
-
<ToastItem key={entry.id} entry={entry} onDismiss={dismiss} />
|
|
216
|
+
<ToastItem key={entry.id} entry={entry} onDismiss={dismiss} dismissLabel={dismissLabel} />
|
|
179
217
|
))}
|
|
180
218
|
</html.div>
|
|
181
219
|
)
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
export {ToastProvider, useToast} from './ToastProvider'
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
ToastVariant,
|
|
4
|
+
ToastOptions,
|
|
5
|
+
ToastAction,
|
|
6
|
+
ToastContextValue,
|
|
7
|
+
ToastProviderProps,
|
|
8
|
+
} from './ToastProvider'
|
|
@@ -62,6 +62,7 @@ export const styles = css.create({
|
|
|
62
62
|
success: {borderLeftColor: colors.successBorder},
|
|
63
63
|
error: {borderLeftColor: colors.errorBorder},
|
|
64
64
|
info: {borderLeftColor: colors.infoBorder},
|
|
65
|
+
warning: {borderLeftColor: colors.warningBorder},
|
|
65
66
|
|
|
66
67
|
iconWrap: {
|
|
67
68
|
flexShrink: 0,
|
|
@@ -73,6 +74,7 @@ export const styles = css.create({
|
|
|
73
74
|
iconSuccess: {color: colors.successText},
|
|
74
75
|
iconError: {color: colors.errorText},
|
|
75
76
|
iconInfo: {color: colors.infoText},
|
|
77
|
+
iconWarning: {color: colors.warningText},
|
|
76
78
|
|
|
77
79
|
content: {
|
|
78
80
|
flex: 1,
|
|
@@ -4,9 +4,10 @@ import {useControllableValue} from '../../hooks/useControllableValue'
|
|
|
4
4
|
import {useToggleGroup} from '../ToggleGroup/ToggleGroupContext'
|
|
5
5
|
import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
|
|
6
6
|
import {mergeRefs} from '../../shared/mergeRefs'
|
|
7
|
+
import type {ControlSize} from '../../shared/types'
|
|
7
8
|
import {styles} from './styles.css'
|
|
8
9
|
|
|
9
|
-
export type ToggleSize =
|
|
10
|
+
export type ToggleSize = ControlSize
|
|
10
11
|
|
|
11
12
|
interface ToggleProps {
|
|
12
13
|
/** Controlled pressed state (standalone usage). */
|
|
@@ -37,6 +37,8 @@ export const styles = css.create({
|
|
|
37
37
|
fontSize: typography.fontSizeXs,
|
|
38
38
|
borderRadius: radii.sm,
|
|
39
39
|
gap: spacing.xs,
|
|
40
|
+
// Every small control is at least controlSm tall, whatever it holds (5.6).
|
|
41
|
+
minHeight: {default: sizes.controlSm, '@media (pointer: coarse)': sizes.touchTarget},
|
|
40
42
|
},
|
|
41
43
|
// wrapped toggles: a fixed block size per size, the touch target under a
|
|
42
44
|
// coarse pointer. ToggleGroup's maxRows arithmetic reads the same tokens.
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export {Icon, type IconName} from './components/Icon'
|
|
|
14
14
|
export {Badge, type BadgeVariant, type BadgeSize} from './components/Badge/Badge'
|
|
15
15
|
export {Breadcrumb} from './components/Breadcrumb/Breadcrumb'
|
|
16
16
|
export {Button, type ButtonVariant, type ButtonSize} from './components/Button/Button'
|
|
17
|
+
export type {ControlSize} from './shared/types'
|
|
17
18
|
export {ButtonGroup, type ButtonGroupProps} from './components/ButtonGroup/ButtonGroup'
|
|
18
19
|
export {Callout, type CalloutVariant} from './components/Callout/Callout'
|
|
19
20
|
export {Card, type CardVariant, type CardSize} from './components/Card/Card'
|
|
@@ -24,9 +25,15 @@ export {ConfirmDialog} from './components/ConfirmDialog/ConfirmDialog'
|
|
|
24
25
|
export {
|
|
25
26
|
DragDrop,
|
|
26
27
|
useDragDrop,
|
|
28
|
+
type DragDropAnnouncement,
|
|
27
29
|
type DragDropEvent,
|
|
28
30
|
type DragDropItemData,
|
|
31
|
+
type DragDropOverEvent,
|
|
32
|
+
type DragDropPlaceholderInfo,
|
|
33
|
+
type DragDropRefusal,
|
|
34
|
+
type DragDropRenderPlaceholder,
|
|
29
35
|
type DragDropTarget,
|
|
36
|
+
type DragDropVerdict,
|
|
30
37
|
} from './components/DragDrop/DragDrop'
|
|
31
38
|
export {Dialog, type DialogSize} from './components/Dialog/Dialog'
|
|
32
39
|
export {DetailPanel, type DetailPanelSize} from './components/DetailPanel/DetailPanel'
|
|
@@ -45,6 +52,7 @@ export {
|
|
|
45
52
|
type LinkButtonSize,
|
|
46
53
|
} from './components/LinkButton/LinkButton'
|
|
47
54
|
export {Menu} from './components/Menu/Menu'
|
|
55
|
+
export {Meter, type MeterProps, type MeterTone} from './components/Meter/Meter'
|
|
48
56
|
export {PageNav} from './components/PageNav/PageNav'
|
|
49
57
|
export {Panel} from './components/Panel/Panel'
|
|
50
58
|
export {RadioGroup} from './components/RadioGroup/RadioGroup'
|
|
@@ -64,6 +72,7 @@ export {
|
|
|
64
72
|
type ToastOptions,
|
|
65
73
|
type ToastAction,
|
|
66
74
|
type ToastContextValue,
|
|
75
|
+
type ToastProviderProps,
|
|
67
76
|
} from './components/Toast'
|
|
68
77
|
export {
|
|
69
78
|
StatusIcon,
|
|
@@ -83,6 +92,14 @@ export {
|
|
|
83
92
|
type TableOfContentsVariant,
|
|
84
93
|
} from './components/TableOfContents/TableOfContents'
|
|
85
94
|
export {Tabs} from './components/Tabs/Tabs'
|
|
95
|
+
export {
|
|
96
|
+
Timeline,
|
|
97
|
+
type TimelineBarDropZone,
|
|
98
|
+
type TimelineBarProps,
|
|
99
|
+
type TimelineDate,
|
|
100
|
+
type TimelineRootProps,
|
|
101
|
+
type TimelineRowProps,
|
|
102
|
+
} from './components/Timeline/Timeline'
|
|
86
103
|
export {Tree} from './components/Tree/Tree'
|
|
87
104
|
export {Textarea, type TextareaVariant} from './components/Textarea/Textarea'
|
|
88
105
|
export {
|
|
@@ -103,6 +120,7 @@ export {Tooltip} from './components/Tooltip/Tooltip'
|
|
|
103
120
|
export {Toolbar, type ToolbarProps} from './components/Toolbar/Toolbar'
|
|
104
121
|
export {Popover} from './components/Popover/Popover'
|
|
105
122
|
export type {PopoverHandle} from './components/Popover/Popover'
|
|
123
|
+
export {useInModal} from './shared/ModalContext'
|
|
106
124
|
export {Switch} from './components/Switch/Switch'
|
|
107
125
|
|
|
108
126
|
// Layout primitives
|
|
@@ -13,6 +13,13 @@ import {createContext, useContext} from 'react'
|
|
|
13
13
|
*/
|
|
14
14
|
export const ModalContext = createContext(false)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* True inside a Dialog or Drawer, false outside. A floating surface that is not
|
|
18
|
+
* a Duro Popover picks its layer with it, the way a Popover does:
|
|
19
|
+
* `useInModal() ? layers.popover : layers.floating`, and for one that must
|
|
20
|
+
* cover another floating surface (a link editor over a format bar)
|
|
21
|
+
* `useInModal() ? layers.popoverRaised : layers.floatingRaised`.
|
|
22
|
+
*/
|
|
23
|
+
export function useInModal(): boolean {
|
|
17
24
|
return useContext(ModalContext)
|
|
18
25
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Compile-only checks that 5.6's additions are on the package root, where a
|
|
2
|
+
// consumer imports them. `pnpm typecheck` fails if one is dropped from
|
|
3
|
+
// index.ts or changes shape. Never imported at runtime.
|
|
4
|
+
|
|
5
|
+
import type {ButtonSize, ControlSize, ToggleSize, useInModal} from '../index'
|
|
6
|
+
|
|
7
|
+
type Same<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false
|
|
8
|
+
|
|
9
|
+
// A floating surface that is not a Popover picks its layer with it.
|
|
10
|
+
export const useInModalIsBoolean: Same<ReturnType<typeof useInModal>, boolean> = true
|
|
11
|
+
export const useInModalTakesNothing: Same<Parameters<typeof useInModal>, []> = true
|
|
12
|
+
|
|
13
|
+
// The shared size, and the two sizes that alias it.
|
|
14
|
+
export const controlSizes: Same<ControlSize, 'default' | 'small'> = true
|
|
15
|
+
export const buttonSizeIsControlSize: Same<ButtonSize, ControlSize> = true
|
|
16
|
+
export const toggleSizeIsControlSize: Same<ToggleSize, ControlSize> = true
|
package/src/shared/types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
// Cross-component prop types that don't belong to any single component.
|
|
2
2
|
|
|
3
3
|
export type Orientation = 'horizontal' | 'vertical'
|
|
4
|
+
|
|
5
|
+
/** The two control sizes shared by Button, Toggle and the Select, Menu and Popover triggers. */
|
|
6
|
+
export type ControlSize = 'default' | 'small'
|