@columenlabs/lumen-ui 0.1.164 → 0.1.165

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 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, 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`).
87
- - 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.
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';
@@ -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 (`alert`), as shares of a cap (0 to 1). */
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;
@@ -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
  }
@@ -1,7 +1,12 @@
1
- import type { MeterLevel, MeterLevels } from '@columenlabs/lumen-ui-types/views';
2
- /** The level of `share` (0 to 1 of a cap), on the app's levels or this meter's own `levels`. */
3
- export declare function meterLevel(share: number, levels?: Partial<MeterLevels>): MeterLevel;
4
- /** Each level's colour, theme tokens: `ok` is a quiet grey so only a meter near its cap draws the eye. */
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
- /** A share's `UnitValue` tone: none while `ok`. */
7
- export declare const meterValueTone: (share: number, levels?: Partial<MeterLevels>) => "loss" | "warning" | undefined;
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;
@@ -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
- /** The level of `share` (0 to 1 of a cap), on the app's levels or this meter's own `levels`. */
3
- export function meterLevel(share, levels) {
4
- const { watch, alert } = { ...KIT.meterLevels, ...levels };
5
- return share >= alert ? 'alert' : share >= watch ? 'watch' : 'ok';
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, theme tokens: `ok` is a quiet grey so only a meter near its cap draws the eye. */
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: 'color-mix(in oklab, var(--muted-foreground) 55%, transparent)',
10
- watch: 'var(--warning)',
11
- alert: 'var(--loss)',
19
+ ok: 'var(--meter-ok)',
20
+ watch: 'var(--meter-watch)',
21
+ alert: 'var(--meter-alert)',
22
+ over: 'var(--meter-over)',
12
23
  };
13
- /** A share's `UnitValue` tone: none while `ok`. */
14
- export const meterValueTone = (share, levels) => ({ ok: undefined, watch: 'warning', alert: 'loss' })[meterLevel(share, levels)];
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)];
@@ -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), in its colour. */
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
- /** A theme token or CSS colour (a label's colour). */
7
- color: string;
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 (with `segments`, the track stretches instead). */
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 (grey, amber, red). */
16
+ /** The bar's colour, a theme token; left out, the share's level. */
17
17
  color?: string;
18
- /** This meter's own levels (e.g. `{ watch: 0.5 }`); left out, the app's `meterLevels`. */
19
- levels?: Partial<MeterLevels>;
20
- /** Splits the bar by who used the cap, largest first; their values add up to `value`. */
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;
@@ -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
- 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
- }
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: {
16
- '--meter-color': color ?? METER_LEVEL_COLOR[meterLevel(value, levels)],
17
- } }), number] }));
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
  }
@@ -82,23 +82,24 @@ export function LumenCard({ domain, tile: t, rows, fields, drawn, loading, style
82
82
  const share = style !== 'number' && target && typeof v === 'number'
83
83
  ? Math.max(0, Math.min(100, (v / target) * 100))
84
84
  : null;
85
- const tone = meterTone(meter, share, bands ?? (meter === 'usage' ? usageBands() : undefined));
85
+ const tone = meterTone(meter, share, bands ?? (meter ? kitBands(meter) : undefined));
86
86
  const of = target ? (_jsx(ViewValueCell, { value: target, format: figs.main.format, amount: figs.main.amount })) : null;
87
87
  if (style === 'progress_circle')
88
88
  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
89
  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
90
  }
91
- /** A usage meter's bands from the app's `meterLevels`, in %. */
92
- const usageBands = () => ({
93
- warn: KIT.meterLevels.watch * 100,
94
- bad: KIT.meterLevels.alert * 100,
95
- });
91
+ /** A meter's bands from the app's `meterLevels` or `healthLevels`, in %. */
92
+ const kitBands = (meter) => {
93
+ const l = meter === 'health' ? KIT.healthLevels : KIT.meterLevels;
94
+ return { warn: l.watch * 100, bad: l.alert * 100 };
95
+ };
96
96
  // The ring draws with the charts library, so it loads only when a card shows one.
97
97
  const ProgressRing = lazyPart(() => import('./progress-ring.js').then((m) => m.ProgressRing), _jsx(Skeleton, { className: "size-16 shrink-0 rounded-full" }));
98
98
  /** A meter's tone on a bar's fill (theme tokens, no glow). */
99
99
  const TONE_BAR = {
100
100
  success: '[&>[data-slot=progress-indicator]]:bg-success',
101
101
  warning: '[&>[data-slot=progress-indicator]]:bg-warning',
102
+ caution: '[&>[data-slot=progress-indicator]]:bg-caution',
102
103
  destructive: '[&>[data-slot=progress-indicator]]:bg-destructive',
103
104
  };
104
105
  /** A Summary's cards over its rows; null when it has none to show. */
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;
@@ -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: 'alert' };
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} 18%, transparent)`,
43
- color: `color-mix(in oklab, ${c} 80%, var(--foreground))`,
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
  }
@@ -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 past (`loss`) a limit: tints the number and shows a dot. */
11
- tone?: 'warning' | 'loss';
12
- /** Its share of a cap (0 to 1): sets `tone` by `meterLevel` (amber from 75%, red from 90%). */
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>;
@@ -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.164",
3
+ "version": "0.1.165",
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.164",
52
+ "@columenlabs/lumen-ui-types": "0.1.165",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",