@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.
- package/README.md +1 -1
- package/dist/assistant-panel.js +1 -1
- package/dist/coverage-calendar-map.js +1 -1
- package/dist/coverage-cells.d.ts +2 -2
- package/dist/coverage-map.js +4 -10
- package/dist/details-table.js +1 -5
- package/dist/form-issues.js +1 -1
- package/dist/frame-script.js +1 -1
- package/dist/growth-chart.d.ts +9 -9
- package/dist/joins.d.ts +4 -3
- package/dist/joins.js +1 -1
- package/dist/lumen-code.js +1 -5
- package/dist/lumen-drawer.js +1 -3
- package/dist/lumen-filter.js +5 -13
- package/dist/lumen-grid.js +2 -4
- package/dist/lumen-layout.js +7 -4
- package/dist/lumen-links.d.ts +1 -1
- package/dist/lumen-selector.js +1 -1
- package/dist/lumen-source.js +2 -4
- package/dist/lumen-tab-drawer.js +1 -1
- package/dist/lumen-toast.d.ts +4 -4
- package/dist/lumen-view-card.js +1 -1
- package/dist/lumen-view.js +2 -4
- package/dist/lumen-widget.d.ts +277 -171
- package/dist/lumen-widget.js +31 -20
- package/dist/market/indicator-picker.js +1 -1
- package/dist/market/live.d.ts +1 -1
- package/dist/market/lumen-chart.js +1 -1
- package/dist/market/price-chart.d.ts +10 -10
- package/dist/nav.js +1 -1
- package/dist/notifications.js +1 -3
- package/dist/overview.js +1 -1
- package/dist/page.js +1 -1
- package/dist/pager.js +1 -1
- package/dist/prefs.d.ts +25 -25
- package/dist/schedules.js +1 -1
- package/dist/settings.js +1 -3
- package/dist/shell.js +1 -1
- package/dist/sourced-card.d.ts +10 -0
- package/dist/sourced-card.js +69 -0
- package/dist/summary-cards.js +2 -6
- package/dist/symbol-sheet.js +1 -1
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +1 -1
- package/dist/tile.js +2 -4
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +2 -2
- package/dist/ui/chart.d.ts +2 -2
- package/dist/ui/chart.js +2 -5
- package/dist/ui/field.d.ts +1 -1
- package/dist/ui/input-group.d.ts +2 -2
- package/dist/ui/item.d.ts +3 -3
- package/dist/ui/sidebar.d.ts +1 -1
- package/dist/ui/sidebar.js +1 -1
- package/dist/ui/toggle.d.ts +1 -1
- package/dist/view-edit-cell.js +1 -4
- package/dist/view-editor.d.ts +1 -1
- package/dist/view-editor.js +188 -47
- package/dist/view-formulas.d.ts +1 -1
- package/dist/view-model.d.ts +15 -15
- package/dist/whats-new.js +1 -1
- package/dist/widget-data.d.ts +4 -0
- package/dist/widget-data.js +32 -0
- package/dist/widget-settings.d.ts +219 -139
- package/dist/widget-settings.js +4 -4
- package/dist/widget-thumbs.d.ts +1 -1
- package/dist/widget-thumbs.js +1 -1
- package/dist/workspace-keys.d.ts +1 -1
- package/dist/workspaces.js +1 -1
- 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" | "
|
|
8
|
-
size?: "default" | "
|
|
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?: "
|
|
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;
|
package/dist/ui/sidebar.d.ts
CHANGED
|
@@ -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" | "
|
|
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;
|
package/dist/ui/sidebar.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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';
|
package/dist/ui/toggle.d.ts
CHANGED
|
@@ -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" | "
|
|
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 };
|
package/dist/view-edit-cell.js
CHANGED
|
@@ -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'
|
package/dist/view-editor.d.ts
CHANGED
|
@@ -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:
|
|
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;
|
package/dist/view-editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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, ' ');
|
package/dist/view-formulas.d.ts
CHANGED
|
@@ -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[
|
|
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
|
package/dist/view-model.d.ts
CHANGED
|
@@ -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:
|
|
131
|
-
readonly failed:
|
|
132
|
-
readonly running:
|
|
133
|
-
readonly queued:
|
|
134
|
-
readonly waiting:
|
|
135
|
-
readonly canceled:
|
|
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: "
|
|
307
|
-
agg: "
|
|
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: "
|
|
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?: "
|
|
322
|
-
link?: "
|
|
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: "
|
|
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?: "
|
|
335
|
-
link?: "
|
|
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: "
|
|
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 {
|
|
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,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
|
+
}
|