@columenlabs/lumen-ui 0.1.189 → 0.1.190

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.
@@ -1,7 +1,10 @@
1
- export declare function FrameScriptDialog({ api, frame, name, open, onOpenChange, }: {
1
+ import type { FrameRun } from '@columenlabs/lumen-ui-types/frames';
2
+ export declare function FrameScriptDialog({ api, frame, name, runs, open, onOpenChange, }: {
2
3
  api: string;
3
4
  frame: string;
4
5
  name: string;
6
+ /** The last runs, newest first: when, how many rows, how long, who, and why one failed. */
7
+ runs?: FrameRun[];
5
8
  open: boolean;
6
9
  onOpenChange: (open: boolean) => void;
7
10
  }): import("react").JSX.Element;
@@ -47,7 +47,7 @@ function parseScript(was, text) {
47
47
  args: (o.args ?? {}),
48
48
  };
49
49
  }
50
- export function FrameScriptDialog({ api, frame, name, open, onOpenChange, }) {
50
+ export function FrameScriptDialog({ api, frame, name, runs, open, onOpenChange, }) {
51
51
  const qc = useQueryClient();
52
52
  const assistant = useAssistantName();
53
53
  const read = useQuery({
@@ -96,8 +96,17 @@ export function FrameScriptDialog({ api, frame, name, open, onOpenChange, }) {
96
96
  setError(e.message);
97
97
  }
98
98
  };
99
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex h-[min(44rem,calc(100dvh-2rem))] flex-col sm:max-w-4xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Script \u00B7 ", name] }), _jsxs(DialogDescription, { children: ["What ", assistant, " runs to make these rows. Saved only if it runs."] })] }), read.isPending ? (_jsx(Skeleton, { className: "min-h-0 flex-1" })) : read.error ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertTitle, { children: "Couldn't read the script" }), _jsx(AlertDescription, { children: read.error.message })] })) : !script ? (_jsxs(Alert, { children: [_jsx(AlertTitle, { children: "No script" }), _jsxs(AlertDescription, { children: [assistant, " wrote these rows once: nothing runs to make them."] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-end gap-1", children: [lang && (_jsx(Badge, { variant: "secondary", className: "font-mono", children: lang })), _jsx(IconButton, { icon: WrapTextIcon, label: wrap ? 'Stop wrapping lines' : 'Wrap long lines', pressed: wrap, onClick: () => setWrap(!wrap) })] }), _jsx(LumenCodeEditor, { className: "flex-1", value: text, lang: lang, wrap: wrap, onChange: setText }), error && (_jsxs(Alert, { variant: "destructive", className: "max-h-32 overflow-y-auto", children: [_jsx(AlertTitle, { children: "Didn't save" }), _jsx(AlertDescription, { className: "break-words font-mono text-xs", children: error })] }))] })), _jsxs(DialogFooter, { children: [_jsx(IconButton, { icon: RotateCcwIcon, label: "Undo my changes", disabled: !changed || save.isPending, onClick: () => {
99
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex h-[min(44rem,calc(100dvh-2rem))] flex-col sm:max-w-4xl", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Script \u00B7 ", name] }), _jsxs(DialogDescription, { children: ["What ", assistant, " runs to make these rows. Saved only if it runs."] })] }), read.isPending ? (_jsx(Skeleton, { className: "min-h-0 flex-1" })) : read.error ? (_jsxs(Alert, { variant: "destructive", children: [_jsx(AlertTitle, { children: "Couldn't read the script" }), _jsx(AlertDescription, { children: read.error.message })] })) : !script ? (_jsxs(Alert, { children: [_jsx(AlertTitle, { children: "No script" }), _jsxs(AlertDescription, { children: [assistant, " wrote these rows once: nothing runs to make them."] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-end gap-1", children: [lang && (_jsx(Badge, { variant: "secondary", className: "font-mono", children: lang })), _jsx(IconButton, { icon: WrapTextIcon, label: wrap ? 'Stop wrapping lines' : 'Wrap long lines', pressed: wrap, onClick: () => setWrap(!wrap) })] }), _jsx(LumenCodeEditor, { className: "flex-1", value: text, lang: lang, wrap: wrap, onChange: setText }), runs && runs.length > 0 && _jsx(RunList, { runs: runs, assistant: assistant }), error && (_jsxs(Alert, { variant: "destructive", className: "max-h-32 overflow-y-auto", children: [_jsx(AlertTitle, { children: "Didn't save" }), _jsx(AlertDescription, { className: "break-words font-mono text-xs", children: error })] }))] })), _jsxs(DialogFooter, { children: [_jsx(IconButton, { icon: RotateCcwIcon, label: "Undo my changes", disabled: !changed || save.isPending, onClick: () => {
100
100
  setText(original);
101
101
  setError(null);
102
102
  } }), _jsxs(Button, { disabled: !script || save.isPending, onClick: run, children: [save.isPending ? _jsx(Spinner, {}) : _jsx(PlayIcon, {}), " Run and save"] })] })] }) }));
103
103
  }
104
+ /** The frame's last runs, one line each; a failure's reason in its hover. */
105
+ function RunList({ runs, assistant }) {
106
+ return (_jsx("ul", { className: "flex max-h-28 flex-col gap-0.5 overflow-y-auto text-xs text-muted-foreground", children: runs.map((r) => (_jsxs("li", { className: "flex min-w-0 items-center gap-2", title: r.error, children: [_jsx("span", { className: `size-1.5 shrink-0 rounded-full ${r.ok ? 'bg-[var(--meter-ok)]' : 'bg-destructive'}` }), _jsx("time", { className: "shrink-0", dateTime: r.at, children: new Date(r.at).toLocaleString('en-US', {
107
+ month: 'short',
108
+ day: 'numeric',
109
+ hour: 'numeric',
110
+ minute: '2-digit',
111
+ }) }), _jsx("span", { className: "num shrink-0", children: r.ok ? `${r.rows.toLocaleString('en-US')} rows` : 'failed' }), _jsxs("span", { className: "num shrink-0", children: [(r.ms / 1000).toFixed(1), " s"] }), _jsx("span", { className: "min-w-0 truncate", children: r.ok ? `by ${r.by === 'you' ? 'you' : assistant}` : r.error })] }, r.at))) }));
112
+ }
package/dist/index.d.ts CHANGED
@@ -105,7 +105,7 @@ export { type AppViewState, useAppView } from './app-views.js';
105
105
  export { type ScreenMap, type ScreenOutcome, type ScreenStatus, type WorkspaceOnScreen, type AssistantMove, type AssistantMoveHandlers, type AssistantPage, type AssistantPageType, AssistantScreenProvider, useAssistantMoves, useAssistantPages, useAssistantScreen, useAssistantWorkspace, } from './assistant-screen.js';
106
106
  export { type AssistantActions, AssistantContext, type AssistantNote, type AssistantRef, type AssistantSlot, useAssistant, AssistantPendingContext, useAssistantName, useAssistantNote, useAssistantPending, } from './assistant-context.js';
107
107
  export { WORKSPACES_KEY, frameKey, workspacesListQuery } from './workspace-keys.js';
108
- export { type WorkspaceApp, type WorkspaceDraftProps, WorkspacePage, type WorkspacesConfig, WorkspacesProvider, WorkspacesSection, } from './workspaces.js';
108
+ export { type WorkspaceApp, type WorkspaceDraftProps, WorkspacePage, type WorkspaceRecipe, type WorkspacesConfig, WorkspacesProvider, WorkspacesSection, } from './workspaces.js';
109
109
  export { CHART_INDICATORS, type ChartIndicator, type IndicatorValues, LINE_COLOR, computeIndicators, formulaIndicator, lineColor, resolveIndicator, runFormula, } from './market/indicators.js';
110
110
  export { IndicatorPicker } from './market/indicator-picker.js';
111
111
  export { type OptionChainField, type OptionChainRow, OptionChainTable, } from './market/option-chain.js';
@@ -1,3 +1,4 @@
1
+ import { type FrameScript } from '@columenlabs/lumen-ui-types/frames';
1
2
  import { type CustomViewConfig } from '@columenlabs/lumen-ui-types/views';
2
3
  import { type ComponentType, type ReactNode } from 'react';
3
4
  import { type Crumb } from './page.js';
@@ -24,6 +25,20 @@ export interface WorkspacesConfig {
24
25
  defaultConfig?: (domain: string) => CustomViewConfig | null;
25
26
  /** The empty workspace's example ask, on an app: `put my holdings with their earnings dates in`. */
26
27
  example?: (app: string) => string;
28
+ /** Recipes a person picks to start a tab by hand (+ in the tab bar); none hides the +. */
29
+ recipes?: WorkspaceRecipe[];
30
+ }
31
+ /** A saved data pull a person can start a tab from: one call to one of the product's read tools. */
32
+ export interface WorkspaceRecipe {
33
+ id: string;
34
+ /** The tab's name and the picker's row: `Jobs scoring 70+`. */
35
+ label: string;
36
+ /** When to pick it, under the label. */
37
+ description?: string;
38
+ /** The picker's section: the app the rows come from. */
39
+ group?: string;
40
+ icon?: Icon;
41
+ recipe: FrameScript;
27
42
  }
28
43
  /** The product's apps and domains for its workspaces. */
29
44
  export declare const WorkspacesProvider: import("react").Provider<WorkspacesConfig>;
@@ -4,7 +4,7 @@ import { customViewConfigSchema, frameSpace, } from '@columenlabs/lumen-ui-types
4
4
  import { workspacePath, } from '@columenlabs/lumen-ui-types/workspaces';
5
5
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
6
6
  import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
7
- import { ArrowRightLeftIcon, CalendarIcon, ExternalLinkIcon, HashIcon, LayersIcon, PencilIcon, SparklesIcon, TableIcon, TextCursorInputIcon, Trash2Icon, TypeIcon, } from 'lucide-react';
7
+ import { ArrowRightLeftIcon, CalendarIcon, ExternalLinkIcon, HashIcon, LayersIcon, PencilIcon, PlusIcon, RefreshCwIcon, SparklesIcon, TableIcon, TextCursorInputIcon, Trash2Icon, TypeIcon, } from 'lucide-react';
8
8
  import { createContext, useContext, useEffect, useMemo, useState, } from 'react';
9
9
  import { apiJson } from './api-fetch.js';
10
10
  import { useAssistant, useAssistantName, useAssistantPending } from './assistant-context.js';
@@ -206,9 +206,48 @@ function WorkspaceBody({ api, w, tab, draft, }) {
206
206
  }).then(() => qc.invalidateQueries({ queryKey: WORKSPACES_KEY }));
207
207
  }, { id: `ws-save:${tab.id}`, duration: 4000 });
208
208
  };
209
- const intro = (_jsx(LumenTabs, { tabs: w.tabs.map((t) => ({ value: t.id, label: t.name, icon: tabIcon(t) })), value: tab?.id, onChange: (t) => void navigate({ href: workspacePath(w.id, t), replace: true }), actions: tab && _jsx(IconButton, { icon: PencilIcon, label: "Edit tab", onClick: () => setEditingTab(true) }) }));
209
+ // + in the tab bar: a tab from a recipe, run once now; ↻ in the header runs every tab again.
210
+ const addTab = useMutation({
211
+ mutationFn: (r) => json(`${api}/workspaces/${w.id}/tabs`, {
212
+ method: 'POST',
213
+ body: JSON.stringify({ name: r.label, recipe: r.recipe }),
214
+ }),
215
+ onSuccess: ({ workspace, tab: t }) => {
216
+ put(workspace);
217
+ void qc.invalidateQueries({ queryKey: [...WORKSPACES_KEY, api, 'list'] });
218
+ void navigate({ href: workspacePath(w.id, t), replace: true });
219
+ },
220
+ onError: (e) => lumenToast.error(`Couldn’t add the tab: ${e.message}`),
221
+ });
222
+ const runAll = useMutation({
223
+ mutationFn: () => json(`${api}/workspaces/${w.id}/refresh`, {
224
+ method: 'POST',
225
+ }),
226
+ onSuccess: (r) => {
227
+ void qc.invalidateQueries({ queryKey: WORKSPACES_KEY });
228
+ const ran = `Ran ${r.ran} ${r.ran === 1 ? 'tab' : 'tabs'}: ${r.rows.toLocaleString('en-US')} rows`;
229
+ if (r.failed.length)
230
+ lumenToast.warning(`${ran}. Failed: ${r.failed.join(', ')}`);
231
+ else
232
+ lumenToast.success(ran);
233
+ },
234
+ onError: (e) => lumenToast.error(`Couldn’t run the tabs: ${e.message}`),
235
+ });
236
+ const recipes = config.recipes ?? [];
237
+ const runnable = w.tabs.some((t) => 'frame' in t.source);
238
+ const intro = (_jsx(LumenTabs, { tabs: w.tabs.map((t) => ({ value: t.id, label: t.name, icon: tabIcon(t) })), value: tab?.id, onChange: (t) => void navigate({ href: workspacePath(w.id, t), replace: true }), actions: _jsxs(_Fragment, { children: [recipes.length > 0 && (_jsx(LumenSelector, { trigger: "icon", icon: PlusIcon, label: addTab.isPending ? 'Adding a tab…' : 'New tab from a recipe', align: "end", disabled: addTab.isPending, options: recipes.map((r) => ({
239
+ value: r.id,
240
+ label: r.label,
241
+ description: r.description,
242
+ group: r.group,
243
+ icon: r.icon,
244
+ })), value: null, onChange: (id) => {
245
+ const r = recipes.find((x) => x.id === id);
246
+ if (r)
247
+ addTab.mutate(r);
248
+ } })), tab && (_jsx(IconButton, { icon: PencilIcon, label: "Edit tab", onClick: () => setEditingTab(true) }))] }) }));
210
249
  const crumbs = [{ label: 'Workspaces' }, { label: w.name }];
211
- const actions = (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: TextCursorInputIcon, label: "Rename workspace", onClick: () => setNaming('workspace') }), moveTo.length > 0 && (_jsx(LumenSelector, { trigger: "icon", icon: ArrowRightLeftIcon, label: `Move to ${noun}`, align: "end", search: false, options: moveTo.map((a) => ({ value: a.id, label: a.label, icon: a.icon })), value: null, onChange: (a) => move.mutate(a) })), _jsx(IconButton, { icon: Trash2Icon, label: "Delete workspace", className: "text-muted-foreground hover:text-destructive", onClick: () => setRemoving('workspace') })] }));
250
+ const actions = (_jsxs(_Fragment, { children: [runnable && (_jsx(IconButton, { icon: RefreshCwIcon, label: runAll.isPending ? 'Running every tab…' : 'Run every tab again', disabled: runAll.isPending, className: runAll.isPending ? '[&_svg]:animate-spin' : undefined, onClick: () => runAll.mutate() })), _jsx(IconButton, { icon: TextCursorInputIcon, label: "Rename workspace", onClick: () => setNaming('workspace') }), moveTo.length > 0 && (_jsx(LumenSelector, { trigger: "icon", icon: ArrowRightLeftIcon, label: `Move to ${noun}`, align: "end", search: false, options: moveTo.map((a) => ({ value: a.id, label: a.label, icon: a.icon })), value: null, onChange: (a) => move.mutate(a) })), _jsx(IconButton, { icon: Trash2Icon, label: "Delete workspace", className: "text-muted-foreground hover:text-destructive", onClick: () => setRemoving('workspace') })] }));
212
251
  const dialogs = (_jsxs(_Fragment, { children: [_jsx(NameDialog, { open: naming !== null, title: "Rename workspace", value: w.name, onClose: () => setNaming(null), onSave: (name) => rename.mutate(name) }), tab && (_jsx(LumenTabDrawer, { open: editingTab, onClose: () => setEditingTab(false), name: tab.name, icon: tab.config?.icon, fallback: tabIcon({ ...tab, config: null }), description: "Its filters and columns save as you change them.", maxName: 60, busy: editTab.isPending, error: editTab.error?.message, onSave: ({ name, icon }) => editTab.mutate(icon === (tab.config?.icon ?? null)
213
252
  ? { name }
214
253
  : { name, config: { ...tabConfig(tab, w, config.defaultConfig), name, icon } }), remove: {
@@ -226,7 +265,9 @@ function WorkspaceBody({ api, w, tab, draft, }) {
226
265
  title: 'No tabs yet',
227
266
  icon: LayersIcon,
228
267
  kind: 'widget',
229
- children: (_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Ask ", assistant, " for data and to open it here: \u201C", config.example?.(here) ?? 'put this month’s numbers in', " ", w.name, "\u201D."] })),
268
+ children: (_jsx("p", { className: "text-sm text-muted-foreground", children: recipes.length > 0
269
+ ? 'Pick a recipe from + above: its rows open here as a tab, and run again whenever you tap ↻.'
270
+ : `Ask ${assistant} for data and to open it here: “${config.example?.(here) ?? 'put this month’s numbers in'} ${w.name}”.` })),
230
271
  },
231
272
  ] }));
232
273
  }
@@ -290,6 +331,7 @@ assistant = 'Assistant') {
290
331
  value: (r) => r.v[i] ?? null,
291
332
  format: c.format,
292
333
  amount: c.amount,
334
+ ...(c.tags ? { tags: true } : {}),
293
335
  filterable: true,
294
336
  groupable: c.type !== 'number',
295
337
  ...(c.type === 'number' ? { aggregate: c.format === 'percent' ? 'mean' : 'sum' } : {}),
@@ -438,7 +480,7 @@ function FrameTab({ api, tab, meta, crumbs, actions, intro, onChange, }) {
438
480
  day: 'numeric',
439
481
  hour: 'numeric',
440
482
  minute: '2-digit',
441
- }) }), failed ? (_jsx(StatusItem, { label: "Refresh failed", className: "min-w-0", value: _jsx("span", { className: "inline-block max-w-72 truncate align-bottom", title: failed, children: failed }), ok: false })) : null] }) }, `frame:${tab.id}`), assistantMade && (_jsx(FrameScriptDialog, { api: api, frame: current.id, name: current.name, open: scriptOpen, onOpenChange: setScriptOpen }))] }));
483
+ }) }), failed ? (_jsx(StatusItem, { label: "Refresh failed", className: "min-w-0", value: _jsx("span", { className: "inline-block max-w-72 truncate align-bottom", title: failed, children: failed }), ok: false })) : null] }) }, `frame:${tab.id}`), assistantMade && (_jsx(FrameScriptDialog, { api: api, frame: current.id, name: current.name, runs: current.runs, open: scriptOpen, onOpenChange: setScriptOpen }))] }));
442
484
  }
443
485
  /** A tab's icon: the person's pick, else the assistant's mark on a frame, else a table. */
444
486
  function tabIcon(t) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.189",
3
+ "version": "0.1.190",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.189",
52
+ "@columenlabs/lumen-ui-types": "0.1.190",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",