@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,18 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
import { html, nothing } from "lit-html";
|
|
3
3
|
import { live } from "lit-html/directives/live.js";
|
|
4
|
+
import { PURE_METHOD_OPS } from "@jxsuite/runtime/expression";
|
|
4
5
|
import { isJsonObject, isRef } from "@jxsuite/schema/guards";
|
|
5
6
|
import { renderFieldRow } from "./field-row";
|
|
7
|
+
import { renderFormulaChips } from "./formula-chips";
|
|
8
|
+
import { applyCatalogPick, calleeEntry, formulaCatalog } from "./formula-catalog";
|
|
9
|
+
import { openFormulaPalette } from "./formula-palette";
|
|
6
10
|
|
|
7
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
JxExpressionNode,
|
|
13
|
+
JxExpressionOperand,
|
|
14
|
+
JxStateDefinition,
|
|
15
|
+
} from "@jxsuite/schema/types";
|
|
8
16
|
import type { TemplateResult } from "lit-html";
|
|
9
17
|
|
|
10
18
|
// ─── Operator Categories ────────────────────────────────────────────────────
|
|
@@ -24,114 +32,149 @@ const BINARY_OPS = new Set([
|
|
|
24
32
|
">=",
|
|
25
33
|
"&&",
|
|
26
34
|
"||",
|
|
35
|
+
"??",
|
|
27
36
|
]);
|
|
28
37
|
const ASSIGN_OPS = new Set(["=", "+=", "-=", "*=", "/="]);
|
|
29
38
|
const NO_ARG_OPS = new Set(["pop", "shift"]);
|
|
30
39
|
const ONE_ARG_OPS = new Set(["push", "unshift"]);
|
|
31
40
|
|
|
41
|
+
const ZERO_ARG_METHOD_OPS = new Set([
|
|
42
|
+
"flat",
|
|
43
|
+
"normalize",
|
|
44
|
+
"toLocaleLowerCase",
|
|
45
|
+
"toLocaleString",
|
|
46
|
+
"toLocaleUpperCase",
|
|
47
|
+
"toLowerCase",
|
|
48
|
+
"toReversed",
|
|
49
|
+
"toSorted",
|
|
50
|
+
"toUpperCase",
|
|
51
|
+
"trim",
|
|
52
|
+
"trimEnd",
|
|
53
|
+
"trimStart",
|
|
54
|
+
]);
|
|
55
|
+
|
|
32
56
|
const OPERATOR_GROUPS = [
|
|
33
57
|
{ label: "Assignment", ops: ["=", "+=", "-=", "*=", "/="] },
|
|
34
58
|
{ label: "Unary", ops: ["!", "-"] },
|
|
35
59
|
{ label: "Arithmetic", ops: ["+", "-", "*", "/", "%"] },
|
|
36
60
|
{ label: "Comparison", ops: ["===", "!==", "<", "<=", ">", ">="] },
|
|
37
|
-
{ label: "Logical", ops: ["&&", "||"] },
|
|
61
|
+
{ label: "Logical", ops: ["&&", "||", "??"] },
|
|
62
|
+
{ label: "Conditional", ops: ["?:", "switch"] },
|
|
38
63
|
{
|
|
39
64
|
label: "Array methods",
|
|
40
65
|
ops: ["push", "pop", "shift", "unshift", "splice"],
|
|
41
66
|
},
|
|
67
|
+
{
|
|
68
|
+
label: "Pure methods (String)",
|
|
69
|
+
ops: [
|
|
70
|
+
"toUpperCase",
|
|
71
|
+
"toLowerCase",
|
|
72
|
+
"trim",
|
|
73
|
+
"trimStart",
|
|
74
|
+
"trimEnd",
|
|
75
|
+
"split",
|
|
76
|
+
"startsWith",
|
|
77
|
+
"endsWith",
|
|
78
|
+
"padStart",
|
|
79
|
+
"padEnd",
|
|
80
|
+
"replaceAll",
|
|
81
|
+
"repeat",
|
|
82
|
+
"charAt",
|
|
83
|
+
"normalize",
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: "Pure methods (Array)",
|
|
88
|
+
ops: [
|
|
89
|
+
"includes",
|
|
90
|
+
"indexOf",
|
|
91
|
+
"lastIndexOf",
|
|
92
|
+
"join",
|
|
93
|
+
"slice",
|
|
94
|
+
"concat",
|
|
95
|
+
"at",
|
|
96
|
+
"flat",
|
|
97
|
+
"toSorted",
|
|
98
|
+
"toReversed",
|
|
99
|
+
"toSpliced",
|
|
100
|
+
"with",
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{ label: "Pure methods (Number)", ops: ["toFixed", "toPrecision", "toLocaleString"] },
|
|
42
104
|
{ label: "Aggregate", ops: ["reduce", "map", "filter"] },
|
|
105
|
+
{ label: "Function", ops: ["call"] },
|
|
43
106
|
];
|
|
44
107
|
|
|
108
|
+
interface OperatorInfo {
|
|
109
|
+
needsValue: boolean;
|
|
110
|
+
needsInitial: boolean;
|
|
111
|
+
targetMustBeRef: boolean;
|
|
112
|
+
spliceArray: boolean;
|
|
113
|
+
valueIsNode: boolean;
|
|
114
|
+
switchCases: boolean;
|
|
115
|
+
/** `call`: value is a positional-args array; target is the callee pointer. */
|
|
116
|
+
callArgs: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const INFO_DEFAULTS: OperatorInfo = {
|
|
120
|
+
callArgs: false,
|
|
121
|
+
needsInitial: false,
|
|
122
|
+
needsValue: false,
|
|
123
|
+
spliceArray: false,
|
|
124
|
+
switchCases: false,
|
|
125
|
+
targetMustBeRef: false,
|
|
126
|
+
valueIsNode: false,
|
|
127
|
+
};
|
|
128
|
+
|
|
45
129
|
/**
|
|
46
130
|
* @param {string} op
|
|
47
|
-
* @returns {
|
|
48
|
-
* needsValue: boolean;
|
|
49
|
-
* needsInitial: boolean;
|
|
50
|
-
* targetMustBeRef: boolean;
|
|
51
|
-
* spliceArray: boolean;
|
|
52
|
-
* valueIsNode: boolean;
|
|
53
|
-
* }}
|
|
131
|
+
* @returns {OperatorInfo}
|
|
54
132
|
*/
|
|
55
|
-
function operatorInfo(op: string) {
|
|
133
|
+
function operatorInfo(op: string): OperatorInfo {
|
|
56
134
|
if (UNARY_OPS.has(op)) {
|
|
57
|
-
return {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
135
|
+
return { ...INFO_DEFAULTS };
|
|
136
|
+
}
|
|
137
|
+
if (op === "?:") {
|
|
138
|
+
return { ...INFO_DEFAULTS, needsInitial: true, needsValue: true };
|
|
139
|
+
}
|
|
140
|
+
if (op === "switch") {
|
|
141
|
+
return { ...INFO_DEFAULTS, switchCases: true };
|
|
142
|
+
}
|
|
143
|
+
if (op === "call") {
|
|
144
|
+
return { ...INFO_DEFAULTS, callArgs: true, targetMustBeRef: true };
|
|
145
|
+
}
|
|
146
|
+
if (PURE_METHOD_OPS.has(op)) {
|
|
147
|
+
// Receiver in target (any operand); zero-arg methods render no value row.
|
|
148
|
+
return { ...INFO_DEFAULTS, needsValue: !ZERO_ARG_METHOD_OPS.has(op) };
|
|
64
149
|
}
|
|
65
150
|
if (BINARY_OPS.has(op)) {
|
|
66
|
-
return {
|
|
67
|
-
needsInitial: false,
|
|
68
|
-
needsValue: true,
|
|
69
|
-
spliceArray: false,
|
|
70
|
-
targetMustBeRef: false,
|
|
71
|
-
valueIsNode: false,
|
|
72
|
-
};
|
|
151
|
+
return { ...INFO_DEFAULTS, needsValue: true };
|
|
73
152
|
}
|
|
74
153
|
if (ASSIGN_OPS.has(op)) {
|
|
75
|
-
return {
|
|
76
|
-
needsInitial: false,
|
|
77
|
-
needsValue: true,
|
|
78
|
-
spliceArray: false,
|
|
79
|
-
targetMustBeRef: true,
|
|
80
|
-
valueIsNode: false,
|
|
81
|
-
};
|
|
154
|
+
return { ...INFO_DEFAULTS, needsValue: true, targetMustBeRef: true };
|
|
82
155
|
}
|
|
83
156
|
if (NO_ARG_OPS.has(op)) {
|
|
84
|
-
return {
|
|
85
|
-
needsInitial: false,
|
|
86
|
-
needsValue: false,
|
|
87
|
-
spliceArray: false,
|
|
88
|
-
targetMustBeRef: true,
|
|
89
|
-
valueIsNode: false,
|
|
90
|
-
};
|
|
157
|
+
return { ...INFO_DEFAULTS, targetMustBeRef: true };
|
|
91
158
|
}
|
|
92
159
|
if (ONE_ARG_OPS.has(op)) {
|
|
93
|
-
return {
|
|
94
|
-
needsInitial: false,
|
|
95
|
-
needsValue: true,
|
|
96
|
-
spliceArray: false,
|
|
97
|
-
targetMustBeRef: true,
|
|
98
|
-
valueIsNode: false,
|
|
99
|
-
};
|
|
160
|
+
return { ...INFO_DEFAULTS, needsValue: true, targetMustBeRef: true };
|
|
100
161
|
}
|
|
101
162
|
if (op === "splice") {
|
|
102
|
-
return {
|
|
103
|
-
needsInitial: false,
|
|
104
|
-
needsValue: true,
|
|
105
|
-
spliceArray: true,
|
|
106
|
-
targetMustBeRef: true,
|
|
107
|
-
valueIsNode: false,
|
|
108
|
-
};
|
|
163
|
+
return { ...INFO_DEFAULTS, needsValue: true, spliceArray: true, targetMustBeRef: true };
|
|
109
164
|
}
|
|
110
165
|
if (op === "reduce") {
|
|
111
166
|
return {
|
|
167
|
+
...INFO_DEFAULTS,
|
|
112
168
|
needsInitial: true,
|
|
113
169
|
needsValue: true,
|
|
114
|
-
spliceArray: false,
|
|
115
170
|
targetMustBeRef: true,
|
|
116
171
|
valueIsNode: true,
|
|
117
172
|
};
|
|
118
173
|
}
|
|
119
174
|
if (op === "map" || op === "filter") {
|
|
120
|
-
return {
|
|
121
|
-
needsInitial: false,
|
|
122
|
-
needsValue: true,
|
|
123
|
-
spliceArray: false,
|
|
124
|
-
targetMustBeRef: true,
|
|
125
|
-
valueIsNode: true,
|
|
126
|
-
};
|
|
175
|
+
return { ...INFO_DEFAULTS, needsValue: true, targetMustBeRef: true, valueIsNode: true };
|
|
127
176
|
}
|
|
128
|
-
return {
|
|
129
|
-
needsInitial: false,
|
|
130
|
-
needsValue: false,
|
|
131
|
-
spliceArray: false,
|
|
132
|
-
targetMustBeRef: false,
|
|
133
|
-
valueIsNode: false,
|
|
134
|
-
};
|
|
177
|
+
return { ...INFO_DEFAULTS };
|
|
135
178
|
}
|
|
136
179
|
|
|
137
180
|
// ─── Operand Mode Detection ─────────────────────────────────────────────────
|
|
@@ -152,6 +195,16 @@ function operandMode(operand: unknown) {
|
|
|
152
195
|
return "literal";
|
|
153
196
|
}
|
|
154
197
|
|
|
198
|
+
/** Positional-arg labels for a `call` node, from the callee's catalog entry when resolvable. */
|
|
199
|
+
function calleeParamLabels(
|
|
200
|
+
target: unknown,
|
|
201
|
+
state?: Record<string, JxStateDefinition> | null,
|
|
202
|
+
): string[] {
|
|
203
|
+
const ref = isRef(target) ? target.$ref : "";
|
|
204
|
+
const entry = ref ? calleeEntry(ref, state) : undefined;
|
|
205
|
+
return entry?.kind === "formula" ? entry.parameters.map((p) => p.name) : [];
|
|
206
|
+
}
|
|
207
|
+
|
|
155
208
|
/**
|
|
156
209
|
* @param {string} mode
|
|
157
210
|
* @returns {JxExpressionOperand}
|
|
@@ -230,15 +283,48 @@ export function expressionHint(node: unknown) {
|
|
|
230
283
|
if (op === "splice") {
|
|
231
284
|
return `splice(${targetLabel})`;
|
|
232
285
|
}
|
|
233
|
-
if (op === "
|
|
286
|
+
if (op === "call") {
|
|
287
|
+
return `${targetLabel.replace("window#/", "").replaceAll("/", ".")}(…)`;
|
|
288
|
+
}
|
|
289
|
+
if (op === "reduce" || op === "map" || op === "filter" || PURE_METHOD_OPS.has(op)) {
|
|
234
290
|
return `${op}(${targetLabel})`;
|
|
235
291
|
}
|
|
292
|
+
if (op === "?:") {
|
|
293
|
+
return `${targetLabel} ? … : …`;
|
|
294
|
+
}
|
|
295
|
+
if (op === "switch") {
|
|
296
|
+
return `switch(${targetLabel})`;
|
|
297
|
+
}
|
|
236
298
|
if (UNARY_OPS.has(op)) {
|
|
237
299
|
return `${op}${targetLabel}`;
|
|
238
300
|
}
|
|
239
301
|
return `${targetLabel} ${op} …`;
|
|
240
302
|
}
|
|
241
303
|
|
|
304
|
+
// ─── Live Value Badge (spec §19.9) ──────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/** Preview data computed by services/preview-eval.ts — display strings keyed by node path. */
|
|
307
|
+
export interface EditorPreview {
|
|
308
|
+
values: Map<string, string>;
|
|
309
|
+
error: string | null;
|
|
310
|
+
mutating: boolean;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderValueBadge(preview: EditorPreview | null | undefined, pathKey: string) {
|
|
314
|
+
const text = preview?.values.get(pathKey);
|
|
315
|
+
if (text === undefined) {
|
|
316
|
+
return nothing;
|
|
317
|
+
}
|
|
318
|
+
return html`
|
|
319
|
+
<span
|
|
320
|
+
class="expr-live-badge"
|
|
321
|
+
title=${text}
|
|
322
|
+
style="font-family:var(--spectrum-code-font-family, monospace);font-size:10px;line-height:16px;padding:0 5px;border-radius:4px;background:var(--spectrum-gray-200, #323232);color:var(--spectrum-seafoam-900, #35a690);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:160px;flex-shrink:1"
|
|
323
|
+
>${text}</span
|
|
324
|
+
>
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
|
|
242
328
|
// ─── Ref Picker ─────────────────────────────────────────────────────────────
|
|
243
329
|
|
|
244
330
|
/**
|
|
@@ -343,6 +429,10 @@ function renderLiteralEditor(operand: unknown, onChange: (newVal: JxExpressionOp
|
|
|
343
429
|
// ─── Operand Editor ─────────────────────────────────────────────────────────
|
|
344
430
|
|
|
345
431
|
/**
|
|
432
|
+
* Single-operand editor (mode picker + literal/ref/nested-expression widget). Exported for
|
|
433
|
+
* statement-position operand slots (spec §20: `if` tests, `$switch` discriminants, `dispatchEvent`
|
|
434
|
+
* detail).
|
|
435
|
+
*
|
|
346
436
|
* @param {unknown} operand
|
|
347
437
|
* @param {(newOperand: unknown) => void} onChange
|
|
348
438
|
* @param {{
|
|
@@ -353,7 +443,7 @@ function renderLiteralEditor(operand: unknown, onChange: (newVal: JxExpressionOp
|
|
|
353
443
|
* }} opts
|
|
354
444
|
* @returns {import("lit-html").TemplateResult}
|
|
355
445
|
*/
|
|
356
|
-
function renderOperandEditor(
|
|
446
|
+
export function renderOperandEditor(
|
|
357
447
|
operand: unknown,
|
|
358
448
|
onChange: (newOperand: unknown) => void,
|
|
359
449
|
opts: {
|
|
@@ -361,6 +451,9 @@ function renderOperandEditor(
|
|
|
361
451
|
allowEventRef: boolean;
|
|
362
452
|
depth: number;
|
|
363
453
|
mustBeRef?: boolean;
|
|
454
|
+
preview?: EditorPreview | null;
|
|
455
|
+
path?: (string | number)[];
|
|
456
|
+
stateEntries?: Record<string, JxStateDefinition> | null;
|
|
364
457
|
},
|
|
365
458
|
): TemplateResult {
|
|
366
459
|
if (opts.mustBeRef) {
|
|
@@ -402,21 +495,24 @@ function renderOperandEditor(
|
|
|
402
495
|
`;
|
|
403
496
|
}
|
|
404
497
|
|
|
405
|
-
// ───
|
|
498
|
+
// ─── Positional Args Editor (splice / call) ─────────────────────────────────
|
|
406
499
|
|
|
407
500
|
/**
|
|
408
501
|
* @param {unknown[]} args
|
|
409
502
|
* @param {(newArgs: unknown[]) => void} onChange
|
|
410
503
|
* @param {{ stateDefs: string[]; allowEventRef: boolean; depth: number }} opts
|
|
504
|
+
* @param {{ labels?: string[]; fallbackLabel?: string }} [naming]
|
|
411
505
|
* @returns {import("lit-html").TemplateResult}
|
|
412
506
|
*/
|
|
413
507
|
function renderSpliceArgsEditor(
|
|
414
508
|
args: unknown[],
|
|
415
509
|
onChange: (newArgs: unknown[]) => void,
|
|
416
510
|
opts: { stateDefs: string[]; allowEventRef: boolean; depth: number },
|
|
511
|
+
naming: { labels?: string[]; fallbackLabel?: string } = {},
|
|
417
512
|
): TemplateResult {
|
|
418
513
|
const safeArgs = Array.isArray(args) ? args : [];
|
|
419
|
-
const labels = ["start", "del", "item"];
|
|
514
|
+
const labels = naming.labels ?? ["start", "del", "item"];
|
|
515
|
+
const fallbackLabel = naming.fallbackLabel ?? "item";
|
|
420
516
|
|
|
421
517
|
return html`
|
|
422
518
|
<div class="array-object-field">
|
|
@@ -427,7 +523,7 @@ function renderSpliceArgsEditor(
|
|
|
427
523
|
style="display:flex;gap:4px;align-items:center;margin-bottom:4px"
|
|
428
524
|
>
|
|
429
525
|
<span style="font-size:10px;color:var(--spectrum-gray-600, #808080);min-width:30px">
|
|
430
|
-
${labels[idx] ??
|
|
526
|
+
${labels[idx] ?? fallbackLabel}
|
|
431
527
|
</span>
|
|
432
528
|
${renderOperandEditor(
|
|
433
529
|
arg,
|
|
@@ -472,20 +568,30 @@ const _operatorMenuCache = OPERATOR_GROUPS.map(
|
|
|
472
568
|
|
|
473
569
|
// ─── Main Expression Editor ─────────────────────────────────────────────────
|
|
474
570
|
|
|
571
|
+
export interface ExpressionEditorOpts {
|
|
572
|
+
stateDefs: string[];
|
|
573
|
+
allowEventRef: boolean;
|
|
574
|
+
depth?: number;
|
|
575
|
+
preview?: EditorPreview | null;
|
|
576
|
+
path?: (string | number)[];
|
|
577
|
+
/** Full state defs map — resolves named-formula catalog entries (call labels, palette). */
|
|
578
|
+
stateEntries?: Record<string, JxStateDefinition> | null;
|
|
579
|
+
/** Chip-strip click hook (depth 0). No-op when absent. */
|
|
580
|
+
onChipSelect?: (path: (string | number)[]) => void;
|
|
581
|
+
/** Vendors a packaged formula's state entry into the document on catalog pick. */
|
|
582
|
+
onInsertDef?: (name: string, def: JxStateDefinition) => void;
|
|
583
|
+
}
|
|
584
|
+
|
|
475
585
|
/**
|
|
476
586
|
* @param {unknown} node
|
|
477
587
|
* @param {(node: unknown) => void} onChange
|
|
478
|
-
* @param {
|
|
479
|
-
* stateDefs: string[];
|
|
480
|
-
* allowEventRef: boolean;
|
|
481
|
-
* depth?: number;
|
|
482
|
-
* }} opts
|
|
588
|
+
* @param {ExpressionEditorOpts} opts
|
|
483
589
|
* @returns {import("lit-html").TemplateResult}
|
|
484
590
|
*/
|
|
485
591
|
export function renderExpressionEditor(
|
|
486
592
|
node: unknown,
|
|
487
593
|
onChange: (node: unknown) => void,
|
|
488
|
-
opts:
|
|
594
|
+
opts: ExpressionEditorOpts,
|
|
489
595
|
): TemplateResult {
|
|
490
596
|
const depth = opts.depth ?? 0;
|
|
491
597
|
const safeNode: Record<string, unknown> =
|
|
@@ -494,71 +600,134 @@ export function renderExpressionEditor(
|
|
|
494
600
|
: { operator: "=", target: null };
|
|
495
601
|
const op = (safeNode.operator as string) || "=";
|
|
496
602
|
const info = operatorInfo(op);
|
|
603
|
+
const preview = opts.preview ?? null;
|
|
604
|
+
const path = opts.path ?? [];
|
|
605
|
+
const pathKey = path.join("/");
|
|
606
|
+
const sub = (...segs: (string | number)[]) => [...path, ...segs].join("/");
|
|
607
|
+
/** Wrap an operand widget with its live value badge. */
|
|
608
|
+
const withBadge = (widget: unknown, key: string) =>
|
|
609
|
+
html`<div style="display:flex;gap:4px;align-items:center;flex:1;min-width:0">
|
|
610
|
+
${widget}${renderValueBadge(preview, key)}
|
|
611
|
+
</div>`;
|
|
497
612
|
|
|
498
613
|
const nestStyle =
|
|
499
614
|
depth > 0
|
|
500
615
|
? "border-left:2px solid var(--spectrum-gray-300, #3c3c3c);margin-left:8px;padding-left:8px;"
|
|
501
616
|
: "";
|
|
502
617
|
|
|
618
|
+
// The root badge: pure roots show their result; mutating roots' effect shows on target/value.
|
|
619
|
+
const rootBadge =
|
|
620
|
+
depth === 0 && preview && !preview.mutating ? renderValueBadge(preview, pathKey) : nothing;
|
|
621
|
+
|
|
503
622
|
return html`
|
|
504
623
|
<div class="expression-editor" style=${nestStyle}>
|
|
624
|
+
${depth === 0
|
|
625
|
+
? renderFormulaChips(safeNode, opts.onChipSelect ?? (() => {}), { path, preview })
|
|
626
|
+
: nothing}
|
|
627
|
+
${depth === 0 && preview?.error
|
|
628
|
+
? html`<div
|
|
629
|
+
style="font-size:10px;color:var(--spectrum-negative-content-color-default, #f76a63);padding:2px 0"
|
|
630
|
+
>
|
|
631
|
+
${preview.error}
|
|
632
|
+
</div>`
|
|
633
|
+
: nothing}
|
|
505
634
|
${renderFieldRow({
|
|
506
635
|
hasValue: false,
|
|
507
636
|
label: "Operator",
|
|
508
637
|
prop: "operator",
|
|
509
638
|
widget: html`
|
|
510
|
-
<
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (newInfo.needsValue) {
|
|
524
|
-
if (newInfo.valueIsNode) {
|
|
525
|
-
const val = safeNode.value as Record<string, unknown> | null;
|
|
526
|
-
updated.value = val?.operator ? safeNode.value : { operator: "!", target: null };
|
|
527
|
-
} else if (newInfo.spliceArray) {
|
|
528
|
-
updated.value = Array.isArray(safeNode.value) ? safeNode.value : [null];
|
|
529
|
-
} else {
|
|
530
|
-
updated.value = safeNode.value ?? null;
|
|
639
|
+
<div style="display:flex;gap:4px;align-items:center;flex:1;min-width:0">
|
|
640
|
+
<sp-picker
|
|
641
|
+
size="s"
|
|
642
|
+
.value=${live(op)}
|
|
643
|
+
@change=${(e: Event) => {
|
|
644
|
+
const newOp = (e.target as HTMLInputElement).value;
|
|
645
|
+
const newInfo = operatorInfo(newOp);
|
|
646
|
+
const updated: Record<string, unknown> = {
|
|
647
|
+
operator: newOp,
|
|
648
|
+
target: safeNode.target,
|
|
649
|
+
};
|
|
650
|
+
if (newInfo.targetMustBeRef && operandMode(safeNode.target) !== "ref") {
|
|
651
|
+
updated.target = { $ref: "" };
|
|
531
652
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
653
|
+
if (newInfo.needsValue) {
|
|
654
|
+
if (newInfo.valueIsNode) {
|
|
655
|
+
const val = safeNode.value as Record<string, unknown> | null;
|
|
656
|
+
updated.value = val?.operator
|
|
657
|
+
? safeNode.value
|
|
658
|
+
: { operator: "!", target: null };
|
|
659
|
+
} else if (newInfo.spliceArray) {
|
|
660
|
+
updated.value = Array.isArray(safeNode.value) ? safeNode.value : [null];
|
|
661
|
+
} else {
|
|
662
|
+
updated.value = safeNode.value ?? null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (newInfo.needsInitial) {
|
|
666
|
+
updated.initial = safeNode.initial ?? (newOp === "?:" ? null : 0);
|
|
667
|
+
}
|
|
668
|
+
if (newInfo.switchCases) {
|
|
669
|
+
updated.cases = isJsonObject(safeNode.cases) ? safeNode.cases : {};
|
|
670
|
+
if ("default" in safeNode) {
|
|
671
|
+
updated.default = safeNode.default;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (newInfo.callArgs) {
|
|
675
|
+
updated.value = Array.isArray(safeNode.value) ? safeNode.value : [];
|
|
676
|
+
}
|
|
677
|
+
onChange(updated);
|
|
678
|
+
}}
|
|
679
|
+
>
|
|
680
|
+
${_operatorMenuCache}
|
|
681
|
+
</sp-picker>
|
|
682
|
+
<sp-action-button
|
|
683
|
+
quiet
|
|
684
|
+
size="s"
|
|
685
|
+
class="expr-browse-catalog"
|
|
686
|
+
title="Browse catalog"
|
|
687
|
+
@click=${(e: Event) =>
|
|
688
|
+
openFormulaPalette({
|
|
689
|
+
anchor: e.currentTarget as HTMLElement,
|
|
690
|
+
entries: formulaCatalog(opts.stateEntries),
|
|
691
|
+
onPick: (entry) =>
|
|
692
|
+
applyCatalogPick(entry, onChange, {
|
|
693
|
+
onInsertDef: opts.onInsertDef,
|
|
694
|
+
stateEntries: opts.stateEntries,
|
|
695
|
+
}),
|
|
696
|
+
})}
|
|
697
|
+
>
|
|
698
|
+
<sp-icon-brackets slot="icon"></sp-icon-brackets>
|
|
699
|
+
</sp-action-button>
|
|
700
|
+
${rootBadge}
|
|
701
|
+
</div>
|
|
541
702
|
`,
|
|
542
703
|
})}
|
|
543
704
|
${renderFieldRow({
|
|
544
705
|
hasValue: false,
|
|
545
|
-
label: "Target",
|
|
706
|
+
label: op === "?:" ? "If" : op === "switch" ? "On" : op === "call" ? "Callee" : "Target",
|
|
546
707
|
prop: "target",
|
|
547
|
-
widget:
|
|
548
|
-
...
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
708
|
+
widget: withBadge(
|
|
709
|
+
renderOperandEditor(safeNode.target, (t) => onChange({ ...safeNode, target: t }), {
|
|
710
|
+
...opts,
|
|
711
|
+
depth,
|
|
712
|
+
mustBeRef: info.targetMustBeRef,
|
|
713
|
+
path: [...path, "target"],
|
|
714
|
+
}),
|
|
715
|
+
sub("target"),
|
|
716
|
+
),
|
|
552
717
|
})}
|
|
553
718
|
${info.needsValue && !info.valueIsNode && !info.spliceArray
|
|
554
719
|
? renderFieldRow({
|
|
555
720
|
hasValue: false,
|
|
556
|
-
label: "Value",
|
|
721
|
+
label: op === "?:" ? "Then" : "Value",
|
|
557
722
|
prop: "value",
|
|
558
|
-
widget:
|
|
559
|
-
safeNode.value,
|
|
560
|
-
|
|
561
|
-
|
|
723
|
+
widget: withBadge(
|
|
724
|
+
renderOperandEditor(safeNode.value, (v) => onChange({ ...safeNode, value: v }), {
|
|
725
|
+
...opts,
|
|
726
|
+
depth,
|
|
727
|
+
mustBeRef: false,
|
|
728
|
+
path: [...path, "value"],
|
|
729
|
+
}),
|
|
730
|
+
sub("value"),
|
|
562
731
|
),
|
|
563
732
|
})
|
|
564
733
|
: nothing}
|
|
@@ -576,7 +745,7 @@ export function renderExpressionEditor(
|
|
|
576
745
|
? safeNode.value
|
|
577
746
|
: { operator: "!", target: null },
|
|
578
747
|
(v) => onChange({ ...safeNode, value: v }),
|
|
579
|
-
{ ...opts, depth: depth + 1 },
|
|
748
|
+
{ ...opts, depth: depth + 1, path: [...path, "value"] },
|
|
580
749
|
)}
|
|
581
750
|
</div>
|
|
582
751
|
`
|
|
@@ -601,18 +770,147 @@ export function renderExpressionEditor(
|
|
|
601
770
|
</div>
|
|
602
771
|
`
|
|
603
772
|
: nothing}
|
|
773
|
+
${info.callArgs
|
|
774
|
+
? html`
|
|
775
|
+
<div style="margin-top:4px">
|
|
776
|
+
${renderFieldRow({
|
|
777
|
+
hasValue: false,
|
|
778
|
+
label: "Args",
|
|
779
|
+
prop: "value",
|
|
780
|
+
widget: nothing,
|
|
781
|
+
})}
|
|
782
|
+
${renderSpliceArgsEditor(
|
|
783
|
+
safeNode.value as unknown[],
|
|
784
|
+
(v) => onChange({ ...safeNode, value: v }),
|
|
785
|
+
{ ...opts, depth },
|
|
786
|
+
{
|
|
787
|
+
fallbackLabel: "arg",
|
|
788
|
+
labels: calleeParamLabels(safeNode.target, opts.stateEntries),
|
|
789
|
+
},
|
|
790
|
+
)}
|
|
791
|
+
</div>
|
|
792
|
+
`
|
|
793
|
+
: nothing}
|
|
794
|
+
${info.switchCases
|
|
795
|
+
? renderSwitchCasesEditor(safeNode, onChange, { ...opts, depth, path }, withBadge)
|
|
796
|
+
: nothing}
|
|
604
797
|
${info.needsInitial
|
|
605
798
|
? renderFieldRow({
|
|
606
799
|
hasValue: false,
|
|
607
|
-
label: "Initial",
|
|
800
|
+
label: op === "?:" ? "Else" : "Initial",
|
|
608
801
|
prop: "initial",
|
|
609
|
-
widget:
|
|
610
|
-
safeNode.initial,
|
|
611
|
-
|
|
612
|
-
|
|
802
|
+
widget: withBadge(
|
|
803
|
+
renderOperandEditor(safeNode.initial, (v) => onChange({ ...safeNode, initial: v }), {
|
|
804
|
+
...opts,
|
|
805
|
+
depth,
|
|
806
|
+
mustBeRef: false,
|
|
807
|
+
path: [...path, "initial"],
|
|
808
|
+
}),
|
|
809
|
+
sub("initial"),
|
|
613
810
|
),
|
|
614
811
|
})
|
|
615
812
|
: nothing}
|
|
616
813
|
</div>
|
|
617
814
|
`;
|
|
618
815
|
}
|
|
816
|
+
|
|
817
|
+
// ─── Switch Cases Editor ────────────────────────────────────────────────────
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Case rows for the `switch` operator: matched value → result operand, plus the default operand.
|
|
821
|
+
* Mirrors the element-level `$switch`/`cases` model (spec §19.4b).
|
|
822
|
+
*/
|
|
823
|
+
function renderSwitchCasesEditor(
|
|
824
|
+
safeNode: Record<string, unknown>,
|
|
825
|
+
onChange: (node: unknown) => void,
|
|
826
|
+
opts: {
|
|
827
|
+
stateDefs: string[];
|
|
828
|
+
allowEventRef: boolean;
|
|
829
|
+
depth: number;
|
|
830
|
+
preview?: EditorPreview | null;
|
|
831
|
+
path: (string | number)[];
|
|
832
|
+
},
|
|
833
|
+
withBadge: (widget: unknown, key: string) => TemplateResult,
|
|
834
|
+
): TemplateResult {
|
|
835
|
+
const cases = isJsonObject(safeNode.cases)
|
|
836
|
+
? (safeNode.cases as Record<string, unknown>)
|
|
837
|
+
: ({} as Record<string, unknown>);
|
|
838
|
+
const entries = Object.entries(cases);
|
|
839
|
+
const setCases = (next: Record<string, unknown>) => onChange({ ...safeNode, cases: next });
|
|
840
|
+
|
|
841
|
+
return html`
|
|
842
|
+
<div class="switch-cases" style="margin-top:4px">
|
|
843
|
+
${entries.map(
|
|
844
|
+
([key, operand]) => html`
|
|
845
|
+
<div style="display:flex;gap:4px;align-items:flex-start;margin-bottom:4px">
|
|
846
|
+
<sp-textfield
|
|
847
|
+
size="s"
|
|
848
|
+
style="width:80px;flex-shrink:0"
|
|
849
|
+
placeholder="value"
|
|
850
|
+
.value=${live(key)}
|
|
851
|
+
@change=${(e: Event) => {
|
|
852
|
+
const newKey = (e.target as HTMLInputElement).value;
|
|
853
|
+
if (newKey === key) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const next: Record<string, unknown> = {};
|
|
857
|
+
for (const [k, v] of entries) {
|
|
858
|
+
next[k === key ? newKey : k] = v;
|
|
859
|
+
}
|
|
860
|
+
setCases(next);
|
|
861
|
+
}}
|
|
862
|
+
></sp-textfield>
|
|
863
|
+
${withBadge(
|
|
864
|
+
renderOperandEditor(operand, (v) => setCases({ ...cases, [key]: v }), {
|
|
865
|
+
...opts,
|
|
866
|
+
mustBeRef: false,
|
|
867
|
+
path: [...opts.path, "cases", key],
|
|
868
|
+
}),
|
|
869
|
+
[...opts.path, "cases", key].join("/"),
|
|
870
|
+
)}
|
|
871
|
+
<sp-action-button
|
|
872
|
+
quiet
|
|
873
|
+
size="xs"
|
|
874
|
+
@click=${() => {
|
|
875
|
+
const next = { ...cases };
|
|
876
|
+
delete next[key];
|
|
877
|
+
setCases(next);
|
|
878
|
+
}}
|
|
879
|
+
>
|
|
880
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
881
|
+
</sp-action-button>
|
|
882
|
+
</div>
|
|
883
|
+
`,
|
|
884
|
+
)}
|
|
885
|
+
<div style="display:flex;gap:4px;align-items:flex-start;margin-bottom:4px">
|
|
886
|
+
<span
|
|
887
|
+
style="width:80px;flex-shrink:0;font-size:10px;line-height:24px;color:var(--spectrum-gray-600, #808080)"
|
|
888
|
+
>default</span
|
|
889
|
+
>
|
|
890
|
+
${withBadge(
|
|
891
|
+
renderOperandEditor(safeNode.default, (v) => onChange({ ...safeNode, default: v }), {
|
|
892
|
+
...opts,
|
|
893
|
+
mustBeRef: false,
|
|
894
|
+
path: [...opts.path, "default"],
|
|
895
|
+
}),
|
|
896
|
+
[...opts.path, "default"].join("/"),
|
|
897
|
+
)}
|
|
898
|
+
</div>
|
|
899
|
+
<sp-action-button
|
|
900
|
+
quiet
|
|
901
|
+
size="s"
|
|
902
|
+
@click=${() => {
|
|
903
|
+
let n = entries.length + 1;
|
|
904
|
+
let key = `case ${n}`;
|
|
905
|
+
while (Object.hasOwn(cases, key)) {
|
|
906
|
+
n += 1;
|
|
907
|
+
key = `case ${n}`;
|
|
908
|
+
}
|
|
909
|
+
setCases({ ...cases, [key]: null });
|
|
910
|
+
}}
|
|
911
|
+
>
|
|
912
|
+
+ Add case
|
|
913
|
+
</sp-action-button>
|
|
914
|
+
</div>
|
|
915
|
+
`;
|
|
916
|
+
}
|