@columenlabs/lumen-ui 0.1.101 → 0.1.103

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.
@@ -0,0 +1,3 @@
1
+ import type { ViewDomain } from './view-model.js';
2
+ /** A trend's date candidates: text fields, the date-like ones first. */
3
+ export declare const dateFields: <T>(domain: ViewDomain<T>) => import("./view-model.js").ViewField<T>[];
@@ -0,0 +1,10 @@
1
+ const DATE_LIKE = /(^|_)(date|day|at|time|month|week|year)($|_)/i;
2
+ /** A trend's date candidates: text fields, the date-like ones first. */
3
+ export const dateFields = (domain) => {
4
+ // A rendered text field still reads its raw value (a report date drawn as Tue, Oct 20).
5
+ const texts = domain.fields.filter((f) => f.format === 'text');
6
+ return [
7
+ ...texts.filter((f) => DATE_LIKE.test(f.key)),
8
+ ...texts.filter((f) => !DATE_LIKE.test(f.key)),
9
+ ];
10
+ };
package/dist/index.d.ts CHANGED
@@ -47,7 +47,7 @@ export { type ChartBand, type ChartMarker, GrowthChart, KEY_DATE_KINDS, KeyDateC
47
47
  export { FoldGroup, NavGroup, type NavItem, type SectionItem, type SectionSubItem, SidebarSection, } from './nav.js';
48
48
  export { type OverviewPageProps, type OverviewStage, type OverviewTone, type OverviewWidget, OverviewList, OverviewPage, ToneDot, } from './overview.js';
49
49
  export { type Crumb, IconButton, type IconButtonProps, Page, PageBody, PageHeader, type PageHeaderProps, } from './page.js';
50
- export { DEFAULT_PREFS, deviceTimeZone, type Prefs, TIME_ZONES, type Theme, type TimeZoneId, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, ASSISTANT_WIDTH, } from './prefs.js';
50
+ export { DEFAULT_PREFS, deviceTimeZone, type Prefs, TIME_ZONES, type Theme, type TimeZoneId, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, zoneAbbr, ASSISTANT_WIDTH, } from './prefs.js';
51
51
  export { KIT, type KitConfig, configureLumen } from './kit-config.js';
52
52
  export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, type KeyTestResult, TestBadge, errorText, useClock, } from './integrations.js';
53
53
  export { ConnectionBadge, type ConnectionField, type ConnectionLine, ConnectionLogo, type ConnectionSignin, type ConnectionStatus, type ConnectionTest, type ConnectionWay, LumenConnection, type LumenConnectionProps, activeWay, connectionStatus, } from './connection.js';
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ export { GrowthChart, KEY_DATE_KINDS, KeyDateChips, PALETTE, benchColour, keyDat
54
54
  export { FoldGroup, NavGroup, SidebarSection, } from './nav.js';
55
55
  export { OverviewList, OverviewPage, ToneDot, } from './overview.js';
56
56
  export { IconButton, Page, PageBody, PageHeader, } from './page.js';
57
- export { DEFAULT_PREFS, deviceTimeZone, TIME_ZONES, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, ASSISTANT_WIDTH, } from './prefs.js';
57
+ export { DEFAULT_PREFS, deviceTimeZone, TIME_ZONES, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, zoneAbbr, ASSISTANT_WIDTH, } from './prefs.js';
58
58
  export { KIT, configureLumen } from './kit-config.js';
59
59
  export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, TestBadge, errorText, useClock, } from './integrations.js';
60
60
  export { ConnectionBadge, ConnectionLogo, LumenConnection, activeWay, connectionStatus, } from './connection.js';
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from 'react';
2
+ type Size = `${number}%` | `${number}px`;
3
+ export declare function DrawerPanels({ defaultSize, minSize, maxSize, children, }: {
4
+ defaultSize: Size;
5
+ minSize: Size;
6
+ maxSize: Size;
7
+ children: ReactNode;
8
+ }): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from './ui/resizable.js';
3
+ export function DrawerPanels({ defaultSize, minSize, maxSize, children, }) {
4
+ return (_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: children })] }));
5
+ }
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode } from 'react';
2
2
  /** A drawer's width on desktop, as the resizable panel takes it ("32%", "320px"). */
3
3
  type Size = `${number}%` | `${number}px`;
4
4
  export interface LumenDrawerProps {
@@ -1,10 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { XIcon } from 'lucide-react';
3
+ import { useEffect } from 'react';
3
4
  import { useIsMobile } from './hooks/use-mobile.js';
4
5
  import { IconButton } from './icon-button.js';
6
+ import { lazyPart } from './lazy-part.js';
5
7
  import { Button } from './ui/button.js';
6
- import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from './ui/resizable.js';
7
8
  import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from './ui/sheet.js';
9
+ /** The resizable panels load with the first drawer opened; until then it opens at its default width. */
10
+ const loadPanels = () => import('./lumen-drawer-panels.js');
11
+ const DrawerPanels = lazyPart(() => loadPanels().then((m) => m.DrawerPanels));
8
12
  /**
9
13
  * The suite's one pullout drawer (named LumenDrawer), on every app.
10
14
  * Phone: a bottom sheet. Desktop: a right-side panel over the page whose width drags at its left
@@ -16,6 +20,13 @@ import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTi
16
20
  */
17
21
  export function LumenDrawer({ open, onClose, title, description, children, footer, footerStart, defaultSize = '32%', minSize = '320px', maxSize = '60%', }) {
18
22
  const isMobile = useIsMobile();
23
+ // Fetched once the page is idle, so the first drawer opened on a desktop has its panels ready.
24
+ useEffect(() => {
25
+ if (isMobile)
26
+ return;
27
+ const t = setTimeout(loadPanels, 2000);
28
+ return () => clearTimeout(t);
29
+ }, [isMobile]);
19
30
  const head = (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx(SheetTitle, { className: "truncate", title: typeof title === 'string' ? title : undefined, children: title }), _jsx(SheetDescription, { className: description ? 'truncate text-muted-foreground text-xs' : 'sr-only', title: typeof description === 'string' ? description : undefined, children: description ?? 'Details' })] }));
20
31
  const body = _jsx("div", { className: "flex flex-col px-4 pb-4", children: children });
21
32
  // Every drawer ends on a footer, styled like a dialog's: the body scrolls above it and always
@@ -26,5 +37,5 @@ export function LumenDrawer({ open, onClose, title, description, children, foote
26
37
  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 pt-3", children: head }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto pt-2", children: body }), !plain && foot] })) : (_jsx(SheetContent, { side: "right", showCloseButton: false,
27
38
  // The resize handle's grab zone is wider than the handle, so a drag can start on the
28
39
  // overlay; the panels library marks that pointerdown handled, and then it doesn't close.
29
- 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] }) })] }) })) }));
40
+ 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: _jsx(DrawerPanels, { defaultSize: defaultSize, minSize: minSize, maxSize: maxSize, 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] }) }) })) }));
30
41
  }
@@ -35,7 +35,6 @@ import { OriginLegend } from './origin-legend.js';
35
35
  import { IconButton, Page } from './page.js';
36
36
  import { SourcedCard } from './sourced-card.js';
37
37
  import { LumenCard, SummaryTiles } from './summary-cards.js';
38
- import { Button } from './ui/button.js';
39
38
  import { computedOf, filterFieldsOf, formulaBlanks, pick, subgroupOf, summaryCash, takesFormulas, withSavedFilters, withViewFields, } from './view-model.js';
40
39
  // Loaded when it first shows (lazy-part.tsx).
41
40
  const ViewEditDialog = lazyPart(() => import('./view-editor.js').then((m) => m.ViewEditDialog), null);
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // month or year, drawn as a line, area, stacked area, bars over time, combo, a spark or a tracker.
4
4
  // With the allocation, donut, bars and card styles they cover every Tremor visualization, drawn on
5
5
  // our own Recharts in ChartContainer. Worked out in the browser from the rows; nothing is fetched.
6
- import { isDollarFormat, } from '@columenlabs/lumen-ui-types/views';
6
+ import { formatBytes, isDollarFormat, } from '@columenlabs/lumen-ui-types/views';
7
7
  import { useMemo } from 'react';
8
8
  import { Area, AreaChart, Bar, BarChart, CartesianGrid, ComposedChart, Line, LineChart, XAxis, YAxis, } from 'recharts';
9
9
  import { cn } from './lib/utils.js';
@@ -24,6 +24,8 @@ function short(v, format, hidden) {
24
24
  const abs = Math.abs(v);
25
25
  if (format === 'percent' || format === 'weight')
26
26
  return `${sign}${abs.toFixed(1)}%`;
27
+ if (format === 'bytes')
28
+ return `${sign}${formatBytes(abs)}`;
27
29
  if (isDollarFormat(format))
28
30
  return `${sign}$${COMPACT.format(abs)}`;
29
31
  return `${sign}${COMPACT.format(abs)}`;
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { controlOn, isCountAgg, isDollarFormat, viewPageSizes, viewPaginationSchema, } from '@columenlabs/lumen-ui-types/views';
2
+ import { bytesParts, controlOn, isCountAgg, isDollarFormat, viewPaginationSchema, } from '@columenlabs/lumen-ui-types/views';
3
3
  // Custom views: any number of views on any app, one page for all of them.
4
4
  // A data domain (holdings, chains, bars, series, research results) describes its fields once in a
5
5
  // `ViewDomain`; a view is a saved `CustomViewConfig` over that domain, written in the page's Edit
@@ -7,12 +7,9 @@ import { controlOn, isCountAgg, isDollarFormat, viewPageSizes, viewPaginationSch
7
7
  // one level deep with collapsible group rows, pivot, totals, sorting, paging), LumenTable and the
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
- import { DndContext, closestCenter } from '@dnd-kit/core';
11
- import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
12
10
  import { createPaginatedRowModel, createSortedRowModel, flexRender, rowPaginationFeature, rowSortingFeature, sortFns, tableFeatures, useTable, } from '@tanstack/react-table';
13
- import { ArrowDownIcon, ArrowUpIcon, ChartCandlestick, ChartSpline, ChevronLeftIcon, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, Grid3x3Icon, LayoutDashboardIcon, ListCollapseIcon, PencilIcon, PlusIcon, Settings2Icon, SigmaIcon, SparklesIcon, TableIcon, UserPenIcon, } from 'lucide-react';
11
+ import { ArrowDownIcon, ArrowUpIcon, ChartCandlestick, ChartSpline, ChevronRightIcon, ChevronsDownIcon, ChevronsDownUpIcon, ChevronsUpDownIcon, Columns3CogIcon, CornerDownRightIcon, DownloadIcon, ListCollapseIcon, PlusIcon, SigmaIcon, UserPenIcon, } from 'lucide-react';
14
12
  import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
- import { useAssistantNote } from './assistant-context.js';
16
13
  import { ColorChip, ColorDot } from './color-dot.js';
17
14
  import { DrawerSection } from './drawer-section.js';
18
15
  import { useFloatingHead } from './floating-head.js';
@@ -21,31 +18,24 @@ import { useIsMobile } from './hooks/use-mobile.js';
21
18
  import { IconButton } from './icon-button.js';
22
19
  import { lazyPart } from './lazy-part.js';
23
20
  import { cn } from './lib/utils.js';
24
- import { LumenTokenizedFilter, useLumenFilter, } from './lumen-filter.js';
25
21
  import { useGridCell } from './lumen-grid.js';
26
22
  import { LumenSelector } from './lumen-selector.js';
27
- import { LumenLazy, LumenLoadFooter, LumenRefresh, LumenSkeleton, LumenStates, useLiveLoad, } from './lumen-source.js';
23
+ import { LumenSkeleton, LumenStates, useLiveLoad } from './lumen-source.js';
28
24
  import { LumenWidget } from './lumen-widget.js';
29
25
  import { MAX_COMPARE, useSymbolChart } from './market/lumen-chart.js';
30
- import { Page } from './page.js';
31
26
  import { LumenPager } from './pager.js';
32
27
  import { maskAmount, useAmountsHidden } from './shell.js';
33
28
  import { useSymbolSheet } from './symbol-sheet-context.js';
34
- import { Tile } from './tile.js';
35
29
  import { Badge } from './ui/badge.js';
36
30
  import { Button } from './ui/button.js';
37
- import { Card } from './ui/card.js';
38
31
  import { Checkbox } from './ui/checkbox.js';
39
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
40
- import { Popover, PopoverAnchor, PopoverContent } from './ui/popover.js';
41
- import { Skeleton } from './ui/skeleton.js';
32
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
42
33
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table.js';
43
- import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
44
34
  import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
45
- import { UnitColumns } from './unit-value.js';
35
+ import { UnitColumns, UnitValue } from './unit-value.js';
46
36
  import { EditCell, EditInputCell, EditManyCell } from './view-edit-cell.js';
47
37
  import { fieldOptions } from './view-formulas.js';
48
- import { CHIP_TONE, FORMULAS_GROUP, chipsOf, computedOf, filterFieldsOf, groupOf, label, pick, rollUp, rollsUp, subgroupOf, withSavedFilters, withViewFields, } from './view-model.js';
38
+ import { CHIP_TONE, FORMULAS_GROUP, chipsOf, computedOf, groupOf, label, pick, rollUp, rollsUp, subgroupOf, } from './view-model.js';
49
39
  // Loaded when it first shows (lazy-part.tsx).
50
40
  const ViewEditDialog = lazyPart(() => import('./view-editor.js').then((m) => m.ViewEditDialog), null);
51
41
  export { CHIP_TONE, FORMULAS_GROUP, RUN_STATE_TONE } from './view-model.js';
@@ -104,6 +94,11 @@ export function ViewValueCell({ value, format, chip, tone, color, amount, compac
104
94
  return _jsx("span", { children: text });
105
95
  return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "block max-w-[24rem] truncate", children: text }) }), _jsx(TooltipContent, { className: "max-w-md whitespace-normal", children: text })] }));
106
96
  }
97
+ if (format === 'bytes' && !hidden) {
98
+ // A size: its number and unit in the column's unit slot, so a column of sizes lines up.
99
+ const [n, unit] = bytesParts(value);
100
+ return _jsx(UnitValue, { value: `${value < 0 ? MINUS : ''}${n}`, unit: unit });
101
+ }
107
102
  const abs = hidden ? maskAmount('0') : digitsOf(format, compact).format(Math.abs(value));
108
103
  const signed = format === 'signed' || format === 'percent' || format === 'price_signed';
109
104
  const sign = value < 0 ? MINUS : signed && value > 0 ? '+' : '';
@@ -197,7 +197,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
197
197
  key: string;
198
198
  placement: "column" | "inline";
199
199
  }[];
200
- formats?: Record<string, "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
200
+ formats?: Record<string, "bytes" | "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
201
201
  aggs?: Record<string, "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum"> | undefined;
202
202
  shows?: Record<string, string> | undefined;
203
203
  group: {
@@ -1,16 +1,17 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { OUTLOOK_INDEXES, marketWindows, } from '@columenlabs/lumen-ui-types/market';
2
+ import { OUTLOOK_INDEXES } from '@columenlabs/lumen-ui-types/market';
3
3
  // LumenSavedWidget (as blocks; renamed): a page (LumenLayout) is an ordered list of cards, each a third, half, two thirds or the full width.
4
4
  // A card is the view (LumenView) or a widget. Chart and top-N widgets read every row of the view
5
5
  // (the table's filter is the table's), narrowed by their own tokens, and work their chart out here in the browser:
6
6
  // nothing new is fetched; market and price chart widgets read their own data. LumenLayout lays the
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
- import { VIEW_CARD_KIND_NAMES, WIDGET_STYLES, controlOn, isDollarFormat, isOwnTile, isRowCard, isTableCard, trendBuckets, viewCardKinds, viewCardSchema, viewWidgetCatalog, widgetStyleId, widgetStyleOf, } from '@columenlabs/lumen-ui-types/views';
9
+ import { VIEW_CARD_KIND_NAMES, WIDGET_STYLES, controlOn, isDollarFormat, isOwnTile, isRowCard, isTableCard, viewCardKinds, viewCardSchema, viewWidgetCatalog, widgetStyleId, widgetStyleOf, } from '@columenlabs/lumen-ui-types/views';
10
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';
14
+ import { dateFields } from './date-fields.js';
14
15
  import { FoldChevron, useFolded, usePageKey } from './fold.js';
15
16
  import { useIsMobile } from './hooks/use-mobile.js';
16
17
  import { IconButton } from './icon-button.js';
@@ -18,7 +19,7 @@ import { useDomainReads } from './joins.js';
18
19
  import { lazyPart } from './lazy-part.js';
19
20
  import { cn } from './lib/utils.js';
20
21
  import { LumenBoundary } from './lumen-boundary.js';
21
- import { LumenTokenizedFilter, filterByTokens, useLumenFilter, } from './lumen-filter.js';
22
+ import { filterByTokens } from './lumen-filter.js';
22
23
  import { useGridCell } from './lumen-grid.js';
23
24
  import { CardIdProvider, LinkDot, LinkPicker, linkVar, useLinks, } from './lumen-links.js';
24
25
  import { LumenSelector } from './lumen-selector.js';
@@ -26,27 +27,19 @@ import { LumenFreshness, LumenLazy, LumenLoadFooter, LumenRefresh, LumenStates,
26
27
  import { useHasMarket } from './market/live.js';
27
28
  import { useBarsUrl, useLumenChart } from './market/lumen-chart.js';
28
29
  import { maskAmount, useAmountsHidden } from './shell.js';
29
- import { SummaryCardsEditor, summaryCardIssues } from './summary-cards.js';
30
30
  import { Badge } from './ui/badge.js';
31
31
  import { Button } from './ui/button.js';
32
32
  import { Card } from './ui/card.js';
33
- import { Checkbox } from './ui/checkbox.js';
34
- import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, dialogBody, } from './ui/dialog.js';
33
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, dialogBody, } from './ui/dialog.js';
35
34
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
36
- import { Input } from './ui/input.js';
37
35
  import { Label } from './ui/label.js';
38
36
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from './ui/popover.js';
39
37
  import { Skeleton } from './ui/skeleton.js';
40
- import { Switch } from './ui/switch.js';
41
38
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table.js';
42
39
  import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs.js';
43
- import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
44
40
  import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
45
- import { fieldOptions } from './view-formulas.js';
46
- import { computedOf, label, summaryCash, } from './view-model.js';
47
- import { WidgetControlsEditor } from './widget-controls.js';
41
+ import { computedOf } from './view-model.js';
48
42
  import { PAGE_SOURCE, useWidgetDataOptions } from './widget-data.js';
49
- import { dateFields } from './widget-settings.js';
50
43
  import { COMPACT, MINUS } from './widget-values.js';
51
44
  // Loaded when it first shows (lazy-part.tsx).
52
45
  const MarketWidget = lazyPart(() => import('./market/market-widget.js').then((m) => m.MarketWidget), _jsx(Skeleton, { className: "h-full min-h-40 w-full" }));
@@ -8,6 +8,7 @@ import { useMemo, useState } from 'react';
8
8
  import { LumenLayout } from '../lumen-layout.js';
9
9
  import { LumenSelector } from '../lumen-selector.js';
10
10
  import { useLumenLoad } from '../lumen-source.js';
11
+ import { getPrefs } from '../prefs.js';
11
12
  import { Tile, TileRow } from '../tile.js';
12
13
  import { Button } from '../ui/button.js';
13
14
  import { CopyLink } from './invite-drawer.js';
@@ -258,7 +259,8 @@ export function AuditSection({ frame }) {
258
259
  key: 'day',
259
260
  label: 'Day',
260
261
  icon: ClockIcon,
261
- value: (e) => new Date(e.at).toLocaleDateString(undefined, {
262
+ value: (e) => new Date(e.at).toLocaleDateString('en-US', {
263
+ timeZone: getPrefs().timeZone,
262
264
  weekday: 'short',
263
265
  day: 'numeric',
264
266
  month: 'short',
@@ -270,7 +272,13 @@ export function AuditSection({ frame }) {
270
272
  key: 'time',
271
273
  label: 'When',
272
274
  icon: ClockIcon,
273
- value: (e) => new Date(e.at).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }),
275
+ value: (e) => new Date(e.at).toLocaleTimeString('en-US', {
276
+ timeZone: getPrefs().timeZone,
277
+ // Two-digit hours keep a column of times aligned.
278
+ hour: '2-digit',
279
+ minute: '2-digit',
280
+ timeZoneName: 'shortGeneric',
281
+ }),
274
282
  format: 'text',
275
283
  },
276
284
  {
@@ -6,7 +6,10 @@ import type { ViewChipTone } from '../view-model.js';
6
6
  import type { PeopleAdapter, WithSync } from './client.js';
7
7
  export declare const STATUS_LABEL: Record<PersonStatus, string>;
8
8
  export declare const statusTone: (p: Pick<Person, 'status'>) => ViewChipTone | undefined;
9
- /** `today 09:12`, `yesterday 18:42`, `24 Oct`, `24 Oct 2025`; a dash for never. */
9
+ /**
10
+ * `today 9:12 AM PT`, `yesterday 6:42 PM PT`, `Oct 24`, `Oct 24, 2025`; a dash for never. Days and
11
+ * times are the person's time zone (Settings ▸ Preferences), not the device's.
12
+ */
10
13
  export declare function when(ts: number | null | undefined, now?: number): string;
11
14
  /** `in 5 d`, `in 3 h`, `2 d ago`: how far a time is from now. */
12
15
  export declare function fromNow(ts: number | null | undefined, now?: number): string;
@@ -8,6 +8,7 @@ import { useCallback, useState } from 'react';
8
8
  import { errorText } from '../integrations.js';
9
9
  import { LumenSelector } from '../lumen-selector.js';
10
10
  import { lumenToast } from '../lumen-toast.js';
11
+ import { getPrefs } from '../prefs.js';
11
12
  export const STATUS_LABEL = {
12
13
  active: 'Active',
13
14
  invited: 'Invited',
@@ -22,24 +23,33 @@ export const statusTone = (p) => p.status === 'active'
22
23
  ? 'amber'
23
24
  : 'muted';
24
25
  const DAY = 86_400_000;
25
- /** Non-breaking spaces, so `today 07:18 PM` and `in 7 d` never split across lines in any text. */
26
+ /** Non-breaking spaces, so `today 7:18 PM PT` and `in 7 d` never split across lines in any text. */
26
27
  const glue = (s) => s.replace(/ /g, '\u00a0');
27
- const sameDay = (a, b) => a.toDateString() === b.toDateString();
28
- /** `today 09:12`, `yesterday 18:42`, `24 Oct`, `24 Oct 2025`; a dash for never. */
28
+ /**
29
+ * `today 9:12 AM PT`, `yesterday 6:42 PM PT`, `Oct 24`, `Oct 24, 2025`; a dash for never. Days and
30
+ * times are the person's time zone (Settings ▸ Preferences), not the device's.
31
+ */
29
32
  export function when(ts, now = Date.now()) {
30
33
  if (!ts)
31
34
  return '—';
32
- const d = new Date(ts);
33
- const n = new Date(now);
34
- const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
35
- if (sameDay(d, n))
35
+ const { timeZone } = getPrefs();
36
+ const day = (t) => new Date(t).toLocaleDateString('en-US', { timeZone });
37
+ const year = (t) => new Date(t).toLocaleDateString('en-US', { timeZone, year: 'numeric' });
38
+ const time = new Date(ts).toLocaleTimeString('en-US', {
39
+ timeZone,
40
+ hour: 'numeric',
41
+ minute: '2-digit',
42
+ timeZoneName: 'shortGeneric',
43
+ });
44
+ if (day(ts) === day(now))
36
45
  return glue(`today ${time}`);
37
- if (sameDay(d, new Date(now - DAY)))
46
+ if (day(ts) === day(now - DAY))
38
47
  return glue(`yesterday ${time}`);
39
- return glue(d.toLocaleDateString(undefined, {
48
+ return glue(new Date(ts).toLocaleDateString('en-US', {
49
+ timeZone,
40
50
  day: 'numeric',
41
51
  month: 'short',
42
- ...(d.getFullYear() === n.getFullYear() ? {} : { year: 'numeric' }),
52
+ ...(year(ts) === year(now) ? {} : { year: 'numeric' }),
43
53
  }));
44
54
  }
45
55
  /** `in 5 d`, `in 3 h`, `2 d ago`: how far a time is from now. */
package/dist/prefs.d.ts CHANGED
@@ -69,5 +69,7 @@ export declare const resolvedTheme: (p?: Prefs) => 'light' | 'dark';
69
69
  export declare function setPref<K extends keyof Prefs>(key: K, value: Prefs[K]): void;
70
70
  export declare const getPrefs: () => Prefs;
71
71
  export declare function usePrefs(): Prefs;
72
+ /** A zone's short generic name, as people say it next to a time: `PT`, `ET`, `UTC` (never PDT or PST). */
73
+ export declare function zoneAbbr(tz: string, at?: Date): string;
72
74
  /** `Pacific`, `UTC`: the short name shown next to times. */
73
75
  export declare const timeZoneLabel: (id: TimeZoneId) => "America/Chicago" | "America/Los_Angeles" | "America/New_York" | "Central" | "Eastern" | "Pacific" | "UTC";
package/dist/prefs.js CHANGED
@@ -130,5 +130,15 @@ export function usePrefs() {
130
130
  return () => listeners.delete(cb);
131
131
  }, getPrefs, () => DEFAULT_PREFS);
132
132
  }
133
+ /** A zone's short generic name, as people say it next to a time: `PT`, `ET`, `UTC` (never PDT or PST). */
134
+ export function zoneAbbr(tz, at = new Date()) {
135
+ // Intl names UTC "GMT+0"; people say UTC.
136
+ if (/^(Etc\/)?(UTC|GMT|Universal|Zulu)$/.test(tz))
137
+ return 'UTC';
138
+ const part = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'shortGeneric' })
139
+ .formatToParts(at)
140
+ .find((p) => p.type === 'timeZoneName');
141
+ return part?.value ?? tz;
142
+ }
133
143
  /** `Pacific`, `UTC`: the short name shown next to times. */
134
144
  export const timeZoneLabel = (id) => TIME_ZONES.find((z) => z.id === id)?.label ?? id;
package/dist/schedules.js CHANGED
@@ -11,7 +11,7 @@ import { LumenConfirm } from './confirm.js';
11
11
  import { useIsMobile } from './hooks/use-mobile.js';
12
12
  import { LumenSelector } from './lumen-selector.js';
13
13
  import { IconButton } from './page.js';
14
- import { timeZoneLabel, usePrefs } from './prefs.js';
14
+ import { usePrefs, zoneAbbr } from './prefs.js';
15
15
  import { useShell } from './shell.js';
16
16
  import { Badge } from './ui/badge.js';
17
17
  import { Button } from './ui/button.js';
@@ -47,14 +47,53 @@ const daysText = (days) => {
47
47
  return 'Weekdays';
48
48
  return d.map((x) => DAYS[x]).join(', ');
49
49
  };
50
- const zoneText = (tz) => tz.split('/').pop()?.replace('_', ' ') ?? tz;
51
- /** When it runs, in words. */
52
- function when(s) {
50
+ /** A window that spans the whole day on every day says nothing a person needs. */
51
+ const allDay = (s) => s.days.length === 7 && s.from === '00:00' && (s.to === '23:59' || s.to === '24:00');
52
+ /** The zone's offset from UTC at `at`, in ms (positive east of UTC). */
53
+ const offsetOf = (tz, at) => {
54
+ const p = Object.fromEntries(new Intl.DateTimeFormat('en-US', {
55
+ timeZone: tz,
56
+ hourCycle: 'h23',
57
+ year: 'numeric',
58
+ month: 'numeric',
59
+ day: 'numeric',
60
+ hour: 'numeric',
61
+ minute: 'numeric',
62
+ })
63
+ .formatToParts(at)
64
+ .map((x) => [x.type, Number(x.value)]));
65
+ const n = (k) => p[k] ?? 0;
66
+ return (Date.UTC(n('year'), n('month') - 1, n('day'), n('hour'), n('minute')) -
67
+ Math.floor(at / 60_000) * 60_000);
68
+ };
69
+ /** `03:15` in `from` (today) as the same moment's clock in `to`: `03:15` UTC → `8:15 PM` PT. */
70
+ const clockIn = (hhmm, from, to) => {
71
+ if (from === to)
72
+ return clockText(hhmm);
73
+ const [h = 0, m = 0] = hhmm.split(':').map(Number);
74
+ const now = new Date();
75
+ const wall = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), h, m);
76
+ const at = wall - offsetOf(from, wall);
77
+ return new Date(at).toLocaleTimeString('en-US', {
78
+ timeZone: to,
79
+ hour: 'numeric',
80
+ minute: '2-digit',
81
+ });
82
+ };
83
+ /**
84
+ * When it runs, in words, in the viewer's zone (`zone`, the person's preference): `Every 6 h`,
85
+ * `Every 1 h · Weekdays 6:00 AM–2:00 PM PT`, `Daily at 8:15 PM PT`. Days stay the schedule's own.
86
+ */
87
+ function when(s, zone) {
53
88
  if (s.when)
54
89
  return s.when;
55
- if (s.kind === 'window')
56
- return `${daysText(s.days)} ${clockText(s.from)}–${clockText(s.to)} ${zoneText(s.tz)}`;
57
- return `Daily at ${clockText(s.from)} ${zoneText(s.tz)}`;
90
+ const at = (hhmm) => clockIn(hhmm, s.tz, zone);
91
+ if (s.kind === 'daily')
92
+ return `Daily at ${at(s.from)} ${zoneAbbr(zone)}`;
93
+ const every = `Every ${everyText(s.every)}`;
94
+ if (allDay(s))
95
+ return every;
96
+ return `${every} · ${daysText(s.days)} ${at(s.from)}–${at(s.to)} ${zoneAbbr(zone)}`;
58
97
  }
59
98
  function useTime() {
60
99
  const { timeZone } = usePrefs();
@@ -63,6 +102,7 @@ function useTime() {
63
102
  weekday: 'short',
64
103
  hour: 'numeric',
65
104
  minute: '2-digit',
105
+ timeZoneName: 'shortGeneric',
66
106
  });
67
107
  }
68
108
  function Last({ s, compact = false }) {
@@ -90,6 +130,7 @@ function TimeFields({ kind, draft, set, }) {
90
130
  function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
91
131
  const assistantName = useAssistantName();
92
132
  const time = useTime();
133
+ const { timeZone } = usePrefs();
93
134
  const assistant = useAssistant();
94
135
  const { closeSettings } = useShell();
95
136
  const [brief, setBrief] = useState({
@@ -141,7 +182,7 @@ function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
141
182
  setBusy(null);
142
183
  }
143
184
  };
144
- return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: s.name }), _jsx(SheetDescription, { children: when(s) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-5 overflow-y-auto px-4", children: [_jsx("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-xs", children: [
185
+ return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: s.name }), _jsx(SheetDescription, { children: when(s, timeZone) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-5 overflow-y-auto px-4", children: [_jsx("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-xs", children: [
145
186
  ['Inspects', s.about.inspects],
146
187
  ['Processes', s.about.processes],
147
188
  ['Enriches', s.about.enriches],
@@ -166,6 +207,7 @@ function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
166
207
  /** The Settings ▸ Schedules section's body. */
167
208
  export function SchedulesSection({ schedules, error, onSave, onRun, onRuns, recent, briefs, }) {
168
209
  const isMobile = useIsMobile();
210
+ const { timeZone } = usePrefs();
169
211
  const { closeSettings } = useShell();
170
212
  const [open, setOpen] = useState(null);
171
213
  const [adding, setAdding] = useState(false);
@@ -178,15 +220,14 @@ export function SchedulesSection({ schedules, error, onSave, onRun, onRuns, rece
178
220
  }
179
221
  : undefined;
180
222
  const editing = schedules?.find((s) => s.id === open);
181
- const { timeZone } = usePrefs();
182
223
  if (error)
183
224
  return _jsx("p", { className: "text-sm text-loss", children: error });
184
225
  if (!schedules)
185
226
  return _jsx(Skeleton, { className: "h-48 w-full" });
186
- return (_jsxs(_Fragment, { children: [isMobile ? (_jsx("div", { className: "grid grid-cols-1 divide-y rounded-lg border", children: schedules.map((s) => (_jsxs("button", { type: "button", className: "grid grid-cols-[1fr_auto] items-center gap-x-3 gap-y-1 p-3 text-left hover:bg-muted/50", onClick: () => setOpen(s.id), children: [_jsx("span", { className: "font-medium", children: s.name }), _jsx(OnSwitch, { s: s, onSave: onSave }), _jsxs("span", { className: "col-span-2 text-xs text-muted-foreground", children: [s.kind === 'daily' ? '' : `Every ${everyText(s.every)} · `, when(s)] }), _jsx("span", { className: "col-span-2 text-xs", children: s.waiting ? _jsx("span", { className: "text-warning", children: s.waiting }) : _jsx(Last, { s: s }) })] }, s.id))) })) : (_jsx("div", { className: "overflow-x-auto overflow-y-hidden rounded-lg border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-12", children: "On" }), _jsx(TableHead, { children: "Schedule" }), _jsx(TableHead, { children: "When" }), _jsx(TableHead, { children: "Last run" }), onRuns ? _jsx(TableHead, { className: "w-0" }) : null] }) }), _jsx(TableBody, { children: schedules.map((s) => (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => setOpen(s.id), children: [_jsx(TableCell, { children: _jsx(OnSwitch, { s: s, onSave: onSave }) }), _jsx(TableCell, { className: "font-medium", children: s.name }), _jsxs(TableCell, { className: "min-w-48 whitespace-normal text-muted-foreground", children: [s.kind === 'daily' ? '' : `Every ${everyText(s.every)} · `, when(s)] }), _jsx(TableCell, { className: "max-w-48", children: s.waiting ? (_jsx(Badge, { variant: "outline", className: "text-warning", title: s.waiting, children: "Not connected" })) : (_jsx(Last, { s: s, compact: true })) }), onRuns ? (_jsx(TableCell, { children: _jsx(IconButton, { icon: ListIcon, label: `${s.name} runs`, onClick: (e) => {
227
+ return (_jsxs(_Fragment, { children: [isMobile ? (_jsx("div", { className: "grid grid-cols-1 divide-y rounded-lg border", children: schedules.map((s) => (_jsxs("button", { type: "button", className: "grid grid-cols-[1fr_auto] items-center gap-x-3 gap-y-1 p-3 text-left hover:bg-muted/50", onClick: () => setOpen(s.id), children: [_jsx("span", { className: "font-medium", children: s.name }), _jsx(OnSwitch, { s: s, onSave: onSave }), _jsx("span", { className: "col-span-2 text-xs text-muted-foreground", children: when(s, timeZone) }), _jsx("span", { className: "col-span-2 text-xs", children: s.waiting ? _jsx("span", { className: "text-warning", children: s.waiting }) : _jsx(Last, { s: s }) })] }, s.id))) })) : (_jsx("div", { className: "overflow-x-auto overflow-y-hidden rounded-lg border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-12", children: "On" }), _jsx(TableHead, { children: "Schedule" }), _jsx(TableHead, { children: "When" }), _jsx(TableHead, { children: "Last run" }), onRuns ? _jsx(TableHead, { className: "w-0" }) : null] }) }), _jsx(TableBody, { children: schedules.map((s) => (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => setOpen(s.id), children: [_jsx(TableCell, { children: _jsx(OnSwitch, { s: s, onSave: onSave }) }), _jsx(TableCell, { className: "font-medium", children: s.name }), _jsx(TableCell, { className: "min-w-48 whitespace-normal text-muted-foreground", children: when(s, timeZone) }), _jsx(TableCell, { className: "max-w-48", children: s.waiting ? (_jsx(Badge, { variant: "outline", className: "text-warning", title: s.waiting, children: "Not connected" })) : (_jsx(Last, { s: s, compact: true })) }), onRuns ? (_jsx(TableCell, { children: _jsx(IconButton, { icon: ListIcon, label: `${s.name} runs`, onClick: (e) => {
187
228
  e.stopPropagation();
188
229
  toRuns?.(s.id);
189
- } }) })) : null] }, s.id))) })] }) })), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Last runs shown in ", timeZoneLabel(timeZone), ".", onRuns ? ' Each run is a line in Logs.' : ''] }), briefs ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setAdding(true), children: [_jsx(PlusIcon, {}), "Brief"] })) : null] }), briefs ? (_jsx(Sheet, { open: adding, onOpenChange: setAdding, children: _jsx(SheetContent, { side: isMobile ? 'bottom' : 'right', className: "gap-4", children: _jsx(NewBrief, { create: async (b, time) => {
230
+ } }) })) : null] }, s.id))) })] }) })), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [onRuns ? (_jsx("p", { className: "text-xs text-muted-foreground", children: "Each run is a line in Logs." })) : (_jsx("span", {})), briefs ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setAdding(true), children: [_jsx(PlusIcon, {}), "Brief"] })) : null] }), briefs ? (_jsx(Sheet, { open: adding, onOpenChange: setAdding, children: _jsx(SheetContent, { side: isMobile ? 'bottom' : 'right', className: "gap-4", children: _jsx(NewBrief, { create: async (b, time) => {
190
231
  const id = await briefs.create(b);
191
232
  await onSave(id, time);
192
233
  return id;
package/dist/settings.js CHANGED
@@ -6,17 +6,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  // sections (an app: Repos). On phone the list becomes a picker.
7
7
  // A search box tops the list and filters the rows on every section.
8
8
  // `?settings=<section>` in the URL opens it at that section (GitHub's install page returns there).
9
- import { LogOutIcon, MonitorIcon, MoonIcon, PaletteIcon, SearchIcon, ShieldCheckIcon, SlidersHorizontalIcon, SunIcon, UserRoundIcon, XIcon, } from 'lucide-react';
9
+ import { MonitorIcon, MoonIcon, PaletteIcon, SearchIcon, ShieldCheckIcon, SlidersHorizontalIcon, SunIcon, UserRoundIcon, XIcon, } from 'lucide-react';
10
10
  import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, } from 'react';
11
11
  import { createPortal } from 'react-dom';
12
- import { toast } from 'sonner';
13
12
  import { useAssistantMoves } from './assistant-screen.js';
14
13
  import { LumenSelector } from './lumen-selector.js';
15
14
  import { FONTS, FONT_SIZES, setPref, usePrefs, } from './prefs.js';
16
15
  import { TimeZoneSelect } from './status.js';
17
- import { Button } from './ui/button.js';
18
16
  import { Dialog, DialogContent, DialogDescription, DialogTitle } from './ui/dialog.js';
19
- import { Input } from './ui/input.js';
20
17
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './ui/input-group.js';
21
18
  import { Label } from './ui/label.js';
22
19
  import { Separator } from './ui/separator.js';
@@ -0,0 +1,19 @@
1
+ import { type ViewTile } from '@columenlabs/lumen-ui-types/views';
2
+ import { type FilterField } from './lumen-filter.js';
3
+ import { type ViewDomain } from './view-model.js';
4
+ /**
5
+ * The Summary's cards in Edit widget: a grid to pick and drag, the domain's own cards to add or
6
+ * remove, and the picked card of the view's own: its main number (label, roll-up, field, rows,
7
+ * colour by sign) and its secondary lines.
8
+ */
9
+ export declare function SummaryCardsEditor<T>({ domain, tiles, onChange, rows, fields, problems, one, }: {
10
+ domain: ViewDomain<T>;
11
+ tiles: ViewTile[];
12
+ onChange: (tiles: ViewTile[]) => void;
13
+ /** One card only (LumenCard): a built-in replaces it and there is no Add card. */
14
+ one?: boolean;
15
+ /** The Summary's rows, for the previews. */
16
+ rows: T[];
17
+ fields: FilterField<T>[];
18
+ problems: (string | null)[];
19
+ }): import("react").JSX.Element;
@@ -0,0 +1,139 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The cards part of the Summary's Edit widget (cards reorder by drag), in a file of its own so the
3
+ // drag library loads with the editor, not with every page that draws a Summary.
4
+ import { MAX_TILE_LINES, isCountAgg, isOwnTile, viewAggs, } from '@columenlabs/lumen-ui-types/views';
5
+ import { DndContext, KeyboardSensor, PointerSensor, TouchSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core';
6
+ import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable, } from '@dnd-kit/sortable';
7
+ import { CSS } from '@dnd-kit/utilities';
8
+ import { PlusIcon, Trash2Icon, XIcon } from 'lucide-react';
9
+ import { useEffect, useState } from 'react';
10
+ import { LumenConfirm } from './confirm.js';
11
+ import { IconButton } from './icon-button.js';
12
+ import { cn } from './lib/utils.js';
13
+ import { LumenTokenizedFilter, useLumenFilter } from './lumen-filter.js';
14
+ import { LumenSelector } from './lumen-selector.js';
15
+ import { Badge } from './ui/badge.js';
16
+ import { Button } from './ui/button.js';
17
+ import { Input } from './ui/input.js';
18
+ import { Label } from './ui/label.js';
19
+ import { Switch } from './ui/switch.js';
20
+ import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
21
+ import { AGG_LABELS, MAX_TILES, fieldOptions } from './view-formulas.js';
22
+ import { takesFormulas } from './view-model.js';
23
+ import { FigureText, OwnTile, figuresOf, isNumber } from './summary-cards.js';
24
+ const dragOptions = { activationConstraint: { distance: 4 } };
25
+ function FieldPick({ value, fields, domain, label, onChange, }) {
26
+ return (_jsx(LumenSelector, { label: label, size: "sm", className: "w-full min-w-0", placeholder: "Pick a field", options: fieldOptions(fields, domain), value: value, onChange: onChange }));
27
+ }
28
+ function AggPick({ value, label, onChange, }) {
29
+ return (_jsx(LumenSelector, { label: label, size: "sm", className: "w-full min-w-0", options: viewAggs.map((a) => ({ value: a, label: AGG_LABELS[a] })), value: value, onChange: onChange }));
30
+ }
31
+ /** A card in the editor's grid: its label, its value now and where it comes from. */
32
+ function CardChip({ id, selected, onSelect, label, value, how, problem, domId, }) {
33
+ const s = useSortable({ id });
34
+ return (_jsxs(Button, { ref: s.setNodeRef, id: domId, variant: "outline", style: { transform: CSS.Transform.toString(s.transform), transition: s.transition }, className: cn('h-auto min-h-16 min-w-0 flex-col items-start justify-start gap-0.5 rounded-lg px-2.5 py-2 text-left font-normal whitespace-normal', selected && 'border-primary ring-3 ring-primary/15', problem && 'border-destructive', s.isDragging && 'z-20 shadow-md'), ...s.attributes, ...s.listeners, "aria-pressed": selected, onClick: onSelect, children: [_jsx("span", { className: "w-full truncate text-xs text-muted-foreground", children: label }), _jsx("span", { className: "num w-full truncate text-sm font-semibold", children: value }), _jsx("span", { className: "w-full truncate text-[0.7rem] text-muted-foreground", children: how })] }));
35
+ }
36
+ /** A card's own rows filter, kept in step with the card. */
37
+ function CardFilter({ rows, fields, tokens, onChange, }) {
38
+ const filter = useLumenFilter(rows, fields, { initial: tokens });
39
+ const next = JSON.stringify(filter.tokens);
40
+ // biome-ignore lint/correctness/useExhaustiveDependencies: reports the box's tokens when they change
41
+ useEffect(() => {
42
+ if (next !== JSON.stringify(tokens))
43
+ onChange(filter.tokens);
44
+ }, [next]);
45
+ return (_jsx(LumenTokenizedFilter, { filter: filter, label: "Card filter", placeholder: "All the Summary's rows" }));
46
+ }
47
+ const LINE_KINDS = [
48
+ { value: 'value', label: 'Number' },
49
+ { value: 'pct', label: '% of' },
50
+ { value: 'text', label: 'Text' },
51
+ ];
52
+ function LineRow({ line, nums, fields, domain, onChange, onRemove, }) {
53
+ const first = nums[0]?.key ?? '';
54
+ const as = (kind) => kind === 'text'
55
+ ? { kind, text: 'Note' }
56
+ : kind === 'pct'
57
+ ? { kind, field: line.kind !== 'text' ? line.field : first, agg: 'sum' }
58
+ : {
59
+ kind,
60
+ field: line.kind !== 'text' ? line.field : first,
61
+ agg: 'sum',
62
+ filter: [],
63
+ suffix: '',
64
+ };
65
+ return (_jsxs("div", { className: "flex flex-col gap-1.5 rounded-md border p-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(ToggleGroup, { type: "single", size: "sm", variant: "outline", spacing: 0, value: line.kind, onValueChange: (v) => v && onChange(as(v)), "aria-label": "Line kind", children: LINE_KINDS.map((k) => (_jsx(ToggleGroupItem, { value: k.value, className: "px-2 text-xs", children: k.label }, k.value))) }), _jsx(IconButton, { icon: XIcon, label: "Remove line", className: "ml-auto", onClick: onRemove })] }), line.kind === 'text' ? (_jsx(Input, { "aria-label": "Line text", value: line.text, maxLength: 60, onChange: (e) => onChange({ ...line, text: e.target.value }) })) : (_jsxs("div", { className: "grid grid-cols-[repeat(2,minmax(0,1fr))] gap-1.5", children: [_jsx(AggPick, { value: line.agg, label: "Line roll-up", onChange: (agg) => onChange({ ...line, agg }) }), _jsx(FieldPick, { value: line.field, fields: isCountAgg(line.agg) ? fields : nums, domain: domain, label: "Line field", onChange: (field) => onChange({ ...line, field }) }), line.kind === 'value' && (_jsx(Input, { "aria-label": "Words after the number", className: "col-span-2", placeholder: "Words after the number (contracts)", value: line.suffix, maxLength: 30, onChange: (e) => onChange({ ...line, suffix: e.target.value }) }))] }))] }));
66
+ }
67
+ /**
68
+ * The Summary's cards in Edit widget: a grid to pick and drag, the domain's own cards to add or
69
+ * remove, and the picked card of the view's own: its main number (label, roll-up, field, rows,
70
+ * colour by sign) and its secondary lines.
71
+ */
72
+ export function SummaryCardsEditor({ domain, tiles, onChange, rows, fields, problems, one = false, }) {
73
+ const sensors = useSensors(useSensor(PointerSensor, dragOptions), useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
74
+ const builtIns = domain.tiles ?? [];
75
+ const [current, setCurrent] = useState(tiles.find(isOwnTile)?.key ?? tiles[0]?.key ?? null);
76
+ const nums = domain.fields.filter(isNumber);
77
+ const pickable = domain.fields;
78
+ const fieldOf = (k) => domain.fields.find((f) => f.key === k);
79
+ const i = tiles.findIndex((t) => t.key === current);
80
+ const picked = tiles[i];
81
+ const own = picked && isOwnTile(picked) ? picked : undefined;
82
+ const set = (p) => onChange(tiles.map((t, j) => (j === i && isOwnTile(t) ? { ...t, ...p } : t)));
83
+ const add = () => {
84
+ let n = tiles.length + 1;
85
+ while (tiles.some((t) => t.key === `tile_${n}`) || builtIns.some((w) => w.key === `tile_${n}`))
86
+ n++;
87
+ const key = `tile_${n}`;
88
+ const field = (nums.find((f) => f.amount) ?? nums[0])?.key ?? '';
89
+ onChange([
90
+ ...tiles,
91
+ { key, label: `Card ${n}`, field, agg: 'sum', filter: [], sign: false, lines: [] },
92
+ ]);
93
+ setCurrent(key);
94
+ };
95
+ const ownCount = tiles.filter(isOwnTile).length;
96
+ const off = builtIns.filter((w) => !tiles.some((t) => t.key === w.key));
97
+ const preview = (t) => {
98
+ if (!isOwnTile(t))
99
+ return { value: 'Built in', how: 'Built in' };
100
+ const figs = figuresOf(domain, t, rows, fields);
101
+ return {
102
+ value: figs ? _jsx(FigureText, { f: figs.main }) : '—',
103
+ how: `${AGG_LABELS[t.agg]} of ${fieldOf(t.field)?.label ?? t.field}`,
104
+ };
105
+ };
106
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: (e) => {
107
+ const keys = tiles.map((t) => t.key);
108
+ const from = keys.indexOf(String(e.active.id));
109
+ const to = e.over ? keys.indexOf(String(e.over.id)) : -1;
110
+ if (from >= 0 && to >= 0 && from !== to)
111
+ onChange(arrayMove(tiles, from, to));
112
+ }, children: _jsx(SortableContext, { items: tiles.map((t) => t.key), strategy: rectSortingStrategy, children: _jsxs("div", { className: "grid grid-cols-3 gap-1.5", "data-testid": "summary-cards", children: [tiles.map((t, j) => {
113
+ const p = preview(t);
114
+ const label = isOwnTile(t)
115
+ ? t.label
116
+ : (builtIns.find((w) => w.key === t.key)?.label ?? t.key);
117
+ return (_jsx(CardChip, { id: t.key, selected: current === t.key, onSelect: () => setCurrent(t.key), label: label, value: p.value, how: p.how, problem: !!problems[j], domId: `summary-card-${j}` }, t.key));
118
+ }), takesFormulas(domain) && !(one && tiles.length) && (_jsxs(Button, { id: "summary-card-add", variant: "outline", className: "h-auto min-h-16 border-dashed font-normal text-muted-foreground", disabled: ownCount >= MAX_TILES, onClick: add, children: [_jsx(PlusIcon, {}), " Add card"] }))] }) }) }), off.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Built in:" }), off.map((w) => (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
119
+ onChange(one ? [{ key: w.key }] : [...tiles, { key: w.key }]);
120
+ setCurrent(w.key);
121
+ }, children: [_jsx(PlusIcon, {}), " ", w.label] }, w.key)))] })), picked && !own && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border p-3", children: [_jsx(Badge, { variant: "secondary", className: "font-normal", children: "Built in" }), _jsxs("span", { className: "flex-1 text-sm", children: [builtIns.find((w) => w.key === picked.key)?.label ?? picked.key, " works out its own lines."] }), _jsx(IconButton, { icon: Trash2Icon, label: "Remove card", onClick: () => {
122
+ onChange(tiles.filter((_, j) => j !== i));
123
+ setCurrent(null);
124
+ } })] })), own && (_jsxs("div", { className: "flex min-w-0 flex-col gap-3 rounded-lg border p-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex-1 text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "Main number" }), _jsx(LumenConfirm, { title: `Remove card ${own.label}?`, description: "It leaves this Summary when you save.", action: "Remove", onConfirm: () => {
125
+ onChange(tiles.filter((_, j) => j !== i));
126
+ setCurrent(null);
127
+ }, children: _jsx(IconButton, { icon: Trash2Icon, label: "Remove card", variant: "destructive" }) })] }), _jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-1.5", children: [_jsx(Label, { htmlFor: "card-label", children: "Label" }), _jsx(Input, { id: "card-label", value: own.label, maxLength: 40, onChange: (e) => set({ label: e.target.value }) })] }), _jsxs("div", { className: "grid grid-cols-[repeat(2,minmax(0,1fr))] gap-1.5", children: [_jsx(AggPick, { value: own.agg, label: "Roll-up", onChange: (agg) => set({ agg }) }), _jsx(FieldPick, { value: own.field, fields: isCountAgg(own.agg) ? pickable : nums, domain: domain, label: "Field", onChange: (field) => set({ field }) }), _jsx(Input, { "aria-label": "Words after the number", className: "col-span-2", placeholder: "Words after the number (places)", value: own.suffix ?? '', maxLength: 30, onChange: (e) => set({ suffix: e.target.value }) })] }), _jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-1.5", children: [_jsx(Label, { children: "Only rows where" }), _jsx(CardFilter, { rows: rows, fields: fields, tokens: own.filter, onChange: (filter) => set({ filter }) }, own.key)] }), _jsxs(Label, { className: "flex items-center gap-2 font-normal", children: [_jsx(Switch, { checked: own.sign, onCheckedChange: (sign) => set({ sign }) }), "Green or red by sign"] }), _jsx("span", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "Secondary lines" }), own.lines.map((l, j) => (_jsx(LineRow
128
+ // biome-ignore lint/suspicious/noArrayIndexKey: lines have no key of their own
129
+ , { line: l, nums: nums, fields: pickable, domain: domain, onChange: (next) => set({ lines: own.lines.map((x, k) => (k === j ? next : x)) }), onRemove: () => set({ lines: own.lines.filter((_, k) => k !== j) }) }, j))), _jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", disabled: own.lines.length >= MAX_TILE_LINES, onClick: () => set({
130
+ lines: [
131
+ ...own.lines,
132
+ {
133
+ kind: 'pct',
134
+ field: (nums.find((f) => f.amount) ?? nums[0])?.key ?? '',
135
+ agg: 'sum',
136
+ },
137
+ ],
138
+ }), children: [_jsx(PlusIcon, {}), " Add line"] }), _jsx("div", { className: "rounded-lg bg-muted/50 p-2", children: _jsx(OwnTile, { domain: domain, tile: own, rows: rows, fields: fields }) }), problems[i] && (_jsx("p", { role: "alert", className: "text-xs text-destructive", children: problems[i] }))] }))] }));
139
+ }
@@ -1,7 +1,35 @@
1
- import { type CardStyle, type MeterKind, type ViewAgg, type ViewTile } from '@columenlabs/lumen-ui-types/views';
1
+ import { type CardStyle, type MeterKind, type ViewAgg, type ViewFormat, type ViewOwnTile, type ViewTile } from '@columenlabs/lumen-ui-types/views';
2
2
  import { type ReactNode } from 'react';
3
3
  import { type FilterField } from './lumen-filter.js';
4
- import { type ViewDomain, type ViewField } from './view-model.js';
4
+ import { type ViewCellValue, type ViewDomain, type ViewField } from './view-model.js';
5
+ /** One figure of a card: a value in its field's format, or plain text. */
6
+ export type Figure = {
7
+ kind: 'value';
8
+ value: ViewCellValue;
9
+ format: ViewFormat;
10
+ amount: boolean;
11
+ suffix?: string;
12
+ } | {
13
+ kind: 'text';
14
+ text: string;
15
+ };
16
+ /** A card's main number and lines over `rows` (the Summary's rows); null when its field is gone. */
17
+ export declare function figuresOf<T>(domain: ViewDomain<T>, t: ViewOwnTile, rows: T[], fields: FilterField<T>[]): {
18
+ main: Figure & {
19
+ kind: 'value';
20
+ };
21
+ lines: Figure[];
22
+ } | null;
23
+ export declare function FigureText({ f }: {
24
+ f: Figure;
25
+ }): import("react").JSX.Element;
26
+ /** A card of the view's own as the page shows it. */
27
+ export declare function OwnTile<T>({ domain, tile: t, rows, fields, }: {
28
+ domain: ViewDomain<T>;
29
+ tile: ViewOwnTile;
30
+ rows: T[];
31
+ fields: FilterField<T>[];
32
+ }): import("react").JSX.Element | null;
5
33
  /**
6
34
  * A LumenCard's number and lines: its frame shows the label, so the body is
7
35
  * the main number and its lines; a domain's own card keeps the app's drawing without its frame.
@@ -36,6 +64,7 @@ export declare function SummaryTiles<T>({ domain, tiles, rows, fields, tile, loa
36
64
  /** While the view's rows load, each card holds its place. */
37
65
  loading?: boolean;
38
66
  }): import("react").JSX.Element | null;
67
+ export declare const isNumber: <T>(f: ViewField<T>) => boolean;
39
68
  /**
40
69
  * The roll-up a card over `f` starts with, so any field makes a sensible number: text counts its
41
70
  * different values, a percent or weight takes its highest (shares never add up), the rest sum.
@@ -44,19 +73,3 @@ export declare function SummaryTiles<T>({ domain, tiles, rows, fields, tile, loa
44
73
  export declare function cardAgg<T>(f: ViewField<T> | undefined, agg?: ViewAgg): ViewAgg;
45
74
  /** What is wrong with each of the cards: a key, a label or a field; null where nothing. */
46
75
  export declare function summaryCardIssues(tiles: ViewTile[], fields: string[], domainTiles: string[]): (string | null)[];
47
- /**
48
- * The Summary's cards in Edit widget: a grid to pick and drag, the domain's own cards to add or
49
- * remove, and the picked card of the view's own: its main number (label, roll-up, field, rows,
50
- * colour by sign) and its secondary lines.
51
- */
52
- export declare function SummaryCardsEditor<T>({ domain, tiles, onChange, rows, fields, problems, one, }: {
53
- domain: ViewDomain<T>;
54
- tiles: ViewTile[];
55
- onChange: (tiles: ViewTile[]) => void;
56
- /** One card only (LumenCard): a built-in replaces it and there is no Add card. */
57
- one?: boolean;
58
- /** The Summary's rows, for the previews. */
59
- rows: T[];
60
- fields: FilterField<T>[];
61
- problems: (string | null)[];
62
- }): import("react").JSX.Element;
@@ -1,38 +1,17 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { MAX_TILE_LINES, isCountAgg, isOwnTile, meterTone, viewAggs, } from '@columenlabs/lumen-ui-types/views';
3
- // The Summary widget's cards:
4
- // each card is one main number and any secondary lines. A domain's own cards (Value, Day P&L) are
5
- // drawn by the app; a card of the view's own rolls one field or formula column up over the rows
6
- // the Summary shows, narrowed by its own filter, and its lines are other roll-ups, a percent of a
7
- // roll-up over all the Summary's rows, or plain text. `SummaryTiles` draws them; `SummaryCardsEditor`
8
- // is the cards part of the Summary's Edit widget. Everything is worked out here from the rows the
9
- // view already loaded.
10
- import { DndContext, KeyboardSensor, PointerSensor, TouchSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core';
11
- import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable, } from '@dnd-kit/sortable';
12
- import { CSS } from '@dnd-kit/utilities';
13
- import { PlusIcon, Trash2Icon, XIcon } from 'lucide-react';
14
- import { useEffect, useMemo, useState } from 'react';
15
- import { LumenConfirm } from './confirm.js';
16
- import { IconButton } from './icon-button.js';
2
+ import { isCountAgg, isOwnTile, meterTone, } from '@columenlabs/lumen-ui-types/views';
3
+ import { useMemo } from 'react';
17
4
  import { KIT } from './kit-config.js';
18
5
  import { lazyPart } from './lazy-part.js';
19
6
  import { cn } from './lib/utils.js';
20
- import { LumenTokenizedFilter, filterByTokens, useLumenFilter, } from './lumen-filter.js';
21
- import { LumenSelector } from './lumen-selector.js';
7
+ import { filterByTokens } from './lumen-filter.js';
22
8
  import { ViewValueCell } from './lumen-view.js';
23
9
  import { Tile } from './tile.js';
24
- import { Badge } from './ui/badge.js';
25
- import { Button } from './ui/button.js';
26
- import { Input } from './ui/input.js';
27
- import { Label } from './ui/label.js';
28
10
  import { Progress } from './ui/progress.js';
29
11
  import { Skeleton } from './ui/skeleton.js';
30
- import { Switch } from './ui/switch.js';
31
- import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
32
- import { AGG_LABELS, MAX_TILES, fieldOptions } from './view-formulas.js';
33
- import { computedOf, rollUp, takesFormulas, } from './view-model.js';
12
+ import { computedOf, rollUp, } from './view-model.js';
34
13
  /** A card's main number and lines over `rows` (the Summary's rows); null when its field is gone. */
35
- function figuresOf(domain, t, rows, fields) {
14
+ export function figuresOf(domain, t, rows, fields) {
36
15
  const computed = computedOf(domain, rows, null);
37
16
  const field = (k) => domain.fields.find((f) => f.key === k);
38
17
  const roll = (f, agg, rs) => rollUp(rs.map((r) => computed.value(f, r)), agg);
@@ -74,13 +53,13 @@ function figuresOf(domain, t, rows, fields) {
74
53
  });
75
54
  return { main, lines };
76
55
  }
77
- function FigureText({ f }) {
56
+ export function FigureText({ f }) {
78
57
  if (f.kind === 'text')
79
58
  return _jsx(_Fragment, { children: f.text });
80
59
  return (_jsxs(_Fragment, { children: [_jsx(ViewValueCell, { value: f.value, format: f.format, amount: f.amount }), f.suffix ? ` ${f.suffix}` : null] }));
81
60
  }
82
61
  /** A card of the view's own as the page shows it. */
83
- function OwnTile({ domain, tile: t, rows, fields, }) {
62
+ export function OwnTile({ domain, tile: t, rows, fields, }) {
84
63
  const figs = useMemo(() => figuresOf(domain, t, rows, fields), [domain, t, rows, fields]);
85
64
  if (!figs)
86
65
  return null;
@@ -135,10 +114,9 @@ export function SummaryTiles({ domain, tiles, rows, fields, tile, loading, }) {
135
114
  // Each tile sits in a `contents` holder, so the odd last one's span goes on what it holds.
136
115
  '[&>*:last-child:nth-child(odd)>*]:col-span-2 md:[&>*:last-child:nth-child(odd)>*]:col-span-1'), children: shown.map((t) => (_jsx("div", { className: "contents", children: isOwnTile(t) ? (_jsx(OwnTile, { domain: domain, tile: t, rows: rows, fields: fields })) : (tile?.(t.key, rows)) }, t.key))) }));
137
116
  }
138
- // ── Edit ───────────────────────────────────────────────────────────────────────────────────────
139
- const dragOptions = { activationConstraint: { distance: 4 } };
117
+ // ── Card rules (the editor lives in summary-cards-editor.tsx) ─────────────────────────────────
140
118
  // A number the page draws its own way (a meter, a unit) still reads its raw value, so it rolls up.
141
- const isNumber = (f) => f.format !== 'text';
119
+ export const isNumber = (f) => f.format !== 'text';
142
120
  /**
143
121
  * The roll-up a card over `f` starts with, so any field makes a sensible number: text counts its
144
122
  * different values, a percent or weight takes its highest (shares never add up), the rest sum.
@@ -169,118 +147,3 @@ export function summaryCardIssues(tiles, fields, domainTiles) {
169
147
  return null;
170
148
  });
171
149
  }
172
- function FieldPick({ value, fields, domain, label, onChange, }) {
173
- return (_jsx(LumenSelector, { label: label, size: "sm", className: "w-full min-w-0", placeholder: "Pick a field", options: fieldOptions(fields, domain), value: value, onChange: onChange }));
174
- }
175
- function AggPick({ value, label, onChange, }) {
176
- return (_jsx(LumenSelector, { label: label, size: "sm", className: "w-full min-w-0", options: viewAggs.map((a) => ({ value: a, label: AGG_LABELS[a] })), value: value, onChange: onChange }));
177
- }
178
- /** A card in the editor's grid: its label, its value now and where it comes from. */
179
- function CardChip({ id, selected, onSelect, label, value, how, problem, domId, }) {
180
- const s = useSortable({ id });
181
- return (_jsxs(Button, { ref: s.setNodeRef, id: domId, variant: "outline", style: { transform: CSS.Transform.toString(s.transform), transition: s.transition }, className: cn('h-auto min-h-16 min-w-0 flex-col items-start justify-start gap-0.5 rounded-lg px-2.5 py-2 text-left font-normal whitespace-normal', selected && 'border-primary ring-3 ring-primary/15', problem && 'border-destructive', s.isDragging && 'z-20 shadow-md'), ...s.attributes, ...s.listeners, "aria-pressed": selected, onClick: onSelect, children: [_jsx("span", { className: "w-full truncate text-xs text-muted-foreground", children: label }), _jsx("span", { className: "num w-full truncate text-sm font-semibold", children: value }), _jsx("span", { className: "w-full truncate text-[0.7rem] text-muted-foreground", children: how })] }));
182
- }
183
- /** A card's own rows filter, kept in step with the card. */
184
- function CardFilter({ rows, fields, tokens, onChange, }) {
185
- const filter = useLumenFilter(rows, fields, { initial: tokens });
186
- const next = JSON.stringify(filter.tokens);
187
- // biome-ignore lint/correctness/useExhaustiveDependencies: reports the box's tokens when they change
188
- useEffect(() => {
189
- if (next !== JSON.stringify(tokens))
190
- onChange(filter.tokens);
191
- }, [next]);
192
- return (_jsx(LumenTokenizedFilter, { filter: filter, label: "Card filter", placeholder: "All the Summary's rows" }));
193
- }
194
- const LINE_KINDS = [
195
- { value: 'value', label: 'Number' },
196
- { value: 'pct', label: '% of' },
197
- { value: 'text', label: 'Text' },
198
- ];
199
- function LineRow({ line, nums, fields, domain, onChange, onRemove, }) {
200
- const first = nums[0]?.key ?? '';
201
- const as = (kind) => kind === 'text'
202
- ? { kind, text: 'Note' }
203
- : kind === 'pct'
204
- ? { kind, field: line.kind !== 'text' ? line.field : first, agg: 'sum' }
205
- : {
206
- kind,
207
- field: line.kind !== 'text' ? line.field : first,
208
- agg: 'sum',
209
- filter: [],
210
- suffix: '',
211
- };
212
- return (_jsxs("div", { className: "flex flex-col gap-1.5 rounded-md border p-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(ToggleGroup, { type: "single", size: "sm", variant: "outline", spacing: 0, value: line.kind, onValueChange: (v) => v && onChange(as(v)), "aria-label": "Line kind", children: LINE_KINDS.map((k) => (_jsx(ToggleGroupItem, { value: k.value, className: "px-2 text-xs", children: k.label }, k.value))) }), _jsx(IconButton, { icon: XIcon, label: "Remove line", className: "ml-auto", onClick: onRemove })] }), line.kind === 'text' ? (_jsx(Input, { "aria-label": "Line text", value: line.text, maxLength: 60, onChange: (e) => onChange({ ...line, text: e.target.value }) })) : (_jsxs("div", { className: "grid grid-cols-[repeat(2,minmax(0,1fr))] gap-1.5", children: [_jsx(AggPick, { value: line.agg, label: "Line roll-up", onChange: (agg) => onChange({ ...line, agg }) }), _jsx(FieldPick, { value: line.field, fields: isCountAgg(line.agg) ? fields : nums, domain: domain, label: "Line field", onChange: (field) => onChange({ ...line, field }) }), line.kind === 'value' && (_jsx(Input, { "aria-label": "Words after the number", className: "col-span-2", placeholder: "Words after the number (contracts)", value: line.suffix, maxLength: 30, onChange: (e) => onChange({ ...line, suffix: e.target.value }) }))] }))] }));
213
- }
214
- /**
215
- * The Summary's cards in Edit widget: a grid to pick and drag, the domain's own cards to add or
216
- * remove, and the picked card of the view's own: its main number (label, roll-up, field, rows,
217
- * colour by sign) and its secondary lines.
218
- */
219
- export function SummaryCardsEditor({ domain, tiles, onChange, rows, fields, problems, one = false, }) {
220
- const sensors = useSensors(useSensor(PointerSensor, dragOptions), useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
221
- const builtIns = domain.tiles ?? [];
222
- const [current, setCurrent] = useState(tiles.find(isOwnTile)?.key ?? tiles[0]?.key ?? null);
223
- const nums = domain.fields.filter(isNumber);
224
- const pickable = domain.fields;
225
- const fieldOf = (k) => domain.fields.find((f) => f.key === k);
226
- const i = tiles.findIndex((t) => t.key === current);
227
- const picked = tiles[i];
228
- const own = picked && isOwnTile(picked) ? picked : undefined;
229
- const set = (p) => onChange(tiles.map((t, j) => (j === i && isOwnTile(t) ? { ...t, ...p } : t)));
230
- const add = () => {
231
- let n = tiles.length + 1;
232
- while (tiles.some((t) => t.key === `tile_${n}`) || builtIns.some((w) => w.key === `tile_${n}`))
233
- n++;
234
- const key = `tile_${n}`;
235
- const field = (nums.find((f) => f.amount) ?? nums[0])?.key ?? '';
236
- onChange([
237
- ...tiles,
238
- { key, label: `Card ${n}`, field, agg: 'sum', filter: [], sign: false, lines: [] },
239
- ]);
240
- setCurrent(key);
241
- };
242
- const ownCount = tiles.filter(isOwnTile).length;
243
- const off = builtIns.filter((w) => !tiles.some((t) => t.key === w.key));
244
- const preview = (t) => {
245
- if (!isOwnTile(t))
246
- return { value: 'Built in', how: 'Built in' };
247
- const figs = figuresOf(domain, t, rows, fields);
248
- return {
249
- value: figs ? _jsx(FigureText, { f: figs.main }) : '—',
250
- how: `${AGG_LABELS[t.agg]} of ${fieldOf(t.field)?.label ?? t.field}`,
251
- };
252
- };
253
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: (e) => {
254
- const keys = tiles.map((t) => t.key);
255
- const from = keys.indexOf(String(e.active.id));
256
- const to = e.over ? keys.indexOf(String(e.over.id)) : -1;
257
- if (from >= 0 && to >= 0 && from !== to)
258
- onChange(arrayMove(tiles, from, to));
259
- }, children: _jsx(SortableContext, { items: tiles.map((t) => t.key), strategy: rectSortingStrategy, children: _jsxs("div", { className: "grid grid-cols-3 gap-1.5", "data-testid": "summary-cards", children: [tiles.map((t, j) => {
260
- const p = preview(t);
261
- const label = isOwnTile(t)
262
- ? t.label
263
- : (builtIns.find((w) => w.key === t.key)?.label ?? t.key);
264
- return (_jsx(CardChip, { id: t.key, selected: current === t.key, onSelect: () => setCurrent(t.key), label: label, value: p.value, how: p.how, problem: !!problems[j], domId: `summary-card-${j}` }, t.key));
265
- }), takesFormulas(domain) && !(one && tiles.length) && (_jsxs(Button, { id: "summary-card-add", variant: "outline", className: "h-auto min-h-16 border-dashed font-normal text-muted-foreground", disabled: ownCount >= MAX_TILES, onClick: add, children: [_jsx(PlusIcon, {}), " Add card"] }))] }) }) }), off.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Built in:" }), off.map((w) => (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
266
- onChange(one ? [{ key: w.key }] : [...tiles, { key: w.key }]);
267
- setCurrent(w.key);
268
- }, children: [_jsx(PlusIcon, {}), " ", w.label] }, w.key)))] })), picked && !own && (_jsxs("div", { className: "flex items-center gap-2 rounded-lg border p-3", children: [_jsx(Badge, { variant: "secondary", className: "font-normal", children: "Built in" }), _jsxs("span", { className: "flex-1 text-sm", children: [builtIns.find((w) => w.key === picked.key)?.label ?? picked.key, " works out its own lines."] }), _jsx(IconButton, { icon: Trash2Icon, label: "Remove card", onClick: () => {
269
- onChange(tiles.filter((_, j) => j !== i));
270
- setCurrent(null);
271
- } })] })), own && (_jsxs("div", { className: "flex min-w-0 flex-col gap-3 rounded-lg border p-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex-1 text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "Main number" }), _jsx(LumenConfirm, { title: `Remove card ${own.label}?`, description: "It leaves this Summary when you save.", action: "Remove", onConfirm: () => {
272
- onChange(tiles.filter((_, j) => j !== i));
273
- setCurrent(null);
274
- }, children: _jsx(IconButton, { icon: Trash2Icon, label: "Remove card", variant: "destructive" }) })] }), _jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-1.5", children: [_jsx(Label, { htmlFor: "card-label", children: "Label" }), _jsx(Input, { id: "card-label", value: own.label, maxLength: 40, onChange: (e) => set({ label: e.target.value }) })] }), _jsxs("div", { className: "grid grid-cols-[repeat(2,minmax(0,1fr))] gap-1.5", children: [_jsx(AggPick, { value: own.agg, label: "Roll-up", onChange: (agg) => set({ agg }) }), _jsx(FieldPick, { value: own.field, fields: isCountAgg(own.agg) ? pickable : nums, domain: domain, label: "Field", onChange: (field) => set({ field }) }), _jsx(Input, { "aria-label": "Words after the number", className: "col-span-2", placeholder: "Words after the number (places)", value: own.suffix ?? '', maxLength: 30, onChange: (e) => set({ suffix: e.target.value }) })] }), _jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-1.5", children: [_jsx(Label, { children: "Only rows where" }), _jsx(CardFilter, { rows: rows, fields: fields, tokens: own.filter, onChange: (filter) => set({ filter }) }, own.key)] }), _jsxs(Label, { className: "flex items-center gap-2 font-normal", children: [_jsx(Switch, { checked: own.sign, onCheckedChange: (sign) => set({ sign }) }), "Green or red by sign"] }), _jsx("span", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "Secondary lines" }), own.lines.map((l, j) => (_jsx(LineRow
275
- // biome-ignore lint/suspicious/noArrayIndexKey: lines have no key of their own
276
- , { line: l, nums: nums, fields: pickable, domain: domain, onChange: (next) => set({ lines: own.lines.map((x, k) => (k === j ? next : x)) }), onRemove: () => set({ lines: own.lines.filter((_, k) => k !== j) }) }, j))), _jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", disabled: own.lines.length >= MAX_TILE_LINES, onClick: () => set({
277
- lines: [
278
- ...own.lines,
279
- {
280
- kind: 'pct',
281
- field: (nums.find((f) => f.amount) ?? nums[0])?.key ?? '',
282
- agg: 'sum',
283
- },
284
- ],
285
- }), children: [_jsx(PlusIcon, {}), " Add line"] }), _jsx("div", { className: "rounded-lg bg-muted/50 p-2", children: _jsx(OwnTile, { domain: domain, tile: own, rows: rows, fields: fields }) }), problems[i] && (_jsx("p", { role: "alert", className: "text-xs text-destructive", children: problems[i] }))] }))] }));
286
- }
@@ -10,7 +10,7 @@ import { CONNECTED_COUNT, VIEW_CARD_KIND_NAMES, WIDGET_STYLES, cardsOf, connecte
10
10
  import { DndContext, KeyboardSensor, PointerSensor, TouchSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core';
11
11
  import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
12
12
  import { CSS } from '@dnd-kit/utilities';
13
- import { ArrowDownIcon, ArrowLeftIcon, ArrowUpIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, Columns3Icon, CopyIcon, DatabaseIcon, EllipsisIcon, FilterIcon, GripVerticalIcon, LayoutDashboardIcon, Link2Icon, MoveHorizontalIcon, PencilIcon, PlusIcon, ReplaceIcon, RotateCcwIcon, SearchIcon, ShapesIcon, ShrinkIcon, SigmaIcon, SlidersHorizontalIcon, SmileIcon, SparklesIcon, StarIcon, TableIcon, ToggleRightIcon, Trash2Icon, Unlink2Icon, UnlinkIcon, XIcon, } from 'lucide-react';
13
+ import { ArrowDownIcon, ArrowUpIcon, CheckIcon, ChevronRightIcon, Columns3Icon, CopyIcon, DatabaseIcon, EllipsisIcon, FilterIcon, GripVerticalIcon, LayoutDashboardIcon, Link2Icon, MoveHorizontalIcon, PencilIcon, PlusIcon, ReplaceIcon, SearchIcon, ShapesIcon, ShrinkIcon, SigmaIcon, SlidersHorizontalIcon, SparklesIcon, StarIcon, ToggleRightIcon, Trash2Icon, Unlink2Icon, UnlinkIcon, XIcon, } from 'lucide-react';
14
14
  import { useEffect, useMemo, useRef, useState } from 'react';
15
15
  import { AddFormulaDialog } from './add-column.js';
16
16
  import { useAssistant, useAssistantName } from './assistant-context.js';
@@ -35,18 +35,15 @@ import { Button } from './ui/button.js';
35
35
  import { Checkbox } from './ui/checkbox.js';
36
36
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible.js';
37
37
  import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from './ui/dialog.js';
38
- import { Input } from './ui/input.js';
39
38
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './ui/input-group.js';
40
39
  import { Label } from './ui/label.js';
41
- import { Popover, PopoverContent, PopoverTrigger } from './ui/popover.js';
42
- import { Separator } from './ui/separator.js';
43
40
  import { Switch } from './ui/switch.js';
44
41
  import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs.js';
45
42
  import { Textarea } from './ui/textarea.js';
46
43
  import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
47
44
  import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
48
45
  import { AGG_LABELS, FORMAT_LABELS, FormatChip, FormulaForm, MAX_FORMULAS, fieldOptions, formatOptions, formulaIssueTarget, formulaIssues, formulaOptions, } from './view-formulas.js';
49
- import { VIEW_ICONS, shapeIcon, viewIcon } from './view-icons.js';
46
+ import { shapeIcon } from './view-icons.js';
50
47
  import { FORMULAS_GROUP, canFilter, chipsOf, formulaBlanks, formulasOf, groupOf, hiddenOf, subgroupLabel, subgroupOf, subgroupsOf, summaryCash, takesFormulas, tilesOf, totalAggOf, withViewFields, } from './view-model.js';
51
48
  import { WidgetControlsEditor } from './widget-controls.js';
52
49
  import { PAGE_SOURCE, useWidgetDataOptions } from './widget-data.js';
@@ -30,6 +30,7 @@ export const FORMAT_LABELS = {
30
30
  price_signed: 'Currency ±',
31
31
  weight: 'Percent',
32
32
  percent: 'Percent ±',
33
+ bytes: 'Size',
33
34
  text: 'Text',
34
35
  };
35
36
  /**
@@ -47,6 +48,7 @@ const FORMAT_GLYPHS = {
47
48
  price_signed: '±$',
48
49
  weight: '%',
49
50
  percent: '±%',
51
+ bytes: 'B',
50
52
  text: 'Aa',
51
53
  };
52
54
  /** Text fields whose icon is a calendar are dates (no date format exists; the icon says it). */
@@ -74,6 +76,7 @@ const FORMAT_EXAMPLES = {
74
76
  price_signed: '+$1,234.56',
75
77
  weight: '12.3%',
76
78
  percent: '+12.3%',
79
+ bytes: '812 KB · 13.1 GB',
77
80
  text: 'Abc',
78
81
  };
79
82
  /** The number formats as picker options, each with how it reads. */
@@ -353,7 +353,7 @@ export declare const formulasOf: (c: CustomViewConfig) => {
353
353
  key: string;
354
354
  label: string;
355
355
  expr: string;
356
- format: "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "text" | "weight";
356
+ format: "bytes" | "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "text" | "weight";
357
357
  agg: "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum" | null;
358
358
  }[];
359
359
  /** Fields listed under Columns but not shown, for their Filter and Group ticks; views saved before it have none. */
@@ -1,6 +1,8 @@
1
1
  import { type ViewWidget } from '@columenlabs/lumen-ui-types/views';
2
2
  import { type ReactNode } from 'react';
3
+ import { dateFields } from './date-fields.js';
3
4
  import type { FilterField } from './lumen-filter.js';
5
+ export { dateFields };
4
6
  import type { ViewDomain, ViewField } from './view-model.js';
5
7
  /** A widget the editor draws its own parts for: anything but a table of the rows. */
6
8
  export type WidgetDraft = Exclude<ViewWidget, {
@@ -14,8 +16,6 @@ export declare function EditorRow({ label, children }: {
14
16
  }): import("react").JSX.Element;
15
17
  export declare const isTextField: <T>(f: ViewField<T>) => boolean;
16
18
  export declare const isNumberField: <T>(f: ViewField<T>) => boolean;
17
- /** A trend's date candidates: text fields, the date-like ones first. */
18
- export declare const dateFields: <T>(domain: ViewDomain<T>) => ViewField<T>[];
19
19
  /**
20
20
  * One place a widget takes fields: what it takes (`accepts`, `why` when a field does not fit),
21
21
  * how many (`max`), the keys in it now and the widget with other keys. A table's one slot is its
@@ -95,7 +95,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
95
95
  key: string;
96
96
  placement: "column" | "inline";
97
97
  }[];
98
- formats?: Record<string, "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
98
+ formats?: Record<string, "bytes" | "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
99
99
  aggs?: Record<string, "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum"> | undefined;
100
100
  shows?: Record<string, string> | undefined;
101
101
  group: {
@@ -9,13 +9,16 @@ import { OUTLOOK_INDEXES, marketWindows, } from '@columenlabs/lumen-ui-types/mar
9
9
  import { ALLOCATION_MAX, isOwnTile, isRowCard, isTableCard, trendBuckets, widgetStyleId, } from '@columenlabs/lumen-ui-types/views';
10
10
  import { ArrowDownIcon, ArrowUpIcon } from 'lucide-react';
11
11
  import { useState } from 'react';
12
+ import { dateFields } from './date-fields.js';
12
13
  import { LumenSelector } from './lumen-selector.js';
13
- import { SummaryCardsEditor, cardAgg, summaryCardIssues } from './summary-cards.js';
14
+ import { cardAgg, summaryCardIssues } from './summary-cards.js';
15
+ import { SummaryCardsEditor } from './summary-cards-editor.js';
14
16
  import { Input } from './ui/input.js';
15
17
  import { Label } from './ui/label.js';
16
18
  import { Switch } from './ui/switch.js';
17
19
  import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
18
20
  import { MAX_TILES, fieldOptions } from './view-formulas.js';
21
+ export { dateFields };
19
22
  /** A label beside its control, the same in every section of Edit widget. */
20
23
  export function EditorRow({ label, children }) {
21
24
  return (_jsxs("div", { className: "grid grid-cols-[5rem_minmax(0,1fr)] items-center gap-2", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: label }), _jsx("div", { className: "min-w-0", children: children })] }));
@@ -23,16 +26,6 @@ export function EditorRow({ label, children }) {
23
26
  export const isTextField = (f) => f.format === 'text' && !f.render;
24
27
  // A number the page draws its own way (a meter, a unit) still reads its raw value, so it rolls up.
25
28
  export const isNumberField = (f) => f.format !== 'text';
26
- const DATE_LIKE = /(^|_)(date|day|at|time|month|week|year)($|_)/i;
27
- /** A trend's date candidates: text fields, the date-like ones first. */
28
- export const dateFields = (domain) => {
29
- // A rendered text field still reads its raw value (a report date drawn as Tue, Oct 20).
30
- const texts = domain.fields.filter((f) => f.format === 'text');
31
- return [
32
- ...texts.filter((f) => DATE_LIKE.test(f.key)),
33
- ...texts.filter((f) => !DATE_LIKE.test(f.key)),
34
- ];
35
- };
36
29
  /** The most columns a Top N shows. */
37
30
  const TOP_N_COLUMNS = 8;
38
31
  /** How many values a trend draws, a spark or tracker one. */
@@ -3,7 +3,7 @@
3
3
  * field. Shared by the widget frame and its chart bodies, so a lazily loaded body never pulls the
4
4
  * frame back in.
5
5
  */
6
- import { isDollarFormat } from '@columenlabs/lumen-ui-types/views';
6
+ import { formatBytes, isDollarFormat } from '@columenlabs/lumen-ui-types/views';
7
7
  import { maskAmount } from './shell.js';
8
8
  import { label } from './view-model.js';
9
9
  export const MINUS = '−';
@@ -19,6 +19,8 @@ export function short(v, format, hidden) {
19
19
  const abs = Math.abs(v);
20
20
  if (format === 'percent' || format === 'weight')
21
21
  return `${sign}${abs.toFixed(1)}%`;
22
+ if (format === 'bytes')
23
+ return `${sign}${formatBytes(abs)}`;
22
24
  if (isDollarFormat(format))
23
25
  return `${sign}$${COMPACT.format(abs)}`;
24
26
  return `${sign}${COMPACT.format(abs)}`;
@@ -31,6 +33,8 @@ export function exact(v, format, hidden) {
31
33
  const abs = Math.abs(v);
32
34
  if (format === 'percent' || format === 'weight')
33
35
  return `${sign}${abs.toFixed(2)}%`;
36
+ if (format === 'bytes')
37
+ return `${sign}${formatBytes(abs)}`;
34
38
  const n = abs.toLocaleString('en-US', {
35
39
  maximumFractionDigits: format === 'rounded' || format === 'price_rounded' ? 0 : 2,
36
40
  useGrouping: format !== 'integer',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.101",
3
+ "version": "0.1.103",
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.101",
52
+ "@columenlabs/lumen-ui-types": "0.1.103",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",