@columenlabs/lumen-ui 0.1.169 → 0.1.171
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/column-widths.d.ts +28 -0
- package/dist/column-widths.js +134 -0
- package/dist/kit-config.d.ts +3 -0
- package/dist/kit-config.js +1 -0
- package/dist/lumen-view.d.ts +9 -1
- package/dist/lumen-view.js +28 -10
- package/dist/notifications.d.ts +6 -1
- package/dist/notifications.js +17 -5
- package/package.json +2 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The narrowest and widest a person can drag a column (px). */
|
|
2
|
+
export declare const COL_MIN = 48;
|
|
3
|
+
export declare const COL_MAX = 960;
|
|
4
|
+
/**
|
|
5
|
+
* The widths a person set on one table's columns, by field key, kept on the device under `key`.
|
|
6
|
+
* Left out `key`: no widths (resizing off). `set` updates while dragging; `save` sets and keeps.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useColumnWidths(key: string | undefined): {
|
|
9
|
+
widths: Readonly<Record<string, number>>;
|
|
10
|
+
set: (col: string, w: number) => void;
|
|
11
|
+
save: (col: string, w: number) => void;
|
|
12
|
+
reset: (col: string) => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The grip on a header cell's right edge. It measures its header cell when a drag starts, so a
|
|
16
|
+
* column that never had a width set starts from where it is drawn.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ColumnResizer({ label, width, onWidth, onDone, onReset, }: {
|
|
19
|
+
/** The column's name, for the grip's label ("Resize Name"). */
|
|
20
|
+
label: string;
|
|
21
|
+
/** The width set, if any. */
|
|
22
|
+
width: number | undefined;
|
|
23
|
+
onWidth: (w: number) => void;
|
|
24
|
+
/** The drag or key press ended: keep this width. */
|
|
25
|
+
onDone: (w: number) => void;
|
|
26
|
+
/** Back to the column's own width. */
|
|
27
|
+
onReset: () => void;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Resizable table columns: a grip on each header's right edge sets that column's width by drag
|
|
3
|
+
// (or ← → on the focused grip), a double click (or Enter) gives it back its own width, and the
|
|
4
|
+
// widths are kept on the device per table. A table turns it on with `resizable`, an app for every
|
|
5
|
+
// table with `configureLumen({ resizableColumns: true })`. Desktop only: a phone's rows are cards.
|
|
6
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
7
|
+
import { cn } from './lib/utils.js';
|
|
8
|
+
/** The narrowest and widest a person can drag a column (px). */
|
|
9
|
+
export const COL_MIN = 48;
|
|
10
|
+
export const COL_MAX = 960;
|
|
11
|
+
/** How far ← or → moves a focused grip (px). */
|
|
12
|
+
const STEP = 16;
|
|
13
|
+
const PREFIX = 'lumen:col-widths:';
|
|
14
|
+
const clamp = (w) => Math.round(Math.min(COL_MAX, Math.max(COL_MIN, w)));
|
|
15
|
+
function read(key) {
|
|
16
|
+
try {
|
|
17
|
+
const raw = window.localStorage.getItem(PREFIX + key);
|
|
18
|
+
const v = raw ? JSON.parse(raw) : null;
|
|
19
|
+
if (!v || typeof v !== 'object')
|
|
20
|
+
return {};
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [k, w] of Object.entries(v))
|
|
23
|
+
if (typeof w === 'number' && w > 0)
|
|
24
|
+
out[k] = clamp(w);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function write(key, widths) {
|
|
32
|
+
try {
|
|
33
|
+
if (Object.keys(widths).length)
|
|
34
|
+
window.localStorage.setItem(PREFIX + key, JSON.stringify(widths));
|
|
35
|
+
else
|
|
36
|
+
window.localStorage.removeItem(PREFIX + key);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Storage off (a private window, blocked site data): widths last for the visit only.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The widths a person set on one table's columns, by field key, kept on the device under `key`.
|
|
44
|
+
* Left out `key`: no widths (resizing off). `set` updates while dragging; `save` sets and keeps.
|
|
45
|
+
*/
|
|
46
|
+
export function useColumnWidths(key) {
|
|
47
|
+
const [widths, setWidths] = useState(() => key && typeof window !== 'undefined' ? read(key) : {});
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setWidths(key ? read(key) : {});
|
|
50
|
+
}, [key]);
|
|
51
|
+
const set = useCallback((col, w) => {
|
|
52
|
+
setWidths((o) => (o[col] === clamp(w) ? o : { ...o, [col]: clamp(w) }));
|
|
53
|
+
}, []);
|
|
54
|
+
const save = useCallback((col, w) => {
|
|
55
|
+
setWidths((o) => {
|
|
56
|
+
const next = { ...o, [col]: clamp(w) };
|
|
57
|
+
if (key)
|
|
58
|
+
write(key, next);
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
}, [key]);
|
|
62
|
+
const reset = useCallback((col) => {
|
|
63
|
+
setWidths((o) => {
|
|
64
|
+
if (!(col in o))
|
|
65
|
+
return o;
|
|
66
|
+
const { [col]: _, ...rest } = o;
|
|
67
|
+
if (key)
|
|
68
|
+
write(key, rest);
|
|
69
|
+
return rest;
|
|
70
|
+
});
|
|
71
|
+
}, [key]);
|
|
72
|
+
return { widths: key ? widths : {}, set, save, reset };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The grip on a header cell's right edge. It measures its header cell when a drag starts, so a
|
|
76
|
+
* column that never had a width set starts from where it is drawn.
|
|
77
|
+
*/
|
|
78
|
+
export function ColumnResizer({ label, width, onWidth, onDone, onReset, }) {
|
|
79
|
+
const drag = useRef(null);
|
|
80
|
+
const [active, setActive] = useState(false);
|
|
81
|
+
const cellWidth = (el) => el.closest('th')?.getBoundingClientRect().width ?? width ?? COL_MIN;
|
|
82
|
+
return (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": `Resize ${label}`, "aria-valuenow": width, "aria-valuemin": COL_MIN, "aria-valuemax": COL_MAX, tabIndex: 0, title: `Drag to resize ${label}; double-click to reset`, "data-slot": "column-resizer", "data-active": active || undefined, className: cn('absolute inset-y-0 right-0 z-10 w-2 cursor-col-resize touch-none select-none rounded-sm outline-none', "after:absolute after:inset-y-2 after:right-0.5 after:w-px after:bg-border after:content-['']", 'hover:after:w-0.5 hover:after:bg-primary data-active:after:w-0.5 data-active:after:bg-primary', 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:after:bg-primary'), onClick: (e) => e.stopPropagation(), onPointerDown: (e) => {
|
|
83
|
+
if (e.button !== 0)
|
|
84
|
+
return;
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
88
|
+
const w = cellWidth(e.currentTarget);
|
|
89
|
+
drag.current = { x: e.clientX, w, frame: 0, next: w };
|
|
90
|
+
setActive(true);
|
|
91
|
+
}, onPointerMove: (e) => {
|
|
92
|
+
const d = drag.current;
|
|
93
|
+
if (!d)
|
|
94
|
+
return;
|
|
95
|
+
d.next = d.w + e.clientX - d.x;
|
|
96
|
+
if (!d.frame)
|
|
97
|
+
d.frame = requestAnimationFrame(() => {
|
|
98
|
+
d.frame = 0;
|
|
99
|
+
onWidth(d.next);
|
|
100
|
+
});
|
|
101
|
+
}, onPointerUp: (e) => {
|
|
102
|
+
const d = drag.current;
|
|
103
|
+
if (!d)
|
|
104
|
+
return;
|
|
105
|
+
if (d.frame)
|
|
106
|
+
cancelAnimationFrame(d.frame);
|
|
107
|
+
drag.current = null;
|
|
108
|
+
setActive(false);
|
|
109
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
110
|
+
if (d.next !== d.w)
|
|
111
|
+
onDone(d.next);
|
|
112
|
+
}, onPointerCancel: () => {
|
|
113
|
+
const d = drag.current;
|
|
114
|
+
if (d?.frame)
|
|
115
|
+
cancelAnimationFrame(d.frame);
|
|
116
|
+
drag.current = null;
|
|
117
|
+
setActive(false);
|
|
118
|
+
}, onDoubleClick: (e) => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
onReset();
|
|
121
|
+
}, onKeyDown: (e) => {
|
|
122
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
const w = width ?? cellWidth(e.currentTarget);
|
|
126
|
+
onDone(w + (e.key === 'ArrowRight' ? STEP : -STEP));
|
|
127
|
+
}
|
|
128
|
+
else if (e.key === 'Enter' || e.key === 'Delete' || e.key === 'Backspace') {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
onReset();
|
|
132
|
+
}
|
|
133
|
+
} }));
|
|
134
|
+
}
|
package/dist/kit-config.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export interface KitConfig {
|
|
|
13
13
|
meterLevels: MeterLevels;
|
|
14
14
|
/** Where every health meter drops below green (`watch`) and below amber (`alert`), 0 to 1. */
|
|
15
15
|
healthLevels: HealthLevels;
|
|
16
|
+
/** Every table's columns resize by a grip on their header's right edge (desktop), the widths
|
|
17
|
+
* kept on the device per table; one table overrides with its `resizable` prop. */
|
|
18
|
+
resizableColumns: boolean;
|
|
16
19
|
}
|
|
17
20
|
export declare const KIT: KitConfig;
|
|
18
21
|
/** Runs `fn` after each `configureLumen` (preferences read again from the app's cookie). */
|
package/dist/kit-config.js
CHANGED
|
@@ -7,6 +7,7 @@ export const KIT = {
|
|
|
7
7
|
marketOff: 'Live market data is not on this app.',
|
|
8
8
|
meterLevels: METER_LEVELS,
|
|
9
9
|
healthLevels: HEALTH_LEVELS,
|
|
10
|
+
resizableColumns: false,
|
|
10
11
|
};
|
|
11
12
|
const changed = new Set();
|
|
12
13
|
/** Runs `fn` after each `configureLumen` (preferences read again from the app's cookie). */
|
package/dist/lumen-view.d.ts
CHANGED
|
@@ -170,6 +170,9 @@ export interface LumenViewProps<T> {
|
|
|
170
170
|
onSort?: (sort: CustomViewConfig['sort']) => void;
|
|
171
171
|
/** Its controls from its card (Edit widget ▸ Controls); missing keys take the Table defaults. */
|
|
172
172
|
shown?: ViewCard['controls'];
|
|
173
|
+
/** A grip on each header's right edge resizes its column (desktop), kept on the device per
|
|
174
|
+
* table (the domain's id and `cardId`); left out: `configureLumen({ resizableColumns })`. */
|
|
175
|
+
resizable?: boolean;
|
|
173
176
|
}
|
|
174
177
|
/** What the header's + offers. */
|
|
175
178
|
export interface AddColumn {
|
|
@@ -188,7 +191,7 @@ export interface AddColumn {
|
|
|
188
191
|
* and ⋯, and its own filter and Group (they narrow this table only); its body
|
|
189
192
|
* the table (grouped one level, or the pivot) with the loading states.
|
|
190
193
|
*/
|
|
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;
|
|
194
|
+
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, resizable, }: LumenViewProps<T>): import("react").JSX.Element;
|
|
192
195
|
/** A field's colour for its header: its topic's source (a broker green, an app brown) where
|
|
193
196
|
* the group names one, else its group's (Holding, Symbol, Account, Formulas). */
|
|
194
197
|
export declare function colorOf<T>(domain: ViewDomain<T>, f: ViewField<T> | undefined): string | null;
|
|
@@ -244,4 +247,9 @@ export interface LumenTableProps<T> {
|
|
|
244
247
|
emptyState?: ReactNode;
|
|
245
248
|
/** Takes every filter off: the empty body offers Clear filters. */
|
|
246
249
|
onClearFilter?: () => void;
|
|
250
|
+
/** A grip on each header's right edge resizes its column (desktop, not a `fit` table); left
|
|
251
|
+
* out: `configureLumen({ resizableColumns })`. */
|
|
252
|
+
resizable?: boolean;
|
|
253
|
+
/** Where the widths are kept on the device (one key per table); left out: they last the visit. */
|
|
254
|
+
widthsKey?: string;
|
|
247
255
|
}
|
package/dist/lumen-view.js
CHANGED
|
@@ -11,12 +11,14 @@ import { createPaginatedRowModel, createSortedRowModel, flexRender, rowPaginatio
|
|
|
11
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
|
+
import { ColumnResizer, useColumnWidths } from './column-widths.js';
|
|
14
15
|
import { DrawerSection } from './drawer-section.js';
|
|
15
16
|
import { useFloatingHead } from './floating-head.js';
|
|
16
17
|
import { FreshnessMark, freshnessLine, freshnessOf } from './freshness.js';
|
|
17
18
|
import { useIsMobile } from './hooks/use-mobile.js';
|
|
18
19
|
import { useTell } from './hooks/use-tell.js';
|
|
19
20
|
import { IconButton } from './icon-button.js';
|
|
21
|
+
import { KIT } from './kit-config.js';
|
|
20
22
|
import { lazyPart } from './lazy-part.js';
|
|
21
23
|
import { cn } from './lib/utils.js';
|
|
22
24
|
import { useGridCell } from './lumen-grid.js';
|
|
@@ -225,7 +227,7 @@ async function downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
|
|
|
225
227
|
* and ⋯, and its own filter and Group (they narrow this table only); its body
|
|
226
228
|
* the table (grouped one level, or the pivot) with the loading states.
|
|
227
229
|
*/
|
|
228
|
-
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, }) {
|
|
230
|
+
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, resizable, }) {
|
|
229
231
|
// Its controls (Edit widget ▸ Controls): Pager, Columns and Download here.
|
|
230
232
|
const on = (k) => controlOn({ kind: 'table', controls: shown }, k);
|
|
231
233
|
const cell = useGridCell();
|
|
@@ -338,7 +340,7 @@ export function LumenView({ domain, config, rows, total = rows.length, paging: s
|
|
|
338
340
|
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
341
|
// The page footer (`pagination.footer`): under the table while there is more than one page.
|
|
340
342
|
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, 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
|
|
343
|
+
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, resizable: resizable, widthsKey: `${domain.id}:${cardId}`, add: onAddColumn || onColumns
|
|
342
344
|
? { ...onAddColumn, field: onColumns && { domain, columns, onColumns } }
|
|
343
345
|
: undefined }));
|
|
344
346
|
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",
|
|
@@ -437,8 +439,17 @@ function sortingOf(sort, columns, serverSorts) {
|
|
|
437
439
|
return [];
|
|
438
440
|
return columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : [];
|
|
439
441
|
}
|
|
440
|
-
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, }) {
|
|
442
|
+
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, resizable = KIT.resizableColumns, widthsKey, }) {
|
|
441
443
|
const isMobile = useIsMobile();
|
|
444
|
+
// Widths a person dragged (desktop only: a phone's rows are cards, and a fit table wraps instead).
|
|
445
|
+
const resizing = resizable && !isMobile && !fit;
|
|
446
|
+
const colWidths = useColumnWidths(resizing ? (widthsKey ?? '') || undefined : undefined);
|
|
447
|
+
const widthOf = (f) => resizing && f ? colWidths.widths[f.key] : undefined;
|
|
448
|
+
// A dragged width caps the column's cells in its place (the cell's own padding taken off).
|
|
449
|
+
const capFor = (f, i) => {
|
|
450
|
+
const w = widthOf(f);
|
|
451
|
+
return w ? `calc(${w}px - 2 * var(--cell-x))` : capOf(f, i);
|
|
452
|
+
};
|
|
442
453
|
const [sorting, setSorting] = useState(() => sortingOf(sort, columns, serverSorts));
|
|
443
454
|
// A saved sort patched while the table is open (the assistant's card move) applies at once.
|
|
444
455
|
const savedSort = JSON.stringify(sort ?? null);
|
|
@@ -560,8 +571,8 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
560
571
|
// A column's `cell` function is a component type to flexRender, so a new function means every
|
|
561
572
|
// cell remounts (a cell being typed into loses its box). The defs change
|
|
562
573
|
// only when the columns do; each cell reads the latest renderers through `latest`.
|
|
563
|
-
const latest = useRef({ shown, cellOf, inline, chips, computed });
|
|
564
|
-
latest.current = { shown, cellOf, inline, chips, computed };
|
|
574
|
+
const latest = useRef({ shown, cellOf, inline, chips, computed, capFor });
|
|
575
|
+
latest.current = { shown, cellOf, inline, chips, computed, capFor };
|
|
565
576
|
const shownKeys = shown.map((f) => f.key).join(',');
|
|
566
577
|
const inlineOn = inline.length > 0;
|
|
567
578
|
// biome-ignore lint/correctness/useExhaustiveDependencies: keyed on the columns shown; the rest comes through `latest`
|
|
@@ -575,11 +586,11 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
575
586
|
enableSorting: !serverSorts || serverSorts.includes(f.key),
|
|
576
587
|
header: i === 0 && flat && subgroup ? subgroup.label : f.label,
|
|
577
588
|
cell: ({ row }) => {
|
|
578
|
-
const { shown: now, cellOf, inline, chips, computed } = latest.current;
|
|
589
|
+
const { shown: now, cellOf, inline, chips, computed, capFor } = latest.current;
|
|
579
590
|
const field = now[i] ?? f;
|
|
580
591
|
const chip = chips.some((c) => c.key === field.key);
|
|
581
592
|
const v = computed.value(field, row.original);
|
|
582
|
-
return (_jsx(CappedCell, { max:
|
|
593
|
+
return (_jsx(CappedCell, { max: capFor(field, i), title: !field.render && typeof v === 'string' ? v : undefined, children: i === 0 && inline.length ? (_jsx(FirstCell, { value: cellOf(field, row.original, chip), chips: inline.flatMap((x) => {
|
|
583
594
|
const v = computed.value(x, row.original);
|
|
584
595
|
if (!x.edit && (v === null || v === ''))
|
|
585
596
|
return [];
|
|
@@ -714,7 +725,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
714
725
|
return n;
|
|
715
726
|
});
|
|
716
727
|
}, children: _jsx(ChevronRightIcon, { className: cn('transition-transform', openParts.has(k) && 'rotate-90') }) }));
|
|
717
|
-
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:
|
|
728
|
+
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: capFor(f, 0), children: name
|
|
718
729
|
? name(p)
|
|
719
730
|
: 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)}`));
|
|
720
731
|
// ↑ and ↓ step to the next data row (past part and panel rows); while a row's details are
|
|
@@ -813,7 +824,14 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
813
824
|
};
|
|
814
825
|
const box = useRef(null);
|
|
815
826
|
useFloatingHead(box, !inView);
|
|
816
|
-
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:
|
|
827
|
+
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: {
|
|
828
|
+
...ruleOf(colorOf(domain, shown[i])),
|
|
829
|
+
...(widthOf(shown[i]) && {
|
|
830
|
+
width: widthOf(shown[i]),
|
|
831
|
+
minWidth: widthOf(shown[i]),
|
|
832
|
+
maxWidth: widthOf(shown[i]),
|
|
833
|
+
}),
|
|
834
|
+
}, onPointerDown: pressStart, onPointerUp: pressEnd, onPointerLeave: pressEnd, onPointerCancel: pressEnd, onContextMenu: (e) => {
|
|
817
835
|
if (press.current)
|
|
818
836
|
e.preventDefault();
|
|
819
837
|
}, "aria-sort": h.column.getIsSorted() === 'asc'
|
|
@@ -829,7 +847,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
829
847
|
}, children: [_jsx(ListCollapseIcon, {}), " Close every ", subgroup?.label.toLowerCase()] }), _jsxs(DropdownMenuItem, { onSelect: () => setAll(false), children: [_jsx(ChevronsDownUpIcon, {}), " Collapse all"] })] })] })), i === 0 &&
|
|
830
848
|
groups &&
|
|
831
849
|
!(folds && !serverGroups) &&
|
|
832
|
-
(!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
|
|
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] }), resizing && shown[i] && (_jsx(ColumnResizer, { label: shown[i].label, width: widthOf(shown[i]), onWidth: (w) => colWidths.set(h.id, w), onDone: (w) => colWidths.save(h.id, w), onReset: () => colWidths.reset(h.id) }))] }, 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
|
|
833
851
|
? groups.map(([key, members], gi) => {
|
|
834
852
|
const open = isOpen(key, gi);
|
|
835
853
|
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/notifications.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/** The Settings ▸ Notifications section's body. */
|
|
2
|
-
export declare function NotificationsSettings({ api: base, product, sender, }: {
|
|
2
|
+
export declare function NotificationsSettings({ api: base, product, sender, parts, }: {
|
|
3
3
|
/** The app's Notifications API base, e.g. `/api/notifications`. */
|
|
4
4
|
api: string;
|
|
5
5
|
/** The product's name in the copy ("Get Acme alerts by email"). */
|
|
6
6
|
product: string;
|
|
7
7
|
/** The address email comes from. */
|
|
8
8
|
sender: string;
|
|
9
|
+
/**
|
|
10
|
+
* What to draw: the channels and the events grid (default), only the channels (where messages
|
|
11
|
+
* go) or only the events grid (what goes where), so an app can set them in two sections.
|
|
12
|
+
*/
|
|
13
|
+
parts?: 'all' | 'channels' | 'events';
|
|
9
14
|
}): import("react").JSX.Element;
|
package/dist/notifications.js
CHANGED
|
@@ -147,12 +147,22 @@ function Events({ status, api }) {
|
|
|
147
147
|
...status.webhooks.map((h) => ({ id: h.id, name: h.name })),
|
|
148
148
|
];
|
|
149
149
|
if (!targets.length)
|
|
150
|
-
return (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to send to yet: turn on email or ntfy, or add a webhook,
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
return (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to send to yet: turn on email or ntfy, or add a webhook, first." }));
|
|
151
|
+
// The event column gives way first, so a phone shows the checkboxes; its detail waits in the hover.
|
|
152
|
+
const grid = { gridTemplateColumns: `minmax(7rem, 1fr) repeat(${targets.length}, 5.5rem)` };
|
|
153
|
+
// Groups in the order they first appear; events without one come first, unlabelled.
|
|
154
|
+
const groups = [];
|
|
155
|
+
for (const e of status.events) {
|
|
156
|
+
const g = groups.find((x) => x.label === e.group);
|
|
157
|
+
if (g)
|
|
158
|
+
g.events.push(e);
|
|
159
|
+
else
|
|
160
|
+
groups.push({ label: e.group, events: [e] });
|
|
161
|
+
}
|
|
162
|
+
return (_jsxs("div", { className: "grid grid-cols-1 gap-2", children: [_jsx("div", { className: "overflow-x-auto", children: _jsxs("div", { "aria-label": "Events", className: "grid min-w-max items-center", style: grid, children: [_jsxs("div", { className: "contents", children: [_jsx("span", { className: "pb-1 text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Event" }), targets.map((t) => (_jsx("span", { className: "truncate pb-1 text-center text-xs font-semibold tracking-wide text-muted-foreground uppercase", title: t.name, children: t.name }, t.id)))] }), groups.map((g) => (_jsxs("div", { className: "contents", children: [g.label ? (_jsx("span", { className: "border-t pt-3 pb-1 text-xs font-semibold tracking-wide text-muted-foreground uppercase", style: { gridColumn: '1 / -1' }, children: g.label })) : null, g.events.map((e) => (_jsxs("div", { className: "contents", children: [_jsxs("span", { className: "flex min-w-0 items-baseline gap-2 border-t py-2 pr-3", title: `${e.label}: ${e.detail}`, children: [_jsx("span", { className: "truncate text-sm whitespace-nowrap sm:shrink-0", children: e.label }), _jsx("span", { className: "hidden truncate text-xs text-muted-foreground sm:inline", children: e.detail })] }), targets.map((t) => (_jsx("span", { className: "self-stretch border-t py-2", children: _jsx("span", { className: "flex h-full items-center justify-center", children: _jsx(EventCell, { event: e, target: t.id, name: t.name, api: api }) }) }, t.id)))] }, e.key)))] }, g.label ?? '')))] }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "A new target starts with every event on." })] }));
|
|
153
163
|
}
|
|
154
164
|
/** The Settings ▸ Notifications section's body. */
|
|
155
|
-
export function NotificationsSettings({ api: base, product, sender, }) {
|
|
165
|
+
export function NotificationsSettings({ api: base, product, sender, parts = 'all', }) {
|
|
156
166
|
const api = useNotifications(base, product, sender);
|
|
157
167
|
const [open, setOpen] = useState(null);
|
|
158
168
|
const toggle = (id) => (o) => setOpen(o ? id : null);
|
|
@@ -161,7 +171,9 @@ export function NotificationsSettings({ api: base, product, sender, }) {
|
|
|
161
171
|
if (api.q.isError)
|
|
162
172
|
return _jsx(ErrorLine, { error: api.q.error });
|
|
163
173
|
const s = api.q.data;
|
|
174
|
+
if (parts === 'events')
|
|
175
|
+
return _jsx(Events, { status: s, api: api });
|
|
164
176
|
return (_jsxs("div", { className: "grid grid-cols-1 gap-6", children: [_jsxs("div", { className: "grid grid-cols-1", children: [_jsx(IntegrationRow, { name: "Email", meta: "To your sign-in address", open: open === 'email', onOpenChange: toggle('email'), badge: s.email.connected ? (_jsx(TestBadge, { test: s.email.last_test })) : (_jsx(Badge, { variant: "outline", children: "Off" })), children: _jsx(Email, { status: s.email, api: api }) }), _jsx(IntegrationRow, { name: "ntfy", meta: "Phone notifications through the ntfy app", open: open === 'ntfy', onOpenChange: toggle('ntfy'), badge: s.ntfy.connected ? (_jsx(TestBadge, { test: s.ntfy.last_test })) : (_jsx(Badge, { variant: "outline", children: "Off" })), children: _jsx(Ntfy, { status: s.ntfy, api: api }) }), _jsx(IntegrationRow, { name: "Webhooks", meta: s.webhooks.length
|
|
165
177
|
? s.webhooks.map((h) => h.name).join(' · ')
|
|
166
|
-
: 'Events posted to your own addresses', open: open === 'webhooks', onOpenChange: toggle('webhooks'), badge: s.webhooks.length ? (s.webhooks.some((h) => h.last_test?.ok === false) ? (_jsxs(Badge, { variant: "outline", className: "text-loss", children: [_jsx(AlertTriangleIcon, {}), "Failed"] })) : (_jsxs(Badge, { variant: "outline", children: [s.webhooks.length, " webhook", s.webhooks.length === 1 ? '' : 's'] }))) : (_jsx(Badge, { variant: "outline", children: "None" })), children: _jsx(Webhooks, { hooks: s.webhooks, api: api }) })] }), _jsxs("section", { className: "grid grid-cols-1 gap-2", children: [_jsx("h4", { className: "font-medium", children: "Events" }), _jsx(Events, { status: s, api: api })] })] }));
|
|
178
|
+
: 'Events posted to your own addresses', open: open === 'webhooks', onOpenChange: toggle('webhooks'), badge: s.webhooks.length ? (s.webhooks.some((h) => h.last_test?.ok === false) ? (_jsxs(Badge, { variant: "outline", className: "text-loss", children: [_jsx(AlertTriangleIcon, {}), "Failed"] })) : (_jsxs(Badge, { variant: "outline", children: [s.webhooks.length, " webhook", s.webhooks.length === 1 ? '' : 's'] }))) : (_jsx(Badge, { variant: "outline", children: "None" })), children: _jsx(Webhooks, { hooks: s.webhooks, api: api }) })] }), parts === 'all' ? (_jsxs("section", { className: "grid grid-cols-1 gap-2", children: [_jsx("h4", { className: "font-medium", children: "Events" }), _jsx(Events, { status: s, api: api })] })) : null] }));
|
|
167
179
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.171",
|
|
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.171",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|