@parallelworks/ui 0.36.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.
Files changed (106) hide show
  1. package/README.md +16 -0
  2. package/dist/ai/agent/permissions.d.ts +2 -2
  3. package/dist/ai/agent/permissions.js +3 -1
  4. package/dist/ai/components/ChatSidebar.js +7 -4
  5. package/dist/ai/components/agent/PermissionPicker.js +1 -0
  6. package/dist/ai/index.d.ts +1 -1
  7. package/dist/ai/index.js +2 -2
  8. package/dist/ai/strings.d.ts +2 -0
  9. package/dist/ai/strings.js +2 -0
  10. package/dist/ai/ui/sidebar.d.ts +6 -0
  11. package/dist/ai/ui/sidebar.js +15 -1
  12. package/dist/components/Dropdown.d.ts +2 -0
  13. package/dist/components/Dropdown.js +7 -2
  14. package/dist/components/Input.d.ts +11 -6
  15. package/dist/components/Input.js +93 -74
  16. package/dist/components/Provider.d.ts +31 -0
  17. package/dist/components/Provider.js +50 -2
  18. package/dist/editing.d.ts +404 -0
  19. package/dist/editor/EditorField.js +1 -1
  20. package/dist/editor/Monaco.d.ts +15 -1
  21. package/dist/editor/Monaco.js +117 -13
  22. package/dist/editor/editorModel.d.ts +5 -0
  23. package/dist/editor/editorModel.js +23 -0
  24. package/dist/editor/index.d.ts +4 -0
  25. package/dist/editor/index.js +5 -1
  26. package/dist/editor/lintContext.d.ts +21 -0
  27. package/dist/editor/lintContext.js +136 -0
  28. package/dist/editor/lintOwner.d.ts +2 -0
  29. package/dist/editor/lintOwner.js +5 -0
  30. package/dist/editor/nestedText.d.ts +13 -0
  31. package/dist/editor/nestedText.js +15 -0
  32. package/dist/editor/reveal.d.ts +4 -0
  33. package/dist/editor/reveal.js +15 -0
  34. package/dist/editor/settingsYaml.d.ts +12 -0
  35. package/dist/editor/settingsYaml.js +41 -0
  36. package/dist/form/FieldWrapper.js +1 -0
  37. package/dist/form/Fields/DropdownField.js +1 -1
  38. package/dist/form/Fields/DurationField.js +1 -1
  39. package/dist/form/Form.js +65 -24
  40. package/dist/form/Wizard/WizardContainer.js +66 -6
  41. package/dist/form/Wizard/WizardStepContent.js +23 -10
  42. package/dist/form/Wizard/wizardPages.d.ts +12 -0
  43. package/dist/form/Wizard/wizardPages.js +5 -0
  44. package/dist/form/fieldRegistry.js +1 -1
  45. package/dist/form/formEditing.d.ts +25 -0
  46. package/dist/form/formEditing.js +20 -0
  47. package/dist/form/index.js +5 -5
  48. package/dist/graph/InputDialog.d.ts +80 -0
  49. package/dist/graph/InputDialog.js +2110 -0
  50. package/dist/graph/InputsEditor.d.ts +12 -0
  51. package/dist/graph/InputsEditor.js +1594 -0
  52. package/dist/graph/ProblemsButton.d.ts +9 -0
  53. package/dist/graph/ProblemsButton.js +61 -0
  54. package/dist/graph/ShortcutsButton.d.ts +15 -0
  55. package/dist/graph/ShortcutsButton.js +56 -0
  56. package/dist/graph/SuggestionInput.d.ts +18 -0
  57. package/dist/graph/SuggestionInput.js +29 -0
  58. package/dist/graph/ToolbarPopover.d.ts +8 -0
  59. package/dist/graph/ToolbarPopover.js +71 -0
  60. package/dist/graph/editorApi.d.ts +51 -0
  61. package/dist/graph/editorApi.js +8 -0
  62. package/dist/graph/editorChrome.d.ts +27 -0
  63. package/dist/graph/editorChrome.js +75 -0
  64. package/dist/graph/editorFields.d.ts +180 -0
  65. package/dist/graph/editorFields.js +394 -0
  66. package/dist/graph/editorPrimitives.d.ts +60 -0
  67. package/dist/graph/editorPrimitives.js +150 -0
  68. package/dist/graph/editorStrings.d.ts +695 -0
  69. package/dist/graph/editorStrings.js +707 -0
  70. package/dist/graph/expressionRefs.d.ts +12 -0
  71. package/dist/graph/fieldProblems.d.ts +32 -0
  72. package/dist/graph/fieldProblems.js +77 -0
  73. package/dist/graph/index.d.ts +7 -0
  74. package/dist/graph/index.js +6 -1
  75. package/dist/graph/inputLayout.d.ts +25 -0
  76. package/dist/graph/inputLayout.js +337 -0
  77. package/dist/graph/inputPages.d.ts +33 -0
  78. package/dist/graph/inputPages.js +186 -0
  79. package/dist/graph/inputRefs.d.ts +103 -0
  80. package/dist/graph/inputRefs.js +196 -0
  81. package/dist/graph/inputRows.d.ts +43 -0
  82. package/dist/graph/inputRows.js +65 -0
  83. package/dist/graph/records.d.ts +3 -0
  84. package/dist/graph/records.js +7 -0
  85. package/dist/graph/refusalText.d.ts +6 -0
  86. package/dist/graph/settingsViews.d.ts +26 -0
  87. package/dist/graph/settingsViews.js +62 -0
  88. package/dist/graph/usesInputs.d.ts +17 -0
  89. package/dist/graph/usesInputs.js +40 -0
  90. package/dist/icons/bi/BiRedo.js +16 -0
  91. package/dist/icons/bi/BiUndo.js +16 -0
  92. package/dist/icons/lu/LuBraces.js +27 -0
  93. package/dist/icons/lu/LuCode.js +27 -0
  94. package/dist/icons/lu/LuKeyboard.js +71 -0
  95. package/dist/icons/lu/LuNetwork.js +63 -0
  96. package/dist/icons/lu/LuTextCursorInput.js +45 -0
  97. package/dist/icons.d.ts +2 -2
  98. package/dist/icons.js +8 -1
  99. package/dist/index.d.ts +2 -1
  100. package/dist/index.js +2 -2
  101. package/dist/list/ActionToolbar.d.ts +2 -0
  102. package/dist/list/ActionToolbar.js +18 -5
  103. package/dist/list/RowContextMenu.d.ts +10 -1
  104. package/dist/list/RowContextMenu.js +185 -30
  105. package/dist/styles.css +1 -1
  106. package/package.json +1 -1
@@ -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 };
@@ -0,0 +1,60 @@
1
+ import type { OpenMenu } from '../list/RowContextMenu.js';
2
+ export interface Point {
3
+ x: number;
4
+ y: number;
5
+ }
6
+ export interface Box {
7
+ left: number;
8
+ top: number;
9
+ right: number;
10
+ bottom: number;
11
+ }
12
+ /** A small store for an editor's view state, so a change redraws only what reads it. */
13
+ export declare function createStore<T extends object>(initial: T): {
14
+ get: () => T;
15
+ set(patch: Partial<T>): void;
16
+ subscribe(listener: () => void): () => void;
17
+ };
18
+ export type Store<T extends object> = ReturnType<typeof createStore<T>>;
19
+ export declare function useStore<T extends object, V>(store: Store<T>, select: (state: T) => V): V;
20
+ /**
21
+ * Gives `container` focus back once it has fallen to the page, as when the row, menu or dialog
22
+ * holding it goes away, so the editor's shortcuts, Cmd+Z among them, keep reaching it.
23
+ */
24
+ export declare function reclaimFocus(container: HTMLElement | null | undefined): void;
25
+ /** Reclaims focus for the editor's container each time its dialog closes. */
26
+ export declare function useFocusAfterDialog<T extends {
27
+ dialog: unknown;
28
+ }>(store: Store<T>, container: () => HTMLElement | null): void;
29
+ /** `openMenu` whose menus reclaim focus for the editor when they close. */
30
+ export declare function returningFocus(openMenu: OpenMenu, container: () => HTMLElement | null): OpenMenu;
31
+ interface DragOptions {
32
+ /** Each move, once the pointer has gone past the threshold. */
33
+ onMove?: (client: Point) => void;
34
+ /** The release; `dragged` tells a drag from a click. */
35
+ onEnd?: (client: Point, dragged: boolean) => void;
36
+ /** A cancelled pointer, or Escape when `escape` is on. */
37
+ onCancel?: (dragged: boolean) => void;
38
+ /** Escape cancels the press, and goes no further. */
39
+ escape?: boolean;
40
+ /** Keeps the page's text from selecting: from the press, or once it is a drag. */
41
+ holdText?: 'press' | 'drag';
42
+ /** The click after the release: swallowed always, only after a drag, or never. */
43
+ swallowClick?: 'always' | 'drag' | 'never';
44
+ }
45
+ /** Follows a press from `start` until it's released or cancelled. */
46
+ export declare function trackDrag(start: Point, { onMove, onEnd, onCancel, escape: cancelOnEscape, holdText, swallowClick, }: DragOptions): void;
47
+ export declare function place(box: Box): {
48
+ left: number;
49
+ top: number;
50
+ width: number;
51
+ height: number;
52
+ };
53
+ /** The box a shift + drag draws, in its layer's coordinates. */
54
+ export declare function MarqueeBox({ box, className }: {
55
+ box: Box;
56
+ className?: string;
57
+ }): import("react").JSX.Element;
58
+ /** Calls `clear` when a press lands outside the editor and everything it has open. */
59
+ export declare function usePressOutside(container: () => HTMLElement | null, clear: () => void): void;
60
+ export {};
@@ -0,0 +1,150 @@
1
+ import cx from "classnames";
2
+ import { useEffect, useRef, useSyncExternalStore } from "react";
3
+ import { jsx } from "react/jsx-runtime";
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
+ }
59
+ var DRAG_THRESHOLD = 4;
60
+ function suppressNextClick() {
61
+ const swallow = (e) => {
62
+ e.stopPropagation();
63
+ e.preventDefault();
64
+ window.removeEventListener("click", swallow, true);
65
+ };
66
+ window.addEventListener("click", swallow, true);
67
+ setTimeout(() => window.removeEventListener("click", swallow, true), 0);
68
+ }
69
+ /** Follows a press from `start` until it's released or cancelled. */
70
+ function trackDrag(start, { onMove, onEnd, onCancel, escape: cancelOnEscape = false, holdText, swallowClick = "never" }) {
71
+ let dragged = false;
72
+ const cleanup = () => {
73
+ window.removeEventListener("pointermove", move);
74
+ window.removeEventListener("pointerup", up);
75
+ window.removeEventListener("pointercancel", cancel);
76
+ window.removeEventListener("keydown", key, true);
77
+ if (holdText) document.body.style.userSelect = "";
78
+ };
79
+ const move = (ev) => {
80
+ if (!dragged && Math.hypot(ev.clientX - start.x, ev.clientY - start.y) < DRAG_THRESHOLD) return;
81
+ if (!dragged && holdText === "drag") document.body.style.userSelect = "none";
82
+ dragged = true;
83
+ onMove?.({
84
+ x: ev.clientX,
85
+ y: ev.clientY
86
+ });
87
+ };
88
+ const up = (ev) => {
89
+ cleanup();
90
+ if (swallowClick === "always" || swallowClick === "drag" && dragged) suppressNextClick();
91
+ onEnd?.({
92
+ x: ev.clientX,
93
+ y: ev.clientY
94
+ }, dragged);
95
+ };
96
+ const cancel = () => {
97
+ cleanup();
98
+ onCancel?.(dragged);
99
+ };
100
+ const key = (ev) => {
101
+ if (ev.key !== "Escape") return;
102
+ ev.stopPropagation();
103
+ if (dragged && swallowClick !== "never") suppressNextClick();
104
+ cancel();
105
+ };
106
+ if (holdText === "press") document.body.style.userSelect = "none";
107
+ window.addEventListener("pointermove", move);
108
+ window.addEventListener("pointerup", up);
109
+ window.addEventListener("pointercancel", cancel);
110
+ if (cancelOnEscape) window.addEventListener("keydown", key, true);
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
+ }
149
+ //#endregion
150
+ export { MarqueeBox, createStore, place, reclaimFocus, returningFocus, trackDrag, useFocusAfterDialog, usePressOutside, useStore };