@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,109 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useUniqueId } from "@charcuterie/logic";
|
|
3
|
+
import { CATEGORICAL_INDEX_COUNT, CATEGORICAL_SEQUENCE, } from "@charcuterie/tokens";
|
|
4
|
+
import { Badge } from "../Badge/Badge.js";
|
|
5
|
+
import { CATEGORICAL_SOLID_FILL_CLASS } from "../categoricalStyles.js";
|
|
6
|
+
import { TextLink } from "../TextLink/TextLink.js";
|
|
7
|
+
import { toClassName } from "../toClassName.js";
|
|
8
|
+
import { LaneTimelineBar } from "./LaneTimelineBar.js";
|
|
9
|
+
import { describeTimelineSpan, toPackedLane, } from "./laneTimelineGeometry.js";
|
|
10
|
+
/**
|
|
11
|
+
* One lane: its name, how much it is carrying, and either a track of
|
|
12
|
+
* bars or a list of lines.
|
|
13
|
+
*
|
|
14
|
+
* ### A lane is a `group`, not a landmark
|
|
15
|
+
*
|
|
16
|
+
* The same call `BoardLaneList` makes, and it was a real axe failure
|
|
17
|
+
* there before it changed. A named `<section>` is a `region`, so a
|
|
18
|
+
* timeline of eight lanes would put eight landmarks on a page that
|
|
19
|
+
* has one job, and two timelines on one page would put two landmarks
|
|
20
|
+
* with the same name in the document — which is `landmark-unique`.
|
|
21
|
+
* It is a `<div role="group">` rather than a `<section role="group">`
|
|
22
|
+
* because axe matches `section[aria-labelledby]` on the **element**
|
|
23
|
+
* and audits it as a landmark even after the role is overridden.
|
|
24
|
+
*
|
|
25
|
+
* ### The count is what the lane carries IN THIS RANGE
|
|
26
|
+
*
|
|
27
|
+
* Not `items.length`. A consumer hands over a month and shows a
|
|
28
|
+
* week; counting the array would print "31" beside a lane with two
|
|
29
|
+
* bars on it. `inRangeCount` includes anything the row limit hid, so
|
|
30
|
+
* a lane cannot look lighter by being more loaded.
|
|
31
|
+
*
|
|
32
|
+
* ### A lane gets TALLER when it is busy, and that is the design
|
|
33
|
+
*
|
|
34
|
+
* Overlapping items stack into rows (`toPackedLane`), so a lane
|
|
35
|
+
* carrying three things at once is three bars tall while one
|
|
36
|
+
* carrying them in sequence is one. Load asymmetry then reads as
|
|
37
|
+
* height from across the room, before anybody counts anything —
|
|
38
|
+
* which is the whole reason this component exists rather than a
|
|
39
|
+
* month grid, where every cell is the same size no matter what is
|
|
40
|
+
* in it.
|
|
41
|
+
*/
|
|
42
|
+
export const LaneTimelineLane = ({ axis, columnTemplate, headingLevel, lane, locale, maxRowCount, position, shape, tickStep, }) => {
|
|
43
|
+
const headingId = useUniqueId();
|
|
44
|
+
/*
|
|
45
|
+
* Position, not `getCategoricalIndex(lane.key)`, and `ActionTiles`
|
|
46
|
+
* made the same call for the same reason. That helper hashes, so a
|
|
47
|
+
* row keeps its colour across a list nobody controls the order of
|
|
48
|
+
* — right for Docket's labels, wrong here: a hash of two lane keys
|
|
49
|
+
* can collide, and two lanes in one hue defeats the only thing the
|
|
50
|
+
* colour is for. Measured on this component's own fixtures, where
|
|
51
|
+
* `wren` and `juno` both hashed to Pink and the screenshot showed
|
|
52
|
+
* one person's week twice.
|
|
53
|
+
*
|
|
54
|
+
* `CATEGORICAL_SEQUENCE` rather than the ring: the ring is
|
|
55
|
+
* hue-ordered for the swatch picker's sake, so walking it in order
|
|
56
|
+
* hands a two-lane set the two hardest colours in the family to
|
|
57
|
+
* tell apart.
|
|
58
|
+
*/
|
|
59
|
+
const categorical = lane.categorical ??
|
|
60
|
+
CATEGORICAL_SEQUENCE[position % CATEGORICAL_INDEX_COUNT];
|
|
61
|
+
const packed = toPackedLane({
|
|
62
|
+
axis,
|
|
63
|
+
items: lane.items,
|
|
64
|
+
/*
|
|
65
|
+
* The Narrow View has no row limit, because there are no rows:
|
|
66
|
+
* it is a vertical list, so the thing the limit protects
|
|
67
|
+
* against — a lane growing past the height of the screen while
|
|
68
|
+
* every other lane stays one bar tall — cannot happen. Hiding
|
|
69
|
+
* items there would lose them for the reader who has the least
|
|
70
|
+
* room to spare.
|
|
71
|
+
*/
|
|
72
|
+
maxRowCount: shape === "list" ? lane.items.length : maxRowCount,
|
|
73
|
+
});
|
|
74
|
+
const Heading = `h${headingLevel}`;
|
|
75
|
+
const header = (_jsxs("header", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { "aria-hidden": "true", className: toClassName("h-4 w-1 shrink-0 rounded-full", CATEGORICAL_SOLID_FILL_CLASS[categorical]) }), _jsx(Heading, { className: "min-w-0 truncate font-semibold text-content-primary text-sm", id: headingId, title: lane.label, children: lane.href ? (_jsx(TextLink, { appearance: "standalone", className: "font-semibold text-inherit", href: lane.href, children: lane.label })) : (lane.label) }), _jsx(Badge, { appearance: "soft", intent: "neutral", size: "sm", children: String(packed.inRangeCount) })] }));
|
|
76
|
+
if (shape === "list") {
|
|
77
|
+
const items = packed.rows
|
|
78
|
+
.flat()
|
|
79
|
+
.sort((first, second) => first.placement.columnStart -
|
|
80
|
+
second.placement.columnStart);
|
|
81
|
+
return (_jsxs("div", { "aria-labelledby": headingId, className: "flex flex-col gap-1.5 border-border-subtle border-t py-2", role: "group", children: [header, items.length === 0 ? (_jsx("p", { className: "text-content-muted text-xs", children: "Nothing in this range." })) : (_jsx("ul", { className: "flex flex-col gap-1", children: items.map(({ item, placement }) => (_jsx("li", { className: "min-w-0", children: _jsx(LaneTimelineBar, { laneCategorical: categorical, description: describeTimelineSpan({
|
|
82
|
+
locale,
|
|
83
|
+
placement,
|
|
84
|
+
span: item,
|
|
85
|
+
}), hasEarlierStart: placement.hasEarlierStart, hasLaterEnd: placement.hasLaterEnd, item: item, shape: "row" }) }, item.key))) }))] }));
|
|
86
|
+
}
|
|
87
|
+
return (_jsxs("div", { "aria-labelledby": headingId, className: "grid grid-cols-[12rem_minmax(0,1fr)] items-start gap-x-3 border-border-subtle border-t py-1.5", role: "group", children: [header, _jsxs("div", { className: "relative min-h-6", children: [_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 grid", style: { gridTemplateColumns: columnTemplate }, children: Array.from({ length: axis.columnCount }, (_unused, index) => axis.startDay + index).map((dayNumber) => (_jsx("span", { className: toClassName(
|
|
88
|
+
// Never on the leading edge: a line down the start of
|
|
89
|
+
// the track reads as a frame the track does not have.
|
|
90
|
+
dayNumber > axis.startDay &&
|
|
91
|
+
(dayNumber - axis.startDay) % tickStep ===
|
|
92
|
+
0 &&
|
|
93
|
+
"border-border-subtle border-s") }, dayNumber))) }), _jsx("ul", { className: "relative grid gap-1", style: { gridTemplateColumns: columnTemplate }, children: packed.rows.flatMap((row, rowIndex) => row.map(({ item, placement }) => (_jsx("li", { className: "min-w-0", style: {
|
|
94
|
+
/*
|
|
95
|
+
* `span`, never a start/end pair. An end line is
|
|
96
|
+
* one past the last column the bar covers, so
|
|
97
|
+
* writing it is the single easiest place to lose
|
|
98
|
+
* a day — and it is invisible, because a bar one
|
|
99
|
+
* day short still looks like a bar.
|
|
100
|
+
*/
|
|
101
|
+
gridColumn: `${placement.columnStart} / span ${placement.columnSpan}`,
|
|
102
|
+
gridRow: rowIndex + 1,
|
|
103
|
+
}, children: _jsx(LaneTimelineBar, { laneCategorical: categorical, description: describeTimelineSpan({
|
|
104
|
+
locale,
|
|
105
|
+
placement,
|
|
106
|
+
span: item,
|
|
107
|
+
}), hasEarlierStart: placement.hasEarlierStart, hasLaterEnd: placement.hasLaterEnd, item: item, shape: "bar" }) }, item.key)))) }), packed.hiddenCount > 0 ? (_jsx("p", { className: "pt-1 text-content-muted text-xs", children: `+ ${packed.hiddenCount} more overlapping in ${lane.label}` })) : null] })] }));
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=LaneTimelineLane.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LaneTimelineLane.js","sourceRoot":"","sources":["../../src/LaneTimeline/LaneTimelineLane.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a bar sits on the axis, and how many bars a lane stacks —
|
|
3
|
+
* the whole of the arithmetic, with no React and no DOM in it.
|
|
4
|
+
*
|
|
5
|
+
* It is its own module for the reason `boardMove.ts` is: every
|
|
6
|
+
* defect a timeline actually ships is an off-by-one, and an
|
|
7
|
+
* off-by-one is invisible in a screenshot. A bar one column to the
|
|
8
|
+
* left of where it belongs looks exactly like a bar. So the
|
|
9
|
+
* arithmetic is unit-tested against fixtures in Node, and the
|
|
10
|
+
* component below it only paints what comes out.
|
|
11
|
+
*
|
|
12
|
+
* ### Every position is a DAY NUMBER, never a `Date`
|
|
13
|
+
*
|
|
14
|
+
* Reached through `plainDate.ts` rather than reimplemented, because
|
|
15
|
+
* that file already argues the case at length and re-arguing it in a
|
|
16
|
+
* second place is how two answers appear. A timeline is the exact
|
|
17
|
+
* shape that bug hides in: "the roofers are here Monday to Friday"
|
|
18
|
+
* is true in every zone, and a `new Date("2026-04-06")` read at
|
|
19
|
+
* 23:30 west of Greenwich puts the bar on Sunday. The whole file is
|
|
20
|
+
* integer maths on days since 1970-01-01.
|
|
21
|
+
*
|
|
22
|
+
* ### The END IS INCLUSIVE, everywhere in here
|
|
23
|
+
*
|
|
24
|
+
* See `LaneTimelineItem.end` for the argument. The one thing to
|
|
25
|
+
* carry into this file: `columnSpan` is `end - start + 1`, so a
|
|
26
|
+
* single-day item is a span of **1**, not 0.
|
|
27
|
+
*/
|
|
28
|
+
import type { PlainDate } from "../DatePicker/plainDate.ts";
|
|
29
|
+
/**
|
|
30
|
+
* A closed interval of calendar days, `YYYY-MM-DD` at both ends.
|
|
31
|
+
* Both the visible range and an item's own extent are this shape,
|
|
32
|
+
* deliberately — the component asks the same question of both.
|
|
33
|
+
*/
|
|
34
|
+
export type TimelineSpan = {
|
|
35
|
+
/** The last day covered. **Inclusive.** */
|
|
36
|
+
end: string;
|
|
37
|
+
/** The first day covered. */
|
|
38
|
+
start: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The visible range, parsed once and reused for every item.
|
|
42
|
+
*
|
|
43
|
+
* Parsed once because a household week with eight lanes and two
|
|
44
|
+
* hundred items would otherwise re-parse the same two strings four
|
|
45
|
+
* hundred times — but mostly because a range that cannot be parsed
|
|
46
|
+
* has to fail in **one** place, rather than resolving to a
|
|
47
|
+
* different silent fallback inside each item's placement.
|
|
48
|
+
*/
|
|
49
|
+
export type TimelineAxis = {
|
|
50
|
+
/** Day columns in the visible range. Never below 1. */
|
|
51
|
+
columnCount: number;
|
|
52
|
+
endDay: number;
|
|
53
|
+
startDay: number;
|
|
54
|
+
};
|
|
55
|
+
export type BarPlacement = {
|
|
56
|
+
/** 1-based CSS grid column the bar starts in. */
|
|
57
|
+
columnStart: number;
|
|
58
|
+
/**
|
|
59
|
+
* Day columns the bar covers **after clipping**, never below 1.
|
|
60
|
+
*
|
|
61
|
+
* A CSS `grid-column: 4 / span 0` is not an error and not a
|
|
62
|
+
* zero-width box — the browser treats a span of 0 as 1 in some
|
|
63
|
+
* engines and as invalid-and-ignored in others, so the bar lands
|
|
64
|
+
* either in the right place or at the end of the row with nothing
|
|
65
|
+
* to say which. The clamp lives here so no caller has to know
|
|
66
|
+
* that.
|
|
67
|
+
*/
|
|
68
|
+
columnSpan: number;
|
|
69
|
+
/**
|
|
70
|
+
* The days the item covers in **total**, clipping ignored — so a
|
|
71
|
+
* two-week thing clipped to the visible Tuesday still says
|
|
72
|
+
* fourteen days out loud. The number a person is owed is the
|
|
73
|
+
* item's own length, not the part of it that fits on screen.
|
|
74
|
+
*/
|
|
75
|
+
dayCount: number;
|
|
76
|
+
/** The item begins before the first visible day. */
|
|
77
|
+
hasEarlierStart: boolean;
|
|
78
|
+
/** The item is still running after the last visible day. */
|
|
79
|
+
hasLaterEnd: boolean;
|
|
80
|
+
};
|
|
81
|
+
export type PlacedTimelineItem<Item> = {
|
|
82
|
+
item: Item;
|
|
83
|
+
placement: BarPlacement;
|
|
84
|
+
};
|
|
85
|
+
export type PackedLane<Item> = {
|
|
86
|
+
/**
|
|
87
|
+
* Items that were placed and then did not fit inside
|
|
88
|
+
* `maxRowCount` stacked rows. Counted rather than drawn, and the
|
|
89
|
+
* lane says the number out loud — the same honest-overflow rule
|
|
90
|
+
* `Board`'s `+ n more` follows, for the same reason: a lane that
|
|
91
|
+
* silently drops the ninth overlapping thing is a lane that lies
|
|
92
|
+
* about how loaded it is, which is the one question this
|
|
93
|
+
* component exists to answer.
|
|
94
|
+
*/
|
|
95
|
+
hiddenCount: number;
|
|
96
|
+
/**
|
|
97
|
+
* Items with any part of themselves inside the visible range,
|
|
98
|
+
* drawn or not.
|
|
99
|
+
*
|
|
100
|
+
* The number the lane's badge shows, and it is **not**
|
|
101
|
+
* `items.length`: a consumer hands over a month and looks at a
|
|
102
|
+
* week, so counting the whole array would put "31" beside a lane
|
|
103
|
+
* carrying two things. It is not `rows` flattened either — that
|
|
104
|
+
* would drop the overflow and make a lane look lighter the more
|
|
105
|
+
* loaded it got, which inverts the one question this component
|
|
106
|
+
* answers.
|
|
107
|
+
*/
|
|
108
|
+
inRangeCount: number;
|
|
109
|
+
rows: PlacedTimelineItem<Item>[][];
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* `null` when either end is not an ISO calendar date.
|
|
113
|
+
*
|
|
114
|
+
* Not a thrown error and not a guessed range. A component that
|
|
115
|
+
* invents a range paints a confident picture of days nobody asked
|
|
116
|
+
* for, and the caller finds out when somebody notices the dates are
|
|
117
|
+
* wrong — which on a timeline is never, because there is nothing to
|
|
118
|
+
* compare them against.
|
|
119
|
+
*
|
|
120
|
+
* An **inverted** range (`end` before `start`) is a different case
|
|
121
|
+
* and is repaired rather than refused: it collapses to the single
|
|
122
|
+
* day at `start`. A caller flipping two variables gets one column
|
|
123
|
+
* and an obviously wrong picture, which is recoverable, instead of
|
|
124
|
+
* an empty component, which reads as "there is nothing on".
|
|
125
|
+
*/
|
|
126
|
+
export declare const toTimelineAxis: (range: TimelineSpan) => null | TimelineAxis;
|
|
127
|
+
/**
|
|
128
|
+
* Where one item's bar goes, or `null` for "it is not in this
|
|
129
|
+
* picture at all".
|
|
130
|
+
*
|
|
131
|
+
* `null` is the right answer for an item outside the range, and it
|
|
132
|
+
* matters that it is distinguishable from a zero-width one: an item
|
|
133
|
+
* in another month must not be drawn, while an item whose own dates
|
|
134
|
+
* are degenerate must be. The four cases a naive implementation
|
|
135
|
+
* gets wrong, and what happens to each here:
|
|
136
|
+
*
|
|
137
|
+
* - **Zero width** — `end` earlier than `start`. The span folds to
|
|
138
|
+
* the single day at `start` and is drawn. It is not dropped,
|
|
139
|
+
* because a dropped row is a data fault nobody can see; and it
|
|
140
|
+
* is not drawn backwards, because there is no such shape.
|
|
141
|
+
* - **A single day** — `start` equal to `end`. One column, with
|
|
142
|
+
* the same bar treatment as any other. Not a dot: a dot is a
|
|
143
|
+
* different mark, and the eye then has two vocabularies to learn
|
|
144
|
+
* on one axis.
|
|
145
|
+
* - **Starts before the range.** Clipped to column 1, with
|
|
146
|
+
* `hasEarlierStart` set so the bar can drop its leading radius
|
|
147
|
+
* and say "starts before this range" in its name. The naive
|
|
148
|
+
* version computes a negative column, and CSS grid reads a
|
|
149
|
+
* negative `grid-column-start` as counting back from the **end**
|
|
150
|
+
* of the row — so the bar appears, in entirely the wrong place,
|
|
151
|
+
* with nothing failing.
|
|
152
|
+
* - **Ends after the range.** Clipped to the last column, with
|
|
153
|
+
* `hasLaterEnd` set. `dayCount` still reports the item's true
|
|
154
|
+
* length.
|
|
155
|
+
*/
|
|
156
|
+
export declare const toBarPlacement: ({ axis, span, }: {
|
|
157
|
+
axis: TimelineAxis;
|
|
158
|
+
span: TimelineSpan;
|
|
159
|
+
}) => null | BarPlacement;
|
|
160
|
+
/**
|
|
161
|
+
* Stack a lane's items into as few rows as they fit in.
|
|
162
|
+
*
|
|
163
|
+
* Greedy first-fit over items sorted by start column, which is the
|
|
164
|
+
* standard interval-partitioning answer and is **optimal** for row
|
|
165
|
+
* count — the number of rows it produces equals the largest number
|
|
166
|
+
* of items overlapping on any one day, which is the smallest number
|
|
167
|
+
* any packing could use. Worth saying because the obvious
|
|
168
|
+
* alternative, one row per item, is what makes a hand-rolled
|
|
169
|
+
* timeline four screens tall.
|
|
170
|
+
*
|
|
171
|
+
* The lane getting taller is not a side effect, it is the point. A
|
|
172
|
+
* lane carrying three things at once is literally three times the
|
|
173
|
+
* height of one carrying them in sequence, so the asymmetry reads
|
|
174
|
+
* from across the room before anybody counts anything.
|
|
175
|
+
*
|
|
176
|
+
* Ties are broken by the **longer** item first and then by `key`,
|
|
177
|
+
* so the packing is stable: the same data renders the same rows on
|
|
178
|
+
* every machine and in every reorder of the input array. Without
|
|
179
|
+
* the `key` tie-break two items starting on the same day swap rows
|
|
180
|
+
* whenever the consumer's sort is unstable, which reads as the
|
|
181
|
+
* timeline flickering for no reason.
|
|
182
|
+
*/
|
|
183
|
+
export declare const toPackedLane: <Item extends TimelineSpan & {
|
|
184
|
+
key: string;
|
|
185
|
+
}>({ axis, items, maxRowCount, }: {
|
|
186
|
+
axis: TimelineAxis;
|
|
187
|
+
items: readonly Item[];
|
|
188
|
+
/**
|
|
189
|
+
* How tall one lane may get. Past this, items are counted rather
|
|
190
|
+
* than drawn.
|
|
191
|
+
*/
|
|
192
|
+
maxRowCount: number;
|
|
193
|
+
}) => PackedLane<Item>;
|
|
194
|
+
/**
|
|
195
|
+
* Is the axis wide enough to be worth drawing?
|
|
196
|
+
*
|
|
197
|
+
* The question a media query cannot ask, and the reason this
|
|
198
|
+
* component measures. The answer depends on **two** numbers and a
|
|
199
|
+
* breakpoint only knows one: 30 columns in 900px is a readable
|
|
200
|
+
* month, and 365 columns in the same 900px is 2.5px a day — a grey
|
|
201
|
+
* smear with bars in it. `Nav` folds its bar on measured width for
|
|
202
|
+
* the same reason, and `useToolbarOverflow` writes the general
|
|
203
|
+
* version of the argument down: a hardcoded `480px` is wrong the
|
|
204
|
+
* moment anything about the content changes.
|
|
205
|
+
*
|
|
206
|
+
* ⚠️ **`isMeasured` is a separate flag and not `inlineSize > 0`.**
|
|
207
|
+
* Before the layout effect runs there is no width, and the honest
|
|
208
|
+
* answer is the axis — it is what the component is for, and
|
|
209
|
+
* defaulting the other way would ship the fallback list to every
|
|
210
|
+
* non-browser render with nothing to say why. But **zero is also a
|
|
211
|
+
* real measurement**: the track sits in a `minmax(0, 1fr)` column
|
|
212
|
+
* beside a fixed label rail, so a container narrower than the rail
|
|
213
|
+
* resolves the track to exactly 0px. Reading that as "not measured
|
|
214
|
+
* yet" is how the first draft drew a thirty-column axis into 150
|
|
215
|
+
* pixels and reported it as fine.
|
|
216
|
+
*/
|
|
217
|
+
export declare const getIsAxisReadable: ({ columnCount, inlineSize, isMeasured, minColumnInlineSize, }: {
|
|
218
|
+
columnCount: number;
|
|
219
|
+
/** The measured inline size of the track, in CSS pixels. */
|
|
220
|
+
inlineSize: number;
|
|
221
|
+
isMeasured: boolean;
|
|
222
|
+
minColumnInlineSize: number;
|
|
223
|
+
}) => boolean;
|
|
224
|
+
export declare const chooseAxisTickStep: ({ columnCount, inlineSize, minTickInlineSize, }: {
|
|
225
|
+
columnCount: number;
|
|
226
|
+
inlineSize: number;
|
|
227
|
+
/** Room one tick label needs, in CSS pixels. */
|
|
228
|
+
minTickInlineSize: number;
|
|
229
|
+
}) => number;
|
|
230
|
+
/**
|
|
231
|
+
* What a bar says when it cannot be seen.
|
|
232
|
+
*
|
|
233
|
+
* This is the load-bearing accessibility decision in the component,
|
|
234
|
+
* not a nicety. A bar carries its meaning in **position and
|
|
235
|
+
* length**, and neither of those reaches a screen reader, a
|
|
236
|
+
* high-contrast rendering, or anybody who cannot tell two hues
|
|
237
|
+
* apart. A one-day bar is also about 20px wide, so its own title
|
|
238
|
+
* does not fit inside it for a sighted reader either — the same
|
|
239
|
+
* sentence solves both.
|
|
240
|
+
*
|
|
241
|
+
* The dates are the item's **own**, never the clipped ones. A thing
|
|
242
|
+
* that runs from March into May reads as March to May even when the
|
|
243
|
+
* visible range is one week of April, because "when is this on" has
|
|
244
|
+
* one true answer and the viewport is not part of it.
|
|
245
|
+
*
|
|
246
|
+
* English, like `Board`'s move announcements. The library has no
|
|
247
|
+
* translation seam yet, and inventing one here would be a bigger
|
|
248
|
+
* decision than this component.
|
|
249
|
+
*/
|
|
250
|
+
export declare const describeTimelineSpan: ({ locale, placement, span, }: {
|
|
251
|
+
locale?: string;
|
|
252
|
+
placement: BarPlacement;
|
|
253
|
+
span: TimelineSpan;
|
|
254
|
+
}) => string;
|
|
255
|
+
/**
|
|
256
|
+
* One date, spelled the way both the axis and the spoken sentence
|
|
257
|
+
* want it: weekday, day, month.
|
|
258
|
+
*
|
|
259
|
+
* The weekday is in there because a timeline is read in weeks. "Is
|
|
260
|
+
* that the weekend?" is the most common question anybody asks of
|
|
261
|
+
* one, and answering it from a bare day number means counting
|
|
262
|
+
* columns.
|
|
263
|
+
*/
|
|
264
|
+
export declare const formatAxisDate: ({ date, locale, }: {
|
|
265
|
+
date: PlainDate;
|
|
266
|
+
locale?: string;
|
|
267
|
+
}) => string;
|
|
268
|
+
//# sourceMappingURL=laneTimelineGeometry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"laneTimelineGeometry.d.ts","sourceRoot":"","sources":["../../src/LaneTimeline/laneTimelineGeometry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a bar sits on the axis, and how many bars a lane stacks —
|
|
3
|
+
* the whole of the arithmetic, with no React and no DOM in it.
|
|
4
|
+
*
|
|
5
|
+
* It is its own module for the reason `boardMove.ts` is: every
|
|
6
|
+
* defect a timeline actually ships is an off-by-one, and an
|
|
7
|
+
* off-by-one is invisible in a screenshot. A bar one column to the
|
|
8
|
+
* left of where it belongs looks exactly like a bar. So the
|
|
9
|
+
* arithmetic is unit-tested against fixtures in Node, and the
|
|
10
|
+
* component below it only paints what comes out.
|
|
11
|
+
*
|
|
12
|
+
* ### Every position is a DAY NUMBER, never a `Date`
|
|
13
|
+
*
|
|
14
|
+
* Reached through `plainDate.ts` rather than reimplemented, because
|
|
15
|
+
* that file already argues the case at length and re-arguing it in a
|
|
16
|
+
* second place is how two answers appear. A timeline is the exact
|
|
17
|
+
* shape that bug hides in: "the roofers are here Monday to Friday"
|
|
18
|
+
* is true in every zone, and a `new Date("2026-04-06")` read at
|
|
19
|
+
* 23:30 west of Greenwich puts the bar on Sunday. The whole file is
|
|
20
|
+
* integer maths on days since 1970-01-01.
|
|
21
|
+
*
|
|
22
|
+
* ### The END IS INCLUSIVE, everywhere in here
|
|
23
|
+
*
|
|
24
|
+
* See `LaneTimelineItem.end` for the argument. The one thing to
|
|
25
|
+
* carry into this file: `columnSpan` is `end - start + 1`, so a
|
|
26
|
+
* single-day item is a span of **1**, not 0.
|
|
27
|
+
*/
|
|
28
|
+
import { formatPlainDate, getDayNumber, parseIsoDate, } from "../DatePicker/plainDate.js";
|
|
29
|
+
const toDayNumber = (isoDate) => {
|
|
30
|
+
const date = parseIsoDate(isoDate);
|
|
31
|
+
return date && getDayNumber(date);
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* `null` when either end is not an ISO calendar date.
|
|
35
|
+
*
|
|
36
|
+
* Not a thrown error and not a guessed range. A component that
|
|
37
|
+
* invents a range paints a confident picture of days nobody asked
|
|
38
|
+
* for, and the caller finds out when somebody notices the dates are
|
|
39
|
+
* wrong — which on a timeline is never, because there is nothing to
|
|
40
|
+
* compare them against.
|
|
41
|
+
*
|
|
42
|
+
* An **inverted** range (`end` before `start`) is a different case
|
|
43
|
+
* and is repaired rather than refused: it collapses to the single
|
|
44
|
+
* day at `start`. A caller flipping two variables gets one column
|
|
45
|
+
* and an obviously wrong picture, which is recoverable, instead of
|
|
46
|
+
* an empty component, which reads as "there is nothing on".
|
|
47
|
+
*/
|
|
48
|
+
export const toTimelineAxis = (range) => {
|
|
49
|
+
const startDay = toDayNumber(range.start);
|
|
50
|
+
const rawEndDay = toDayNumber(range.end);
|
|
51
|
+
if (startDay === null || rawEndDay === null) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const endDay = Math.max(startDay, rawEndDay);
|
|
55
|
+
return {
|
|
56
|
+
columnCount: endDay - startDay + 1,
|
|
57
|
+
endDay,
|
|
58
|
+
startDay,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Where one item's bar goes, or `null` for "it is not in this
|
|
63
|
+
* picture at all".
|
|
64
|
+
*
|
|
65
|
+
* `null` is the right answer for an item outside the range, and it
|
|
66
|
+
* matters that it is distinguishable from a zero-width one: an item
|
|
67
|
+
* in another month must not be drawn, while an item whose own dates
|
|
68
|
+
* are degenerate must be. The four cases a naive implementation
|
|
69
|
+
* gets wrong, and what happens to each here:
|
|
70
|
+
*
|
|
71
|
+
* - **Zero width** — `end` earlier than `start`. The span folds to
|
|
72
|
+
* the single day at `start` and is drawn. It is not dropped,
|
|
73
|
+
* because a dropped row is a data fault nobody can see; and it
|
|
74
|
+
* is not drawn backwards, because there is no such shape.
|
|
75
|
+
* - **A single day** — `start` equal to `end`. One column, with
|
|
76
|
+
* the same bar treatment as any other. Not a dot: a dot is a
|
|
77
|
+
* different mark, and the eye then has two vocabularies to learn
|
|
78
|
+
* on one axis.
|
|
79
|
+
* - **Starts before the range.** Clipped to column 1, with
|
|
80
|
+
* `hasEarlierStart` set so the bar can drop its leading radius
|
|
81
|
+
* and say "starts before this range" in its name. The naive
|
|
82
|
+
* version computes a negative column, and CSS grid reads a
|
|
83
|
+
* negative `grid-column-start` as counting back from the **end**
|
|
84
|
+
* of the row — so the bar appears, in entirely the wrong place,
|
|
85
|
+
* with nothing failing.
|
|
86
|
+
* - **Ends after the range.** Clipped to the last column, with
|
|
87
|
+
* `hasLaterEnd` set. `dayCount` still reports the item's true
|
|
88
|
+
* length.
|
|
89
|
+
*/
|
|
90
|
+
export const toBarPlacement = ({ axis, span, }) => {
|
|
91
|
+
const startDay = toDayNumber(span.start);
|
|
92
|
+
if (startDay === null) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const parsedEndDay = toDayNumber(span.end);
|
|
96
|
+
// An unparseable or inverted end is the zero-width case, and both
|
|
97
|
+
// resolve the same way: the item covers its start day and nothing
|
|
98
|
+
// else.
|
|
99
|
+
const endDay = Math.max(startDay, parsedEndDay ?? startDay);
|
|
100
|
+
if (endDay < axis.startDay || startDay > axis.endDay) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const visibleStartDay = Math.max(startDay, axis.startDay);
|
|
104
|
+
const visibleEndDay = Math.min(endDay, axis.endDay);
|
|
105
|
+
return {
|
|
106
|
+
columnStart: visibleStartDay - axis.startDay + 1,
|
|
107
|
+
columnSpan: visibleEndDay - visibleStartDay + 1,
|
|
108
|
+
dayCount: endDay - startDay + 1,
|
|
109
|
+
hasEarlierStart: startDay < axis.startDay,
|
|
110
|
+
hasLaterEnd: endDay > axis.endDay,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Stack a lane's items into as few rows as they fit in.
|
|
115
|
+
*
|
|
116
|
+
* Greedy first-fit over items sorted by start column, which is the
|
|
117
|
+
* standard interval-partitioning answer and is **optimal** for row
|
|
118
|
+
* count — the number of rows it produces equals the largest number
|
|
119
|
+
* of items overlapping on any one day, which is the smallest number
|
|
120
|
+
* any packing could use. Worth saying because the obvious
|
|
121
|
+
* alternative, one row per item, is what makes a hand-rolled
|
|
122
|
+
* timeline four screens tall.
|
|
123
|
+
*
|
|
124
|
+
* The lane getting taller is not a side effect, it is the point. A
|
|
125
|
+
* lane carrying three things at once is literally three times the
|
|
126
|
+
* height of one carrying them in sequence, so the asymmetry reads
|
|
127
|
+
* from across the room before anybody counts anything.
|
|
128
|
+
*
|
|
129
|
+
* Ties are broken by the **longer** item first and then by `key`,
|
|
130
|
+
* so the packing is stable: the same data renders the same rows on
|
|
131
|
+
* every machine and in every reorder of the input array. Without
|
|
132
|
+
* the `key` tie-break two items starting on the same day swap rows
|
|
133
|
+
* whenever the consumer's sort is unstable, which reads as the
|
|
134
|
+
* timeline flickering for no reason.
|
|
135
|
+
*/
|
|
136
|
+
export const toPackedLane = ({ axis, items, maxRowCount, }) => {
|
|
137
|
+
const placed = items.flatMap((item) => {
|
|
138
|
+
const placement = toBarPlacement({
|
|
139
|
+
axis,
|
|
140
|
+
span: item,
|
|
141
|
+
});
|
|
142
|
+
return placement ? [{ item, placement }] : [];
|
|
143
|
+
});
|
|
144
|
+
const sorted = [...placed].sort((first, second) => first.placement.columnStart -
|
|
145
|
+
second.placement.columnStart ||
|
|
146
|
+
second.placement.columnSpan -
|
|
147
|
+
first.placement.columnSpan ||
|
|
148
|
+
(first.item.key < second.item.key ? -1 : 1));
|
|
149
|
+
const rows = [];
|
|
150
|
+
let hiddenCount = 0;
|
|
151
|
+
for (const entry of sorted) {
|
|
152
|
+
const row = rows.find((candidate) => {
|
|
153
|
+
const last = candidate.at(-1);
|
|
154
|
+
return (!last ||
|
|
155
|
+
last.placement.columnStart +
|
|
156
|
+
last.placement.columnSpan <=
|
|
157
|
+
entry.placement.columnStart);
|
|
158
|
+
});
|
|
159
|
+
if (row) {
|
|
160
|
+
row.push(entry);
|
|
161
|
+
}
|
|
162
|
+
else if (rows.length < maxRowCount) {
|
|
163
|
+
rows.push([entry]);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
hiddenCount += 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
hiddenCount,
|
|
171
|
+
inRangeCount: placed.length,
|
|
172
|
+
rows,
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Is the axis wide enough to be worth drawing?
|
|
177
|
+
*
|
|
178
|
+
* The question a media query cannot ask, and the reason this
|
|
179
|
+
* component measures. The answer depends on **two** numbers and a
|
|
180
|
+
* breakpoint only knows one: 30 columns in 900px is a readable
|
|
181
|
+
* month, and 365 columns in the same 900px is 2.5px a day — a grey
|
|
182
|
+
* smear with bars in it. `Nav` folds its bar on measured width for
|
|
183
|
+
* the same reason, and `useToolbarOverflow` writes the general
|
|
184
|
+
* version of the argument down: a hardcoded `480px` is wrong the
|
|
185
|
+
* moment anything about the content changes.
|
|
186
|
+
*
|
|
187
|
+
* ⚠️ **`isMeasured` is a separate flag and not `inlineSize > 0`.**
|
|
188
|
+
* Before the layout effect runs there is no width, and the honest
|
|
189
|
+
* answer is the axis — it is what the component is for, and
|
|
190
|
+
* defaulting the other way would ship the fallback list to every
|
|
191
|
+
* non-browser render with nothing to say why. But **zero is also a
|
|
192
|
+
* real measurement**: the track sits in a `minmax(0, 1fr)` column
|
|
193
|
+
* beside a fixed label rail, so a container narrower than the rail
|
|
194
|
+
* resolves the track to exactly 0px. Reading that as "not measured
|
|
195
|
+
* yet" is how the first draft drew a thirty-column axis into 150
|
|
196
|
+
* pixels and reported it as fine.
|
|
197
|
+
*/
|
|
198
|
+
export const getIsAxisReadable = ({ columnCount, inlineSize, isMeasured, minColumnInlineSize, }) => !isMeasured ||
|
|
199
|
+
columnCount <= 0 ||
|
|
200
|
+
inlineSize / columnCount >= minColumnInlineSize;
|
|
201
|
+
/**
|
|
202
|
+
* Label every day, every week, or every four weeks.
|
|
203
|
+
*
|
|
204
|
+
* Thinning the ticks rather than shrinking the type, because type
|
|
205
|
+
* that shrinks to fit is type somebody cannot read and because the
|
|
206
|
+
* density axis already owns font size. The steps are 1, 7 and 28 —
|
|
207
|
+
* all whole weeks past the first, so a labelled column is the same
|
|
208
|
+
* weekday every time and the axis stays scannable. A step of 2 or 3
|
|
209
|
+
* was rejected for exactly that: labels landing on Tuesday, then
|
|
210
|
+
* Thursday, then Saturday give the eye nothing to lock onto.
|
|
211
|
+
*/
|
|
212
|
+
const TICK_STEPS = [1, 7, 28];
|
|
213
|
+
export const chooseAxisTickStep = ({ columnCount, inlineSize, minTickInlineSize, }) => {
|
|
214
|
+
if (inlineSize <= 0 || columnCount <= 0) {
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
const columnInlineSize = inlineSize / columnCount;
|
|
218
|
+
return (TICK_STEPS.find((step) => columnInlineSize * step >= minTickInlineSize) ?? TICK_STEPS[TICK_STEPS.length - 1]);
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* What a bar says when it cannot be seen.
|
|
222
|
+
*
|
|
223
|
+
* This is the load-bearing accessibility decision in the component,
|
|
224
|
+
* not a nicety. A bar carries its meaning in **position and
|
|
225
|
+
* length**, and neither of those reaches a screen reader, a
|
|
226
|
+
* high-contrast rendering, or anybody who cannot tell two hues
|
|
227
|
+
* apart. A one-day bar is also about 20px wide, so its own title
|
|
228
|
+
* does not fit inside it for a sighted reader either — the same
|
|
229
|
+
* sentence solves both.
|
|
230
|
+
*
|
|
231
|
+
* The dates are the item's **own**, never the clipped ones. A thing
|
|
232
|
+
* that runs from March into May reads as March to May even when the
|
|
233
|
+
* visible range is one week of April, because "when is this on" has
|
|
234
|
+
* one true answer and the viewport is not part of it.
|
|
235
|
+
*
|
|
236
|
+
* English, like `Board`'s move announcements. The library has no
|
|
237
|
+
* translation seam yet, and inventing one here would be a bigger
|
|
238
|
+
* decision than this component.
|
|
239
|
+
*/
|
|
240
|
+
export const describeTimelineSpan = ({ locale, placement, span, }) => {
|
|
241
|
+
const start = parseIsoDate(span.start);
|
|
242
|
+
if (!start) {
|
|
243
|
+
return "";
|
|
244
|
+
}
|
|
245
|
+
const format = (date) => formatAxisDate({ date, locale });
|
|
246
|
+
const end = parseIsoDate(span.end);
|
|
247
|
+
const sentence = placement.dayCount <= 1 || !end
|
|
248
|
+
? format(start)
|
|
249
|
+
: `${format(start)} to ${format(end)}, ${placement.dayCount} days`;
|
|
250
|
+
return [
|
|
251
|
+
sentence,
|
|
252
|
+
placement.hasEarlierStart
|
|
253
|
+
? "starts before this range"
|
|
254
|
+
: "",
|
|
255
|
+
placement.hasLaterEnd
|
|
256
|
+
? "continues past this range"
|
|
257
|
+
: "",
|
|
258
|
+
]
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
.join(", ");
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* One date, spelled the way both the axis and the spoken sentence
|
|
264
|
+
* want it: weekday, day, month.
|
|
265
|
+
*
|
|
266
|
+
* The weekday is in there because a timeline is read in weeks. "Is
|
|
267
|
+
* that the weekend?" is the most common question anybody asks of
|
|
268
|
+
* one, and answering it from a bare day number means counting
|
|
269
|
+
* columns.
|
|
270
|
+
*/
|
|
271
|
+
export const formatAxisDate = ({ date, locale, }) => formatPlainDate(date, {
|
|
272
|
+
day: "numeric",
|
|
273
|
+
locale,
|
|
274
|
+
month: "short",
|
|
275
|
+
weekday: "short",
|
|
276
|
+
});
|
|
277
|
+
//# sourceMappingURL=laneTimelineGeometry.js.map
|