@columenlabs/lumen-ui 0.1.119 → 0.1.120
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-filter.d.ts +6 -1
- package/dist/lumen-filter.js +28 -9
- package/dist/lumen-layout.js +3 -1
- package/dist/lumen-view-card.js +1 -1
- package/dist/lumen-view.d.ts +10 -2
- package/dist/lumen-view.js +11 -7
- package/dist/theme.css +4 -3
- package/package.json +2 -2
package/dist/lumen-filter.d.ts
CHANGED
|
@@ -36,11 +36,14 @@ export declare function filterByTokens<T>(rows: T[], fields: FilterField<T>[], t
|
|
|
36
36
|
/** The row's text, for `contains` tokens. */
|
|
37
37
|
text?: (row: T) => string[]): T[];
|
|
38
38
|
/** Keeps the tokens and text, and filters `rows` by them. */
|
|
39
|
-
export declare function useLumenFilter<T>(rows: T[], fields: FilterField<T>[], { text: textOf, initial, }?: {
|
|
39
|
+
export declare function useLumenFilter<T>(rows: T[], fields: FilterField<T>[], { text: textOf, initial, partial, }?: {
|
|
40
40
|
/** Free-text haystack per row (a name, an id), besides the field values. */
|
|
41
41
|
text?: (row: T) => string[];
|
|
42
42
|
/** Tokens applied when the view opens (a view's default filters). */
|
|
43
43
|
initial?: FilterToken[];
|
|
44
|
+
/** `rows` are one page of a server's rows: a count over them is not the real count, so the
|
|
45
|
+
* menu shows none. */
|
|
46
|
+
partial?: boolean;
|
|
44
47
|
}): {
|
|
45
48
|
fields: FilterField<T>[];
|
|
46
49
|
rows: T[];
|
|
@@ -55,6 +58,8 @@ export declare function useLumenFilter<T>(rows: T[], fields: FilterField<T>[], {
|
|
|
55
58
|
setText: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
56
59
|
results: T[];
|
|
57
60
|
hasText: boolean;
|
|
61
|
+
partial: boolean;
|
|
62
|
+
clear: () => void;
|
|
58
63
|
};
|
|
59
64
|
export type LumenFilterState<T> = ReturnType<typeof useLumenFilter<T>>;
|
|
60
65
|
export declare function LumenTokenizedFilter<T>({ filter, placeholder, label, className, saved, onSave, changed, link: given, }: {
|
package/dist/lumen-filter.js
CHANGED
|
@@ -114,7 +114,7 @@ function onePerField(tokens) {
|
|
|
114
114
|
/** How many values a field lists in the menu before typing narrows it. */
|
|
115
115
|
const PER_FIELD = 8;
|
|
116
116
|
/** Keeps the tokens and text, and filters `rows` by them. */
|
|
117
|
-
export function useLumenFilter(rows, fields, { text: textOf, initial = [], } = {}) {
|
|
117
|
+
export function useLumenFilter(rows, fields, { text: textOf, initial = [], partial = false, } = {}) {
|
|
118
118
|
const [tokens, setRaw] = useState(() => onePerField(initial));
|
|
119
119
|
const setTokens = useCallback((next) => setRaw(onePerField(next)), []);
|
|
120
120
|
const [text, setText] = useState('');
|
|
@@ -146,7 +146,24 @@ export function useLumenFilter(rows, fields, { text: textOf, initial = [], } = {
|
|
|
146
146
|
}, [indexed, fields, tokens, text]);
|
|
147
147
|
/** Whether rows have text of their own, so the box offers "Text contains". */
|
|
148
148
|
const hasText = !!textOf;
|
|
149
|
-
|
|
149
|
+
/** Takes every token and the typed text off. */
|
|
150
|
+
const clear = useCallback(() => {
|
|
151
|
+
setRaw([]);
|
|
152
|
+
setText('');
|
|
153
|
+
}, []);
|
|
154
|
+
return {
|
|
155
|
+
fields,
|
|
156
|
+
rows,
|
|
157
|
+
indexed,
|
|
158
|
+
tokens,
|
|
159
|
+
setTokens,
|
|
160
|
+
text,
|
|
161
|
+
setText,
|
|
162
|
+
results,
|
|
163
|
+
hasText,
|
|
164
|
+
partial,
|
|
165
|
+
clear,
|
|
166
|
+
};
|
|
150
167
|
}
|
|
151
168
|
/** A chip's widest (max-w-60), and the gap between chips (gap-1). */
|
|
152
169
|
const CHIP_MAX = 240;
|
|
@@ -251,10 +268,12 @@ function NumberCondition({ label, current, tall, count, onSet, }) {
|
|
|
251
268
|
} }), op === 'between' && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground text-sm", children: "and" }), _jsx(Input, { type: "number", inputMode: "decimal", className: box, "aria-label": `${label} to`, value: b, onFocus: (e) => e.target.select(), onChange: (e) => {
|
|
252
269
|
setB(e.target.value);
|
|
253
270
|
set(op, a, e.target.value);
|
|
254
|
-
} })] })), _jsx("span", { className: "w-16 shrink-0 text-right text-muted-foreground text-xs tabular-nums", children: current && ok ? `${count(op, value(op, a, b))} rows` : '' })] })] }));
|
|
271
|
+
} })] })), _jsx("span", { className: "w-16 shrink-0 text-right text-muted-foreground text-xs tabular-nums", children: current && ok && count ? `${count(op, value(op, a, b))} rows` : '' })] })] }));
|
|
255
272
|
}
|
|
256
273
|
export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label = 'Filter', className, saved, onSave, changed, link: given, }) {
|
|
257
|
-
const { fields, indexed, tokens, setTokens, text, setText, hasText } = filter;
|
|
274
|
+
const { fields, indexed, tokens, setTokens, text, setText, hasText, partial } = filter;
|
|
275
|
+
// Over one page of a server's rows a count is not the real one: show none.
|
|
276
|
+
const tally = (n) => (partial ? '' : n);
|
|
258
277
|
// A box its card passes no link takes the card's own (every card carries a square).
|
|
259
278
|
const link = useAutoFilterLink(tokens, given);
|
|
260
279
|
// Phone: the box is one tappable row and the menu is a bottom sheet.
|
|
@@ -493,14 +512,14 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
493
512
|
return (_jsxs(CommandItem, { value: `${key}:${value}`, onSelect: () => toggle(key, value), className: tall, children: [_jsx(Checkbox, { checked: on, tabIndex: -1, "aria-hidden": true, className: "pointer-events-none" }), prefix && _jsx("span", { className: "shrink-0 text-muted-foreground", children: prefix }), _jsx("span", { className: "min-w-0 truncate", children: value }), _jsx(Button, { variant: "ghost", size: "xs", className: "-my-1 ml-auto h-6 px-1.5 font-normal text-muted-foreground opacity-0 group-data-selected/command-item:opacity-100 max-md:opacity-100", "aria-label": `Only ${value}`, onMouseDown: (e) => e.preventDefault(), onClick: (e) => {
|
|
494
513
|
e.stopPropagation();
|
|
495
514
|
only(key, value);
|
|
496
|
-
}, children: "Only" }), _jsx(CommandShortcut, { className: cn('ml-0 w-8 text-right', shortcut), children: count })] }, `${key}:${value}`));
|
|
515
|
+
}, children: "Only" }), _jsx(CommandShortcut, { className: cn('ml-0 w-8 text-right', shortcut), children: tally(count) })] }, `${key}:${value}`));
|
|
497
516
|
};
|
|
498
517
|
// A field's "No <field>" row, pinned above its values: the rows with no value (Exclude: any).
|
|
499
518
|
const emptyRow = (f, include) => {
|
|
500
519
|
const n = empties.get(f.key) ?? 0;
|
|
501
520
|
if (!n && !hasEmpty(f.key))
|
|
502
521
|
return null;
|
|
503
|
-
return (_jsxs(CommandItem, { value: `${f.key}:${EMPTY_VALUE}`, onSelect: () => toggleEmpty(f.key, include ? false : undefined), className: tall, children: [_jsx(Checkbox, { checked: hasEmpty(f.key), tabIndex: -1, "aria-hidden": true, className: "pointer-events-none" }), _jsx("span", { className: "min-w-0 truncate text-muted-foreground italic", children: noneOf(f.label) }), _jsx(CommandShortcut, { className: cn('ml-auto w-8 text-right', shortcut), children: n })] }, `${f.key}:${EMPTY_VALUE}`));
|
|
522
|
+
return (_jsxs(CommandItem, { value: `${f.key}:${EMPTY_VALUE}`, onSelect: () => toggleEmpty(f.key, include ? false : undefined), className: tall, children: [_jsx(Checkbox, { checked: hasEmpty(f.key), tabIndex: -1, "aria-hidden": true, className: "pointer-events-none" }), _jsx("span", { className: "min-w-0 truncate text-muted-foreground italic", children: noneOf(f.label) }), _jsx(CommandShortcut, { className: cn('ml-auto w-8 text-right', shortcut), children: tally(n) })] }, `${f.key}:${EMPTY_VALUE}`));
|
|
504
523
|
};
|
|
505
524
|
const summary = (c) => {
|
|
506
525
|
const w = chipWords(c, fieldOf(c.key), 'full');
|
|
@@ -512,7 +531,7 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
512
531
|
setField(null);
|
|
513
532
|
setText('');
|
|
514
533
|
focus();
|
|
515
|
-
}, children: _jsx(ChevronLeftIcon, {}) }), _jsx(activeField.icon, { className: "size-4 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: activeField.label }), !activeField.number && (_jsxs(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, className: "ml-auto", value: notFor(activeField.key) ? 'exclude' : 'include', onValueChange: (v) => v && setNot(activeField.key, v === 'exclude'), "aria-label": `${activeField.label}: include or exclude`, children: [_jsx(ToggleGroupItem, { value: "include", className: "h-7 px-2 text-xs font-normal", children: "Include" }), _jsx(ToggleGroupItem, { value: "exclude", className: "h-7 px-2 text-xs font-normal", children: "Exclude" })] }))] }), activeField.number && (_jsx(NumberCondition, { label: activeField.label, current: tokens.find((t) => t.key === activeField.key), tall: mobile, count: (op, value) => numberCount(activeField.key, op, value), onSet: (op, value) => setCondition(activeField.key, op, value) }, activeField.key))] }), active === CONTAINS_KEY ? (_jsxs(CommandGroup, { className: "px-1 py-0.5", children: [containsWord && !picked(CONTAINS_KEY, containsWord) && (_jsxs(CommandItem, { value: `${CONTAINS_KEY}:${containsWord}`, onSelect: () => toggle(CONTAINS_KEY, containsWord), className: tall, children: [_jsx(TextSearchIcon, { className: "text-muted-foreground" }), _jsxs("span", { className: "min-w-0 truncate", children: ["\u201C", containsWord, "\u201D"] }), _jsx(CommandShortcut, { className: shortcut, children: wordOf(containsWord, notFor(CONTAINS_KEY)) })] })), tokens
|
|
534
|
+
}, children: _jsx(ChevronLeftIcon, {}) }), _jsx(activeField.icon, { className: "size-4 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: activeField.label }), !activeField.number && (_jsxs(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, className: "ml-auto", value: notFor(activeField.key) ? 'exclude' : 'include', onValueChange: (v) => v && setNot(activeField.key, v === 'exclude'), "aria-label": `${activeField.label}: include or exclude`, children: [_jsx(ToggleGroupItem, { value: "include", className: "h-7 px-2 text-xs font-normal", children: "Include" }), _jsx(ToggleGroupItem, { value: "exclude", className: "h-7 px-2 text-xs font-normal", children: "Exclude" })] }))] }), activeField.number && (_jsx(NumberCondition, { label: activeField.label, current: tokens.find((t) => t.key === activeField.key), tall: mobile, count: partial ? undefined : (op, value) => numberCount(activeField.key, op, value), onSet: (op, value) => setCondition(activeField.key, op, value) }, activeField.key))] }), active === CONTAINS_KEY ? (_jsxs(CommandGroup, { className: "px-1 py-0.5", children: [containsWord && !picked(CONTAINS_KEY, containsWord) && (_jsxs(CommandItem, { value: `${CONTAINS_KEY}:${containsWord}`, onSelect: () => toggle(CONTAINS_KEY, containsWord), className: tall, children: [_jsx(TextSearchIcon, { className: "text-muted-foreground" }), _jsxs("span", { className: "min-w-0 truncate", children: ["\u201C", containsWord, "\u201D"] }), _jsx(CommandShortcut, { className: shortcut, children: tally(wordOf(containsWord, notFor(CONTAINS_KEY))) })] })), tokens
|
|
516
535
|
.filter((t) => t.key === CONTAINS_KEY)
|
|
517
536
|
.map((t) => valueRow(CONTAINS_KEY, t.value, wordOf(t.value, !!t.not)))] })) : (!activeField.number && (_jsxs(CommandGroup, { className: "px-1 py-0.5", children: [(!needle || noneOf(activeField.label).toLowerCase().includes(needle)) &&
|
|
518
537
|
emptyRow(activeField), valuesOf(activeField.key, needle, 200).map(([v, n]) => valueRow(activeField.key, v, n))] }))), _jsx(CommandEmpty, { children: "No matching values." })] })) : q ? (_jsxs(_Fragment, { children: [_jsx(CommandEmpty, { children: "No matching values." }), word && (_jsx(CommandGroup, { className: "px-1 py-0.5", children: _jsxs(CommandItem, { value: `${CONTAINS_KEY}:${word}`, onSelect: () => {
|
|
@@ -521,7 +540,7 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
521
540
|
token(CONTAINS_KEY, word, chipOf(CONTAINS_KEY)?.not ?? negate),
|
|
522
541
|
]);
|
|
523
542
|
setText('');
|
|
524
|
-
}, className: tall, children: [_jsx(TextSearchIcon, { className: "text-muted-foreground" }), _jsxs("span", { className: "min-w-0 truncate", children: [_jsxs("span", { className: "text-muted-foreground", children: ["Text ", negate ? 'not contains' : 'contains', ' '] }), "\u201C", word, "\u201D"] }), _jsx(CommandShortcut, { className: shortcut, children: wordOf(word, negate) })] }) })), fields.some((f) => f.label.toLowerCase().startsWith(q) || f.key.startsWith(q)) && (_jsx(CommandGroup, { heading: "Fields", className: "px-1 py-0.5", children: fields
|
|
543
|
+
}, className: tall, children: [_jsx(TextSearchIcon, { className: "text-muted-foreground" }), _jsxs("span", { className: "min-w-0 truncate", children: [_jsxs("span", { className: "text-muted-foreground", children: ["Text ", negate ? 'not contains' : 'contains', ' '] }), "\u201C", word, "\u201D"] }), _jsx(CommandShortcut, { className: shortcut, children: tally(wordOf(word, negate)) })] }) })), fields.some((f) => f.label.toLowerCase().startsWith(q) || f.key.startsWith(q)) && (_jsx(CommandGroup, { heading: "Fields", className: "px-1 py-0.5", children: fields
|
|
525
544
|
.filter((f) => f.label.toLowerCase().startsWith(q) || f.key.startsWith(q))
|
|
526
545
|
.map((f) => (_jsxs(CommandItem, { value: `field:${f.key}`, onSelect: () => openField(f.key), className: tall, children: [_jsx(f.icon, { className: "text-muted-foreground" }), f.label, _jsx(ChevronRightIcon, { className: "ml-auto text-muted-foreground" })] }, f.key))) })), (q.startsWith('no') || (q.length > 1 && 'empty'.startsWith(q))) &&
|
|
527
546
|
fields.some((f) => !f.number &&
|
|
@@ -543,7 +562,7 @@ export function LumenTokenizedFilter({ filter, placeholder = 'Filter…', label
|
|
|
543
562
|
}, children: _jsx(XIcon, {}) })] }, c.key));
|
|
544
563
|
}) })), _jsx(CommandGroup, { heading: chips.length ? 'Add' : undefined, className: "px-1 py-0.5", children: fields
|
|
545
564
|
.filter((f) => !chipOf(f.key))
|
|
546
|
-
.map((f) => (_jsxs(CommandItem, { value: `field:${f.key}`, onSelect: () => openField(f.key), className: tall, children: [_jsx(f.icon, { className: "text-muted-foreground" }), f.label, _jsx(CommandShortcut, { className: shortcut, children: f.number ? '' : (counts.get(f.key)?.size ?? 0) })] }, f.key))) })] }));
|
|
565
|
+
.map((f) => (_jsxs(CommandItem, { value: `field:${f.key}`, onSelect: () => openField(f.key), className: tall, children: [_jsx(f.icon, { className: "text-muted-foreground" }), f.label, _jsx(CommandShortcut, { className: shortcut, children: f.number ? '' : tally(counts.get(f.key)?.size ?? 0) })] }, f.key))) })] }));
|
|
547
566
|
// A field's last row: the card's colour (every chip of a coloured card is shared) and Remove.
|
|
548
567
|
const footer = activeChip && (_jsxs("div", { className: "flex items-center gap-1 border-t px-2 py-1.5", children: [link && (_jsxs("span", { className: "flex items-center gap-1 text-muted-foreground text-xs", children: [_jsx(LinkPicker, { color: link.color, label: activeField?.label ?? activeChip.key, onPick: link.onPick }), link.color
|
|
549
568
|
? `Shared with ${LINK_NAMES[link.color].toLowerCase()} cards`
|
package/dist/lumen-layout.js
CHANGED
|
@@ -228,6 +228,8 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
|
|
|
228
228
|
const filter = useLumenFilter(rows, filterFields, {
|
|
229
229
|
text: domain.text,
|
|
230
230
|
initial: config.filters.default,
|
|
231
|
+
// A server's page is not every row: the menu's counts would be over that page only.
|
|
232
|
+
partial: !!paging && (paging.more || paging.page > 0 || (paging.total ?? 0) > rows.length),
|
|
231
233
|
});
|
|
232
234
|
// A view patched while open (the assistant's card move, a save from another tab) applies its new saved
|
|
233
235
|
// filters at once; the person's own unsaved tokens stay while the saved ones
|
|
@@ -549,7 +551,7 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
|
|
|
549
551
|
return {
|
|
550
552
|
...place,
|
|
551
553
|
wide: isTableCard(c) && c.w === GRID_COLS && c.h === undefined,
|
|
552
|
-
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, 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
|
|
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
|
|
553
555
|
? (columns) => onConfigChange({ ...config, columns })
|
|
554
556
|
: 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
|
|
555
557
|
? cardOf(c)
|
package/dist/lumen-view-card.js
CHANGED
|
@@ -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, 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, 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
|
@@ -116,7 +116,9 @@ export interface LumenViewProps<T> {
|
|
|
116
116
|
/** The rows' loading state (`useLumenLoad`): the view shows its skeleton, progress bar, ↻, a
|
|
117
117
|
* failure with Retry and, with Observability details on, its load time. */
|
|
118
118
|
load?: LumenLoad;
|
|
119
|
-
/** Shown in place of the table instead of the rows, e.g. an empty state ("No accounts yet").
|
|
119
|
+
/** Shown in place of the table instead of the rows, e.g. an empty state ("No accounts yet").
|
|
120
|
+
* While a filter is on it shows in the table's body instead, under the headers, so the
|
|
121
|
+
* filter stays in reach. */
|
|
120
122
|
state?: ReactNode;
|
|
121
123
|
/** The card's title; left out: the view's name. */
|
|
122
124
|
title?: string;
|
|
@@ -144,6 +146,8 @@ export interface LumenViewProps<T> {
|
|
|
144
146
|
* not the page's), and how many tokens it holds. */
|
|
145
147
|
filter?: ReactNode;
|
|
146
148
|
filterCount?: number;
|
|
149
|
+
/** Takes every filter off: an empty table under a filter offers Clear filters. */
|
|
150
|
+
onClearFilter?: () => void;
|
|
147
151
|
/** A line under the filter: the card's Link offer. */
|
|
148
152
|
strip?: ReactNode;
|
|
149
153
|
/** Its link colour on the layout. */
|
|
@@ -180,7 +184,7 @@ export interface AddColumn {
|
|
|
180
184
|
* and ⋯, and its own filter and Group (they narrow this table only); its body
|
|
181
185
|
* the table (grouped one level, or the pivot) with the loading states.
|
|
182
186
|
*/
|
|
183
|
-
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, strip, link, note, onColumns, onAddColumn, sort, onSort, shown, }: LumenViewProps<T>): import("react").JSX.Element;
|
|
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;
|
|
184
188
|
/** A field's colour for its header: its topic's source (a broker green, an app brown) where
|
|
185
189
|
* the group names one, else its group's (Holding, Symbol, Account, Formulas). */
|
|
186
190
|
export declare function colorOf<T>(domain: ViewDomain<T>, f: ViewField<T> | undefined): string | null;
|
|
@@ -231,4 +235,8 @@ export interface LumenTableProps<T> {
|
|
|
231
235
|
/** The groups come from a server (`ServerPaging.groups`): their order, counts and open state,
|
|
232
236
|
* and each open group's rows. */
|
|
233
237
|
serverGroups?: ServerGroups | null;
|
|
238
|
+
/** What the body says when no row shows (an app's empty state); left out: "No rows match." */
|
|
239
|
+
emptyState?: ReactNode;
|
|
240
|
+
/** Takes every filter off: the empty body offers Clear filters. */
|
|
241
|
+
onClearFilter?: () => void;
|
|
234
242
|
}
|
package/dist/lumen-view.js
CHANGED
|
@@ -8,7 +8,7 @@ import { bytesParts, controlOn, isCountAgg, isDollarFormat, viewPaginationSchema
|
|
|
8
8
|
// details drawer; LumenLayout (lumen-layout.tsx) is the page around it. Rows are the ones the app
|
|
9
9
|
// already loaded; everything here runs on the client.
|
|
10
10
|
import { createPaginatedRowModel, createSortedRowModel, flexRender, rowPaginationFeature, rowSortingFeature, sortFns, tableFeatures, useTable, } from '@tanstack/react-table';
|
|
11
|
-
import { ArrowDownIcon, ArrowDownWideNarrowIcon, ArrowUpIcon, ArrowUpNarrowWideIcon, ChartCandlestick, ChartSpline, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
|
|
11
|
+
import { ArrowDownIcon, ArrowDownWideNarrowIcon, ArrowUpIcon, ArrowUpNarrowWideIcon, ChartCandlestick, ChartSpline, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, FilterXIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
|
|
12
12
|
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
13
13
|
import { ColorChip, ColorDot } from './color-dot.js';
|
|
14
14
|
import { DrawerSection } from './drawer-section.js';
|
|
@@ -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, 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, 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();
|
|
@@ -258,6 +258,10 @@ export function LumenView({ domain, config, rows, total = rows.length, paging: s
|
|
|
258
258
|
const [pageIndex, setPageIndex] = useState(0);
|
|
259
259
|
useEffect(() => setPageSize(pagination.size), [pagination.size]);
|
|
260
260
|
const sub = subgroupOf(domain, subgroup === undefined ? config.subgroup : subgroup);
|
|
261
|
+
// An empty state replaces the table, its filter and tools, unless a filter is on: then it sits
|
|
262
|
+
// in the table's body, under the headers, with the filter still there to change or clear.
|
|
263
|
+
const filtered = !!filterCount;
|
|
264
|
+
const blank = filtered ? null : state;
|
|
261
265
|
// Folded lines and their rows stay on one page, so a fold shows every row like a grouping.
|
|
262
266
|
const paging = !server && pagination.on && on('pager') && !state && !groupBy && !pivot && !sub?.under;
|
|
263
267
|
// A fit table wraps its text to the width there is; the rest scroll sideways.
|
|
@@ -334,10 +338,10 @@ export function LumenView({ domain, config, rows, total = rows.length, paging: s
|
|
|
334
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 }));
|
|
335
339
|
// The page footer (`pagination.footer`): under the table while there is more than one page.
|
|
336
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;
|
|
337
|
-
const table = pivot && config.pivot ? (_jsx(PivotTable, { domain: domain, rows: rows, pivot: config.pivot })) : (_jsx(LumenTable, { domain: domain, columns: columns, rows: 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: served, 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, 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
|
|
338
342
|
? { ...onAddColumn, field: onColumns && { domain, columns, onColumns } }
|
|
339
343
|
: undefined }));
|
|
340
|
-
return (_jsx(LumenWidget, { id: cardId, kind: "table", style: "table", title: title, phoneTitle: false, controls: controls, tools:
|
|
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",
|
|
341
345
|
// A phone's card header has room for the pager, filter, refresh and menu only.
|
|
342
346
|
className: "max-md:hidden", onClick: () => downloadCsv({
|
|
343
347
|
name: title,
|
|
@@ -346,7 +350,7 @@ export function LumenView({ domain, config, rows, total = rows.length, paging: s
|
|
|
346
350
|
groupBy,
|
|
347
351
|
sort: config.sort,
|
|
348
352
|
hidden: amountsHidden,
|
|
349
|
-
}) }))] })), filter:
|
|
353
|
+
}) }))] })), filter: blank ? undefined : filter, filterCount: filterCount, strip: blank ? undefined : strip, foot: foot, link: link, note: note, load: load, shown: shown, rows: total, pinned: inLayout && !fit, className: cn('relative', !inLayout && 'overflow-x-auto overflow-y-hidden [--pin-top:0px]'), children: blank ??
|
|
350
354
|
(load ? (_jsx(LumenStates, { load: load, skeleton: _jsx(LumenSkeleton, { rows: 10 }), children: table })) : (table)) }));
|
|
351
355
|
}
|
|
352
356
|
/**
|
|
@@ -441,7 +445,7 @@ function sortingOf(sort, columns, serverSorts) {
|
|
|
441
445
|
return [];
|
|
442
446
|
return columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : [];
|
|
443
447
|
}
|
|
444
|
-
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, }) {
|
|
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, }) {
|
|
445
449
|
const isMobile = useIsMobile();
|
|
446
450
|
const [sorting, setSorting] = useState(() => sortingOf(sort, columns, serverSorts));
|
|
447
451
|
// A saved sort patched while the table is open (the assistant's card move) applies at once.
|
|
@@ -847,7 +851,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
847
851
|
}, children: [_jsx(ListCollapseIcon, {}), " Close every ", subgroup?.label.toLowerCase()] }), _jsxs(DropdownMenuItem, { onSelect: () => setAll(false), children: [_jsx(ChevronsDownUpIcon, {}), " Collapse all"] })] })] })), i === 0 &&
|
|
848
852
|
groups &&
|
|
849
853
|
!(folds && !serverGroups) &&
|
|
850
|
-
(!serverGroups || serverGroups.onToggleAll) && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] })] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && !serverGroups?.list?.length && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center text-muted-foreground", children: "No rows match." }) })), groups && groupBy
|
|
854
|
+
(!serverGroups || serverGroups.onToggleAll) && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] })] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && !serverGroups?.list?.length && (_jsx(TableRow, { "data-slot": "table-empty", className: "hover:bg-transparent", children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center whitespace-normal text-muted-foreground", children: _jsxs("div", { className: "flex flex-col items-center gap-3", children: [emptyState ?? 'No rows match.', onClearFilter && (_jsxs(Button, { variant: "outline", size: "sm", onClick: onClearFilter, children: [_jsx(FilterXIcon, {}), "Clear filters"] }))] }) }) })), groups && groupBy
|
|
851
855
|
? groups.map(([key, members], gi) => {
|
|
852
856
|
const open = isOpen(key, gi);
|
|
853
857
|
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { className: ROLLUP, "data-group": key, "data-rollup": "", children: [selection && (_jsx(TableCell, { "data-card": "select", className: cn('w-8 pr-0', PINNED_GROUP), children: _jsx(Checkbox, { checked: members.every((m) => selection.keys.has(keyOf(m.original)))
|
package/dist/theme.css
CHANGED
|
@@ -479,9 +479,10 @@
|
|
|
479
479
|
table[data-cards] td[colspan] {
|
|
480
480
|
flex: 1 1 100%;
|
|
481
481
|
}
|
|
482
|
-
/* A row's panel (`row-panel`)
|
|
483
|
-
|
|
484
|
-
table[data-cards] tr[data-slot="row-panel"]
|
|
482
|
+
/* A row's panel (`row-panel`) and the empty table's message (`table-empty`) are boxes of their
|
|
483
|
+
own, not rows: they keep their lines. */
|
|
484
|
+
table[data-cards] tr:is([data-slot="row-panel"], [data-slot="table-empty"]),
|
|
485
|
+
table[data-cards] tr:is([data-slot="row-panel"], [data-slot="table-empty"]) > td {
|
|
485
486
|
white-space: normal;
|
|
486
487
|
}
|
|
487
488
|
table[data-cards] td:not([data-card]):not([colspan]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.120",
|
|
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.120",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|