@candor-ui/react 2.2.0 → 2.4.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/adapters/use-ag-ui.d.ts +1 -1
- package/dist/adapters/use-candor-chat.d.ts +1 -1
- package/dist/adapters/use-claude-agent.d.ts +1 -1
- package/dist/blocks/copy-field.js +4 -1
- package/dist/blocks/degraded-panel.js +3 -1
- package/dist/blocks/locked.js +23 -2
- package/dist/blocks/relative-time.js +1 -1
- package/dist/blocks/status-dot.d.ts +14 -3
- package/dist/blocks/status-dot.js +17 -22
- package/dist/blocks/step-list.d.ts +2 -1
- package/dist/blocks/step-list.js +19 -27
- package/dist/blocks/streaming-text.js +3 -1
- package/dist/blocks/surface.d.ts +1 -1
- package/dist/chat/attachment-preview.d.ts +6 -1
- package/dist/chat/attachment-preview.js +9 -3
- package/dist/chat/card-registry.js +6 -4
- package/dist/chat/citation.js +3 -3
- package/dist/chat/composer.js +19 -53
- package/dist/chat/context-meter.d.ts +5 -0
- package/dist/chat/context-meter.js +15 -1
- package/dist/chat/entity-chip.d.ts +3 -1
- package/dist/chat/entity-chip.js +2 -2
- package/dist/chat/folded-turns.js +3 -1
- package/dist/chat/message-actions.js +5 -2
- package/dist/chat/message.js +9 -5
- package/dist/chat/payload.js +1 -1
- package/dist/chat/permission-prompt.d.ts +5 -0
- package/dist/chat/permission-prompt.js +36 -2
- package/dist/chat/prompt-suggestions.js +4 -1
- package/dist/chat/reasoning.js +6 -1
- package/dist/chat/thread.js +37 -9
- package/dist/chat/tool-call.js +6 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/lib/motion.d.ts +466 -0
- package/dist/lib/motion.js +380 -0
- package/dist/lib/sound.d.ts +44 -0
- package/dist/lib/sound.js +63 -0
- package/dist/styles.css +562 -45
- package/dist/ui/accordion.d.ts +23 -0
- package/dist/ui/accordion.js +45 -0
- package/dist/ui/alert.d.ts +22 -0
- package/dist/ui/alert.js +59 -0
- package/dist/ui/button.d.ts +1 -1
- package/dist/ui/button.js +5 -3
- package/dist/ui/checkbox.d.ts +14 -0
- package/dist/ui/checkbox.js +32 -0
- package/dist/ui/collapsible.d.ts +3 -2
- package/dist/ui/collapsible.js +6 -3
- package/dist/ui/label.d.ts +10 -0
- package/dist/ui/label.js +16 -0
- package/dist/ui/popover.d.ts +1 -1
- package/dist/ui/popover.js +4 -2
- package/dist/ui/progress.d.ts +29 -0
- package/dist/ui/progress.js +27 -0
- package/dist/ui/skeleton.d.ts +1 -1
- package/dist/ui/skeleton.js +5 -2
- package/dist/ui/slider.d.ts +28 -0
- package/dist/ui/slider.js +41 -0
- package/dist/ui/switch.d.ts +14 -0
- package/dist/ui/switch.js +23 -0
- package/dist/ui/tooltip.d.ts +1 -1
- package/dist/ui/tooltip.js +4 -2
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ copy code into their app. If you want to own and edit the source, install with t
|
|
|
17
17
|
npm install @candor-ui/react
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Needs React 19 and Tailwind CSS 4. Radix, lucide-react, class-variance-authority, clsx, tailwind-merge
|
|
21
|
-
|
|
20
|
+
Needs React 19 and Tailwind CSS 4. Radix, lucide-react, class-variance-authority, clsx, tailwind-merge and the Inter
|
|
21
|
+
font come with it.
|
|
22
22
|
|
|
23
23
|
## Styles (Tailwind 4)
|
|
24
24
|
|
|
@@ -30,7 +30,7 @@ The components are styled with Tailwind classes and Candor's tokens. Import the
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
`styles.css` brings Candor's tokens (colours, type scale, radii, shadows, motion), the light, dark and high-contrast
|
|
33
|
-
themes, Inter
|
|
33
|
+
themes, Inter and an `@source` for the package's own files, so Tailwind generates the classes the
|
|
34
34
|
components use even though it skips `node_modules` when scanning your app. If your setup doesn't follow `@source`
|
|
35
35
|
from imported CSS, add it yourself, relative to your CSS file:
|
|
36
36
|
|
|
@@ -52,7 +52,7 @@ export declare function useAgUi(options: UseAgUiOptions): {
|
|
|
52
52
|
composer: {
|
|
53
53
|
value: string;
|
|
54
54
|
onChange: React.Dispatch<React.SetStateAction<string>>;
|
|
55
|
-
state: "streaming" | "
|
|
55
|
+
state: "streaming" | "sending" | "ready";
|
|
56
56
|
references: EntityRef[];
|
|
57
57
|
onReferencesChange: React.Dispatch<React.SetStateAction<EntityRef[]>>;
|
|
58
58
|
onStop: () => void | undefined;
|
|
@@ -31,7 +31,7 @@ export declare function useCandorChat(chat: UseChatHelpers<UIMessage>, options:
|
|
|
31
31
|
composer: {
|
|
32
32
|
value: string;
|
|
33
33
|
onChange: React.Dispatch<React.SetStateAction<string>>;
|
|
34
|
-
state: "streaming" | "
|
|
34
|
+
state: "streaming" | "sending" | "ready";
|
|
35
35
|
references: EntityRef[];
|
|
36
36
|
onReferencesChange: React.Dispatch<React.SetStateAction<EntityRef[]>>;
|
|
37
37
|
onStop: () => Promise<void>;
|
|
@@ -68,7 +68,7 @@ export declare function useClaudeAgent(options: UseClaudeAgentOptions): {
|
|
|
68
68
|
composer: {
|
|
69
69
|
value: string;
|
|
70
70
|
onChange: React.Dispatch<React.SetStateAction<string>>;
|
|
71
|
-
state: "streaming" | "
|
|
71
|
+
state: "streaming" | "sending" | "ready";
|
|
72
72
|
references: EntityRef[];
|
|
73
73
|
onReferencesChange: React.Dispatch<React.SetStateAction<EntityRef[]>>;
|
|
74
74
|
onStop: () => void;
|
|
@@ -4,6 +4,9 @@ import { Check, Copy, X } from "lucide-react";
|
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { useCopy } from "../lib/use-copy.js";
|
|
6
6
|
import { useStrings } from "./locale.js";
|
|
7
|
+
import { motion } from "../lib/motion.js";
|
|
8
|
+
/** The tick or cross after the clipboard write settles. */
|
|
9
|
+
const tickMotion = motion("copy-field", "tick", "pop");
|
|
7
10
|
/** CopyField's fixed words; override them under `copyField` in LocaleProvider's `strings`. */
|
|
8
11
|
export const copyFieldStrings = {
|
|
9
12
|
copy: "Copy",
|
|
@@ -19,6 +22,6 @@ export const copyFieldStrings = {
|
|
|
19
22
|
function CopyField({ label, value, className, }) {
|
|
20
23
|
const { state, copy } = useCopy();
|
|
21
24
|
const t = useStrings("copyField", copyFieldStrings);
|
|
22
|
-
return (_jsxs("div", { "data-slot": "copy-field", className: cn("flex flex-col gap-1", className), children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("button", { type: "button", onClick: () => copy(value), className: "group/copy flex w-full items-center gap-2 rounded-lg border bg-sunken px-3 py-2 text-start outline-none
|
|
25
|
+
return (_jsxs("div", { "data-slot": "copy-field", className: cn("flex flex-col gap-1", className), children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("button", { type: "button", onClick: () => copy(value), className: "group/copy flex w-full items-center gap-2 rounded-lg border bg-sunken px-3 py-2 text-start outline-none cd-tint hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/80", children: [_jsx("code", { className: "min-w-0 flex-1 truncate font-mono text-xs", children: value }), _jsx("span", { "aria-live": "polite", "data-copy": state, className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground group-hover/copy:text-foreground", children: state === "copied" ? (_jsxs(_Fragment, { children: [_jsx(Check, { style: tickMotion, className: "size-3 text-success-text cd-pop" }), " ", t("copied")] })) : state === "failed" ? (_jsxs(_Fragment, { children: [_jsx(X, { style: tickMotion, className: "size-3 text-destructive-text cd-pop" }), " ", t("failed")] })) : (_jsxs(_Fragment, { children: [_jsx(Copy, { className: "size-3" }), " ", t("copy")] })) })] })] }));
|
|
23
26
|
}
|
|
24
27
|
export { CopyField };
|
|
@@ -4,6 +4,8 @@ import { CircleAlert, RotateCw } from "lucide-react";
|
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { Button } from "../ui/button.js";
|
|
6
6
|
import { useStrings } from "./locale.js";
|
|
7
|
+
import { motion } from "../lib/motion.js";
|
|
8
|
+
const spinMotion = motion("degraded-panel", "busy", "live", { dur: 1000 });
|
|
7
9
|
/** DegradedPanel's fixed words; override them under `degradedPanel` in LocaleProvider's `strings`. */
|
|
8
10
|
export const degradedPanelStrings = {
|
|
9
11
|
missing: "Missing:",
|
|
@@ -17,6 +19,6 @@ export const degradedPanelStrings = {
|
|
|
17
19
|
*/
|
|
18
20
|
function DegradedPanel({ reason, missing, onRetry, retrying = false, variant = "block", className, }) {
|
|
19
21
|
const t = useStrings("degradedPanel", degradedPanelStrings);
|
|
20
|
-
return (_jsxs("div", { "data-slot": "degraded-panel", role: "status", className: cn("flex gap-2.5 text-sm", variant === "block" ? "items-start rounded-lg border border-warning/40 bg-warning-weak/60 p-3" : "items-center", className), children: [_jsx(CircleAlert, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0 text-warning-text" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-foreground", children: reason }), missing && _jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [t("missing"), " ", missing] })] }), onRetry && (_jsxs(Button, { size: "xs", variant: "outline", onClick: onRetry, disabled: retrying, "aria-busy": retrying, className: "shrink-0", children: [_jsx(RotateCw, { className: cn(retrying && "
|
|
22
|
+
return (_jsxs("div", { "data-slot": "degraded-panel", role: "status", className: cn("flex gap-2.5 text-sm", variant === "block" ? "items-start rounded-lg border border-warning/40 bg-warning-weak/60 p-3" : "items-center", className), children: [_jsx(CircleAlert, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0 text-warning-text" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-foreground", children: reason }), missing && _jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [t("missing"), " ", missing] })] }), onRetry && (_jsxs(Button, { size: "xs", variant: "outline", onClick: onRetry, disabled: retrying, "aria-busy": retrying, className: "shrink-0", children: [_jsx(RotateCw, { style: spinMotion, className: cn(retrying && "cd-live-spin") }), " ", retrying ? t("retrying") : t("retry")] }))] }));
|
|
21
23
|
}
|
|
22
24
|
export { DegradedPanel };
|
package/dist/blocks/locked.js
CHANGED
|
@@ -4,6 +4,20 @@ import * as React from "react";
|
|
|
4
4
|
import { LockKeyhole } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
7
|
+
import { motion } from "../lib/motion.js";
|
|
8
|
+
/** The running animation's duration on `el`, in milliseconds (its longest, if there are several); undefined when it can't be read. */
|
|
9
|
+
function animationMs(el) {
|
|
10
|
+
if (!el || typeof getComputedStyle === "undefined")
|
|
11
|
+
return undefined;
|
|
12
|
+
const times = getComputedStyle(el)
|
|
13
|
+
.animationDuration.split(",")
|
|
14
|
+
.map((t) => (t.trim().endsWith("ms") ? Number.parseFloat(t) : Number.parseFloat(t) * 1000))
|
|
15
|
+
.filter((n) => Number.isFinite(n));
|
|
16
|
+
return times.length ? Math.max(...times) : undefined;
|
|
17
|
+
}
|
|
18
|
+
/** A press on the locked control: the reason is emphasised, and under expressive the control shakes. */
|
|
19
|
+
const refuseMotion = motion("locked", "refuse", "refuse");
|
|
20
|
+
const reasonMotion = motion("locked", "reason", "tint", { dur: 200 });
|
|
7
21
|
/**
|
|
8
22
|
* The honest-state rule: a control that cannot be used says why.
|
|
9
23
|
*
|
|
@@ -24,10 +38,14 @@ function Locked({ reason, inline = false, children, className, }) {
|
|
|
24
38
|
const [press, setPress] = React.useState(0);
|
|
25
39
|
const [shaking, setShaking] = React.useState(false);
|
|
26
40
|
const [held, setHeld] = React.useState(false);
|
|
41
|
+
const box = React.useRef(null);
|
|
27
42
|
React.useEffect(() => {
|
|
28
43
|
if (press === 0)
|
|
29
44
|
return;
|
|
30
|
-
|
|
45
|
+
// The shake ends when its animation does (onAnimationEnd below). This timer is only the fallback for when no
|
|
46
|
+
// animation runs (motion off, or no Candor CSS), set from the duration the page resolved: a brand's longer shake,
|
|
47
|
+
// or the speed token, isn't cut short.
|
|
48
|
+
const shake = window.setTimeout(() => setShaking(false), (animationMs(box.current) ?? 400) + 100);
|
|
31
49
|
const hold = window.setTimeout(() => setHeld(false), 1800);
|
|
32
50
|
return () => {
|
|
33
51
|
window.clearTimeout(shake);
|
|
@@ -53,6 +71,9 @@ function Locked({ reason, inline = false, children, className, }) {
|
|
|
53
71
|
block(e);
|
|
54
72
|
},
|
|
55
73
|
});
|
|
56
|
-
return (_jsxs("span", {
|
|
74
|
+
return (_jsxs("span", { ref: box, "data-slot": "locked", onAnimationEnd: (e) => {
|
|
75
|
+
if (e.target === e.currentTarget)
|
|
76
|
+
setShaking(false);
|
|
77
|
+
}, "data-pressed": held ? "" : undefined, style: refuseMotion, className: cn("relative inline-flex flex-col items-start gap-1.5", shaking && "cd-refuse", className), children: [inline ? (control) : (_jsxs(Tooltip, { open: shown || held, onOpenChange: setShown, children: [_jsx(TooltipTrigger, { asChild: true, children: control }), _jsx(TooltipContent, { children: reason })] })), _jsxs("span", { id: id, "data-slot": "locked-reason", style: reasonMotion, className: cn("items-center gap-1 text-xs text-muted-foreground cd-tint", inline ? "inline-flex" : "sr-only", held && "text-foreground"), children: [_jsx(LockKeyhole, { "aria-hidden": true, className: "size-3" }), reason] })] }));
|
|
57
78
|
}
|
|
58
79
|
export { Locked };
|
|
@@ -80,6 +80,6 @@ function RelativeTime({ date, bare = false, format = "relative", className, }) {
|
|
|
80
80
|
// A bare time ("2 days") is read aloud without its direction, so a screen reader hears the whole phrase ("2 days ago")
|
|
81
81
|
// while the page keeps the short one. "always" keeps the number ("1 day ago", not "yesterday"), so the spoken words match the shown ones.
|
|
82
82
|
const spoken = bare && format === "relative" && current !== null ? formatRelative(d, new Date(current), { locale: fmt.tag, justNow: t("justNow"), numeric: "always" }) : text;
|
|
83
|
-
return (_jsx("time", { dateTime: d.toISOString(), title: current === null ? undefined : fmt.date(d, "datetime"), className: className, children: spoken === text ? (text) : (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: text }), _jsx("span", { className: "sr-only", children: spoken })] })) }));
|
|
83
|
+
return (_jsx("time", { "data-slot": "relative-time", dateTime: d.toISOString(), title: current === null ? undefined : fmt.date(d, "datetime"), className: className, children: spoken === text ? (text) : (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: text }), _jsx("span", { className: "sr-only", children: spoken })] })) }));
|
|
84
84
|
}
|
|
85
85
|
export { RelativeTime };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
1
|
import { type RunStatus } from "../lib/status.js";
|
|
3
2
|
/**
|
|
4
3
|
* A small status mark. Running and waiting pulse, since they are the two
|
|
@@ -6,14 +5,26 @@ import { type RunStatus } from "../lib/status.js";
|
|
|
6
5
|
* or failed while on screen, the new dot pops into place (too small for a drawn
|
|
7
6
|
* mark); a dot that mounts finished is still. The label is always available
|
|
8
7
|
* to assistive tech; pass `showLabel` to print it too.
|
|
8
|
+
*
|
|
9
|
+
* Live means heard from: with `lastHeardAt`, a running dot pulses only while the
|
|
10
|
+
* run has been heard from within `staleAfter`, and is still (with its word)
|
|
11
|
+
* once it hasn't, or before the page has a clock. Without it the app isn't
|
|
12
|
+
* tracking freshness, and running pulses as before.
|
|
9
13
|
*/
|
|
10
|
-
declare function StatusDot({ status, showLabel, label, className, }: {
|
|
14
|
+
declare function StatusDot({ status, showLabel, label, lastHeardAt, staleAfter, className, }: {
|
|
11
15
|
/** Which status to show; running and waiting pulse. */
|
|
12
16
|
status: RunStatus;
|
|
13
17
|
/** Print the status name next to the dot. */
|
|
14
18
|
showLabel?: boolean | undefined;
|
|
15
19
|
/** Words to use instead of the status name. */
|
|
16
20
|
label?: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* When the run was last heard from (an event, a heartbeat). A running dot pulses only while this is within
|
|
23
|
+
* `staleAfter`; past it, or when the time can't be read, the dot is still. Left out, the dot doesn't claim to know.
|
|
24
|
+
*/
|
|
25
|
+
lastHeardAt?: Date | string | undefined;
|
|
26
|
+
/** How long a running run may be silent, in milliseconds, before the dot stops pulsing. Two minutes by default. */
|
|
27
|
+
staleAfter?: number | undefined;
|
|
17
28
|
className?: string | undefined;
|
|
18
|
-
}):
|
|
29
|
+
}): import("react").JSX.Element;
|
|
19
30
|
export { StatusDot };
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import * as React from "react";
|
|
4
3
|
import { cn } from "../lib/utils.js";
|
|
5
|
-
import {
|
|
4
|
+
import { useNow } from "./relative-time.js";
|
|
5
|
+
import { motion, useSettled } from "../lib/motion.js";
|
|
6
|
+
import { runLiveness, STALE_AFTER, useStatusLabel } from "../lib/status.js";
|
|
7
|
+
/** The pulse while a run is heard from. */
|
|
8
|
+
const liveMotion = motion("status-dot", "live", "live");
|
|
9
|
+
/** The dot popping into its final colour when the run settles on screen. */
|
|
10
|
+
const settleMotion = motion("status-dot", "settle", "pop");
|
|
6
11
|
const dotColor = {
|
|
7
12
|
idle: "bg-faint-foreground/60",
|
|
8
13
|
queued: "bg-faint-foreground",
|
|
@@ -14,35 +19,25 @@ const dotColor = {
|
|
|
14
19
|
cancelled: "bg-faint-foreground/60",
|
|
15
20
|
skipped: "bg-faint-foreground/40",
|
|
16
21
|
};
|
|
17
|
-
const finished = (status) => status === "succeeded" || status === "failed";
|
|
18
|
-
const over = (status) => finished(status) || status === "cancelled" || status === "skipped";
|
|
19
|
-
/**
|
|
20
|
-
* How many times the status reached done or failed from an unfinished one while the dot was on screen; zero when it
|
|
21
|
-
* mounted finished, so nothing moves for a change the reader never saw.
|
|
22
|
-
*/
|
|
23
|
-
function useSettled(status) {
|
|
24
|
-
const [settled, setSettled] = React.useState(0);
|
|
25
|
-
const previous = React.useRef(status);
|
|
26
|
-
React.useLayoutEffect(() => {
|
|
27
|
-
const was = previous.current;
|
|
28
|
-
previous.current = status;
|
|
29
|
-
if (was !== status && !over(was) && finished(status))
|
|
30
|
-
setSettled((n) => n + 1);
|
|
31
|
-
}, [status]);
|
|
32
|
-
return settled;
|
|
33
|
-
}
|
|
34
22
|
/**
|
|
35
23
|
* A small status mark. Running and waiting pulse, since they are the two
|
|
36
24
|
* states a person may need to watch or act on. When one of them reaches done
|
|
37
25
|
* or failed while on screen, the new dot pops into place (too small for a drawn
|
|
38
26
|
* mark); a dot that mounts finished is still. The label is always available
|
|
39
27
|
* to assistive tech; pass `showLabel` to print it too.
|
|
28
|
+
*
|
|
29
|
+
* Live means heard from: with `lastHeardAt`, a running dot pulses only while the
|
|
30
|
+
* run has been heard from within `staleAfter`, and is still (with its word)
|
|
31
|
+
* once it hasn't, or before the page has a clock. Without it the app isn't
|
|
32
|
+
* tracking freshness, and running pulses as before.
|
|
40
33
|
*/
|
|
41
|
-
function StatusDot({ status, showLabel = false, label, className, }) {
|
|
34
|
+
function StatusDot({ status, showLabel = false, label, lastHeardAt, staleAfter = STALE_AFTER, className, }) {
|
|
42
35
|
const statusLabel = useStatusLabel();
|
|
43
36
|
const text = label ?? statusLabel(status);
|
|
44
|
-
const
|
|
37
|
+
const now = useNow();
|
|
38
|
+
const liveness = runLiveness({ status, lastHeardAt }, now, staleAfter);
|
|
39
|
+
const live = (status === "running" || status === "waiting") && liveness === "live";
|
|
45
40
|
const settled = useSettled(status);
|
|
46
|
-
return (_jsxs("span", { "data-slot": "status-dot", "data-status": status, "data-settled": settled > 0 ? "" : undefined, className: cn("inline-flex items-center gap-1.5 text-xs text-muted-foreground", className), children: [_jsx("span", { "aria-hidden": true, className: cn("size-2 shrink-0 rounded-full", dotColor[status], live && "
|
|
41
|
+
return (_jsxs("span", { "data-slot": "status-dot", "data-status": status, "data-settled": settled > 0 ? "" : undefined, "data-liveness": lastHeardAt === undefined ? undefined : liveness, className: cn("inline-flex items-center gap-1.5 text-xs text-muted-foreground", className), children: [_jsx("span", { "aria-hidden": true, style: live ? liveMotion : settleMotion, className: cn("size-2 shrink-0 rounded-full", dotColor[status], live && "cd-live-dot", settled > 0 && "cd-pop") }, settled), showLabel ? _jsx("span", { children: text }) : _jsx("span", { className: "sr-only", children: text })] }));
|
|
47
42
|
}
|
|
48
43
|
export { StatusDot };
|
|
@@ -18,7 +18,8 @@ declare function StepMark({ status, }: {
|
|
|
18
18
|
/**
|
|
19
19
|
* An ordered list of steps joined by a rail: an agent's plan, its reasoning,
|
|
20
20
|
* a run's stages. The rail fills to each step as it finishes, never ahead of
|
|
21
|
-
* it, and a step that finishes while on screen draws its mark.
|
|
21
|
+
* it, and a step that finishes while on screen draws its mark. A step added
|
|
22
|
+
* after the list first drew rises in; the steps it loaded with are still.
|
|
22
23
|
*/
|
|
23
24
|
declare function StepList({ steps, className, }: {
|
|
24
25
|
/** The steps in order, each with a status, and optional detail and meta. */
|
package/dist/blocks/step-list.js
CHANGED
|
@@ -3,33 +3,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { CircleSlash, Hand, LoaderCircle } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { motion, useArrived, useSettled } from "../lib/motion.js";
|
|
6
7
|
import { useStatusLabel } from "../lib/status.js";
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function useSettled(status) {
|
|
15
|
-
const [settled, setSettled] = React.useState(0);
|
|
16
|
-
const previous = React.useRef(status);
|
|
17
|
-
React.useLayoutEffect(() => {
|
|
18
|
-
const was = previous.current;
|
|
19
|
-
previous.current = status;
|
|
20
|
-
if (was !== status && !over(was) && finished(status))
|
|
21
|
-
setSettled((n) => n + 1);
|
|
22
|
-
}, [status]);
|
|
23
|
-
return settled;
|
|
24
|
-
}
|
|
8
|
+
/** A step's tick or cross, drawn when its status settles on screen. */
|
|
9
|
+
const markMotion = motion("step-list", "mark", "mark");
|
|
10
|
+
/** The rail growing to a step that finished on screen. */
|
|
11
|
+
const railMotion = motion("step-list", "rail", "fill");
|
|
12
|
+
/** A step added after the list first drew. */
|
|
13
|
+
const arriveMotion = motion("step-list", "arrive", "arrive");
|
|
14
|
+
const workingMotion = motion("step-list", "working", "live", { dur: 1000 });
|
|
25
15
|
/** A tick, drawn along its path when `draw` is above zero (and again each time it changes). */
|
|
26
16
|
function Tick({ draw }) {
|
|
27
|
-
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: _jsx("path", { d: "M20 6 9 17l-5-5", pathLength: 1, className: cn(draw > 0 && "
|
|
17
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: _jsx("path", { d: "M20 6 9 17l-5-5", pathLength: 1, style: markMotion, className: cn(draw > 0 && "cd-mark") }) }, draw));
|
|
28
18
|
}
|
|
29
19
|
/** A cross, its two strokes drawn one after the other when `draw` is above zero. */
|
|
30
20
|
function Cross({ draw }) {
|
|
31
|
-
const drawn = draw > 0 && "
|
|
32
|
-
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: [_jsx("path", { d: "M18 6 6 18", pathLength: 1, className: cn(drawn) }), _jsx("path", { d: "m6 6 12 12", pathLength: 1, className: cn(drawn, drawn && "[
|
|
21
|
+
const drawn = draw > 0 && "cd-mark";
|
|
22
|
+
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: "size-3", children: [_jsx("path", { d: "M18 6 6 18", pathLength: 1, style: markMotion, className: cn(drawn) }), _jsx("path", { d: "m6 6 12 12", pathLength: 1, style: markMotion, className: cn(drawn, drawn && "[--cd-mark-step:1]") })] }, draw));
|
|
33
23
|
}
|
|
34
24
|
/**
|
|
35
25
|
* A step's status as a mark. When the status reaches done or failed while the mark is on screen, the spinner
|
|
@@ -37,14 +27,14 @@ function Cross({ draw }) {
|
|
|
37
27
|
*/
|
|
38
28
|
function StepMark({ status, }) {
|
|
39
29
|
const settled = useSettled(status);
|
|
40
|
-
const base = "relative z-10 flex size-5 shrink-0 items-center justify-center rounded-full
|
|
30
|
+
const base = "relative z-10 flex size-5 shrink-0 items-center justify-center rounded-full cd-tint";
|
|
41
31
|
switch (status) {
|
|
42
32
|
case "succeeded":
|
|
43
33
|
return (_jsx("span", { className: cn(base, "bg-success text-on-success"), "data-settled": settled > 0 ? "" : undefined, children: _jsx(Tick, { draw: settled }) }));
|
|
44
34
|
case "failed":
|
|
45
35
|
return (_jsx("span", { className: cn(base, "bg-destructive text-on-destructive"), "data-settled": settled > 0 ? "" : undefined, children: _jsx(Cross, { draw: settled }) }));
|
|
46
36
|
case "running":
|
|
47
|
-
return (_jsx("span", { className: cn(base, "bg-accent-weak text-accent-text"), children: _jsx(LoaderCircle, { className: "size-3.5
|
|
37
|
+
return (_jsx("span", { className: cn(base, "bg-accent-weak text-accent-text"), children: _jsx(LoaderCircle, { style: workingMotion, className: "size-3.5 cd-live-spin", strokeWidth: 2.5 }) }));
|
|
48
38
|
case "waiting":
|
|
49
39
|
return (_jsx("span", { className: cn(base, "bg-warning-weak text-warning-text"), children: _jsx(Hand, { className: "size-3", strokeWidth: 2.5 }) }));
|
|
50
40
|
case "blocked":
|
|
@@ -56,18 +46,20 @@ function StepMark({ status, }) {
|
|
|
56
46
|
return (_jsx("span", { className: cn(base, "bg-background"), children: _jsx("span", { className: "size-2 rounded-full border-[1.5px] border-faint-foreground/70" }) }));
|
|
57
47
|
}
|
|
58
48
|
}
|
|
59
|
-
function StepRow({ step,
|
|
49
|
+
function StepRow({ step, arrived, last, }) {
|
|
60
50
|
const statusLabel = useStatusLabel();
|
|
61
51
|
const done = step.status === "succeeded";
|
|
62
52
|
const settled = useSettled(step.status);
|
|
63
|
-
return (_jsxs("li", { className: "relative flex gap-3 pb-4 last:pb-0
|
|
53
|
+
return (_jsxs("li", { style: arriveMotion, className: cn("relative flex gap-3 pb-4 last:pb-0", arrived && "cd-arrive"), children: [!last && (_jsx("span", { "aria-hidden": true, className: "absolute top-5 bottom-0 start-[9.5px] w-px bg-input", children: done && (_jsx("span", { "data-slot": "step-rail-fill", style: railMotion, className: cn("absolute inset-0 origin-top bg-success/50", settled > 0 && "cd-fill-grow [--cd-fill-from:1_0]") }, settled)) })), _jsx(StepMark, { status: step.status }), _jsxs("div", { className: "min-w-0 flex-1 pt-px", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsxs("p", { className: cn("text-sm font-medium", step.status === "idle" && "text-muted-foreground", (step.status === "skipped" || step.status === "cancelled") && "text-faint-foreground line-through"), children: [step.title, _jsxs("span", { className: "sr-only", children: [" \u2014 ", statusLabel(step.status)] })] }), step.meta && _jsx("span", { className: "shrink-0 text-xs tabular-nums text-faint-foreground", children: step.meta })] }), step.detail && _jsx("div", { className: "mt-1 text-[0.8125rem] text-muted-foreground", children: step.detail })] })] }));
|
|
64
54
|
}
|
|
65
55
|
/**
|
|
66
56
|
* An ordered list of steps joined by a rail: an agent's plan, its reasoning,
|
|
67
57
|
* a run's stages. The rail fills to each step as it finishes, never ahead of
|
|
68
|
-
* it, and a step that finishes while on screen draws its mark.
|
|
58
|
+
* it, and a step that finishes while on screen draws its mark. A step added
|
|
59
|
+
* after the list first drew rises in; the steps it loaded with are still.
|
|
69
60
|
*/
|
|
70
61
|
function StepList({ steps, className, }) {
|
|
71
|
-
|
|
62
|
+
const arrived = useArrived(React.useMemo(() => steps.map((s) => s.id), [steps]));
|
|
63
|
+
return (_jsx("ol", { "data-slot": "step-list", className: cn("flex flex-col", className), children: steps.map((step, i) => (_jsx(StepRow, { step: step, arrived: arrived(step.id), last: i === steps.length - 1 }, step.id))) }));
|
|
72
64
|
}
|
|
73
65
|
export { StepList, StepMark };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { motion } from "../lib/motion.js";
|
|
4
|
+
const caretMotion = motion("streaming-text", "caret", "live", { dur: 1000 });
|
|
3
5
|
/**
|
|
4
6
|
* Text that is still arriving. Shows a caret while `streaming`, and announces
|
|
5
7
|
* politely so screen readers hear the finished text rather than every token.
|
|
6
8
|
*/
|
|
7
9
|
function StreamingText({ text, streaming = false, className, }) {
|
|
8
|
-
return (_jsxs("div", { "data-slot": "streaming-text", "aria-live": "polite", "aria-busy": streaming, className: cn("whitespace-pre-wrap text-[0.9375rem] leading-relaxed text-foreground", className), children: [text, streaming && (_jsx("span", { "aria-hidden": true, className: "ms-0.5 inline-block h-[1.05em] w-[2px] translate-y-[0.18em] rounded-full bg-primary
|
|
10
|
+
return (_jsxs("div", { "data-slot": "streaming-text", "aria-live": "polite", "aria-busy": streaming, className: cn("whitespace-pre-wrap text-[0.9375rem] leading-relaxed text-foreground", className), children: [text, streaming && (_jsx("span", { "aria-hidden": true, style: caretMotion, className: "ms-0.5 inline-block h-[1.05em] w-[2px] translate-y-[0.18em] rounded-full bg-primary cd-live-caret" }))] }));
|
|
9
11
|
}
|
|
10
12
|
/** A label that shimmers while an agent is working ("Thinking", "Reading the docs"). */
|
|
11
13
|
function ShimmerLabel({ children, className, }) {
|
package/dist/blocks/surface.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const surfaceVariants: (props?: ({
|
|
4
|
-
tone?: "plain" | "
|
|
4
|
+
tone?: "plain" | "subtle" | "muted" | "raised" | "floating" | "accent" | null | undefined;
|
|
5
5
|
radius?: "md" | "lg" | "xl" | null | undefined;
|
|
6
6
|
pad?: "none" | "md" | "sm" | "lg" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
@@ -22,13 +22,18 @@ export declare function fileIcon(a: Pick<Attachment, "name" | "mediaType">): Luc
|
|
|
22
22
|
/**
|
|
23
23
|
* A file waiting to be sent, as a chip in the composer: its name, then upload progress, an error, or its size, and a
|
|
24
24
|
* remove button. Images show their preview when the app gives one.
|
|
25
|
+
*
|
|
26
|
+
* @motion fill:progress The upload bar under the chip moves from the old reported fraction to the new one as the service
|
|
27
|
+
* reports it. It is drawn at its first value in place, never grown from empty, and a file with no progress draws no bar.
|
|
25
28
|
*/
|
|
26
|
-
declare function AttachmentChip({ attachment: a, onRemove, className, }: {
|
|
29
|
+
declare function AttachmentChip({ attachment: a, onRemove, className, style, }: {
|
|
27
30
|
/** The file, with progress (0–1) while uploading or an error if it failed. */
|
|
28
31
|
attachment: Attachment;
|
|
29
32
|
/** Shows a remove button. */
|
|
30
33
|
onRemove?: ((id: string) => void) | undefined;
|
|
31
34
|
className?: string | undefined;
|
|
35
|
+
/** Inline style for the chip's outer element, such as a motion chain from `motion()`. */
|
|
36
|
+
style?: React.CSSProperties | undefined;
|
|
32
37
|
}): React.JSX.Element;
|
|
33
38
|
/**
|
|
34
39
|
* A sent file, as a tile in a message: an image preview when the app supplied one, else an icon for its type, with
|
|
@@ -4,6 +4,9 @@ import * as React from "react";
|
|
|
4
4
|
import { File, FileArchive, FileCode, FileImage, FileSpreadsheet, FileText, Music, Paperclip, Video, X } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { useFormat, useStrings } from "../blocks/locale.js";
|
|
7
|
+
import { motion } from "../lib/motion.js";
|
|
8
|
+
/** The upload bar moving between measured byte counts: 2.2's 300 ms. */
|
|
9
|
+
const progressMotion = motion("attachment-preview", "progress", "fill", { dur: 300 });
|
|
7
10
|
/** AttachmentPreview's fixed words; override them under `attachmentPreview` in LocaleProvider's `strings`. */
|
|
8
11
|
export const attachmentPreviewStrings = {
|
|
9
12
|
remove: "Remove {name}",
|
|
@@ -53,12 +56,15 @@ const thumbnail = (a) => (a.previewUrl && (a.mediaType ?? "").startsWith("image/
|
|
|
53
56
|
/**
|
|
54
57
|
* A file waiting to be sent, as a chip in the composer: its name, then upload progress, an error, or its size, and a
|
|
55
58
|
* remove button. Images show their preview when the app gives one.
|
|
59
|
+
*
|
|
60
|
+
* @motion fill:progress The upload bar under the chip moves from the old reported fraction to the new one as the service
|
|
61
|
+
* reports it. It is drawn at its first value in place, never grown from empty, and a file with no progress draws no bar.
|
|
56
62
|
*/
|
|
57
|
-
function AttachmentChip({ attachment: a, onRemove, className, }) {
|
|
63
|
+
function AttachmentChip({ attachment: a, onRemove, className, style, }) {
|
|
58
64
|
const t = useStrings("attachmentPreview", attachmentPreviewStrings);
|
|
59
65
|
const fmt = useFormat();
|
|
60
66
|
const thumb = thumbnail(a);
|
|
61
|
-
return (_jsxs("span", { "data-slot": "attachment-chip", className: cn("relative inline-flex max-w-56 items-center gap-1.5 overflow-hidden rounded-md border bg-card py-1 pe-1 ps-2 text-xs", a.error && "border-destructive/50", className), children: [thumb ? _jsx("img", { src: thumb, alt: "", className: "size-4 shrink-0 rounded-xs object-cover" }) : _jsx(Paperclip, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: a.name }), _jsx("span", { className: cn("shrink-0 tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ? a.error : a.progress !== undefined ? fmt.number(Math.round(a.progress * 100) / 100, { style: "percent" }) : formatFileSize(a.size, fmt.number, t) }), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: a.name }), onClick: () => onRemove(a.id), className: "relative after:absolute after:-inset-1 rounded-sm p-0.5 text-faint-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { className: "size-3" }) })), a.progress !== undefined && !a.error && _jsx("span", { "aria-hidden": true, className: "absolute bottom-0 start-0 h-0.5 bg-primary
|
|
67
|
+
return (_jsxs("span", { "data-slot": "attachment-chip", style: style, className: cn("relative inline-flex max-w-56 items-center gap-1.5 overflow-hidden rounded-md border bg-card py-1 pe-1 ps-2 text-xs", a.error && "border-destructive/50", className), children: [thumb ? _jsx("img", { src: thumb, alt: "", className: "size-4 shrink-0 rounded-xs object-cover" }) : _jsx(Paperclip, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: a.name }), _jsx("span", { className: cn("shrink-0 tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ? a.error : a.progress !== undefined ? fmt.number(Math.round(a.progress * 100) / 100, { style: "percent" }) : formatFileSize(a.size, fmt.number, t) }), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: a.name }), onClick: () => onRemove(a.id), className: "relative after:absolute after:-inset-1 rounded-sm p-0.5 text-faint-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { className: "size-3" }) })), a.progress !== undefined && !a.error && _jsx("span", { "aria-hidden": true, className: "absolute bottom-0 start-0 h-0.5 bg-primary cd-fill", style: { ...progressMotion, width: `${a.progress * 100}%` } })] }));
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* A sent file, as a tile in a message: an image preview when the app supplied one, else an icon for its type, with
|
|
@@ -72,7 +78,7 @@ function AttachmentPreview({ attachment: a, onOpen, className, }) {
|
|
|
72
78
|
const size = formatFileSize(a.size, fmt.number, t);
|
|
73
79
|
const body = (_jsxs(_Fragment, { children: [thumb ? (_jsx("img", { src: thumb, alt: "", className: "h-24 w-full rounded-md bg-muted object-cover" })) : (_jsx("span", { "aria-hidden": true, className: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(FileGlyph, { attachment: a }) })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm", children: a.name }), (size || a.error) && _jsx("span", { className: cn("block text-xs tabular-nums", a.error ? "text-destructive-text" : "text-faint-foreground"), children: a.error ?? size })] })] }));
|
|
74
80
|
const frame = cn("flex w-56 max-w-full gap-2.5 rounded-lg border bg-card p-2 text-start", thumb ? "flex-col" : "items-center", className);
|
|
75
|
-
return onOpen ? (_jsx("button", { type: "button", "data-slot": "attachment-preview", "aria-label": t("open", { name: a.name }), onClick: () => onOpen(a), className: cn(frame, "outline-none
|
|
81
|
+
return onOpen ? (_jsx("button", { type: "button", "data-slot": "attachment-preview", "aria-label": t("open", { name: a.name }), onClick: () => onOpen(a), className: cn(frame, "outline-none cd-tint hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/80"), children: body })) : (_jsx("div", { "data-slot": "attachment-preview", className: frame, children: body }));
|
|
76
82
|
}
|
|
77
83
|
/** Several sent files, as a wrapping row of tiles, announced as a list. */
|
|
78
84
|
function AttachmentList({ attachments, onOpen, className, }) {
|
|
@@ -5,6 +5,8 @@ import { ChevronRight, LayoutTemplate } from "lucide-react";
|
|
|
5
5
|
import { DegradedPanel } from "../blocks/degraded-panel.js";
|
|
6
6
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
7
7
|
import { useStrings } from "../blocks/locale.js";
|
|
8
|
+
import { motion } from "../lib/motion.js";
|
|
9
|
+
const turnMotion = motion("card-registry", "open", "reveal");
|
|
8
10
|
/** CardRegistry's fixed words; override them under `cardRegistry` in LocaleProvider's `strings`. */
|
|
9
11
|
export const cardRegistryStrings = {
|
|
10
12
|
showAsText: "Show it as text",
|
|
@@ -32,7 +34,7 @@ class CardBoundary extends React.Component {
|
|
|
32
34
|
}
|
|
33
35
|
function Summary({ text, }) {
|
|
34
36
|
const t = useStrings("cardRegistry", cardRegistryStrings);
|
|
35
|
-
return (_jsxs(Collapsible, { className: "group/summary", children: [_jsxs(CollapsibleTrigger, { className: "relative after:absolute after:inset-x-0 after:-inset-y-1 inline-flex items-center gap-1 text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80 rounded-sm", children: [_jsx(ChevronRight, { "aria-hidden": true, className: "rtl:-scale-x-100 size-3
|
|
37
|
+
return (_jsxs(Collapsible, { className: "group/summary", children: [_jsxs(CollapsibleTrigger, { className: "relative after:absolute after:inset-x-0 after:-inset-y-1 inline-flex items-center gap-1 text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80 rounded-sm", children: [_jsx(ChevronRight, { "aria-hidden": true, style: turnMotion, className: "rtl:-scale-x-100 size-3 cd-reveal-turn group-data-[state=open]/summary:[transform:rotate(90deg)]" }), " ", t("showAsText")] }), _jsx(CollapsibleContent, { style: turnMotion, children: _jsx("p", { className: "mt-1.5 rounded-md bg-sunken px-3 py-2 text-sm whitespace-pre-wrap", children: text }) })] }));
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
40
|
* Renders one card from the registry. Three honest fallbacks, each with the
|
|
@@ -44,13 +46,13 @@ function Card({ name, props, summary, }) {
|
|
|
44
46
|
const cards = React.useContext(Registry);
|
|
45
47
|
const def = cards[name];
|
|
46
48
|
if (!def) {
|
|
47
|
-
return (_jsxs("div", { "data-slot": "card", "data-card": name, "data-state": "unknown", className: "flex flex-col gap-1.5 rounded-lg border border-dashed px-3 py-2.5", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-sm text-muted-foreground", children: [_jsx(LayoutTemplate, { "aria-hidden": true, className: "size-3.5" }), " ", t("unknown", { name })] }), _jsx(Summary, { text: summary })] }));
|
|
49
|
+
return (_jsxs("div", { "data-slot": "card-registry", "data-card": name, "data-state": "unknown", className: "flex flex-col gap-1.5 rounded-lg border border-dashed px-3 py-2.5", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-sm text-muted-foreground", children: [_jsx(LayoutTemplate, { "aria-hidden": true, className: "size-3.5" }), " ", t("unknown", { name })] }), _jsx(Summary, { text: summary })] }));
|
|
48
50
|
}
|
|
49
51
|
const problem = def.check(props);
|
|
50
52
|
if (problem) {
|
|
51
|
-
return (_jsxs("div", { "data-slot": "card", "data-card": name, "data-state": "invalid", className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason: t("invalid", { label: def.label.toLowerCase(), problem }) }), _jsx(Summary, { text: summary })] }));
|
|
53
|
+
return (_jsxs("div", { "data-slot": "card-registry", "data-card": name, "data-state": "invalid", className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason: t("invalid", { label: def.label.toLowerCase(), problem }) }), _jsx(Summary, { text: summary })] }));
|
|
52
54
|
}
|
|
53
|
-
return (_jsx("div", { "data-slot": "card", "data-card": name, "data-state": "ready", children: _jsx(CardBoundary, { fallback: _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason: t("failed", { label: def.label.toLowerCase() }) }), _jsx(Summary, { text: summary })] }), children: def.render(props) }) }));
|
|
55
|
+
return (_jsx("div", { "data-slot": "card-registry", "data-card": name, "data-state": "ready", children: _jsx(CardBoundary, { fallback: _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(DegradedPanel, { reason: t("failed", { label: def.label.toLowerCase() }) }), _jsx(Summary, { text: summary })] }), children: def.render(props) }) }));
|
|
54
56
|
}
|
|
55
57
|
/* ---------------------------------------------------------------- small checking helpers */
|
|
56
58
|
export const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
package/dist/chat/citation.js
CHANGED
|
@@ -52,9 +52,9 @@ function SourceDetail({ source, }) {
|
|
|
52
52
|
function Citation({ source, marker, }) {
|
|
53
53
|
const t = useStrings("citation", citationStrings);
|
|
54
54
|
if (!source) {
|
|
55
|
-
return _jsxs("sup", { className: "text-faint-foreground", children: ["[", marker ?? "?", "]"] });
|
|
55
|
+
return _jsxs("sup", { "data-slot": "citation", className: "text-faint-foreground", children: ["[", marker ?? "?", "]"] });
|
|
56
56
|
}
|
|
57
|
-
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": t("sourceLabel", { marker: source.marker, label: source.label }), className: cn("mx-0.5 inline-flex h-4 min-w-4 -translate-y-px items-center justify-center rounded-sm px-1 align-middle text-2xs font-semibold tabular-nums
|
|
57
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "data-slot": "citation", "aria-label": t("sourceLabel", { marker: source.marker, label: source.label }), className: cn("mx-0.5 inline-flex h-4 min-w-4 -translate-y-px items-center justify-center rounded-sm px-1 align-middle text-2xs font-semibold tabular-nums cd-tint outline-none focus-visible:ring-[3px] focus-visible:ring-ring/80", source.provisional
|
|
58
58
|
? "border border-dashed border-provisional/60 text-provisional hover:bg-muted"
|
|
59
59
|
: "bg-accent-weak text-accent-text hover:bg-primary hover:text-primary-foreground"), children: source.marker }) }), _jsx(PopoverContent, { align: "start", className: "w-80 max-w-[calc(100vw-2rem)] p-3", children: _jsx(SourceDetail, { source: source }) })] }));
|
|
60
60
|
}
|
|
@@ -67,7 +67,7 @@ function SourceList({ sources, className, }) {
|
|
|
67
67
|
.sort((a, b) => a.marker - b.marker)
|
|
68
68
|
.map((s) => {
|
|
69
69
|
const { Icon } = kindOf(s.kind, t);
|
|
70
|
-
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("relative after:absolute after:inset-x-0 after:-inset-y-0.5 inline-flex max-w-56 items-center gap-1.5 rounded-md border bg-card py-0.5 pe-2 ps-1 text-xs text-muted-foreground
|
|
70
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("relative after:absolute after:inset-x-0 after:-inset-y-0.5 inline-flex max-w-56 items-center gap-1.5 rounded-md border bg-card py-0.5 pe-2 ps-1 text-xs text-muted-foreground cd-tint outline-none hover:border-border-strong hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/80", s.provisional && "border-dashed"), children: [_jsx("span", { className: "flex h-4 min-w-4 items-center justify-center rounded-sm bg-muted px-1 text-2xs font-semibold tabular-nums", children: s.marker }), _jsx(Icon, { "aria-hidden": true, className: "size-3 shrink-0" }), _jsx("span", { className: "truncate", children: s.label })] }) }), _jsx(PopoverContent, { align: "start", className: "w-80 max-w-[calc(100vw-2rem)] p-3", children: _jsx(SourceDetail, { source: s }) })] }, s.id));
|
|
71
71
|
})] }));
|
|
72
72
|
}
|
|
73
73
|
export { Citation, SourceList, SourceDetail };
|