@candor-ui/react 2.3.0 → 2.5.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.
Files changed (52) hide show
  1. package/README.md +3 -3
  2. package/dist/blocks/copy-field.js +4 -1
  3. package/dist/blocks/degraded-panel.js +4 -0
  4. package/dist/blocks/locked.js +29 -3
  5. package/dist/blocks/relative-time.js +1 -1
  6. package/dist/blocks/status-dot.js +9 -2
  7. package/dist/blocks/step-list.js +16 -3
  8. package/dist/blocks/streaming-text.js +4 -0
  9. package/dist/chat/attachment-preview.d.ts +6 -1
  10. package/dist/chat/attachment-preview.js +5 -2
  11. package/dist/chat/card-registry.js +7 -3
  12. package/dist/chat/citation.d.ts +3 -0
  13. package/dist/chat/citation.js +5 -2
  14. package/dist/chat/composer.js +17 -4
  15. package/dist/chat/context-meter.d.ts +5 -0
  16. package/dist/chat/context-meter.js +13 -2
  17. package/dist/chat/entity-chip.d.ts +6 -1
  18. package/dist/chat/entity-chip.js +5 -2
  19. package/dist/chat/folded-turns.js +4 -0
  20. package/dist/chat/message-actions.js +4 -1
  21. package/dist/chat/message.js +8 -1
  22. package/dist/chat/payload.d.ts +3 -0
  23. package/dist/chat/payload.js +3 -0
  24. package/dist/chat/permission-prompt.d.ts +5 -0
  25. package/dist/chat/permission-prompt.js +31 -2
  26. package/dist/chat/prompt-suggestions.js +4 -1
  27. package/dist/chat/reasoning.js +8 -0
  28. package/dist/chat/thread.js +32 -5
  29. package/dist/chat/tool-call.js +8 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +1 -0
  32. package/dist/lib/motion.d.ts +16 -1
  33. package/dist/lib/motion.js +33 -3
  34. package/dist/lib/sound.d.ts +44 -0
  35. package/dist/lib/sound.js +63 -0
  36. package/dist/styles.css +19 -1
  37. package/dist/ui/accordion.d.ts +2 -2
  38. package/dist/ui/accordion.js +2 -2
  39. package/dist/ui/alert.d.ts +2 -1
  40. package/dist/ui/alert.js +17 -10
  41. package/dist/ui/button.js +4 -0
  42. package/dist/ui/collapsible.js +5 -0
  43. package/dist/ui/popover.js +4 -0
  44. package/dist/ui/progress.d.ts +1 -1
  45. package/dist/ui/progress.js +1 -1
  46. package/dist/ui/skeleton.js +4 -1
  47. package/dist/ui/slider.d.ts +1 -1
  48. package/dist/ui/slider.js +1 -1
  49. package/dist/ui/switch.d.ts +1 -1
  50. package/dist/ui/switch.js +1 -1
  51. package/dist/ui/tooltip.js +4 -0
  52. package/package.json +2 -3
@@ -7,6 +7,16 @@ import { useStrings } from "../blocks/locale.js";
7
7
  import { Payload } from "./payload.js";
8
8
  import { Button } from "../ui/button.js";
9
9
  import { motion } from "../lib/motion.js";
10
+ /** How long the animations running on `el` take in all, in milliseconds (the longest of duration × iteration count, capped at 6 s); undefined when it can't be read. */
11
+ function animationMs(el) {
12
+ if (!el || typeof getComputedStyle === "undefined")
13
+ return undefined;
14
+ const style = getComputedStyle(el);
15
+ const durations = style.animationDuration.split(",").map((t) => (t.trim().endsWith("ms") ? Number.parseFloat(t) : Number.parseFloat(t) * 1000));
16
+ const counts = style.animationIterationCount.split(",").map((c) => (c.trim() === "infinite" ? Number.POSITIVE_INFINITY : Number.parseFloat(c)));
17
+ const totals = durations.map((d, i) => d * (counts[i % counts.length] || 1)).filter((n) => Number.isFinite(n) || n === Number.POSITIVE_INFINITY);
18
+ return totals.length ? Math.min(6000, Math.max(...totals)) : undefined;
19
+ }
10
20
  /** Three pulses of 1.6 s while the decision waits (4.8 s, then still), shortened from 2.2's 7.2 s. */
11
21
  const waitingMotion = motion("permission-prompt", "waiting", "attention", { dur: 1600, amp: "3" });
12
22
  /** A press around the prompt instead of on it: one ring, and the refuse shake under expressive. */
@@ -88,6 +98,11 @@ function parseObject(text) {
88
98
  * nothing else is ever counted as a denial. While the decision waits, its shield pulses slowly; a press on inert
89
99
  * space outside it (the gesture that would dismiss a dialog) nudges it instead, so the person sees it is still
90
100
  * waiting. Both stop once the answer is being sent.
101
+ *
102
+ * @motion attention:waiting While the decision waits, a ring pulses out from the shield three times and then is still.
103
+ * It stops once the answer is being sent, and under calm it is one ring that fades.
104
+ * @motion attention:nudge A press on inert space outside the prompt rings it once, so the person sees it is still waiting.
105
+ * @motion refuse:nudge Under expressive the same press also shakes the prompt a little; nothing is dismissed.
91
106
  */
92
107
  function PermissionPrompt({ request, mode, suggestions, editable = false, onAllow, onDeny, busy = false, className, }) {
93
108
  // Keyed by the request, so an edit, a ticked rule or an answer never carries over to the next request.
@@ -116,6 +131,7 @@ function PromptBody({ request, mode, suggestions, editable, onAllow, onDeny, bus
116
131
  // A press on inert space outside every prompt, while it waits, nudges it rather than dismissing anything.
117
132
  const section = React.useRef(null);
118
133
  const [nudged, setNudged] = React.useState(false);
134
+ const ended = React.useRef(new Set());
119
135
  React.useEffect(() => {
120
136
  if (busy)
121
137
  return;
@@ -136,7 +152,11 @@ function PromptBody({ request, mode, suggestions, editable, onAllow, onDeny, bus
136
152
  React.useEffect(() => {
137
153
  if (!nudged)
138
154
  return;
139
- const timer = window.setTimeout(() => setNudged(false), 1000);
155
+ // The nudge ends when its animations do (onAnimationEnd on the section). This timer is only the fallback for when
156
+ // none runs (motion off, or no Candor CSS), set from the duration and count the page resolved, so a brand's longer
157
+ // ring or shake isn't cut short.
158
+ ended.current = new Set();
159
+ const timer = window.setTimeout(() => setNudged(false), (animationMs(section.current) ?? 1200) + 100);
140
160
  return () => window.clearTimeout(timer);
141
161
  }, [nudged]);
142
162
  const word = (s) => {
@@ -171,7 +191,16 @@ function PromptBody({ request, mode, suggestions, editable, onAllow, onDeny, bus
171
191
  return;
172
192
  onAllow({ ...(edited && changedKeys.length && { updatedInput: edited }), ...(ticked.length && { alwaysAllow: ticked }) });
173
193
  };
174
- return (_jsxs("section", { ref: section, "data-slot": "permission-prompt", "data-waiting": busy ? undefined : "", "data-nudged": nudged ? "" : undefined, "aria-label": t("label", { title: questions ? t("questions") : title }), style: nudgeMotion, className: cn("overflow-hidden rounded-xl border border-warning/50 bg-card text-sm shadow-xs [--cd-attention-color:var(--warning)]", nudged && "cd-nudge", className), children: [_jsxs("header", { className: "flex items-start gap-2.5 bg-warning-weak/50 px-3.5 py-2.5", children: [_jsx("span", { "aria-hidden": true, style: waitingMotion, className: cn("mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full text-warning-text", !busy && "cd-attention"), children: _jsx(ShieldQuestion, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-semibold leading-snug", children: questions ? t("questions") : title }), request.description && _jsx("p", { className: "mt-0.5 text-muted-foreground", children: request.description })] }), _jsx("code", { className: "shrink-0 font-mono text-2xs text-muted-foreground", children: request.tool })] }), _jsxs("dl", { className: "flex flex-col gap-1.5 px-3.5 pt-2.5 text-sm", children: [request.reason && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("why") }), _jsx("dd", { className: "min-w-0", children: request.reason })] })), mode && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("mode") }), _jsx("dd", { "data-slot": "permission-mode", children: modeKeys[mode] ? t(modeKeys[mode]) : _jsx("code", { className: "font-mono text-xs", children: mode }) })] }))] }), (request.blockedPath || request.agentId || request.mcpServer) && (_jsxs("ul", { className: "mt-1.5 flex flex-col gap-0.5 px-3.5 text-xs text-muted-foreground", children: [request.blockedPath && _jsx("li", { children: t("blockedPath", { path: request.blockedPath }) }), request.agentId && _jsx("li", { children: t("subagent") }), request.mcpServer && _jsx("li", { children: t("server", { name: request.mcpServer.name, source: request.mcpServer.source }) })] })), questions ? (_jsx("div", { className: "flex flex-col gap-4 px-3.5 py-3", children: questions.map((q) => {
194
+ return (_jsxs("section", { ref: section, "data-slot": "permission-prompt", "data-waiting": busy ? undefined : "", "data-nudged": nudged ? "" : undefined, onAnimationEnd: (e) => {
195
+ // The nudge runs two animations (the shake and the ring) and ends with the last of them. A child's own
196
+ // animation, or one that isn't Candor's, doesn't end it.
197
+ if (e.target !== e.currentTarget || !e.animationName.startsWith("cd-"))
198
+ return;
199
+ ended.current.add(e.animationName);
200
+ const running = getComputedStyle(e.currentTarget).animationName.split(",").map((n) => n.trim()).filter((n) => n.startsWith("cd-"));
201
+ if (running.every((n) => ended.current.has(n)))
202
+ setNudged(false);
203
+ }, "aria-label": t("label", { title: questions ? t("questions") : title }), style: nudgeMotion, className: cn("overflow-hidden rounded-xl border border-warning/50 bg-card text-sm shadow-xs [--cd-attention-color:var(--warning)]", nudged && "cd-nudge", className), children: [_jsxs("header", { className: "flex items-start gap-2.5 bg-warning-weak/50 px-3.5 py-2.5", children: [_jsx("span", { "aria-hidden": true, style: waitingMotion, className: cn("mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full text-warning-text", !busy && "cd-attention"), children: _jsx(ShieldQuestion, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-semibold leading-snug", children: questions ? t("questions") : title }), request.description && _jsx("p", { className: "mt-0.5 text-muted-foreground", children: request.description })] }), _jsx("code", { className: "shrink-0 font-mono text-2xs text-muted-foreground", children: request.tool })] }), _jsxs("dl", { className: "flex flex-col gap-1.5 px-3.5 pt-2.5 text-sm", children: [request.reason && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("why") }), _jsx("dd", { className: "min-w-0", children: request.reason })] })), mode && (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "shrink-0 text-muted-foreground", children: t("mode") }), _jsx("dd", { "data-slot": "permission-mode", children: modeKeys[mode] ? t(modeKeys[mode]) : _jsx("code", { className: "font-mono text-xs", children: mode }) })] }))] }), (request.blockedPath || request.agentId || request.mcpServer) && (_jsxs("ul", { className: "mt-1.5 flex flex-col gap-0.5 px-3.5 text-xs text-muted-foreground", children: [request.blockedPath && _jsx("li", { children: t("blockedPath", { path: request.blockedPath }) }), request.agentId && _jsx("li", { children: t("subagent") }), request.mcpServer && _jsx("li", { children: t("server", { name: request.mcpServer.name, source: request.mcpServer.source }) })] })), questions ? (_jsx("div", { className: "flex flex-col gap-4 px-3.5 py-3", children: questions.map((q) => {
175
204
  const a = answers[q.id];
176
205
  return (_jsxs("fieldset", { className: "flex flex-col gap-1.5", children: [_jsxs("legend", { className: "mb-1 font-medium", children: [q.tag && _jsx("span", { className: "me-1.5 rounded-xs bg-muted px-1 text-2xs font-medium text-muted-foreground", children: q.tag }), q.prompt] }), q.options.map((o) => (_jsxs("label", { className: "flex min-h-6 items-start gap-2", children: [_jsx("input", { type: q.multiple ? "checkbox" : "radio", name: `${id}-${q.id}`, checked: a?.choice.includes(o.id) ?? false, onChange: () => choose(q, o.id), className: "mt-0.5 size-4 accent-[var(--primary)]" }), _jsxs("span", { children: [o.label, o.detail && _jsx("span", { className: "block text-xs text-muted-foreground", children: o.detail })] })] }, o.id))), q.other && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex min-h-6 items-start gap-2", children: [_jsx("input", { type: q.multiple ? "checkbox" : "radio", name: `${id}-${q.id}`, checked: a?.other ?? false, onChange: () => toggleOther(q), className: "mt-0.5 size-4 accent-[var(--primary)]" }), _jsx("span", { children: t("otherOption") })] }), a?.other && (_jsx("textarea", { "aria-label": t("otherLabel", { question: q.prompt }), rows: 2, value: a.text, onChange: (e) => setAnswers((prev) => ({ ...prev, [q.id]: { ...prev[q.id], text: e.target.value } })), className: "ms-6 block resize-none rounded-md border border-input bg-card px-2.5 py-1.5 text-sm outline-none [field-sizing:content] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30" }))] }))] }, q.id));
177
206
  }) })) : (_jsxs("div", { className: "flex flex-col gap-2 px-3.5 py-3", children: [editing ? (_jsxs(_Fragment, { children: [_jsx("label", { htmlFor: `${id}-edit`, className: "text-xs font-medium text-muted-foreground", children: t("editLabel") }), _jsx("textarea", { id: `${id}-edit`, value: draft, onChange: (e) => setDraft(e.target.value), spellCheck: false, rows: Math.min(12, draft.split("\n").length + 1), "aria-invalid": invalid || undefined, "aria-describedby": `${id}-changes`, className: "block w-full resize-y rounded-md border border-input bg-card px-2.5 py-1.5 font-mono text-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 aria-invalid:border-destructive" }), _jsx("div", { id: `${id}-changes`, role: "status", className: "text-xs", children: invalid ? (_jsx("p", { className: "text-destructive-text", children: t("editInvalid") })) : changedKeys.length ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("p", { className: "text-muted-foreground", children: t("changed") }), _jsx("ul", { className: "flex flex-col gap-1", children: changedKeys.map((k) => (_jsxs("li", { className: "grid gap-0.5 font-mono", children: [_jsx("span", { className: "text-muted-foreground", children: k }), k in proposed && _jsx("span", { className: "rounded-xs bg-destructive-weak/60 px-1 line-through", children: json(proposed[k]) }), k in edited && _jsx("span", { className: "rounded-xs bg-success-weak/70 px-1", children: json(edited[k]) })] }, k))) })] })) : (_jsx("p", { className: "text-muted-foreground", children: t("unchanged") })) }), _jsx(Button, { size: "sm", variant: "ghost", className: "self-start", onClick: () => (setEditing(false), setDraft(original)), children: t("cancelEdit") })] })) : (_jsxs(_Fragment, { children: [_jsx(Payload, { language: "json", title: t("input"), text: original }), editable && proposed && (_jsxs(Button, { size: "sm", variant: "outline", className: "self-start", onClick: () => setEditing(true), children: [_jsx(Pencil, { "aria-hidden": true }), " ", t("edit")] }))] })), suggestions && suggestions.length > 0 && (_jsxs("fieldset", { className: "mt-1 flex flex-col gap-1 rounded-lg border border-dashed px-3 py-2", children: [_jsx("legend", { className: "px-1 text-xs font-medium", children: t("remember") }), _jsxs("p", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Info, { "aria-hidden": true, className: "size-3" }), " ", t("rememberHint")] }), suggestions.map((s, i) => {
@@ -5,7 +5,10 @@ import { cn } from "../lib/utils.js";
5
5
  import { Skeleton } from "../ui/skeleton.js";
6
6
  import { useStrings } from "../blocks/locale.js";
7
7
  import { motion } from "../lib/motion.js";
8
- /** A suggestion pressed: 2.2's slight scale of 0.99. */
8
+ /**
9
+ * @motion press:press A suggestion shrinks slightly while it is held down and returns when it is let go. Under
10
+ * reduced motion no suggestion changes size.
11
+ */
9
12
  const pressMotion = motion("prompt-suggestions", "press", "press", { amp: "0.98" });
10
13
  /** PromptSuggestions' fixed words; override them under `promptSuggestions` in LocaleProvider's `strings`. */
11
14
  export const promptSuggestionsStrings = {
@@ -7,7 +7,15 @@ import { ShimmerLabel } from "../blocks/streaming-text.js";
7
7
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
8
8
  import { useStrings } from "../blocks/locale.js";
9
9
  import { motion, useArrived } from "../lib/motion.js";
10
+ /**
11
+ * @motion arrive:arrive A step of reasoning rises in when the agent adds it while the reasoning is on screen. A step
12
+ * added while it is folded rises in when it is opened. The steps it was drawn with are still.
13
+ */
10
14
  const arriveMotion = motion("reasoning", "arrive", "arrive");
15
+ /**
16
+ * @motion reveal:open The steps open to their full height when the person opens the summary, or when the agent starts
17
+ * thinking, and the chevron turns with it; closing reverses both. Under reduced motion they snap open or shut.
18
+ */
11
19
  const turnMotion = motion("reasoning", "open", "reveal");
12
20
  /** Reasoning's fixed words; override them under `reasoning` in LocaleProvider's `strings`. */
13
21
  export const reasoningStrings = {
@@ -6,11 +6,31 @@ import { cn } from "../lib/utils.js";
6
6
  import { useStrings } from "../blocks/locale.js";
7
7
  import { useVirtualList } from "../lib/use-virtual-list.js";
8
8
  import { motion, useMotionAllowed } from "../lib/motion.js";
9
- /** The smooth scroll to the latest message. */
9
+ /**
10
+ * @motion move:scroll When the person presses Jump to latest, the thread glides down to the newest message. Under
11
+ * reduced motion it jumps. Opening the thread and following new messages while the person is at the bottom are never
12
+ * smooth scrolls.
13
+ */
10
14
  const scrollMotion = motion("thread", "scroll", "move");
11
- /** Jump to latest appearing while the reader is scrolled up: 2.2's 150 ms and 4 px rise. */
15
+ /** How long the animations running on `el` take in all, in milliseconds (the longest of duration × iteration count, capped at 6 s); undefined when it can't be read. */
16
+ function animationMs(el) {
17
+ if (!el || typeof getComputedStyle === "undefined")
18
+ return undefined;
19
+ const style = getComputedStyle(el);
20
+ const durations = style.animationDuration.split(",").map((t) => (t.trim().endsWith("ms") ? Number.parseFloat(t) : Number.parseFloat(t) * 1000));
21
+ const counts = style.animationIterationCount.split(",").map((c) => (c.trim() === "infinite" ? Number.POSITIVE_INFINITY : Number.parseFloat(c)));
22
+ const totals = durations.map((d, i) => d * (counts[i % counts.length] || 1)).filter((n) => Number.isFinite(n) || n === Number.POSITIVE_INFINITY);
23
+ return totals.length ? Math.min(6000, Math.max(...totals)) : undefined;
24
+ }
25
+ /**
26
+ * @motion arrive:jump The Jump to latest control fades in when the person scrolls up from the bottom and goes when
27
+ * they return. It is not shown for a thread that has no messages.
28
+ */
12
29
  const jumpMotion = motion("thread", "jump", "arrive", { dur: 150 });
13
- /** Its ring when a message arrives while the reader is scrolled up. */
30
+ /**
31
+ * @motion attention:arrival While the person is scrolled up, Jump to latest pulses once each time a new message
32
+ * arrives, then is still. Nothing pulses at the bottom, where the new message is already in view.
33
+ */
14
34
  const arrivalMotion = motion("thread", "arrival", "attention");
15
35
  /** Thread's fixed words; override them under `thread` in LocaleProvider's `strings`. */
16
36
  export const threadStrings = {
@@ -58,6 +78,7 @@ function Thread({ children, empty, footer, className, contentClassName, virtuali
58
78
  const [arrived, setArrived] = React.useState(false);
59
79
  const arrivedRef = React.useRef(false);
60
80
  const frame = React.useRef(0);
81
+ const jump = React.useRef(null);
61
82
  React.useEffect(() => {
62
83
  arrivedRef.current = arrived;
63
84
  }, [arrived]);
@@ -119,7 +140,10 @@ function Thread({ children, empty, footer, className, contentClassName, virtuali
119
140
  React.useEffect(() => {
120
141
  if (!arrived)
121
142
  return;
122
- const timer = window.setTimeout(() => setArrived(false), 1000);
143
+ // The ring ends when its animation does (onAnimationEnd on the button). This timer is only the fallback for when
144
+ // no animation runs (motion off, or no Candor CSS), set from the duration and count the page resolved, so a brand's
145
+ // longer ring isn't cut short.
146
+ const timer = window.setTimeout(() => setArrived(false), (animationMs(jump.current) ?? 1200) + 100);
123
147
  return () => window.clearTimeout(timer);
124
148
  }, [arrived]);
125
149
  // Open at the anchor once, when it first appears.
@@ -188,7 +212,10 @@ function Thread({ children, empty, footer, className, contentClassName, virtuali
188
212
  }
189
213
  : undefined, onBlur: virtualize ? (e) => !e.currentTarget.contains(e.relatedTarget) && setFocused(null) : undefined, className: "h-full scroll-pb-14 overflow-y-auto overscroll-contain", children: _jsx("div", { ref: content, role: "log", "aria-label": t("messages"), className: cn("mx-auto flex w-full max-w-(--cd-thread-width) flex-col gap-7 px-4 py-6", contentClassName), children: !hasMessages ? (empty) : virtualize ? (_jsx(_Fragment, { children: list.rows.map((row) => "spacer" in row ? (_jsx("div", { "aria-hidden": true, style: { height: row.spacer } }, row.key)) : (_jsx("div", { ref: list.measure, "data-virtual-key": String(keyOf(row.index)), className: "flex flex-col", children: items[row.index] }, keyOf(row.index)))) })) : (children) }) }), !pinned && hasMessages && (
190
214
  // The entrance is the wrapper's, so the pulse on the button never replays it.
191
- _jsx("span", { style: jumpMotion, className: "absolute bottom-3 left-1/2 z-10 -translate-x-1/2 cd-arrive", children: _jsxs("button", { type: "button", "data-slot": "thread-jump", "data-arrived": arrived ? "" : undefined, onClick: () => {
215
+ _jsx("span", { style: jumpMotion, className: "absolute bottom-3 left-1/2 z-10 -translate-x-1/2 cd-arrive", children: _jsxs("button", { type: "button", ref: jump, "data-slot": "thread-jump", onAnimationEnd: (e) => {
216
+ if (e.target === e.currentTarget && e.animationName.startsWith("cd-"))
217
+ setArrived(false);
218
+ }, "data-arrived": arrived ? "" : undefined, onClick: () => {
192
219
  pinnedRef.current = true;
193
220
  setPinned(true);
194
221
  toBottom("smooth");
@@ -10,8 +10,15 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/colla
10
10
  import { Payload } from "./payload.js";
11
11
  import { useStrings } from "../blocks/locale.js";
12
12
  import { motion } from "../lib/motion.js";
13
- /** A call's detail opening, and a group of calls. */
13
+ /**
14
+ * @motion reveal:open A call's detail opens to its full height when the person opens it, and the chevron turns with
15
+ * it; closing reverses both. Under reduced motion it snaps open or shut.
16
+ */
14
17
  const openMotion = motion("tool-call", "open", "reveal");
18
+ /**
19
+ * @motion reveal:group-open A group of calls opens to its full height when the person opens it, and its chevron turns
20
+ * with it; closing reverses both. Under reduced motion it snaps open or shut.
21
+ */
15
22
  const groupMotion = motion("tool-call", "group-open", "reveal");
16
23
  /** ToolCall's fixed words; override them under `toolCall` in LocaleProvider's `strings`. */
17
24
  export const toolCallStrings = {
package/dist/index.d.ts CHANGED
@@ -51,6 +51,7 @@ export * from "./ui/slider.js";
51
51
  export * from "./ui/progress.js";
52
52
  export * from "./ui/alert.js";
53
53
  export * from "./ui/accordion.js";
54
+ export * from "./lib/sound.js";
54
55
  export * from "./lib/utils.js";
55
56
  export { ToolCall } from "./chat/tool-call.js";
56
57
  export type { ToolCall as ToolCallData } from "./lib/chat.js";
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ export * from "./ui/slider.js";
51
51
  export * from "./ui/progress.js";
52
52
  export * from "./ui/alert.js";
53
53
  export * from "./ui/accordion.js";
54
+ export * from "./lib/sound.js";
54
55
  export * from "./lib/utils.js";
55
56
  export { ToolCall } from "./chat/tool-call.js";
56
57
  export { Message } from "./chat/message.js";
@@ -350,6 +350,13 @@ export declare const motionFamilies: {
350
350
  export declare const motionFamilyNames: MotionFamily[];
351
351
  /** A family's variants, by name: `MotionVariant<"mark">` is `"draw" | "pop" | "drop"`. */
352
352
  export type MotionVariant<F extends MotionFamily> = (typeof motionFamilies)[F]["variants"][number];
353
+ /**
354
+ * True when a family has one way of drawing its change (fill, move, press, tint, refuse, attention, live and clock).
355
+ * Its single variant is fixed: there is no `-variant` token to set, and `motion()` writes none, because the family's
356
+ * utility has nothing to choose between. The docs mark such a family "(fixed)" instead of listing a value that does
357
+ * nothing. A later release that adds a second word turns the token on without a new name.
358
+ */
359
+ export declare const motionVariantIsFixed: (family: MotionFamily) => boolean;
353
360
  /**
354
361
  * The CSS value a variant is written as. CSS can only choose a drawing by keyframe name, so a variant's value is its
355
362
  * keyframe's: the word with Candor's prefix, which keeps an app's own `@keyframes rise` from taking over.
@@ -420,7 +427,8 @@ export declare function useSettled<T>(value: T, test: SettledTest<T>): number;
420
427
  export declare function useArrived(keys: readonly string[]): (key: string) => boolean;
421
428
  /**
422
429
  * Items that just left, kept as inert ghosts for the length of their out moment. Each ghost goes when `left(item)` is
423
- * called (from its `onAnimationEnd`), or after `fallbackMs` when nothing animated. An item that comes back while its
430
+ * called (from its `onAnimationEnd`), or after `fallbackMs` when nothing animated. The default, 600 ms, is longer than
431
+ * the longest out moment a brand can set (pop's 300 ms at the slowest speed, 1.5×, is 450 ms). An item that comes back while its
424
432
  * ghost is still leaving takes the ghost's place.
425
433
  */
426
434
  export declare function useLeaving<T>(items: readonly T[], keyOf: (item: T) => string, fallbackMs?: number): {
@@ -436,6 +444,13 @@ export type MotionMode = "full" | "calm" | "none";
436
444
  * panels) are ignored.
437
445
  */
438
446
  export declare function motionMode(el: Element | null | undefined): MotionMode;
447
+ /**
448
+ * `motionMode` for a component that has to react to it: the person's setting where `ref`'s element is, read after
449
+ * mount and again whenever the app changes `data-motion` or the OS setting changes. It starts as `full` for the first
450
+ * render, since there is nothing to read until the element exists. Use it for behaviour (a timer that must stop),
451
+ * not for styling, which the CSS variants already answer.
452
+ */
453
+ export declare function useMotionMode(ref: React.RefObject<Element | null>): MotionMode;
439
454
  /**
440
455
  * The level a moment resolves to on `el`, read from its computed style: the moment's token, its family's, the preset,
441
456
  * then capped by `data-motion`. A page without Candor's CSS reads as subtle.
@@ -179,6 +179,13 @@ export const motionFamilies = {
179
179
  };
180
180
  /** The family names, in the order the docs and the theme builder list them. */
181
181
  export const motionFamilyNames = Object.keys(motionFamilies);
182
+ /**
183
+ * True when a family has one way of drawing its change (fill, move, press, tint, refuse, attention, live and clock).
184
+ * Its single variant is fixed: there is no `-variant` token to set, and `motion()` writes none, because the family's
185
+ * utility has nothing to choose between. The docs mark such a family "(fixed)" instead of listing a value that does
186
+ * nothing. A later release that adds a second word turns the token on without a new name.
187
+ */
188
+ export const motionVariantIsFixed = (family) => motionFamilies[family].variants.length < 2;
182
189
  /**
183
190
  * The CSS value a variant is written as. CSS can only choose a drawing by keyframe name, so a variant's value is its
184
191
  * keyframe's: the word with Candor's prefix, which keeps an app's own `@keyframes rise` from taking over.
@@ -215,7 +222,7 @@ export function motion(item, moment, family, defaults = {}) {
215
222
  style[`${local}-ease`] = `var(${own}-ease, var(${fam}-ease, ${motionEases[defaults.ease ?? spec.ease]}))`;
216
223
  if (spec.amp)
217
224
  style[`${local}-amp`] = `var(${own}-${spec.amp.name}, var(${fam}-${spec.amp.name}, ${defaults.amp ?? spec.amp.default}))`;
218
- if (spec.variants.length > 1) {
225
+ if (!motionVariantIsFixed(family)) {
219
226
  const variant = motionVariantValue(defaults.variant ?? spec.variants[0]);
220
227
  style[`${local}-variant`] = `var(${own}-variant, var(${fam}-variant, ${variant}))`;
221
228
  }
@@ -272,10 +279,11 @@ export function useArrived(keys) {
272
279
  }
273
280
  /**
274
281
  * Items that just left, kept as inert ghosts for the length of their out moment. Each ghost goes when `left(item)` is
275
- * called (from its `onAnimationEnd`), or after `fallbackMs` when nothing animated. An item that comes back while its
282
+ * called (from its `onAnimationEnd`), or after `fallbackMs` when nothing animated. The default, 600 ms, is longer than
283
+ * the longest out moment a brand can set (pop's 300 ms at the slowest speed, 1.5×, is 450 ms). An item that comes back while its
276
284
  * ghost is still leaving takes the ghost's place.
277
285
  */
278
- export function useLeaving(items, keyOf, fallbackMs = 400) {
286
+ export function useLeaving(items, keyOf, fallbackMs = 600) {
279
287
  const [leaving, setLeaving] = React.useState([]);
280
288
  const previous = React.useRef(null);
281
289
  React.useLayoutEffect(() => {
@@ -314,6 +322,28 @@ export function motionMode(el) {
314
322
  return "calm";
315
323
  return "full";
316
324
  }
325
+ /**
326
+ * `motionMode` for a component that has to react to it: the person's setting where `ref`'s element is, read after
327
+ * mount and again whenever the app changes `data-motion` or the OS setting changes. It starts as `full` for the first
328
+ * render, since there is nothing to read until the element exists. Use it for behaviour (a timer that must stop),
329
+ * not for styling, which the CSS variants already answer.
330
+ */
331
+ export function useMotionMode(ref) {
332
+ const [mode, setMode] = React.useState("full");
333
+ React.useEffect(() => {
334
+ const read = () => setMode(motionMode(ref.current));
335
+ read();
336
+ const observer = new MutationObserver(read);
337
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-motion"], subtree: true });
338
+ const query = window.matchMedia?.("(prefers-reduced-motion: reduce)");
339
+ query?.addEventListener("change", read);
340
+ return () => {
341
+ observer.disconnect();
342
+ query?.removeEventListener("change", read);
343
+ };
344
+ }, [ref]);
345
+ return mode;
346
+ }
317
347
  const number = (value) => {
318
348
  const n = Number.parseFloat(value);
319
349
  return Number.isFinite(n) ? n : undefined;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The names and the reading half of Candor's sound layer. A brand file names a sound per event
3
+ * (`--cd-sound-needs-you: "/sounds/needs-you.mp3"`); this file knows the five events, reads that token from the
4
+ * page's computed style, and holds the small arithmetic the Pro hooks share. It plays nothing: nothing is fetched
5
+ * until `useAttention` or `sound-setting` plays a sound, and a sound is always off until the person turns it on.
6
+ *
7
+ * Tokens, all optional, all read when a sound is about to play:
8
+ * - `--cd-sound-<event>`: a string, the path of an MP3 hosted by the app. No token, no sound, silently.
9
+ * - `--cd-sound-<event>-ogg`: a string, an Ogg/Opus alternative the browser may prefer.
10
+ * - `--cd-sound-volume`: 0 to 1, how loud every sound is. `--cd-sound-<event>-volume`: 0 to 1, scales that for one event.
11
+ *
12
+ * Safe to import on the server: the document is touched only inside `readSoundToken`.
13
+ */
14
+ /** The five things the app may make a sound for. A closed list: `reply` and `silent` have no `AttentionKind` until 3.0. */
15
+ export type SoundEvent = "needs-you" | "finished" | "failed" | "reply" | "silent";
16
+ /** Every event, in the order the settings control lists them. */
17
+ export declare const soundEvents: readonly SoundEvent[];
18
+ /** The events a sound is offered for once the person turns sound on; `reply` and `silent` stay off until they pick them. */
19
+ export declare const defaultSoundEvents: readonly SoundEvent[];
20
+ /** A sound as the brand names it: where the file is, an optional Ogg alternative, and how loud (0 to 1). */
21
+ export type SoundToken = {
22
+ src: string;
23
+ ogg?: string | undefined;
24
+ volume: number;
25
+ };
26
+ /** The longest a sound may be, in seconds. `theme check` measures what it can read. */
27
+ export declare const maxSoundSeconds = 1.5;
28
+ /** The least time between two plays of the same event, in milliseconds. A burst plays once. */
29
+ export declare const soundCoalesceMs = 3000;
30
+ /** A CSS custom property's value as the text it names: quotes removed, or the inside of `url(…)`. Empty when unset. */
31
+ export declare function soundString(raw: string): string;
32
+ /** How loud `event` is, 0 to 1: the global `--cd-sound-volume` times the event's own, each 1 when unset. 1 on the server. */
33
+ export declare function readSoundVolume(event: SoundEvent, element?: Element | undefined): number;
34
+ /**
35
+ * The brand's sound for `event`, read from the computed style of `element` (the document's root when left out), or
36
+ * `undefined` when the brand names none, or on the server.
37
+ */
38
+ export declare function readSoundToken(event: SoundEvent, element?: Element | undefined): SoundToken | undefined;
39
+ /**
40
+ * Whether a play at `now` is allowed, given when each event last played and when anything last played: the same event
41
+ * waits `soundCoalesceMs` after it played, and any sound in the same instant (a burst) is the one already playing.
42
+ * Pure; the caller keeps the clock.
43
+ */
44
+ export declare function mayPlay(event: SoundEvent, last: Partial<Record<SoundEvent, number>>, now: number, burstMs?: number): boolean;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The names and the reading half of Candor's sound layer. A brand file names a sound per event
3
+ * (`--cd-sound-needs-you: "/sounds/needs-you.mp3"`); this file knows the five events, reads that token from the
4
+ * page's computed style, and holds the small arithmetic the Pro hooks share. It plays nothing: nothing is fetched
5
+ * until `useAttention` or `sound-setting` plays a sound, and a sound is always off until the person turns it on.
6
+ *
7
+ * Tokens, all optional, all read when a sound is about to play:
8
+ * - `--cd-sound-<event>`: a string, the path of an MP3 hosted by the app. No token, no sound, silently.
9
+ * - `--cd-sound-<event>-ogg`: a string, an Ogg/Opus alternative the browser may prefer.
10
+ * - `--cd-sound-volume`: 0 to 1, how loud every sound is. `--cd-sound-<event>-volume`: 0 to 1, scales that for one event.
11
+ *
12
+ * Safe to import on the server: the document is touched only inside `readSoundToken`.
13
+ */
14
+ /** Every event, in the order the settings control lists them. */
15
+ export const soundEvents = ["needs-you", "finished", "failed", "reply", "silent"];
16
+ /** The events a sound is offered for once the person turns sound on; `reply` and `silent` stay off until they pick them. */
17
+ export const defaultSoundEvents = ["needs-you", "finished", "failed"];
18
+ /** The longest a sound may be, in seconds. `theme check` measures what it can read. */
19
+ export const maxSoundSeconds = 1.5;
20
+ /** The least time between two plays of the same event, in milliseconds. A burst plays once. */
21
+ export const soundCoalesceMs = 3000;
22
+ /** A CSS custom property's value as the text it names: quotes removed, or the inside of `url(…)`. Empty when unset. */
23
+ export function soundString(raw) {
24
+ const v = raw.trim();
25
+ const url = /^url\(\s*(['"]?)(.*?)\1\s*\)$/.exec(v);
26
+ const text = url ? (url[2] ?? "") : v;
27
+ return text.replace(/^(['"])(.*)\1$/, "$2").trim();
28
+ }
29
+ const unit = (raw) => {
30
+ const n = Number.parseFloat(raw);
31
+ return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : undefined;
32
+ };
33
+ const styleOf = (element) => typeof document === "undefined" || typeof getComputedStyle === "undefined" ? undefined : getComputedStyle(element ?? document.documentElement);
34
+ /** How loud `event` is, 0 to 1: the global `--cd-sound-volume` times the event's own, each 1 when unset. 1 on the server. */
35
+ export function readSoundVolume(event, element) {
36
+ const style = styleOf(element);
37
+ if (!style)
38
+ return 1;
39
+ return (unit(style.getPropertyValue("--cd-sound-volume")) ?? 1) * (unit(style.getPropertyValue(`--cd-sound-${event}-volume`)) ?? 1);
40
+ }
41
+ /**
42
+ * The brand's sound for `event`, read from the computed style of `element` (the document's root when left out), or
43
+ * `undefined` when the brand names none, or on the server.
44
+ */
45
+ export function readSoundToken(event, element) {
46
+ const style = styleOf(element);
47
+ const src = style && soundString(style.getPropertyValue(`--cd-sound-${event}`));
48
+ if (!style || !src)
49
+ return undefined;
50
+ const ogg = soundString(style.getPropertyValue(`--cd-sound-${event}-ogg`));
51
+ return { src, ...(ogg && { ogg }), volume: readSoundVolume(event, element) };
52
+ }
53
+ /**
54
+ * Whether a play at `now` is allowed, given when each event last played and when anything last played: the same event
55
+ * waits `soundCoalesceMs` after it played, and any sound in the same instant (a burst) is the one already playing.
56
+ * Pure; the caller keeps the clock.
57
+ */
58
+ export function mayPlay(event, last, now, burstMs = 400) {
59
+ const before = last[event];
60
+ if (before !== undefined && now - before < soundCoalesceMs)
61
+ return false;
62
+ return !soundEvents.some((e) => (last[e] ?? -Infinity) > now - burstMs);
63
+ }
package/dist/styles.css CHANGED
@@ -9,7 +9,6 @@
9
9
  * Tailwind skips node_modules when it scans your app.
10
10
  */
11
11
  @import "@fontsource-variable/inter/opsz.css";
12
- @import "tw-animate-css";
13
12
 
14
13
  @source "./**/*.js";
15
14
 
@@ -1104,3 +1103,22 @@
1104
1103
  animation: none !important;
1105
1104
  }
1106
1105
  }
1106
+
1107
+ /*
1108
+ * Sound (2.4). Candor sets no defaults here: no sound plays unless a brand file names one, and no sound plays at all
1109
+ * until the person turns sound on in your app. These are the names a brand file may set, in `:root`. Each value is a
1110
+ * string, the path of a file your app hosts (MP3, at most 1.5 s); nothing is fetched until a sound plays.
1111
+ *
1112
+ * --cd-sound-needs-you a new approval, question or disambiguation waits on the person
1113
+ * --cd-sound-finished a run the person started finished
1114
+ * --cd-sound-failed a run failed, or a guardrail stopped it
1115
+ * --cd-sound-reply a person (not an agent) replied in a thread the person is in
1116
+ * --cd-sound-silent a run the person started has not been heard from past its `staleAfter`
1117
+ *
1118
+ * --cd-sound-<event>-ogg optional Ogg/Opus alternative for the same event, as a string
1119
+ * --cd-sound-volume 0 to 1, how loud every sound is (1 when left out)
1120
+ * --cd-sound-<event>-volume 0 to 1, scales the global volume for one event (1 when left out)
1121
+ *
1122
+ * `useAttention` (Pro) reads them when a signal lands; `sound-setting` (Pro) plays them as a preview. Free items
1123
+ * never play a sound. The starter set (`sounds`, Pro) installs /sounds/<event>.mp3 for all five.
1124
+ */
@@ -16,8 +16,8 @@ declare function AccordionTrigger(allProps: React.ComponentProps<typeof Accordio
16
16
  * The panel. It opens and closes to the height Radix measured, so a change is seen settling rather than snapping;
17
17
  * data-motion="none" and the OS's reduced-motion setting make it snap.
18
18
  *
19
- * @motion reveal:open The panel grows to its measured height when opened and shrinks back when closed, in the base
20
- * duration. Content the app puts in it is not animated.
19
+ * @motion reveal:open The panel grows to its full height when the person opens it and shrinks back when they close it.
20
+ * What the app puts inside it doesn't move. Under reduced motion the panel snaps open or shut.
21
21
  */
22
22
  declare function AccordionContent(allProps: React.ComponentProps<typeof AccordionPrimitive.Content> & RenderProp): React.JSX.Element;
23
23
  export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -32,8 +32,8 @@ function AccordionTrigger(allProps) {
32
32
  * The panel. It opens and closes to the height Radix measured, so a change is seen settling rather than snapping;
33
33
  * data-motion="none" and the OS's reduced-motion setting make it snap.
34
34
  *
35
- * @motion reveal:open The panel grows to its measured height when opened and shrinks back when closed, in the base
36
- * duration. Content the app puts in it is not animated.
35
+ * @motion reveal:open The panel grows to its full height when the person opens it and shrinks back when they close it.
36
+ * What the app puts inside it doesn't move. Under reduced motion the panel snaps open or shut.
37
37
  */
38
38
  function AccordionContent(allProps) {
39
39
  const { className, children, render, nativeButton: _nativeButton, style, ...props } = allProps;
@@ -7,7 +7,8 @@ import * as React from "react";
7
7
  * For a short notice that goes away by itself use a toast; an Alert doesn't dismiss itself.
8
8
  *
9
9
  * @motion arrive:enter An Alert that appears after the page has drawn rises in over a quarter of a second. One that is
10
- * there when the page first draws, or when it is rendered on the server, is simply there.
10
+ * there when the page first draws, or that the server rendered, is simply there; after a client-side navigation, an
11
+ * Alert on the new route rises in.
11
12
  */
12
13
  declare function Alert({ className, variant, role, style, ...props }: React.ComponentProps<"div"> & {
13
14
  /** The kind of message. `destructive` is for a failure or something that can't be undone; `warning` for care before acting; `attention` for something waiting on the person; `success` for a confirmed result. Defaults to a plain card. */
package/dist/ui/alert.js CHANGED
@@ -5,8 +5,17 @@ import { cva } from "class-variance-authority";
5
5
  import { cn } from "../lib/utils.js";
6
6
  import { motion } from "../lib/motion.js";
7
7
  const enterMotion = motion("alert", "enter", "arrive");
8
- // Set once a first Alert has been drawn and the browser has painted; an Alert that mounts after that has arrived.
8
+ // Set one frame after this file loads in the browser, which is when the page's first draw is done; an Alert that mounts
9
+ // after that has arrived. It is per page load, not per route: after a client-side route change, an Alert on the new route
10
+ // rises in, because it arrived after the page drew. An Alert in the first render, or on the server, is simply there.
9
11
  let pageDrawn = false;
12
+ if (typeof window !== "undefined") {
13
+ const frame = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : (cb) => window.setTimeout(cb, 0);
14
+ frame(() => {
15
+ pageDrawn = true;
16
+ });
17
+ }
18
+ const subscribeNever = () => () => { };
10
19
  const alertVariants = cva("relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", {
11
20
  variants: {
12
21
  variant: {
@@ -27,17 +36,15 @@ const alertVariants = cva("relative grid w-full grid-cols-[0_1fr] items-start ga
27
36
  * For a short notice that goes away by itself use a toast; an Alert doesn't dismiss itself.
28
37
  *
29
38
  * @motion arrive:enter An Alert that appears after the page has drawn rises in over a quarter of a second. One that is
30
- * there when the page first draws, or when it is rendered on the server, is simply there.
39
+ * there when the page first draws, or that the server rendered, is simply there; after a client-side navigation, an
40
+ * Alert on the new route rises in.
31
41
  */
32
42
  function Alert({ className, variant, role, style, ...props }) {
33
- // Whether this Alert mounted after the page's first draw. Decided once, so a re-render doesn't replay it.
34
- const [arrived] = React.useState(() => pageDrawn);
35
- React.useEffect(() => {
36
- const raf = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame : (cb) => window.setTimeout(cb, 0);
37
- raf(() => {
38
- pageDrawn = true;
39
- });
40
- }, []);
43
+ // Whether this Alert mounted after the page's first draw. The server and the hydrating render answer no, so what
44
+ // the server sent is what hydrates; an Alert the browser creates later answers from the page. Decided once, so a
45
+ // re-render doesn't replay it.
46
+ const drawn = React.useSyncExternalStore(subscribeNever, () => pageDrawn, () => false);
47
+ const [arrived] = React.useState(drawn);
41
48
  return (_jsx("div", { "data-slot": "alert", "data-variant": variant ?? "default", role: role ?? (variant === "destructive" ? "alert" : "status"), style: { ...enterMotion, ...style }, className: cn(alertVariants({ variant }), arrived && "cd-arrive", className), ...props }));
42
49
  }
43
50
  /** The alert's headline: what happened or what to know, in a few words. */
package/dist/ui/button.js CHANGED
@@ -6,6 +6,10 @@ import { Slot } from "radix-ui";
6
6
  import { withRender } from "../lib/render-prop.js";
7
7
  import { cn } from "../lib/utils.js";
8
8
  import { motion } from "../lib/motion.js";
9
+ /**
10
+ * @motion press:press The button shrinks a little while it is held down and returns when it is let go. A button that
11
+ * is locked or disabled never moves, and under reduced motion no button changes size.
12
+ */
9
13
  const pressMotion = motion("button", "press", "press");
10
14
  // The accent fills the one primary act, the radius is the theme's
11
15
  // --cd-radius-button, and buttons press down slightly on :active.
@@ -4,6 +4,11 @@ import { Collapsible as CollapsiblePrimitive } from "radix-ui";
4
4
  import { cn } from "../lib/utils.js";
5
5
  import { withRender } from "../lib/render-prop.js";
6
6
  import { motion } from "../lib/motion.js";
7
+ /**
8
+ * @motion reveal:open The content grows to its full height when the person opens it and shrinks when they close it. A
9
+ * collapsible that is already open when it first appears is shown open at once, and under reduced motion it snaps
10
+ * open or shut.
11
+ */
7
12
  const openMotion = motion("collapsible", "open", "reveal");
8
13
  function Collapsible({ ...props }) {
9
14
  return _jsx(CollapsiblePrimitive.Root, { "data-slot": "collapsible", ...props });
@@ -5,6 +5,10 @@ import { cn } from "../lib/utils.js";
5
5
  import { Popover as PopoverPrimitive } from "radix-ui";
6
6
  import { withRender } from "../lib/render-prop.js";
7
7
  import { motion } from "../lib/motion.js";
8
+ /**
9
+ * @motion overlay:open The popover fades in and scales up a little from its trigger when the person opens it, and
10
+ * fades out when it closes. Under reduced motion it is a short fade.
11
+ */
8
12
  const openMotion = motion("popover", "open", "overlay");
9
13
  function Popover({ ...props }) {
10
14
  return _jsx(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
@@ -4,7 +4,7 @@ import { type RenderProp } from "../lib/render-prop.js";
4
4
  /**
5
5
  * A bar for work whose progress is measured: an upload, an import, an index. It moves only between values the service
6
6
  * reported, and it starts at the first value it is given rather than filling from empty. With no `value` it is
7
- * indeterminate: a moving sweep and the `label` as text, and never a made-up percentage.
7
+ * indeterminate: a pulsing bar and the `label` as text, and never a made-up percentage.
8
8
  *
9
9
  * @motion fill:value The filled part moves to each new measured value in the slow duration, and not on first draw.
10
10
  * @motion live:indeterminate While the total is unknown, the bar pulses (the live family's pulse) and the label shimmers
@@ -10,7 +10,7 @@ const indeterminateMotion = motion("progress", "indeterminate", "live");
10
10
  /**
11
11
  * A bar for work whose progress is measured: an upload, an import, an index. It moves only between values the service
12
12
  * reported, and it starts at the first value it is given rather than filling from empty. With no `value` it is
13
- * indeterminate: a moving sweep and the `label` as text, and never a made-up percentage.
13
+ * indeterminate: a pulsing bar and the `label` as text, and never a made-up percentage.
14
14
  *
15
15
  * @motion fill:value The filled part moves to each new measured value in the slow duration, and not on first draw.
16
16
  * @motion live:indeterminate While the total is unknown, the bar pulses (the live family's pulse) and the label shimmers