@columenlabs/lumen-ui 0.1.118 → 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 +66 -9
- package/dist/theme.css +4 -3
- package/dist/view-model.d.ts +6 -0
- 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,8 +8,8 @@ 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';
|
|
12
|
-
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from '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
|
+
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';
|
|
15
15
|
import { useFloatingHead } from './floating-head.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
|
/**
|
|
@@ -384,6 +388,14 @@ function pathOf(domain, f) {
|
|
|
384
388
|
}
|
|
385
389
|
/** Every header carries its colour (`colorOf`), text and a 2px rule. */
|
|
386
390
|
const ruleOf = (color) => color ? { boxShadow: `inset 0 -2px 0 ${color}` } : undefined;
|
|
391
|
+
/** The width a table has to fit in: its box, or inside a page's view scroller the view's width. */
|
|
392
|
+
function roomOf(el) {
|
|
393
|
+
const view = el.closest('[data-slot=view-scroll]');
|
|
394
|
+
if (!view)
|
|
395
|
+
return el.clientWidth;
|
|
396
|
+
const cs = getComputedStyle(view);
|
|
397
|
+
return view.clientWidth - Number.parseFloat(cs.paddingLeft) - Number.parseFloat(cs.paddingRight);
|
|
398
|
+
}
|
|
387
399
|
const alignOf = (format) => (format === 'text' ? 'text-left' : 'text-right');
|
|
388
400
|
/** A column's cell classes: its alignment, and hidden on phones where the field says so. */
|
|
389
401
|
/** An ISO date, alone or with a time after it: `2026-10-14`, `2026-10-14 16:17`. */
|
|
@@ -433,7 +445,7 @@ function sortingOf(sort, columns, serverSorts) {
|
|
|
433
445
|
return [];
|
|
434
446
|
return columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : [];
|
|
435
447
|
}
|
|
436
|
-
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, }) {
|
|
437
449
|
const isMobile = useIsMobile();
|
|
438
450
|
const [sorting, setSorting] = useState(() => sortingOf(sort, columns, serverSorts));
|
|
439
451
|
// A saved sort patched while the table is open (the assistant's card move) applies at once.
|
|
@@ -483,9 +495,21 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
483
495
|
const onCard = shown.filter((f, i) => i > 0 && !f.hideOnPhone);
|
|
484
496
|
const meta = (onCard.find((f) => f.key === sorting[0]?.id) ?? onCard.find(short) ?? onCard[0])
|
|
485
497
|
?.key;
|
|
498
|
+
// Too narrow for every column (a docked drawer beside it, a small window), the table drops its
|
|
499
|
+
// lowest-`priority` columns, right to left, until it fits, instead of scrolling sideways; never
|
|
500
|
+
// its name or sorted column. More room brings them back. A phone keeps its own cards.
|
|
501
|
+
const rank = (f) => (f.priority ?? 0) - (f.hideOnPhone ? 1 : 0);
|
|
502
|
+
const dropOrder = shown
|
|
503
|
+
.slice(1)
|
|
504
|
+
.map((f, i) => ({ f, i }))
|
|
505
|
+
.filter(({ f }) => f.key !== sorting[0]?.id)
|
|
506
|
+
.sort((a, b) => rank(a.f) - rank(b.f) || b.i - a.i)
|
|
507
|
+
.map(({ f }) => f.key);
|
|
508
|
+
const [drop, setDrop] = useState(0);
|
|
509
|
+
const dropped = new Set(isMobile || fit ? [] : dropOrder.slice(0, drop));
|
|
486
510
|
const cardOf = (f) => f === shown[0] ? 'lead' : f.key === meta ? 'meta' : short(f) ? 'num' : 'text';
|
|
487
511
|
const col = (f, ...more) => ({
|
|
488
|
-
...colProps(f, ...more),
|
|
512
|
+
...colProps(f, ...more, f && dropped.has(f.key) && 'hidden'),
|
|
489
513
|
'data-card': f ? cardOf(f) : undefined,
|
|
490
514
|
'data-card-unit': f?.cardUnit,
|
|
491
515
|
});
|
|
@@ -778,6 +802,39 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
778
802
|
};
|
|
779
803
|
const box = useRef(null);
|
|
780
804
|
useFloatingHead(box, !inView);
|
|
805
|
+
// Before paint: one more column drops while the table is wider than its room: its box, and in a
|
|
806
|
+
// page's view scroller the view's width (theme.css `--view-w`).
|
|
807
|
+
useLayoutEffect(() => {
|
|
808
|
+
const el = box.current;
|
|
809
|
+
const t = el?.querySelector('table');
|
|
810
|
+
if (!el || !t || isMobile || fit || drop >= dropOrder.length)
|
|
811
|
+
return;
|
|
812
|
+
if (t.offsetWidth > roomOf(el) + 1)
|
|
813
|
+
setDrop((d) => d + 1);
|
|
814
|
+
});
|
|
815
|
+
// Every column comes back to be measured again on a new set of columns or more room.
|
|
816
|
+
const dropKey = dropOrder.join('\u0000');
|
|
817
|
+
const lastDropKey = useRef(dropKey);
|
|
818
|
+
if (lastDropKey.current !== dropKey) {
|
|
819
|
+
lastDropKey.current = dropKey;
|
|
820
|
+
if (drop)
|
|
821
|
+
setDrop(0);
|
|
822
|
+
}
|
|
823
|
+
useEffect(() => {
|
|
824
|
+
const el = box.current;
|
|
825
|
+
if (!el)
|
|
826
|
+
return;
|
|
827
|
+
const view = el.closest('[data-slot=view-scroll]');
|
|
828
|
+
let room = roomOf(el);
|
|
829
|
+
const watch = new ResizeObserver(() => {
|
|
830
|
+
const now = roomOf(el);
|
|
831
|
+
if (now > room + 1)
|
|
832
|
+
setDrop(0);
|
|
833
|
+
room = now;
|
|
834
|
+
});
|
|
835
|
+
watch.observe(view ?? el);
|
|
836
|
+
return () => watch.disconnect();
|
|
837
|
+
}, []);
|
|
781
838
|
return (_jsxs("div", { ref: box, className: cn(inView ? IN_VIEW : 'overflow-x-auto overflow-y-hidden [--pin-top:0px]', fit && FIT), children: [cardSort, _jsx(UnitColumns.Provider, { value: markUnits, children: _jsxs(Table, { "data-cards": "", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => (_jsxs(TableRow, { children: [selection && (_jsx(TableHead, { className: cn('w-8 pr-0', PINNED_HEAD), children: _jsx(Checkbox, { checked: allOn ? true : someOn ? 'indeterminate' : false, onCheckedChange: (on) => toggle(allKeys, on === true), "aria-label": "Select every row" }) })), hg.headers.map((h, i) => (_jsxs(TableHead, { ...col(shown[i], PINNED_HEAD), style: ruleOf(colorOf(domain, shown[i])), onPointerDown: pressStart, onPointerUp: pressEnd, onPointerLeave: pressEnd, onPointerCancel: pressEnd, onContextMenu: (e) => {
|
|
782
839
|
if (press.current)
|
|
783
840
|
e.preventDefault();
|
|
@@ -794,7 +851,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
794
851
|
}, children: [_jsx(ListCollapseIcon, {}), " Close every ", subgroup?.label.toLowerCase()] }), _jsxs(DropdownMenuItem, { onSelect: () => setAll(false), children: [_jsx(ChevronsDownUpIcon, {}), " Collapse all"] })] })] })), i === 0 &&
|
|
795
852
|
groups &&
|
|
796
853
|
!(folds && !serverGroups) &&
|
|
797
|
-
(!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
|
|
798
855
|
? groups.map(([key, members], gi) => {
|
|
799
856
|
const open = isOpen(key, gi);
|
|
800
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/dist/view-model.d.ts
CHANGED
|
@@ -139,6 +139,12 @@ export interface ViewField<T> {
|
|
|
139
139
|
maxWidth?: string | false;
|
|
140
140
|
/** Left out on phones, where the table has no room for it (a returns table's longer windows). */
|
|
141
141
|
hideOnPhone?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Where a table has too little room (a docked drawer beside it), its columns drop until it fits:
|
|
144
|
+
* the lowest `priority` first (default 0; `hideOnPhone` ones count one lower), right to left.
|
|
145
|
+
* The name and the sorted column never drop.
|
|
146
|
+
*/
|
|
147
|
+
priority?: number;
|
|
142
148
|
/** Left out of a table when no row in it has a value (none, empty or 0): an Attachments column on mail with none. */
|
|
143
149
|
hideEmpty?: boolean;
|
|
144
150
|
/** On a phone card, where no header names it: the word after a bare count (`msgs`, so `5,861 msgs`). */
|
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",
|