@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.
- package/README.md +3 -3
- package/dist/blocks/copy-field.js +4 -1
- package/dist/blocks/degraded-panel.js +4 -0
- package/dist/blocks/locked.js +29 -3
- package/dist/blocks/relative-time.js +1 -1
- package/dist/blocks/status-dot.js +9 -2
- package/dist/blocks/step-list.js +16 -3
- package/dist/blocks/streaming-text.js +4 -0
- package/dist/chat/attachment-preview.d.ts +6 -1
- package/dist/chat/attachment-preview.js +5 -2
- package/dist/chat/card-registry.js +7 -3
- package/dist/chat/citation.d.ts +3 -0
- package/dist/chat/citation.js +5 -2
- package/dist/chat/composer.js +17 -4
- package/dist/chat/context-meter.d.ts +5 -0
- package/dist/chat/context-meter.js +13 -2
- package/dist/chat/entity-chip.d.ts +6 -1
- package/dist/chat/entity-chip.js +5 -2
- package/dist/chat/folded-turns.js +4 -0
- package/dist/chat/message-actions.js +4 -1
- package/dist/chat/message.js +8 -1
- package/dist/chat/payload.d.ts +3 -0
- package/dist/chat/payload.js +3 -0
- package/dist/chat/permission-prompt.d.ts +5 -0
- package/dist/chat/permission-prompt.js +31 -2
- package/dist/chat/prompt-suggestions.js +4 -1
- package/dist/chat/reasoning.js +8 -0
- package/dist/chat/thread.js +32 -5
- package/dist/chat/tool-call.js +8 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/motion.d.ts +16 -1
- package/dist/lib/motion.js +33 -3
- package/dist/lib/sound.d.ts +44 -0
- package/dist/lib/sound.js +63 -0
- package/dist/styles.css +19 -1
- package/dist/ui/accordion.d.ts +2 -2
- package/dist/ui/accordion.js +2 -2
- package/dist/ui/alert.d.ts +2 -1
- package/dist/ui/alert.js +17 -10
- package/dist/ui/button.js +4 -0
- package/dist/ui/collapsible.js +5 -0
- package/dist/ui/popover.js +4 -0
- package/dist/ui/progress.d.ts +1 -1
- package/dist/ui/progress.js +1 -1
- package/dist/ui/skeleton.js +4 -1
- package/dist/ui/slider.d.ts +1 -1
- package/dist/ui/slider.js +1 -1
- package/dist/ui/switch.d.ts +1 -1
- package/dist/ui/switch.js +1 -1
- package/dist/ui/tooltip.js +4 -0
- 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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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 = {
|
package/dist/chat/reasoning.js
CHANGED
|
@@ -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 = {
|
package/dist/chat/thread.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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",
|
|
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");
|
package/dist/chat/tool-call.js
CHANGED
|
@@ -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
|
-
/**
|
|
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";
|
package/dist/lib/motion.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
package/dist/lib/motion.js
CHANGED
|
@@ -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 (
|
|
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.
|
|
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 =
|
|
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
|
+
*/
|
package/dist/ui/accordion.d.ts
CHANGED
|
@@ -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
|
|
20
|
-
*
|
|
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 };
|
package/dist/ui/accordion.js
CHANGED
|
@@ -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
|
|
36
|
-
*
|
|
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;
|
package/dist/ui/alert.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
package/dist/ui/collapsible.js
CHANGED
|
@@ -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 });
|
package/dist/ui/popover.js
CHANGED
|
@@ -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 });
|
package/dist/ui/progress.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/ui/progress.js
CHANGED
|
@@ -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
|
|
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
|