@columenlabs/lumen-ui 0.1.165 → 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.
@@ -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: (p) => `${p >= 0 ? '+' : MINUS}${Math.abs(p).toFixed(2)}%`,
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-nowrap justify-between gap-x-1 overflow-x-auto overflow-y-hidden [scrollbar-width:none] sm:justify-start sm:gap-x-3", children: shown.map((s, i) => {
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
- const DelayedChip = () => (_jsx(Badge, { variant: "outline", className: cn(CHIP, 'border-warning/50 bg-warning/10 text-warning'), children: "Delayed" }));
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 ?? 'No bars yet' })), _jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "num text-sm", children: last !== undefined ? fmtValue(s, last) : '—' }), s.daily ? (_jsxs("span", { className: "text-[10px] text-muted-foreground", children: ["Daily \u00B7 ", day(s.daily.asOf)] })) : s.delayed ? (_jsx(DelayedChip, {})) : 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] })] }));
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?.label && _jsx(DelayedChip, {})] }), _jsxs("span", { className: "flex shrink-0 items-center gap-3", children: [_jsx("span", { className: "num text-xs text-muted-foreground", children: today ? fmtValue(s, today.value) : '—' }), _jsx("span", { className: cn('num w-16 text-right text-sm font-semibold', tone), children: ch === null ? '—' : fmtPct(ch) })] })] }, s.key));
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." })] }))] }));
@@ -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 value = roll(f, t.agg, filterByTokens(rows, fields, t.filter));
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: roll(lf, l.agg, filterByTokens(rows, fields, l.filter)),
55
+ value: lv,
48
56
  format: c ? 'rounded' : lf.format,
49
57
  amount: !c && !!lf.amount,
50
58
  suffix: l.suffix || undefined,
@@ -108,7 +116,7 @@ export function SummaryTiles({ domain, tiles, rows, fields, tile, loading, }) {
108
116
  if (!shown.length)
109
117
  return null;
110
118
  // Each card fills its column, as a header's metric cards do (`TileRow fill`), not the fixed tile width.
111
- const grid = 'grid grid-cols-2 gap-(--shell-section-gap) md:auto-cols-fr md:grid-flow-col lg:*:*:w-auto';
119
+ const grid = 'grid grid-cols-2 gap-(--shell-section-gap) md:auto-cols-[minmax(0,1fr)] md:grid-flow-col lg:*:*:w-auto';
112
120
  if (loading)
113
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))) }));
114
122
  return (_jsx("div", { "data-slot": "view-tiles", className: cn(grid,
package/dist/theme.css CHANGED
@@ -740,11 +740,15 @@ body[data-spotlight="nav"] [data-spotlit] {
740
740
  }
741
741
 
742
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. */
743
745
  .lumen-grid {
744
746
  position: relative;
747
+ }
748
+ .lumen-grid-edit {
745
749
  transition: height 200ms ease;
746
750
  }
747
- .lumen-grid > .react-grid-item {
751
+ .lumen-grid-edit > .react-grid-item {
748
752
  transition: transform 200ms ease, width 200ms ease, height 200ms ease;
749
753
  }
750
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: "num line-clamp-2 text-xs text-muted-foreground", children: l }, i))), spark ? _jsx(TileSpark, { values: spark, color: sparkColor, className: "mt-1 self-stretch" }) : null] }));
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.165",
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.165",
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",