@columenlabs/lumen-ui 0.1.99 → 0.1.100

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.
@@ -4,7 +4,7 @@ import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, Table
4
4
  export function DetailsTable({ head, rows, foot, selected, onSelect }) {
5
5
  const marked = head.findIndex((h) => h.grow);
6
6
  const grow = marked >= 0 ? marked : head.length - 1;
7
- const cell = (i) => cn('num', head[i]?.right && 'text-right', i !== grow && 'w-px', head[i]?.hideOnPhone && 'max-md:hidden');
7
+ const cell = (i) => cn(head[i]?.right && 'num text-right', i !== grow && 'w-px', head[i]?.hideOnPhone && 'max-md:hidden');
8
8
  return (_jsx("div", { className: "-mx-1 overflow-x-auto overflow-y-hidden rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: head.map((h, i) => (_jsx(TableHead, { className: cn(h.right && 'text-right', i !== grow && 'w-px', h.hideOnPhone && 'max-md:hidden'), children: h.label }, h.label))) }) }), _jsx(TableBody, { children: rows.map((r) => (_jsx(TableRow, { "data-state": r.key === selected ? 'selected' : undefined, "aria-current": r.key === selected || undefined, tabIndex: onSelect ? 0 : undefined, className: cn(onSelect &&
9
9
  'cursor-pointer focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring'), onClick: onSelect && (() => onSelect(r.key)), onKeyDown: onSelect &&
10
10
  ((e) => {
@@ -99,6 +99,28 @@ export function axisStyle(t) {
99
99
  nameTextStyle: { color: t.muted, fontSize: 11 },
100
100
  };
101
101
  }
102
+ /** `over` laid on `under`, plain objects merged key by key: what the chart sets wins. */
103
+ function layer(under, over) {
104
+ const plain = (v) => !!v && typeof v === 'object' && Object.getPrototypeOf(v) === Object.prototype;
105
+ if (!plain(under) || !plain(over))
106
+ return over === undefined ? under : over;
107
+ const out = { ...under };
108
+ for (const [k, v] of Object.entries(over))
109
+ out[k] = layer(under[k], v);
110
+ return out;
111
+ }
112
+ /** `option` with its axes in the theme's colours under whatever the chart sets itself, so a chart
113
+ * that names no axis colours still draws its grid lines faint in dark mode too. */
114
+ function themedAxes(option, t) {
115
+ const style = axisStyle(t);
116
+ const theme = (a) => Array.isArray(a) ? a.map((x) => layer(style, x)) : layer(style, a);
117
+ const o = option;
118
+ return {
119
+ ...option,
120
+ ...(o.xAxis ? { xAxis: theme(o.xAxis) } : {}),
121
+ ...(o.yAxis ? { yAxis: theme(o.yAxis) } : {}),
122
+ };
123
+ }
102
124
  export function LumenEChart({ option, height = 260, on, className, label, }) {
103
125
  const box = useRef(null);
104
126
  const chart = useRef(null);
@@ -132,12 +154,11 @@ export function LumenEChart({ option, height = 260, on, className, label, }) {
132
154
  const tokens = useChartTokens();
133
155
  // An app re-rendering hands a new but equal option (a hover, a refetch): drawing it again
134
156
  // replays the line's entry animation, so the chart blanks under the tooltip. Only a change redraws.
135
- // biome-ignore lint/correctness/useExhaustiveDependencies: tokens change when the theme flips.
136
157
  useEffect(() => {
137
158
  const c = chart.current;
138
159
  if (!ready || !c)
139
160
  return;
140
- const next = resolveVars(option);
161
+ const next = resolveVars(themedAxes(option, tokens));
141
162
  const key = JSON.stringify(next);
142
163
  if (key === drawn.current)
143
164
  return;
@@ -21,6 +21,11 @@ const GUESS = GRID_BLOCK_ROWS * 2;
21
21
  const GridCellContext = createContext(null);
22
22
  export const useGridCell = () => useContext(GridCellContext);
23
23
  const CARD = '[data-slot=lumen-card]';
24
+ /** What a card's part adds to its height: nothing when it floats (a card's corner actions). */
25
+ const flowHeight = (el) => {
26
+ const at = getComputedStyle(el).position;
27
+ return at === 'absolute' || at === 'fixed' ? 0 : el.offsetHeight;
28
+ };
24
29
  const rowsOf = (px) => Math.max(2, Math.ceil((px + GRID_GAP) / GRID_ROW_PX));
25
30
  const byPlace = (a, b) => a.y - b.y || a.x - b.x;
26
31
  /** Cards placed where they say and, in order after them, the ones that do not, row by row. */
@@ -120,7 +125,7 @@ function Cell({ id, fixed, report, children, }) {
120
125
  // filter, body and footer, plus whatever else the wrapper holds.
121
126
  const card = el.matches(CARD) ? el : el.querySelector(`:scope > ${CARD}`);
122
127
  const px = card
123
- ? [...card.children].reduce((s, c) => s + c.offsetHeight, 0) +
128
+ ? [...card.children].reduce((s, c) => s + flowHeight(c), 0) +
124
129
  (card.offsetHeight - card.clientHeight) +
125
130
  (el.offsetHeight - card.offsetHeight)
126
131
  : el.offsetHeight;
package/dist/page.js CHANGED
@@ -31,7 +31,7 @@ export function PageHeader({ crumbs, actions, layout, meta, status }) {
31
31
  label: current ? crumbName(current) : '',
32
32
  data: { page: crumbs.map(crumbName).filter(Boolean).join(' › ') },
33
33
  });
34
- return (_jsxs(_Fragment, { children: [_jsxs("header", { "data-slot": "shell-header", className: "sticky top-0 z-20 flex h-(--shell-header-height) shrink-0 items-center gap-2 overflow-hidden border-b bg-background px-(--shell-gutter)", children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(SidebarTrigger, { className: "-ml-1" }) }), _jsx(TooltipContent, { children: "Toggle sidebar" })] }), _jsx(Separator, { orientation: "vertical", className: "hidden data-vertical:h-4 data-vertical:self-center md:block" }), parent && (_jsx(IconButton, { icon: ChevronLeftIcon, label: `Back to ${crumbName(parent)}`, className: "md:hidden", onClick: () => router.navigate({ to: parent.to }) })), !isMobile && (_jsx(Breadcrumb, { className: "min-w-0", children: _jsx(BreadcrumbList, { className: "flex-nowrap", children: crumbs.map((c, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx(BreadcrumbSeparator, {}), _jsx(BreadcrumbItem, { className: "min-w-0", children: c.to && i < crumbs.length - 1 ? (_jsx(BreadcrumbLink, { asChild: true, children: _jsx(Link, { to: c.to, children: c.label }) })) : i === crumbs.length - 1 ? (_jsx(BreadcrumbPage, { className: "truncate", children: _jsx("h1", { className: "flex min-w-0 items-center gap-2 truncate font-semibold", children: c.label }) })) : (_jsx("span", { className: "truncate", children: c.label })) })] }, `${i}-${crumbName(c)}`))) }) })), isMobile && (_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "flex min-w-0 items-center gap-2 truncate text-sm leading-tight font-semibold", children: current?.label }), meta && (_jsx("div", { className: "flex items-center gap-1.5 truncate text-[11px] leading-tight text-muted-foreground", children: meta }))] })), meta && !isMobile && (_jsx("div", { className: "ml-1 hidden min-w-0 shrink items-center gap-2 truncate border-l pl-3 text-xs text-muted-foreground md:flex", children: meta })), _jsxs("div", { className: cn('ml-auto flex shrink-0 items-center gap-0.5',
34
+ return (_jsxs(_Fragment, { children: [_jsxs("header", { "data-slot": "shell-header", className: "sticky top-0 z-20 flex h-(--shell-header-height) shrink-0 items-center gap-2 overflow-hidden border-b bg-background px-(--shell-gutter)", children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(SidebarTrigger, { className: "-ml-1" }) }), _jsx(TooltipContent, { children: "Toggle sidebar" })] }), _jsx(Separator, { orientation: "vertical", className: "hidden data-vertical:h-4 data-vertical:self-center md:block" }), parent && (_jsx(IconButton, { icon: ChevronLeftIcon, label: `Back to ${crumbName(parent)}`, className: "md:hidden", onClick: () => router.navigate({ to: parent.to }) })), !isMobile && (_jsx(Breadcrumb, { className: "min-w-0", children: _jsx(BreadcrumbList, { className: "flex-nowrap", children: crumbs.map((c, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx(BreadcrumbSeparator, {}), _jsx(BreadcrumbItem, { className: "min-w-0", children: c.to && i < crumbs.length - 1 ? (_jsx(BreadcrumbLink, { asChild: true, children: _jsx(Link, { to: c.to, children: c.label }) })) : i === crumbs.length - 1 ? (_jsx(BreadcrumbPage, { className: "truncate", children: _jsx("h1", { className: "flex min-w-0 items-center gap-2 truncate font-semibold", children: c.label }) })) : (_jsx("span", { className: "truncate", children: c.label })) })] }, `${i}-${crumbName(c)}`))) }) })), isMobile && (_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "flex min-w-0 items-center gap-2 truncate text-sm leading-tight font-semibold", children: current?.label }), meta && (_jsx("div", { className: "flex min-w-0 items-center gap-1.5 text-[11px] leading-tight text-muted-foreground", children: _jsx("span", { className: "truncate", children: meta }) }))] })), meta && !isMobile && (_jsx("div", { className: "ml-1 hidden min-w-0 shrink items-center gap-2 truncate border-l pl-3 text-xs text-muted-foreground md:flex", children: meta })), _jsxs("div", { className: cn('ml-auto flex shrink-0 items-center gap-0.5',
35
35
  // Phone: a page's text buttons show as icons too; their text stays as the accessible name.
36
36
  isMobile &&
37
37
  '[&_[data-slot=button]]:size-8 [&_[data-slot=button]]:gap-0 [&_[data-slot=button]]:px-0 [&_[data-slot=button]]:text-[0px]'), children: [actions && (_jsxs(_Fragment, { children: [_jsx("div", { "data-slot": "shell-page-actions", className: "flex items-center gap-0.5", children: actions }), _jsx(Separator, { orientation: "vertical", className: "mx-2 data-vertical:h-5 data-vertical:self-center" })] })), _jsxs("div", { "data-slot": "shell-actions", className: "flex items-center gap-0.5", children: [layout, _jsx(LumenFinderButton, {}), hideAmounts && (_jsx(IconButton, { icon: hidden ? EyeOffIcon : EyeIcon, label: hidden ? 'Show amounts' : 'Hide amounts', "aria-pressed": hidden, onClick: () => setPref('hideAmounts', !hidden) })), refresh && (_jsx(IconButton, { icon: RefreshCwIcon, busy: refreshing, label: "Refresh (skip cache)", disabled: refreshing, onClick: () => void refresh() })), headerActions] })] })] }), status && statusSlot && createPortal(status, statusSlot)] }));
package/dist/shell.js CHANGED
@@ -143,5 +143,5 @@ export function LumenShell({ app, appLabel, brand, nav, status, health, statusEn
143
143
  }
144
144
  /** A status bar entry: an optional health dot, a label and a mono value. */
145
145
  export function StatusItem({ label, value, ok, className, }) {
146
- return (_jsxs("span", { className: cn('flex items-center gap-1.5', className), children: [ok !== undefined && (_jsx("span", { "aria-hidden": true, className: cn('size-2 rounded-full', ok === null ? 'bg-muted-foreground/40' : ok ? 'bg-gain' : 'bg-destructive') })), label, value !== undefined && (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "num text-foreground/80", children: value })] }))] }));
146
+ return (_jsxs("span", { className: cn('flex items-center gap-1.5', value === undefined && 'min-w-0', className), children: [ok !== undefined && (_jsx("span", { "aria-hidden": true, className: cn('size-2 shrink-0 rounded-full', ok === null ? 'bg-muted-foreground/40' : ok ? 'bg-gain' : 'bg-destructive') })), _jsx("span", { className: value === undefined ? 'min-w-0 truncate' : 'max-md:hidden', children: label }), value !== undefined && (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "num shrink-0 text-foreground/80", children: value })] }))] }));
147
147
  }
@@ -131,7 +131,9 @@ export function SummaryTiles({ domain, tiles, rows, fields, tile, loading, }) {
131
131
  const grid = 'grid grid-cols-2 gap-(--shell-section-gap) md:auto-cols-fr md:grid-flow-col lg:*:*:w-auto';
132
132
  if (loading)
133
133
  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))) }));
134
- return (_jsx("div", { "data-slot": "view-tiles", className: cn(grid, '[&>*:last-child:nth-child(odd)]:col-span-2 md:[&>*:last-child:nth-child(odd)]:col-span-1'), children: shown.map((t) => (_jsx("div", { className: "contents", children: isOwnTile(t) ? (_jsx(OwnTile, { domain: domain, tile: t, rows: rows, fields: fields })) : (tile?.(t.key, rows)) }, t.key))) }));
134
+ return (_jsx("div", { "data-slot": "view-tiles", className: cn(grid,
135
+ // Each tile sits in a `contents` holder, so the odd last one's span goes on what it holds.
136
+ '[&>*:last-child:nth-child(odd)>*]:col-span-2 md:[&>*:last-child:nth-child(odd)>*]:col-span-1'), children: shown.map((t) => (_jsx("div", { className: "contents", children: isOwnTile(t) ? (_jsx(OwnTile, { domain: domain, tile: t, rows: rows, fields: fields })) : (tile?.(t.key, rows)) }, t.key))) }));
135
137
  }
136
138
  // ── Edit ───────────────────────────────────────────────────────────────────────────────────────
137
139
  const dragOptions = { activationConstraint: { distance: 4 } };
package/dist/tile.js CHANGED
@@ -30,7 +30,7 @@ export function TileRow({ children, className, fill, ...props }) {
30
30
  }
31
31
  export function TileToggleGroup({ options, selected, onSelect, label, className, fill, }) {
32
32
  return (_jsx(Card, { size: "sm", className: cn('min-w-0 gap-0 bg-border py-0 lg:w-fit lg:flex-initial', fill &&
33
- 'lg:w-full lg:*:data-[slot=toggle-group]:w-full lg:**:data-[slot=toggle-group-item]:flex-1', className), children: _jsx(ToggleGroup, { type: "single", spacing: 0, value: selected, onValueChange: (v) => v && onSelect(v), "aria-label": label, className: "grid w-full grid-cols-[repeat(auto-fit,minmax(6.5rem,1fr))] gap-px lg:flex lg:w-fit", children: options.map((o) => (_jsx(ToggleGroupItem, { value: o.id, "aria-label": o.label, className: "h-auto min-w-0 flex-col lg:w-(--tile-width) lg:shrink items-start justify-start gap-0.5 rounded-none! bg-card px-3 py-3 text-left font-normal data-[state=on]:bg-primary/5 data-[state=on]:shadow-[inset_0_-2px_0_var(--primary)]", children: _jsx(TileBody, { label: o.label, value: o.value, caption: o.caption, spark: o.spark, sparkColor: o.sparkColor }) }, o.id))) }) }));
33
+ 'lg:w-full lg:*:data-[slot=toggle-group]:w-full lg:**:data-[slot=toggle-group-item]:flex-1', className), children: _jsx(ToggleGroup, { type: "single", spacing: 0, value: selected, onValueChange: (v) => v && onSelect(v), "aria-label": label, className: "grid w-full grid-cols-2 gap-px max-sm:[&>*:last-child:nth-child(odd)]:col-span-2 sm:grid-cols-[repeat(auto-fit,minmax(6.5rem,1fr))] lg:flex lg:w-fit", children: options.map((o) => (_jsx(ToggleGroupItem, { value: o.id, "aria-label": o.label, className: "h-auto min-w-0 flex-col lg:w-(--tile-width) lg:shrink items-start justify-start gap-0.5 rounded-none! bg-card px-3 py-3 text-left font-normal data-[state=on]:bg-primary/5 data-[state=on]:shadow-[inset_0_-2px_0_var(--primary)]", children: _jsx(TileBody, { label: o.label, value: o.value, caption: o.caption, spark: o.spark, sparkColor: o.sparkColor }) }, o.id))) }) }));
34
34
  }
35
35
  /** A compact tile: a small caps label over one short value (a quote strip's IV, expiry and move). */
36
36
  export function TileChip({ label, value }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.99",
3
+ "version": "0.1.100",
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.99",
52
+ "@columenlabs/lumen-ui-types": "0.1.100",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",