@columenlabs/lumen-ui 0.1.59 → 0.1.60

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type CustomViewConfig, LumenTable, type LumenTableProps, LumenView, type LumenViewProps, type ServerGroup, type ServerGroups, type ServerPaging, type ViewCellValue, type ViewChipTone, type ViewDomain, type ViewField, type ViewFieldGroup, type ViewSource, CHIP_TONE, RUN_STATE_TONE, FORMULAS_GROUP, ViewValueCell, rollUp, } from './lumen-view.js';
1
+ export { type CustomViewConfig, LumenTable, type LumenTableProps, LumenView, type LumenViewProps, type ServerGroup, type ServerGroups, type ServerPaging, type ViewCellValue, type ViewChipTone, type ViewDomain, type ViewField, type RollLevel, type ViewFieldGroup, type ViewSource, CHIP_TONE, RUN_STATE_TONE, FORMULAS_GROUP, ViewValueCell, rollUp, } from './lumen-view.js';
2
2
  export { LumenLayout, type LumenLayoutCard, type LumenLayoutProps, type LumenLayoutViewProps, domainKeys, followRows, } from './lumen-layout.js';
3
3
  export { LumenViewCard, type LumenViewCardProps } from './lumen-view-card.js';
4
4
  export { LumenSelector, LumenSelectorList, type LumenSelectorListHandle, type LumenSelectorProps, type LumenOption, type LumenOptionGroup, SelectorRowContent, } from './lumen-selector.js';
@@ -4,7 +4,7 @@ import { type ReactNode } from 'react';
4
4
  import { type LumenLoad } from './lumen-source.js';
5
5
  import { type CardControls, type CardLink } from './lumen-widget.js';
6
6
  import { type ViewCellValue, type ViewChipTone, type ViewDomain, type ViewField, type ViewSubgroup } from './view-model.js';
7
- export type { ViewCellValue, ViewChipTone, ViewDomain, ViewField, ViewFieldGroup, ViewSource, } from './view-model.js';
7
+ export type { ViewCellValue, ViewChipTone, ViewDomain, ViewField, RollLevel, ViewFieldGroup, ViewSource, } from './view-model.js';
8
8
  export { CHIP_TONE, FORMULAS_GROUP, RUN_STATE_TONE } from './view-model.js';
9
9
  export { rollUp } from './view-model.js';
10
10
  export type { ViewEditorSection } from './view-editor.js';
@@ -422,10 +422,14 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
422
422
  const col = colPropsOf(units);
423
423
  const [flipped, setFlipped] = useState(new Set());
424
424
  const [more, setMore] = useState(new Map());
425
- // The grouped field is the group row's label, so its column hides.
425
+ // Under a sub-group's `flat` grouping each row names itself by its sub-group key instead of
426
+ // folding; the first column keeps its place to show that name.
427
+ const flat = !!groupBy && !!subgroup?.flat?.includes(groupBy.key);
428
+ // The grouped field is the group row's label, so its column hides (a flat lead stays).
426
429
  // Each key once: two columns under one React key can lose a cell on a re-render, and every
427
430
  // row after it slides left by one.
428
- const visible = columns.filter((f, i) => f.key !== groupBy?.key && columns.findIndex((x) => x.key === f.key) === i);
431
+ const visible = columns.filter((f, i) => (f.key !== groupBy?.key || (flat && i === 0)) &&
432
+ columns.findIndex((x) => x.key === f.key) === i);
429
433
  // Inline chips sit on the first column, next to the row's name, not in their own.
430
434
  const inline = visible.filter((f, i) => i > 0 && chips.some((c) => c.key === f.key && c.placement === 'inline'));
431
435
  const shown = visible.filter((f) => !inline.includes(f));
@@ -506,7 +510,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
506
510
  },
507
511
  sortUndefined: 'last',
508
512
  enableSorting: !serverSorts || serverSorts.includes(f.key),
509
- header: f.label,
513
+ header: i === 0 && flat && subgroup ? subgroup.label : f.label,
510
514
  cell: ({ row }) => {
511
515
  const { shown: now, cellOf, inline, chips, computed } = latest.current;
512
516
  const field = now[i] ?? f;
@@ -519,7 +523,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
519
523
  return [{ key: x.key, node: cellOf(x, row.original, true) }];
520
524
  }) })) : (cellOf(field, row.original, chip)) }));
521
525
  },
522
- })), [shownKeys, inlineOn, serverSorts?.join(',')]);
526
+ })), [shownKeys, inlineOn, serverSorts?.join(','), flat]);
523
527
  const table = useTable({
524
528
  features: SORTING,
525
529
  data: rows,
@@ -601,15 +605,16 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
601
605
  // On a phone the hideOnPhone columns are gone, so the label spans only the leading ones still
602
606
  // drawn; spanning the hidden ones too pushed the group's totals right of their headers.
603
607
  const leadSpan = Math.max(1, lead - (isMobile ? shown.slice(0, lead).filter((f) => f.hideOnPhone).length : 0));
604
- const rollCell = (f, members) => rollsUp(f) && f.rollRender ? (f.rollRender(members)) : rollsUp(f) ? (_jsx(ViewValueCell, { value: computed.roll(f, members), format: f.aggregate && isCountAgg(f.aggregate) ? 'rounded' : f.format, amount: f.aggregate !== 'count' && f.amount, compact: f.compact })) : null;
605
- const rollCells = (members) => shown.map((f) => (_jsx(TableCell, { ...col(f), children: rollCell(f, members) }, f.key)));
608
+ const rollCell = (f, members, level) => rollsUp(f) && f.rollRender ? (f.rollRender(members, level)) : rollsUp(f) ? (_jsx(ViewValueCell, { value: computed.roll(f, members), format: f.aggregate && isCountAgg(f.aggregate) ? 'rounded' : f.format, amount: f.aggregate !== 'count' && f.amount, compact: f.compact })) : null;
609
+ const rollCells = (members) => shown.map((f) => (_jsx(TableCell, { ...col(f), children: rollCell(f, members, 'total') }, f.key)));
606
610
  // A row's chevron opens one level under it, closed by default: its panel, else the view's
607
611
  // sub-group ("Each row opens"; None opens nothing).
608
612
  const partsOf = (row) => (domain.panel ? [] : (subgroup?.rows?.(row) ?? []));
609
613
  const panelOf = (row) => domain.panel?.render(row) ?? null;
610
614
  const opens = (row) => (domain.panel ? panelOf(row) !== null : partsOf(row).length > 1);
611
615
  const openLabel = () => domain.panel?.label ?? subgroup?.label.toLowerCase() ?? 'parts';
612
- const folds = !domain.panel && !!subgroup?.under;
616
+ const folds = !domain.panel && !!subgroup?.under && !flat;
617
+ const flatName = (row) => flat && subgroup?.under ? (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [subgroup.mark?.(subgroup.under(row), [row]), _jsx("span", { className: "truncate", children: subgroup.under(row) })] })) : null;
613
618
  const nests = folds || sorted.some((r) => opens(r.original));
614
619
  const chevron = (k, name) => (_jsx(Button, { variant: "ghost", size: "icon-xs", className: "-ml-1", "aria-expanded": openParts.has(k), "aria-label": `${openParts.has(k) ? 'Hide' : 'Show'} ${name}`, onClick: (e) => {
615
620
  e.stopPropagation();
@@ -632,14 +637,14 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
632
637
  onDetails(r.original);
633
638
  }
634
639
  }
635
- : undefined, children: [selectCell(keyOf(r.original), keyOf(r.original)), r.getAllCells().map((c, i) => (_jsx(TableCell, { ...col(shown[i], groupBy && i === 0 && 'pl-8'), style: { background: shown[i]?.tint?.(r.original) }, children: i === 0 && nests ? (_jsxs("span", { className: "flex items-center gap-1", children: [opens(r.original) ? (chevron(keyOf(r.original), openLabel())) : (_jsx("span", { className: "-ml-1 size-6 shrink-0" })), flexRender(c.column.columnDef.cell, c.getContext())] })) : (flexRender(c.column.columnDef.cell, c.getContext())) }, c.id)))] }, domain.rowKey(r.original)), openParts.has(keyOf(r.original)) &&
640
+ : undefined, children: [selectCell(keyOf(r.original), keyOf(r.original)), r.getAllCells().map((c, i) => (_jsx(TableCell, { ...col(shown[i], groupBy && i === 0 && 'pl-8'), style: { background: shown[i]?.tint?.(r.original) }, children: i === 0 && nests ? (_jsxs("span", { className: "flex items-center gap-1", children: [opens(r.original) ? (chevron(keyOf(r.original), openLabel())) : (_jsx("span", { className: "-ml-1 size-6 shrink-0" })), flexRender(c.column.columnDef.cell, c.getContext())] })) : i === 0 && flat ? (flatName(r.original)) : (flexRender(c.column.columnDef.cell, c.getContext())) }, c.id)))] }, domain.rowKey(r.original)), openParts.has(keyOf(r.original)) &&
636
641
  partsOf(r.original).map((p) => partRow(p, subgroup?.name)), openParts.has(keyOf(r.original)) && domain.panel ? (_jsx(TableRow, { "data-slot": "row-panel", className: "bg-muted/20 hover:bg-muted/20", children: _jsx(TableCell, { colSpan: shown.length + (selection ? 1 : 0), className: "p-0 whitespace-normal", children: _jsx("div", { className: "sticky left-0 w-[min(56rem,calc(100vw-2rem))] max-w-full px-4 py-3 md:pl-10", children: panelOf(r.original) }) }) }, `panel:${keyOf(r.original)}`)) : null] }, domain.rowKey(r.original)));
637
642
  // Rows sharing the sub-group's `under` key fold into one rolled-up line that opens them; a key
638
643
  // with one row keeps that row (NVDA opens Shares, then each option).
639
644
  const foldRow = (key, members, scope) => {
640
645
  const k = `under:${scope}\u0000${key}`;
641
646
  const ms = members.map((m) => m.original);
642
- return (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-row": k, children: [selection && _jsx(TableCell, {}), shown.map((f, i) => (_jsx(TableCell, { ...col(f, groupBy && i === 0 && 'pl-8'), children: i === 0 ? (_jsxs("span", { className: "flex items-center gap-1", children: [chevron(k, `${key} ${openLabel()}`), _jsx("span", { className: "font-medium", children: key }), _jsx("span", { className: "num text-xs text-muted-foreground", children: ms.length })] })) : (rollCell(f, ms)) }, f.key)))] }), openParts.has(k) && ms.map((p) => partRow(p, subgroup?.name))] }, k));
647
+ return (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-row": k, children: [selection && _jsx(TableCell, {}), shown.map((f, i) => (_jsx(TableCell, { ...col(f, groupBy && i === 0 && 'pl-8'), children: i === 0 ? (_jsxs("span", { className: "flex items-center gap-1", children: [chevron(k, `${key} ${openLabel()}`), subgroup?.mark?.(key, ms), _jsx("span", { className: "font-medium", children: key }), _jsx("span", { className: "num text-xs text-muted-foreground", children: ms.length })] })) : (rollCell(f, ms, 'fold')) }, f.key)))] }), openParts.has(k) && ms.map((p) => partRow(p, subgroup?.name))] }, k));
643
648
  };
644
649
  const rowsOf = (list, scope) => {
645
650
  const under = folds ? subgroup?.under : undefined;
@@ -706,10 +711,10 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
706
711
  const n = new Set(c);
707
712
  n.has(key) ? n.delete(key) : n.add(key);
708
713
  return n;
709
- }), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(groupBy.icon, {}), key, _jsx("span", { className: "num font-normal text-muted-foreground", children: serverGroups
714
+ }), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(GroupMark, { field: groupBy, value: key }), key, _jsx("span", { className: "num font-normal text-muted-foreground", children: serverGroups
710
715
  ? (served.get(key)?.n?.toLocaleString() ?? '')
711
716
  : members.length })] }), groupActions?.(key, members.map((m) => m.original))] }, f.key)) : (_jsx(TableCell, { ...col(f, PINNED_GROUP), children: !serverGroups &&
712
- rollCell(f, members.map((m) => m.original)) }, f.key)))] }), open && (serverGroups ? servedRows(members, key) : drawn(members, key))] }, key));
717
+ rollCell(f, members.map((m) => m.original), 'group') }, f.key)))] }), open && (serverGroups ? servedRows(members, key) : drawn(members, key))] }, key));
713
718
  })
714
719
  : drawn(sorted, ''), serverGroups?.more && !serverGroups.pages && (_jsx(TableRow, { "data-more": true, children: _jsx(TableCell, { colSpan: span, className: "py-1", children: _jsxs(Button, { variant: "ghost", size: "xs", onClick: serverGroups.more, children: [_jsx(ChevronsDownIcon, {}), "More groups"] }) }) })), hasTotal && rows.length > 0 && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, children: [selection && _jsx(TableCell, {}), rollCells(rows).map((cell, i) => i === 0 && !(shown[0] && rollsUp(shown[0])) ? (_jsx(TableCell, { children: "Total" }, "total")) : (cell))] }))] })] }) }) }));
715
720
  }
@@ -738,3 +743,14 @@ function PivotTable({ domain, rows, pivot, }) {
738
743
  const amount = pivot.agg !== 'count' && vf.amount;
739
744
  return (_jsx("div", { className: IN_VIEW, "data-testid": "view-pivot", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: PINNED_HEAD, style: ruleOf(colorOf(domain, rf)), children: _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(rf.icon, { className: "size-3.5" }), _jsx("span", { style: { color: colorOf(domain, rf) ?? undefined }, children: rf.label }), " \\", ' ', _jsx("span", { style: { color: colorOf(domain, cf) ?? undefined }, children: cf.label }), _jsxs("span", { className: "font-normal text-muted-foreground", children: ["(", _jsx("span", { style: { color: colorOf(domain, vf) ?? undefined }, children: vf.label }), ",", ' ', pivot.agg, ")"] })] }) }), grid.colKeys.map((c) => (_jsx(TableHead, { className: cn('text-right', PINNED_HEAD), style: ruleOf(colorOf(domain, cf)), children: c }, c))), totals && _jsx(TableHead, { className: cn('text-right', PINNED_HEAD), children: "Total" })] }) }), _jsxs(TableBody, { children: [grid.rowKeys.map((r) => (_jsxs(TableRow, { "data-row": r, children: [_jsx(TableCell, { children: r }), grid.colKeys.map((c) => (_jsx(TableCell, { className: "text-right", children: _jsx(ViewValueCell, { value: grid.cell(r, c), format: fmt, amount: amount }) }, c))), totals && (_jsx(TableCell, { className: cn('text-right', ROLLUP), children: _jsx(ViewValueCell, { value: grid.cell(r, null), format: fmt, amount: amount }) }))] }, r))), totals && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, children: [_jsx(TableCell, { children: "Total" }), grid.colKeys.map((c) => (_jsx(TableCell, { className: "text-right", children: _jsx(ViewValueCell, { value: grid.cell(null, c), format: fmt, amount: amount }) }, c))), _jsx(TableCell, { className: "text-right", children: _jsx(ViewValueCell, { value: grid.cell(null, null), format: fmt, amount: amount }) })] }))] })] }) }));
740
745
  }
746
+ /** A group heading's mark: its value's icon in its colour, else its colour's dot, else the
747
+ * field's icon. */
748
+ function GroupMark({ field, value }) {
749
+ const color = field.color?.(value);
750
+ const Icon = field.iconOf?.(value);
751
+ if (Icon)
752
+ return (_jsx("span", { className: "inline-flex", style: color ? { color } : undefined, children: _jsx(Icon, { className: "size-3.5" }) }));
753
+ if (color)
754
+ return _jsx(ColorDot, { color: color });
755
+ return _jsx(field.icon, {});
756
+ }
@@ -98,9 +98,10 @@ export interface ViewField<T> {
98
98
  /**
99
99
  * Draws a group or Total row's cell from its rows instead of the rolled-up number: an amount with
100
100
  * its unit, or a share worked out again as the rows' total over their allowance (never a sum of
101
- * percents). Only where the field rolls up (`aggregate`).
101
+ * percents). Only where the field rolls up (`aggregate`). `level` says which line it draws: a
102
+ * group heading, a sub-group's folded line or the Total (a split bar on a fold only).
102
103
  */
103
- rollRender?: (rows: T[]) => ReactNode;
104
+ rollRender?: (rows: T[], level: RollLevel) => ReactNode;
104
105
  /**
105
106
  * Ways the column can read, picked per column in Edit view ("Show as"), e.g. an amount or its % of
106
107
  * an allowance. The first is the field as defined; each other one's `field` replaces those parts
@@ -123,7 +124,8 @@ export interface ViewField<T> {
123
124
  * cell, its chip and the edit picker. A CSS colour; a theme token reads in light and dark.
124
125
  */
125
126
  color?: (value: string) => string | undefined;
126
- /** A text value's icon (a label's), drawn in its `color` on its chip and in the edit picker. */
127
+ /** A text value's icon (a label's, a product's), drawn in its `color` on its chip, in the edit
128
+ * picker and on its group's heading. */
127
129
  iconOf?: (value: string) => ComponentType<{
128
130
  className?: string;
129
131
  }> | undefined;
@@ -300,6 +302,8 @@ export interface ViewDomain<T> {
300
302
  export declare const summaryCash: (card: {
301
303
  cash?: boolean;
302
304
  }, narrowed: boolean) => boolean;
305
+ /** The line a `rollRender` draws: a group heading, a sub-group's folded line, the Total. */
306
+ export type RollLevel = 'group' | 'fold' | 'total';
303
307
  /** One choice of a domain's "Each row opens". */
304
308
  export interface ViewSubgroup<T> {
305
309
  key: string;
@@ -312,6 +316,13 @@ export interface ViewSubgroup<T> {
312
316
  always?: boolean;
313
317
  /** Rows start open, their parts showing (a meter's labels); a click still closes one. */
314
318
  open?: boolean;
319
+ /** Drawn before a folded line's key (a meter's product icon, in its colour). */
320
+ mark?: (key: string, rows: T[]) => ReactNode;
321
+ /**
322
+ * Group fields under which rows do not fold: each row is one line named by its `under` key (and
323
+ * `mark`) instead of its first column (grouped by label, a label's meters).
324
+ */
325
+ flat?: string[];
315
326
  }
316
327
  /**
317
328
  * What a row can open: the domain's own sub-groups, else every groupable field as a second level
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "@codemirror/lang-javascript": "6.2.5",
42
42
  "@codemirror/lang-json": "6.0.2",
43
43
  "@codemirror/lang-sql": "6.10.0",
44
- "@columenlabs/lumen-ui-types": "0.1.59",
44
+ "@columenlabs/lumen-ui-types": "0.1.60",
45
45
  "@dnd-kit/core": "6.3.1",
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@dnd-kit/utilities": "3.2.2",