@parallelworks/ui 0.78.0 → 0.80.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 +12 -1
- package/dist/access/AccessDrawer.d.ts +22 -1
- package/dist/access/AccessDrawer.js +33 -9
- package/dist/access/index.d.ts +1 -1
- package/dist/access/strings.d.ts +7 -1
- package/dist/access/strings.js +7 -1
- package/dist/ai/components/ChatMessageList.js +21 -6
- package/dist/components/Combobox.js +56 -32
- package/dist/components/ConfirmModal.d.ts +5 -3
- package/dist/components/ConfirmModal.js +22 -3
- package/dist/components/ContextMenu.d.ts +8 -0
- package/dist/components/ContextMenu.js +42 -6
- package/dist/components/CreateModal.d.ts +5 -1
- package/dist/components/CreateModal.js +2 -2
- package/dist/components/Disclosure.d.ts +6 -2
- package/dist/components/Disclosure.js +24 -2
- package/dist/components/MenuEntries.d.ts +3 -2
- package/dist/components/MenuEntries.js +1 -1
- package/dist/components/Popover.d.ts +4 -1
- package/dist/components/Popover.js +3 -3
- package/dist/components/Provider.d.ts +32 -2
- package/dist/components/Provider.js +16 -3
- package/dist/components/segmented.js +1 -1
- package/dist/graph/InputsEditor.js +3 -2
- package/dist/graph/ProblemPageLink.d.ts +9 -0
- package/dist/graph/ProblemPageLink.js +21 -0
- package/dist/graph/ProblemsButton.js +23 -13
- package/dist/graph/RunWarningsList.js +20 -11
- package/dist/graph/editorApi.d.ts +10 -0
- package/dist/graph/editorRows.js +3 -1
- package/dist/graph/editorStrings.d.ts +1 -0
- package/dist/graph/editorStrings.js +1 -0
- package/dist/graph/runWarnings.d.ts +12 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/styles.css +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
|
546
|
-
className:
|
|
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). */
|
package/dist/access/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/access/strings.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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. */
|
package/dist/access/strings.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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", {
|
|
@@ -91,6 +91,11 @@ function Combobox$1(props) {
|
|
|
91
91
|
const list = useRef(null);
|
|
92
92
|
const uid = useId();
|
|
93
93
|
const [inputValue, setInputValue] = useState(null);
|
|
94
|
+
/** Empties the search, and tells a host that mirrors it. */
|
|
95
|
+
const clearQuery = (to = "") => {
|
|
96
|
+
if (inputValue) onQueryChange?.("");
|
|
97
|
+
setInputValue(to);
|
|
98
|
+
};
|
|
94
99
|
const [highlighted, setHighlighted] = useState(void 0);
|
|
95
100
|
const [startsOpen] = useState(open === void 0 && defaultOpen === true);
|
|
96
101
|
const [ownOpen, setOwnOpen] = useState(false);
|
|
@@ -152,7 +157,7 @@ function Combobox$1(props) {
|
|
|
152
157
|
}
|
|
153
158
|
if (next.length === chosen.length) return false;
|
|
154
159
|
setValues(next);
|
|
155
|
-
|
|
160
|
+
clearQuery();
|
|
156
161
|
return true;
|
|
157
162
|
};
|
|
158
163
|
const separators = props.multiple && props.creatable ? props.separators ?? [] : [];
|
|
@@ -173,11 +178,16 @@ function Combobox$1(props) {
|
|
|
173
178
|
};
|
|
174
179
|
const freeEntry = creatable && entries.flat.length === 0;
|
|
175
180
|
const [first] = entries.items;
|
|
176
|
-
const
|
|
181
|
+
const createLast = created?.action === true;
|
|
182
|
+
const listItems = !created ? entries.items : first !== void 0 && "items" in first ? createLast ? [...entries.items, {
|
|
183
|
+
label: "",
|
|
184
|
+
items: [created],
|
|
185
|
+
order: entries.items.length
|
|
186
|
+
}] : [{
|
|
177
187
|
label: "",
|
|
178
188
|
items: [created],
|
|
179
189
|
order: -1
|
|
180
|
-
}, ...entries.items] : [created, ...entries.items];
|
|
190
|
+
}, ...entries.items] : createLast ? [...entries.items, created] : [created, ...entries.items];
|
|
181
191
|
const selectedIcon = selected?.icon && (inputValue === null || inputValue === selected.label) ? selected.icon : null;
|
|
182
192
|
const input = /* @__PURE__ */ jsx(Combobox.Input, {
|
|
183
193
|
id: trigger ? void 0 : id,
|
|
@@ -227,34 +237,48 @@ function Combobox$1(props) {
|
|
|
227
237
|
})]
|
|
228
238
|
})]
|
|
229
239
|
}, rowProps.key);
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
240
|
+
const labelId = `${uid}-label`;
|
|
241
|
+
const valueId = `${uid}-value`;
|
|
242
|
+
const named = Boolean(ariaLabel || ariaLabelledBy);
|
|
243
|
+
const parts = trigger ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
244
|
+
named && /* @__PURE__ */ jsxs(Fragment$1, { children: [ariaLabel && !ariaLabelledBy && /* @__PURE__ */ jsx("span", {
|
|
245
|
+
id: labelId,
|
|
246
|
+
hidden: true,
|
|
247
|
+
children: ariaLabel
|
|
248
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
249
|
+
id: valueId,
|
|
250
|
+
hidden: true,
|
|
251
|
+
children: /* @__PURE__ */ jsx(Combobox.Value, { children: (value) => Array.isArray(value) ? value.map(entryLabel).join(", ") : entryLabel(value) })
|
|
252
|
+
})] }),
|
|
253
|
+
/* @__PURE__ */ jsx(Tooltip, {
|
|
254
|
+
content: triggerTooltip,
|
|
255
|
+
children: /* @__PURE__ */ jsx(Combobox.Trigger, {
|
|
256
|
+
id,
|
|
257
|
+
"aria-labelledby": named ? `${ariaLabelledBy || labelId} ${valueId}` : void 0,
|
|
258
|
+
"aria-describedby": ariaDescribedBy,
|
|
259
|
+
"aria-invalid": ariaInvalid || void 0,
|
|
260
|
+
"aria-required": ariaRequired || required || void 0,
|
|
261
|
+
render: trigger
|
|
262
|
+
})
|
|
249
263
|
}),
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
264
|
+
/* @__PURE__ */ jsx(ListPopup, {
|
|
265
|
+
listRef: list,
|
|
266
|
+
empty: t.noOptionsFound,
|
|
267
|
+
statusGroups: entries.statusGroups,
|
|
268
|
+
query: inputValue ?? "",
|
|
269
|
+
header: /* @__PURE__ */ jsx("div", {
|
|
270
|
+
className: "border-b border-(--theme-border) p-1.5",
|
|
271
|
+
children: input
|
|
272
|
+
}),
|
|
273
|
+
className: details ? "w-[min(36rem,var(--available-width))]" : "w-max min-w-[min(max(14rem,var(--anchor-width)),var(--available-width))] max-w-[min(28rem,var(--available-width))]",
|
|
274
|
+
sizer: details ? void 0 : /* @__PURE__ */ jsx(ListSizer, { entries: entries.flat }),
|
|
275
|
+
align,
|
|
276
|
+
loading: loading ? t.searching : void 0,
|
|
277
|
+
details: details?.(highlighted?.created ? void 0 : highlighted?.value),
|
|
278
|
+
collisionBoundary,
|
|
279
|
+
renderItem: itemRow
|
|
280
|
+
})
|
|
281
|
+
] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
258
282
|
/* @__PURE__ */ jsxs(Combobox.InputGroup, {
|
|
259
283
|
"data-testid": "combobox",
|
|
260
284
|
className: cx(fieldClassName(size), freeEntry && "pr-2", className),
|
|
@@ -324,7 +348,7 @@ function Combobox$1(props) {
|
|
|
324
348
|
...open !== void 0 ? { open } : startsOpen ? { open: ownOpen } : {},
|
|
325
349
|
onOpenChange: (next) => {
|
|
326
350
|
setOwnOpen(next);
|
|
327
|
-
if (!next && (trigger || !props.multiple))
|
|
351
|
+
if (!next && (trigger || !props.multiple)) clearQuery(null);
|
|
328
352
|
onOpenChange?.(next);
|
|
329
353
|
},
|
|
330
354
|
children: parts
|
|
@@ -343,7 +367,7 @@ function Combobox$1(props) {
|
|
|
343
367
|
const action = next.find((entry) => entry.action);
|
|
344
368
|
if (action) {
|
|
345
369
|
details.cancel();
|
|
346
|
-
|
|
370
|
+
clearQuery();
|
|
347
371
|
onCreate?.(String(action.value));
|
|
348
372
|
return;
|
|
349
373
|
}
|
|
@@ -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
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
|
|
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
|
|
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:
|
|
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:
|
|
@@ -10,7 +10,11 @@ export interface OpenMenuOptions {
|
|
|
10
10
|
size?: MenuSize | undefined;
|
|
11
11
|
/** As `MenuPopup`'s: classes on the menu and its submenus, such as a terminal's colors. */
|
|
12
12
|
themeClassName?: string | undefined;
|
|
13
|
+
/** What else closes it, for a menu over something that moves under it, such as a
|
|
14
|
+
* terminal's: a wheel outside the menu, the window losing focus, or the window resizing. */
|
|
15
|
+
closeOn?: readonly MenuCloseTrigger[] | undefined;
|
|
13
16
|
}
|
|
17
|
+
export type MenuCloseTrigger = 'wheel' | 'blur' | 'resize';
|
|
14
18
|
/** Where a menu opens: an element, such as the button that opened it, or a pointer event,
|
|
15
19
|
* such as a right-click. An event with Shift held is left to the browser, whose own menu
|
|
16
20
|
* then shows; any other has its default prevented. */
|
|
@@ -29,6 +33,7 @@ export type OpenMenu = (at: MenuOpener, items: MenuEntry[], options?: OpenMenuOp
|
|
|
29
33
|
*/
|
|
30
34
|
export declare function useContextMenu(): {
|
|
31
35
|
openMenu: OpenMenu;
|
|
36
|
+
closeMenu: () => void;
|
|
32
37
|
contextMenu: import("react").JSX.Element | null;
|
|
33
38
|
openedBy: Element | null;
|
|
34
39
|
};
|
|
@@ -40,5 +45,8 @@ export declare function ContextMenuProvider({ children }: {
|
|
|
40
45
|
/** Opens the app's one shared menu: `openMenu(e, items)` from a right-click, or
|
|
41
46
|
* `openMenu(button, items)` under a button, which a second press closes. */
|
|
42
47
|
export declare function useOpenMenu(): OpenMenu;
|
|
48
|
+
/** Closes the app's shared menu, if it is open, for a host whose surface moves out from under
|
|
49
|
+
* it in a way `closeOn` doesn't cover, such as a pane that switches. */
|
|
50
|
+
export declare function useCloseMenu(): () => void;
|
|
43
51
|
/** The element the shared menu is open under, so its button can say it's expanded. */
|
|
44
52
|
export declare function useMenuOpenedBy(): Element | null;
|
|
@@ -3,7 +3,7 @@ import { MenuPopup } from "./Menu.js";
|
|
|
3
3
|
import { isDev } from "../dev.js";
|
|
4
4
|
import { MenuEntries, MenuFilter } from "./MenuEntries.js";
|
|
5
5
|
import { Menu } from "@base-ui/react/menu";
|
|
6
|
-
import { createContext, use, useCallback, useRef, useState } from "react";
|
|
6
|
+
import { createContext, use, useCallback, useEffect, useRef, useState } from "react";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/components/ContextMenu.tsx
|
|
9
9
|
/**
|
|
@@ -44,6 +44,31 @@ function useContextMenu() {
|
|
|
44
44
|
setOpen(true);
|
|
45
45
|
return true;
|
|
46
46
|
}, [release]);
|
|
47
|
+
/** Closes the menu, if one is open, as Escape would. */
|
|
48
|
+
const closeMenu = useCallback(() => {
|
|
49
|
+
if (current.current) {
|
|
50
|
+
current.current = null;
|
|
51
|
+
setOpen(false);
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
const closeOn = open ? opened?.closeOn : void 0;
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!closeOn?.length) return;
|
|
57
|
+
const onWheel = (event) => {
|
|
58
|
+
if (!(event.target instanceof Element && event.target.closest("[role=\"menu\"]"))) closeMenu();
|
|
59
|
+
};
|
|
60
|
+
const listeners = [];
|
|
61
|
+
if (closeOn.includes("wheel")) listeners.push(["wheel", onWheel]);
|
|
62
|
+
if (closeOn.includes("blur")) listeners.push(["blur", closeMenu]);
|
|
63
|
+
if (closeOn.includes("resize")) listeners.push(["resize", closeMenu]);
|
|
64
|
+
for (const [type, listener] of listeners) window.addEventListener(type, listener, {
|
|
65
|
+
capture: true,
|
|
66
|
+
passive: true
|
|
67
|
+
});
|
|
68
|
+
return () => {
|
|
69
|
+
for (const [type, listener] of listeners) window.removeEventListener(type, listener, { capture: true });
|
|
70
|
+
};
|
|
71
|
+
}, [closeOn, closeMenu]);
|
|
47
72
|
const onOpenChange = useCallback((next, details) => {
|
|
48
73
|
if (next) return;
|
|
49
74
|
const element = current.current?.element;
|
|
@@ -61,6 +86,7 @@ function useContextMenu() {
|
|
|
61
86
|
}, [release]);
|
|
62
87
|
return {
|
|
63
88
|
openMenu,
|
|
89
|
+
closeMenu,
|
|
64
90
|
contextMenu: opened && /* @__PURE__ */ jsx(MenuFilter, {
|
|
65
91
|
search: opened.search,
|
|
66
92
|
children: (query) => /* @__PURE__ */ jsx(Menu.Root, {
|
|
@@ -85,15 +111,19 @@ function useContextMenu() {
|
|
|
85
111
|
}
|
|
86
112
|
var OpenMenuContext = createContext(null);
|
|
87
113
|
var OpenedByContext = createContext(null);
|
|
114
|
+
var CloseMenuContext = createContext(null);
|
|
88
115
|
/** The one menu every row, button and canvas below it opens through `useOpenMenu`, so none
|
|
89
116
|
* renders a menu of its own. `UIProvider` mounts it; nothing else needs to. */
|
|
90
117
|
function ContextMenuProvider({ children }) {
|
|
91
|
-
const { openMenu, contextMenu, openedBy } = useContextMenu();
|
|
118
|
+
const { openMenu, closeMenu, contextMenu, openedBy } = useContextMenu();
|
|
92
119
|
return /* @__PURE__ */ jsx(OpenMenuContext, {
|
|
93
120
|
value: openMenu,
|
|
94
|
-
children: /* @__PURE__ */
|
|
95
|
-
value:
|
|
96
|
-
children:
|
|
121
|
+
children: /* @__PURE__ */ jsx(CloseMenuContext, {
|
|
122
|
+
value: closeMenu,
|
|
123
|
+
children: /* @__PURE__ */ jsxs(OpenedByContext, {
|
|
124
|
+
value: openedBy,
|
|
125
|
+
children: [children, contextMenu]
|
|
126
|
+
})
|
|
97
127
|
})
|
|
98
128
|
});
|
|
99
129
|
}
|
|
@@ -106,9 +136,15 @@ var warnNoProvider = () => {
|
|
|
106
136
|
function useOpenMenu() {
|
|
107
137
|
return use(OpenMenuContext) ?? warnNoProvider;
|
|
108
138
|
}
|
|
139
|
+
var noMenu = () => {};
|
|
140
|
+
/** Closes the app's shared menu, if it is open, for a host whose surface moves out from under
|
|
141
|
+
* it in a way `closeOn` doesn't cover, such as a pane that switches. */
|
|
142
|
+
function useCloseMenu() {
|
|
143
|
+
return use(CloseMenuContext) ?? noMenu;
|
|
144
|
+
}
|
|
109
145
|
/** The element the shared menu is open under, so its button can say it's expanded. */
|
|
110
146
|
function useMenuOpenedBy() {
|
|
111
147
|
return use(OpenedByContext);
|
|
112
148
|
}
|
|
113
149
|
//#endregion
|
|
114
|
-
export { ContextMenuProvider, useContextMenu, useMenuOpenedBy, useOpenMenu };
|
|
150
|
+
export { ContextMenuProvider, useCloseMenu, useContextMenu, useMenuOpenedBy, useOpenMenu };
|
|
@@ -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
|
|
78
|
+
size,
|
|
79
79
|
dismissible: !isDirty,
|
|
80
80
|
initialFocus: () => nameRef.current ?? true,
|
|
81
81
|
onKeyDown: (e) => {
|