@columenlabs/lumen-ui 0.1.15 → 0.1.17

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.
@@ -22,7 +22,7 @@ export function LumenDrawer({ open, onClose, title, description, children, foote
22
22
  // On a phone the drawer is a full screen and the footer a bar under the header, so the
23
23
  // keyboard never covers its actions (Dialog does the same).
24
24
  const foot = (_jsxs("footer", { "data-slot": "drawer-footer", className: "flex shrink-0 items-center gap-2 border-t bg-muted/50 px-4 py-2.5 max-md:order-first max-md:border-t-0 max-md:border-b", children: [footerStart && (_jsx("div", { className: "min-w-0 flex-1 truncate text-muted-foreground text-xs", children: footerStart })), _jsx("div", { className: "ml-auto flex items-center gap-2", children: footer ?? (_jsx(SheetClose, { asChild: true, children: _jsx(Button, { type: "button", size: "sm", variant: "outline", children: "Close" }) })) })] }));
25
- return (_jsx(Sheet, { open: open, onOpenChange: (o) => !o && onClose(), modal: isMobile, children: isMobile ? (_jsxs(SheetContent, { side: "bottom", className: "max-h-[85svh] gap-0 overflow-hidden", children: [_jsx(SheetHeader, { className: "order-first shrink-0 border-b", children: head }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto pt-2", children: body }), foot] })) : (_jsx(SheetContent, { side: "right", showCloseButton: false,
25
+ return (_jsx(Sheet, { open: open, onOpenChange: (o) => !o && onClose(), modal: isMobile, children: isMobile ? (_jsxs(SheetContent, { side: "bottom", className: "max-h-[85svh] gap-0 overflow-hidden", children: [_jsx(SheetHeader, { className: "order-first shrink-0 border-b", children: head }), _jsx("div", { className: "end-fade min-h-0 flex-1 overflow-y-auto pt-2", children: body }), foot] })) : (_jsx(SheetContent, { side: "right", showCloseButton: false,
26
26
  // The resize handle's grab zone is wider than the handle, so a drag can start on the
27
27
  // overlay; the panels library marks that pointerdown handled, and then it doesn't close.
28
28
  onPointerDownOutside: (e) => e.detail.originalEvent.defaultPrevented && e.preventDefault(), className: "pointer-events-none! gap-0 border-0 bg-transparent shadow-none data-[side=right]:left-0 data-[side=right]:w-auto data-[side=right]:border-l-0 data-[side=right]:sm:max-w-none", children: _jsxs(ResizablePanelGroup, { orientation: "horizontal", children: [_jsx(ResizablePanel, { minSize: "40%" }), _jsx(ResizableHandle, { withHandle: true, className: "pointer-events-auto" }), _jsx(ResizablePanel, { defaultSize: defaultSize, minSize: minSize, maxSize: maxSize, "data-slot": "lumen-drawer", className: "pointer-events-auto bg-popover shadow-lg", children: _jsxs("section", { className: "flex h-full flex-col overflow-hidden", children: [_jsxs("header", { className: "flex shrink-0 items-center gap-2 border-b px-4 py-3", children: [head, _jsx(SheetClose, { asChild: true, children: _jsx(IconButton, { icon: XIcon, label: "Close", className: "ml-auto" }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto pt-2", children: body }), foot] }) })] }) })) }));
@@ -7,7 +7,7 @@ import { OUTLOOK_INDEXES, marketWindows, } from '@columenlabs/lumen-ui-types/mar
7
7
  // cards out; this file draws the card frame every card shares (its header, ↻ and ⋯ menu), each
8
8
  // widget, the Edit layout bar, Add widget (Pre-built and Build your own) and Edit widget.
9
9
  import { ALLOCATION_MAX, VIEW_CARD_KIND_NAMES, WIDGET_STYLES, controlOn, isDollarFormat, isOwnTile, isRowCard, isTableCard, trendBuckets, viewCardKinds, viewCardSchema, viewWidgetCatalog, widgetStyleId, widgetStyleOf, } from '@columenlabs/lumen-ui-types/views';
10
- import { ActivityIcon, ArrowDownIcon, ArrowUpIcon, BarChart3Icon, CandlestickChartIcon, ChartLineIcon, ChartPieIcon, CheckIcon, CodeIcon, CopyIcon, DatabaseIcon, EyeOffIcon, FoldVerticalIcon, FunnelIcon, GripVerticalIcon, InfoIcon, LayoutGridIcon, ListOrderedIcon, MoreHorizontalIcon, PackageIcon, PencilIcon, PlusIcon, RotateCcwIcon, Rows3Icon, SparklesIcon, SquareIcon, TableIcon, Trash2Icon, TrendingUpIcon, WrenchIcon, XIcon, } from 'lucide-react';
10
+ import { ActivityIcon, ArrowDownIcon, ArrowUpIcon, BarChart3Icon, CandlestickChartIcon, ChartLineIcon, ChartPieIcon, CheckIcon, CodeIcon, CopyIcon, DatabaseIcon, EyeOffIcon, FoldVerticalIcon, FunnelIcon, GripVerticalIcon, InfoIcon, LayoutGridIcon, ListOrderedIcon, MoreHorizontalIcon, PackageIcon, PencilIcon, PlusIcon, RefreshCwIcon, RotateCcwIcon, Rows3Icon, SparklesIcon, SquareIcon, TableIcon, Trash2Icon, TrendingUpIcon, WrenchIcon, XIcon, } from 'lucide-react';
11
11
  import { useEffect, useMemo, useRef, useState } from 'react';
12
12
  import { useAssistant, useAssistantName } from './assistant-context.js';
13
13
  import { LumenConfirm } from './confirm.js';
@@ -39,7 +39,7 @@ import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, Dia
39
39
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
40
40
  import { Input } from './ui/input.js';
41
41
  import { Label } from './ui/label.js';
42
- import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
42
+ import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from './ui/popover.js';
43
43
  import { Skeleton } from './ui/skeleton.js';
44
44
  import { Switch } from './ui/switch.js';
45
45
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table.js';
@@ -366,10 +366,12 @@ export function LumenWidget({ id, kind, icon, title, note, controls, tools, filt
366
366
  // The edit outline sits inside the card, over its pinned rows: an outer one is clipped by
367
367
  // the page's scroller, and an inset outline paints under the sticky header and group rows.
368
368
  layout &&
369
- "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', 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'), 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',
369
+ "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',
370
+ // The corner ⋯ of a card with its header off sits over the body's top right.
371
+ 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'), 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',
370
372
  // On the grid a card fills its cell: with a height of its own its body scrolls; one as
371
373
  // tall as it draws keeps its body's height, so the grid can measure it.
372
- !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 }) }), !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') }))] }) }));
374
+ !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 }) }), 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') }))] }) }));
373
375
  }
374
376
  /**
375
377
  * One widget in its card. Widgets over the view's rows (tiles,
@@ -593,6 +595,29 @@ export function cardsAfter(cards, i, action) {
593
595
  }
594
596
  return next;
595
597
  }
598
+ /**
599
+ * The controls of a card with its header off (Edit widget ▸ Controls): a faint ⋯ in the top right
600
+ * corner, always there on desktop and phone, opening Ask the assistant, Refresh, Filter and Script
601
+ * (only those the card has on). Filter opens the card's filter in a popover from the same corner;
602
+ * while it holds tokens their count sits beside the ⋯, so a filtered card never looks unfiltered.
603
+ */
604
+ function HeaderlessMenu({ title, ask, askLabel, load, editScript, filter, filterCount, }) {
605
+ const [filterOpen, setFilterOpen] = useState(false);
606
+ const opening = useRef(false);
607
+ if (!ask && !load && !editScript && !filter)
608
+ return null;
609
+ return (_jsxs(Popover, { open: filterOpen, onOpenChange: setFilterOpen, children: [_jsx(PopoverAnchor, { asChild: true, children: _jsxs("div", { "data-card-corner": true, className: "absolute top-1.5 right-1.5 z-10 flex items-center gap-1 rounded-md bg-card/80", children: [filter && filterCount > 0 && (_jsxs(Badge, { variant: "secondary", className: "num cursor-pointer gap-1 px-1.5 text-xs", title: `${filterCount} filter ${filterCount === 1 ? 'token' : 'tokens'}`, onClick: () => setFilterOpen(true), children: [_jsx(FunnelIcon, { className: "size-3" }), filterCount] })), _jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(IconButton, { icon: load?.fetching ? RefreshCwIcon : MoreHorizontalIcon, busy: load?.fetching, label: `${title} menu`, className: "text-muted-foreground opacity-60 hover:opacity-100 focus-visible:opacity-100 data-[state=open]:opacity-100" }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-44", onCloseAutoFocus: (e) => {
610
+ if (!opening.current)
611
+ return;
612
+ opening.current = false;
613
+ e.preventDefault();
614
+ setFilterOpen(true);
615
+ }, children: [ask && (_jsxs(DropdownMenuItem, { onSelect: ask, children: [_jsx(SparklesIcon, {}), " ", askLabel] })), load && (_jsxs(DropdownMenuItem, { disabled: load.fetching, onSelect: () => void load.refresh(), children: [_jsx(RefreshCwIcon, {}), " Refresh"] })), filter && (_jsxs(DropdownMenuItem, { onSelect: () => {
616
+ // Opened once the menu has closed (its close hands focus back, which would
617
+ // shut a popover opened sooner).
618
+ opening.current = true;
619
+ }, children: [_jsx(FunnelIcon, {}), " Filter", filterCount > 0 && (_jsx("span", { className: "num ml-auto text-xs text-muted-foreground", children: filterCount }))] })), editScript && (_jsxs(DropdownMenuItem, { onSelect: editScript, children: [_jsx(CodeIcon, {}), " Script"] }))] })] })] }) }), filter && (_jsx(PopoverContent, { align: "end", collisionPadding: 8, className: "w-xl max-w-(--radix-popover-content-available-width) p-2", children: _jsx("div", { "data-card-filter": true, className: "flex items-center gap-2", children: filter }) }))] }));
620
+ }
596
621
  /** ⓘ How to read: the card's own few lines on what it shows and what to look for. */
597
622
  function CardHelp({ title, children }) {
598
623
  return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(IconButton, { icon: InfoIcon, label: `How to read ${title}` }) }), _jsxs(PopoverContent, { align: "end", className: "w-80 text-sm", children: [_jsxs("p", { className: "mb-1.5 font-medium", children: ["How to read ", title] }), _jsx("div", { className: "flex flex-col gap-1.5 text-muted-foreground", children: children })] })] }));
package/dist/settings.js CHANGED
@@ -177,5 +177,5 @@ export function LumenSettings({ open, section, onOpen, onClose, appLabel, profil
177
177
  return (_jsx(ActionsScope, { children: (slot) => (_jsxs("div", { "data-settings-section": s.id, "data-whole": whole, "aria-labelledby": `settings-${s.id}-title`, className: whole
178
178
  ? 'flex flex-col gap-3'
179
179
  : 'hidden flex-col gap-3 has-[[data-slot=setting-row]]:flex', children: [header(s, slot), _jsx(Separator, {}), _jsx(SearchContext.Provider, { value: whole ? '' : q, children: s.content })] })) }, s.id));
180
- }), !found.length && (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["No settings match \u201C", q, "\u201D."] }))] })) : (_jsx(ActionsScope, { children: (slot, footer) => (_jsxs("section", { "aria-labelledby": `settings-${current.id}-title`, className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex shrink-0 flex-col gap-3 px-4 pt-4 md:px-6 md:pt-6", children: [header(current, slot), _jsx(Separator, {})] }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-4 pt-3 pb-4 *:shrink-0 md:px-6", children: current.content }), _jsx("div", { ref: footer, "data-slot": "settings-footer", className: "flex min-h-12 shrink-0 items-center justify-end gap-2 border-t px-4 py-2 md:px-6", children: current.footer })] })) }, current.id))] }) }));
180
+ }), !found.length && (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["No settings match \u201C", q, "\u201D."] }))] })) : (_jsx(ActionsScope, { children: (slot, footer) => (_jsxs("section", { "aria-labelledby": `settings-${current.id}-title`, className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex shrink-0 flex-col gap-3 px-4 pt-4 md:px-6 md:pt-6", children: [header(current, slot), _jsx(Separator, {})] }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-4 pt-3 pb-4 *:shrink-0 md:px-6 [&:has(+[data-slot=settings-footer]:empty)]:end-fade", children: current.content }), _jsx("div", { ref: footer, "data-slot": "settings-footer", className: "flex min-h-12 shrink-0 items-center justify-end gap-2 border-t px-4 py-2 empty:hidden md:px-6", children: current.footer })] })) }, current.id))] }) }));
181
181
  }
package/dist/theme.css CHANGED
@@ -331,6 +331,17 @@
331
331
  background-position: calc(-1 * var(--wash-x, 0px)) calc(-1 * var(--wash-y, 0px));
332
332
  mask-image: linear-gradient(to bottom, #000 calc(100% - var(--intro-fade)), transparent);
333
333
  }
334
+ /* The end of a dialog's or drawer's scrolling body with no buttons under it: its last --end-fade
335
+ (--spacing(4) unless set) fades out, the same affordance as the sticky tab row's --intro-fade, so
336
+ content never stops at a hard edge. The element keeps at least that much padding at its end, so
337
+ its last row clears the fade once scrolled to the bottom. */
338
+ @utility end-fade {
339
+ mask-image: linear-gradient(
340
+ to bottom,
341
+ #000 calc(100% - var(--end-fade, calc(var(--spacing) * 4))),
342
+ transparent
343
+ );
344
+ }
334
345
  @keyframes intro-page-in {
335
346
  from {
336
347
  opacity: 0;
@@ -18,7 +18,7 @@ function AlertDialogOverlay({ className, ...props }) {
18
18
  function AlertDialogContent({ className, size = 'default', ...props }) {
19
19
  // On a phone it is a full screen pinned to the top, its footer a bar at the top (usePhoneScreen).
20
20
  const style = usePhoneScreen(props.style);
21
- 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', className), ...props, style: style })] }));
21
+ 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 })] }));
22
22
  }
23
23
  function AlertDialogHeader({ className, ...props }) {
24
24
  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 }));
@@ -14,6 +14,7 @@ declare function DialogHeader({ className, ...props }: React.ComponentProps<'div
14
14
  * footer bleed by `--dialog-pad`, the content's padding, so a dialog that changes its padding sets
15
15
  * that variable (`[--dialog-pad:0px]`), never `p-0`, and they still meet its edges. */
16
16
  declare const dialogBody = "-mx-(--dialog-pad) max-h-[60svh] min-h-0 overflow-y-auto px-(--dialog-pad) max-md:max-h-none max-md:overflow-visible";
17
+ /** With no footer under it the body reaches the dialog's bottom edge and fades into it (`end-fade`). */
17
18
  declare function DialogBody({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
18
19
  declare function DialogFooter({ className, showCloseButton, children, ...props }: React.ComponentProps<'div'> & {
19
20
  showCloseButton?: boolean;
package/dist/ui/dialog.js CHANGED
@@ -22,7 +22,7 @@ function DialogOverlay({ className, ...props }) {
22
22
  function DialogContent({ className, children, showCloseButton = true, ...props }) {
23
23
  // On a phone it is a full screen pinned to the top, its footer a bar at the top (usePhoneScreen).
24
24
  const style = usePhoneScreen(props.style);
25
- return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn('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 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" })] }) }))] })] }));
25
+ return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn('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" })] }) }))] })] }));
26
26
  }
27
27
  function DialogHeader({ className, ...props }) {
28
28
  return (_jsx("div", { "data-slot": "dialog-header", className: cn('flex flex-col gap-2', className), ...props }));
@@ -32,8 +32,9 @@ function DialogHeader({ className, ...props }) {
32
32
  * footer bleed by `--dialog-pad`, the content's padding, so a dialog that changes its padding sets
33
33
  * that variable (`[--dialog-pad:0px]`), never `p-0`, and they still meet its edges. */
34
34
  const dialogBody = '-mx-(--dialog-pad) max-h-[60svh] min-h-0 overflow-y-auto px-(--dialog-pad) max-md:max-h-none max-md:overflow-visible';
35
+ /** With no footer under it the body reaches the dialog's bottom edge and fades into it (`end-fade`). */
35
36
  function DialogBody({ className, ...props }) {
36
- return _jsx("div", { "data-slot": "dialog-body", className: cn(dialogBody, className), ...props });
37
+ return (_jsx("div", { "data-slot": "dialog-body", className: cn(dialogBody, 'last:-mb-(--dialog-pad) last:pb-(--dialog-pad) last:end-fade last:[--end-fade:var(--dialog-pad)]', className), ...props }));
37
38
  }
38
39
  function DialogFooter({ className, showCloseButton = false, children, ...props }) {
39
40
  return (_jsxs("div", { "data-slot": "dialog-footer", className: cn('-mx-(--dialog-pad) -mb-(--dialog-pad) flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end max-md:order-first max-md:-mt-(--dialog-pad) max-md:mb-0 max-md:flex-row max-md:justify-end max-md:rounded-none max-md:border-t-0 max-md:border-b max-md:pr-12', className), ...props, children: [children, showCloseButton && (_jsx(DialogPrimitive.Close, { asChild: true, children: _jsx(Button, { variant: "outline", children: "Close" }) }))] }));
@@ -653,7 +653,7 @@ export function ViewEditDialog({ open, onOpenChange, domain: pageDomain, config,
653
653
  onOpenChange(false);
654
654
  onDelete();
655
655
  }) }) })), parsed && !parsed.success && (_jsxs("p", { id: "view-editor-check", tabIndex: -1, className: "px-3 text-xs text-destructive", children: [parsed.error.issues[0]?.path.join('.'), ": ", parsed.error.issues[0]?.message] }))] }));
656
- return (_jsxs(Dialog, { open: open, onOpenChange: onOpenChange, children: [_jsxs(DialogContent, { "data-slot": "view-editor", showCloseButton: false, className: "flex h-[min(780px,calc(100svh-2rem))] flex-col gap-0 overflow-hidden p-0 max-md:h-svh max-md:max-w-full max-md:rounded-none max-md:ring-0 sm:max-w-6xl", children: [_jsx(DialogDescription, { className: "sr-only", children: "This widget's type, fields, settings, rows and controls, and the fields it can use." }), _jsxs("header", { className: "flex shrink-0 items-center gap-2 border-b px-3 py-2 md:px-4", children: [isMobile && (_jsx(DialogClose, { asChild: true, children: _jsx(IconButton, { icon: XIcon, label: "Close" }) })), _jsxs(DialogTitle, { className: "min-w-0 truncate text-sm font-semibold", children: ["Edit widget", ' ', _jsxs("span", { className: "font-normal text-muted-foreground", children: ["\u00B7 ", name.trim() || styleLabel(styleId)] })] }), _jsx("span", { className: "flex-1" }), !isMobile && (_jsx(DialogClose, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(XIcon, {}), "Cancel"] }) })), _jsx(SaveButton, { issues: issues, onSave: save })] }), isMobile ? (_jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), className: "min-h-0 flex-1 gap-0 pt-2", children: [_jsxs(TabsList, { className: "mx-3 w-[calc(100%-1.5rem)]", children: [_jsxs(TabsTrigger, { value: "view", children: [_jsx(LayoutDashboardIcon, {}), "Widget"] }), _jsxs(TabsTrigger, { value: "fields", children: [_jsx(DatabaseIcon, {}), "Data"] })] }), _jsx(TabsContent, { value: "view", className: "min-h-0 overflow-y-auto px-2 py-2 pb-[max(0.75rem,env(safe-area-inset-bottom))]", children: main }), _jsxs(TabsContent, { value: "fields", className: "min-h-0 overflow-y-auto px-3 py-2", children: [treeHint, tree] })] })) : (_jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_360px]", children: [_jsx("section", { "aria-label": "Widget", className: "min-h-0 overflow-y-auto px-3 py-2", children: main }), _jsxs("aside", { "aria-label": "Data", className: "flex min-h-0 flex-col overflow-y-auto border-l bg-sidebar px-2 py-2", children: [treeHint, tree, _jsxs("p", { className: "mt-auto px-2 pt-4 text-xs text-muted-foreground", children: ["Agents change the same settings with", ' ', _jsx("code", { className: "font-mono", children: card ? 'custom_view_widget_write' : 'custom_view_write' }), "."] })] })] }))] }), _jsx(AddFormulaDialog, { open: addingFormula, onOpenChange: setAddingFormula, domain: withLinks, formulas: formulas, onAdd: (f) => {
656
+ return (_jsxs(Dialog, { open: open, onOpenChange: onOpenChange, children: [_jsxs(DialogContent, { "data-slot": "view-editor", showCloseButton: false, className: "flex h-[min(780px,calc(100svh-2rem))] flex-col gap-0 overflow-hidden p-0 max-md:h-svh max-md:max-w-full max-md:rounded-none max-md:ring-0 max-md:[--dialog-pad:0px] sm:max-w-6xl", children: [_jsx(DialogDescription, { className: "sr-only", children: "This widget's type, fields, settings, rows and controls, and the fields it can use." }), _jsxs("header", { className: "flex shrink-0 items-center gap-2 border-b px-3 py-2 md:px-4", children: [isMobile && (_jsx(DialogClose, { asChild: true, children: _jsx(IconButton, { icon: XIcon, label: "Close" }) })), _jsxs(DialogTitle, { className: "min-w-0 truncate text-sm font-semibold", children: ["Edit widget", ' ', _jsxs("span", { className: "font-normal text-muted-foreground", children: ["\u00B7 ", name.trim() || styleLabel(styleId)] })] }), _jsx("span", { className: "flex-1" }), !isMobile && (_jsx(DialogClose, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(XIcon, {}), "Cancel"] }) })), _jsx(SaveButton, { issues: issues, onSave: save })] }), isMobile ? (_jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), className: "min-h-0 flex-1 gap-0 pt-2", children: [_jsxs(TabsList, { className: "mx-3 w-[calc(100%-1.5rem)]", children: [_jsxs(TabsTrigger, { value: "view", children: [_jsx(LayoutDashboardIcon, {}), "Widget"] }), _jsxs(TabsTrigger, { value: "fields", children: [_jsx(DatabaseIcon, {}), "Data"] })] }), _jsx(TabsContent, { value: "view", className: "end-fade min-h-0 overflow-y-auto px-2 pt-2 pb-[max(1rem,env(safe-area-inset-bottom))]", children: main }), _jsxs(TabsContent, { value: "fields", className: "end-fade min-h-0 overflow-y-auto px-3 pt-2 pb-4", children: [treeHint, tree] })] })) : (_jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)_360px]", children: [_jsx("section", { "aria-label": "Widget", className: "end-fade min-h-0 overflow-y-auto px-3 pt-2 pb-4", children: main }), _jsxs("aside", { "aria-label": "Data", className: "end-fade flex min-h-0 flex-col overflow-y-auto border-l bg-sidebar px-2 pt-2 pb-4", children: [treeHint, tree, _jsxs("p", { className: "mt-auto px-2 pt-4 text-xs text-muted-foreground", children: ["Agents change the same settings with", ' ', _jsx("code", { className: "font-mono", children: card ? 'custom_view_widget_write' : 'custom_view_write' }), "."] })] })] }))] }), _jsx(AddFormulaDialog, { open: addingFormula, onOpenChange: setAddingFormula, domain: withLinks, formulas: formulas, onAdd: (f) => {
657
657
  setAddingFormula(false);
658
658
  setDraft({
659
659
  ...draft,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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.15",
44
+ "@columenlabs/lumen-ui-types": "0.1.17",
45
45
  "@dnd-kit/core": "6.3.1",
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@dnd-kit/utilities": "3.2.2",