@parallelworks/ui 0.81.0 → 0.82.1
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/dist/ai/components/ChatInput.js +1 -1
- package/dist/ai/components/ChatMessage.js +2 -2
- package/dist/ai/components/agent/AgentMessageParts.js +1 -1
- package/dist/components/Avatar.d.ts +5 -3
- package/dist/components/Avatar.js +6 -2
- package/dist/components/CodeBlock.js +1 -1
- package/dist/components/CopyButton.d.ts +11 -4
- package/dist/components/CopyButton.js +86 -11
- package/dist/components/CreateModal.js +16 -3
- package/dist/components/Listbox.d.ts +12 -3
- package/dist/components/Listbox.js +38 -15
- package/dist/components/PageHeader.d.ts +3 -1
- package/dist/components/PageHeader.js +6 -3
- package/dist/components/SettingsCard.d.ts +7 -2
- package/dist/components/SettingsCard.js +42 -7
- package/dist/components/Tooltip.js +35 -2
- package/dist/list/ListFilterBar.js +6 -5
- package/dist/list/filterModel.d.ts +4 -0
- package/dist/list/filterModel.js +7 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -207,7 +207,7 @@ function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEd
|
|
|
207
207
|
className: "chat-composer-dock relative bg-(--theme-panel-bg) px-4 pt-2 pb-5",
|
|
208
208
|
...attachmentsAvailable ? dragHandlers : {},
|
|
209
209
|
children: [isDragging && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-2xl" }), /* @__PURE__ */ jsxs("div", {
|
|
210
|
-
className: "mx-auto w-full max-w-[var(--chat-column,50rem)] space-y-2 px-4",
|
|
210
|
+
className: "mx-auto w-full max-w-[var(--chat-column,50rem)] space-y-2 sm:px-4",
|
|
211
211
|
children: [notices, /* @__PURE__ */ jsxs("div", { children: [
|
|
212
212
|
activity && /* @__PURE__ */ jsx("div", {
|
|
213
213
|
className: "mb-2",
|
|
@@ -108,7 +108,7 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
108
108
|
if (hasParts && !message.content) {
|
|
109
109
|
const said = replyText(message);
|
|
110
110
|
return /* @__PURE__ */ jsxs("div", {
|
|
111
|
-
className: cx("group py-3 w-full", !flush && "px-4"),
|
|
111
|
+
className: cx("group py-3 w-full", !flush && "sm:px-4"),
|
|
112
112
|
children: [/* @__PURE__ */ jsx(AgentMessageParts, {
|
|
113
113
|
message: partsMessage,
|
|
114
114
|
isStreaming,
|
|
@@ -145,7 +145,7 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
145
145
|
const collapsed = !isUser && collapsible && !expanded && !isStreaming && !latest;
|
|
146
146
|
return /* @__PURE__ */ jsx("div", {
|
|
147
147
|
role: "none",
|
|
148
|
-
className: cx("group w-full", isUser ? "pt-8 pb-5" : "py-3", !flush && "px-4"),
|
|
148
|
+
className: cx("group w-full", isUser ? "pt-8 pb-5" : "py-3", !flush && "sm:px-4"),
|
|
149
149
|
children: isEditing ? /* @__PURE__ */ jsxs("div", {
|
|
150
150
|
className: "w-full",
|
|
151
151
|
children: [/* @__PURE__ */ jsx("textarea", {
|
|
@@ -283,7 +283,7 @@ function ToolCallBlock({ part, children }) {
|
|
|
283
283
|
}), /* @__PURE__ */ jsx(ResultBody, { part })]
|
|
284
284
|
}),
|
|
285
285
|
children && /* @__PURE__ */ jsx("div", {
|
|
286
|
-
className: "
|
|
286
|
+
className: "mt-1.5 min-w-0 sm:ml-5",
|
|
287
287
|
children
|
|
288
288
|
}),
|
|
289
289
|
!children && !isDiff && !expanded && part.status !== "waiting" && /* @__PURE__ */ jsx("div", {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
1
|
+
import { type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
2
2
|
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
3
3
|
export type AvatarStatus = 'online' | 'offline' | 'busy' | 'away';
|
|
4
4
|
/** Who an avatar stands for. An agent's avatar is a rounded square, so its shape, and not only
|
|
@@ -51,7 +51,9 @@ export declare function avatarInitials(name: string | undefined, { twoLetters }?
|
|
|
51
51
|
* `--avatar-person-text`) and agents `--avatar-agent` (with `--avatar-agent-text`, and the
|
|
52
52
|
* edge `--avatar-agent-ring`). */
|
|
53
53
|
export declare function Avatar({ src, name, kind, shape, colorKey, colorClassName, size: sizeProp, status, decorative, tooltip, className, }: AvatarProps): import("react").JSX.Element;
|
|
54
|
-
|
|
54
|
+
/** Any other span attribute and the ref reach the stack's element, so the group can be a
|
|
55
|
+
* Tooltip's or a popover's direct child. */
|
|
56
|
+
export interface AvatarGroupProps extends Omit<ComponentPropsWithRef<'span'>, 'children' | 'className'> {
|
|
55
57
|
/** Avatars, each possibly wrapped, such as in a hover card. Each child counts as one. */
|
|
56
58
|
children: ReactNode;
|
|
57
59
|
/** Shows at most this many; the rest become a "+N" chip. */
|
|
@@ -77,4 +79,4 @@ export interface AvatarGroupProps {
|
|
|
77
79
|
/** Overlapping avatars, such as a task's assignees or who is here, with a "+N" chip for the
|
|
78
80
|
* rest. Each avatar inside takes the group's size and a ring in the color behind it, which
|
|
79
81
|
* follows its shape, round or square. */
|
|
80
|
-
export declare function AvatarGroup({ children, max, total, showOverflow, size, ring, ringColor, label, className, }: AvatarGroupProps): import("react").JSX.Element;
|
|
82
|
+
export declare function AvatarGroup({ children, max, total, showOverflow, size, ring, ringColor, label, className, style, ...spanProps }: AvatarGroupProps): import("react").JSX.Element;
|
|
@@ -144,7 +144,7 @@ function formatNumber(n, locale) {
|
|
|
144
144
|
/** Overlapping avatars, such as a task's assignees or who is here, with a "+N" chip for the
|
|
145
145
|
* rest. Each avatar inside takes the group's size and a ring in the color behind it, which
|
|
146
146
|
* follows its shape, round or square. */
|
|
147
|
-
function AvatarGroup({ children, max, total, showOverflow = true, size = "sm", ring = true, ringColor, label, className }) {
|
|
147
|
+
function AvatarGroup({ children, max, total, showOverflow = true, size = "sm", ring = true, ringColor, label, className, style, ...spanProps }) {
|
|
148
148
|
const { common } = useStrings();
|
|
149
149
|
const locale = useLocale();
|
|
150
150
|
const items = Children.toArray(children);
|
|
@@ -157,11 +157,15 @@ function AvatarGroup({ children, max, total, showOverflow = true, size = "sm", r
|
|
|
157
157
|
return /* @__PURE__ */ jsx(AvatarGroupContext, {
|
|
158
158
|
value: context,
|
|
159
159
|
children: /* @__PURE__ */ jsxs("span", {
|
|
160
|
+
...spanProps,
|
|
160
161
|
...label ? {
|
|
161
162
|
role: "group",
|
|
162
163
|
"aria-label": label
|
|
163
164
|
} : {},
|
|
164
|
-
style: {
|
|
165
|
+
style: {
|
|
166
|
+
"--avatar-ring": ringColor ?? "var(--theme-panel-bg)",
|
|
167
|
+
...style
|
|
168
|
+
},
|
|
165
169
|
className: cx("inline-flex shrink-0 items-center", OVERLAP_CLASSES[size], className),
|
|
166
170
|
children: [shown, showOverflow && rest > 0 && /* @__PURE__ */ jsxs("span", {
|
|
167
171
|
className: cx("relative inline-flex shrink-0 items-center justify-center rounded-full bg-(--theme-muted-panel-bg) font-medium text-(--theme-muted-text-color) tabular-nums select-none", CHIP_SIZE_CLASSES[size], ring && "ring-2 ring-(--avatar-ring)"),
|
|
@@ -104,7 +104,7 @@ function CodeBlock({ code, language = "", title, label, filename, headerActions,
|
|
|
104
104
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
105
105
|
className: "flex h-9 shrink-0 items-center gap-2 pr-1.5 pl-4",
|
|
106
106
|
children: [/* @__PURE__ */ jsx("div", {
|
|
107
|
-
className: "min-w-0 flex-1 truncate text-xs text-(--theme-muted-text-color)",
|
|
107
|
+
className: cx("min-w-0 flex-1 truncate", title === void 0 ? "text-xs text-(--theme-muted-text-color)" : "text-[13px] font-medium text-(--theme-app)"),
|
|
108
108
|
children: heading
|
|
109
109
|
}), /* @__PURE__ */ jsxs("div", {
|
|
110
110
|
className: "flex shrink-0 items-center gap-0.5",
|
|
@@ -40,12 +40,19 @@ export interface CopyCodeBlockProps {
|
|
|
40
40
|
revealed?: boolean | undefined;
|
|
41
41
|
defaultRevealed?: boolean | undefined;
|
|
42
42
|
onRevealedChange?: ((revealed: boolean) => void) | undefined;
|
|
43
|
-
/** Breaks a long value across lines instead of scrolling it, such as a token.
|
|
43
|
+
/** Breaks a long value across lines instead of scrolling it, such as a token in a block.
|
|
44
|
+
* Default false: an inline token, key or ID stays on one line and scrolls inside. */
|
|
44
45
|
wrap?: boolean | undefined;
|
|
46
|
+
/** A compact value in the line, such as a token shown once in a Callout: as wide as the
|
|
47
|
+
* value, up to its container, where a longer one scrolls sideways on its one line. A quiet
|
|
48
|
+
* surface reads on a panel and a tinted callout alike, and the buttons stay right after the
|
|
49
|
+
* value, always shown. The copy confirms with its check alone, as the change shows there. */
|
|
50
|
+
inline?: boolean | undefined;
|
|
45
51
|
className?: string | undefined;
|
|
46
52
|
}
|
|
47
53
|
/** A block of code, such as a command to run, with a button that copies it. The
|
|
48
54
|
* buttons show while the pointer is over the block or focus is in it, and always
|
|
49
|
-
* where there is no hover to reveal them. With `
|
|
50
|
-
* such as a
|
|
51
|
-
|
|
55
|
+
* where there is no hover to reveal them. With `inline` it is a compact value in the line
|
|
56
|
+
* instead, such as a token shown once, its buttons always beside it. With `secret` it
|
|
57
|
+
* masks the value until the eye shows it, and copies it either way. */
|
|
58
|
+
export declare function CopyCodeBlock({ text, label, children, secret, revealed: revealedProp, defaultRevealed, onRevealedChange, inline, wrap, className, }: CopyCodeBlockProps): import("react").JSX.Element;
|
|
@@ -4,7 +4,7 @@ import { CheckIcon, CopyIcon, EyeIcon, EyeOffIcon } from "../icons.js";
|
|
|
4
4
|
import { useStrings } from "./Provider.js";
|
|
5
5
|
import { useCopy } from "./useCopy.js";
|
|
6
6
|
import { IconButton } from "./IconButton.js";
|
|
7
|
-
import { useState } from "react";
|
|
7
|
+
import { useEffectEvent, useLayoutEffect, useRef, useState } from "react";
|
|
8
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/components/CopyButton.tsx
|
|
10
10
|
/** The one copy control: a button that copies its text, turns into a check for a
|
|
@@ -40,12 +40,60 @@ function CopyButton({ text, label, quiet = false, showLabel = false, variant = s
|
|
|
40
40
|
onClick
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
+
var NO_ENDS = {
|
|
44
|
+
start: false,
|
|
45
|
+
end: false
|
|
46
|
+
};
|
|
47
|
+
function useHiddenEnds(active) {
|
|
48
|
+
const ref = useRef(null);
|
|
49
|
+
const [ends, setEnds] = useState(NO_ENDS);
|
|
50
|
+
const measureNow = () => {
|
|
51
|
+
const el = ref.current;
|
|
52
|
+
if (!el || !active) {
|
|
53
|
+
setEnds(NO_ENDS);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const scrolled = Math.abs(el.scrollLeft);
|
|
57
|
+
const start = scrolled > 1;
|
|
58
|
+
const end = scrolled + el.clientWidth < el.scrollWidth - 1;
|
|
59
|
+
setEnds((was) => was.start === start && was.end === end ? was : {
|
|
60
|
+
start,
|
|
61
|
+
end
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const measure = useEffectEvent(measureNow);
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
measure();
|
|
67
|
+
});
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
const el = ref.current;
|
|
70
|
+
if (!el || !active || typeof ResizeObserver === "undefined") return;
|
|
71
|
+
const observer = new ResizeObserver(() => measure());
|
|
72
|
+
observer.observe(el);
|
|
73
|
+
return () => observer.disconnect();
|
|
74
|
+
}, [active]);
|
|
75
|
+
return {
|
|
76
|
+
ref,
|
|
77
|
+
ends,
|
|
78
|
+
onScroll: measureNow
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
var FADE = "1.25rem";
|
|
82
|
+
function fadeMask({ start, end }) {
|
|
83
|
+
if (!start && !end) return;
|
|
84
|
+
const image = `linear-gradient(to var(--fade-end, right), ${start ? `transparent, #000 ${FADE}` : "#000"}, ${end ? `#000 calc(100% - ${FADE}), transparent` : "#000"})`;
|
|
85
|
+
return {
|
|
86
|
+
maskImage: image,
|
|
87
|
+
WebkitMaskImage: image
|
|
88
|
+
};
|
|
89
|
+
}
|
|
43
90
|
var MASK = "•".repeat(24);
|
|
44
91
|
/** A block of code, such as a command to run, with a button that copies it. The
|
|
45
92
|
* buttons show while the pointer is over the block or focus is in it, and always
|
|
46
|
-
* where there is no hover to reveal them. With `
|
|
47
|
-
* such as a
|
|
48
|
-
|
|
93
|
+
* where there is no hover to reveal them. With `inline` it is a compact value in the line
|
|
94
|
+
* instead, such as a token shown once, its buttons always beside it. With `secret` it
|
|
95
|
+
* masks the value until the eye shows it, and copies it either way. */
|
|
96
|
+
function CopyCodeBlock({ text, label, children, secret = false, revealed: revealedProp, defaultRevealed = false, onRevealedChange, inline = false, wrap = false, className }) {
|
|
49
97
|
const { common: t } = useStrings();
|
|
50
98
|
const [ownRevealed, setOwnRevealed] = useState(defaultRevealed);
|
|
51
99
|
const revealed = revealedProp ?? ownRevealed;
|
|
@@ -63,6 +111,39 @@ function CopyCodeBlock({ text, label, children, secret = false, revealed: reveal
|
|
|
63
111
|
className: "sr-only",
|
|
64
112
|
children: t.hiddenValue
|
|
65
113
|
})] });
|
|
114
|
+
const scroller = useHiddenEnds(inline && !wrap);
|
|
115
|
+
const overflows = scroller.ends.start || scroller.ends.end;
|
|
116
|
+
const revealButton = secret && /* @__PURE__ */ jsx(IconButton, {
|
|
117
|
+
label: t.showValue,
|
|
118
|
+
icon: revealed ? /* @__PURE__ */ jsx(EyeIcon, {}) : /* @__PURE__ */ jsx(EyeOffIcon, {}),
|
|
119
|
+
pressed: revealed,
|
|
120
|
+
onClick: () => setRevealed(!revealed),
|
|
121
|
+
size: inline ? "xs" : "sm",
|
|
122
|
+
className: inline ? void 0 : cx("bg-(--theme-muted-panel-bg)", reveal)
|
|
123
|
+
});
|
|
124
|
+
if (inline) return /* @__PURE__ */ jsxs("span", {
|
|
125
|
+
className: cx("inline-flex max-w-full items-start gap-1 rounded-md border border-current/15 bg-(--theme-panel-bg)/70 py-px ps-2 pe-px align-middle", "has-[code:focus-visible]:outline-2 has-[code:focus-visible]:outline-offset-1 has-[code:focus-visible]:outline-(--theme-focus)", className),
|
|
126
|
+
children: [/* @__PURE__ */ jsx("code", {
|
|
127
|
+
ref: scroller.ref,
|
|
128
|
+
onScroll: scroller.onScroll,
|
|
129
|
+
...overflows ? {
|
|
130
|
+
tabIndex: 0,
|
|
131
|
+
role: "region",
|
|
132
|
+
"aria-label": label ?? t.codeOf("")
|
|
133
|
+
} : {},
|
|
134
|
+
style: fadeMask(scroller.ends),
|
|
135
|
+
className: cx("min-w-0 py-[3px] font-mono text-[0.8125rem] leading-[18px] text-(--theme-app)", "outline-none", "rtl:[--fade-end:left]", wrap ? "break-all whitespace-pre-wrap" : "overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"),
|
|
136
|
+
children: shown
|
|
137
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
138
|
+
className: "flex shrink-0 items-center",
|
|
139
|
+
children: [/* @__PURE__ */ jsx(CopyButton, {
|
|
140
|
+
text,
|
|
141
|
+
label,
|
|
142
|
+
size: "xs",
|
|
143
|
+
quiet: true
|
|
144
|
+
}), revealButton]
|
|
145
|
+
})]
|
|
146
|
+
});
|
|
66
147
|
return /* @__PURE__ */ jsxs("div", {
|
|
67
148
|
className: cx("group/code relative", className),
|
|
68
149
|
children: [/* @__PURE__ */ jsx("pre", {
|
|
@@ -70,13 +151,7 @@ function CopyCodeBlock({ text, label, children, secret = false, revealed: reveal
|
|
|
70
151
|
children: shown
|
|
71
152
|
}), /* @__PURE__ */ jsxs("div", {
|
|
72
153
|
className: "absolute top-2 right-2 flex items-center gap-1",
|
|
73
|
-
children: [
|
|
74
|
-
label: t.showValue,
|
|
75
|
-
icon: revealed ? /* @__PURE__ */ jsx(EyeIcon, {}) : /* @__PURE__ */ jsx(EyeOffIcon, {}),
|
|
76
|
-
pressed: revealed,
|
|
77
|
-
onClick: () => setRevealed(!revealed),
|
|
78
|
-
className: cx("bg-(--theme-muted-panel-bg)", reveal)
|
|
79
|
-
}), /* @__PURE__ */ jsx(CopyButton, {
|
|
154
|
+
children: [revealButton, /* @__PURE__ */ jsx(CopyButton, {
|
|
80
155
|
text,
|
|
81
156
|
label,
|
|
82
157
|
className: cx("bg-(--theme-muted-panel-bg)", reveal)
|
|
@@ -12,7 +12,7 @@ import { Combobox } from "./Combobox.js";
|
|
|
12
12
|
import { Modal } from "./Modal.js";
|
|
13
13
|
import { Disclosure } from "./Disclosure.js";
|
|
14
14
|
import { Switch } from "./Switch.js";
|
|
15
|
-
import { useId, useRef, useState } from "react";
|
|
15
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
16
16
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
17
17
|
import { Toggle } from "@base-ui/react/toggle";
|
|
18
18
|
//#region src/components/CreateModal.tsx
|
|
@@ -50,7 +50,17 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
|
|
|
50
50
|
const hint = !nameError && name.trim() ? nameHint?.(name.trim()) : null;
|
|
51
51
|
const canSubmit = (hideName || nameOptional || name.trim().length > 0) && !nameError && !submitting && !submitDisabled;
|
|
52
52
|
const isDirty = !submitting && (interacted || name !== (initialName ?? "") || description !== "" || favorite || dirty);
|
|
53
|
-
const nameMissing = !hideName && !nameOptional && (!name.trim() || !!nameError) &&
|
|
53
|
+
const nameMissing = !hideName && !nameOptional && (!name.trim() || !!nameError) && nameTouched;
|
|
54
|
+
const focusInside = useRef(false);
|
|
55
|
+
const leaveCheck = useRef(void 0);
|
|
56
|
+
useEffect(() => () => clearTimeout(leaveCheck.current), []);
|
|
57
|
+
const checkLeft = () => {
|
|
58
|
+
focusInside.current = false;
|
|
59
|
+
clearTimeout(leaveCheck.current);
|
|
60
|
+
leaveCheck.current = setTimeout(() => {
|
|
61
|
+
if (!focusInside.current && document.hasFocus() && !nameRef.current?.value.trim()) setNameTouched(true);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
54
64
|
const flashName = () => {
|
|
55
65
|
setNameTouched(true);
|
|
56
66
|
nameRef.current?.focus();
|
|
@@ -80,6 +90,9 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
|
|
|
80
90
|
size,
|
|
81
91
|
dismissible: !isDirty,
|
|
82
92
|
initialFocus: () => nameRef.current ?? true,
|
|
93
|
+
onFocus: () => {
|
|
94
|
+
focusInside.current = true;
|
|
95
|
+
},
|
|
83
96
|
onKeyDown: (e) => {
|
|
84
97
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleSubmit(true);
|
|
85
98
|
},
|
|
@@ -160,7 +173,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, typeMenuSea
|
|
|
160
173
|
resizeName?.();
|
|
161
174
|
},
|
|
162
175
|
onBlur: (e) => {
|
|
163
|
-
if (!e.target.value.trim())
|
|
176
|
+
if (!e.target.value.trim()) checkLeft();
|
|
164
177
|
},
|
|
165
178
|
onKeyDown: (e) => {
|
|
166
179
|
if (e.key === "Enter" && !e.nativeEvent.isComposing && !e.metaKey && !e.ctrlKey) {
|
|
@@ -6,7 +6,9 @@ export interface ListboxNavigationOptions<T> {
|
|
|
6
6
|
onChoose: (item: T, index: number) => void;
|
|
7
7
|
/** Called on Escape, such as to close the list until the text changes. */
|
|
8
8
|
onDismiss?: (() => void) | undefined;
|
|
9
|
-
/** False while the list isn't shown: the keys are then the host's own.
|
|
9
|
+
/** False while the list isn't shown: the keys are then the host's own. A list that hides
|
|
10
|
+
* itself while empty passes false then too, or Escape dismisses what isn't there. Default
|
|
11
|
+
* true. */
|
|
10
12
|
open?: boolean | undefined;
|
|
11
13
|
/** Past the end comes the start, and before the start the end. Default true. */
|
|
12
14
|
loop?: boolean | undefined;
|
|
@@ -91,10 +93,17 @@ export interface ListboxOptionProps {
|
|
|
91
93
|
inline?: boolean | undefined;
|
|
92
94
|
/** Short muted text at the row's end, such as a key. */
|
|
93
95
|
hint?: ReactNode;
|
|
96
|
+
/** In a list of choices that stay chosen, such as the labels on an issue: true shows the
|
|
97
|
+
* check at the row's end and tells assistive technology, false leaves it unchecked. Leave
|
|
98
|
+
* it out for a list that only acts. */
|
|
99
|
+
checked?: boolean | undefined;
|
|
100
|
+
/** The option opens more choices, such as a submenu or a next step: a chevron ends the row. */
|
|
101
|
+
submenu?: boolean | undefined;
|
|
94
102
|
className?: string | undefined;
|
|
95
103
|
}
|
|
96
|
-
/** One option of a ListboxPanel: an icon, its label and description, and
|
|
97
|
-
|
|
104
|
+
/** One option of a ListboxPanel: an icon, its label and description, and at its end a hint, a
|
|
105
|
+
* check while it is chosen and a chevron when it opens more. */
|
|
106
|
+
export declare function ListboxOption({ option, icon, label, description, inline, hint, checked, submenu, className, }: ListboxOptionProps): import("react").JSX.Element;
|
|
98
107
|
/** Options under a heading in a ListboxPanel. */
|
|
99
108
|
export declare function ListboxGroup({ label, children }: {
|
|
100
109
|
label: ReactNode;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
|
+
import { CheckIcon, ChevronRightIcon } from "../icons.js";
|
|
2
3
|
import { panelClassName, panelItemClassName } from "./floating.js";
|
|
3
4
|
import { Popover } from "./Popover.js";
|
|
4
5
|
import "./comboboxItems.js";
|
|
5
|
-
import { useEffect, useId, useState } from "react";
|
|
6
|
+
import { Children, useEffect, useId, useState } from "react";
|
|
6
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/components/Listbox.tsx
|
|
8
9
|
/** The parts a highlighted list needs while focus stays on the box the reader types in, such
|
|
@@ -44,7 +45,14 @@ function useListboxNavigation({ items, onChoose, onDismiss, open = true, loop =
|
|
|
44
45
|
/** Handles a key the list owns, and says whether it did; leave the rest to the host. */
|
|
45
46
|
const handleKey = (event) => {
|
|
46
47
|
const composing = "nativeEvent" in event ? event.nativeEvent.isComposing : event.isComposing;
|
|
47
|
-
if (!
|
|
48
|
+
if (!open || composing) return false;
|
|
49
|
+
if (event.key === "Escape") {
|
|
50
|
+
if (!onDismiss) return false;
|
|
51
|
+
onDismiss();
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (!shownOpen) return false;
|
|
48
56
|
const ctrlOnly = event.ctrlKey && !event.metaKey && !event.altKey;
|
|
49
57
|
switch (true) {
|
|
50
58
|
case event.key === "ArrowDown" || ctrlOnly && event.key === "n":
|
|
@@ -59,10 +67,6 @@ function useListboxNavigation({ items, onChoose, onDismiss, open = true, loop =
|
|
|
59
67
|
case homeEnd && event.key === "End":
|
|
60
68
|
setActive(step(count, -1));
|
|
61
69
|
break;
|
|
62
|
-
case event.key === "Escape":
|
|
63
|
-
if (!onDismiss) return false;
|
|
64
|
-
onDismiss();
|
|
65
|
-
break;
|
|
66
70
|
case event.key === "Enter" || chooseKeys.includes(event.key):
|
|
67
71
|
if (event.shiftKey || event.altKey || event.metaKey || event.ctrlKey) return false;
|
|
68
72
|
choose(shown);
|
|
@@ -110,7 +114,7 @@ function useListboxNavigation({ items, onChoose, onDismiss, open = true, loop =
|
|
|
110
114
|
}
|
|
111
115
|
/** The panel of a highlighted list that the reader drives from a box that keeps focus. */
|
|
112
116
|
function ListboxPanel({ listProps, "aria-label": label, footer, empty, at, onDismiss, className, children }) {
|
|
113
|
-
const hasOptions =
|
|
117
|
+
const hasOptions = Children.toArray(children).length > 0;
|
|
114
118
|
const body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
115
119
|
id: listProps.id,
|
|
116
120
|
role: "listbox",
|
|
@@ -143,11 +147,17 @@ function ListboxPanel({ listProps, "aria-label": label, footer, empty, at, onDis
|
|
|
143
147
|
children: body
|
|
144
148
|
});
|
|
145
149
|
}
|
|
146
|
-
/** One option of a ListboxPanel: an icon, its label and description, and
|
|
147
|
-
|
|
150
|
+
/** One option of a ListboxPanel: an icon, its label and description, and at its end a hint, a
|
|
151
|
+
* check while it is chosen and a chevron when it opens more. */
|
|
152
|
+
function ListboxOption({ option, icon, label, description, inline = false, hint, checked, submenu = false, className }) {
|
|
153
|
+
const checkable = checked !== void 0;
|
|
148
154
|
return /* @__PURE__ */ jsxs("div", {
|
|
149
155
|
...option,
|
|
150
|
-
|
|
156
|
+
role: "option",
|
|
157
|
+
tabIndex: -1,
|
|
158
|
+
"aria-checked": checked,
|
|
159
|
+
"data-checked": checked || void 0,
|
|
160
|
+
className: cx(panelItemClassName, inline && "items-baseline", checked && "font-medium", className),
|
|
151
161
|
children: [
|
|
152
162
|
icon && /* @__PURE__ */ jsx("span", {
|
|
153
163
|
className: "flex shrink-0 items-center self-center",
|
|
@@ -163,12 +173,25 @@ function ListboxOption({ option, icon, label, description, inline = false, hint,
|
|
|
163
173
|
children: description
|
|
164
174
|
})]
|
|
165
175
|
}),
|
|
166
|
-
hint && /* @__PURE__ */
|
|
176
|
+
(hint || checkable || submenu) && /* @__PURE__ */ jsxs("span", {
|
|
167
177
|
className: "ml-auto flex h-[1lh] shrink-0 items-center gap-2 self-start pl-2",
|
|
168
|
-
children:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
children: [
|
|
179
|
+
hint && /* @__PURE__ */ jsx("span", {
|
|
180
|
+
className: "text-xs tabular-nums text-(--theme-muted-text-color)",
|
|
181
|
+
children: hint
|
|
182
|
+
}),
|
|
183
|
+
checkable && (checked || hint) && /* @__PURE__ */ jsx("span", {
|
|
184
|
+
className: cx("flex items-center", hint && "w-3.5"),
|
|
185
|
+
children: checked && /* @__PURE__ */ jsx(CheckIcon, {
|
|
186
|
+
"aria-hidden": "true",
|
|
187
|
+
className: "size-3.5"
|
|
188
|
+
})
|
|
189
|
+
}),
|
|
190
|
+
submenu && /* @__PURE__ */ jsx(ChevronRightIcon, {
|
|
191
|
+
"aria-hidden": "true",
|
|
192
|
+
className: "size-3 opacity-60"
|
|
193
|
+
})
|
|
194
|
+
]
|
|
172
195
|
})
|
|
173
196
|
]
|
|
174
197
|
});
|
|
@@ -33,7 +33,9 @@ export interface PageHeaderProps {
|
|
|
33
33
|
* toolbar. Under a `PageHeaderProvider` it renders nothing in place: the
|
|
34
34
|
* shell's `PageHeaderBar` shows it, while the actions stay in the page's React
|
|
35
35
|
* tree, with its context and state. Without a provider it renders the bar
|
|
36
|
-
* itself, for a page with no shell
|
|
36
|
+
* itself, for a page with no shell, with no fill of its own, so it shows the
|
|
37
|
+
* surface it sits on. A host that pins it gives its pinned wrapper the page's
|
|
38
|
+
* surface, so content scrolling under it is covered.
|
|
37
39
|
*
|
|
38
40
|
* Any component may render one, such as a form adding its Save button: the bar
|
|
39
41
|
* shows the actions of all of them, outermost first, and the trail of the last
|
|
@@ -42,7 +42,9 @@ function createRegionNodes() {
|
|
|
42
42
|
* toolbar. Under a `PageHeaderProvider` it renders nothing in place: the
|
|
43
43
|
* shell's `PageHeaderBar` shows it, while the actions stay in the page's React
|
|
44
44
|
* tree, with its context and state. Without a provider it renders the bar
|
|
45
|
-
* itself, for a page with no shell
|
|
45
|
+
* itself, for a page with no shell, with no fill of its own, so it shows the
|
|
46
|
+
* surface it sits on. A host that pins it gives its pinned wrapper the page's
|
|
47
|
+
* surface, so content scrolling under it is covered.
|
|
46
48
|
*
|
|
47
49
|
* Any component may render one, such as a form adding its Save button: the bar
|
|
48
50
|
* shows the actions of all of them, outermost first, and the trail of the last
|
|
@@ -51,6 +53,7 @@ function createRegionNodes() {
|
|
|
51
53
|
function PageHeader(props) {
|
|
52
54
|
const store = use(StoreContext);
|
|
53
55
|
if (!store) return props.hidden ? null : /* @__PURE__ */ jsx(HeaderLayout, {
|
|
56
|
+
inPlace: true,
|
|
54
57
|
breadcrumbs: props.breadcrumbs ?? [],
|
|
55
58
|
heading: props.heading ?? true,
|
|
56
59
|
titleActions: props.titleActions,
|
|
@@ -140,7 +143,7 @@ function Mount({ node }) {
|
|
|
140
143
|
className: "contents"
|
|
141
144
|
});
|
|
142
145
|
}
|
|
143
|
-
function HeaderLayout({ breadcrumbs, heading, titleActions, actions, toolbar, fit: sharedFit, compact = false, className }) {
|
|
146
|
+
function HeaderLayout({ breadcrumbs, heading, titleActions, actions, toolbar, fit: sharedFit, compact = false, inPlace = false, className }) {
|
|
144
147
|
const { breadcrumbPin, navToggle } = useSlots();
|
|
145
148
|
const { list: t } = useStrings();
|
|
146
149
|
const [ownFit] = useState(() => new HeaderActionsFit());
|
|
@@ -153,7 +156,7 @@ function HeaderLayout({ breadcrumbs, heading, titleActions, actions, toolbar, fi
|
|
|
153
156
|
const moved = fit.menu();
|
|
154
157
|
const hasTrail = breadcrumbs.length > 0;
|
|
155
158
|
return /* @__PURE__ */ jsxs("div", {
|
|
156
|
-
className: cx("@container w-full shrink-0 bg-(--theme-app-bg)", className),
|
|
159
|
+
className: cx("@container w-full shrink-0", !inPlace && "bg-(--theme-app-bg)", className),
|
|
157
160
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
158
161
|
ref: setRow,
|
|
159
162
|
className: cx("flex items-center justify-between gap-4 border-b border-(--theme-border) px-4", compact ? "h-8" : "h-12"),
|
|
@@ -17,7 +17,9 @@ export interface SettingsCardProps {
|
|
|
17
17
|
children: ReactNode;
|
|
18
18
|
}
|
|
19
19
|
/** A page's settings, grouped: a titled panel of SettingRows, each ruled off from
|
|
20
|
-
* the next. Cards stack with room between them.
|
|
20
|
+
* the next. Cards stack with room between them. An error about one setting, such as its
|
|
21
|
+
* Save failing, is its row's `error`; one about the whole card goes outside it, such as
|
|
22
|
+
* an ErrorText under the card, since every child of the card is ruled off as a row. */
|
|
21
23
|
export declare function SettingsCard({ title, description, action, actionAlign, headingLevel, overflowVisible, className, children, }: SettingsCardProps): import("react").JSX.Element;
|
|
22
24
|
/** A heading between groups of rows inside a SettingsCard, a level below the card's title. */
|
|
23
25
|
export declare function SettingsHeading({ children }: {
|
|
@@ -25,7 +27,7 @@ export declare function SettingsHeading({ children }: {
|
|
|
25
27
|
}): import("react").JSX.Element;
|
|
26
28
|
/** One setting in a SettingsCard: its name and help on the left, its control on
|
|
27
29
|
* the right, or under the name across the row with `fill`. */
|
|
28
|
-
export declare function SettingRow({ label, htmlFor, helpText, fill, onClick, children, }: {
|
|
30
|
+
export declare function SettingRow({ label, htmlFor, helpText, fill, onClick, error, children, }: {
|
|
29
31
|
label: string;
|
|
30
32
|
/** The control's id, so the name labels it. */
|
|
31
33
|
htmlFor?: string | undefined;
|
|
@@ -35,5 +37,8 @@ export declare function SettingRow({ label, htmlFor, helpText, fill, onClick, ch
|
|
|
35
37
|
/** Makes the whole row press its control, such as a switch. The control still
|
|
36
38
|
* handles its own press; stop it propagating there so it doesn't toggle twice. */
|
|
37
39
|
onClick?: (() => void) | undefined;
|
|
40
|
+
/** An error about this setting, such as its Save failing: under the control, at its start,
|
|
41
|
+
* announced, and describing the control that `htmlFor` names. */
|
|
42
|
+
error?: ReactNode;
|
|
38
43
|
children: ReactNode;
|
|
39
44
|
}): import("react").JSX.Element;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ErrorText } from "./Field.js";
|
|
3
|
+
import { createContext, useContext, useId, useLayoutEffect } from "react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/components/SettingsCard.tsx
|
|
5
6
|
var HeadingLevel = createContext(3);
|
|
6
7
|
/** A page's settings, grouped: a titled panel of SettingRows, each ruled off from
|
|
7
|
-
* the next. Cards stack with room between them.
|
|
8
|
+
* the next. Cards stack with room between them. An error about one setting, such as its
|
|
9
|
+
* Save failing, is its row's `error`; one about the whole card goes outside it, such as
|
|
10
|
+
* an ErrorText under the card, since every child of the card is ruled off as a row. */
|
|
8
11
|
function SettingsCard({ title, description, action, actionAlign = "beside", headingLevel = 2, overflowVisible = false, className, children }) {
|
|
9
12
|
const Heading = `h${headingLevel}`;
|
|
10
13
|
return /* @__PURE__ */ jsx(HeadingLevel, {
|
|
@@ -40,7 +43,34 @@ function SettingsHeading({ children }) {
|
|
|
40
43
|
}
|
|
41
44
|
/** One setting in a SettingsCard: its name and help on the left, its control on
|
|
42
45
|
* the right, or under the name across the row with `fill`. */
|
|
43
|
-
function SettingRow({ label, htmlFor, helpText, fill = false, onClick, children }) {
|
|
46
|
+
function SettingRow({ label, htmlFor, helpText, fill = false, onClick, error, children }) {
|
|
47
|
+
const errorId = useId();
|
|
48
|
+
const hasError = error !== void 0 && error !== null && error !== false && error !== "";
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
const control = hasError && htmlFor ? document.getElementById(htmlFor) : null;
|
|
51
|
+
if (!control) return;
|
|
52
|
+
const ids = (control.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean);
|
|
53
|
+
if (!ids.includes(errorId)) control.setAttribute("aria-describedby", [...ids, errorId].join(" "));
|
|
54
|
+
return () => {
|
|
55
|
+
const rest = (control.getAttribute("aria-describedby") ?? "").split(/\s+/).filter((id) => id && id !== errorId);
|
|
56
|
+
if (rest.length > 0) control.setAttribute("aria-describedby", rest.join(" "));
|
|
57
|
+
else control.removeAttribute("aria-describedby");
|
|
58
|
+
};
|
|
59
|
+
}, [
|
|
60
|
+
hasError,
|
|
61
|
+
htmlFor,
|
|
62
|
+
errorId
|
|
63
|
+
]);
|
|
64
|
+
const control = /* @__PURE__ */ jsx("div", {
|
|
65
|
+
className: cx("min-w-0", !fill && [
|
|
66
|
+
"flex items-center justify-end",
|
|
67
|
+
"max-md:has-[input:not([type=checkbox],[type=radio],[type=hidden],[type=range]),textarea,[role=combobox]]:justify-start",
|
|
68
|
+
"max-md:has-[input:not([type=checkbox],[type=radio],[type=hidden],[type=range]),textarea,[role=combobox]]:*:grow",
|
|
69
|
+
"max-md:[&_:is(input:not([type=checkbox],[type=radio],[type=hidden],[type=range]),textarea,[role=combobox])]:min-w-0",
|
|
70
|
+
"max-md:[&_:is(input:not([type=checkbox],[type=radio],[type=hidden],[type=range]),textarea,[role=combobox])]:flex-1"
|
|
71
|
+
]),
|
|
72
|
+
children
|
|
73
|
+
});
|
|
44
74
|
return /* @__PURE__ */ jsxs("div", {
|
|
45
75
|
role: "none",
|
|
46
76
|
onClick,
|
|
@@ -55,10 +85,15 @@ function SettingRow({ label, htmlFor, helpText, fill = false, onClick, children
|
|
|
55
85
|
className: "text-xs leading-snug theme-muted-text",
|
|
56
86
|
children: helpText
|
|
57
87
|
})]
|
|
58
|
-
}), /* @__PURE__ */
|
|
59
|
-
className:
|
|
60
|
-
children
|
|
61
|
-
|
|
88
|
+
}), hasError ? /* @__PURE__ */ jsxs("div", {
|
|
89
|
+
className: "flex min-w-0 flex-col gap-y-2",
|
|
90
|
+
children: [control, /* @__PURE__ */ jsx(ErrorText, {
|
|
91
|
+
id: errorId,
|
|
92
|
+
size: "md",
|
|
93
|
+
className: "md:w-0 md:min-w-full",
|
|
94
|
+
children: error
|
|
95
|
+
})]
|
|
96
|
+
}) : control]
|
|
62
97
|
});
|
|
63
98
|
}
|
|
64
99
|
//#endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { positionerClassName, tipClassName } from "./floating.js";
|
|
2
2
|
import { usePopupContainer } from "./popupContainer.js";
|
|
3
3
|
import { Kbd } from "./Kbd.js";
|
|
4
|
-
import { cloneElement, createContext, use, useId, useState } from "react";
|
|
4
|
+
import { cloneElement, createContext, use, useEffect, useId, useState } from "react";
|
|
5
5
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { Tooltip } from "@base-ui/react/tooltip";
|
|
7
7
|
//#region src/components/Tooltip.tsx
|
|
@@ -52,12 +52,44 @@ function skipUnclipped(open, details) {
|
|
|
52
52
|
const anchor = details.trigger;
|
|
53
53
|
if (open && anchor instanceof HTMLElement && anchor.hasAttribute("data-overflow-only") && anchor.scrollWidth <= anchor.clientWidth) details.cancel();
|
|
54
54
|
}
|
|
55
|
+
/** True from when the anchor's own popup, such as its menu, closes until the pointer leaves the
|
|
56
|
+
* anchor or focus does. Closing the popup hands focus back to the anchor, and a tip opening
|
|
57
|
+
* then would take the next Escape, meant for the dialog around it, and repeat a label the
|
|
58
|
+
* reader has just seen. */
|
|
59
|
+
function useRestAfterPopup(anchor) {
|
|
60
|
+
const [resting, setResting] = useState(false);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!anchor || typeof MutationObserver === "undefined") return;
|
|
63
|
+
let expanded = anchor.getAttribute("aria-expanded") === "true";
|
|
64
|
+
const observer = new MutationObserver(() => {
|
|
65
|
+
const now = anchor.getAttribute("aria-expanded") === "true";
|
|
66
|
+
if (expanded && !now) setResting(true);
|
|
67
|
+
else if (now) setResting(false);
|
|
68
|
+
expanded = now;
|
|
69
|
+
});
|
|
70
|
+
observer.observe(anchor, {
|
|
71
|
+
attributes: true,
|
|
72
|
+
attributeFilter: ["aria-expanded"]
|
|
73
|
+
});
|
|
74
|
+
const wake = () => setResting(false);
|
|
75
|
+
anchor.addEventListener("blur", wake);
|
|
76
|
+
anchor.addEventListener("pointerleave", wake);
|
|
77
|
+
return () => {
|
|
78
|
+
observer.disconnect();
|
|
79
|
+
anchor.removeEventListener("blur", wake);
|
|
80
|
+
anchor.removeEventListener("pointerleave", wake);
|
|
81
|
+
};
|
|
82
|
+
}, [anchor]);
|
|
83
|
+
return resting;
|
|
84
|
+
}
|
|
55
85
|
/** A visual hint for sighted pointer and keyboard users. The anchor still needs
|
|
56
86
|
* its own accessible name; `describesAnchor` also gives assistive technology the
|
|
57
87
|
* hint. Use InfoTip for content everyone must reach, such as on a touch screen. */
|
|
58
88
|
function Tooltip$1({ content, children, side = "top", shortcut, disabled = false, overflowOnly = false, describesAnchor = false, ...anchorProps }) {
|
|
59
89
|
const handle = use(TooltipHandleContext);
|
|
60
90
|
const descriptionId = useId();
|
|
91
|
+
const [anchorElement, setAnchorElement] = useState(null);
|
|
92
|
+
const resting = useRestAfterPopup(anchorElement);
|
|
61
93
|
const payload = {
|
|
62
94
|
content,
|
|
63
95
|
shortcut,
|
|
@@ -75,7 +107,8 @@ function Tooltip$1({ content, children, side = "top", shortcut, disabled = false
|
|
|
75
107
|
...!handle && anchorProps.delay === void 0 && { delay: OPEN_DELAY },
|
|
76
108
|
handle: handle ?? void 0,
|
|
77
109
|
payload,
|
|
78
|
-
|
|
110
|
+
ref: setAnchorElement,
|
|
111
|
+
disabled: disabled || !content || resting,
|
|
79
112
|
"data-overflow-only": overflowOnly || void 0,
|
|
80
113
|
render: described ? cloneElement(anchor, { "aria-describedby": describedBy }) : children
|
|
81
114
|
});
|