@columenlabs/lumen-ui 0.1.164 → 0.1.166
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 +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/kit-config.d.ts +6 -3
- package/dist/kit-config.js +5 -2
- package/dist/market/market-widget.js +38 -5
- package/dist/meter-level.d.ts +11 -6
- package/dist/meter-level.js +28 -10
- package/dist/share-meter.d.ts +12 -10
- package/dist/share-meter.js +14 -13
- package/dist/summary-cards.js +18 -9
- package/dist/theme.css +18 -1
- package/dist/tile.js +1 -1
- package/dist/time-to-cap.js +4 -4
- package/dist/unit-value.d.ts +3 -3
- package/dist/unit-value.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -83,8 +83,8 @@ Every export, its props and an example are on the [docs](https://lumen.columenla
|
|
|
83
83
|
- 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.
|
|
84
84
|
- 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.
|
|
85
85
|
- Bars in a column line up: use `ShareMeter` (fixed track, fixed right-aligned number), never a bar sized by its label.
|
|
86
|
-
- A shared cap split by who used it (an account's allowance by label): `ShareMeter segments={[{ value,
|
|
87
|
-
- Meter levels are the kit's, not the app's
|
|
86
|
+
- A shared cap split by who used it (an account's allowance by label): `ShareMeter segments={[{ value, label }]}` names each part's share in the bar's hover; the bar itself keeps one colour, its level's, so colour always says how full it is. 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`).
|
|
87
|
+
- Meter levels are the kit's, not the app's. A usage share reads `ok` (green) under 75%, `watch` (amber) from 75%, `alert` (orange) from 90%, `over` (red) from 100%; past 100% the `ShareMeter` stays full and a hatched stub spills past a tick at the cap, up to +50%. A health share (`kind="health"`: uptime, success rate) reads green at 99% and over, amber from 95%, red under 95%. `ShareMeter` takes its colour from the share unless you pass `color`; `UnitValue share={0.8}` tints its number and dot from amber up (green numbers stay plain); progress cards follow the same levels. For any other mark (a dot, a chip, a "nearest cap" cell) call `meterLevel(share, levels, kind)` and colour it with `METER_LEVEL_COLOR` or `meterValueTone(share)`. Override per app with `configureLumen({ meterLevels: { watch: 0.8 }, healthLevels: { watch: 0.995 } })` (each level can move alone) or per meter with `levels`. Don't hand-roll `share >= 0.75` checks.
|
|
88
88
|
- 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.
|
|
89
89
|
- 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.
|
|
90
90
|
- 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.
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export { LumenSecret } from './lumen-secret.js';
|
|
|
27
27
|
export { RankedBars, StackedBars, StackedColumns, TimeLines } from './lumen-widget-charts.js';
|
|
28
28
|
export { type ShareSegment, ShareMeter } from './share-meter.js';
|
|
29
29
|
export { type Pool, poolShare } from './pool-share.js';
|
|
30
|
-
export { METER_LEVEL_COLOR, meterLevel, meterValueTone } from './meter-level.js';
|
|
30
|
+
export { METER_LEVEL_COLOR, METER_LEVEL_TEXT, meterLevel, meterValueTone } from './meter-level.js';
|
|
31
31
|
export { contrast, readableInk } from './ink.js';
|
|
32
32
|
export { UnitValue } from './unit-value.js';
|
|
33
33
|
export { type BurnRate, burnRate, durationText, PACE_WINDOWS, type Pace, type PaceWindow, paceHours, setDefaultPaceWindow, timeToCap, usePaceWindow, } from './pace.js';
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export { LumenSecret } from './lumen-secret.js';
|
|
|
34
34
|
export { RankedBars, StackedBars, StackedColumns, TimeLines } from './lumen-widget-charts.js';
|
|
35
35
|
export { ShareMeter } from './share-meter.js';
|
|
36
36
|
export { poolShare } from './pool-share.js';
|
|
37
|
-
export { METER_LEVEL_COLOR, meterLevel, meterValueTone } from './meter-level.js';
|
|
37
|
+
export { METER_LEVEL_COLOR, METER_LEVEL_TEXT, meterLevel, meterValueTone } from './meter-level.js';
|
|
38
38
|
export { contrast, readableInk } from './ink.js';
|
|
39
39
|
export { UnitValue } from './unit-value.js';
|
|
40
40
|
export { burnRate, durationText, PACE_WINDOWS, paceHours, setDefaultPaceWindow, timeToCap, usePaceWindow, } from './pace.js';
|
package/dist/kit-config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MeterLevels } from '@columenlabs/lumen-ui-types/views';
|
|
1
|
+
import { type HealthLevels, type MeterLevels } from '@columenlabs/lumen-ui-types/views';
|
|
2
2
|
import type { Prefs } from './prefs.js';
|
|
3
3
|
export interface KitConfig {
|
|
4
4
|
/** The cookie that keeps each person's display preferences. */
|
|
@@ -9,13 +9,16 @@ export interface KitConfig {
|
|
|
9
9
|
marketOff: string;
|
|
10
10
|
/** The app's own defaults for preferences a person has not set (theme, time zone, ...). */
|
|
11
11
|
defaultPrefs?: Partial<Prefs>;
|
|
12
|
-
/** Where every meter turns amber (`watch`) and red (`
|
|
12
|
+
/** Where every usage meter turns amber (`watch`), orange (`alert`) and red (`over`), as shares of a cap (0 to 1). */
|
|
13
13
|
meterLevels: MeterLevels;
|
|
14
|
+
/** Where every health meter drops below green (`watch`) and below amber (`alert`), 0 to 1. */
|
|
15
|
+
healthLevels: HealthLevels;
|
|
14
16
|
}
|
|
15
17
|
export declare const KIT: KitConfig;
|
|
16
18
|
/** Runs `fn` after each `configureLumen` (preferences read again from the app's cookie). */
|
|
17
19
|
export declare function onConfigure(fn: () => void): void;
|
|
18
20
|
/** Sets what `c` names; the rest keep the kit's defaults (one meter level can move alone). */
|
|
19
|
-
export declare function configureLumen(c: Partial<Omit<KitConfig, 'meterLevels'>> & {
|
|
21
|
+
export declare function configureLumen(c: Partial<Omit<KitConfig, 'meterLevels' | 'healthLevels'>> & {
|
|
20
22
|
meterLevels?: Partial<MeterLevels>;
|
|
23
|
+
healthLevels?: Partial<HealthLevels>;
|
|
21
24
|
}): void;
|
package/dist/kit-config.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// What an app tells the kit about itself (the kit is product-neutral): where
|
|
2
2
|
// people's display preferences live and the line a market widget shows where the app has no feed.
|
|
3
3
|
// Set once at start with `configureLumen`. The assistant's name comes from its provider.
|
|
4
|
-
import { METER_LEVELS } from '@columenlabs/lumen-ui-types/views';
|
|
4
|
+
import { HEALTH_LEVELS, METER_LEVELS, } from '@columenlabs/lumen-ui-types/views';
|
|
5
5
|
export const KIT = {
|
|
6
6
|
prefsCookie: 'lumen_prefs',
|
|
7
7
|
marketOff: 'Live market data is not on this app.',
|
|
8
8
|
meterLevels: METER_LEVELS,
|
|
9
|
+
healthLevels: HEALTH_LEVELS,
|
|
9
10
|
};
|
|
10
11
|
const changed = new Set();
|
|
11
12
|
/** Runs `fn` after each `configureLumen` (preferences read again from the app's cookie). */
|
|
@@ -14,10 +15,12 @@ export function onConfigure(fn) {
|
|
|
14
15
|
}
|
|
15
16
|
/** Sets what `c` names; the rest keep the kit's defaults (one meter level can move alone). */
|
|
16
17
|
export function configureLumen(c) {
|
|
17
|
-
const { meterLevels, ...rest } = c;
|
|
18
|
+
const { meterLevels, healthLevels, ...rest } = c;
|
|
18
19
|
Object.assign(KIT, rest);
|
|
19
20
|
if (meterLevels)
|
|
20
21
|
KIT.meterLevels = { ...KIT.meterLevels, ...meterLevels };
|
|
22
|
+
if (healthLevels)
|
|
23
|
+
KIT.healthLevels = { ...KIT.healthLevels, ...healthLevels };
|
|
21
24
|
for (const fn of changed)
|
|
22
25
|
fn();
|
|
23
26
|
}
|
|
@@ -34,6 +34,10 @@ import { useSymbolChart } from './lumen-chart.js';
|
|
|
34
34
|
import { faint } from './price-chart.js';
|
|
35
35
|
const LINE_TOKENS = ['--chart-1', '--chart-3', '--chart-4', '--chart-6', '--chart-2', '--chart-5'];
|
|
36
36
|
const MINUS = '−';
|
|
37
|
+
/** A signed percent on the chart's price axis. */
|
|
38
|
+
const pct = (p) => `${p >= 0 ? '+' : MINUS}${Math.abs(p).toFixed(2)}%`;
|
|
39
|
+
/** How near (px) a tick may sit to a line's value tag before it is left blank: one tag's height. */
|
|
40
|
+
const TAG_PX = 16;
|
|
37
41
|
export function fmtValue(s, v) {
|
|
38
42
|
return `${s.prefix}${v
|
|
39
43
|
.toLocaleString('en-US', {
|
|
@@ -143,7 +147,25 @@ export function MarketLines({ symbols: all }) {
|
|
|
143
147
|
},
|
|
144
148
|
localization: {
|
|
145
149
|
timeFormatter: (t) => clock(t, timeZone),
|
|
146
|
-
priceFormatter:
|
|
150
|
+
priceFormatter: pct,
|
|
151
|
+
// A tick a line's value tag would cover is left blank, so the two never overlap.
|
|
152
|
+
tickmarksPriceFormatter: (prices) => {
|
|
153
|
+
const ref = [...lines.current.values()][0];
|
|
154
|
+
const tags = [...lines.current.values(), ...extLines.current.values()]
|
|
155
|
+
.filter((s) => s.options().lastValueVisible)
|
|
156
|
+
.map((s) => s.data().at(-1))
|
|
157
|
+
.flatMap((d) => (d?.value === undefined ? [] : [d.value]));
|
|
158
|
+
const y = (p) => ref?.priceToCoordinate(p) ?? null;
|
|
159
|
+
return prices.map((p) => {
|
|
160
|
+
const at = y(p);
|
|
161
|
+
const covered = at !== null &&
|
|
162
|
+
tags.some((t) => {
|
|
163
|
+
const ty = y(t);
|
|
164
|
+
return ty !== null && Math.abs(ty - at) < TAG_PX;
|
|
165
|
+
});
|
|
166
|
+
return covered ? '' : pct(p);
|
|
167
|
+
});
|
|
168
|
+
},
|
|
147
169
|
},
|
|
148
170
|
crosshair: { mode: CrosshairMode.Normal },
|
|
149
171
|
handleScale: false,
|
|
@@ -234,7 +256,7 @@ export function MarketLines({ symbols: all }) {
|
|
|
234
256
|
return (_jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No bars for this session yet." }));
|
|
235
257
|
if (!ready)
|
|
236
258
|
return _jsx(Skeleton, { "aria-busy": true, className: "h-72 w-full sm:h-80" });
|
|
237
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsx("div", { className: "flex flex-
|
|
259
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsx("div", { className: "flex flex-wrap justify-between gap-x-1 sm:justify-start sm:gap-x-3", children: shown.map((s, i) => {
|
|
238
260
|
const ch = hover?.[s.key] ?? changeOf(s);
|
|
239
261
|
const last = s.bars.at(-1)?.c;
|
|
240
262
|
const ext = extendedOf(s);
|
|
@@ -248,7 +270,11 @@ export function MarketLines({ symbols: all }) {
|
|
|
248
270
|
const CHIP = 'h-4 px-1.5 text-[10px]';
|
|
249
271
|
/** "≈ SPY": the row is priced from that ETF. Purple, so it never reads as a warning. */
|
|
250
272
|
const ProxyChip = ({ text, title }) => (_jsx(Badge, { variant: "outline", title: title, className: cn(CHIP, 'num shrink-0 border-proxy/40 bg-proxy/10 text-proxy'), children: text }));
|
|
251
|
-
|
|
273
|
+
/** "Delayed", and the time of the value shown when it is known. */
|
|
274
|
+
const DelayedChip = ({ at }) => {
|
|
275
|
+
const { timeZone } = usePrefs();
|
|
276
|
+
return (_jsxs(Badge, { variant: "outline", className: cn(CHIP, 'border-warning/50 bg-warning/10 text-warning'), children: ["Delayed", at !== undefined ? ` · ${clock(at, timeZone).replace(/ [A-Z]{2,5}$/, '')}` : ''] }));
|
|
277
|
+
};
|
|
252
278
|
/** One widget per symbol: change, sparkline over the prior close, value, and its source chip. */
|
|
253
279
|
export function MarketTiles({ symbols }) {
|
|
254
280
|
const liveKeys = useMemo(() => symbols.filter((k) => !isDailyMarketKey(k)), [symbols]);
|
|
@@ -281,7 +307,14 @@ export function MarketTiles({ symbols }) {
|
|
|
281
307
|
function TileState({ label, text, tone, loading, children, }) {
|
|
282
308
|
return (_jsxs("div", { "aria-busy": loading || undefined, className: "flex h-[7.5rem] min-w-0 flex-col gap-1 rounded-lg border bg-card p-2.5", children: [_jsx("span", { className: "truncate font-semibold", children: label }), loading ? (_jsx(Skeleton, { className: "h-14 w-full" })) : (_jsxs("span", { className: "flex h-14 items-center gap-1.5 text-xs text-muted-foreground", children: [tone === 'error' ? (_jsx(CircleAlert, { className: "size-3.5 shrink-0 text-loss" })) : (_jsx(CircleSlash, { className: "size-3.5 shrink-0" })), _jsx("span", { className: "line-clamp-3", children: text })] })), _jsxs("span", { className: "flex items-center justify-between gap-2 text-[10px] text-muted-foreground", children: [loading ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(LoaderCircle, { className: "size-3 animate-spin" }), text] })) : (_jsx("span", {})), children] })] }));
|
|
283
309
|
}
|
|
310
|
+
/** How long a widget shows its first bars as on the way before it says there are none. */
|
|
311
|
+
const FIRST_BARS_MS = 90_000;
|
|
284
312
|
function Widget({ s, onOpen, live }) {
|
|
313
|
+
const [waited, setWaited] = useState(false);
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
const t = setTimeout(() => setWaited(true), FIRST_BARS_MS);
|
|
316
|
+
return () => clearTimeout(t);
|
|
317
|
+
}, []);
|
|
285
318
|
const last = s.bars.at(-1)?.c;
|
|
286
319
|
const ch = s.daily?.points && last !== undefined && s.prevClose !== null
|
|
287
320
|
? last - s.prevClose
|
|
@@ -306,7 +339,7 @@ function Widget({ s, onOpen, live }) {
|
|
|
306
339
|
const hi = Math.max(...values, s.prevClose ?? Number.NEGATIVE_INFINITY);
|
|
307
340
|
const pad = (hi - lo) * 0.12 || 1;
|
|
308
341
|
const tone = up ? 'var(--gain)' : 'var(--loss)';
|
|
309
|
-
return (_jsxs("button", { type: "button", onClick: onOpen, disabled: !onOpen, "aria-label": `${s.label} ${ch === null ? '' : fmtCh(ch)}, open chart`, className: "flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-2.5 text-left transition-colors hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-default", children: [_jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "min-w-0 truncate font-semibold", children: s.label }), ch !== null && (_jsx("span", { className: cn('num shrink-0 text-xs whitespace-nowrap', up ? 'text-gain' : 'text-loss'), children: fmtCh(ch) }))] }), ext ? (_jsxs("span", { className: "num truncate text-xs text-muted-foreground", children: [ext.label, ' ', _jsx("span", { className: ext.change >= 0 ? 'text-gain' : 'text-loss', children: fmtPct(ext.change).slice(0, -2) })] })) : (_jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: s.name })), data.length > 1 ? (_jsx(ChartContainer, { config: { c: { label: s.label, color: tone } }, className: "aspect-auto h-14 w-full", children: _jsxs(AreaChart, { data: data, margin: { top: 2, bottom: 2, left: 0, right: 0 }, children: [_jsx(YAxis, { hide: true, domain: [lo - pad, hi + pad] }), s.prevClose !== null && !s.daily && (_jsx(ReferenceLine, { y: s.prevClose, stroke: "var(--muted-foreground)", strokeDasharray: "3 3" })), _jsx(Area, { dataKey: "c", type: "linear", stroke: tone, strokeWidth: 1.5, fill: tone, fillOpacity: 0.1, isAnimationActive: false, dot: false }), ext && (_jsx(Area, { dataKey: "e", type: "linear", stroke: ext.change >= 0 ? 'var(--gain)' : 'var(--loss)', strokeWidth: 1.5, strokeDasharray: "3 2", fill: "none", isAnimationActive: false, dot: false }))] }) })) : (_jsx("span", { className: "grid h-14 place-items-center text-xs text-muted-foreground", children: s.error
|
|
342
|
+
return (_jsxs("button", { type: "button", onClick: onOpen, disabled: !onOpen, "aria-label": `${s.label} ${ch === null ? '' : fmtCh(ch)}, open chart`, className: "flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-2.5 text-left transition-colors hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-default", children: [_jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "min-w-0 truncate font-semibold", children: s.label }), ch !== null && (_jsx("span", { className: cn('num shrink-0 text-xs whitespace-nowrap', up ? 'text-gain' : 'text-loss'), children: fmtCh(ch) }))] }), ext ? (_jsxs("span", { className: "num truncate text-xs text-muted-foreground", children: [ext.label, ' ', _jsx("span", { className: ext.change >= 0 ? 'text-gain' : 'text-loss', children: fmtPct(ext.change).slice(0, -2) })] })) : (_jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: s.name })), data.length > 1 ? (_jsx(ChartContainer, { config: { c: { label: s.label, color: tone } }, className: "aspect-auto h-14 w-full", children: _jsxs(AreaChart, { data: data, margin: { top: 2, bottom: 2, left: 0, right: 0 }, children: [_jsx(YAxis, { hide: true, domain: [lo - pad, hi + pad] }), s.prevClose !== null && !s.daily && (_jsx(ReferenceLine, { y: s.prevClose, stroke: "var(--muted-foreground)", strokeDasharray: "3 3" })), _jsx(Area, { dataKey: "c", type: "linear", stroke: tone, strokeWidth: 1.5, fill: tone, fillOpacity: 0.1, isAnimationActive: false, dot: false }), ext && (_jsx(Area, { dataKey: "e", type: "linear", stroke: ext.change >= 0 ? 'var(--gain)' : 'var(--loss)', strokeWidth: 1.5, strokeDasharray: "3 2", fill: "none", isAnimationActive: false, dot: false }))] }) })) : s.error ? (_jsx("span", { className: "grid h-14 place-items-center text-xs text-muted-foreground", children: s.error })) : waited ? (_jsx("span", { className: "grid h-14 place-items-center text-xs text-muted-foreground", children: "No bars yet" })) : (_jsx(Skeleton, { "aria-busy": true, className: "h-14 w-full" })), _jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [last !== undefined ? (_jsx("span", { className: "num text-sm", children: fmtValue(s, last) })) : (_jsx(Skeleton, { className: "h-5 w-14" })), s.daily ? (_jsxs("span", { className: "text-[10px] text-muted-foreground", children: ["Daily \u00B7 ", day(s.daily.asOf)] })) : s.delayed ? (last !== undefined && _jsx(DelayedChip, { at: s.bars.at(-1)?.t })) : s.via ? (_jsxs("span", { className: "text-[10px] text-muted-foreground", children: ["via ", s.via] })) : live ? (_jsx("span", { className: "text-[10px] text-muted-foreground", children: "Live" })) : null] })] }));
|
|
310
343
|
}
|
|
311
344
|
/**
|
|
312
345
|
* Up or down today: one row per symbol with its price now and its change today,
|
|
@@ -339,7 +372,7 @@ export function MarketToday({ symbols: all }) {
|
|
|
339
372
|
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("p", { className: "num flex items-center gap-2 text-xs text-muted-foreground", children: [_jsxs("span", { className: "text-gain", children: ["\u25B2 ", up, " up"] }), _jsxs("span", { className: "text-loss", children: ["\u25BC ", down, " down"] }), session && _jsx("span", { className: "ml-auto font-sans", children: session })] }), _jsx("div", { className: "flex flex-col", children: rows.map(({ s, today, proxy }) => {
|
|
340
373
|
const ch = today?.change ?? null;
|
|
341
374
|
const tone = ch === null ? 'text-muted-foreground' : ch >= 0 ? 'text-gain' : 'text-loss';
|
|
342
|
-
return (_jsxs(Button, { variant: "ghost", className: "h-auto min-w-0 justify-between gap-3 px-2 py-1.5 font-normal", disabled: !open, onClick: () => open?.(s.key), title: `Open ${s.label}'s chart`, children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "truncate font-semibold", children: s.label }), proxy && (_jsx(ProxyChip, { text: `≈ ${proxy}`, title: `${s.label} is priced from ${proxy}'s move${today?.label ? ` ${today.label.toLowerCase()}` : ''}.` })), s.delayed && !today
|
|
375
|
+
return (_jsxs(Button, { variant: "ghost", className: "h-auto min-w-0 justify-between gap-3 px-2 py-1.5 font-normal", disabled: !open, onClick: () => open?.(s.key), title: `Open ${s.label}'s chart`, children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "truncate font-semibold", children: s.label }), proxy && (_jsx(ProxyChip, { text: `≈ ${proxy}`, title: `${s.label} is priced from ${proxy}'s move${today?.label ? ` ${today.label.toLowerCase()}` : ''}.` })), s.delayed && today && !today.label && _jsx(DelayedChip, {})] }), today ? (_jsxs("span", { className: "flex shrink-0 items-center gap-3", children: [_jsx("span", { className: "num text-xs text-muted-foreground", children: fmtValue(s, today.value) }), _jsx("span", { className: cn('num w-16 text-right text-sm font-semibold', tone), children: ch === null ? '—' : fmtPct(ch) })] })) : (_jsx(Skeleton, { className: "h-4 w-28 shrink-0" }))] }, s.key));
|
|
343
376
|
}) }), proxied.length > 0 && (_jsxs("p", { className: "flex items-start gap-2 border-t px-2 pt-2 text-xs text-muted-foreground", children: [_jsx(ProxyChip, { text: "\u2248" }), _jsx("span", { children: proxied.length === 1
|
|
344
377
|
? `Proxy: ${proxied[0]?.s.label} moves with ${proxied[0]?.proxy}.`
|
|
345
378
|
: "Proxy: these don't trade now, so each moves with its ETF." })] }))] }));
|
package/dist/meter-level.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { type HealthLevels, type MeterKind, type MeterLevel, type MeterLevels } from '@columenlabs/lumen-ui-types/views';
|
|
2
|
+
/**
|
|
3
|
+
* The level of `share` (0 to 1), on the app's levels or this meter's own `levels`. `kind`
|
|
4
|
+
* `health` reads it the other way (higher is better) on the app's `healthLevels`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function meterLevel(share: number, levels?: Partial<MeterLevels> | Partial<HealthLevels>, kind?: MeterKind): MeterLevel;
|
|
7
|
+
/** Each level's colour on a bar, a dot or a chip's tint: theme tokens (`theme.css`). */
|
|
5
8
|
export declare const METER_LEVEL_COLOR: Record<MeterLevel, string>;
|
|
6
|
-
/**
|
|
7
|
-
export declare const
|
|
9
|
+
/** Each level's text colour on its own tint (a chip): darker than the fill, so it reads at 4.5:1. */
|
|
10
|
+
export declare const METER_LEVEL_TEXT: Record<MeterLevel, string>;
|
|
11
|
+
/** A share's `UnitValue` tone: none while `ok`, so only a number that needs a look is tinted. */
|
|
12
|
+
export declare const meterValueTone: (share: number, levels?: Partial<MeterLevels> | Partial<HealthLevels>, kind?: MeterKind) => "caution" | "loss" | "warning" | undefined;
|
package/dist/meter-level.js
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
+
// How full a meter reads, the same way in every app. A usage meter is green while there is room,
|
|
2
|
+
// amber from `watch` (75%), orange from `alert` (90%) and red from `over` (the cap). A health
|
|
3
|
+
// meter (uptime, success rate) is green at 99% and over, amber down to 95%, red below.
|
|
4
|
+
// `ShareMeter` and `UnitValue` colour themselves with it; anything else drawing a share of a cap
|
|
5
|
+
// (a dot, a chip, a "Watch for" cell) calls `meterLevel` rather than its own numbers.
|
|
6
|
+
import { healthLevel, usageLevel, } from '@columenlabs/lumen-ui-types/views';
|
|
1
7
|
import { KIT } from './kit-config.js';
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The level of `share` (0 to 1), on the app's levels or this meter's own `levels`. `kind`
|
|
10
|
+
* `health` reads it the other way (higher is better) on the app's `healthLevels`.
|
|
11
|
+
*/
|
|
12
|
+
export function meterLevel(share, levels, kind = 'usage') {
|
|
13
|
+
return kind === 'health'
|
|
14
|
+
? healthLevel(share, { ...KIT.healthLevels, ...levels })
|
|
15
|
+
: usageLevel(share, { ...KIT.meterLevels, ...levels });
|
|
6
16
|
}
|
|
7
|
-
/** Each level's colour
|
|
17
|
+
/** Each level's colour on a bar, a dot or a chip's tint: theme tokens (`theme.css`). */
|
|
8
18
|
export const METER_LEVEL_COLOR = {
|
|
9
|
-
ok: '
|
|
10
|
-
watch: 'var(--
|
|
11
|
-
alert: 'var(--
|
|
19
|
+
ok: 'var(--meter-ok)',
|
|
20
|
+
watch: 'var(--meter-watch)',
|
|
21
|
+
alert: 'var(--meter-alert)',
|
|
22
|
+
over: 'var(--meter-over)',
|
|
12
23
|
};
|
|
13
|
-
/**
|
|
14
|
-
export const
|
|
24
|
+
/** Each level's text colour on its own tint (a chip): darker than the fill, so it reads at 4.5:1. */
|
|
25
|
+
export const METER_LEVEL_TEXT = {
|
|
26
|
+
ok: 'var(--foreground)',
|
|
27
|
+
watch: 'color-mix(in oklab, var(--warning) 85%, var(--foreground))',
|
|
28
|
+
alert: 'color-mix(in oklab, var(--caution) 85%, var(--foreground))',
|
|
29
|
+
over: 'color-mix(in oklab, var(--loss) 85%, var(--foreground))',
|
|
30
|
+
};
|
|
31
|
+
/** A share's `UnitValue` tone: none while `ok`, so only a number that needs a look is tinted. */
|
|
32
|
+
export const meterValueTone = (share, levels, kind) => ({ ok: undefined, watch: 'warning', alert: 'caution', over: 'loss' })[meterLevel(share, levels, kind)];
|
package/dist/share-meter.d.ts
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
import type { MeterLevels } from '@columenlabs/lumen-ui-types/views';
|
|
1
|
+
import type { HealthLevels, MeterKind, MeterLevels } from '@columenlabs/lumen-ui-types/views';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
/** One part of a split meter: its own share of the cap (0 to 1 of the whole cap)
|
|
3
|
+
/** One part of a split meter: its own share of the cap (0 to 1 of the whole cap). */
|
|
4
4
|
export interface ShareSegment {
|
|
5
5
|
value: number;
|
|
6
|
-
/**
|
|
7
|
-
color
|
|
6
|
+
/** No longer drawn (the bar takes its level's colour); kept so older calls still compile. */
|
|
7
|
+
color?: string;
|
|
8
8
|
/** Who it is, read out by screen readers and in the bar's hover (`web 122%`). */
|
|
9
9
|
label?: string;
|
|
10
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
|
|
11
|
+
export declare function ShareMeter({ value, label, color, kind, levels, segments, wide, className, }: {
|
|
12
|
+
/** 0 to 1 fills the bar; past 1 it stays full and the overage spills past a tick at the cap. */
|
|
13
13
|
value: number;
|
|
14
14
|
/** The number beside the bar, e.g. `113%`; left out, the rounded percent. */
|
|
15
15
|
label?: ReactNode;
|
|
16
|
-
/** The bar's colour, a theme token; left out, the share's level
|
|
16
|
+
/** The bar's colour, a theme token; left out, the share's level. */
|
|
17
17
|
color?: string;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
/**
|
|
18
|
+
/** `health` reads higher as better (uptime): green at 99% and over, amber, red under 95%. */
|
|
19
|
+
kind?: MeterKind;
|
|
20
|
+
/** This meter's own levels (e.g. `{ watch: 0.5 }`); left out, the app's levels for its kind. */
|
|
21
|
+
levels?: Partial<MeterLevels> | Partial<HealthLevels>;
|
|
22
|
+
/** Who used the cap, largest first; their values add up to `value`. Shown in the hover. */
|
|
21
23
|
segments?: ShareSegment[];
|
|
22
24
|
/** Fills its cell (a list row) instead of the fixed table width. */
|
|
23
25
|
wide?: boolean;
|
package/dist/share-meter.js
CHANGED
|
@@ -2,17 +2,18 @@ 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
/** How far past the cap the spill stub can grow (a share of the cap). */
|
|
6
|
+
const SPILL_MAX = 0.5;
|
|
7
|
+
export function ShareMeter({ value, label, color, kind, levels, segments, wide, className, }) {
|
|
8
|
+
const said = segments
|
|
9
|
+
?.filter((s) => s.value > 0)
|
|
10
|
+
.map((s) => `${s.label ?? ''} ${Math.round(s.value * 100)}%`.trim());
|
|
11
|
+
const over = kind !== 'health' && value > 1;
|
|
12
|
+
const spill = over ? Math.min(value - 1, SPILL_MAX) / SPILL_MAX : 0;
|
|
13
|
+
return (_jsxs("span", { className: cn('inline-flex items-center gap-2', wide && 'w-full', className), title: said?.length ? said.join(' · ') : undefined, children: [_jsxs("span", { "data-slot": "share-meter", "data-over": over || undefined, className: cn('flex shrink-0 items-center', wide ? 'w-full shrink' : 'w-16'), style: {
|
|
14
|
+
'--meter-color': color ?? METER_LEVEL_COLOR[meterLevel(value, levels, kind)],
|
|
15
|
+
}, children: [_jsx(Progress, { value: Math.max(0, Math.min(value, 1)) * 100, "aria-label": said?.length ? said.join(', ') : undefined, className: cn('h-1.5 w-auto flex-2 [&>[data-slot=progress-indicator]]:bg-(--meter-color)', over && 'rounded-r-none') }), _jsx("span", { "aria-hidden": true, className: "flex h-1.5 flex-1", children: over && (_jsx("span", { "data-slot": "share-spill", className: "h-full min-w-1 rounded-r-full border-foreground/70 border-l-2", style: {
|
|
16
|
+
width: `${spill * 100}%`,
|
|
17
|
+
background: 'repeating-linear-gradient(135deg, var(--meter-color) 0 2px, transparent 2px 4px)',
|
|
18
|
+
} })) })] }), _jsx("span", { className: "num w-11 shrink-0 text-right text-xs", children: label ?? `${Math.round(value * 100)}%` })] }));
|
|
18
19
|
}
|
package/dist/summary-cards.js
CHANGED
|
@@ -19,7 +19,9 @@ export function figuresOf(domain, t, rows, fields) {
|
|
|
19
19
|
if (!f)
|
|
20
20
|
return null;
|
|
21
21
|
const count = isCountAgg(t.agg);
|
|
22
|
-
const
|
|
22
|
+
const own = filterByTokens(rows, fields, t.filter);
|
|
23
|
+
// A sum over no rows is nothing yet: $0, never a dash.
|
|
24
|
+
const value = roll(f, t.agg, own) ?? (t.agg === 'sum' && !own.length ? 0 : null);
|
|
23
25
|
const main = {
|
|
24
26
|
kind: 'value',
|
|
25
27
|
value,
|
|
@@ -36,15 +38,21 @@ export function figuresOf(domain, t, rows, fields) {
|
|
|
36
38
|
if (l.kind === 'pct') {
|
|
37
39
|
const of = roll(lf, l.agg, rows);
|
|
38
40
|
const pct = typeof value === 'number' && typeof of === 'number' && of !== 0 ? (value / of) * 100 : null;
|
|
41
|
+
// A line with nothing to say is left out, never a dash in a sentence.
|
|
42
|
+
if (pct === null)
|
|
43
|
+
return [];
|
|
39
44
|
return [
|
|
40
45
|
{ kind: 'value', value: pct, format: 'weight', amount: false, suffix: `of ${lf.label}` },
|
|
41
46
|
];
|
|
42
47
|
}
|
|
43
48
|
const c = isCountAgg(l.agg);
|
|
49
|
+
const lv = roll(lf, l.agg, filterByTokens(rows, fields, l.filter));
|
|
50
|
+
if (lv === null)
|
|
51
|
+
return [];
|
|
44
52
|
return [
|
|
45
53
|
{
|
|
46
54
|
kind: 'value',
|
|
47
|
-
value:
|
|
55
|
+
value: lv,
|
|
48
56
|
format: c ? 'rounded' : lf.format,
|
|
49
57
|
amount: !c && !!lf.amount,
|
|
50
58
|
suffix: l.suffix || undefined,
|
|
@@ -82,23 +90,24 @@ export function LumenCard({ domain, tile: t, rows, fields, drawn, loading, style
|
|
|
82
90
|
const share = style !== 'number' && target && typeof v === 'number'
|
|
83
91
|
? Math.max(0, Math.min(100, (v / target) * 100))
|
|
84
92
|
: null;
|
|
85
|
-
const tone = meterTone(meter, share, bands ?? (meter
|
|
93
|
+
const tone = meterTone(meter, share, bands ?? (meter ? kitBands(meter) : undefined));
|
|
86
94
|
const of = target ? (_jsx(ViewValueCell, { value: target, format: figs.main.format, amount: figs.main.amount })) : null;
|
|
87
95
|
if (style === 'progress_circle')
|
|
88
96
|
return (_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(ProgressRing, { value: share, tone: tone }), _jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: "num truncate text-xl leading-7 font-semibold", children: _jsx(FigureText, { f: figs.main }) }), _jsx("span", { className: "num truncate text-xs text-muted-foreground", children: of ? _jsxs(_Fragment, { children: ["of ", of] }) : 'Set a target in Edit widget' })] })] }));
|
|
89
97
|
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: cn('num truncate text-2xl leading-8 font-semibold', t.sign && typeof v === 'number' && (v > 0 ? 'text-gain' : v < 0 ? 'text-loss' : '')), children: _jsx(FigureText, { f: figs.main }) }), style === 'progress_bar' && (_jsxs("div", { className: "flex flex-col gap-1 pt-1", children: [_jsx(Progress, { value: share ?? 0, className: cn('h-2', tone && TONE_BAR[tone]), "aria-label": "Progress to target" }), _jsx("span", { className: "num text-xs text-muted-foreground", children: of ? (_jsxs(_Fragment, { children: [share === null ? '—' : `${share.toFixed(0)}%`, " of ", of] })) : ('Set a target in Edit widget') })] })), figs.lines.map((l, i) => (_jsx("span", { className: "num line-clamp-2 text-xs text-muted-foreground", children: _jsx(FigureText, { f: l }) }, i)))] }));
|
|
90
98
|
}
|
|
91
|
-
/** A
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
bad:
|
|
95
|
-
}
|
|
99
|
+
/** A meter's bands from the app's `meterLevels` or `healthLevels`, in %. */
|
|
100
|
+
const kitBands = (meter) => {
|
|
101
|
+
const l = meter === 'health' ? KIT.healthLevels : KIT.meterLevels;
|
|
102
|
+
return { warn: l.watch * 100, bad: l.alert * 100 };
|
|
103
|
+
};
|
|
96
104
|
// The ring draws with the charts library, so it loads only when a card shows one.
|
|
97
105
|
const ProgressRing = lazyPart(() => import('./progress-ring.js').then((m) => m.ProgressRing), _jsx(Skeleton, { className: "size-16 shrink-0 rounded-full" }));
|
|
98
106
|
/** A meter's tone on a bar's fill (theme tokens, no glow). */
|
|
99
107
|
const TONE_BAR = {
|
|
100
108
|
success: '[&>[data-slot=progress-indicator]]:bg-success',
|
|
101
109
|
warning: '[&>[data-slot=progress-indicator]]:bg-warning',
|
|
110
|
+
caution: '[&>[data-slot=progress-indicator]]:bg-caution',
|
|
102
111
|
destructive: '[&>[data-slot=progress-indicator]]:bg-destructive',
|
|
103
112
|
};
|
|
104
113
|
/** A Summary's cards over its rows; null when it has none to show. */
|
|
@@ -107,7 +116,7 @@ export function SummaryTiles({ domain, tiles, rows, fields, tile, loading, }) {
|
|
|
107
116
|
if (!shown.length)
|
|
108
117
|
return null;
|
|
109
118
|
// Each card fills its column, as a header's metric cards do (`TileRow fill`), not the fixed tile width.
|
|
110
|
-
const grid = 'grid grid-cols-2 gap-(--shell-section-gap) md:auto-cols-
|
|
119
|
+
const grid = 'grid grid-cols-2 gap-(--shell-section-gap) md:auto-cols-[minmax(0,1fr)] md:grid-flow-col lg:*:*:w-auto';
|
|
111
120
|
if (loading)
|
|
112
121
|
return (_jsx("div", { "data-slot": "view-tiles", "aria-busy": "true", className: grid, children: shown.map((t) => (_jsx(Skeleton, { className: "h-[4.5rem] w-full rounded-xl" }, t.key))) }));
|
|
113
122
|
return (_jsx("div", { "data-slot": "view-tiles", className: cn(grid,
|
package/dist/theme.css
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
--color-info: var(--info);
|
|
30
30
|
--color-proxy: var(--proxy);
|
|
31
31
|
--color-warning: var(--warning);
|
|
32
|
+
--color-caution: var(--caution);
|
|
32
33
|
--color-rule-yes: var(--rule-yes);
|
|
33
34
|
--color-rule-no: var(--rule-no);
|
|
34
35
|
--color-gap: var(--gap);
|
|
@@ -182,6 +183,13 @@
|
|
|
182
183
|
/* A price taken from a stand-in ETF (≈ SPY). */
|
|
183
184
|
--proxy: #7c3aed;
|
|
184
185
|
--warning: #9a5b06;
|
|
186
|
+
/* Orange text between amber and red: a meter from 90% of its cap (UnitValue `caution`). */
|
|
187
|
+
--caution: #b24c08;
|
|
188
|
+
/* Meter fills (meterLevel): green with room, amber from 75%, orange from 90%, red at the cap. */
|
|
189
|
+
--meter-ok: #2f9e5a;
|
|
190
|
+
--meter-watch: #e0a800;
|
|
191
|
+
--meter-alert: #ea6c17;
|
|
192
|
+
--meter-over: #d63838;
|
|
185
193
|
/* Playbook rules: yes (fired, the path and the decision) green, no amber. */
|
|
186
194
|
--rule-yes: #15803d;
|
|
187
195
|
--rule-no: #b45309;
|
|
@@ -278,6 +286,11 @@
|
|
|
278
286
|
--info: #6aa9ff;
|
|
279
287
|
--proxy: #b69cff;
|
|
280
288
|
--warning: #e5a73a;
|
|
289
|
+
--caution: #ffa266;
|
|
290
|
+
--meter-ok: #44bf78;
|
|
291
|
+
--meter-watch: #f2c14e;
|
|
292
|
+
--meter-alert: #ff8a3d;
|
|
293
|
+
--meter-over: #ff5a5a;
|
|
281
294
|
--rule-yes: #4ade80;
|
|
282
295
|
--rule-no: #fbbf24;
|
|
283
296
|
--gap: #f0883e;
|
|
@@ -727,11 +740,15 @@ body[data-spotlight="nav"] [data-spotlit] {
|
|
|
727
740
|
}
|
|
728
741
|
|
|
729
742
|
/* LumenGrid: react-grid-layout's own rules, in the shell's tokens. */
|
|
743
|
+
/* Cards glide only while a person moves them in Edit layout. On a page that loads they take their
|
|
744
|
+
measured place at once, so a card that settles never leaves a gap sliding shut behind it. */
|
|
730
745
|
.lumen-grid {
|
|
731
746
|
position: relative;
|
|
747
|
+
}
|
|
748
|
+
.lumen-grid-edit {
|
|
732
749
|
transition: height 200ms ease;
|
|
733
750
|
}
|
|
734
|
-
.lumen-grid > .react-grid-item {
|
|
751
|
+
.lumen-grid-edit > .react-grid-item {
|
|
735
752
|
transition: transform 200ms ease, width 200ms ease, height 200ms ease;
|
|
736
753
|
}
|
|
737
754
|
.lumen-grid > .react-grid-item.resizing,
|
package/dist/tile.js
CHANGED
|
@@ -18,7 +18,7 @@ export function TileSpark({ values, color = 'var(--chart-1)', className, }) {
|
|
|
18
18
|
/** A value with no digit in it ("Workers Paid", "Healthy") is a word, not a number. */
|
|
19
19
|
const isWord = (v) => typeof v === 'string' && !/\d/.test(v);
|
|
20
20
|
function TileBody({ label, value, caption, lines, valueClassName, spark, sparkColor, }) {
|
|
21
|
-
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-full truncate text-xs text-muted-foreground", children: label }), _jsx("span", { className: cn(!isWord(value) && 'num', 'truncate text-xl leading-7 font-semibold', valueClassName), children: value }), caption ? (_jsx("span", { className: "line-clamp-2 text-xs text-muted-foreground tabular-nums", children: caption })) : null, lines?.map((l, i) => (_jsx("span", { className: "
|
|
21
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-full truncate text-xs text-muted-foreground", children: label }), _jsx("span", { className: cn(!isWord(value) && 'num', 'truncate text-xl leading-7 font-semibold', valueClassName), children: value }), caption ? (_jsx("span", { className: "line-clamp-2 text-xs text-muted-foreground tabular-nums", children: caption })) : null, lines?.map((l, i) => (_jsx("span", { className: "line-clamp-2 text-xs text-muted-foreground tabular-nums", children: l }, i))), spark ? _jsx(TileSpark, { values: spark, color: sparkColor, className: "mt-1 self-stretch" }) : null] }));
|
|
22
22
|
}
|
|
23
23
|
export function Tile({ label, value, caption, lines, className, valueClassName, spark, sparkColor, }) {
|
|
24
24
|
return (_jsx(Card, { size: "sm", className: cn('min-w-0 flex-1 basis-2/5 gap-0.5 px-3 lg:basis-auto', TILE_WIDTH, className), children: _jsx(TileBody, { label: label, value: value, caption: caption, lines: lines, valueClassName: valueClassName, spark: spark, sparkColor: sparkColor }) }));
|
package/dist/time-to-cap.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ZapIcon } from 'lucide-react';
|
|
|
3
3
|
import { useIsMobile } from './hooks/use-mobile.js';
|
|
4
4
|
import { cn } from './lib/utils.js';
|
|
5
5
|
import { LumenSelector } from './lumen-selector.js';
|
|
6
|
-
import { METER_LEVEL_COLOR, meterLevel } from './meter-level.js';
|
|
6
|
+
import { METER_LEVEL_COLOR, METER_LEVEL_TEXT, meterLevel } from './meter-level.js';
|
|
7
7
|
import { PACE_WINDOWS, durationText, usePaceWindow } from './pace.js';
|
|
8
8
|
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
|
|
9
9
|
/** Picks the pace window (6h, 12h, 24h, 7d) for every page at once; a header action. */
|
|
@@ -17,7 +17,7 @@ export function PaceWindowPicker({ className }) {
|
|
|
17
17
|
/** What a pace reads as: the words and the level they're coloured by. */
|
|
18
18
|
export function paceText(p, short = false) {
|
|
19
19
|
if (p.eta === 0)
|
|
20
|
-
return { text: short ? 'cap' : 'At cap', level: '
|
|
20
|
+
return { text: short ? 'cap' : 'At cap', level: 'over' };
|
|
21
21
|
const level = meterLevel(p.atReset);
|
|
22
22
|
if (p.hits)
|
|
23
23
|
return { text: short ? durationText(p.eta, true) : `in ${durationText(p.eta)}`, level };
|
|
@@ -39,7 +39,7 @@ export function TimeToCap({ pace, label, short, levels, className, }) {
|
|
|
39
39
|
: level === 'ok'
|
|
40
40
|
? { background: 'var(--muted)' }
|
|
41
41
|
: {
|
|
42
|
-
background: `color-mix(in oklab, ${c}
|
|
43
|
-
color:
|
|
42
|
+
background: `color-mix(in oklab, ${c} 14%, transparent)`,
|
|
43
|
+
color: METER_LEVEL_TEXT[level],
|
|
44
44
|
}, children: [label && _jsx("span", { className: "truncate max-md:max-w-16", children: label }), _jsx("span", { className: "num", children: t.text })] }));
|
|
45
45
|
}
|
package/dist/unit-value.d.ts
CHANGED
|
@@ -7,9 +7,9 @@ export declare function UnitValue({ value, unit, tone: toneProp, share, levels,
|
|
|
7
7
|
value: ReactNode;
|
|
8
8
|
/** Its unit (`GB`, `M`, `/day`); left out, the slot stays empty so the column still lines up. */
|
|
9
9
|
unit?: ReactNode;
|
|
10
|
-
/** Near (`warning`) or
|
|
11
|
-
tone?: 'warning' | 'loss';
|
|
12
|
-
/** Its share of a cap (0 to 1): sets `tone` by `meterLevel` (amber from 75%, red from
|
|
10
|
+
/** Near (`warning`, amber), close to (`caution`, orange) or at (`loss`, red) a limit: tints the number and shows a dot. */
|
|
11
|
+
tone?: 'warning' | 'caution' | 'loss';
|
|
12
|
+
/** Its share of a cap (0 to 1): sets `tone` by `meterLevel` (amber from 75%, orange from 90%, red from 100%). */
|
|
13
13
|
share?: number;
|
|
14
14
|
/** This value's own levels; left out, the app's `meterLevels`. */
|
|
15
15
|
levels?: Partial<MeterLevels>;
|
package/dist/unit-value.js
CHANGED
|
@@ -14,5 +14,5 @@ export function UnitValue({ value, unit, tone: toneProp, share, levels, title, c
|
|
|
14
14
|
if (mark && col)
|
|
15
15
|
mark(col);
|
|
16
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 })] }));
|
|
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 === 'caution' && 'text-caution', 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' : tone === 'caution' ? 'bg-caution' : '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 })] }));
|
|
18
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.166",
|
|
4
4
|
"description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@codemirror/lang-javascript": "6.2.5",
|
|
50
50
|
"@codemirror/lang-json": "6.0.2",
|
|
51
51
|
"@codemirror/lang-sql": "6.10.0",
|
|
52
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.166",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|