@parallelworks/ui 0.35.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 (81) 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/AgentMessageParts.js +8 -2
  5. package/dist/ai/components/agent/PermissionPicker.js +1 -0
  6. package/dist/ai/strings.d.ts +8 -0
  7. package/dist/ai/strings.js +8 -0
  8. package/dist/ai/types.d.ts +2 -0
  9. package/dist/components/Dropdown.d.ts +2 -0
  10. package/dist/components/Dropdown.js +7 -2
  11. package/dist/components/Input.d.ts +11 -6
  12. package/dist/components/Input.js +93 -74
  13. package/dist/components/Provider.d.ts +31 -0
  14. package/dist/components/Provider.js +50 -2
  15. package/dist/editing.d.ts +404 -0
  16. package/dist/editor/EditorField.js +1 -1
  17. package/dist/editor/Monaco.d.ts +15 -1
  18. package/dist/editor/Monaco.js +117 -13
  19. package/dist/editor/editorModel.d.ts +5 -0
  20. package/dist/editor/editorModel.js +23 -0
  21. package/dist/editor/index.d.ts +4 -0
  22. package/dist/editor/index.js +5 -1
  23. package/dist/editor/lintContext.d.ts +21 -0
  24. package/dist/editor/lintContext.js +136 -0
  25. package/dist/editor/lintOwner.d.ts +2 -0
  26. package/dist/editor/lintOwner.js +5 -0
  27. package/dist/editor/nestedText.d.ts +13 -0
  28. package/dist/editor/nestedText.js +15 -0
  29. package/dist/editor/reveal.d.ts +4 -0
  30. package/dist/editor/reveal.js +15 -0
  31. package/dist/editor/settingsYaml.d.ts +12 -0
  32. package/dist/editor/settingsYaml.js +41 -0
  33. package/dist/form/Fields/DropdownField.js +1 -1
  34. package/dist/form/Fields/DurationField.js +1 -1
  35. package/dist/form/Form.js +1 -1
  36. package/dist/form/fieldRegistry.js +1 -1
  37. package/dist/form/index.js +5 -5
  38. package/dist/graph/InputDialog.d.ts +80 -0
  39. package/dist/graph/InputDialog.js +596 -0
  40. package/dist/graph/ProblemsButton.d.ts +9 -0
  41. package/dist/graph/ProblemsButton.js +61 -0
  42. package/dist/graph/ShortcutsButton.d.ts +15 -0
  43. package/dist/graph/SuggestionInput.d.ts +18 -0
  44. package/dist/graph/ToolbarPopover.d.ts +8 -0
  45. package/dist/graph/ToolbarPopover.js +71 -0
  46. package/dist/graph/editorApi.d.ts +51 -0
  47. package/dist/graph/editorChrome.d.ts +27 -0
  48. package/dist/graph/editorFields.d.ts +180 -0
  49. package/dist/graph/editorPrimitives.d.ts +60 -0
  50. package/dist/graph/editorPrimitives.js +59 -0
  51. package/dist/graph/editorStrings.d.ts +695 -0
  52. package/dist/graph/editorStrings.js +699 -0
  53. package/dist/graph/expressionRefs.d.ts +12 -0
  54. package/dist/graph/fieldProblems.d.ts +32 -0
  55. package/dist/graph/index.d.ts +6 -0
  56. package/dist/graph/index.js +5 -1
  57. package/dist/graph/inputLayout.d.ts +25 -0
  58. package/dist/graph/inputRefs.d.ts +103 -0
  59. package/dist/graph/records.d.ts +3 -0
  60. package/dist/graph/records.js +7 -0
  61. package/dist/graph/refusalText.d.ts +6 -0
  62. package/dist/graph/settingsViews.d.ts +26 -0
  63. package/dist/graph/usesInputs.d.ts +17 -0
  64. package/dist/graph/usesInputs.js +40 -0
  65. package/dist/icons/bi/BiRedo.js +16 -0
  66. package/dist/icons/bi/BiUndo.js +16 -0
  67. package/dist/icons/lu/LuBraces.js +27 -0
  68. package/dist/icons/lu/LuCode.js +27 -0
  69. package/dist/icons/lu/LuKeyboard.js +71 -0
  70. package/dist/icons/lu/LuNetwork.js +63 -0
  71. package/dist/icons/lu/LuTextCursorInput.js +45 -0
  72. package/dist/icons.d.ts +2 -2
  73. package/dist/icons.js +8 -1
  74. package/dist/index.d.ts +2 -1
  75. package/dist/index.js +2 -2
  76. package/dist/list/ActionToolbar.d.ts +2 -0
  77. package/dist/list/ActionToolbar.js +18 -5
  78. package/dist/list/RowContextMenu.d.ts +10 -1
  79. package/dist/list/RowContextMenu.js +185 -30
  80. package/dist/styles.css +1 -1
  81. package/package.json +1 -1
package/README.md CHANGED
@@ -99,6 +99,22 @@ const engine = { ...createWorkflowEngine(), editing: workflowPackage }
99
99
 
100
100
  Without `editing`, the graph lays its jobs out by their needs alone.
101
101
 
102
+ The editor checks YAML against the schemas a host registers with `configureEditorYaml` from
103
+ `@parallelworks/ui/editor`. The job, step, input and settings dialogs edit their YAML at their own
104
+ paths (`JOB_YAML_PATH`, `STEP_YAML_PATH`, `INPUT_YAML_PATH`, `SETTINGS_YAML_PATH`); `settingsSchemas`
105
+ cuts the part of a workflow schema each one edits, for the host to register at that path under a URI
106
+ of its own (the YAML service reads a `#fragment` as an anchor and stops validating):
107
+
108
+ ```ts
109
+ const { job, step, input, settings } = settingsSchemas(workflowSchema)
110
+ configureEditorYaml([
111
+ { fileMatch: [JOB_YAML_PATH], uri: `${origin}/workflow-job.schema.json`, schema: job },
112
+ // ...and the same for step, input and settings
113
+ ])
114
+ ```
115
+
116
+ Without them, those views still edit, but check nothing against the schema.
117
+
102
118
  ## AI chat
103
119
 
104
120
  `@parallelworks/ui/ai` is a chat interface (thread, composer, streaming,
@@ -1,5 +1,5 @@
1
- declare const REQUESTABLE_PERMISSION_MODES: readonly ['read-only', 'accept-edits', 'bypass-permissions'];
2
- export declare const NAMED_PERMISSION_MODES: readonly ["read-only", "accept-edits", "bypass-permissions", "plan"];
1
+ declare const REQUESTABLE_PERMISSION_MODES: readonly ['read-only', 'accept-edits', 'auto', 'bypass-permissions'];
2
+ export declare const NAMED_PERMISSION_MODES: readonly ["read-only", "accept-edits", "auto", "bypass-permissions", "plan"];
3
3
  export type PermissionMode = (typeof REQUESTABLE_PERMISSION_MODES)[number];
4
4
  export type NamedPermissionMode = (typeof NAMED_PERMISSION_MODES)[number];
5
5
  export declare const DEFAULT_PERMISSION_MODE: PermissionMode;
@@ -2,6 +2,7 @@
2
2
  var REQUESTABLE_PERMISSION_MODES = [
3
3
  "read-only",
4
4
  "accept-edits",
5
+ "auto",
5
6
  "bypass-permissions"
6
7
  ];
7
8
  var NAMED_PERMISSION_MODES = [...REQUESTABLE_PERMISSION_MODES, "plan"];
@@ -10,7 +11,8 @@ var PERMISSION_RANK = {
10
11
  "read-only": 0,
11
12
  plan: 1,
12
13
  "accept-edits": 2,
13
- "bypass-permissions": 3
14
+ auto: 3,
15
+ "bypass-permissions": 4
14
16
  };
15
17
  function isNamedPermissionMode(mode) {
16
18
  return NAMED_PERMISSION_MODES.some((m) => m === mode);
@@ -291,6 +291,12 @@ function ConfirmApproval({ part, onAnswer }) {
291
291
  const t = strings.agentTranscript;
292
292
  const [feedback, setFeedback] = useState(null);
293
293
  const answer = (a) => onAnswer?.(part.id, a);
294
+ const [allowLabel, denyLabel] = {
295
+ command: [t.allowOnce, t.deny],
296
+ code: [t.allowCode, t.denyCode],
297
+ path: [t.allowPath, t.denyPath],
298
+ call: [t.allowCall, t.denyCall]
299
+ }[part.subject ?? "command"];
294
300
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
295
301
  part.command && /* @__PURE__ */ jsx("pre", {
296
302
  className: "overflow-x-auto rounded-md bg-(--theme-muted-panel-bg) p-2 font-mono text-xs whitespace-pre-wrap",
@@ -313,7 +319,7 @@ function ConfirmApproval({ part, onAnswer }) {
313
319
  allowed: true
314
320
  }),
315
321
  className: "px-3 py-1 text-xs font-medium rounded-full bg-(--theme-element) text-(--theme-element-text)",
316
- children: t.allowOnce
322
+ children: allowLabel
317
323
  }),
318
324
  part.canAllowDirectory !== false && /* @__PURE__ */ jsx("button", {
319
325
  type: "button",
@@ -332,7 +338,7 @@ function ConfirmApproval({ part, onAnswer }) {
332
338
  allowed: false
333
339
  }),
334
340
  className: "px-3 py-1 text-xs font-medium rounded-full border theme-border hover:theme-hover text-red-600",
335
- children: t.deny
341
+ children: denyLabel
336
342
  }),
337
343
  /* @__PURE__ */ jsx("button", {
338
344
  type: "button",
@@ -6,6 +6,7 @@ import { jsx } from "react/jsx-runtime";
6
6
  var LABEL_KEY = {
7
7
  "read-only": "readOnly",
8
8
  "accept-edits": "acceptEdits",
9
+ auto: "auto",
9
10
  "bypass-permissions": "bypassPermissions",
10
11
  plan: "plan"
11
12
  };
@@ -163,10 +163,12 @@ export interface ChatStrings {
163
163
  choose: string;
164
164
  readOnly: string;
165
165
  acceptEdits: string;
166
+ auto: string;
166
167
  bypassPermissions: string;
167
168
  plan: string;
168
169
  readOnlyHint: string;
169
170
  acceptEditsHint: string;
171
+ autoHint: string;
170
172
  bypassPermissionsHint: string;
171
173
  planHint: string;
172
174
  };
@@ -183,6 +185,12 @@ export interface ChatStrings {
183
185
  allowOnce: string;
184
186
  allowDirectory: string;
185
187
  deny: string;
188
+ allowCode: string;
189
+ denyCode: string;
190
+ allowPath: string;
191
+ denyPath: string;
192
+ allowCall: string;
193
+ denyCall: string;
186
194
  denyWithFeedback: string;
187
195
  feedbackPlaceholder: string;
188
196
  send: string;
@@ -163,10 +163,12 @@ var defaultChatStrings = {
163
163
  choose: "Choose what the agent may do",
164
164
  readOnly: "Read only",
165
165
  acceptEdits: "Accept edits",
166
+ auto: "Auto",
166
167
  bypassPermissions: "Bypass permissions",
167
168
  plan: "Plan",
168
169
  readOnlyHint: "The workspace can be read but not changed.",
169
170
  acceptEditsHint: "File edits are applied without asking. The workspace can be changed without your review.",
171
+ autoHint: "File edits are applied without asking. An automatic reviewer approves or denies most actions that would ask you.",
170
172
  bypassPermissionsHint: "Every action is taken without asking, including commands that change the system.",
171
173
  planHint: "The workspace is left unchanged while a plan is drafted."
172
174
  },
@@ -183,6 +185,12 @@ var defaultChatStrings = {
183
185
  allowOnce: "Yes, allow this command",
184
186
  allowDirectory: "Yes, and allow this directory for the session",
185
187
  deny: "No, deny this command",
188
+ allowCode: "Yes, run this code",
189
+ denyCode: "No, don't run this code",
190
+ allowPath: "Yes, allow this once",
191
+ denyPath: "No, deny",
192
+ allowCall: "Yes, allow this call",
193
+ denyCall: "No, deny this call",
186
194
  denyWithFeedback: "No, and provide feedback to the agent",
187
195
  feedbackPlaceholder: "Tell the agent what to do instead…",
188
196
  send: "Send",
@@ -84,11 +84,13 @@ export type ApprovalAnswerValue = {
84
84
  text: string;
85
85
  chat?: boolean;
86
86
  };
87
+ export type ApprovalSubject = 'command' | 'code' | 'path' | 'call';
87
88
  export interface ApprovalPart {
88
89
  kind: 'approval';
89
90
  id: string;
90
91
  approvalKind: 'confirm' | 'ask' | 'plan';
91
92
  toolName?: string;
93
+ subject?: ApprovalSubject;
92
94
  command?: string;
93
95
  reason?: string;
94
96
  agent?: string;
@@ -22,5 +22,7 @@ export interface IProps {
22
22
  portalEl?: HTMLElement;
23
23
  /** Smaller option rows and text, for dense toolbars. */
24
24
  compact?: boolean | undefined;
25
+ /** False hides the caret, for a field that has nothing to pick from yet. */
26
+ showCaret?: boolean | undefined;
25
27
  }
26
28
  export default function Dropdown(props: IProps): import("react").JSX.Element;
@@ -129,7 +129,7 @@ function ToggleOption({ option, category, expanded, onToggle, ...virtualRowProps
129
129
  })
130
130
  });
131
131
  }
132
- function DropdownHelper({ wrapperClassName = "w-full", className, textBoxClassName = "w-full", options = [], onChange, onActiveChange, value, id, ariaLabel, "aria-describedby": describedBy, required, disabled = false, placeholder = "", loading = false, allowCustomValue = false, customValueLabel = "Use", collapsedLabel = defaultCollapsedLabel, portalEl, compact = false }) {
132
+ function DropdownHelper({ wrapperClassName = "w-full", className, textBoxClassName = "w-full", options = [], onChange, onActiveChange, value, id, ariaLabel, "aria-describedby": describedBy, required, disabled = false, placeholder = "", loading = false, allowCustomValue = false, customValueLabel = "Use", collapsedLabel = defaultCollapsedLabel, portalEl, compact = false, showCaret = true }) {
133
133
  const DEBOUNCE_MS = 500;
134
134
  const t = useStrings().dropdown;
135
135
  const [query, setQuery] = useState("");
@@ -164,6 +164,11 @@ function DropdownHelper({ wrapperClassName = "w-full", className, textBoxClassNa
164
164
  }
165
165
  setBoundaryEl(dialog);
166
166
  }, [referenceElement]);
167
+ useEffect(() => {
168
+ if (!referenceElement) return;
169
+ if (describedBy) referenceElement.setAttribute("aria-describedby", describedBy);
170
+ else referenceElement.removeAttribute("aria-describedby");
171
+ }, [referenceElement, describedBy]);
167
172
  const sameWidthModifier = useMemo(() => ({
168
173
  name: "sameWidth",
169
174
  enabled: true,
@@ -336,7 +341,7 @@ function DropdownHelper({ wrapperClassName = "w-full", className, textBoxClassNa
336
341
  /* @__PURE__ */ jsx(ComboboxButton, {
337
342
  ref: buttonRef,
338
343
  "aria-label": t.showOptions,
339
- className: "absolute inset-y-0 right-0 flex items-center pr-3 disabled:cursor-not-allowed",
344
+ className: cx("absolute inset-y-0 right-0 flex items-center pr-3 disabled:cursor-not-allowed", !showCaret && "hidden"),
340
345
  "data-testid": "combobox-button",
341
346
  disabled,
342
347
  children: /* @__PURE__ */ jsx(DropdownCaret, {})
@@ -1,14 +1,19 @@
1
1
  import type { InputHTMLAttributes, ReactNode, TextareaHTMLAttributes } from 'react';
2
- interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
2
+ interface FieldProps {
3
3
  label?: ReactNode;
4
+ /** Shown beside the label, outside it, so it stays out of the field's name. */
5
+ labelHint?: ReactNode;
6
+ /** Help text shown under the label. */
7
+ description?: ReactNode;
4
8
  error?: string | undefined;
5
9
  mono?: boolean;
6
10
  }
7
- export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
8
- interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
9
- label?: string;
10
- error?: string;
11
- mono?: boolean;
11
+ interface InputProps extends InputHTMLAttributes<HTMLInputElement>, FieldProps {
12
12
  }
13
+ interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement>, FieldProps {
14
+ }
15
+ /** The box a text field draws, for other controls that should look like one. */
16
+ export declare const fieldBoxClasses: string;
17
+ export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
13
18
  export declare const Textarea: import("react").ForwardRefExoticComponent<TextareaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
14
19
  export {};
@@ -6,85 +6,104 @@ import cx from "classnames";
6
6
  import { forwardRef, useId, useState } from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/components/Input.tsx
9
- var Input = forwardRef(({ label, error, className = "", mono, type, required, ...props }, ref) => {
10
- const [showPassword, setShowPassword] = useState(false);
11
- const isPassword = type === "password";
12
- const inputType = isPassword && showPassword ? "text" : type;
9
+ /** The box a text field draws, for other controls that should look like one. */
10
+ var fieldBoxClasses = cx("w-full py-3 px-4 bg-[var(--theme-muted-panel-bg)] border border-[var(--theme-border)] rounded-lg text-[var(--theme-app)] text-sm transition-all duration-200 box-border", "focus:outline-none focus:border-[var(--theme-element)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--theme-element)_25%,transparent)]", "placeholder:text-[var(--theme-muted-text-color)]");
11
+ function LabelRow({ id, label, hint, required }) {
12
+ const element = /* @__PURE__ */ jsxs("label", {
13
+ className: "text-[0.8125rem] font-medium text-[var(--theme-app)]",
14
+ htmlFor: id,
15
+ children: [label, required && /* @__PURE__ */ jsx(RequiredMark, {})]
16
+ });
17
+ return hint ? /* @__PURE__ */ jsxs("div", {
18
+ className: "flex flex-wrap items-baseline gap-x-1.5",
19
+ children: [element, hint]
20
+ }) : element;
21
+ }
22
+ function useFieldFrame({ label, labelHint, description, error }, ownId, describedBy, required) {
13
23
  const generatedId = useId();
14
- const id = props.id ?? generatedId;
24
+ const id = ownId ?? generatedId;
15
25
  const errorId = `${id}-error`;
16
- const describedBy = [props["aria-describedby"], error ? errorId : void 0].filter(Boolean).join(" ") || void 0;
17
- const inputClasses = cx("w-full py-3 px-4 bg-[var(--theme-muted-panel-bg)] border border-[var(--theme-border)] rounded-lg text-[var(--theme-app)] text-sm transition-all duration-200 box-border", "focus:outline-none focus:border-[var(--theme-element)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--theme-element)_25%,transparent)]", "placeholder:text-[var(--theme-muted-text-color)]", "disabled:bg-[var(--theme-muted-panel-bg)] disabled:text-[var(--theme-muted-text-color)] disabled:cursor-not-allowed disabled:opacity-70", mono && "font-mono text-[0.8125rem]", error && "border-[#ef4444]", isPassword && "pr-11", className);
18
- return /* @__PURE__ */ jsxs("div", {
19
- className: "flex flex-col gap-1.5",
20
- children: [
21
- label && /* @__PURE__ */ jsxs("label", {
22
- className: "text-[0.8125rem] font-medium text-[var(--theme-app)]",
23
- htmlFor: id,
24
- children: [label, required && /* @__PURE__ */ jsx(RequiredMark, {})]
25
- }),
26
- /* @__PURE__ */ jsxs("div", {
27
- className: isPassword ? "relative w-full" : "",
28
- children: [/* @__PURE__ */ jsx("input", {
29
- ref,
30
- type: inputType,
31
- className: inputClasses,
32
- ...props,
26
+ const descriptionId = `${id}-description`;
27
+ return {
28
+ id,
29
+ describedBy: [
30
+ describedBy,
31
+ description ? descriptionId : void 0,
32
+ error ? errorId : void 0
33
+ ].filter(Boolean).join(" ") || void 0,
34
+ frame: (control) => /* @__PURE__ */ jsxs("div", {
35
+ className: "flex flex-col gap-1.5",
36
+ children: [
37
+ label && /* @__PURE__ */ jsx(LabelRow, {
33
38
  id,
34
- required,
35
- "aria-invalid": error ? true : props["aria-invalid"],
36
- "aria-describedby": describedBy
37
- }), isPassword && /* @__PURE__ */ jsx("button", {
38
- type: "button",
39
- className: "absolute right-3 top-1/2 -translate-y-1/2 bg-transparent border-none text-[var(--theme-muted-text-color)] cursor-pointer p-1 transition-colors duration-200 flex items-center justify-center hover:text-[var(--theme-app)]",
40
- onClick: () => setShowPassword(!showPassword),
41
- tabIndex: -1,
42
- "aria-label": showPassword ? "Hide password" : "Show password",
43
- children: showPassword ? /* @__PURE__ */ jsx(LuEyeOff, { className: "w-5 h-5" }) : /* @__PURE__ */ jsx(LuEye, { className: "w-5 h-5" })
44
- })]
45
- }),
46
- error && /* @__PURE__ */ jsxs("span", {
47
- id: errorId,
48
- role: "alert",
49
- className: "text-[0.8125rem] text-[#ef4444] flex items-center gap-1.5",
50
- children: [/* @__PURE__ */ jsx(TbAlertTriangle, { className: "w-4 h-4" }), error]
51
- })
52
- ]
53
- });
39
+ label,
40
+ hint: labelHint,
41
+ required
42
+ }),
43
+ description && /* @__PURE__ */ jsx("span", {
44
+ id: descriptionId,
45
+ className: "-mt-1 text-xs text-(--theme-muted-text-color)",
46
+ children: description
47
+ }),
48
+ control,
49
+ error && /* @__PURE__ */ jsxs("span", {
50
+ id: errorId,
51
+ role: "alert",
52
+ className: "text-[0.8125rem] text-[#ef4444] flex items-center gap-1.5",
53
+ children: [/* @__PURE__ */ jsx(TbAlertTriangle, { className: "w-4 h-4" }), error]
54
+ })
55
+ ]
56
+ })
57
+ };
58
+ }
59
+ var Input = forwardRef(({ label, labelHint, description, error, className = "", mono, type, required, ...props }, ref) => {
60
+ const [showPassword, setShowPassword] = useState(false);
61
+ const isPassword = type === "password";
62
+ const field = useFieldFrame({
63
+ label,
64
+ labelHint,
65
+ description,
66
+ error
67
+ }, props.id, props["aria-describedby"], required);
68
+ return field.frame(/* @__PURE__ */ jsxs("div", {
69
+ className: isPassword ? "relative w-full" : "",
70
+ children: [/* @__PURE__ */ jsx("input", {
71
+ ref,
72
+ type: isPassword && showPassword ? "text" : type,
73
+ className: cx(fieldBoxClasses, "disabled:bg-[var(--theme-muted-panel-bg)] disabled:text-[var(--theme-muted-text-color)] disabled:cursor-not-allowed disabled:opacity-70", mono && "font-mono text-[0.8125rem]", error && "!border-[#ef4444]", isPassword && "pr-11", className),
74
+ ...props,
75
+ id: field.id,
76
+ required,
77
+ "aria-invalid": error ? true : props["aria-invalid"],
78
+ "aria-describedby": field.describedBy
79
+ }), isPassword && /* @__PURE__ */ jsx("button", {
80
+ type: "button",
81
+ className: "absolute right-3 top-1/2 -translate-y-1/2 bg-transparent border-none text-[var(--theme-muted-text-color)] cursor-pointer p-1 transition-colors duration-200 flex items-center justify-center hover:text-[var(--theme-app)]",
82
+ onClick: () => setShowPassword(!showPassword),
83
+ tabIndex: -1,
84
+ "aria-label": showPassword ? "Hide password" : "Show password",
85
+ children: showPassword ? /* @__PURE__ */ jsx(LuEyeOff, { className: "w-5 h-5" }) : /* @__PURE__ */ jsx(LuEye, { className: "w-5 h-5" })
86
+ })]
87
+ }));
54
88
  });
55
89
  Input.displayName = "Input";
56
- var Textarea = forwardRef(({ label, error, className = "", mono, required, ...props }, ref) => {
57
- const generatedId = useId();
58
- const id = props.id ?? generatedId;
59
- const errorId = `${id}-error`;
60
- const describedBy = [props["aria-describedby"], error ? errorId : void 0].filter(Boolean).join(" ") || void 0;
61
- const textareaClasses = cx("w-full py-3 px-4 bg-[var(--theme-muted-panel-bg)] border border-[var(--theme-border)] rounded-lg text-[var(--theme-app)] text-sm transition-all duration-200 box-border min-h-[140px] resize-y", "focus:outline-none focus:border-[var(--theme-element)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--theme-element)_25%,transparent)]", "placeholder:text-[var(--theme-muted-text-color)]", mono && "font-mono text-[0.8125rem]", error && "border-[#ef4444]", className);
62
- return /* @__PURE__ */ jsxs("div", {
63
- className: "flex flex-col gap-1.5",
64
- children: [
65
- label && /* @__PURE__ */ jsxs("label", {
66
- className: "text-[0.8125rem] font-medium text-[var(--theme-app)]",
67
- htmlFor: id,
68
- children: [label, required && /* @__PURE__ */ jsx(RequiredMark, {})]
69
- }),
70
- /* @__PURE__ */ jsx("textarea", {
71
- ref,
72
- className: textareaClasses,
73
- ...props,
74
- id,
75
- required,
76
- "aria-invalid": error ? true : props["aria-invalid"],
77
- "aria-describedby": describedBy
78
- }),
79
- error && /* @__PURE__ */ jsxs("span", {
80
- id: errorId,
81
- role: "alert",
82
- className: "text-[0.8125rem] text-[#ef4444] flex items-center gap-1.5",
83
- children: [/* @__PURE__ */ jsx(TbAlertTriangle, { className: "w-4 h-4" }), error]
84
- })
85
- ]
86
- });
90
+ var Textarea = forwardRef(({ label, labelHint, description, error, className = "", mono, required, ...props }, ref) => {
91
+ const field = useFieldFrame({
92
+ label,
93
+ labelHint,
94
+ description,
95
+ error
96
+ }, props.id, props["aria-describedby"], required);
97
+ return field.frame(/* @__PURE__ */ jsx("textarea", {
98
+ ref,
99
+ className: cx(fieldBoxClasses, "min-h-[140px] resize-y", mono && "font-mono text-[0.8125rem]", error && "!border-[#ef4444]", className),
100
+ ...props,
101
+ id: field.id,
102
+ required,
103
+ "aria-invalid": error ? true : props["aria-invalid"],
104
+ "aria-describedby": field.describedBy
105
+ }));
87
106
  });
88
107
  Textarea.displayName = "Textarea";
89
108
  //#endregion
90
- export { Input, Textarea };
109
+ export { Input, Textarea, fieldBoxClasses };
@@ -1,5 +1,7 @@
1
1
  import type { Dispatch, ReactNode, SetStateAction } from 'react';
2
+ import type { WorkflowAction, WorkflowEditing } from '../editing.js';
2
3
  import type { WorkflowEngine } from '../engine.js';
4
+ import type { GraphEditorStrings, InputsEditorStrings } from '../graph/editorStrings.js';
3
5
  import type { RunLink } from '../graph/types.js';
4
6
  export type UIToastId = string | number;
5
7
  export interface UIToastOptions {
@@ -118,6 +120,9 @@ export interface UIStrings {
118
120
  openOriginalWorkflow: string;
119
121
  openInNewGraph: string;
120
122
  };
123
+ /** The visual editor's text a host translates; the editor fills in the rest in English. */
124
+ graphEditor: Partial<GraphEditorStrings>;
125
+ inputsEditor: Partial<InputsEditorStrings>;
121
126
  fileExplorer: {
122
127
  preview: {
123
128
  shareFile: string;
@@ -348,6 +353,8 @@ export interface UIStrings {
348
353
  copied: (label: string) => string;
349
354
  couldntCopy: (label: string) => string;
350
355
  moreActions: string;
356
+ /** A toolbar toggle's name in the overflow menu, where a row can't be pressed. */
357
+ toggleState: (label: string, on: boolean) => string;
351
358
  labelName: string;
352
359
  labelEmail: string;
353
360
  labelUsername: string;
@@ -392,6 +399,14 @@ export interface UINavigation {
392
399
  openStepSource: (uses: string, sourceUrl?: string) => void;
393
400
  openExternal: (href: string) => void;
394
401
  }
402
+ export type WorkflowJsonRef = {
403
+ kind: 'marketplace';
404
+ slug: string;
405
+ version: string;
406
+ } | {
407
+ kind: 'workflow';
408
+ name: string;
409
+ };
395
410
  export interface RunFileResult {
396
411
  data: string | undefined;
397
412
  isLoading: boolean;
@@ -407,6 +422,14 @@ export interface UIData {
407
422
  useRunFile: (slug: string, path: string | null, options?: {
408
423
  refreshInterval?: number;
409
424
  }) => RunFileResult;
425
+ resolveWorkflowJson?: (ref: WorkflowJsonRef) => Promise<unknown>;
426
+ /** The built-in actions a workflow step can use, for the editor's step form. */
427
+ workflowActions?: Record<string, WorkflowAction>;
428
+ resolveSecretVariables?: () => Promise<string[]>;
429
+ /** Repository files, so the editor can read the workflow a step's repository `uses` names. */
430
+ repoSuggestions?: {
431
+ file: (repo: string, ref: string, path: string) => Promise<string>;
432
+ };
410
433
  /**
411
434
  * Provisions browser-access CORS rules on a storage. Absent, the storage
412
435
  * explorer hides its "add CORS rules" affordance.
@@ -496,10 +519,18 @@ export type WorkflowEngineSource = WorkflowEngine | (() => Promise<WorkflowEngin
496
519
  export declare function useNotify(): UINotify;
497
520
  export declare function useStrings(): UIStrings;
498
521
  export declare function useNavigation(): UINavigation;
522
+ export declare function useWorkflowJsonResolver(): UIData['resolveWorkflowJson'];
523
+ export declare function useRepoSuggestions(): UIData['repoSuggestions'];
524
+ export declare function useWorkflowActions(): Record<string, WorkflowAction>;
525
+ export declare function useSecretVariablesResolver(): UIData['resolveSecretVariables'];
499
526
  export declare function useSlots(): UISlots;
500
527
  /** The engine when one is provided and `enabled`; suspends while a loader resolves. */
501
528
  export declare function useOptionalWorkflowEngine(enabled?: boolean): WorkflowEngine | undefined;
502
529
  export declare function useWorkflowEngine(): WorkflowEngine;
530
+ /** The engine once it has loaded, without suspending; renders again when a loader resolves. */
531
+ export declare function useLoadedWorkflowEngine(): WorkflowEngine | undefined;
532
+ /** The engine's workflow editing, which the visual editor can't work without. */
533
+ export declare function useWorkflowEditing(): WorkflowEditing;
503
534
  /** Resolves the engine without suspending, for async callers such as editor completions. */
504
535
  export declare function useWorkflowEngineLoader(): () => Promise<WorkflowEngine | undefined>;
505
536
  export declare function useProvisionCorsRules(): UIData['provisionCorsRules'];
@@ -1,5 +1,5 @@
1
1
  import { safeUrl } from "../safeUrl.js";
2
- import { createContext, use, useCallback, useContext, useEffect, useMemo } from "react";
2
+ import { createContext, use, useCallback, useContext, useEffect, useMemo, useState } from "react";
3
3
  import { useLocalStorage } from "usehooks-ts";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/components/Provider.tsx
@@ -122,6 +122,8 @@ var DEFAULTS = {
122
122
  openOriginalWorkflow: "Open original workflow",
123
123
  openInNewGraph: "Open in new graph"
124
124
  },
125
+ graphEditor: {},
126
+ inputsEditor: {},
125
127
  fileExplorer: {
126
128
  preview: {
127
129
  shareFile: "Share file",
@@ -348,6 +350,7 @@ var DEFAULTS = {
348
350
  copied: (label) => `Copied ${label}`,
349
351
  couldntCopy: (label) => `Couldn't copy ${label}`,
350
352
  moreActions: "More actions",
353
+ toggleState: (label, on) => `${label}, ${on ? "on" : "off"}`,
351
354
  labelName: "name",
352
355
  labelEmail: "email",
353
356
  labelUsername: "username",
@@ -442,6 +445,19 @@ function useStrings() {
442
445
  function useNavigation() {
443
446
  return useContext(UIContext).navigation;
444
447
  }
448
+ function useWorkflowJsonResolver() {
449
+ return useContext(UIContext).data.resolveWorkflowJson;
450
+ }
451
+ function useRepoSuggestions() {
452
+ return useContext(UIContext).data.repoSuggestions;
453
+ }
454
+ var NO_ACTIONS = {};
455
+ function useWorkflowActions() {
456
+ return useContext(UIContext).data.workflowActions ?? NO_ACTIONS;
457
+ }
458
+ function useSecretVariablesResolver() {
459
+ return useContext(UIContext).data.resolveSecretVariables;
460
+ }
445
461
  function useSlots() {
446
462
  return useContext(UIContext).slots;
447
463
  }
@@ -474,6 +490,30 @@ function useWorkflowEngine() {
474
490
  if (!source) throw new Error("Workflow forms and graphs need a UIProvider with an engine.");
475
491
  return resolveEngine(source);
476
492
  }
493
+ /** The engine once it has loaded, without suspending; renders again when a loader resolves. */
494
+ function useLoadedWorkflowEngine() {
495
+ const source = useContext(UIContext).engine;
496
+ const [, setLoaded] = useState(0);
497
+ const entry = typeof source === "function" ? loadEngine(source) : void 0;
498
+ const engine = typeof source === "function" ? entry?.engine : source;
499
+ useEffect(() => {
500
+ if (!entry || entry.engine) return;
501
+ let live = true;
502
+ entry.promise.then(() => {
503
+ if (live) setLoaded((n) => n + 1);
504
+ });
505
+ return () => {
506
+ live = false;
507
+ };
508
+ }, [entry]);
509
+ return engine;
510
+ }
511
+ /** The engine's workflow editing, which the visual editor can't work without. */
512
+ function useWorkflowEditing() {
513
+ const { editing } = useWorkflowEngine();
514
+ if (!editing) throw new Error("The workflow editor needs a UIProvider engine with editing.");
515
+ return editing;
516
+ }
477
517
  /** Resolves the engine without suspending, for async callers such as editor completions. */
478
518
  function useWorkflowEngineLoader() {
479
519
  const source = useContext(UIContext).engine;
@@ -528,6 +568,14 @@ function UIProvider({ notify, strings, navigation, data, slots, engine, children
528
568
  ...DEFAULTS.strings.jobActions,
529
569
  ...strings?.jobActions
530
570
  },
571
+ graphEditor: {
572
+ ...DEFAULTS.strings.graphEditor,
573
+ ...strings?.graphEditor
574
+ },
575
+ inputsEditor: {
576
+ ...DEFAULTS.strings.inputsEditor,
577
+ ...strings?.inputsEditor
578
+ },
531
579
  fileExplorer: {
532
580
  ...DEFAULTS.strings.fileExplorer,
533
581
  ...strings?.fileExplorer
@@ -569,4 +617,4 @@ function UIProvider({ notify, strings, navigation, data, slots, engine, children
569
617
  });
570
618
  }
571
619
  //#endregion
572
- export { UIProvider, useLink, useNavigation, useNotify, useOptionalWorkflowEngine, usePersistedState, useProvisionCorsRules, useRunFile, useSlots, useStrings, useWorkflowEngine, useWorkflowEngineLoader };
620
+ export { UIProvider, useLink, useLoadedWorkflowEngine, useNavigation, useNotify, useOptionalWorkflowEngine, usePersistedState, useProvisionCorsRules, useRepoSuggestions, useRunFile, useSecretVariablesResolver, useSlots, useStrings, useWorkflowActions, useWorkflowEditing, useWorkflowEngine, useWorkflowEngineLoader, useWorkflowJsonResolver };