@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.
- package/README.md +42 -3
- package/dist/ai/ai.css +5 -3
- package/dist/ai/components/agent/AgentMessageParts.js +1 -1
- package/dist/ai/strings.js +0 -1
- package/dist/ai/ui/sidebar.js +1 -1
- package/dist/components/Field.d.ts +5 -4
- package/dist/components/Field.js +27 -4
- package/dist/components/InfoTip.d.ts +12 -4
- package/dist/components/InfoTip.js +13 -3
- package/dist/components/MarkdownImpl.d.ts +1 -0
- package/dist/components/MarkdownImpl.js +1 -0
- package/dist/components/Popover.d.ts +4 -1
- package/dist/components/Popover.js +2 -1
- package/dist/components/Provider.d.ts +2 -0
- package/dist/components/Provider.js +1 -0
- package/dist/components/focus.d.ts +5 -0
- package/dist/components/focus.js +21 -1
- package/dist/editor/Editor.js +13 -3
- package/dist/editor/Monaco.d.ts +8 -1
- package/dist/editor/Monaco.js +16 -3
- package/dist/editor/editorModel.d.ts +1 -1
- package/dist/editor/editorModel.js +1 -1
- package/dist/editor/index.d.ts +1 -0
- package/dist/editor/index.js +2 -1
- package/dist/editor/languages.d.ts +24 -0
- package/dist/editor/languages.js +31 -0
- package/dist/editor/monacoFeatures.d.ts +7 -0
- package/dist/editor/monacoFeatures.js +19 -0
- package/dist/editor/themes.js +7 -2
- package/dist/editor/workers.d.ts +3 -3
- package/dist/editor/workers.js +8 -4
- package/dist/editor/yamlLanguage.js +55 -38
- package/dist/file-explorer/FileExplorer.d.ts +18 -4
- package/dist/file-explorer/FileExplorer.js +62 -19
- package/dist/file-explorer/FilePreview/Viewer.js +12 -8
- package/dist/file-explorer/FileRow.d.ts +6 -2
- package/dist/file-explorer/FileRow.js +35 -17
- package/dist/file-explorer/FileRowsWindow.d.ts +2 -1
- package/dist/file-explorer/FileRowsWindow.js +2 -1
- package/dist/file-explorer/FileTable.d.ts +3 -1
- package/dist/file-explorer/FileTable.js +3 -2
- package/dist/file-explorer/TreeView.d.ts +5 -1
- package/dist/file-explorer/TreeView.js +49 -25
- package/dist/file-explorer/helpers.js +3 -3
- package/dist/file-explorer/lib/utils.d.ts +3 -0
- package/dist/file-explorer/lib/utils.js +6 -1
- package/dist/form/Wizard/utils.js +2 -1
- package/dist/list/ActionToolbar.js +3 -2
- package/dist/list/ListPage.js +1 -1
- package/dist/list/ListTable.d.ts +5 -0
- package/dist/list/ListTable.js +6 -2
- package/dist/styles.css +1 -1
- package/package.json +5 -2
- package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +0 -6
- 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
|
|
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
|
|
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
|
-
.
|
|
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";
|
package/dist/ai/strings.js
CHANGED
package/dist/ai/ui/sidebar.js
CHANGED
|
@@ -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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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. */
|
package/dist/components/Field.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
/**
|
|
12
|
-
*
|
|
13
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
46
|
-
|
|
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
|
}),
|
|
@@ -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;
|
|
@@ -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;
|
package/dist/components/focus.js
CHANGED
|
@@ -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 };
|
package/dist/editor/Editor.js
CHANGED
|
@@ -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
|
|
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(
|
|
28
|
+
children: /* @__PURE__ */ jsx(LoadedEditor, { ...props })
|
|
19
29
|
});
|
|
20
30
|
}
|
|
21
31
|
//#endregion
|
package/dist/editor/Monaco.d.ts
CHANGED
|
@@ -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. */
|
package/dist/editor/Monaco.js
CHANGED
|
@@ -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
|
-
|
|
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. */
|
package/dist/editor/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/editor/index.js
CHANGED
|
@@ -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
|