@columenlabs/lumen-ui 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export { LumenGeoMap } from './lumen-geo-map.js';
16
16
  export { LumenWidget, type CardControls, type CardLink, LumenSavedWidget } from './lumen-widget.js';
17
17
  export { LumenConfirm, type LumenConfirmProps } from './confirm.js';
18
18
  export { type LumenAuthPoint, LumenAuthHeading, LumenAuthNote, LumenAuthProblem, LumenAuthStage, type LumenAuthStageProps, LumenCodeField, LumenEmailField, LumenPasswordField, LumenStepBar, } from './lumen-auth.js';
19
+ export { LumenDateInput, type LumenDateInputProps, stepDate, } from './lumen-date-input.js';
19
20
  export { LumenChoice, type LumenChoiceOption } from './lumen-choice.js';
20
21
  export { LumenSecret } from './lumen-secret.js';
21
22
  export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ export { LumenGeoMap } from './lumen-geo-map.js';
23
23
  export { LumenWidget, LumenSavedWidget } from './lumen-widget.js';
24
24
  export { LumenConfirm } from './confirm.js';
25
25
  export { LumenAuthHeading, LumenAuthNote, LumenAuthProblem, LumenAuthStage, LumenCodeField, LumenEmailField, LumenPasswordField, LumenStepBar, } from './lumen-auth.js';
26
+ export { LumenDateInput, stepDate, } from './lumen-date-input.js';
26
27
  export { LumenChoice } from './lumen-choice.js';
27
28
  export { LumenSecret } from './lumen-secret.js';
28
29
  export { RankedBars, StackedBars, StackedColumns } from './lumen-widget-charts.js';
@@ -0,0 +1,30 @@
1
+ export type LumenDateInputProps = {
2
+ /** The day as ISO `yyyy-mm-dd`, or '' for none. */
3
+ value: string;
4
+ onChange: (iso: string) => void;
5
+ /** Earliest and latest day the picker and the steps reach. */
6
+ min?: string;
7
+ max?: string;
8
+ /** Which days the steps land on (a trading calendar); every day when left out. */
9
+ isOpen?: (iso: string) => boolean;
10
+ /** What a day is called in the step tooltips: "Previous trading day". Default "day". */
11
+ unit?: string;
12
+ /** Shows a Today icon button that runs this. */
13
+ onToday?: () => void;
14
+ /** Today is already shown: the Today button is off. */
15
+ atToday?: boolean;
16
+ /** `sm` for toolbars and card headers, `default` in forms. */
17
+ size?: 'sm' | 'default';
18
+ disabled?: boolean;
19
+ id?: string;
20
+ 'aria-label'?: string;
21
+ className?: string;
22
+ };
23
+ /** The next open day from `iso` in direction `dir`, inside min and max; null when none. */
24
+ export declare function stepDate(iso: string, dir: 1 | -1, { min, max, isOpen }?: Pick<LumenDateInputProps, 'min' | 'max' | 'isOpen'>): string | null;
25
+ /**
26
+ * The kit's date picker: the browser's date input between ‹ and › icon buttons that step
27
+ * one open day (an app passes its trading calendar as `isOpen`), Alt+← and Alt+→ while it
28
+ * has focus, and an optional Today. Every date field in an app uses it.
29
+ */
30
+ export declare function LumenDateInput({ value, onChange, min, max, isOpen, unit, onToday, atToday, size, disabled, id, 'aria-label': ariaLabel, className, }: LumenDateInputProps): import("react").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A date picker with ‹ › to step one open day back or forward, and an optional Today.
3
+ import { CalendarClockIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
4
+ import { IconButton } from './icon-button.js';
5
+ import { cn } from './lib/utils.js';
6
+ import { Input } from './ui/input.js';
7
+ const DAY = 86_400_000;
8
+ /** The furthest a step looks for an open day (long holiday runs fit easily). */
9
+ const REACH = 14;
10
+ function addDays(iso, n) {
11
+ return new Date(Date.parse(`${iso}T00:00:00Z`) + n * DAY).toISOString().slice(0, 10);
12
+ }
13
+ /** The next open day from `iso` in direction `dir`, inside min and max; null when none. */
14
+ export function stepDate(iso, dir, { min, max, isOpen } = {}) {
15
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(iso))
16
+ return null;
17
+ let day = iso;
18
+ for (let i = 0; i < REACH; i++) {
19
+ day = addDays(day, dir);
20
+ if ((min && day < min) || (max && day > max))
21
+ return null;
22
+ if (!isOpen || isOpen(day))
23
+ return day;
24
+ }
25
+ return null;
26
+ }
27
+ /**
28
+ * The kit's date picker: the browser's date input between ‹ and › icon buttons that step
29
+ * one open day (an app passes its trading calendar as `isOpen`), Alt+← and Alt+→ while it
30
+ * has focus, and an optional Today. Every date field in an app uses it.
31
+ */
32
+ export function LumenDateInput({ value, onChange, min, max, isOpen, unit = 'day', onToday, atToday, size = 'sm', disabled, id, 'aria-label': ariaLabel, className, }) {
33
+ const prev = disabled ? null : stepDate(value, -1, { min, max, isOpen });
34
+ const next = disabled ? null : stepDate(value, 1, { min, max, isOpen });
35
+ const btn = size === 'sm' ? 'icon-xs' : 'icon-sm';
36
+ const onKeyDown = (e) => {
37
+ if (!e.altKey)
38
+ return;
39
+ const to = e.key === 'ArrowLeft' ? prev : e.key === 'ArrowRight' ? next : undefined;
40
+ if (to === undefined)
41
+ return;
42
+ e.preventDefault();
43
+ if (to)
44
+ onChange(to);
45
+ };
46
+ return (_jsxs("div", { className: cn('inline-flex shrink-0 items-center gap-0.5', className), children: [_jsx(IconButton, { icon: ChevronLeftIcon, label: `Previous ${unit}`, size: btn, disabled: !prev, onClick: () => prev && onChange(prev) }), _jsx(Input, { type: "date", id: id, "aria-label": ariaLabel ?? 'Date', className: cn('tabular-nums', size === 'sm' ? 'h-7 w-32 text-xs md:text-xs' : 'w-36'), min: min, max: max, value: value, disabled: disabled, onKeyDown: onKeyDown, onChange: (e) => onChange(e.target.value) }), _jsx(IconButton, { icon: ChevronRightIcon, label: `Next ${unit}`, size: btn, disabled: !next, onClick: () => next && onChange(next) }), onToday && (_jsx(IconButton, { icon: CalendarClockIcon, label: "Today", size: btn, disabled: disabled || atToday, onClick: onToday }))] }));
47
+ }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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.16",
44
+ "@columenlabs/lumen-ui-types": "0.1.18",
45
45
  "@dnd-kit/core": "6.3.1",
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@dnd-kit/utilities": "3.2.2",