@columenlabs/lumen-ui 0.1.179 → 0.1.180
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lumen-layout.js +1 -1
- package/dist/lumen-view-card.js +1 -1
- package/dist/lumen-view.d.ts +4 -1
- package/dist/lumen-view.js +81 -18
- package/dist/lumen-widget.d.ts +3 -1
- package/dist/lumen-widget.js +2 -2
- package/package.json +2 -2
package/dist/lumen-layout.js
CHANGED
|
@@ -559,7 +559,7 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
|
|
|
559
559
|
return {
|
|
560
560
|
...place,
|
|
561
561
|
wide: isTableCard(c) && c.w === GRID_COLS && c.h === undefined,
|
|
562
|
-
node: c.source ? (_jsx(SourcedCard, { card: c, name: config.name, controls: controls })) : isTableCard(c) && c.id !== viewCardId ? (_jsx(LayoutTable, { card: c, base: joins.domain, config: config, rows: c.filter.length ? filterByTokens(rows, widgetFields, c.filter) : rows, load: scope(c.id, load), state: state, onDetails: onDetails, openKey: openKey, controls: controls, onLayout: onConfigChange ? (table) => putCard({ ...c, table }) : undefined })) : isTableCard(c) ? (_jsx(LumenView, { domain: domain, config: config, rows: viewRows, total: rows.length, paging: paging, groupBy: group.groupField, pivot: pivot, subgroup: group.sub, load: scope(c.id, load), state: state, title: c.title ?? config.name, selection: selection, onDetails: rowPick.onTap ?? onDetails, tapped: rowPick.tapped, openKey: openKey, groupActions: groupActions, controls: controls, inLayout: true, tools: viewTools, filter: viewFilter, filterCount: filter.tokens.length, onClearFilter: filter.clear, strip: _jsx(ConnectOffer, { domain: domain, rows: rows, config: config, read: joins.read, cardKey: viewId ?? config.name, onConnect: onConfigChange ? connect : undefined }), link: cardLink(c.id, viewFollows, viewColor ? filter.tokens.length : 0), shown: c.controls, onColumns: onConfigChange && columnPicker !== false
|
|
562
|
+
node: c.source ? (_jsx(SourcedCard, { card: c, name: config.name, controls: controls })) : isTableCard(c) && c.id !== viewCardId ? (_jsx(LayoutTable, { card: c, base: joins.domain, config: config, rows: c.filter.length ? filterByTokens(rows, widgetFields, c.filter) : rows, load: scope(c.id, load), state: state, onDetails: onDetails, openKey: openKey, controls: controls, onLayout: onConfigChange ? (table) => putCard({ ...c, table }) : undefined })) : isTableCard(c) ? (_jsx(LumenView, { domain: domain, config: config, rows: viewRows, total: rows.length, allRows: rows, paging: paging, groupBy: group.groupField, pivot: pivot, subgroup: group.sub, load: scope(c.id, load), state: state, title: c.title ?? config.name, selection: selection, onDetails: rowPick.onTap ?? onDetails, tapped: rowPick.tapped, openKey: openKey, groupActions: groupActions, controls: controls, inLayout: true, tools: viewTools, filter: viewFilter, filterCount: filter.tokens.length, onClearFilter: filter.clear, strip: _jsx(ConnectOffer, { domain: domain, rows: rows, config: config, read: joins.read, cardKey: viewId ?? config.name, onConnect: onConfigChange ? connect : undefined }), link: cardLink(c.id, viewFollows, viewColor ? filter.tokens.length : 0), shown: c.controls, onColumns: onConfigChange && columnPicker !== false
|
|
563
563
|
? (columns) => onConfigChange({ ...config, columns })
|
|
564
564
|
: 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
|
|
565
565
|
? 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, openKey: openKey, title: title ?? config.name, inLayout: true, cardId: cardId, controls: controls, tools: tools, note: note, filter: _jsxs(_Fragment, { children: [on('filter') && (_jsx(LumenTokenizedFilter, { filter: filter, label: `Filter ${config.name}`, className: "min-w-0 flex-1", link: link || undefined })), on('group') && group.button] }), filterCount: filter.tokens.length, onClearFilter: filter.clear, link: link || undefined, shown: savedControls, onColumns: (columns) => {
|
|
46
|
+
return (_jsx(LumenView, { domain: domain, config: config, rows: shown, total: rows.length, allRows: rows, groupBy: group.groupField, pivot: group.pivot, subgroup: group.sub, load: load, state: state, onDetails: pick.onTap ?? onDetails, tapped: pick.tapped, openKey: openKey, title: title ?? config.name, inLayout: true, cardId: cardId, controls: controls, tools: tools, note: note, filter: _jsxs(_Fragment, { children: [on('filter') && (_jsx(LumenTokenizedFilter, { filter: filter, label: `Filter ${config.name}`, className: "min-w-0 flex-1", link: link || undefined })), on('group') && group.button] }), filterCount: filter.tokens.length, onClearFilter: filter.clear, link: link || undefined, shown: savedControls, onColumns: (columns) => {
|
|
47
47
|
setConfig((c) => ({ ...c, columns }));
|
|
48
48
|
onColumns?.(columns);
|
|
49
49
|
} }));
|
package/dist/lumen-view.d.ts
CHANGED
|
@@ -173,6 +173,9 @@ export interface LumenViewProps<T> {
|
|
|
173
173
|
/** A grip on each header's right edge resizes its column (desktop), kept on the device per
|
|
174
174
|
* table (the domain's id and `cardId`); left out: `configureLumen({ resizableColumns })`. */
|
|
175
175
|
resizable?: boolean;
|
|
176
|
+
/** Every row before the page's filter: Export then offers Filtered or All while a filter hides
|
|
177
|
+
* some. Left out (or a server's page): Export takes the rows shown. */
|
|
178
|
+
allRows?: T[];
|
|
176
179
|
}
|
|
177
180
|
/** What the header's + offers. */
|
|
178
181
|
export interface AddColumn {
|
|
@@ -191,7 +194,7 @@ export interface AddColumn {
|
|
|
191
194
|
* and ⋯, and its own filter and Group (they narrow this table only); its body
|
|
192
195
|
* the table (grouped one level, or the pivot) with the loading states.
|
|
193
196
|
*/
|
|
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;
|
|
197
|
+
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, allRows, }: LumenViewProps<T>): import("react").JSX.Element;
|
|
195
198
|
/** A field's colour for its header: its topic's source (a broker green, an app brown) where
|
|
196
199
|
* the group names one, else its group's (Holding, Symbol, Account, Formulas). */
|
|
197
200
|
export declare function colorOf<T>(domain: ViewDomain<T>, f: ViewField<T> | undefined): string | null;
|
package/dist/lumen-view.js
CHANGED
|
@@ -8,8 +8,9 @@ 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, FilterXIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
|
|
11
|
+
import { ArrowDownIcon, ArrowDownWideNarrowIcon, ArrowUpIcon, ArrowUpNarrowWideIcon, ChartCandlestick, ChartSpline, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CopyIcon, CornerDownRightIcon, DownloadIcon, FilterXIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
|
|
12
12
|
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
13
|
+
import { toast } from 'sonner';
|
|
13
14
|
import { ColorChip, ColorDot } from './color-dot.js';
|
|
14
15
|
import { ColumnResizer, useColumnWidths } from './column-widths.js';
|
|
15
16
|
import { DrawerSection } from './drawer-section.js';
|
|
@@ -33,7 +34,7 @@ import { TagChips, tagColor, tagIcon } from './tag-chips.js';
|
|
|
33
34
|
import { Badge } from './ui/badge.js';
|
|
34
35
|
import { Button } from './ui/button.js';
|
|
35
36
|
import { Checkbox } from './ui/checkbox.js';
|
|
36
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
|
|
37
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
|
|
37
38
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table.js';
|
|
38
39
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
39
40
|
import { UnitColumns, UnitValue } from './unit-value.js';
|
|
@@ -196,11 +197,11 @@ function ColumnPicker({ domain, columns, onColumns, open, onOpenChange, classNam
|
|
|
196
197
|
: keys.filter((k) => next.includes(k))) }));
|
|
197
198
|
}
|
|
198
199
|
/**
|
|
199
|
-
* The view's rows as
|
|
200
|
-
*
|
|
201
|
-
*
|
|
200
|
+
* The view's rows as CSV text: the rows in the view's sort, its shown columns (the grouping first
|
|
201
|
+
* when it is not one), plain numbers; amounts stay masked while Hide amounts is on. The CSV writer
|
|
202
|
+
* loads on the first export, not with the page.
|
|
202
203
|
*/
|
|
203
|
-
async function
|
|
204
|
+
async function csvOf({ columns, rows, groupBy, sort, hidden, }) {
|
|
204
205
|
const fields = groupBy && !columns.includes(groupBy) ? [groupBy, ...columns] : columns;
|
|
205
206
|
const by = sort && fields.find((f) => f.key === sort.key);
|
|
206
207
|
const sorted = by
|
|
@@ -222,10 +223,13 @@ async function downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
|
|
|
222
223
|
return hidden && f.amount ? maskAmount(String(v)) : v;
|
|
223
224
|
};
|
|
224
225
|
const { default: Papa } = await import('papaparse');
|
|
225
|
-
|
|
226
|
+
return Papa.unparse({
|
|
226
227
|
fields: fields.map((f) => f.label),
|
|
227
228
|
data: sorted.map((r) => fields.map((f) => cell(f, r))),
|
|
228
229
|
});
|
|
230
|
+
}
|
|
231
|
+
/** Saves CSV text as `<name>-<date>.csv`. */
|
|
232
|
+
function saveCsv(name, csv) {
|
|
229
233
|
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
|
230
234
|
const a = document.createElement('a');
|
|
231
235
|
a.href = url;
|
|
@@ -233,13 +237,69 @@ async function downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
|
|
|
233
237
|
a.click();
|
|
234
238
|
URL.revokeObjectURL(url);
|
|
235
239
|
}
|
|
240
|
+
const SCOPE_KEY = 'lumen:export-scope:';
|
|
241
|
+
/** The table's last Filtered / All pick, kept on the device per table. */
|
|
242
|
+
function useExportScope(key) {
|
|
243
|
+
const [scope, setScope] = useState(() => {
|
|
244
|
+
try {
|
|
245
|
+
return localStorage.getItem(SCOPE_KEY + key) === 'all' ? 'all' : 'filtered';
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return 'filtered';
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
const pick = (s) => {
|
|
252
|
+
setScope(s);
|
|
253
|
+
try {
|
|
254
|
+
localStorage.setItem(SCOPE_KEY + key, s);
|
|
255
|
+
}
|
|
256
|
+
catch { }
|
|
257
|
+
};
|
|
258
|
+
return [scope, pick];
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Export: Download CSV and Copy CSV of the table's rows. While a filter hides rows (`all` holds
|
|
262
|
+
* more than `rows`), a Filtered / All switch at the top picks which; Filtered is every matching
|
|
263
|
+
* row on every page. On desktop it is an icon button with its menu; on a phone (`items`) its
|
|
264
|
+
* entries join the card's ⋯ menu.
|
|
265
|
+
*/
|
|
266
|
+
function useExport({ name, columns, rows, all, groupBy, sort, hidden, scopeKey, }) {
|
|
267
|
+
const [picked, setScope] = useExportScope(scopeKey);
|
|
268
|
+
const split = !!all && all.length !== rows.length;
|
|
269
|
+
const scope = split ? picked : 'filtered';
|
|
270
|
+
const chosen = scope === 'all' && all ? all : rows;
|
|
271
|
+
const text = () => csvOf({ columns, rows: chosen, groupBy, sort, hidden });
|
|
272
|
+
const download = () => text().then((csv) => saveCsv(name, csv));
|
|
273
|
+
const n = chosen.length;
|
|
274
|
+
const counted = `${n.toLocaleString()} ${n === 1 ? 'row' : 'rows'}`;
|
|
275
|
+
const copy = async () => {
|
|
276
|
+
try {
|
|
277
|
+
const csv = await text();
|
|
278
|
+
await navigator.clipboard.writeText(csv);
|
|
279
|
+
toast.success(`Copied ${counted} to the clipboard`);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
toast.error('The browser blocked the copy', {
|
|
283
|
+
action: { label: 'Download', onClick: () => void download() },
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const keep = (s) => (e) => {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
setScope(s);
|
|
290
|
+
};
|
|
291
|
+
const seg = (s, label, count) => (_jsxs(DropdownMenuItem, { onSelect: keep(s), "aria-checked": scope === s, role: "menuitemradio", className: cn('num justify-center whitespace-nowrap text-xs text-muted-foreground', scope === s && 'bg-accent text-accent-foreground'), children: [label, " \u00B7 ", count.toLocaleString()] }));
|
|
292
|
+
const items = (_jsxs(_Fragment, { children: [split && all && (_jsxs("div", { className: "grid grid-cols-2 gap-0.5 rounded-md border p-0.5", children: [seg('filtered', 'Filtered', rows.length), seg('all', 'All', all.length)] })), _jsxs(DropdownMenuLabel, { className: "num truncate text-xs font-normal text-muted-foreground", children: [counted, " \u00B7 ", columns.length, " ", columns.length === 1 ? 'column' : 'columns'] }), _jsxs(DropdownMenuItem, { onSelect: () => void download(), children: [_jsx(DownloadIcon, {}), " Download CSV"] }), _jsxs(DropdownMenuItem, { onSelect: () => void copy(), children: [_jsx(CopyIcon, {}), " Copy to clipboard"] })] }));
|
|
293
|
+
const button = (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(IconButton, { icon: DownloadIcon, label: "Export" }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-52", children: items })] }));
|
|
294
|
+
return { items, button };
|
|
295
|
+
}
|
|
236
296
|
/**
|
|
237
297
|
* LumenView (a card of its own): a data
|
|
238
298
|
* view over one domain, as a card. Its header has the view's name, the row count and pager, its ↻
|
|
239
299
|
* and ⋯, and its own filter and Group (they narrow this table only); its body
|
|
240
300
|
* the table (grouped one level, or the pivot) with the loading states.
|
|
241
301
|
*/
|
|
242
|
-
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, }) {
|
|
302
|
+
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, allRows, }) {
|
|
243
303
|
// Its controls (Edit widget ▸ Controls): Pager, Columns and Download here.
|
|
244
304
|
const on = (k) => controlOn({ kind: 'table', controls: shown }, k);
|
|
245
305
|
const cell = useGridCell();
|
|
@@ -267,6 +327,18 @@ export function LumenView({ domain, config, rows, total = rows.length, paging: s
|
|
|
267
327
|
// the page buttons sit in the card's header. Views saved before then
|
|
268
328
|
// carry no pagination, so they read the defaults.
|
|
269
329
|
const isMobile = useIsMobile();
|
|
330
|
+
const everyRow = useMemo(() => (allRows && !server && !groupRows ? mergeRows(domain, allRows, groupBy) : undefined), [allRows, server, groupRows, domain, groupBy]);
|
|
331
|
+
const exports = useExport({
|
|
332
|
+
name: title,
|
|
333
|
+
columns,
|
|
334
|
+
rows: tableRows,
|
|
335
|
+
all: everyRow,
|
|
336
|
+
groupBy,
|
|
337
|
+
sort: config.sort,
|
|
338
|
+
hidden: amountsHidden,
|
|
339
|
+
scopeKey: `${domain.id}:${cardId}`,
|
|
340
|
+
});
|
|
341
|
+
const exporter = !pivot && on('download') ? exports : null;
|
|
270
342
|
const pagination = config.pagination ?? viewPaginationSchema.parse({});
|
|
271
343
|
const [pageSize, setPageSize] = useState(pagination.size);
|
|
272
344
|
const [pageIndex, setPageIndex] = useState(0);
|
|
@@ -355,16 +427,7 @@ export function LumenView({ domain, config, rows, total = rows.length, paging: s
|
|
|
355
427
|
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
|
|
356
428
|
? { ...onAddColumn, field: onColumns && { domain, columns, onColumns } }
|
|
357
429
|
: undefined }));
|
|
358
|
-
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 })), !
|
|
359
|
-
// A phone's card header has room for the pager, filter, refresh and menu only.
|
|
360
|
-
className: "max-md:hidden", onClick: () => downloadCsv({
|
|
361
|
-
name: title,
|
|
362
|
-
columns,
|
|
363
|
-
rows: tableRows,
|
|
364
|
-
groupBy,
|
|
365
|
-
sort: config.sort,
|
|
366
|
-
hidden: amountsHidden,
|
|
367
|
-
}) }))] })), 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 ??
|
|
430
|
+
return (_jsx(LumenWidget, { id: cardId, kind: "table", style: "table", title: title, phoneTitle: false, controls: controls, menu: blank || !isMobile ? undefined : exporter?.items, tools: blank ? (tools) : (_jsxs(_Fragment, { children: [tools, compare, pager, onColumns && !onAddColumn && on('columns') && (_jsx(ColumnPicker, { domain: domain, columns: columns, onColumns: onColumns })), exporter && !isMobile && exporter.button] })), 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 ??
|
|
368
431
|
(load ? (_jsx(LumenStates, { load: load, skeleton: _jsx(LumenSkeleton, { rows: 10 }), children: table })) : (table)) }));
|
|
369
432
|
}
|
|
370
433
|
/**
|
package/dist/lumen-widget.d.ts
CHANGED
|
@@ -52,8 +52,10 @@ export interface CardLink {
|
|
|
52
52
|
/** Its own filter's linked chips (the phone's count beside the funnel). */
|
|
53
53
|
linked?: number;
|
|
54
54
|
}
|
|
55
|
-
export declare function LumenWidget({ id, kind, icon, title, note, controls, tools, phoneTitle, filter, filterCount, load, rows, loadNote, pad, pinned, compact, className, children, link: given, help, assistant, strip, foot, shown, style, }: {
|
|
55
|
+
export declare function LumenWidget({ id, kind, icon, title, note, controls, tools, phoneTitle, filter, filterCount, load, rows, loadNote, pad, pinned, compact, className, children, link: given, help, assistant, strip, foot, shown, style, menu, }: {
|
|
56
56
|
id: string;
|
|
57
|
+
/** Entries of its own at the top of the ⋯ menu (a table's Export on a phone). */
|
|
58
|
+
menu?: ReactNode;
|
|
57
59
|
/** Its controls (Edit widget ▸ Controls): Ask the assistant, Refresh, Filter box and
|
|
58
60
|
* Footer show or hide here; missing keys take the type's default. */
|
|
59
61
|
shown?: ViewCard['controls'];
|
package/dist/lumen-widget.js
CHANGED
|
@@ -180,7 +180,7 @@ function useWidth() {
|
|
|
180
180
|
}, []);
|
|
181
181
|
return [ref, width];
|
|
182
182
|
}
|
|
183
|
-
export function LumenWidget({ id, kind, icon, title, note, controls, tools, phoneTitle, filter, filterCount = 0, load, rows, loadNote, pad = true, pinned = false, compact = false, className, children, link: given, help, assistant, strip, foot, shown, style, }) {
|
|
183
|
+
export function LumenWidget({ id, kind, icon, title, note, controls, tools, phoneTitle, filter, filterCount = 0, load, rows, loadNote, pad = true, pinned = false, compact = false, className, children, link: given, help, assistant, strip, foot, shown, style, menu, }) {
|
|
184
184
|
const [removing, setRemoving] = useState(false);
|
|
185
185
|
const cell = useGridCell();
|
|
186
186
|
const links = useLinks();
|
|
@@ -234,7 +234,7 @@ export function LumenWidget({ id, kind, icon, title, note, controls, tools, phon
|
|
|
234
234
|
layout &&
|
|
235
235
|
"relative after:pointer-events-none after:absolute after:inset-0 after:z-30 after:rounded-[inherit] after:border-2 after:border-primary/40 after:border-dashed after:content-['']", flat && 'border-0 bg-transparent shadow-none',
|
|
236
236
|
// The corner ⋯ of a card with its header off sits over the body's top right.
|
|
237
|
-
headless && 'relative', className), children: [!headless && (_jsxs("div", { ref: head, className: cn('flex shrink-0 items-center gap-2 px-3', compact ? 'h-8' : 'h-10 border-b', (folded || below) && 'border-b-0', pinned && 'sticky left-0 w-(--view-w) max-w-full'), children: [layout && (_jsx(Button, { variant: "ghost", size: "icon-xs", className: "lumen-drag -ml-1 cursor-grab touch-none text-muted-foreground active:cursor-grabbing", "aria-label": `Drag ${title}`, children: _jsx(GripVerticalIcon, {}) })), link && !bare && (_jsx(LinkPicker, { color: link.color, onPick: link.onPick, label: title, className: "-mr-1" })), !compact && _jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: cn('truncate', compact ? 'text-xs text-muted-foreground' : 'text-sm font-semibold', phoneTitle === false && !layout && 'max-md:hidden'), children: title }), link?.color && link.follows && !folded && (_jsxs("span", { className: "min-w-0 shrink truncate text-xs", style: { color: linkVar(link.color) }, title: `Follows ${link.follows}`, children: ["\u00B7 ", link.follows] })), layout && headerOff && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Badge, { variant: "secondary", className: "shrink-0 gap-1 text-muted-foreground", children: [_jsx(EyeOffIcon, {}), " Header off"] }) }), _jsx(TooltipContent, { children: "Shown only in Edit layout. Change it in Edit widget \u25B8 Controls." })] })), !layout && !bare && (_jsx(FoldChevron, { folded: folded, label: title, onToggle: toggle, className: "-ml-1" })), folded && rows != null && (_jsxs("span", { className: "shrink-0 text-xs text-muted-foreground num", children: [rows.toLocaleString(), " ", rows === 1 ? 'row' : 'rows'] })), inline && (_jsx("div", { "data-card-filter": true, className: "flex min-w-40 max-w-xl flex-1 items-center gap-2", children: filter })), note && !layout && !folded && (_jsx("span", { className: "hidden truncate text-xs text-muted-foreground sm:inline", children: note })), _jsxs("span", { className: cn('ml-auto flex shrink-0 items-center gap-1', bare && 'hidden'), children: [!layout && load?.updated && _jsx(LumenFreshness, { load: load }), !layout && !folded && tools, showFilter && mobile && link?.color && !!link.linked && (_jsxs("span", { className: "num flex items-center gap-1 text-xs text-muted-foreground", title: `${link.linked} linked`, children: [_jsx(LinkDot, { color: link.color, className: "size-2" }), link.linked] })), showFilter && mobile && (_jsxs("span", { className: "relative", children: [_jsx(IconButton, { icon: FunnelIcon, label: filterOpen ? 'Hide filter' : 'Filter', "aria-expanded": filterOpen, className: cn(filterOpen && 'text-primary'), onClick: () => setFilterOpen(!filterOpen) }), filterCount > 0 && (_jsx(Badge, { className: "num pointer-events-none absolute -top-1 -right-1 h-4 min-w-4 px-1 text-[10px]", children: filterCount }))] })), !layout && !folded && help && _jsx(CardHelp, { title: title, children: help }), !layout && load && on('refresh') && (_jsx(LumenRefresh, { load: load, label: `Refresh ${title}` })), (controls?.editable || (!layout && (ask || load?.editScript))) && (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(IconButton, { icon: MoreHorizontalIcon, label: `${title} menu` }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-
|
|
237
|
+
headless && 'relative', className), children: [!headless && (_jsxs("div", { ref: head, className: cn('flex shrink-0 items-center gap-2 px-3', compact ? 'h-8' : 'h-10 border-b', (folded || below) && 'border-b-0', pinned && 'sticky left-0 w-(--view-w) max-w-full'), children: [layout && (_jsx(Button, { variant: "ghost", size: "icon-xs", className: "lumen-drag -ml-1 cursor-grab touch-none text-muted-foreground active:cursor-grabbing", "aria-label": `Drag ${title}`, children: _jsx(GripVerticalIcon, {}) })), link && !bare && (_jsx(LinkPicker, { color: link.color, onPick: link.onPick, label: title, className: "-mr-1" })), !compact && _jsx(Icon, { className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: cn('truncate', compact ? 'text-xs text-muted-foreground' : 'text-sm font-semibold', phoneTitle === false && !layout && 'max-md:hidden'), children: title }), link?.color && link.follows && !folded && (_jsxs("span", { className: "min-w-0 shrink truncate text-xs", style: { color: linkVar(link.color) }, title: `Follows ${link.follows}`, children: ["\u00B7 ", link.follows] })), layout && headerOff && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Badge, { variant: "secondary", className: "shrink-0 gap-1 text-muted-foreground", children: [_jsx(EyeOffIcon, {}), " Header off"] }) }), _jsx(TooltipContent, { children: "Shown only in Edit layout. Change it in Edit widget \u25B8 Controls." })] })), !layout && !bare && (_jsx(FoldChevron, { folded: folded, label: title, onToggle: toggle, className: "-ml-1" })), folded && rows != null && (_jsxs("span", { className: "shrink-0 text-xs text-muted-foreground num", children: [rows.toLocaleString(), " ", rows === 1 ? 'row' : 'rows'] })), inline && (_jsx("div", { "data-card-filter": true, className: "flex min-w-40 max-w-xl flex-1 items-center gap-2", children: filter })), note && !layout && !folded && (_jsx("span", { className: "hidden truncate text-xs text-muted-foreground sm:inline", children: note })), _jsxs("span", { className: cn('ml-auto flex shrink-0 items-center gap-1', bare && 'hidden'), children: [!layout && load?.updated && _jsx(LumenFreshness, { load: load }), !layout && !folded && tools, showFilter && mobile && link?.color && !!link.linked && (_jsxs("span", { className: "num flex items-center gap-1 text-xs text-muted-foreground", title: `${link.linked} linked`, children: [_jsx(LinkDot, { color: link.color, className: "size-2" }), link.linked] })), showFilter && mobile && (_jsxs("span", { className: "relative", children: [_jsx(IconButton, { icon: FunnelIcon, label: filterOpen ? 'Hide filter' : 'Filter', "aria-expanded": filterOpen, className: cn(filterOpen && 'text-primary'), onClick: () => setFilterOpen(!filterOpen) }), filterCount > 0 && (_jsx(Badge, { className: "num pointer-events-none absolute -top-1 -right-1 h-4 min-w-4 px-1 text-[10px]", children: filterCount }))] })), !layout && !folded && help && _jsx(CardHelp, { title: title, children: help }), !layout && load && on('refresh') && (_jsx(LumenRefresh, { load: load, label: `Refresh ${title}` })), (controls?.editable || (!layout && (ask || load?.editScript || menu))) && (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(IconButton, { icon: MoreHorizontalIcon, label: `${title} menu` }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-52", children: [ask && !layout && (_jsxs(DropdownMenuItem, { onSelect: ask, children: [_jsx(SparklesIcon, {}), " Ask ", assistantName] })), load?.editScript && !layout && (_jsxs(DropdownMenuItem, { onSelect: load.editScript, children: [_jsx(CodeIcon, {}), " Script"] })), menu && !layout && (_jsxs(_Fragment, { children: [(ask || load?.editScript) && _jsx(DropdownMenuSeparator, {}), menu] })), (ask || load?.editScript || menu) && !layout && controls?.editable && (_jsx(DropdownMenuSeparator, {})), controls?.editable && (_jsxs(_Fragment, { children: [!controls.fixed && (_jsxs(_Fragment, { children: [_jsxs(DropdownMenuItem, { onSelect: () => controls.onAction('edit'), children: [_jsx(PencilIcon, {}), " Edit widget"] }), _jsxs(DropdownMenuItem, { disabled: !own, onSelect: () => controls.onAction('duplicate'), children: [_jsx(CopyIcon, {}), " Duplicate"] })] })), _jsxs(DropdownMenuItem, { disabled: controls.first, onSelect: () => controls.onAction('up'), children: [_jsx(ArrowUpIcon, {}), " Move up"] }), _jsxs(DropdownMenuItem, { disabled: controls.last, onSelect: () => controls.onAction('down'), children: [_jsx(ArrowDownIcon, {}), " Move down"] }), controls.onFit && (_jsxs(DropdownMenuItem, { onSelect: controls.onFit, children: [_jsx(FoldVerticalIcon, {}), " Fit height"] })), !controls.fixed && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-muted-foreground focus:text-destructive", onSelect: () => setRemoving(true), children: [_jsx(Trash2Icon, {}), " Remove"] })] }))] }))] })] }))] })] })), below && !headless && (_jsx("div", { "data-card-filter": true, className: cn('flex items-center gap-2 border-b px-3 pb-2', pinned && 'sticky left-0 w-(--view-w) max-w-full'), children: filter })), strip && !layout && !folded && !headless ? (_jsx("div", { "data-card-strip": true, className: cn('flex min-w-0 items-center gap-2 border-b px-3 py-1.5 empty:hidden', pinned && 'sticky left-0 w-(--view-w) max-w-full'), children: strip })) : null, _jsx("div", { hidden: folded, className: cn('relative min-w-0',
|
|
238
238
|
// On the grid a card fills its cell: with a height of its own its body scrolls; one as
|
|
239
239
|
// tall as it draws keeps its body's height, so the grid can measure it.
|
|
240
240
|
!cell ? 'flex-1' : cell.fixed && 'min-h-0 flex-1 overflow-auto', pad && !table && !flat && (compact ? 'px-3 pb-3' : 'p-3')), children: _jsx(LumenBoundary, { where: id, children: children }) }), foot && !folded && (_jsx("div", { className: cn('shrink-0', pinned && 'sticky left-0 w-(--view-w) max-w-full'), children: foot })), headless && (_jsx(HeaderlessMenu, { title: title, ask: ask, askLabel: `Ask ${assistantName}`, load: load && on('refresh') ? load : undefined, editScript: load?.editScript, filter: showFilter ? filter : undefined, filterCount: filterCount })), !layout && !folded && load && on('footer') && (_jsx("div", { className: cn('mt-auto shrink-0', pinned && 'sticky left-0 w-(--view-w) max-w-full'), children: _jsx(LumenLoadFooter, { load: load, rows: rows, note: loadNote }) })), controls && (_jsx(LumenConfirm, { open: removing, onOpenChange: setRemoving, title: `Remove ${title}?`, description: table ? 'Add widget brings a table back.' : 'Add widget brings it back.', action: "Remove", onConfirm: () => controls.onAction('remove') }))] }) }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.180",
|
|
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.180",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|