@columenlabs/lumen-ui 0.1.39 → 0.1.41

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.
@@ -250,6 +250,9 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
250
250
  savedSortRef.current = savedSort;
251
251
  setHeldSort(config.sort);
252
252
  }, [savedSort, config.sort]);
253
+ // A server's page sorts on the server: it hears each sort held.
254
+ const onServerSort = paging?.onSort;
255
+ useEffect(() => onServerSort?.(heldSort ?? null), [onServerSort, heldSort]);
253
256
  const [details, setDetails] = useState(null);
254
257
  const onDetails = onOpen ?? (domain.details?.length ? setDetails : undefined);
255
258
  // The assistant sees the view, its data domain, its filter, how many rows show, the first of them and the
@@ -493,7 +496,8 @@ function ViewLayout({ crumbs, actions, meta, status, intro, cards: extra, lead,
493
496
  const viewCard = mainTableOf(cards);
494
497
  const viewOn = (k) => !viewCard || controlOn(viewCard, k);
495
498
  const showBox = !hideFilter && viewOn('filter');
496
- const groupButton = viewOn('group') ? group.button : null;
499
+ // A server's page cannot be grouped: a group would bucket the one page only.
500
+ const groupButton = viewOn('group') && !paging ? group.button : null;
497
501
  const viewFilter = !showBox && !groupButton ? undefined : (_jsxs(_Fragment, { children: [showBox && (_jsx(LumenTokenizedFilter, { filter: filter, label: `Filter ${config.name}`, className: "min-w-0 flex-1", saved: config.filters.default, link: {
498
502
  color: viewColor,
499
503
  onPick: (color) => links.setColor(viewCardId, color),
@@ -57,6 +57,11 @@ export interface ServerPaging {
57
57
  onPage: (page: number) => void;
58
58
  onSize?: (size: number) => void;
59
59
  sizes?: readonly number[];
60
+ /** The columns the server sorts by; the rest take no header sort, and the view groups nothing,
61
+ * since either would order or bucket one page only. */
62
+ sorts?: readonly string[];
63
+ /** Told the sort held (a header click, a revert), to fetch the rows in that order. */
64
+ onSort?: (sort: CustomViewConfig['sort']) => void;
60
65
  }
61
66
  export interface LumenViewProps<T> {
62
67
  /** The view's domain, its formula columns included (`withViewFields`). */
@@ -142,7 +147,7 @@ export interface AddColumn {
142
147
  * and ⋯, and its own filter and Group (they narrow this table only); its body
143
148
  * the table (grouped one level, or the pivot) with the loading states.
144
149
  */
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;
150
+ export declare function LumenView<T extends RowData>({ domain, config, rows, total, paging: server, groupBy: pickedGroup, pivot: pickedPivot, subgroup, load: sourceLoad, state, title, selection, onDetails, tapped, groupActions, controls, inLayout: inScroller, cardId, tools, filter, filterCount, strip, link, note, onColumns, onAddColumn, sort, onSort, shown, }: LumenViewProps<T>): import("react").JSX.Element;
146
151
  /** A field's colour for its header: its topic's source (a broker green, an app brown) where
147
152
  * the group names one, else its group's (Holding, Symbol, Account, Formulas). */
148
153
  export declare function colorOf<T>(domain: ViewDomain<T>, f: ViewField<T> | undefined): string | null;
@@ -175,6 +180,9 @@ export interface LumenTableProps<T> {
175
180
  sort?: CustomViewConfig['sort'];
176
181
  /** Told the sort a header click sets, so a page can hold or save it (rule C). */
177
182
  onSort?: (sort: CustomViewConfig['sort']) => void;
183
+ /** The rows come sorted by a server, which sorts by these columns only: a header click asks
184
+ * the server (through `onSort`) and the other headers take no click. */
185
+ serverSorts?: readonly string[];
178
186
  /** The + Add column head at the end of the header. */
179
187
  add?: AddColumn & {
180
188
  field?: {
@@ -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, 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, }) {
229
+ export function LumenView({ domain, config, rows, total = rows.length, paging: server, groupBy: pickedGroup = null, pivot: pickedPivot = false, subgroup, load: sourceLoad, state, title = config.name, selection, onDetails, tapped, groupActions, controls, inLayout: inScroller = false, cardId = 'view', tools, filter, filterCount, strip, link, note, onColumns, onAddColumn, sort = config.sort, onSort, shown, }) {
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();
@@ -234,6 +234,9 @@ export function LumenView({ domain, config, rows, total = rows.length, paging: s
234
234
  // scrolls with the page.
235
235
  const inLayout = inScroller && !cell;
236
236
  const columns = pick(domain.fields, config.columns);
237
+ // A server's page groups and pivots nothing: either would cover the one page only.
238
+ const groupBy = server ? null : pickedGroup;
239
+ const pivot = server ? false : pickedPivot;
237
240
  // Live columns on screen keep their source reading while the market trades (#861).
238
241
  const load = useLiveLoad(sourceLoad, columns.some((c) => {
239
242
  const r = freshnessOf(domain, c);
@@ -280,7 +283,7 @@ export function LumenView({ domain, config, rows, total = rows.length, paging: s
280
283
  const compare = openChart && picked.length > 1 ? (_jsx(IconButton, { icon: ChartSpline, label: `Compare ${picked.join(', ')}`, variant: "outline", onClick: () => openChart(picked) })) : null;
281
284
  // A server's page: the pager counts from the server's total, or the rows seen so far.
282
285
  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 }));
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
286
+ 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, serverSorts: server ? (server.sorts ?? []) : undefined, total: config.total !== false, add: onAddColumn || onColumns
284
287
  ? { ...onAddColumn, field: onColumns && { domain, columns, onColumns } }
285
288
  : undefined }));
286
289
  return (_jsx(LumenWidget, { id: cardId, kind: "table", style: "table", title: title, controls: controls, tools: state ? (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", onClick: () => downloadCsv({
@@ -349,8 +352,15 @@ export function LumenTable({ load, ...props }) {
349
352
  return _jsx(LumenTableRows, { ...props });
350
353
  return (_jsx("div", { className: "relative", children: _jsx(LumenStates, { load: load, children: _jsx(LumenTableRows, { ...props }) }) }));
351
354
  }
352
- function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null, chips = [], page = null, inView = false, fit = false, onDetails, tapped, selection, groupActions, sort, onSort, total = true, subgroup = null, add, }) {
353
- const [sorting, setSorting] = useState(sort && columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : []);
355
+ /** The table's sort from a view's: none when its column is not shown, or the server cannot sort
356
+ * by it. */
357
+ function sortingOf(sort, columns, serverSorts) {
358
+ if (!sort || (serverSorts && !serverSorts.includes(sort.key)))
359
+ return [];
360
+ return columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : [];
361
+ }
362
+ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null, chips = [], page = null, inView = false, fit = false, onDetails, tapped, selection, groupActions, sort, onSort, serverSorts, total = true, subgroup = null, add, }) {
363
+ const [sorting, setSorting] = useState(() => sortingOf(sort, columns, serverSorts));
354
364
  // A saved sort patched while the table is open (the assistant's card move) applies at once.
355
365
  const savedSort = JSON.stringify(sort ?? null);
356
366
  const savedSortRef = useRef(savedSort);
@@ -358,8 +368,8 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
358
368
  if (savedSortRef.current === savedSort)
359
369
  return;
360
370
  savedSortRef.current = savedSort;
361
- setSorting(sort && columns.some((f) => f.key === sort.key) ? [{ id: sort.key, desc: sort.desc }] : []);
362
- }, [savedSort, sort, columns]);
371
+ setSorting(sortingOf(sort, columns, serverSorts));
372
+ }, [savedSort, sort, columns, serverSorts]);
363
373
  const paging = page !== null && !groupBy;
364
374
  const pagination = {
365
375
  pageIndex: paging ? page.index : 0,
@@ -451,6 +461,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
451
461
  return computed.value(now[i] ?? f, r) ?? undefined;
452
462
  },
453
463
  sortUndefined: 'last',
464
+ enableSorting: !serverSorts || serverSorts.includes(f.key),
454
465
  header: f.label,
455
466
  cell: ({ row }) => {
456
467
  const { shown: now, cellOf, inline, chips, computed } = latest.current;
@@ -463,12 +474,14 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
463
474
  return [{ key: x.key, node: cellOf(x, row.original, true) }];
464
475
  }) })) : (cellOf(field, row.original, chip));
465
476
  },
466
- })), [shownKeys, inlineOn]);
477
+ })), [shownKeys, inlineOn, serverSorts?.join(',')]);
467
478
  const table = useTable({
468
479
  features: SORTING,
469
480
  data: rows,
470
481
  columns: defs,
471
482
  state: { sorting, pagination },
483
+ // A server's rows come sorted; the table only shows which column it sorted by.
484
+ manualSorting: !!serverSorts,
472
485
  onSortingChange: (u) => {
473
486
  const next = typeof u === 'function' ? u(sorting) : u;
474
487
  setSorting(next);
@@ -604,7 +617,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
604
617
  ? 'ascending'
605
618
  : h.column.getIsSorted() === 'desc'
606
619
  ? 'descending'
607
- : undefined, children: [i === 0 && groups && (_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.getToggleSortingHandler(), 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 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center text-muted-foreground", children: "No rows match." }) })), groups && groupBy
620
+ : undefined, children: [i === 0 && groups && (_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 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center text-muted-foreground", children: "No rows match." }) })), groups && groupBy
608
621
  ? groups.map(([key, members], gi) => {
609
622
  const open = isOpen(key, gi);
610
623
  return (_jsxs(Fragment, { children: [_jsxs(TableRow, { className: ROLLUP, "data-group": key, children: [selection && (_jsx(TableCell, { className: cn('w-8 pr-0', PINNED_GROUP), children: _jsx(Checkbox, { checked: members.every((m) => selection.keys.has(keyOf(m.original)))
package/dist/ui/tabs.d.ts CHANGED
@@ -2,6 +2,6 @@ import { Tabs as TabsPrimitive } from 'radix-ui';
2
2
  import type * as React from 'react';
3
3
  declare function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>): React.JSX.Element;
4
4
  declare function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>): React.JSX.Element;
5
- declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.JSX.Element;
5
+ declare function TabsTrigger({ className, onMouseDown, onKeyDown, onBlur, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.JSX.Element;
6
6
  declare function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>): React.JSX.Element;
7
7
  export { Tabs, TabsList, TabsTrigger, TabsContent };
package/dist/ui/tabs.js CHANGED
@@ -13,8 +13,22 @@ function Tabs({ className, ...props }) {
13
13
  function TabsList({ className, ...props }) {
14
14
  return (_jsx(TabsPrimitive.List, { "data-slot": "tabs-list", className: cn('inline-flex w-full items-center justify-start gap-6 overflow-x-auto overflow-y-hidden text-muted-foreground [scrollbar-width:none]', className), ...props }));
15
15
  }
16
- function TabsTrigger({ className, ...props }) {
17
- return (_jsx(TabsPrimitive.Trigger, { "data-slot": "tabs-trigger", className: cn("relative inline-flex shrink-0 items-center gap-1.5 px-0 pt-0 pb-2 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-full after:bg-transparent text-sm leading-4 font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:rounded-sm focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground data-[state=active]:after:bg-primary data-[state=active]:[&_svg]:text-primary [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props }));
16
+ // Radix focuses a trigger from its own mousedown, which browsers count as :focus-visible, so a
17
+ // clicked tab drew the keyboard ring over its underline. A pointer press marks the trigger until
18
+ // the next key or blur; the ring shows only without that mark.
19
+ const pointerOn = (e) => e.currentTarget.setAttribute('data-pointer', '');
20
+ const pointerOff = (e) => e.currentTarget.removeAttribute('data-pointer');
21
+ function TabsTrigger({ className, onMouseDown, onKeyDown, onBlur, ...props }) {
22
+ return (_jsx(TabsPrimitive.Trigger, { "data-slot": "tabs-trigger", onMouseDown: (e) => {
23
+ pointerOn(e);
24
+ onMouseDown?.(e);
25
+ }, onKeyDown: (e) => {
26
+ pointerOff(e);
27
+ onKeyDown?.(e);
28
+ }, onBlur: (e) => {
29
+ pointerOff(e);
30
+ onBlur?.(e);
31
+ }, className: cn("relative inline-flex shrink-0 items-center gap-1.5 px-0 pt-0 pb-2 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-full after:bg-transparent text-sm leading-4 font-medium whitespace-nowrap text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:rounded-sm focus-visible:not-data-[pointer]:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground data-[state=active]:after:bg-primary data-[state=active]:[&_svg]:text-primary [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props }));
18
32
  }
19
33
  function TabsContent({ className, ...props }) {
20
34
  return (_jsx(TabsPrimitive.Content, { "data-slot": "tabs-content", className: cn('flex-1 outline-none', className), ...props }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
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.39",
44
+ "@columenlabs/lumen-ui-types": "0.1.41",
45
45
  "@dnd-kit/core": "6.3.1",
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@dnd-kit/utilities": "3.2.2",