@duro-app/ui 5.6.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-B1sWL3GA.js → Table-Dj7PXqO_.js} +3 -2
- package/dist/Table-Dj7PXqO_.js.map +1 -0
- 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/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/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/index.css +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5029 -3927
- package/dist/index.js.map +1 -1
- package/dist/table.js +1 -1
- package/package.json +2 -2
- 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/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/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/index.ts +16 -0
- package/dist/Table-B1sWL3GA.js.map +0 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import {createContext, useContext, useMemo, type ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {DragDrop, type DragDropItemData, type DragDropVerdict} from '../DragDrop/DragDrop'
|
|
4
|
+
import {styles} from './styles.css'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Timeline — rows of date bars against a date axis: milestones, releases,
|
|
8
|
+
// sprints. Dates are calendar days ('YYYY-MM-DD', read as UTC) so a bar
|
|
9
|
+
// never shifts with the viewer's time zone.
|
|
10
|
+
//
|
|
11
|
+
// A bar is a toggle button (select it) with a progress fill, and can be a
|
|
12
|
+
// DragDrop.Zone target (`dropZone`) inside a DragDrop.Root. Its two edges are
|
|
13
|
+
// sliders whose value text is a date: Left/Right move a day, PageUp/PageDown
|
|
14
|
+
// a week, Home/End to the limit. They are keyboard-only — there is no
|
|
15
|
+
// pointer drag of an edge.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** A calendar day, 'YYYY-MM-DD'. */
|
|
19
|
+
export type TimelineDate = string
|
|
20
|
+
|
|
21
|
+
const DAY_MS = 86_400_000
|
|
22
|
+
|
|
23
|
+
const dayOf = (date: TimelineDate): number => Math.floor(Date.parse(`${date}T00:00:00Z`) / DAY_MS)
|
|
24
|
+
const dateOf = (day: number): TimelineDate => new Date(day * DAY_MS).toISOString().slice(0, 10)
|
|
25
|
+
|
|
26
|
+
interface TimelineContextValue {
|
|
27
|
+
readonly first: number
|
|
28
|
+
readonly days: number
|
|
29
|
+
readonly today: number | null
|
|
30
|
+
readonly todayLabel: string
|
|
31
|
+
readonly formatDate: (date: TimelineDate) => string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TimelineContext = createContext<TimelineContextValue | null>(null)
|
|
35
|
+
|
|
36
|
+
const useTimeline = (part: string) => {
|
|
37
|
+
const ctx = useContext(TimelineContext)
|
|
38
|
+
if (!ctx) throw new Error(`Timeline.${part} must be used within Timeline.Root`)
|
|
39
|
+
return ctx
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Percent of the axis from its first day to `day`. */
|
|
43
|
+
const at = (ctx: TimelineContextValue, day: number) => ((day - ctx.first) / ctx.days) * 100
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Root
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export interface TimelineRootProps {
|
|
50
|
+
/** First day on the axis. */
|
|
51
|
+
start: TimelineDate
|
|
52
|
+
/** Last day on the axis (included). */
|
|
53
|
+
end: TimelineDate
|
|
54
|
+
/** Draws the today marker when inside the range. */
|
|
55
|
+
today?: TimelineDate
|
|
56
|
+
/** The timeline's accessible name. */
|
|
57
|
+
'aria-label': string
|
|
58
|
+
/** Text above the row labels, e.g. "Milestones". */
|
|
59
|
+
heading?: ReactNode
|
|
60
|
+
/** The today marker's text. */
|
|
61
|
+
todayLabel?: string
|
|
62
|
+
/** BCP 47 locale for the default date format. */
|
|
63
|
+
locale?: string
|
|
64
|
+
/** Formats a day for the axis and the edge sliders' value text. */
|
|
65
|
+
formatDate?: (date: TimelineDate) => string
|
|
66
|
+
children: ReactNode
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function Root({
|
|
70
|
+
start,
|
|
71
|
+
end,
|
|
72
|
+
today,
|
|
73
|
+
'aria-label': ariaLabel,
|
|
74
|
+
heading,
|
|
75
|
+
todayLabel = 'Today',
|
|
76
|
+
locale,
|
|
77
|
+
formatDate,
|
|
78
|
+
children,
|
|
79
|
+
}: TimelineRootProps) {
|
|
80
|
+
const ctx = useMemo<TimelineContextValue>(() => {
|
|
81
|
+
const first = dayOf(start)
|
|
82
|
+
const days = Math.max(dayOf(end) - first + 1, 1)
|
|
83
|
+
const t = today === undefined ? null : dayOf(today)
|
|
84
|
+
return {
|
|
85
|
+
first,
|
|
86
|
+
days,
|
|
87
|
+
today: t !== null && t >= first && t < first + days ? t : null,
|
|
88
|
+
todayLabel,
|
|
89
|
+
formatDate:
|
|
90
|
+
formatDate ??
|
|
91
|
+
((date) =>
|
|
92
|
+
new Date(`${date}T00:00:00Z`).toLocaleDateString(locale, {
|
|
93
|
+
day: 'numeric',
|
|
94
|
+
month: 'short',
|
|
95
|
+
timeZone: 'UTC',
|
|
96
|
+
})),
|
|
97
|
+
}
|
|
98
|
+
}, [start, end, today, todayLabel, locale, formatDate])
|
|
99
|
+
|
|
100
|
+
// Daily ticks for up to three weeks, weekly beyond.
|
|
101
|
+
const step = ctx.days > 21 ? 7 : 1
|
|
102
|
+
const ticks: number[] = []
|
|
103
|
+
for (let d = ctx.first; d < ctx.first + ctx.days; d += step) ticks.push(d)
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<TimelineContext.Provider value={ctx}>
|
|
107
|
+
<html.div role="group" aria-label={ariaLabel} style={styles.root}>
|
|
108
|
+
<html.div style={styles.heading}>{heading}</html.div>
|
|
109
|
+
<html.div aria-hidden style={styles.axis}>
|
|
110
|
+
{ticks.map((d) => (
|
|
111
|
+
<html.span key={d} style={[styles.tick, styles.left(at(ctx, d))]}>
|
|
112
|
+
{ctx.formatDate(dateOf(d))}
|
|
113
|
+
</html.span>
|
|
114
|
+
))}
|
|
115
|
+
{ctx.today !== null && (
|
|
116
|
+
<html.span style={[styles.todayLabel, styles.left(at(ctx, ctx.today + 0.5))]}>
|
|
117
|
+
{todayLabel}
|
|
118
|
+
</html.span>
|
|
119
|
+
)}
|
|
120
|
+
</html.div>
|
|
121
|
+
{children}
|
|
122
|
+
</html.div>
|
|
123
|
+
</TimelineContext.Provider>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Row
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
export interface TimelineRowProps {
|
|
132
|
+
/** The row's name, shown in the label column. */
|
|
133
|
+
label: string
|
|
134
|
+
/** A second line under the label, e.g. "4 of 7 done". */
|
|
135
|
+
description?: ReactNode
|
|
136
|
+
children?: ReactNode
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function Row({label, description, children}: TimelineRowProps) {
|
|
140
|
+
const ctx = useTimeline('Row')
|
|
141
|
+
return (
|
|
142
|
+
<>
|
|
143
|
+
<html.div style={styles.label}>
|
|
144
|
+
<html.span style={styles.labelText}>{label}</html.span>
|
|
145
|
+
{description !== undefined && (
|
|
146
|
+
<html.span style={styles.labelDescription}>{description}</html.span>
|
|
147
|
+
)}
|
|
148
|
+
</html.div>
|
|
149
|
+
<html.div role="group" aria-label={label} style={styles.track}>
|
|
150
|
+
{ctx.today !== null && (
|
|
151
|
+
<html.div aria-hidden style={[styles.today, styles.left(at(ctx, ctx.today + 0.5))]} />
|
|
152
|
+
)}
|
|
153
|
+
{children}
|
|
154
|
+
</html.div>
|
|
155
|
+
</>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Bar
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
export interface TimelineBarDropZone<T = unknown> {
|
|
164
|
+
/** The DragDrop zone id; drops arrive in the Root's onDrop with it. */
|
|
165
|
+
id: string
|
|
166
|
+
/** Read to screen readers when an item moves or drops here. */
|
|
167
|
+
label: string
|
|
168
|
+
accepts?: (item: DragDropItemData<T>) => DragDropVerdict
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface TimelineBarProps<T = unknown> {
|
|
172
|
+
start: TimelineDate
|
|
173
|
+
end: TimelineDate
|
|
174
|
+
/** The bar's accessible name. */
|
|
175
|
+
label: string
|
|
176
|
+
/** Done fraction, 0–1, drawn as a fill from the start edge. */
|
|
177
|
+
progress?: number
|
|
178
|
+
/** `planned` draws a dashed outline; `done` the success tone. */
|
|
179
|
+
tone?: 'default' | 'planned' | 'done'
|
|
180
|
+
/** Makes the bar a toggle button (aria-pressed). */
|
|
181
|
+
selected?: boolean
|
|
182
|
+
onSelect?: () => void
|
|
183
|
+
/** Adds the start-edge slider. */
|
|
184
|
+
onStartChange?: (date: TimelineDate) => void
|
|
185
|
+
/** Adds the end-edge slider. */
|
|
186
|
+
onEndChange?: (date: TimelineDate) => void
|
|
187
|
+
/** Names of the edge sliders; default "<label>, start" / "<label>, end". */
|
|
188
|
+
startLabel?: string
|
|
189
|
+
endLabel?: string
|
|
190
|
+
/** Makes the bar a DragDrop.Zone (needs a DragDrop.Root above). */
|
|
191
|
+
dropZone?: TimelineBarDropZone<T>
|
|
192
|
+
/** Text inside the bar, e.g. "4/7". */
|
|
193
|
+
children?: ReactNode
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const toneStyle = {
|
|
197
|
+
default: null,
|
|
198
|
+
planned: styles.barPlanned,
|
|
199
|
+
done: styles.barDone,
|
|
200
|
+
} as const
|
|
201
|
+
|
|
202
|
+
function Bar<T = unknown>({
|
|
203
|
+
start,
|
|
204
|
+
end,
|
|
205
|
+
label,
|
|
206
|
+
progress,
|
|
207
|
+
tone = 'default',
|
|
208
|
+
selected,
|
|
209
|
+
onSelect,
|
|
210
|
+
onStartChange,
|
|
211
|
+
onEndChange,
|
|
212
|
+
startLabel,
|
|
213
|
+
endLabel,
|
|
214
|
+
dropZone,
|
|
215
|
+
children,
|
|
216
|
+
}: TimelineBarProps<T>) {
|
|
217
|
+
const ctx = useTimeline('Bar')
|
|
218
|
+
const s = dayOf(start)
|
|
219
|
+
const e = dayOf(end)
|
|
220
|
+
const left = at(ctx, s)
|
|
221
|
+
const width = ((e - s + 1) / ctx.days) * 100
|
|
222
|
+
const last = ctx.first + ctx.days - 1
|
|
223
|
+
|
|
224
|
+
const face = (
|
|
225
|
+
<>
|
|
226
|
+
<html.button
|
|
227
|
+
type="button"
|
|
228
|
+
aria-label={label}
|
|
229
|
+
aria-pressed={selected}
|
|
230
|
+
onClick={onSelect}
|
|
231
|
+
style={[styles.face, toneStyle[tone], selected === true && styles.faceSelected]}
|
|
232
|
+
>
|
|
233
|
+
{progress !== undefined && progress > 0 && (
|
|
234
|
+
<html.span
|
|
235
|
+
aria-hidden
|
|
236
|
+
style={[
|
|
237
|
+
styles.fill,
|
|
238
|
+
tone === 'done' && styles.fillDone,
|
|
239
|
+
styles.width(Math.min(Math.max(progress, 0), 1) * 100),
|
|
240
|
+
]}
|
|
241
|
+
/>
|
|
242
|
+
)}
|
|
243
|
+
{children !== undefined && <html.span style={styles.barText}>{children}</html.span>}
|
|
244
|
+
</html.button>
|
|
245
|
+
{onStartChange && (
|
|
246
|
+
<EdgeSlider
|
|
247
|
+
edge="start"
|
|
248
|
+
label={startLabel ?? `${label}, start`}
|
|
249
|
+
day={s}
|
|
250
|
+
min={ctx.first}
|
|
251
|
+
max={e}
|
|
252
|
+
onChange={onStartChange}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
{onEndChange && (
|
|
256
|
+
<EdgeSlider
|
|
257
|
+
edge="end"
|
|
258
|
+
label={endLabel ?? `${label}, end`}
|
|
259
|
+
day={e}
|
|
260
|
+
min={s}
|
|
261
|
+
max={last}
|
|
262
|
+
onChange={onEndChange}
|
|
263
|
+
/>
|
|
264
|
+
)}
|
|
265
|
+
</>
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<html.div style={[styles.bar, styles.left(left), styles.width(width)]}>
|
|
270
|
+
{dropZone ? (
|
|
271
|
+
<DragDrop.Zone<T> id={dropZone.id} label={dropZone.label} accepts={dropZone.accepts} fill>
|
|
272
|
+
{face}
|
|
273
|
+
</DragDrop.Zone>
|
|
274
|
+
) : (
|
|
275
|
+
face
|
|
276
|
+
)}
|
|
277
|
+
</html.div>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function EdgeSlider({
|
|
282
|
+
edge,
|
|
283
|
+
label,
|
|
284
|
+
day,
|
|
285
|
+
min,
|
|
286
|
+
max,
|
|
287
|
+
onChange,
|
|
288
|
+
}: {
|
|
289
|
+
edge: 'start' | 'end'
|
|
290
|
+
label: string
|
|
291
|
+
day: number
|
|
292
|
+
min: number
|
|
293
|
+
max: number
|
|
294
|
+
onChange: (date: TimelineDate) => void
|
|
295
|
+
}) {
|
|
296
|
+
const ctx = useTimeline('Bar')
|
|
297
|
+
const onKeyDown = (event: unknown) => {
|
|
298
|
+
const e = event as {key: string; preventDefault(): void}
|
|
299
|
+
const by: Record<string, number> = {
|
|
300
|
+
ArrowLeft: -1,
|
|
301
|
+
ArrowDown: -1,
|
|
302
|
+
ArrowRight: 1,
|
|
303
|
+
ArrowUp: 1,
|
|
304
|
+
PageDown: -7,
|
|
305
|
+
PageUp: 7,
|
|
306
|
+
}
|
|
307
|
+
let next: number
|
|
308
|
+
if (e.key in by) next = day + by[e.key]
|
|
309
|
+
else if (e.key === 'Home') next = min
|
|
310
|
+
else if (e.key === 'End') next = max
|
|
311
|
+
else return
|
|
312
|
+
e.preventDefault()
|
|
313
|
+
next = Math.min(Math.max(next, min), max)
|
|
314
|
+
if (next !== day) onChange(dateOf(next))
|
|
315
|
+
}
|
|
316
|
+
return (
|
|
317
|
+
<html.div
|
|
318
|
+
role="slider"
|
|
319
|
+
tabIndex={0}
|
|
320
|
+
aria-label={label}
|
|
321
|
+
aria-orientation="horizontal"
|
|
322
|
+
aria-valuemin={min - ctx.first}
|
|
323
|
+
aria-valuemax={max - ctx.first}
|
|
324
|
+
aria-valuenow={day - ctx.first}
|
|
325
|
+
aria-valuetext={ctx.formatDate(dateOf(day))}
|
|
326
|
+
onKeyDown={onKeyDown}
|
|
327
|
+
style={[styles.edge, edge === 'start' ? styles.edgeStart : styles.edgeEnd]}
|
|
328
|
+
/>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const Timeline = {Root, Row, Bar}
|
|
@@ -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
|
+
}
|