@columenlabs/lumen-ui 0.1.117 → 0.1.119

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.
@@ -9,7 +9,7 @@ import { bytesParts, controlOn, isCountAgg, isDollarFormat, viewPaginationSchema
9
9
  // already loaded; everything here runs on the client.
10
10
  import { createPaginatedRowModel, createSortedRowModel, flexRender, rowPaginationFeature, rowSortingFeature, sortFns, tableFeatures, useTable, } from '@tanstack/react-table';
11
11
  import { ArrowDownIcon, ArrowDownWideNarrowIcon, ArrowUpIcon, ArrowUpNarrowWideIcon, ChartCandlestick, ChartSpline, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
12
- import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
12
+ import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
13
13
  import { ColorChip, ColorDot } from './color-dot.js';
14
14
  import { DrawerSection } from './drawer-section.js';
15
15
  import { useFloatingHead } from './floating-head.js';
@@ -384,6 +384,14 @@ function pathOf(domain, f) {
384
384
  }
385
385
  /** Every header carries its colour (`colorOf`), text and a 2px rule. */
386
386
  const ruleOf = (color) => color ? { boxShadow: `inset 0 -2px 0 ${color}` } : undefined;
387
+ /** The width a table has to fit in: its box, or inside a page's view scroller the view's width. */
388
+ function roomOf(el) {
389
+ const view = el.closest('[data-slot=view-scroll]');
390
+ if (!view)
391
+ return el.clientWidth;
392
+ const cs = getComputedStyle(view);
393
+ return view.clientWidth - Number.parseFloat(cs.paddingLeft) - Number.parseFloat(cs.paddingRight);
394
+ }
387
395
  const alignOf = (format) => (format === 'text' ? 'text-left' : 'text-right');
388
396
  /** A column's cell classes: its alignment, and hidden on phones where the field says so. */
389
397
  /** An ISO date, alone or with a time after it: `2026-10-14`, `2026-10-14 16:17`. */
@@ -483,9 +491,21 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
483
491
  const onCard = shown.filter((f, i) => i > 0 && !f.hideOnPhone);
484
492
  const meta = (onCard.find((f) => f.key === sorting[0]?.id) ?? onCard.find(short) ?? onCard[0])
485
493
  ?.key;
494
+ // Too narrow for every column (a docked drawer beside it, a small window), the table drops its
495
+ // lowest-`priority` columns, right to left, until it fits, instead of scrolling sideways; never
496
+ // its name or sorted column. More room brings them back. A phone keeps its own cards.
497
+ const rank = (f) => (f.priority ?? 0) - (f.hideOnPhone ? 1 : 0);
498
+ const dropOrder = shown
499
+ .slice(1)
500
+ .map((f, i) => ({ f, i }))
501
+ .filter(({ f }) => f.key !== sorting[0]?.id)
502
+ .sort((a, b) => rank(a.f) - rank(b.f) || b.i - a.i)
503
+ .map(({ f }) => f.key);
504
+ const [drop, setDrop] = useState(0);
505
+ const dropped = new Set(isMobile || fit ? [] : dropOrder.slice(0, drop));
486
506
  const cardOf = (f) => f === shown[0] ? 'lead' : f.key === meta ? 'meta' : short(f) ? 'num' : 'text';
487
507
  const col = (f, ...more) => ({
488
- ...colProps(f, ...more),
508
+ ...colProps(f, ...more, f && dropped.has(f.key) && 'hidden'),
489
509
  'data-card': f ? cardOf(f) : undefined,
490
510
  'data-card-unit': f?.cardUnit,
491
511
  });
@@ -778,6 +798,39 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
778
798
  };
779
799
  const box = useRef(null);
780
800
  useFloatingHead(box, !inView);
801
+ // Before paint: one more column drops while the table is wider than its room: its box, and in a
802
+ // page's view scroller the view's width (theme.css `--view-w`).
803
+ useLayoutEffect(() => {
804
+ const el = box.current;
805
+ const t = el?.querySelector('table');
806
+ if (!el || !t || isMobile || fit || drop >= dropOrder.length)
807
+ return;
808
+ if (t.offsetWidth > roomOf(el) + 1)
809
+ setDrop((d) => d + 1);
810
+ });
811
+ // Every column comes back to be measured again on a new set of columns or more room.
812
+ const dropKey = dropOrder.join('\u0000');
813
+ const lastDropKey = useRef(dropKey);
814
+ if (lastDropKey.current !== dropKey) {
815
+ lastDropKey.current = dropKey;
816
+ if (drop)
817
+ setDrop(0);
818
+ }
819
+ useEffect(() => {
820
+ const el = box.current;
821
+ if (!el)
822
+ return;
823
+ const view = el.closest('[data-slot=view-scroll]');
824
+ let room = roomOf(el);
825
+ const watch = new ResizeObserver(() => {
826
+ const now = roomOf(el);
827
+ if (now > room + 1)
828
+ setDrop(0);
829
+ room = now;
830
+ });
831
+ watch.observe(view ?? el);
832
+ return () => watch.disconnect();
833
+ }, []);
781
834
  return (_jsxs("div", { ref: box, className: cn(inView ? IN_VIEW : 'overflow-x-auto overflow-y-hidden [--pin-top:0px]', fit && FIT), children: [cardSort, _jsx(UnitColumns.Provider, { value: markUnits, children: _jsxs(Table, { "data-cards": "", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => (_jsxs(TableRow, { children: [selection && (_jsx(TableHead, { className: cn('w-8 pr-0', PINNED_HEAD), children: _jsx(Checkbox, { checked: allOn ? true : someOn ? 'indeterminate' : false, onCheckedChange: (on) => toggle(allKeys, on === true), "aria-label": "Select every row" }) })), hg.headers.map((h, i) => (_jsxs(TableHead, { ...col(shown[i], PINNED_HEAD), style: ruleOf(colorOf(domain, shown[i])), onPointerDown: pressStart, onPointerUp: pressEnd, onPointerLeave: pressEnd, onPointerCancel: pressEnd, onContextMenu: (e) => {
782
835
  if (press.current)
783
836
  e.preventDefault();
@@ -43,7 +43,7 @@ export function StackedColumns({ rows, x, parts, label, height = 220, tick = Str
43
43
  // The highest part with a value carries the column's rounded cap.
44
44
  const top = (r) => parts.findLast((p) => Number(r[p.key]) > 0)?.key;
45
45
  const ticks = niceTicks(Math.max(0, ...rows.map(total)));
46
- return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ChartContainer, { config: config, role: "img", "aria-label": label, className: "aspect-auto w-full", style: { height }, children: _jsxs(BarChart, { data: rows, margin: { left: 0, right: 0, top: 8, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: x, tickLine: false, axisLine: false, tickMargin: 8, minTickGap: 12, tickFormatter: (v) => tick(String(v)) }), _jsx(YAxis, { tickLine: false, axisLine: false, domain: [0, ticks.at(-1) ?? 'auto'], ticks: ticks, width: axisWidth(ticks.map(fmt)), tickFormatter: (v) => fmt(Number(v)) }), _jsx(ChartTooltip, { cursor: { fill: 'var(--muted)', opacity: 0.5 }, content: _jsx(ChartTooltipContent, { labelFormatter: (_, payload) => {
46
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ChartContainer, { config: config, role: "img", "aria-label": label, className: "aspect-auto w-full", style: { height }, children: _jsxs(BarChart, { data: rows, margin: { left: 0, right: 0, top: 8, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: x, tickLine: false, axisLine: false, tickMargin: 8, minTickGap: 12, tickFormatter: (v) => tick(String(v)) }), _jsx(YAxis, { tickLine: false, axisLine: false, domain: [0, ticks.at(-1) ?? 'auto'], ticks: ticks, width: axisWidth(ticks.map(fmt)), tick: axisTick(fmt) }), _jsx(ChartTooltip, { cursor: { fill: 'var(--muted)', opacity: 0.5 }, content: _jsx(ChartTooltipContent, { labelFormatter: (_, payload) => {
47
47
  const r = payload?.[0]?.payload;
48
48
  return r ? (_jsxs("span", { className: "flex w-full gap-3", children: [_jsx("span", { children: tick(String(r[x])) }), _jsx("span", { className: "num ml-auto", children: fmt(total(r)) })] })) : null;
49
49
  }, formatter: (v, name) => (_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-sm", style: { background: config[String(name)]?.color } }), _jsx("span", { children: config[String(name)]?.label }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(Number(v)) })] })) }) }), parts.map((p) => (_jsx(Bar, { dataKey: p.key, stackId: "a", fill: p.fill, isAnimationActive: false, shape: (b) => (_jsx(Rectangle, { ...b, radius: top(b.payload) === p.key ? CAP : 0 })) }, p.key)))] }) }), legend && (_jsx("ul", { className: "flex flex-wrap justify-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: parts.map((p) => (_jsxs("li", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "size-2 rounded-sm", style: { background: p.fill } }), p.name] }, p.key))) }))] }));
@@ -59,7 +59,7 @@ export function TimeLines({ rows, x, lines, label, height = 220, tick = String,
59
59
  const left = axis(false);
60
60
  const hasRight = lines.some((l) => l.right);
61
61
  const right = hasRight ? axis(true) : [];
62
- return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ChartContainer, { config: config, role: "img", "aria-label": label, className: "aspect-auto w-full", style: { height }, children: _jsxs(LineChart, { data: rows, margin: { left: 0, right: 0, top: 8, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: x, tickLine: false, axisLine: false, tickMargin: 8, minTickGap: 12, tickFormatter: (v) => tick(String(v)) }), _jsx(YAxis, { yAxisId: "left", tickLine: false, axisLine: false, domain: [0, left.at(-1) ?? 'auto'], ticks: left, width: axisWidth(left.map(fmt)), tickFormatter: (v) => fmt(Number(v)) }), hasRight && (_jsx(YAxis, { yAxisId: "right", orientation: "right", tickLine: false, axisLine: false, domain: [0, right.at(-1) ?? 'auto'], ticks: right, width: axisWidth(right.map(fmt)), tickFormatter: (v) => fmt(Number(v)) })), _jsx(ChartTooltip, { cursor: { stroke: 'var(--border)' }, content: _jsx(ChartTooltipContent, { labelFormatter: (_, payload) => {
62
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ChartContainer, { config: config, role: "img", "aria-label": label, className: "aspect-auto w-full", style: { height }, children: _jsxs(LineChart, { data: rows, margin: { left: 0, right: 0, top: 8, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: x, tickLine: false, axisLine: false, tickMargin: 8, minTickGap: 12, tickFormatter: (v) => tick(String(v)) }), _jsx(YAxis, { yAxisId: "left", tickLine: false, axisLine: false, domain: [0, left.at(-1) ?? 'auto'], ticks: left, width: axisWidth(left.map(fmt)), tick: axisTick(fmt) }), hasRight && (_jsx(YAxis, { yAxisId: "right", orientation: "right", tickLine: false, axisLine: false, domain: [0, right.at(-1) ?? 'auto'], ticks: right, width: axisWidth(right.map(fmt)), tick: axisTick(fmt) })), _jsx(ChartTooltip, { cursor: { stroke: 'var(--border)' }, content: _jsx(ChartTooltipContent, { labelFormatter: (_, payload) => {
63
63
  const r = payload?.[0]?.payload;
64
64
  return r ? tick(String(r[x])) : null;
65
65
  }, formatter: (v, name) => (_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-sm", style: { background: config[String(name)]?.color } }), _jsx("span", { children: config[String(name)]?.label }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(Number(v)) })] })) }) }), lines.map((l) => (_jsx(Line, { dataKey: l.key, yAxisId: l.right ? 'right' : 'left', type: "monotone", stroke: l.fill, strokeWidth: 2, dot: false, connectNulls: false, isAnimationActive: false }, l.key)))] }) }), legend && (_jsx("ul", { className: "flex flex-wrap justify-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: lines.map((l) => (_jsxs("li", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "h-0.5 w-3 rounded-full", style: { background: l.fill } }), l.name] }, l.key))) }))] }));
@@ -124,5 +124,10 @@ function niceTicks(max, count = 4) {
124
124
  const n = Math.ceil(max / step - 1e-9);
125
125
  return Array.from({ length: n + 1 }, (_, i) => Math.round(i * step * 1e6) / 1e6);
126
126
  }
127
- /** Room for the widest tick label (about 7px a character at text-xs), so none is clipped. */
128
- const axisWidth = (labels) => Math.max(24, Math.max(0, ...labels.map((l) => l.length)) * 7 + 8);
127
+ /** Room for the widest tick label (about 7.5px a character at text-xs), so none is clipped. */
128
+ const axisWidth = (labels) => Math.max(24, Math.max(0, ...labels.map((l) => l.length)) * 7.5 + 8);
129
+ /**
130
+ * A value axis's tick on one line: the chart's own text breaks a label at its space (`300` over
131
+ * `GB`) when the axis is a hair too narrow, and the top one then runs off the chart.
132
+ */
133
+ const axisTick = (fmt) => ({ x, y, payload, textAnchor }) => (_jsx("text", { x: x, y: y, dy: "0.355em", textAnchor: textAnchor ?? 'end', className: "fill-muted-foreground", style: { whiteSpace: 'pre' }, children: fmt(Number(payload.value)) }));
@@ -139,6 +139,12 @@ export interface ViewField<T> {
139
139
  maxWidth?: string | false;
140
140
  /** Left out on phones, where the table has no room for it (a returns table's longer windows). */
141
141
  hideOnPhone?: boolean;
142
+ /**
143
+ * Where a table has too little room (a docked drawer beside it), its columns drop until it fits:
144
+ * the lowest `priority` first (default 0; `hideOnPhone` ones count one lower), right to left.
145
+ * The name and the sorted column never drop.
146
+ */
147
+ priority?: number;
142
148
  /** Left out of a table when no row in it has a value (none, empty or 0): an Attachments column on mail with none. */
143
149
  hideEmpty?: boolean;
144
150
  /** On a phone card, where no header names it: the word after a bare count (`msgs`, so `5,861 msgs`). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.117",
3
+ "version": "0.1.119",
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.117",
52
+ "@columenlabs/lumen-ui-types": "0.1.119",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",