@columenlabs/lumen-ui 0.1.36 → 0.1.38

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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type CustomViewConfig, LumenTable, type LumenTableProps, LumenView, type LumenViewProps, type ViewCellValue, type ViewChipTone, type ViewDomain, type ViewField, type ViewFieldGroup, type ViewSource, CHIP_TONE, RUN_STATE_TONE, FORMULAS_GROUP, ViewValueCell, rollUp, } from './lumen-view.js';
1
+ export { type CustomViewConfig, LumenTable, type LumenTableProps, LumenView, type LumenViewProps, type ServerPaging, type ViewCellValue, type ViewChipTone, type ViewDomain, type ViewField, type ViewFieldGroup, type ViewSource, CHIP_TONE, RUN_STATE_TONE, FORMULAS_GROUP, ViewValueCell, rollUp, } from './lumen-view.js';
2
2
  export { LumenLayout, type LumenLayoutCard, type LumenLayoutProps, type LumenLayoutViewProps, domainKeys, followRows, } from './lumen-layout.js';
3
3
  export { LumenViewCard, type LumenViewCardProps } from './lumen-view-card.js';
4
4
  export { LumenSelector, LumenSelectorList, type LumenSelectorListHandle, type LumenSelectorProps, type LumenOption, type LumenOptionGroup, SelectorRowContent, } from './lumen-selector.js';
@@ -5,7 +5,7 @@ import { type ReactElement, type ReactNode } from 'react';
5
5
  import { type FilterToken } from './lumen-filter.js';
6
6
  import { type LinkColor } from './lumen-links.js';
7
7
  import { type LumenLoad } from './lumen-source.js';
8
- import { type LumenViewProps } from './lumen-view.js';
8
+ import { type LumenViewProps, type ServerPaging } from './lumen-view.js';
9
9
  import { type CardControls, type CardLink } from './lumen-widget.js';
10
10
  import { type Crumb } from './page.js';
11
11
  import { type ViewEditorSection } from './view-editor.js';
@@ -115,6 +115,9 @@ export interface LumenLayoutViewProps<T> {
115
115
  /** Told the filter's tokens whenever they change, so an app can load rows for what they pick
116
116
  * (the Symbols view's `in` filter). */
117
117
  onTokens?: (tokens: FilterToken[]) => void;
118
+ /** The rows are one page of a server's, already filtered by `onTokens`' chips: the table
119
+ * shows them as given and its pager pages through the server. */
120
+ paging?: ServerPaging;
118
121
  /** Buttons in the view card's header ("+ Add time"). */
119
122
  viewTools?: ReactNode;
120
123
  /** False leaves Field out of the header's + Add column (a table whose columns are fixed, a
@@ -213,7 +213,7 @@ function LayoutTable({ card, base, config, rows, load, state, onDetails, control
213
213
  const domain = useMemo(() => withViewFields(base, own), [base, own]);
214
214
  return (_jsx(LumenViewCard, { id: card.id, domain: domain, config: own, rows: rows, load: load, state: state, title: card.title ?? config.name, onDetails: onDetails, controls: controls, shown: card.controls, onColumns: onLayout && ((columns) => onLayout(tableLayoutOf({ ...own, columns }))) }));
215
215
  }
216
- function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead, domain: base, config, rows, onConfigChange: saveConfig, editable: canEdit, editor, onDuplicate, onDelete, onReset, load, state, tile, rowTitle, viewId, onShownRows, onSummaryRows, selection, onOpen, rowPick: initialPick, groupActions, onTokens, viewTools, columnPicker, hideFilter, save: saveMode = 'document', }) {
216
+ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead, domain: base, config, rows, onConfigChange: saveConfig, editable: canEdit, editor, onDuplicate, onDelete, onReset, load, state, tile, rowTitle, viewId, onShownRows, onSummaryRows, selection, onOpen, rowPick: initialPick, groupActions, onTokens, paging, viewTools, columnPicker, hideFilter, save: saveMode = 'document', }) {
217
217
  const onConfigChange = canEdit === false ? undefined : saveConfig;
218
218
  // The registry's join fields for the keys the rows carry (Symbol, Account), then the view's own
219
219
  // formulas and formats.
@@ -326,7 +326,9 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
326
326
  const viewFollows = followed(viewCardId);
327
327
  const viewFollowKey = JSON.stringify(viewFollows);
328
328
  // biome-ignore lint/correctness/useExhaustiveDependencies: keyed on what the table follows.
329
- const viewRows = useMemo(() => narrow(filter.results, viewFollows), [filter.results, viewFollowKey, widgetFields]);
329
+ const viewRows = useMemo(
330
+ // A server's page is filtered there: its chips do not narrow it again here.
331
+ () => narrow(paging ? rows : filter.results, viewFollows), [filter.results, viewFollowKey, widgetFields, paging ? rows : null]);
330
332
  useEffect(() => onShownRows?.(viewRows), [onShownRows, viewRows]);
331
333
  const linksChanged = saved.some((c) => (colors[c.id] ?? null) !== (c.link ?? null));
332
334
  const cardLink = (id, follows, linked) => ({
@@ -539,7 +541,7 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
539
541
  return {
540
542
  ...place,
541
543
  wide: isTableCard(c) && c.w === GRID_COLS && c.h === undefined,
542
- node: c.source ? (_jsx(SourcedCard, { card: c, name: config.name, controls: controls })) : isTableCard(c) && c.id !== viewCardId ? (_jsx(LayoutTable, { card: c, base: joins.domain, config: config, rows: c.filter.length ? filterByTokens(rows, widgetFields, c.filter) : rows, load: scope(c.id, load), state: state, onDetails: onDetails, controls: controls, onLayout: onConfigChange ? (table) => putCard({ ...c, table }) : undefined })) : isTableCard(c) ? (_jsx(LumenView, { domain: domain, config: config, rows: viewRows, total: rows.length, groupBy: group.groupField, pivot: pivot, subgroup: group.sub, load: scope(c.id, load), state: state, title: c.title ?? config.name, selection: selection, onDetails: rowPick.onTap ?? onDetails, tapped: rowPick.tapped, groupActions: groupActions, controls: controls, inLayout: true, tools: viewTools, filter: viewFilter, filterCount: filter.tokens.length, strip: _jsx(ConnectOffer, { domain: domain, rows: rows, config: config, read: joins.read, cardKey: viewId ?? config.name, onConnect: onConfigChange ? connect : undefined }), link: cardLink(c.id, viewFollows, viewColor ? filter.tokens.length : 0), shown: c.controls, onColumns: onConfigChange && columnPicker !== false
544
+ node: c.source ? (_jsx(SourcedCard, { card: c, name: config.name, controls: controls })) : isTableCard(c) && c.id !== viewCardId ? (_jsx(LayoutTable, { card: c, base: joins.domain, config: config, rows: c.filter.length ? filterByTokens(rows, widgetFields, c.filter) : rows, load: scope(c.id, load), state: state, onDetails: onDetails, controls: controls, onLayout: onConfigChange ? (table) => putCard({ ...c, table }) : undefined })) : isTableCard(c) ? (_jsx(LumenView, { domain: domain, config: config, rows: viewRows, total: rows.length, paging: paging, groupBy: group.groupField, pivot: pivot, subgroup: group.sub, load: scope(c.id, load), state: state, title: c.title ?? config.name, selection: selection, onDetails: rowPick.onTap ?? onDetails, tapped: rowPick.tapped, groupActions: groupActions, controls: controls, inLayout: true, tools: viewTools, filter: viewFilter, filterCount: filter.tokens.length, strip: _jsx(ConnectOffer, { domain: domain, rows: rows, config: config, read: joins.read, cardKey: viewId ?? config.name, onConnect: onConfigChange ? connect : undefined }), link: cardLink(c.id, viewFollows, viewColor ? filter.tokens.length : 0), shown: c.controls, onColumns: onConfigChange && columnPicker !== false
543
545
  ? (columns) => onConfigChange({ ...config, columns })
544
546
  : 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
545
547
  ? cardOf(c)
@@ -42,6 +42,22 @@ export declare function DetailsBody<T extends RowData>({ domain, row, columns, c
42
42
  columns: ViewField<T>[];
43
43
  chips: ViewChip[];
44
44
  }): import("react").JSX.Element;
45
+ /**
46
+ * Rows a server pages: the view shows `rows` as one page, as given (the server already filtered
47
+ * and sorted them), and the pager turns pages through `onPage`.
48
+ */
49
+ export interface ServerPaging {
50
+ /** The page shown, from 0. */
51
+ page: number;
52
+ size: number;
53
+ /** Every row the server has for the filter; null when it does not count them. */
54
+ total: number | null;
55
+ /** With no `total`: whether a page follows this one. */
56
+ more: boolean;
57
+ onPage: (page: number) => void;
58
+ onSize?: (size: number) => void;
59
+ sizes?: readonly number[];
60
+ }
45
61
  export interface LumenViewProps<T> {
46
62
  /** The view's domain, its formula columns included (`withViewFields`). */
47
63
  domain: ViewDomain<T>;
@@ -50,6 +66,8 @@ export interface LumenViewProps<T> {
50
66
  rows: T[];
51
67
  /** Every row before the filter, for "12 of 40 rows"; left out: `rows`. */
52
68
  total?: number;
69
+ /** The rows are one page of a server's: the pager pages through the server. */
70
+ paging?: ServerPaging;
53
71
  /** The grouping the page picked (its Group by switch); null: flat rows. */
54
72
  groupBy?: ViewField<T> | null;
55
73
  /** Shows the view's pivot instead of its rows. */
@@ -124,7 +142,7 @@ export interface AddColumn {
124
142
  * and ⋯, and its own filter and Group (they narrow this table only); its body
125
143
  * the table (grouped one level, or the pivot) with the loading states.
126
144
  */
127
- export declare function LumenView<T extends RowData>({ domain, config, rows, total, groupBy, pivot, subgroup, load: sourceLoad, state, title, selection, onDetails, tapped, groupActions, controls, inLayout: inScroller, cardId, tools, filter, filterCount, strip, link, note, onColumns, onAddColumn, sort, onSort, shown, }: LumenViewProps<T>): import("react").JSX.Element;
145
+ export declare function LumenView<T extends RowData>({ domain, config, rows, total, paging: server, groupBy, pivot, subgroup, load: sourceLoad, state, title, selection, onDetails, tapped, groupActions, controls, inLayout: inScroller, cardId, tools, filter, filterCount, strip, link, note, onColumns, onAddColumn, sort, onSort, shown, }: LumenViewProps<T>): import("react").JSX.Element;
128
146
  /** A field's colour for its header: its topic's source (a broker green, an app brown) where
129
147
  * the group names one, else its group's (Holding, Symbol, Account, Formulas). */
130
148
  export declare function colorOf<T>(domain: ViewDomain<T>, f: ViewField<T> | undefined): string | null;
@@ -226,7 +226,7 @@ function downloadCsv({ name, columns, rows, groupBy, sort, hidden, }) {
226
226
  * and ⋯, and its own filter and Group (they narrow this table only); its body
227
227
  * the table (grouped one level, or the pivot) with the loading states.
228
228
  */
229
- export function LumenView({ domain, config, rows, total = rows.length, groupBy = null, pivot = false, subgroup, load: sourceLoad, state, title = config.name, selection, onDetails, tapped, groupActions, controls, inLayout: inScroller = false, cardId = 'view', tools, filter, filterCount, strip, link, note, onColumns, onAddColumn, sort = config.sort, onSort, shown, }) {
229
+ export function LumenView({ domain, config, rows, total = rows.length, paging: server, groupBy = null, pivot = false, subgroup, load: sourceLoad, state, title = config.name, selection, onDetails, tapped, groupActions, controls, inLayout: inScroller = false, cardId = 'view', tools, filter, filterCount, strip, link, note, onColumns, onAddColumn, sort = config.sort, onSort, shown, }) {
230
230
  // Its controls (Edit widget ▸ Controls): Pager, Columns and Download here.
231
231
  const on = (k) => controlOn({ kind: 'table', controls: shown }, k);
232
232
  const cell = useGridCell();
@@ -252,7 +252,7 @@ export function LumenView({ domain, config, rows, total = rows.length, groupBy =
252
252
  useEffect(() => setPageSize(pagination.size), [pagination.size]);
253
253
  const sub = subgroupOf(domain, subgroup === undefined ? config.subgroup : subgroup);
254
254
  // Folded lines and their rows stay on one page, so a fold shows every row like a grouping.
255
- const paging = pagination.on && on('pager') && !state && !groupBy && !pivot && !sub?.under;
255
+ const paging = !server && pagination.on && on('pager') && !state && !groupBy && !pivot && !sub?.under;
256
256
  // A fit table wraps its text to the width there is; the rest scroll sideways.
257
257
  // A phone has no width to fit to: every table scrolls sideways there.
258
258
  const fit = config.width === 'fit' && !pivot && !isMobile;
@@ -278,7 +278,8 @@ export function LumenView({ domain, config, rows, total = rows.length, groupBy =
278
278
  return [...out].slice(0, MAX_COMPARE);
279
279
  }, [selection, chartField, rows, domain]);
280
280
  const compare = openChart && picked.length > 1 ? (_jsx(IconButton, { icon: ChartSpline, label: `Compare ${picked.join(', ')}`, variant: "outline", onClick: () => openChart(picked) })) : null;
281
- const pager = paging ? (_jsx(LumenPager, { total: tableRows.length, page: page, size: pageSize, onPage: setPageIndex, onSize: setPageSize, noun: rows.length === total ? 'rows' : `of ${total} rows` })) : (_jsx("span", { className: "num px-2 text-xs text-muted-foreground", children: count }));
281
+ // A server's page: the pager counts from the server's total, or the rows seen so far.
282
+ const pager = server ? (_jsx(LumenPager, { total: server.total ?? server.page * server.size + rows.length, page: server.page, size: server.size, onPage: server.onPage, onSize: server.onSize ?? (() => { }), sizes: server.sizes ?? [server.size], more: server.total === null && server.more })) : paging ? (_jsx(LumenPager, { total: tableRows.length, page: page, size: pageSize, onPage: setPageIndex, onSize: setPageSize, noun: rows.length === total ? 'rows' : `of ${total} rows` })) : (_jsx("span", { className: "num px-2 text-xs text-muted-foreground", children: count }));
282
283
  const table = pivot && config.pivot ? (_jsx(PivotTable, { domain: domain, rows: rows, pivot: config.pivot })) : (_jsx(LumenTable, { domain: domain, columns: columns, rows: tableRows, groupBy: groupBy, subgroup: sub, chips: chips, page: paging ? { index: page, size: pageSize } : null, inView: inLayout, fit: fit, onDetails: onDetails, tapped: tapped, selection: selection, groupActions: groupActions, sort: sort, onSort: onSort, total: config.total !== false, add: onAddColumn || onColumns
283
284
  ? { ...onAddColumn, field: onColumns && { domain, columns, onColumns } }
284
285
  : undefined }));
package/dist/pager.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export declare function LumenPager({ total, page, size, onPage, onSize, noun, sizes, }: {
1
+ export declare function LumenPager({ total, page, size, onPage, onSize, noun, sizes, more, }: {
2
+ /** The rows there are, or (with `more`) the rows known so far. */
2
3
  total: number;
3
4
  page: number;
4
5
  size: number;
@@ -7,4 +8,6 @@ export declare function LumenPager({ total, page, size, onPage, onSize, noun, si
7
8
  noun?: string;
8
9
  /** Page sizes offered; a server-paged source takes its own. */
9
10
  sizes?: readonly number[];
11
+ /** A server-paged source has rows past `total` it has not counted: shows `of 50+` and Next. */
12
+ more?: boolean;
10
13
  }): import("react").JSX.Element;
package/dist/pager.js CHANGED
@@ -6,9 +6,9 @@ import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
6
6
  import { IconButton } from './icon-button.js';
7
7
  import { Button } from './ui/button.js';
8
8
  import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
9
- export function LumenPager({ total, page, size, onPage, onSize, noun = 'rows', sizes = viewPageSizes, }) {
10
- const pages = Math.max(1, Math.ceil(total / size));
9
+ export function LumenPager({ total, page, size, onPage, onSize, noun = 'rows', sizes = viewPageSizes, more = false, }) {
10
+ const pages = Math.max(1, Math.ceil(total / size)) + (more ? 1 : 0);
11
11
  return (_jsxs("nav", { "aria-label": "Pages", className: "flex items-center", children: [_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", className: "num h-7 px-2 text-xs font-normal text-muted-foreground", "aria-label": "Rows per page", children: pages > 1
12
- ? `${page * size + 1}–${Math.min((page + 1) * size, total)} of ${total}`
12
+ ? `${page * size + 1}–${Math.min((page + 1) * size, total)} of ${total}${more ? '+' : ''}`
13
13
  : `${total} ${noun}` }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-40", children: _jsx(DropdownMenuRadioGroup, { value: String(size), onValueChange: (v) => onSize(Number(v)), children: sizes.map((n) => (_jsxs(DropdownMenuRadioItem, { value: String(n), className: "num whitespace-nowrap", children: [n, " per page"] }, n))) }) })] }), pages > 1 && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: ChevronLeftIcon, label: "Previous page", disabled: page === 0, onClick: () => onPage(page - 1) }), _jsx(IconButton, { icon: ChevronRightIcon, label: "Next page", disabled: page >= pages - 1, onClick: () => onPage(page + 1) })] }))] }));
14
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "@codemirror/lang-javascript": "6.2.5",
42
42
  "@codemirror/lang-json": "6.0.2",
43
43
  "@codemirror/lang-sql": "6.10.0",
44
- "@columenlabs/lumen-ui-types": "0.1.36",
44
+ "@columenlabs/lumen-ui-types": "0.1.38",
45
45
  "@dnd-kit/core": "6.3.1",
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@dnd-kit/utilities": "3.2.2",