@columenlabs/lumen-ui 0.1.194 → 0.1.195

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/dist/joins.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type JoinKey, type SourceState } from '@columenlabs/lumen-ui-types/domains';
2
- import { type FrameChoice, type FrameColumn, type FrameData, type FrameEditInput, type FrameMeta, type YourColumn } from '@columenlabs/lumen-ui-types/frames';
2
+ import { type FrameChoice, type FrameColumn, type FrameData, type FrameEditInput, type FrameMeta, type FrameScript, type YourColumn } from '@columenlabs/lumen-ui-types/frames';
3
3
  import { type CustomViewConfig } from '@columenlabs/lumen-ui-types/views';
4
4
  import { type ReactNode } from 'react';
5
5
  import { type ConnectedDomain } from './connections.js';
@@ -88,6 +88,12 @@ export declare const frameGroupId: typeof recipeId;
88
88
  export declare function useDraftFrames<T>(domain: ViewDomain<T>, ids: readonly string[]): ViewDomain<T>;
89
89
  /** The person's linked frames (recipes) a card can attach, for Edit widget's + Data. */
90
90
  export declare function useFrameChoices(enabled: boolean): import("@tanstack/react-query").UseQueryResult<FrameChoice[], Error>;
91
+ /** Makes a frame from one of the product's recipes (+ Data's app recipes), or reuses the one that
92
+ * already runs it, and lists it with the person's frames. */
93
+ export declare function useMakeFrame(): import("@tanstack/react-query").UseMutationResult<FrameChoice, Error, {
94
+ name: string;
95
+ recipe: FrameScript;
96
+ }, unknown>;
91
97
  /** A row of plain cells, as a link to a catalog domain or a recipe reads it. */
92
98
  type CellRow = Record<string, ViewCellValue>;
93
99
  interface PlainColumn {
package/dist/joins.js CHANGED
@@ -375,6 +375,20 @@ export function useFrameChoices(enabled) {
375
375
  staleTime: 30_000,
376
376
  });
377
377
  }
378
+ /** Makes a frame from one of the product's recipes (+ Data's app recipes), or reuses the one that
379
+ * already runs it, and lists it with the person's frames. */
380
+ export function useMakeFrame() {
381
+ const api = useAssistant()?.api ?? null;
382
+ const qc = useQueryClient();
383
+ return useMutation({
384
+ mutationFn: (r) => json(`${api}/frames`, {
385
+ method: 'POST',
386
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
387
+ body: JSON.stringify(r),
388
+ }),
389
+ onSuccess: () => void qc.invalidateQueries({ queryKey: [...WORKSPACES_KEY, api, 'frames'] }),
390
+ });
391
+ }
378
392
  const titled = (k) => (k.charAt(0).toUpperCase() + k.slice(1)).replace(/_/g, ' ');
379
393
  /** Plain rows as a domain: a field per column, and the keys (symbol, account) a column carries. */
380
394
  export function rowsDomain(id, columns, keys) {
@@ -1,4 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { FRAME_LINK } from '@columenlabs/lumen-ui-types/catalog';
2
3
  import { OUTLOOK_INDEXES } from '@columenlabs/lumen-ui-types/market';
3
4
  // LumenSavedWidget (as blocks; renamed): a page (LumenLayout) is an ordered list of cards, each a third, half, two thirds or the full width.
4
5
  // A card is the view (LumenView) or a widget. Chart and top-N widgets read every row of the view
@@ -39,7 +40,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '.
39
40
  import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs.js';
40
41
  import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
41
42
  import { computedOf } from './view-model.js';
42
- import { PAGE_SOURCE, useWidgetDataOptions } from './widget-data.js';
43
+ import { MAKE_RECIPE, PAGE_SOURCE, useMakeRecipe, useWidgetDataOptions } from './widget-data.js';
43
44
  import { COMPACT, MINUS } from './widget-values.js';
44
45
  // Loaded when it first shows (lazy-part.tsx).
45
46
  const MarketWidget = lazyPart(() => import('./market/market-widget.js').then((m) => m.MarketWidget), _jsx(Skeleton, { className: "h-full min-h-40 w-full" }));
@@ -394,6 +395,14 @@ export function AddWidgetDialog({ open, onOpenChange, domain, cards, onAdd, onBu
394
395
  useEffect(() => setData(PAGE_SOURCE), [open]);
395
396
  const pageLabel = `${name} (this page)`;
396
397
  const dataOptions = useWidgetDataOptions(open, pageLabel);
398
+ // A product recipe not made yet pulls its rows once, then is the widget's data.
399
+ const maker = useMakeRecipe();
400
+ const pickData = (v) => v.startsWith(MAKE_RECIPE)
401
+ ? void maker.run(v).then((f) => {
402
+ if (f)
403
+ setData(`${FRAME_LINK}${f.id}`);
404
+ })
405
+ : setData(v);
397
406
  const own = data !== PAGE_SOURCE;
398
407
  const ids = useMemo(() => (own ? [data] : []), [own, data]);
399
408
  const read = useDomainReads(ids, ids)[data];
@@ -404,7 +413,7 @@ export function AddWidgetDialog({ open, onOpenChange, domain, cards, onAdd, onBu
404
413
  const hasRow = !own && cards.some(isRowCard);
405
414
  const full = cards.length >= 12;
406
415
  const offered = useOffered(shapeDomain);
407
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Add widget" }), _jsxs(DialogDescription, { children: ["Pick its data, then its shape", full ? '. A page holds 12 cards at most.' : '.'] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "add-widget-data", className: "text-xs font-medium text-muted-foreground", children: "Data" }), _jsx(LumenSelector, { id: "add-widget-data", icon: DatabaseIcon, label: "Widget data", search: true, value: data, options: dataOptions, onChange: setData }), own && read?.state !== 'ok' && (_jsx("p", { className: cn('text-xs', read?.state === 'loading' || !read ? 'text-muted-foreground' : 'text-destructive'), children: read?.state === 'loading' || !read
416
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Add widget" }), _jsxs(DialogDescription, { children: ["Pick its data, then its shape", full ? '. A page holds 12 cards at most.' : '.'] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "add-widget-data", className: "text-xs font-medium text-muted-foreground", children: "Data" }), _jsx(LumenSelector, { id: "add-widget-data", icon: DatabaseIcon, label: "Widget data", search: true, value: data, options: dataOptions, onChange: pickData }), own && read?.state !== 'ok' && (_jsx("p", { className: cn('text-xs', read?.state === 'loading' || !read ? 'text-muted-foreground' : 'text-destructive'), children: read?.state === 'loading' || !read
408
417
  ? 'Reading its fields…'
409
418
  : (read.detail ?? 'Its rows could not be read.') }))] }), _jsxs(Tabs, { defaultValue: catalog.length ? 'prebuilt' : 'own', children: [_jsxs(TabsList, { children: [!own && (_jsxs(TabsTrigger, { value: "prebuilt", children: [_jsx(PackageIcon, {}), "Pre-built"] })), _jsxs(TabsTrigger, { value: "own", children: [_jsx(WrenchIcon, {}), "Build your own"] })] }), _jsx(TabsContent, { value: "prebuilt", className: dialogBody, children: catalog.length ? (_jsx("div", { className: "grid grid-cols-1 gap-2 sm:grid-cols-2", children: catalog.map((it) => {
410
419
  const added = cards.some((b) => b.id === it.widget.id);
@@ -46,7 +46,7 @@ import { AGG_LABELS, FORMAT_LABELS, FormatChip, FormulaForm, MAX_FORMULAS, field
46
46
  import { shapeIcon } from './view-icons.js';
47
47
  import { FORMULAS_GROUP, canFilter, chipsOf, formulaBlanks, formulasOf, groupOf, hiddenOf, subgroupLabel, subgroupOf, subgroupsOf, summaryCash, takesFormulas, tilesOf, totalAggOf, withViewFields, } from './view-model.js';
48
48
  import { WidgetControlsEditor } from './widget-controls.js';
49
- import { PAGE_SOURCE, useWidgetDataOptions } from './widget-data.js';
49
+ import { MAKE_RECIPE, PAGE_SOURCE, recipeOptions, useMakeRecipe, useWidgetDataOptions, } from './widget-data.js';
50
50
  import { EditorRow as Row, WidgetFields, WidgetSettings, hasWidgetSettings, readsRows, slotKeys, widgetSlots, } from './widget-settings.js';
51
51
  import { widgetThumb } from './widget-thumbs.js';
52
52
  /** The field's own group and Total rows: a formula over the rolled-up inputs, or a share. */
@@ -246,6 +246,13 @@ export function ViewEditDialog({ open, onOpenChange, domain: pageDomain, config,
246
246
  const byKey = (f) => !!f.key && !!base.keys?.[f.key];
247
247
  const recipes = (choices.data ?? []).filter((f) => !frameIds.includes(f.id) && !connections.some((c) => c.to === `${FRAME_LINK}${f.id}`));
248
248
  const addRecipe = (f) => (byKey(f) ? addFrame(f) : link(`${FRAME_LINK}${f.id}`, []));
249
+ // The product's recipes not made yet, by app: picking one pulls its rows once, then links them.
250
+ const maker = useMakeRecipe();
251
+ const templates = recipeOptions(maker.recipes, choices.data ?? []);
252
+ const makeRecipe = (v) => void maker.run(v).then((f) => {
253
+ if (f)
254
+ addRecipe(f);
255
+ });
249
256
  const addFrame = (f) => setDraft({
250
257
  ...draft,
251
258
  frames: [...frameIds, f.id],
@@ -625,7 +632,12 @@ export function ViewEditDialog({ open, onOpenChange, domain: pageDomain, config,
625
632
  label: mainLabel,
626
633
  empty: source === '',
627
634
  options: mainOptions,
628
- onPick: pickMain,
635
+ onPick: (to) => to.startsWith(MAKE_RECIPE)
636
+ ? void maker.run(to).then((f) => {
637
+ if (f)
638
+ pickMain(`${FRAME_LINK}${f.id}`);
639
+ })
640
+ : pickMain(to),
629
641
  onDrop: dropMain,
630
642
  onClear: clearAll,
631
643
  onMakeMain: makeMain,
@@ -635,7 +647,7 @@ export function ViewEditDialog({ open, onOpenChange, domain: pageDomain, config,
635
647
  fieldsOf: (to) => allRead[to]?.domain.fields ?? [],
636
648
  onOf: (to) => connections.find((c) => c.to === to)?.on ?? [],
637
649
  set: setPairs,
638
- }, 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')
650
+ }, deskLabel: app?.label ?? null, recipes: recipes, recipeNote: choices.isError ? `Recipes did not load: ${choices.error.message}` : null, onAddRecipe: addRecipe, templates: templates, onMakeRecipe: makeRecipe, making: maker.pending, 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')
639
651
  ? 'This chart reads its symbols, not fields.'
640
652
  : 'This widget takes no fields.' }));
641
653
  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
@@ -718,7 +730,7 @@ function LinkPairs({ label, on, cardFields, fields, onChange, }) {
718
730
  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'));
719
731
  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"] }))] }));
720
732
  }
721
- function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEditFormula, onRemoveFormula, onAddFormula, deskDomains = [], onLink, onUnlink, deskLabel, recipes = [], recipeNote, onAddRecipe, recipeByKey = () => false, onRemoveRecipe, onNewRecipe, linkPairs, main, }) {
733
+ function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEditFormula, onRemoveFormula, onAddFormula, deskDomains = [], onLink, onUnlink, deskLabel, recipes = [], recipeNote, onAddRecipe, templates = [], onMakeRecipe, making = false, recipeByKey = () => false, onRemoveRecipe, onNewRecipe, linkPairs, main, }) {
722
734
  const assistantName = useAssistantName();
723
735
  const [query, setQuery] = useState('');
724
736
  // New recipe's box, open while not null.
@@ -753,6 +765,9 @@ function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEdit
753
765
  : {}),
754
766
  }))
755
767
  : []),
768
+ ...(onMakeRecipe
769
+ ? templates.map((o) => (making ? { ...o, disabled: 'Pulling rows…' } : o))
770
+ : []),
756
771
  ...(onNewRecipe
757
772
  ? [
758
773
  {
@@ -768,6 +783,8 @@ function FieldTree({ domain, draft, picked, onPick, why, formulaProblems, onEdit
768
783
  const addData = (v) => {
769
784
  if (v === 'new')
770
785
  setWish('');
786
+ else if (v.startsWith(MAKE_RECIPE))
787
+ onMakeRecipe?.(v);
771
788
  else if (v.startsWith('link:')) {
772
789
  const d = deskDomains.find((x) => `link:${x.id}` === v);
773
790
  if (d && !d.why)
@@ -1,4 +1,19 @@
1
+ import type { FrameChoice } from '@columenlabs/lumen-ui-types/frames';
1
2
  import type { LumenOption } from './lumen-selector.js';
3
+ import { type WorkspaceRecipe } from './workspaces-config.js';
2
4
  /** The page's own rows, as a data option's value. */
3
5
  export declare const PAGE_SOURCE = "_page";
6
+ /** A product recipe not made yet, as a data option's value: picking it runs it once. */
7
+ export declare const MAKE_RECIPE = "make:";
8
+ /**
9
+ * The product's recipes (WorkspacesProvider `recipes`) the person has no frame of yet, as options
10
+ * grouped by the app they read. A recipe already made shows once, as the person's recipe.
11
+ */
12
+ export declare function recipeOptions(recipes: readonly WorkspaceRecipe[], made: readonly FrameChoice[]): LumenOption[];
13
+ /** Turns a picked product recipe into its frame (made once, then reused). */
14
+ export declare function useMakeRecipe(): {
15
+ run: (value: string) => Promise<FrameChoice | null>;
16
+ pending: boolean;
17
+ recipes: WorkspaceRecipe[];
18
+ };
4
19
  export declare function useWidgetDataOptions(enabled: boolean, pageLabel: string): LumenOption[];
@@ -1,14 +1,55 @@
1
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.
2
+ // reader, the person's recipes, and the product's recipes not yet made (by app). Add widget's
3
+ // first step and Edit widget ▸ Data both pick from it.
3
4
  import { CATALOG, FRAME_LINK, catalogKeys, readableDomain, } from '@columenlabs/lumen-ui-types/catalog';
4
5
  import { DatabaseIcon, SparklesIcon, TableIcon } from 'lucide-react';
5
- import { asOf, useFrameChoices } from './joins.js';
6
+ import { asOf, useFrameChoices, useMakeFrame } from './joins.js';
7
+ import { lumenToast } from './lumen-toast.js';
6
8
  import { useApp } from './shell.js';
9
+ import { useWorkspacesConfig } from './workspaces-config.js';
7
10
  /** The page's own rows, as a data option's value. */
8
11
  export const PAGE_SOURCE = '_page';
12
+ /** A product recipe not made yet, as a data option's value: picking it runs it once. */
13
+ export const MAKE_RECIPE = 'make:';
14
+ /**
15
+ * The product's recipes (WorkspacesProvider `recipes`) the person has no frame of yet, as options
16
+ * grouped by the app they read. A recipe already made shows once, as the person's recipe.
17
+ */
18
+ export function recipeOptions(recipes, made) {
19
+ const names = new Set(made.map((f) => f.name));
20
+ return recipes
21
+ .filter((r) => !names.has(r.label))
22
+ .map((r) => ({
23
+ value: `${MAKE_RECIPE}${r.id}`,
24
+ label: r.label,
25
+ group: r.group ?? 'Recipes',
26
+ ...(r.description ? { detail: r.description } : {}),
27
+ icon: r.icon ?? SparklesIcon,
28
+ keywords: [r.group ?? '', r.description ?? ''],
29
+ }));
30
+ }
31
+ /** Turns a picked product recipe into its frame (made once, then reused). */
32
+ export function useMakeRecipe() {
33
+ const { recipes = [] } = useWorkspacesConfig();
34
+ const make = useMakeFrame();
35
+ const run = async (value) => {
36
+ const r = recipes.find((x) => `${MAKE_RECIPE}${x.id}` === value);
37
+ if (!r)
38
+ return null;
39
+ try {
40
+ return await make.mutateAsync({ name: r.label, recipe: r.recipe });
41
+ }
42
+ catch (e) {
43
+ lumenToast.error(`Couldn’t pull ${r.label}: ${e.message}`);
44
+ return null;
45
+ }
46
+ };
47
+ return { run, pending: make.isPending, recipes };
48
+ }
9
49
  export function useWidgetDataOptions(enabled, pageLabel) {
10
50
  const app = useApp();
11
51
  const choices = useFrameChoices(enabled);
52
+ const { recipes = [] } = useWorkspacesConfig();
12
53
  return [
13
54
  { value: PAGE_SOURCE, label: pageLabel, group: 'This page', icon: TableIcon },
14
55
  ...Object.entries(CATALOG)
@@ -28,5 +69,6 @@ export function useWidgetDataOptions(enabled, pageLabel) {
28
69
  detail: asOf(f.at),
29
70
  icon: SparklesIcon,
30
71
  })),
72
+ ...recipeOptions(recipes, choices.data ?? []),
31
73
  ];
32
74
  }
@@ -0,0 +1,46 @@
1
+ import type { FrameScript } from '@columenlabs/lumen-ui-types/frames';
2
+ import type { CustomViewConfig } from '@columenlabs/lumen-ui-types/views';
3
+ import { type ComponentType } from 'react';
4
+ type Icon = ComponentType<{
5
+ className?: string;
6
+ }>;
7
+ /** An app a workspace can belong to (the product's apps). */
8
+ export interface WorkspaceApp {
9
+ id: string;
10
+ label: string;
11
+ /** Its address; a workspace there is at `${href}workspaces/<id>`. */
12
+ href: string;
13
+ icon?: Icon;
14
+ }
15
+ /** What the product tells its workspaces; every field may be left out by a one-app product. */
16
+ export interface WorkspacesConfig {
17
+ /** The apps a workspace may move to that this person can open, this one included. */
18
+ apps?: WorkspaceApp[];
19
+ /** What the product calls an app: e.g. `desk`. */
20
+ appNoun?: string;
21
+ /** Which app draws a tab over a domain; one it does not say is this app's. */
22
+ domainApp?: (domain: string) => string | undefined;
23
+ /** A domain tab's starting layout. */
24
+ defaultConfig?: (domain: string) => CustomViewConfig | null;
25
+ /** The empty workspace's example ask, on an app: `put my holdings with their earnings dates in`. */
26
+ example?: (app: string) => string;
27
+ /** Recipes a person picks to start a tab by hand (+ in the tab bar); none hides the +. */
28
+ recipes?: WorkspaceRecipe[];
29
+ }
30
+ /** A saved data pull a person can start a tab from: one call to one of the product's read tools. */
31
+ export interface WorkspaceRecipe {
32
+ id: string;
33
+ /** The tab's name and the picker's row: `Jobs scoring 70+`. */
34
+ label: string;
35
+ /** When to pick it, under the label. */
36
+ description?: string;
37
+ /** The picker's section: the app the rows come from. */
38
+ group?: string;
39
+ icon?: Icon;
40
+ recipe: FrameScript;
41
+ }
42
+ /** The product's apps and domains for its workspaces. */
43
+ export declare const WorkspacesProvider: import("react").Provider<WorkspacesConfig>;
44
+ /** The product's workspace config; empty outside a WorkspacesProvider. */
45
+ export declare const useWorkspacesConfig: () => WorkspacesConfig;
46
+ export {};
@@ -0,0 +1,6 @@
1
+ import { createContext, useContext } from 'react';
2
+ const ConfigCtx = createContext({});
3
+ /** The product's apps and domains for its workspaces. */
4
+ export const WorkspacesProvider = ConfigCtx.Provider;
5
+ /** The product's workspace config; empty outside a WorkspacesProvider. */
6
+ export const useWorkspacesConfig = () => useContext(ConfigCtx);
@@ -1,47 +1,8 @@
1
- import { type FrameScript } from '@columenlabs/lumen-ui-types/frames';
2
1
  import { type CustomViewConfig } from '@columenlabs/lumen-ui-types/views';
3
- import { type ComponentType, type ReactNode } from 'react';
2
+ import { type ReactNode } from 'react';
4
3
  import { type Crumb } from './page.js';
5
- type Icon = ComponentType<{
6
- className?: string;
7
- }>;
8
- /** An app a workspace can belong to (the product's apps). */
9
- export interface WorkspaceApp {
10
- id: string;
11
- label: string;
12
- /** Its address; a workspace there is at `${href}workspaces/<id>`. */
13
- href: string;
14
- icon?: Icon;
15
- }
16
- /** What the product tells its workspaces; every field may be left out by a one-app product. */
17
- export interface WorkspacesConfig {
18
- /** The apps a workspace may move to that this person can open, this one included. */
19
- apps?: WorkspaceApp[];
20
- /** What the product calls an app: e.g. `desk`. */
21
- appNoun?: string;
22
- /** Which app draws a tab over a domain; one it does not say is this app's. */
23
- domainApp?: (domain: string) => string | undefined;
24
- /** A domain tab's starting layout. */
25
- defaultConfig?: (domain: string) => CustomViewConfig | null;
26
- /** The empty workspace's example ask, on an app: `put my holdings with their earnings dates in`. */
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;
42
- }
43
- /** The product's apps and domains for its workspaces. */
44
- export declare const WorkspacesProvider: import("react").Provider<WorkspacesConfig>;
4
+ export { WorkspacesProvider } from './workspaces-config.js';
5
+ export type { WorkspaceApp, WorkspaceRecipe, WorkspacesConfig } from './workspaces-config.js';
45
6
  /**
46
7
  * Workspaces in the sidebar: one item per workspace, newest first; `+` starts an empty one, the
47
8
  * trash icon deletes one (its frames stay until no tab shows them). Hidden where the app has no
@@ -69,4 +30,3 @@ export interface WorkspaceDraftProps {
69
30
  export declare function WorkspacePage({ draft }: {
70
31
  draft?: (p: WorkspaceDraftProps) => ReactNode;
71
32
  }): import("react").JSX.Element;
72
- export {};
@@ -5,7 +5,7 @@ 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
7
  import { ArrowRightLeftIcon, CalendarIcon, ExternalLinkIcon, HashIcon, LayersIcon, PencilIcon, PlusIcon, RefreshCwIcon, SparklesIcon, TableIcon, TextCursorInputIcon, Trash2Icon, TypeIcon, } from 'lucide-react';
8
- import { createContext, useContext, useEffect, useMemo, useState, } from 'react';
8
+ import { useEffect, useMemo, useState } from 'react';
9
9
  import { apiJson } from './api-fetch.js';
10
10
  import { useAssistant, useAssistantName, useAssistantPending } from './assistant-context.js';
11
11
  import { useAssistantWorkspace } from './assistant-screen.js';
@@ -38,9 +38,8 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
38
38
  import { Input } from './ui/input.js';
39
39
  import { iconOf } from './view-icons.js';
40
40
  import { WORKSPACES_KEY, frameKey, workspacesListQuery } from './workspace-keys.js';
41
- const ConfigCtx = createContext({});
42
- /** The product's apps and domains for its workspaces. */
43
- export const WorkspacesProvider = ConfigCtx.Provider;
41
+ import { useWorkspacesConfig, } from './workspaces-config.js';
42
+ export { WorkspacesProvider } from './workspaces-config.js';
44
43
  const json = (url, init) => apiJson(url, init, 'workspaces');
45
44
  const workspaceQuery = (api, id) => ({
46
45
  queryKey: [...WORKSPACES_KEY, api, 'ws', id],
@@ -146,7 +145,7 @@ function WorkspaceBody({ api, w, tab, draft, }) {
146
145
  });
147
146
  // Move to app: the workspace leaves this app's sidebar for another's.
148
147
  const here = useShell().app;
149
- const config = useContext(ConfigCtx);
148
+ const config = useWorkspacesConfig();
150
149
  const assistant = useAssistantName();
151
150
  const noun = config.appNoun ?? 'app';
152
151
  const appOf = (id) => config.apps?.find((a) => a.id === id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.194",
3
+ "version": "0.1.195",
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.194",
52
+ "@columenlabs/lumen-ui-types": "0.1.195",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",