@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.
- package/dist/ai/components/ChatSidebar.js +7 -4
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.js +2 -2
- package/dist/ai/ui/sidebar.d.ts +6 -0
- package/dist/ai/ui/sidebar.js +15 -1
- package/dist/form/FieldWrapper.js +1 -0
- package/dist/form/Form.js +64 -23
- package/dist/form/Wizard/WizardContainer.js +66 -6
- package/dist/form/Wizard/WizardStepContent.js +23 -10
- package/dist/form/Wizard/wizardPages.d.ts +12 -0
- package/dist/form/Wizard/wizardPages.js +5 -0
- package/dist/form/formEditing.d.ts +25 -0
- package/dist/form/formEditing.js +20 -0
- package/dist/graph/InputDialog.js +1568 -54
- package/dist/graph/InputsEditor.d.ts +12 -0
- package/dist/graph/InputsEditor.js +1594 -0
- package/dist/graph/ShortcutsButton.js +56 -0
- package/dist/graph/SuggestionInput.js +29 -0
- package/dist/graph/editorApi.js +8 -0
- package/dist/graph/editorChrome.js +75 -0
- package/dist/graph/editorFields.js +394 -0
- package/dist/graph/editorPrimitives.js +95 -4
- package/dist/graph/editorStrings.js +9 -1
- package/dist/graph/fieldProblems.js +77 -0
- package/dist/graph/index.d.ts +1 -0
- package/dist/graph/index.js +2 -1
- package/dist/graph/inputLayout.js +337 -0
- package/dist/graph/inputPages.d.ts +33 -0
- package/dist/graph/inputPages.js +186 -0
- package/dist/graph/inputRefs.js +196 -0
- package/dist/graph/inputRows.d.ts +43 -0
- package/dist/graph/inputRows.js +65 -0
- package/dist/graph/settingsViews.js +62 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { LuKeyboard } from "../icons/lu/LuKeyboard.js";
|
|
2
|
+
import { IconButton } from "../components/IconButton.js";
|
|
3
|
+
import { useGraphEditorStrings } from "./editorStrings.js";
|
|
4
|
+
import { ToolbarPopover } from "./ToolbarPopover.js";
|
|
5
|
+
import { Fragment, useRef, useState } from "react";
|
|
6
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/graph/ShortcutsButton.tsx
|
|
8
|
+
var MOD_KEY = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl";
|
|
9
|
+
var ALT_KEY = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌥" : "Alt";
|
|
10
|
+
/** A toolbar button that opens the list of gestures and keys a pane takes. */
|
|
11
|
+
function ShortcutsButton({ groups }) {
|
|
12
|
+
const t = useGraphEditorStrings();
|
|
13
|
+
const [open, setOpen] = useState(false);
|
|
14
|
+
const button = useRef(null);
|
|
15
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
16
|
+
ref: button,
|
|
17
|
+
icon: /* @__PURE__ */ jsx(LuKeyboard, { className: "h-4 w-4" }),
|
|
18
|
+
label: t.shortcuts,
|
|
19
|
+
size: "sm",
|
|
20
|
+
variant: "ghost",
|
|
21
|
+
"aria-expanded": open,
|
|
22
|
+
showTooltip: !open,
|
|
23
|
+
onClick: () => setOpen((current) => !current)
|
|
24
|
+
}), open && /* @__PURE__ */ jsxs(ToolbarPopover, {
|
|
25
|
+
anchor: button,
|
|
26
|
+
label: t.shortcuts,
|
|
27
|
+
onClose: () => setOpen(false),
|
|
28
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
29
|
+
className: "text-[0.8125rem] font-semibold",
|
|
30
|
+
children: t.shortcuts
|
|
31
|
+
}), groups.map((group) => /* @__PURE__ */ jsxs("section", {
|
|
32
|
+
className: "mt-3",
|
|
33
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
34
|
+
className: "mb-1.5 text-xs font-semibold theme-muted-text",
|
|
35
|
+
children: group.title
|
|
36
|
+
}), /* @__PURE__ */ jsx("dl", {
|
|
37
|
+
className: "grid grid-cols-[8.5rem_1fr] items-baseline gap-x-3 gap-y-1.5",
|
|
38
|
+
children: group.shortcuts.map((shortcut) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
|
|
39
|
+
className: "flex flex-wrap items-center gap-1",
|
|
40
|
+
children: shortcut.combos.map((keys, i) => /* @__PURE__ */ jsxs(Fragment, { children: [i > 0 && /* @__PURE__ */ jsx("span", {
|
|
41
|
+
className: "theme-muted-text",
|
|
42
|
+
children: "/"
|
|
43
|
+
}), keys.map((key) => /* @__PURE__ */ jsx("kbd", {
|
|
44
|
+
className: "rounded border theme-border px-1 py-px text-[11px]",
|
|
45
|
+
children: key
|
|
46
|
+
}, key))] }, keys.join("+")))
|
|
47
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
48
|
+
className: "theme-muted-text",
|
|
49
|
+
children: shortcut.does
|
|
50
|
+
})] }, shortcut.does))
|
|
51
|
+
})]
|
|
52
|
+
}, group.title))]
|
|
53
|
+
})] });
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ALT_KEY, MOD_KEY, ShortcutsButton };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Dropdown from "../components/Dropdown.js";
|
|
2
|
+
import cx from "classnames";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/graph/SuggestionInput.tsx
|
|
5
|
+
function suggestionOptions(suggestions) {
|
|
6
|
+
return suggestions.map(({ value, label }) => label ? {
|
|
7
|
+
label,
|
|
8
|
+
value,
|
|
9
|
+
description: value
|
|
10
|
+
} : value);
|
|
11
|
+
}
|
|
12
|
+
var FIELD_TEXT_BOX = "py-3 pl-4 rounded-lg text-sm !bg-[var(--theme-muted-panel-bg)]";
|
|
13
|
+
/**
|
|
14
|
+
* The dropdown for a field that takes any typed value as well as the ones it offers. Any of
|
|
15
|
+
* the dropdown's own props can still be passed, such as `allowCustomValue={false}`.
|
|
16
|
+
*/
|
|
17
|
+
function SuggestionInput({ value, onChange, suggestions, allowCustomValue = true, ...props }) {
|
|
18
|
+
return /* @__PURE__ */ jsx(Dropdown, {
|
|
19
|
+
showCaret: suggestions.length > 0,
|
|
20
|
+
...props,
|
|
21
|
+
textBoxClassName: cx(FIELD_TEXT_BOX, props.textBoxClassName),
|
|
22
|
+
allowCustomValue,
|
|
23
|
+
value,
|
|
24
|
+
options: suggestionOptions(suggestions),
|
|
25
|
+
onChange: (next) => onChange(next === void 0 ? "" : String(next))
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { FIELD_TEXT_BOX, SuggestionInput, suggestionOptions };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/graph/editorApi.ts
|
|
2
|
+
var TEXT_INPUT = "input, textarea, select, [contenteditable=\"true\"]";
|
|
3
|
+
var typing = (target) => target instanceof Element && !!target.closest(TEXT_INPUT);
|
|
4
|
+
function overlaps(rect, box) {
|
|
5
|
+
return rect.left <= box.right && rect.right >= box.left && rect.top <= box.bottom && rect.bottom >= box.top;
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { overlaps, typing };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { BiRedo } from "../icons/bi/BiRedo.js";
|
|
2
|
+
import { BiUndo } from "../icons/bi/BiUndo.js";
|
|
3
|
+
import { IoSettingsOutline } from "../icons/io5/IoSettingsOutline.js";
|
|
4
|
+
import { TOOLTIP_ID } from "../components/Tooltip.js";
|
|
5
|
+
import { IconButton } from "../components/IconButton.js";
|
|
6
|
+
import { useGraphEditorStrings } from "./editorStrings.js";
|
|
7
|
+
import { ProblemsButton } from "./ProblemsButton.js";
|
|
8
|
+
import { ShortcutsButton } from "./ShortcutsButton.js";
|
|
9
|
+
import cx from "classnames";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
//#region src/graph/editorChrome.tsx
|
|
12
|
+
/** What a drag carries, beside the pointer at `at` in its layer's coordinates. */
|
|
13
|
+
function DragLabel({ at, text, refused = false }) {
|
|
14
|
+
return /* @__PURE__ */ jsx("div", {
|
|
15
|
+
className: cx("pointer-events-none absolute z-30 rounded-md border bg-(--theme-panel-bg) px-2 py-1 text-xs shadow", refused ? "border-(--theme-error) text-(--theme-error)" : "theme-border"),
|
|
16
|
+
style: {
|
|
17
|
+
left: at.x + 12,
|
|
18
|
+
top: at.y + 12
|
|
19
|
+
},
|
|
20
|
+
children: text
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function BarDivider() {
|
|
24
|
+
return /* @__PURE__ */ jsx("div", { className: "mx-0.5 h-5 w-px bg-(--theme-border)" });
|
|
25
|
+
}
|
|
26
|
+
/** A toolbar chip that adds something: click to add it, or, given `onPointerDown`, drag it to where it goes. */
|
|
27
|
+
function AddChip({ icon, label, hint, className, onPointerDown, onClick }) {
|
|
28
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
29
|
+
type: "button",
|
|
30
|
+
className: cx("flex h-7 items-center gap-1 whitespace-nowrap rounded px-2 text-xs theme-hover", onPointerDown ? "cursor-grab" : "cursor-pointer", className),
|
|
31
|
+
"data-tooltip-id": TOOLTIP_ID,
|
|
32
|
+
"data-tooltip-content": hint,
|
|
33
|
+
onPointerDown,
|
|
34
|
+
onClick,
|
|
35
|
+
children: [icon, label]
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** The bar both editors draw: problems, undo and redo, the editor's own controls, settings, shortcuts. */
|
|
39
|
+
function EditorBar({ editor, groups, className, children }) {
|
|
40
|
+
const t = useGraphEditorStrings();
|
|
41
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
42
|
+
className: cx("flex max-w-full flex-wrap items-center justify-end gap-0.5 rounded-md border theme-border bg-(--theme-panel-bg) p-0.5", className),
|
|
43
|
+
children: [
|
|
44
|
+
!!editor.listedProblems?.length && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ProblemsButton, { problems: editor.listedProblems }), /* @__PURE__ */ jsx(BarDivider, {})] }),
|
|
45
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
46
|
+
icon: /* @__PURE__ */ jsx(BiUndo, { className: "h-4 w-4" }),
|
|
47
|
+
label: t.undo,
|
|
48
|
+
size: "sm",
|
|
49
|
+
variant: "ghost",
|
|
50
|
+
disabled: !editor.canUndo,
|
|
51
|
+
onClick: editor.onUndo
|
|
52
|
+
}),
|
|
53
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
54
|
+
icon: /* @__PURE__ */ jsx(BiRedo, { className: "h-4 w-4" }),
|
|
55
|
+
label: t.redo,
|
|
56
|
+
size: "sm",
|
|
57
|
+
variant: "ghost",
|
|
58
|
+
disabled: !editor.canRedo,
|
|
59
|
+
onClick: editor.onRedo
|
|
60
|
+
}),
|
|
61
|
+
/* @__PURE__ */ jsx(BarDivider, {}),
|
|
62
|
+
children,
|
|
63
|
+
editor.onOpenSettings && /* @__PURE__ */ jsx(IconButton, {
|
|
64
|
+
icon: /* @__PURE__ */ jsx(IoSettingsOutline, { className: "h-4 w-4" }),
|
|
65
|
+
label: t.workflowSettings,
|
|
66
|
+
size: "sm",
|
|
67
|
+
variant: "ghost",
|
|
68
|
+
onClick: editor.onOpenSettings
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ jsx(ShortcutsButton, { groups })
|
|
71
|
+
]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { AddChip, BarDivider, DragLabel, EditorBar };
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { FaPlus } from "../icons/fa/FaPlus.js";
|
|
2
|
+
import { IoMdClose } from "../icons/io/IoMdClose.js";
|
|
3
|
+
import { LuBraces } from "../icons/lu/LuBraces.js";
|
|
4
|
+
import { LuChevronRight } from "../icons/lu/LuChevronRight.js";
|
|
5
|
+
import { withPositionKeys } from "../components/keys.js";
|
|
6
|
+
import { TOOLTIP_ID } from "../components/Tooltip.js";
|
|
7
|
+
import { BareModal, modalPanelClasses } from "../components/BareModal.js";
|
|
8
|
+
import { Button } from "../components/Button.js";
|
|
9
|
+
import { ghostButtonClasses, primaryButtonClasses } from "../components/ghostButton.js";
|
|
10
|
+
import { FilterPill } from "../components/FilterPill.js";
|
|
11
|
+
import { IconButton } from "../components/IconButton.js";
|
|
12
|
+
import { Textarea } from "../components/Input.js";
|
|
13
|
+
import { Toggle } from "../components/Toggle.js";
|
|
14
|
+
import { useGraphEditorStrings } from "./editorStrings.js";
|
|
15
|
+
import { useFieldLint } from "./fieldProblems.js";
|
|
16
|
+
import { asRecord } from "./records.js";
|
|
17
|
+
import { SuggestionInput } from "./SuggestionInput.js";
|
|
18
|
+
import cx from "classnames";
|
|
19
|
+
import { createContext, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
20
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
21
|
+
//#region src/graph/editorFields.tsx
|
|
22
|
+
var EXPRESSION = /^\$\{\{.*\}\}$/;
|
|
23
|
+
var EMPTY_EXPRESSION = /^\$\{\{\s*\}\}$/;
|
|
24
|
+
function withoutUndefined(value) {
|
|
25
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
|
|
26
|
+
}
|
|
27
|
+
function text(value) {
|
|
28
|
+
return value === void 0 || value === null ? "" : String(value);
|
|
29
|
+
}
|
|
30
|
+
function isScalar(value) {
|
|
31
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
32
|
+
}
|
|
33
|
+
function parseScalar(value) {
|
|
34
|
+
if (/^-?\d+(\.\d+)?$/.test(value) && String(Number(value)) === value) return Number(value);
|
|
35
|
+
if (value === "true" || value === "false") return value === "true";
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function sameValue(a, b) {
|
|
39
|
+
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => sameValue(item, b[i]));
|
|
40
|
+
if (typeof a === "object" && typeof b === "object" && a !== null && b !== null) {
|
|
41
|
+
const ak = Object.keys(a);
|
|
42
|
+
const bk = Object.keys(b);
|
|
43
|
+
return ak.length === bk.length && ak.every((k) => sameValue(a[k], b[k]));
|
|
44
|
+
}
|
|
45
|
+
return a === b;
|
|
46
|
+
}
|
|
47
|
+
/** Keys whose value changed: undefined removes a key, anything else writes it. */
|
|
48
|
+
function diffPatch(original, next) {
|
|
49
|
+
const set = {};
|
|
50
|
+
const unset = [];
|
|
51
|
+
for (const [key, value] of Object.entries(next)) if (value === void 0) {
|
|
52
|
+
if (Object.hasOwn(original, key)) unset.push(key);
|
|
53
|
+
} else if (!sameValue(original[key], value)) set[key] = value;
|
|
54
|
+
return {
|
|
55
|
+
set,
|
|
56
|
+
unset
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function isEmptyPatch(patch) {
|
|
60
|
+
return Object.keys(patch.set).length === 0 && patch.unset.length === 0;
|
|
61
|
+
}
|
|
62
|
+
function expressionError(value, t) {
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
return !trimmed || EXPRESSION.test(trimmed) && !EMPTY_EXPRESSION.test(trimmed) ? void 0 : t.invalidExpressionValue;
|
|
65
|
+
}
|
|
66
|
+
function parseJson(value) {
|
|
67
|
+
try {
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
value: JSON.parse(value)
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
return { ok: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function unchangedValue(text, original, nested = false) {
|
|
77
|
+
return original !== void 0 && text === (nested ? JSON.stringify(original) : String(original));
|
|
78
|
+
}
|
|
79
|
+
function flagError(value, t) {
|
|
80
|
+
return typeof value === "string" ? expressionError(value, t) : void 0;
|
|
81
|
+
}
|
|
82
|
+
function flagValue(checked, fallback, original) {
|
|
83
|
+
if (checked !== fallback) return checked;
|
|
84
|
+
return original === checked ? checked : void 0;
|
|
85
|
+
}
|
|
86
|
+
/** Whether each section of a dialog is open, kept while the dialog is up so switching views keeps it. */
|
|
87
|
+
var SectionMemory = createContext(null);
|
|
88
|
+
function useSectionMemory() {
|
|
89
|
+
return useState(() => /* @__PURE__ */ new Map())[0];
|
|
90
|
+
}
|
|
91
|
+
function Section({ title, description, open = false, alert = false, children }) {
|
|
92
|
+
const memory = useContext(SectionMemory);
|
|
93
|
+
const [initial] = useState(() => {
|
|
94
|
+
const decided = memory?.get(title) ?? open;
|
|
95
|
+
memory?.set(title, decided);
|
|
96
|
+
return decided;
|
|
97
|
+
});
|
|
98
|
+
const details = useRef(null);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (alert && details.current && !details.current.open) details.current.open = true;
|
|
101
|
+
}, [alert]);
|
|
102
|
+
return /* @__PURE__ */ jsxs("details", {
|
|
103
|
+
ref: details,
|
|
104
|
+
open: initial,
|
|
105
|
+
onToggle: (e) => memory?.set(title, e.currentTarget.open),
|
|
106
|
+
className: "group border-t theme-border",
|
|
107
|
+
children: [/* @__PURE__ */ jsxs("summary", {
|
|
108
|
+
className: "flex cursor-pointer select-none list-none items-center gap-1.5 py-2.5 text-sm font-semibold [&::-webkit-details-marker]:hidden",
|
|
109
|
+
children: [/* @__PURE__ */ jsx(LuChevronRight, {
|
|
110
|
+
"aria-hidden": "true",
|
|
111
|
+
className: "h-3.5 w-3.5 shrink-0 theme-muted-text transition-transform group-open:rotate-90 motion-reduce:transition-none"
|
|
112
|
+
}), title]
|
|
113
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
114
|
+
className: "flex flex-col gap-3 pb-4",
|
|
115
|
+
children: [description && /* @__PURE__ */ jsx("div", {
|
|
116
|
+
className: "text-xs theme-muted-text",
|
|
117
|
+
children: description
|
|
118
|
+
}), children]
|
|
119
|
+
})]
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function FieldError({ message }) {
|
|
123
|
+
return message ? /* @__PURE__ */ jsx("div", {
|
|
124
|
+
className: "text-xs text-(--theme-error)",
|
|
125
|
+
children: message
|
|
126
|
+
}) : null;
|
|
127
|
+
}
|
|
128
|
+
/** The YAML key a field sets, beside its name; left out where the name already says it. */
|
|
129
|
+
function KeyHint({ label, yamlKey }) {
|
|
130
|
+
return label.trim().toLowerCase() === yamlKey.toLowerCase() ? null : /* @__PURE__ */ jsx("code", {
|
|
131
|
+
"aria-hidden": "true",
|
|
132
|
+
className: "font-mono text-xs theme-muted-text",
|
|
133
|
+
children: yamlKey
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** Label props for an Input or Textarea that sets `yamlKey`. */
|
|
137
|
+
function labelled(label, yamlKey) {
|
|
138
|
+
return {
|
|
139
|
+
label,
|
|
140
|
+
labelHint: /* @__PURE__ */ jsx(KeyHint, {
|
|
141
|
+
label,
|
|
142
|
+
yamlKey
|
|
143
|
+
})
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** A field's name and help text, for controls without their own label; `actions` sit at its right. */
|
|
147
|
+
function FieldLabel({ label, yamlKey, description, htmlFor, descriptionId, actions }) {
|
|
148
|
+
const nameClass = "text-[0.8125rem] font-medium";
|
|
149
|
+
const text = /* @__PURE__ */ jsxs("div", {
|
|
150
|
+
className: "flex min-w-0 flex-col",
|
|
151
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
152
|
+
className: "flex flex-wrap items-baseline gap-x-1.5",
|
|
153
|
+
children: [htmlFor ? /* @__PURE__ */ jsx("label", {
|
|
154
|
+
htmlFor,
|
|
155
|
+
className: nameClass,
|
|
156
|
+
children: label
|
|
157
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
158
|
+
className: nameClass,
|
|
159
|
+
children: label
|
|
160
|
+
}), yamlKey && /* @__PURE__ */ jsx(KeyHint, {
|
|
161
|
+
label,
|
|
162
|
+
yamlKey
|
|
163
|
+
})]
|
|
164
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
165
|
+
id: descriptionId,
|
|
166
|
+
className: "text-xs theme-muted-text",
|
|
167
|
+
children: description
|
|
168
|
+
})]
|
|
169
|
+
});
|
|
170
|
+
return actions ? /* @__PURE__ */ jsxs("div", {
|
|
171
|
+
className: "flex items-start justify-between gap-3",
|
|
172
|
+
children: [text, /* @__PURE__ */ jsx("div", {
|
|
173
|
+
className: "flex shrink-0 items-center gap-1",
|
|
174
|
+
children: actions
|
|
175
|
+
})]
|
|
176
|
+
}) : text;
|
|
177
|
+
}
|
|
178
|
+
/** A field's label and description above the editor that sets it. */
|
|
179
|
+
function LabelledField({ children, ...label }) {
|
|
180
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
181
|
+
className: "flex flex-col gap-1.5",
|
|
182
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, { ...label }), children]
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Switches a field between its own control and a typed expression, where the schema takes one. Its
|
|
187
|
+
* name says what pressing it does, so it isn't also announced as pressed.
|
|
188
|
+
*/
|
|
189
|
+
function ExpressionToggle({ active, onChange }) {
|
|
190
|
+
const t = useGraphEditorStrings();
|
|
191
|
+
return /* @__PURE__ */ jsx(IconButton, {
|
|
192
|
+
icon: /* @__PURE__ */ jsx(LuBraces, { className: "h-4 w-4" }),
|
|
193
|
+
label: active ? t.stopExpression : t.useExpression,
|
|
194
|
+
size: "sm",
|
|
195
|
+
variant: "ghost",
|
|
196
|
+
active,
|
|
197
|
+
onClick: () => onChange(!active)
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function ToggleField({ label, yamlKey, description, checked, onChange, disabled }) {
|
|
201
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
202
|
+
className: "flex items-center justify-between gap-3",
|
|
203
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
204
|
+
label,
|
|
205
|
+
yamlKey,
|
|
206
|
+
description
|
|
207
|
+
}), /* @__PURE__ */ jsx(Toggle, {
|
|
208
|
+
checked,
|
|
209
|
+
onChange,
|
|
210
|
+
label,
|
|
211
|
+
...disabled ? { disabled } : {}
|
|
212
|
+
})]
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function ChoiceButtons({ options, value, onChange }) {
|
|
216
|
+
return /* @__PURE__ */ jsx("div", {
|
|
217
|
+
className: "flex flex-wrap gap-1",
|
|
218
|
+
children: options.map((option) => /* @__PURE__ */ jsx(FilterPill, {
|
|
219
|
+
active: value === option.value,
|
|
220
|
+
onClick: () => onChange(option.value),
|
|
221
|
+
children: option.label
|
|
222
|
+
}, option.value))
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function AddRowButton({ label, onClick }) {
|
|
226
|
+
return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
|
|
227
|
+
type: "button",
|
|
228
|
+
variant: "ghost",
|
|
229
|
+
size: "sm",
|
|
230
|
+
fullWidth: false,
|
|
231
|
+
icon: /* @__PURE__ */ jsx(FaPlus, { className: "h-3 w-3" }),
|
|
232
|
+
iconPosition: "left",
|
|
233
|
+
onClick,
|
|
234
|
+
children: label
|
|
235
|
+
}) });
|
|
236
|
+
}
|
|
237
|
+
function updateAt(items, index, item) {
|
|
238
|
+
return items.map((current, i) => i === index ? item : current);
|
|
239
|
+
}
|
|
240
|
+
function removeAt(items, index) {
|
|
241
|
+
return items.filter((_, i) => i !== index);
|
|
242
|
+
}
|
|
243
|
+
/** A list of single values, such as permissions or variable names. */
|
|
244
|
+
function StringListEditor({ label, values, onChange, addLabel, placeholder, error, suggestions = [] }) {
|
|
245
|
+
const t = useGraphEditorStrings();
|
|
246
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
247
|
+
className: "flex flex-col gap-2",
|
|
248
|
+
children: [
|
|
249
|
+
withPositionKeys(values).map(({ key, item: value }, i) => /* @__PURE__ */ jsxs("div", {
|
|
250
|
+
className: "flex items-center gap-2",
|
|
251
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
252
|
+
className: "flex-1",
|
|
253
|
+
children: /* @__PURE__ */ jsx(SuggestionInput, {
|
|
254
|
+
ariaLabel: t.listItem(label, i + 1),
|
|
255
|
+
value,
|
|
256
|
+
placeholder,
|
|
257
|
+
suggestions: suggestions.map((option) => ({ value: option })),
|
|
258
|
+
onChange: (next) => onChange(updateAt(values, i, next))
|
|
259
|
+
})
|
|
260
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
261
|
+
icon: /* @__PURE__ */ jsx(IoMdClose, { className: "h-4 w-4" }),
|
|
262
|
+
label: t.removeRow,
|
|
263
|
+
variant: "ghost",
|
|
264
|
+
size: "sm",
|
|
265
|
+
onClick: () => onChange(removeAt(values, i))
|
|
266
|
+
})]
|
|
267
|
+
}, key)),
|
|
268
|
+
/* @__PURE__ */ jsx(FieldError, { message: error }),
|
|
269
|
+
/* @__PURE__ */ jsx(AddRowButton, {
|
|
270
|
+
label: addLabel,
|
|
271
|
+
onClick: () => onChange([...values, ""])
|
|
272
|
+
})
|
|
273
|
+
]
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function SuggestedInput({ label, yamlKey, description, value, onChange, suggestions, error, placeholder }) {
|
|
277
|
+
const id = useId();
|
|
278
|
+
const lint = useFieldLint(yamlKey, id, value, onChange);
|
|
279
|
+
return /* @__PURE__ */ jsxs(LabelledField, {
|
|
280
|
+
label,
|
|
281
|
+
yamlKey,
|
|
282
|
+
description: description ?? "",
|
|
283
|
+
htmlFor: id,
|
|
284
|
+
...description ? { descriptionId: `${id}-help` } : {},
|
|
285
|
+
children: [
|
|
286
|
+
/* @__PURE__ */ jsx(SuggestionInput, {
|
|
287
|
+
id,
|
|
288
|
+
"aria-describedby": description ? `${id}-help` : void 0,
|
|
289
|
+
value,
|
|
290
|
+
placeholder,
|
|
291
|
+
suggestions: suggestions.map((option) => ({ value: option })),
|
|
292
|
+
onChange
|
|
293
|
+
}),
|
|
294
|
+
/* @__PURE__ */ jsx(FieldError, { message: error ?? lint.message }),
|
|
295
|
+
lint.fixes
|
|
296
|
+
]
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/** A text area that starts one line tall and grows with its text. */
|
|
300
|
+
function useGrowingArea(value) {
|
|
301
|
+
const ref = useRef(null);
|
|
302
|
+
useLayoutEffect(() => {
|
|
303
|
+
const area = ref.current;
|
|
304
|
+
if (area) {
|
|
305
|
+
area.style.height = "auto";
|
|
306
|
+
area.style.height = `${area.scrollHeight + area.offsetHeight - area.clientHeight}px`;
|
|
307
|
+
}
|
|
308
|
+
}, [value]);
|
|
309
|
+
return ref;
|
|
310
|
+
}
|
|
311
|
+
var GROWING_AREA = "!min-h-0 !resize-none overflow-hidden";
|
|
312
|
+
function GrowingTextarea(props) {
|
|
313
|
+
const ref = useGrowingArea(props.value);
|
|
314
|
+
return /* @__PURE__ */ jsx(Textarea, {
|
|
315
|
+
...props,
|
|
316
|
+
ref,
|
|
317
|
+
rows: 1,
|
|
318
|
+
className: GROWING_AREA
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/** The dialogs' "open when adding" switch, when the host keeps the setting. */
|
|
322
|
+
function openOnAddOf(editor) {
|
|
323
|
+
return editor?.onOpenOnAddChange ? {
|
|
324
|
+
open: editor.openOnAdd !== false,
|
|
325
|
+
onChange: editor.onOpenOnAddChange
|
|
326
|
+
} : void 0;
|
|
327
|
+
}
|
|
328
|
+
function OpenOnAddSwitch({ open, onChange }) {
|
|
329
|
+
const t = useGraphEditorStrings();
|
|
330
|
+
return /* @__PURE__ */ jsxs("label", {
|
|
331
|
+
className: "flex cursor-pointer items-center gap-2 text-xs theme-muted-text",
|
|
332
|
+
"data-tooltip-id": TOOLTIP_ID,
|
|
333
|
+
"data-tooltip-content": t.openOnAddHelp,
|
|
334
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
335
|
+
type: "checkbox",
|
|
336
|
+
className: "h-4 w-4 cursor-pointer accent-(--theme-element)",
|
|
337
|
+
checked: open,
|
|
338
|
+
onChange: (e) => onChange(e.target.checked)
|
|
339
|
+
}), t.openOnAdd]
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function DialogShell({ title, onClose, onSubmit, saveDisabled, dirty, locked = false, footerStart, headerEnd, openOnAdd, children }) {
|
|
343
|
+
const t = useGraphEditorStrings();
|
|
344
|
+
const submit = () => {
|
|
345
|
+
if (!saveDisabled) onSubmit();
|
|
346
|
+
};
|
|
347
|
+
return /* @__PURE__ */ jsx(BareModal, {
|
|
348
|
+
open: true,
|
|
349
|
+
onClose,
|
|
350
|
+
ariaLabel: title,
|
|
351
|
+
align: "center",
|
|
352
|
+
preventClose: dirty || locked,
|
|
353
|
+
className: cx(modalPanelClasses, "flex max-h-[85vh] w-full max-w-2xl flex-col"),
|
|
354
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
355
|
+
className: "flex min-h-0 flex-col",
|
|
356
|
+
children: [
|
|
357
|
+
/* @__PURE__ */ jsxs("div", {
|
|
358
|
+
className: "flex items-center gap-3 border-b theme-border px-5 py-4",
|
|
359
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
360
|
+
className: "flex-1 text-[1rem] font-semibold",
|
|
361
|
+
children: title
|
|
362
|
+
}), headerEnd]
|
|
363
|
+
}),
|
|
364
|
+
/* @__PURE__ */ jsx("div", {
|
|
365
|
+
className: "flex min-h-0 flex-col gap-3 overflow-y-auto px-5 py-4",
|
|
366
|
+
children
|
|
367
|
+
}),
|
|
368
|
+
/* @__PURE__ */ jsxs("div", {
|
|
369
|
+
className: "flex items-center gap-2 border-t theme-border px-5 py-3",
|
|
370
|
+
children: [
|
|
371
|
+
footerStart,
|
|
372
|
+
openOnAdd && /* @__PURE__ */ jsx(OpenOnAddSwitch, { ...openOnAdd }),
|
|
373
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
374
|
+
/* @__PURE__ */ jsx("button", {
|
|
375
|
+
type: "button",
|
|
376
|
+
className: ghostButtonClasses,
|
|
377
|
+
onClick: onClose,
|
|
378
|
+
children: t.cancel
|
|
379
|
+
}),
|
|
380
|
+
/* @__PURE__ */ jsx("button", {
|
|
381
|
+
type: "button",
|
|
382
|
+
disabled: saveDisabled,
|
|
383
|
+
onClick: submit,
|
|
384
|
+
className: cx(primaryButtonClasses, "disabled:cursor-not-allowed disabled:opacity-50"),
|
|
385
|
+
children: t.save
|
|
386
|
+
})
|
|
387
|
+
]
|
|
388
|
+
})
|
|
389
|
+
]
|
|
390
|
+
})
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
394
|
+
export { AddRowButton, ChoiceButtons, DialogShell, EXPRESSION, ExpressionToggle, FieldError, FieldLabel, GROWING_AREA, GrowingTextarea, KeyHint, LabelledField, Section, SectionMemory, StringListEditor, SuggestedInput, ToggleField, asRecord, diffPatch, expressionError, flagError, flagValue, isEmptyPatch, isScalar, labelled, openOnAddOf, parseJson, parseScalar, removeAt, sameValue, text, unchangedValue, updateAt, useGrowingArea, useSectionMemory, withoutUndefined };
|
|
@@ -1,7 +1,61 @@
|
|
|
1
|
-
import "classnames";
|
|
2
|
-
import "react";
|
|
3
|
-
import "react/jsx-runtime";
|
|
1
|
+
import cx from "classnames";
|
|
2
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
//#region src/graph/editorPrimitives.tsx
|
|
5
|
+
/** A small store for an editor's view state, so a change redraws only what reads it. */
|
|
6
|
+
function createStore(initial) {
|
|
7
|
+
let state = initial;
|
|
8
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
9
|
+
return {
|
|
10
|
+
get: () => state,
|
|
11
|
+
set(patch) {
|
|
12
|
+
state = {
|
|
13
|
+
...state,
|
|
14
|
+
...patch
|
|
15
|
+
};
|
|
16
|
+
for (const listener of listeners) listener();
|
|
17
|
+
},
|
|
18
|
+
subscribe(listener) {
|
|
19
|
+
listeners.add(listener);
|
|
20
|
+
return () => {
|
|
21
|
+
listeners.delete(listener);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function useStore(store, select) {
|
|
27
|
+
return useSyncExternalStore(store.subscribe, () => select(store.get()));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Gives `container` focus back once it has fallen to the page, as when the row, menu or dialog
|
|
31
|
+
* holding it goes away, so the editor's shortcuts, Cmd+Z among them, keep reaching it.
|
|
32
|
+
*/
|
|
33
|
+
function reclaimFocus(container) {
|
|
34
|
+
window.setTimeout(() => {
|
|
35
|
+
const active = document.activeElement;
|
|
36
|
+
if (container?.isConnected && (!active || active === document.body)) container.focus({ preventScroll: true });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Reclaims focus for the editor's container each time its dialog closes. */
|
|
40
|
+
function useFocusAfterDialog(store, container) {
|
|
41
|
+
const latest = useRef(container);
|
|
42
|
+
latest.current = container;
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let open = store.get().dialog !== null;
|
|
45
|
+
return store.subscribe(() => {
|
|
46
|
+
const now = store.get().dialog !== null;
|
|
47
|
+
if (open && !now) reclaimFocus(latest.current());
|
|
48
|
+
open = now;
|
|
49
|
+
});
|
|
50
|
+
}, [store]);
|
|
51
|
+
}
|
|
52
|
+
/** `openMenu` whose menus reclaim focus for the editor when they close. */
|
|
53
|
+
function returningFocus(openMenu, container) {
|
|
54
|
+
return (x, y, items, onClose, search) => openMenu(x, y, items, () => {
|
|
55
|
+
onClose?.();
|
|
56
|
+
reclaimFocus(container());
|
|
57
|
+
}, search);
|
|
58
|
+
}
|
|
5
59
|
var DRAG_THRESHOLD = 4;
|
|
6
60
|
function suppressNextClick() {
|
|
7
61
|
const swallow = (e) => {
|
|
@@ -55,5 +109,42 @@ function trackDrag(start, { onMove, onEnd, onCancel, escape: cancelOnEscape = fa
|
|
|
55
109
|
window.addEventListener("pointercancel", cancel);
|
|
56
110
|
if (cancelOnEscape) window.addEventListener("keydown", key, true);
|
|
57
111
|
}
|
|
112
|
+
function place(box) {
|
|
113
|
+
return {
|
|
114
|
+
left: box.left,
|
|
115
|
+
top: box.top,
|
|
116
|
+
width: box.right - box.left,
|
|
117
|
+
height: box.bottom - box.top
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** The box a shift + drag draws, in its layer's coordinates. */
|
|
121
|
+
function MarqueeBox({ box, className }) {
|
|
122
|
+
return /* @__PURE__ */ jsx("div", {
|
|
123
|
+
className: cx("absolute rounded-md border-2 border-dashed border-(--theme-element) bg-(--theme-element)/10", className),
|
|
124
|
+
style: place(box)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
var OPENED = "[role=\"menu\"], [role=\"dialog\"], [role=\"listbox\"]";
|
|
128
|
+
/** Calls `clear` when a press lands outside the editor and everything it has open. */
|
|
129
|
+
function usePressOutside(container, clear) {
|
|
130
|
+
const latest = useRef({
|
|
131
|
+
container,
|
|
132
|
+
clear
|
|
133
|
+
});
|
|
134
|
+
latest.current = {
|
|
135
|
+
container,
|
|
136
|
+
clear
|
|
137
|
+
};
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const onDown = (e) => {
|
|
140
|
+
const root = latest.current.container();
|
|
141
|
+
const target = e.target;
|
|
142
|
+
if (!root || !(target instanceof Element) || root.contains(target) || target.closest(OPENED) || target.querySelector(`:scope > :is(${OPENED})`)) return;
|
|
143
|
+
latest.current.clear();
|
|
144
|
+
};
|
|
145
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
146
|
+
return () => document.removeEventListener("pointerdown", onDown, true);
|
|
147
|
+
}, []);
|
|
148
|
+
}
|
|
58
149
|
//#endregion
|
|
59
|
-
export { trackDrag };
|
|
150
|
+
export { MarqueeBox, createStore, place, reclaimFocus, returningFocus, trackDrag, useFocusAfterDialog, usePressOutside, useStore };
|
|
@@ -695,5 +695,13 @@ function useGraphEditorStrings() {
|
|
|
695
695
|
...overrides
|
|
696
696
|
}), [overrides]);
|
|
697
697
|
}
|
|
698
|
+
/** The input form editor's strings: the host's translations over the English defaults. */
|
|
699
|
+
function useInputsEditorStrings() {
|
|
700
|
+
const overrides = useStrings().inputsEditor;
|
|
701
|
+
return useMemo(() => ({
|
|
702
|
+
...INPUTS_EDITOR_STRINGS,
|
|
703
|
+
...overrides
|
|
704
|
+
}), [overrides]);
|
|
705
|
+
}
|
|
698
706
|
//#endregion
|
|
699
|
-
export { GRAPH_EDITOR_STRINGS, INPUTS_EDITOR_STRINGS, useGraphEditorStrings };
|
|
707
|
+
export { GRAPH_EDITOR_STRINGS, INPUTS_EDITOR_STRINGS, useGraphEditorStrings, useInputsEditorStrings };
|