@columenlabs/lumen-ui 0.1.113 → 0.1.115

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
@@ -78,7 +78,7 @@ Every export, its props and an example are on the [docs](https://lumen.columenla
78
78
  - Numbers use tabular figures and a true minus `−`; a gain or loss always shows a sign or arrow.
79
79
  - A page's header numbers are metric cards: `TileRow` of `Tile` (or `TileToggleGroup` when they pick what the page shows), with `fill` so they share the row, each with `spark` set to its daily history where it has one. Don't build a card of your own for this.
80
80
  - Column widths: the first column (a row's name) grows only to `--lead-col-max` (18rem, theme.css); past it the cell truncates on one line and a plain text value shows in full on hover (a rendered cell brings its own hover card). Cap any other column with the field's `maxWidth` (`'12rem'`), or lift the cap with `maxWidth: false`. In a rendered name cell, put the name in a `truncate` span so the ellipsis lands on it, not on a chip after it.
81
- - Group, sub-group and Total rows read on one line at every width, a phone's cards included: the group's name shortens in an ellipsis (full name on hover) and its roll-up figure stays on the right. The view marks them `data-rollup` and theme.css keeps them to one line, so a `mark`, `rollRender` or `groupActions` an app passes never wraps them.
81
+ - Rows never wrap. Every row (data, group, sub-group, Total) reads on one line at every width. On a phone each row is a one-line card: its name (ellipsis, full name on hover) and one value on the right, the sorted column, else the first number or date. The other values are in the row's drawer or panel. Pick which value a phone shows by sorting (Sort by) or by marking the others `hideOnPhone`.
82
82
  - Column alignment is the table's, not the cell's. Text left, numbers right, and a number column has one right edge for its header, values, dashes and Total. `UnitValue` puts the unit in a fixed slot after the number (`--unit-slot`, 2rem) and its status dot just left of the number. In a LumenView table the first `UnitValue` drawn in a column marks it (`data-units` on every cell of that column, through the `UnitColumns` context and the cell's `data-col`); theme.css then widens that column's right gutter by the slot and hangs each unit in it. So a plain `—`, `Free` or `$0.21` an app draws in the same column sits on the digits' edge, and the header sits over the digits. Don't hand-place units, dots or padding to line a column up; if a column is off, draw its numbers through `UnitValue`. Outside a LumenView table (a hover card, a tile) `UnitValue` keeps its slot inline.
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.
@@ -0,0 +1,23 @@
1
+ /** Focus stays on the trigger: moving it into the pop-up, or back on close, would dismiss a
2
+ * neighbour's pop-up the mouse just opened (Radix closes a pop-up on focus outside). */
3
+ declare const keepFocus: (e: Event) => void;
4
+ /**
5
+ * Open state for a status bar pop-up that opens on hover with a mouse and on tap with a finger or
6
+ * keyboard (a `Popover`'s own trigger). Spread `hover` on the trigger and `content` on the
7
+ * `PopoverContent`.
8
+ */
9
+ export declare function useHoverOpen(): {
10
+ open: boolean;
11
+ setOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
12
+ hover: {
13
+ onPointerEnter: (e: React.PointerEvent) => void;
14
+ onPointerLeave: (e: React.PointerEvent) => void;
15
+ };
16
+ content: {
17
+ onPointerEnter: (e: React.PointerEvent) => void;
18
+ onPointerLeave: (e: React.PointerEvent) => void;
19
+ onOpenAutoFocus: typeof keepFocus;
20
+ onCloseAutoFocus: typeof keepFocus;
21
+ };
22
+ };
23
+ export {};
@@ -0,0 +1,32 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ /** How long a pop-up stays after the mouse leaves, so it can travel from trigger to pop-up. */
3
+ const HOVER_CLOSE_MS = 150;
4
+ /** Focus stays on the trigger: moving it into the pop-up, or back on close, would dismiss a
5
+ * neighbour's pop-up the mouse just opened (Radix closes a pop-up on focus outside). */
6
+ const keepFocus = (e) => e.preventDefault();
7
+ /**
8
+ * Open state for a status bar pop-up that opens on hover with a mouse and on tap with a finger or
9
+ * keyboard (a `Popover`'s own trigger). Spread `hover` on the trigger and `content` on the
10
+ * `PopoverContent`.
11
+ */
12
+ export function useHoverOpen() {
13
+ const [open, setOpen] = useState(false);
14
+ const timer = useRef(undefined);
15
+ useEffect(() => () => clearTimeout(timer.current), []);
16
+ const hover = (on) => (e) => {
17
+ if (e.pointerType !== 'mouse')
18
+ return;
19
+ clearTimeout(timer.current);
20
+ if (on)
21
+ setOpen(true);
22
+ else
23
+ timer.current = setTimeout(() => setOpen(false), HOVER_CLOSE_MS);
24
+ };
25
+ const handlers = { onPointerEnter: hover(true), onPointerLeave: hover(false) };
26
+ return {
27
+ open,
28
+ setOpen,
29
+ hover: handlers,
30
+ content: { ...handlers, onOpenAutoFocus: keepFocus, onCloseAutoFocus: keepFocus },
31
+ };
32
+ }
package/dist/index.d.ts CHANGED
@@ -78,6 +78,7 @@ export { LumenShell, type LumenShellProps, type ShellUser, StatusItem, firstName
78
78
  export { apiJson } from './api-fetch.js';
79
79
  export { cn } from './lib/utils.js';
80
80
  export { type HealthProbe, StatusHealth } from './status-health.js';
81
+ export { type StatusMessage, type StatusMessageTone, StatusMessages, groupStatusMessages, } from './status-messages.js';
81
82
  export { TimeZoneSelect } from './status.js';
82
83
  export { type AccountFacts, type JoinReaders, JoinsProvider, SourceStatusItems, YOURS_COLOR, editedCells, editedKey, frameColor, useFrameEdits, useJoins, usedJoinKeys, } from './joins.js';
83
84
  export { OriginLegend, originsOf } from './origin-legend.js';
package/dist/index.js CHANGED
@@ -85,6 +85,7 @@ export { LumenShell, StatusItem, firstName, initials, maskAmount, useAmountsHidd
85
85
  export { apiJson } from './api-fetch.js';
86
86
  export { cn } from './lib/utils.js';
87
87
  export { StatusHealth } from './status-health.js';
88
+ export { StatusMessages, groupStatusMessages, } from './status-messages.js';
88
89
  export { TimeZoneSelect } from './status.js';
89
90
  export { JoinsProvider, SourceStatusItems, YOURS_COLOR, editedCells, editedKey, frameColor, useFrameEdits, useJoins, usedJoinKeys, } from './joins.js';
90
91
  export { OriginLegend, originsOf } from './origin-legend.js';
@@ -440,12 +440,13 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
440
440
  // Inline chips sit on the first column, next to the row's name, not in their own.
441
441
  const inline = visible.filter((f, i) => i > 0 && chips.some((c) => c.key === f.key && c.placement === 'inline'));
442
442
  const shown = visible.filter((f) => !inline.includes(f));
443
- // On a narrow phone each row reads as a card (theme.css, `data-card`): its name and one figure
444
- // (the sorted one, else the first number or date) on the first line, the rest on the second.
445
- // Numbers and dates keep their width there; only text shortens.
443
+ // On a narrow phone each row reads as a card on ONE line (theme.css, `data-card`): its name and
444
+ // one value (the sorted one, else the first number or date, else the next column). The rest
445
+ // waits in the row's detail, never on a second line.
446
446
  const short = (f) => f.format !== 'text' || (rows[0] !== undefined && isDateText(f.value(rows[0])));
447
- const meta = (shown.find((f, i) => i > 0 && f.key === sorting[0]?.id && short(f)) ??
448
- shown.find((f, i) => i > 0 && short(f)))?.key;
447
+ const onCard = shown.filter((f, i) => i > 0 && !f.hideOnPhone);
448
+ const meta = (onCard.find((f) => f.key === sorting[0]?.id) ?? onCard.find(short) ?? onCard[0])
449
+ ?.key;
449
450
  const cardOf = (f) => f === shown[0] ? 'lead' : f.key === meta ? 'meta' : short(f) ? 'num' : 'text';
450
451
  const col = (f, ...more) => ({
451
452
  ...colProps(f, ...more),
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from 'react';
1
+ import type { ReactNode } from 'react';
2
2
  export interface HealthProbe {
3
3
  name: string;
4
4
  /** Up when true, down when false, not checked yet when null. */
@@ -1,12 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Service health in the status bar ("consolidate into a clickable or hoverable
3
- // pop-up"): one chip with the worst state of every service the app checks, opening a list of each
4
- // service and its latency, on hover with a mouse and on tap with a finger or keyboard.
5
- import { useEffect, useRef, useState } from 'react';
2
+ import { useHoverOpen } from './hooks/use-hover-open.js';
6
3
  import { cn } from './lib/utils.js';
7
4
  import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
8
- /** How long the list stays after the mouse leaves, so it can travel from chip to list. */
9
- const HOVER_CLOSE_MS = 150;
10
5
  /** Keeps the list off the window's edge on a narrow screen. */
11
6
  const COLLISION_PAD = 8;
12
7
  const dot = (ok) => cn('size-2 shrink-0 rounded-full', ok === null ? 'bg-muted-foreground/40' : ok ? 'bg-gain' : 'bg-destructive');
@@ -22,20 +17,9 @@ function summary(probes) {
22
17
  return { ok: true, text: 'All up' };
23
18
  }
24
19
  export function StatusHealth({ probes, checked, error, }) {
25
- const [open, setOpen] = useState(false);
26
- const timer = useRef(undefined);
27
- useEffect(() => () => clearTimeout(timer.current), []);
28
- const hover = (on) => (e) => {
29
- if (e.pointerType !== 'mouse')
30
- return;
31
- clearTimeout(timer.current);
32
- if (on)
33
- setOpen(true);
34
- else
35
- timer.current = setTimeout(() => setOpen(false), HOVER_CLOSE_MS);
36
- };
20
+ const { open, setOpen, hover, content: pop } = useHoverOpen();
37
21
  if (!probes.length && !error)
38
22
  return null;
39
23
  const s = error ? { ok: false, text: 'Check failed' } : summary(probes);
40
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { "aria-label": `Services: ${s.text}`, className: "flex h-6 shrink-0 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground", onPointerEnter: hover(true), onPointerLeave: hover(false), children: [_jsx("span", { "aria-hidden": true, className: dot(s.ok) }), "Services", _jsx("span", { className: cn('num', s.ok === false ? 'text-destructive' : 'text-foreground/80'), children: s.text })] }), _jsxs(PopoverContent, { side: "top", align: "end", collisionPadding: COLLISION_PAD, className: "w-auto min-w-56 gap-1.5 text-xs", onPointerEnter: hover(true), onPointerLeave: hover(false), onOpenAutoFocus: (e) => e.preventDefault(), children: [_jsx("ul", { "aria-label": "Services", className: "flex flex-col gap-1", children: probes.map((p) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": true, className: dot(p.ok) }), _jsxs("span", { className: "flex-1", children: [p.name, p.note && _jsx("span", { className: "ml-1.5 text-muted-foreground", children: p.note })] }), _jsx("span", { className: cn('num', p.ok === false ? 'text-destructive' : 'text-foreground/80'), children: p.ok === false ? 'down' : p.ms != null ? `${p.ms} ms` : '—' })] }, p.name))) }), error && _jsx("p", { className: "text-destructive", children: error }), checked && (_jsxs("p", { className: "border-t pt-1.5 text-muted-foreground", children: ["Checked ", _jsx("span", { className: "num", children: checked })] }))] })] }));
24
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { "aria-label": `Services: ${s.text}`, className: "flex h-6 shrink-0 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground", ...hover, children: [_jsx("span", { "aria-hidden": true, className: dot(s.ok) }), "Services", _jsx("span", { className: cn('num', s.ok === false ? 'text-destructive' : 'text-foreground/80'), children: s.text })] }), _jsxs(PopoverContent, { side: "top", align: "end", collisionPadding: COLLISION_PAD, className: "w-auto min-w-56 gap-1.5 text-xs", ...pop, children: [_jsx("ul", { "aria-label": "Services", className: "flex flex-col gap-1", children: probes.map((p) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": true, className: dot(p.ok) }), _jsxs("span", { className: "flex-1", children: [p.name, p.note && _jsx("span", { className: "ml-1.5 text-muted-foreground", children: p.note })] }), _jsx("span", { className: cn('num', p.ok === false ? 'text-destructive' : 'text-foreground/80'), children: p.ok === false ? 'down' : p.ms != null ? `${p.ms} ms` : '—' })] }, p.name))) }), error && _jsx("p", { className: "text-destructive", children: error }), checked && (_jsxs("p", { className: "border-t pt-1.5 text-muted-foreground", children: ["Checked ", _jsx("span", { className: "num", children: checked })] }))] })] }));
41
25
  }
@@ -0,0 +1,25 @@
1
+ export type StatusMessageTone = 'info' | 'success' | 'warning' | 'error';
2
+ export interface StatusMessage {
3
+ text: string;
4
+ /** Colours the text and its dot: error red, warning amber, success green; info has no dot. */
5
+ tone?: StatusMessageTone;
6
+ /** How many times it was posted, for an app that counts repeats itself; repeats in the list add up too. */
7
+ count?: number;
8
+ }
9
+ interface Group extends Required<StatusMessage> {
10
+ count: number;
11
+ }
12
+ /** One entry per distinct message (same tone, same words), counted, most severe first. */
13
+ export declare function groupStatusMessages(messages: readonly StatusMessage[]): Group[];
14
+ /**
15
+ * The app's messages in the status bar. Each message is one line that ends in an ellipsis past
16
+ * `--status-message-max`, with its full text on hover or tap; a repeated message shows once with
17
+ * `×N`; past `max` messages (one on a phone), a `+N` chip lists the rest.
18
+ */
19
+ export declare function StatusMessages({ messages, max, className, }: {
20
+ messages: readonly StatusMessage[];
21
+ /** How many messages show at once on a wider screen; the rest go behind the `+N` chip. */
22
+ max?: number;
23
+ className?: string;
24
+ }): import("react").JSX.Element | null;
25
+ export {};
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useHoverOpen } from './hooks/use-hover-open.js';
3
+ import { useIsMobile } from './hooks/use-mobile.js';
4
+ import { cn } from './lib/utils.js';
5
+ import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
6
+ /** Errors first, then warnings, so the message that matters most is the one in view. */
7
+ const RANK = { error: 0, warning: 1, success: 2, info: 3 };
8
+ const TEXT = {
9
+ error: 'text-destructive',
10
+ warning: 'text-warning',
11
+ success: undefined,
12
+ info: undefined,
13
+ };
14
+ const DOT = {
15
+ error: 'bg-destructive',
16
+ warning: 'bg-warning',
17
+ success: 'bg-gain',
18
+ info: undefined,
19
+ };
20
+ /** One entry per distinct message (same tone, same words), counted, most severe first. */
21
+ export function groupStatusMessages(messages) {
22
+ const groups = new Map();
23
+ for (const m of messages) {
24
+ const text = m.text.replace(/\s+/g, ' ').trim();
25
+ if (!text)
26
+ continue;
27
+ const tone = m.tone ?? 'info';
28
+ const key = `${tone}\u0000${text}`;
29
+ const count = Math.max(1, m.count ?? 1);
30
+ const g = groups.get(key);
31
+ if (g)
32
+ g.count += count;
33
+ else
34
+ groups.set(key, { text, tone, count });
35
+ }
36
+ // Map keeps first-seen order; the sort is stable, so equal tones keep it.
37
+ return [...groups.values()].sort((a, b) => RANK[a.tone] - RANK[b.tone]);
38
+ }
39
+ const label = (g) => (g.count > 1 ? `${g.text} (×${g.count})` : g.text);
40
+ function Dot({ tone }) {
41
+ const color = DOT[tone];
42
+ return color ? _jsx("span", { "aria-hidden": true, className: cn('size-2 shrink-0 rounded-full', color) }) : null;
43
+ }
44
+ /** Keeps the pop-up off the window's edge on a narrow screen. */
45
+ const COLLISION_PAD = 8;
46
+ /**
47
+ * A status bar trigger whose pop-up opens on hover with a mouse and on tap with a finger or
48
+ * keyboard, like the services chip, so a phone can read the full text too (a tooltip never opens
49
+ * on touch). Styled on its own classes only: a trigger owns `data-state`.
50
+ */
51
+ function Pop({ label: name, className, children, content, }) {
52
+ const { open, setOpen, hover, content: pop } = useHoverOpen();
53
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { "aria-label": name, className: className, ...hover, children: children }), _jsx(PopoverContent, { side: "top", align: "start", collisionPadding: COLLISION_PAD, className: "w-auto max-w-sm gap-1.5 text-xs break-words", ...pop, children: content })] }));
54
+ }
55
+ const TRIGGER = 'flex h-6 items-center gap-1.5 rounded-md px-1.5 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none';
56
+ function Line({ group }) {
57
+ return (_jsxs(Pop, { className: cn(TRIGGER, 'min-w-0 max-w-(--status-message-max)'), content: _jsx("p", { className: TEXT[group.tone], children: label(group) }), children: [_jsx(Dot, { tone: group.tone }), _jsx("span", { className: cn('min-w-0 truncate', TEXT[group.tone]), children: group.text }), group.count > 1 && (_jsxs("span", { className: "num shrink-0 text-foreground/80", "aria-label": `${group.count} times`, children: ["\u00D7", group.count] }))] }));
58
+ }
59
+ /**
60
+ * The app's messages in the status bar. Each message is one line that ends in an ellipsis past
61
+ * `--status-message-max`, with its full text on hover or tap; a repeated message shows once with
62
+ * `×N`; past `max` messages (one on a phone), a `+N` chip lists the rest.
63
+ */
64
+ export function StatusMessages({ messages, max = 2, className, }) {
65
+ const phone = useIsMobile();
66
+ const groups = groupStatusMessages(messages);
67
+ if (!groups.length)
68
+ return null;
69
+ const shown = groups.slice(0, phone ? 1 : Math.max(1, max));
70
+ const rest = groups.slice(shown.length);
71
+ return (_jsxs("span", { "aria-live": "polite", className: cn('flex min-w-0 items-center gap-2', className), children: [shown.map((g) => (_jsx(Line, { group: g }, `${g.tone}:${g.text}`))), rest.length > 0 && (_jsxs(Pop, { label: `${rest.length} more messages`, className: cn(TRIGGER, 'num shrink-0 text-foreground/80'), content: _jsx("ul", { className: "flex flex-col gap-1", children: rest.map((g) => (_jsxs("li", { className: "flex items-start gap-1.5", children: [_jsx("span", { className: "flex h-4 items-center", children: _jsx(Dot, { tone: g.tone }) }), _jsx("span", { className: TEXT[g.tone], children: label(g) })] }, `${g.tone}:${g.text}`))) }), children: ["+", rest.length] }))] }));
72
+ }
package/dist/theme.css CHANGED
@@ -88,6 +88,9 @@
88
88
  --shell-assistant-width: 26rem;
89
89
  --shell-header-height: 48px;
90
90
  --shell-status-height: 32px;
91
+ /* A status bar message ends in an ellipsis here (StatusMessages), so it fits beside the bar's
92
+ other items; the full text opens on hover or tap. A phone shows one message, a wider screen two. */
93
+ --status-message-max: 45vw;
91
94
  --tile-width: 8.5rem;
92
95
  --shell-gutter: 12px;
93
96
  --shell-section-gap: 12px;
@@ -209,6 +212,7 @@
209
212
  @media (min-width: 768px) {
210
213
  :root {
211
214
  --shell-gutter: 16px;
215
+ --status-message-max: min(28rem, 30vw);
212
216
  }
213
217
  }
214
218
 
@@ -439,9 +443,11 @@
439
443
  }
440
444
 
441
445
  /* A LumenView table on a narrow phone (`data-cards`): each row is a card, so nothing runs off
442
- the right edge. Line 1: the row's name (shortens) and one figure (`data-card="meta"`); line 2:
443
- the other values, small and muted. Numbers, dates and their units keep their width; only text
444
- shortens, in an ellipsis. Group, Total and More rows span the card's width. */
446
+ the right edge, and every row is ONE line, whatever an app draws in it: the row's name (shortens
447
+ in an ellipsis), then one figure (`data-card="meta"`, the sorted one, else the first number or
448
+ date) on the right. The other values wait in the row's detail (its drawer or panel), never on a
449
+ second line. Group, fold and Total rows (`data-rollup`) read the same way. More and empty rows
450
+ are one cell across the card. */
445
451
  @media (max-width: 639px) {
446
452
  table[data-cards],
447
453
  table[data-cards] > tbody,
@@ -458,55 +464,46 @@
458
464
  }
459
465
  table[data-cards] tr {
460
466
  display: flex;
461
- flex-wrap: wrap;
462
- align-items: baseline;
467
+ flex-wrap: nowrap;
468
+ align-items: center;
463
469
  column-gap: 0.75rem;
464
- row-gap: 0.125rem;
465
470
  padding: 0.5rem var(--cell-x);
471
+ white-space: nowrap;
466
472
  }
467
473
  table[data-cards] td {
468
474
  padding: 0 !important;
469
475
  width: auto !important;
470
476
  min-width: 0;
477
+ white-space: nowrap;
471
478
  }
472
479
  table[data-cards] td[colspan] {
473
480
  flex: 1 1 100%;
474
481
  }
475
- table[data-cards] td:not([data-card]):not([colspan]) {
482
+ /* A row's panel (`row-panel`) is a box of its own under the row, not a row: it keeps its lines. */
483
+ table[data-cards] tr[data-slot="row-panel"],
484
+ table[data-cards] tr[data-slot="row-panel"] > td {
485
+ white-space: normal;
486
+ }
487
+ table[data-cards] td:not([data-card]):not([colspan]),
488
+ table[data-cards] td:is([data-card="num"], [data-card="text"]) {
476
489
  display: none;
477
490
  }
478
491
  table[data-cards] td[data-card="select"] {
479
492
  order: 0;
480
493
  flex: none;
481
- align-self: center;
482
494
  }
483
495
  table[data-cards] td[data-card="lead"] {
484
496
  order: 1;
485
497
  flex: 1 1 0;
486
498
  overflow: hidden;
499
+ text-overflow: ellipsis;
487
500
  font-weight: 500;
488
501
  }
489
502
  table[data-cards] td[data-card="meta"] {
490
503
  order: 2;
491
504
  flex: none;
492
- }
493
- table[data-cards] tr:has(> td[data-card="lead"])::after {
494
- content: "";
495
- order: 3;
496
- flex-basis: 100%;
497
- }
498
- table[data-cards] td:is([data-card="num"], [data-card="text"]) {
499
- order: 4;
500
- font-size: 0.8125rem;
501
- color: var(--muted-foreground);
502
- }
503
- table[data-cards] td[data-card="num"] {
504
- flex: none;
505
- }
506
- table[data-cards] td[data-card="text"] {
507
- flex: 0 1 auto;
505
+ max-width: 50%;
508
506
  overflow: hidden;
509
- text-overflow: ellipsis;
510
507
  }
511
508
  table[data-cards] [data-slot="unit-value-unit"] {
512
509
  position: static;
@@ -515,31 +512,7 @@
515
512
  margin-left: 0.25em;
516
513
  font-size: inherit;
517
514
  }
518
- /* Group, fold and Total rows (`data-rollup`) are one line at every width: the name shortens
519
- in an ellipsis, the row's one figure keeps its width on the right, and the rest waits in the
520
- open rows. Whatever an app draws in them (a mark, a roll-up, group actions) never wraps.
521
- The group band pins as a whole row here: a cell can't pin inside a one-line flex row. */
522
- table[data-cards] tr[data-rollup] {
523
- flex-wrap: nowrap;
524
- align-items: center;
525
- white-space: nowrap;
526
- }
527
- table[data-cards] tr[data-rollup]::after {
528
- display: none;
529
- }
530
- table[data-cards] tr[data-rollup] > td {
531
- white-space: nowrap;
532
- }
533
- table[data-cards] tr[data-rollup] > td[data-card="lead"] {
534
- flex: 1 1 0;
535
- min-width: 0;
536
- overflow: hidden;
537
- }
538
- table[data-cards]
539
- tr[data-rollup]
540
- > td:is([data-card="num"], [data-card="text"], :not([data-card])) {
541
- display: none;
542
- }
515
+ /* The group band pins as a whole row here: a cell can't pin inside a one-line flex row. */
543
516
  table[data-cards] tr[data-group] {
544
517
  position: sticky;
545
518
  top: var(--pin-top, 0px);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
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.113",
52
+ "@columenlabs/lumen-ui-types": "0.1.115",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",