@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.
@@ -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)
@@ -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
  } }));
@@ -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;
@@ -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 a CSV file: the filtered rows in the view's sort, its
200
- * shown columns (the grouping first when it is not one), plain numbers; amounts stay masked while
201
- * Hide amounts is on. The CSV writer loads on the first download, not with the page.
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 downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
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
- const csv = Papa.unparse({
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 })), !pivot && on('download') && (_jsx(IconButton, { icon: DownloadIcon, label: "Download CSV",
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
  /**
@@ -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'];
@@ -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-44", children: [ask && !layout && (_jsxs(DropdownMenuItem, { onSelect: ask, children: [_jsx(SparklesIcon, {}), " Ask ", assistantName] })), load?.editScript && !layout && (_jsxs(DropdownMenuItem, { onSelect: load.editScript, children: [_jsx(CodeIcon, {}), " Script"] })), (ask || load?.editScript) && !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',
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.179",
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.179",
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",