quotacap 0.0.46 → 0.0.48
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/version.js +1 -1
- package/dist/web/src/components/ResetRail.d.ts +28 -7
- package/dist/web/src/components/ResetRail.js +184 -38
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-BSQW0C_t.css +1 -0
- package/web/dist/assets/index-Dq9ZvTUe.js +13 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-C6ltEeGL.css +0 -1
- package/web/dist/assets/index-DRDDTtxF.js +0 -13
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by scripts/build-embed.mjs — do not edit
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.48";
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ProviderView } from "../state.js";
|
|
3
3
|
export declare const RAIL_DAYS = 7;
|
|
4
|
+
/** Stem height; every pin sits at tier 0, so the rail never grows. */
|
|
5
|
+
export declare const STEM_PX = 14;
|
|
4
6
|
export interface RailRow {
|
|
5
7
|
id: string;
|
|
8
|
+
/** Pill text, used to estimate the width for dodge packing. */
|
|
9
|
+
label: string;
|
|
6
10
|
resetsAt: string | null;
|
|
7
11
|
estimated?: boolean;
|
|
8
12
|
}
|
|
@@ -10,13 +14,16 @@ export type RailBand = "above" | "below";
|
|
|
10
14
|
export interface PlacedPin extends RailRow {
|
|
11
15
|
positionPct: number;
|
|
12
16
|
band: RailBand;
|
|
13
|
-
|
|
17
|
+
/** Sideways pill offset in px; the dot stays on the time coordinate. */
|
|
18
|
+
offsetPx: number;
|
|
14
19
|
}
|
|
15
20
|
export interface PinCluster {
|
|
16
21
|
ids: string[];
|
|
22
|
+
label: string;
|
|
17
23
|
positionPct: number;
|
|
18
24
|
band: RailBand;
|
|
19
|
-
|
|
25
|
+
/** Sideways pill offset in px; the dot stays on the time coordinate. */
|
|
26
|
+
offsetPx: number;
|
|
20
27
|
}
|
|
21
28
|
export interface PinPlacement {
|
|
22
29
|
placed: PlacedPin[];
|
|
@@ -24,15 +31,29 @@ export interface PinPlacement {
|
|
|
24
31
|
overflow: RailRow[];
|
|
25
32
|
invalid: RailRow[];
|
|
26
33
|
}
|
|
34
|
+
/** Half the pill width for a display name: 16ch cap, ~7px per char + chrome. */
|
|
35
|
+
export declare function pillHalfWidth(label: string): number;
|
|
36
|
+
/** Half the timestamp width: tabular mono, ~6.6px per char + padding. */
|
|
37
|
+
export declare function stampHalfWidth(text: string): number;
|
|
38
|
+
export type RailMode = "separate" | "grouped";
|
|
39
|
+
export declare const RAIL_MODE_KEY = "quotacap-rail-mode";
|
|
40
|
+
export declare function storedRailMode(): RailMode;
|
|
27
41
|
/**
|
|
28
42
|
* Place reset pins proportionally over the next 7 days from `asOf`.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
43
|
+
* Same-band pills that would overlap dodge sideways by pill width (earlier
|
|
44
|
+
* left, later right); crowds past the spread cap shingle with small overlap
|
|
45
|
+
* instead, resolved by hover. Dots stay on the time coordinate and the rail
|
|
46
|
+
* stays flat. Resets beyond the rail overflow; invalid rows are reported;
|
|
47
|
+
* reset-passed rows place no pin (the ledger shows "awaiting fresh window"
|
|
48
|
+
* for them). Grouped mode collapses crowded windows into cluster pins.
|
|
32
49
|
*/
|
|
33
|
-
export declare function placePins(rows: RailRow[], asOf: Date
|
|
34
|
-
|
|
50
|
+
export declare function placePins(rows: RailRow[], asOf: Date, opts?: {
|
|
51
|
+
grouped?: boolean;
|
|
52
|
+
railWidthPx?: number;
|
|
53
|
+
}): PinPlacement;
|
|
54
|
+
export declare function ResetRail({ providers, asOf, onSelectProvider, initialMode, }: {
|
|
35
55
|
providers: ProviderView[];
|
|
36
56
|
asOf: string;
|
|
37
57
|
onSelectProvider: (id: string) => void;
|
|
58
|
+
initialMode?: RailMode;
|
|
38
59
|
}): React.JSX.Element;
|
|
@@ -3,17 +3,62 @@ import { useEffect, useRef, useState } from "react";
|
|
|
3
3
|
import { paceBadge, resetClock, resetCountdown, timeLeft } from "./PaceBar.js";
|
|
4
4
|
export const RAIL_DAYS = 7;
|
|
5
5
|
const RAIL_MS = RAIL_DAYS * 24 * 60 * 60 * 1000;
|
|
6
|
-
/** Pins within this span of rail group into one collision window. */
|
|
6
|
+
/** Pins within this span of rail group into one collision window (grouped mode). */
|
|
7
7
|
const COLLISION_PCT = 3;
|
|
8
|
-
/** Pins per window
|
|
9
|
-
const WINDOW_CAPACITY =
|
|
8
|
+
/** Pins per window past which they group into a cluster pin (grouped mode). */
|
|
9
|
+
const WINDOW_CAPACITY = 2;
|
|
10
|
+
/** Stem height; every pin sits at tier 0, so the rail never grows. */
|
|
11
|
+
export const STEM_PX = 14;
|
|
12
|
+
/** Layout width before the rail measures itself (dashboard content max). */
|
|
13
|
+
const RAIL_REF_PX = 1072;
|
|
14
|
+
/** Gap kept between dodged pills. */
|
|
15
|
+
const DODGE_GAP_PX = 6;
|
|
16
|
+
/** Computed overlap tolerated before pins dodge; pills may touch slightly. */
|
|
17
|
+
const GROUP_OVERLAP_TOL_PX = 10;
|
|
18
|
+
/** Widest one dodge group may spread; past this pills shingle with overlap. */
|
|
19
|
+
const MAX_GROUP_SPREAD_PX = 160;
|
|
20
|
+
/** Half the pill width for a display name: 16ch cap, ~7px per char + chrome. */
|
|
21
|
+
export function pillHalfWidth(label) {
|
|
22
|
+
return (Math.min(label.length, 16) * 7 + 30) / 2;
|
|
23
|
+
}
|
|
24
|
+
/** Half the timestamp width: tabular mono, ~6.6px per char + padding. */
|
|
25
|
+
export function stampHalfWidth(text) {
|
|
26
|
+
return (text.length * 6.6 + 12) / 2;
|
|
27
|
+
}
|
|
28
|
+
function pinHalfWidth(pin) {
|
|
29
|
+
// Candidates reaching layout always parsed, but the row type allows null.
|
|
30
|
+
const clock = pin.resetsAt === null ? "" : (resetClock(pin.resetsAt) ?? "");
|
|
31
|
+
const stamp = `${clock}${pin.estimated ? " (est.)" : ""}`;
|
|
32
|
+
return Math.max(pillHalfWidth(pin.label), stampHalfWidth(stamp));
|
|
33
|
+
}
|
|
34
|
+
export const RAIL_MODE_KEY = "quotacap-rail-mode";
|
|
35
|
+
export function storedRailMode() {
|
|
36
|
+
try {
|
|
37
|
+
const v = typeof localStorage === "undefined" ? null : localStorage.getItem(RAIL_MODE_KEY);
|
|
38
|
+
return v === "grouped" ? "grouped" : "separate";
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return "separate";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function persistRailMode(mode) {
|
|
45
|
+
try {
|
|
46
|
+
localStorage.setItem(RAIL_MODE_KEY, mode);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Private mode or non-DOM: the choice simply does not persist.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
10
52
|
/**
|
|
11
53
|
* Place reset pins proportionally over the next 7 days from `asOf`.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
54
|
+
* Same-band pills that would overlap dodge sideways by pill width (earlier
|
|
55
|
+
* left, later right); crowds past the spread cap shingle with small overlap
|
|
56
|
+
* instead, resolved by hover. Dots stay on the time coordinate and the rail
|
|
57
|
+
* stays flat. Resets beyond the rail overflow; invalid rows are reported;
|
|
58
|
+
* reset-passed rows place no pin (the ledger shows "awaiting fresh window"
|
|
59
|
+
* for them). Grouped mode collapses crowded windows into cluster pins.
|
|
15
60
|
*/
|
|
16
|
-
export function placePins(rows, asOf) {
|
|
61
|
+
export function placePins(rows, asOf, opts) {
|
|
17
62
|
const asOfMs = asOf.getTime();
|
|
18
63
|
const placed = [];
|
|
19
64
|
const clusters = [];
|
|
@@ -48,41 +93,99 @@ export function placePins(rows, asOf) {
|
|
|
48
93
|
}
|
|
49
94
|
const items = [];
|
|
50
95
|
for (const window of windows) {
|
|
51
|
-
if (window.length
|
|
96
|
+
if (opts?.grouped && window.length > WINDOW_CAPACITY) {
|
|
97
|
+
const positionPct = window.reduce((sum, c) => sum + c.positionPct, 0) / window.length;
|
|
98
|
+
const ids = window.map((c) => c.row.id);
|
|
99
|
+
items.push({
|
|
100
|
+
kind: "cluster",
|
|
101
|
+
cluster: { ids, label: `${ids.length} resets`, positionPct, band: "above", offsetPx: 0 },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
52
105
|
for (const c of window) {
|
|
53
106
|
items.push({
|
|
54
107
|
kind: "pin",
|
|
55
|
-
pin: { ...c.row, positionPct: c.positionPct, band: "above",
|
|
108
|
+
pin: { ...c.row, positionPct: c.positionPct, band: "above", offsetPx: 0 },
|
|
56
109
|
});
|
|
57
110
|
}
|
|
58
111
|
}
|
|
59
|
-
else {
|
|
60
|
-
const positionPct = window.reduce((sum, c) => sum + c.positionPct, 0) / window.length;
|
|
61
|
-
items.push({
|
|
62
|
-
kind: "cluster",
|
|
63
|
-
cluster: { ids: window.map((c) => c.row.id), positionPct, band: "above", tier: 0 },
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
112
|
}
|
|
67
113
|
items.sort((a, b) => {
|
|
68
114
|
const pa = a.kind === "pin" ? a.pin.positionPct : a.cluster.positionPct;
|
|
69
115
|
const pb = b.kind === "pin" ? b.pin.positionPct : b.cluster.positionPct;
|
|
70
116
|
return pa - pb;
|
|
71
117
|
});
|
|
72
|
-
// Alternate above/below bands
|
|
73
|
-
const lastByBand = {
|
|
74
|
-
above: { position: -Infinity, tier: 0 },
|
|
75
|
-
below: { position: -Infinity, tier: 0 },
|
|
76
|
-
};
|
|
118
|
+
// Alternate above/below bands so time-adjacent pins never share a side.
|
|
77
119
|
items.forEach((item, index) => {
|
|
78
120
|
const target = item.kind === "pin" ? item.pin : item.cluster;
|
|
79
|
-
|
|
80
|
-
const prev = lastByBand[band];
|
|
81
|
-
const tier = target.positionPct - prev.position < COLLISION_PCT * 2 ? prev.tier + 1 : 0;
|
|
82
|
-
target.band = band;
|
|
83
|
-
target.tier = tier;
|
|
84
|
-
lastByBand[band] = { position: target.positionPct, tier };
|
|
121
|
+
target.band = index % 2 === 0 ? "above" : "below";
|
|
85
122
|
});
|
|
123
|
+
const width = opts?.railWidthPx && opts.railWidthPx > 0 ? opts.railWidthPx : RAIL_REF_PX;
|
|
124
|
+
const cap = Math.min(MAX_GROUP_SPREAD_PX, width * 0.3);
|
|
125
|
+
for (const band of ["above", "below"]) {
|
|
126
|
+
const lane = items
|
|
127
|
+
.filter((item) => (item.kind === "pin" ? item.pin.band : item.cluster.band) === band)
|
|
128
|
+
.map((item) => {
|
|
129
|
+
const target = item.kind === "pin" ? item.pin : item.cluster;
|
|
130
|
+
return {
|
|
131
|
+
target,
|
|
132
|
+
x: (target.positionPct / 100) * width,
|
|
133
|
+
half: item.kind === "pin" ? pinHalfWidth(item.pin) : pillHalfWidth(item.cluster.label),
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
// Chain natural extents into dodge groups.
|
|
137
|
+
const groups = [];
|
|
138
|
+
let group = [];
|
|
139
|
+
let edge = -Infinity;
|
|
140
|
+
const flush = () => {
|
|
141
|
+
if (group.length > 0)
|
|
142
|
+
groups.push(group);
|
|
143
|
+
group = [];
|
|
144
|
+
edge = -Infinity;
|
|
145
|
+
};
|
|
146
|
+
for (const slot of lane) {
|
|
147
|
+
if (slot.x - slot.half > edge - GROUP_OVERLAP_TOL_PX)
|
|
148
|
+
flush();
|
|
149
|
+
group.push(slot);
|
|
150
|
+
edge = Math.max(edge, slot.x + slot.half);
|
|
151
|
+
}
|
|
152
|
+
flush();
|
|
153
|
+
for (const g of groups) {
|
|
154
|
+
if (g.length === 1)
|
|
155
|
+
continue; // offsetPx stays 0
|
|
156
|
+
const mid = (g[0].x + g[g.length - 1].x) / 2;
|
|
157
|
+
const totalW = g.reduce((sum, s) => sum + s.half * 2, 0) + DODGE_GAP_PX * (g.length - 1);
|
|
158
|
+
let cursor = mid - totalW / 2;
|
|
159
|
+
let centers = g.map((s) => {
|
|
160
|
+
const center = cursor + s.half;
|
|
161
|
+
cursor += s.half * 2 + DODGE_GAP_PX;
|
|
162
|
+
return center;
|
|
163
|
+
});
|
|
164
|
+
const boxSpan = centers[centers.length - 1] + g[g.length - 1].half - (centers[0] - g[0].half);
|
|
165
|
+
if (boxSpan > cap)
|
|
166
|
+
centers = centers.map((c) => mid + ((c - mid) * cap) / boxSpan);
|
|
167
|
+
const shift = centers[0] - g[0].half < 0
|
|
168
|
+
? -(centers[0] - g[0].half)
|
|
169
|
+
: centers[centers.length - 1] + g[g.length - 1].half > width
|
|
170
|
+
? width - (centers[centers.length - 1] + g[g.length - 1].half)
|
|
171
|
+
: 0;
|
|
172
|
+
g.forEach((s, i) => {
|
|
173
|
+
s.target.offsetPx = centers[i] + shift - s.x;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Singletons near the edges shift just enough to stay on the rail; the
|
|
177
|
+
// diagonal stem reconnects the pill to its dot. Groups already clamp.
|
|
178
|
+
for (const s of lane) {
|
|
179
|
+
if (s.target.offsetPx !== 0)
|
|
180
|
+
continue;
|
|
181
|
+
const left = s.x - s.half;
|
|
182
|
+
const right = s.x + s.half;
|
|
183
|
+
if (left < 0)
|
|
184
|
+
s.target.offsetPx = -left;
|
|
185
|
+
else if (right > width)
|
|
186
|
+
s.target.offsetPx = width - right;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
86
189
|
for (const item of items) {
|
|
87
190
|
if (item.kind === "pin")
|
|
88
191
|
placed.push(item.pin);
|
|
@@ -154,16 +257,59 @@ function dayLabels(asOfMs) {
|
|
|
154
257
|
return `${weekday} ${day}`;
|
|
155
258
|
});
|
|
156
259
|
}
|
|
157
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Stem from the dot to the pill: vertical when centered, a short diagonal
|
|
262
|
+
* when the pill dodged sideways. The over-tall layout box is pulled back
|
|
263
|
+
* with a negative margin so the pill meets the stem's visual end.
|
|
264
|
+
*/
|
|
265
|
+
function PinStem({ band, offsetPx }) {
|
|
266
|
+
if (offsetPx === 0)
|
|
267
|
+
return _jsx("span", { className: "pin-stem" });
|
|
268
|
+
const dx = Math.abs(offsetPx);
|
|
269
|
+
const len = Math.sqrt(dx * dx + STEM_PX * STEM_PX);
|
|
270
|
+
const angle = (Math.atan2(dx, STEM_PX) * 180) / Math.PI;
|
|
271
|
+
const deg = band === "above" ? Math.sign(offsetPx) * angle : -Math.sign(offsetPx) * angle;
|
|
272
|
+
const pull = len - STEM_PX;
|
|
273
|
+
const style = band === "above"
|
|
274
|
+
? { height: len, marginTop: -pull, transform: `rotate(${deg}deg)` }
|
|
275
|
+
: { height: len, marginBottom: -pull, transform: `rotate(${deg}deg)` };
|
|
276
|
+
return _jsx("span", { className: "pin-stem", style: style });
|
|
277
|
+
}
|
|
278
|
+
export function ResetRail({ providers, asOf, onSelectProvider, initialMode, }) {
|
|
158
279
|
const [openCluster, setOpenCluster] = useState(null);
|
|
280
|
+
const [mode, setMode] = useState(() => initialMode ?? storedRailMode());
|
|
281
|
+
const [railWidth, setRailWidth] = useState(RAIL_REF_PX);
|
|
282
|
+
const railRef = useRef(null);
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
const el = railRef.current;
|
|
285
|
+
if (!el || typeof ResizeObserver === "undefined")
|
|
286
|
+
return;
|
|
287
|
+
const ro = new ResizeObserver((entries) => {
|
|
288
|
+
const w = entries[0]?.contentRect.width;
|
|
289
|
+
if (w && w > 0)
|
|
290
|
+
setRailWidth(w);
|
|
291
|
+
});
|
|
292
|
+
ro.observe(el);
|
|
293
|
+
return () => ro.disconnect();
|
|
294
|
+
}, []);
|
|
159
295
|
const asOfMs = Date.parse(asOf);
|
|
160
296
|
const byId = new Map(providers.map((p) => [p.id, p]));
|
|
161
297
|
const rows = providers
|
|
162
298
|
.filter((p) => p.enabled && p.quota)
|
|
163
|
-
.map((p) => ({
|
|
164
|
-
|
|
299
|
+
.map((p) => ({
|
|
300
|
+
id: p.id,
|
|
301
|
+
label: p.displayName ?? p.id,
|
|
302
|
+
resetsAt: p.quota.resetsAt,
|
|
303
|
+
estimated: !!p.quota.resetsAtEstimated,
|
|
304
|
+
}));
|
|
305
|
+
const placement = placePins(rows, new Date(asOfMs), { grouped: mode === "grouped", railWidthPx: railWidth });
|
|
165
306
|
const labels = dayLabels(asOfMs);
|
|
166
|
-
|
|
307
|
+
const setModePersist = (next) => {
|
|
308
|
+
setOpenCluster(null);
|
|
309
|
+
setMode(next);
|
|
310
|
+
persistRailMode(next);
|
|
311
|
+
};
|
|
312
|
+
return (_jsxs("section", { "aria-label": "Upcoming resets", children: [_jsxs("div", { className: "section-head", children: [_jsx("h2", { id: "resets-h", children: "Upcoming resets" }), _jsxs("div", { className: "section-head-controls", children: [_jsxs("span", { className: "note", children: ["Next 7 days \u00B7 hover pin for usage \u00B7 click for details \u00B7 ", tzAbbrev(), placement.overflow.length > 0 && (_jsxs("span", { "data-testid": "rail-overflow", title: placement.overflow.map((o) => byId.get(o.id)?.displayName ?? o.id).join(", "), style: { marginLeft: 8, fontWeight: 600, color: "var(--ahead)" }, children: ["+", placement.overflow.length, " beyond rail"] }))] }), _jsxs("div", { "data-testid": "rail-mode", role: "group", "aria-label": "Reset pin grouping", className: "seg", children: [_jsx("button", { type: "button", "aria-pressed": mode === "separate", onClick: () => setModePersist("separate"), children: "Separate" }), _jsx("button", { type: "button", "aria-pressed": mode === "grouped", onClick: () => setModePersist("grouped"), children: "Grouped" })] })] })] }), _jsx("div", { "data-testid": "rail", className: "rail-scroller", tabIndex: 0, role: "region", "aria-label": `Upcoming resets over the next ${RAIL_DAYS} days`, children: _jsxs("div", { className: "rail", ref: railRef, children: [_jsx("div", { className: "rail-grid", "aria-hidden": "true", children: Array.from({ length: RAIL_DAYS }, (_, i) => (_jsx("span", {}, i))) }), _jsx("div", { className: "rail-axis", "aria-hidden": "true" }), _jsx("div", { className: "rail-now", "aria-hidden": "true" }), _jsx("span", { className: "rail-nowlabel", "aria-hidden": "true", children: "Now" }), placement.placed.map((pin) => {
|
|
167
313
|
const provider = byId.get(pin.id);
|
|
168
314
|
const when = provider?.quota ? pinWhen(provider.quota.resetsAt) : provider ? resetCountdown(provider, asOfMs) : "";
|
|
169
315
|
const pace = provider ? paceBadge(provider) : "";
|
|
@@ -171,20 +317,20 @@ export function ResetRail({ providers, asOf, onSelectProvider, }) {
|
|
|
171
317
|
const name = provider?.displayName ?? pin.id;
|
|
172
318
|
const { cls: paceCls, color: paceColor } = pinPace(provider);
|
|
173
319
|
const alignClass = pin.positionPct < 12 ? "pin-start" : pin.positionPct > 88 ? "pin-end" : "";
|
|
174
|
-
const tierClass = pin.tier > 0 ? `tier${pin.tier + 1}` : "tier1";
|
|
175
320
|
const pinBorder = {
|
|
176
321
|
["--pin-border"]: `color-mix(in oklch, ${paceColor} 45%, var(--line))`,
|
|
177
322
|
};
|
|
178
|
-
|
|
323
|
+
const dodgeX = pin.offsetPx === 0 ? undefined : { translate: `${pin.offsetPx}px 0` };
|
|
324
|
+
return (_jsxs("button", { "data-testid": "pin", type: "button", className: `pin ${pin.band} ${alignClass}`, style: {
|
|
179
325
|
left: `${pin.positionPct}%`,
|
|
180
|
-
"--dot-color": paceColor,
|
|
181
|
-
}, "aria-label": `${name}: ${pace}, resets ${when}`, onClick: () => onSelectProvider(pin.id), children: [pin.band === "above" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx(
|
|
326
|
+
["--dot-color"]: paceColor,
|
|
327
|
+
}, "aria-label": `${name}: ${pace}, resets ${when}`, onClick: () => onSelectProvider(pin.id), children: [pin.band === "above" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-when", style: dodgeX, children: pin.estimated ? `${when} (est.)` : when }), _jsx("span", { className: `pin-label ${paceCls}`, style: { ...pinBorder, ...dodgeX }, children: name }), _jsx(PinStem, { band: pin.band, offsetPx: pin.offsetPx }), _jsx("span", { className: "pin-dot" })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-dot" }), _jsx(PinStem, { band: pin.band, offsetPx: pin.offsetPx }), _jsx("span", { className: `pin-label ${paceCls}`, style: { ...pinBorder, ...dodgeX }, children: name }), _jsx("span", { className: "pin-when", style: dodgeX, children: pin.estimated ? `${when} (est.)` : when })] })), _jsxs("span", { className: "pin-tooltip", role: "tooltip", children: [_jsx("b", { children: name }), _jsx("span", { className: "pt-sep", children: "\u00B7" }), provider?.quota ? `${provider.quota.weeklyPct}% used` : "no readings", _jsx("span", { className: "pt-sep", children: "\u00B7" }), left ? `${left} left` : when, pin.estimated ? " (est.)" : ""] })] }, pin.id));
|
|
182
328
|
}), placement.clusters.map((cluster, index) => {
|
|
183
329
|
const alignClass = cluster.positionPct < 12 ? "pin-start" : cluster.positionPct > 88 ? "pin-end" : "";
|
|
184
|
-
const
|
|
185
|
-
return (_jsx("button", { "data-testid": "cluster-pin", type: "button", className: `pin ${cluster.band} ${
|
|
330
|
+
const dodgeX = cluster.offsetPx === 0 ? undefined : { translate: `${cluster.offsetPx}px 0` };
|
|
331
|
+
return (_jsx("button", { "data-testid": "cluster-pin", type: "button", className: `pin ${cluster.band} ${alignClass}`, style: {
|
|
186
332
|
left: `${cluster.positionPct}%`,
|
|
187
333
|
"--dot-color": "var(--accent)",
|
|
188
|
-
}, "aria-label": `${cluster.ids.length} resets at the same time: ${cluster.ids.map((id) => byId.get(id)?.displayName ?? id).join(", ")}`, "aria-expanded": openCluster === index, onClick: () => setOpenCluster(openCluster === index ? null : index), children: cluster.band === "above" ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "pin-label", style: { borderColor: "var(--accent)", color: "var(--accent)" }, children: [cluster.ids.length, " resets"] }), _jsx(
|
|
334
|
+
}, "aria-label": `${cluster.ids.length} resets at the same time: ${cluster.ids.map((id) => byId.get(id)?.displayName ?? id).join(", ")}`, "aria-expanded": openCluster === index, onClick: () => setOpenCluster(openCluster === index ? null : index), children: cluster.band === "above" ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "pin-label", style: { borderColor: "var(--accent)", color: "var(--accent)", ...dodgeX }, children: [cluster.ids.length, " resets"] }), _jsx(PinStem, { band: cluster.band, offsetPx: cluster.offsetPx }), _jsx("span", { className: "pin-dot" })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-dot" }), _jsx(PinStem, { band: cluster.band, offsetPx: cluster.offsetPx }), _jsxs("span", { className: "pin-label", style: { borderColor: "var(--accent)", color: "var(--accent)", ...dodgeX }, children: [cluster.ids.length, " resets"] })] })) }, cluster.ids.join("+")));
|
|
189
335
|
}), openCluster !== null && placement.clusters[openCluster] && (_jsx(ClusterPopover, { cluster: placement.clusters[openCluster], providers: byId, asOfMs: asOfMs, onSelect: onSelectProvider, onClose: () => setOpenCluster(null) })), _jsx("div", { className: "rail-days", "aria-hidden": "true", children: labels.map((label) => (_jsx("span", { children: label }, label))) })] }) })] }));
|
|
190
336
|
}
|