@columenlabs/lumen-ui 0.1.40 → 0.1.42
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/coverage-cells.d.ts +8 -1
- package/dist/coverage-cells.js +14 -7
- package/dist/coverage-map.d.ts +5 -2
- package/dist/coverage-map.js +113 -25
- package/dist/ui/tabs.d.ts +1 -1
- package/dist/ui/tabs.js +16 -2
- package/package.json +2 -2
package/dist/coverage-cells.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export declare function CoverageLegend({ levels, hint, labels, }: {
|
|
|
32
32
|
* cell made a 100-row map 13,000 tooltips, seconds per render). `label` is the
|
|
33
33
|
* accessible name and the tooltip's first line; `tip` marks the cell for the shared tooltip.
|
|
34
34
|
*/
|
|
35
|
-
export declare const CoverageCell: import("react").MemoExoticComponent<({ level, label, tip, picked, disabled, pickable, onClick, className, fill, }: {
|
|
35
|
+
export declare const CoverageCell: import("react").MemoExoticComponent<({ level, label, tip, picked, disabled, pickable, onClick, className, fill, mark, start, preview, }: {
|
|
36
36
|
level: CoverageLevel;
|
|
37
37
|
label: string;
|
|
38
38
|
/** The share in hand (0–1): a part-done block fills to it from the left, whatever its state. */
|
|
@@ -45,6 +45,13 @@ export declare const CoverageCell: import("react").MemoExoticComponent<({ level,
|
|
|
45
45
|
pickable?: boolean;
|
|
46
46
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
47
47
|
className?: string;
|
|
48
|
+
/** A backfill pick: a frame and a check (`add`) or a minus (`remove`, out of the queue).
|
|
49
|
+
* Ink only, so it never reads as a state's colour or the queued dashes. */
|
|
50
|
+
mark?: 'add' | 'remove';
|
|
51
|
+
/** The first block of a range being picked: its mark pulses until the range ends. */
|
|
52
|
+
start?: boolean;
|
|
53
|
+
/** Inside the range the pointer would pick (or, with `mark`, unpick) next. */
|
|
54
|
+
preview?: boolean;
|
|
48
55
|
}) => import("react").JSX.Element>;
|
|
49
56
|
/**
|
|
50
57
|
* The one tooltip of a coverage map: it follows the cell under the pointer or keyboard focus inside
|
package/dist/coverage-cells.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// What LumenCoverageMap and LumenCoverageCalendarMap share: one state per cell, its look, the legend of the states on screen and the cell with its
|
|
3
3
|
// tooltip. A colour fix here reaches every coverage surface.
|
|
4
|
+
import { CheckIcon, MinusIcon } from 'lucide-react';
|
|
4
5
|
import { memo, useRef, useState } from 'react';
|
|
5
6
|
import { cn } from './lib/utils.js';
|
|
6
7
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
@@ -67,14 +68,20 @@ export function CoverageLegend({ levels, hint, labels, }) {
|
|
|
67
68
|
* cell made a 100-row map 13,000 tooltips, seconds per render). `label` is the
|
|
68
69
|
* accessible name and the tooltip's first line; `tip` marks the cell for the shared tooltip.
|
|
69
70
|
*/
|
|
70
|
-
export const CoverageCell = memo(function CoverageCell({ level, label, tip, picked = false, disabled = false, pickable = false, onClick, className, fill, }) {
|
|
71
|
-
return (
|
|
71
|
+
export const CoverageCell = memo(function CoverageCell({ level, label, tip, picked = false, disabled = false, pickable = false, onClick, className, fill, mark, start = false, preview = false, }) {
|
|
72
|
+
return (_jsxs("button", { type: "button", "data-cell": tip, "aria-label": `${label}: ${coverageLabel(level)}`, "aria-pressed": pickable && !disabled ? picked : undefined, disabled: disabled, onClick: onClick, className: cn('min-w-0 rounded-[3px] p-0 focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default', coverageClass(level), 'relative overflow-hidden',
|
|
72
73
|
// outline-none would also hide the picked outline, so a picked cell must not carry it.
|
|
73
|
-
|
|
74
|
-
? '
|
|
75
|
-
:
|
|
76
|
-
? ''
|
|
77
|
-
:
|
|
74
|
+
mark && !preview
|
|
75
|
+
? 'shadow-[inset_0_0_0_2px_var(--foreground),inset_0_0_0_3px_var(--card)] outline-none'
|
|
76
|
+
: preview
|
|
77
|
+
? 'shadow-[inset_0_0_0_2px_color-mix(in_oklab,var(--foreground)_45%,transparent)] outline-none'
|
|
78
|
+
: picked
|
|
79
|
+
? 'outline-2 outline-offset-1 outline-foreground'
|
|
80
|
+
: level === 'queued' || level === 'failed'
|
|
81
|
+
? ''
|
|
82
|
+
: 'outline-none', pickable && !disabled && 'cursor-pointer', className), children: [fill !== undefined && fill > 0 && fill < 1 && (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-y-0 left-0 bg-cov-full", style: { width: `${Math.round(fill * 100)}%` } })), mark && (_jsx("span", { "aria-hidden": true, className: cn('pointer-events-none absolute top-1/2 right-0.5 flex size-2.5 -translate-y-1/2 items-center justify-center rounded-full bg-foreground text-background sm:size-3', start && 'motion-safe:animate-pulse',
|
|
83
|
+
// A picked block inside an unpicking range: its check fades, about to go.
|
|
84
|
+
preview && 'opacity-40'), children: mark === 'add' ? (_jsx(CheckIcon, { className: "size-2 stroke-[3.5] sm:size-2.5" })) : (_jsx(MinusIcon, { className: "size-2 stroke-[3.5] sm:size-2.5" })) }))] }));
|
|
78
85
|
});
|
|
79
86
|
/**
|
|
80
87
|
* The one tooltip of a coverage map: it follows the cell under the pointer or keyboard focus inside
|
package/dist/coverage-map.d.ts
CHANGED
|
@@ -72,7 +72,8 @@ export interface LumenCoverageMapProps<TRaw = unknown, TKey extends QueryKey = Q
|
|
|
72
72
|
unit?: string;
|
|
73
73
|
/** Right-hand columns; a phone keeps Complete only. */
|
|
74
74
|
summary?: CoverageSummary[];
|
|
75
|
-
/** `backfill`:
|
|
75
|
+
/** `backfill`: tap a block, then another to pick the range between (a year's label picks its
|
|
76
|
+
* year), then the selection bar's buttons. `drill`: a click opens the block. */
|
|
76
77
|
select?: 'backfill' | 'drill';
|
|
77
78
|
/** Months a backfill can take (the source's own limits). */
|
|
78
79
|
selectable?: (row: string, month: string) => boolean;
|
|
@@ -88,6 +89,8 @@ export interface LumenCoverageMapProps<TRaw = unknown, TKey extends QueryKey = Q
|
|
|
88
89
|
stateLabels?: Partial<Record<CoverageLevel, string>>;
|
|
89
90
|
/** Picks these months as if clicked (pass a new object each time); nothing is queued until the bar's button. */
|
|
90
91
|
preselect?: CoveragePick;
|
|
92
|
+
/** In the selection bar while nothing is picked: quick picks (buttons that set `preselect`). */
|
|
93
|
+
pickTools?: ReactNode;
|
|
91
94
|
/** The drilled block (`2026-09`) and the click that changes it. */
|
|
92
95
|
drilled?: string | null;
|
|
93
96
|
onDrill?: (row: string, period: string) => void;
|
|
@@ -121,4 +124,4 @@ export interface LumenCoverageMapProps<TRaw = unknown, TKey extends QueryKey = Q
|
|
|
121
124
|
export declare function blockOf(period: string, block: CoverageBlock): string;
|
|
122
125
|
/** `2026-09` → `Sep 2026`, `2026-Q3` → `2026 Q3`, days stay as they are. */
|
|
123
126
|
export declare function blockName(key: string): string;
|
|
124
|
-
export declare function LumenCoverageMap<TRaw, TKey extends QueryKey>({ source, title, icon, rows: rowSpec, layout, block: initialBlock, zoom, window, unit, summary, select, selectable, onQueue, queueLabel, pickNote, removable, onRemove, preselect, stateLabels, drilled, onDrill, groupBy, hint, pageSize: initialPageSize, pager, tools, filter, filterCount, empty, lazy, }: LumenCoverageMapProps<TRaw, TKey>): import("react").JSX.Element;
|
|
127
|
+
export declare function LumenCoverageMap<TRaw, TKey extends QueryKey>({ source, title, icon, rows: rowSpec, layout, block: initialBlock, zoom, window, unit, summary, select, selectable, onQueue, queueLabel, pickNote, removable, onRemove, preselect, pickTools, stateLabels, drilled, onDrill, groupBy, hint, pageSize: initialPageSize, pager, tools, filter, filterCount, empty, lazy, }: LumenCoverageMapProps<TRaw, TKey>): import("react").JSX.Element;
|
package/dist/coverage-map.js
CHANGED
|
@@ -180,7 +180,7 @@ function sameNode(a, b) {
|
|
|
180
180
|
* changes (`sig`), so a poll redraws the rows whose blocks moved and a pick redraws one row
|
|
181
181
|
* (a 100-row map used to redraw all 13,000 cells on every poll).
|
|
182
182
|
*/
|
|
183
|
-
const MapRow = memo(function MapRow({ r, cols, marks, picks, total, look, }) {
|
|
183
|
+
const MapRow = memo(function MapRow({ r, cols, marks, picks, start, previews, total, look, }) {
|
|
184
184
|
const text = r.label ?? r.row;
|
|
185
185
|
const link = look.link?.(r.row);
|
|
186
186
|
return (_jsxs(_Fragment, { children: [_jsxs("span", {
|
|
@@ -189,7 +189,7 @@ const MapRow = memo(function MapRow({ r, cols, marks, picks, total, look, }) {
|
|
|
189
189
|
const m = marks[i];
|
|
190
190
|
if (!m)
|
|
191
191
|
return _jsx("span", {}, k);
|
|
192
|
-
return (_jsx(CoverageCell, { level: m.level, fill: m.fill, label: `${text} · ${blockName(k)}`, tip: `${r.row}|${k}`, picked: picks.includes(k) || look.drilled === k, pickable: look.select === 'backfill' || look.select === 'drill', disabled: look.select === 'backfill' ? !m.canPick : look.select !== 'drill', className: "h-[18px]" }, k));
|
|
192
|
+
return (_jsx(CoverageCell, { level: m.level, fill: m.fill, label: `${text} · ${blockName(k)}`, tip: `${r.row}|${k}`, picked: picks.includes(k) || look.drilled === k, mark: picks.includes(k) ? m.kind : undefined, start: start === k, preview: m.canPick && previews.includes(k), pickable: look.select === 'backfill' || look.select === 'drill', disabled: look.select === 'backfill' ? !m.canPick : look.select !== 'drill', className: "h-[18px]" }, k));
|
|
193
193
|
}), look.sums.map((s) => (_jsx("span", { className: cn('num pl-3 text-right text-xs whitespace-nowrap', s !== 'complete' && 'text-muted-foreground'), children: s === 'complete'
|
|
194
194
|
? fmtPct(total.have, total.expected)
|
|
195
195
|
: s === 'first'
|
|
@@ -200,7 +200,7 @@ const MapRow = memo(function MapRow({ r, cols, marks, picks, total, look, }) {
|
|
|
200
200
|
? fmtCounts(total.have, total.expected)
|
|
201
201
|
: fmtInt(total.have) }, s)))] }));
|
|
202
202
|
}, (a, b) => a.sig === b.sig && sameNode(a.r.badge, b.r.badge));
|
|
203
|
-
export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowSpec, layout = 'grid', block: initialBlock = 'month', zoom, window = 'all', unit = 'sessions', summary = ['complete', 'last'], select, selectable = () => true, onQueue, queueLabel = 'Queue backfill', pickNote, removable = () => false, onRemove, preselect, stateLabels, drilled = null, onDrill, groupBy = false, hint, pageSize: initialPageSize, pager, tools, filter, filterCount, empty, lazy = false, }) {
|
|
203
|
+
export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowSpec, layout = 'grid', block: initialBlock = 'month', zoom, window = 'all', unit = 'sessions', summary = ['complete', 'last'], select, selectable = () => true, onQueue, queueLabel = 'Queue backfill', pickNote, removable = () => false, onRemove, preselect, pickTools, stateLabels, drilled = null, onDrill, groupBy = false, hint, pageSize: initialPageSize, pager, tools, filter, filterCount, empty, lazy = false, }) {
|
|
204
204
|
const { data, load: raw, ref } = useLumenSource(source, { lazy });
|
|
205
205
|
// A coverage map refreshes in place: any refresh, asked for or a poll, only spins its ↻; no bar or dimming over the cells.
|
|
206
206
|
const load = { ...raw, fetching: raw.fetching || raw.background };
|
|
@@ -209,11 +209,18 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
209
209
|
const [block, setBlock] = useState(layout === 'months' ? 'month' : initialBlock);
|
|
210
210
|
const [picked, setPicked] = useState(new Set());
|
|
211
211
|
const [queueing, setQueueing] = useState(false);
|
|
212
|
-
|
|
212
|
+
// A range: the first tap picks a block and starts it, the next tap in the row picks every block
|
|
213
|
+
// between. Started on a picked block, the range unpicks instead (`remove`).
|
|
214
|
+
// `over` is the block under the pointer meanwhile; `last` anchors shift-click.
|
|
215
|
+
const [start, setStart] = useState(null);
|
|
216
|
+
const [over, setOver] = useState(null);
|
|
217
|
+
const last = useRef(null);
|
|
213
218
|
// A preselect picks its months as if clicked; it replaces the current pick.
|
|
214
219
|
useEffect(() => {
|
|
215
|
-
if (preselect)
|
|
216
|
-
|
|
220
|
+
if (!preselect)
|
|
221
|
+
return;
|
|
222
|
+
setPicked(new Set(preselect.flatMap((p) => p.months.map((m) => `${p.row}|${m}`))));
|
|
223
|
+
setStart(null);
|
|
217
224
|
}, [preselect]);
|
|
218
225
|
const scroller = useRef(null);
|
|
219
226
|
const [pageSize, setPageSize] = useState(initialPageSize ?? 0);
|
|
@@ -234,7 +241,7 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
234
241
|
// biome-ignore lint/correctness/useExhaustiveDependencies: re-run when the columns change
|
|
235
242
|
useLayoutEffect(() => {
|
|
236
243
|
const el = scroller.current;
|
|
237
|
-
if (el)
|
|
244
|
+
if (el && layout === 'grid')
|
|
238
245
|
el.scrollLeft = el.scrollWidth;
|
|
239
246
|
}, [strip]);
|
|
240
247
|
const sums = mobile ? summary.filter((s) => s === 'complete') : summary;
|
|
@@ -246,7 +253,7 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
246
253
|
const ids = keys.map((k) => `${row}|${k}`);
|
|
247
254
|
const all = ids.every((id) => next.has(id));
|
|
248
255
|
for (const id of ids) {
|
|
249
|
-
if (mode === 'toggle' && all)
|
|
256
|
+
if (mode === 'remove' || (mode === 'toggle' && all))
|
|
250
257
|
next.delete(id);
|
|
251
258
|
else
|
|
252
259
|
next.add(id);
|
|
@@ -277,6 +284,7 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
277
284
|
try {
|
|
278
285
|
await act(pickedMonths(remove ? removable : selectable));
|
|
279
286
|
setPicked(new Set());
|
|
287
|
+
setStart(null);
|
|
280
288
|
}
|
|
281
289
|
finally {
|
|
282
290
|
setQueueing(false);
|
|
@@ -303,6 +311,15 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
303
311
|
.get(row)
|
|
304
312
|
?.get(key)
|
|
305
313
|
?.months.some((m) => selectable(row, m) || (onRemove !== undefined && removable(row, m)));
|
|
314
|
+
/** What picking a block does: queue its months, or take them out of the queue. */
|
|
315
|
+
const kindOf = (row, key) => folded?.cells
|
|
316
|
+
.get(row)
|
|
317
|
+
?.get(key)
|
|
318
|
+
?.months.some((m) => selectable(row, m))
|
|
319
|
+
? 'add'
|
|
320
|
+
: 'remove';
|
|
321
|
+
/** A row's blocks in time order; the months layout runs across years. */
|
|
322
|
+
const orderOf = (row) => layout === 'months' ? [...(folded?.cells.get(row)?.keys() ?? [])].sort() : cols;
|
|
306
323
|
// One click handler for every cell: drill, or pick (shift-click picks a range in the row).
|
|
307
324
|
const onCellClick = (e) => {
|
|
308
325
|
const el = e.target instanceof Element ? e.target.closest('[data-cell]') : null;
|
|
@@ -318,33 +335,88 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
318
335
|
}
|
|
319
336
|
if (select !== 'backfill')
|
|
320
337
|
return;
|
|
321
|
-
|
|
322
|
-
const order = layout === 'months' ? [...(folded?.cells.get(row)?.keys() ?? [])].sort() : cols;
|
|
338
|
+
const order = orderOf(row);
|
|
323
339
|
const i = order.indexOf(k);
|
|
324
|
-
|
|
325
|
-
const [lo, hi] = [
|
|
326
|
-
|
|
327
|
-
|
|
340
|
+
const range = (from, mode = 'add') => {
|
|
341
|
+
const [lo, hi] = [from, i].sort((x, y) => x - y);
|
|
342
|
+
toggle(row, order.slice(lo, hi + 1).filter((key) => canPick(row, key)), mode);
|
|
343
|
+
};
|
|
344
|
+
if (e.shiftKey && last.current?.row === row) {
|
|
345
|
+
range(last.current.index);
|
|
346
|
+
setStart(null);
|
|
347
|
+
}
|
|
348
|
+
else if (start?.row === row) {
|
|
349
|
+
// The second tap ends the range. On its own first block it only ends it: a picked range
|
|
350
|
+
// start is out already, a new one goes back out.
|
|
351
|
+
if (start.key === k)
|
|
352
|
+
toggle(row, [k], 'remove');
|
|
353
|
+
else
|
|
354
|
+
range(order.indexOf(start.key), start.remove ? 'remove' : 'add');
|
|
355
|
+
setStart(null);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
const remove = picked.has(`${row}|${k}`);
|
|
359
|
+
toggle(row, [k], remove ? 'remove' : 'add');
|
|
360
|
+
setStart({ row, key: k, remove });
|
|
328
361
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
362
|
+
setOver(null);
|
|
363
|
+
last.current = { row, index: i };
|
|
364
|
+
};
|
|
365
|
+
// While a range is open, the block under the pointer shows what the next tap would pick.
|
|
366
|
+
const onCellOver = (e) => {
|
|
367
|
+
if (!start)
|
|
368
|
+
return;
|
|
369
|
+
const el = e.target instanceof Element ? e.target.closest('[data-cell]') : null;
|
|
370
|
+
const tip = el?.dataset.cell ?? '';
|
|
371
|
+
const cut = tip.lastIndexOf('|');
|
|
372
|
+
if (el && tip.slice(0, cut) === start.row)
|
|
373
|
+
setOver(tip.slice(cut + 1));
|
|
374
|
+
};
|
|
375
|
+
const previewOf = (row) => {
|
|
376
|
+
if (!start || start.row !== row || !over)
|
|
377
|
+
return [];
|
|
378
|
+
const order = orderOf(row);
|
|
379
|
+
const [lo, hi] = [order.indexOf(start.key), order.indexOf(over)].sort((x, y) => x - y);
|
|
380
|
+
// An unpicking range previews the picked blocks it would take out; a picking one the rest.
|
|
381
|
+
return lo < 0
|
|
382
|
+
? []
|
|
383
|
+
: order
|
|
384
|
+
.slice(lo, hi + 1)
|
|
385
|
+
.filter((k) => k === start.key ? start.remove : picked.has(`${row}|${k}`) === start.remove);
|
|
386
|
+
};
|
|
387
|
+
const picking = picked.size > 0 || !!start;
|
|
388
|
+
// Picking fades the blocks a pick can't take, so what can be picked stands out.
|
|
389
|
+
const routes = {
|
|
390
|
+
onClick: onCellClick,
|
|
391
|
+
onPointerOver: onCellOver,
|
|
392
|
+
onPointerLeave: () => setOver(null),
|
|
393
|
+
onKeyDown: (e) => {
|
|
394
|
+
if (e.key === 'Escape')
|
|
395
|
+
setStart(null);
|
|
396
|
+
},
|
|
332
397
|
};
|
|
398
|
+
const fade = select === 'backfill' && picking && '[&_[data-cell]:disabled]:opacity-30';
|
|
333
399
|
let body = null;
|
|
334
400
|
if (data && folded) {
|
|
335
401
|
if (layout === 'months') {
|
|
336
402
|
const row = data.rows[0]?.row ?? data.facts[0]?.row ?? '';
|
|
337
403
|
const cells = folded.cells.get(row) ?? new Map();
|
|
404
|
+
const previews = new Set(previewOf(row));
|
|
338
405
|
const years = [...new Set([...cells.keys()].map((k) => k.slice(0, 4)))].sort().reverse();
|
|
339
406
|
const sumCols = sums.map((s) => SUMMARY_WIDTH[s]).join(' ');
|
|
340
407
|
const grid = {
|
|
341
408
|
gridTemplateColumns: `${mobile ? '2.75rem' : '4rem'} repeat(12, minmax(${mobile ? '14px' : '18px'}, 1fr)) ${sumCols}`,
|
|
342
409
|
};
|
|
343
|
-
body = (_jsxs("div", { className:
|
|
410
|
+
body = (_jsxs("div", { className: cn('grid items-center gap-[3px]', fade), style: grid, ...routes, children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Year" }), MON.map((m, i) => (_jsx("span", { className: "text-xs text-muted-foreground", children: mobile ? (i % 3 === 0 ? m : '') : m }, m))), sums.map((s) => (_jsx("span", { className: "pl-3 text-right text-xs text-muted-foreground", children: s === 'rows' || s === 'counts' ? rowsHead : SUMMARY_HEAD[s] }, s))), years.map((y) => {
|
|
344
411
|
let have = 0;
|
|
345
412
|
let expected = 0;
|
|
413
|
+
// A year's label picks the months in it that can be queued (again: takes them out).
|
|
414
|
+
const yearKeys = MON.map((_, i) => `${y}-${String(i + 1).padStart(2, '0')}`).filter((k) => canPick(row, k) && kindOf(row, k) === 'add');
|
|
346
415
|
return [
|
|
347
|
-
_jsx("
|
|
416
|
+
select === 'backfill' && yearKeys.length ? (_jsx("button", { type: "button", "aria-label": `Pick ${y}`, className: "num -ml-1 w-fit cursor-pointer rounded px-1 text-left text-xs hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", onClick: () => {
|
|
417
|
+
toggle(row, yearKeys, 'toggle');
|
|
418
|
+
setStart(null);
|
|
419
|
+
}, children: y }, `${y}-label`)) : (_jsx("span", { className: "num text-xs", children: y }, `${y}-label`)),
|
|
348
420
|
...MON.map((_, i) => {
|
|
349
421
|
const key = `${y}-${String(i + 1).padStart(2, '0')}`;
|
|
350
422
|
const c = cells.get(key);
|
|
@@ -353,7 +425,7 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
353
425
|
levels.add(c.level);
|
|
354
426
|
have += c.have;
|
|
355
427
|
expected += c.expected;
|
|
356
|
-
return (_jsx(CoverageCell, { level: c.level, fill: c.expected > 0 ? c.have / c.expected : undefined, label: `${row} · ${blockName(key)}`, tip: `${row}|${key}`, picked: picked.has(`${row}|${key}`) || drilled === key, pickable: select === 'backfill' || select === 'drill', disabled: select === 'backfill' ? !canPick(row, key) : select !== 'drill', className: mobile ? 'h-4' : 'h-5' }, key));
|
|
428
|
+
return (_jsx(CoverageCell, { level: c.level, fill: c.expected > 0 ? c.have / c.expected : undefined, label: `${row} · ${blockName(key)}`, tip: `${row}|${key}`, picked: picked.has(`${row}|${key}`) || drilled === key, mark: picked.has(`${row}|${key}`) ? kindOf(row, key) : undefined, start: start?.key === key, preview: previews.has(key) && canPick(row, key), pickable: select === 'backfill' || select === 'drill', disabled: select === 'backfill' ? !canPick(row, key) : select !== 'drill', className: mobile ? 'h-4' : 'h-5' }, key));
|
|
357
429
|
}),
|
|
358
430
|
...sums.map((s) => (_jsx("span", { className: cn('num pl-3 text-right text-xs', s !== 'complete' && 'text-muted-foreground'), children: s === 'complete'
|
|
359
431
|
? fmtPct(have, expected)
|
|
@@ -390,7 +462,7 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
390
462
|
}
|
|
391
463
|
const look = { mobile, groupBy, select, sums, drilled, link: rowSpec.link };
|
|
392
464
|
let group;
|
|
393
|
-
body = (_jsxs("div", { className:
|
|
465
|
+
body = (_jsxs("div", { className: cn('grid min-w-min items-center gap-[3px]', fade), style: grid, ...routes, children: [_jsx("span", { className: "sticky left-0 z-1 bg-card text-xs text-muted-foreground", children: mobile ? '' : rowSpec.title }), cols.map((k) => (_jsx("span", { className: "num h-4 overflow-visible text-[11px] whitespace-nowrap text-muted-foreground", children: headLabel(k, block, cols.length) }, k))), sums.map((s) => (_jsx("span", { className: "pl-3 text-right text-xs text-muted-foreground", children: s === 'rows' || s === 'counts' ? rowsHead : SUMMARY_HEAD[s] }, s))), rowList.flatMap((r) => {
|
|
394
466
|
const out = [];
|
|
395
467
|
const shut = groupBy && folds.has(foldKey + (r.group ?? ''));
|
|
396
468
|
if (groupBy && r.group !== group) {
|
|
@@ -411,10 +483,13 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
411
483
|
level: c.level,
|
|
412
484
|
canPick: select === 'backfill' && canPick(r.row, k),
|
|
413
485
|
fill: c.expected > 0 ? c.have / c.expected : undefined,
|
|
486
|
+
kind: select === 'backfill' && canPick(r.row, k) ? kindOf(r.row, k) : undefined,
|
|
414
487
|
};
|
|
415
488
|
});
|
|
416
489
|
const picks = pickedBy.get(r.row) ?? [];
|
|
417
|
-
|
|
490
|
+
const rowStart = start?.row === r.row ? start.key : undefined;
|
|
491
|
+
const previews = previewOf(r.row);
|
|
492
|
+
out.push(_jsx(MapRow, { r: r, cols: cols, marks: marks, picks: picks, start: rowStart, previews: previews, total: folded.totals.get(r.row) ?? { have: 0, expected: 0 }, look: look, sig: [
|
|
418
493
|
r.label,
|
|
419
494
|
r.sub,
|
|
420
495
|
r.first,
|
|
@@ -429,8 +504,10 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
429
504
|
cols[0],
|
|
430
505
|
cols.at(-1),
|
|
431
506
|
picks.join(),
|
|
507
|
+
rowStart,
|
|
508
|
+
previews.join(),
|
|
432
509
|
marks
|
|
433
|
-
.map((m) =>
|
|
510
|
+
.map((m) => m ? `${m.level}${m.canPick ? '+' : ''}${m.kind ?? ''}${m.fill ?? ''}` : '')
|
|
434
511
|
.join(),
|
|
435
512
|
JSON.stringify(folded.totals.get(r.row) ?? null),
|
|
436
513
|
].join('\u0001') }, r.row));
|
|
@@ -446,20 +523,31 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
446
523
|
const pickedRows = [...new Set([...pick, ...unpick].map((p) => p.row))];
|
|
447
524
|
const rowName = (row) => data?.rows.find((r) => r.row === row)?.label ?? row;
|
|
448
525
|
const months = (n) => `${n} month${n > 1 ? 's' : ''}`;
|
|
526
|
+
const word = layout === 'months' ? 'month' : 'block in the row';
|
|
449
527
|
const head = (_jsxs(_Fragment, { children: [tools, zoom && layout === 'grid' && (_jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: block, onValueChange: (v) => {
|
|
450
528
|
if (!v)
|
|
451
529
|
return;
|
|
452
530
|
setBlock(v);
|
|
453
531
|
setPicked(new Set());
|
|
532
|
+
setStart(null);
|
|
454
533
|
}, "aria-label": "Block size", className: "max-sm:hidden", children: zoom.map((z) => (_jsx(ToggleGroupItem, { value: z, children: BLOCK_LABEL[z] }, z))) })), pager ? (_jsx(LumenPager, { total: pager.total, page: pager.page, size: pager.size, sizes: pager.sizes, onPage: pager.onPage, onSize: pager.onSize, noun: plural(rowSpec.title) })) : (pageSize > 0 &&
|
|
455
534
|
layout === 'grid' &&
|
|
456
535
|
data && (_jsx(LumenPager, { total: rowTotal, page: page, size: pageSize, onPage: setPageIndex, onSize: (n) => {
|
|
457
536
|
setPageSize(n);
|
|
458
537
|
setPageIndex(0);
|
|
459
538
|
}, noun: plural(rowSpec.title) })))] }));
|
|
460
|
-
return (_jsx("div", { ref: ref, className: "min-w-0", children: _jsx(LumenWidget, { id: title, kind: null, icon: icon, title: title, tools: head, filter: filter, filterCount: filterCount, load: load, rows: data?.rows.length, pad: false, children: _jsx("div", { className: "flex flex-col gap-3 px-3 pt-3 pb-3", children: _jsxs(LumenStates, { load: cells, skeleton: _jsx(GridSkeleton, {}), empty: data && !data.rows.length && !data.facts.length ? empty : undefined, children: [
|
|
539
|
+
return (_jsx("div", { ref: ref, className: "min-w-0", children: _jsx(LumenWidget, { id: title, kind: null, icon: icon, title: title, tools: head, filter: filter, filterCount: filterCount, load: load, rows: data?.rows.length, pad: false, children: _jsx("div", { className: "flex flex-col gap-3 px-3 pt-3 pb-3", children: _jsxs(LumenStates, { load: cells, skeleton: _jsx(GridSkeleton, {}), empty: data && !data.rows.length && !data.facts.length ? empty : undefined, children: [select === 'backfill' && (_jsxs("div", { "aria-live": "polite", className: cn('flex min-h-11 flex-wrap items-center gap-2 rounded-lg px-3 py-1.5 text-sm', pickedCount > 0
|
|
540
|
+
? 'bg-foreground/5 ring-1 ring-foreground ring-inset'
|
|
541
|
+
: 'bg-muted'), children: [_jsx("span", { className: "min-w-0 flex-1 basis-56", children: pickedCount > 0 ? (_jsxs(_Fragment, { children: [layout === 'grid' && (_jsx("span", { className: "num font-semibold", children: `${pickedRows.map(rowName).join(', ')} · ` })), _jsx("span", { className: "font-semibold", children: `${months(pickedCount)} picked` }), _jsxs("span", { className: "text-muted-foreground", children: [pickNote && queueCount > 0 ? ` · ${pickNote(pick)}` : '', start
|
|
542
|
+
? ` · Tap another ${word} to ${start.remove ? 'unpick' : 'pick'} the range`
|
|
543
|
+
: ''] })] })) : (_jsx("span", { className: "text-muted-foreground", children: pickTools
|
|
544
|
+
? 'Pick on the map, or'
|
|
545
|
+
: `Tap a ${word}, then another to pick the range` })) }), pickedCount > 0 ? (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
546
|
+
setPicked(new Set());
|
|
547
|
+
setStart(null);
|
|
548
|
+
}, children: "Clear" }), removeCount > 0 && (_jsxs(Button, { variant: "outline", size: "sm", disabled: queueing, onClick: () => void queue(true), children: [_jsx(ListXIcon, {}), " Remove from queue \u00B7 ", months(removeCount)] })), queueCount > 0 && (_jsxs(Button, { size: "sm", disabled: queueing || !onQueue, onClick: () => void queue(), children: [_jsx(ListPlusIcon, {}), " ", queueLabel, " \u00B7 ", months(queueCount)] }))] })) : (pickTools)] })), _jsx("div", { ref: scroller, className: "overflow-x-auto overflow-y-hidden pb-1", children: _jsx(CoverageTip, { content: tipFor, className: layout === 'months' ? 'min-w-0' : 'w-max min-w-full', children: body }) }), _jsx("div", { className: "border-t pt-3", children: _jsx(CoverageLegend, { levels: levels, labels: stateLabels, hint: hint ??
|
|
461
549
|
(select === 'backfill'
|
|
462
|
-
? '
|
|
550
|
+
? 'Tap a block, then another for a range'
|
|
463
551
|
: select === 'drill'
|
|
464
552
|
? 'Click a block to see its days'
|
|
465
553
|
: undefined) }) })] }) }) }) }));
|
package/dist/ui/tabs.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import { Tabs as TabsPrimitive } from 'radix-ui';
|
|
|
2
2
|
import type * as React from 'react';
|
|
3
3
|
declare function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>): React.JSX.Element;
|
|
4
4
|
declare function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>): React.JSX.Element;
|
|
5
|
-
declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.JSX.Element;
|
|
5
|
+
declare function TabsTrigger({ className, onMouseDown, onKeyDown, onBlur, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.JSX.Element;
|
|
6
6
|
declare function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>): React.JSX.Element;
|
|
7
7
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
package/dist/ui/tabs.js
CHANGED
|
@@ -13,8 +13,22 @@ function Tabs({ className, ...props }) {
|
|
|
13
13
|
function TabsList({ className, ...props }) {
|
|
14
14
|
return (_jsx(TabsPrimitive.List, { "data-slot": "tabs-list", className: cn('inline-flex w-full items-center justify-start gap-6 overflow-x-auto overflow-y-hidden text-muted-foreground [scrollbar-width:none]', className), ...props }));
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Radix focuses a trigger from its own mousedown, which browsers count as :focus-visible, so a
|
|
17
|
+
// clicked tab drew the keyboard ring over its underline. A pointer press marks the trigger until
|
|
18
|
+
// the next key or blur; the ring shows only without that mark.
|
|
19
|
+
const pointerOn = (e) => e.currentTarget.setAttribute('data-pointer', '');
|
|
20
|
+
const pointerOff = (e) => e.currentTarget.removeAttribute('data-pointer');
|
|
21
|
+
function TabsTrigger({ className, onMouseDown, onKeyDown, onBlur, ...props }) {
|
|
22
|
+
return (_jsx(TabsPrimitive.Trigger, { "data-slot": "tabs-trigger", onMouseDown: (e) => {
|
|
23
|
+
pointerOn(e);
|
|
24
|
+
onMouseDown?.(e);
|
|
25
|
+
}, onKeyDown: (e) => {
|
|
26
|
+
pointerOff(e);
|
|
27
|
+
onKeyDown?.(e);
|
|
28
|
+
}, onBlur: (e) => {
|
|
29
|
+
pointerOff(e);
|
|
30
|
+
onBlur?.(e);
|
|
31
|
+
}, className: cn("relative inline-flex shrink-0 items-center gap-1.5 px-0 pt-0 pb-2 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-full after:bg-transparent text-sm leading-4 font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:rounded-sm focus-visible:not-data-[pointer]:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground data-[state=active]:after:bg-primary data-[state=active]:[&_svg]:text-primary [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props }));
|
|
18
32
|
}
|
|
19
33
|
function TabsContent({ className, ...props }) {
|
|
20
34
|
return (_jsx(TabsPrimitive.Content, { "data-slot": "tabs-content", className: cn('flex-1 outline-none', className), ...props }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@codemirror/lang-javascript": "6.2.5",
|
|
42
42
|
"@codemirror/lang-json": "6.0.2",
|
|
43
43
|
"@codemirror/lang-sql": "6.10.0",
|
|
44
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
44
|
+
"@columenlabs/lumen-ui-types": "0.1.42",
|
|
45
45
|
"@dnd-kit/core": "6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "10.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "3.2.2",
|