@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.
@@ -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: "ml-5 mt-1.5 min-w-0",
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
- export interface AvatarGroupProps {
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: { "--avatar-ring": ringColor ?? "var(--theme-panel-bg)" },
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 `secret` it holds a value shown once,
50
- * such as a new API key: masked until the eye shows it, and copied either way. */
51
- export declare function CopyCodeBlock({ text, label, children, secret, revealed: revealedProp, defaultRevealed, onRevealedChange, wrap, className, }: CopyCodeBlockProps): import("react").JSX.Element;
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 `secret` it holds a value shown once,
47
- * such as a new API key: masked until the eye shows it, and copied either way. */
48
- function CopyCodeBlock({ text, label, children, secret = false, revealed: revealedProp, defaultRevealed = false, onRevealedChange, wrap = false, className }) {
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: [secret && /* @__PURE__ */ jsx(IconButton, {
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) && (nameTouched || description !== "" || favorite || dirty);
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()) setNameTouched(true);
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. Default true. */
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 a hint at its end. */
97
- export declare function ListboxOption({ option, icon, label, description, inline, hint, className, }: ListboxOptionProps): import("react").JSX.Element;
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 (!shownOpen || composing) return false;
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 = Array.isArray(children) ? children.some(Boolean) : Boolean(children);
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 a hint at its end. */
147
- function ListboxOption({ option, icon, label, description, inline = false, hint, className }) {
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
- className: cx(panelItemClassName, inline && "items-baseline", className),
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__ */ jsx("span", {
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: /* @__PURE__ */ jsx("span", {
169
- className: "text-xs tabular-nums text-(--theme-muted-text-color)",
170
- children: hint
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 { createContext, useContext } from "react";
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__ */ jsx("div", {
59
- className: cx("min-w-0", !fill && "flex items-center justify-end"),
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
- disabled: disabled || !content,
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
  });