@parallelworks/ui 0.37.0 → 0.38.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.
@@ -0,0 +1,1594 @@
1
+ import { useWorkflowEditing } from "../components/Provider.js";
2
+ import { BiSolidTrashAlt } from "../icons/bi/BiSolidTrashAlt.js";
3
+ import { BsArrowsCollapse } from "../icons/bs/BsArrowsCollapse.js";
4
+ import { BsDash } from "../icons/bs/BsDash.js";
5
+ import { BsFiles } from "../icons/bs/BsFiles.js";
6
+ import { BsThreeDots } from "../icons/bs/BsThreeDots.js";
7
+ import { FaArrowDown } from "../icons/fa/FaArrowDown.js";
8
+ import { FaArrowUp } from "../icons/fa/FaArrowUp.js";
9
+ import { FaEdit } from "../icons/fa/FaEdit.js";
10
+ import { FaPlus } from "../icons/fa/FaPlus.js";
11
+ import { GoPlus } from "../icons/go/GoPlus.js";
12
+ import { HiDuplicate } from "../icons/hi/HiDuplicate.js";
13
+ import { MdDragHandle } from "../icons/md/MdDragHandle.js";
14
+ import { TbAlertTriangle } from "../icons/tb/TbAlertTriangle.js";
15
+ import { TOOLTIP_ID } from "../components/Tooltip.js";
16
+ import { IconButton } from "../components/IconButton.js";
17
+ import { useRowMenu } from "../list/RowContextMenu.js";
18
+ import { MarqueeBox, createStore, reclaimFocus, returningFocus, trackDrag, useFocusAfterDialog, usePressOutside, useStore } from "./editorPrimitives.js";
19
+ import { useGraphEditorStrings, useInputsEditorStrings } from "./editorStrings.js";
20
+ import { FormEditingContext } from "../form/formEditing.js";
21
+ import { WizardPagesContext } from "../form/Wizard/wizardPages.js";
22
+ import { asRecord } from "./records.js";
23
+ import { openOnAddOf, text } from "./editorFields.js";
24
+ import { dropLayoutEdits, isWizard, resizeLayout, withInputLayouts } from "./inputLayout.js";
25
+ import { INPUT_TYPE_GROUPS, InputDialog, TypeBadge, allNames, newInputDefinition, withCreatedInputs } from "./InputDialog.js";
26
+ import { overlaps, typing } from "./editorApi.js";
27
+ import { ALT_KEY, MOD_KEY } from "./ShortcutsButton.js";
28
+ import { AddChip, BarDivider, DragLabel, EditorBar } from "./editorChrome.js";
29
+ import { addPageEdit, splitIntoPagesEdits, unsplitPagesEdits, wizardGroupDefinition } from "./inputPages.js";
30
+ import { inputCell, linesIn, rowsOf, snapSplit } from "./inputRows.js";
31
+ import cx from "classnames";
32
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
33
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
34
+ //#region src/graph/InputsEditor.tsx
35
+ var createFormStore = () => createStore({
36
+ drag: null,
37
+ edge: null,
38
+ resizing: false,
39
+ dialog: null,
40
+ hovered: null,
41
+ problems: [],
42
+ selection: [],
43
+ cursor: null,
44
+ cursorCopy: null,
45
+ open: [],
46
+ marquee: null
47
+ });
48
+ var ApiContext = createContext(null);
49
+ var InputsContext = createContext(void 0);
50
+ var EMPTY_STORE = createFormStore();
51
+ function useUi(store, select) {
52
+ return useStore(store, select);
53
+ }
54
+ function containerAt(editing, inputs, parent) {
55
+ let map = asRecord(inputs);
56
+ for (const name of parent) {
57
+ const definition = asRecord(map[name]);
58
+ const key = editing.inputChildrenKey(definition["type"]);
59
+ map = key ? asRecord(definition[key]) : {};
60
+ }
61
+ return map;
62
+ }
63
+ function namesIn(container) {
64
+ return Object.keys(container).filter((name) => name !== "$meta");
65
+ }
66
+ var key = (path) => JSON.stringify(path);
67
+ function samePath(a, b) {
68
+ return a.length === b.length && a.every((name, i) => b[i] === name);
69
+ }
70
+ function isInside(path, ancestor) {
71
+ return path.length >= ancestor.length && ancestor.every((name, i) => path[i] === name);
72
+ }
73
+ var below = (path, ancestor) => path.length > ancestor.length && isInside(path, ancestor);
74
+ var pathOf = (rowKey) => JSON.parse(rowKey);
75
+ function outermost(paths) {
76
+ return paths.filter((path) => !paths.some((other) => below(path, other)));
77
+ }
78
+ function contains(rect, box) {
79
+ return rect.left <= box.left && rect.right >= box.right && rect.top <= box.top && rect.bottom >= box.bottom;
80
+ }
81
+ var CHROME = "[data-input-chrome], [data-input-add], [role=\"menu\"], [role=\"dialog\"]";
82
+ var CONTROLS = "input, textarea, select, button, a[href], label, [contenteditable=\"true\"], .monaco-editor, [role=\"button\"], [role=\"checkbox\"], [role=\"combobox\"], [role=\"listbox\"], [role=\"option\"], [role=\"radio\"], [role=\"slider\"], [role=\"switch\"], [role=\"tab\"]";
83
+ var KEYED_WIDGETS = ".monaco-editor, [role=\"combobox\"], [role=\"listbox\"], [role=\"option\"], [role=\"radio\"], [role=\"slider\"], [role=\"switch\"], [role=\"tab\"]";
84
+ function rowsIn(root) {
85
+ return [...root.querySelectorAll("[data-input-path]")].map((el) => ({
86
+ path: pathOf(el.dataset["inputPath"] ?? "[]"),
87
+ copy: el.dataset["inputInstance"] ?? "",
88
+ rect: el.getBoundingClientRect()
89
+ }));
90
+ }
91
+ /**
92
+ * The inputs a box picks: the ones it touches in the innermost group holding it, each taken
93
+ * whole, so a box works inside a group as well as across the form.
94
+ */
95
+ function inputsInBox(root, box) {
96
+ const touched = rowsIn(root).filter(({ rect }) => overlaps(rect, box));
97
+ const holder = touched.filter(({ path, rect }) => contains(rect, box) && touched.some((other) => below(other.path, path))).reduce((deepest, { path }) => deepest && deepest.length >= path.length ? deepest : path, null);
98
+ return outermost((holder ? touched.filter(({ path }) => below(path, holder)) : touched).map(({ path }) => path));
99
+ }
100
+ function ChromeButton({ label, onClick, children }) {
101
+ return /* @__PURE__ */ jsx("button", {
102
+ type: "button",
103
+ "aria-label": label,
104
+ "data-tooltip-id": TOOLTIP_ID,
105
+ "data-tooltip-content": label,
106
+ className: "flex h-5 w-5 cursor-pointer items-center justify-center rounded hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app)",
107
+ onClick: (e) => {
108
+ e.stopPropagation();
109
+ onClick(e);
110
+ },
111
+ children
112
+ });
113
+ }
114
+ /** An input's row: its field, with a toolbar of its type, drag grip and actions on hover. */
115
+ function InputRow({ path, hidden = false, instance, children }) {
116
+ const api = useContext(ApiContext);
117
+ const inputs = useContext(InputsContext);
118
+ const editing = useWorkflowEditing();
119
+ const t = useInputsEditorStrings();
120
+ const g = useGraphEditorStrings();
121
+ const rowKey = key(path);
122
+ const copy = instance ?? rowKey;
123
+ const active = useUi(api?.store ?? EMPTY_STORE, (state) => state.open.includes(copy) && !state.drag && !state.resizing);
124
+ const selected = useUi(api?.store ?? EMPTY_STORE, (state) => state.selection.includes(rowKey));
125
+ const problems = useUi(api?.store ?? EMPTY_STORE, (state) => state.problems.filter((problem) => problem.input && key(problem.input) === rowKey).map((problem) => `${problem.line}\n${problem.message}`).join("\n\n"));
126
+ if (!api) return children;
127
+ const found = problems ? problems.split("\n\n") : [];
128
+ const definition = api.definition(path);
129
+ const name = path.at(-1) ?? "";
130
+ const parent = path.slice(0, -1);
131
+ const index = namesIn(containerAt(editing, inputs, parent)).indexOf(name);
132
+ const childKey = hidden ? editing.inputChildrenKey(definition["type"]) : void 0;
133
+ const outline = childKey && childKey !== "template" ? namesIn(asRecord(definition[childKey])) : null;
134
+ const flagged = hidden && definition["hidden"] !== void 0 && definition["hidden"] !== false;
135
+ const actions = /* @__PURE__ */ jsxs(Fragment$1, { children: [
136
+ /* @__PURE__ */ jsx("button", {
137
+ type: "button",
138
+ "aria-label": t.inputActions,
139
+ "data-drag-handle": true,
140
+ className: "flex h-5 w-5 cursor-grab items-center justify-center rounded hover:bg-(--theme-muted-panel-bg)",
141
+ onPointerDown: (e) => api.startDrag(e, path, name),
142
+ onClick: (e) => {
143
+ const r = e.currentTarget.getBoundingClientRect();
144
+ api.openMenu(r.left, r.bottom, path);
145
+ },
146
+ children: /* @__PURE__ */ jsx(MdDragHandle, { className: "h-3.5 w-3.5" })
147
+ }),
148
+ /* @__PURE__ */ jsx("span", {
149
+ "data-tooltip-id": TOOLTIP_ID,
150
+ "data-tooltip-content": name,
151
+ children: /* @__PURE__ */ jsx(TypeBadge, { type: text(definition["type"]) })
152
+ }),
153
+ (definition["type"] === "group" || definition["type"] === "list") && /* @__PURE__ */ jsx(PageActions, {
154
+ api,
155
+ parent: path,
156
+ list: definition[editing.inputChildrenKey(definition["type"]) ?? ""]
157
+ }),
158
+ /* @__PURE__ */ jsx(ChromeButton, {
159
+ label: t.editInput,
160
+ onClick: () => api.edit(path),
161
+ children: /* @__PURE__ */ jsx(FaEdit, { className: "h-3 w-3" })
162
+ }),
163
+ /* @__PURE__ */ jsx(ChromeButton, {
164
+ label: t.addInputBelow,
165
+ onClick: (e) => {
166
+ const r = e.currentTarget.getBoundingClientRect();
167
+ api.openTypeMenu(r.right, r.bottom, parent, index + 1);
168
+ },
169
+ children: /* @__PURE__ */ jsx(FaPlus, { className: "h-3 w-3" })
170
+ }),
171
+ /* @__PURE__ */ jsx(ChromeButton, {
172
+ label: t.deleteInput,
173
+ onClick: () => api.remove(path),
174
+ children: /* @__PURE__ */ jsx(BsDash, { className: "h-3 w-3" })
175
+ }),
176
+ /* @__PURE__ */ jsx(ChromeButton, {
177
+ label: t.inputActions,
178
+ onClick: (e) => {
179
+ const r = e.currentTarget.getBoundingClientRect();
180
+ api.openMenu(r.right, r.bottom, path);
181
+ },
182
+ children: /* @__PURE__ */ jsx(BsThreeDots, { className: "h-3 w-3" })
183
+ })
184
+ ] });
185
+ return /* @__PURE__ */ jsxs("div", {
186
+ role: "none",
187
+ "data-input-path": rowKey,
188
+ "data-input-instance": copy,
189
+ "data-input-index": index,
190
+ ...hidden ? { "data-input-hidden": "" } : {},
191
+ ...selected ? { "data-selected": "" } : {},
192
+ className: cx("relative w-full", hidden && "mb-[15px]", found.length > 0 && "rounded-md ring-2 ring-(--theme-error)", selected && "isolate before:pointer-events-none before:absolute before:-inset-x-2 before:-inset-y-1 before:-z-10 before:rounded-lg before:bg-(--theme-element)/15 before:ring-2 before:ring-(--theme-element) before:content-['']"),
193
+ onPointerOver: (e) => {
194
+ e.stopPropagation();
195
+ api.hover(copy);
196
+ },
197
+ onContextMenu: (e) => {
198
+ e.preventDefault();
199
+ e.stopPropagation();
200
+ api.openMenu(e.clientX, e.clientY, path);
201
+ },
202
+ children: [
203
+ hidden ? /* @__PURE__ */ jsxs("div", {
204
+ className: "flex items-center gap-1.5 text-[11px] leading-4 theme-muted-text",
205
+ children: [
206
+ /* @__PURE__ */ jsx("span", {
207
+ className: "font-mono",
208
+ children: name
209
+ }),
210
+ flagged && /* @__PURE__ */ jsx("span", {
211
+ className: "italic",
212
+ children: t.hidden
213
+ }),
214
+ /* @__PURE__ */ jsx("div", {
215
+ "data-input-chrome": true,
216
+ className: "ml-auto flex items-center gap-0.5",
217
+ children: actions
218
+ })
219
+ ]
220
+ }) : /* @__PURE__ */ jsx("div", {
221
+ className: cx("flex justify-end overflow-hidden transition-[height] duration-150 focus-within:h-7", active ? "h-7" : "h-0"),
222
+ children: /* @__PURE__ */ jsx("div", {
223
+ "data-input-chrome": true,
224
+ className: "flex h-6 items-center gap-0.5 rounded-md border theme-border bg-(--theme-panel-bg) px-1 py-0.5 text-[11px] leading-4 theme-muted-text shadow-sm",
225
+ children: actions
226
+ })
227
+ }),
228
+ found.length > 0 && /* @__PURE__ */ jsx("button", {
229
+ type: "button",
230
+ "data-input-chrome": true,
231
+ "aria-label": g.problemCount(found.length),
232
+ "data-tooltip-id": "global-tooltip",
233
+ "data-tooltip-content": found.map((problem) => problem.slice(problem.indexOf("\n") + 1)).join("\n"),
234
+ className: "absolute -left-2 -top-2 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-(--theme-panel-bg) text-(--theme-error) shadow-sm",
235
+ onClick: () => api.reveal(Number.parseInt(found[0] ?? "0", 10)),
236
+ children: /* @__PURE__ */ jsx(TbAlertTriangle, { className: "h-3.5 w-3.5" })
237
+ }),
238
+ children,
239
+ outline && /* @__PURE__ */ jsxs("div", {
240
+ className: "ml-2 border-l theme-border pl-3",
241
+ children: [outline.map((child) => /* @__PURE__ */ jsx(InputRow, {
242
+ path: [...path, child],
243
+ hidden: true
244
+ }, child)), /* @__PURE__ */ jsx(AddInput, { parent: path })]
245
+ })
246
+ ]
247
+ });
248
+ }
249
+ /**
250
+ * A list's page commands: split it into pages, or, once it is a wizard, add a page or put its
251
+ * inputs back. `bar` is the editor's own bar, for the form; otherwise a group's or list's row.
252
+ */
253
+ function PageActions({ api, parent, list, bar = false }) {
254
+ const t = useInputsEditorStrings();
255
+ if (!isWizard(list)) return bar ? /* @__PURE__ */ jsx(IconButton, {
256
+ icon: /* @__PURE__ */ jsx(BsFiles, { className: "h-4 w-4" }),
257
+ label: t.splitIntoPages,
258
+ size: "sm",
259
+ variant: "ghost",
260
+ onClick: () => api.splitIntoPages(parent)
261
+ }) : /* @__PURE__ */ jsx(ChromeButton, {
262
+ label: t.splitIntoPages,
263
+ onClick: () => api.splitIntoPages(parent),
264
+ children: /* @__PURE__ */ jsx(BsFiles, { className: "h-3 w-3" })
265
+ });
266
+ return bar ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(AddChip, {
267
+ icon: /* @__PURE__ */ jsx(FaPlus, { className: "h-3 w-3" }),
268
+ label: t.page,
269
+ hint: t.addPage,
270
+ onClick: () => api.addPage(parent)
271
+ }), /* @__PURE__ */ jsx(IconButton, {
272
+ icon: /* @__PURE__ */ jsx(BsArrowsCollapse, { className: "h-4 w-4" }),
273
+ label: t.unsplitPages,
274
+ size: "sm",
275
+ variant: "ghost",
276
+ onClick: () => api.unsplitPages(parent)
277
+ })] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ChromeButton, {
278
+ label: t.addPage,
279
+ onClick: () => api.addPage(parent),
280
+ children: /* @__PURE__ */ jsx(GoPlus, { className: "h-3 w-3" })
281
+ }), /* @__PURE__ */ jsx(ChromeButton, {
282
+ label: t.unsplitPages,
283
+ onClick: () => api.unsplitPages(parent),
284
+ children: /* @__PURE__ */ jsx(BsArrowsCollapse, { className: "h-3 w-3" })
285
+ })] });
286
+ }
287
+ function pagesIn(list) {
288
+ const record = asRecord(list);
289
+ return namesIn(record).filter((name) => asRecord(record[name])["type"] === "step");
290
+ }
291
+ /** The bar's page of the form's wizard, turned by typing its number or with the arrow keys. */
292
+ function PageNumber({ count, page, onChange }) {
293
+ const t = useInputsEditorStrings();
294
+ const shown = Math.min(Math.max(page, 0), Math.max(count - 1, 0));
295
+ return /* @__PURE__ */ jsxs("label", {
296
+ className: "flex h-7 items-center gap-1 px-1 text-xs theme-muted-text",
297
+ children: [
298
+ t.page,
299
+ /* @__PURE__ */ jsx("input", {
300
+ type: "number",
301
+ min: 1,
302
+ max: count,
303
+ value: shown + 1,
304
+ "aria-label": t.pageNumber,
305
+ className: "h-6 w-10 rounded border theme-border bg-(--theme-input-bg) px-1 text-center text-(--theme-input) text-xs",
306
+ onChange: (e) => {
307
+ const typed = Number(e.target.value);
308
+ if (Number.isInteger(typed) && typed >= 1 && typed <= count) onChange(typed - 1);
309
+ }
310
+ }),
311
+ /* @__PURE__ */ jsxs("span", { children: ["/ ", count] })
312
+ ]
313
+ });
314
+ }
315
+ var ADD_INDENT = [
316
+ "",
317
+ "ml-4",
318
+ "ml-8",
319
+ "ml-12"
320
+ ];
321
+ /** The add button that ends each list of inputs, naming the list it adds to. */
322
+ function AddInput({ parent }) {
323
+ const api = useContext(ApiContext);
324
+ const t = useInputsEditorStrings();
325
+ if (!api) return null;
326
+ const holder = parent.length > 0 ? api.definition(parent) : {};
327
+ const name = text(holder["label"]) || text(holder["title"]) || (parent.at(-1) ?? "");
328
+ return /* @__PURE__ */ jsxs("button", {
329
+ type: "button",
330
+ "data-input-add": key(parent),
331
+ className: cx("mb-[15px] flex cursor-pointer items-center gap-1.5 text-[13px] theme-muted-text hover:text-(--theme-link)", ADD_INDENT[Math.min(parent.length, ADD_INDENT.length - 1)]),
332
+ onClick: (e) => {
333
+ const r = e.currentTarget.getBoundingClientRect();
334
+ api.openTypeMenu(r.left, r.bottom, parent, Number.MAX_SAFE_INTEGER);
335
+ },
336
+ children: [/* @__PURE__ */ jsx(FaPlus, { className: "h-3 w-3" }), parent.length === 0 ? t.addInputHere : t.addFieldTo(name)]
337
+ });
338
+ }
339
+ var EDITING = {
340
+ parent: [],
341
+ Row: InputRow,
342
+ Add: AddInput
343
+ };
344
+ function typeSearch(labels) {
345
+ return {
346
+ placeholder: labels.searchTypes,
347
+ empty: labels.noMatchingTypes
348
+ };
349
+ }
350
+ function typeMenu(labels, pick) {
351
+ const types = labels.types;
352
+ const item = (type) => ({
353
+ kind: "action",
354
+ label: types[type] ?? type,
355
+ keywords: type,
356
+ onSelect: () => pick(type)
357
+ });
358
+ return [...INPUT_TYPE_GROUPS.map(([group, list]) => ({
359
+ kind: "submenu",
360
+ label: labels.typeGroups[group],
361
+ items: list.map(item)
362
+ }))];
363
+ }
364
+ var extent = (rows) => ({
365
+ left: Math.min(...rows.map((row) => row.rect.left)),
366
+ right: Math.max(...rows.map((row) => row.rect.right))
367
+ });
368
+ /** Gaps between lines: above each line, after a list's shown inputs, and at each list's end. */
369
+ function lineGaps(root, lines) {
370
+ const gaps = [];
371
+ const lists = /* @__PURE__ */ new Map();
372
+ for (const line of lines) {
373
+ const rows = rowsOf(line);
374
+ if (rows.length === 0) continue;
375
+ gaps.push({
376
+ ...aboveGap(line),
377
+ ...rows.every((row) => row.trailing) ? { trailing: true } : {}
378
+ });
379
+ const list = lists.get(key(line.parent)) ?? {
380
+ parent: line.parent,
381
+ rows: []
382
+ };
383
+ list.rows.push(...rows);
384
+ lists.set(key(line.parent), list);
385
+ }
386
+ for (const { parent, rows } of lists.values()) {
387
+ const shown = rows.filter((row) => !row.trailing);
388
+ if (shown.length > 0 && shown.length < rows.length) gaps.push({
389
+ parent,
390
+ index: Math.max(...shown.map((row) => row.index)) + 1,
391
+ ...extent(shown),
392
+ y: Math.max(...shown.map((row) => row.rect.bottom))
393
+ });
394
+ }
395
+ for (const { parent, rows } of lists.values()) gaps.push({
396
+ parent,
397
+ index: Math.max(...rows.map((row) => row.index)) + 1,
398
+ ...extent(rows),
399
+ y: Math.max(...rows.map((row) => row.rect.bottom))
400
+ });
401
+ for (const el of root.querySelectorAll("[data-input-add]")) {
402
+ const parent = JSON.parse(el.dataset["inputAdd"] ?? "[]");
403
+ if (!lists.has(key(parent))) {
404
+ const rect = el.getBoundingClientRect();
405
+ gaps.push({
406
+ parent,
407
+ index: 0,
408
+ left: rect.left,
409
+ right: rect.right,
410
+ y: rect.top
411
+ });
412
+ }
413
+ }
414
+ return gaps;
415
+ }
416
+ var into = (parent, mover) => mover.paths?.some((path) => isInside(parent, path)) ?? false;
417
+ function nearestLineGap(gaps, client, mover) {
418
+ let best = null;
419
+ let bestDistance = Number.POSITIVE_INFINITY;
420
+ for (const gap of gaps) {
421
+ if (into(gap.parent, mover) || gap.trailing && mover.shown) continue;
422
+ if (client.x < gap.left - 24 || client.x > gap.right + 24) continue;
423
+ const distance = Math.abs(client.y - gap.y);
424
+ if (distance < bestDistance || distance === bestDistance && best !== null && gap.parent.length > best.parent.length) {
425
+ best = gap;
426
+ bestDistance = distance;
427
+ }
428
+ }
429
+ return best;
430
+ }
431
+ /** A column of its own beside `column`, marked by a bar down that side. */
432
+ function besideGap(line, column, side) {
433
+ const { rows } = column;
434
+ const own = extent(rows);
435
+ const i = line.columns.indexOf(column);
436
+ const before = line.columns[i - 1];
437
+ const after = line.columns[i + 1];
438
+ let x = side === "left" ? own.left - 4 : own.right + 4;
439
+ if (side === "left" && before) x = (extent(before.rows).right + own.left) / 2;
440
+ else if (side === "right" && after) x = (own.right + extent(after.rows).left) / 2;
441
+ const top = Math.min(...rows.map((row) => row.rect.top));
442
+ return {
443
+ parent: line.parent,
444
+ index: side === "left" ? Math.min(...rows.map((row) => row.index)) : Math.max(...rows.map((row) => row.index)) + 1,
445
+ ...own,
446
+ y: top,
447
+ beside: {
448
+ path: rows[0].path,
449
+ side,
450
+ x,
451
+ top,
452
+ bottom: Math.max(...rows.map((row) => row.rect.bottom))
453
+ }
454
+ };
455
+ }
456
+ /** Above or below `row` in its column, marked by a bar across the column. */
457
+ function stackGap(line, column, row, side) {
458
+ return {
459
+ parent: line.parent,
460
+ index: side === "above" ? row.index : row.index + 1,
461
+ ...extent(column.rows),
462
+ y: side === "above" ? row.rect.top : row.rect.bottom,
463
+ stack: {
464
+ path: row.path,
465
+ side
466
+ }
467
+ };
468
+ }
469
+ function aboveGap(line) {
470
+ const rows = rowsOf(line);
471
+ return {
472
+ parent: line.parent,
473
+ index: Math.min(...rows.map((row) => row.index)),
474
+ ...extent(rows),
475
+ y: Math.min(...rows.map((row) => row.rect.top))
476
+ };
477
+ }
478
+ function belowGap(line) {
479
+ const rows = rowsOf(line);
480
+ return {
481
+ parent: line.parent,
482
+ index: Math.max(...rows.map((row) => row.index)) + 1,
483
+ ...extent(rows),
484
+ y: Math.max(...rows.map((row) => row.rect.bottom))
485
+ };
486
+ }
487
+ /** A column narrower than its list stacks a drop on its inputs' halves instead of starting a line. */
488
+ function stacks(line, column) {
489
+ const width = column.cell?.style.getPropertyValue(WIDTH_VAR) ?? "";
490
+ return line.columns.length > 1 || column.rows.length > 1 || width !== "" && width !== "100%";
491
+ }
492
+ /** Where a group's own inputs start, below its title; null for an input holding none drawn. */
493
+ function bodyTop(root, lines, row) {
494
+ const tops = lines.filter((line) => samePath(line.parent, row.path)).flatMap((line) => rowsOf(line).map((child) => child.rect));
495
+ for (const el of root.querySelectorAll("[data-input-add]")) if (el.dataset["inputAdd"] === key(row.path)) tops.push(el.getBoundingClientRect());
496
+ const drawn = tops.filter((rect) => rect.height > 0).map((rect) => rect.top);
497
+ return drawn.length > 0 ? Math.min(...drawn) : null;
498
+ }
499
+ /**
500
+ * Where a drop at `client` lands: on an input's left or right quarter, in a column beside its
501
+ * own; over its middle, above or below it by which half, in its column or as a line of its own.
502
+ */
503
+ function dropGap(root, client, mover) {
504
+ const lines = linesIn(root);
505
+ const gaps = lineGaps(root, lines);
506
+ const moving = (path) => mover.paths?.some((other) => isInside(path, other)) ?? false;
507
+ const under = (rect) => client.x >= rect.left && client.x <= rect.right && client.y >= rect.top && client.y <= rect.bottom;
508
+ let hit = null;
509
+ for (const line of lines) for (const column of line.columns) for (const row of column.rows) if (!moving(row.path) && under(row.rect) && (!hit || row.path.length > hit.row.path.length)) hit = {
510
+ line,
511
+ column,
512
+ row
513
+ };
514
+ if (hit) {
515
+ const { line, column, row } = hit;
516
+ const body = bodyTop(root, lines, row);
517
+ if (body !== null && client.y >= body) return nearestLineGap(gaps.filter((gap) => samePath(gap.parent, row.path)), client, mover);
518
+ if (row.trailing && mover.shown) return nearestLineGap(gaps.filter((gap) => samePath(gap.parent, line.parent)), client, mover);
519
+ const { rect } = row;
520
+ const quarter = rect.width / 4;
521
+ const shares = !mover.hidden && !row.hidden;
522
+ if (shares && client.x < rect.left + quarter) return besideGap(line, column, "left");
523
+ if (shares && client.x > rect.right - quarter) return besideGap(line, column, "right");
524
+ const above = client.y < (rect.top + (body ?? rect.bottom)) / 2;
525
+ if (shares && stacks(line, column)) return stackGap(line, column, row, above ? "above" : "below");
526
+ return above ? aboveGap(line) : belowGap(line);
527
+ }
528
+ if (!mover.hidden) for (const line of lines) {
529
+ if (into(line.parent, mover)) continue;
530
+ for (let i = 1; i < line.columns.length; i++) {
531
+ const before = line.columns[i - 1];
532
+ const after = line.columns[i];
533
+ if (!before || !after) continue;
534
+ const left = extent(before.rows).right;
535
+ const right = extent(after.rows).left;
536
+ const rows = [...before.rows, ...after.rows];
537
+ if (client.x <= left || client.x >= right || client.y < Math.min(...rows.map((row) => row.rect.top)) || client.y > Math.max(...rows.map((row) => row.rect.bottom))) continue;
538
+ if (before.rows.some((row) => !moving(row.path))) return besideGap(line, before, "right");
539
+ if (after.rows.some((row) => !moving(row.path))) return besideGap(line, after, "left");
540
+ }
541
+ }
542
+ return nearestLineGap(gaps, client, mover);
543
+ }
544
+ var WIDTH_VAR = "--input-width";
545
+ /** The cells two neighbouring columns sit in, by their heads, and the row of cells holding them. */
546
+ function cellsAt(root, edge) {
547
+ const cellOf = (path, copy) => {
548
+ const el = [...root.querySelectorAll("[data-input-path]")].find((el) => el.dataset["inputPath"] === key(path) && el.dataset["inputInstance"] === copy);
549
+ return el ? inputCell(el) : null;
550
+ };
551
+ const left = cellOf(edge.left, edge.copies[0]);
552
+ const right = cellOf(edge.right, edge.copies[1]);
553
+ const flow = left?.parentElement;
554
+ return left && right && flow && right.parentElement === flow ? {
555
+ left,
556
+ right,
557
+ flow
558
+ } : null;
559
+ }
560
+ function edgeOf(root, before, after) {
561
+ const [leftHead] = before.rows;
562
+ const [rightHead] = after.rows;
563
+ const copies = [leftHead?.instance ?? "", rightHead?.instance ?? ""];
564
+ const cells = leftHead && rightHead && cellsAt(root, {
565
+ left: leftHead.path,
566
+ right: rightHead.path,
567
+ copies
568
+ });
569
+ const width = cells ? cells.flow.getBoundingClientRect().width : 0;
570
+ if (!leftHead || !rightHead || !cells || width <= 0) return null;
571
+ const share = (cell) => Math.floor(cell.getBoundingClientRect().width / width * 100);
572
+ const rows = [...before.rows, ...after.rows];
573
+ return {
574
+ left: leftHead.path,
575
+ right: rightHead.path,
576
+ copies,
577
+ x: cells.left.getBoundingClientRect().right,
578
+ top: Math.min(...rows.map((row) => row.rect.top)),
579
+ bottom: Math.max(...rows.map((row) => row.rect.bottom)),
580
+ split: [share(cells.left), share(cells.right)]
581
+ };
582
+ }
583
+ /** The edge between columns on a line that the pointer is over, the innermost list's first. */
584
+ function edgeAt(root, x, y) {
585
+ let found = null;
586
+ for (const { parent, columns } of linesIn(root)) for (let i = 1; i < columns.length; i++) {
587
+ const before = columns[i - 1];
588
+ const after = columns[i];
589
+ if (!before || !after) continue;
590
+ const rows = [...before.rows, ...after.rows];
591
+ if (x < extent(before.rows).right - 2 || x > extent(after.rows).left + 2 || y < Math.min(...rows.map((row) => row.rect.top)) || y > Math.max(...rows.map((row) => row.rect.bottom))) continue;
592
+ if (!found || parent.length > found.depth) found = {
593
+ depth: parent.length,
594
+ before,
595
+ after
596
+ };
597
+ }
598
+ return found ? edgeOf(root, found.before, found.after) : null;
599
+ }
600
+ /** The edge between the same two columns, by their heads, if they still share a line. */
601
+ function edgeBetween(root, left, right) {
602
+ for (const { columns } of linesIn(root)) {
603
+ const i = columns.findIndex((column) => samePath(column.rows[0]?.path ?? [], left));
604
+ const before = columns[i];
605
+ const after = columns[i + 1];
606
+ if (before && after && samePath(after.rows[0]?.path ?? [], right)) return edgeOf(root, before, after);
607
+ }
608
+ return null;
609
+ }
610
+ var sameEdge = (a, b) => a === b || a !== null && b !== null && samePath(a.left, b.left) && samePath(a.right, b.right) && Math.round(a.x) === Math.round(b.x);
611
+ var DELETE_KEYS = /* @__PURE__ */ new Set(["Delete", "Backspace"]);
612
+ var ARROWS = {
613
+ ArrowUp: "up",
614
+ ArrowDown: "down",
615
+ ArrowLeft: "left",
616
+ ArrowRight: "right"
617
+ };
618
+ /** The input an arrow key reaches from `from` as the form draws them: the nearest one starting
619
+ * above or below it, or beside it, those in line with it first. */
620
+ function neighbour(rows, from, direction) {
621
+ const r = from.rect;
622
+ const middle = (rect) => ({
623
+ x: (rect.left + rect.right) / 2,
624
+ y: (rect.top + rect.bottom) / 2
625
+ });
626
+ const distance = (rect) => Math.hypot(middle(rect).x - middle(r).x, middle(rect).y - middle(r).y);
627
+ const others = rows.filter((row) => row !== from);
628
+ const vertical = direction === "up" || direction === "down";
629
+ const ahead = others.filter(({ rect }) => direction === "down" ? rect.top > r.top + 1 : direction === "up" ? rect.top < r.top - 1 : direction === "right" ? rect.left >= r.right - 1 : rect.right <= r.left + 1);
630
+ const inLine = ahead.filter(({ rect }) => vertical ? Math.min(rect.right, r.right) > Math.max(rect.left, r.left) : Math.min(rect.bottom, r.bottom) > Math.max(rect.top, r.top));
631
+ const gap = ({ rect }) => direction === "down" ? rect.top - r.top : direction === "up" ? r.top - rect.top : direction === "right" ? rect.left - r.right : r.left - rect.right;
632
+ return (inLine.length > 0 ? inLine : ahead).reduce((best, row) => !best || gap(row) < gap(best) || gap(row) === gap(best) && distance(row.rect) < distance(best.rect) ? row : best, void 0);
633
+ }
634
+ function undoKeys(e, editor) {
635
+ const pressed = e.key.toLowerCase();
636
+ if (pressed === "z" && !e.shiftKey) {
637
+ e.preventDefault();
638
+ editor.onUndo();
639
+ } else if (pressed === "z" && e.shiftKey || pressed === "y") {
640
+ e.preventDefault();
641
+ editor.onRedo();
642
+ }
643
+ }
644
+ function formShortcuts(g) {
645
+ const key = g.shortcutKeys;
646
+ const does = g.shortcutDoes;
647
+ return [{
648
+ title: g.shortcutGroups.select,
649
+ shortcuts: [
650
+ {
651
+ combos: [[key.shift, key.click]],
652
+ does: does.selectInput
653
+ },
654
+ {
655
+ combos: [[key.shift, key.drag]],
656
+ does: does.boxInputs
657
+ },
658
+ {
659
+ combos: [[
660
+ "←",
661
+ "↑",
662
+ "↓",
663
+ "→"
664
+ ]],
665
+ does: does.pickInput
666
+ },
667
+ {
668
+ combos: [[key.shift, "↑"], [key.shift, "↓"]],
669
+ does: does.extendInputs
670
+ },
671
+ {
672
+ combos: [["Esc"]],
673
+ does: does.escape
674
+ }
675
+ ]
676
+ }, {
677
+ title: g.shortcutGroups.change,
678
+ shortcuts: [
679
+ {
680
+ combos: [[key.dragHandle]],
681
+ does: does.moveInput
682
+ },
683
+ {
684
+ combos: [[key.dragEdge]],
685
+ does: does.resizeInputs
686
+ },
687
+ {
688
+ combos: [[ALT_KEY, "↑"], [ALT_KEY, "↓"]],
689
+ does: does.reorderInputs
690
+ },
691
+ {
692
+ combos: [[key.dragButton]],
693
+ does: does.addInput
694
+ },
695
+ {
696
+ combos: [["E"]],
697
+ does: does.editInput
698
+ },
699
+ {
700
+ combos: [[key.delete]],
701
+ does: does.removeInputs
702
+ },
703
+ {
704
+ combos: [[key.rightClick]],
705
+ does: does.inputMenu
706
+ },
707
+ {
708
+ combos: [[MOD_KEY, "Z"], [
709
+ key.shift,
710
+ MOD_KEY,
711
+ "Z"
712
+ ]],
713
+ does: does.undo
714
+ }
715
+ ]
716
+ }];
717
+ }
718
+ /**
719
+ * Turns the input form rendered in `children` into an editor of the
720
+ * workflow's `on.execute.inputs`, with edits sent through `editor`.
721
+ */
722
+ function InputsFormEditor({ editor, inputs, children }) {
723
+ const t = useInputsEditorStrings();
724
+ const g = useGraphEditorStrings();
725
+ const editing = useWorkflowEditing();
726
+ const [store] = useState(createFormStore);
727
+ const [pages, setPages] = useState({});
728
+ const [counts, setCounts] = useState({});
729
+ const setPage = useCallback((wizard, index) => setPages((now) => ({
730
+ ...now,
731
+ [wizard]: index
732
+ })), []);
733
+ const setCount = useCallback((wizard, count) => setCounts((now) => now[wizard] === count ? now : {
734
+ ...now,
735
+ [wizard]: count
736
+ }), []);
737
+ const wizardPages = useMemo(() => ({
738
+ page: (wizard) => Math.min(pages[wizard] ?? 0, Math.max((counts[wizard] ?? 1) - 1, 0)),
739
+ setPage,
740
+ count: (wizard) => counts[wizard],
741
+ setCount
742
+ }), [
743
+ pages,
744
+ counts,
745
+ setPage,
746
+ setCount
747
+ ]);
748
+ const containerRef = useRef(null);
749
+ const latest = useRef({
750
+ editor,
751
+ inputs
752
+ });
753
+ latest.current = {
754
+ editor,
755
+ inputs
756
+ };
757
+ const rowMenu = useRowMenu();
758
+ const openMenu = useMemo(() => returningFocus(rowMenu.openMenu, () => containerRef.current), [rowMenu.openMenu]);
759
+ const contextMenu = rowMenu.contextMenu;
760
+ useFocusAfterDialog(store, () => containerRef.current);
761
+ usePressOutside(() => containerRef.current, () => {
762
+ if (store.get().selection.length > 0) store.set({ selection: [] });
763
+ });
764
+ const api = useMemo(() => {
765
+ const send = (edit) => {
766
+ const result = latest.current.editor.onEdit(withInputLayouts(editing, latest.current.inputs, edit));
767
+ reclaimFocus(containerRef.current);
768
+ return result;
769
+ };
770
+ const rowElement = (rowKey) => [...containerRef.current?.querySelectorAll("[data-input-path]") ?? []].find((el) => el.dataset["inputPath"] === rowKey);
771
+ const definition = (path) => path.length === 0 ? asRecord(latest.current.inputs) : asRecord(containerAt(editing, latest.current.inputs, path.slice(0, -1))[path.at(-1) ?? ""]);
772
+ const indexOf = (path) => namesIn(containerAt(editing, latest.current.inputs, path.slice(0, -1))).indexOf(path.at(-1) ?? "");
773
+ const create = (parent, index, type, placement) => {
774
+ const { editor, inputs } = latest.current;
775
+ const wizardAbove = parent.slice(0, -1);
776
+ if (type === "step" && parent.length > 0 && definition(parent)["type"] === "step" && isWizard(containerAt(editing, inputs, wizardAbove))) {
777
+ addPage(wizardAbove);
778
+ return;
779
+ }
780
+ const container = containerAt(editing, inputs, parent);
781
+ if (type !== "step" && isWizard(container)) {
782
+ const pages = namesIn(container);
783
+ const page = pages[Math.max(Math.min(index, pages.length) - 1, 0)];
784
+ if (page !== void 0) {
785
+ const into = [...parent, page];
786
+ create(into, namesIn(containerAt(editing, inputs, into)).length, type, placement);
787
+ return;
788
+ }
789
+ }
790
+ const ownWizard = type === "step" && !isWizard(container) ? wizardGroupDefinition(t.stepsGroup, t.stepTitle(1)) : void 0;
791
+ if (editor.openOnAdd === false) {
792
+ const name = nextInputName(editing, inputs);
793
+ const add = {
794
+ type: "addInput",
795
+ parent,
796
+ index: Math.min(index, namesIn(container).length),
797
+ name,
798
+ definition: ownWizard ?? newInputDefinition(type)
799
+ };
800
+ send(placeNewInput(editing, inputs, add, parent, name, placement));
801
+ return;
802
+ }
803
+ store.set({ dialog: {
804
+ kind: "create",
805
+ parent,
806
+ index,
807
+ type: ownWizard ? "group" : type,
808
+ ...ownWizard ? { definition: ownWizard } : {},
809
+ ...placement ? { placement } : {}
810
+ } });
811
+ };
812
+ const listAt = (parent) => containerAt(editing, latest.current.inputs, parent);
813
+ const childKeyAt = (parent) => parent.length > 0 ? editing.inputChildrenKey(definition(parent)["type"]) : void 0;
814
+ const sendAll = ([only, ...more]) => {
815
+ if (only) send(more.length > 0 ? {
816
+ type: "batch",
817
+ edits: [only, ...more]
818
+ } : only);
819
+ };
820
+ const splitIntoPages = (parent) => sendAll(splitIntoPagesEdits(editing, listAt(parent), parent, childKeyAt(parent), t.stepTitle(1), parent.length === 0 ? t.submitLabel : void 0));
821
+ const addPage = (parent) => {
822
+ const list = listAt(parent);
823
+ send(addPageEdit(editing, list, parent, t.stepTitle));
824
+ setPages((now) => ({
825
+ ...now,
826
+ [key(parent)]: Number.MAX_SAFE_INTEGER
827
+ }));
828
+ };
829
+ const unsplitPages = (parent) => sendAll(unsplitPagesEdits(listAt(parent), parent, childKeyAt(parent)));
830
+ const openTypeMenu = (x, y, parent, index, placement) => openMenu(x, y, typeMenu(t, (type) => create(parent, index, type, placement)), void 0, typeSearch(t));
831
+ const edit = (path) => store.set({ dialog: {
832
+ kind: "edit",
833
+ path
834
+ } });
835
+ const remove = (path) => send({
836
+ type: "deleteInput",
837
+ path
838
+ });
839
+ const inFormOrder = (paths) => {
840
+ const order = rowsIn(containerRef.current ?? document.body).map((row) => key(row.path));
841
+ return [...paths].sort((a, b) => order.indexOf(key(a)) - order.indexOf(key(b)));
842
+ };
843
+ const staysPut = (paths, gap) => {
844
+ if (!paths.every((path) => samePath(path.slice(0, -1), gap.parent))) return false;
845
+ const at = paths.map(indexOf).sort((a, b) => a - b);
846
+ const first = at[0] ?? 0;
847
+ const last = at.at(-1) ?? 0;
848
+ return at.every((index, i) => index === first + i) && gap.index >= first && gap.index <= last + 1;
849
+ };
850
+ const dropOf = (gap) => ({
851
+ parent: gap.parent,
852
+ index: gap.index,
853
+ ...gap.beside ? { beside: {
854
+ path: gap.beside.path,
855
+ side: gap.beside.side
856
+ } } : {},
857
+ ...gap.stack ? { stack: gap.stack } : {}
858
+ });
859
+ const placementOf = (gap) => gap.beside || gap.stack ? { drop: dropOf(gap) } : void 0;
860
+ const followSelection = (moved, parent) => store.set({ selection: store.get().selection.map((rowKey) => {
861
+ const path = pathOf(rowKey);
862
+ const from = moved.find((other) => isInside(path, other));
863
+ return from ? key([...parent, ...path.slice(from.length - 1)]) : rowKey;
864
+ }) });
865
+ const removeSelection = () => {
866
+ const edits = inFormOrder(outermost(store.get().selection.map(pathOf))).map((path) => ({
867
+ type: "deleteInput",
868
+ path
869
+ }));
870
+ const [only] = edits;
871
+ if (!only) return false;
872
+ if (send(edits.length === 1 ? only : {
873
+ type: "batch",
874
+ edits
875
+ })) store.set({ selection: [] });
876
+ return true;
877
+ };
878
+ const moveSelection = (delta) => {
879
+ const paths = inFormOrder(outermost(store.get().selection.map(pathOf)));
880
+ const [first] = paths;
881
+ if (!first) return false;
882
+ const parent = first.slice(0, -1);
883
+ if (!paths.every((path) => samePath(path.slice(0, -1), parent))) return false;
884
+ const at = paths.map(indexOf).sort((a, b) => a - b);
885
+ const count = namesIn(containerAt(editing, latest.current.inputs, parent)).length;
886
+ const index = delta < 0 ? (at[0] ?? 0) - 1 : (at.at(-1) ?? 0) + 2;
887
+ if (index < 0 || index > count) return false;
888
+ send({
889
+ type: "moveInputs",
890
+ paths,
891
+ parent,
892
+ index
893
+ });
894
+ return true;
895
+ };
896
+ const startDrag = (e, path, label, how = {}) => {
897
+ if (e.button !== 0) return;
898
+ e.stopPropagation();
899
+ const selected = store.get().selection.map(pathOf);
900
+ const paths = path ? selected.length > 1 && selected.some((other) => samePath(other, path)) ? inFormOrder(outermost(selected)) : [path] : null;
901
+ const shown = paths && paths.length > 1 ? paths.map((other) => other.at(-1)).join(", ") : label;
902
+ const hiddenPaths = (paths ?? []).filter((path) => containerRef.current?.querySelector(`[data-input-path='${key(path)}']`)?.hasAttribute("data-input-hidden"));
903
+ const mover = {
904
+ paths,
905
+ hidden: hiddenPaths.length > 0,
906
+ shown: !paths || hiddenPaths.length < paths.length
907
+ };
908
+ const onEnd = (client, dragged) => {
909
+ const gap = store.get().drag?.gap ?? null;
910
+ store.set({ drag: null });
911
+ if (!dragged) {
912
+ how.onClick?.();
913
+ return;
914
+ }
915
+ if (!gap) return;
916
+ if (!paths) {
917
+ openTypeMenu(client.x, client.y, gap.parent, gap.index, placementOf(gap));
918
+ return;
919
+ }
920
+ containerRef.current?.focus({ preventScroll: true });
921
+ const drop = dropOf(gap);
922
+ const move = staysPut(paths, gap) ? {
923
+ type: "batch",
924
+ edits: []
925
+ } : {
926
+ type: "moveInputs",
927
+ paths,
928
+ parent: gap.parent,
929
+ index: gap.index
930
+ };
931
+ const edits = [move, ...dropLayoutEdits(editing, latest.current.inputs, paths, drop, move)];
932
+ const [only] = edits;
933
+ if (only && send(edits.length === 1 ? only : {
934
+ type: "batch",
935
+ edits
936
+ })) followSelection(paths, gap.parent);
937
+ };
938
+ trackDrag({
939
+ x: e.clientX,
940
+ y: e.clientY
941
+ }, {
942
+ escape: true,
943
+ swallowClick: how.swallowClick ?? "drag",
944
+ ...how.holdText ? { holdText: how.holdText } : {},
945
+ onMove: (client) => {
946
+ const root = containerRef.current;
947
+ const found = root ? dropGap(root, client, mover) : null;
948
+ const pages = paths?.every((path) => definition(path)["type"] === "step") ?? false;
949
+ const gap = found && (pages || !isWizard(containerAt(editing, latest.current.inputs, found.parent))) ? found : null;
950
+ store.set({ drag: {
951
+ paths,
952
+ label: shown,
953
+ client,
954
+ gap
955
+ } });
956
+ },
957
+ onEnd,
958
+ onCancel: () => store.set({ drag: null })
959
+ });
960
+ };
961
+ const menuFor = (path) => {
962
+ const parent = path.slice(0, -1);
963
+ const index = indexOf(path);
964
+ const count = namesIn(containerAt(editing, latest.current.inputs, parent)).length;
965
+ return [
966
+ {
967
+ kind: "action",
968
+ label: t.editInput,
969
+ icon: /* @__PURE__ */ jsx(FaEdit, {}),
970
+ onSelect: () => edit(path)
971
+ },
972
+ {
973
+ kind: "submenu",
974
+ label: t.addInputBelow,
975
+ icon: /* @__PURE__ */ jsx(FaPlus, {}),
976
+ items: typeMenu(t, (type) => create(parent, index + 1, type)),
977
+ search: typeSearch(t)
978
+ },
979
+ {
980
+ kind: "action",
981
+ label: t.duplicate,
982
+ icon: /* @__PURE__ */ jsx(HiDuplicate, {}),
983
+ onSelect: () => send({
984
+ type: "duplicateInput",
985
+ path
986
+ })
987
+ },
988
+ {
989
+ kind: "action",
990
+ label: t.moveUp,
991
+ icon: /* @__PURE__ */ jsx(FaArrowUp, {}),
992
+ disabled: index <= 0,
993
+ onSelect: () => send({
994
+ type: "moveInputs",
995
+ paths: [path],
996
+ parent,
997
+ index: index - 1
998
+ })
999
+ },
1000
+ {
1001
+ kind: "action",
1002
+ label: t.moveDown,
1003
+ icon: /* @__PURE__ */ jsx(FaArrowDown, {}),
1004
+ disabled: index >= count - 1,
1005
+ onSelect: () => send({
1006
+ type: "moveInputs",
1007
+ paths: [path],
1008
+ parent,
1009
+ index: index + 2
1010
+ })
1011
+ },
1012
+ { kind: "divider" },
1013
+ {
1014
+ kind: "action",
1015
+ label: t.deleteInput,
1016
+ icon: /* @__PURE__ */ jsx(BiSolidTrashAlt, {}),
1017
+ destructive: true,
1018
+ onSelect: () => remove(path)
1019
+ }
1020
+ ];
1021
+ };
1022
+ const resize = (edge, split) => {
1023
+ const edit = resizeLayout(editing, latest.current.inputs, edge.left, edge.right, split);
1024
+ return edit ? Boolean(send(edit)) : false;
1025
+ };
1026
+ const startResize = (e, edge) => {
1027
+ const root = containerRef.current;
1028
+ const cells = root && cellsAt(root, edge);
1029
+ if (e.button !== 0 || !cells) return;
1030
+ e.stopPropagation();
1031
+ const from = cells.left.getBoundingClientRect().left;
1032
+ const width = cells.flow.getBoundingClientRect().width;
1033
+ const total = edge.split[0] + edge.split[1];
1034
+ const grid = cells.flow.parentElement?.hasAttribute("data-layout-grid");
1035
+ const oldColumns = cells.flow.style.gridTemplateColumns;
1036
+ const drawn = [cells.left, cells.right].map((cell) => cell.style.getPropertyValue(WIDTH_VAR));
1037
+ const children = [...cells.flow.children];
1038
+ const weights = children.map((cell) => Math.max(1, cell.getBoundingClientRect().width));
1039
+ const leftIndex = children.indexOf(cells.left);
1040
+ const rightIndex = children.indexOf(cells.right);
1041
+ const combined = (weights[leftIndex] ?? 0) + (weights[rightIndex] ?? 0);
1042
+ const show = (split) => {
1043
+ if (grid) {
1044
+ weights[leftIndex] = combined * split[0] / total;
1045
+ weights[rightIndex] = combined * split[1] / total;
1046
+ cells.flow.style.gridTemplateColumns = weights.map((weight) => `minmax(0, ${weight}fr)`).join(" ");
1047
+ } else {
1048
+ cells.left.style.setProperty(WIDTH_VAR, `${split[0]}%`);
1049
+ cells.right.style.setProperty(WIDTH_VAR, `${split[1]}%`);
1050
+ }
1051
+ };
1052
+ const restore = () => {
1053
+ cells.flow.style.gridTemplateColumns = oldColumns;
1054
+ cells.left.style.setProperty(WIDTH_VAR, drawn[0] ?? "");
1055
+ cells.right.style.setProperty(WIDTH_VAR, drawn[1] ?? "");
1056
+ };
1057
+ let split = edge.split;
1058
+ store.set({ resizing: true });
1059
+ trackDrag({
1060
+ x: e.clientX,
1061
+ y: e.clientY
1062
+ }, {
1063
+ escape: true,
1064
+ holdText: "press",
1065
+ swallowClick: "always",
1066
+ onMove: (client) => {
1067
+ split = snapSplit(total, (client.x - from) / width * 100);
1068
+ show(split);
1069
+ store.set({ edge: {
1070
+ ...edge,
1071
+ x: cells.left.getBoundingClientRect().right,
1072
+ split
1073
+ } });
1074
+ },
1075
+ onEnd: () => {
1076
+ store.set({ resizing: false });
1077
+ root.focus({ preventScroll: true });
1078
+ restore();
1079
+ resize(edge, split);
1080
+ },
1081
+ onCancel: () => {
1082
+ store.set({ resizing: false });
1083
+ root.focus({ preventScroll: true });
1084
+ restore();
1085
+ }
1086
+ });
1087
+ };
1088
+ return {
1089
+ store,
1090
+ definition,
1091
+ edit,
1092
+ remove,
1093
+ openMenu: (x, y, path) => openMenu(x, y, menuFor(path)),
1094
+ openTypeMenu,
1095
+ startDrag,
1096
+ startResize,
1097
+ nudgeEdge: (edge, delta) => {
1098
+ resize(edge, snapSplit(edge.split[0] + edge.split[1], edge.split[0] + delta));
1099
+ },
1100
+ reveal: (line) => latest.current.editor.onReveal?.({ line }),
1101
+ removeSelection,
1102
+ moveSelection,
1103
+ navigate: (direction, extend) => {
1104
+ const root = containerRef.current;
1105
+ const rows = root ? rowsIn(root) : [];
1106
+ const { cursor, cursorCopy, selection } = store.get();
1107
+ const at = cursor ?? selection.at(-1);
1108
+ const from = rows.find((row) => row.copy === cursorCopy && key(row.path) === at) ?? rows.find((row) => key(row.path) === at);
1109
+ const to = from ? neighbour(rows.filter((row) => row === from || !samePath(row.path, from.path)), from, direction) : direction === "up" || direction === "left" ? rows.at(-1) : rows[0];
1110
+ if (!to) return false;
1111
+ const reached = key(to.path);
1112
+ const visited = from ? [key(from.path), reached] : [reached];
1113
+ store.set({
1114
+ cursor: reached,
1115
+ cursorCopy: to.copy,
1116
+ selection: extend ? [.../* @__PURE__ */ new Set([...selection, ...visited])] : [reached]
1117
+ });
1118
+ rowElement(reached)?.scrollIntoView?.({ block: "nearest" });
1119
+ return true;
1120
+ },
1121
+ hover: (instance) => {
1122
+ const root = containerRef.current;
1123
+ if (!root || store.get().hovered === instance) return;
1124
+ const heads = linesIn(root).find(({ columns }) => columns.length > 1 && columns.some((column) => column.rows.some((row) => row.instance === instance)))?.columns.flatMap((column) => column.rows[0] ? [column.rows[0].instance] : []);
1125
+ store.set({
1126
+ hovered: instance,
1127
+ open: [.../* @__PURE__ */ new Set([instance, ...heads ?? []])]
1128
+ });
1129
+ },
1130
+ splitIntoPages,
1131
+ addPage,
1132
+ unsplitPages,
1133
+ editSelection: () => {
1134
+ const [only, ...more] = store.get().selection;
1135
+ if (only === void 0 || more.length > 0) return false;
1136
+ edit(pathOf(only));
1137
+ return true;
1138
+ }
1139
+ };
1140
+ }, [
1141
+ store,
1142
+ openMenu,
1143
+ t,
1144
+ editing
1145
+ ]);
1146
+ const problems = editor.problems;
1147
+ useEffect(() => {
1148
+ store.set({ problems: problems ?? [] });
1149
+ }, [store, problems]);
1150
+ useEffect(() => {
1151
+ const { edge, resizing } = store.get();
1152
+ const root = containerRef.current;
1153
+ if (edge && !resizing && root && inputs) store.set({ edge: edgeBetween(root, edge.left, edge.right) });
1154
+ }, [store, inputs]);
1155
+ useEffect(() => {
1156
+ const { selection } = store.get();
1157
+ const kept = selection.filter((rowKey) => {
1158
+ const path = pathOf(rowKey);
1159
+ return Object.hasOwn(containerAt(editing, inputs, path.slice(0, -1)), path.at(-1) ?? "");
1160
+ });
1161
+ if (kept.length !== selection.length) store.set({ selection: kept });
1162
+ }, [
1163
+ store,
1164
+ inputs,
1165
+ editing
1166
+ ]);
1167
+ useEffect(() => {
1168
+ const container = containerRef.current;
1169
+ if (!container) return;
1170
+ const onDown = (e) => {
1171
+ if (e.button !== 0 || !(e.target instanceof Element) || e.target.closest(CHROME)) return;
1172
+ const row = e.target.closest("[data-input-path]");
1173
+ const start = {
1174
+ x: e.clientX,
1175
+ y: e.clientY
1176
+ };
1177
+ if (!e.shiftKey) {
1178
+ const rowKey = row?.dataset["inputPath"];
1179
+ if (!rowKey) {
1180
+ trackDrag(start, { onEnd: (_, dragged) => {
1181
+ if (!dragged) store.set({ selection: [] });
1182
+ } });
1183
+ return;
1184
+ }
1185
+ const selected = store.get().selection.includes(rowKey);
1186
+ const title = e.target.closest("[data-field-label]");
1187
+ const ownTitle = title !== null && title.closest("[data-input-path]") === row;
1188
+ if (!ownTitle && e.target.closest(CONTROLS)) {
1189
+ if (!selected) store.set({ selection: [] });
1190
+ return;
1191
+ }
1192
+ const path = pathOf(rowKey);
1193
+ const name = path.at(-1) ?? "";
1194
+ if (ownTitle && title.tagName !== "LABEL") {
1195
+ api.startDrag(e, path, name, { holdText: "drag" });
1196
+ return;
1197
+ }
1198
+ e.preventDefault();
1199
+ container.focus({ preventScroll: true });
1200
+ if (!selected) store.set({
1201
+ selection: [rowKey],
1202
+ cursor: rowKey,
1203
+ cursorCopy: row?.dataset["inputInstance"] ?? null
1204
+ });
1205
+ api.startDrag(e, path, name, {
1206
+ swallowClick: "always",
1207
+ holdText: "drag"
1208
+ });
1209
+ return;
1210
+ }
1211
+ e.preventDefault();
1212
+ e.stopPropagation();
1213
+ container.focus({ preventScroll: true });
1214
+ const base = store.get().selection;
1215
+ trackDrag(start, {
1216
+ holdText: "press",
1217
+ swallowClick: "always",
1218
+ onMove: (client) => {
1219
+ const box = {
1220
+ left: Math.min(start.x, client.x),
1221
+ right: Math.max(start.x, client.x),
1222
+ top: Math.min(start.y, client.y),
1223
+ bottom: Math.max(start.y, client.y)
1224
+ };
1225
+ store.set({
1226
+ selection: [.../* @__PURE__ */ new Set([...base, ...inputsInBox(container, box).map(key)])],
1227
+ marquee: box
1228
+ });
1229
+ },
1230
+ onEnd: (_, dragged) => {
1231
+ store.set({ marquee: null });
1232
+ const rowKey = row?.dataset["inputPath"];
1233
+ if (!dragged && rowKey) store.set({
1234
+ selection: base.includes(rowKey) ? base.filter((other) => other !== rowKey) : [...base, rowKey],
1235
+ cursor: rowKey,
1236
+ cursorCopy: row?.dataset["inputInstance"] ?? null
1237
+ });
1238
+ },
1239
+ onCancel: () => store.set({ marquee: null })
1240
+ });
1241
+ };
1242
+ container.addEventListener("pointerdown", onDown, true);
1243
+ return () => container.removeEventListener("pointerdown", onDown, true);
1244
+ }, [api, store]);
1245
+ useEffect(() => {
1246
+ const container = containerRef.current;
1247
+ if (!container) return;
1248
+ const onKey = (e) => {
1249
+ if (typing(e.target) || e.target instanceof Element && e.target.closest(KEYED_WIDGETS)) return;
1250
+ const arrow = ARROWS[e.key];
1251
+ if (e.metaKey || e.ctrlKey) undoKeys(e, latest.current.editor);
1252
+ else if (DELETE_KEYS.has(e.key) && !e.altKey && api.removeSelection()) e.preventDefault();
1253
+ else if (e.key.toLowerCase() === "e" && !e.altKey && api.editSelection()) e.preventDefault();
1254
+ else if (e.altKey && (e.key === "ArrowUp" || e.key === "ArrowDown") && api.moveSelection(e.key === "ArrowUp" ? -1 : 1)) e.preventDefault();
1255
+ else if (!e.altKey && arrow && api.navigate(arrow, e.shiftKey)) e.preventDefault();
1256
+ else if (e.key === "Escape" && !store.get().drag) store.set({ selection: [] });
1257
+ };
1258
+ container.addEventListener("keydown", onKey);
1259
+ return () => container.removeEventListener("keydown", onKey);
1260
+ }, [api, store]);
1261
+ const registerShow = editor.registerShow;
1262
+ useEffect(() => {
1263
+ if (!registerShow) return;
1264
+ return registerShow((problem) => {
1265
+ const key = problem.input ? JSON.stringify(problem.input) : null;
1266
+ const row = [...containerRef.current?.querySelectorAll("[data-input-path]") ?? []].find((el) => el.dataset["inputPath"] === key);
1267
+ if (!row) return false;
1268
+ const still = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1269
+ row.scrollIntoView?.({
1270
+ block: "center",
1271
+ behavior: still ? "auto" : "smooth"
1272
+ });
1273
+ if (!still) row.animate?.([{ boxShadow: "0 0 0 4px color-mix(in srgb, var(--theme-element) 45%, transparent)" }, { boxShadow: "0 0 0 0 transparent" }], {
1274
+ duration: 1200,
1275
+ easing: "ease-out"
1276
+ });
1277
+ if (problem.input) store.set({ dialog: {
1278
+ kind: "edit",
1279
+ path: problem.input
1280
+ } });
1281
+ return true;
1282
+ });
1283
+ }, [registerShow, store]);
1284
+ const empty = namesIn(asRecord(inputs)).length === 0;
1285
+ return /* @__PURE__ */ jsx(ApiContext.Provider, {
1286
+ value: api,
1287
+ children: /* @__PURE__ */ jsx(InputsContext.Provider, {
1288
+ value: inputs,
1289
+ children: /* @__PURE__ */ jsxs(FormEditingContext.Provider, {
1290
+ value: EDITING,
1291
+ children: [
1292
+ /* @__PURE__ */ jsxs("div", {
1293
+ ref: containerRef,
1294
+ tabIndex: 0,
1295
+ className: "relative outline-none",
1296
+ onPointerMove: (e) => {
1297
+ const state = store.get();
1298
+ const root = containerRef.current;
1299
+ if (!root || state.drag || state.marquee || state.resizing || resizerFocused(root)) return;
1300
+ const edge = root.querySelector("[data-input-cell], [data-layout-grid]") ? edgeAt(root, e.clientX, e.clientY) : null;
1301
+ if (!sameEdge(edge, state.edge)) store.set({ edge });
1302
+ },
1303
+ onPointerLeave: () => {
1304
+ const root = containerRef.current;
1305
+ const keep = store.get().resizing || root !== null && resizerFocused(root);
1306
+ store.set({
1307
+ hovered: null,
1308
+ open: [],
1309
+ ...keep ? {} : { edge: null }
1310
+ });
1311
+ },
1312
+ children: [
1313
+ empty && /* @__PURE__ */ jsx("div", {
1314
+ className: "mb-3 text-sm theme-muted-text",
1315
+ children: t.noInputs
1316
+ }),
1317
+ /* @__PURE__ */ jsx(WizardPagesContext.Provider, {
1318
+ value: wizardPages,
1319
+ children
1320
+ }),
1321
+ /* @__PURE__ */ jsx(DropIndicator, {
1322
+ store,
1323
+ container: containerRef
1324
+ }),
1325
+ /* @__PURE__ */ jsx(RowResizer, {
1326
+ api,
1327
+ container: containerRef
1328
+ }),
1329
+ /* @__PURE__ */ jsx(Marquee, {
1330
+ store,
1331
+ container: containerRef
1332
+ }),
1333
+ /* @__PURE__ */ jsx("div", {
1334
+ "data-input-chrome": true,
1335
+ className: "sticky bottom-2 z-10 mt-2 flex justify-end",
1336
+ children: /* @__PURE__ */ jsxs(EditorBar, {
1337
+ editor,
1338
+ groups: formShortcuts(g),
1339
+ children: [
1340
+ /* @__PURE__ */ jsx(AddChip, {
1341
+ icon: /* @__PURE__ */ jsx(FaPlus, { className: "h-3 w-3" }),
1342
+ label: t.addInput,
1343
+ hint: t.addInputHint,
1344
+ onPointerDown: (e) => api.startDrag(e, null, t.addInput),
1345
+ onClick: (e) => {
1346
+ const r = e.currentTarget.getBoundingClientRect();
1347
+ const form = isWizard(inputs) ? pagesIn(inputs) : [];
1348
+ const last = form.at(-1);
1349
+ if (last) wizardPages.setPage("[]", (wizardPages.count("[]") ?? form.length) - 1);
1350
+ api.openTypeMenu(r.left, r.bottom, last ? [last] : [], Number.MAX_SAFE_INTEGER);
1351
+ }
1352
+ }),
1353
+ /* @__PURE__ */ jsx(PageActions, {
1354
+ api,
1355
+ parent: [],
1356
+ list: inputs,
1357
+ bar: true
1358
+ }),
1359
+ isWizard(inputs) && /* @__PURE__ */ jsx(PageNumber, {
1360
+ count: wizardPages.count("[]") ?? pagesIn(inputs).length,
1361
+ page: wizardPages.page("[]"),
1362
+ onChange: (index) => wizardPages.setPage("[]", index)
1363
+ }),
1364
+ editor.onOpenSettings && /* @__PURE__ */ jsx(BarDivider, {})
1365
+ ]
1366
+ })
1367
+ })
1368
+ ]
1369
+ }),
1370
+ /* @__PURE__ */ jsx(Dialogs, {
1371
+ api,
1372
+ inputs,
1373
+ editor
1374
+ }),
1375
+ contextMenu
1376
+ ]
1377
+ })
1378
+ })
1379
+ });
1380
+ }
1381
+ function DropIndicator({ store, container }) {
1382
+ const drag = useUi(store, (state) => state.drag);
1383
+ const root = container.current;
1384
+ if (!drag || !root) return null;
1385
+ const rect = root.getBoundingClientRect();
1386
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [drag.gap?.beside ? /* @__PURE__ */ jsx("div", {
1387
+ className: "pointer-events-none absolute z-20 w-1 rounded-full bg-(--theme-element)",
1388
+ style: {
1389
+ left: drag.gap.beside.x - rect.left - 2,
1390
+ top: drag.gap.beside.top - rect.top,
1391
+ height: drag.gap.beside.bottom - drag.gap.beside.top
1392
+ }
1393
+ }) : drag.gap && /* @__PURE__ */ jsx("div", {
1394
+ className: "pointer-events-none absolute z-20 h-1 rounded-full bg-(--theme-element)",
1395
+ style: {
1396
+ left: drag.gap.left - rect.left,
1397
+ top: drag.gap.y - rect.top - 2,
1398
+ width: drag.gap.right - drag.gap.left
1399
+ }
1400
+ }), /* @__PURE__ */ jsx(DragLabel, {
1401
+ at: {
1402
+ x: drag.client.x - rect.left,
1403
+ y: drag.client.y - rect.top
1404
+ },
1405
+ text: drag.label
1406
+ })] });
1407
+ }
1408
+ var resizerFocused = (root) => document.activeElement instanceof HTMLElement && root.contains(document.activeElement) && document.activeElement.dataset["inputResizer"] !== void 0;
1409
+ /** The handle on the edge between two inputs sharing a line, which moves width between them. */
1410
+ function RowResizer({ api, container }) {
1411
+ const t = useInputsEditorStrings();
1412
+ const edge = useUi(api.store, (state) => state.edge);
1413
+ const root = container.current;
1414
+ if (!edge || !root) return null;
1415
+ const rect = root.getBoundingClientRect();
1416
+ const total = edge.split[0] + edge.split[1];
1417
+ return /* @__PURE__ */ jsx("div", {
1418
+ role: "separator",
1419
+ "aria-orientation": "vertical",
1420
+ "aria-label": t.resizeInputs,
1421
+ "aria-valuenow": edge.split[0],
1422
+ "aria-valuemin": Math.min(10, total / 2),
1423
+ "aria-valuemax": total - Math.min(10, total / 2),
1424
+ tabIndex: 0,
1425
+ "data-input-chrome": true,
1426
+ "data-input-resizer": true,
1427
+ className: "group/edge absolute z-20 flex w-3 -translate-x-1/2 cursor-col-resize touch-none justify-center outline-none",
1428
+ style: {
1429
+ left: edge.x - rect.left,
1430
+ top: edge.top - rect.top,
1431
+ height: edge.bottom - edge.top
1432
+ },
1433
+ onPointerDown: (e) => api.startResize(e, edge),
1434
+ onKeyDown: (e) => {
1435
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
1436
+ e.preventDefault();
1437
+ e.stopPropagation();
1438
+ api.nudgeEdge(edge, e.key === "ArrowLeft" ? -5 : 5);
1439
+ }
1440
+ },
1441
+ onBlur: () => {
1442
+ if (!api.store.get().resizing) api.store.set({ edge: null });
1443
+ },
1444
+ children: /* @__PURE__ */ jsx("div", { className: "h-full w-0.5 rounded-full bg-(--theme-element) opacity-50 group-hover/edge:opacity-100 group-focus-visible/edge:opacity-100" })
1445
+ });
1446
+ }
1447
+ function Marquee({ store, container }) {
1448
+ const box = useUi(store, (state) => state.marquee);
1449
+ const root = container.current;
1450
+ if (!box || !root) return null;
1451
+ const rect = root.getBoundingClientRect();
1452
+ return /* @__PURE__ */ jsx(MarqueeBox, {
1453
+ className: "pointer-events-none z-20",
1454
+ box: {
1455
+ left: box.left - rect.left,
1456
+ top: box.top - rect.top,
1457
+ right: box.right - rect.left,
1458
+ bottom: box.bottom - rect.top
1459
+ }
1460
+ });
1461
+ }
1462
+ function Dialogs({ api, inputs, editor }) {
1463
+ const editing = useWorkflowEditing();
1464
+ const dialog = useUi(api.store, (state) => state.dialog);
1465
+ const close = () => api.store.set({ dialog: null });
1466
+ if (!dialog) return null;
1467
+ const save = (edit, created, home) => {
1468
+ const merged = withCreatedInputs(editing, created, home, edit);
1469
+ if (merged) editor.onEdit(withInputLayouts(editing, inputs, merged));
1470
+ };
1471
+ if (dialog.kind === "create") {
1472
+ const siblings = namesIn(containerAt(editing, inputs, dialog.parent));
1473
+ const index = Math.min(dialog.index, siblings.length);
1474
+ const home = homeFor(editing, inputs, dialog.parent, index);
1475
+ const placement = dialog.placement;
1476
+ return /* @__PURE__ */ jsx(InputDialog, {
1477
+ name: nextInputName(editing, inputs),
1478
+ definition: dialog.definition ?? newInputDefinition(dialog.type),
1479
+ isNew: true,
1480
+ siblings,
1481
+ allowStep: isWizard(containerAt(editing, inputs, dialog.parent)),
1482
+ inputs,
1483
+ home,
1484
+ onClose: close,
1485
+ view: editor.settingsView,
1486
+ onViewChange: editor.onSettingsViewChange,
1487
+ openOnAdd: openOnAddOf(editor),
1488
+ yaml: { onSave: (name, text, created) => save(placeNewInput(editing, inputs, {
1489
+ type: "batch",
1490
+ edits: [{
1491
+ type: "addInput",
1492
+ parent: dialog.parent,
1493
+ index: samePath(home.parent, dialog.parent) ? index + created.length : index,
1494
+ name,
1495
+ definition: asRecord(editing.loadYaml(text))
1496
+ }, {
1497
+ type: "setInputYaml",
1498
+ path: [...dialog.parent, name],
1499
+ yaml: text
1500
+ }]
1501
+ }, dialog.parent, name, placement), created, home) },
1502
+ onSave: (name, definition, _patch, created) => {
1503
+ const add = {
1504
+ type: "addInput",
1505
+ parent: dialog.parent,
1506
+ index: samePath(home.parent, dialog.parent) ? index + created.length : index,
1507
+ name,
1508
+ definition
1509
+ };
1510
+ save(placeNewInput(editing, inputs, add, dialog.parent, name, placement), created, home);
1511
+ }
1512
+ }, `create:${key(dialog.parent)}:${dialog.index}`);
1513
+ }
1514
+ const { path } = dialog;
1515
+ const name = path.at(-1) ?? "";
1516
+ const container = containerAt(editing, inputs, path.slice(0, -1));
1517
+ if (!Object.hasOwn(container, name)) return null;
1518
+ const home = homeFor(editing, inputs, path.slice(0, -1), namesIn(container).indexOf(name));
1519
+ return /* @__PURE__ */ jsx(InputDialog, {
1520
+ name,
1521
+ definition: asRecord(container[name]),
1522
+ isNew: false,
1523
+ siblings: namesIn(container).filter((other) => other !== name),
1524
+ allowStep: isWizard(container),
1525
+ inputs,
1526
+ home,
1527
+ onClose: close,
1528
+ onDelete: () => editor.onEdit(withInputLayouts(editing, inputs, {
1529
+ type: "deleteInput",
1530
+ path
1531
+ })),
1532
+ view: editor.settingsView,
1533
+ onViewChange: editor.onSettingsViewChange,
1534
+ openOnAdd: openOnAddOf(editor),
1535
+ yaml: {
1536
+ source: editor.readSource?.(),
1537
+ path,
1538
+ onSave: (next, text, created) => {
1539
+ const at = [...path.slice(0, -1), next];
1540
+ save(next === name ? {
1541
+ type: "setInputYaml",
1542
+ path,
1543
+ yaml: text
1544
+ } : {
1545
+ type: "batch",
1546
+ edits: [{
1547
+ type: "updateInput",
1548
+ path,
1549
+ name: next
1550
+ }, {
1551
+ type: "setInputYaml",
1552
+ path: at,
1553
+ yaml: text
1554
+ }]
1555
+ }, created, home);
1556
+ }
1557
+ },
1558
+ onSave: (next, _definition, patch, created) => save({
1559
+ type: "updateInput",
1560
+ path,
1561
+ ...next !== name ? { name: next } : {},
1562
+ ...patch
1563
+ }, created, home)
1564
+ }, `edit:${key(path)}`);
1565
+ }
1566
+ function homeFor(editing, inputs, container, index) {
1567
+ const [top, next] = container;
1568
+ if (top === void 0) return {
1569
+ parent: [],
1570
+ index
1571
+ };
1572
+ if (text(asRecord(asRecord(inputs)[top])["type"]) === "step") {
1573
+ const fields = namesIn(containerAt(editing, inputs, [top]));
1574
+ return {
1575
+ parent: [top],
1576
+ index: next === void 0 ? index : fields.indexOf(next)
1577
+ };
1578
+ }
1579
+ return {
1580
+ parent: [],
1581
+ index: namesIn(asRecord(inputs)).indexOf(top)
1582
+ };
1583
+ }
1584
+ function nextInputName(editing, inputs) {
1585
+ return editing.newInputName(allNames(editing, asRecord(inputs), /* @__PURE__ */ new Set()));
1586
+ }
1587
+ function placeNewInput(editing, inputs, edit, parent, name, placement) {
1588
+ return placement ? {
1589
+ type: "batch",
1590
+ edits: [edit, ...dropLayoutEdits(editing, inputs, [[...parent, name]], placement.drop, edit)]
1591
+ } : edit;
1592
+ }
1593
+ //#endregion
1594
+ export { InputsFormEditor };