@columenlabs/lumen-ui 0.1.54 → 0.1.56
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 +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lumen-view.js +65 -38
- package/dist/pace.d.ts +53 -0
- package/dist/pace.js +120 -0
- package/dist/theme.css +18 -0
- package/dist/time-to-cap.d.ts +25 -0
- package/dist/time-to-cap.js +45 -0
- package/dist/unit-value.d.ts +3 -1
- package/dist/unit-value.js +12 -1
- package/dist/view-model.d.ts +6 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -75,10 +75,13 @@ Every export, its props and an example are on the [docs](https://lumen.columenla
|
|
|
75
75
|
- Every card body sits in `LumenBoundary`, so one crash never blanks a page.
|
|
76
76
|
- Numbers use tabular figures and a true minus `−`; a gain or loss always shows a sign or arrow.
|
|
77
77
|
- A page's header numbers are metric cards: `TileRow` of `Tile` (or `TileToggleGroup` when they pick what the page shows), with `fill` so they share the row, each with `spark` set to its daily history where it has one. Don't build a card of your own for this.
|
|
78
|
+
- Column widths: the first column (a row's name) grows only to `--lead-col-max` (18rem, theme.css); past it the cell truncates on one line and a plain text value shows in full on hover (a rendered cell brings its own hover card). Cap any other column with the field's `maxWidth` (`'12rem'`), or lift the cap with `maxWidth: false`. In a rendered name cell, put the name in a `truncate` span so the ellipsis lands on it, not on a chip after it.
|
|
79
|
+
- Column alignment is the table's, not the cell's. Text left, numbers right, and a number column has one right edge for its header, values, dashes and Total. `UnitValue` puts the unit in a fixed slot after the number (`--unit-slot`, 2rem) and its status dot just left of the number. In a LumenView table the first `UnitValue` drawn in a column marks it (`data-units` on every cell of that column, through the `UnitColumns` context and the cell's `data-col`); theme.css then widens that column's right gutter by the slot and hangs each unit in it. So a plain `—`, `Free` or `$0.21` an app draws in the same column sits on the digits' edge, and the header sits over the digits. Don't hand-place units, dots or padding to line a column up; if a column is off, draw its numbers through `UnitValue`. Outside a LumenView table (a hover card, a tile) `UnitValue` keeps its slot inline.
|
|
78
80
|
- A metered column (use against an allowance) shows one number per cell with `UnitValue` (right-aligned number, fixed muted unit slot, status dot), never `value [pct]`. Offer both readings through the field's `showAs` (Amount / % of allowance, picked per column in Edit view, saved as `shows`); default amounts for sizes (storage) and shares for capped counts. Put the other reading, the allowance and the cost formula in a hover card. Totals use `rollRender`: sum amounts with their unit; a share is the rows' total over their allowance, never a sum of percents.
|
|
79
81
|
- A floating bulk-action bar over a table's selection: Apply clears the selection and the bar goes away; the toast's Undo restores the change, not the selection. Cancel keeps the selection.
|
|
80
82
|
- Bars in a column line up: use `ShareMeter` (fixed track, fixed right-aligned number), never a bar sized by its label.
|
|
81
83
|
- Meter levels are the kit's, not the app's: a share of a cap reads `ok` (grey) under 75%, `watch` (amber) from 75%, `alert` (red) from 90%. `ShareMeter` takes its colour from the share unless you pass `color`; `UnitValue share={0.8}` tints its number and dot the same way; usage progress cards follow the same levels. For any other mark (a dot, a chip, a "nearest cap" cell) call `meterLevel(share)` and colour it with `METER_LEVEL_COLOR` or `meterValueTone(share)`. Override per app with `configureLumen({ meterLevels: { watch: 0.8 } })` (each level can move alone) or per meter with `levels={{ watch: 0.5 }}`. Don't hand-roll `share >= 0.75` checks.
|
|
84
|
+
- Pace (time to a cap at the recent rate) is the kit's: `burnRate(samples, { per, gauge })` over the window's complete samples (hourly, or daily for `7d`), then `timeToCap(rate, used, cap, resetInHours)`. A lone sample above 3× the window's median counts as 3× the median (`spikes` says how many); a run of hot samples counts in full. Show it with `TimeToCap` (`in 2h 05m`, `At cap`, `Resets first`; `short` on a phone), coloured by the meter level of where the meter lands at reset, and pick the window with `PaceWindowPicker` (6h, 12h, 24h, 7d) in the page header; `usePaceWindow()` reads the same choice everywhere, kept in the viewer's browser (`setDefaultPaceWindow` sets the app's default). Don't hand-roll ETAs or a window toggle.
|
|
82
85
|
- Table rows are one line. A cell shows one value (with a small bar or tag at most); anything more (other metrics, the allowance, a breakdown) goes in a hover tooltip, never in stacked text or sub-rows.
|
|
83
86
|
- Chart colours are theme tokens (`var(--chart-1)`, `useChartTokens()`), never hex picked for one theme; `LumenEChart` resolves `var(--…)` in its option so sparklines read in light and dark.
|
|
84
87
|
- "No label" filters come built in: `LumenTokenizedFilter` pins a "No <field>" row on any text field where some rows have no value (an `empty` token); don't add your own toggle for it.
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.j
|
|
|
24
24
|
export { ShareMeter } from './share-meter.js';
|
|
25
25
|
export { METER_LEVEL_COLOR, meterLevel, meterValueTone } from './meter-level.js';
|
|
26
26
|
export { UnitValue } from './unit-value.js';
|
|
27
|
+
export { type BurnRate, burnRate, durationText, PACE_WINDOWS, type Pace, type PaceWindow, paceHours, setDefaultPaceWindow, timeToCap, usePaceWindow, } from './pace.js';
|
|
28
|
+
export { PaceWindowPicker, paceText, TimeToCap } from './time-to-cap.js';
|
|
27
29
|
export { COVERAGE_LEVELS, CoverageCell, type CoverageJobState, type CoverageLevel, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
|
|
28
30
|
export { type CoverageBlock, type CoverageData, type CoverageFact, type CoveragePick, type CoverageRow, type CoverageSummary, LumenCoverageMap, type LumenCoverageMapProps, blockName, blockOf, } from './coverage-map.js';
|
|
29
31
|
export { type CalendarFact, LumenCoverageCalendarMap, type LumenCoverageCalendarMapProps, } from './coverage-calendar-map.js';
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,8 @@ export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.j
|
|
|
31
31
|
export { ShareMeter } from './share-meter.js';
|
|
32
32
|
export { METER_LEVEL_COLOR, meterLevel, meterValueTone } from './meter-level.js';
|
|
33
33
|
export { UnitValue } from './unit-value.js';
|
|
34
|
+
export { burnRate, durationText, PACE_WINDOWS, paceHours, setDefaultPaceWindow, timeToCap, usePaceWindow, } from './pace.js';
|
|
35
|
+
export { PaceWindowPicker, paceText, TimeToCap } from './time-to-cap.js';
|
|
34
36
|
export { COVERAGE_LEVELS, CoverageCell, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
|
|
35
37
|
export { LumenCoverageMap, blockName, blockOf, } from './coverage-map.js';
|
|
36
38
|
export { LumenCoverageCalendarMap, } from './coverage-calendar-map.js';
|
package/dist/lumen-view.js
CHANGED
|
@@ -42,6 +42,7 @@ import { Skeleton } from './ui/skeleton.js';
|
|
|
42
42
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table.js';
|
|
43
43
|
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
|
|
44
44
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
45
|
+
import { UnitColumns } from './unit-value.js';
|
|
45
46
|
import { EditCell, EditInputCell, EditManyCell } from './view-edit-cell.js';
|
|
46
47
|
import { ViewEditDialog } from './view-editor.js';
|
|
47
48
|
import { fieldOptions } from './view-formulas.js';
|
|
@@ -343,6 +344,27 @@ const alignOf = (format) => (format === 'text' ? 'text-left' : 'text-right');
|
|
|
343
344
|
/** An ISO date, alone or with a time after it: `2026-10-14`, `2026-10-14 16:17`. */
|
|
344
345
|
const isDateText = (v) => typeof v === 'string' && v.length <= 25 && /^\d{4}-\d{2}-\d{2}(?:$|[ T])/.test(v);
|
|
345
346
|
const colOf = (f) => cn(alignOf(f?.format ?? 'text'), f?.hideOnPhone && 'max-md:hidden');
|
|
347
|
+
/**
|
|
348
|
+
* A column's cell props: its classes, its key (`data-col`), and `data-units` once a `UnitValue`
|
|
349
|
+
* has drawn in it. Every cell of such a column (header, dash, Total) then right-aligns on the
|
|
350
|
+
* numbers' edge while the units hang in the gutter (theme.css), so one column has one edge.
|
|
351
|
+
*/
|
|
352
|
+
const colPropsOf = (units) => (f, ...more) => ({
|
|
353
|
+
className: cn(colOf(f), ...more),
|
|
354
|
+
'data-col': f?.key,
|
|
355
|
+
'data-units': f && units.has(f.key) ? '' : undefined,
|
|
356
|
+
});
|
|
357
|
+
/**
|
|
358
|
+
* A column's widest cell (`maxWidth`): the first column defaults to `--lead-col-max` (18rem in
|
|
359
|
+
* theme.css), so one long name never pushes the other columns off screen; `false` lifts it.
|
|
360
|
+
*/
|
|
361
|
+
const capOf = (f, i) => f.maxWidth === false ? undefined : (f.maxWidth ?? (i === 0 ? 'var(--lead-col-max)' : undefined));
|
|
362
|
+
/** Holds a cell to its column's cap on one line: text past it ends in an ellipsis. */
|
|
363
|
+
function CappedCell({ max, title, children, }) {
|
|
364
|
+
if (!max)
|
|
365
|
+
return children;
|
|
366
|
+
return (_jsx("div", { className: "flex min-w-0 *:min-w-0 *:max-w-full *:truncate", style: { maxWidth: max }, title: title, children: children }));
|
|
367
|
+
}
|
|
346
368
|
/** The flat or grouped table: sortable headers, a Total row, group rows that collapse. */
|
|
347
369
|
// Sorting, and paging of flat tables (the page is LumenView's, set right of the row count).
|
|
348
370
|
const SORTING = tableFeatures({
|
|
@@ -384,6 +406,10 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
384
406
|
pageSize: paging ? page.size : Math.max(rows.length, 1),
|
|
385
407
|
};
|
|
386
408
|
// Groups whose state differs from where they start: open, or past OPEN_ALL rows only the first.
|
|
409
|
+
// Columns a UnitValue has drawn in (`UnitColumns`): their cells share the numbers' edge.
|
|
410
|
+
const [units, setUnits] = useState(() => new Set());
|
|
411
|
+
const markUnits = useCallback((key) => setUnits((s) => (s.has(key) ? s : new Set(s).add(key))), []);
|
|
412
|
+
const col = colPropsOf(units);
|
|
387
413
|
const [flipped, setFlipped] = useState(new Set());
|
|
388
414
|
const [more, setMore] = useState(new Map());
|
|
389
415
|
// The grouped field is the group row's label, so its column hides.
|
|
@@ -475,12 +501,13 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
475
501
|
const { shown: now, cellOf, inline, chips, computed } = latest.current;
|
|
476
502
|
const field = now[i] ?? f;
|
|
477
503
|
const chip = chips.some((c) => c.key === field.key);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
504
|
+
const v = computed.value(field, row.original);
|
|
505
|
+
return (_jsx(CappedCell, { max: capOf(field, i), title: !field.render && typeof v === 'string' ? v : undefined, children: i === 0 && inline.length ? (_jsx(FirstCell, { value: cellOf(field, row.original, chip), chips: inline.flatMap((x) => {
|
|
506
|
+
const v = computed.value(x, row.original);
|
|
507
|
+
if (!x.edit && (v === null || v === ''))
|
|
508
|
+
return [];
|
|
509
|
+
return [{ key: x.key, node: cellOf(x, row.original, true) }];
|
|
510
|
+
}) })) : (cellOf(field, row.original, chip)) }));
|
|
484
511
|
},
|
|
485
512
|
})), [shownKeys, inlineOn, serverSorts?.join(',')]);
|
|
486
513
|
const table = useTable({
|
|
@@ -560,7 +587,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
560
587
|
// drawn; spanning the hidden ones too pushed the group's totals right of their headers.
|
|
561
588
|
const leadSpan = Math.max(1, lead - (isMobile ? shown.slice(0, lead).filter((f) => f.hideOnPhone).length : 0));
|
|
562
589
|
const rollCell = (f, members) => rollsUp(f) && f.rollRender ? (f.rollRender(members)) : rollsUp(f) ? (_jsx(ViewValueCell, { value: computed.roll(f, members), format: f.aggregate && isCountAgg(f.aggregate) ? 'rounded' : f.format, amount: f.aggregate !== 'count' && f.amount, compact: f.compact })) : null;
|
|
563
|
-
const rollCells = (members) => shown.map((f) => (_jsx(TableCell, {
|
|
590
|
+
const rollCells = (members) => shown.map((f) => (_jsx(TableCell, { ...col(f), children: rollCell(f, members) }, f.key)));
|
|
564
591
|
// A row's chevron opens one level under it, closed by default: its panel, else the view's
|
|
565
592
|
// sub-group ("Each row opens"; None opens nothing).
|
|
566
593
|
const partsOf = (row) => (domain.panel ? [] : (subgroup?.rows?.(row) ?? []));
|
|
@@ -577,9 +604,9 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
577
604
|
return n;
|
|
578
605
|
});
|
|
579
606
|
}, children: _jsx(ChevronRightIcon, { className: cn('transition-transform', openParts.has(k) && 'rotate-90') }) }));
|
|
580
|
-
const partRow = (p, name) => (_jsxs(TableRow, { className: cn('bg-muted/30 text-muted-foreground hover:bg-muted/40', onDetails && 'cursor-pointer'), onClick: onDetails ? () => onDetails(p) : undefined, children: [selection && _jsx(TableCell, {}), shown.map((f, i) => (_jsx(TableCell, {
|
|
581
|
-
|
|
582
|
-
|
|
607
|
+
const partRow = (p, name) => (_jsxs(TableRow, { className: cn('bg-muted/30 text-muted-foreground hover:bg-muted/40', onDetails && 'cursor-pointer'), onClick: onDetails ? () => onDetails(p) : undefined, children: [selection && _jsx(TableCell, {}), shown.map((f, i) => (_jsx(TableCell, { ...col(f, i === 0 && 'pl-12'), children: i === 0 ? (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(CornerDownRightIcon, { className: "size-3.5 shrink-0" }), _jsx(CappedCell, { max: capOf(f, 0), children: name
|
|
608
|
+
? name(p)
|
|
609
|
+
: cellOf(f, p, chips.some((c) => c.key === f.key)) })] })) : (cellOf(f, p, chips.some((c) => c.key === f.key))) }, f.key)))] }, `part:${keyOf(p)}`));
|
|
583
610
|
// The whole row opens the details drawer.
|
|
584
611
|
const dataRow = (r) => (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-row": domain.rowKey(r.original), "data-state": tapped?.(r.original) ? 'selected' : undefined, className: cn('group/row', onDetails && 'cursor-pointer'), tabIndex: onDetails ? 0 : undefined, onClick: onDetails ? () => onDetails(r.original) : undefined, onKeyDown: onDetails
|
|
585
612
|
? (e) => {
|
|
@@ -590,14 +617,14 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
590
617
|
onDetails(r.original);
|
|
591
618
|
}
|
|
592
619
|
}
|
|
593
|
-
: undefined, children: [selectCell(keyOf(r.original), keyOf(r.original)), r.getAllCells().map((c, i) => (_jsx(TableCell, {
|
|
620
|
+
: undefined, children: [selectCell(keyOf(r.original), keyOf(r.original)), r.getAllCells().map((c, i) => (_jsx(TableCell, { ...col(shown[i], groupBy && i === 0 && 'pl-8'), style: { background: shown[i]?.tint?.(r.original) }, children: i === 0 && nests ? (_jsxs("span", { className: "flex items-center gap-1", children: [opens(r.original) ? (chevron(keyOf(r.original), openLabel())) : (_jsx("span", { className: "-ml-1 size-6 shrink-0" })), flexRender(c.column.columnDef.cell, c.getContext())] })) : (flexRender(c.column.columnDef.cell, c.getContext())) }, c.id)))] }, domain.rowKey(r.original)), openParts.has(keyOf(r.original)) &&
|
|
594
621
|
partsOf(r.original).map((p) => partRow(p, subgroup?.name)), openParts.has(keyOf(r.original)) && domain.panel ? (_jsx(TableRow, { "data-slot": "row-panel", className: "bg-muted/20 hover:bg-muted/20", children: _jsx(TableCell, { colSpan: shown.length + (selection ? 1 : 0), className: "p-0 whitespace-normal", children: _jsx("div", { className: "sticky left-0 w-[min(56rem,calc(100vw-2rem))] max-w-full px-4 py-3 md:pl-10", children: panelOf(r.original) }) }) }, `panel:${keyOf(r.original)}`)) : null] }, domain.rowKey(r.original)));
|
|
595
622
|
// Rows sharing the sub-group's `under` key fold into one rolled-up line that opens them; a key
|
|
596
623
|
// with one row keeps that row (NVDA opens Shares, then each option).
|
|
597
624
|
const foldRow = (key, members, scope) => {
|
|
598
625
|
const k = `under:${scope}\u0000${key}`;
|
|
599
626
|
const ms = members.map((m) => m.original);
|
|
600
|
-
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-row": k, children: [selection && _jsx(TableCell, {}), shown.map((f, i) => (_jsx(TableCell, {
|
|
627
|
+
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-row": k, children: [selection && _jsx(TableCell, {}), shown.map((f, i) => (_jsx(TableCell, { ...col(f, groupBy && i === 0 && 'pl-8'), children: i === 0 ? (_jsxs("span", { className: "flex items-center gap-1", children: [chevron(k, `${key} ${openLabel()}`), _jsx("span", { className: "font-medium", children: key }), _jsx("span", { className: "num text-xs text-muted-foreground", children: ms.length })] })) : (rollCell(f, ms)) }, f.key)))] }), openParts.has(k) && ms.map((p) => partRow(p, subgroup?.name))] }, k));
|
|
601
628
|
};
|
|
602
629
|
const rowsOf = (list, scope) => {
|
|
603
630
|
const under = folds ? subgroup?.under : undefined;
|
|
@@ -644,32 +671,32 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
644
671
|
};
|
|
645
672
|
const box = useRef(null);
|
|
646
673
|
useFloatingHead(box, !inView);
|
|
647
|
-
return (_jsx("div", { ref: box, className: cn(inView ? IN_VIEW : 'overflow-x-auto overflow-y-hidden [--pin-top:0px]', fit && FIT), children: _jsxs(Table, { children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => (_jsxs(TableRow, { children: [selection && (_jsx(TableHead, { className: cn('w-8 pr-0', PINNED_HEAD), children: _jsx(Checkbox, { checked: allOn ? true : someOn ? 'indeterminate' : false, onCheckedChange: (on) => toggle(allKeys, on === true), "aria-label": "Select every row" }) })), hg.headers.map((h, i) => (_jsxs(TableHead, {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
674
|
+
return (_jsx("div", { ref: box, className: cn(inView ? IN_VIEW : 'overflow-x-auto overflow-y-hidden [--pin-top:0px]', fit && FIT), children: _jsx(UnitColumns.Provider, { value: markUnits, children: _jsxs(Table, { children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => (_jsxs(TableRow, { children: [selection && (_jsx(TableHead, { className: cn('w-8 pr-0', PINNED_HEAD), children: _jsx(Checkbox, { checked: allOn ? true : someOn ? 'indeterminate' : false, onCheckedChange: (on) => toggle(allKeys, on === true), "aria-label": "Select every row" }) })), hg.headers.map((h, i) => (_jsxs(TableHead, { ...col(shown[i], PINNED_HEAD), style: ruleOf(colorOf(domain, shown[i])), onPointerDown: pressStart, onPointerUp: pressEnd, onPointerLeave: pressEnd, onPointerCancel: pressEnd, onContextMenu: (e) => {
|
|
675
|
+
if (press.current)
|
|
676
|
+
e.preventDefault();
|
|
677
|
+
}, "aria-sort": h.column.getIsSorted() === 'asc'
|
|
678
|
+
? 'ascending'
|
|
679
|
+
: h.column.getIsSorted() === 'desc'
|
|
680
|
+
? 'descending'
|
|
681
|
+
: undefined, children: [i === 0 && groups && (!serverGroups || serverGroups.onToggleAll) && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] })] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && !serverGroups?.list?.length && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center text-muted-foreground", children: "No rows match." }) })), groups && groupBy
|
|
682
|
+
? groups.map(([key, members], gi) => {
|
|
683
|
+
const open = isOpen(key, gi);
|
|
684
|
+
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { className: ROLLUP, "data-group": key, children: [selection && (_jsx(TableCell, { className: cn('w-8 pr-0', PINNED_GROUP), children: _jsx(Checkbox, { checked: members.every((m) => selection.keys.has(keyOf(m.original)))
|
|
685
|
+
? true
|
|
686
|
+
: members.some((m) => selection.keys.has(keyOf(m.original)))
|
|
687
|
+
? 'indeterminate'
|
|
688
|
+
: false, onCheckedChange: (on) => toggle(members.map((m) => keyOf(m.original)), on === true), "aria-label": `Select every row of ${key}` }) })), shown.slice(lead - 1).map((f, i) => i === 0 ? (_jsxs(TableCell, { colSpan: leadSpan, className: PINNED_GROUP, children: [_jsxs(Button, { variant: "ghost", size: "xs", className: "-mx-2 hover:bg-transparent aria-expanded:bg-transparent", "aria-expanded": open, "aria-label": `${open ? 'Collapse' : 'Expand'} ${key}`, onClick: () => serverGroups
|
|
689
|
+
? serverGroups.onToggle(key)
|
|
690
|
+
: setFlipped((c) => {
|
|
691
|
+
const n = new Set(c);
|
|
692
|
+
n.has(key) ? n.delete(key) : n.add(key);
|
|
693
|
+
return n;
|
|
694
|
+
}), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(groupBy.icon, {}), key, _jsx("span", { className: "num font-normal text-muted-foreground", children: serverGroups
|
|
695
|
+
? (served.get(key)?.n?.toLocaleString() ?? '')
|
|
696
|
+
: members.length })] }), groupActions?.(key, members.map((m) => m.original))] }, f.key)) : (_jsx(TableCell, { ...col(f, PINNED_GROUP), children: !serverGroups &&
|
|
697
|
+
rollCell(f, members.map((m) => m.original)) }, f.key)))] }), open && (serverGroups ? servedRows(members, key) : drawn(members, key))] }, key));
|
|
698
|
+
})
|
|
699
|
+
: drawn(sorted, ''), serverGroups?.more && !serverGroups.pages && (_jsx(TableRow, { "data-more": true, children: _jsx(TableCell, { colSpan: span, className: "py-1", children: _jsxs(Button, { variant: "ghost", size: "xs", onClick: serverGroups.more, children: [_jsx(ChevronsDownIcon, {}), "More groups"] }) }) })), hasTotal && rows.length > 0 && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, children: [selection && _jsx(TableCell, {}), rollCells(rows).map((cell, i) => i === 0 && !(shown[0] && rollsUp(shown[0])) ? (_jsx(TableCell, { children: "Total" }, "total")) : (cell))] }))] })] }) }) }));
|
|
673
700
|
}
|
|
674
701
|
/** One value rolled up into a rows × columns grid, with totals where the roll-up adds. */
|
|
675
702
|
function PivotTable({ domain, rows, pivot, }) {
|
package/dist/pace.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The windows a pace is read over. */
|
|
2
|
+
export declare const PACE_WINDOWS: readonly [{
|
|
3
|
+
readonly id: '6h';
|
|
4
|
+
readonly hours: 6;
|
|
5
|
+
}, {
|
|
6
|
+
readonly id: '12h';
|
|
7
|
+
readonly hours: 12;
|
|
8
|
+
}, {
|
|
9
|
+
readonly id: '24h';
|
|
10
|
+
readonly hours: 24;
|
|
11
|
+
}, {
|
|
12
|
+
readonly id: '7d';
|
|
13
|
+
readonly hours: 168;
|
|
14
|
+
}];
|
|
15
|
+
export type PaceWindow = (typeof PACE_WINDOWS)[number]['id'];
|
|
16
|
+
export declare const paceHours: (w: PaceWindow) => 6 | 12 | 24 | 168;
|
|
17
|
+
export interface BurnRate {
|
|
18
|
+
/** Units per hour; a stored amount's growth per hour (can be negative). */
|
|
19
|
+
rate: number;
|
|
20
|
+
/** The plain average, before a lone spike was capped. */
|
|
21
|
+
raw: number;
|
|
22
|
+
/** Lone hot samples that were capped. */
|
|
23
|
+
spikes: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The rate over `samples` (oldest first, complete periods only), each `per` hours long. `gauge`:
|
|
27
|
+
* the samples are sizes, so the rate is their growth from first to last. `damp: false` keeps spikes.
|
|
28
|
+
*/
|
|
29
|
+
export declare function burnRate(samples: readonly number[], { per, gauge, damp }?: {
|
|
30
|
+
per?: number;
|
|
31
|
+
gauge?: boolean;
|
|
32
|
+
damp?: boolean;
|
|
33
|
+
}): BurnRate;
|
|
34
|
+
export interface Pace extends BurnRate {
|
|
35
|
+
/** Hours until the cap at this rate: 0 when already there, Infinity when not growing. */
|
|
36
|
+
eta: number;
|
|
37
|
+
/** Where the meter lands at reset, as a share of the cap (1.2 = 120%). */
|
|
38
|
+
atReset: number;
|
|
39
|
+
/** Reaches the cap before it resets. */
|
|
40
|
+
hits: boolean;
|
|
41
|
+
/** Hours until the cap resets (or the horizon for a stored amount). */
|
|
42
|
+
resetIn: number;
|
|
43
|
+
/** The rate that lands exactly on the cap at reset (0 once there). */
|
|
44
|
+
safe: number;
|
|
45
|
+
}
|
|
46
|
+
/** When `used` reaches `cap` at `rate`, and where it stands when the cap resets in `resetIn` hours. */
|
|
47
|
+
export declare function timeToCap(rate: BurnRate, used: number, cap: number, resetIn: number): Pace;
|
|
48
|
+
/** `40m`, `2h 05m`, `29h`, `3 days`; `short` gives `2h`, `29h`, `3d` (a phone cell). */
|
|
49
|
+
export declare function durationText(hours: number, short?: boolean): string;
|
|
50
|
+
/** The app's window before anyone picks one (`12h` unless set). */
|
|
51
|
+
export declare function setDefaultPaceWindow(w: PaceWindow): void;
|
|
52
|
+
/** The pace window in use and its setter; every `PaceWindowPicker` and reader shares it. */
|
|
53
|
+
export declare function usePaceWindow(): [PaceWindow, (w: PaceWindow) => void];
|
package/dist/pace.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Pace: how fast a meter is being used over a recent window, carried forward to when it reaches its
|
|
2
|
+
// cap. The same for every product: the app passes the window's samples (hourly or daily amounts, or
|
|
3
|
+
// sizes for a stored amount), what is used so far, the cap and the hours until the cap resets.
|
|
4
|
+
// A lone hot sample (above 3× the window's median, with neither neighbour above it) counts as 3×
|
|
5
|
+
// the median, so one burst can't fake a crisis; a run of hot samples is a real change and counts.
|
|
6
|
+
// `TimeToCap` draws the answer; its colour is where the meter lands at reset, on the meter levels.
|
|
7
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
8
|
+
/** The windows a pace is read over. */
|
|
9
|
+
export const PACE_WINDOWS = [
|
|
10
|
+
{ id: '6h', hours: 6 },
|
|
11
|
+
{ id: '12h', hours: 12 },
|
|
12
|
+
{ id: '24h', hours: 24 },
|
|
13
|
+
{ id: '7d', hours: 168 },
|
|
14
|
+
];
|
|
15
|
+
export const paceHours = (w) => PACE_WINDOWS.find((x) => x.id === w)?.hours ?? 12;
|
|
16
|
+
const median = (xs) => {
|
|
17
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
18
|
+
return s[Math.floor(s.length / 2)] ?? 0;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The rate over `samples` (oldest first, complete periods only), each `per` hours long. `gauge`:
|
|
22
|
+
* the samples are sizes, so the rate is their growth from first to last. `damp: false` keeps spikes.
|
|
23
|
+
*/
|
|
24
|
+
export function burnRate(samples, { per = 1, gauge = false, damp = true } = {}) {
|
|
25
|
+
const n = samples.length;
|
|
26
|
+
if (!n)
|
|
27
|
+
return { rate: 0, raw: 0, spikes: 0 };
|
|
28
|
+
if (gauge) {
|
|
29
|
+
const r = n > 1 ? ((samples[n - 1] ?? 0) - (samples[0] ?? 0)) / ((n - 1) * per) : 0;
|
|
30
|
+
return { rate: r, raw: r, spikes: 0 };
|
|
31
|
+
}
|
|
32
|
+
const total = samples.reduce((s, v) => s + v, 0);
|
|
33
|
+
const cap = median(samples) * 3;
|
|
34
|
+
const hot = (i) => cap > 0 && i >= 0 && i < n && (samples[i] ?? 0) > cap;
|
|
35
|
+
let kept = 0;
|
|
36
|
+
let spikes = 0;
|
|
37
|
+
for (let i = 0; i < n; i++) {
|
|
38
|
+
if (damp && hot(i) && !hot(i - 1) && !hot(i + 1)) {
|
|
39
|
+
kept += cap;
|
|
40
|
+
spikes++;
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
kept += samples[i] ?? 0;
|
|
44
|
+
}
|
|
45
|
+
const hours = n * per;
|
|
46
|
+
return { rate: kept / hours, raw: total / hours, spikes };
|
|
47
|
+
}
|
|
48
|
+
/** When `used` reaches `cap` at `rate`, and where it stands when the cap resets in `resetIn` hours. */
|
|
49
|
+
export function timeToCap(rate, used, cap, resetIn) {
|
|
50
|
+
const left = cap - used;
|
|
51
|
+
const eta = left <= 0 ? 0 : rate.rate > 0 ? left / rate.rate : Number.POSITIVE_INFINITY;
|
|
52
|
+
return {
|
|
53
|
+
...rate,
|
|
54
|
+
eta,
|
|
55
|
+
atReset: cap > 0 ? (used + Math.max(rate.rate, 0) * resetIn) / cap : 0,
|
|
56
|
+
hits: eta <= resetIn,
|
|
57
|
+
resetIn,
|
|
58
|
+
safe: left > 0 && resetIn > 0 ? left / resetIn : 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** `40m`, `2h 05m`, `29h`, `3 days`; `short` gives `2h`, `29h`, `3d` (a phone cell). */
|
|
62
|
+
export function durationText(hours, short = false) {
|
|
63
|
+
if (!Number.isFinite(hours))
|
|
64
|
+
return '—';
|
|
65
|
+
if (hours < 1)
|
|
66
|
+
return `${Math.max(1, Math.round(hours * 60))}m`;
|
|
67
|
+
if (short)
|
|
68
|
+
return hours < 48 ? `${Math.round(hours)}h` : `${Math.round(hours / 24)}d`;
|
|
69
|
+
if (hours < 10) {
|
|
70
|
+
const h = Math.floor(hours);
|
|
71
|
+
const m = Math.round((hours - h) * 60);
|
|
72
|
+
return m === 60 ? `${h + 1}h 00m` : `${h}h ${String(m).padStart(2, '0')}m`;
|
|
73
|
+
}
|
|
74
|
+
return hours < 48 ? `${Math.round(hours)}h` : `${Math.round(hours / 24)} days`;
|
|
75
|
+
}
|
|
76
|
+
// The window someone picked, kept in their browser (one choice per app host, every page shares it).
|
|
77
|
+
const KEY = 'lumen:pace-window';
|
|
78
|
+
const listeners = new Set();
|
|
79
|
+
let picked = null;
|
|
80
|
+
let fallback = '12h';
|
|
81
|
+
const isWindow = (v) => PACE_WINDOWS.some((x) => x.id === v);
|
|
82
|
+
function read() {
|
|
83
|
+
if (picked)
|
|
84
|
+
return picked;
|
|
85
|
+
try {
|
|
86
|
+
const v = localStorage.getItem(KEY);
|
|
87
|
+
if (isWindow(v))
|
|
88
|
+
picked = v;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Storage refused: the app's default holds.
|
|
92
|
+
}
|
|
93
|
+
return picked ?? fallback;
|
|
94
|
+
}
|
|
95
|
+
function write(w) {
|
|
96
|
+
picked = w;
|
|
97
|
+
try {
|
|
98
|
+
localStorage.setItem(KEY, w);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Storage refused: the pick holds until reload.
|
|
102
|
+
}
|
|
103
|
+
for (const l of listeners)
|
|
104
|
+
l();
|
|
105
|
+
}
|
|
106
|
+
const subscribe = (l) => {
|
|
107
|
+
listeners.add(l);
|
|
108
|
+
return () => listeners.delete(l);
|
|
109
|
+
};
|
|
110
|
+
/** The app's window before anyone picks one (`12h` unless set). */
|
|
111
|
+
export function setDefaultPaceWindow(w) {
|
|
112
|
+
fallback = w;
|
|
113
|
+
for (const l of listeners)
|
|
114
|
+
l();
|
|
115
|
+
}
|
|
116
|
+
/** The pace window in use and its setter; every `PaceWindowPicker` and reader shares it. */
|
|
117
|
+
export function usePaceWindow() {
|
|
118
|
+
const w = useSyncExternalStore(subscribe, read, () => fallback);
|
|
119
|
+
return [w, useCallback((x) => write(x), [])];
|
|
120
|
+
}
|
package/dist/theme.css
CHANGED
|
@@ -95,6 +95,10 @@
|
|
|
95
95
|
--row-h: 28px;
|
|
96
96
|
--cell-x: 8px;
|
|
97
97
|
--cell-y: 4px;
|
|
98
|
+
/* A UnitValue's unit slot (`GB`, `%`, `/day`), hung in the gutter of a units column. */
|
|
99
|
+
--unit-slot: 2rem;
|
|
100
|
+
/* A table's first (name) column stops growing here; longer names end in an ellipsis. */
|
|
101
|
+
--lead-col-max: 18rem;
|
|
98
102
|
|
|
99
103
|
--background: #f4f6fa;
|
|
100
104
|
--foreground: #121826;
|
|
@@ -388,6 +392,20 @@
|
|
|
388
392
|
}
|
|
389
393
|
}
|
|
390
394
|
|
|
395
|
+
/* A table column a UnitValue drew in (`data-units`, set by LumenView): every cell, header and
|
|
396
|
+
Total included, right-aligns on the numbers' edge; each UnitValue's unit slot hangs in the
|
|
397
|
+
widened gutter (out of flow, so no wrapper an app puts round it can shift it). One column, one edge, whatever an app's render draws beside it. */
|
|
398
|
+
[data-units] {
|
|
399
|
+
padding-right: calc(var(--cell-x) + var(--unit-slot));
|
|
400
|
+
}
|
|
401
|
+
[data-units] [data-slot="unit-value"] {
|
|
402
|
+
position: relative;
|
|
403
|
+
}
|
|
404
|
+
[data-units] [data-slot="unit-value-unit"] {
|
|
405
|
+
position: absolute;
|
|
406
|
+
left: 100%;
|
|
407
|
+
}
|
|
408
|
+
|
|
391
409
|
@layer utilities {
|
|
392
410
|
.bg-gap-hatch {
|
|
393
411
|
background: repeating-linear-gradient(135deg, var(--gap) 0 3px, var(--gap-soft) 3px 6px);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MeterLevels } from '@columenlabs/lumen-ui-types/views';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { meterLevel } from './meter-level.js';
|
|
4
|
+
import { type Pace } from './pace.js';
|
|
5
|
+
/** Picks the pace window (6h, 12h, 24h, 7d) for every page at once; a header action. */
|
|
6
|
+
export declare function PaceWindowPicker({ className }: {
|
|
7
|
+
className?: string;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
/** What a pace reads as: the words and the level they're coloured by. */
|
|
10
|
+
export declare function paceText(p: Pace, short?: boolean): {
|
|
11
|
+
text: string;
|
|
12
|
+
level: ReturnType<typeof meterLevel>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* One meter's time to its cap at the pace window's rate. `label` names the meter when it isn't the
|
|
16
|
+
* one the row already shows; `short` is the phone cell (`2h`, `3d`, `—`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function TimeToCap({ pace, label, short, levels, className, }: {
|
|
19
|
+
pace: Pace | null;
|
|
20
|
+
label?: ReactNode;
|
|
21
|
+
short?: boolean;
|
|
22
|
+
/** This cell's own levels; left out, the app's `meterLevels`. */
|
|
23
|
+
levels?: Partial<MeterLevels>;
|
|
24
|
+
className?: string;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ZapIcon } from 'lucide-react';
|
|
3
|
+
import { useIsMobile } from './hooks/use-mobile.js';
|
|
4
|
+
import { cn } from './lib/utils.js';
|
|
5
|
+
import { LumenSelector } from './lumen-selector.js';
|
|
6
|
+
import { METER_LEVEL_COLOR, meterLevel } from './meter-level.js';
|
|
7
|
+
import { PACE_WINDOWS, durationText, usePaceWindow } from './pace.js';
|
|
8
|
+
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
|
|
9
|
+
/** Picks the pace window (6h, 12h, 24h, 7d) for every page at once; a header action. */
|
|
10
|
+
export function PaceWindowPicker({ className }) {
|
|
11
|
+
const [w, setW] = usePaceWindow();
|
|
12
|
+
const mobile = useIsMobile();
|
|
13
|
+
if (mobile)
|
|
14
|
+
return (_jsx(LumenSelector, { trigger: "chip", icon: ZapIcon, label: "Pace window", size: "sm", align: "end", value: w, onChange: (v) => setW(v), options: PACE_WINDOWS.map((x) => ({ value: x.id, label: x.id, detail: `last ${x.id}` })), className: className }));
|
|
15
|
+
return (_jsxs(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: w, onValueChange: (v) => v && setW(v), "aria-label": "Pace window", title: "Pace: the rate over the last\u2026", className: className, children: [_jsxs("span", { className: "flex h-8 items-center gap-1 rounded-l-md border border-r-0 px-2 text-muted-foreground text-xs", children: [_jsx(ZapIcon, { className: "size-3.5" }), "Pace"] }), PACE_WINDOWS.map((x) => (_jsx(ToggleGroupItem, { value: x.id, className: "num px-2.5 text-xs", children: x.id }, x.id)))] }));
|
|
16
|
+
}
|
|
17
|
+
/** What a pace reads as: the words and the level they're coloured by. */
|
|
18
|
+
export function paceText(p, short = false) {
|
|
19
|
+
if (p.eta === 0)
|
|
20
|
+
return { text: short ? 'cap' : 'At cap', level: 'alert' };
|
|
21
|
+
const level = meterLevel(p.atReset);
|
|
22
|
+
if (p.hits)
|
|
23
|
+
return { text: short ? durationText(p.eta, true) : `in ${durationText(p.eta)}`, level };
|
|
24
|
+
return { text: short ? '—' : 'Resets first', level: short ? 'ok' : level };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One meter's time to its cap at the pace window's rate. `label` names the meter when it isn't the
|
|
28
|
+
* one the row already shows; `short` is the phone cell (`2h`, `3d`, `—`).
|
|
29
|
+
*/
|
|
30
|
+
export function TimeToCap({ pace, label, short, levels, className, }) {
|
|
31
|
+
if (!pace)
|
|
32
|
+
return _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
33
|
+
const t = paceText(pace, short);
|
|
34
|
+
const level = levels && pace.eta > 0 ? meterLevel(pace.atReset, levels) : t.level;
|
|
35
|
+
const c = METER_LEVEL_COLOR[level];
|
|
36
|
+
const quiet = level === 'ok' && !pace.hits;
|
|
37
|
+
return (_jsxs("span", { className: cn('inline-flex h-5 min-w-0 items-center gap-1.5 whitespace-nowrap rounded-sm text-xs', quiet ? 'text-muted-foreground' : 'px-1.5', className), style: quiet
|
|
38
|
+
? undefined
|
|
39
|
+
: level === 'ok'
|
|
40
|
+
? { background: 'var(--muted)' }
|
|
41
|
+
: {
|
|
42
|
+
background: `color-mix(in oklab, ${c} 18%, transparent)`,
|
|
43
|
+
color: `color-mix(in oklab, ${c} 80%, var(--foreground))`,
|
|
44
|
+
}, children: [label && _jsx("span", { className: "truncate max-md:max-w-16", children: label }), _jsx("span", { className: "num", children: t.text })] }));
|
|
45
|
+
}
|
package/dist/unit-value.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MeterLevels } from '@columenlabs/lumen-ui-types/views';
|
|
2
|
-
import type
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
/** Set by a table: marks the column (its cell's `data-col`) a UnitValue draws in. */
|
|
4
|
+
export declare const UnitColumns: import("react").Context<((col: string) => void) | null>;
|
|
3
5
|
export declare function UnitValue({ value, unit, tone: toneProp, share, levels, title, className, }: {
|
|
4
6
|
/** The number, already formatted (`231.4`). */
|
|
5
7
|
value: ReactNode;
|
package/dist/unit-value.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useLayoutEffect, useRef } from 'react';
|
|
2
3
|
import { cn } from './lib/utils.js';
|
|
3
4
|
import { meterValueTone } from './meter-level.js';
|
|
5
|
+
/** Set by a table: marks the column (its cell's `data-col`) a UnitValue draws in. */
|
|
6
|
+
export const UnitColumns = createContext(null);
|
|
4
7
|
export function UnitValue({ value, unit, tone: toneProp, share, levels, title, className, }) {
|
|
5
8
|
const tone = toneProp ?? (share === undefined ? undefined : meterValueTone(share, levels));
|
|
6
|
-
|
|
9
|
+
const mark = useContext(UnitColumns);
|
|
10
|
+
const ref = useRef(null);
|
|
11
|
+
// Before paint, so the column never draws on two edges, even for a frame.
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
const col = ref.current?.closest('[data-col]')?.getAttribute('data-col');
|
|
14
|
+
if (mark && col)
|
|
15
|
+
mark(col);
|
|
16
|
+
}, [mark]);
|
|
17
|
+
return (_jsxs("span", { ref: ref, "data-slot": "unit-value", title: title, className: cn('inline-flex w-full items-center justify-end whitespace-nowrap', className), children: [_jsxs("span", { className: cn('num relative text-right', tone === 'warning' && 'text-warning', tone === 'loss' && 'text-loss'), children: [tone && (_jsx("span", { "aria-hidden": "true", className: cn('-translate-y-1/2 absolute top-1/2 right-full mr-1 size-1.5 rounded-full', tone === 'warning' ? 'bg-warning' : 'bg-loss') })), value] }), _jsx("span", { "data-slot": "unit-value-unit", className: "w-(--unit-slot) shrink-0 pl-1 text-left text-muted-foreground text-xs", children: unit })] }));
|
|
7
18
|
}
|
package/dist/view-model.d.ts
CHANGED
|
@@ -129,6 +129,12 @@ export interface ViewField<T> {
|
|
|
129
129
|
}> | undefined;
|
|
130
130
|
/** The cell's background, a theme colour mix (a P/L heat grid). */
|
|
131
131
|
tint?: (row: T) => string | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* The column's widest cell, a CSS length (`14rem`): past it the cell ends in an ellipsis on one
|
|
134
|
+
* line, the full text in its hover. The first column defaults to `--lead-col-max` (18rem);
|
|
135
|
+
* `false` lets it grow.
|
|
136
|
+
*/
|
|
137
|
+
maxWidth?: string | false;
|
|
132
138
|
/** Left out on phones, where the table has no room for it (a returns table's longer windows). */
|
|
133
139
|
hideOnPhone?: boolean;
|
|
134
140
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
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.56",
|
|
45
45
|
"@dnd-kit/core": "6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "10.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "3.2.2",
|