@columenlabs/lumen-ui 0.1.1 → 0.1.3

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.
Files changed (70) hide show
  1. package/README.md +1 -1
  2. package/dist/assistant-panel.js +1 -1
  3. package/dist/coverage-calendar-map.js +1 -1
  4. package/dist/coverage-cells.d.ts +2 -2
  5. package/dist/coverage-map.js +4 -10
  6. package/dist/details-table.js +1 -5
  7. package/dist/form-issues.js +1 -1
  8. package/dist/frame-script.js +1 -1
  9. package/dist/growth-chart.d.ts +9 -9
  10. package/dist/joins.d.ts +4 -3
  11. package/dist/joins.js +1 -1
  12. package/dist/lumen-code.js +1 -5
  13. package/dist/lumen-drawer.js +1 -3
  14. package/dist/lumen-filter.js +5 -13
  15. package/dist/lumen-grid.js +2 -4
  16. package/dist/lumen-layout.js +7 -4
  17. package/dist/lumen-links.d.ts +1 -1
  18. package/dist/lumen-selector.js +1 -1
  19. package/dist/lumen-source.js +2 -4
  20. package/dist/lumen-tab-drawer.js +1 -1
  21. package/dist/lumen-toast.d.ts +4 -4
  22. package/dist/lumen-view-card.js +1 -1
  23. package/dist/lumen-view.js +2 -4
  24. package/dist/lumen-widget.d.ts +277 -171
  25. package/dist/lumen-widget.js +31 -20
  26. package/dist/market/indicator-picker.js +1 -1
  27. package/dist/market/live.d.ts +1 -1
  28. package/dist/market/lumen-chart.js +1 -1
  29. package/dist/market/price-chart.d.ts +10 -10
  30. package/dist/nav.js +1 -1
  31. package/dist/notifications.js +1 -3
  32. package/dist/overview.js +1 -1
  33. package/dist/page.js +1 -1
  34. package/dist/pager.js +1 -1
  35. package/dist/prefs.d.ts +25 -25
  36. package/dist/schedules.js +1 -1
  37. package/dist/settings.js +1 -3
  38. package/dist/shell.js +1 -1
  39. package/dist/sourced-card.d.ts +10 -0
  40. package/dist/sourced-card.js +69 -0
  41. package/dist/summary-cards.js +2 -6
  42. package/dist/symbol-sheet.js +1 -1
  43. package/dist/tasks.d.ts +1 -1
  44. package/dist/tasks.js +1 -1
  45. package/dist/tile.js +2 -4
  46. package/dist/ui/badge.d.ts +1 -1
  47. package/dist/ui/button.d.ts +2 -2
  48. package/dist/ui/chart.d.ts +2 -2
  49. package/dist/ui/chart.js +2 -5
  50. package/dist/ui/field.d.ts +1 -1
  51. package/dist/ui/input-group.d.ts +2 -2
  52. package/dist/ui/item.d.ts +3 -3
  53. package/dist/ui/sidebar.d.ts +1 -1
  54. package/dist/ui/sidebar.js +1 -1
  55. package/dist/ui/toggle.d.ts +1 -1
  56. package/dist/view-edit-cell.js +1 -4
  57. package/dist/view-editor.d.ts +1 -1
  58. package/dist/view-editor.js +188 -47
  59. package/dist/view-formulas.d.ts +1 -1
  60. package/dist/view-model.d.ts +15 -15
  61. package/dist/whats-new.js +1 -1
  62. package/dist/widget-data.d.ts +4 -0
  63. package/dist/widget-data.js +32 -0
  64. package/dist/widget-settings.d.ts +219 -139
  65. package/dist/widget-settings.js +4 -4
  66. package/dist/widget-thumbs.d.ts +1 -1
  67. package/dist/widget-thumbs.js +1 -1
  68. package/dist/workspace-keys.d.ts +1 -1
  69. package/dist/workspaces.js +1 -1
  70. package/package.json +5 -5
package/dist/ui/item.d.ts CHANGED
@@ -4,14 +4,14 @@ import { Separator } from './separator.js';
4
4
  declare function ItemGroup({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
5
5
  declare function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>): React.JSX.Element;
6
6
  declare const itemVariants: (props?: ({
7
- variant?: "default" | "outline" | "muted" | null | undefined;
8
- size?: "default" | "xs" | "sm" | null | undefined;
7
+ variant?: "default" | "muted" | "outline" | null | undefined;
8
+ size?: "default" | "sm" | "xs" | null | undefined;
9
9
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
10
10
  declare function Item({ className, variant, size, asChild, ...props }: React.ComponentProps<'div'> & VariantProps<typeof itemVariants> & {
11
11
  asChild?: boolean;
12
12
  }): React.JSX.Element;
13
13
  declare const itemMediaVariants: (props?: ({
14
- variant?: "image" | "default" | "icon" | null | undefined;
14
+ variant?: "default" | "icon" | "image" | null | undefined;
15
15
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
16
16
  declare function ItemMedia({ className, variant, ...props }: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>): React.JSX.Element;
17
17
  declare function ItemContent({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
@@ -44,7 +44,7 @@ declare function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>
44
44
  declare function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>): React.JSX.Element;
45
45
  declare const sidebarMenuButtonVariants: (props?: ({
46
46
  variant?: "default" | "outline" | null | undefined;
47
- size?: "default" | "sm" | "lg" | null | undefined;
47
+ size?: "default" | "lg" | "sm" | null | undefined;
48
48
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
49
49
  declare function SidebarMenuButton({ asChild, isActive, variant, size, tooltip, className, ...props }: React.ComponentProps<'button'> & {
50
50
  asChild?: boolean;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from '../lib/utils.js';
3
3
  import { cva } from 'class-variance-authority';
4
4
  import { Slot } from 'radix-ui';
@@ -3,7 +3,7 @@ import { Toggle as TogglePrimitive } from 'radix-ui';
3
3
  import type * as React from 'react';
4
4
  declare const toggleVariants: (props?: ({
5
5
  variant?: "default" | "outline" | null | undefined;
6
- size?: "default" | "sm" | "lg" | null | undefined;
6
+ size?: "default" | "lg" | "sm" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  declare function Toggle({ className, variant, size, ...props }: React.ComponentProps<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>): React.JSX.Element;
9
9
  export { Toggle, toggleVariants };
@@ -49,10 +49,7 @@ export function EditCell({ label, value, options, unused = [], chip, note, color
49
49
  ]
50
50
  : []),
51
51
  ];
52
- return (
53
- // Clicks and keys stay off the row, which opens its details (the list is a portal, but React
54
- // events still bubble through here).
55
- _jsxs("span", { className: edited ? 'inline-flex items-center gap-1' : 'contents', onClick: stop, onKeyDown: stop, children: [_jsx(LumenSelector, { trigger: value === null ? 'icon' : chip ? 'chip' : 'field', icon: value === null ? PlusIcon : undefined, size: "sm", label: value === null ? `Set ${label}` : `${label}: ${value}`, className: value === null
52
+ return (_jsxs("span", { className: edited ? 'inline-flex items-center gap-1' : 'contents', onClick: stop, onKeyDown: stop, children: [_jsx(LumenSelector, { trigger: value === null ? 'icon' : chip ? 'chip' : 'field', icon: value === null ? PlusIcon : undefined, size: "sm", label: value === null ? `Set ${label}` : `${label}: ${value}`, className: value === null
56
53
  ? '-ml-1 size-6 text-muted-foreground'
57
54
  : chip
58
55
  ? 'h-6 border-0 bg-secondary px-2 text-xs font-normal text-secondary-foreground dark:bg-secondary'
@@ -49,4 +49,4 @@ export interface ViewEditDialogProps<T> {
49
49
  * only Fields and Settings; a type tried and left comes back as it was. The view's own table stays
50
50
  * a table.
51
51
  */
52
- export declare function ViewEditDialog<T>({ open, onOpenChange, domain: joined, config, rows, current, onSave, card, onSaveCard, fields: filterFields, viewTiles, read, viewId, editor, onDuplicate, onDelete, }: ViewEditDialogProps<T>): import("react").JSX.Element;
52
+ export declare function ViewEditDialog<T>({ open, onOpenChange, domain: pageDomain, config, rows: pageRows, current, onSave, card, onSaveCard, fields: pageFields, viewTiles, read, viewId, editor, onDuplicate, onDelete, }: ViewEditDialogProps<T>): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { CATALOG, FRAME_LINK, catalogKeys, linkColumns, readableDomain, } from '@columenlabs/lumen-ui-types/catalog';
3
- import { CONNECTED_COUNT, VIEW_CARD_KIND_NAMES, WIDGET_STYLES, cardsOf, connectedKey, inferConnection, isOwnTile, isRowCard, isTableCard, mainTableOf, tableConfigOf, tableLayoutOf, viewAggs, viewCardSchema, viewPageSizes, viewPaginationSchema, widgetStyleId, } from '@columenlabs/lumen-ui-types/views';
3
+ import { CONNECTED_COUNT, VIEW_CARD_KIND_NAMES, WIDGET_STYLES, cardsOf, connectedKey, customViewConfigSchema, inferConnection, isOwnTile, isRowCard, isTableCard, mainTableOf, sourceConfigOf, tableConfigOf, tableLayoutOf, viewAggs, viewCardSchema, viewPageSizes, viewPaginationSchema, widgetStyleId, } from '@columenlabs/lumen-ui-types/views';
4
4
  // The Edit view dialog (mock 2b): two panes. Left, a tree of every field the
5
5
  // view's rows reach, grouped by where it comes from (Holding, Symbol by topic, Account, Formulas),
6
6
  // ticked to show. Right, what the view does with them: column order (drag), shape, group, filter,
@@ -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, RotateCcwIcon, SearchIcon, ShapesIcon, ShrinkIcon, SigmaIcon, SlidersHorizontalIcon, SmileIcon, SparklesIcon, ToggleRightIcon, Trash2Icon, Unlink2Icon, XIcon, } from 'lucide-react';
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, 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';
@@ -21,12 +21,13 @@ import { SaveButton } from './form-issues.js';
21
21
  import { useIsMobile } from './hooks/use-mobile.js';
22
22
  import { IconButton } from './icon-button.js';
23
23
  import { IconPicker } from './icon-picker.js';
24
- import { asOf, frameGroupId, useDomainReads, useDraftFrames, useFrameChoices } from './joins.js';
24
+ import { asOf, frameGroupId, rowsDomain, useDomainReads, useDraftFrames, useFrameChoices, } from './joins.js';
25
25
  import { cn } from './lib/utils.js';
26
26
  import { LumenTokenizedFilter, filterByTokens, tokenText, useLumenFilter, } from './lumen-filter.js';
27
27
  import { LinkPicker } from './lumen-links.js';
28
28
  import { LumenSelector } from './lumen-selector.js';
29
- import { CARD_COLUMNS, CARD_TITLE_MAX, cardTitle, draftOf, useOffered } from './lumen-widget.js';
29
+ import { lumenToast } from './lumen-toast.js';
30
+ import { CARD_COLUMNS, CARD_TITLE_MAX, cardFilterFields, cardTitle, draftOf, useOffered, } from './lumen-widget.js';
30
31
  import { useApp } from './shell.js';
31
32
  import { summaryCardIssues } from './summary-cards.js';
32
33
  import { Badge } from './ui/badge.js';
@@ -48,6 +49,7 @@ import { AGG_LABELS, FORMAT_LABELS, FormatChip, FormulaForm, MAX_FORMULAS, field
48
49
  import { VIEW_ICONS, lucideIconNames, shapeIcon, viewIcon } from './view-icons.js';
49
50
  import { FORMULAS_GROUP, canFilter, chipsOf, formulaBlanks, formulasOf, groupOf, hiddenOf, subgroupLabel, subgroupOf, subgroupsOf, summaryCash, takesFormulas, tilesOf, totalAggOf, withViewFields, } from './view-model.js';
50
51
  import { WidgetControlsEditor } from './widget-controls.js';
52
+ import { PAGE_SOURCE, useWidgetDataOptions } from './widget-data.js';
51
53
  import { EditorRow as Row, WidgetFields, WidgetSettings, hasWidgetSettings, readsRows, slotKeys, widgetSlots, } from './widget-settings.js';
52
54
  import { widgetThumb } from './widget-thumbs.js';
53
55
  /** The field's own group and Total rows: a formula over the rolled-up inputs, or a share. */
@@ -60,6 +62,7 @@ const SHAPES = [
60
62
  { value: 'pivot', label: 'Pivot' },
61
63
  ];
62
64
  const NONE = '_none';
65
+ /** The main data picker's value for the page's own rows. */
63
66
  const NO_FRAMES = [];
64
67
  /** A recipe's (a linked the assistant frame's) group id starts with this (`recipe:fr_…`). */
65
68
  const FRAME_PREFIX = frameGroupId('');
@@ -123,10 +126,17 @@ const tableDraftOf = (config, card) => tableConfigOf(config, isTableCard(card)
123
126
  * only Fields and Settings; a type tried and left comes back as it was. The view's own table stays
124
127
  * a table.
125
128
  */
126
- export function ViewEditDialog({ open, onOpenChange, domain: joined, config, rows, current, onSave, card, onSaveCard, fields: filterFields = [], viewTiles, read, viewId, editor, onDuplicate, onDelete, }) {
129
+ export function ViewEditDialog({ open, onOpenChange, domain: pageDomain, config, rows: pageRows, current, onSave, card, onSaveCard, fields: pageFields = [], viewTiles, read, viewId, editor, onDuplicate, onDelete, }) {
127
130
  const isMobile = useIsMobile();
128
- const start = () => (card ? tableDraftOf(config, card) : config);
131
+ // A widget's main data (`source`): null is the page's rows, '' none yet (all cleared).
132
+ const [source, setSource] = useState(card?.source ?? null);
133
+ const start = () => card ? tableDraftOf(card.source ? sourceConfigOf(card, config.name) : config, card) : config;
129
134
  const [draft, setDraft] = useState(start);
135
+ const srcIds = useMemo(() => (open && source ? [source] : NO_FRAMES), [open, source]);
136
+ const srcRead = useDomainReads(srcIds, srcIds);
137
+ const srcNow = source ? srcRead[source] : undefined;
138
+ const joined = (source === null ? pageDomain : (srcNow?.domain ?? rowsDomain(source || 'none', [], [])));
139
+ const rows = (source === null ? pageRows : (srcNow?.rows ?? []));
130
140
  // Recipes: the assistant frames the draft attaches, their columns in the tree
131
141
  // before Save.
132
142
  const frameIds = draft.frames ?? NO_FRAMES;
@@ -156,6 +166,7 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
156
166
  if (open !== wasOpen) {
157
167
  setWasOpen(open);
158
168
  if (open) {
169
+ setSource(card?.source ?? null);
159
170
  setDraft(start());
160
171
  setWidget(card && !isTableCard(card) ? card : null);
161
172
  setFrame(card ?? null);
@@ -273,6 +284,8 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
273
284
  }
274
285
  : undefined;
275
286
  const domain = withViewFields(withLinks, draft);
287
+ // A widget's own filter reads the page's fields, or its own source's.
288
+ const filterFields = source === null ? pageFields : cardFilterFields(domain.fields);
276
289
  const formulas = formulasOf(draft);
277
290
  // Each formula's blank rows on the page's rows now, and why.
278
291
  const formulaKey = JSON.stringify(formulas);
@@ -348,6 +361,97 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
348
361
  description: w.note,
349
362
  thumb: widgetThumb(w.id),
350
363
  }));
364
+ // Main data (a widget only): change it, drop it (the next source moves
365
+ // up and links re-infer against it), make a linked one the main, or clear all. Each has Undo.
366
+ const keysOf = (id) => (id.startsWith(FRAME_LINK) ? [] : catalogKeys(id));
367
+ const labelOf = (id) => id.startsWith(FRAME_LINK)
368
+ ? ((choices.data ?? []).find((f) => `${FRAME_LINK}${f.id}` === id)?.name ?? 'Recipe')
369
+ : (allRead[id]?.label ?? CATALOG[id]?.label ?? id);
370
+ const pageLabel = `${config.name} (this page)`;
371
+ const mainLabel = source === null ? pageLabel : source ? (srcNow?.label ?? labelOf(source)) : '';
372
+ // Everything the widget joins besides its main data: its links, and recipes attached by key.
373
+ const others = () => [
374
+ ...connections.map((c) => c.to),
375
+ ...frameIds.map((id) => `${FRAME_LINK}${id}`),
376
+ ];
377
+ const relink = (main, ids) => ids
378
+ .filter((x) => x !== main)
379
+ .slice(0, 4)
380
+ .map((to) => ({ to, on: inferConnection(keysOf(main), keysOf(to)) ?? [] }));
381
+ const rebuild = useRef(false);
382
+ const changeMain = (next, conns, message) => {
383
+ if (!card)
384
+ return;
385
+ const before = { source, draft, widget, tokens };
386
+ setSource(next);
387
+ setDraft(next === null
388
+ ? tableDraftOf(config, card)
389
+ : {
390
+ ...customViewConfigSchema.parse({ connections: conns }),
391
+ name: draft.name,
392
+ cards: draft.cards,
393
+ });
394
+ setTokens([]);
395
+ rebuild.current = true;
396
+ lumenToast.undo(message, () => {
397
+ setSource(before.source);
398
+ setDraft(before.draft);
399
+ setWidget(before.widget);
400
+ setTokens(before.tokens);
401
+ });
402
+ };
403
+ const dropMain = () => {
404
+ const rest = others();
405
+ const next = rest[0];
406
+ if (next)
407
+ changeMain(next, relink(next, rest), `${mainLabel} dropped. ${labelOf(next)} is the main data now.`);
408
+ else
409
+ changeMain('', [], `${mainLabel} dropped.`);
410
+ };
411
+ const makeMain = (to) => {
412
+ const old = source ?? (readableDomain(pageDomain.id) ? pageDomain.id : null);
413
+ const rest = [...others().filter((x) => x !== to), ...(old ? [old] : [])];
414
+ changeMain(to, relink(to, rest), `${labelOf(to)} is the main data now.`);
415
+ };
416
+ const pickMain = (to) => to === PAGE_SOURCE
417
+ ? changeMain(null, [], `${pageLabel} is the main data now.`)
418
+ : changeMain(to, relink(to, others()), `${labelOf(to)} is the main data now.`);
419
+ const clearAll = () => changeMain('', [], 'All data cleared.');
420
+ const dropLink = (to) => {
421
+ const before = draft;
422
+ unlink(to);
423
+ lumenToast.undo(`${labelOf(to)} dropped.`, () => setDraft(before));
424
+ };
425
+ const dropRecipe = (id) => {
426
+ const before = draft;
427
+ removeFrame(id);
428
+ lumenToast.undo(`${labelOf(`${FRAME_LINK}${id}`)} dropped.`, () => setDraft(before));
429
+ };
430
+ // The main data picker: this page's rows, the app's live domains, the person's recipes.
431
+ const dataOptions = useWidgetDataOptions(open && !!card, pageLabel);
432
+ const mainOptions = card ? dataOptions.filter((o) => o.value !== (source ?? PAGE_SOURCE)) : [];
433
+ // A new main data's fields: a widget whose fields it lacks starts afresh once its fields load.
434
+ // biome-ignore lint/correctness/useExhaustiveDependencies: runs when the new main data's fields arrive.
435
+ useEffect(() => {
436
+ if (!rebuild.current)
437
+ return;
438
+ if (source && srcNow?.state !== 'ok')
439
+ return;
440
+ rebuild.current = false;
441
+ if (!widget)
442
+ return;
443
+ const keys = new Set(domain.fields.map((f) => f.key));
444
+ const fits = widgetSlots(widget, domain).every((sl) => sl.keys.every((k) => keys.has(k)));
445
+ if (fits)
446
+ return;
447
+ const kept = Object.fromEntries(KEPT.filter((k) => widget[k] !== undefined).map((k) => [k, widget[k]]));
448
+ // A shape the new data cannot fill (a number with no number field) stays as it is, and Save
449
+ // says which field is missing.
450
+ try {
451
+ setWidget({ ...draftOf(styleId, domain, []), ...kept });
452
+ }
453
+ catch { }
454
+ }, [source, srcNow?.state, domain.fields.length]);
351
455
  // A widget's rows: the view's, narrowed by its own filter, with or without cash.
352
456
  const widgetRows = tokens.length ? filterByTokens(rows, filterFields, tokens, domain.text) : rows;
353
457
  const cashRows = widget && isRowCard(widget) && domain.cash && !summaryCash(widget, tokens.length > 0)
@@ -368,9 +472,25 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
368
472
  why: (f) => (slot.accepts(f) ? null : slot.why),
369
473
  }
370
474
  : { on: (k) => shownKeys.includes(k), set: setShown, why: () => null };
475
+ // A widget on its own data keeps it and its links on the card; the page's stay as they were.
476
+ const sourcePart = source
477
+ ? { source, connections: draft.connections ?? [] }
478
+ : { source: undefined, connections: undefined };
479
+ const viewPart = (next) => source !== null
480
+ ? {
481
+ formulas: config.formulas,
482
+ connections: config.connections,
483
+ ...(config.frames ? { frames: config.frames } : {}),
484
+ }
485
+ : {
486
+ formulas: next.formulas,
487
+ connections: next.connections,
488
+ ...(next.frames ? { frames: next.frames } : {}),
489
+ };
371
490
  const parsed = widget
372
491
  ? viewCardSchema.safeParse({
373
492
  ...widget,
493
+ ...sourcePart,
374
494
  title: title.trim() || null,
375
495
  // A widget's own filter is its own: it never shares with the colour group.
376
496
  filter: readsRows(widget) ? tokens.map(({ link: _, ...t }) => t) : [],
@@ -385,6 +505,16 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
385
505
  setSections((s) => new Set(s).add(id));
386
506
  };
387
507
  const issues = [
508
+ ...(source === ''
509
+ ? [
510
+ {
511
+ where: 'Data',
512
+ message: 'Pick the main data',
513
+ target: 'view-editor-main-data',
514
+ reveal: () => setTab('fields'),
515
+ },
516
+ ]
517
+ : []),
388
518
  ...(!card && !draft.name.trim()
389
519
  ? [
390
520
  {
@@ -463,11 +593,7 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
463
593
  }
464
594
  if (parsed) {
465
595
  if (parsed.success)
466
- onSaveCard?.(parsed.data, {
467
- formulas: draft.formulas,
468
- connections: draft.connections,
469
- ...(draft.frames ? { frames: draft.frames } : {}),
470
- });
596
+ onSaveCard?.(parsed.data, viewPart(draft));
471
597
  }
472
598
  else if (frame) {
473
599
  // A table of its own: the frame, its name and its layout.
@@ -476,25 +602,32 @@ export function ViewEditDialog({ open, onOpenChange, domain: joined, config, row
476
602
  onSaveCard?.(viewCardSchema.parse({
477
603
  ...Object.fromEntries(KEPT.filter((k) => frame[k] !== undefined).map((k) => [k, frame[k]])),
478
604
  ...(own?.controls ? { controls: own.controls } : {}),
605
+ ...sourcePart,
479
606
  kind: 'table',
480
607
  style: 'table',
481
608
  title: title.trim() || null,
482
609
  filter: [],
483
610
  table: tableLayoutOf(next),
484
- }), {
485
- formulas: next.formulas,
486
- connections: next.connections,
487
- ...(next.frames ? { frames: next.frames } : {}),
488
- });
611
+ }), viewPart(next));
489
612
  }
490
613
  onOpenChange(false);
491
614
  };
492
- const tree = pick ? (_jsx(FieldTree, { domain: domain, draft: draft, picked: pick.on, onPick: pick.set, why: pick.why, formulaProblems: formulaProblems, onEditFormula: edit, onRemoveFormula: removeFormula, onAddFormula: addFormula, deskDomains: deskDomains, onLink: link, onUnlink: unlink, linkPairs: {
615
+ const tree = pick ? (_jsx(FieldTree, { domain: domain, draft: draft, picked: pick.on, onPick: pick.set, why: pick.why, formulaProblems: formulaProblems, onEditFormula: edit, onRemoveFormula: removeFormula, onAddFormula: addFormula, deskDomains: deskDomains, onLink: link, onUnlink: card ? dropLink : unlink, main: card
616
+ ? {
617
+ label: mainLabel,
618
+ empty: source === '',
619
+ options: mainOptions,
620
+ onPick: pickMain,
621
+ onDrop: dropMain,
622
+ onClear: clearAll,
623
+ onMakeMain: makeMain,
624
+ }
625
+ : undefined, linkPairs: {
493
626
  cardFields: unlinked.fields.filter((f) => f.group !== FORMULAS_GROUP),
494
627
  fieldsOf: (to) => allRead[to]?.domain.fields ?? [],
495
628
  onOf: (to) => connections.find((c) => c.to === to)?.on ?? [],
496
629
  set: setPairs,
497
- }, deskLabel: app?.label ?? null, recipes: recipes, recipeNote: choices.isError ? `Recipes did not load: ${choices.error.message}` : null, onAddRecipe: addRecipe, recipeByKey: byKey, onRemoveRecipe: removeFrame, onNewRecipe: askRecipe })) : (_jsx("p", { className: "px-1.5 text-sm text-muted-foreground", children: widget?.kind === 'chart' && (widget.style === 'price' || widget.style === 'market')
630
+ }, deskLabel: app?.label ?? null, recipes: recipes, recipeNote: choices.isError ? `Recipes did not load: ${choices.error.message}` : null, onAddRecipe: addRecipe, recipeByKey: byKey, onRemoveRecipe: card ? dropRecipe : removeFrame, onNewRecipe: askRecipe })) : (_jsx("p", { className: "px-1.5 text-sm text-muted-foreground", children: widget?.kind === 'chart' && (widget.style === 'price' || widget.style === 'market')
498
631
  ? 'This chart reads its symbols, not fields.'
499
632
  : 'This widget takes no fields.' }));
500
633
  const treeHint = widget ? (slot && (_jsxs("div", { className: "flex flex-col gap-2 px-1.5 pb-2", children: [slots.length > 1 && (_jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, className: "justify-self-start", "aria-label": "Fill", value: slot.id, onValueChange: (v) => v && setSlotId(v), children: slots.map((s) => (_jsx(ToggleGroupItem, { value: s.id, className: "px-2.5", children: s.label }, s.id))) })), _jsx("p", { className: "text-xs text-muted-foreground", children: slot.max === 1
@@ -577,8 +710,7 @@ function LinkPairs({ label, on, cardFields, fields, onChange, }) {
577
710
  const row = (p, i) => (_jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [_jsx(LumenSelector, { label: "This card's field", size: "sm", className: "min-w-0 flex-1", placeholder: "This card", search: true, value: p.from || null, options: mine, onChange: (v) => put({ ...p, from: v }, i) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "=" }), _jsx(LumenSelector, { label: `${label}'s field`, size: "sm", className: "min-w-0 flex-1", placeholder: theirs.length ? label : 'Loading…', search: true, disabled: !theirs.length, value: p.to || null, options: theirs, onChange: (v) => put({ ...p, to: v }, i) }), i !== null && (_jsx(IconButton, { icon: XIcon, size: "icon-xs", label: "Remove this pair", onClick: () => onChange(on.filter((_, j) => j !== i)) }))] }, i ?? 'new'));
578
711
  return (_jsxs("div", { className: "flex flex-col gap-1 px-1 py-1", children: [on.map((p, i) => row(p, i)), open && row(open, null), !open && on.length < MAX_PAIRS && (_jsxs(Button, { size: "xs", variant: "ghost", className: "self-start text-primary", onClick: () => setPart({ from: '', to: '' }), children: [_jsx(PlusIcon, {}), "Field pair"] }))] }));
579
712
  }
580
- /** The right pane: every field by group and topic, ticked to show it as a column. */
581
- function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEditFormula, onRemoveFormula, onAddFormula, deskDomains = [], onLink, onUnlink, deskLabel, recipes = [], recipeNote, onAddRecipe, recipeByKey = () => false, onRemoveRecipe, onNewRecipe, linkPairs, }) {
713
+ function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEditFormula, onRemoveFormula, onAddFormula, deskDomains = [], onLink, onUnlink, deskLabel, recipes = [], recipeNote, onAddRecipe, recipeByKey = () => false, onRemoveRecipe, onNewRecipe, linkPairs, main, }) {
582
714
  const assistantName = useAssistantName();
583
715
  const [query, setQuery] = useState('');
584
716
  // New recipe's box, open while not null.
@@ -655,32 +787,41 @@ function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEdit
655
787
  const box = (_jsx(Checkbox, { checked: picked(f.key), disabled: !!off, onCheckedChange: (v) => onPick(f.key, v === true), "aria-label": `Use ${f.label}` }));
656
788
  return (_jsxs("div", { className: cn('flex min-h-7 items-center gap-2 rounded-md px-1 hover:bg-accent/60', off && 'text-muted-foreground'), children: [_jsxs(Label, { className: "flex min-w-0 flex-1 cursor-pointer items-center gap-2 py-1 text-[0.8125rem] font-normal", children: [off ? _jsx(WhyOff, { note: off, children: box }) : box, _jsx("span", { className: "truncate", children: f.label }), f.note && (_jsx(Badge, { variant: "secondary", className: "font-normal", children: f.note })), fi >= 0 && formulaProblems[fi] && (_jsx(Badge, { variant: "destructive", className: "font-normal", children: "fix" }))] }), _jsx("span", { className: "flex w-8 shrink-0 justify-end", children: _jsx(FormatChip, { format: f.format, icon: f.icon }) }), _jsx("code", { className: "w-28 shrink-0 truncate font-mono text-[0.7rem] text-muted-foreground max-md:hidden", title: f.formula ?? f.key, children: fi >= 0 ? formulas[fi]?.expr || 'no expression yet' : f.key }), fi >= 0 && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: PencilIcon, size: "icon-xs", label: `Edit formula ${f.label}`, onClick: () => onEditFormula(f.key) }), _jsx(LumenConfirm, { title: `Remove ${f.label}?`, description: "The formula and its column leave this view when you save.", action: "Remove", onConfirm: () => onRemoveFormula(f.key), children: _jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Remove formula ${f.label}` }) })] }))] }, f.key));
657
789
  };
658
- return (_jsxs("div", { className: "flex flex-col gap-px", "data-testid": "view-fields", children: [_jsxs("div", { className: "mb-1 flex items-center gap-1.5", children: [_jsxs(InputGroup, { className: "min-w-0 flex-1 bg-background", children: [_jsx(InputGroupAddon, { children: _jsx(SearchIcon, {}) }), _jsx(InputGroupInput, { type: "search", className: "[&::-webkit-search-cancel-button]:hidden", "aria-label": "Search fields", placeholder: `Search ${domain.fields.length} fields`, value: query, onChange: (e) => setQuery(e.target.value) }), query && (_jsx(InputGroupAddon, { align: "inline-end", children: _jsx(InputGroupButton, { size: "icon-xs", "aria-label": "Clear search", onClick: () => setQuery(''), children: _jsx(XIcon, {}) }) }))] }), dataOptions.length > 0 && (_jsx(LumenSelector, { trigger: "chip", icon: PlusIcon, placeholder: "Data", label: "Add data", align: "end", search: true, value: null, options: dataOptions, empty: "Nothing else to add", onChange: addData }))] }), recipeNote && _jsx("p", { className: "px-1.5 pb-1 text-xs text-destructive", children: recipeNote }), wish !== null && onNewRecipe && (_jsxs("div", { className: "mb-1 flex flex-col gap-1.5 rounded-md border bg-background p-2", children: [_jsx(Label, { htmlFor: "new-recipe", className: "text-xs font-medium", children: "New recipe" }), _jsx(Textarea, { id: "new-recipe", autoFocus: true, rows: 2, value: wish, placeholder: "How far each symbol is off its 52-week high", onChange: (e) => setWish(e.target.value), className: "min-h-14 text-sm" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Saves this widget, then opens ", assistantName, " with your words to send. Its columns join here by ", Object.keys(domain.keys ?? {}).join(' or '), "."] }), _jsxs("div", { className: "flex justify-end gap-1.5", children: [_jsx(Button, { size: "xs", variant: "ghost", onClick: () => setWish(null), children: "Cancel" }), _jsxs(Button, { size: "xs", disabled: !wish.trim(), onClick: () => onNewRecipe(wish), children: [_jsx(SparklesIcon, {}), "Save and ask ", assistantName] })] })] })), groupsOf(domain).map((g, gi) => {
659
- const all = domain.fields.filter((f) => groupOf(domain, f) === g.id);
660
- const fields = all.filter(match);
661
- const formulasGroup = g.id === FORMULAS_GROUP;
662
- const linkTo = g.id.startsWith(LINK_PREFIX) ? g.id.slice(LINK_PREFIX.length) : null;
663
- const recipe = g.id.startsWith(FRAME_PREFIX) ? g.id.slice(FRAME_PREFIX.length) : null;
664
- if (q && !fields.length)
665
- return null;
666
- if (!all.length && !formulasGroup && !linkTo)
667
- return null;
668
- const color = g.color ?? PLAIN;
669
- // A link starts open, so its pairs and fields show where they are picked.
670
- const open = linkTo
671
- ? !!q || (toggled.get(`g:${g.id}`) ?? true)
672
- : isOpen(`g:${g.id}`, all) || (formulasGroup && !all.length);
673
- const topics = [...new Set(fields.map((f) => f.topic ?? ''))];
674
- return (_jsxs(Collapsible, { open: open, onOpenChange: () => flip(`g:${g.id}`, open), children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-7 min-w-0 flex-1 justify-start gap-2 px-1.5 text-[0.8125rem] font-semibold", children: [_jsx(ChevronRightIcon, { className: cn('text-muted-foreground transition-transform', open && 'rotate-90') }), _jsx("span", { className: "size-2.5 shrink-0 rounded-[3px]", style: { background: color } }), formulasGroup && _jsx(SigmaIcon, { className: "text-muted-foreground" }), linkTo && _jsx(Link2Icon, { className: "text-muted-foreground" }), recipe && _jsx(SparklesIcon, { className: "text-muted-foreground" }), _jsx("span", { className: "truncate", children: g.label }), gi === 0 && !linkTo && !recipe && !formulasGroup && (_jsx(Badge, { variant: "secondary", className: "h-4 px-1.5 text-[0.6875rem] font-normal", children: "Rows" })), _jsxs("span", { className: "num ml-auto text-xs font-normal text-muted-foreground", children: [all.filter((f) => picked(f.key)).length, "/", all.length] })] }) }), linkTo && onUnlink && (_jsx(LumenConfirm, { title: `Unlink ${g.label}?`, description: "Its columns leave this widget when you save.", action: "Unlink", onConfirm: () => onUnlink(linkTo), children: _jsx(IconButton, { icon: Unlink2Icon, size: "icon-xs", label: `Unlink ${g.label}` }) })), recipe && onRemoveRecipe && (_jsx(LumenConfirm, { title: `Remove ${g.label}?`, description: `Its columns leave this widget when you save. The recipe stays in ${assistantName}.`, action: "Remove", onConfirm: () => onRemoveRecipe(recipe), children: _jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Remove ${g.label}` }) }))] }), _jsxs(CollapsibleContent, { className: "ml-[0.9rem] flex flex-col gap-px border-l-2 pl-2", style: { borderColor: guide(color) }, children: [g.note && !q && _jsx("p", { className: "px-1 py-1 text-xs text-muted-foreground", children: g.note }), linkTo && linkPairs && !q && (_jsx(LinkPairs, { label: g.label, on: linkPairs.onOf(linkTo), cardFields: linkPairs.cardFields, fields: linkPairs.fieldsOf(linkTo), onChange: (on) => linkPairs.set(linkTo, on) })), topics.map((t) => {
675
- const inTopic = fields.filter((f) => (f.topic ?? '') === t);
676
- if (!t)
677
- return inTopic.map(leaf);
678
- const src = g.sources?.[t];
679
- const id = `t:${g.id}:${t}`;
680
- const topicOpen = isOpen(id, all.filter((f) => f.topic === t));
681
- return (_jsxs(Collapsible, { open: topicOpen, onOpenChange: () => flip(id, topicOpen), children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-7 w-full justify-start gap-1.5 px-1 text-[0.8125rem] font-semibold", children: [_jsx(ChevronRightIcon, { className: cn('text-muted-foreground transition-transform', topicOpen && 'rotate-90') }), _jsx("span", { className: "truncate", children: t }), _jsxs("span", { className: "num text-xs font-normal text-muted-foreground", children: [inTopic.filter((f) => picked(f.key)).length, "/", inTopic.length] }), src && (_jsxs("span", { className: "ml-auto flex min-w-0 items-center gap-1.5 text-xs font-normal text-muted-foreground", children: [_jsx("span", { className: "size-1.5 shrink-0 rounded-full", style: { background: src.color } }), _jsx("span", { className: "font-medium", style: { color: src.color }, children: src.name }), src.detail && (_jsxs("span", { className: "truncate max-md:hidden", children: ["\u00B7 ", src.detail] }))] }))] }) }), _jsx(CollapsibleContent, { className: "ml-[0.7rem] flex flex-col gap-px border-l-2 pl-2", style: { borderColor: guide(src?.color ?? PLAIN) }, children: inTopic.map(leaf) })] }, t));
682
- }), formulasGroup && onAddFormula && !q && (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 justify-start gap-2 px-1 font-normal text-muted-foreground", onClick: onAddFormula, children: [_jsx(PlusIcon, {}), "Add formula"] }))] })] }, g.id));
683
- })] }));
790
+ return (_jsxs("div", { className: "flex flex-col gap-px", "data-testid": "view-fields", children: [_jsxs("div", { className: cn('mb-1 flex items-center gap-1.5', main?.empty && 'hidden'), children: [_jsxs(InputGroup, { className: "min-w-0 flex-1 bg-background", children: [_jsx(InputGroupAddon, { children: _jsx(SearchIcon, {}) }), _jsx(InputGroupInput, { type: "search", className: "[&::-webkit-search-cancel-button]:hidden", "aria-label": "Search fields", placeholder: `Search ${domain.fields.length} fields`, value: query, onChange: (e) => setQuery(e.target.value) }), query && (_jsx(InputGroupAddon, { align: "inline-end", children: _jsx(InputGroupButton, { size: "icon-xs", "aria-label": "Clear search", onClick: () => setQuery(''), children: _jsx(XIcon, {}) }) }))] }), dataOptions.length > 0 && !main?.empty && (_jsx(LumenSelector, { trigger: "chip", icon: PlusIcon, placeholder: "Data", label: "Add data", align: "end", search: true, value: null, options: dataOptions, empty: "Nothing else to add", onChange: addData })), main && !main.empty && (_jsx(LumenConfirm, { title: "Clear all data?", description: "Every source leaves this widget. Pick its main data again, or Undo.", action: "Clear all", onConfirm: main.onClear, children: _jsx(IconButton, { icon: Trash2Icon, size: "icon-xs", label: "Clear all data" }) }))] }), recipeNote && _jsx("p", { className: "px-1.5 pb-1 text-xs text-destructive", children: recipeNote }), wish !== null && onNewRecipe && (_jsxs("div", { className: "mb-1 flex flex-col gap-1.5 rounded-md border bg-background p-2", children: [_jsx(Label, { htmlFor: "new-recipe", className: "text-xs font-medium", children: "New recipe" }), _jsx(Textarea, { id: "new-recipe", autoFocus: true, rows: 2, value: wish, placeholder: "How far each symbol is off its 52-week high", onChange: (e) => setWish(e.target.value), className: "min-h-14 text-sm" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Saves this widget, then opens ", assistantName, " with your words to send. Its columns join here by ", Object.keys(domain.keys ?? {}).join(' or '), "."] }), _jsxs("div", { className: "flex justify-end gap-1.5", children: [_jsx(Button, { size: "xs", variant: "ghost", onClick: () => setWish(null), children: "Cancel" }), _jsxs(Button, { size: "xs", disabled: !wish.trim(), onClick: () => onNewRecipe(wish), children: [_jsx(SparklesIcon, {}), "Save and ask ", assistantName] })] })] })), main?.empty && (_jsxs("div", { className: "flex flex-col items-center gap-2 rounded-md border border-dashed px-3 py-6 text-center", children: [_jsx(DatabaseIcon, { className: "size-5 text-muted-foreground" }), _jsx("p", { className: "text-sm font-medium", children: "Pick the main data" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Its rows are what this widget shows." }), _jsx(LumenSelector, { id: "view-editor-main-data", trigger: "chip", icon: DatabaseIcon, placeholder: "Main data", label: "Pick the main data", search: true, value: null, options: main.options, empty: "No data to pick", onChange: main.onPick })] })), !main?.empty &&
791
+ groupsOf(domain).map((g, gi) => {
792
+ const all = domain.fields.filter((f) => groupOf(domain, f) === g.id);
793
+ const fields = all.filter(match);
794
+ const formulasGroup = g.id === FORMULAS_GROUP;
795
+ const linkTo = g.id.startsWith(LINK_PREFIX) ? g.id.slice(LINK_PREFIX.length) : null;
796
+ const rowsGroup = gi === 0 && !linkTo && !formulasGroup;
797
+ const recipe = !rowsGroup && g.id.startsWith(FRAME_PREFIX) ? g.id.slice(FRAME_PREFIX.length) : null;
798
+ // Not linked: no field pairs yet, so it adds nothing; its fields wait greyed out.
799
+ const unlinked = !!linkTo && !all.length && !(linkPairs?.onOf(linkTo).length ?? 0);
800
+ if (q && !fields.length)
801
+ return null;
802
+ if (!all.length && !formulasGroup && !linkTo)
803
+ return null;
804
+ const color = g.color ?? PLAIN;
805
+ // A link starts open, so its pairs and fields show where they are picked.
806
+ const open = linkTo
807
+ ? !!q || (toggled.get(`g:${g.id}`) ?? true)
808
+ : isOpen(`g:${g.id}`, all) || (formulasGroup && !all.length);
809
+ const topics = [...new Set(fields.map((f) => f.topic ?? ''))];
810
+ return (_jsxs(Collapsible, { open: open, onOpenChange: () => flip(`g:${g.id}`, open), children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-7 min-w-0 flex-1 justify-start gap-2 px-1.5 text-[0.8125rem] font-semibold", children: [_jsx(ChevronRightIcon, { className: cn('text-muted-foreground transition-transform', open && 'rotate-90') }), _jsx("span", { className: "size-2.5 shrink-0 rounded-[3px]", style: { background: color } }), formulasGroup && _jsx(SigmaIcon, { className: "text-muted-foreground" }), linkTo &&
811
+ (unlinked ? (_jsx(Unlink2Icon, { className: "text-destructive" })) : (_jsx(Link2Icon, { className: "text-muted-foreground" }))), recipe && _jsx(SparklesIcon, { className: "text-muted-foreground" }), _jsx("span", { className: "truncate", children: rowsGroup && main ? main.label : g.label }), rowsGroup && (_jsx(Badge, { variant: "secondary", className: "h-4 px-1.5 text-[0.6875rem] font-normal", children: "Rows" })), unlinked && (_jsx(Badge, { variant: "destructive", className: "h-4 px-1.5 text-xs font-normal", children: "Not linked" })), _jsxs("span", { className: "num ml-auto text-xs font-normal text-muted-foreground", children: [all.filter((f) => picked(f.key)).length, "/", all.length] })] }) }), rowsGroup && main && (_jsxs(_Fragment, { children: [_jsx(LumenSelector, { trigger: "icon", icon: ReplaceIcon, label: "Change the main data", align: "end", search: true, value: null, options: main.options, empty: "No other data", onChange: main.onPick }), _jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Drop ${main.label}`, onClick: main.onDrop })] })), main && (linkTo || recipe) && (_jsx(IconButton, { icon: StarIcon, size: "icon-xs", label: `Make ${g.label} the main data`, onClick: () => main.onMakeMain(linkTo ?? `${FRAME_LINK}${recipe}`) })), main && linkTo && onUnlink && (_jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Drop ${g.label}`, onClick: () => onUnlink(linkTo) })), main && recipe && onRemoveRecipe && (_jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Drop ${g.label}`, onClick: () => onRemoveRecipe(recipe) })), !main && linkTo && onUnlink && (_jsx(LumenConfirm, { title: `Unlink ${g.label}?`, description: "Its columns leave this widget when you save.", action: "Unlink", onConfirm: () => onUnlink(linkTo), children: _jsx(IconButton, { icon: Unlink2Icon, size: "icon-xs", label: `Unlink ${g.label}` }) })), !main && recipe && onRemoveRecipe && (_jsx(LumenConfirm, { title: `Remove ${g.label}?`, description: `Its columns leave this widget when you save. The recipe stays in ${assistantName}.`, action: "Remove", onConfirm: () => onRemoveRecipe(recipe), children: _jsx(IconButton, { icon: XIcon, size: "icon-xs", label: `Remove ${g.label}` }) }))] }), _jsxs(CollapsibleContent, { className: "ml-[0.9rem] flex flex-col gap-px border-l-2 pl-2", style: { borderColor: guide(color) }, children: [g.note && !q && (_jsx("p", { className: cn('px-1 py-1 text-xs', unlinked ? 'text-destructive' : 'text-muted-foreground'), children: g.note })), linkTo && linkPairs && !q && (_jsx(LinkPairs, { label: g.label, on: linkPairs.onOf(linkTo), cardFields: linkPairs.cardFields, fields: linkPairs.fieldsOf(linkTo), onChange: (on) => linkPairs.set(linkTo, on) })), unlinked &&
812
+ linkPairs
813
+ ?.fieldsOf(linkTo)
814
+ .filter(match)
815
+ .map((f) => (_jsxs("div", { className: "flex min-h-7 items-center gap-2 px-1 text-sm text-muted-foreground opacity-60", children: [_jsx(WhyOff, { note: "Link it first", children: _jsx(Checkbox, { checked: false, disabled: true, "aria-label": `Use ${f.label}` }) }), _jsx("span", { className: "truncate", children: f.label })] }, f.key))), topics.map((t) => {
816
+ const inTopic = fields.filter((f) => (f.topic ?? '') === t);
817
+ if (!t)
818
+ return inTopic.map(leaf);
819
+ const src = g.sources?.[t];
820
+ const id = `t:${g.id}:${t}`;
821
+ const topicOpen = isOpen(id, all.filter((f) => f.topic === t));
822
+ return (_jsxs(Collapsible, { open: topicOpen, onOpenChange: () => flip(id, topicOpen), children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-7 w-full justify-start gap-1.5 px-1 text-[0.8125rem] font-semibold", children: [_jsx(ChevronRightIcon, { className: cn('text-muted-foreground transition-transform', topicOpen && 'rotate-90') }), _jsx("span", { className: "truncate", children: t }), _jsxs("span", { className: "num text-xs font-normal text-muted-foreground", children: [inTopic.filter((f) => picked(f.key)).length, "/", inTopic.length] }), src && (_jsxs("span", { className: "ml-auto flex min-w-0 items-center gap-1.5 text-xs font-normal text-muted-foreground", children: [_jsx("span", { className: "size-1.5 shrink-0 rounded-full", style: { background: src.color } }), _jsx("span", { className: "font-medium", style: { color: src.color }, children: src.name }), src.detail && (_jsxs("span", { className: "truncate max-md:hidden", children: ["\u00B7 ", src.detail] }))] }))] }) }), _jsx(CollapsibleContent, { className: "ml-[0.7rem] flex flex-col gap-px border-l-2 pl-2", style: { borderColor: guide(src?.color ?? PLAIN) }, children: inTopic.map(leaf) })] }, t));
823
+ }), formulasGroup && onAddFormula && !q && (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 justify-start gap-2 px-1 font-normal text-muted-foreground", onClick: onAddFormula, children: [_jsx(PlusIcon, {}), "Add formula"] }))] })] }, g.id));
824
+ })] }));
684
825
  }
685
826
  /** A key's name as a person reads it: `symbol` is Symbol. */
686
827
  const fieldName = (k) => (k[0]?.toUpperCase() ?? '') + k.slice(1).replace(/_/g, ' ');
@@ -14,7 +14,7 @@ export declare function FormatChip({ format, icon }: {
14
14
  icon?: LucideIcon;
15
15
  }): import("react").JSX.Element;
16
16
  /** The number formats as picker options, each with how it reads. */
17
- export declare const formatOptions: () => LumenOption<ViewFormula["format"]>[];
17
+ export declare const formatOptions: () => LumenOption<ViewFormula['format']>[];
18
18
  /**
19
19
  * A formula as it shows: a Text one has no sum or average of words, so its
20
20
  * group and Total rows are blank unless they count, or show the first or last value. Its format is
@@ -127,12 +127,12 @@ export declare const CHIP_TONE: Record<ViewChipTone, string>;
127
127
  /** A run's state as a chip, the same on every app: done green, failed
128
128
  * red, running blue, queued, waiting and canceled grey. */
129
129
  export declare const RUN_STATE_TONE: {
130
- readonly done: "gain";
131
- readonly failed: "loss";
132
- readonly running: "info";
133
- readonly queued: "muted";
134
- readonly waiting: "muted";
135
- readonly canceled: "muted";
130
+ readonly done: 'gain';
131
+ readonly failed: 'loss';
132
+ readonly running: 'info';
133
+ readonly queued: 'muted';
134
+ readonly waiting: 'muted';
135
+ readonly canceled: 'muted';
136
136
  };
137
137
  /** A group of fields by where they come from: their colour and, per topic, the source line. */
138
138
  export interface ViewFieldGroup {
@@ -303,8 +303,8 @@ export declare const formulasOf: (c: CustomViewConfig) => {
303
303
  key: string;
304
304
  label: string;
305
305
  expr: string;
306
- format: "number" | "text" | "weight" | "compact" | "rounded" | "price" | "price_rounded" | "price_signed" | "percent" | "signed" | "integer";
307
- agg: "none" | "max" | "min" | "first" | "last" | "sum" | "count" | "mean" | "distinct" | null;
306
+ format: "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "text" | "weight";
307
+ agg: "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum" | null;
308
308
  }[];
309
309
  /** Fields listed under Columns but not shown, for their Filter and Group ticks; views saved before it have none. */
310
310
  export declare const hiddenOf: (c: CustomViewConfig) => string[];
@@ -313,32 +313,32 @@ export declare const tilesOf: (c: CustomViewConfig) => ({
313
313
  key: string;
314
314
  label: string;
315
315
  field: string;
316
- agg: "max" | "min" | "first" | "last" | "sum" | "count" | "mean" | "distinct";
316
+ agg: "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "sum";
317
317
  filter: {
318
318
  key: string;
319
319
  value: string;
320
320
  not?: boolean | undefined;
321
- op?: "gt" | "lt" | "eq" | "between" | undefined;
322
- link?: "blue" | "lime" | "pink" | "teal" | "amber" | undefined;
321
+ op?: "between" | "eq" | "gt" | "lt" | undefined;
322
+ link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
323
323
  }[];
324
324
  sign: boolean;
325
325
  suffix: string;
326
326
  lines: ({
327
327
  kind: "value";
328
328
  field: string;
329
- agg: "max" | "min" | "first" | "last" | "sum" | "count" | "mean" | "distinct";
329
+ agg: "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "sum";
330
330
  filter: {
331
331
  key: string;
332
332
  value: string;
333
333
  not?: boolean | undefined;
334
- op?: "gt" | "lt" | "eq" | "between" | undefined;
335
- link?: "blue" | "lime" | "pink" | "teal" | "amber" | undefined;
334
+ op?: "between" | "eq" | "gt" | "lt" | undefined;
335
+ link?: "amber" | "blue" | "lime" | "pink" | "teal" | undefined;
336
336
  }[];
337
337
  suffix: string;
338
338
  } | {
339
339
  kind: "pct";
340
340
  field: string;
341
- agg: "max" | "min" | "first" | "last" | "sum" | "count" | "mean" | "distinct";
341
+ agg: "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "sum";
342
342
  } | {
343
343
  kind: "text";
344
344
  text: string;
package/dist/whats-new.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // What's new (in Lumen for
3
3
  // every app): the version in the status bar opens the release notes, newest
4
4
  // first, each release's Key takeaways only. An app turns it on with LumenShell's `releases`, its
@@ -0,0 +1,4 @@
1
+ import type { LumenOption } from './lumen-selector.js';
2
+ /** The page's own rows, as a data option's value. */
3
+ export declare const PAGE_SOURCE = "_page";
4
+ export declare function useWidgetDataOptions(enabled: boolean, pageLabel: string): LumenOption[];
@@ -0,0 +1,32 @@
1
+ // The data a widget can take as its main data: this page's rows, the app's live domains with a
2
+ // reader, and the person's recipes. Add widget's first step and Edit widget ▸ Data both pick from it.
3
+ import { CATALOG, FRAME_LINK, catalogKeys, readableDomain, } from '@columenlabs/lumen-ui-types/catalog';
4
+ import { DatabaseIcon, SparklesIcon, TableIcon } from 'lucide-react';
5
+ import { asOf, useFrameChoices } from './joins.js';
6
+ import { useApp } from './shell.js';
7
+ /** The page's own rows, as a data option's value. */
8
+ export const PAGE_SOURCE = '_page';
9
+ export function useWidgetDataOptions(enabled, pageLabel) {
10
+ const app = useApp();
11
+ const choices = useFrameChoices(enabled);
12
+ return [
13
+ { value: PAGE_SOURCE, label: pageLabel, group: 'This page', icon: TableIcon },
14
+ ...Object.entries(CATALOG)
15
+ .filter(([id, d]) => d.desk === app?.id && d.status === 'live' && readableDomain(id))
16
+ .map(([id, d]) => ({
17
+ value: id,
18
+ label: d.label,
19
+ group: `${app?.label ?? 'This app'} data`,
20
+ detail: catalogKeys(id).join(', ') || undefined,
21
+ icon: DatabaseIcon,
22
+ keywords: [d.about],
23
+ })),
24
+ ...(choices.data ?? []).map((f) => ({
25
+ value: `${FRAME_LINK}${f.id}`,
26
+ label: f.name,
27
+ group: 'Recipes',
28
+ detail: asOf(f.at),
29
+ icon: SparklesIcon,
30
+ })),
31
+ ];
32
+ }