@columenlabs/lumen-ui 0.1.123 → 0.1.125
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/lumen-layout.d.ts +3 -0
- package/dist/lumen-layout.js +5 -4
- package/dist/lumen-trend.js +24 -9
- package/dist/lumen-view-card.d.ts +3 -1
- package/dist/lumen-view-card.js +2 -2
- package/dist/lumen-view.d.ts +6 -1
- package/dist/lumen-view.js +30 -4
- package/dist/lumen-widget.d.ts +1 -1
- package/dist/theme.css +33 -0
- package/dist/ui/chart.d.ts +5 -1
- package/dist/ui/chart.js +11 -3
- package/dist/widget-settings.d.ts +1 -1
- package/package.json +2 -2
package/dist/lumen-layout.d.ts
CHANGED
|
@@ -110,6 +110,9 @@ export interface LumenLayoutViewProps<T> {
|
|
|
110
110
|
selection?: LumenViewProps<T>['selection'];
|
|
111
111
|
/** A row click opens it (a page of its own) instead of the details drawer. */
|
|
112
112
|
onOpen?: (row: T) => void;
|
|
113
|
+
/** The key of the row `onOpen` shows in a drawer of the app's own (a docked one), so the table
|
|
114
|
+
* marks it selected; the layout's own details drawer marks its row itself. */
|
|
115
|
+
openKey?: string | null;
|
|
113
116
|
/** The `rowLink` value tapped when the page opens, so linked cards start narrowed to one row
|
|
114
117
|
* (a playbook's Results open on its Default variant). */
|
|
115
118
|
rowPick?: string;
|
package/dist/lumen-layout.js
CHANGED
|
@@ -211,12 +211,12 @@ function CardsLayout({ crumbs, actions, meta, status, intro, cards, lead, fit })
|
|
|
211
211
|
* columns, sort and grouping over the view's rows (and formulas), its own chips and tapped row,
|
|
212
212
|
* shared with the cards of its colour like any card. A Columns change saves into its layout.
|
|
213
213
|
*/
|
|
214
|
-
function LayoutTable({ card, base, config, rows, load, state, onDetails, controls, onLayout, }) {
|
|
214
|
+
function LayoutTable({ card, base, config, rows, load, state, onDetails, openKey, controls, onLayout, }) {
|
|
215
215
|
const own = useMemo(() => tableConfigOf(config, { ...card, table: card.table ?? tableLayoutOf(config) }), [config, card]);
|
|
216
216
|
const domain = useMemo(() => withViewFields(base, own), [base, own]);
|
|
217
|
-
return (_jsx(LumenViewCard, { id: card.id, domain: domain, config: own, rows: rows, load: load, state: state, title: card.title ?? config.name, onDetails: onDetails, controls: controls, shown: card.controls, onColumns: onLayout && ((columns) => onLayout(tableLayoutOf({ ...own, columns }))) }));
|
|
217
|
+
return (_jsx(LumenViewCard, { id: card.id, domain: domain, config: own, rows: rows, load: load, state: state, title: card.title ?? config.name, onDetails: onDetails, openKey: openKey, controls: controls, shown: card.controls, onColumns: onLayout && ((columns) => onLayout(tableLayoutOf({ ...own, columns }))) }));
|
|
218
218
|
}
|
|
219
|
-
function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead, domain: base, config, rows, onConfigChange: saveConfig, editable: canEdit, editor, onDuplicate, onDelete, onReset, load, state, tile, rowTitle, viewId, onShownRows, onSummaryRows, selection, onOpen, rowPick: initialPick, groupActions, onTokens, paging, viewTools, columnPicker, hideFilter, save: saveMode = 'document', }) {
|
|
219
|
+
function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead, domain: base, config, rows, onConfigChange: saveConfig, editable: canEdit, editor, onDuplicate, onDelete, onReset, load, state, tile, rowTitle, viewId, onShownRows, onSummaryRows, selection, onOpen, openKey: openedKey, rowPick: initialPick, groupActions, onTokens, paging, viewTools, columnPicker, hideFilter, save: saveMode = 'document', }) {
|
|
220
220
|
const onConfigChange = canEdit === false ? undefined : saveConfig;
|
|
221
221
|
// The registry's join fields for the keys the rows carry (Symbol, Account), then the view's own
|
|
222
222
|
// formulas and formats.
|
|
@@ -260,6 +260,7 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
|
|
|
260
260
|
useEffect(() => onServerSort?.(heldSort ?? null), [onServerSort, heldSort]);
|
|
261
261
|
const [details, setDetails] = useState(null);
|
|
262
262
|
const onDetails = onOpen ?? (domain.details?.length ? setDetails : undefined);
|
|
263
|
+
const openKey = onOpen ? (openedKey ?? null) : details === null ? null : domain.rowKey(details);
|
|
263
264
|
// The assistant sees the view, its data domain, its filter, how many rows show, the first of them and the
|
|
264
265
|
// row open in the drawer (it saw only a row count and could not answer).
|
|
265
266
|
useAssistantNote('view', {
|
|
@@ -551,7 +552,7 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
|
|
|
551
552
|
return {
|
|
552
553
|
...place,
|
|
553
554
|
wide: isTableCard(c) && c.w === GRID_COLS && c.h === undefined,
|
|
554
|
-
node: c.source ? (_jsx(SourcedCard, { card: c, name: config.name, controls: controls })) : isTableCard(c) && c.id !== viewCardId ? (_jsx(LayoutTable, { card: c, base: joins.domain, config: config, rows: c.filter.length ? filterByTokens(rows, widgetFields, c.filter) : rows, load: scope(c.id, load), state: state, onDetails: onDetails, controls: controls, onLayout: onConfigChange ? (table) => putCard({ ...c, table }) : undefined })) : isTableCard(c) ? (_jsx(LumenView, { domain: domain, config: config, rows: viewRows, total: rows.length, paging: paging, groupBy: group.groupField, pivot: pivot, subgroup: group.sub, load: scope(c.id, load), state: state, title: c.title ?? config.name, selection: selection, onDetails: rowPick.onTap ?? onDetails, tapped: rowPick.tapped, groupActions: groupActions, controls: controls, inLayout: true, tools: viewTools, filter: viewFilter, filterCount: filter.tokens.length, onClearFilter: filter.clear, strip: _jsx(ConnectOffer, { domain: domain, rows: rows, config: config, read: joins.read, cardKey: viewId ?? config.name, onConnect: onConfigChange ? connect : undefined }), link: cardLink(c.id, viewFollows, viewColor ? filter.tokens.length : 0), shown: c.controls, onColumns: onConfigChange && columnPicker !== false
|
|
555
|
+
node: c.source ? (_jsx(SourcedCard, { card: c, name: config.name, controls: controls })) : isTableCard(c) && c.id !== viewCardId ? (_jsx(LayoutTable, { card: c, base: joins.domain, config: config, rows: c.filter.length ? filterByTokens(rows, widgetFields, c.filter) : rows, load: scope(c.id, load), state: state, onDetails: onDetails, openKey: openKey, controls: controls, onLayout: onConfigChange ? (table) => putCard({ ...c, table }) : undefined })) : isTableCard(c) ? (_jsx(LumenView, { domain: domain, config: config, rows: viewRows, total: rows.length, paging: paging, groupBy: group.groupField, pivot: pivot, subgroup: group.sub, load: scope(c.id, load), state: state, title: c.title ?? config.name, selection: selection, onDetails: rowPick.onTap ?? onDetails, tapped: rowPick.tapped, openKey: openKey, groupActions: groupActions, controls: controls, inLayout: true, tools: viewTools, filter: viewFilter, filterCount: filter.tokens.length, onClearFilter: filter.clear, strip: _jsx(ConnectOffer, { domain: domain, rows: rows, config: config, read: joins.read, cardKey: viewId ?? config.name, onConnect: onConfigChange ? connect : undefined }), link: cardLink(c.id, viewFollows, viewColor ? filter.tokens.length : 0), shown: c.controls, onColumns: onConfigChange && columnPicker !== false
|
|
555
556
|
? (columns) => onConfigChange({ ...config, columns })
|
|
556
557
|
: undefined, onAddColumn: addColumn, sort: heldSort, onSort: setHeldSort })) : (_jsx(LumenSavedWidget, { widget: c, title: cardTitle(c, config.name), domain: domain, rows: narrow(rows, followed(c.id)), link: cardLink(c.id, followed(c.id)), fields: widgetFields, merge: mergeFlat, onDetails: onDetails, load: scope(c.id, load), tiles: 'tile' in c
|
|
557
558
|
? cardOf(c)
|
package/dist/lumen-trend.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Trend widgets: up to 4 number fields over a date field of the view's rows, rolled up per day, week,
|
|
3
|
-
// month or year, drawn as a line, area, stacked area, bars over time, combo, a spark
|
|
3
|
+
// month or year, drawn as a line, area, stacked area, bars over time, stacked bars, combo, a spark
|
|
4
|
+
// or a tracker.
|
|
4
5
|
// With the allocation, donut, bars and card styles they cover every Tremor visualization, drawn on
|
|
5
6
|
// our own Recharts in ChartContainer. Worked out in the browser from the rows; nothing is fetched.
|
|
6
7
|
import { formatBytes, isDollarFormat, } from '@columenlabs/lumen-ui-types/views';
|
|
7
|
-
import { useMemo } from 'react';
|
|
8
|
+
import { useMemo, useState } from 'react';
|
|
8
9
|
import { Area, AreaChart, Bar, BarChart, CartesianGrid, ComposedChart, Line, LineChart, XAxis, YAxis, } from 'recharts';
|
|
9
10
|
import { cn } from './lib/utils.js';
|
|
10
11
|
import { Headline } from './lumen-widget-charts.js';
|
|
@@ -115,6 +116,8 @@ function tick(at, bucket) {
|
|
|
115
116
|
export function TrendBody({ style, date, values, bucket, agg, rows, read, legend = true, }) {
|
|
116
117
|
const hidden = useAmountsHidden();
|
|
117
118
|
const points = useMemo(() => trendPoints(rows, date, values, bucket, agg, read), [rows, date, values, bucket, agg, read]);
|
|
119
|
+
// Series hidden by a tap on the legend; the last one shown can't be hidden.
|
|
120
|
+
const [off, setOff] = useState([]);
|
|
118
121
|
const first = values[0];
|
|
119
122
|
if (!points.length || !first)
|
|
120
123
|
return _jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No dated rows to draw." });
|
|
@@ -124,6 +127,12 @@ export function TrendBody({ style, date, values, bucket, agg, rows, read, legend
|
|
|
124
127
|
`v${i}`,
|
|
125
128
|
{ label: agg === 'count' ? `${f.label} (count)` : f.label, color: COLORS[i] },
|
|
126
129
|
]));
|
|
130
|
+
const keys = values.map((_, i) => `v${i}`);
|
|
131
|
+
const on = keys.filter((k) => !off.includes(k));
|
|
132
|
+
const toggle = (k) => setOff((o) => o.includes(k) ? o.filter((x) => x !== k) : o.length + 1 < keys.length ? [...o, k] : o);
|
|
133
|
+
// Stacked parts add up: the headline and tooltip read their total.
|
|
134
|
+
const stacks = (style === 'stacked' || style === 'stacked_bars') && on.length > 1;
|
|
135
|
+
const totalOf = (p) => on.reduce((t, k) => t + Number(p?.[k] ?? 0), 0);
|
|
127
136
|
const last = Number(points[points.length - 1]?.v0 ?? 0);
|
|
128
137
|
const start = Number(points[0]?.v0 ?? 0);
|
|
129
138
|
if (style === 'tracker') {
|
|
@@ -143,17 +152,23 @@ export function TrendBody({ style, date, values, bucket, agg, rows, read, legend
|
|
|
143
152
|
_jsx(CartesianGrid, { vertical: false, strokeDasharray: "3 3" }, "g"),
|
|
144
153
|
_jsx(XAxis, { dataKey: "at", tickLine: false, axisLine: false, minTickGap: 24, tickFormatter: (v) => tick(String(v), bucket), fontSize: 11 }, "x"),
|
|
145
154
|
_jsx(YAxis, { tickLine: false, axisLine: false, width: 48, tickFormatter: (v) => fmt(Number(v)), fontSize: 11 }, "y"),
|
|
146
|
-
_jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { labelFormatter: (v) => tick(String(v), bucket), formatter: (v, name) => {
|
|
155
|
+
_jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { labelFormatter: (v) => tick(String(v), bucket), formatter: (v, name, item) => {
|
|
147
156
|
const i = Number(String(name).slice(1));
|
|
148
157
|
const f = values[i] ?? first;
|
|
149
|
-
return (_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: COLORS[i] } }), _jsx("span", { className: "truncate", children: config[String(name)]?.label }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(Number(v), f) })] }));
|
|
158
|
+
return (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: COLORS[i] } }), _jsx("span", { className: "truncate", children: config[String(name)]?.label }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(Number(v), f) })] }), stacks && String(name) === on[on.length - 1] && (_jsxs("span", { className: "flex w-full items-center gap-2 border-t pt-1.5 font-medium", children: [_jsx("span", { className: "size-2 shrink-0" }), _jsx("span", { className: "truncate", children: "Total" }), _jsx("span", { className: "num ml-auto pl-3", children: fmt(totalOf(item.payload), f) })] }))] }));
|
|
150
159
|
} }) }, "t"),
|
|
151
160
|
...(legend && values.length > 1
|
|
152
|
-
? [_jsx(ChartLegend, { content: _jsx(ChartLegendContent, {}) }, "l")]
|
|
161
|
+
? [_jsx(ChartLegend, { content: _jsx(ChartLegendContent, { off: off, onSeries: toggle }) }, "l")]
|
|
153
162
|
: []),
|
|
154
163
|
];
|
|
155
|
-
const
|
|
164
|
+
const end = points[points.length - 1];
|
|
165
|
+
const lead0 = on[0] ?? 'v0';
|
|
166
|
+
const lead = (_jsx(Headline, { value: fmt(stacks ? totalOf(end) : Number(end?.[lead0] ?? 0)), caption: `${stacks ? 'Total' : config[lead0]?.label} · ${tick(String(end?.at), bucket)}` }));
|
|
156
167
|
const box = 'aspect-auto h-56 w-full';
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
// Room above the top gridline, so its label sits on the line rather than below it (a bar near
|
|
169
|
+
// the top read as reaching the label's value).
|
|
170
|
+
const margin = { top: 8, left: 0, right: 4 };
|
|
171
|
+
// A stack's top part rounds its data end; a 2px gap of the card's colour parts the segments.
|
|
172
|
+
const top = on[on.length - 1];
|
|
173
|
+
return (_jsxs("div", { className: "flex flex-col", children: [lead, _jsx(ChartContainer, { config: config, className: box, children: style === 'bars' || style === 'stacked_bars' ? (_jsxs(BarChart, { data: points, margin: margin, children: [axes, keys.map((k) => style === 'stacked_bars' ? (_jsx(Bar, { dataKey: k, hide: off.includes(k), stackId: "a", fill: `var(--color-${k})`, stroke: "var(--card)", strokeWidth: on.length > 1 ? 2 : 0, radius: k === top ? [2, 2, 0, 0] : 0, isAnimationActive: false }, k)) : (_jsx(Bar, { dataKey: k, hide: off.includes(k), fill: `var(--color-${k})`, radius: 2, isAnimationActive: false }, k)))] })) : style === 'combo' ? (_jsxs(ComposedChart, { data: points, margin: margin, children: [axes, keys.map((k, i) => i === 0 ? (_jsx(Bar, { dataKey: k, hide: off.includes(k), fill: `var(--color-${k})`, radius: 2, isAnimationActive: false }, k)) : (_jsx(Line, { dataKey: k, hide: off.includes(k), type: "monotone", stroke: `var(--color-${k})`, strokeWidth: 1.5, dot: false, isAnimationActive: false }, k)))] })) : style === 'area' || style === 'stacked' ? (_jsxs(AreaChart, { data: points, margin: margin, children: [axes, keys.map((k) => (_jsx(Area, { dataKey: k, hide: off.includes(k), type: "monotone", stackId: style === 'stacked' ? 'a' : undefined, stroke: `var(--color-${k})`, fill: `var(--color-${k})`, fillOpacity: 0.15, strokeWidth: 1.5, isAnimationActive: false }, k)))] })) : (_jsxs(LineChart, { data: points, margin: margin, children: [axes, keys.map((k) => (_jsx(Line, { dataKey: k, hide: off.includes(k), type: "monotone", stroke: `var(--color-${k})`, strokeWidth: 1.5, dot: false, isAnimationActive: false }, k)))] })) })] }));
|
|
159
174
|
}
|
|
@@ -22,6 +22,8 @@ export interface LumenViewCardProps<T> {
|
|
|
22
22
|
state?: ReactNode;
|
|
23
23
|
/** A row click (pick a version, open a drawer). */
|
|
24
24
|
onDetails?: (row: T) => void;
|
|
25
|
+
/** The row whose details are open, marked selected (LumenView `openKey`). */
|
|
26
|
+
openKey?: string | null;
|
|
25
27
|
/** Its place in the layout: the layout's `frame` hands these over. */
|
|
26
28
|
controls?: CardControls;
|
|
27
29
|
/** Its saved controls (Edit widget ▸ Controls), on a saved page's table. */
|
|
@@ -32,4 +34,4 @@ export interface LumenViewCardProps<T> {
|
|
|
32
34
|
* header picked (`{key: 'book', value: 'Live'}`), like a code card's `publishes`. */
|
|
33
35
|
publishes?: FilterToken[];
|
|
34
36
|
}
|
|
35
|
-
export declare function LumenViewCard<T extends RowData>({ id, domain: base, config: saved, rows, load, title, tools, note, state, onDetails, controls, shown: savedControls, onColumns, publishes, }: LumenViewCardProps<T>): import("react").JSX.Element;
|
|
37
|
+
export declare function LumenViewCard<T extends RowData>({ id, domain: base, config: saved, rows, load, title, tools, note, state, onDetails, openKey, controls, shown: savedControls, onColumns, publishes, }: LumenViewCardProps<T>): import("react").JSX.Element;
|
package/dist/lumen-view-card.js
CHANGED
|
@@ -8,7 +8,7 @@ import { followedBy, followsText, keyFilterFields, useLinks, usePublishLinks, us
|
|
|
8
8
|
import { LumenView } from './lumen-view.js';
|
|
9
9
|
import { cardFilterFields } from './lumen-widget.js';
|
|
10
10
|
import { filterFieldsOf } from './view-model.js';
|
|
11
|
-
export function LumenViewCard({ id, domain: base, config: saved, rows, load, title, tools, note, state, onDetails, controls, shown: savedControls, onColumns, publishes, }) {
|
|
11
|
+
export function LumenViewCard({ id, domain: base, config: saved, rows, load, title, tools, note, state, onDetails, openKey, controls, shown: savedControls, onColumns, publishes, }) {
|
|
12
12
|
const [config, setConfig] = useState(saved);
|
|
13
13
|
// A saved layout that changes (an edit, the assistant move) is drawn at once.
|
|
14
14
|
const savedKey = JSON.stringify(saved);
|
|
@@ -43,7 +43,7 @@ export function LumenViewCard({ id, domain: base, config: saved, rows, load, tit
|
|
|
43
43
|
linked: color ? filter.tokens.length : 0,
|
|
44
44
|
};
|
|
45
45
|
const on = (k) => controlOn({ kind: 'table', controls: savedControls }, k);
|
|
46
|
-
return (_jsx(LumenView, { domain: domain, config: config, rows: shown, total: rows.length, groupBy: group.groupField, pivot: group.pivot, subgroup: group.sub, load: load, state: state, onDetails: pick.onTap ?? onDetails, tapped: pick.tapped, title: title ?? config.name, inLayout: true, cardId: cardId, controls: controls, tools: tools, note: note, filter: _jsxs(_Fragment, { children: [on('filter') && (_jsx(LumenTokenizedFilter, { filter: filter, label: `Filter ${config.name}`, className: "min-w-0 flex-1", link: link || undefined })), on('group') && group.button] }), filterCount: filter.tokens.length, onClearFilter: filter.clear, link: link || undefined, shown: savedControls, onColumns: (columns) => {
|
|
46
|
+
return (_jsx(LumenView, { domain: domain, config: config, rows: shown, total: rows.length, groupBy: group.groupField, pivot: group.pivot, subgroup: group.sub, load: load, state: state, onDetails: pick.onTap ?? onDetails, tapped: pick.tapped, openKey: openKey, title: title ?? config.name, inLayout: true, cardId: cardId, controls: controls, tools: tools, note: note, filter: _jsxs(_Fragment, { children: [on('filter') && (_jsx(LumenTokenizedFilter, { filter: filter, label: `Filter ${config.name}`, className: "min-w-0 flex-1", link: link || undefined })), on('group') && group.button] }), filterCount: filter.tokens.length, onClearFilter: filter.clear, link: link || undefined, shown: savedControls, onColumns: (columns) => {
|
|
47
47
|
setConfig((c) => ({ ...c, columns }));
|
|
48
48
|
onColumns?.(columns);
|
|
49
49
|
} }));
|
package/dist/lumen-view.d.ts
CHANGED
|
@@ -132,6 +132,10 @@ export interface LumenViewProps<T> {
|
|
|
132
132
|
onDetails?: (row: T) => void;
|
|
133
133
|
/** Rows picked by a tap (a linked table's row filter): a muted fill, no border. */
|
|
134
134
|
tapped?: (row: T) => boolean;
|
|
135
|
+
/** The key of the row whose details are open (a docked drawer beside the table): the row reads
|
|
136
|
+
* as selected (an accent fill and edge bar, `aria-selected`) and scrolls into view when it
|
|
137
|
+
* shows. ↑ and ↓ on a focused row move to the next one, and open it while a row is open. */
|
|
138
|
+
openKey?: string | null;
|
|
135
139
|
/** Buttons at the end of a group row's label (pin a group), given its key and rows. */
|
|
136
140
|
groupActions?: (key: string, rows: T[]) => ReactNode;
|
|
137
141
|
/** Its place in a LumenLayout: the layout's edit state and the card's ⋯ menu. */
|
|
@@ -184,7 +188,7 @@ export interface AddColumn {
|
|
|
184
188
|
* and ⋯, and its own filter and Group (they narrow this table only); its body
|
|
185
189
|
* the table (grouped one level, or the pivot) with the loading states.
|
|
186
190
|
*/
|
|
187
|
-
export declare function LumenView<T extends RowData>({ domain, config, rows, total, paging: server, groupBy: pickedGroup, pivot: pickedPivot, subgroup, load: sourceLoad, state, title, selection, onDetails, tapped, groupActions, controls, inLayout: inScroller, cardId, tools, filter, filterCount, onClearFilter, strip, link, note, onColumns, onAddColumn, sort, onSort, shown, }: LumenViewProps<T>): import("react").JSX.Element;
|
|
191
|
+
export declare function LumenView<T extends RowData>({ domain, config, rows, total, paging: server, groupBy: pickedGroup, pivot: pickedPivot, subgroup, load: sourceLoad, state, title, selection, onDetails, tapped, openKey, groupActions, controls, inLayout: inScroller, cardId, tools, filter, filterCount, onClearFilter, strip, link, note, onColumns, onAddColumn, sort, onSort, shown, }: LumenViewProps<T>): import("react").JSX.Element;
|
|
188
192
|
/** A field's colour for its header: its topic's source (a broker green, an app brown) where
|
|
189
193
|
* the group names one, else its group's (Holding, Symbol, Account, Formulas). */
|
|
190
194
|
export declare function colorOf<T>(domain: ViewDomain<T>, f: ViewField<T> | undefined): string | null;
|
|
@@ -211,6 +215,7 @@ export interface LumenTableProps<T> {
|
|
|
211
215
|
fit?: boolean;
|
|
212
216
|
onDetails?: (row: T) => void;
|
|
213
217
|
tapped?: LumenViewProps<T>['tapped'];
|
|
218
|
+
openKey?: LumenViewProps<T>['openKey'];
|
|
214
219
|
selection?: LumenViewProps<T>['selection'];
|
|
215
220
|
groupActions?: LumenViewProps<T>['groupActions'];
|
|
216
221
|
/** The view's default sort; a header click changes it for the visit. */
|
package/dist/lumen-view.js
CHANGED
|
@@ -224,7 +224,7 @@ async function downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
|
|
|
224
224
|
* and ⋯, and its own filter and Group (they narrow this table only); its body
|
|
225
225
|
* the table (grouped one level, or the pivot) with the loading states.
|
|
226
226
|
*/
|
|
227
|
-
export function LumenView({ domain, config, rows, total = rows.length, paging: server, groupBy: pickedGroup = null, pivot: pickedPivot = false, subgroup, load: sourceLoad, state, title = config.name, selection, onDetails, tapped, groupActions, controls, inLayout: inScroller = false, cardId = 'view', tools, filter, filterCount, onClearFilter, strip, link, note, onColumns, onAddColumn, sort = config.sort, onSort, shown, }) {
|
|
227
|
+
export function LumenView({ domain, config, rows, total = rows.length, paging: server, groupBy: pickedGroup = null, pivot: pickedPivot = false, subgroup, load: sourceLoad, state, title = config.name, selection, onDetails, tapped, openKey, groupActions, controls, inLayout: inScroller = false, cardId = 'view', tools, filter, filterCount, onClearFilter, strip, link, note, onColumns, onAddColumn, sort = config.sort, onSort, shown, }) {
|
|
228
228
|
// Its controls (Edit widget ▸ Controls): Pager, Columns and Download here.
|
|
229
229
|
const on = (k) => controlOn({ kind: 'table', controls: shown }, k);
|
|
230
230
|
const cell = useGridCell();
|
|
@@ -338,7 +338,7 @@ export function LumenView({ domain, config, rows, total = rows.length, paging: s
|
|
|
338
338
|
const pager = turn ? (_jsx(LumenPager, { ...turn })) : served ? (_jsx("span", { className: "num px-2 text-xs text-muted-foreground", children: served.list ? `${served.list.length.toLocaleString()}${served.more ? '+' : ''} groups` : '' })) : (_jsx("span", { className: "num px-2 text-xs text-muted-foreground", children: count }));
|
|
339
339
|
// The page footer (`pagination.footer`): under the table while there is more than one page.
|
|
340
340
|
const foot = turn && pagination.footer && !state && (turn.more || turn.total > turn.size) ? (_jsx(LumenPageFooter, { total: turn.total, page: turn.page, size: turn.size, onPage: turn.onPage, more: turn.more })) : null;
|
|
341
|
-
const table = pivot && config.pivot && !state ? (_jsx(PivotTable, { domain: domain, rows: rows, pivot: config.pivot })) : (_jsx(LumenTable, { domain: domain, columns: columns, rows: state ? [] : tableRows, groupBy: groupBy, subgroup: sub, chips: chips, page: paging ? { index: page, size: pageSize } : null, inView: inLayout, fit: fit, onDetails: onDetails, tapped: tapped, selection: selection, groupActions: groupActions, sort: sort, onSort: sorted, serverSorts: server ? (server.sorts ?? []) : undefined, serverGroups: state ? null : served, emptyState: state, onClearFilter: filtered ? onClearFilter : undefined, total: config.total !== false, add: onAddColumn || onColumns
|
|
341
|
+
const table = pivot && config.pivot && !state ? (_jsx(PivotTable, { domain: domain, rows: rows, pivot: config.pivot })) : (_jsx(LumenTable, { domain: domain, columns: columns, rows: state ? [] : tableRows, groupBy: groupBy, subgroup: sub, chips: chips, page: paging ? { index: page, size: pageSize } : null, inView: inLayout, fit: fit, onDetails: onDetails, tapped: tapped, openKey: openKey, selection: selection, groupActions: groupActions, sort: sort, onSort: sorted, serverSorts: server ? (server.sorts ?? []) : undefined, serverGroups: state ? null : served, emptyState: state, onClearFilter: filtered ? onClearFilter : undefined, total: config.total !== false, add: onAddColumn || onColumns
|
|
342
342
|
? { ...onAddColumn, field: onColumns && { domain, columns, onColumns } }
|
|
343
343
|
: undefined }));
|
|
344
344
|
return (_jsx(LumenWidget, { id: cardId, kind: "table", style: "table", title: title, phoneTitle: false, controls: controls, tools: blank ? (tools) : (_jsxs(_Fragment, { children: [tools, compare, pager, onColumns && !onAddColumn && on('columns') && (_jsx(ColumnPicker, { domain: domain, columns: columns, onColumns: onColumns })), !pivot && on('download') && (_jsx(IconButton, { icon: DownloadIcon, label: "Download CSV",
|
|
@@ -445,7 +445,7 @@ function sortingOf(sort, columns, serverSorts) {
|
|
|
445
445
|
return [];
|
|
446
446
|
return columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : [];
|
|
447
447
|
}
|
|
448
|
-
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, emptyState, onClearFilter, }) {
|
|
448
|
+
function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null, chips = [], page = null, inView = false, fit = false, onDetails, tapped, openKey = null, selection, groupActions, sort, onSort, serverSorts, total = true, subgroup = null, add, serverGroups = null, emptyState, onClearFilter, }) {
|
|
449
449
|
const isMobile = useIsMobile();
|
|
450
450
|
const [sorting, setSorting] = useState(() => sortingOf(sort, columns, serverSorts));
|
|
451
451
|
// A saved sort patched while the table is open (the assistant's card move) applies at once.
|
|
@@ -734,8 +734,30 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
734
734
|
const partRow = (p, name) => (_jsxs(TableRow, { className: cn('bg-muted/30 text-muted-foreground hover:bg-muted', onDetails && 'cursor-pointer'), onClick: onDetails ? () => onDetails(p) : undefined, children: [selection && _jsx(TableCell, {}), shown.map((f, i) => (_jsx(TableCell, { ...col(f, i === 0 && 'pl-12'), children: i === 0 ? (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(CornerDownRightIcon, { className: "size-3.5 shrink-0" }), _jsx(CappedCell, { max: capOf(f, 0), children: name
|
|
735
735
|
? name(p)
|
|
736
736
|
: cellOf(f, p, chips.some((c) => c.key === f.key)) })] })) : (cellOf(f, p, chips.some((c) => c.key === f.key))) }, f.key))), add && _jsx(TableCell, {})] }, `part:${keyOf(p)}`));
|
|
737
|
+
// ↑ and ↓ step to the next data row (past part and panel rows); while a row's details are
|
|
738
|
+
// open, the step opens that row too, so the docked drawer follows the focus.
|
|
739
|
+
const step = (from, down) => {
|
|
740
|
+
let n = down ? from.nextElementSibling : from.previousElementSibling;
|
|
741
|
+
while (n && !(n instanceof HTMLElement && n.dataset.row !== undefined))
|
|
742
|
+
n = down ? n.nextElementSibling : n.previousElementSibling;
|
|
743
|
+
if (!(n instanceof HTMLElement))
|
|
744
|
+
return;
|
|
745
|
+
n.focus();
|
|
746
|
+
const next = openKey === null ? undefined : sorted.find((x) => keyOf(x.original) === n.dataset.row);
|
|
747
|
+
if (next && onDetails)
|
|
748
|
+
onDetails(next.original);
|
|
749
|
+
};
|
|
750
|
+
// The open row scrolls into view when it shows: opened by keyboard, or a page that holds it.
|
|
751
|
+
const pageKeys = sorted.map((r) => keyOf(r.original)).join('\n');
|
|
752
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: scrolls when the open row or the shown rows change, not on every render.
|
|
753
|
+
useEffect(() => {
|
|
754
|
+
if (openKey === null)
|
|
755
|
+
return;
|
|
756
|
+
const el = box.current?.querySelector(`tr[data-row="${CSS.escape(openKey)}"]`);
|
|
757
|
+
el?.scrollIntoView({ block: 'nearest' });
|
|
758
|
+
}, [openKey, pageKeys, page?.index]);
|
|
737
759
|
// The whole row opens the details drawer.
|
|
738
|
-
const dataRow = (r) => (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-row": domain.rowKey(r.original), "data-state": tapped?.(r.original) ? 'selected' : undefined, className: cn('group/row', onDetails && 'cursor-pointer'), tabIndex: onDetails ? 0 : undefined, onClick: onDetails ? () => onDetails(r.original) : undefined, onKeyDown: onDetails
|
|
760
|
+
const dataRow = (r) => (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-row": domain.rowKey(r.original), "data-state": tapped?.(r.original) ? 'selected' : undefined, "aria-selected": openKey === null ? undefined : openKey === keyOf(r.original), className: cn('group/row', onDetails && 'cursor-pointer'), tabIndex: onDetails ? 0 : undefined, onClick: onDetails ? () => onDetails(r.original) : undefined, onKeyDown: onDetails
|
|
739
761
|
? (e) => {
|
|
740
762
|
if (e.target !== e.currentTarget)
|
|
741
763
|
return;
|
|
@@ -743,6 +765,10 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
743
765
|
e.preventDefault();
|
|
744
766
|
onDetails(r.original);
|
|
745
767
|
}
|
|
768
|
+
else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
769
|
+
e.preventDefault();
|
|
770
|
+
step(e.currentTarget, e.key === 'ArrowDown');
|
|
771
|
+
}
|
|
746
772
|
}
|
|
747
773
|
: 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))), add && _jsx(TableCell, {})] }, domain.rowKey(r.original)), openParts.has(keyOf(r.original)) &&
|
|
748
774
|
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: span, 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)));
|
package/dist/lumen-widget.d.ts
CHANGED
|
@@ -690,7 +690,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
|
|
|
690
690
|
bucket: "day" | "month" | "week" | "year";
|
|
691
691
|
agg: "count" | "last" | "mean" | "sum";
|
|
692
692
|
kind: "chart";
|
|
693
|
-
style: "area" | "bars" | "combo" | "line" | "stacked";
|
|
693
|
+
style: "area" | "bars" | "combo" | "line" | "stacked" | "stacked_bars";
|
|
694
694
|
} | {
|
|
695
695
|
id: string;
|
|
696
696
|
title: string | null;
|
package/dist/theme.css
CHANGED
|
@@ -118,6 +118,9 @@
|
|
|
118
118
|
--popover: #fbfcfe;
|
|
119
119
|
--popover-foreground: #121826;
|
|
120
120
|
--primary: #2563eb;
|
|
121
|
+
/* The open row's fill (LumenView `openKey`). */
|
|
122
|
+
--row-open: color-mix(in oklab, var(--primary) 14%, var(--card));
|
|
123
|
+
--row-open-hover: color-mix(in oklab, var(--primary) 20%, var(--card));
|
|
121
124
|
--primary-foreground: #f5f8ff;
|
|
122
125
|
--secondary: #e9edf5;
|
|
123
126
|
--secondary-foreground: #121826;
|
|
@@ -225,6 +228,8 @@
|
|
|
225
228
|
--popover: #121826;
|
|
226
229
|
--popover-foreground: #e6eaf2;
|
|
227
230
|
--primary: #5b8cff;
|
|
231
|
+
--row-open: color-mix(in oklab, var(--primary) 24%, var(--card));
|
|
232
|
+
--row-open-hover: color-mix(in oklab, var(--primary) 30%, var(--card));
|
|
228
233
|
--primary-foreground: #0b0f17;
|
|
229
234
|
--secondary: #1c2435;
|
|
230
235
|
--secondary-foreground: #e6eaf2;
|
|
@@ -535,6 +540,34 @@
|
|
|
535
540
|
}
|
|
536
541
|
}
|
|
537
542
|
|
|
543
|
+
/* The row whose details are open (LumenView `openKey`, a docked drawer beside the table): an
|
|
544
|
+
accent fill and an accent bar on its leading edge, readable in light and dark. It scrolls clear
|
|
545
|
+
of the pinned header and the page footer when it comes into view. */
|
|
546
|
+
tr[aria-selected="true"] {
|
|
547
|
+
scroll-margin-top: calc(var(--pin-top, 0px) + var(--row-h));
|
|
548
|
+
scroll-margin-bottom: var(--row-h);
|
|
549
|
+
}
|
|
550
|
+
tr[aria-selected="true"] > td {
|
|
551
|
+
background-color: var(--row-open);
|
|
552
|
+
}
|
|
553
|
+
tr[aria-selected="true"]:hover > td {
|
|
554
|
+
background-color: var(--row-open-hover);
|
|
555
|
+
}
|
|
556
|
+
tr[aria-selected="true"] > td:first-child {
|
|
557
|
+
box-shadow: inset 3px 0 0 var(--primary);
|
|
558
|
+
}
|
|
559
|
+
/* A phone's card row is one flex box: the fill and bar go on the row itself. */
|
|
560
|
+
@media (max-width: 639px) {
|
|
561
|
+
table[data-cards] tr[aria-selected="true"] {
|
|
562
|
+
background-color: var(--row-open);
|
|
563
|
+
box-shadow: inset 3px 0 0 var(--primary);
|
|
564
|
+
}
|
|
565
|
+
table[data-cards] tr[aria-selected="true"] > td {
|
|
566
|
+
background-color: transparent;
|
|
567
|
+
box-shadow: none;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
538
571
|
@layer utilities {
|
|
539
572
|
.bg-gap-hatch {
|
|
540
573
|
background: repeating-linear-gradient(135deg, var(--gap) 0 3px, var(--gap-soft) 3px 6px);
|
package/dist/ui/chart.d.ts
CHANGED
|
@@ -37,8 +37,12 @@ declare function ChartTooltipContent({ active, payload, className, indicator, hi
|
|
|
37
37
|
labelKey?: string;
|
|
38
38
|
} & Omit<RechartsPrimitive.DefaultTooltipContentProps<TooltipValueType, TooltipNameType>, 'accessibilityLayer'>): React.JSX.Element | null;
|
|
39
39
|
declare const ChartLegend: React.MemoExoticComponent<(outsideProps: RechartsPrimitive.LegendProps) => React.ReactPortal | null>;
|
|
40
|
-
declare function ChartLegendContent({ className, hideIcon, payload, verticalAlign, nameKey, }: React.ComponentProps<'div'> & {
|
|
40
|
+
declare function ChartLegendContent({ className, hideIcon, payload, verticalAlign, nameKey, off, onSeries, }: React.ComponentProps<'div'> & {
|
|
41
41
|
hideIcon?: boolean;
|
|
42
42
|
nameKey?: string;
|
|
43
|
+
/** Series keys hidden by a tap on the legend; they stay listed, dimmed. */
|
|
44
|
+
off?: string[];
|
|
45
|
+
/** Given, each item is a button that shows or hides its series. */
|
|
46
|
+
onSeries?: (key: string) => void;
|
|
43
47
|
} & RechartsPrimitive.DefaultLegendContentProps): React.JSX.Element | null;
|
|
44
48
|
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, };
|
package/dist/ui/chart.js
CHANGED
|
@@ -81,7 +81,7 @@ function ChartTooltipContent({ active, payload, className, indicator = 'dot', hi
|
|
|
81
81
|
}) })] }));
|
|
82
82
|
}
|
|
83
83
|
const ChartLegend = RechartsPrimitive.Legend;
|
|
84
|
-
function ChartLegendContent({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey, }) {
|
|
84
|
+
function ChartLegendContent({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey, off, onSeries, }) {
|
|
85
85
|
const { config } = useChart();
|
|
86
86
|
if (!payload?.length) {
|
|
87
87
|
return null;
|
|
@@ -91,8 +91,16 @@ function ChartLegendContent({ className, hideIcon = false, payload, verticalAlig
|
|
|
91
91
|
.map((item, index) => {
|
|
92
92
|
const key = `${nameKey ?? item.dataKey ?? 'value'}`;
|
|
93
93
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const series = String(item.dataKey ?? key);
|
|
95
|
+
const hidden = off?.includes(series) ?? false;
|
|
96
|
+
const Item = onSeries ? 'button' : 'div';
|
|
97
|
+
return (_jsxs(Item, { title: typeof itemConfig?.label === 'string' ? itemConfig.label : undefined, ...(onSeries && {
|
|
98
|
+
type: 'button',
|
|
99
|
+
'aria-pressed': !hidden,
|
|
100
|
+
onClick: () => onSeries(series),
|
|
101
|
+
}), className: cn('flex max-w-full min-w-0 items-center gap-1.5 sm:max-w-64 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground', onSeries &&
|
|
102
|
+
'min-h-6 cursor-pointer rounded-sm px-1 hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', hidden && 'text-muted-foreground line-through'), children: [itemConfig?.icon && !hideIcon ? (_jsx(itemConfig.icon, {})) : (_jsx("div", { className: "h-2 w-2 shrink-0 rounded-[2px]", style: {
|
|
103
|
+
backgroundColor: hidden ? 'var(--muted-foreground)' : item.color,
|
|
96
104
|
} })), _jsx("span", { className: "truncate", children: itemConfig?.label })] }, index));
|
|
97
105
|
}) }));
|
|
98
106
|
}
|
|
@@ -514,5 +514,5 @@ export declare const readsRows: (card: ViewWidget) => card is {
|
|
|
514
514
|
bucket: "day" | "month" | "week" | "year";
|
|
515
515
|
agg: "count" | "last" | "mean" | "sum";
|
|
516
516
|
kind: "chart";
|
|
517
|
-
style: "area" | "bars" | "combo" | "line" | "stacked";
|
|
517
|
+
style: "area" | "bars" | "combo" | "line" | "stacked" | "stacked_bars";
|
|
518
518
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.125",
|
|
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.
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.125",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|