@columenlabs/lumen-ui 0.1.102 → 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
+ };
@@ -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);
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { bytesParts, 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 { bytesParts, controlOn, isCountAgg, isDollarFormat, viewPageSizes, viewP
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
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';
@@ -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" }));
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';
@@ -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
@@ -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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.102",
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.102",
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",