@columenlabs/lumen-ui 0.1.58 → 0.1.59
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 +2 -1
- package/dist/index.js +1 -0
- package/dist/lumen-view.js +6 -1
- 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 +2 -0
- 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
|
@@ -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.js
CHANGED
|
@@ -585,7 +585,12 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
585
585
|
const allOn = allKeys.length > 0 && allKeys.every((k) => selection?.keys.has(k));
|
|
586
586
|
const someOn = allKeys.some((k) => selection?.keys.has(k));
|
|
587
587
|
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
|
-
|
|
588
|
+
// The rows whose open state the person flipped: a sub-group that starts open (`open`) lists
|
|
589
|
+
// the closed ones, any other the open ones.
|
|
590
|
+
const [partFlips, setOpenParts] = useState(new Set());
|
|
591
|
+
const openParts = {
|
|
592
|
+
has: (k) => (subgroup?.open && !domain.panel ? !partFlips.has(k) : partFlips.has(k)),
|
|
593
|
+
};
|
|
589
594
|
// A server group's roll-ups would add up its loaded rows only, so they and the Total stay off.
|
|
590
595
|
const hasTotal = total && !serverGroups && shown.some(rollsUp);
|
|
591
596
|
// A group row's label spans the leading columns that roll nothing up.
|
|
@@ -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
|
@@ -310,6 +310,8 @@ export interface ViewSubgroup<T> {
|
|
|
310
310
|
name?: (row: T) => string;
|
|
311
311
|
/** `under` folds a key even when one row has it (a field's second level of grouping). */
|
|
312
312
|
always?: boolean;
|
|
313
|
+
/** Rows start open, their parts showing (a meter's labels); a click still closes one. */
|
|
314
|
+
open?: boolean;
|
|
313
315
|
}
|
|
314
316
|
/**
|
|
315
317
|
* 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.59",
|
|
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.59",
|
|
45
45
|
"@dnd-kit/core": "6.3.1",
|
|
46
46
|
"@dnd-kit/sortable": "10.0.0",
|
|
47
47
|
"@dnd-kit/utilities": "3.2.2",
|