@parallelworks/ui 0.78.0 → 0.79.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 CHANGED
@@ -59,6 +59,15 @@ even for a one-word name. The host sets the tints with `--avatar-person`,
59
59
  edge `--avatar-agent-ring`. An agent's avatar (`kind="agent"`) is a rounded
60
60
  square under either coloring, so its shape and not only its color sets it apart.
61
61
 
62
+ A run's warnings and the workflow editor's problems may carry a problem `code`, `params` and
63
+ `type`, as a server's problem details do. Bind `data.useProblemMessage` to `useCodeMessage` from
64
+ `@parallelworks/problem/react`, and they read in the app's language: the message at
65
+ `apiErrors.<code>` in the app's use-intl catalog, or the shared one. It gets each one's `type` too:
66
+ for a type that names an application, such as `/problems/workflow/undefined_input`, the message at
67
+ `apiErrors.workflow.undefined_input` comes first. The app's own codes can stay at the top level of
68
+ `apiErrors`; another registry's go under its name, so a code both use keeps each one's message and
69
+ params. A code with no message, or no `useProblemMessage`, shows the `message` the server sent.
70
+
62
71
  Menus, selects, pickers, popovers, tooltips and hover cards open over the page,
63
72
  or over the fullscreen element while there is one. Opened inside a `<dialog>`
64
73
  shown with `showModal()`, they open inside that dialog instead: the browser
@@ -806,7 +815,9 @@ each linking to its job or step: the link opens the subworkflows, matrix and job
806
815
  it and what it is in into view below the graph's view bar, readable, and moves focus to it. Each job and step a warning is about, and each one it sits
807
816
  inside, carries a mark that lists the warnings on hover, so one skipped without a log is still
808
817
  flagged. A job name a matrix ran as several jobs marks each of them. `dag.warningCount` words the
809
- count.
818
+ count. A warning with a `code` reads as the host's `useProblemMessage` words it, and one whose `type`
819
+ names a page, such as `/problems/workflow/undefined_input`, links to that page in a new tab
820
+ (`dag.warningAbout`); the editor's problems list does the same (`problemAbout`).
810
821
 
811
822
  The editor checks YAML against the schemas a host registers with `configureEditorYaml` from
812
823
  `@parallelworks/ui/editor`. The job, step, input and settings dialogs edit their YAML at their own
@@ -59,6 +59,24 @@ export interface AccessEditorProps {
59
59
  /** Each person and group holds one role here. Their row picks it from a list, a change
60
60
  * swaps the old role for the new one in a single change, and the grant form picks one. */
61
61
  singleRole?: boolean | undefined;
62
+ /** Why a save failed, in the reader's words, under "Couldn’t grant…": a fixed sentence, or
63
+ * one made from what `onChange` rejected with, such as a localized message for its error
64
+ * code. Without it, a generic sentence; the error's own message never shows, as a
65
+ * server's words aren't the reader's. */
66
+ errorMessage?: string | ((error: unknown) => string | undefined) | undefined;
67
+ /** Access here is set somewhere else, such as on what the resource is part of: the list
68
+ * shows, read-only, under a note that says where, with a link there. */
69
+ managedElsewhere?: AccessManagedElsewhere | undefined;
70
+ }
71
+ export interface AccessManagedElsewhere {
72
+ /** Where access is set and why: "This team follows the workspace’s access." Without it, a
73
+ * generic sentence. */
74
+ reason?: ReactNode;
75
+ /** Where to change it, through the host's router. */
76
+ href?: string | undefined;
77
+ /** The link's text, which says where it goes: "Workspace access". Without it, "Manage
78
+ * access". */
79
+ linkLabel?: string | undefined;
62
80
  }
63
81
  export interface AccessDrawerProps extends AccessEditorProps {
64
82
  open: boolean;
@@ -69,6 +87,9 @@ export interface AccessDrawerProps extends AccessEditorProps {
69
87
  export interface AccessPanelProps extends AccessEditorProps {
70
88
  /** On the panel's frame. */
71
89
  className?: string | undefined;
90
+ /** Under the list, inside the frame and ruled off from it: a note or actions about access
91
+ * as a whole. The grant form takes its place while it shows. */
92
+ footer?: ReactNode;
72
93
  }
73
94
  /** Who has access to one resource, and the place to change it: roles granted to people and
74
95
  * groups. Overlapping roles are allowed and only redundant, which the drawer points out; taking
@@ -78,7 +99,7 @@ export interface AccessPanelProps extends AccessEditorProps {
78
99
  export declare function AccessDrawer({ open, onClose, width, ...props }: AccessDrawerProps): import("react").JSX.Element;
79
100
  /** The access editor inline on a page, such as a settings section, rather than in a drawer: the
80
101
  * list in a frame, which the grant form takes over with a back arrow in its own header. */
81
- export declare function AccessPanel({ className, ...props }: AccessPanelProps): import("react").JSX.Element;
102
+ export declare function AccessPanel({ className, footer, ...props }: AccessPanelProps): import("react").JSX.Element;
82
103
  type Match = (query: string, ...texts: (string | undefined)[]) => boolean;
83
104
  export declare function filterAccess(model: AccessModel, arrangement: AccessArrangement, query: string, match: Match, t: AccessStrings): {
84
105
  shown: Map<string, readonly string[]>;
@@ -9,7 +9,7 @@ import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
9
9
  import { Button } from "../components/Button.js";
10
10
  import { IconButton } from "../components/IconButton.js";
11
11
  import { Callout } from "../components/Callout.js";
12
- import "../components/dismiss.js";
12
+ import { dialogFooterClassName } from "../components/dismiss.js";
13
13
  import { Drawer } from "../components/Drawer.js";
14
14
  import { Select } from "../components/Select.js";
15
15
  import { SearchInput } from "../components/SearchInput.js";
@@ -28,7 +28,7 @@ var EMPTY_SELECTION = {
28
28
  };
29
29
  /** The list and the grant form, and the saving, failing and undoing they share, for the drawer
30
30
  * and the inline panel to lay out. */
31
- function useAccessEditor({ resourceName, roles, principals, grants, owner, inherited, permissions, onChange, loading = false, readOnly = false, readOnlyReason, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, singleRole = false, defaultArrangement = singleRole ? "principal" : "role", visibility, rules }, inline) {
31
+ function useAccessEditor({ resourceName, roles, principals, grants, owner, inherited, permissions, onChange, loading = false, readOnly = false, readOnlyReason, describeGroup, searchPrincipals, groupCount, peopleCount, recentPrincipalIds, singleRole = false, defaultArrangement = singleRole ? "principal" : "role", visibility, rules, errorMessage, managedElsewhere }, inline) {
32
32
  const t = useAccessStrings();
33
33
  const uid = useId();
34
34
  const openMenu = useOpenMenu();
@@ -93,7 +93,8 @@ function useAccessEditor({ resourceName, roles, principals, grants, owner, inher
93
93
  visibilityChange.current = visibility?.onChange;
94
94
  const match = useQueryMatch();
95
95
  const filtered = useFiltered(model, arrangement, query.trim(), match, t);
96
- const editable = !readOnly && onChange !== void 0;
96
+ const editable = !readOnly && !managedElsewhere && onChange !== void 0;
97
+ const messageOf = (error) => (typeof errorMessage === "function" ? errorMessage(error) : errorMessage) || t.failedReason;
97
98
  const sectionId = (id) => `${uid}${arrangement}${id}`;
98
99
  const names = (changed) => {
99
100
  const all = [...changed.grant, ...changed.revoke];
@@ -344,7 +345,28 @@ function useAccessEditor({ resourceName, roles, principals, grants, owner, inher
344
345
  const list = /* @__PURE__ */ jsxs("div", {
345
346
  className: "@container/access flex min-h-0 flex-1 flex-col",
346
347
  children: [
347
- readOnly && /* @__PURE__ */ jsx("div", {
348
+ managedElsewhere ? /* @__PURE__ */ jsx("div", {
349
+ className: cx("px-4 pt-3", empty && !visibility && "pb-3"),
350
+ children: /* @__PURE__ */ jsx(Callout, {
351
+ tone: "info",
352
+ icon: /* @__PURE__ */ jsx(LockIcon, {}),
353
+ children: /* @__PURE__ */ jsxs("div", {
354
+ className: "flex flex-col items-start gap-2 @min-[30rem]/access:flex-row @min-[30rem]/access:gap-3",
355
+ children: [/* @__PURE__ */ jsx("div", {
356
+ className: "min-w-0 flex-1",
357
+ children: managedElsewhere.reason ?? t.managedElsewhere
358
+ }), managedElsewhere.href !== void 0 && /* @__PURE__ */ jsx("div", {
359
+ className: "flex shrink-0 items-center @min-[30rem]/access:h-5",
360
+ children: /* @__PURE__ */ jsx(Button, {
361
+ size: "sm",
362
+ variant: "secondary",
363
+ href: managedElsewhere.href,
364
+ children: managedElsewhere.linkLabel ?? t.manageAccess
365
+ })
366
+ })]
367
+ })
368
+ })
369
+ }) : readOnly && /* @__PURE__ */ jsx("div", {
348
370
  className: "px-4 pt-3",
349
371
  children: /* @__PURE__ */ jsxs(Callout, {
350
372
  tone: "info",
@@ -397,7 +419,7 @@ function useAccessEditor({ resourceName, roles, principals, grants, owner, inher
397
419
  children: loading ? /* @__PURE__ */ jsx(LoadingRegion, {
398
420
  label: t.loading,
399
421
  children: /* @__PURE__ */ jsx(RowsSkeleton, {})
400
- }) : empty ? /* @__PURE__ */ jsxs(Fragment$1, { children: [ruleRows, /* @__PURE__ */ jsx(EmptyState, {
422
+ }) : empty && managedElsewhere ? ruleRows : empty ? /* @__PURE__ */ jsxs(Fragment$1, { children: [ruleRows, /* @__PURE__ */ jsx(EmptyState, {
401
423
  variant: "bare",
402
424
  icon: /* @__PURE__ */ jsx(UsersIcon, {}),
403
425
  title: t.emptyTitle,
@@ -513,7 +535,7 @@ function AccessDrawer({ open, onClose, width = 560, ...props }) {
513
535
  }
514
536
  /** The access editor inline on a page, such as a settings section, rather than in a drawer: the
515
537
  * list in a frame, which the grant form takes over with a back arrow in its own header. */
516
- function AccessPanel({ className, ...props }) {
538
+ function AccessPanel({ className, footer, ...props }) {
517
539
  const t = useAccessStrings();
518
540
  const editor = useAccessEditor(props, true);
519
541
  return /* @__PURE__ */ jsxs("section", {
@@ -542,14 +564,16 @@ function AccessPanel({ className, ...props }) {
542
564
  })]
543
565
  }),
544
566
  editor.body,
545
- editor.footer && /* @__PURE__ */ jsx("div", {
546
- className: "flex shrink-0 items-center justify-end gap-2 border-t border-(--theme-border) px-4 py-3",
567
+ editor.footer ? /* @__PURE__ */ jsx("div", {
568
+ className: dialogFooterClassName,
547
569
  children: editor.footer
570
+ }) : footer !== void 0 && footer !== null && footer !== false && /* @__PURE__ */ jsx("div", {
571
+ className: "border-t border-(--theme-border) px-4 py-3 text-sm text-(--theme-muted-text-color)",
572
+ children: footer
548
573
  })
549
574
  ]
550
575
  });
551
576
  }
552
- var messageOf = (error) => error instanceof Error && error.message ? error.message : void 0;
553
577
  var anchorOf = (failure, arrangement) => arrangement === "role" ? failure.roleId ?? "" : failure.principalId;
554
578
  /** The one of `elements` that is laid out, such as the one of two buttons a width shows, or the
555
579
  * first where nothing is (as in tests). */
@@ -1,3 +1,3 @@
1
- export { type AccessArrangement, AccessDrawer, type AccessDrawerProps, type AccessEditorProps, AccessPanel, type AccessPanelProps, } from './AccessDrawer.js';
1
+ export { type AccessArrangement, AccessDrawer, type AccessDrawerProps, type AccessEditorProps, type AccessManagedElsewhere, AccessPanel, type AccessPanelProps, } from './AccessDrawer.js';
2
2
  export { ACCESS_STRINGS, type AccessStrings, type AccessText, useAccessStrings, } from './strings.js';
3
3
  export type { AccessChange, AccessGrant, AccessGroupDetails, AccessInheritedGrant, AccessMember, AccessPermission, AccessPrincipal, AccessRole, AccessRule, AccessVisibility, } from './types.js';
@@ -66,7 +66,9 @@ export declare const ACCESS_STRINGS: {
66
66
  removeFailed: (principal: string, roles: readonly string[]) => string;
67
67
  grantFailed: (principals: readonly string[], roles: readonly string[]) => string;
68
68
  undoFailed: string;
69
- /** Under a failed save; `reason` is the host's error message, when it gave one. */
69
+ /** Why a save failed, when the host gives no `errorMessage`. */
70
+ failedReason: string;
71
+ /** Under a failed save; `reason` is the host's `errorMessage`, or `failedReason`. */
70
72
  nothingChanged: (reason: string | undefined) => string;
71
73
  /** The toast after a grant. */
72
74
  granted: (principals: readonly string[], roles: readonly string[]) => string;
@@ -74,6 +76,10 @@ export declare const ACCESS_STRINGS: {
74
76
  removed: (principals: readonly string[], roles: readonly string[]) => string;
75
77
  undone: string;
76
78
  readOnly: string;
79
+ /** A note atop access set somewhere else, when the host gives no reason. */
80
+ managedElsewhere: string;
81
+ /** The note's link to where access is set, when the host doesn't name it. */
82
+ manageAccess: string;
77
83
  /** After `readOnly` when the host gives no reason. */
78
84
  readOnlyReason: string;
79
85
  /** Names the switch between open to everyone and only those listed. */
@@ -91,7 +91,9 @@ var ACCESS_STRINGS = {
91
91
  removeFailed: (principal, roles) => `Couldn’t remove ${principal} from ${and(roles)}`,
92
92
  grantFailed: (principals, roles) => `Couldn’t grant ${and(roles)} to ${and(principals)}`,
93
93
  undoFailed: "Couldn’t undo the change",
94
- /** Under a failed save; `reason` is the host's error message, when it gave one. */
94
+ /** Why a save failed, when the host gives no `errorMessage`. */
95
+ failedReason: "Something went wrong.",
96
+ /** Under a failed save; `reason` is the host's `errorMessage`, or `failedReason`. */
95
97
  nothingChanged: (reason) => reason ? `${reason.replace(/\.?$/, ".")} Nothing changed.` : "Nothing changed.",
96
98
  /** The toast after a grant. */
97
99
  granted: (principals, roles) => `Granted ${and(roles)} to ${and(principals)}`,
@@ -99,6 +101,10 @@ var ACCESS_STRINGS = {
99
101
  removed: (principals, roles) => `Removed ${and(principals)} from ${and(roles)}`,
100
102
  undone: "Change undone",
101
103
  readOnly: "View only.",
104
+ /** A note atop access set somewhere else, when the host gives no reason. */
105
+ managedElsewhere: "Access here is set somewhere else, so it can’t be changed here.",
106
+ /** The note's link to where access is set, when the host doesn't name it. */
107
+ manageAccess: "Manage access",
102
108
  /** After `readOnly` when the host gives no reason. */
103
109
  readOnlyReason: "Only people who manage access can make changes.",
104
110
  /** Names the switch between open to everyone and only those listed. */
@@ -45,8 +45,8 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
45
45
  const open = adapter.attachments?.onOpen;
46
46
  return open ? open.bind(adapter.attachments) : void 0;
47
47
  }, [adapter]);
48
- const bottomRef = useRef(null);
49
48
  const containerRef = useRef(null);
49
+ const contentRef = useRef(null);
50
50
  const reasoningScrollRef = useRef(null);
51
51
  const messageCount = messages.length;
52
52
  const streamingText = streamingMessage?.content;
@@ -145,8 +145,25 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
145
145
  ]);
146
146
  useEffect(() => {
147
147
  if (!containerRef.current || !isNearBottomRef.current || isStreaming || !hadMessagesRef.current) return;
148
- bottomRef.current?.scrollIntoView({ behavior: prefersReducedMotion() ? "auto" : "smooth" });
148
+ const el = containerRef.current;
149
+ el.scrollTo({
150
+ top: el.scrollHeight,
151
+ behavior: prefersReducedMotion() ? "auto" : "smooth"
152
+ });
149
153
  }, [messageCount, isStreaming]);
154
+ useEffect(() => {
155
+ const content = contentRef.current;
156
+ if (!content || typeof ResizeObserver === "undefined") return;
157
+ const observer = new ResizeObserver(() => {
158
+ const el = containerRef.current;
159
+ if (el && isNearBottomRef.current && !hasQueuedMessages) {
160
+ el.scrollTop = el.scrollHeight;
161
+ lastScrollTopRef.current = el.scrollTop;
162
+ }
163
+ });
164
+ observer.observe(content);
165
+ return () => observer.disconnect();
166
+ }, [hasContent, hasQueuedMessages]);
150
167
  if (messages.length === 0 && !streamingMessage && !hasQueuedMessages) return /* @__PURE__ */ jsxs("div", {
151
168
  className: "chat-thread flex-1 overflow-y-auto flex flex-col bg-(--theme-panel-bg)",
152
169
  children: [
@@ -170,6 +187,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
170
187
  className: "chat-thread flex-1 overflow-y-auto theme-scrollbar flex flex-col bg-(--theme-panel-bg)",
171
188
  children: [
172
189
  /* @__PURE__ */ jsxs("div", {
190
+ ref: contentRef,
173
191
  className: "w-full max-w-[var(--chat-column,50rem)] mx-auto pt-4 pb-6 flex-1 px-4",
174
192
  children: [
175
193
  /* @__PURE__ */ jsx(MessageRows, {
@@ -283,10 +301,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
283
301
  }, msg.id);
284
302
  }),
285
303
  footer,
286
- /* @__PURE__ */ jsx("div", {
287
- ref: bottomRef,
288
- className: "h-8"
289
- })
304
+ /* @__PURE__ */ jsx("div", { className: "h-8" })
290
305
  ]
291
306
  }),
292
307
  !pinned && /* @__PURE__ */ jsx("div", {
@@ -23,9 +23,11 @@ export interface ConfirmModalProps {
23
23
  cancelLabel?: string | undefined;
24
24
  /** When false, the dialog stays open after a successful confirm. */
25
25
  closeOnConfirm?: boolean | undefined;
26
- /** Which button has focus as it opens. Cancel for a destructive dialog, so a stray Enter
27
- * destroys nothing; the confirm button otherwise, so the keyboard answers at once. */
28
- initialFocus?: 'cancel' | 'confirm' | undefined;
26
+ /** What has focus as it opens. With a field in the body, such as a name to type, the first
27
+ * of them (`first-control`), so the reader types at once. Without one, Cancel for a
28
+ * destructive dialog, so a stray Enter destroys nothing, and the confirm button otherwise,
29
+ * so the keyboard answers at once. */
30
+ initialFocus?: 'cancel' | 'confirm' | 'first-control' | undefined;
29
31
  /** Work the host runs for it, such as a mutation already under way: the buttons wait, and
30
32
  * the dialog can't be dismissed, as while its own handler runs. */
31
33
  pending?: boolean | undefined;
@@ -5,14 +5,25 @@ import { Modal } from "./Modal.js";
5
5
  import { useCallback, useEffect, useRef, useState } from "react";
6
6
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/components/ConfirmModal.tsx
8
+ /** What the reader types or picks in, which takes focus ahead of the buttons. A checkbox or
9
+ * radio doesn't: a destructive dialog with one still opens on Cancel. Inputs a picker keeps
10
+ * out of the tab order, for its form value, aren't fields. */
11
+ var FIELDS = [
12
+ "input:not([disabled]):not([type=\"hidden\"]):not([type=\"checkbox\"]):not([type=\"radio\"])",
13
+ "select:not([disabled])",
14
+ "textarea:not([disabled])",
15
+ "[role=\"combobox\"]:not([disabled]):not([aria-disabled=\"true\"])",
16
+ "[contenteditable=\"true\"]"
17
+ ].map((field) => `${field}:not([tabindex="-1"]):not([aria-hidden="true"])`).join(", ");
8
18
  /** Asks before an action: its title the question, its description what follows,
9
19
  * and Cancel beside a confirm button named for the action. */
10
- function ConfirmModal({ open, onClose, title, description, children, confirmLabel, onConfirm, confirmDisabled = false, destructive = false, align = "top", cancelLabel, closeOnConfirm = true, initialFocus = destructive ? "cancel" : "confirm", pending: pendingProp = false, error, size = "md" }) {
20
+ function ConfirmModal({ open, onClose, title, description, children, confirmLabel, onConfirm, confirmDisabled = false, destructive = false, align = "top", cancelLabel, closeOnConfirm = true, initialFocus, pending: pendingProp = false, error, size = "md" }) {
11
21
  const { common } = useStrings();
12
22
  const [running, setRunning] = useState(false);
13
23
  const fired = useRef(false);
14
24
  const cancelButton = useRef(null);
15
25
  const confirmButton = useRef(null);
26
+ const bodyElement = useRef(null);
16
27
  useEffect(() => {
17
28
  if (open) fired.current = false;
18
29
  }, [open]);
@@ -44,7 +55,11 @@ function ConfirmModal({ open, onClose, title, description, children, confirmLabe
44
55
  size,
45
56
  title,
46
57
  description,
47
- initialFocus: initialFocus === "cancel" ? cancelButton : confirmButton,
58
+ initialFocus: () => {
59
+ const button = (initialFocus === "cancel" || initialFocus !== "confirm" && destructive ? cancelButton : confirmButton).current;
60
+ if (initialFocus === "cancel" || initialFocus === "confirm") return button ?? true;
61
+ return bodyElement.current?.querySelector(FIELDS) ?? button ?? true;
62
+ },
48
63
  footer: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
49
64
  ref: cancelButton,
50
65
  variant: "ghost",
@@ -61,7 +76,11 @@ function ConfirmModal({ open, onClose, title, description, children, confirmLabe
61
76
  loading: pending,
62
77
  children: confirmLabel
63
78
  })] }),
64
- children: body
79
+ children: body !== void 0 && /* @__PURE__ */ jsx("div", {
80
+ ref: bodyElement,
81
+ className: "contents",
82
+ children: body
83
+ })
65
84
  });
66
85
  }
67
86
  /** Asks from anywhere, such as a menu row, without keeping a dialog's state by hand:
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { type ComboboxItems } from './comboboxItems.js';
3
3
  import type { MenuEntry, MenuSearch } from './MenuEntries.js';
4
+ import { type ModalSize } from './Modal.js';
4
5
  interface CreateModalProps {
5
6
  open: boolean;
6
7
  onClose: () => void;
@@ -43,6 +44,9 @@ interface CreateModalProps {
43
44
  footerHint?: ReactNode;
44
45
  /** Why the last submit failed, at the footer's start, read out as it appears. */
45
46
  error?: ReactNode;
47
+ /** Its width, as `Modal`'s: `xl` for a long body such as a rich text editor. It narrows to
48
+ * fit a small window. */
49
+ size?: ModalSize | undefined;
46
50
  /** Resolves true to close the modal, false to keep it open (e.g. on error). `modifier` is
47
51
  * true when submitted with ⌘/Ctrl+Enter, so a host can create and then open what it made. */
48
52
  onSubmit: (name: string, description: string, favorite: boolean, how: {
@@ -50,7 +54,7 @@ interface CreateModalProps {
50
54
  }) => Promise<boolean>;
51
55
  }
52
56
  /** Submits on Enter in the name field or ⌘/Ctrl+Enter from anywhere in the panel. */
53
- export declare function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSearch, namePlaceholder, hideName, initialName, showDescription, descriptionPlaceholder, descriptionField, submitLabel, submittingLabel, validateName, sanitizeName, nameHint, children, properties, advanced, submitDisabled, hideSubmit, nameOptional, onNameChange, dirty, showFavorite, footerHint, error, onSubmit, }: CreateModalProps): import("react").JSX.Element;
57
+ export declare function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSearch, namePlaceholder, hideName, initialName, showDescription, descriptionPlaceholder, descriptionField, submitLabel, submittingLabel, validateName, sanitizeName, nameHint, children, properties, advanced, submitDisabled, hideSubmit, nameOptional, onNameChange, dirty, showFavorite, footerHint, error, size, onSubmit, }: CreateModalProps): import("react").JSX.Element;
54
58
  /** Compact property pill with an on/off switch. */
55
59
  export declare function CreateModalSwitch({ label, checked, onCheckedChange, tooltip, }: {
56
60
  label: string;
@@ -16,7 +16,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
16
16
  import { Toggle } from "@base-ui/react/toggle";
17
17
  //#region src/components/CreateModal.tsx
18
18
  /** Submits on Enter in the name field or ⌘/Ctrl+Enter from anywhere in the panel. */
19
- function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSearch, namePlaceholder, hideName = false, initialName, showDescription = true, descriptionPlaceholder, descriptionField, submitLabel, submittingLabel, validateName, sanitizeName, nameHint, children, properties, advanced, submitDisabled = false, hideSubmit = false, nameOptional = false, onNameChange, dirty = false, showFavorite = false, footerHint, error, onSubmit }) {
19
+ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSearch, namePlaceholder, hideName = false, initialName, showDescription = true, descriptionPlaceholder, descriptionField, submitLabel, submittingLabel, validateName, sanitizeName, nameHint, children, properties, advanced, submitDisabled = false, hideSubmit = false, nameOptional = false, onNameChange, dirty = false, showFavorite = false, footerHint, error, size = "lg", onSubmit }) {
20
20
  const t = useStrings().modal;
21
21
  const { common } = useStrings();
22
22
  const submitText = submitLabel ?? common.create;
@@ -75,7 +75,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
75
75
  open,
76
76
  onClose,
77
77
  "aria-label": `${submitText}: ${typeLabel}`,
78
- size: "lg",
78
+ size,
79
79
  dismissible: !isDirty,
80
80
  initialFocus: () => nameRef.current ?? true,
81
81
  onKeyDown: (e) => {
@@ -45,11 +45,12 @@ interface MenuLinkAction extends MenuActionBase {
45
45
  checked?: never;
46
46
  copy?: never;
47
47
  }
48
- /** Copies `copy.text` to the clipboard, confirmed with a toast naming `copy.label`. */
48
+ /** Copies `copy.text` to the clipboard, confirmed with a toast naming `copy.label`, or
49
+ * saying only that it copied without one. A failure says so too. */
49
50
  interface MenuCopyAction extends MenuActionBase {
50
51
  copy: {
51
52
  text: string;
52
- label: string;
53
+ label?: string | undefined;
53
54
  };
54
55
  onClick?: never;
55
56
  checked?: never;
@@ -188,7 +188,7 @@ var shortcutClassName = "ps-3 text-xs text-(--theme-muted-text-color) tabular-nu
188
188
  function RowContent({ icon, label, description, end }) {
189
189
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
190
190
  icon !== void 0 && icon !== null && /* @__PURE__ */ jsx("span", {
191
- className: "flex h-5 shrink-0 items-center in-data-[size=sm]:h-4",
191
+ className: "flex h-5 min-w-4 shrink-0 items-center justify-center in-data-[size=sm]:h-4",
192
192
  children: icon
193
193
  }),
194
194
  /* @__PURE__ */ jsxs("span", {
@@ -52,6 +52,9 @@ export interface PopoverOptions extends Omit<BasePopover.Popup.Props, 'className
52
52
  sheet?: boolean | undefined;
53
53
  /** `panel` holds controls or a list; `tip` is a tooltip-sized note in the tooltip's look. */
54
54
  variant?: 'panel' | 'tip';
55
+ /** A panel's content reaches its edges, such as a list of suggestions whose rows pad
56
+ * themselves and highlight edge to edge. A title keeps its own padding. */
57
+ flush?: boolean | undefined;
55
58
  /** On the popup: its width, padding or text size. */
56
59
  className?: string;
57
60
  children: ReactNode;
@@ -60,5 +63,5 @@ export interface PopoverOptions extends Omit<BasePopover.Popup.Props, 'className
60
63
  * flips and shifts to stay on screen, closes on Escape or a press outside, and
61
64
  * returns focus to the trigger. Use a Menu for a list of actions, and a
62
65
  * HoverCard for a preview a link already leads to. */
63
- export declare function Popover({ trigger, at, title, open, defaultOpen, onOpenChange, side, align, openOnHover, hoverDelay, hoverCloseDelay, initialFocus, anchor, keepSide, variant, sheet, className, children, ...popupProps }: PopoverProps): import("react").JSX.Element;
66
+ export declare function Popover({ trigger, at, title, open, defaultOpen, onOpenChange, side, align, openOnHover, hoverDelay, hoverCloseDelay, initialFocus, anchor, keepSide, variant, sheet, flush, className, children, ...popupProps }: PopoverProps): import("react").JSX.Element;
64
67
  export {};
@@ -19,7 +19,7 @@ var sheetPopupClassName = "max-sm:w-auto! max-sm:max-w-none! max-sm:max-h-[min(7
19
19
  * flips and shifts to stay on screen, closes on Escape or a press outside, and
20
20
  * returns focus to the trigger. Use a Menu for a list of actions, and a
21
21
  * HoverCard for a preview a link already leads to. */
22
- function Popover$1({ trigger, at, title, open, defaultOpen, onOpenChange, side = "bottom", align = "center", openOnHover = false, hoverDelay = 300, hoverCloseDelay = 150, initialFocus = "first-control", anchor, keepSide = false, variant = "panel", sheet, className, children, ...popupProps }) {
22
+ function Popover$1({ trigger, at, title, open, defaultOpen, onOpenChange, side = "bottom", align = "center", openOnHover = false, hoverDelay = 300, hoverCloseDelay = 150, initialFocus = "first-control", anchor, keepSide = false, variant = "panel", sheet, flush = false, className, children, ...popupProps }) {
23
23
  const portal = usePopupContainer();
24
24
  const tip = variant === "tip";
25
25
  const asSheet = sheet ?? (!tip && !at);
@@ -52,9 +52,9 @@ function Popover$1({ trigger, at, title, open, defaultOpen, onOpenChange, side =
52
52
  ...popupProps,
53
53
  ref: popup,
54
54
  initialFocus: initialFocus === "popup" ? popup : initialFocus === "first-control",
55
- className: cx(tip ? cx("text-left", tipClassName) : cx(panelClassName, "max-h-(--available-height) max-w-[calc(100vw-16px)] overflow-y-auto p-3"), className, asSheet && sheetPopupClassName),
55
+ className: cx(tip ? cx("text-left", tipClassName) : cx(panelClassName, "max-h-(--available-height) max-w-[calc(100vw-16px)] overflow-y-auto", !flush && "p-3"), className, asSheet && sheetPopupClassName),
56
56
  children: [title && /* @__PURE__ */ jsx(Popover.Title, {
57
- className: "mb-2 text-sm font-semibold",
57
+ className: cx("mb-2 text-sm font-semibold", flush && !tip && "px-3 pt-3"),
58
58
  children: title
59
59
  }), /* @__PURE__ */ jsx(CloseContext, {
60
60
  value: close,
@@ -288,6 +288,8 @@ export interface UIStrings {
288
288
  /** How many warnings: the title of a run's list of warnings, and the name of the mark on a job
289
289
  * or step they're about, such as "2 warnings". */
290
290
  warningCount: (count: number) => string;
291
+ /** The link from a warning to the page that explains it, when it has one. */
292
+ warningAbout: string;
291
293
  };
292
294
  jobActions: {
293
295
  openSubworkflowLogs: string;
@@ -733,6 +735,22 @@ export interface RunFileResult {
733
735
  error: unknown;
734
736
  }
735
737
  export type PersistedStateHook = <T>(key: string, defaultValue: T) => [T, Dispatch<SetStateAction<T>>];
738
+ /** The values a problem's message shows, by name, as a problem's `params` carries them. */
739
+ export type ProblemParams = Readonly<Record<string, string | number>>;
740
+ /** A problem code's message in the reader's language, with its params; undefined when the host has
741
+ * none for the code. `type` is the problem's type URI: one that names an application, such as
742
+ * `/problems/workflow/undefined_input`, picks that application's message for the code over the
743
+ * host's own. */
744
+ export type ProblemMessage = (code: string, params?: ProblemParams, type?: string) => string | undefined;
745
+ /** A problem that may carry a code: a run's warning, or one the workflow editor marks. */
746
+ export interface CodedProblem {
747
+ /** Its text as the server wrote it, shown when the host has no message for its code. */
748
+ message: string;
749
+ code?: string | null | undefined;
750
+ params?: ProblemParams | null | undefined;
751
+ /** Its type URI, which the host's message for its code is looked up by too. */
752
+ type?: string | null | undefined;
753
+ }
736
754
  export interface UIData {
737
755
  /**
738
756
  * Host-bound hook resolving a file in a run's artifact tree (logs, scripts).
@@ -763,6 +781,14 @@ export interface UIData {
763
781
  error?: string;
764
782
  }>;
765
783
  usePersistedState: PersistedStateHook;
784
+ /**
785
+ * Host-bound hook returning a problem code's message in the reader's language, such as
786
+ * `useCodeMessage` from `@parallelworks/problem/react`. A run's warnings and the editor's
787
+ * problems that carry a `code` show it, given their `params` and `type`, and their own `message`
788
+ * when it gives nothing. Absent, they show their `message`. Must be a stable function reference —
789
+ * components call it as a hook.
790
+ */
791
+ useProblemMessage: () => ProblemMessage;
766
792
  }
767
793
  import type { FieldComponent } from '../form/fieldRegistry.js';
768
794
  import type { Breadcrumb } from './Breadcrumbs.js';
@@ -864,6 +890,9 @@ export declare function useProvisionCorsRules(): UIData['provisionCorsRules'];
864
890
  export declare function useRunFile(slug: string, path: string | null, options?: {
865
891
  refreshInterval?: number;
866
892
  }): RunFileResult;
893
+ /** A coded problem's text in the reader's language: the host's message for its code and type, or
894
+ * else its own `message`. */
895
+ export declare function useProblemText(): (problem: CodedProblem) => string;
867
896
  export declare function usePersistedState<T>(key: string, defaultValue: T): [T, Dispatch<SetStateAction<T>>];
868
897
  export interface UIProviderProps {
869
898
  /** Translations over the English defaults, at any depth. Define them outside a component, or
@@ -871,8 +900,9 @@ export interface UIProviderProps {
871
900
  strings?: UIStringOverrides | undefined;
872
901
  /** How the package navigates; read at the moment it navigates, so a new one never re-renders. */
873
902
  navigation?: Partial<UINavigation> | undefined;
874
- /** The host's data hooks and resolvers. `useRunFile` and `usePersistedState` are called as
875
- * hooks, so each must stay the same function for the life of the app. */
903
+ /** The host's data hooks and resolvers. `useRunFile`, `usePersistedState` and
904
+ * `useProblemMessage` are called as hooks, so each must stay the same function for the life of
905
+ * the app. */
876
906
  data?: Partial<UIData> | undefined;
877
907
  /** Components the host renders inside the package's; a component made each render would
878
908
  * remount everything it renders. */
@@ -320,7 +320,8 @@ var DEFAULTS = {
320
320
  ].filter(([count]) => count).map(([count, one, many]) => `${count} ${count === 1 ? one : many}`).join(", ")}`,
321
321
  annotationSource: (job, step) => `in ${job} → ${step}`,
322
322
  stepNumber: (index) => `step ${index}`,
323
- warningCount: (count) => count === 1 ? "1 warning" : `${count} warnings`
323
+ warningCount: (count) => count === 1 ? "1 warning" : `${count} warnings`,
324
+ warningAbout: "About this warning"
324
325
  },
325
326
  jobActions: {
326
327
  openSubworkflowLogs: "Open subworkflow logs",
@@ -692,7 +693,8 @@ var DEFAULTS = {
692
693
  },
693
694
  data: {
694
695
  useRunFile: defaultUseRunFile,
695
- usePersistedState: useLocalPersistedState
696
+ usePersistedState: useLocalPersistedState,
697
+ useProblemMessage: useNoProblemMessage
696
698
  },
697
699
  slots: {}
698
700
  };
@@ -802,6 +804,17 @@ function useRunFile(slug, path, options) {
802
804
  const { useRunFile: boundUseRunFile } = useContext(DataContext);
803
805
  return boundUseRunFile(slug, path, options);
804
806
  }
807
+ var noProblemMessage = () => void 0;
808
+ function useNoProblemMessage() {
809
+ return noProblemMessage;
810
+ }
811
+ /** A coded problem's text in the reader's language: the host's message for its code and type, or
812
+ * else its own `message`. */
813
+ function useProblemText() {
814
+ const { useProblemMessage: boundUseProblemMessage } = useContext(DataContext);
815
+ const message = boundUseProblemMessage();
816
+ return useCallback((problem) => (problem.code ? message(problem.code, problem.params ?? void 0, problem.type ?? void 0) : void 0) ?? problem.message, [message]);
817
+ }
805
818
  function usePersistedState(key, defaultValue) {
806
819
  const { usePersistedState: boundUsePersistedState } = useContext(DataContext);
807
820
  return boundUsePersistedState(key, defaultValue);
@@ -869,4 +882,4 @@ function UIProvider({ strings, navigation, data, slots, engine: engineProp, nonc
869
882
  });
870
883
  }
871
884
  //#endregion
872
- export { UIProvider, useAvatarColoring, useLink, useLoadedWorkflowEngine, useLocale, useNavigation, useOptionalWorkflowEngine, usePersistedState, useProvisionCorsRules, useRepoSuggestions, useRunFile, useSecretVariablesResolver, useSlots, useStrings, useWorkflowActions, useWorkflowEditing, useWorkflowEngine, useWorkflowEngineLoader, useWorkflowJsonResolver };
885
+ export { UIProvider, useAvatarColoring, useLink, useLoadedWorkflowEngine, useLocale, useNavigation, useOptionalWorkflowEngine, usePersistedState, useProblemText, useProvisionCorsRules, useRepoSuggestions, useRunFile, useSecretVariablesResolver, useSlots, useStrings, useWorkflowActions, useWorkflowEditing, useWorkflowEngine, useWorkflowEngineLoader, useWorkflowJsonResolver };
@@ -34,7 +34,7 @@ function segmentGroupClassName({ fill, iconOnly }) {
34
34
  return cx("gap-0.5", iconOnly ? "flex flex-col items-center" : cx("rounded-lg theme-muted-panel p-0.5", fillGroup[fillKey(fill)]));
35
35
  }
36
36
  function segmentClassName(current, { fill, iconOnly, size = "md" }, tone) {
37
- return cx("relative flex min-w-0 cursor-pointer items-center justify-center rounded-md font-medium transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--theme-focus) data-disabled:cursor-not-allowed data-disabled:opacity-50", current ? "bg-(--theme-panel-bg) shadow-sm ring-1 ring-(--theme-border)" : "hover:not-data-disabled:bg-(--theme-hover)", tone ? toneText[tone] : current ? "theme-text" : "theme-muted-text hover:not-data-disabled:theme-text", iconOnly ? "h-8 w-8" : segmentSizes[size][fillKey(fill)]);
37
+ return cx("relative flex min-w-0 cursor-pointer items-center justify-center rounded-md font-medium transition-colors duration-150 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--theme-focus) data-disabled:cursor-not-allowed data-disabled:opacity-50", current ? "bg-(--theme-panel-bg) shadow-sm ring-1 ring-(--theme-border)" : "hover:not-data-disabled:bg-(--theme-hover)", tone ? toneText[tone] : current ? "theme-text" : "theme-muted-text hover:not-data-disabled:theme-text", iconOnly ? "h-8 w-8" : segmentSizes[size][fillKey(fill)]);
38
38
  }
39
39
  /** How many things a tab or segment holds, as a pill after its label. */
40
40
  function CountPill({ count, tone }) {
@@ -3,7 +3,7 @@ import { EmptyState } from "../components/EmptyState.js";
3
3
  import { AddIcon, ArrowDownIcon, ArrowUpIcon, CollapseIcon, DragHandleIcon, DuplicateIcon, EditIcon, FilesIcon, MinusIcon, MoreIcon, TrashIcon, WarningIcon } from "../icons.js";
4
4
  import { useContextMenu } from "../components/ContextMenu.js";
5
5
  import { Tooltip } from "../components/Tooltip.js";
6
- import { useWorkflowEditing } from "../components/Provider.js";
6
+ import { useProblemText, useWorkflowEditing } from "../components/Provider.js";
7
7
  import { menuDivider } from "../components/MenuEntries.js";
8
8
  import { IconButton } from "../components/IconButton.js";
9
9
  import { NumberInput } from "../components/NumberInput.js";
@@ -100,7 +100,8 @@ function InputRow({ path, hidden = false, instance, children }) {
100
100
  const copy = instance ?? rowKey;
101
101
  const active = useUi(api?.store ?? EMPTY_STORE, (state) => state.open.includes(copy) && !state.drag && !state.resizing);
102
102
  const selected = useUi(api?.store ?? EMPTY_STORE, (state) => state.selection.includes(rowKey));
103
- const problems = useUi(api?.store ?? EMPTY_STORE, (state) => state.problems.filter((problem) => problem.input && key(problem.input) === rowKey).map((problem) => `${problem.line}\n${problem.message}`).join("\n\n"));
103
+ const problemText = useProblemText();
104
+ const problems = useUi(api?.store ?? EMPTY_STORE, (state) => state.problems.filter((problem) => problem.input && key(problem.input) === rowKey).map((problem) => `${problem.line}\n${problemText(problem)}`).join("\n\n"));
104
105
  if (!api) return children;
105
106
  const found = problems ? problems.split("\n\n") : [];
106
107
  const definition = api.definition(path);
@@ -0,0 +1,9 @@
1
+ /** The page a problem's type URI resolves to, or undefined for `about:blank` and no type, which
2
+ * have none. */
3
+ export declare function problemPage(type: string | null | undefined): string | undefined;
4
+ /** Links a problem to the page that explains it, in a new tab, when its type has one. */
5
+ export declare function ProblemPageLink({ type, label, className, }: {
6
+ type: string | null | undefined;
7
+ label: string;
8
+ className?: string | undefined;
9
+ }): import("react").JSX.Element | null;