@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
@@ -24,15 +24,24 @@ export type RowMenuItem = {
24
24
  text: string;
25
25
  label: string;
26
26
  };
27
+ /** More text a menu's search matches, beside the label. */
28
+ keywords?: string;
27
29
  } | {
28
30
  kind: 'submenu';
29
31
  label: string;
30
32
  icon?: ReactNode;
31
33
  items: RowMenuItem[];
34
+ search?: MenuSearch | undefined;
32
35
  } | {
33
36
  kind: 'divider';
34
37
  };
35
- export type OpenMenu = (x: number, y: number, items: RowMenuItem[], onClose?: () => void) => void;
38
+ /** A field atop a menu that narrows it, submenus included, to the items it matches. */
39
+ export interface MenuSearch {
40
+ placeholder: string;
41
+ /** Shown when nothing matches. */
42
+ empty: string;
43
+ }
44
+ export type OpenMenu = (x: number, y: number, items: RowMenuItem[], onClose?: () => void, search?: MenuSearch) => void;
36
45
  /** The shared shape every entity action hook's per-row actions boil down to.
37
46
  * Each hook's bespoke `*RowAction` type is structurally assignable to this. */
38
47
  export interface RowAction {
@@ -14,7 +14,7 @@ import { keyedByContent } from "../components/keys.js";
14
14
  import { TOOLTIP_ID } from "../components/Tooltip.js";
15
15
  import cx from "classnames";
16
16
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
17
- import { jsx, jsxs } from "react/jsx-runtime";
17
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
18
18
  import { createPortal } from "react-dom";
19
19
  //#region src/list/RowContextMenu.tsx
20
20
  /** Converts a single row action to a menu item: a `to` makes it a link, otherwise an action. */
@@ -74,14 +74,15 @@ function assembleRowMenu({ lead = [], actions, exclude, order, trailing = ["dele
74
74
  function useRowMenu(zClassName = "z-50") {
75
75
  const [menu, setMenu] = useState(null);
76
76
  const onCloseRef = useRef(void 0);
77
- const openMenu = useCallback((x, y, items, onClose) => {
77
+ const openMenu = useCallback((x, y, items, onClose, search) => {
78
78
  if (items.length > 0) {
79
79
  onCloseRef.current?.();
80
80
  onCloseRef.current = onClose;
81
81
  setMenu({
82
82
  x,
83
83
  y,
84
- items
84
+ items,
85
+ search
85
86
  });
86
87
  }
87
88
  }, []);
@@ -104,9 +105,12 @@ function useRowMenuActive(openMenu) {
104
105
  const [active, setActive] = useState(false);
105
106
  return {
106
107
  active,
107
- openMenu: useCallback((x, y, items) => {
108
+ openMenu: useCallback((x, y, items, onClose, search) => {
108
109
  setActive(true);
109
- openMenu(x, y, items, () => setActive(false));
110
+ openMenu(x, y, items, () => {
111
+ setActive(false);
112
+ onClose?.();
113
+ }, search);
110
114
  }, [openMenu])
111
115
  };
112
116
  }
@@ -120,22 +124,46 @@ function rowContextMenuProps(openMenu, items) {
120
124
  function RowContextMenu({ state, onClose, zClassName }) {
121
125
  const ref = useRef(null);
122
126
  const [pos, setPos] = useState(null);
127
+ const opener = useRef(null);
128
+ const wasOpen = useRef(false);
123
129
  useLayoutEffect(() => {
124
- if (!state || !ref.current) {
130
+ const menu = ref.current;
131
+ if (!state || !menu) {
125
132
  setPos(null);
133
+ wasOpen.current = false;
126
134
  return;
127
135
  }
128
- const { width, height } = ref.current.getBoundingClientRect();
129
- const pad = 8;
130
- const top = height > window.innerHeight - pad - state.y && state.y - height - pad >= 0 ? state.y - height : Math.min(state.y, window.innerHeight - height - pad);
136
+ if (!wasOpen.current) {
137
+ opener.current = document.activeElement;
138
+ wasOpen.current = true;
139
+ }
140
+ const { width, height } = menu.getBoundingClientRect();
141
+ const pad = MENU_EDGE;
142
+ const openUp = height > window.innerHeight - pad - state.y && state.y - height - pad >= 0;
143
+ const top = Math.max(pad, openUp ? state.y - height : Math.min(state.y, window.innerHeight - height - pad));
131
144
  setPos({
132
145
  left: Math.max(pad, Math.min(state.x, window.innerWidth - width - pad)),
133
- top: Math.max(pad, top)
146
+ top,
147
+ maxHeight: (openUp ? state.y : window.innerHeight - pad) - top
134
148
  });
135
149
  }, [state]);
150
+ const placed = pos !== null;
151
+ useEffect(() => {
152
+ const menu = ref.current;
153
+ if (!placed || !menu) return;
154
+ const from = opener.current;
155
+ const target = menu.querySelector(":scope > div > input[type=\"search\"]") ?? (focusVisible(from) ? menu.querySelector(MENU_ITEMS) : null);
156
+ if (!target) return;
157
+ target.focus({ preventScroll: true });
158
+ return () => {
159
+ if (from instanceof HTMLElement && document.activeElement === document.body) from.focus({ preventScroll: true });
160
+ };
161
+ }, [placed, state]);
136
162
  useEffect(() => {
137
163
  if (!state) return;
138
- const close = () => onClose();
164
+ const close = (e) => {
165
+ if (!(e.target instanceof Node && ref.current?.contains(e.target))) onClose();
166
+ };
139
167
  const onKey = (e) => {
140
168
  if (e.key === "Escape") onClose();
141
169
  };
@@ -178,23 +206,116 @@ function RowContextMenu({ state, onClose, zClassName }) {
178
206
  children: /* @__PURE__ */ jsx("div", {
179
207
  ref,
180
208
  role: "menu",
181
- className: "absolute min-w-44 rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) py-1 shadow-lg backdrop-blur-xl",
209
+ className: "absolute flex min-w-44 flex-col rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) py-1 shadow-lg backdrop-blur-xl",
182
210
  style: {
183
211
  left: pos?.left ?? state.x,
184
212
  top: pos?.top ?? state.y,
213
+ maxHeight: state.search ? pos?.maxHeight : void 0,
185
214
  visibility: pos ? "visible" : "hidden"
186
215
  },
216
+ onKeyDown: (e) => {
217
+ const step = {
218
+ ArrowDown: 1,
219
+ ArrowUp: -1
220
+ }[e.key];
221
+ if (!step || e.defaultPrevented || !ref.current) return;
222
+ e.preventDefault();
223
+ const items = [...ref.current.querySelectorAll(MENU_ITEMS)];
224
+ const at = items.indexOf(document.activeElement);
225
+ focusItem(items[((at < 0 ? step > 0 ? 0 : items.length - 1 : at + step) + items.length) % items.length]);
226
+ },
187
227
  children: /* @__PURE__ */ jsx(MenuItemList, {
188
228
  items: state.items,
229
+ search: state.search,
189
230
  onClose,
190
231
  side
191
232
  })
192
233
  })
193
234
  }), document.body);
194
235
  }
236
+ var MENU_EDGE = 8;
237
+ function focusItem(item) {
238
+ item?.focus({ preventScroll: true });
239
+ item?.scrollIntoView?.({ block: "nearest" });
240
+ }
241
+ function SubmenuPanel({ side, panelRef, searchable, children }) {
242
+ const [place, setPlace] = useState(null);
243
+ useLayoutEffect(() => {
244
+ const rect = panelRef.current?.getBoundingClientRect();
245
+ if (rect) {
246
+ const bottom = window.innerHeight - MENU_EDGE;
247
+ const lift = Math.max(0, Math.min(rect.bottom - bottom, rect.top - MENU_EDGE));
248
+ setPlace({
249
+ lift,
250
+ maxHeight: bottom - (rect.top - lift)
251
+ });
252
+ }
253
+ }, [panelRef]);
254
+ return /* @__PURE__ */ jsx("div", {
255
+ ref: panelRef,
256
+ className: cx("absolute -top-1 z-10 flex min-w-44 flex-col rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) py-1 shadow-lg", side === "left" ? "right-full" : "left-full"),
257
+ style: place ? {
258
+ ...searchable ? { maxHeight: place.maxHeight } : {},
259
+ ...place.lift > 0 ? { transform: `translateY(-${place.lift}px)` } : {}
260
+ } : void 0,
261
+ children
262
+ });
263
+ }
264
+ var MENU_ITEMS = "button:not([disabled]), a[href]";
265
+ function focusVisible(element) {
266
+ try {
267
+ return !!element?.matches(":focus-visible");
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
195
272
  var MENU_ITEM_CLASSES = "flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-[13px] hover:bg-(--theme-muted-panel-bg) transition-colors cursor-pointer [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0";
196
- function MenuItemList({ items, onClose, side }) {
197
- return keyedByContent(items, (it) => it.kind === "divider" ? "divider" : it.label).map(({ key, item }) => item.kind === "divider" ? /* @__PURE__ */ jsx("div", {
273
+ function matchesSearch(item, needle) {
274
+ const keywords = item.kind === "action" ? item.keywords ?? "" : "";
275
+ return `${item.label} ${keywords}`.toLowerCase().includes(needle);
276
+ }
277
+ function searchMatches(items, needle) {
278
+ const found = /* @__PURE__ */ new Map();
279
+ const walk = (list) => {
280
+ for (const item of list) if (item.kind === "submenu") walk(item.items);
281
+ else if (item.kind !== "divider" && !found.has(item.label) && matchesSearch(item, needle)) found.set(item.label, item);
282
+ };
283
+ walk(items);
284
+ return [...found.values()];
285
+ }
286
+ function MenuSearchField({ search, query, onQuery, onEnter }) {
287
+ return /* @__PURE__ */ jsx("div", {
288
+ className: "px-2 pt-0.5 pb-1",
289
+ children: /* @__PURE__ */ jsx("input", {
290
+ type: "search",
291
+ "aria-label": search.placeholder,
292
+ placeholder: search.placeholder,
293
+ value: query,
294
+ onChange: (e) => onQuery(e.target.value),
295
+ onKeyDown: (e) => {
296
+ if (e.key === "Enter") {
297
+ e.preventDefault();
298
+ onEnter();
299
+ } else if (e.key === "ArrowDown") {
300
+ e.preventDefault();
301
+ focusItem(e.currentTarget.parentElement?.parentElement?.querySelector(MENU_ITEMS));
302
+ }
303
+ },
304
+ className: "w-full rounded-md border border-(--theme-border) bg-(--theme-muted-panel-bg) px-2 py-1 text-[13px] text-(--theme-app) placeholder:text-(--theme-muted-text-color) focus:border-(--theme-element) focus:outline-none"
305
+ })
306
+ });
307
+ }
308
+ function MenuItemList({ items, search, onClose, side }) {
309
+ const [query, setQuery] = useState("");
310
+ const needle = query.trim().toLowerCase();
311
+ const searching = !!search && !!needle;
312
+ const shown = searching ? searchMatches(items, needle) : items;
313
+ const run = useRunItem(onClose);
314
+ const pickFirst = () => {
315
+ const first = shown.find((item) => item.kind === "action" && !item.disabled);
316
+ if (first?.kind === "action") run(first);
317
+ };
318
+ const rows = keyedByContent(shown, (it) => it.kind === "divider" ? "divider" : it.label).map(({ key, item }) => item.kind === "divider" ? /* @__PURE__ */ jsx("div", {
198
319
  "aria-hidden": "true",
199
320
  className: "my-1 h-px bg-(--theme-border)"
200
321
  }, key) : /* @__PURE__ */ jsx(MenuRow, {
@@ -202,13 +323,30 @@ function MenuItemList({ items, onClose, side }) {
202
323
  onClose,
203
324
  side
204
325
  }, key));
326
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
327
+ search && /* @__PURE__ */ jsx(MenuSearchField, {
328
+ search,
329
+ query,
330
+ onQuery: setQuery,
331
+ onEnter: pickFirst
332
+ }),
333
+ searching ? /* @__PURE__ */ jsx("div", {
334
+ className: "max-h-[min(20rem,60vh)] min-h-0 overflow-y-auto",
335
+ children: rows
336
+ }) : rows,
337
+ searching && shown.length === 0 && /* @__PURE__ */ jsx("div", {
338
+ className: "px-3 py-1.5 text-[13px] text-(--theme-muted-text-color)",
339
+ children: search.empty
340
+ })
341
+ ] });
205
342
  }
206
343
  function MenuRow({ item, onClose, side }) {
207
344
  const Link = useLink();
208
- const notify = useNotify();
209
- const { list: t } = useStrings();
345
+ const run = useRunItem(onClose);
210
346
  const [openSub, setOpenSub] = useState(false);
211
347
  const closeTimer = useRef(void 0);
348
+ const panel = useRef(null);
349
+ const hovered = useRef(false);
212
350
  if (item.kind === "submenu") {
213
351
  const open = () => {
214
352
  clearTimeout(closeTimer.current);
@@ -220,10 +358,19 @@ function MenuRow({ item, onClose, side }) {
220
358
  return /* @__PURE__ */ jsxs("div", {
221
359
  className: "relative",
222
360
  role: "none",
223
- onMouseEnter: open,
224
- onMouseLeave: close,
361
+ onMouseEnter: () => {
362
+ hovered.current = true;
363
+ open();
364
+ },
365
+ onMouseLeave: () => {
366
+ hovered.current = false;
367
+ if (!panel.current?.contains(document.activeElement)) close();
368
+ },
225
369
  onFocus: open,
226
- onBlur: close,
370
+ onBlur: (e) => {
371
+ const to = e.relatedTarget;
372
+ if (!e.currentTarget.contains(to) && (to !== null || !hovered.current)) close();
373
+ },
227
374
  children: [/* @__PURE__ */ jsxs("button", {
228
375
  type: "button",
229
376
  "aria-expanded": openSub,
@@ -234,10 +381,13 @@ function MenuRow({ item, onClose, side }) {
234
381
  className: "flex items-center gap-2.5",
235
382
  children: [item.icon, item.label]
236
383
  }), /* @__PURE__ */ jsx(LuChevronRight, { className: "h-3 w-3 shrink-0 opacity-60" })]
237
- }), openSub && /* @__PURE__ */ jsx("div", {
238
- className: cx("absolute -top-1 z-10 min-w-44 rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) py-1 shadow-lg", side === "left" ? "right-full" : "left-full"),
384
+ }), openSub && /* @__PURE__ */ jsx(SubmenuPanel, {
385
+ side,
386
+ panelRef: panel,
387
+ searchable: !!item.search,
239
388
  children: /* @__PURE__ */ jsx(MenuItemList, {
240
389
  items: item.items,
390
+ search: item.search,
241
391
  onClose,
242
392
  side
243
393
  })
@@ -267,15 +417,7 @@ function MenuRow({ item, onClose, side }) {
267
417
  "data-tooltip-id": TOOLTIP_ID,
268
418
  "data-tooltip-content": item.tooltip
269
419
  } : {},
270
- onClick: () => {
271
- if (item.disabled) return;
272
- if (item.copy) {
273
- const { text, label } = item.copy;
274
- navigator.clipboard?.writeText(text).then(() => notify.success(t.copied(label)), () => notify.error(t.couldntCopy(label)));
275
- }
276
- item.onSelect?.();
277
- onClose();
278
- },
420
+ onClick: () => run(item),
279
421
  className: cx(MENU_ITEM_CLASSES, item.destructive ? "text-red-500" : "text-(--theme-app)", item.disabled && "opacity-40 cursor-not-allowed hover:bg-transparent"),
280
422
  children: [
281
423
  item.icon,
@@ -284,6 +426,19 @@ function MenuRow({ item, onClose, side }) {
284
426
  ]
285
427
  });
286
428
  }
429
+ function useRunItem(onClose) {
430
+ const notify = useNotify();
431
+ const { list: t } = useStrings();
432
+ return (item) => {
433
+ if (item.disabled) return;
434
+ if (item.copy) {
435
+ const { text, label } = item.copy;
436
+ navigator.clipboard?.writeText(text).then(() => notify.success(t.copied(label)), () => notify.error(t.couldntCopy(label)));
437
+ }
438
+ item.onSelect?.();
439
+ onClose();
440
+ };
441
+ }
287
442
  /** Returns a builder for the Copy submenu, labeled from the provider's strings. */
288
443
  function useCopySubmenu() {
289
444
  const { list: t } = useStrings();