@parallelworks/ui 0.52.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 (55) hide show
  1. package/README.md +42 -3
  2. package/dist/ai/ai.css +5 -3
  3. package/dist/ai/components/agent/AgentMessageParts.js +1 -1
  4. package/dist/ai/strings.js +0 -1
  5. package/dist/ai/ui/sidebar.js +1 -1
  6. package/dist/components/Field.d.ts +5 -4
  7. package/dist/components/Field.js +27 -4
  8. package/dist/components/InfoTip.d.ts +12 -4
  9. package/dist/components/InfoTip.js +13 -3
  10. package/dist/components/MarkdownImpl.d.ts +1 -0
  11. package/dist/components/MarkdownImpl.js +1 -0
  12. package/dist/components/Popover.d.ts +4 -1
  13. package/dist/components/Popover.js +2 -1
  14. package/dist/components/Provider.d.ts +2 -0
  15. package/dist/components/Provider.js +1 -0
  16. package/dist/components/focus.d.ts +5 -0
  17. package/dist/components/focus.js +21 -1
  18. package/dist/editor/Editor.js +13 -3
  19. package/dist/editor/Monaco.d.ts +8 -1
  20. package/dist/editor/Monaco.js +16 -3
  21. package/dist/editor/editorModel.d.ts +1 -1
  22. package/dist/editor/editorModel.js +1 -1
  23. package/dist/editor/index.d.ts +1 -0
  24. package/dist/editor/index.js +2 -1
  25. package/dist/editor/languages.d.ts +24 -0
  26. package/dist/editor/languages.js +31 -0
  27. package/dist/editor/monacoFeatures.d.ts +7 -0
  28. package/dist/editor/monacoFeatures.js +19 -0
  29. package/dist/editor/themes.js +7 -2
  30. package/dist/editor/workers.d.ts +3 -3
  31. package/dist/editor/workers.js +8 -4
  32. package/dist/editor/yamlLanguage.js +55 -38
  33. package/dist/file-explorer/FileExplorer.d.ts +18 -4
  34. package/dist/file-explorer/FileExplorer.js +62 -19
  35. package/dist/file-explorer/FilePreview/Viewer.js +12 -8
  36. package/dist/file-explorer/FileRow.d.ts +6 -2
  37. package/dist/file-explorer/FileRow.js +35 -17
  38. package/dist/file-explorer/FileRowsWindow.d.ts +2 -1
  39. package/dist/file-explorer/FileRowsWindow.js +2 -1
  40. package/dist/file-explorer/FileTable.d.ts +3 -1
  41. package/dist/file-explorer/FileTable.js +3 -2
  42. package/dist/file-explorer/TreeView.d.ts +5 -1
  43. package/dist/file-explorer/TreeView.js +49 -25
  44. package/dist/file-explorer/helpers.js +3 -3
  45. package/dist/file-explorer/lib/utils.d.ts +3 -0
  46. package/dist/file-explorer/lib/utils.js +6 -1
  47. package/dist/form/Wizard/utils.js +2 -1
  48. package/dist/list/ActionToolbar.js +3 -2
  49. package/dist/list/ListPage.js +1 -1
  50. package/dist/list/ListTable.d.ts +5 -0
  51. package/dist/list/ListTable.js +6 -2
  52. package/dist/styles.css +1 -1
  53. package/package.json +5 -2
  54. package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +0 -6
  55. package/dist/file-explorer/FilePreview/CodePreviewEditor.js +0 -64
package/README.md CHANGED
@@ -11,7 +11,7 @@ so an application ships only what it imports:
11
11
  | `./list` | list pages, tables, row menus and list controls |
12
12
  | `./form` | `DynamicForm` and its field registry |
13
13
  | `./graph` | job dependency graph and tree view, and the visual workflow editor: the graph, the run form as an inputs editor, and their dialogs |
14
- | `./editor` | Monaco editor (`monaco-editor` is an optional peer); YAML validation, completion and hover run in a bundled worker |
14
+ | `./editor` | Monaco editor (`monaco-editor` is an optional peer), loaded on first mount with the languages it needs; YAML validation, completion and hover run in a bundled worker |
15
15
  | `./logviewer` | log viewer with ANSI color and search |
16
16
  | `./file-explorer` | object storage explorer over a host-supplied provider |
17
17
  | `./ai`, `./ai/openai`, `./ai/mock` | chat interface and adapters |
@@ -73,7 +73,7 @@ Each need has one component. Reach for it before styling markup by hand.
73
73
  | Views of one thing | `Tabs` |
74
74
  | A list of actions | `ActionMenu`; `useContextMenu` for a right-click or a row's menu |
75
75
  | A panel of controls anchored to a trigger | `Popover` |
76
- | A hint for a control | `Tooltip`; `InfoTip` for help everyone must reach |
76
+ | A hint for a control | `Tooltip`; a `Field`'s `help` for more about a field, which its control reads and which opens as Tab reaches it; `InfoTip` for help about anything else |
77
77
  | A preview of what a link leads to | `HoverCard` |
78
78
  | A task that blocks the page | `Modal`, laid out with `title`, `description` and `footer` as a `Drawer` is; `ConfirmModal` to confirm one action; `CreateModal` to name and create one thing |
79
79
  | A panel beside the page | `Drawer` |
@@ -434,6 +434,7 @@ where it opens elsewhere, and renders it through `AppLink`:
434
434
  | A list row | `ListRow` `href` with a `NameCell` of the same `href`: the name's link covers the row |
435
435
  | A toast's action | `action: { label, href }` |
436
436
  | `SidebarRailItem` | `href`; `onSelect` then runs only on a plain click |
437
+ | `FileExplorer` rows, tree, trail and "Open folder" | `pathHref(path)`; `onPathChange` is then optional, and the explorer's own moves go to `navigation.goTo` |
437
438
  | `EmptyState`, `SettingsCard` actions | a `Button` with `href` |
438
439
 
439
440
  `AppLink` routes in-app URLs through the provider's `link` slot, so a host
@@ -611,6 +612,38 @@ Text that comes back unchanged leaves the cursor alone, and a reset from outside
611
612
  and scroll where they were. Each editor has a model of its own unless it is given a `path`, and it
612
613
  is named `UIStrings.common.codeEditor` unless given an `ariaLabel`.
613
614
 
615
+ Monaco comes from the host's own `monaco-editor` through its bundler, never from a CDN, and loads
616
+ when the first editor mounts, so it stays out of the host's first bundle. Its workers are files in
617
+ the package that the host's bundler copies, started with `new Worker(url)`: a CSP needs
618
+ `worker-src 'self'` and no `blob:`. The YAML language server starts with the first YAML model.
619
+
620
+ The editor colors every language Monaco has, Bash among them (as shell), and fetches each
621
+ language's coloring only when an editor shows it. It validates and formats JSON. The TypeScript,
622
+ CSS and HTML language services are left out, as each runs in a worker of megabytes; their text is
623
+ still colored. `addEditorLanguages` adds a language service and its workers, by the label Monaco
624
+ asks for, or a language of the host's own. It loads with the next editor to mount; call it once
625
+ at startup. With Vite:
626
+
627
+ ```ts
628
+ import { addEditorLanguages } from '@parallelworks/ui/editor'
629
+
630
+ const tsWorker = () =>
631
+ new Worker(new URL('monaco-editor/language/typescript/ts.worker.js', import.meta.url), {
632
+ type: 'module',
633
+ })
634
+ // The TypeScript service checks JavaScript too, and asks for a worker under each label.
635
+ addEditorLanguages({
636
+ load: () => import('monaco-editor/languages/features/typescript/register'),
637
+ workers: { typescript: tsWorker, javascript: tsWorker },
638
+ })
639
+ ```
640
+
641
+ `readOnly` makes it a view of code to read, such as a file's contents: the text stays selectable,
642
+ copyable and reachable by keyboard and is announced read-only, while the cursor, current line and
643
+ quick fixes are hidden, the view ends at the last line, and the wheel scrolls the page once the text
644
+ has scrolled to its end. Any of these can be set otherwise with Monaco's own options. `disabled`
645
+ greys the text out as well, as a disabled field does.
646
+
614
647
  ## AI chat
615
648
 
616
649
  `@parallelworks/ui/ai` is a chat interface (thread, composer, streaming,
@@ -674,6 +707,12 @@ animations) and streamdown's classes next to the theme contract:
674
707
 
675
708
  The prebuilt `@parallelworks/ui/styles.css` already includes both.
676
709
 
710
+ Math needs nothing from the host: `Markdown` imports KaTeX's stylesheet
711
+ itself, from the `katex` package, so the host's bundler loads it and its fonts
712
+ with the markdown renderer's lazy chunk. The bundler must accept CSS imported
713
+ from JavaScript, as the editor's Monaco also requires. Inline math takes `$$`
714
+ within a sentence; a single `$` stays text, so prices are never read as math.
715
+
677
716
  ## Form state
678
717
 
679
718
  `DynamicForm` keeps its values in react-hook-form, a peer dependency, so the
@@ -775,7 +814,7 @@ controls. A label that wraps to two lines sits at the foot of a label row the
775
814
  fields in that row share, so its control stays level with its neighbors'.
776
815
  Set `align: rows` when every immediate child is a
777
816
  section. Adjacent sections then share a header row and one row per direct child.
778
- A description, help text, or validation message increases the shared row height,
817
+ A description or validation message increases the shared row height,
779
818
  so the following rows stay aligned. A nested grid counts as one child row.
780
819
  The grid's gap controls shared row spacing. Hidden children retain their row slot
781
820
  while a neighboring section uses it; empty sections disappear. When the configured
package/dist/ai/ai.css CHANGED
@@ -71,13 +71,15 @@
71
71
  color: inherit;
72
72
  }
73
73
 
74
- /* KaTeX overrides */
75
- .katex-display {
74
+ /* KaTeX overrides. Its own sheet loads with the markdown renderer, after
75
+ * this one, so these need the extra class to win. A wide display formula
76
+ * scrolls instead of widening the answer. */
77
+ .markdown .katex-display {
76
78
  margin: 1em 0;
77
79
  overflow-x: auto;
78
80
  overflow-y: hidden;
79
81
  }
80
- .katex {
82
+ .markdown .katex {
81
83
  font-size: 1.1em;
82
84
  }
83
85
 
@@ -4,6 +4,7 @@ import { Loader } from "../../../components/Loader.js";
4
4
  import { toneSurface } from "../../../components/tone.js";
5
5
  import { useStrings } from "../../../components/Provider.js";
6
6
  import { fieldBox } from "../../../components/fieldBox.js";
7
+ import { focusOnMount } from "../../../components/focus.js";
7
8
  import { Disclosure } from "../../../components/Disclosure.js";
8
9
  import { formatDuration } from "../../../format.js";
9
10
  import { LogViewer } from "../../../logviewer/LogViewer.js";
@@ -12,7 +13,6 @@ import { computeEditDiff, computeWriteRows, editSummary, isEditDiffTool, parseEd
12
13
  import { languageForPath, tokenizeDiffLines } from "../../agent/diffHighlight.js";
13
14
  import { thinkingHeading } from "../../agent/transcript.js";
14
15
  import { useChatConfig } from "../../core/config.js";
15
- import { focusOnMount } from "../../../components/focus.js";
16
16
  import Markdown from "../../ui/Markdown.js";
17
17
  import { chatProseClasses } from "../../ui/prose.js";
18
18
  import { Reasoning, ReasoningBody } from "../Reasoning.js";
@@ -93,7 +93,6 @@ var defaultChatStrings = {
93
93
  "Manifesting",
94
94
  "Moseying",
95
95
  "Musing",
96
- "Percussing",
97
96
  "Reticulating",
98
97
  "Schlepping",
99
98
  "Stewing",
@@ -4,9 +4,9 @@ import { Tooltip } from "../../components/Tooltip.js";
4
4
  import { AppLink, isPlainClick } from "../../components/AppLink.js";
5
5
  import { IconButton } from "../../components/IconButton.js";
6
6
  import { fieldBox } from "../../components/fieldBox.js";
7
+ import { focusOnMount } from "../../components/focus.js";
7
8
  import { ConfirmModal } from "../../components/ConfirmModal.js";
8
9
  import { MoreButton, rowContextMenuProps, useRowMenuActive } from "../../list/rowMenu.js";
9
- import { focusOnMount } from "../../components/focus.js";
10
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
11
  import { useState } from "react";
12
12
  //#region src/ai/ui/sidebar.tsx
@@ -26,10 +26,11 @@ export interface FieldProps {
26
26
  /** Beside the label and outside it, so it stays out of the control's name, such as a count. For
27
27
  * help text, use `help`. */
28
28
  hint?: ReactNode;
29
- /** More about the field, behind a ? beside the label that opens on hover or press. It is read
30
- * with the control as part of its description, so the ? stays out of the Tab order, and it shows
31
- * under the control while the control has keyboard focus. `[text](url)` links and line breaks
32
- * show as such. */
29
+ /** More about the field, behind a ? beside the label. It is read with the control as part of
30
+ * its description, so the ? is hidden from assistive technology and Tab passes over it. It
31
+ * opens on hover or press of the ?, and while the control has keyboard focus, until Escape.
32
+ * `[text](url)` links and line breaks show as such; a link keeps the ? in the Tab order, as
33
+ * the keyboard's way to it. */
33
34
  help?: string | undefined;
34
35
  /** Buttons at the far end of the label's row that act on the field, such as one that
35
36
  * switches its control to a typed expression. */
@@ -1,10 +1,11 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { EyeIcon, EyeOffIcon, WarningIcon } from "../icons.js";
3
3
  import { useStrings } from "./Provider.js";
4
+ import { focusCameFromKeyboard, trackFocusModality } from "./focus.js";
4
5
  import { InfoTip, formatInfoTipContent } from "./InfoTip.js";
5
6
  import { RequiredMark } from "./RequiredMark.js";
6
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
- import { createContext, use, useId } from "react";
8
+ import { createContext, use, useEffect, useId, useState } from "react";
8
9
  import { Field } from "@base-ui/react/field";
9
10
  import { Fieldset } from "@base-ui/react/fieldset";
10
11
  //#region src/components/Field.tsx
@@ -81,10 +82,31 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
81
82
  const inline = layout === "inline";
82
83
  const inRow = use(FieldRowContext) && !inline && !group;
83
84
  const labelSize = labelSizes[size];
85
+ const [helpOpen, setHelpOpen] = useState(false);
86
+ useEffect(() => {
87
+ if (help) trackFocusModality();
88
+ }, [help]);
84
89
  const helpTip = help && /* @__PURE__ */ jsx(InfoTip, {
85
90
  text: help,
86
- tabIndex: -1
91
+ describesControl: true,
92
+ open: Boolean(helpOpen),
93
+ onOpenChange: (open) => setHelpOpen(open && "pointer"),
94
+ keepSide: helpOpen === "focus"
87
95
  });
96
+ const helpFocus = help ? {
97
+ onFocus: () => {
98
+ if (focusCameFromKeyboard()) setHelpOpen("focus");
99
+ },
100
+ onBlur: (event) => {
101
+ if (!event.currentTarget.contains(event.relatedTarget)) setHelpOpen(false);
102
+ },
103
+ onKeyDown: (event) => {
104
+ if (event.key === "Escape" && helpOpen) {
105
+ event.stopPropagation();
106
+ setHelpOpen(false);
107
+ }
108
+ }
109
+ } : {};
88
110
  const labelBox = cx(labelClassName, labelSize.text, inline && cx(labelSize.inlineHeight, "shrink-0", compact ? "mr-2 w-fit" : "w-52 pr-3"), inRow && "row-start-1 self-end");
89
111
  const labelContent = /* @__PURE__ */ jsxs(Fragment, { children: [
90
112
  /* @__PURE__ */ jsxs("span", { children: [label, required && /* @__PURE__ */ jsx(RequiredMark, {})] }),
@@ -98,6 +120,7 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
98
120
  const placed = typeof children === "function" ? children(control) : children;
99
121
  const body = /* @__PURE__ */ jsxs("div", {
100
122
  className: cx("flex w-full min-w-0 flex-col gap-1", inRow && "row-start-2"),
123
+ ...helpFocus,
101
124
  children: [
102
125
  /* @__PURE__ */ jsx(FieldRowContext, {
103
126
  value: false,
@@ -116,7 +139,7 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
116
139
  }),
117
140
  help && /* @__PURE__ */ jsx(Field.Description, {
118
141
  id: helpId,
119
- className: "hidden text-xs leading-relaxed text-(--theme-muted-text-color) group-has-[:focus-visible]/field:block [&_a]:underline",
142
+ hidden: true,
120
143
  children: formatInfoTipContent(help)
121
144
  }),
122
145
  error && /* @__PURE__ */ jsxs(Field.Error, {
@@ -131,7 +154,7 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
131
154
  })
132
155
  ]
133
156
  });
134
- const frame = inRow ? cx("group/field contents", className) : cx("group/field flex w-full", inline ? "items-start" : cx("flex-col", labelSize.stackGap), className);
157
+ const frame = inRow ? cx("contents", className) : cx("flex w-full", inline ? "items-start" : cx("flex-col", labelSize.stackGap), className);
135
158
  if (group) return /* @__PURE__ */ jsx(Field.Root, {
136
159
  invalid: isInvalid,
137
160
  disabled,
@@ -8,11 +8,19 @@ interface InfoTipProps {
8
8
  className?: string | undefined;
9
9
  /** Replaces the info icon as the trigger, such as a status label the text explains. */
10
10
  children?: ReactNode;
11
- /** -1 takes the trigger out of the Tab order, for help that is also read another way, such as
12
- * a Field's, which its control is described by. */
13
- tabIndex?: number | undefined;
11
+ /** The text is also a control's description, which assistive technology reads with the
12
+ * control, so the ? is hidden from it and skipped by Tab rather than offering the text twice.
13
+ * Text with a link keeps the ? reachable, as the only way to the link from the keyboard. */
14
+ describesControl?: boolean | undefined;
15
+ /** Opens it from outside as well as on hover and press, such as while the described control
16
+ * has keyboard focus. Pair it with `onOpenChange`. */
17
+ open?: boolean | undefined;
18
+ onOpenChange?: ((open: boolean) => void) | undefined;
19
+ /** Stays on `side` rather than flipping where it doesn't fit, as while the control it
20
+ * describes is typed in, which a flipped tip would cover. */
21
+ keepSide?: boolean | undefined;
14
22
  }
15
23
  /** Extra detail behind an info trigger. A popover that also opens on hover, so
16
24
  * touch and screen reader users can reach it and its links. */
17
- export declare function InfoTip({ text, side, className, children, tabIndex }: InfoTipProps): import("react").JSX.Element | null;
25
+ export declare function InfoTip({ text, side, className, children, describesControl, open, onOpenChange, keepSide, }: InfoTipProps): import("react").JSX.Element | null;
18
26
  export {};
@@ -28,22 +28,32 @@ function formatInfoTipContent(content) {
28
28
  if (!content) return null;
29
29
  return keyedByContent(content.split(/<br\s*\/?>|\n/i), (l) => l).map(({ key, item: line }, idx) => /* @__PURE__ */ jsxs(Fragment$1, { children: [idx > 0 && /* @__PURE__ */ jsx("br", {}), formatLine(line)] }, key));
30
30
  }
31
+ var hasLink = (content) => new RegExp(linkRegex.source).test(content);
31
32
  /** Extra detail behind an info trigger. A popover that also opens on hover, so
32
33
  * touch and screen reader users can reach it and its links. */
33
- function InfoTip({ text, side = "top", className, children, tabIndex }) {
34
+ function InfoTip({ text, side = "top", className, children, describesControl = false, open, onOpenChange, keepSide = false }) {
34
35
  const t = useStrings().common;
35
36
  if (!text) return children ? /* @__PURE__ */ jsx("span", {
36
37
  className,
37
38
  children
38
39
  }) : null;
40
+ const hidden = describesControl && !hasLink(text);
39
41
  return /* @__PURE__ */ jsx(Popover, {
40
42
  variant: "tip",
41
43
  openOnHover: true,
42
44
  side,
45
+ keepSide,
46
+ ...open === void 0 ? {} : { open },
47
+ ...onOpenChange ? { onOpenChange } : {},
48
+ ...describesControl && { initialFocus: "none" },
49
+ ...hidden && { finalFocus: false },
43
50
  trigger: /* @__PURE__ */ jsx("button", {
44
51
  type: "button",
45
- tabIndex,
46
- "aria-label": children ? void 0 : t.moreInfo,
52
+ ...hidden && {
53
+ tabIndex: -1,
54
+ "aria-hidden": true
55
+ },
56
+ "aria-label": children || hidden ? void 0 : t.moreInfo,
47
57
  className: cx("inline-flex items-center cursor-help", !children && "tooltip", className),
48
58
  children: children ?? /* @__PURE__ */ jsx(HelpIcon, { "aria-hidden": "true" })
49
59
  }),
@@ -1,2 +1,3 @@
1
+ import 'katex/dist/katex.min.css';
1
2
  import type { MarkdownProps } from './Markdown.js';
2
3
  export default function MarkdownImpl({ children, isStreaming, controls, components, }: MarkdownProps): import("react").JSX.Element;
@@ -7,6 +7,7 @@ import { jsx } from "react/jsx-runtime";
7
7
  import { useEffect, useState } from "react";
8
8
  import { code } from "@streamdown/code";
9
9
  import { math } from "@streamdown/math";
10
+ import "katex/dist/katex.min.css";
10
11
  import { Streamdown, defaultRehypePlugins } from "streamdown";
11
12
  //#region src/components/MarkdownImpl.tsx
12
13
  var basePlugins = {
@@ -33,6 +33,9 @@ export interface PopoverOptions extends Omit<BasePopover.Popup.Props, 'className
33
33
  * a list read before one item in it is picked, or nowhere, for a popover that
34
34
  * offers an action on what the reader is doing, such as copying a selection. */
35
35
  initialFocus?: 'first-control' | 'popup' | 'none';
36
+ /** Stays on `side` where it doesn't fit rather than flipping to the other, for a popup that
37
+ * must not cover what is there, such as the control a tip describes while it is typed in. */
38
+ keepSide?: boolean | undefined;
36
39
  /** `panel` holds controls or a list; `tip` is a tooltip-sized note with an arrow. */
37
40
  variant?: 'panel' | 'tip';
38
41
  /** On the popup: its width, padding or text size. */
@@ -43,5 +46,5 @@ export interface PopoverOptions extends Omit<BasePopover.Popup.Props, 'className
43
46
  * flips and shifts to stay on screen, closes on Escape or a press outside, and
44
47
  * returns focus to the trigger. Use a Menu for a list of actions, and a
45
48
  * HoverCard for a preview a link already leads to. */
46
- export declare function Popover({ trigger, at, title, open, defaultOpen, onOpenChange, side, align, openOnHover, initialFocus, variant, className, children, ...popupProps }: PopoverProps): import("react").JSX.Element;
49
+ export declare function Popover({ trigger, at, title, open, defaultOpen, onOpenChange, side, align, openOnHover, initialFocus, keepSide, variant, className, children, ...popupProps }: PopoverProps): import("react").JSX.Element;
47
50
  export {};
@@ -16,7 +16,7 @@ function useClosePopover() {
16
16
  * flips and shifts to stay on screen, closes on Escape or a press outside, and
17
17
  * returns focus to the trigger. Use a Menu for a list of actions, and a
18
18
  * HoverCard for a preview a link already leads to. */
19
- function Popover$1({ trigger, at, title, open, defaultOpen, onOpenChange, side = "bottom", align = "center", openOnHover = false, initialFocus = "first-control", variant = "panel", className, children, ...popupProps }) {
19
+ function Popover$1({ trigger, at, title, open, defaultOpen, onOpenChange, side = "bottom", align = "center", openOnHover = false, initialFocus = "first-control", keepSide = false, variant = "panel", className, children, ...popupProps }) {
20
20
  const tip = variant === "tip";
21
21
  const popup = useRef(null);
22
22
  const actions = useRef(null);
@@ -38,6 +38,7 @@ function Popover$1({ trigger, at, title, open, defaultOpen, onOpenChange, side =
38
38
  align,
39
39
  sideOffset: tip ? 8 : 6,
40
40
  collisionPadding: 8,
41
+ ...keepSide && { collisionAvoidance: { side: "none" } },
41
42
  ...at ? { anchor: toPositionerAnchor(at) } : {},
42
43
  className: positionerClassName,
43
44
  children: /* @__PURE__ */ jsxs(Popover.Popup, {
@@ -268,6 +268,8 @@ export interface UIStrings {
268
268
  select: (name: string) => string;
269
269
  viewDetails: string;
270
270
  openFolder: string;
271
+ /** A path's page in the app, as in "Copy link". */
272
+ link: string;
271
273
  detailsTab: string;
272
274
  previewTab: string;
273
275
  preview: string;
@@ -285,6 +285,7 @@ var DEFAULTS = {
285
285
  select: (name) => `Select ${name}`,
286
286
  viewDetails: "View details",
287
287
  openFolder: "Open folder",
288
+ link: "link",
288
289
  detailsTab: "Details",
289
290
  previewTab: "Preview",
290
291
  preview: "Preview",
@@ -1,2 +1,7 @@
1
1
  /** A ref that focuses a field once, when it mounts; stable so re-renders never move focus back. */
2
2
  export declare function focusOnMount(element: HTMLElement | null): void;
3
+ /** Starts telling keyboard focus from pointer focus, for `focusCameFromKeyboard`. Call it once
4
+ * something needs to know, such as when a component that asks mounts. */
5
+ export declare function trackFocusModality(): void;
6
+ /** Whether the focus that just landed came from the keyboard rather than a press. */
7
+ export declare function focusCameFromKeyboard(): boolean;
@@ -3,5 +3,25 @@
3
3
  function focusOnMount(element) {
4
4
  element?.focus();
5
5
  }
6
+ var fromKeyboard = false;
7
+ var listening = false;
8
+ function onKeyDown(event) {
9
+ if (!event.metaKey && !event.ctrlKey && !event.altKey) fromKeyboard = true;
10
+ }
11
+ function onPointerDown() {
12
+ fromKeyboard = false;
13
+ }
14
+ /** Starts telling keyboard focus from pointer focus, for `focusCameFromKeyboard`. Call it once
15
+ * something needs to know, such as when a component that asks mounts. */
16
+ function trackFocusModality() {
17
+ if (listening || typeof document === "undefined") return;
18
+ listening = true;
19
+ document.addEventListener("keydown", onKeyDown, true);
20
+ document.addEventListener("pointerdown", onPointerDown, true);
21
+ }
22
+ /** Whether the focus that just landed came from the keyboard rather than a press. */
23
+ function focusCameFromKeyboard() {
24
+ return fromKeyboard;
25
+ }
6
26
  //#endregion
7
- export { focusOnMount };
27
+ export { focusCameFromKeyboard, focusOnMount, trackFocusModality };
@@ -1,8 +1,18 @@
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
+ }
6
16
  /** A Monaco code editor, loaded on first render so Monaco stays out of the host's first bundle. */
7
17
  function Editor(props) {
8
18
  const { width = "100%", height = "100%" } = props;
@@ -15,7 +25,7 @@ function Editor(props) {
15
25
  className: "flex items-center justify-center",
16
26
  children: /* @__PURE__ */ jsx(Loader, {})
17
27
  }),
18
- children: /* @__PURE__ */ jsx(MonacoEditor, { ...props })
28
+ children: /* @__PURE__ */ jsx(LoadedEditor, { ...props })
19
29
  });
20
30
  }
21
31
  //#endregion
@@ -1,5 +1,6 @@
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
+ import './monacoFeatures.js';
3
4
  /** A problem Monaco or the YAML service found in the text, as `onValidate` reports it. */
4
5
  export type ValidationMarker = monaco.editor.IMarker;
5
6
  /** A problem to mark on one line of the editor. */
@@ -11,6 +12,12 @@ export interface EditorProps extends monaco.editor.IStandaloneEditorConstruction
11
12
  className?: string;
12
13
  /** Shows the text greyed out and read-only, as a disabled field does. */
13
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;
14
21
  /** The text as the person edits it, with carriage returns removed. */
15
22
  onChange?: (value: string) => void;
16
23
  /** Every marker on the text, each time they change: the schema's, the lint's and the host's. */
@@ -8,10 +8,11 @@ import { registerRevealer } from "./reveal.js";
8
8
  import { defineEditorThemes, followThemeTokens, getThemeName } from "./themes.js";
9
9
  import { setupMonacoWorkers } from "./workers.js";
10
10
  import { holdListeners, modelFor } from "./editorModel.js";
11
+ import "./monacoFeatures.js";
11
12
  import { ensureYamlLanguage } from "./yamlLanguage.js";
12
13
  import { jsx } from "react/jsx-runtime";
13
14
  import { useEffect, useId, useLayoutEffect, useRef } from "react";
14
- import * as monaco from "monaco-editor";
15
+ import * as monaco from "monaco-editor/editor/editor.api";
15
16
  //#region src/editor/Monaco.tsx
16
17
  setupMonacoWorkers();
17
18
  var SCHEMA_UNREADABLE = "768";
@@ -86,9 +87,17 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
86
87
  const editorOptions = {
87
88
  ariaLabel: t.codeEditor,
88
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,
89
94
  ...options,
95
+ scrollbar: {
96
+ alwaysConsumeMouseWheel: !readOnly,
97
+ ...options.scrollbar
98
+ },
90
99
  readOnly,
91
- renderLineHighlight: disabled ? "none" : options.renderLineHighlight ?? "line"
100
+ renderLineHighlight: disabled ? "none" : options.renderLineHighlight ?? (readOnly ? "none" : "line")
92
101
  };
93
102
  const optionsKey = JSON.stringify(editorOptions);
94
103
  const optionsRef = useRef(editorOptions);
@@ -192,6 +201,9 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
192
201
  for (const name of DISABLED_BACKGROUNDS) if (disabled) node?.style.setProperty(name, "var(--theme-input-disabled-bg)");
193
202
  else node?.style.removeProperty(name);
194
203
  }, [disabled]);
204
+ useEffect(() => {
205
+ monacoRef.current?.getDomNode()?.querySelector("textarea.inputarea, .native-edit-context")?.setAttribute("aria-readonly", String(readOnly));
206
+ }, [readOnly, path]);
195
207
  useEffect(() => {
196
208
  monacoRef.current?.updateOptions(optionsRef.current);
197
209
  }, [optionsKey]);
@@ -240,7 +252,8 @@ function MonacoEditor({ className, disabled, onChange = noop, onValidate = noop,
240
252
  height
241
253
  },
242
254
  "data-disabled": disabled ? "" : void 0,
243
- className: cx("relative rounded border border-(--theme-border)", "focus-within:border-(--theme-accent) focus-within:ring-1 focus-within:ring-(--theme-accent)", "data-disabled:[&_.cursors-layer]:hidden data-disabled:[&_.view-lines]:opacity-60", className)
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)
244
257
  });
245
258
  }
246
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`. */
@@ -1,6 +1,7 @@
1
1
  export { Editor } from './Editor.js';
2
2
  export { EditorField, type IEditorField } from './EditorField.js';
3
3
  export { FormCodePanel, type FormCodePanelProps } from './FormCodePanel.js';
4
+ export { addEditorLanguages, type EditorLanguages } from './languages.js';
4
5
  export { HOST_MARKER_OWNER, LINT_OWNER } from './lintOwner.js';
5
6
  export type { EditorMarker, EditorProps, ValidationMarker } from './Monaco.js';
6
7
  export { type NestedWorkflowText, nestedWorkflowText } from './nestedText.js';
@@ -1,5 +1,6 @@
1
1
  import { HOST_MARKER_OWNER, LINT_OWNER } from "./lintOwner.js";
2
2
  import { INPUT_YAML_PATH, JOB_YAML_PATH, SETTINGS_YAML_PATH, STEP_YAML_PATH, settingsSchemas } from "./settingsYaml.js";
3
+ import { addEditorLanguages } from "./languages.js";
3
4
  import { Editor } from "./Editor.js";
4
5
  import { EditorField } from "./EditorField.js";
5
6
  import { FormCodePanel } from "./FormCodePanel.js";
@@ -8,4 +9,4 @@ import { revealLines } from "./reveal.js";
8
9
  import { defineEditorThemes, followThemeTokens, getThemeName } from "./themes.js";
9
10
  import { setupMonacoWorkers } from "./workers.js";
10
11
  import { configureEditorYaml } from "./yaml.js";
11
- export { Editor, EditorField, FormCodePanel, HOST_MARKER_OWNER, INPUT_YAML_PATH, JOB_YAML_PATH, LINT_OWNER, SETTINGS_YAML_PATH, STEP_YAML_PATH, configureEditorYaml, defineEditorThemes, followThemeTokens, getThemeName, nestedWorkflowText, revealLines, settingsSchemas, setupMonacoWorkers };
12
+ export { Editor, EditorField, FormCodePanel, HOST_MARKER_OWNER, INPUT_YAML_PATH, JOB_YAML_PATH, LINT_OWNER, SETTINGS_YAML_PATH, STEP_YAML_PATH, addEditorLanguages, configureEditorYaml, defineEditorThemes, followThemeTokens, getThemeName, nestedWorkflowText, revealLines, settingsSchemas, setupMonacoWorkers };
@@ -0,0 +1,24 @@
1
+ /** Languages a host adds to the ones the editor carries. */
2
+ export interface EditorLanguages {
3
+ /**
4
+ * Imports Monaco's registration for the languages, such as
5
+ * `() => import('monaco-editor/languages/definitions/ruby/register')`. It runs when the next
6
+ * editor mounts, so Monaco still loads only once an editor needs it.
7
+ */
8
+ load: () => Promise<unknown>;
9
+ /**
10
+ * A worker for each language service the registration brings, by the label Monaco asks for,
11
+ * such as `typescript` and `javascript`. Basic languages color text on the page and need none.
12
+ */
13
+ workers?: Record<string, () => Worker>;
14
+ }
15
+ /**
16
+ * Adds language services with their workers, such as TypeScript's, or languages of the host's
17
+ * own, to the coloring the editor has for every Monaco language. Call it once at startup, before
18
+ * the first editor mounts; an editor that mounts while they load waits for them.
19
+ */
20
+ export declare function addEditorLanguages({ load, workers: added }: EditorLanguages): void;
21
+ /** The languages still loading, starting any added since, or undefined once all have. */
22
+ export declare function editorLanguagesLoading(): Promise<unknown> | undefined;
23
+ /** The host's worker for a Monaco worker label, if it added one. */
24
+ export declare function hostEditorWorker(label: string): (() => Worker) | undefined;
@@ -0,0 +1,31 @@
1
+ //#region src/editor/languages.ts
2
+ var workers = /* @__PURE__ */ new Map();
3
+ var waiting = [];
4
+ var loading;
5
+ /**
6
+ * Adds language services with their workers, such as TypeScript's, or languages of the host's
7
+ * own, to the coloring the editor has for every Monaco language. Call it once at startup, before
8
+ * the first editor mounts; an editor that mounts while they load waits for them.
9
+ */
10
+ function addEditorLanguages({ load, workers: added = {} }) {
11
+ for (const [label, create] of Object.entries(added)) workers.set(label, create);
12
+ waiting.push(load);
13
+ }
14
+ /** The languages still loading, starting any added since, or undefined once all have. */
15
+ function editorLanguagesLoading() {
16
+ if (waiting.length > 0) {
17
+ const loads = waiting;
18
+ waiting = [];
19
+ const started = Promise.all([loading, ...loads.map((load) => load().catch((error) => console.error("An editor language failed to load", error)))]).finally(() => {
20
+ if (loading === started) loading = void 0;
21
+ });
22
+ loading = started;
23
+ }
24
+ return loading;
25
+ }
26
+ /** The host's worker for a Monaco worker label, if it added one. */
27
+ function hostEditorWorker(label) {
28
+ return workers.get(label);
29
+ }
30
+ //#endregion
31
+ export { addEditorLanguages, editorLanguagesLoading, hostEditorWorker };
@@ -0,0 +1,7 @@
1
+ import 'monaco-editor/features/register.all';
2
+ import 'monaco-editor/editor/contrib/caretOperations/browser/caretOperations';
3
+ import 'monaco-editor/editor/contrib/dropOrPasteInto/browser/copyPasteContribution';
4
+ import 'monaco-editor/editor/contrib/gotoError/browser/markerSelectionStatus';
5
+ import 'monaco-editor/editor/contrib/semanticTokens/browser/documentSemanticTokens';
6
+ import 'monaco-editor/languages/definitions/register.all';
7
+ import 'monaco-editor/languages/features/json/register';
@@ -0,0 +1,19 @@
1
+ import { languages } from "monaco-editor/editor/editor.api";
2
+ import "monaco-editor/features/register.all";
3
+ import "monaco-editor/editor/contrib/caretOperations/browser/caretOperations";
4
+ import "monaco-editor/editor/contrib/dropOrPasteInto/browser/copyPasteContribution";
5
+ import "monaco-editor/editor/contrib/gotoError/browser/markerSelectionStatus";
6
+ import "monaco-editor/editor/contrib/semanticTokens/browser/documentSemanticTokens";
7
+ import "monaco-editor/languages/definitions/register.all";
8
+ import "monaco-editor/languages/features/json/register";
9
+ //#region src/editor/monacoFeatures.ts
10
+ var shell = () => import("monaco-editor/languages/definitions/shell/shell");
11
+ languages.register({
12
+ id: "bash",
13
+ aliases: ["Bash", "bash"]
14
+ });
15
+ languages.registerTokensProviderFactory("bash", { create: async () => (await shell()).language });
16
+ languages.onLanguageEncountered("bash", async () => {
17
+ languages.setLanguageConfiguration("bash", (await shell()).conf);
18
+ });
19
+ //#endregion