@columenlabs/lumen-ui 0.1.30 → 0.1.32
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-map.d.ts +11 -2
- package/dist/coverage-map.js +39 -19
- package/package.json +2 -2
package/dist/coverage-map.d.ts
CHANGED
|
@@ -72,11 +72,20 @@ 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`: click, shift-click a range, then
|
|
75
|
+
/** `backfill`: click, shift-click a range, then the selection bar's buttons. `drill`: a click opens the block. */
|
|
76
76
|
select?: 'backfill' | 'drill';
|
|
77
77
|
/** Months a backfill can take (the source's own limits). */
|
|
78
78
|
selectable?: (row: string, month: string) => boolean;
|
|
79
79
|
onQueue?: (pick: CoveragePick) => Promise<unknown> | undefined;
|
|
80
|
+
/** The selection bar's queue button (`Queue backfill`). */
|
|
81
|
+
queueLabel?: string;
|
|
82
|
+
/** What the months to queue come to, shown after their count (`1,240 messages · about 2 h`). */
|
|
83
|
+
pickNote?: (pick: CoveragePick) => string;
|
|
84
|
+
/** Months already queued that a pick can take back out, with the bar's Remove from queue. */
|
|
85
|
+
removable?: (row: string, month: string) => boolean;
|
|
86
|
+
onRemove?: (pick: CoveragePick) => Promise<unknown> | undefined;
|
|
87
|
+
/** Picks these months as if clicked (pass a new object each time); nothing is queued until the bar's button. */
|
|
88
|
+
preselect?: CoveragePick;
|
|
80
89
|
/** The drilled block (`2026-09`) and the click that changes it. */
|
|
81
90
|
drilled?: string | null;
|
|
82
91
|
onDrill?: (row: string, period: string) => void;
|
|
@@ -110,4 +119,4 @@ export interface LumenCoverageMapProps<TRaw = unknown, TKey extends QueryKey = Q
|
|
|
110
119
|
export declare function blockOf(period: string, block: CoverageBlock): string;
|
|
111
120
|
/** `2026-09` → `Sep 2026`, `2026-Q3` → `2026 Q3`, days stay as they are. */
|
|
112
121
|
export declare function blockName(key: string): string;
|
|
113
|
-
export declare function LumenCoverageMap<TRaw, TKey extends QueryKey>({ source, title, icon, rows: rowSpec, layout, block: initialBlock, zoom, window, unit, summary, select, selectable, onQueue, drilled, onDrill, groupBy, hint, pageSize: initialPageSize, pager, tools, filter, filterCount, empty, lazy, }: LumenCoverageMapProps<TRaw, TKey>): import("react").JSX.Element;
|
|
122
|
+
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, 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
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Link } from '@tanstack/react-router';
|
|
3
|
-
import { Grid3x3Icon, ListPlusIcon,
|
|
4
|
-
import { isValidElement, memo, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { Grid3x3Icon, ListPlusIcon, ListXIcon } from 'lucide-react';
|
|
4
|
+
import { isValidElement, memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
5
5
|
import { CoverageCell, CoverageLegend, CoverageTip, coverageLabel, coverageLevel, firstJobState, } from './coverage-cells.js';
|
|
6
6
|
import { FoldChevron, useFolds, usePageKey } from './fold.js';
|
|
7
7
|
import { useIsMobile } from './hooks/use-mobile.js';
|
|
8
|
-
import { IconButton } from './icon-button.js';
|
|
9
8
|
import { cn } from './lib/utils.js';
|
|
10
9
|
import { LumenStates, useLumenSource } from './lumen-source.js';
|
|
11
10
|
import { LumenWidget } from './lumen-widget.js';
|
|
12
11
|
import { LumenPager } from './pager.js';
|
|
13
12
|
import { Badge } from './ui/badge.js';
|
|
13
|
+
import { Button } from './ui/button.js';
|
|
14
14
|
import { Skeleton } from './ui/skeleton.js';
|
|
15
15
|
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
|
|
16
16
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
@@ -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, 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, 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 };
|
|
@@ -210,6 +210,11 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
210
210
|
const [picked, setPicked] = useState(new Set());
|
|
211
211
|
const [queueing, setQueueing] = useState(false);
|
|
212
212
|
const anchor = useRef(null);
|
|
213
|
+
// A preselect picks its months as if clicked; it replaces the current pick.
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (preselect)
|
|
216
|
+
setPicked(new Set(preselect.flatMap((p) => p.months.map((m) => `${p.row}|${m}`))));
|
|
217
|
+
}, [preselect]);
|
|
213
218
|
const scroller = useRef(null);
|
|
214
219
|
const [pageSize, setPageSize] = useState(initialPageSize ?? 0);
|
|
215
220
|
const [pageIndex, setPageIndex] = useState(0);
|
|
@@ -248,14 +253,15 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
248
253
|
}
|
|
249
254
|
return next;
|
|
250
255
|
});
|
|
251
|
-
const pickedMonths = () => {
|
|
256
|
+
const pickedMonths = (keep = selectable) => {
|
|
252
257
|
const byRow = new Map();
|
|
253
258
|
for (const id of picked) {
|
|
254
|
-
const
|
|
255
|
-
const
|
|
259
|
+
const cut = id.lastIndexOf('|');
|
|
260
|
+
const row = id.slice(0, cut);
|
|
261
|
+
const c = folded?.cells.get(row)?.get(id.slice(cut + 1));
|
|
256
262
|
const set = byRow.get(row) ?? new Set();
|
|
257
263
|
for (const m of c?.months ?? [])
|
|
258
|
-
if (
|
|
264
|
+
if (keep(row, m))
|
|
259
265
|
set.add(m);
|
|
260
266
|
byRow.set(row, set);
|
|
261
267
|
}
|
|
@@ -263,12 +269,13 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
263
269
|
.filter(([, ms]) => ms.size)
|
|
264
270
|
.map(([row, ms]) => ({ row, months: [...ms].sort() }));
|
|
265
271
|
};
|
|
266
|
-
const queue = async () => {
|
|
267
|
-
|
|
272
|
+
const queue = async (remove = false) => {
|
|
273
|
+
const act = remove ? onRemove : onQueue;
|
|
274
|
+
if (!act)
|
|
268
275
|
return;
|
|
269
276
|
setQueueing(true);
|
|
270
277
|
try {
|
|
271
|
-
await
|
|
278
|
+
await act(pickedMonths(remove ? removable : selectable));
|
|
272
279
|
setPicked(new Set());
|
|
273
280
|
}
|
|
274
281
|
finally {
|
|
@@ -291,6 +298,11 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
291
298
|
detail: (_jsxs(_Fragment, { children: [_jsxs("div", { children: [unitLabel(c.have, c.expected), " \u00B7 ", coverageLabel(c.level)] }), c.notes.map((n) => (_jsx("div", { children: n }, n))), layout === 'months' && select === 'drill' && _jsx("div", { children: "Click to see its days" })] })),
|
|
292
299
|
};
|
|
293
300
|
};
|
|
301
|
+
/** A block a pick can take: a month in it can be queued, or taken out of the queue. */
|
|
302
|
+
const canPick = (row, key) => !!folded?.cells
|
|
303
|
+
.get(row)
|
|
304
|
+
?.get(key)
|
|
305
|
+
?.months.some((m) => selectable(row, m) || (onRemove !== undefined && removable(row, m)));
|
|
294
306
|
// One click handler for every cell: drill, or pick (shift-click picks a range in the row).
|
|
295
307
|
const onCellClick = (e) => {
|
|
296
308
|
const el = e.target instanceof Element ? e.target.closest('[data-cell]') : null;
|
|
@@ -306,10 +318,13 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
306
318
|
}
|
|
307
319
|
if (select !== 'backfill')
|
|
308
320
|
return;
|
|
309
|
-
|
|
321
|
+
// The months layout ranges over its months in time order, across years.
|
|
322
|
+
const order = layout === 'months' ? [...(folded?.cells.get(row)?.keys() ?? [])].sort() : cols;
|
|
323
|
+
const i = order.indexOf(k);
|
|
310
324
|
if (e.shiftKey && anchor.current?.row === row) {
|
|
311
325
|
const [lo, hi] = [anchor.current.index, i].sort((x, y) => x - y);
|
|
312
|
-
|
|
326
|
+
const span = order.slice(lo, hi + 1);
|
|
327
|
+
toggle(row, span.filter((key) => canPick(row, key)), 'add');
|
|
313
328
|
}
|
|
314
329
|
else
|
|
315
330
|
toggle(row, [k], 'toggle');
|
|
@@ -338,7 +353,7 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
338
353
|
levels.add(c.level);
|
|
339
354
|
have += c.have;
|
|
340
355
|
expected += c.expected;
|
|
341
|
-
return (_jsx(CoverageCell, { level: c.level, label: `${row} · ${blockName(key)}`, tip: `${row}|${key}`, picked: drilled === key, pickable: select === 'drill', disabled: select !== 'drill', className: mobile ? 'h-4' : 'h-5' }, key));
|
|
356
|
+
return (_jsx(CoverageCell, { level: c.level, 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));
|
|
342
357
|
}),
|
|
343
358
|
...sums.map((s) => (_jsx("span", { className: cn('num pl-3 text-right text-xs', s !== 'complete' && 'text-muted-foreground'), children: s === 'complete'
|
|
344
359
|
? fmtPct(have, expected)
|
|
@@ -392,8 +407,7 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
392
407
|
if (!c)
|
|
393
408
|
return null;
|
|
394
409
|
levels.add(c.level);
|
|
395
|
-
|
|
396
|
-
return { level: c.level, canPick };
|
|
410
|
+
return { level: c.level, canPick: select === 'backfill' && canPick(r.row, k) };
|
|
397
411
|
});
|
|
398
412
|
const picks = pickedBy.get(r.row) ?? [];
|
|
399
413
|
out.push(_jsx(MapRow, { r: r, cols: cols, marks: marks, picks: picks, total: folded.totals.get(r.row) ?? { have: 0, expected: 0 }, look: look, sig: [
|
|
@@ -419,7 +433,13 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
419
433
|
}
|
|
420
434
|
}
|
|
421
435
|
const pick = picked.size ? pickedMonths() : [];
|
|
422
|
-
const
|
|
436
|
+
const queueCount = pick.reduce((n, p) => n + p.months.length, 0);
|
|
437
|
+
const unpick = picked.size && onRemove ? pickedMonths(removable) : [];
|
|
438
|
+
const removeCount = unpick.reduce((n, p) => n + p.months.length, 0);
|
|
439
|
+
const pickedCount = queueCount + removeCount;
|
|
440
|
+
const pickedRows = [...new Set([...pick, ...unpick].map((p) => p.row))];
|
|
441
|
+
const rowName = (row) => data?.rows.find((r) => r.row === row)?.label ?? row;
|
|
442
|
+
const months = (n) => `${n} month${n > 1 ? 's' : ''}`;
|
|
423
443
|
const head = (_jsxs(_Fragment, { children: [tools, zoom && layout === 'grid' && (_jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: block, onValueChange: (v) => {
|
|
424
444
|
if (!v)
|
|
425
445
|
return;
|
|
@@ -431,9 +451,9 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
|
|
|
431
451
|
setPageSize(n);
|
|
432
452
|
setPageIndex(0);
|
|
433
453
|
}, noun: plural(rowSpec.title) })))] }));
|
|
434
|
-
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: [_jsx("div", { ref: scroller, className: "overflow-x-auto overflow-y-hidden pb-1", children: _jsx(CoverageTip, { content: tipFor, className: "w-max min-w-full", children: body }) }), pickedCount > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-lg bg-muted px-3 py-2 text-sm", children: [_jsxs("span", { className: "min-w-0
|
|
454
|
+
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: [_jsx("div", { ref: scroller, className: "overflow-x-auto overflow-y-hidden pb-1", children: _jsx(CoverageTip, { content: tipFor, className: "w-max min-w-full", children: body }) }), pickedCount > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-lg bg-muted px-3 py-2 text-sm", children: [_jsxs("span", { className: "min-w-0", children: [layout === 'grid' && (_jsx("span", { className: "num font-semibold", children: `${pickedRows.map(rowName).join(', ')} · ` })), `${months(pickedCount)} picked`, pickNote && queueCount > 0 ? ` · ${pickNote(pick)}` : ''] }), _jsx("span", { className: "flex-1" }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setPicked(new Set()), 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)] }))] })), _jsx("div", { className: "border-t pt-3", children: _jsx(CoverageLegend, { levels: levels, hint: hint ??
|
|
435
455
|
(select === 'backfill'
|
|
436
|
-
? 'Click a
|
|
456
|
+
? 'Click a block, shift-click for a range'
|
|
437
457
|
: select === 'drill'
|
|
438
458
|
? 'Click a block to see its days'
|
|
439
459
|
: undefined) }) })] }) }) }) }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
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.32",
|
|
45
45
|
"@dnd-kit/core": "6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "10.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "3.2.2",
|