@parallelworks/ui 0.36.0 → 0.37.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 (79) 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/agent/PermissionPicker.js +1 -0
  5. package/dist/ai/strings.d.ts +2 -0
  6. package/dist/ai/strings.js +2 -0
  7. package/dist/components/Dropdown.d.ts +2 -0
  8. package/dist/components/Dropdown.js +7 -2
  9. package/dist/components/Input.d.ts +11 -6
  10. package/dist/components/Input.js +93 -74
  11. package/dist/components/Provider.d.ts +31 -0
  12. package/dist/components/Provider.js +50 -2
  13. package/dist/editing.d.ts +404 -0
  14. package/dist/editor/EditorField.js +1 -1
  15. package/dist/editor/Monaco.d.ts +15 -1
  16. package/dist/editor/Monaco.js +117 -13
  17. package/dist/editor/editorModel.d.ts +5 -0
  18. package/dist/editor/editorModel.js +23 -0
  19. package/dist/editor/index.d.ts +4 -0
  20. package/dist/editor/index.js +5 -1
  21. package/dist/editor/lintContext.d.ts +21 -0
  22. package/dist/editor/lintContext.js +136 -0
  23. package/dist/editor/lintOwner.d.ts +2 -0
  24. package/dist/editor/lintOwner.js +5 -0
  25. package/dist/editor/nestedText.d.ts +13 -0
  26. package/dist/editor/nestedText.js +15 -0
  27. package/dist/editor/reveal.d.ts +4 -0
  28. package/dist/editor/reveal.js +15 -0
  29. package/dist/editor/settingsYaml.d.ts +12 -0
  30. package/dist/editor/settingsYaml.js +41 -0
  31. package/dist/form/Fields/DropdownField.js +1 -1
  32. package/dist/form/Fields/DurationField.js +1 -1
  33. package/dist/form/Form.js +1 -1
  34. package/dist/form/fieldRegistry.js +1 -1
  35. package/dist/form/index.js +5 -5
  36. package/dist/graph/InputDialog.d.ts +80 -0
  37. package/dist/graph/InputDialog.js +596 -0
  38. package/dist/graph/ProblemsButton.d.ts +9 -0
  39. package/dist/graph/ProblemsButton.js +61 -0
  40. package/dist/graph/ShortcutsButton.d.ts +15 -0
  41. package/dist/graph/SuggestionInput.d.ts +18 -0
  42. package/dist/graph/ToolbarPopover.d.ts +8 -0
  43. package/dist/graph/ToolbarPopover.js +71 -0
  44. package/dist/graph/editorApi.d.ts +51 -0
  45. package/dist/graph/editorChrome.d.ts +27 -0
  46. package/dist/graph/editorFields.d.ts +180 -0
  47. package/dist/graph/editorPrimitives.d.ts +60 -0
  48. package/dist/graph/editorPrimitives.js +59 -0
  49. package/dist/graph/editorStrings.d.ts +695 -0
  50. package/dist/graph/editorStrings.js +699 -0
  51. package/dist/graph/expressionRefs.d.ts +12 -0
  52. package/dist/graph/fieldProblems.d.ts +32 -0
  53. package/dist/graph/index.d.ts +6 -0
  54. package/dist/graph/index.js +5 -1
  55. package/dist/graph/inputLayout.d.ts +25 -0
  56. package/dist/graph/inputRefs.d.ts +103 -0
  57. package/dist/graph/records.d.ts +3 -0
  58. package/dist/graph/records.js +7 -0
  59. package/dist/graph/refusalText.d.ts +6 -0
  60. package/dist/graph/settingsViews.d.ts +26 -0
  61. package/dist/graph/usesInputs.d.ts +17 -0
  62. package/dist/graph/usesInputs.js +40 -0
  63. package/dist/icons/bi/BiRedo.js +16 -0
  64. package/dist/icons/bi/BiUndo.js +16 -0
  65. package/dist/icons/lu/LuBraces.js +27 -0
  66. package/dist/icons/lu/LuCode.js +27 -0
  67. package/dist/icons/lu/LuKeyboard.js +71 -0
  68. package/dist/icons/lu/LuNetwork.js +63 -0
  69. package/dist/icons/lu/LuTextCursorInput.js +45 -0
  70. package/dist/icons.d.ts +2 -2
  71. package/dist/icons.js +8 -1
  72. package/dist/index.d.ts +2 -1
  73. package/dist/index.js +2 -2
  74. package/dist/list/ActionToolbar.d.ts +2 -0
  75. package/dist/list/ActionToolbar.js +18 -5
  76. package/dist/list/RowContextMenu.d.ts +10 -1
  77. package/dist/list/RowContextMenu.js +185 -30
  78. package/dist/styles.css +1 -1
  79. package/package.json +1 -1
@@ -0,0 +1,71 @@
1
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createPortal } from "react-dom";
4
+ //#region src/graph/ToolbarPopover.tsx
5
+ var GAP = 8;
6
+ var PANEL_WIDTH = 384;
7
+ function placeBy(anchor, width) {
8
+ const rect = anchor.getBoundingClientRect();
9
+ const above = rect.top - 16;
10
+ const below = window.innerHeight - rect.bottom - 16;
11
+ const right = Math.max(GAP, Math.min(window.innerWidth - rect.right, window.innerWidth - GAP - width));
12
+ return above >= below ? {
13
+ right,
14
+ bottom: window.innerHeight - rect.top + GAP,
15
+ maxHeight: above
16
+ } : {
17
+ right,
18
+ top: rect.bottom + GAP,
19
+ maxHeight: below
20
+ };
21
+ }
22
+ /** A toolbar button's panel, drawn over the page so the pane it sits in can't clip it. */
23
+ function ToolbarPopover({ anchor, label, onClose, children }) {
24
+ const panel = useRef(null);
25
+ const [place, setPlace] = useState(null);
26
+ const close = useRef(onClose);
27
+ close.current = onClose;
28
+ useLayoutEffect(() => {
29
+ const update = () => {
30
+ if (anchor.current) setPlace(placeBy(anchor.current, panel.current?.offsetWidth || PANEL_WIDTH));
31
+ };
32
+ update();
33
+ window.addEventListener("resize", update);
34
+ window.addEventListener("scroll", update, true);
35
+ return () => {
36
+ window.removeEventListener("resize", update);
37
+ window.removeEventListener("scroll", update, true);
38
+ };
39
+ }, [anchor]);
40
+ useEffect(() => {
41
+ panel.current?.focus({ preventScroll: true });
42
+ const away = (e) => {
43
+ const target = e.target;
44
+ if (!panel.current?.contains(target) && !anchor.current?.contains(target)) close.current();
45
+ };
46
+ const onEscape = (e) => {
47
+ if (e.key === "Escape") {
48
+ e.stopPropagation();
49
+ close.current();
50
+ anchor.current?.focus({ preventScroll: true });
51
+ }
52
+ };
53
+ window.addEventListener("pointerdown", away, true);
54
+ window.addEventListener("keydown", onEscape, true);
55
+ return () => {
56
+ window.removeEventListener("pointerdown", away, true);
57
+ window.removeEventListener("keydown", onEscape, true);
58
+ };
59
+ }, [anchor]);
60
+ return createPortal(/* @__PURE__ */ jsx("div", {
61
+ ref: panel,
62
+ role: "dialog",
63
+ "aria-label": label,
64
+ tabIndex: -1,
65
+ className: "fixed z-50 w-96 max-w-[calc(100vw-16px)] overflow-y-auto rounded-md border theme-border bg-(--theme-panel-bg) p-3 text-xs text-(--theme-app) shadow-lg outline-none",
66
+ style: place ?? { visibility: "hidden" },
67
+ children
68
+ }), document.body);
69
+ }
70
+ //#endregion
71
+ export { ToolbarPopover };
@@ -0,0 +1,51 @@
1
+ import type { GraphEdit, GraphEditResult } from '../editing.js';
2
+ import type { Box } from './editorPrimitives.js';
3
+ import type { ListedProblem } from './ProblemsButton.js';
4
+ import type { SettingsView } from './settingsViews.js';
5
+ export type { Box };
6
+ export interface EditorProblem {
7
+ message: string;
8
+ line: number;
9
+ job?: string;
10
+ step?: number;
11
+ input?: string[];
12
+ /** In a workflow-level setting the settings dialog edits. */
13
+ setting?: boolean;
14
+ }
15
+ /** What to show in the YAML beside the graph: a job, one of its steps, or a line. */
16
+ export type RevealTarget = {
17
+ job: string;
18
+ step?: number;
19
+ } | {
20
+ line: number;
21
+ };
22
+ /** Build-page callbacks that turn the dependency graph into an editor. */
23
+ export interface DependencyGraphEditor {
24
+ onEdit: (edit: GraphEdit) => GraphEditResult | undefined;
25
+ canUndo: boolean;
26
+ canRedo: boolean;
27
+ onUndo: () => void;
28
+ onRedo: () => void;
29
+ /** `uses` values offered in the step editor, such as the user's own workflows. */
30
+ usesSuggestions?: string[];
31
+ /** Opens the workflow-level settings: env, permissions, sessions and more. */
32
+ onOpenSettings?: () => void;
33
+ /** The YAML the edits apply to, for editing a job or step as text. */
34
+ readSource?: () => string;
35
+ /** The view the job, step and input dialogs open in; YAML when not given. */
36
+ settingsView?: SettingsView;
37
+ onSettingsViewChange?: (view: SettingsView) => void;
38
+ /** False adds a new job, step or input with its defaults, without opening its dialog. */
39
+ openOnAdd?: boolean;
40
+ onOpenOnAddChange?: (open: boolean) => void;
41
+ /** Problems in the workflow, marked on the jobs, steps and inputs they're in. */
42
+ problems?: EditorProblem[];
43
+ /** The problems this pane's toolbar counts and lists. */
44
+ listedProblems?: ListedProblem[];
45
+ /** Hands the page this pane's way to bring a problem into view, false where it can't; returns its undo. */
46
+ registerShow?: (show: (problem: EditorProblem) => boolean) => () => void;
47
+ /** Shows a job's, step's or line's YAML, when the YAML editor is beside the graph. */
48
+ onReveal?: (target: RevealTarget) => void;
49
+ }
50
+ export declare const typing: (target: EventTarget | null) => boolean;
51
+ export declare function overlaps(rect: DOMRect, box: Box): boolean;
@@ -0,0 +1,27 @@
1
+ import type { ReactNode, PointerEvent as ReactPointerEvent } from 'react';
2
+ import type { DependencyGraphEditor } from './editorApi.js';
3
+ import type { Point } from './editorPrimitives.js';
4
+ import { type ShortcutGroup } from './ShortcutsButton.js';
5
+ /** What a drag carries, beside the pointer at `at` in its layer's coordinates. */
6
+ export declare function DragLabel({ at, text, refused, }: {
7
+ at: Point;
8
+ text: string;
9
+ refused?: boolean;
10
+ }): import("react").JSX.Element;
11
+ export declare function BarDivider(): import("react").JSX.Element;
12
+ /** A toolbar chip that adds something: click to add it, or, given `onPointerDown`, drag it to where it goes. */
13
+ export declare function AddChip({ icon, label, hint, className, onPointerDown, onClick, }: {
14
+ icon: ReactNode;
15
+ label: string;
16
+ hint: string;
17
+ className?: string;
18
+ onPointerDown?: (e: ReactPointerEvent<HTMLButtonElement>) => void;
19
+ onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
20
+ }): import("react").JSX.Element;
21
+ /** The bar both editors draw: problems, undo and redo, the editor's own controls, settings, shortcuts. */
22
+ export declare function EditorBar({ editor, groups, className, children, }: {
23
+ editor: DependencyGraphEditor;
24
+ groups: ShortcutGroup[];
25
+ className?: string;
26
+ children: ReactNode;
27
+ }): import("react").JSX.Element;
@@ -0,0 +1,180 @@
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ import { Textarea } from '../components/Input.js';
3
+ import type { FieldPatch } from '../editing.js';
4
+ import { type GraphEditorStrings } from './editorStrings.js';
5
+ import { type Json } from './records.js';
6
+ export { asRecord, type Json } from './records.js';
7
+ export type Strings = GraphEditorStrings;
8
+ export declare const EXPRESSION: RegExp;
9
+ export declare const ENV_KEY: RegExp;
10
+ export declare const DURATION: RegExp;
11
+ export declare function withoutUndefined(value: Json): Json;
12
+ export declare function text(value: unknown): string;
13
+ export declare function isScalar(value: unknown): boolean;
14
+ export declare function parseScalar(value: string): unknown;
15
+ export declare function sameValue(a: unknown, b: unknown): boolean;
16
+ /** Keys whose value changed: undefined removes a key, anything else writes it. */
17
+ export declare function diffPatch(original: Json, next: Json): Required<FieldPatch>;
18
+ export declare function isEmptyPatch(patch: Required<FieldPatch>): boolean;
19
+ export declare function conditionText(value: unknown): string;
20
+ export declare function parseCondition(value: string): unknown;
21
+ export declare function durationError(value: string, t: Strings, pattern?: RegExp): string | undefined;
22
+ export declare function expressionError(value: string, t: Strings): string | undefined;
23
+ export declare function orUndefined(value: string): string | undefined;
24
+ export declare function countError(value: string, min: number, message: string): string | undefined;
25
+ export declare function parseCount(value: string): unknown;
26
+ export declare function parseJson(value: string): {
27
+ ok: boolean;
28
+ value?: unknown;
29
+ };
30
+ /** A key/value row; nested values are edited as JSON. */
31
+ export interface Row {
32
+ key: string;
33
+ value: string;
34
+ nested?: boolean;
35
+ /** What the row was read from; while its text is unchanged a save writes this back as it was. */
36
+ original?: unknown;
37
+ }
38
+ export declare function unchangedValue(text: string, original: unknown, nested?: boolean): boolean;
39
+ /** Rows for a map; with `nested` false a non-scalar is shown as its JSON text and saved as a string. */
40
+ export declare function rowsFrom(value: unknown, nested?: boolean): Row[];
41
+ export declare function rowsTo(rows: Row[], parse: (value: string) => unknown): Json | undefined;
42
+ export declare function rowsError(rows: Row[], keyPattern: RegExp, keyMessage: string, t: Strings, valueError?: (value: string) => string | undefined): string | undefined;
43
+ /** A switch that may instead hold a `${{ }}` expression. */
44
+ export type Flag = boolean | string | undefined;
45
+ export declare function flagOf(value: unknown): Flag;
46
+ export declare function flagError(value: Flag, t: Strings): string | undefined;
47
+ export declare function flagValue(checked: boolean, fallback: boolean, original: unknown): Flag;
48
+ /** Whether each section of a dialog is open, kept while the dialog is up so switching views keeps it. */
49
+ export declare const SectionMemory: import("react").Context<Map<string, boolean> | null>;
50
+ export declare function useSectionMemory(): Map<string, boolean>;
51
+ export declare function Section({ title, description, open, alert, children, }: {
52
+ title: string;
53
+ description?: string;
54
+ /** Whether it starts open; read once, when its dialog first shows it. */
55
+ open?: boolean;
56
+ /** A problem inside, which opens it so a folded section can't hide why Save is off. */
57
+ alert?: boolean;
58
+ children: ReactNode;
59
+ }): import("react").JSX.Element;
60
+ export declare function FieldError({ message }: {
61
+ message: string | undefined;
62
+ }): import("react").JSX.Element | null;
63
+ /** The YAML key a field sets, beside its name; left out where the name already says it. */
64
+ export declare function KeyHint({ label, yamlKey }: {
65
+ label: string;
66
+ yamlKey: string;
67
+ }): import("react").JSX.Element | null;
68
+ /** Label props for an Input or Textarea that sets `yamlKey`. */
69
+ export declare function labelled(label: string, yamlKey: string): {
70
+ label: string;
71
+ labelHint: import("react").JSX.Element;
72
+ };
73
+ /** A field's name and help text, for controls without their own label; `actions` sit at its right. */
74
+ export declare function FieldLabel({ label, yamlKey, description, htmlFor, descriptionId, actions, }: {
75
+ label: string;
76
+ yamlKey?: string | undefined;
77
+ description: string;
78
+ htmlFor?: string;
79
+ descriptionId?: string;
80
+ actions?: ReactNode;
81
+ }): import("react").JSX.Element;
82
+ /** A field's label and description above the editor that sets it. */
83
+ export declare function LabelledField({ children, ...label }: ComponentProps<typeof FieldLabel> & {
84
+ children: ReactNode;
85
+ }): import("react").JSX.Element;
86
+ /**
87
+ * Switches a field between its own control and a typed expression, where the schema takes one. Its
88
+ * name says what pressing it does, so it isn't also announced as pressed.
89
+ */
90
+ export declare function ExpressionToggle({ active, onChange, }: {
91
+ active: boolean;
92
+ onChange: (active: boolean) => void;
93
+ }): import("react").JSX.Element;
94
+ export declare function ToggleField({ label, yamlKey, description, checked, onChange, disabled, }: {
95
+ label: string;
96
+ yamlKey?: string;
97
+ description: string;
98
+ checked: boolean;
99
+ onChange: (checked: boolean) => void;
100
+ disabled?: boolean;
101
+ }): import("react").JSX.Element;
102
+ export declare function ChoiceButtons<T extends string>({ options, value, onChange, }: {
103
+ options: {
104
+ value: T;
105
+ label: string;
106
+ }[];
107
+ value: T;
108
+ onChange: (value: T) => void;
109
+ }): import("react").JSX.Element;
110
+ export declare function AddRowButton({ label, onClick }: {
111
+ label: string;
112
+ onClick: () => void;
113
+ }): import("react").JSX.Element;
114
+ export declare function KeyValueEditor({ rows, onChange, error, valuePlaceholder, keyPlaceholder, addLabel, valueSuggestions, lint, }: {
115
+ rows: Row[];
116
+ onChange: (rows: Row[]) => void;
117
+ error: string | undefined;
118
+ valuePlaceholder?: string;
119
+ keyPlaceholder?: string;
120
+ addLabel?: string;
121
+ /** Offered on each value, such as references to the workflow's inputs. */
122
+ valueSuggestions?: string[];
123
+ /** Whether each row shows the problems at its key, inside the caller's LintScope. */
124
+ lint?: boolean;
125
+ }): import("react").JSX.Element;
126
+ export declare function updateAt<T>(items: T[], index: number, item: T): T[];
127
+ export declare function removeAt<T>(items: T[], index: number): T[];
128
+ /** A list of single values, such as permissions or variable names. */
129
+ export declare function StringListEditor({ label, values, onChange, addLabel, placeholder, error, suggestions, }: {
130
+ /** The list's name, which names each of its rows. */
131
+ label: string;
132
+ values: string[];
133
+ onChange: (values: string[]) => void;
134
+ addLabel: string;
135
+ placeholder?: string;
136
+ error?: string | undefined;
137
+ /** Offered on each row; any other value can still be typed. */
138
+ suggestions?: string[];
139
+ }): import("react").JSX.Element;
140
+ export declare function SuggestedInput({ label, yamlKey, description, value, onChange, suggestions, error, placeholder, }: {
141
+ label: string;
142
+ yamlKey?: string;
143
+ description?: string;
144
+ value: string;
145
+ onChange: (value: string) => void;
146
+ suggestions: string[];
147
+ error?: string | undefined;
148
+ placeholder?: string;
149
+ }): import("react").JSX.Element;
150
+ /** A text area that starts one line tall and grows with its text. */
151
+ export declare function useGrowingArea(value: string): import("react").RefObject<HTMLTextAreaElement | null>;
152
+ export declare const GROWING_AREA = "!min-h-0 !resize-none overflow-hidden";
153
+ export declare function GrowingTextarea(props: Omit<ComponentProps<typeof Textarea>, 'rows'> & {
154
+ value: string;
155
+ }): import("react").JSX.Element;
156
+ /** Whether adding a job, step or input opens its dialog first; any of those dialogs sets it. */
157
+ export interface OpenOnAdd {
158
+ open: boolean;
159
+ onChange: (open: boolean) => void;
160
+ }
161
+ /** The dialogs' "open when adding" switch, when the host keeps the setting. */
162
+ export declare function openOnAddOf(editor: {
163
+ openOnAdd?: boolean;
164
+ onOpenOnAddChange?: (open: boolean) => void;
165
+ } | undefined): OpenOnAdd | undefined;
166
+ export interface DialogShellProps {
167
+ title: string;
168
+ onClose: () => void;
169
+ onSubmit: () => void;
170
+ saveDisabled: boolean;
171
+ dirty: boolean;
172
+ /** A dialog opened on top of this one owns Escape and outside clicks. */
173
+ locked?: boolean;
174
+ footerStart?: ReactNode;
175
+ /** Controls beside the title, such as a switch between views. */
176
+ headerEnd?: ReactNode;
177
+ openOnAdd?: OpenOnAdd | undefined;
178
+ children: ReactNode;
179
+ }
180
+ export declare function DialogShell({ title, onClose, onSubmit, saveDisabled, dirty, locked, footerStart, headerEnd, openOnAdd, children, }: DialogShellProps): import("react").JSX.Element;
@@ -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,59 @@
1
+ import "classnames";
2
+ import "react";
3
+ import "react/jsx-runtime";
4
+ //#region src/graph/editorPrimitives.tsx
5
+ var DRAG_THRESHOLD = 4;
6
+ function suppressNextClick() {
7
+ const swallow = (e) => {
8
+ e.stopPropagation();
9
+ e.preventDefault();
10
+ window.removeEventListener("click", swallow, true);
11
+ };
12
+ window.addEventListener("click", swallow, true);
13
+ setTimeout(() => window.removeEventListener("click", swallow, true), 0);
14
+ }
15
+ /** Follows a press from `start` until it's released or cancelled. */
16
+ function trackDrag(start, { onMove, onEnd, onCancel, escape: cancelOnEscape = false, holdText, swallowClick = "never" }) {
17
+ let dragged = false;
18
+ const cleanup = () => {
19
+ window.removeEventListener("pointermove", move);
20
+ window.removeEventListener("pointerup", up);
21
+ window.removeEventListener("pointercancel", cancel);
22
+ window.removeEventListener("keydown", key, true);
23
+ if (holdText) document.body.style.userSelect = "";
24
+ };
25
+ const move = (ev) => {
26
+ if (!dragged && Math.hypot(ev.clientX - start.x, ev.clientY - start.y) < DRAG_THRESHOLD) return;
27
+ if (!dragged && holdText === "drag") document.body.style.userSelect = "none";
28
+ dragged = true;
29
+ onMove?.({
30
+ x: ev.clientX,
31
+ y: ev.clientY
32
+ });
33
+ };
34
+ const up = (ev) => {
35
+ cleanup();
36
+ if (swallowClick === "always" || swallowClick === "drag" && dragged) suppressNextClick();
37
+ onEnd?.({
38
+ x: ev.clientX,
39
+ y: ev.clientY
40
+ }, dragged);
41
+ };
42
+ const cancel = () => {
43
+ cleanup();
44
+ onCancel?.(dragged);
45
+ };
46
+ const key = (ev) => {
47
+ if (ev.key !== "Escape") return;
48
+ ev.stopPropagation();
49
+ if (dragged && swallowClick !== "never") suppressNextClick();
50
+ cancel();
51
+ };
52
+ if (holdText === "press") document.body.style.userSelect = "none";
53
+ window.addEventListener("pointermove", move);
54
+ window.addEventListener("pointerup", up);
55
+ window.addEventListener("pointercancel", cancel);
56
+ if (cancelOnEscape) window.addEventListener("keydown", key, true);
57
+ }
58
+ //#endregion
59
+ export { trackDrag };