@columenlabs/lumen-ui 0.1.58 → 0.1.60
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 +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/lumen-view.d.ts +1 -1
- package/dist/lumen-view.js +33 -12
- package/dist/pool-share.d.ts +22 -0
- package/dist/pool-share.js +24 -0
- package/dist/share-meter.d.ts +12 -2
- package/dist/share-meter.js +11 -2
- package/dist/view-model.d.ts +16 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ Every export, its props and an example are on the [docs](https://lumen.columenla
|
|
|
80
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.
|
|
81
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.
|
|
82
82
|
- Bars in a column line up: use `ShareMeter` (fixed track, fixed right-aligned number), never a bar sized by its label.
|
|
83
|
+
- A shared cap split by who used it (an account's allowance by label): `ShareMeter segments={[{ value, color, label }]}` stacks each part's share of the cap in its own colour on the same fixed track; past the cap the track reads as the total and a tick marks the cap. Roll such rows up with `poolShare(rows, { pool, used, cap })` in a field's `rollRender`: a pool's share is its rows' use over the cap (never a sum or max of their own %), and a group across several pools reads the fullest one (`top`).
|
|
83
84
|
- 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
85
|
- 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). Where most hours are empty (median 0) a lone nonzero hour counts as 0, and a window with too little use (`timeToCap(..., { minTotal })`, default 0.1% of the cap) is `thin`: no time to the cap. Don't hand-roll ETAs or a window toggle.
|
|
85
86
|
- A change between periods (this week against last) is `trendChange(now, prev, { minBase })` drawn by `TrendValue`: `▲ 12%` over a big enough earlier period, the plain difference `▲ +3` when it is at or under `minBase`, `▲ >10×` past ten times. A % over a near-empty baseline misleads; never show one.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type CustomViewConfig, LumenTable, type LumenTableProps, LumenView, type LumenViewProps, type ServerGroup, type ServerGroups, type ServerPaging, type ViewCellValue, type ViewChipTone, type ViewDomain, type ViewField, type ViewFieldGroup, type ViewSource, CHIP_TONE, RUN_STATE_TONE, FORMULAS_GROUP, ViewValueCell, rollUp, } from './lumen-view.js';
|
|
1
|
+
export { type CustomViewConfig, LumenTable, type LumenTableProps, LumenView, type LumenViewProps, type ServerGroup, type ServerGroups, type ServerPaging, type ViewCellValue, type ViewChipTone, type ViewDomain, type ViewField, type RollLevel, type ViewFieldGroup, type ViewSource, CHIP_TONE, RUN_STATE_TONE, FORMULAS_GROUP, ViewValueCell, rollUp, } from './lumen-view.js';
|
|
2
2
|
export { LumenLayout, type LumenLayoutCard, type LumenLayoutProps, type LumenLayoutViewProps, domainKeys, followRows, } from './lumen-layout.js';
|
|
3
3
|
export { LumenViewCard, type LumenViewCardProps } from './lumen-view-card.js';
|
|
4
4
|
export { LumenSelector, LumenSelectorList, type LumenSelectorListHandle, type LumenSelectorProps, type LumenOption, type LumenOptionGroup, SelectorRowContent, } from './lumen-selector.js';
|
|
@@ -21,7 +21,8 @@ export { LumenDateInput, type LumenDateInputProps, stepDate, } from './lumen-dat
|
|
|
21
21
|
export { LumenChoice, type LumenChoiceOption } from './lumen-choice.js';
|
|
22
22
|
export { LumenSecret } from './lumen-secret.js';
|
|
23
23
|
export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
|
|
24
|
-
export { ShareMeter } from './share-meter.js';
|
|
24
|
+
export { type ShareSegment, ShareMeter } from './share-meter.js';
|
|
25
|
+
export { type Pool, poolShare } from './pool-share.js';
|
|
25
26
|
export { METER_LEVEL_COLOR, meterLevel, meterValueTone } from './meter-level.js';
|
|
26
27
|
export { UnitValue } from './unit-value.js';
|
|
27
28
|
export { type BurnRate, burnRate, durationText, PACE_WINDOWS, type Pace, type PaceWindow, paceHours, setDefaultPaceWindow, timeToCap, usePaceWindow, } from './pace.js';
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ export { LumenChoice } from './lumen-choice.js';
|
|
|
29
29
|
export { LumenSecret } from './lumen-secret.js';
|
|
30
30
|
export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
|
|
31
31
|
export { ShareMeter } from './share-meter.js';
|
|
32
|
+
export { poolShare } from './pool-share.js';
|
|
32
33
|
export { METER_LEVEL_COLOR, meterLevel, meterValueTone } from './meter-level.js';
|
|
33
34
|
export { UnitValue } from './unit-value.js';
|
|
34
35
|
export { burnRate, durationText, PACE_WINDOWS, paceHours, setDefaultPaceWindow, timeToCap, usePaceWindow, } from './pace.js';
|
package/dist/lumen-view.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type ReactNode } from 'react';
|
|
|
4
4
|
import { type LumenLoad } from './lumen-source.js';
|
|
5
5
|
import { type CardControls, type CardLink } from './lumen-widget.js';
|
|
6
6
|
import { type ViewCellValue, type ViewChipTone, type ViewDomain, type ViewField, type ViewSubgroup } from './view-model.js';
|
|
7
|
-
export type { ViewCellValue, ViewChipTone, ViewDomain, ViewField, ViewFieldGroup, ViewSource, } from './view-model.js';
|
|
7
|
+
export type { ViewCellValue, ViewChipTone, ViewDomain, ViewField, RollLevel, ViewFieldGroup, ViewSource, } from './view-model.js';
|
|
8
8
|
export { CHIP_TONE, FORMULAS_GROUP, RUN_STATE_TONE } from './view-model.js';
|
|
9
9
|
export { rollUp } from './view-model.js';
|
|
10
10
|
export type { ViewEditorSection } from './view-editor.js';
|
package/dist/lumen-view.js
CHANGED
|
@@ -422,10 +422,14 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
422
422
|
const col = colPropsOf(units);
|
|
423
423
|
const [flipped, setFlipped] = useState(new Set());
|
|
424
424
|
const [more, setMore] = useState(new Map());
|
|
425
|
-
//
|
|
425
|
+
// Under a sub-group's `flat` grouping each row names itself by its sub-group key instead of
|
|
426
|
+
// folding; the first column keeps its place to show that name.
|
|
427
|
+
const flat = !!groupBy && !!subgroup?.flat?.includes(groupBy.key);
|
|
428
|
+
// The grouped field is the group row's label, so its column hides (a flat lead stays).
|
|
426
429
|
// Each key once: two columns under one React key can lose a cell on a re-render, and every
|
|
427
430
|
// row after it slides left by one.
|
|
428
|
-
const visible = columns.filter((f, i) => f.key !== groupBy?.key
|
|
431
|
+
const visible = columns.filter((f, i) => (f.key !== groupBy?.key || (flat && i === 0)) &&
|
|
432
|
+
columns.findIndex((x) => x.key === f.key) === i);
|
|
429
433
|
// Inline chips sit on the first column, next to the row's name, not in their own.
|
|
430
434
|
const inline = visible.filter((f, i) => i > 0 && chips.some((c) => c.key === f.key && c.placement === 'inline'));
|
|
431
435
|
const shown = visible.filter((f) => !inline.includes(f));
|
|
@@ -506,7 +510,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
506
510
|
},
|
|
507
511
|
sortUndefined: 'last',
|
|
508
512
|
enableSorting: !serverSorts || serverSorts.includes(f.key),
|
|
509
|
-
header: f.label,
|
|
513
|
+
header: i === 0 && flat && subgroup ? subgroup.label : f.label,
|
|
510
514
|
cell: ({ row }) => {
|
|
511
515
|
const { shown: now, cellOf, inline, chips, computed } = latest.current;
|
|
512
516
|
const field = now[i] ?? f;
|
|
@@ -519,7 +523,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
519
523
|
return [{ key: x.key, node: cellOf(x, row.original, true) }];
|
|
520
524
|
}) })) : (cellOf(field, row.original, chip)) }));
|
|
521
525
|
},
|
|
522
|
-
})), [shownKeys, inlineOn, serverSorts?.join(',')]);
|
|
526
|
+
})), [shownKeys, inlineOn, serverSorts?.join(','), flat]);
|
|
523
527
|
const table = useTable({
|
|
524
528
|
features: SORTING,
|
|
525
529
|
data: rows,
|
|
@@ -585,7 +589,12 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
585
589
|
const allOn = allKeys.length > 0 && allKeys.every((k) => selection?.keys.has(k));
|
|
586
590
|
const someOn = allKeys.some((k) => selection?.keys.has(k));
|
|
587
591
|
const selectCell = (key, name) => selection && (_jsx(TableCell, { className: "w-8 pr-0", onClick: (e) => e.stopPropagation(), children: _jsx(Checkbox, { checked: selection.keys.has(key), onCheckedChange: (on) => toggle([key], on === true), "aria-label": `Select ${name}` }) }));
|
|
588
|
-
|
|
592
|
+
// The rows whose open state the person flipped: a sub-group that starts open (`open`) lists
|
|
593
|
+
// the closed ones, any other the open ones.
|
|
594
|
+
const [partFlips, setOpenParts] = useState(new Set());
|
|
595
|
+
const openParts = {
|
|
596
|
+
has: (k) => (subgroup?.open && !domain.panel ? !partFlips.has(k) : partFlips.has(k)),
|
|
597
|
+
};
|
|
589
598
|
// A server group's roll-ups would add up its loaded rows only, so they and the Total stay off.
|
|
590
599
|
const hasTotal = total && !serverGroups && shown.some(rollsUp);
|
|
591
600
|
// A group row's label spans the leading columns that roll nothing up.
|
|
@@ -596,15 +605,16 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
596
605
|
// On a phone the hideOnPhone columns are gone, so the label spans only the leading ones still
|
|
597
606
|
// drawn; spanning the hidden ones too pushed the group's totals right of their headers.
|
|
598
607
|
const leadSpan = Math.max(1, lead - (isMobile ? shown.slice(0, lead).filter((f) => f.hideOnPhone).length : 0));
|
|
599
|
-
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;
|
|
600
|
-
const rollCells = (members) => shown.map((f) => (_jsx(TableCell, { ...col(f), children: rollCell(f, members) }, f.key)));
|
|
608
|
+
const rollCell = (f, members, level) => rollsUp(f) && f.rollRender ? (f.rollRender(members, level)) : 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;
|
|
609
|
+
const rollCells = (members) => shown.map((f) => (_jsx(TableCell, { ...col(f), children: rollCell(f, members, 'total') }, f.key)));
|
|
601
610
|
// A row's chevron opens one level under it, closed by default: its panel, else the view's
|
|
602
611
|
// sub-group ("Each row opens"; None opens nothing).
|
|
603
612
|
const partsOf = (row) => (domain.panel ? [] : (subgroup?.rows?.(row) ?? []));
|
|
604
613
|
const panelOf = (row) => domain.panel?.render(row) ?? null;
|
|
605
614
|
const opens = (row) => (domain.panel ? panelOf(row) !== null : partsOf(row).length > 1);
|
|
606
615
|
const openLabel = () => domain.panel?.label ?? subgroup?.label.toLowerCase() ?? 'parts';
|
|
607
|
-
const folds = !domain.panel && !!subgroup?.under;
|
|
616
|
+
const folds = !domain.panel && !!subgroup?.under && !flat;
|
|
617
|
+
const flatName = (row) => flat && subgroup?.under ? (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [subgroup.mark?.(subgroup.under(row), [row]), _jsx("span", { className: "truncate", children: subgroup.under(row) })] })) : null;
|
|
608
618
|
const nests = folds || sorted.some((r) => opens(r.original));
|
|
609
619
|
const chevron = (k, name) => (_jsx(Button, { variant: "ghost", size: "icon-xs", className: "-ml-1", "aria-expanded": openParts.has(k), "aria-label": `${openParts.has(k) ? 'Hide' : 'Show'} ${name}`, onClick: (e) => {
|
|
610
620
|
e.stopPropagation();
|
|
@@ -627,14 +637,14 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
627
637
|
onDetails(r.original);
|
|
628
638
|
}
|
|
629
639
|
}
|
|
630
|
-
: 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)) &&
|
|
640
|
+
: 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())] })) : i === 0 && flat ? (flatName(r.original)) : (flexRender(c.column.columnDef.cell, c.getContext())) }, c.id)))] }, domain.rowKey(r.original)), openParts.has(keyOf(r.original)) &&
|
|
631
641
|
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)));
|
|
632
642
|
// Rows sharing the sub-group's `under` key fold into one rolled-up line that opens them; a key
|
|
633
643
|
// with one row keeps that row (NVDA opens Shares, then each option).
|
|
634
644
|
const foldRow = (key, members, scope) => {
|
|
635
645
|
const k = `under:${scope}\u0000${key}`;
|
|
636
646
|
const ms = members.map((m) => m.original);
|
|
637
|
-
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));
|
|
647
|
+
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()}`), subgroup?.mark?.(key, ms), _jsx("span", { className: "font-medium", children: key }), _jsx("span", { className: "num text-xs text-muted-foreground", children: ms.length })] })) : (rollCell(f, ms, 'fold')) }, f.key)))] }), openParts.has(k) && ms.map((p) => partRow(p, subgroup?.name))] }, k));
|
|
638
648
|
};
|
|
639
649
|
const rowsOf = (list, scope) => {
|
|
640
650
|
const under = folds ? subgroup?.under : undefined;
|
|
@@ -701,10 +711,10 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
701
711
|
const n = new Set(c);
|
|
702
712
|
n.has(key) ? n.delete(key) : n.add(key);
|
|
703
713
|
return n;
|
|
704
|
-
}), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(groupBy
|
|
714
|
+
}), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(GroupMark, { field: groupBy, value: key }), key, _jsx("span", { className: "num font-normal text-muted-foreground", children: serverGroups
|
|
705
715
|
? (served.get(key)?.n?.toLocaleString() ?? '')
|
|
706
716
|
: members.length })] }), groupActions?.(key, members.map((m) => m.original))] }, f.key)) : (_jsx(TableCell, { ...col(f, PINNED_GROUP), children: !serverGroups &&
|
|
707
|
-
rollCell(f, members.map((m) => m.original)) }, f.key)))] }), open && (serverGroups ? servedRows(members, key) : drawn(members, key))] }, key));
|
|
717
|
+
rollCell(f, members.map((m) => m.original), 'group') }, f.key)))] }), open && (serverGroups ? servedRows(members, key) : drawn(members, key))] }, key));
|
|
708
718
|
})
|
|
709
719
|
: 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))] }))] })] }) }) }));
|
|
710
720
|
}
|
|
@@ -733,3 +743,14 @@ function PivotTable({ domain, rows, pivot, }) {
|
|
|
733
743
|
const amount = pivot.agg !== 'count' && vf.amount;
|
|
734
744
|
return (_jsx("div", { className: IN_VIEW, "data-testid": "view-pivot", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: PINNED_HEAD, style: ruleOf(colorOf(domain, rf)), children: _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(rf.icon, { className: "size-3.5" }), _jsx("span", { style: { color: colorOf(domain, rf) ?? undefined }, children: rf.label }), " \\", ' ', _jsx("span", { style: { color: colorOf(domain, cf) ?? undefined }, children: cf.label }), _jsxs("span", { className: "font-normal text-muted-foreground", children: ["(", _jsx("span", { style: { color: colorOf(domain, vf) ?? undefined }, children: vf.label }), ",", ' ', pivot.agg, ")"] })] }) }), grid.colKeys.map((c) => (_jsx(TableHead, { className: cn('text-right', PINNED_HEAD), style: ruleOf(colorOf(domain, cf)), children: c }, c))), totals && _jsx(TableHead, { className: cn('text-right', PINNED_HEAD), children: "Total" })] }) }), _jsxs(TableBody, { children: [grid.rowKeys.map((r) => (_jsxs(TableRow, { "data-row": r, children: [_jsx(TableCell, { children: r }), grid.colKeys.map((c) => (_jsx(TableCell, { className: "text-right", children: _jsx(ViewValueCell, { value: grid.cell(r, c), format: fmt, amount: amount }) }, c))), totals && (_jsx(TableCell, { className: cn('text-right', ROLLUP), children: _jsx(ViewValueCell, { value: grid.cell(r, null), format: fmt, amount: amount }) }))] }, r))), totals && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, children: [_jsx(TableCell, { children: "Total" }), grid.colKeys.map((c) => (_jsx(TableCell, { className: "text-right", children: _jsx(ViewValueCell, { value: grid.cell(null, c), format: fmt, amount: amount }) }, c))), _jsx(TableCell, { className: "text-right", children: _jsx(ViewValueCell, { value: grid.cell(null, null), format: fmt, amount: amount }) })] }))] })] }) }));
|
|
735
745
|
}
|
|
746
|
+
/** A group heading's mark: its value's icon in its colour, else its colour's dot, else the
|
|
747
|
+
* field's icon. */
|
|
748
|
+
function GroupMark({ field, value }) {
|
|
749
|
+
const color = field.color?.(value);
|
|
750
|
+
const Icon = field.iconOf?.(value);
|
|
751
|
+
if (Icon)
|
|
752
|
+
return (_jsx("span", { className: "inline-flex", style: color ? { color } : undefined, children: _jsx(Icon, { className: "size-3.5" }) }));
|
|
753
|
+
if (color)
|
|
754
|
+
return _jsx(ColorDot, { color: color });
|
|
755
|
+
return _jsx(field.icon, {});
|
|
756
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Pool<T> {
|
|
2
|
+
key: string;
|
|
3
|
+
used: number;
|
|
4
|
+
cap: number;
|
|
5
|
+
/** `used / cap`; 0 when the pool has no cap (pay as you go). */
|
|
6
|
+
share: number;
|
|
7
|
+
rows: T[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The pools `rows` fall in and the fullest one's share. `cap` is read from each pool's first row
|
|
11
|
+
* (every row of a pool shares it); a cap of 0 or less has no share. Use it in a field's
|
|
12
|
+
* `rollRender`, so a group or Total row reads the share again from its rows.
|
|
13
|
+
*/
|
|
14
|
+
export declare function poolShare<T>(rows: T[], by: {
|
|
15
|
+
pool: (row: T) => string;
|
|
16
|
+
used: (row: T) => number;
|
|
17
|
+
cap: (row: T) => number;
|
|
18
|
+
}): {
|
|
19
|
+
share: number;
|
|
20
|
+
top: Pool<T> | null;
|
|
21
|
+
pools: Pool<T>[];
|
|
22
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Rows that each use part of one shared cap (an account's allowance, split by label or resource)
|
|
2
|
+
// roll up as the pool's use over that cap: never a sum or a max of the rows' own shares. A group
|
|
3
|
+
// that spans several pools reads its fullest one, the cap it will hit first.
|
|
4
|
+
/**
|
|
5
|
+
* The pools `rows` fall in and the fullest one's share. `cap` is read from each pool's first row
|
|
6
|
+
* (every row of a pool shares it); a cap of 0 or less has no share. Use it in a field's
|
|
7
|
+
* `rollRender`, so a group or Total row reads the share again from its rows.
|
|
8
|
+
*/
|
|
9
|
+
export function poolShare(rows, by) {
|
|
10
|
+
const m = new Map();
|
|
11
|
+
for (const r of rows) {
|
|
12
|
+
const key = by.pool(r);
|
|
13
|
+
const p = m.get(key) ?? { key, used: 0, cap: by.cap(r), share: 0, rows: [] };
|
|
14
|
+
p.used += by.used(r);
|
|
15
|
+
p.rows.push(r);
|
|
16
|
+
m.set(key, p);
|
|
17
|
+
}
|
|
18
|
+
const pools = [...m.values()];
|
|
19
|
+
for (const p of pools)
|
|
20
|
+
p.share = p.cap > 0 ? p.used / p.cap : 0;
|
|
21
|
+
pools.sort((a, b) => b.share - a.share);
|
|
22
|
+
const top = pools[0] ?? null;
|
|
23
|
+
return { share: top?.share ?? 0, top, pools };
|
|
24
|
+
}
|
package/dist/share-meter.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { MeterLevels } from '@columenlabs/lumen-ui-types/views';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/** One part of a split meter: its own share of the cap (0 to 1 of the whole cap), in its colour. */
|
|
4
|
+
export interface ShareSegment {
|
|
5
|
+
value: number;
|
|
6
|
+
/** A theme token or CSS colour (a label's colour). */
|
|
7
|
+
color: string;
|
|
8
|
+
/** Who it is, read out by screen readers and in the bar's hover (`web 122%`). */
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function ShareMeter({ value, label, color, levels, segments, wide, className, }: {
|
|
12
|
+
/** 0 to 1 fills the bar; past 1 it stays full (with `segments`, the track stretches instead). */
|
|
5
13
|
value: number;
|
|
6
14
|
/** The number beside the bar, e.g. `113%`; left out, the rounded percent. */
|
|
7
15
|
label?: ReactNode;
|
|
@@ -9,6 +17,8 @@ export declare function ShareMeter({ value, label, color, levels, wide, classNam
|
|
|
9
17
|
color?: string;
|
|
10
18
|
/** This meter's own levels (e.g. `{ watch: 0.5 }`); left out, the app's `meterLevels`. */
|
|
11
19
|
levels?: Partial<MeterLevels>;
|
|
20
|
+
/** Splits the bar by who used the cap, largest first; their values add up to `value`. */
|
|
21
|
+
segments?: ShareSegment[];
|
|
12
22
|
/** Fills its cell (a list row) instead of the fixed table width. */
|
|
13
23
|
wide?: boolean;
|
|
14
24
|
className?: string;
|
package/dist/share-meter.js
CHANGED
|
@@ -2,8 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cn } from './lib/utils.js';
|
|
3
3
|
import { METER_LEVEL_COLOR, meterLevel } from './meter-level.js';
|
|
4
4
|
import { Progress } from './ui/progress.js';
|
|
5
|
-
export function ShareMeter({ value, label, color, levels, wide, className, }) {
|
|
5
|
+
export function ShareMeter({ value, label, color, levels, segments, wide, className, }) {
|
|
6
|
+
const number = (_jsx("span", { className: "num w-11 shrink-0 text-right text-xs", children: label ?? `${Math.round(value * 100)}%` }));
|
|
7
|
+
if (segments?.length) {
|
|
8
|
+
// Past the cap the bar keeps its width: the track reads as the total and the tick as the cap.
|
|
9
|
+
const scale = Math.max(1, value);
|
|
10
|
+
const said = segments
|
|
11
|
+
.filter((s) => s.value > 0)
|
|
12
|
+
.map((s) => `${s.label ?? ''} ${Math.round(s.value * 100)}%`.trim());
|
|
13
|
+
return (_jsxs("span", { className: cn('inline-flex items-center gap-2', wide && 'w-full', className), children: [_jsxs("span", { role: "img", "aria-label": said.join(', '), title: said.join(' · '), "data-slot": "share-segments", className: cn('relative flex h-1.5 shrink-0 overflow-hidden rounded-full bg-muted', wide ? 'w-full shrink' : 'w-14'), children: [segments.map((s, i) => s.value > 0 ? (_jsx("span", { className: "h-full shrink-0 border-background not-first:border-l", style: { width: `${(s.value / scale) * 100}%`, background: s.color } }, i)) : null), value > 1 && (_jsx("span", { "aria-hidden": true, className: "absolute inset-y-0 w-0.5 -translate-x-1/2 bg-foreground/70", style: { left: `${(1 / scale) * 100}%` } }))] }), number] }));
|
|
14
|
+
}
|
|
6
15
|
return (_jsxs("span", { className: cn('inline-flex items-center gap-2', wide && 'w-full', className), children: [_jsx(Progress, { value: Math.max(0, Math.min(value, 1)) * 100, className: cn('h-1.5 shrink-0 [&>[data-slot=progress-indicator]]:bg-(--meter-color)', wide ? 'w-full shrink' : 'w-14'), style: {
|
|
7
16
|
'--meter-color': color ?? METER_LEVEL_COLOR[meterLevel(value, levels)],
|
|
8
|
-
} }),
|
|
17
|
+
} }), number] }));
|
|
9
18
|
}
|
package/dist/view-model.d.ts
CHANGED
|
@@ -98,9 +98,10 @@ export interface ViewField<T> {
|
|
|
98
98
|
/**
|
|
99
99
|
* Draws a group or Total row's cell from its rows instead of the rolled-up number: an amount with
|
|
100
100
|
* its unit, or a share worked out again as the rows' total over their allowance (never a sum of
|
|
101
|
-
* percents). Only where the field rolls up (`aggregate`).
|
|
101
|
+
* percents). Only where the field rolls up (`aggregate`). `level` says which line it draws: a
|
|
102
|
+
* group heading, a sub-group's folded line or the Total (a split bar on a fold only).
|
|
102
103
|
*/
|
|
103
|
-
rollRender?: (rows: T[]) => ReactNode;
|
|
104
|
+
rollRender?: (rows: T[], level: RollLevel) => ReactNode;
|
|
104
105
|
/**
|
|
105
106
|
* Ways the column can read, picked per column in Edit view ("Show as"), e.g. an amount or its % of
|
|
106
107
|
* an allowance. The first is the field as defined; each other one's `field` replaces those parts
|
|
@@ -123,7 +124,8 @@ export interface ViewField<T> {
|
|
|
123
124
|
* cell, its chip and the edit picker. A CSS colour; a theme token reads in light and dark.
|
|
124
125
|
*/
|
|
125
126
|
color?: (value: string) => string | undefined;
|
|
126
|
-
/** A text value's icon (a label's), drawn in its `color` on its chip
|
|
127
|
+
/** A text value's icon (a label's, a product's), drawn in its `color` on its chip, in the edit
|
|
128
|
+
* picker and on its group's heading. */
|
|
127
129
|
iconOf?: (value: string) => ComponentType<{
|
|
128
130
|
className?: string;
|
|
129
131
|
}> | undefined;
|
|
@@ -300,6 +302,8 @@ export interface ViewDomain<T> {
|
|
|
300
302
|
export declare const summaryCash: (card: {
|
|
301
303
|
cash?: boolean;
|
|
302
304
|
}, narrowed: boolean) => boolean;
|
|
305
|
+
/** The line a `rollRender` draws: a group heading, a sub-group's folded line, the Total. */
|
|
306
|
+
export type RollLevel = 'group' | 'fold' | 'total';
|
|
303
307
|
/** One choice of a domain's "Each row opens". */
|
|
304
308
|
export interface ViewSubgroup<T> {
|
|
305
309
|
key: string;
|
|
@@ -310,6 +314,15 @@ export interface ViewSubgroup<T> {
|
|
|
310
314
|
name?: (row: T) => string;
|
|
311
315
|
/** `under` folds a key even when one row has it (a field's second level of grouping). */
|
|
312
316
|
always?: boolean;
|
|
317
|
+
/** Rows start open, their parts showing (a meter's labels); a click still closes one. */
|
|
318
|
+
open?: boolean;
|
|
319
|
+
/** Drawn before a folded line's key (a meter's product icon, in its colour). */
|
|
320
|
+
mark?: (key: string, rows: T[]) => ReactNode;
|
|
321
|
+
/**
|
|
322
|
+
* Group fields under which rows do not fold: each row is one line named by its `under` key (and
|
|
323
|
+
* `mark`) instead of its first column (grouped by label, a label's meters).
|
|
324
|
+
*/
|
|
325
|
+
flat?: string[];
|
|
313
326
|
}
|
|
314
327
|
/**
|
|
315
328
|
* What a row can open: the domain's own sub-groups, else every groupable field as a second level
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.60",
|
|
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.60",
|
|
45
45
|
"@dnd-kit/core": "6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "10.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "3.2.2",
|