@charcuterie/ui 4.6.0 → 4.7.1
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/README.md +12 -0
- package/dist/LaneTimeline/LaneTimeline.d.ts +132 -0
- package/dist/LaneTimeline/LaneTimeline.d.ts.map +1 -0
- package/dist/LaneTimeline/LaneTimeline.js +165 -0
- package/dist/LaneTimeline/LaneTimeline.js.map +1 -0
- package/dist/LaneTimeline/LaneTimelineBar.d.ts +97 -0
- package/dist/LaneTimeline/LaneTimelineBar.d.ts.map +1 -0
- package/dist/LaneTimeline/LaneTimelineBar.js +69 -0
- package/dist/LaneTimeline/LaneTimelineBar.js.map +1 -0
- package/dist/LaneTimeline/LaneTimelineLane.d.ts +84 -0
- package/dist/LaneTimeline/LaneTimelineLane.d.ts.map +1 -0
- package/dist/LaneTimeline/LaneTimelineLane.js +109 -0
- package/dist/LaneTimeline/LaneTimelineLane.js.map +1 -0
- package/dist/LaneTimeline/laneTimelineGeometry.d.ts +268 -0
- package/dist/LaneTimeline/laneTimelineGeometry.d.ts.map +1 -0
- package/dist/LaneTimeline/laneTimelineGeometry.js +277 -0
- package/dist/LaneTimeline/laneTimelineGeometry.js.map +1 -0
- package/dist/LaneTimeline/useTrackMeasurements.d.ts +69 -0
- package/dist/LaneTimeline/useTrackMeasurements.d.ts.map +1 -0
- package/dist/LaneTimeline/useTrackMeasurements.js +134 -0
- package/dist/LaneTimeline/useTrackMeasurements.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/testing/expectAgentDrivable.d.ts +1 -1
- package/dist/testing/expectAgentDrivable.js +1 -1
- package/package.json +3 -3
- package/src/LaneTimeline/LaneTimeline.mdx +191 -0
- package/src/LaneTimeline/LaneTimeline.stories.tsx +561 -0
- package/src/LaneTimeline/LaneTimeline.test.tsx +330 -0
- package/src/LaneTimeline/LaneTimeline.tsx +381 -0
- package/src/LaneTimeline/LaneTimelineBar.tsx +237 -0
- package/src/LaneTimeline/LaneTimelineLane.tsx +332 -0
- package/src/LaneTimeline/laneTimelineGeometry.test.ts +429 -0
- package/src/LaneTimeline/laneTimelineGeometry.ts +476 -0
- package/src/LaneTimeline/useTrackMeasurements.ts +165 -0
- package/src/Toast/Toast.mdx +1 -1
- package/src/index.ts +13 -0
- package/src/sourceRules.test.ts +22 -1
- package/src/testing/expectAgentDrivable.ts +1 -1
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import type { ReactNode } from "react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ContainerBoard,
|
|
8
|
+
StoryCell,
|
|
9
|
+
StoryGrid,
|
|
10
|
+
} from "../board.storyHelpers.tsx"
|
|
11
|
+
import { EmptyState } from "../EmptyState/EmptyState.tsx"
|
|
12
|
+
import { LaneTimeline } from "./LaneTimeline.tsx"
|
|
13
|
+
import type { TimelineItem } from "./LaneTimelineBar.tsx"
|
|
14
|
+
import type { TimelineLane } from "./LaneTimelineLane.tsx"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* **Invented, all of it.** The calendar this component was built for
|
|
18
|
+
* is a real household, and a fixture taken from one would be
|
|
19
|
+
* unremovable the moment it reached a committed screenshot — a PNG
|
|
20
|
+
* is opaque to every grep and no text scrub redacts it.
|
|
21
|
+
*
|
|
22
|
+
* So this is a fictional recording studio's week. The names are the
|
|
23
|
+
* same four the `Board` stories use, because a reader moving between
|
|
24
|
+
* two docs pages should not have to learn a second cast.
|
|
25
|
+
*
|
|
26
|
+
* Every date is **April 2026**, and `2026-04-06` is a Monday — so a
|
|
27
|
+
* seven-day week is `06` to `12`, and the inclusive end is something
|
|
28
|
+
* a reader can check by counting columns rather than by trusting
|
|
29
|
+
* prose.
|
|
30
|
+
*/
|
|
31
|
+
const WEEK = {
|
|
32
|
+
end: "2026-04-12",
|
|
33
|
+
start: "2026-04-06",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MONTH = {
|
|
37
|
+
end: "2026-04-30",
|
|
38
|
+
start: "2026-04-01",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The load asymmetry, which is the whole reason the component exists
|
|
43
|
+
* rather than a month grid. Wren is carrying most of this week and
|
|
44
|
+
* Ada is carrying one afternoon of it, and that is meant to be
|
|
45
|
+
* obvious before anybody reads a word: Wren's lane stacks three bars
|
|
46
|
+
* deep and is three times the height of Ada's.
|
|
47
|
+
*/
|
|
48
|
+
const LANES: TimelineLane[] = [
|
|
49
|
+
{
|
|
50
|
+
items: [
|
|
51
|
+
{
|
|
52
|
+
end: "2026-04-08",
|
|
53
|
+
key: "foley",
|
|
54
|
+
start: "2026-04-07",
|
|
55
|
+
title: "Foley session",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
key: "ada",
|
|
59
|
+
label: "Ada",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
items: [
|
|
63
|
+
{
|
|
64
|
+
end: "2026-04-12",
|
|
65
|
+
key: "on-call",
|
|
66
|
+
start: "2026-04-06",
|
|
67
|
+
title: "On call for the rig",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
end: "2026-04-09",
|
|
71
|
+
key: "field-trip",
|
|
72
|
+
start: "2026-04-06",
|
|
73
|
+
title: "Field recording trip",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
end: "2026-04-06",
|
|
77
|
+
key: "mic-locker",
|
|
78
|
+
start: "2026-04-06",
|
|
79
|
+
title: "Mic locker inventory",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
end: "2026-04-10",
|
|
83
|
+
key: "mixing",
|
|
84
|
+
start: "2026-04-08",
|
|
85
|
+
title: "Mixing block",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
end: "2026-04-09",
|
|
89
|
+
key: "listen-back",
|
|
90
|
+
start: "2026-04-09",
|
|
91
|
+
title: "Client listen back",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
end: "2026-04-12",
|
|
95
|
+
key: "stems",
|
|
96
|
+
start: "2026-04-10",
|
|
97
|
+
title: "Stems delivery",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
key: "wren",
|
|
101
|
+
label: "Wren",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
items: [
|
|
105
|
+
{
|
|
106
|
+
end: "2026-04-08",
|
|
107
|
+
key: "score-revisions",
|
|
108
|
+
start: "2026-04-06",
|
|
109
|
+
title: "Score revisions",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
end: "2026-04-10",
|
|
113
|
+
key: "coast-train",
|
|
114
|
+
start: "2026-04-10",
|
|
115
|
+
title: "Train to the coast",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
end: "2026-04-12",
|
|
119
|
+
key: "rig-service",
|
|
120
|
+
start: "2026-04-11",
|
|
121
|
+
title: "Rig service",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
key: "juno",
|
|
125
|
+
label: "Juno",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
items: [
|
|
129
|
+
{
|
|
130
|
+
end: "2026-04-11",
|
|
131
|
+
key: "patch-bay",
|
|
132
|
+
start: "2026-04-09",
|
|
133
|
+
title: "Patch bay rewire",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
key: "rook",
|
|
137
|
+
label: "Rook",
|
|
138
|
+
},
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The four edge cases, in one lane, because each of them is a place
|
|
143
|
+
* a naive implementation draws nothing or draws it somewhere else —
|
|
144
|
+
* and none of the four is visible in a screenshot without a correct
|
|
145
|
+
* one beside it.
|
|
146
|
+
*/
|
|
147
|
+
const EDGE_CASE_ITEMS: TimelineItem[] = [
|
|
148
|
+
{
|
|
149
|
+
// Starts nine days before the week. Clipped to column 1, square
|
|
150
|
+
// leading edge, and its hidden sentence still says 30 March.
|
|
151
|
+
end: "2026-04-08",
|
|
152
|
+
key: "long-hire",
|
|
153
|
+
start: "2026-03-30",
|
|
154
|
+
title: "Desk hire, started last month",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
// Runs to the end of April. Clipped to the last column, square
|
|
158
|
+
// trailing edge.
|
|
159
|
+
end: "2026-04-30",
|
|
160
|
+
key: "residency",
|
|
161
|
+
start: "2026-04-10",
|
|
162
|
+
title: "Residency, runs into May",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
// One day. A bar, not a dot — one vocabulary on the axis.
|
|
166
|
+
end: "2026-04-09",
|
|
167
|
+
key: "delivery",
|
|
168
|
+
start: "2026-04-09",
|
|
169
|
+
title: "Tape delivery",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
// `end` before `start`: somebody typed the dates the wrong way
|
|
173
|
+
// round. Drawn on its start day rather than dropped, because a
|
|
174
|
+
// dropped row is a data fault nobody can see.
|
|
175
|
+
end: "2026-04-06",
|
|
176
|
+
key: "reversed",
|
|
177
|
+
start: "2026-04-11",
|
|
178
|
+
title: "Kit return, dates entered backwards",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
// Entirely outside the week. Not drawn at all, and not counted —
|
|
182
|
+
// nothing is missing from this picture, the picture is of
|
|
183
|
+
// another week.
|
|
184
|
+
end: "2026-04-26",
|
|
185
|
+
key: "next-month",
|
|
186
|
+
start: "2026-04-20",
|
|
187
|
+
title: "Next month's session",
|
|
188
|
+
},
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A **fixed** inline size, because the fold is a measurement of the
|
|
193
|
+
* component's own box. A story that inherited the canvas width would
|
|
194
|
+
* draw an axis on one machine and the Narrow View on another, and a
|
|
195
|
+
* screenshot of it would be a screenshot of somebody's window.
|
|
196
|
+
*/
|
|
197
|
+
const Frame = ({
|
|
198
|
+
children,
|
|
199
|
+
inlineSize,
|
|
200
|
+
}: {
|
|
201
|
+
children: ReactNode
|
|
202
|
+
inlineSize: string
|
|
203
|
+
}): ReactNode => (
|
|
204
|
+
<div style={{ inlineSize }}>{children}</div>
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
const meta = {
|
|
208
|
+
title: "Components/Data/LaneTimeline",
|
|
209
|
+
component: LaneTimeline,
|
|
210
|
+
parameters: { layout: "padded" },
|
|
211
|
+
args: {
|
|
212
|
+
headingLevel: 3,
|
|
213
|
+
label: "The week",
|
|
214
|
+
lanes: LANES,
|
|
215
|
+
/*
|
|
216
|
+
* Pinned, and not because anybody prefers it. `Intl` with no
|
|
217
|
+
* locale uses the runtime's, so the same story renders
|
|
218
|
+
* "Mon 6 Apr" on one machine and "Mon, Apr 6" on the next — and
|
|
219
|
+
* the test asserting the bar's name would then be asserting the
|
|
220
|
+
* test runner's locale. An app passes its own, or omits it and
|
|
221
|
+
* gets the reader's.
|
|
222
|
+
*/
|
|
223
|
+
locale: "en-GB",
|
|
224
|
+
maxRowCount: 6,
|
|
225
|
+
minColumnSize: 1.75,
|
|
226
|
+
range: WEEK,
|
|
227
|
+
},
|
|
228
|
+
} satisfies Meta<typeof LaneTimeline>
|
|
229
|
+
|
|
230
|
+
export default meta
|
|
231
|
+
|
|
232
|
+
type Story = StoryObj<typeof meta>
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Four lanes across one week, at the density a real schedule
|
|
236
|
+
* reaches.
|
|
237
|
+
*
|
|
238
|
+
* Read it for the **shape** rather than the entries: Wren's lane is
|
|
239
|
+
* three bars deep for most of the week and Ada's is one bar on one
|
|
240
|
+
* afternoon. That is the question a month grid cannot answer, because
|
|
241
|
+
* a grid gives every day the same box no matter what is in it.
|
|
242
|
+
*/
|
|
243
|
+
export const Playground: Story = {
|
|
244
|
+
parameters: playgroundParameters,
|
|
245
|
+
render: (timelineProps) => (
|
|
246
|
+
<Frame inlineSize="64rem">
|
|
247
|
+
<LaneTimeline {...timelineProps} />
|
|
248
|
+
</Frame>
|
|
249
|
+
),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Plain bars, lanes whose headings navigate, and bars that act.
|
|
254
|
+
*
|
|
255
|
+
* `href` and `onSelect` are a **union** on the item, not two
|
|
256
|
+
* optionals: the first renders an `<a>` and the second a `<button>`,
|
|
257
|
+
* so an item carrying both would be one of them with the other
|
|
258
|
+
* silently dropped, and the two are identical on screen.
|
|
259
|
+
*/
|
|
260
|
+
export const AllVariants: Story = {
|
|
261
|
+
render: (timelineProps) => (
|
|
262
|
+
<StoryGrid columns={1}>
|
|
263
|
+
<StoryCell
|
|
264
|
+
align="stretch"
|
|
265
|
+
label="plain — nothing to press, which is a real mode for a wall display"
|
|
266
|
+
>
|
|
267
|
+
<Frame inlineSize="56rem">
|
|
268
|
+
<LaneTimeline
|
|
269
|
+
{...timelineProps}
|
|
270
|
+
label="The week, read-only"
|
|
271
|
+
/>
|
|
272
|
+
</Frame>
|
|
273
|
+
</StoryCell>
|
|
274
|
+
|
|
275
|
+
<StoryCell
|
|
276
|
+
align="stretch"
|
|
277
|
+
label="lane headings that navigate — the show-me-only-this-lane route"
|
|
278
|
+
>
|
|
279
|
+
<Frame inlineSize="56rem">
|
|
280
|
+
<LaneTimeline
|
|
281
|
+
{...timelineProps}
|
|
282
|
+
label="The week, with linked lanes"
|
|
283
|
+
lanes={LANES.map((lane) => ({
|
|
284
|
+
...lane,
|
|
285
|
+
href: `/lane/${lane.key}`,
|
|
286
|
+
}))}
|
|
287
|
+
/>
|
|
288
|
+
</Frame>
|
|
289
|
+
</StoryCell>
|
|
290
|
+
|
|
291
|
+
<StoryCell
|
|
292
|
+
align="stretch"
|
|
293
|
+
label="bars that navigate — every bar is a real `<a href>`"
|
|
294
|
+
>
|
|
295
|
+
<Frame inlineSize="56rem">
|
|
296
|
+
<LaneTimeline
|
|
297
|
+
{...timelineProps}
|
|
298
|
+
label="The week, with linked bars"
|
|
299
|
+
lanes={LANES.map((lane) => ({
|
|
300
|
+
...lane,
|
|
301
|
+
/*
|
|
302
|
+
* The two action keys are taken off before the new
|
|
303
|
+
* one goes on. A spread of a union widens both of
|
|
304
|
+
* them back to optional, and the type then refuses
|
|
305
|
+
* the result — which is the type doing its job: an
|
|
306
|
+
* item with an `href` *and* an `onSelect` is an `<a>`
|
|
307
|
+
* with a dropped handler, and it looks correct.
|
|
308
|
+
*/
|
|
309
|
+
items: lane.items.map(
|
|
310
|
+
({
|
|
311
|
+
href: _href,
|
|
312
|
+
onSelect: _onSelect,
|
|
313
|
+
...item
|
|
314
|
+
}): TimelineItem => ({
|
|
315
|
+
...item,
|
|
316
|
+
href: `/item/${item.key}`,
|
|
317
|
+
}),
|
|
318
|
+
),
|
|
319
|
+
}))}
|
|
320
|
+
/>
|
|
321
|
+
</Frame>
|
|
322
|
+
</StoryCell>
|
|
323
|
+
</StoryGrid>
|
|
324
|
+
),
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The states nobody screenshots: the four edge cases, a lane past
|
|
329
|
+
* its row limit, a lane with nothing on, and a timeline with no
|
|
330
|
+
* lanes at all.
|
|
331
|
+
*
|
|
332
|
+
* `maxRowCount={2}` on the second panel is the honest-overflow rule
|
|
333
|
+
* `Board`'s `+ n more` already follows. A lane that silently drops
|
|
334
|
+
* its ninth overlapping thing is a lane that lies about how loaded
|
|
335
|
+
* it is, which is the one question this component answers.
|
|
336
|
+
*/
|
|
337
|
+
export const AllStates: Story = {
|
|
338
|
+
render: (timelineProps) => (
|
|
339
|
+
<StoryGrid columns={1}>
|
|
340
|
+
<StoryCell
|
|
341
|
+
align="stretch"
|
|
342
|
+
label="the four edge cases, plus one item that is in another week entirely"
|
|
343
|
+
>
|
|
344
|
+
<Frame inlineSize="56rem">
|
|
345
|
+
<LaneTimeline
|
|
346
|
+
{...timelineProps}
|
|
347
|
+
label="The week, edge cases"
|
|
348
|
+
lanes={[
|
|
349
|
+
{
|
|
350
|
+
items: EDGE_CASE_ITEMS,
|
|
351
|
+
key: "studio",
|
|
352
|
+
label: "Studio floor",
|
|
353
|
+
},
|
|
354
|
+
]}
|
|
355
|
+
/>
|
|
356
|
+
</Frame>
|
|
357
|
+
</StoryCell>
|
|
358
|
+
|
|
359
|
+
<StoryCell
|
|
360
|
+
align="stretch"
|
|
361
|
+
label="a lane past its row limit, and a lane with nothing on"
|
|
362
|
+
>
|
|
363
|
+
<Frame inlineSize="56rem">
|
|
364
|
+
<LaneTimeline
|
|
365
|
+
{...timelineProps}
|
|
366
|
+
label="The week, at the row limit"
|
|
367
|
+
lanes={[
|
|
368
|
+
LANES[1] as TimelineLane,
|
|
369
|
+
{
|
|
370
|
+
items: [],
|
|
371
|
+
key: "spare",
|
|
372
|
+
label: "Spare room",
|
|
373
|
+
},
|
|
374
|
+
]}
|
|
375
|
+
maxRowCount={2}
|
|
376
|
+
/>
|
|
377
|
+
</Frame>
|
|
378
|
+
</StoryCell>
|
|
379
|
+
|
|
380
|
+
<StoryCell
|
|
381
|
+
align="stretch"
|
|
382
|
+
label="no lanes — a real `EmptyState`, which is what a good week looks like"
|
|
383
|
+
>
|
|
384
|
+
<Frame inlineSize="56rem">
|
|
385
|
+
<LaneTimeline
|
|
386
|
+
{...timelineProps}
|
|
387
|
+
label="The week, empty"
|
|
388
|
+
lanes={[]}
|
|
389
|
+
/>
|
|
390
|
+
</Frame>
|
|
391
|
+
</StoryCell>
|
|
392
|
+
|
|
393
|
+
<StoryCell
|
|
394
|
+
align="stretch"
|
|
395
|
+
label="the consumer's own empty state"
|
|
396
|
+
>
|
|
397
|
+
<Frame inlineSize="56rem">
|
|
398
|
+
<LaneTimeline
|
|
399
|
+
{...timelineProps}
|
|
400
|
+
emptyState={
|
|
401
|
+
<EmptyState
|
|
402
|
+
description="Add a lane for each person and the week fills itself in."
|
|
403
|
+
heading="Nobody has a lane yet"
|
|
404
|
+
headingLevel={3}
|
|
405
|
+
size="sm"
|
|
406
|
+
/>
|
|
407
|
+
}
|
|
408
|
+
label="The week, empty with a custom message"
|
|
409
|
+
lanes={[]}
|
|
410
|
+
/>
|
|
411
|
+
</Frame>
|
|
412
|
+
</StoryCell>
|
|
413
|
+
</StoryGrid>
|
|
414
|
+
),
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* **The story that proves the fold**, and the only honest way to show
|
|
419
|
+
* it: three fixed-width panels inside one browser window that never
|
|
420
|
+
* moves.
|
|
421
|
+
*
|
|
422
|
+
* The narrow panel is not a phone. It is the same timeline in a
|
|
423
|
+
* sidebar, in a split window, or on a display somebody has zoomed to
|
|
424
|
+
* 175% — and the component cannot tell those apart from each other,
|
|
425
|
+
* only from a box with room for an axis in it.
|
|
426
|
+
*
|
|
427
|
+
* Watch what the fold actually is. It is **not** a width: it is the
|
|
428
|
+
* width divided by the number of days, so the same 24rem panel keeps
|
|
429
|
+
* its axis for a week and loses it for a quarter. When it goes, the
|
|
430
|
+
* axis is abandoned rather than squeezed — each lane becomes a
|
|
431
|
+
* one-column list with every span written out in words, and nothing
|
|
432
|
+
* scrolls sideways.
|
|
433
|
+
*/
|
|
434
|
+
export const Responsive: Story = {
|
|
435
|
+
render: (timelineProps) => (
|
|
436
|
+
<div className="flex flex-col gap-8">
|
|
437
|
+
<ContainerBoard>
|
|
438
|
+
{(width) => (
|
|
439
|
+
<LaneTimeline
|
|
440
|
+
{...timelineProps}
|
|
441
|
+
label={`The week at ${width}`}
|
|
442
|
+
/>
|
|
443
|
+
)}
|
|
444
|
+
</ContainerBoard>
|
|
445
|
+
|
|
446
|
+
<div className="flex flex-col gap-2">
|
|
447
|
+
<span className="font-mono text-content-muted text-xs">
|
|
448
|
+
the same three widths, one month instead of one
|
|
449
|
+
week
|
|
450
|
+
</span>
|
|
451
|
+
|
|
452
|
+
<ContainerBoard>
|
|
453
|
+
{(width) => (
|
|
454
|
+
<LaneTimeline
|
|
455
|
+
{...timelineProps}
|
|
456
|
+
label={`The month at ${width}`}
|
|
457
|
+
range={MONTH}
|
|
458
|
+
/>
|
|
459
|
+
)}
|
|
460
|
+
</ContainerBoard>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
),
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The Narrow View on its own, big enough to read.
|
|
468
|
+
*
|
|
469
|
+
* Each lane keeps its name, its count and its colour, and every item
|
|
470
|
+
* becomes a line with its span in words underneath it. Nothing is
|
|
471
|
+
* behind a gesture and nothing is off the side of the box — *"no
|
|
472
|
+
* horizontal scroll, ever"*, the same rule `Board` settled, for the
|
|
473
|
+
* same reason: a pan surface hides content with no affordance and
|
|
474
|
+
* fights the trackpad's own back-navigation.
|
|
475
|
+
*/
|
|
476
|
+
export const NarrowView: Story = {
|
|
477
|
+
render: (timelineProps) => (
|
|
478
|
+
// 20rem is a small phone, and it is deliberately a width rather
|
|
479
|
+
// than a device: the same layout is what a docked sidebar gets on
|
|
480
|
+
// a 4K monitor.
|
|
481
|
+
<Frame inlineSize="20rem">
|
|
482
|
+
<LaneTimeline
|
|
483
|
+
{...timelineProps}
|
|
484
|
+
label="The week, Narrow View"
|
|
485
|
+
/>
|
|
486
|
+
</Frame>
|
|
487
|
+
),
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* A month, where the tick labels **thin** rather than shrink.
|
|
492
|
+
*
|
|
493
|
+
* Type that shrinks to fit is type somebody cannot read, and the
|
|
494
|
+
* density axis already owns font size. So the axis drops to one
|
|
495
|
+
* label a week — whole weeks, never a step of two or three, because
|
|
496
|
+
* labels landing on Tuesday then Thursday then Saturday give the eye
|
|
497
|
+
* nothing to lock onto.
|
|
498
|
+
*/
|
|
499
|
+
export const LongRange: Story = {
|
|
500
|
+
render: (timelineProps) => (
|
|
501
|
+
<Frame inlineSize="72rem">
|
|
502
|
+
<LaneTimeline
|
|
503
|
+
{...timelineProps}
|
|
504
|
+
label="The month"
|
|
505
|
+
range={MONTH}
|
|
506
|
+
/>
|
|
507
|
+
</Frame>
|
|
508
|
+
),
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Every bar is a real control, and the keyboard path is the same one
|
|
513
|
+
* a pointer takes.
|
|
514
|
+
*
|
|
515
|
+
* Tab moves through the bars in **chronological** order within a
|
|
516
|
+
* lane and lane by lane down the page, which is the order the DOM is
|
|
517
|
+
* written in rather than the order the packer happened to stack
|
|
518
|
+
* them. Enter or Space fires the item. There is no drag and no
|
|
519
|
+
* resize: moving a bar is a write, and a write is a different
|
|
520
|
+
* component with a different decision behind it.
|
|
521
|
+
*/
|
|
522
|
+
const SelectableTimeline = (): ReactNode => {
|
|
523
|
+
const [selectedTitle, setSelectedTitle] = useState("")
|
|
524
|
+
|
|
525
|
+
return (
|
|
526
|
+
<Frame inlineSize="64rem">
|
|
527
|
+
<div className="flex flex-col gap-3">
|
|
528
|
+
<LaneTimeline
|
|
529
|
+
label="The week, selectable"
|
|
530
|
+
locale="en-GB"
|
|
531
|
+
lanes={LANES.map((lane) => ({
|
|
532
|
+
...lane,
|
|
533
|
+
items: lane.items.map(
|
|
534
|
+
({
|
|
535
|
+
href: _href,
|
|
536
|
+
onSelect: _onSelect,
|
|
537
|
+
...item
|
|
538
|
+
}): TimelineItem => ({
|
|
539
|
+
...item,
|
|
540
|
+
onSelect: () => {
|
|
541
|
+
setSelectedTitle(item.title)
|
|
542
|
+
},
|
|
543
|
+
}),
|
|
544
|
+
),
|
|
545
|
+
}))}
|
|
546
|
+
range={WEEK}
|
|
547
|
+
/>
|
|
548
|
+
|
|
549
|
+
<p className="text-content-secondary text-sm">
|
|
550
|
+
{selectedTitle
|
|
551
|
+
? `Picked: ${selectedTitle}`
|
|
552
|
+
: "Nothing picked yet."}
|
|
553
|
+
</p>
|
|
554
|
+
</div>
|
|
555
|
+
</Frame>
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export const Interactive: Story = {
|
|
560
|
+
render: () => <SelectableTimeline />,
|
|
561
|
+
}
|