@columenlabs/lumen-ui 0.1.111 → 0.1.113

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/color-dot.js CHANGED
@@ -33,5 +33,11 @@ export function tintedIcon(icon, color) {
33
33
  }
34
34
  /** A value as a chip in its colour (a label): a tint behind, the colour as text, its icon first. */
35
35
  export function ColorChip({ color, icon: Icon, children, }) {
36
- return (_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-sm bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground", style: color ? { background: `color-mix(in oklab, ${color} 18%, transparent)`, color } : undefined, children: [Icon && _jsx(Icon, { className: "size-3 shrink-0" }), children] }));
36
+ return (_jsxs("span", { className: "inline-flex h-5 shrink-0 items-center gap-1 rounded-sm bg-secondary px-1.5 text-xs font-medium whitespace-nowrap text-secondary-foreground", style: color
37
+ ? {
38
+ background: `color-mix(in oklab, ${color} 18%, transparent)`,
39
+ // The colour pulled toward the text colour, so a light hue (yellow, sky) still reads.
40
+ color: `color-mix(in oklab, ${color} 55%, var(--foreground))`,
41
+ }
42
+ : undefined, children: [Icon && _jsx(Icon, { className: "size-3 shrink-0" }), children] }));
37
43
  }
@@ -143,7 +143,10 @@ function Cell({ id, fixed, report, children, }) {
143
143
  return () => ro.disconnect();
144
144
  });
145
145
  const value = useMemo(() => ({ fixed }), [fixed]);
146
- return (_jsx(GridCellContext.Provider, { value: value, children: _jsx("div", { ref: ref, className: "h-full [&>*:has(>[data-slot=lumen-card])]:h-full", style: { paddingBottom: GRID_GAP }, children: children }) }));
146
+ return (_jsx(GridCellContext.Provider, { value: value, children: _jsx("div", { ref: ref,
147
+ // The cell's card fills it (a folded one keeps its header's height); nowhere else does a
148
+ // card take its parent's height.
149
+ className: "h-full [&>*:has(>[data-slot=lumen-card])]:h-full [&>*>[data-slot=lumen-card]:not([data-folded])]:h-full [&>[data-slot=lumen-card]:not([data-folded])]:h-full", style: { paddingBottom: GRID_GAP }, children: children }) }));
147
150
  }
148
151
  const gridConfig = (phone) => ({
149
152
  cols: phone ? GRID_PHONE_COLS : GRID_COLS,
@@ -172,7 +172,7 @@ export function LinkPicker({ color, onPick, label, className, }) {
172
172
  onPick(c);
173
173
  setOpen(false);
174
174
  };
175
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-xs", className: cn('size-5 shrink-0', className), "aria-label": `${label} link colour: ${color ? LINK_NAMES[color] : 'None'}`, title: color ? `Linked: ${LINK_NAMES[color]}` : 'Link colour', children: _jsx(LinkDot, { color: color }) }) }), _jsxs(PopoverContent, { align: "start", className: "flex w-auto flex-col gap-2 p-2", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Link colour" }), _jsxs("div", { className: "flex items-center gap-1", children: [LINK_COLORS.map((c) => (_jsx(Button, { variant: c === color ? 'secondary' : 'ghost', size: "icon-sm", "aria-label": LINK_NAMES[c], "aria-pressed": c === color, title: LINK_NAMES[c], onClick: () => pick(c), children: _jsx(LinkDot, { color: c, className: "size-3.5" }) }, c))), _jsx(Button, { variant: color === null ? 'secondary' : 'ghost', size: "sm", className: "h-8 px-2 text-xs", "aria-pressed": color === null, onClick: () => pick(null), children: "None" })] })] })] }));
175
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-xs", className: cn('size-5 shrink-0 max-md:size-6', className), "aria-label": `${label} link colour: ${color ? LINK_NAMES[color] : 'None'}`, title: color ? `Linked: ${LINK_NAMES[color]}` : 'Link colour', children: _jsx(LinkDot, { color: color }) }) }), _jsxs(PopoverContent, { align: "start", className: "flex w-auto flex-col gap-2 p-2", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Link colour" }), _jsxs("div", { className: "flex items-center gap-1", children: [LINK_COLORS.map((c) => (_jsx(Button, { variant: c === color ? 'secondary' : 'ghost', size: "icon-sm", "aria-label": LINK_NAMES[c], "aria-pressed": c === color, title: LINK_NAMES[c], onClick: () => pick(c), children: _jsx(LinkDot, { color: c, className: "size-3.5" }) }, c))), _jsx(Button, { variant: color === null ? 'secondary' : 'ghost', size: "sm", className: "h-8 px-2 text-xs", "aria-pressed": color === null, onClick: () => pick(null), children: "None" })] })] })] }));
176
176
  }
177
177
  /**
178
178
  * Row-tap filtering (keyed): in a coloured card a row tap picks the
@@ -8,7 +8,7 @@ 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, ArrowUpIcon, ChartCandlestick, ChartSpline, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
11
+ import { ArrowDownIcon, ArrowDownWideNarrowIcon, ArrowUpIcon, ArrowUpNarrowWideIcon, ChartCandlestick, ChartSpline, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
12
12
  import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
13
  import { ColorChip, ColorDot } from './color-dot.js';
14
14
  import { DrawerSection } from './drawer-section.js';
@@ -558,6 +558,16 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
558
558
  },
559
559
  autoResetPageIndex: false,
560
560
  });
561
+ // A phone's cards hide the header row, so a Sort by picker and a direction toggle stand in for
562
+ // the header clicks (numbers and dates start high to low, text A to Z, as a header click does).
563
+ const sortBy = sorting[0];
564
+ const sortable = shown.filter((f) => !serverSorts || serverSorts.includes(f.key));
565
+ const cardSort = sortable.length > 1 && (_jsxs("div", { "data-slot": "card-sort", className: "hidden items-center justify-end gap-1 px-(--cell-x) py-1 max-sm:flex", children: [_jsx(LumenSelector, { label: "Sort by", size: "sm", placeholder: "Sort by", options: sortable.map((f) => ({ value: f.key, label: f.label, icon: f.icon })), value: sortBy?.id ?? null, onChange: (id) => table.setSorting([
566
+ {
567
+ id,
568
+ desc: sortBy?.id === id ? sortBy.desc : sortable.some((f) => f.key === id && short(f)),
569
+ },
570
+ ]) }), sortBy && (_jsx(IconButton, { icon: sortBy.desc ? ArrowDownWideNarrowIcon : ArrowUpNarrowWideIcon, label: sortBy.desc ? 'High to low' : 'Low to high', onClick: () => table.setSorting([{ id: sortBy.id, desc: !sortBy.desc }]) }))] }));
561
571
  // Phone: a long press on a header opens the Columns picker as a sheet.
562
572
  const [fieldOpen, setFieldOpen] = useState(false);
563
573
  const press = useRef(null);
@@ -731,41 +741,41 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
731
741
  };
732
742
  const box = useRef(null);
733
743
  useFloatingHead(box, !inView);
734
- return (_jsx("div", { ref: box, className: cn(inView ? IN_VIEW : 'overflow-x-auto overflow-y-hidden [--pin-top:0px]', fit && FIT), children: _jsx(UnitColumns.Provider, { value: markUnits, children: _jsxs(Table, { "data-cards": "", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => (_jsxs(TableRow, { children: [selection && (_jsx(TableHead, { className: cn('w-8 pr-0', PINNED_HEAD), children: _jsx(Checkbox, { checked: allOn ? true : someOn ? 'indeterminate' : false, onCheckedChange: (on) => toggle(allKeys, on === true), "aria-label": "Select every row" }) })), hg.headers.map((h, i) => (_jsxs(TableHead, { ...col(shown[i], PINNED_HEAD), style: ruleOf(colorOf(domain, shown[i])), onPointerDown: pressStart, onPointerUp: pressEnd, onPointerLeave: pressEnd, onPointerCancel: pressEnd, onContextMenu: (e) => {
735
- if (press.current)
736
- e.preventDefault();
737
- }, "aria-sort": h.column.getIsSorted() === 'asc'
738
- ? 'ascending'
739
- : h.column.getIsSorted() === 'desc'
740
- ? 'descending'
741
- : undefined, children: [i === 0 && groups && folds && !serverGroups && (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(IconButton, { icon: anyClosed || anyFoldClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: "Expand or collapse rows" }) }), _jsxs(DropdownMenuContent, { align: "start", className: "w-52", children: [_jsxs(DropdownMenuItem, { onSelect: () => {
742
- setAll(true);
743
- setAllParts(true);
744
- }, children: [_jsx(ChevronsUpDownIcon, {}), " Expand all"] }), _jsxs(DropdownMenuItem, { onSelect: () => {
745
- setAll(true);
746
- setAllParts(false);
747
- }, children: [_jsx(ListCollapseIcon, {}), " Close every ", subgroup?.label.toLowerCase()] }), _jsxs(DropdownMenuItem, { onSelect: () => setAll(false), children: [_jsx(ChevronsDownUpIcon, {}), " Collapse all"] })] })] })), i === 0 &&
748
- groups &&
749
- !(folds && !serverGroups) &&
750
- (!serverGroups || serverGroups.onToggleAll) && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] })] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && !serverGroups?.list?.length && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center text-muted-foreground", children: "No rows match." }) })), groups && groupBy
751
- ? groups.map(([key, members], gi) => {
752
- const open = isOpen(key, gi);
753
- return (_jsxs(Fragment, { children: [_jsxs(TableRow, { className: ROLLUP, "data-group": key, "data-rollup": "", children: [selection && (_jsx(TableCell, { "data-card": "select", className: cn('w-8 pr-0', PINNED_GROUP), children: _jsx(Checkbox, { checked: members.every((m) => selection.keys.has(keyOf(m.original)))
754
- ? true
755
- : members.some((m) => selection.keys.has(keyOf(m.original)))
756
- ? 'indeterminate'
757
- : false, onCheckedChange: (on) => toggle(members.map((m) => keyOf(m.original)), on === true), "aria-label": `Select every row of ${key}` }) })), shown.slice(lead - 1).map((f, i) => i === 0 ? (_jsx(TableCell, { colSpan: leadSpan, "data-card": "lead", className: PINNED_GROUP, children: _jsxs("span", { className: "flex max-w-(--lead-col-max) min-w-0 items-center whitespace-nowrap", children: [_jsxs(Button, { variant: "ghost", size: "xs", title: key, className: "-mx-2 min-w-0 hover:bg-transparent aria-expanded:bg-transparent", "aria-expanded": open, "aria-label": `${open ? 'Collapse' : 'Expand'} ${key}`, onClick: () => serverGroups
758
- ? serverGroups.onToggle(key)
759
- : setFlipped((c) => {
760
- const n = new Set(c);
761
- n.has(key) ? n.delete(key) : n.add(key);
762
- return n;
763
- }), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(GroupMark, { field: groupBy, value: key }), _jsx("span", { className: "truncate", children: key }), _jsx("span", { className: "num shrink-0 font-normal text-muted-foreground", children: serverGroups
764
- ? (served.get(key)?.n?.toLocaleString() ?? '')
765
- : members.length })] }), groupActions?.(key, members.map((m) => m.original))] }) }, f.key)) : (_jsx(TableCell, { ...col(f, PINNED_GROUP), children: !serverGroups &&
766
- rollCell(f, members.map((m) => m.original), 'group') }, f.key))), add && _jsx(TableCell, { className: PINNED_GROUP })] }), open && (serverGroups ? servedRows(members, key) : drawn(members, key))] }, key));
767
- })
768
- : drawn(sorted, ''), serverGroups?.more && !serverGroups.pages && (_jsx(TableRow, { "data-more": true, children: _jsx(TableCell, { colSpan: span, className: "py-1", children: _jsxs(Button, { variant: "ghost", size: "xs", onClick: serverGroups.more, children: [_jsx(ChevronsDownIcon, {}), "More groups"] }) }) })), hasTotal && rows.length > 0 && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, "data-rollup": "", children: [selection && _jsx(TableCell, {}), rollCells(rows).map((cell, i) => i === 0 && !(shown[0] && rollsUp(shown[0])) ? (_jsx(TableCell, { "data-card": "lead", children: "Total" }, "total")) : (cell)), add && _jsx(TableCell, {})] }))] })] }) }) }));
744
+ return (_jsxs("div", { ref: box, className: cn(inView ? IN_VIEW : 'overflow-x-auto overflow-y-hidden [--pin-top:0px]', fit && FIT), children: [cardSort, _jsx(UnitColumns.Provider, { value: markUnits, children: _jsxs(Table, { "data-cards": "", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((hg) => (_jsxs(TableRow, { children: [selection && (_jsx(TableHead, { className: cn('w-8 pr-0', PINNED_HEAD), children: _jsx(Checkbox, { checked: allOn ? true : someOn ? 'indeterminate' : false, onCheckedChange: (on) => toggle(allKeys, on === true), "aria-label": "Select every row" }) })), hg.headers.map((h, i) => (_jsxs(TableHead, { ...col(shown[i], PINNED_HEAD), style: ruleOf(colorOf(domain, shown[i])), onPointerDown: pressStart, onPointerUp: pressEnd, onPointerLeave: pressEnd, onPointerCancel: pressEnd, onContextMenu: (e) => {
745
+ if (press.current)
746
+ e.preventDefault();
747
+ }, "aria-sort": h.column.getIsSorted() === 'asc'
748
+ ? 'ascending'
749
+ : h.column.getIsSorted() === 'desc'
750
+ ? 'descending'
751
+ : undefined, children: [i === 0 && groups && folds && !serverGroups && (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(IconButton, { icon: anyClosed || anyFoldClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: "Expand or collapse rows" }) }), _jsxs(DropdownMenuContent, { align: "start", className: "w-52", children: [_jsxs(DropdownMenuItem, { onSelect: () => {
752
+ setAll(true);
753
+ setAllParts(true);
754
+ }, children: [_jsx(ChevronsUpDownIcon, {}), " Expand all"] }), _jsxs(DropdownMenuItem, { onSelect: () => {
755
+ setAll(true);
756
+ setAllParts(false);
757
+ }, children: [_jsx(ListCollapseIcon, {}), " Close every ", subgroup?.label.toLowerCase()] }), _jsxs(DropdownMenuItem, { onSelect: () => setAll(false), children: [_jsx(ChevronsDownUpIcon, {}), " Collapse all"] })] })] })), i === 0 &&
758
+ groups &&
759
+ !(folds && !serverGroups) &&
760
+ (!serverGroups || serverGroups.onToggleAll) && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] })] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && !serverGroups?.list?.length && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center text-muted-foreground", children: "No rows match." }) })), groups && groupBy
761
+ ? groups.map(([key, members], gi) => {
762
+ const open = isOpen(key, gi);
763
+ return (_jsxs(Fragment, { children: [_jsxs(TableRow, { className: ROLLUP, "data-group": key, "data-rollup": "", children: [selection && (_jsx(TableCell, { "data-card": "select", className: cn('w-8 pr-0', PINNED_GROUP), children: _jsx(Checkbox, { checked: members.every((m) => selection.keys.has(keyOf(m.original)))
764
+ ? true
765
+ : members.some((m) => selection.keys.has(keyOf(m.original)))
766
+ ? 'indeterminate'
767
+ : false, onCheckedChange: (on) => toggle(members.map((m) => keyOf(m.original)), on === true), "aria-label": `Select every row of ${key}` }) })), shown.slice(lead - 1).map((f, i) => i === 0 ? (_jsx(TableCell, { colSpan: leadSpan, "data-card": "lead", className: PINNED_GROUP, children: _jsxs("span", { className: "flex max-w-(--lead-col-max) min-w-0 items-center whitespace-nowrap", children: [_jsxs(Button, { variant: "ghost", size: "xs", title: key, className: "-mx-2 min-w-0 hover:bg-transparent aria-expanded:bg-transparent", "aria-expanded": open, "aria-label": `${open ? 'Collapse' : 'Expand'} ${key}`, onClick: () => serverGroups
768
+ ? serverGroups.onToggle(key)
769
+ : setFlipped((c) => {
770
+ const n = new Set(c);
771
+ n.has(key) ? n.delete(key) : n.add(key);
772
+ return n;
773
+ }), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(GroupMark, { field: groupBy, value: key }), _jsx("span", { className: "truncate", children: key }), _jsx("span", { className: "num shrink-0 font-normal text-muted-foreground", children: serverGroups
774
+ ? (served.get(key)?.n?.toLocaleString() ?? '')
775
+ : members.length })] }), groupActions?.(key, members.map((m) => m.original))] }) }, f.key)) : (_jsx(TableCell, { ...col(f, PINNED_GROUP), children: !serverGroups &&
776
+ rollCell(f, members.map((m) => m.original), 'group') }, f.key))), add && _jsx(TableCell, { className: PINNED_GROUP })] }), open && (serverGroups ? servedRows(members, key) : drawn(members, key))] }, key));
777
+ })
778
+ : drawn(sorted, ''), serverGroups?.more && !serverGroups.pages && (_jsx(TableRow, { "data-more": true, children: _jsx(TableCell, { colSpan: span, className: "py-1", children: _jsxs(Button, { variant: "ghost", size: "xs", onClick: serverGroups.more, children: [_jsx(ChevronsDownIcon, {}), "More groups"] }) }) })), hasTotal && rows.length > 0 && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, "data-rollup": "", children: [selection && _jsx(TableCell, {}), rollCells(rows).map((cell, i) => i === 0 && !(shown[0] && rollsUp(shown[0])) ? (_jsx(TableCell, { "data-card": "lead", children: "Total" }, "total")) : (cell)), add && _jsx(TableCell, {})] }))] })] }) })] }));
769
779
  }
770
780
  /** One value rolled up into a rows × columns grid, with totals where the roll-up adds. */
771
781
  function PivotTable({ domain, rows, pivot, }) {
@@ -221,10 +221,12 @@ export function LumenWidget({ id, kind, icon, title, note, controls, tools, phon
221
221
  const below = showFilter && !inline && (!mobile || filterOpen);
222
222
  // A row of numbers with no header drops its own frame: its cards sit on the page.
223
223
  const flat = headless && styleId === 'metric:row';
224
- return (_jsx(CardIdProvider, { id: id, children: _jsxs(Card, { "data-slot": "lumen-card", "data-card": id, "data-card-title": title, "data-card-kind": styleId, className: cn(
224
+ return (_jsx(CardIdProvider, { id: id, children: _jsxs(Card, { "data-slot": "lumen-card", "data-card": id, "data-card-title": title, "data-card-kind": styleId, "data-folded": folded || undefined, className: cn(
225
225
  // A border, not the card's ring: a ring draws outside the box, so the page's scroller
226
226
  // clipped it wherever a card touched its edge (the top row's top line).
227
- 'h-full gap-0 border border-foreground/10 py-0 ring-0', folded && 'h-auto',
227
+ // Its height is its content's: only a grid cell (LumenGrid) stretches it to the cell, so
228
+ // a card in a scrolling column (Settings, a drawer) never clips rows the column could scroll to.
229
+ 'gap-0 border border-foreground/10 py-0 ring-0',
228
230
  // The allocation bar's tooltip reaches past its short card, over the cards below.
229
231
  styleId === 'allocation:bar' ? 'relative overflow-visible hover:z-20' : 'overflow-clip',
230
232
  // The edit outline sits inside the card, over its pinned rows: an outer one is clipped by
@@ -4,6 +4,7 @@ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui';
4
4
  import { useOverlayOpen } from '../hooks/use-back-close.js';
5
5
  import { usePhoneScreen } from '../hooks/use-keyboard-fit.js';
6
6
  import { Button } from './button.js';
7
+ import { escapeToPicker } from './overlay-guard.js';
7
8
  function AlertDialog({ open: given, defaultOpen, onOpenChange: changed, ...props }) {
8
9
  // Phone back closes it, as its Cancel does (useBackClose).
9
10
  const state = useOverlayOpen({ open: given, defaultOpen, onOpenChange: changed });
@@ -21,7 +22,7 @@ function AlertDialogOverlay({ className, ...props }) {
21
22
  function AlertDialogContent({ className, size = 'default', ...props }) {
22
23
  // On a phone it is a full screen pinned to the top, its footer a bar at the top (usePhoneScreen).
23
24
  const style = usePhoneScreen(props.style);
24
- return (_jsxs(AlertDialogPortal, { children: [_jsx(AlertDialogOverlay, {}), _jsx(AlertDialogPrimitive.Content, { "data-slot": "alert-dialog-content", "data-size": size, className: cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none max-md:inset-0 max-md:h-full max-md:w-full max-md:max-w-none! max-md:translate-x-0 max-md:translate-y-0 max-md:content-start max-md:overflow-y-auto max-md:rounded-none max-md:pt-[calc(1rem+env(safe-area-inset-top))] max-md:ring-0 max-md:end-fade', className), ...props, style: style })] }));
25
+ return (_jsxs(AlertDialogPortal, { children: [_jsx(AlertDialogOverlay, {}), _jsx(AlertDialogPrimitive.Content, { "data-slot": "alert-dialog-content", "data-size": size, className: cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none max-md:inset-0 max-md:h-full max-md:w-full max-md:max-w-none! max-md:translate-x-0 max-md:translate-y-0 max-md:content-start max-md:overflow-y-auto max-md:rounded-none max-md:pt-[calc(1rem+env(safe-area-inset-top))] max-md:ring-0 max-md:end-fade', className), ...props, onEscapeKeyDown: escapeToPicker(props.onEscapeKeyDown), style: style })] }));
25
26
  }
26
27
  function AlertDialogHeader({ className, ...props }) {
27
28
  return (_jsx("div", { "data-slot": "alert-dialog-header", className: cn('grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]', className), ...props }));
package/dist/ui/badge.js CHANGED
@@ -7,13 +7,13 @@ const badgeVariants = cva('group/badge inline-flex h-5 w-fit shrink-0 items-cent
7
7
  variant: {
8
8
  default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
9
9
  secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
10
- destructive: 'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
10
+ destructive: 'bg-destructive/10 text-[color-mix(in_oklab,var(--destructive)_80%,var(--foreground))] focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
11
11
  outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
12
12
  ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
13
13
  link: 'text-primary underline-offset-4 hover:underline',
14
- success: 'bg-success/10 text-success dark:bg-success/20',
15
- warning: 'bg-warning/10 text-warning dark:bg-warning/20',
16
- info: 'bg-info/10 text-info dark:bg-info/20',
14
+ success: 'bg-success/10 text-[color-mix(in_oklab,var(--success)_80%,var(--foreground))] dark:bg-success/20',
15
+ warning: 'bg-warning/10 text-[color-mix(in_oklab,var(--warning)_80%,var(--foreground))] dark:bg-warning/20',
16
+ info: 'bg-info/10 text-[color-mix(in_oklab,var(--info)_80%,var(--foreground))] dark:bg-info/20',
17
17
  },
18
18
  },
19
19
  defaultVariants: {
package/dist/ui/dialog.js CHANGED
@@ -5,6 +5,7 @@ import { Button } from './button.js';
5
5
  import { XIcon } from 'lucide-react';
6
6
  import { useOverlayOpen } from '../hooks/use-back-close.js';
7
7
  import { usePhoneScreen } from '../hooks/use-keyboard-fit.js';
8
+ import { escapeToPicker } from './overlay-guard.js';
8
9
  function Dialog({ open: given, defaultOpen, onOpenChange: changed, ...props }) {
9
10
  // Phone back closes it (useBackClose).
10
11
  const state = useOverlayOpen({ open: given, defaultOpen, onOpenChange: changed });
@@ -25,7 +26,7 @@ function DialogOverlay({ className, ...props }) {
25
26
  function DialogContent({ className, children, showCloseButton = true, ...props }) {
26
27
  // On a phone it is a full screen pinned to the top, its footer a bar at the top (usePhoneScreen).
27
28
  const style = usePhoneScreen(props.style);
28
- return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", "data-close": showCloseButton || undefined, className: cn('group/dialog fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-(--dialog-pad) text-sm [--dialog-pad:--spacing(4)] text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm max-md:inset-0 max-md:h-full max-md:max-h-none max-md:w-full max-md:max-w-none max-md:translate-x-0 max-md:translate-y-0 max-md:content-start max-md:overflow-y-auto max-md:rounded-none max-md:pt-[calc(var(--dialog-pad)+env(safe-area-inset-top))] max-md:ring-0 max-md:end-fade max-md:[--end-fade:var(--dialog-pad)] data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95', className), ...props, style: style, children: [children, showCloseButton && (_jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", asChild: true, children: _jsxs(Button, { variant: "ghost", className: "absolute top-2 right-2 max-md:top-[calc(1.25rem+env(safe-area-inset-top))]", size: "icon-sm", children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }) }))] })] }));
29
+ return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", "data-close": showCloseButton || undefined, className: cn('group/dialog fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-(--dialog-pad) text-sm [--dialog-pad:--spacing(4)] text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm max-md:inset-0 max-md:h-full max-md:max-h-none max-md:w-full max-md:max-w-none max-md:translate-x-0 max-md:translate-y-0 max-md:content-start max-md:overflow-y-auto max-md:rounded-none max-md:pt-[calc(var(--dialog-pad)+env(safe-area-inset-top))] max-md:ring-0 max-md:end-fade max-md:[--end-fade:var(--dialog-pad)] data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95', className), ...props, onEscapeKeyDown: escapeToPicker(props.onEscapeKeyDown), style: style, children: [children, showCloseButton && (_jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", asChild: true, children: _jsxs(Button, { variant: "ghost", className: "absolute top-2 right-2 max-md:top-[calc(1.25rem+env(safe-area-inset-top))]", size: "icon-sm", children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }) }))] })] }));
29
30
  }
30
31
  function DialogHeader({ className, ...props }) {
31
32
  return (_jsx("div", { "data-slot": "dialog-header",
@@ -1,6 +1,6 @@
1
1
  import { HoverCard as HoverCardPrimitive } from 'radix-ui';
2
2
  import type * as React from 'react';
3
3
  declare function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>): React.JSX.Element;
4
- declare function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>): React.JSX.Element;
5
- declare function HoverCardContent({ className, align, sideOffset, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>): React.JSX.Element;
4
+ declare function HoverCardTrigger({ className, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>): React.JSX.Element;
5
+ declare function HoverCardContent({ className, align, sideOffset, collisionPadding, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>): React.JSX.Element;
6
6
  export { HoverCard, HoverCardTrigger, HoverCardContent };
@@ -4,10 +4,13 @@ import { cn } from '../lib/utils.js';
4
4
  function HoverCard({ ...props }) {
5
5
  return _jsx(HoverCardPrimitive.Root, { "data-slot": "hover-card", ...props });
6
6
  }
7
- function HoverCardTrigger({ ...props }) {
8
- return _jsx(HoverCardPrimitive.Trigger, { "data-slot": "hover-card-trigger", ...props });
7
+ // Keyboard users reach a hover card by Tab, so its trigger shows the focus ring like a button.
8
+ function HoverCardTrigger({ className, ...props }) {
9
+ return (_jsx(HoverCardPrimitive.Trigger, { "data-slot": "hover-card-trigger", className: cn('rounded-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/50', className), ...props }));
9
10
  }
10
- function HoverCardContent({ className, align = 'center', sideOffset = 4, ...props }) {
11
- return (_jsx(HoverCardPrimitive.Portal, { "data-slot": "hover-card-portal", children: _jsx(HoverCardPrimitive.Content, { "data-slot": "hover-card-content", align: align, sideOffset: sideOffset, className: cn('bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 z-[70] w-72 origin-(--radix-hover-card-content-transform-origin) rounded-lg p-3 text-sm shadow-md ring-1 outline-hidden duration-100', className), ...props }) }));
11
+ function HoverCardContent({ className, align = 'center', sideOffset = 4, collisionPadding = 8, ...props }) {
12
+ return (_jsx(HoverCardPrimitive.Portal, { "data-slot": "hover-card-portal", children: _jsx(HoverCardPrimitive.Content, { "data-slot": "hover-card-content", align: align, sideOffset: sideOffset,
13
+ // Never flush against (or past) the screen's edge on a phone.
14
+ collisionPadding: collisionPadding, className: cn('bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 z-[70] w-72 origin-(--radix-hover-card-content-transform-origin) rounded-lg p-3 text-sm shadow-md ring-1 outline-hidden duration-100', className), ...props }) }));
12
15
  }
13
16
  export { HoverCard, HoverCardTrigger, HoverCardContent };
@@ -0,0 +1,2 @@
1
+ /** The overlay's `onEscapeKeyDown`: ignores Esc while a picker is open, else runs the caller's. */
2
+ export declare function escapeToPicker(own?: (e: KeyboardEvent) => void): (e: KeyboardEvent) => void;
@@ -0,0 +1,14 @@
1
+ // Esc closes the topmost thing only. A Radix overlay (Dialog, Sheet, AlertDialog) hears Esc on the
2
+ // document before a base-ui picker open inside it (LumenSelector, Combobox), which isn't one of
3
+ // Radix's layers, so one Esc closed both. While such a picker is open the overlay leaves Esc to it.
4
+ const OPEN_PICKER = '[data-slot="lumen-selector"][data-open], [data-slot="combobox-content"][data-open]';
5
+ /** The overlay's `onEscapeKeyDown`: ignores Esc while a picker is open, else runs the caller's. */
6
+ export function escapeToPicker(own) {
7
+ return (e) => {
8
+ if (document.querySelector(OPEN_PICKER)) {
9
+ e.preventDefault();
10
+ return;
11
+ }
12
+ own?.(e);
13
+ };
14
+ }
package/dist/ui/sheet.js CHANGED
@@ -7,6 +7,7 @@ import * as React from 'react';
7
7
  import { useOverlayOpen } from '../hooks/use-back-close.js';
8
8
  import { useKeyboardFit } from '../hooks/use-keyboard-fit.js';
9
9
  import { useIsMobile } from '../hooks/use-mobile.js';
10
+ import { escapeToPicker } from './overlay-guard.js';
10
11
  /** Closes the sheet from inside it (the grab handle's swipe). */
11
12
  const SheetCloseContext = React.createContext(() => undefined);
12
13
  function Sheet({ open: given, defaultOpen, onOpenChange: changed, ...props }) {
@@ -101,7 +102,7 @@ function SheetContent({ className, children, side = 'right', showCloseButton = t
101
102
  return (_jsxs(SheetPortal, { children: [_jsx(SheetOverlay, {}), _jsxs(SheetPrimitive.Content, { "data-slot": "sheet-content", "data-side": side, "data-card": card || undefined, "data-close": closable || undefined, className: cn('group/sheet bg-popover text-popover-foreground fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-(--assistant-docked,0px) data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=left]:pt-[env(safe-area-inset-top)] data-[side=right]:pt-[env(safe-area-inset-top)]', className,
102
103
  // After the caller's classes: a phone's bottom sheet always keeps its shape and room.
103
104
  card &&
104
- 'max-h-[calc(100dvh-env(safe-area-inset-top)-2.75rem)] rounded-t-2xl border-t-0 pt-5'), ...props, style: style,
105
+ 'max-h-[calc(100dvh-env(safe-area-inset-top)-2.75rem)] rounded-t-2xl border-t-0 pt-5'), ...props, onEscapeKeyDown: escapeToPicker(props.onEscapeKeyDown), style: style,
105
106
  // A click or focus in the docked the assistant panel leaves the sheet open, so the person can ask
106
107
  // about what it shows (LumenShell).
107
108
  onInteractOutside: (e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
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.111",
52
+ "@columenlabs/lumen-ui-types": "0.1.113",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",