@columenlabs/lumen-ui 0.1.46 → 0.1.48

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
@@ -74,6 +74,9 @@ Every export, its props and an example are on the [docs](https://lumen.columenla
74
74
  - Pickers are `LumenSelector`, side panels `LumenDrawer`, tabs `LumenTabs`, destructive confirms `LumenConfirm`; reach for the raw `ui/*` part only when no Lumen part fits.
75
75
  - Every card body sits in `LumenBoundary`, so one crash never blanks a page.
76
76
  - Numbers use tabular figures and a true minus `−`; a gain or loss always shows a sign or arrow.
77
+ - 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.
78
+ - 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.
79
+ - 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.
77
80
  - Bars in a column line up: use `ShareMeter` (fixed track, fixed right-aligned number), never a bar sized by its label.
78
81
  - Table rows are one line. A cell shows one value (with a small bar or tag at most); anything more (other metrics, the allowance, a breakdown) goes in a hover tooltip, never in stacked text or sub-rows.
79
82
  - Chart colours are theme tokens (`var(--chart-1)`, `useChartTokens()`), never hex picked for one theme; `LumenEChart` resolves `var(--…)` in its option so sparklines read in light and dark.
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ export { LumenChoice, type LumenChoiceOption } from './lumen-choice.js';
22
22
  export { LumenSecret } from './lumen-secret.js';
23
23
  export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
24
24
  export { ShareMeter } from './share-meter.js';
25
+ export { UnitValue } from './unit-value.js';
25
26
  export { COVERAGE_LEVELS, CoverageCell, type CoverageJobState, type CoverageLevel, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
26
27
  export { type CoverageBlock, type CoverageData, type CoverageFact, type CoveragePick, type CoverageRow, type CoverageSummary, LumenCoverageMap, type LumenCoverageMapProps, blockName, blockOf, } from './coverage-map.js';
27
28
  export { type CalendarFact, LumenCoverageCalendarMap, type LumenCoverageCalendarMapProps, } from './coverage-calendar-map.js';
@@ -67,7 +68,7 @@ export { OriginLegend, originsOf } from './origin-legend.js';
67
68
  export { type ConnectedDomain, ConnectOffer, connectionFields, linkGroupId } from './connections.js';
68
69
  export { FRESHNESS, type Freshness, FreshnessIcon, FreshnessMark, freshnessLine, freshnessOf, } from './freshness.js';
69
70
  export { SourcesLine, useSourceHealth } from './source-health.js';
70
- export { TILE_WIDTH, Tile, TileChip, type TileOption, TileRow, TileToggleGroup, } from './tile.js';
71
+ export { TILE_WIDTH, Tile, TileChip, type TileOption, TileRow, TileSpark, TileToggleGroup, } from './tile.js';
71
72
  export { type FilterField, type FilterToken, LumenTokenizedFilter, type LumenFilterState, useLumenFilter, } from './lumen-filter.js';
72
73
  export { useIsMobile } from './hooks/use-mobile.js';
73
74
  export { VIEW_ICONS, iconOf, viewIcon } from './view-icons.js';
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ export { LumenChoice } from './lumen-choice.js';
29
29
  export { LumenSecret } from './lumen-secret.js';
30
30
  export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
31
31
  export { ShareMeter } from './share-meter.js';
32
+ export { UnitValue } from './unit-value.js';
32
33
  export { COVERAGE_LEVELS, CoverageCell, CoverageLegend, coverageClass, coverageLabel, coverageLevel, } from './coverage-cells.js';
33
34
  export { LumenCoverageMap, blockName, blockOf, } from './coverage-map.js';
34
35
  export { LumenCoverageCalendarMap, } from './coverage-calendar-map.js';
@@ -74,7 +75,7 @@ export { OriginLegend, originsOf } from './origin-legend.js';
74
75
  export { ConnectOffer, connectionFields, linkGroupId } from './connections.js';
75
76
  export { FRESHNESS, FreshnessIcon, FreshnessMark, freshnessLine, freshnessOf, } from './freshness.js';
76
77
  export { SourcesLine, useSourceHealth } from './source-health.js';
77
- export { TILE_WIDTH, Tile, TileChip, TileRow, TileToggleGroup, } from './tile.js';
78
+ export { TILE_WIDTH, Tile, TileChip, TileRow, TileSpark, TileToggleGroup, } from './tile.js';
78
79
  export { LumenTokenizedFilter, useLumenFilter, } from './lumen-filter.js';
79
80
  export { useIsMobile } from './hooks/use-mobile.js';
80
81
  export { VIEW_ICONS, iconOf, viewIcon } from './view-icons.js';
@@ -367,6 +367,7 @@ function sortingOf(sort, columns, serverSorts) {
367
367
  return columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : [];
368
368
  }
369
369
  function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null, chips = [], page = null, inView = false, fit = false, onDetails, tapped, selection, groupActions, sort, onSort, serverSorts, total = true, subgroup = null, add, serverGroups = null, }) {
370
+ const isMobile = useIsMobile();
370
371
  const [sorting, setSorting] = useState(() => sortingOf(sort, columns, serverSorts));
371
372
  // A saved sort patched while the table is open (the assistant's card move) applies at once.
372
373
  const savedSort = JSON.stringify(sort ?? null);
@@ -555,7 +556,10 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
555
556
  const lead = Math.max(1, shown.findIndex((f, i) => i > 0 && rolls(f)) < 0
556
557
  ? shown.length
557
558
  : shown.findIndex((f, i) => i > 0 && rolls(f)));
558
- const rollCell = (f, 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;
559
+ // On a phone the hideOnPhone columns are gone, so the label spans only the leading ones still
560
+ // drawn; spanning the hidden ones too pushed the group's totals right of their headers.
561
+ const leadSpan = Math.max(1, lead - (isMobile ? shown.slice(0, lead).filter((f) => f.hideOnPhone).length : 0));
562
+ 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;
559
563
  const rollCells = (members) => shown.map((f) => (_jsx(TableCell, { className: colOf(f), children: rollCell(f, members) }, f.key)));
560
564
  // A row's chevron opens one level under it, closed by default: its panel, else the view's
561
565
  // sub-group ("Each row opens"; None opens nothing).
@@ -654,7 +658,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
654
658
  ? true
655
659
  : members.some((m) => selection.keys.has(keyOf(m.original)))
656
660
  ? 'indeterminate'
657
- : false, onCheckedChange: (on) => toggle(members.map((m) => keyOf(m.original)), on === true), "aria-label": `Select every row of ${key}` }) })), shown.slice(lead - 1).map((f, i) => i === 0 ? (_jsxs(TableCell, { colSpan: lead, className: PINNED_GROUP, children: [_jsxs(Button, { variant: "ghost", size: "xs", className: "-mx-2 hover:bg-transparent aria-expanded:bg-transparent", "aria-expanded": open, "aria-label": `${open ? 'Collapse' : 'Expand'} ${key}`, onClick: () => serverGroups
661
+ : false, onCheckedChange: (on) => toggle(members.map((m) => keyOf(m.original)), on === true), "aria-label": `Select every row of ${key}` }) })), shown.slice(lead - 1).map((f, i) => i === 0 ? (_jsxs(TableCell, { colSpan: leadSpan, className: PINNED_GROUP, children: [_jsxs(Button, { variant: "ghost", size: "xs", className: "-mx-2 hover:bg-transparent aria-expanded:bg-transparent", "aria-expanded": open, "aria-label": `${open ? 'Collapse' : 'Expand'} ${key}`, onClick: () => serverGroups
658
662
  ? serverGroups.onToggle(key)
659
663
  : setFlipped((c) => {
660
664
  const n = new Set(c);
@@ -199,6 +199,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
199
199
  }[];
200
200
  formats?: Record<string, "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
201
201
  aggs?: Record<string, "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum"> | undefined;
202
+ shows?: Record<string, string> | undefined;
202
203
  group: {
203
204
  fields: string[];
204
205
  default: string | null;
package/dist/tile.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import type { ReactNode } from 'react';
2
- export declare function Tile({ label, value, caption, lines, className, valueClassName, }: {
2
+ /** A number's history as a small line and area, scaled to its own range; `color` is a theme
3
+ * token (SVG reads CSS variables, so it follows the theme). Fewer than two points draw nothing. */
4
+ export declare function TileSpark({ values, color, className, }: {
5
+ values: number[];
6
+ color?: string;
7
+ className?: string;
8
+ }): import("react").JSX.Element | null;
9
+ export declare function Tile({ label, value, caption, lines, className, valueClassName, spark, sparkColor, }: {
3
10
  label: ReactNode;
4
11
  value: ReactNode;
5
12
  caption?: ReactNode;
@@ -7,13 +14,19 @@ export declare function Tile({ label, value, caption, lines, className, valueCla
7
14
  lines?: ReactNode[];
8
15
  className?: string;
9
16
  valueClassName?: string;
17
+ /** The number's history (oldest first), drawn as a small line under it: a metric card. */
18
+ spark?: number[];
19
+ /** The line's colour, a theme token; left out, `var(--chart-1)`. */
20
+ sparkColor?: string;
10
21
  }): import("react").JSX.Element;
11
22
  /** Width of every header tile on desktop, toggle tiles and plain Tiles alike; shrinks to fit the row. */
12
23
  export declare const TILE_WIDTH = "lg:w-(--tile-width) lg:flex-initial";
13
- /** A page's header row of tiles: wraps on phones, one line on desktop. */
14
- export declare function TileRow({ children, className, ...props }: {
24
+ /** A page's header row of tiles: wraps on phones, one line on desktop. `fill` shares the row's
25
+ * width between its tiles (a row of metric cards) instead of the fixed tile width. */
26
+ export declare function TileRow({ children, className, fill, ...props }: {
15
27
  children: ReactNode;
16
28
  className?: string;
29
+ fill?: boolean;
17
30
  'aria-label'?: string;
18
31
  }): import("react").JSX.Element;
19
32
  export interface TileOption {
@@ -21,14 +34,19 @@ export interface TileOption {
21
34
  label: string;
22
35
  value: ReactNode;
23
36
  caption?: ReactNode;
37
+ /** The option's history, a small line under its number (see `Tile`). */
38
+ spark?: number[];
39
+ sparkColor?: string;
24
40
  }
25
- export declare function TileToggleGroup({ options, selected, onSelect, label, className, }: {
41
+ export declare function TileToggleGroup({ options, selected, onSelect, label, className, fill, }: {
26
42
  options: TileOption[];
27
43
  selected: string;
28
44
  onSelect: (id: string) => void;
29
45
  /** Accessible name of the group. */
30
46
  label: string;
31
47
  className?: string;
48
+ /** Spans the row, its tiles sharing the width (see `TileRow`). */
49
+ fill?: boolean;
32
50
  }): import("react").JSX.Element;
33
51
  /** A compact tile: a small caps label over one short value (a quote strip's IV, expiry and move). */
34
52
  export declare function TileChip({ label, value }: {
package/dist/tile.js CHANGED
@@ -2,20 +2,35 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { cn } from './lib/utils.js';
3
3
  import { Card } from './ui/card.js';
4
4
  import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
5
- function TileBody({ label, value, caption, lines, valueClassName, }) {
6
- return (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-full truncate text-xs text-muted-foreground", children: label }), _jsx("span", { className: cn('num truncate text-xl leading-7 font-semibold', valueClassName), children: value }), caption ? (_jsx("span", { className: "num line-clamp-2 text-xs text-muted-foreground", children: caption })) : null, lines?.map((l, i) => (_jsx("span", { className: "num line-clamp-2 text-xs text-muted-foreground", children: l }, i)))] }));
5
+ /** A number's history as a small line and area, scaled to its own range; `color` is a theme
6
+ * token (SVG reads CSS variables, so it follows the theme). Fewer than two points draw nothing. */
7
+ export function TileSpark({ values, color = 'var(--chart-1)', className, }) {
8
+ if (values.length < 2)
9
+ return null;
10
+ const max = Math.max(...values);
11
+ const min = Math.min(0, ...values);
12
+ const span = max - min || 1;
13
+ const pts = values.map((v, i) => `${((i / (values.length - 1)) * 100).toFixed(2)},${(22 - ((v - min) / span) * 20).toFixed(2)}`);
14
+ return (_jsxs("svg", { viewBox: "0 0 100 24", preserveAspectRatio: "none", "aria-hidden": "true",
15
+ // Inline size: a parent's `[&_svg]:size-4` (toggle items, buttons) must not shrink it.
16
+ style: { width: '100%', height: '1.5rem' }, className: cn('overflow-visible', className), children: [_jsx("polygon", { points: `0,24 ${pts.join(' ')} 100,24`, fill: color, fillOpacity: 0.15 }), _jsx("polyline", { points: pts.join(' '), fill: "none", stroke: color, strokeWidth: 1.5, strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" })] }));
7
17
  }
8
- export function Tile({ label, value, caption, lines, className, valueClassName, }) {
9
- 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 }) }));
18
+ function TileBody({ label, value, caption, lines, valueClassName, spark, sparkColor, }) {
19
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-full truncate text-xs text-muted-foreground", children: label }), _jsx("span", { className: cn('num truncate text-xl leading-7 font-semibold', valueClassName), children: value }), caption ? (_jsx("span", { className: "num line-clamp-2 text-xs text-muted-foreground", 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] }));
20
+ }
21
+ export function Tile({ label, value, caption, lines, className, valueClassName, spark, sparkColor, }) {
22
+ 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 }) }));
10
23
  }
11
24
  /** Width of every header tile on desktop, toggle tiles and plain Tiles alike; shrinks to fit the row. */
12
25
  export const TILE_WIDTH = 'lg:w-(--tile-width) lg:flex-initial';
13
- /** A page's header row of tiles: wraps on phones, one line on desktop. */
14
- export function TileRow({ children, className, ...props }) {
15
- return (_jsx("div", { className: cn('flex flex-wrap gap-2.5 lg:flex-nowrap', className), ...props, children: children }));
26
+ /** A page's header row of tiles: wraps on phones, one line on desktop. `fill` shares the row's
27
+ * width between its tiles (a row of metric cards) instead of the fixed tile width. */
28
+ export function TileRow({ children, className, fill, ...props }) {
29
+ return (_jsx("div", { className: cn('flex flex-wrap gap-2.5 lg:flex-nowrap', fill && 'lg:*:w-auto lg:*:flex-1', className), ...props, children: children }));
16
30
  }
17
- export function TileToggleGroup({ options, selected, onSelect, label, className, }) {
18
- return (_jsx(Card, { size: "sm", className: cn('min-w-0 gap-0 bg-border py-0 lg:w-fit lg:flex-initial', 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 }) }, o.id))) }) }));
31
+ export function TileToggleGroup({ options, selected, onSelect, label, className, fill, }) {
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))) }) }));
19
34
  }
20
35
  /** A compact tile: a small caps label over one short value (a quote strip's IV, expiry and move). */
21
36
  export function TileChip({ label, value }) {
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function UnitValue({ value, unit, tone, title, className, }: {
3
+ /** The number, already formatted (`231.4`). */
4
+ value: ReactNode;
5
+ /** Its unit (`GB`, `M`, `/day`); left out, the slot stays empty so the column still lines up. */
6
+ unit?: ReactNode;
7
+ /** Near (`warning`) or past (`loss`) a limit: tints the number and shows a dot. */
8
+ tone?: 'warning' | 'loss';
9
+ title?: string;
10
+ className?: string;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from './lib/utils.js';
3
+ export function UnitValue({ value, unit, tone, title, className, }) {
4
+ return (_jsxs("span", { title: title, className: cn('inline-flex w-full items-center justify-end gap-1 whitespace-nowrap', className), children: [_jsx("span", { className: cn('num text-right', tone === 'warning' && 'text-warning', tone === 'loss' && 'text-loss'), children: value }), _jsx("span", { className: "w-10 shrink-0 text-left text-muted-foreground text-xs", children: unit }), _jsx("span", { "aria-hidden": "true", className: cn('size-1.5 shrink-0 rounded-full', tone === 'warning' ? 'bg-warning' : tone === 'loss' ? 'bg-loss' : 'bg-transparent') })] }));
5
+ }
@@ -1033,7 +1033,13 @@ function ColumnList({ base, domain, draft, setDraft, shownKeys, hiddenKeys, onEd
1033
1033
  }) }) })) : (_jsx(WhyOff, { note: groupNote(f), children: _jsx(Checkbox, { "aria-label": `Group by ${f.label}`, disabled: true, checked: false }) })), _jsx("span", { className: "max-md:hidden", children: chippable(f) ? (_jsx(LumenSelector, { label: `Show ${f.label} as`, size: "sm", className: "h-7 w-full", options: Object.entries(CHIP_LABELS).map(([v, l]) => ({
1034
1034
  value: v,
1035
1035
  label: v === 'inline' ? `On ${first?.label ?? 'first'}` : l,
1036
- })), value: placement(f.key), onChange: (v) => setPlacement(f.key, v) })) : f.format !== 'text' ? (_jsx(LumenSelector, { label: `Format of ${f.label}`, size: "sm", className: "h-7 w-full", options: formatOptions(), value: f.format, onChange: (v) => setDraft({
1036
+ })), value: placement(f.key), onChange: (v) => setPlacement(f.key, v) })) : f.showAs && f.showAs.length > 1 ? (_jsx(LumenSelector, { label: `Show ${f.label} as`, size: "sm", className: "h-7 w-full", options: f.showAs.map((o) => ({ value: o.id, label: o.label })), value: draft.shows?.[f.key] ?? f.showAs[0]?.id ?? '', onChange: (v) => {
1037
+ const { [f.key]: _, ...rest } = draft.shows ?? {};
1038
+ setDraft({
1039
+ ...draft,
1040
+ shows: v === f.showAs?.[0]?.id ? rest : { ...rest, [f.key]: v },
1041
+ });
1042
+ } })) : f.format !== 'text' ? (_jsx(LumenSelector, { label: `Format of ${f.label}`, size: "sm", className: "h-7 w-full", options: formatOptions(), value: f.format, onChange: (v) => setDraft({
1037
1043
  ...draft,
1038
1044
  formats: {
1039
1045
  ...draft.formats,
@@ -95,6 +95,22 @@ export interface ViewField<T> {
95
95
  /** Draws the cell instead of its formatted value (a sparkline); sorting and filters still use
96
96
  * `value` (strategy lab). */
97
97
  render?: (row: T) => ReactNode;
98
+ /**
99
+ * Draws a group or Total row's cell from its rows instead of the rolled-up number: an amount with
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`).
102
+ */
103
+ rollRender?: (rows: T[]) => ReactNode;
104
+ /**
105
+ * Ways the column can read, picked per column in Edit view ("Show as"), e.g. an amount or its % of
106
+ * an allowance. The first is the field as defined; each other one's `field` replaces those parts
107
+ * of it (value, format, render, aggregate...). The pick saves with the view (`shows`).
108
+ */
109
+ showAs?: {
110
+ id: string;
111
+ label: string;
112
+ field?: Partial<Omit<ViewField<T>, 'key' | 'showAs'>>;
113
+ }[];
98
114
  /** The ticker a click on the cell charts in the app's chart dialog (an option charts its
99
115
  * underlying); null leaves the row's cell plain. */
100
116
  chart?: (row: T) => string | null;
@@ -101,14 +101,22 @@ export function withViewFields(domain, c) {
101
101
  const own = takesFormulas(domain) && formulasOf(c).length ? formulaFields(formulasOf(c)) : [];
102
102
  const picked = c.formats ?? {};
103
103
  const aggs = c.aggs ?? {};
104
- if (!own.length && !Object.keys(picked).length && !Object.keys(aggs).length)
104
+ const shows = c.shows ?? {};
105
+ if (!own.length &&
106
+ !Object.keys(picked).length &&
107
+ !Object.keys(aggs).length &&
108
+ !Object.keys(shows).length)
105
109
  return domain;
106
110
  const mine = new Set(own.map((f) => f.key));
107
111
  // A text formula groups like any text field: each row's value is the formula
108
112
  // over that row alone. One that reads group_sum or total_sum depends on the rows shown, so it
109
113
  // cannot pick its own group.
110
114
  const rowValue = (f) => (r) => computedOf(full, [r], null).value(f, r);
111
- const fields = [...domain.fields, ...own].map((f) => {
115
+ const shown = (f) => {
116
+ const alt = f.showAs?.find((o) => o.id === shows[f.key]);
117
+ return alt?.field ? { ...f, ...alt.field } : f;
118
+ };
119
+ const fields = [...domain.fields, ...own].map(shown).map((f) => {
112
120
  if (f.format === 'text') {
113
121
  if (!mine.has(f.key) || !f.formula)
114
122
  return f;
@@ -97,6 +97,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
97
97
  }[];
98
98
  formats?: Record<string, "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
99
99
  aggs?: Record<string, "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum"> | undefined;
100
+ shows?: Record<string, string> | undefined;
100
101
  group: {
101
102
  fields: string[];
102
103
  default: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
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.46",
44
+ "@columenlabs/lumen-ui-types": "0.1.48",
45
45
  "@dnd-kit/core": "6.3.1",
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@dnd-kit/utilities": "3.2.2",