@parallelworks/ui 0.51.0 → 0.53.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 (250) hide show
  1. package/README.md +197 -17
  2. package/dist/ai/agent/toolStrings.d.ts +2 -0
  3. package/dist/ai/agent/toolStrings.js +20 -18
  4. package/dist/ai/ai.css +17 -3
  5. package/dist/ai/components/AllocationSelector.js +5 -1
  6. package/dist/ai/components/AttachmentManager.js +51 -42
  7. package/dist/ai/components/AttachmentUpload.d.ts +4 -2
  8. package/dist/ai/components/AttachmentUpload.js +9 -9
  9. package/dist/ai/components/BlockedGroupBanner.js +8 -14
  10. package/dist/ai/components/BranchNavigator.js +11 -12
  11. package/dist/ai/components/ChatEmptyState.js +12 -19
  12. package/dist/ai/components/ChatInput.d.ts +4 -3
  13. package/dist/ai/components/ChatInput.js +27 -13
  14. package/dist/ai/components/ChatMessage.js +45 -121
  15. package/dist/ai/components/ChatMessageList.d.ts +5 -2
  16. package/dist/ai/components/ChatMessageList.js +89 -51
  17. package/dist/ai/components/ChatSidebar.js +11 -7
  18. package/dist/ai/components/ChatThread.js +1 -13
  19. package/dist/ai/components/DragOverlay.js +3 -3
  20. package/dist/ai/components/MessageAttachments.d.ts +10 -0
  21. package/dist/ai/components/MessageAttachments.js +78 -0
  22. package/dist/ai/components/ModelSelector.js +3 -3
  23. package/dist/ai/components/PastedText.js +2 -2
  24. package/dist/ai/components/ProviderIssueBanner.js +19 -23
  25. package/dist/ai/components/ShareDialog.js +2 -2
  26. package/dist/ai/components/agent/ActivityLine.d.ts +0 -2
  27. package/dist/ai/components/agent/ActivityLine.js +8 -51
  28. package/dist/ai/components/agent/AgentMessageParts.js +114 -53
  29. package/dist/ai/core/ChatProvider.d.ts +16 -5
  30. package/dist/ai/core/ChatProvider.js +41 -8
  31. package/dist/ai/core/config.js +4 -3
  32. package/dist/ai/index.d.ts +1 -1
  33. package/dist/ai/index.js +3 -3
  34. package/dist/ai/strings.d.ts +29 -3
  35. package/dist/ai/strings.js +80 -26
  36. package/dist/ai/types.d.ts +4 -0
  37. package/dist/ai/ui/Notice.d.ts +2 -2
  38. package/dist/ai/ui/Notice.js +29 -62
  39. package/dist/ai/ui/sidebar.d.ts +5 -3
  40. package/dist/ai/ui/sidebar.js +22 -10
  41. package/dist/components/ActionMenu.d.ts +1 -0
  42. package/dist/components/ActionMenu.js +6 -1
  43. package/dist/components/AppLink.d.ts +26 -0
  44. package/dist/components/AppLink.js +51 -0
  45. package/dist/components/Avatar.d.ts +2 -1
  46. package/dist/components/Avatar.js +4 -2
  47. package/dist/components/Button.d.ts +15 -5
  48. package/dist/components/Button.js +47 -40
  49. package/dist/components/Callout.d.ts +31 -7
  50. package/dist/components/Callout.js +50 -19
  51. package/dist/components/Card.d.ts +11 -6
  52. package/dist/components/Card.js +14 -8
  53. package/dist/components/Checkbox.js +2 -1
  54. package/dist/components/ColorInput.js +5 -3
  55. package/dist/components/Combobox.js +49 -19
  56. package/dist/components/ConfirmModal.d.ts +19 -8
  57. package/dist/components/ConfirmModal.js +31 -53
  58. package/dist/components/CopyButton.d.ts +38 -0
  59. package/dist/components/CopyButton.js +68 -0
  60. package/dist/components/CreateModal.js +118 -111
  61. package/dist/components/DatePicker.js +12 -8
  62. package/dist/components/DescriptionList.d.ts +25 -5
  63. package/dist/components/DescriptionList.js +56 -22
  64. package/dist/components/Disclosure.js +1 -1
  65. package/dist/components/Drawer.js +10 -16
  66. package/dist/components/Field.d.ts +16 -5
  67. package/dist/components/Field.js +63 -8
  68. package/dist/components/IconButton.d.ts +14 -5
  69. package/dist/components/IconButton.js +25 -14
  70. package/dist/components/InfoTip.d.ts +12 -1
  71. package/dist/components/InfoTip.js +13 -2
  72. package/dist/components/Input.d.ts +14 -8
  73. package/dist/components/Input.js +46 -31
  74. package/dist/components/Loader.js +1 -1
  75. package/dist/components/MarkdownImpl.d.ts +1 -0
  76. package/dist/components/MarkdownImpl.js +1 -0
  77. package/dist/components/MenuEntries.d.ts +4 -0
  78. package/dist/components/MenuEntries.js +11 -12
  79. package/dist/components/Meter.js +5 -4
  80. package/dist/components/Modal.d.ts +28 -4
  81. package/dist/components/Modal.js +37 -14
  82. package/dist/components/Overview.d.ts +20 -12
  83. package/dist/components/Overview.js +14 -9
  84. package/dist/components/PageHeader.d.ts +47 -65
  85. package/dist/components/PageHeader.js +275 -153
  86. package/dist/components/PageHeaderAction.d.ts +67 -0
  87. package/dist/components/PageHeaderAction.js +107 -0
  88. package/dist/components/Pagination.js +6 -16
  89. package/dist/components/Popover.d.ts +4 -1
  90. package/dist/components/Popover.js +5 -3
  91. package/dist/components/Provider.d.ts +79 -19
  92. package/dist/components/Provider.js +140 -130
  93. package/dist/components/Radio.js +2 -1
  94. package/dist/components/RefreshButton.js +7 -8
  95. package/dist/components/RequiredMark.js +1 -1
  96. package/dist/components/Section.js +1 -1
  97. package/dist/components/Select.d.ts +4 -9
  98. package/dist/components/Select.js +24 -11
  99. package/dist/components/SettingModeOption.d.ts +1 -1
  100. package/dist/components/SettingModeOption.js +7 -1
  101. package/dist/components/SettingsCard.d.ts +5 -3
  102. package/dist/components/SettingsCard.js +27 -20
  103. package/dist/components/Slider.js +1 -1
  104. package/dist/components/StatusBadge.js +2 -2
  105. package/dist/components/Switch.js +5 -4
  106. package/dist/components/Table.d.ts +30 -20
  107. package/dist/components/Table.js +50 -73
  108. package/dist/components/Tabs.d.ts +3 -7
  109. package/dist/components/Tabs.js +6 -10
  110. package/dist/components/ThemeEditor.js +18 -8
  111. package/dist/components/Toast.d.ts +11 -3
  112. package/dist/components/Toast.js +45 -20
  113. package/dist/components/buttonClasses.js +1 -1
  114. package/dist/components/comboboxItems.d.ts +4 -0
  115. package/dist/components/comboboxItems.js +6 -1
  116. package/dist/components/dismiss.d.ts +12 -0
  117. package/dist/components/dismiss.js +19 -0
  118. package/dist/components/fieldBox.d.ts +4 -0
  119. package/dist/components/fieldBox.js +7 -3
  120. package/dist/components/floating.d.ts +3 -3
  121. package/dist/components/floating.js +1 -1
  122. package/dist/components/focus.d.ts +5 -0
  123. package/dist/components/focus.js +21 -1
  124. package/dist/components/markdownCodeBreaks.js +3 -4
  125. package/dist/components/pageHeaderStore.d.ts +41 -0
  126. package/dist/components/pageHeaderStore.js +67 -0
  127. package/dist/components/pathBreaks.d.ts +8 -0
  128. package/dist/components/pathBreaks.js +28 -0
  129. package/dist/components/segmented.js +1 -1
  130. package/dist/components/shortcutKey.d.ts +2 -1
  131. package/dist/components/shortcutKey.js +19 -2
  132. package/dist/components/splitter.js +1 -1
  133. package/dist/components/useCssIsDark.js +40 -18
  134. package/dist/components/useTheme.js +9 -10
  135. package/dist/dev.d.ts +9 -0
  136. package/dist/dev.js +14 -0
  137. package/dist/editor/Editor.d.ts +3 -2
  138. package/dist/editor/Editor.js +24 -5
  139. package/dist/editor/EditorField.d.ts +2 -1
  140. package/dist/editor/EditorField.js +11 -29
  141. package/dist/editor/FormCodePanel.d.ts +7 -3
  142. package/dist/editor/FormCodePanel.js +14 -7
  143. package/dist/editor/Monaco.d.ts +22 -6
  144. package/dist/editor/Monaco.js +177 -113
  145. package/dist/editor/editorModel.d.ts +1 -1
  146. package/dist/editor/editorModel.js +1 -1
  147. package/dist/editor/index.d.ts +6 -5
  148. package/dist/editor/index.js +5 -4
  149. package/dist/editor/languages.d.ts +24 -0
  150. package/dist/editor/languages.js +31 -0
  151. package/dist/editor/monacoFeatures.d.ts +7 -0
  152. package/dist/editor/monacoFeatures.js +19 -0
  153. package/dist/editor/themes.d.ts +8 -5
  154. package/dist/editor/themes.js +65 -32
  155. package/dist/editor/workers.d.ts +3 -3
  156. package/dist/editor/workers.js +8 -4
  157. package/dist/editor/yaml.d.ts +6 -2
  158. package/dist/editor/yaml.js +11 -2
  159. package/dist/editor/yamlLanguage.js +55 -38
  160. package/dist/file-explorer/FileExplorer.d.ts +18 -4
  161. package/dist/file-explorer/FileExplorer.js +204 -142
  162. package/dist/file-explorer/FileExplorerContext.js +1 -1
  163. package/dist/file-explorer/FilePreview/StreamedBadge.js +3 -1
  164. package/dist/file-explorer/FilePreview/Viewer.js +14 -10
  165. package/dist/file-explorer/FilePreview/index.js +4 -4
  166. package/dist/file-explorer/FileRow.d.ts +6 -2
  167. package/dist/file-explorer/FileRow.js +35 -17
  168. package/dist/file-explorer/FileRowsWindow.d.ts +2 -1
  169. package/dist/file-explorer/FileRowsWindow.js +36 -4
  170. package/dist/file-explorer/FileTable.d.ts +3 -1
  171. package/dist/file-explorer/FileTable.js +6 -3
  172. package/dist/file-explorer/TreeView.d.ts +5 -1
  173. package/dist/file-explorer/TreeView.js +86 -39
  174. package/dist/file-explorer/helpers.d.ts +2 -1
  175. package/dist/file-explorer/helpers.js +24 -23
  176. package/dist/file-explorer/lib/utils.d.ts +3 -0
  177. package/dist/file-explorer/lib/utils.js +6 -1
  178. package/dist/file-explorer/toasts.js +27 -19
  179. package/dist/file-explorer/treeRows.d.ts +6 -0
  180. package/dist/file-explorer/treeRows.js +19 -1
  181. package/dist/file-explorer/useNearEnd.js +3 -6
  182. package/dist/form/Fields/CheckboxGroupField.js +12 -5
  183. package/dist/form/Fields/DurationField.js +1 -1
  184. package/dist/form/Fields/LabelField.js +1 -1
  185. package/dist/form/Fields/ObjectField.d.ts +1 -1
  186. package/dist/form/Fields/ObjectField.js +3 -2
  187. package/dist/form/Fields/StringField.js +16 -29
  188. package/dist/form/Fields/TextAreaField.js +3 -4
  189. package/dist/form/Form.js +3 -8
  190. package/dist/form/Wizard/WizardNavigation.js +0 -3
  191. package/dist/form/Wizard/utils.js +2 -1
  192. package/dist/form/fieldFrame.d.ts +1 -1
  193. package/dist/form/fieldFrame.js +2 -2
  194. package/dist/form/types/fieldComponentTypes.d.ts +2 -1
  195. package/dist/graph/AnnotationBanner.d.ts +1 -2
  196. package/dist/graph/AnnotationBanner.js +30 -32
  197. package/dist/graph/Connectors.js +4 -1
  198. package/dist/graph/DependencyGraph.js +2 -2
  199. package/dist/graph/GraphEditor.js +11 -9
  200. package/dist/graph/GraphEditorDialogs.js +1 -1
  201. package/dist/graph/GraphNode.js +7 -4
  202. package/dist/graph/InputDialog.js +1 -1
  203. package/dist/graph/InputsEditor.js +14 -11
  204. package/dist/graph/MatrixGroup.js +9 -3
  205. package/dist/graph/Reveal.js +18 -10
  206. package/dist/graph/ShortcutsButton.d.ts +11 -2
  207. package/dist/graph/ShortcutsButton.js +14 -3
  208. package/dist/graph/TreeView.js +35 -18
  209. package/dist/graph/WorkflowSettingsDialog.js +1 -1
  210. package/dist/graph/editorFields.js +1 -1
  211. package/dist/graph/editorRows.js +16 -10
  212. package/dist/graph/editorStrings.d.ts +7 -0
  213. package/dist/graph/editorStrings.js +8 -1
  214. package/dist/graph/inputRefs.js +1 -1
  215. package/dist/graph/settingsViews.js +11 -18
  216. package/dist/graph/stepWith.js +1 -1
  217. package/dist/icons.d.ts +2 -0
  218. package/dist/icons.js +4 -2
  219. package/dist/index.d.ts +12 -12
  220. package/dist/index.js +16 -16
  221. package/dist/list/ActionToolbar.d.ts +42 -6
  222. package/dist/list/ActionToolbar.js +298 -101
  223. package/dist/list/ListPage.d.ts +15 -23
  224. package/dist/list/ListPage.js +42 -43
  225. package/dist/list/ListTable.d.ts +37 -7
  226. package/dist/list/ListTable.js +52 -12
  227. package/dist/list/ListViewControls.js +1 -1
  228. package/dist/list/UserHoverCard.js +7 -15
  229. package/dist/list/index.d.ts +2 -2
  230. package/dist/list/index.js +2 -2
  231. package/dist/list/listSearch.js +12 -10
  232. package/dist/list/listView.d.ts +10 -1
  233. package/dist/list/listView.js +15 -1
  234. package/dist/logviewer/LogViewer.d.ts +4 -2
  235. package/dist/logviewer/LogViewer.js +87 -69
  236. package/dist/logviewer/logviewer.css +3 -2
  237. package/dist/reducedMotion.d.ts +5 -0
  238. package/dist/reducedMotion.js +20 -0
  239. package/dist/safeUrl.d.ts +1 -1
  240. package/dist/safeUrl.js +4 -2
  241. package/dist/styles.css +1 -1
  242. package/package.json +5 -2
  243. package/dist/components/Breadcrumbs.d.ts +0 -37
  244. package/dist/components/Breadcrumbs.js +0 -90
  245. package/dist/components/CopyToClipboard.d.ts +0 -17
  246. package/dist/components/CopyToClipboard.js +0 -66
  247. package/dist/components/Sort.d.ts +0 -9
  248. package/dist/components/Sort.js +0 -42
  249. package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +0 -6
  250. package/dist/file-explorer/FilePreview/CodePreviewEditor.js +0 -64
@@ -1,21 +1,20 @@
1
1
  import { modeOf, resolveMode, schemeFor } from "../theme/model.js";
2
2
  import { generateTheme } from "../theme/derive.js";
3
3
  import { applyTheme } from "../theme/index.js";
4
- import { useEffect, useLayoutEffect, useState } from "react";
4
+ import { useLayoutEffect, useSyncExternalStore } from "react";
5
5
  //#region src/components/useTheme.ts
6
6
  var DARK = "(prefers-color-scheme: dark)";
7
7
  var prefersDark = () => typeof matchMedia === "function" && matchMedia(DARK).matches;
8
+ function subscribeToScheme(onChange) {
9
+ if (typeof matchMedia !== "function") return () => {};
10
+ const media = matchMedia(DARK);
11
+ media.addEventListener("change", onChange);
12
+ return () => media.removeEventListener("change", onChange);
13
+ }
14
+ var ignore = () => () => {};
8
15
  /** The mode a preference shows now, following the device while it's `system`. */
9
16
  function useColorMode(preference) {
10
- const [dark, setDark] = useState(prefersDark);
11
- useEffect(() => {
12
- if (preference !== "system" || typeof matchMedia !== "function") return;
13
- const media = matchMedia(DARK);
14
- const update = () => setDark(media.matches);
15
- update();
16
- media.addEventListener("change", update);
17
- return () => media.removeEventListener("change", update);
18
- }, [preference]);
17
+ const dark = useSyncExternalStore(preference === "system" ? subscribeToScheme : ignore, prefersDark, () => false);
19
18
  return resolveMode(preference, dark);
20
19
  }
21
20
  /**
package/dist/dev.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Whether the host runs a development build, for checks that warn about misuse and cost too much
3
+ * to run in production.
4
+ *
5
+ * It reads `process.env.NODE_ENV`, which the host's bundler replaces, so a production build drops
6
+ * the checks and a development build keeps them. Vite's `DEV` flag would not do: this
7
+ * package's own library build replaces it with `false`, so no host would ever see the warning.
8
+ */
9
+ export declare function isDev(): boolean;
package/dist/dev.js ADDED
@@ -0,0 +1,14 @@
1
+ //#region src/dev.ts
2
+ /**
3
+ * Whether the host runs a development build, for checks that warn about misuse and cost too much
4
+ * to run in production.
5
+ *
6
+ * It reads `process.env.NODE_ENV`, which the host's bundler replaces, so a production build drops
7
+ * the checks and a development build keeps them. Vite's `DEV` flag would not do: this
8
+ * package's own library build replaces it with `false`, so no host would ever see the warning.
9
+ */
10
+ function isDev() {
11
+ return process.env.NODE_ENV !== "production";
12
+ }
13
+ //#endregion
14
+ export { isDev };
@@ -1,2 +1,3 @@
1
- import type { IEditorProps } from './Monaco.js';
2
- export default function Editor(props: IEditorProps): import("react").JSX.Element;
1
+ import type { EditorProps } from './Monaco.js';
2
+ /** A Monaco code editor, loaded on first render so Monaco stays out of the host's first bundle. */
3
+ export declare function Editor(props: EditorProps): import("react").JSX.Element;
@@ -1,13 +1,32 @@
1
1
  import { Loader } from "../components/Loader.js";
2
+ import { editorLanguagesLoading } from "./languages.js";
2
3
  import { jsx } from "react/jsx-runtime";
3
- import { Suspense, lazy } from "react";
4
+ import { Suspense, lazy, use } from "react";
4
5
  //#region src/editor/Editor.tsx
5
- var MonacoEditor = lazy(() => import("./Monaco.js"));
6
+ var loadMonaco = () => import("./Monaco.js");
7
+ var MonacoEditor = lazy(loadMonaco);
8
+ function LoadedEditor(props) {
9
+ const languages = editorLanguagesLoading();
10
+ if (languages) {
11
+ loadMonaco();
12
+ use(languages);
13
+ }
14
+ return /* @__PURE__ */ jsx(MonacoEditor, { ...props });
15
+ }
16
+ /** A Monaco code editor, loaded on first render so Monaco stays out of the host's first bundle. */
6
17
  function Editor(props) {
18
+ const { width = "100%", height = "100%" } = props;
7
19
  return /* @__PURE__ */ jsx(Suspense, {
8
- fallback: /* @__PURE__ */ jsx(Loader, {}),
9
- children: /* @__PURE__ */ jsx(MonacoEditor, { ...props })
20
+ fallback: /* @__PURE__ */ jsx("div", {
21
+ style: {
22
+ width,
23
+ height
24
+ },
25
+ className: "flex items-center justify-center",
26
+ children: /* @__PURE__ */ jsx(Loader, {})
27
+ }),
28
+ children: /* @__PURE__ */ jsx(LoadedEditor, { ...props })
10
29
  });
11
30
  }
12
31
  //#endregion
13
- export { Editor as default };
32
+ export { Editor };
@@ -7,4 +7,5 @@ export interface IEditorField extends BaseField {
7
7
  language?: string;
8
8
  };
9
9
  }
10
- export default function EditorField(props: FieldComponentProps<IEditorField>): import("react").JSX.Element;
10
+ /** A form field that edits its value as code, in the language its `options` name (shell by default). */
11
+ export declare function EditorField(props: FieldComponentProps<IEditorField>): import("react").JSX.Element;
@@ -1,45 +1,27 @@
1
- import { cx } from "../cx.js";
2
1
  import { Field } from "../components/Field.js";
3
2
  import { fieldFrame } from "../form/fieldFrame.js";
4
3
  import { useFieldValue } from "../form/useFieldValue.js";
5
- import Editor from "./Editor.js";
4
+ import { Editor } from "./Editor.js";
6
5
  import { jsx } from "react/jsx-runtime";
7
- import { useMemo, useRef } from "react";
8
- import { useFormContext, useWatch } from "react-hook-form";
9
6
  //#region src/editor/EditorField.tsx
7
+ /** A form field that edits its value as code, in the language its `options` name (shell by default). */
10
8
  function EditorField(props) {
11
9
  const { field, label, disabled, onChange } = props;
12
- const fieldName = field.name ?? "";
13
- const [value, change] = useFieldValue(fieldName);
14
- const touchedRef = useRef(false);
15
- const handleChange = (val) => {
16
- touchedRef.current = true;
17
- onChange?.(val);
18
- change(val);
19
- };
20
- const { control } = useFormContext();
21
- const resetKey = useWatch({
22
- control,
23
- name: "editorKeyPostfix"
24
- });
25
- const editorKeyPostfix = typeof resetKey === "string" ? resetKey : "";
26
- const postfixKey = useMemo(() => {
27
- const randomKey = Math.random().toString(36).substring(7);
28
- return editorKeyPostfix || randomKey;
29
- }, [editorKeyPostfix]);
10
+ const [value, change] = useFieldValue(field.name ?? "");
30
11
  return /* @__PURE__ */ jsx(Field, {
31
12
  ...fieldFrame(props),
32
13
  children: /* @__PURE__ */ jsx(Editor, {
33
14
  ariaLabel: label,
34
15
  height: "200px",
35
- path: fieldName,
36
- ...touchedRef.current || value === void 0 ? {} : { value },
37
- className: cx("bg-(--theme-input-bg) w-11 border focus:border rounded text-[10px] leading-4 pl-[6px]", disabled && "opacity-75"),
38
- readOnly: disabled,
39
- onChange: handleChange,
16
+ value: value ?? "",
17
+ disabled,
18
+ onChange: (text) => {
19
+ onChange?.(text);
20
+ change(text);
21
+ },
40
22
  language: field.options?.language || "shell"
41
- }, fieldName + postfixKey)
23
+ })
42
24
  });
43
25
  }
44
26
  //#endregion
45
- export { EditorField as default };
27
+ export { EditorField };
@@ -1,8 +1,12 @@
1
- declare function FormCodePanel({ formJSON, data, mutate, height, disabled, }: {
1
+ export interface FormCodePanelProps {
2
+ /** The form's input definitions, which say which values are sensitive or hidden. */
2
3
  formJSON: Record<string, unknown>;
4
+ /** The form's values, shown as JSON. */
3
5
  data: Record<string, unknown>;
6
+ /** Called with the values each time the JSON parses as an object. */
4
7
  mutate: (values: Record<string, unknown>) => void;
5
8
  height?: string;
6
9
  disabled?: boolean;
7
- }): import("react").JSX.Element;
8
- export default FormCodePanel;
10
+ }
11
+ /** A form's values as editable JSON, with sensitive and hidden fields left out unless asked for. */
12
+ export declare function FormCodePanel({ formJSON, data, mutate, height, disabled, }: FormCodePanelProps): import("react").JSX.Element;
@@ -5,7 +5,7 @@ import { useStrings, useWorkflowEngine } from "../components/Provider.js";
5
5
  import { useCopy } from "../components/useCopy.js";
6
6
  import { Switch } from "../components/Switch.js";
7
7
  import { flattenGroups, impureSetValueFromPath } from "../form/lib.js";
8
- import Editor from "./Editor.js";
8
+ import { Editor } from "./Editor.js";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
10
  import { useRef, useState } from "react";
11
11
  //#region src/editor/FormCodePanel.tsx
@@ -68,6 +68,7 @@ var populateFiltered = (filteredFields, data) => {
68
68
  });
69
69
  return data;
70
70
  };
71
+ /** A form's values as editable JSON, with sensitive and hidden fields left out unless asked for. */
71
72
  function FormCodePanel({ formJSON, data, mutate, height = "600px", disabled = false }) {
72
73
  const engine = useWorkflowEngine();
73
74
  const { common: t } = useStrings();
@@ -85,12 +86,14 @@ function FormCodePanel({ formJSON, data, mutate, height = "600px", disabled = fa
85
86
  const filteredFieldsRef = useRef(filteredFields);
86
87
  filteredFieldsRef.current = filteredFields;
87
88
  const json = JSON.stringify(displayObj, null, 2);
89
+ const typedRef = useRef(null);
90
+ const shown = typedRef.current?.json === json ? typedRef.current.text : json;
88
91
  const handleCopy = () => void copy(json, { quiet: true });
89
92
  return /* @__PURE__ */ jsxs("div", {
90
93
  style: { height },
91
94
  className: "flex w-full flex-col gap-2",
92
95
  children: [/* @__PURE__ */ jsxs("div", {
93
- className: cx("flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border", jsonHasErrors ? "border-red-500" : "border-(--theme-border)"),
96
+ className: cx("flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border", jsonHasErrors ? "border-(--theme-error)" : "border-(--theme-border)"),
94
97
  children: [/* @__PURE__ */ jsxs("div", {
95
98
  className: "bg-(--theme-muted-panel-bg) text-(--theme-muted-text-color) flex shrink-0 items-center gap-3 border-b border-(--theme-border) px-3 py-2",
96
99
  children: [hasHiddenFields && /* @__PURE__ */ jsx(Tooltip, {
@@ -111,13 +114,13 @@ function FormCodePanel({ formJSON, data, mutate, height = "600px", disabled = fa
111
114
  onClick: handleCopy,
112
115
  "aria-label": copied ? t.copied : t.copy,
113
116
  className: "ml-auto flex shrink-0 items-center gap-1.5 rounded-md border border-(--theme-border) px-2.5 py-1.5 text-sm text-(--theme-app) transition-colors duration-150 hover:bg-(--theme-hover) focus:outline-none focus-visible:ring-2 focus-visible:ring-(--theme-link)",
114
- children: [copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-green-500" }) : /* @__PURE__ */ jsx(CopyIcon, { className: "text-(--theme-muted-text-color)" }), copied ? t.copied : t.copy]
117
+ children: [copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-(--theme-success)" }) : /* @__PURE__ */ jsx(CopyIcon, { className: "text-(--theme-muted-text-color)" }), copied ? t.copied : t.copy]
115
118
  })]
116
119
  }), /* @__PURE__ */ jsx("div", {
117
120
  className: "min-h-0 flex-1",
118
121
  children: /* @__PURE__ */ jsx(Editor, {
119
122
  height: "100%",
120
- value: json,
123
+ value: shown,
121
124
  onValidate: isValidJson,
122
125
  className: "!rounded-none !border-0",
123
126
  minimap: { enabled: false },
@@ -140,7 +143,11 @@ function FormCodePanel({ formJSON, data, mutate, height = "600px", disabled = fa
140
143
  try {
141
144
  const parsed = JSON.parse(e);
142
145
  if (!isObject(parsed)) return;
143
- mutate(showHiddenRef.current ? parsed : populateFiltered(filteredFieldsRef.current, parsed));
146
+ typedRef.current = {
147
+ text: e,
148
+ json: JSON.stringify(parsed, null, 2)
149
+ };
150
+ mutate(showHiddenRef.current ? parsed : populateFiltered(filteredFieldsRef.current, structuredClone(parsed)));
144
151
  } catch {}
145
152
  },
146
153
  language: "json",
@@ -148,10 +155,10 @@ function FormCodePanel({ formJSON, data, mutate, height = "600px", disabled = fa
148
155
  })
149
156
  })]
150
157
  }), jsonHasErrors && /* @__PURE__ */ jsx("p", {
151
- className: "shrink-0 text-sm text-red-500",
158
+ className: "shrink-0 text-sm text-(--theme-error-text)",
152
159
  children: t.jsonHasErrors
153
160
  })]
154
161
  });
155
162
  }
156
163
  //#endregion
157
- export { FormCodePanel as default };
164
+ export { FormCodePanel };
@@ -1,19 +1,35 @@
1
- import * as monaco from 'monaco-editor';
1
+ import * as monaco from 'monaco-editor/editor/editor.api';
2
2
  import { type NestedWorkflowText } from './nestedText.js';
3
- export type ISchemaError = monaco.editor.IMarker;
3
+ import './monacoFeatures.js';
4
+ /** A problem Monaco or the YAML service found in the text, as `onValidate` reports it. */
5
+ export type ValidationMarker = monaco.editor.IMarker;
4
6
  /** A problem to mark on one line of the editor. */
5
7
  export interface EditorMarker {
6
8
  line: number;
7
9
  message: string;
8
10
  }
9
- export interface IEditorProps extends monaco.editor.IStandaloneEditorConstructionOptions {
11
+ export interface EditorProps extends monaco.editor.IStandaloneEditorConstructionOptions {
10
12
  className?: string;
13
+ /** Shows the text greyed out and read-only, as a disabled field does. */
11
14
  disabled?: boolean | undefined;
15
+ /**
16
+ * Shows the text to read rather than edit: it stays selectable, copyable and reachable by
17
+ * keyboard, announces itself read-only, and shows no cursor, current line or quick fixes. The
18
+ * view ends at the last line, and the wheel scrolls the page once the text has none left.
19
+ */
20
+ readOnly?: boolean;
21
+ /** The text as the person edits it, with carriage returns removed. */
12
22
  onChange?: (value: string) => void;
13
- onValidate?: (errors: ISchemaError[]) => void;
23
+ /** Every marker on the text, each time they change: the schema's, the lint's and the host's. */
24
+ onValidate?: (markers: ValidationMarker[]) => void;
14
25
  width?: string | number;
15
26
  height?: string | number;
16
- path?: 'file:///workflow.yaml' | string;
27
+ /**
28
+ * The model's URI. A YAML schema registered with `configureEditorYaml` applies where its
29
+ * `fileMatch` matches this path, and `revealLines` finds the editor by it. Each editor gets one
30
+ * of its own when left out. It is read once: give the editor a `key` to edit another path.
31
+ */
32
+ path?: string;
17
33
  /** Apply outside `value` changes as undoable edits instead of resetting the undo history. */
18
34
  preserveUndo?: boolean;
19
35
  /** Checks a whole workflow for what the schema can't, such as needs read but never listed. */
@@ -23,4 +39,4 @@ export interface IEditorProps extends monaco.editor.IStandaloneEditorConstructio
23
39
  /** Set when the text is one job or step, for completions that touch the rest of the workflow. */
24
40
  nested?: NestedWorkflowText;
25
41
  }
26
- export default function MonacoEditor({ className, disabled, onChange, onValidate, width, height, path, preserveUndo, lint, markers, nested, ...options }: IEditorProps): import("react").JSX.Element;
42
+ export default function MonacoEditor({ className, disabled, onChange, onValidate, width, height, path: pathProp, preserveUndo, lint, markers, nested, value, language, ...options }: EditorProps): import("react").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { cx } from "../cx.js";
2
+ import { useStrings } from "../components/Provider.js";
2
3
  import { useCssIsDark } from "../components/useCssIsDark.js";
3
4
  import { HOST_MARKER_OWNER, LINT_OWNER } from "./lintOwner.js";
4
5
  import { lintContext, useLintEditing, useLintSources } from "./lintContext.js";
@@ -7,10 +8,11 @@ import { registerRevealer } from "./reveal.js";
7
8
  import { defineEditorThemes, followThemeTokens, getThemeName } from "./themes.js";
8
9
  import { setupMonacoWorkers } from "./workers.js";
9
10
  import { holdListeners, modelFor } from "./editorModel.js";
11
+ import "./monacoFeatures.js";
10
12
  import { ensureYamlLanguage } from "./yamlLanguage.js";
11
13
  import { jsx } from "react/jsx-runtime";
12
- import { useEffect, useLayoutEffect, useRef } from "react";
13
- import * as monaco from "monaco-editor";
14
+ import { useEffect, useId, useLayoutEffect, useRef } from "react";
15
+ import * as monaco from "monaco-editor/editor/editor.api";
14
16
  //#region src/editor/Monaco.tsx
15
17
  setupMonacoWorkers();
16
18
  var SCHEMA_UNREADABLE = "768";
@@ -38,25 +40,177 @@ function editorText(value) {
38
40
  return value.replace(/\r/g, "");
39
41
  }
40
42
  var noop = () => {};
41
- function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop, width = "100%", height = "100%", path = "file:///default", preserveUndo = false, lint = false, markers, nested, ...options }) {
43
+ var DISABLED_BACKGROUNDS = [
44
+ "--vscode-editor-background",
45
+ "--vscode-editorGutter-background",
46
+ "--vscode-editorStickyScroll-background",
47
+ "--vscode-minimap-background",
48
+ "--vscode-editorOverviewRuler-background"
49
+ ];
50
+ function minimalEdit(model, from, to) {
51
+ let start = 0;
52
+ const shortest = Math.min(from.length, to.length);
53
+ while (start < shortest && from[start] === to[start]) start++;
54
+ let end = 0;
55
+ while (end < shortest - start && from[from.length - 1 - end] === to[to.length - 1 - end]) end++;
56
+ const a = model.getPositionAt(start);
57
+ const b = model.getPositionAt(from.length - end);
58
+ return {
59
+ range: new monaco.Range(a.lineNumber, a.column, b.lineNumber, b.column),
60
+ text: to.slice(start, to.length - end)
61
+ };
62
+ }
63
+ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop, width = "100%", height = "100%", path: pathProp, preserveUndo = false, lint = false, markers, nested, value, language, ...options }) {
64
+ const { common: t } = useStrings();
65
+ const ownPath = `file:///editor/${useId().replace(/[^\w-]/g, "")}`;
66
+ const path = useRef(pathProp ?? ownPath).current;
42
67
  const isDark = useCssIsDark();
68
+ const isDarkRef = useRef(isDark);
69
+ isDarkRef.current = isDark;
43
70
  const readOnly = Boolean(disabled || options.readOnly);
71
+ const containerRef = useRef(null);
44
72
  const monacoRef = useRef(null);
45
73
  const isSyncingValueRef = useRef(false);
46
74
  const lintSources = useLintSources();
47
75
  const lintSourcesRef = useRef(lintSources);
48
76
  lintSourcesRef.current = lintSources;
49
77
  const runLintRef = useRef(null);
50
- const unregisterRevealRef = useRef(null);
51
- const listenersRef = useRef([]);
52
78
  const nestedRef = useRef(nested);
53
79
  nestedRef.current = nested;
54
80
  const isNested = nested !== void 0;
81
+ const onChangeRef = useRef(onChange);
82
+ onChangeRef.current = onChange;
83
+ const onValidateRef = useRef(onValidate);
84
+ onValidateRef.current = onValidate;
85
+ const markersRef = useRef(markers);
86
+ markersRef.current = markers;
87
+ const editorOptions = {
88
+ ariaLabel: t.codeEditor,
89
+ readOnlyMessage: { value: t.readOnlyText },
90
+ domReadOnly: readOnly,
91
+ hideCursorInOverviewRuler: readOnly,
92
+ lightbulb: { enabled: readOnly ? monaco.editor.ShowLightbulbIconMode.Off : monaco.editor.ShowLightbulbIconMode.OnCode },
93
+ scrollBeyondLastLine: !readOnly,
94
+ ...options,
95
+ scrollbar: {
96
+ alwaysConsumeMouseWheel: !readOnly,
97
+ ...options.scrollbar
98
+ },
99
+ readOnly,
100
+ renderLineHighlight: disabled ? "none" : options.renderLineHighlight ?? (readOnly ? "none" : "line")
101
+ };
102
+ const optionsKey = JSON.stringify(editorOptions);
103
+ const optionsRef = useRef(editorOptions);
104
+ optionsRef.current = editorOptions;
105
+ const initialRef = useRef({
106
+ value,
107
+ language
108
+ });
55
109
  const lintEditing = useLintEditing(lint);
56
110
  const lintEditingRef = useRef(lintEditing);
57
111
  lintEditingRef.current = lintEditing;
58
112
  const lintRef = useRef(lint);
59
113
  lintRef.current = lint;
114
+ useLayoutEffect(() => {
115
+ const container = containerRef.current;
116
+ if (!container) return;
117
+ defineEditorThemes(monaco.editor);
118
+ monaco.editor.setTheme(getThemeName(isDarkRef.current));
119
+ const { value: initial, language: initialLanguage } = initialRef.current;
120
+ const model = modelFor(monaco.Uri.parse(path), initial ?? "", initialLanguage);
121
+ const editor = monaco.editor.create(container, {
122
+ automaticLayout: true,
123
+ model,
124
+ ...optionsRef.current,
125
+ fixedOverflowWidgets: true,
126
+ suggest: {
127
+ preview: true,
128
+ showWords: false,
129
+ showInlineDetails: true
130
+ }
131
+ });
132
+ setMarkers(model, HOST_MARKER_OWNER, (markersRef.current ?? []).map((marker) => lintMarker(model, marker)));
133
+ setMarkers(model, LINT_OWNER, []);
134
+ const runLint = () => {
135
+ const editing = lintEditingRef.current;
136
+ if (!lintRef.current || !editing || model.isDisposed()) return;
137
+ const schema = monaco.editor.getModelMarkers({ resource: model.uri }).filter((marker) => marker.owner !== "workflowlint" && marker.owner !== "workflowmarkers" && String(marker.code ?? "") !== SCHEMA_UNREADABLE);
138
+ const source = model.getValue();
139
+ setMarkers(model, LINT_OWNER, schema.length > 0 ? [] : editing.lintWorkflow(source, lintContext(editing, source, lintSourcesRef.current, runLint)).map((problem) => lintMarker(model, problem)));
140
+ };
141
+ runLintRef.current = runLint;
142
+ let lintTimer;
143
+ const validate = () => {
144
+ runLint();
145
+ onValidateRef.current(monaco.editor.getModelMarkers({ resource: model.uri }));
146
+ };
147
+ const modelListeners = [
148
+ model.onDidChangeContent(() => {
149
+ if (lintRef.current) {
150
+ window.clearTimeout(lintTimer);
151
+ lintTimer = window.setTimeout(runLint, 300);
152
+ }
153
+ if (isSyncingValueRef.current) return;
154
+ onChangeRef.current(editorText(editor.getValue()));
155
+ }),
156
+ monaco.editor.onDidChangeMarkers((resources) => {
157
+ const uri = model.uri.toString();
158
+ if (resources.some((resource) => resource.toString() === uri)) validate();
159
+ }),
160
+ { dispose: () => window.clearTimeout(lintTimer) }
161
+ ];
162
+ holdListeners(model, modelListeners);
163
+ validate();
164
+ const unregisterReveal = registerRevealer(path, (start, end) => {
165
+ const last = Math.min(Math.max(start, end), model.getLineCount());
166
+ editor.revealLinesInCenterIfOutsideViewport(start, last);
167
+ editor.setPosition({
168
+ lineNumber: start,
169
+ column: model.getLineFirstNonWhitespaceColumn(start) || 1
170
+ });
171
+ const flash = editor.createDecorationsCollection([{
172
+ range: new monaco.Range(start, 1, last, 1),
173
+ options: {
174
+ isWholeLine: true,
175
+ className: "bg-(--theme-accent)/20"
176
+ }
177
+ }]);
178
+ window.setTimeout(() => flash.clear(), 1500);
179
+ });
180
+ const unfollow = followThemeTokens(monaco.editor, () => getThemeName(isDarkRef.current));
181
+ ensureYamlLanguage();
182
+ monacoRef.current = editor;
183
+ return () => {
184
+ unfollow();
185
+ unregisterReveal();
186
+ for (const listener of modelListeners) listener.dispose();
187
+ editor.dispose();
188
+ monacoRef.current = null;
189
+ runLintRef.current = null;
190
+ if (!model.isDisposed() && model.getLanguageId() !== "yaml") model.dispose();
191
+ };
192
+ }, [path]);
193
+ useEffect(() => {
194
+ if (monacoRef.current) {
195
+ defineEditorThemes(monaco.editor);
196
+ monaco.editor.setTheme(getThemeName(isDark));
197
+ }
198
+ }, [isDark]);
199
+ useEffect(() => {
200
+ const node = monacoRef.current?.getDomNode();
201
+ for (const name of DISABLED_BACKGROUNDS) if (disabled) node?.style.setProperty(name, "var(--theme-input-disabled-bg)");
202
+ else node?.style.removeProperty(name);
203
+ }, [disabled]);
204
+ useEffect(() => {
205
+ monacoRef.current?.getDomNode()?.querySelector("textarea.inputarea, .native-edit-context")?.setAttribute("aria-readonly", String(readOnly));
206
+ }, [readOnly, path]);
207
+ useEffect(() => {
208
+ monacoRef.current?.updateOptions(optionsRef.current);
209
+ }, [optionsKey]);
210
+ useEffect(() => {
211
+ const model = monacoRef.current?.getModel();
212
+ if (model && language && model.getLanguageId() !== language) monaco.editor.setModelLanguage(model, language);
213
+ }, [language]);
60
214
  useEffect(() => {
61
215
  const model = monacoRef.current?.getModel();
62
216
  if (!lint && model) setMarkers(model, LINT_OWNER, []);
@@ -70,126 +224,36 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
70
224
  if (!isNested) return;
71
225
  return publishNestedText(monaco.Uri.parse(path).toString(), nestedRef);
72
226
  }, [path, isNested]);
73
- const themeName = useRef(getThemeName(isDark, disabled));
74
- themeName.current = getThemeName(isDark, disabled);
75
- useEffect(() => {
76
- if (!monacoRef.current) return;
77
- defineEditorThemes(monaco.editor);
78
- monaco.editor.setTheme(getThemeName(isDark, disabled));
79
- monacoRef.current.updateOptions({ readOnly });
80
- }, [
81
- disabled,
82
- isDark,
83
- readOnly
84
- ]);
85
- useEffect(() => followThemeTokens(monaco.editor, () => themeName.current), []);
86
227
  useLayoutEffect(() => {
87
- const model = monacoRef.current?.getModel();
88
- if (!model || options.value === void 0 || editorText(model.getValue()) === editorText(options.value)) return;
228
+ const editor = monacoRef.current;
229
+ const model = editor?.getModel();
230
+ if (!editor || !model || value === void 0) return;
231
+ const current = model.getValue();
232
+ if (editorText(current) === editorText(value)) return;
89
233
  isSyncingValueRef.current = true;
90
234
  try {
91
235
  if (preserveUndo) {
92
- const editor = monacoRef.current;
93
- editor?.pushUndoStop();
94
- model.pushEditOperations([], [{
95
- range: model.getFullModelRange(),
96
- text: options.value
97
- }], () => null);
98
- editor?.pushUndoStop();
99
- } else model.setValue(options.value);
236
+ editor.pushUndoStop();
237
+ model.pushEditOperations([], [minimalEdit(model, current, value)], () => null);
238
+ editor.pushUndoStop();
239
+ } else {
240
+ const view = editor.saveViewState();
241
+ model.setValue(value);
242
+ if (view) editor.restoreViewState(view);
243
+ }
100
244
  } finally {
101
245
  isSyncingValueRef.current = false;
102
246
  }
103
- }, [options.value, preserveUndo]);
104
- useEffect(() => {
105
- return () => {
106
- unregisterRevealRef.current?.();
107
- for (const listener of listenersRef.current) listener.dispose();
108
- monacoRef.current?.dispose();
109
- monacoRef.current = null;
110
- };
111
- }, []);
112
- /** Strips CR (\r) — Windows line endings break bash scripts. */
113
- const onChangeWrapper = (value) => {
114
- value = editorText(value);
115
- onChange(value);
116
- };
247
+ }, [value, preserveUndo]);
117
248
  return /* @__PURE__ */ jsx("div", {
249
+ ref: containerRef,
118
250
  style: {
119
251
  width,
120
252
  height
121
253
  },
122
- className: cx("relative border rounded", className),
123
- ref: (ref) => {
124
- if (ref && !monacoRef.current) {
125
- defineEditorThemes(monaco.editor);
126
- monaco.editor.setTheme(getThemeName(isDark, disabled));
127
- const model = modelFor(monaco.Uri.parse(path), options.value ?? "", options.language);
128
- const editor = monaco.editor.create(ref, {
129
- automaticLayout: true,
130
- model,
131
- ...options,
132
- readOnly,
133
- fixedOverflowWidgets: true,
134
- suggest: {
135
- preview: true,
136
- showWords: false,
137
- showInlineDetails: true
138
- }
139
- });
140
- setMarkers(model, HOST_MARKER_OWNER, (markers ?? []).map((marker) => lintMarker(model, marker)));
141
- setMarkers(model, LINT_OWNER, []);
142
- const runLint = () => {
143
- const editing = lintEditingRef.current;
144
- if (!lintRef.current || !editing || model.isDisposed()) return;
145
- const schema = monaco.editor.getModelMarkers({ resource: model.uri }).filter((marker) => marker.owner !== "workflowlint" && marker.owner !== "workflowmarkers" && String(marker.code ?? "") !== SCHEMA_UNREADABLE);
146
- const source = model.getValue();
147
- setMarkers(model, LINT_OWNER, schema.length > 0 ? [] : editing.lintWorkflow(source, lintContext(editing, source, lintSourcesRef.current, runLint)).map((problem) => lintMarker(model, problem)));
148
- };
149
- runLintRef.current = runLint;
150
- let lintTimer;
151
- const validate = () => {
152
- runLint();
153
- onValidate(monaco.editor.getModelMarkers({ resource: model.uri }));
154
- };
155
- const modelListeners = [
156
- model.onDidChangeContent(() => {
157
- if (lintRef.current) {
158
- window.clearTimeout(lintTimer);
159
- lintTimer = window.setTimeout(runLint, 300);
160
- }
161
- if (isSyncingValueRef.current) return;
162
- onChangeWrapper(editor.getValue());
163
- }),
164
- monaco.editor.onDidChangeMarkers((resources) => {
165
- const uri = model.uri.toString();
166
- if (resources.some((resource) => resource.toString() === uri)) validate();
167
- }),
168
- { dispose: () => window.clearTimeout(lintTimer) }
169
- ];
170
- holdListeners(model, modelListeners);
171
- listenersRef.current = modelListeners;
172
- validate();
173
- unregisterRevealRef.current = registerRevealer(path, (start, end) => {
174
- const last = Math.min(Math.max(start, end), model.getLineCount());
175
- editor.revealLinesInCenterIfOutsideViewport(start, last);
176
- editor.setPosition({
177
- lineNumber: start,
178
- column: model.getLineFirstNonWhitespaceColumn(start) || 1
179
- });
180
- const flash = editor.createDecorationsCollection([{
181
- range: new monaco.Range(start, 1, last, 1),
182
- options: {
183
- isWholeLine: true,
184
- className: "bg-(--theme-accent)/20"
185
- }
186
- }]);
187
- window.setTimeout(() => flash.clear(), 1500);
188
- });
189
- ensureYamlLanguage();
190
- monacoRef.current = editor;
191
- }
192
- }
254
+ "data-disabled": disabled ? "" : void 0,
255
+ "data-read-only": readOnly ? "" : void 0,
256
+ className: cx("relative rounded border border-(--theme-border)", "focus-within:border-(--theme-accent) focus-within:ring-1 focus-within:ring-(--theme-accent)", "data-read-only:[&_.cursors-layer]:hidden data-disabled:[&_.view-lines]:opacity-60", className)
193
257
  });
194
258
  }
195
259
  //#endregion
@@ -1,4 +1,4 @@
1
- import * as monaco from 'monaco-editor';
1
+ import * as monaco from 'monaco-editor/editor/editor.api';
2
2
  /** The model a new editor on `uri` edits, holding `value`. */
3
3
  export declare function modelFor(uri: monaco.Uri, value: string, language: string | undefined): monaco.editor.ITextModel;
4
4
  /** An editor's listeners on its model, dropped when the next editor takes the model over. */
@@ -1,4 +1,4 @@
1
- import * as monaco from "monaco-editor";
1
+ import * as monaco from "monaco-editor/editor/editor.api";
2
2
  //#region src/editor/editorModel.ts
3
3
  var listeners = /* @__PURE__ */ new WeakMap();
4
4
  /** The model a new editor on `uri` edits, holding `value`. */