@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
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// oxlint-disable unicorn/no-thenable -- `then` is the JSON Schema conditional keyword (spec §20), not a promise
|
|
3
|
+
/**
|
|
4
|
+
* Statement editor (spec §20) — vertical statement-card list for structured function bodies.
|
|
5
|
+
*
|
|
6
|
+
* Renders a Function entry's `body: JxStatement[]` as cards on a connector line: bare expression
|
|
7
|
+
* nodes (mutation or `call`), `if`/`then`/`else` branches, `$switch`/`cases` multiway branches, and
|
|
8
|
+
* WHATWG `dispatchEvent` statements. Branch bodies render as indented lanes (the expression
|
|
9
|
+
* editor's `border-left` nesting idiom) with their own add-statement pickers. Cards drag-reorder
|
|
10
|
+
* within their lane via the pragmatic-drag-and-drop tree-item pattern (see panels/dnd.ts). All
|
|
11
|
+
* edits flow through `onChange(next)` immutably — no statement object is mutated in place.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { html, nothing } from "lit-html";
|
|
15
|
+
import { live } from "lit-html/directives/live.js";
|
|
16
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
17
|
+
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
18
|
+
import { disableNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview";
|
|
19
|
+
import {
|
|
20
|
+
attachInstruction,
|
|
21
|
+
extractInstruction,
|
|
22
|
+
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
|
23
|
+
import { isJsonObject } from "@jxsuite/schema/guards";
|
|
24
|
+
import { renderExpressionEditor, renderOperandEditor } from "../ui/expression-editor";
|
|
25
|
+
import { renderFieldRow } from "../ui/field-row";
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
CemEvent,
|
|
29
|
+
JxDispatchStatement,
|
|
30
|
+
JxIfStatement,
|
|
31
|
+
JxStatement,
|
|
32
|
+
JxStateDefinition,
|
|
33
|
+
JxSwitchStatement,
|
|
34
|
+
} from "@jxsuite/schema/types";
|
|
35
|
+
import type { TemplateResult } from "lit-html";
|
|
36
|
+
|
|
37
|
+
// ─── Options ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export interface StatementEditorOpts {
|
|
40
|
+
stateDefs: string[];
|
|
41
|
+
stateEntries?: Record<string, JxStateDefinition> | null;
|
|
42
|
+
allowEventRef: boolean;
|
|
43
|
+
/** The entry's declared CEM events — offered as dispatchEvent name completions. */
|
|
44
|
+
emits?: CemEvent[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A lane address inside the statement tree: alternating [index, branchKey] steps, where a `cases`
|
|
49
|
+
* step consumes three entries ([index, "cases", caseKey]). The empty path is the top-level
|
|
50
|
+
* statement list.
|
|
51
|
+
*/
|
|
52
|
+
type LanePath = (string | number)[];
|
|
53
|
+
|
|
54
|
+
// ─── Statement Kind Detection ────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** Discriminate a statement's kind, mirroring the runtime's detection order (spec §20.2). */
|
|
57
|
+
export function statementKind(stmt: unknown): "expression" | "if" | "switch" | "dispatch" {
|
|
58
|
+
if (isJsonObject(stmt)) {
|
|
59
|
+
if ("operator" in stmt) {
|
|
60
|
+
return "expression";
|
|
61
|
+
}
|
|
62
|
+
if ("if" in stmt) {
|
|
63
|
+
return "if";
|
|
64
|
+
}
|
|
65
|
+
if ("$switch" in stmt) {
|
|
66
|
+
return "switch";
|
|
67
|
+
}
|
|
68
|
+
if ("dispatchEvent" in stmt) {
|
|
69
|
+
return "dispatch";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return "expression";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ASSIGN_OPS = new Set(["=", "+=", "-=", "*=", "/="]);
|
|
76
|
+
|
|
77
|
+
/** Card header label — ECMA/WHATWG naming (spec §20.2). */
|
|
78
|
+
function kindLabel(stmt: unknown): string {
|
|
79
|
+
switch (statementKind(stmt)) {
|
|
80
|
+
case "if": {
|
|
81
|
+
return "If / Else";
|
|
82
|
+
}
|
|
83
|
+
case "switch": {
|
|
84
|
+
return "Switch";
|
|
85
|
+
}
|
|
86
|
+
case "dispatch": {
|
|
87
|
+
return "Dispatch event";
|
|
88
|
+
}
|
|
89
|
+
default: {
|
|
90
|
+
const op = isJsonObject(stmt) ? String(stmt.operator ?? "") : "";
|
|
91
|
+
if (ASSIGN_OPS.has(op)) {
|
|
92
|
+
return "Set state";
|
|
93
|
+
}
|
|
94
|
+
if (op === "call") {
|
|
95
|
+
return "Call";
|
|
96
|
+
}
|
|
97
|
+
return "Expression";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Add-statement Seeds (spec §20 shapes) ───────────────────────────────────
|
|
103
|
+
|
|
104
|
+
const STATEMENT_SEEDS: Record<string, () => JxStatement> = {
|
|
105
|
+
call: () => ({ operator: "call", target: { $ref: "" }, value: [] }),
|
|
106
|
+
dispatch: () => ({ dispatchEvent: "" }),
|
|
107
|
+
if: () => ({
|
|
108
|
+
if: { operator: "===", target: { $ref: "" }, value: null },
|
|
109
|
+
then: [],
|
|
110
|
+
}),
|
|
111
|
+
set: () => ({ operator: "=", target: { $ref: "" }, value: null }),
|
|
112
|
+
switch: () => ({ $switch: { $ref: "" }, cases: {} }),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ─── Lane Addressing (immutable read/write through nested statement lists) ───
|
|
116
|
+
|
|
117
|
+
/** Resolve the statement list a lane path points at; null when the path is stale. */
|
|
118
|
+
export function laneListAt(list: JxStatement[], path: LanePath): JxStatement[] | null {
|
|
119
|
+
if (path.length === 0) {
|
|
120
|
+
return list;
|
|
121
|
+
}
|
|
122
|
+
const [idx, key] = path;
|
|
123
|
+
const stmt = typeof idx === "number" ? (list[idx] as Record<string, unknown> | undefined) : null;
|
|
124
|
+
if (!isJsonObject(stmt) || typeof key !== "string") {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (key === "cases") {
|
|
128
|
+
const caseKey = path.at(2);
|
|
129
|
+
const { cases } = stmt;
|
|
130
|
+
if (typeof caseKey !== "string" || !isJsonObject(cases) || !Array.isArray(cases[caseKey])) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return laneListAt(cases[caseKey] as unknown as JxStatement[], path.slice(3));
|
|
134
|
+
}
|
|
135
|
+
if (!Array.isArray(stmt[key])) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return laneListAt(stmt[key] as unknown as JxStatement[], path.slice(2));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Rebuild the statement tree with the lane at `path` replaced by `next` — fully immutable. */
|
|
142
|
+
export function withLaneList(
|
|
143
|
+
list: JxStatement[],
|
|
144
|
+
path: LanePath,
|
|
145
|
+
next: JxStatement[],
|
|
146
|
+
): JxStatement[] {
|
|
147
|
+
if (path.length === 0) {
|
|
148
|
+
return next;
|
|
149
|
+
}
|
|
150
|
+
const idx = path[0] as number;
|
|
151
|
+
const key = path[1] as string;
|
|
152
|
+
const stmt = list[idx] as unknown as Record<string, unknown>;
|
|
153
|
+
let updated: Record<string, unknown>;
|
|
154
|
+
if (key === "cases") {
|
|
155
|
+
const caseKey = path[2] as string;
|
|
156
|
+
const cases = { ...(stmt.cases as Record<string, JxStatement[]>) };
|
|
157
|
+
cases[caseKey] = withLaneList(cases[caseKey] ?? [], path.slice(3), next);
|
|
158
|
+
updated = { ...stmt, cases };
|
|
159
|
+
} else {
|
|
160
|
+
updated = {
|
|
161
|
+
...stmt,
|
|
162
|
+
[key]: withLaneList((stmt[key] as JxStatement[] | undefined) ?? [], path.slice(2), next),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return list.map((s, i) => (i === idx ? (updated as unknown as JxStatement) : s));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Drag-reorder (pragmatic-drag-and-drop, per-lane) ────────────────────────
|
|
169
|
+
|
|
170
|
+
/** Active DnD registration cleanup per editor root — replaced on every re-render. */
|
|
171
|
+
const dndRegistrations = new WeakMap<HTMLElement, () => void>();
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Register drag-reorder on all statement rows under `root` (post-render, like registerLayersDnD).
|
|
175
|
+
* Rows may only reorder within their own lane — the source's lane id must match the target's.
|
|
176
|
+
*/
|
|
177
|
+
function registerStatementsDnD(
|
|
178
|
+
root: HTMLElement,
|
|
179
|
+
statements: JxStatement[],
|
|
180
|
+
onChange: (next: JxStatement[]) => void,
|
|
181
|
+
) {
|
|
182
|
+
requestAnimationFrame(async () => {
|
|
183
|
+
// Deferred adapter import — keeps this panel's import graph adapter-free at module load.
|
|
184
|
+
const { draggable, dropTargetForElements } =
|
|
185
|
+
await import("@atlaskit/pragmatic-drag-and-drop/element/adapter");
|
|
186
|
+
dndRegistrations.get(root)?.();
|
|
187
|
+
const cleanups: (() => void)[] = [];
|
|
188
|
+
|
|
189
|
+
for (const row of root.querySelectorAll("[data-stmt-row]") as NodeListOf<HTMLElement>) {
|
|
190
|
+
const laneId = row.dataset.stmtLane ?? "[]";
|
|
191
|
+
const index = Math.trunc(Number(row.dataset.stmtIndex)) || 0;
|
|
192
|
+
const handle = row.querySelector(".statement-drag-handle");
|
|
193
|
+
|
|
194
|
+
cleanups.push(
|
|
195
|
+
combine(
|
|
196
|
+
draggable({
|
|
197
|
+
element: row,
|
|
198
|
+
...(handle ? { dragHandle: handle } : {}),
|
|
199
|
+
getInitialData() {
|
|
200
|
+
return { index, lane: laneId, type: "statement" };
|
|
201
|
+
},
|
|
202
|
+
onGenerateDragPreview({
|
|
203
|
+
nativeSetDragImage,
|
|
204
|
+
}: {
|
|
205
|
+
nativeSetDragImage: ((image: Element, x: number, y: number) => void) | null;
|
|
206
|
+
}) {
|
|
207
|
+
disableNativeDragPreview({ nativeSetDragImage });
|
|
208
|
+
},
|
|
209
|
+
onDragStart() {
|
|
210
|
+
row.classList.add("dragging");
|
|
211
|
+
},
|
|
212
|
+
onDrop() {
|
|
213
|
+
row.classList.remove("dragging");
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
dropTargetForElements({
|
|
217
|
+
element: row,
|
|
218
|
+
canDrop({ source }: { source: { data: Record<string, unknown> } }) {
|
|
219
|
+
return source.data.type === "statement" && source.data.lane === laneId;
|
|
220
|
+
},
|
|
221
|
+
getData({
|
|
222
|
+
input,
|
|
223
|
+
element,
|
|
224
|
+
}: {
|
|
225
|
+
input: Parameters<typeof attachInstruction>[1]["input"];
|
|
226
|
+
element: Element;
|
|
227
|
+
}) {
|
|
228
|
+
return attachInstruction(
|
|
229
|
+
{ index },
|
|
230
|
+
{
|
|
231
|
+
block: ["make-child"],
|
|
232
|
+
currentLevel: 0,
|
|
233
|
+
element,
|
|
234
|
+
indentPerLevel: 16,
|
|
235
|
+
input,
|
|
236
|
+
mode: "standard",
|
|
237
|
+
},
|
|
238
|
+
) as Record<string | symbol, unknown>;
|
|
239
|
+
},
|
|
240
|
+
onDrag({ self }: { self: { data: Record<string, unknown> } }) {
|
|
241
|
+
const instruction = extractInstruction(self.data);
|
|
242
|
+
row.classList.toggle("drop-above", instruction?.type === "reorder-above");
|
|
243
|
+
row.classList.toggle("drop-below", instruction?.type === "reorder-below");
|
|
244
|
+
},
|
|
245
|
+
onDragLeave() {
|
|
246
|
+
row.classList.remove("drop-above", "drop-below");
|
|
247
|
+
},
|
|
248
|
+
onDrop({
|
|
249
|
+
self,
|
|
250
|
+
source,
|
|
251
|
+
}: {
|
|
252
|
+
self: { data: Record<string, unknown> };
|
|
253
|
+
source: { data: Record<string, unknown> };
|
|
254
|
+
}) {
|
|
255
|
+
row.classList.remove("drop-above", "drop-below");
|
|
256
|
+
const instruction = extractInstruction(self.data);
|
|
257
|
+
if (
|
|
258
|
+
!instruction ||
|
|
259
|
+
(instruction.type !== "reorder-above" && instruction.type !== "reorder-below")
|
|
260
|
+
) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const from = source.data.index as number;
|
|
264
|
+
const lanePath = JSON.parse(laneId) as LanePath;
|
|
265
|
+
const lane = laneListAt(statements, lanePath);
|
|
266
|
+
if (!lane || from === index) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
let insertAt = instruction.type === "reorder-above" ? index : index + 1;
|
|
270
|
+
if (from < insertAt) {
|
|
271
|
+
insertAt -= 1;
|
|
272
|
+
}
|
|
273
|
+
if (insertAt === from) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const nextLane = lane.filter((_, i) => i !== from);
|
|
277
|
+
nextLane.splice(insertAt, 0, lane[from]!);
|
|
278
|
+
onChange(withLaneList(statements, lanePath, nextLane));
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
dndRegistrations.set(root, () => {
|
|
286
|
+
for (const cleanup of cleanups) {
|
|
287
|
+
cleanup();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── Small shared widgets ────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
const laneLabelStyle =
|
|
296
|
+
"font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;color:var(--spectrum-gray-600, #808080)";
|
|
297
|
+
|
|
298
|
+
function operandOpts(opts: StatementEditorOpts) {
|
|
299
|
+
return {
|
|
300
|
+
allowEventRef: opts.allowEventRef,
|
|
301
|
+
depth: 0,
|
|
302
|
+
stateDefs: opts.stateDefs,
|
|
303
|
+
stateEntries: opts.stateEntries ?? null,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The "+ Add statement" picker appended to every lane. */
|
|
308
|
+
function renderAddStatement(
|
|
309
|
+
list: JxStatement[],
|
|
310
|
+
commit: (next: JxStatement[]) => void,
|
|
311
|
+
): TemplateResult {
|
|
312
|
+
return html`
|
|
313
|
+
<sp-picker
|
|
314
|
+
size="s"
|
|
315
|
+
quiet
|
|
316
|
+
class="statement-add"
|
|
317
|
+
label="+ Add statement"
|
|
318
|
+
placeholder="+ Add statement"
|
|
319
|
+
.value=${live("")}
|
|
320
|
+
@change=${(e: Event) => {
|
|
321
|
+
const kind = (e.target as HTMLInputElement).value;
|
|
322
|
+
const seed = STATEMENT_SEEDS[kind];
|
|
323
|
+
if (!seed) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
(e.target as HTMLInputElement).value = "";
|
|
327
|
+
commit([...list, seed()]);
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
<sp-menu-item value="set">Set state</sp-menu-item>
|
|
331
|
+
<sp-menu-item value="call">Call function</sp-menu-item>
|
|
332
|
+
<sp-menu-item value="if">If / Else</sp-menu-item>
|
|
333
|
+
<sp-menu-item value="switch">Switch</sp-menu-item>
|
|
334
|
+
<sp-menu-item value="dispatch">Dispatch event</sp-menu-item>
|
|
335
|
+
</sp-picker>
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** An indented branch lane: header label (+ optional remove) above a nested statement list. */
|
|
340
|
+
function renderLane(
|
|
341
|
+
label: string | TemplateResult,
|
|
342
|
+
list: JxStatement[],
|
|
343
|
+
commit: (next: JxStatement[]) => void,
|
|
344
|
+
opts: StatementEditorOpts,
|
|
345
|
+
lanePath: LanePath,
|
|
346
|
+
extras: { onRemove?: () => void } = {},
|
|
347
|
+
): TemplateResult {
|
|
348
|
+
return html`
|
|
349
|
+
<div class="statement-lane" style="margin:4px 0 4px 6px">
|
|
350
|
+
<div class="statement-lane-header" style="display:flex;align-items:center;gap:4px">
|
|
351
|
+
<span style=${laneLabelStyle}>${label}</span>
|
|
352
|
+
${extras.onRemove
|
|
353
|
+
? html`
|
|
354
|
+
<sp-action-button
|
|
355
|
+
quiet
|
|
356
|
+
size="xs"
|
|
357
|
+
class="statement-lane-remove"
|
|
358
|
+
title="Remove branch"
|
|
359
|
+
@click=${extras.onRemove}
|
|
360
|
+
>
|
|
361
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
362
|
+
</sp-action-button>
|
|
363
|
+
`
|
|
364
|
+
: nothing}
|
|
365
|
+
</div>
|
|
366
|
+
${renderStatementList(list, commit, opts, lanePath)}
|
|
367
|
+
</div>
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── Kind-specific card bodies ───────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
function renderIfBody(
|
|
374
|
+
stmt: JxIfStatement,
|
|
375
|
+
commit: (next: JxStatement) => void,
|
|
376
|
+
opts: StatementEditorOpts,
|
|
377
|
+
lanePath: LanePath,
|
|
378
|
+
index: number,
|
|
379
|
+
): TemplateResult {
|
|
380
|
+
return html`
|
|
381
|
+
${renderFieldRow({
|
|
382
|
+
hasValue: false,
|
|
383
|
+
label: "If",
|
|
384
|
+
prop: "if",
|
|
385
|
+
widget: renderOperandEditor(
|
|
386
|
+
stmt.if,
|
|
387
|
+
(v) => commit({ ...stmt, if: v } as JxStatement),
|
|
388
|
+
operandOpts(opts),
|
|
389
|
+
),
|
|
390
|
+
})}
|
|
391
|
+
${renderLane("Then", stmt.then ?? [], (next) => commit({ ...stmt, then: next }), opts, [
|
|
392
|
+
...lanePath,
|
|
393
|
+
index,
|
|
394
|
+
"then",
|
|
395
|
+
])}
|
|
396
|
+
${Array.isArray(stmt.else)
|
|
397
|
+
? renderLane(
|
|
398
|
+
"Else",
|
|
399
|
+
stmt.else,
|
|
400
|
+
(next) => commit({ ...stmt, else: next }),
|
|
401
|
+
opts,
|
|
402
|
+
[...lanePath, index, "else"],
|
|
403
|
+
{
|
|
404
|
+
onRemove: () => {
|
|
405
|
+
const { else: _else, ...rest } = stmt;
|
|
406
|
+
commit(rest as JxStatement);
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
: html`
|
|
411
|
+
<sp-action-button
|
|
412
|
+
quiet
|
|
413
|
+
size="s"
|
|
414
|
+
class="statement-add-else"
|
|
415
|
+
@click=${() => commit({ ...stmt, else: [] })}
|
|
416
|
+
>
|
|
417
|
+
+ Add else
|
|
418
|
+
</sp-action-button>
|
|
419
|
+
`}
|
|
420
|
+
`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function renderSwitchBody(
|
|
424
|
+
stmt: JxSwitchStatement,
|
|
425
|
+
commit: (next: JxStatement) => void,
|
|
426
|
+
opts: StatementEditorOpts,
|
|
427
|
+
lanePath: LanePath,
|
|
428
|
+
index: number,
|
|
429
|
+
): TemplateResult {
|
|
430
|
+
const cases = isJsonObject(stmt.cases) ? (stmt.cases as Record<string, JxStatement[]>) : {};
|
|
431
|
+
const entries = Object.entries(cases);
|
|
432
|
+
const setCases = (next: Record<string, JxStatement[]>) =>
|
|
433
|
+
commit({ ...stmt, cases: next } as JxStatement);
|
|
434
|
+
|
|
435
|
+
return html`
|
|
436
|
+
${renderFieldRow({
|
|
437
|
+
hasValue: false,
|
|
438
|
+
label: "Switch on",
|
|
439
|
+
prop: "$switch",
|
|
440
|
+
widget: renderOperandEditor(
|
|
441
|
+
stmt.$switch,
|
|
442
|
+
(v) => commit({ ...stmt, $switch: v } as JxStatement),
|
|
443
|
+
operandOpts(opts),
|
|
444
|
+
),
|
|
445
|
+
})}
|
|
446
|
+
${entries.map(([key, list]) =>
|
|
447
|
+
renderLane(
|
|
448
|
+
html`
|
|
449
|
+
<sp-textfield
|
|
450
|
+
size="s"
|
|
451
|
+
class="statement-case-key"
|
|
452
|
+
style="width:96px"
|
|
453
|
+
placeholder="value"
|
|
454
|
+
.value=${live(key)}
|
|
455
|
+
@change=${(e: Event) => {
|
|
456
|
+
const newKey = (e.target as HTMLInputElement).value;
|
|
457
|
+
if (newKey === key) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const next: Record<string, JxStatement[]> = {};
|
|
461
|
+
for (const [k, v] of entries) {
|
|
462
|
+
next[k === key ? newKey : k] = v;
|
|
463
|
+
}
|
|
464
|
+
setCases(next);
|
|
465
|
+
}}
|
|
466
|
+
></sp-textfield>
|
|
467
|
+
`,
|
|
468
|
+
Array.isArray(list) ? list : [],
|
|
469
|
+
(next) => setCases({ ...cases, [key]: next }),
|
|
470
|
+
opts,
|
|
471
|
+
[...lanePath, index, "cases", key],
|
|
472
|
+
{
|
|
473
|
+
onRemove: () => {
|
|
474
|
+
const next = { ...cases };
|
|
475
|
+
delete next[key];
|
|
476
|
+
setCases(next);
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
),
|
|
480
|
+
)}
|
|
481
|
+
${renderLane(
|
|
482
|
+
"Default",
|
|
483
|
+
Array.isArray(stmt.default) ? stmt.default : [],
|
|
484
|
+
(next) => {
|
|
485
|
+
if (next.length === 0) {
|
|
486
|
+
const { default: _default, ...rest } = stmt;
|
|
487
|
+
commit(rest as JxStatement);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
commit({ ...stmt, default: next } as JxStatement);
|
|
491
|
+
},
|
|
492
|
+
opts,
|
|
493
|
+
[...lanePath, index, "default"],
|
|
494
|
+
)}
|
|
495
|
+
<sp-action-button
|
|
496
|
+
quiet
|
|
497
|
+
size="s"
|
|
498
|
+
class="statement-add-case"
|
|
499
|
+
@click=${() => {
|
|
500
|
+
let n = entries.length + 1;
|
|
501
|
+
let key = `case ${n}`;
|
|
502
|
+
while (Object.hasOwn(cases, key)) {
|
|
503
|
+
n += 1;
|
|
504
|
+
key = `case ${n}`;
|
|
505
|
+
}
|
|
506
|
+
setCases({ ...cases, [key]: [] });
|
|
507
|
+
}}
|
|
508
|
+
>
|
|
509
|
+
+ Add case
|
|
510
|
+
</sp-action-button>
|
|
511
|
+
`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function renderDispatchBody(
|
|
515
|
+
stmt: JxDispatchStatement,
|
|
516
|
+
commit: (next: JxStatement) => void,
|
|
517
|
+
opts: StatementEditorOpts,
|
|
518
|
+
): TemplateResult {
|
|
519
|
+
const emitNames = (opts.emits ?? [])
|
|
520
|
+
.map((e) => e.name)
|
|
521
|
+
.filter((n): n is string => typeof n === "string" && n.length > 0);
|
|
522
|
+
const commitName = (e: Event) =>
|
|
523
|
+
commit({ ...stmt, dispatchEvent: (e.target as HTMLInputElement).value } as JxStatement);
|
|
524
|
+
const toggle = (key: "bubbles" | "composed") => (e: Event) => {
|
|
525
|
+
const { checked } = e.target as HTMLInputElement;
|
|
526
|
+
const { [key]: _removed, ...rest } = stmt;
|
|
527
|
+
commit((checked ? { ...rest, [key]: true } : rest) as JxStatement);
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
return html`
|
|
531
|
+
${renderFieldRow({
|
|
532
|
+
hasValue: false,
|
|
533
|
+
label: "Event",
|
|
534
|
+
prop: "dispatchEvent",
|
|
535
|
+
widget:
|
|
536
|
+
emitNames.length > 0
|
|
537
|
+
? html`
|
|
538
|
+
<sp-combobox
|
|
539
|
+
size="s"
|
|
540
|
+
allows-custom-value
|
|
541
|
+
class="statement-dispatch-name"
|
|
542
|
+
style="flex:1"
|
|
543
|
+
.value=${live(stmt.dispatchEvent ?? "")}
|
|
544
|
+
@change=${commitName}
|
|
545
|
+
>
|
|
546
|
+
${emitNames.map((n) => html`<sp-menu-item value=${n}>${n}</sp-menu-item>`)}
|
|
547
|
+
</sp-combobox>
|
|
548
|
+
`
|
|
549
|
+
: html`
|
|
550
|
+
<sp-textfield
|
|
551
|
+
size="s"
|
|
552
|
+
class="statement-dispatch-name"
|
|
553
|
+
style="flex:1"
|
|
554
|
+
placeholder="event-name"
|
|
555
|
+
.value=${live(stmt.dispatchEvent ?? "")}
|
|
556
|
+
@input=${commitName}
|
|
557
|
+
></sp-textfield>
|
|
558
|
+
`,
|
|
559
|
+
})}
|
|
560
|
+
${renderFieldRow({
|
|
561
|
+
hasValue: false,
|
|
562
|
+
label: "Detail",
|
|
563
|
+
prop: "detail",
|
|
564
|
+
widget: renderOperandEditor(
|
|
565
|
+
stmt.detail ?? null,
|
|
566
|
+
(v) => commit({ ...stmt, detail: v } as JxStatement),
|
|
567
|
+
operandOpts(opts),
|
|
568
|
+
),
|
|
569
|
+
})}
|
|
570
|
+
${renderFieldRow({
|
|
571
|
+
hasValue: false,
|
|
572
|
+
label: "Options",
|
|
573
|
+
prop: "eventInit",
|
|
574
|
+
widget: html`
|
|
575
|
+
<div style="display:flex;gap:12px;align-items:center">
|
|
576
|
+
<sp-checkbox
|
|
577
|
+
size="s"
|
|
578
|
+
class="statement-dispatch-bubbles"
|
|
579
|
+
?checked=${Boolean(stmt.bubbles)}
|
|
580
|
+
@change=${toggle("bubbles")}
|
|
581
|
+
>Bubbles</sp-checkbox
|
|
582
|
+
>
|
|
583
|
+
<sp-checkbox
|
|
584
|
+
size="s"
|
|
585
|
+
class="statement-dispatch-composed"
|
|
586
|
+
?checked=${Boolean(stmt.composed)}
|
|
587
|
+
@change=${toggle("composed")}
|
|
588
|
+
>Composed</sp-checkbox
|
|
589
|
+
>
|
|
590
|
+
</div>
|
|
591
|
+
`,
|
|
592
|
+
})}
|
|
593
|
+
`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ─── Statement Card + List ───────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
function renderStatementCard(
|
|
599
|
+
stmt: JxStatement,
|
|
600
|
+
index: number,
|
|
601
|
+
list: JxStatement[],
|
|
602
|
+
commitList: (next: JxStatement[]) => void,
|
|
603
|
+
opts: StatementEditorOpts,
|
|
604
|
+
lanePath: LanePath,
|
|
605
|
+
): TemplateResult {
|
|
606
|
+
const kind = statementKind(stmt);
|
|
607
|
+
const commit = (next: JxStatement) => commitList(list.map((s, i) => (i === index ? next : s)));
|
|
608
|
+
|
|
609
|
+
let body: TemplateResult;
|
|
610
|
+
switch (kind) {
|
|
611
|
+
case "if": {
|
|
612
|
+
body = renderIfBody(stmt as JxIfStatement, commit, opts, lanePath, index);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case "switch": {
|
|
616
|
+
body = renderSwitchBody(stmt as JxSwitchStatement, commit, opts, lanePath, index);
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
case "dispatch": {
|
|
620
|
+
body = renderDispatchBody(stmt as JxDispatchStatement, commit, opts);
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
default: {
|
|
624
|
+
body = renderExpressionEditor(stmt, (n) => commit(n as JxStatement), {
|
|
625
|
+
allowEventRef: opts.allowEventRef,
|
|
626
|
+
stateDefs: opts.stateDefs,
|
|
627
|
+
stateEntries: opts.stateEntries ?? null,
|
|
628
|
+
});
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return html`
|
|
634
|
+
<div
|
|
635
|
+
class="statement-card"
|
|
636
|
+
data-stmt-row
|
|
637
|
+
data-stmt-kind=${kind}
|
|
638
|
+
data-stmt-lane=${JSON.stringify(lanePath)}
|
|
639
|
+
data-stmt-index=${index}
|
|
640
|
+
style="border:1px solid var(--spectrum-gray-300, #3c3c3c);border-radius:4px;padding:4px 6px;margin:0 0 6px;background:var(--spectrum-gray-75, #1e1e1e)"
|
|
641
|
+
>
|
|
642
|
+
<div class="statement-card-header" style="display:flex;align-items:center;gap:6px">
|
|
643
|
+
<span
|
|
644
|
+
class="statement-drag-handle"
|
|
645
|
+
title="Drag to reorder"
|
|
646
|
+
style="cursor:grab;color:var(--spectrum-gray-500, #6e6e6e);font-size:11px;line-height:1;user-select:none"
|
|
647
|
+
>⠿</span
|
|
648
|
+
>
|
|
649
|
+
<span class="statement-kind-label" style=${laneLabelStyle}>${kindLabel(stmt)}</span>
|
|
650
|
+
<span style="flex:1"></span>
|
|
651
|
+
<sp-action-button
|
|
652
|
+
quiet
|
|
653
|
+
size="xs"
|
|
654
|
+
class="statement-delete"
|
|
655
|
+
title="Delete statement"
|
|
656
|
+
@click=${() => commitList(list.filter((_, i) => i !== index))}
|
|
657
|
+
>
|
|
658
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
659
|
+
</sp-action-button>
|
|
660
|
+
</div>
|
|
661
|
+
<div class="statement-card-body">${body}</div>
|
|
662
|
+
</div>
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** One lane's vertical card list on its connector line, ending in the add-statement picker. */
|
|
667
|
+
function renderStatementList(
|
|
668
|
+
list: JxStatement[],
|
|
669
|
+
commitList: (next: JxStatement[]) => void,
|
|
670
|
+
opts: StatementEditorOpts,
|
|
671
|
+
lanePath: LanePath,
|
|
672
|
+
): TemplateResult {
|
|
673
|
+
return html`
|
|
674
|
+
<div
|
|
675
|
+
class="statement-list"
|
|
676
|
+
style="margin-left:4px;border-left:2px solid var(--spectrum-gray-300, #3c3c3c);padding:2px 0 0 8px"
|
|
677
|
+
>
|
|
678
|
+
${list.map((stmt, index) =>
|
|
679
|
+
renderStatementCard(stmt, index, list, commitList, opts, lanePath),
|
|
680
|
+
)}
|
|
681
|
+
${renderAddStatement(list, commitList)}
|
|
682
|
+
</div>
|
|
683
|
+
`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ─── Entry Point ─────────────────────────────────────────────────────────────
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Render a structured function body (spec §20) as an editable statement-card list. `onChange`
|
|
690
|
+
* receives a fresh statement array on every edit; the input array is never mutated.
|
|
691
|
+
*/
|
|
692
|
+
export function renderStatementEditor(
|
|
693
|
+
statements: JxStatement[],
|
|
694
|
+
onChange: (next: JxStatement[]) => void,
|
|
695
|
+
opts: StatementEditorOpts,
|
|
696
|
+
): TemplateResult {
|
|
697
|
+
const safe = Array.isArray(statements) ? statements : [];
|
|
698
|
+
return html`
|
|
699
|
+
<div
|
|
700
|
+
class="statement-editor"
|
|
701
|
+
${ref((el) => {
|
|
702
|
+
if (el) {
|
|
703
|
+
registerStatementsDnD(el as HTMLElement, safe, onChange);
|
|
704
|
+
}
|
|
705
|
+
})}
|
|
706
|
+
>
|
|
707
|
+
${renderStatementList(safe, onChange, opts, [])}
|
|
708
|
+
</div>
|
|
709
|
+
`;
|
|
710
|
+
}
|