@duro-app/ui 5.6.0 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/dist/{Table-B1sWL3GA.js → Table-CAjPFgTX.js} +398 -394
  2. package/dist/Table-CAjPFgTX.js.map +1 -0
  3. package/dist/{compiler-runtime-B2IcI8_O.js → compiler-runtime-BuugS4D0.js} +67 -59
  4. package/dist/{compiler-runtime-B2IcI8_O.js.map → compiler-runtime-BuugS4D0.js.map} +1 -1
  5. package/dist/components/AppShell/AppShell.d.ts +74 -0
  6. package/dist/components/AppShell/AppShell.d.ts.map +1 -0
  7. package/dist/components/AppShell/AppShell.meta.d.ts +3 -0
  8. package/dist/components/AppShell/AppShell.meta.d.ts.map +1 -0
  9. package/dist/components/AppShell/AppShell.stories.d.ts +186 -0
  10. package/dist/components/AppShell/AppShell.stories.d.ts.map +1 -0
  11. package/dist/components/AppShell/styles.css.d.ts +187 -0
  12. package/dist/components/AppShell/styles.css.d.ts.map +1 -0
  13. package/dist/components/Aside/Aside.d.ts +23 -0
  14. package/dist/components/Aside/Aside.d.ts.map +1 -0
  15. package/dist/components/Aside/Aside.meta.d.ts +3 -0
  16. package/dist/components/Aside/Aside.meta.d.ts.map +1 -0
  17. package/dist/components/Aside/Aside.stories.d.ts +45 -0
  18. package/dist/components/Aside/Aside.stories.d.ts.map +1 -0
  19. package/dist/components/Aside/styles.css.d.ts +44 -0
  20. package/dist/components/Aside/styles.css.d.ts.map +1 -0
  21. package/dist/components/Button/styles.css.d.ts +2 -0
  22. package/dist/components/Button/styles.css.d.ts.map +1 -1
  23. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +3 -3
  24. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
  25. package/dist/components/CodeBlock/CodeBlock.d.ts +29 -0
  26. package/dist/components/CodeBlock/CodeBlock.d.ts.map +1 -0
  27. package/dist/components/CodeBlock/CodeBlock.meta.d.ts +3 -0
  28. package/dist/components/CodeBlock/CodeBlock.meta.d.ts.map +1 -0
  29. package/dist/components/CodeBlock/CodeBlock.stories.d.ts +67 -0
  30. package/dist/components/CodeBlock/CodeBlock.stories.d.ts.map +1 -0
  31. package/dist/components/CodeBlock/styles.css.d.ts +32 -0
  32. package/dist/components/CodeBlock/styles.css.d.ts.map +1 -0
  33. package/dist/components/DragDrop/DragDrop.d.ts +95 -14
  34. package/dist/components/DragDrop/DragDrop.d.ts.map +1 -1
  35. package/dist/components/DragDrop/DragDrop.meta.d.ts.map +1 -1
  36. package/dist/components/DragDrop/DragDropBoard.stories.d.ts +69 -0
  37. package/dist/components/DragDrop/DragDropBoard.stories.d.ts.map +1 -0
  38. package/dist/components/DragDrop/styles.css.d.ts +80 -3
  39. package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
  40. package/dist/components/Grid/Grid.d.ts +13 -4
  41. package/dist/components/Grid/Grid.d.ts.map +1 -1
  42. package/dist/components/Grid/Grid.meta.d.ts.map +1 -1
  43. package/dist/components/Grid/Grid.stories.d.ts +28 -0
  44. package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
  45. package/dist/components/Grid/styles.css.d.ts +3 -0
  46. package/dist/components/Grid/styles.css.d.ts.map +1 -1
  47. package/dist/components/Heading/Heading.d.ts +3 -1
  48. package/dist/components/Heading/Heading.d.ts.map +1 -1
  49. package/dist/components/Heading/Heading.stories.d.ts +9 -0
  50. package/dist/components/Heading/Heading.stories.d.ts.map +1 -0
  51. package/dist/components/LinkButton/LinkButton.stories.d.ts +7 -0
  52. package/dist/components/LinkButton/LinkButton.stories.d.ts.map +1 -1
  53. package/dist/components/LinkButton/styles.css.d.ts +2 -0
  54. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  55. package/dist/components/LiveRegion/LiveRegion.d.ts +28 -0
  56. package/dist/components/LiveRegion/LiveRegion.d.ts.map +1 -0
  57. package/dist/components/LiveRegion/LiveRegion.meta.d.ts +3 -0
  58. package/dist/components/LiveRegion/LiveRegion.meta.d.ts.map +1 -0
  59. package/dist/components/LiveRegion/LiveRegion.stories.d.ts +66 -0
  60. package/dist/components/LiveRegion/LiveRegion.stories.d.ts.map +1 -0
  61. package/dist/components/Menu/Menu.stories.d.ts +7 -0
  62. package/dist/components/Menu/Menu.stories.d.ts.map +1 -1
  63. package/dist/components/Menu/styles.css.d.ts +2 -0
  64. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  65. package/dist/components/Meter/Meter.d.ts +17 -0
  66. package/dist/components/Meter/Meter.d.ts.map +1 -0
  67. package/dist/components/Meter/Meter.meta.d.ts +3 -0
  68. package/dist/components/Meter/Meter.meta.d.ts.map +1 -0
  69. package/dist/components/Meter/Meter.stories.d.ts +12 -0
  70. package/dist/components/Meter/Meter.stories.d.ts.map +1 -0
  71. package/dist/components/Meter/styles.css.d.ts +34 -0
  72. package/dist/components/Meter/styles.css.d.ts.map +1 -0
  73. package/dist/components/PageNav/styles.css.d.ts +1 -1
  74. package/dist/components/Prose/Prose.d.ts +17 -0
  75. package/dist/components/Prose/Prose.d.ts.map +1 -0
  76. package/dist/components/Prose/Prose.meta.d.ts +3 -0
  77. package/dist/components/Prose/Prose.meta.d.ts.map +1 -0
  78. package/dist/components/Prose/Prose.stories.d.ts +63 -0
  79. package/dist/components/Prose/Prose.stories.d.ts.map +1 -0
  80. package/dist/components/SideNav/SideNav.d.ts +7 -1
  81. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  82. package/dist/components/SideNav/SideNav.stories.d.ts +3 -0
  83. package/dist/components/SideNav/SideNav.stories.d.ts.map +1 -1
  84. package/dist/components/Time/Time.d.ts +26 -0
  85. package/dist/components/Time/Time.d.ts.map +1 -0
  86. package/dist/components/Time/Time.meta.d.ts +3 -0
  87. package/dist/components/Time/Time.meta.d.ts.map +1 -0
  88. package/dist/components/Time/Time.stories.d.ts +77 -0
  89. package/dist/components/Time/Time.stories.d.ts.map +1 -0
  90. package/dist/components/Time/relative.d.ts +27 -0
  91. package/dist/components/Time/relative.d.ts.map +1 -0
  92. package/dist/components/Timeline/Timeline.d.ts +71 -0
  93. package/dist/components/Timeline/Timeline.d.ts.map +1 -0
  94. package/dist/components/Timeline/Timeline.meta.d.ts +3 -0
  95. package/dist/components/Timeline/Timeline.meta.d.ts.map +1 -0
  96. package/dist/components/Timeline/Timeline.stories.d.ts +26 -0
  97. package/dist/components/Timeline/Timeline.stories.d.ts.map +1 -0
  98. package/dist/components/Timeline/styles.css.d.ts +158 -0
  99. package/dist/components/Timeline/styles.css.d.ts.map +1 -0
  100. package/dist/components/Toast/Toast.meta.d.ts.map +1 -1
  101. package/dist/components/Toast/Toast.stories.d.ts +9 -0
  102. package/dist/components/Toast/Toast.stories.d.ts.map +1 -1
  103. package/dist/components/Toast/ToastProvider.d.ts +11 -5
  104. package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
  105. package/dist/components/Toast/index.d.ts +1 -1
  106. package/dist/components/Toast/index.d.ts.map +1 -1
  107. package/dist/components/Toast/styles.css.d.ts +6 -0
  108. package/dist/components/Toast/styles.css.d.ts.map +1 -1
  109. package/dist/form.js +1 -1
  110. package/dist/hooks/useContainerBelow.d.ts +18 -0
  111. package/dist/hooks/useContainerBelow.d.ts.map +1 -0
  112. package/dist/index.css +1 -1
  113. package/dist/index.d.ts +13 -3
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +7316 -5536
  116. package/dist/index.js.map +1 -1
  117. package/dist/table.js +2 -2
  118. package/package.json +2 -2
  119. package/src/components/AppShell/AppShell.meta.ts +62 -0
  120. package/src/components/AppShell/AppShell.stories.tsx +770 -0
  121. package/src/components/AppShell/AppShell.tsx +346 -0
  122. package/src/components/AppShell/styles.css.ts +219 -0
  123. package/src/components/Aside/Aside.meta.ts +33 -0
  124. package/src/components/Aside/Aside.stories.tsx +106 -0
  125. package/src/components/Aside/Aside.tsx +42 -0
  126. package/src/components/Aside/styles.css.ts +60 -0
  127. package/src/components/Button/styles.css.ts +7 -2
  128. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +4 -5
  129. package/src/components/CodeBlock/CodeBlock.meta.ts +29 -0
  130. package/src/components/CodeBlock/CodeBlock.stories.tsx +162 -0
  131. package/src/components/CodeBlock/CodeBlock.tsx +99 -0
  132. package/src/components/CodeBlock/styles.css.ts +36 -0
  133. package/src/components/DragDrop/DragDrop.meta.ts +22 -21
  134. package/src/components/DragDrop/DragDrop.tsx +1147 -246
  135. package/src/components/DragDrop/DragDropBoard.stories.tsx +668 -0
  136. package/src/components/DragDrop/styles.css.ts +74 -4
  137. package/src/components/Grid/Grid.meta.ts +2 -1
  138. package/src/components/Grid/Grid.stories.tsx +202 -3
  139. package/src/components/Grid/Grid.tsx +35 -6
  140. package/src/components/Grid/styles.css.ts +11 -0
  141. package/src/components/Heading/Heading.stories.tsx +51 -0
  142. package/src/components/Heading/Heading.tsx +38 -7
  143. package/src/components/LinkButton/LinkButton.stories.tsx +41 -1
  144. package/src/components/LinkButton/styles.css.ts +6 -2
  145. package/src/components/LiveRegion/LiveRegion.meta.ts +29 -0
  146. package/src/components/LiveRegion/LiveRegion.stories.tsx +139 -0
  147. package/src/components/LiveRegion/LiveRegion.tsx +48 -0
  148. package/src/components/Menu/Menu.stories.tsx +62 -0
  149. package/src/components/Menu/styles.css.ts +6 -3
  150. package/src/components/Meter/Meter.meta.ts +24 -0
  151. package/src/components/Meter/Meter.stories.tsx +64 -0
  152. package/src/components/Meter/Meter.tsx +55 -0
  153. package/src/components/Meter/styles.css.ts +30 -0
  154. package/src/components/Prose/Prose.meta.ts +32 -0
  155. package/src/components/Prose/Prose.stories.tsx +233 -0
  156. package/src/components/Prose/Prose.tsx +25 -0
  157. package/src/components/Prose/prose.css +177 -0
  158. package/src/components/SideNav/SideNav.stories.tsx +29 -0
  159. package/src/components/SideNav/SideNav.tsx +14 -2
  160. package/src/components/Time/Time.meta.ts +17 -0
  161. package/src/components/Time/Time.stories.tsx +173 -0
  162. package/src/components/Time/Time.tsx +33 -0
  163. package/src/components/Time/relative.ts +89 -0
  164. package/src/components/Timeline/Timeline.meta.ts +46 -0
  165. package/src/components/Timeline/Timeline.stories.tsx +238 -0
  166. package/src/components/Timeline/Timeline.tsx +332 -0
  167. package/src/components/Timeline/styles.css.ts +162 -0
  168. package/src/components/Toast/Toast.meta.ts +2 -1
  169. package/src/components/Toast/Toast.stories.tsx +84 -0
  170. package/src/components/Toast/ToastProvider.tsx +49 -11
  171. package/src/components/Toast/index.ts +7 -1
  172. package/src/components/Toast/styles.css.ts +2 -0
  173. package/src/hooks/useContainerBelow.ts +39 -0
  174. package/src/index.ts +29 -1
  175. package/src/strict.css +4 -1
  176. 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
+ }