@jxsuite/studio 0.37.1 → 1.1.0
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/iframe-entry.js +622 -49
- package/dist/iframe-entry.js.map +15 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +103709 -81001
- package/dist/studio.js.map +201 -157
- package/package.json +47 -45
- package/src/account-status.ts +39 -0
- package/src/browse/browse.ts +10 -14
- package/src/canvas/canvas-live-render.ts +21 -2
- package/src/canvas/canvas-render.ts +35 -16
- package/src/canvas/canvas-utils.ts +118 -72
- package/src/canvas/iframe-entry.ts +20 -0
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +132 -6
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +43 -3
- package/src/canvas/iframe-render.ts +18 -0
- package/src/collab/collab-session.ts +23 -8
- package/src/collab/collab-state.ts +6 -3
- package/src/component-props.ts +104 -0
- package/src/editor/context-menu.ts +1 -1
- package/src/editor/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/repeater-scope.ts +8 -13
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +56 -1
- package/src/format/format-host.ts +63 -1
- package/src/grid/cell-editors.ts +288 -0
- package/src/grid/cell-popovers.ts +108 -0
- package/src/grid/csv-codec.ts +122 -0
- package/src/grid/edit-buffer.ts +490 -0
- package/src/grid/grid-controller.ts +417 -0
- package/src/grid/grid-layout.ts +83 -0
- package/src/grid/grid-open.ts +167 -0
- package/src/grid/grid-panel.ts +302 -0
- package/src/grid/grid-source.ts +210 -0
- package/src/grid/grid-view.ts +367 -0
- package/src/grid/schema-columns.ts +261 -0
- package/src/grid/sources/connector-source.ts +217 -0
- package/src/grid/sources/content-source.ts +542 -0
- package/src/grid/sources/csv-file-source.ts +247 -0
- package/src/grid/tabulator-tables.d.ts +120 -0
- package/src/new-project/add-repo-modal.ts +183 -0
- package/src/new-project/new-project-modal.ts +22 -3
- package/src/page-params.ts +34 -8
- package/src/panels/ai-chat/chat-markdown.ts +2 -2
- package/src/panels/ai-panel.ts +61 -4
- package/src/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +241 -0
- package/src/panels/editors.ts +43 -17
- package/src/panels/events-panel.ts +137 -44
- package/src/panels/formula-workspace.ts +379 -0
- package/src/panels/frontmatter-fields.ts +231 -0
- package/src/panels/frontmatter-panel.ts +132 -0
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/head-panel.ts +11 -175
- package/src/panels/properties-panel.ts +154 -144
- package/src/panels/right-panel.ts +9 -47
- package/src/panels/signals-panel.ts +217 -460
- package/src/panels/statement-editor.ts +710 -0
- package/src/panels/statusbar.ts +1 -1
- package/src/panels/style-panel.ts +25 -1
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +175 -6
- package/src/panels/tab-strip.ts +62 -6
- package/src/panels/toolbar.ts +37 -3
- package/src/panels/welcome-screen.ts +50 -0
- package/src/platform-errors.ts +30 -0
- package/src/platforms/cloud.ts +172 -89
- package/src/platforms/devserver.ts +172 -0
- package/src/services/ai-project-tools.ts +541 -0
- package/src/services/ai-session-store.ts +28 -0
- package/src/services/ai-system-prompt.ts +194 -24
- package/src/services/ai-tools.ts +88 -21
- package/src/services/automation.ts +16 -0
- package/src/services/context-resolver.ts +73 -0
- package/src/services/data-service.ts +155 -0
- package/src/services/document-assistant.ts +81 -11
- package/src/services/gated-registry.ts +72 -0
- package/src/services/live-preview.ts +0 -0
- package/src/services/monaco-setup.ts +75 -26
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/settings/contributed-section.ts +406 -0
- package/src/settings/extension-sections.ts +145 -0
- package/src/settings/schema-field-ui.ts +4 -2
- package/src/settings/settings-modal.ts +101 -42
- package/src/site-context.ts +12 -1
- package/src/store.ts +4 -0
- package/src/studio.ts +107 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +39 -1
- package/src/tabs/transact.ts +61 -14
- package/src/types.ts +132 -1
- package/src/ui/dynamic-slot.ts +272 -0
- package/src/ui/expression-editor.ts +423 -125
- package/src/ui/field-row.ts +5 -0
- package/src/ui/form-controls.ts +322 -0
- package/src/ui/formula-catalog.ts +557 -0
- package/src/ui/formula-chips.ts +216 -0
- package/src/ui/formula-palette.ts +211 -0
- package/src/ui/layers.ts +40 -0
- package/src/ui/media-picker.ts +1 -1
- package/src/ui/panel-resize.ts +15 -1
- package/src/ui/progress-modal.ts +2 -2
- package/src/ui/schema-form.ts +524 -0
- package/src/utils/preview-format.ts +26 -0
- package/src/utils/studio-utils.ts +4 -3
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
- package/src/settings/content-types-editor.ts +0 -599
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import { getNodeAtPath } from "../store";
|
|
2
|
+
import { getNodeAtPath, renderOnly } from "../store";
|
|
3
3
|
import { html, nothing } from "lit-html";
|
|
4
4
|
import { live } from "lit-html/directives/live.js";
|
|
5
5
|
import { activeTab } from "../workspace/workspace";
|
|
6
|
-
import { mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
6
|
+
import { mutateAddDef, mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
7
7
|
import { renderExpressionEditor } from "../ui/expression-editor";
|
|
8
|
+
import { renderStatementEditor } from "./statement-editor";
|
|
9
|
+
import { livePreviewExpression } from "../services/live-preview";
|
|
8
10
|
import {
|
|
9
11
|
getEventBinding,
|
|
10
12
|
isExpressionDef,
|
|
@@ -171,53 +173,144 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
171
173
|
</div>
|
|
172
174
|
${inlineFn
|
|
173
175
|
? html`
|
|
174
|
-
<div class="event-body-
|
|
175
|
-
<sp-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
176
|
+
<div class="event-body-mode" style="display:flex;justify-content:flex-end">
|
|
177
|
+
<sp-action-group size="s" compact class="body-mode-toggle">
|
|
178
|
+
<sp-action-button
|
|
179
|
+
size="s"
|
|
180
|
+
class="body-mode-statements"
|
|
181
|
+
?selected=${Array.isArray(inlineFn.body)}
|
|
182
|
+
@click=${() => {
|
|
183
|
+
if (!Array.isArray(inlineFn.body)) {
|
|
184
|
+
transactDoc(activeTab.value, (t) =>
|
|
185
|
+
mutateUpdateProperty(t, selection, evKey, {
|
|
186
|
+
...inlineFn,
|
|
187
|
+
body: [],
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
Statements
|
|
194
|
+
</sp-action-button>
|
|
195
|
+
<sp-action-button
|
|
196
|
+
size="s"
|
|
197
|
+
class="body-mode-code"
|
|
198
|
+
?selected=${!Array.isArray(inlineFn.body)}
|
|
199
|
+
@click=${() => {
|
|
200
|
+
if (Array.isArray(inlineFn.body)) {
|
|
201
|
+
transactDoc(activeTab.value, (t) =>
|
|
202
|
+
mutateUpdateProperty(t, selection, evKey, {
|
|
203
|
+
...inlineFn,
|
|
204
|
+
body: "",
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
Code
|
|
211
|
+
</sp-action-button>
|
|
212
|
+
</sp-action-group>
|
|
206
213
|
</div>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
${renderExpressionEditor(
|
|
212
|
-
expression,
|
|
213
|
-
(newNode: unknown) =>
|
|
214
|
+
${Array.isArray(inlineFn.body)
|
|
215
|
+
? renderStatementEditor(
|
|
216
|
+
inlineFn.body,
|
|
217
|
+
(next) =>
|
|
214
218
|
transactDoc(activeTab.value, (t) =>
|
|
215
219
|
mutateUpdateProperty(t, selection, evKey, {
|
|
216
|
-
|
|
220
|
+
...inlineFn,
|
|
221
|
+
body: next,
|
|
217
222
|
}),
|
|
218
223
|
),
|
|
219
|
-
{
|
|
220
|
-
|
|
224
|
+
{
|
|
225
|
+
allowEventRef: true,
|
|
226
|
+
emits: inlineFn.emits ?? [],
|
|
227
|
+
stateDefs: Object.keys(defs),
|
|
228
|
+
stateEntries: defs,
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
: html`
|
|
232
|
+
<div class="event-body-row">
|
|
233
|
+
<sp-textfield
|
|
234
|
+
size="s"
|
|
235
|
+
multiline
|
|
236
|
+
grows
|
|
237
|
+
placeholder="// handler body"
|
|
238
|
+
.value=${live(typeof inlineFn.body === "string" ? inlineFn.body : "")}
|
|
239
|
+
@input=${(e: Event) => {
|
|
240
|
+
transactDoc(activeTab.value, (t) =>
|
|
241
|
+
mutateUpdateProperty(t, selection, evKey, {
|
|
242
|
+
$prototype: "Function",
|
|
243
|
+
body: (e.target as HTMLInputElement).value,
|
|
244
|
+
parameters: inlineFn?.parameters || [],
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
</sp-textfield>
|
|
250
|
+
<sp-action-button
|
|
251
|
+
size="xs"
|
|
252
|
+
quiet
|
|
253
|
+
title="Open in editor"
|
|
254
|
+
@click=${() => {
|
|
255
|
+
tab.session.ui.editingFunction = {
|
|
256
|
+
eventKey: evKey,
|
|
257
|
+
path: selection,
|
|
258
|
+
type: "event",
|
|
259
|
+
};
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
<sp-icon-code slot="icon"></sp-icon-code>
|
|
263
|
+
</sp-action-button>
|
|
264
|
+
</div>
|
|
265
|
+
`}
|
|
266
|
+
`
|
|
267
|
+
: expression
|
|
268
|
+
? html`
|
|
269
|
+
<div class="event-body-row">
|
|
270
|
+
<div style="flex:1;min-width:0">
|
|
271
|
+
${renderExpressionEditor(
|
|
272
|
+
expression,
|
|
273
|
+
(newNode: unknown) =>
|
|
274
|
+
transactDoc(activeTab.value, (t) =>
|
|
275
|
+
mutateUpdateProperty(t, selection, evKey, {
|
|
276
|
+
$expression: newNode,
|
|
277
|
+
}),
|
|
278
|
+
),
|
|
279
|
+
{
|
|
280
|
+
allowEventRef: true,
|
|
281
|
+
// Live-context evaluation in the canvas iframe, snapshot fallback
|
|
282
|
+
// (M6). The selection path is the context, so a binding inside a
|
|
283
|
+
// Repeater template previews with the first item's $map scope.
|
|
284
|
+
preview: livePreviewExpression(
|
|
285
|
+
tab,
|
|
286
|
+
`event:${JSON.stringify(selection)}:${evKey}`,
|
|
287
|
+
expression,
|
|
288
|
+
selection,
|
|
289
|
+
() => renderOnly("rightPanel"),
|
|
290
|
+
),
|
|
291
|
+
onInsertDef: (defName, def) =>
|
|
292
|
+
transactDoc(activeTab.value, (t) =>
|
|
293
|
+
mutateAddDef(t, defName, def as Record<string, JsonValue>),
|
|
294
|
+
),
|
|
295
|
+
stateDefs: Object.keys(defs),
|
|
296
|
+
stateEntries: defs,
|
|
297
|
+
},
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
<sp-action-button
|
|
301
|
+
size="xs"
|
|
302
|
+
quiet
|
|
303
|
+
title="Open in formula workspace"
|
|
304
|
+
@click=${() => {
|
|
305
|
+
tab.session.ui.editingFormula = {
|
|
306
|
+
eventKey: evKey,
|
|
307
|
+
path: selection,
|
|
308
|
+
type: "event",
|
|
309
|
+
};
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
<sp-icon-full-screen slot="icon"></sp-icon-full-screen>
|
|
313
|
+
</sp-action-button>
|
|
221
314
|
</div>
|
|
222
315
|
`
|
|
223
316
|
: html`
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Formula workspace — the full-screen structured editing surface for a single `$expression` (spec
|
|
4
|
+
* §19.9, milestone M4). Takes over the canvas area exactly like the Monaco function editor
|
|
5
|
+
* (renderFunctionEditor): `TabUi.editingFormula` identifies the document position being edited (a
|
|
6
|
+
* state entry's `$expression` via defName, or an element event binding's via path + eventKey).
|
|
7
|
+
*
|
|
8
|
+
* Layout: the chip pipeline summarizes the whole tree with live value badges on top; the recursive
|
|
9
|
+
* expression form edits the chip-selected sub-node in the main pane; a data rail mirrors the canvas
|
|
10
|
+
* dataScope snapshot on the right; the footer reports the root result (or the evaluation error).
|
|
11
|
+
*
|
|
12
|
+
* Every edit immutably replaces the selected sub-node within the root and writes the WHOLE root
|
|
13
|
+
* node back through transactDoc — undo/redo and canvas/panel refresh come free.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { html, render as litRender } from "lit-html";
|
|
17
|
+
import { getEventBinding, isExpressionDef, isJsonObject } from "@jxsuite/schema/guards";
|
|
18
|
+
|
|
19
|
+
import { canvasPanels, canvasWrap, getNodeAtPath, updateUi } from "../store";
|
|
20
|
+
import { activeTab } from "../workspace/workspace";
|
|
21
|
+
import { mutateAddDef, mutateUpdateDef, mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
22
|
+
import { view } from "../view";
|
|
23
|
+
import { chipSummary, renderFormulaChips } from "../ui/formula-chips";
|
|
24
|
+
import { renderExpressionEditor } from "../ui/expression-editor";
|
|
25
|
+
import { applyCatalogPick, formulaCatalog } from "../ui/formula-catalog";
|
|
26
|
+
import { openFormulaPalette } from "../ui/formula-palette";
|
|
27
|
+
import { livePreviewExpression } from "../services/live-preview";
|
|
28
|
+
import { dataTypeLabel, renderDataTreeTemplate, unwrapSignal } from "./data-explorer";
|
|
29
|
+
|
|
30
|
+
import type { JxNodeValue } from "../tabs/transact";
|
|
31
|
+
import type { FormulaEditDef, JsonValue } from "../types";
|
|
32
|
+
import type { Tab } from "../tabs/tab";
|
|
33
|
+
import type { JxStateDefinition } from "@jxsuite/schema/types";
|
|
34
|
+
import type { TemplateResult } from "lit-html";
|
|
35
|
+
|
|
36
|
+
type NodePath = (string | number)[];
|
|
37
|
+
|
|
38
|
+
// ─── Module state ────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/** Chip-selected sub-node path within the root expression (root = []). */
|
|
41
|
+
let _selectedPath: NodePath = [];
|
|
42
|
+
|
|
43
|
+
/** Serialized editing target — the selection resets when the workspace retargets. */
|
|
44
|
+
let _targetKey: string | null = null;
|
|
45
|
+
|
|
46
|
+
/** Owning tab of the current target — a same-looking target in another tab is a retarget. */
|
|
47
|
+
let _targetTab: Tab | null = null;
|
|
48
|
+
|
|
49
|
+
function isExprNode(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return isJsonObject(value) && typeof value.operator === "string";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Document access ─────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/** Read the workspace target's current root expression node from the document. */
|
|
56
|
+
export function formulaRoot(tab: Tab, editing: FormulaEditDef): Record<string, unknown> | null {
|
|
57
|
+
const { document } = tab.doc;
|
|
58
|
+
if (editing.type === "def" && editing.defName) {
|
|
59
|
+
const def = document?.state?.[editing.defName];
|
|
60
|
+
return isExpressionDef(def) ? (def.$expression as unknown as Record<string, unknown>) : null;
|
|
61
|
+
}
|
|
62
|
+
if (editing.type === "event" && editing.path && editing.eventKey) {
|
|
63
|
+
const node = getNodeAtPath(document, editing.path);
|
|
64
|
+
const binding = node ? getEventBinding(node, editing.eventKey) : undefined;
|
|
65
|
+
return isExpressionDef(binding)
|
|
66
|
+
? (binding.$expression as unknown as Record<string, unknown>)
|
|
67
|
+
: null;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a chip selection to its editable node: the deepest expression-node prefix of `path` (head
|
|
74
|
+
* chips target ref/literal operands; stale paths fall back toward the root).
|
|
75
|
+
*/
|
|
76
|
+
function resolveSelection(
|
|
77
|
+
root: Record<string, unknown>,
|
|
78
|
+
path: NodePath,
|
|
79
|
+
): { node: Record<string, unknown>; path: NodePath } {
|
|
80
|
+
let current: unknown = root;
|
|
81
|
+
let node: Record<string, unknown> = root;
|
|
82
|
+
let nodePath: NodePath = [];
|
|
83
|
+
for (const [i, seg] of path.entries()) {
|
|
84
|
+
if (!current || typeof current !== "object") {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
current = (current as Record<string, unknown>)[seg as keyof typeof current];
|
|
88
|
+
if (isExprNode(current)) {
|
|
89
|
+
node = current;
|
|
90
|
+
nodePath = path.slice(0, i + 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { node, path: nodePath };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Immutably replace the value at `path` within `node` (object keys / array indexes). */
|
|
97
|
+
function replaceAtPath(node: unknown, path: NodePath, value: unknown): unknown {
|
|
98
|
+
if (path.length === 0) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
const [head, ...rest] = path as [string | number, ...NodePath];
|
|
102
|
+
if (Array.isArray(node)) {
|
|
103
|
+
const copy = [...(node as unknown[])];
|
|
104
|
+
copy[Number(head)] = replaceAtPath(copy[Number(head)], rest, value);
|
|
105
|
+
return copy;
|
|
106
|
+
}
|
|
107
|
+
const base: Record<string, unknown> = isJsonObject(node) ? node : {};
|
|
108
|
+
return { ...base, [head]: replaceAtPath(base[head as string], rest, value) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Write the whole updated root node back to the document position (one undo step). */
|
|
112
|
+
function writeRoot(tab: Tab, editing: FormulaEditDef, newRoot: unknown) {
|
|
113
|
+
if (editing.type === "def" && editing.defName) {
|
|
114
|
+
const { defName } = editing;
|
|
115
|
+
transactDoc(tab, (t) => mutateUpdateDef(t, defName, { $expression: newRoot as JsonValue }));
|
|
116
|
+
} else if (editing.type === "event" && editing.path && editing.eventKey) {
|
|
117
|
+
const { eventKey, path } = editing;
|
|
118
|
+
transactDoc(tab, (t) =>
|
|
119
|
+
mutateUpdateProperty(t, path, eventKey, { $expression: newRoot } as JxNodeValue),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
renderFormulaWorkspace();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Close the workspace — clears `editingFormula`; the canvas re-renders its regular mode. */
|
|
126
|
+
export function closeFormulaWorkspace() {
|
|
127
|
+
updateUi("editingFormula", null);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Rendering ───────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
const PANE_BORDER = "1px solid var(--spectrum-gray-200, #323232)";
|
|
133
|
+
|
|
134
|
+
const MONO = "font-family:var(--spectrum-code-font-family, monospace)";
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Render the formula workspace into the canvas area. Mirrors renderFunctionEditor's takeover: the
|
|
138
|
+
* canvas DnD/event handlers and panel registrations are cleared and canvasWrap becomes a stretched
|
|
139
|
+
* column owned by this template.
|
|
140
|
+
*/
|
|
141
|
+
export function renderFormulaWorkspace() {
|
|
142
|
+
const tab = activeTab.value;
|
|
143
|
+
const editing = tab?.session.ui.editingFormula as FormulaEditDef | null | undefined;
|
|
144
|
+
if (!tab || !editing) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Reset the chip selection when the workspace retargets.
|
|
149
|
+
const targetKey = JSON.stringify(editing);
|
|
150
|
+
if (targetKey !== _targetKey || tab !== _targetTab) {
|
|
151
|
+
_targetKey = targetKey;
|
|
152
|
+
_targetTab = tab;
|
|
153
|
+
_selectedPath = [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clean up canvas DnD and event handlers (the canvas surface is being replaced).
|
|
157
|
+
for (const fn of view.canvasDndCleanups) {
|
|
158
|
+
fn();
|
|
159
|
+
}
|
|
160
|
+
view.canvasDndCleanups = [];
|
|
161
|
+
for (const fn of view.canvasEventCleanups) {
|
|
162
|
+
fn();
|
|
163
|
+
}
|
|
164
|
+
view.canvasEventCleanups = [];
|
|
165
|
+
canvasPanels.length = 0;
|
|
166
|
+
|
|
167
|
+
// Eject foreign DOM (iframe canvas markup) plus any stale Lit part before the first render;
|
|
168
|
+
// Subsequent renders diff in place so form focus survives re-renders.
|
|
169
|
+
if (!canvasWrap.firstElementChild?.classList.contains("formula-workspace")) {
|
|
170
|
+
canvasWrap.textContent = "";
|
|
171
|
+
// @ts-expect-error -- _$litPart$ is Lit's private render-part marker, not in the DOM types
|
|
172
|
+
delete canvasWrap["_$litPart$"];
|
|
173
|
+
}
|
|
174
|
+
canvasWrap.style.padding = "0";
|
|
175
|
+
canvasWrap.style.flexDirection = "column";
|
|
176
|
+
canvasWrap.style.alignItems = "stretch";
|
|
177
|
+
|
|
178
|
+
litRender(workspaceTemplate(tab, editing), canvasWrap);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function workspaceTemplate(tab: Tab, editing: FormulaEditDef): TemplateResult {
|
|
182
|
+
const stateEntries = (tab.doc.document?.state ?? {}) as Record<string, JxStateDefinition>;
|
|
183
|
+
const title = (editing.type === "def" ? editing.defName : editing.eventKey) ?? "?";
|
|
184
|
+
const root = formulaRoot(tab, editing);
|
|
185
|
+
|
|
186
|
+
const closeButton = html`
|
|
187
|
+
<sp-action-button
|
|
188
|
+
quiet
|
|
189
|
+
size="s"
|
|
190
|
+
class="fw-close"
|
|
191
|
+
title="Close formula workspace"
|
|
192
|
+
@click=${closeFormulaWorkspace}
|
|
193
|
+
>
|
|
194
|
+
<sp-icon-close slot="icon"></sp-icon-close>
|
|
195
|
+
Close
|
|
196
|
+
</sp-action-button>
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
if (!root) {
|
|
200
|
+
return html`
|
|
201
|
+
<div
|
|
202
|
+
class="formula-workspace"
|
|
203
|
+
style="display:flex;flex-direction:column;flex:1;min-height:0;gap:12px;padding:20px"
|
|
204
|
+
>
|
|
205
|
+
<div class="fw-header" style="display:flex;align-items:center;gap:8px">
|
|
206
|
+
<span class="fw-title" style="${MONO};font-size:14px">fx ${title}</span>
|
|
207
|
+
<div style="flex:1"></div>
|
|
208
|
+
${closeButton}
|
|
209
|
+
</div>
|
|
210
|
+
<div class="empty-state">No expression found at this document position.</div>
|
|
211
|
+
</div>
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { scope } = tab.session.canvas;
|
|
216
|
+
// Live-context evaluation in the canvas iframe with snapshot fallback (M6). While the workspace
|
|
217
|
+
// Owns canvasWrap the iframe is usually unmounted (immediate snapshot); when a live host exists
|
|
218
|
+
// (e.g. another panel's canvas), a landed result re-renders this workspace. An event target's
|
|
219
|
+
// Element path is the context, so repeater-template formulas bind the first item's $map scope.
|
|
220
|
+
const preview = livePreviewExpression(
|
|
221
|
+
tab,
|
|
222
|
+
`formula:${JSON.stringify(editing)}`,
|
|
223
|
+
root,
|
|
224
|
+
editing.type === "event" ? (editing.path ?? null) : null,
|
|
225
|
+
() => renderFormulaWorkspace(),
|
|
226
|
+
);
|
|
227
|
+
const { node: selected, path: selectedPath } = resolveSelection(root, _selectedPath);
|
|
228
|
+
const write = (next: unknown) => writeRoot(tab, editing, next);
|
|
229
|
+
const writeSelected = (next: unknown) => write(replaceAtPath(root, selectedPath, next));
|
|
230
|
+
const onChipSelect = (path: NodePath) => {
|
|
231
|
+
_selectedPath = path;
|
|
232
|
+
renderFormulaWorkspace();
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// ── Header: target title, catalog browser (palette), Close ──
|
|
236
|
+
const header = html`
|
|
237
|
+
<div class="fw-header" style="display:flex;align-items:center;gap:8px;padding:14px 20px 6px">
|
|
238
|
+
<span
|
|
239
|
+
class="fw-title"
|
|
240
|
+
style="${MONO};font-size:14px;color:var(--spectrum-gray-800, #d0d0d0)"
|
|
241
|
+
title=${editing.type === "def" ? `state/${title}` : title}
|
|
242
|
+
>fx ${title}</span
|
|
243
|
+
>
|
|
244
|
+
<span class="fw-kind" style="font-size:11px;color:var(--spectrum-gray-600, #808080)">
|
|
245
|
+
${editing.type === "def" ? "state expression" : "event expression"}
|
|
246
|
+
</span>
|
|
247
|
+
<div style="flex:1"></div>
|
|
248
|
+
<sp-action-button
|
|
249
|
+
quiet
|
|
250
|
+
size="s"
|
|
251
|
+
class="fw-browse-catalog"
|
|
252
|
+
title="Browse catalog"
|
|
253
|
+
@click=${(e: Event) =>
|
|
254
|
+
openFormulaPalette({
|
|
255
|
+
anchor: e.currentTarget as HTMLElement,
|
|
256
|
+
entries: formulaCatalog(stateEntries),
|
|
257
|
+
onPick: (entry) =>
|
|
258
|
+
applyCatalogPick(entry, writeSelected, {
|
|
259
|
+
onInsertDef: (name, def) =>
|
|
260
|
+
transactDoc(activeTab.value, (t) =>
|
|
261
|
+
mutateAddDef(t, name, def as Record<string, JsonValue>),
|
|
262
|
+
),
|
|
263
|
+
stateEntries,
|
|
264
|
+
}),
|
|
265
|
+
})}
|
|
266
|
+
>
|
|
267
|
+
<sp-icon-brackets slot="icon"></sp-icon-brackets>
|
|
268
|
+
Catalog
|
|
269
|
+
</sp-action-button>
|
|
270
|
+
${closeButton}
|
|
271
|
+
</div>
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
// ── Chip strip: the whole tree as chips with live badges; clicking selects the sub-node ──
|
|
275
|
+
const chips = html`
|
|
276
|
+
<div class="fw-chips" style="padding:2px 20px 10px;border-bottom:${PANE_BORDER}">
|
|
277
|
+
${renderFormulaChips(root, onChipSelect, { preview })}
|
|
278
|
+
</div>
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
// ── Main pane: the recursive expression form for the SELECTED sub-node ──
|
|
282
|
+
const editor = html`
|
|
283
|
+
<div class="fw-editor" style="flex:1;min-width:0;overflow:auto;padding:16px 20px">
|
|
284
|
+
<div
|
|
285
|
+
class="fw-selected"
|
|
286
|
+
style="font-size:11px;color:var(--spectrum-gray-600, #808080);padding-bottom:8px"
|
|
287
|
+
>
|
|
288
|
+
Selected:
|
|
289
|
+
<span style=${MONO}>${selectedPath.length === 0 ? "root" : chipSummary(selected)}</span>
|
|
290
|
+
</div>
|
|
291
|
+
${renderExpressionEditor(selected, writeSelected, {
|
|
292
|
+
allowEventRef: editing.type === "event",
|
|
293
|
+
depth: 1,
|
|
294
|
+
path: selectedPath,
|
|
295
|
+
preview,
|
|
296
|
+
stateDefs: Object.keys(stateEntries),
|
|
297
|
+
stateEntries,
|
|
298
|
+
})}
|
|
299
|
+
</div>
|
|
300
|
+
`;
|
|
301
|
+
|
|
302
|
+
// ── Right rail: live data context (the canvas dataScope snapshot) ──
|
|
303
|
+
const scopeEntries = scope ? Object.entries(scope) : [];
|
|
304
|
+
const rail = html`
|
|
305
|
+
<div
|
|
306
|
+
class="fw-context"
|
|
307
|
+
style="width:280px;flex-shrink:0;overflow:auto;border-left:${PANE_BORDER};padding:16px"
|
|
308
|
+
>
|
|
309
|
+
<div
|
|
310
|
+
class="fw-context-title"
|
|
311
|
+
style="font-size:11px;text-transform:uppercase;letter-spacing:0.5px;color:var(--spectrum-gray-600, #808080);padding-bottom:8px"
|
|
312
|
+
>
|
|
313
|
+
Data
|
|
314
|
+
</div>
|
|
315
|
+
${scopeEntries.length === 0
|
|
316
|
+
? html`<div class="empty-state">No canvas data snapshot yet</div>`
|
|
317
|
+
: scopeEntries.map(([name, value]) => {
|
|
318
|
+
const unwrapped = unwrapSignal(value);
|
|
319
|
+
return html`
|
|
320
|
+
<div class="fw-context-entry" style="margin-bottom:10px">
|
|
321
|
+
<div style="display:flex;gap:6px;align-items:baseline">
|
|
322
|
+
<span
|
|
323
|
+
class="fw-context-name"
|
|
324
|
+
style="${MONO};font-size:12px;color:var(--spectrum-gray-800, #d0d0d0)"
|
|
325
|
+
>${name}</span
|
|
326
|
+
>
|
|
327
|
+
<span style="font-size:10px;color:var(--spectrum-gray-600, #808080)"
|
|
328
|
+
>${dataTypeLabel(value)}</span
|
|
329
|
+
>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="data-tree">${renderDataTreeTemplate(unwrapped, 0, 4)}</div>
|
|
332
|
+
</div>
|
|
333
|
+
`;
|
|
334
|
+
})}
|
|
335
|
+
</div>
|
|
336
|
+
`;
|
|
337
|
+
|
|
338
|
+
// ── Footer: the root result badge, or the evaluation error ──
|
|
339
|
+
const footer = preview?.error
|
|
340
|
+
? html`
|
|
341
|
+
<div
|
|
342
|
+
class="fw-result fw-result--error"
|
|
343
|
+
style="${MONO};font-size:12px;padding:10px 20px;border-top:${PANE_BORDER};color:var(--spectrum-negative-content-color-default, #f76a63)"
|
|
344
|
+
>
|
|
345
|
+
${preview.error}
|
|
346
|
+
</div>
|
|
347
|
+
`
|
|
348
|
+
: preview
|
|
349
|
+
? html`
|
|
350
|
+
<div
|
|
351
|
+
class="fw-result"
|
|
352
|
+
style="${MONO};font-size:12px;padding:10px 20px;border-top:${PANE_BORDER};color:var(--spectrum-seafoam-900, #35a690)"
|
|
353
|
+
>
|
|
354
|
+
= ${preview.values.get("") ?? "undefined"}
|
|
355
|
+
${preview.mutating
|
|
356
|
+
? html`<span style="color:var(--spectrum-gray-600, #808080)">(mutates target)</span>`
|
|
357
|
+
: ""}
|
|
358
|
+
</div>
|
|
359
|
+
`
|
|
360
|
+
: html`
|
|
361
|
+
<div
|
|
362
|
+
class="fw-result fw-result--pending"
|
|
363
|
+
style="font-size:12px;padding:10px 20px;border-top:${PANE_BORDER};color:var(--spectrum-gray-600, #808080)"
|
|
364
|
+
>
|
|
365
|
+
Preview unavailable — the canvas has not posted a data snapshot yet
|
|
366
|
+
</div>
|
|
367
|
+
`;
|
|
368
|
+
|
|
369
|
+
return html`
|
|
370
|
+
<div
|
|
371
|
+
class="formula-workspace"
|
|
372
|
+
style="display:flex;flex-direction:column;flex:1;min-height:0;background:var(--spectrum-gray-75, #1d1d1d)"
|
|
373
|
+
>
|
|
374
|
+
${header} ${chips}
|
|
375
|
+
<div class="fw-body" style="display:flex;flex:1;min-height:0">${editor} ${rail}</div>
|
|
376
|
+
${footer}
|
|
377
|
+
</div>
|
|
378
|
+
`;
|
|
379
|
+
}
|