@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
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
|
|
|
@@ -5,7 +5,10 @@ import { cn } from "../lib/utils.js";
|
|
|
5
5
|
import { useCopy } from "../lib/use-copy.js";
|
|
6
6
|
import { useStrings } from "./locale.js";
|
|
7
7
|
import { motion } from "../lib/motion.js";
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* @motion pop:tick After the person copies, a tick (or a cross, if the browser refused the clipboard) pops in on the
|
|
10
|
+
* button once the browser has answered. Nothing shows before the answer, and under reduced motion it fades in.
|
|
11
|
+
*/
|
|
9
12
|
const tickMotion = motion("copy-field", "tick", "pop");
|
|
10
13
|
/** CopyField's fixed words; override them under `copyField` in LocaleProvider's `strings`. */
|
|
11
14
|
export const copyFieldStrings = {
|
|
@@ -5,6 +5,10 @@ import { cn } from "../lib/utils.js";
|
|
|
5
5
|
import { Button } from "../ui/button.js";
|
|
6
6
|
import { useStrings } from "./locale.js";
|
|
7
7
|
import { motion } from "../lib/motion.js";
|
|
8
|
+
/**
|
|
9
|
+
* @motion live:busy While a retry is under way, the retry icon turns in place and the button says it is retrying. It
|
|
10
|
+
* stops when the app ends the retry, and under reduced motion the icon is still.
|
|
11
|
+
*/
|
|
8
12
|
const spinMotion = motion("degraded-panel", "busy", "live", { dur: 1000 });
|
|
9
13
|
/** DegradedPanel's fixed words; override them under `degradedPanel` in LocaleProvider's `strings`. */
|
|
10
14
|
export const degradedPanelStrings = {
|
package/dist/blocks/locked.js
CHANGED
|
@@ -5,8 +5,27 @@ import { LockKeyhole } from "lucide-react";
|
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
7
7
|
import { motion } from "../lib/motion.js";
|
|
8
|
-
/**
|
|
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
|
+
/**
|
|
19
|
+
* @motion refuse:refuse A press on a locked control shows the reason it is locked; under the expressive setting the
|
|
20
|
+
* control also shakes once. The control stays locked and does nothing it would have done unlocked. Nothing plays
|
|
21
|
+
* until the person presses it.
|
|
22
|
+
*/
|
|
9
23
|
const refuseMotion = motion("locked", "refuse", "refuse");
|
|
24
|
+
/**
|
|
25
|
+
* @motion tint:reason When the person presses a locked control, its reason changes from muted to full-strength text
|
|
26
|
+
* so it can be read, and goes back to muted for a moment after the press. Screen readers always have the reason,
|
|
27
|
+
* whether or not it is shown.
|
|
28
|
+
*/
|
|
10
29
|
const reasonMotion = motion("locked", "reason", "tint", { dur: 200 });
|
|
11
30
|
/**
|
|
12
31
|
* The honest-state rule: a control that cannot be used says why.
|
|
@@ -28,10 +47,14 @@ function Locked({ reason, inline = false, children, className, }) {
|
|
|
28
47
|
const [press, setPress] = React.useState(0);
|
|
29
48
|
const [shaking, setShaking] = React.useState(false);
|
|
30
49
|
const [held, setHeld] = React.useState(false);
|
|
50
|
+
const box = React.useRef(null);
|
|
31
51
|
React.useEffect(() => {
|
|
32
52
|
if (press === 0)
|
|
33
53
|
return;
|
|
34
|
-
|
|
54
|
+
// The shake ends when its animation does (onAnimationEnd below). This timer is only the fallback for when no
|
|
55
|
+
// animation runs (motion off, or no Candor CSS), set from the duration the page resolved: a brand's longer shake,
|
|
56
|
+
// or the speed token, isn't cut short.
|
|
57
|
+
const shake = window.setTimeout(() => setShaking(false), (animationMs(box.current) ?? 400) + 100);
|
|
35
58
|
const hold = window.setTimeout(() => setHeld(false), 1800);
|
|
36
59
|
return () => {
|
|
37
60
|
window.clearTimeout(shake);
|
|
@@ -57,6 +80,9 @@ function Locked({ reason, inline = false, children, className, }) {
|
|
|
57
80
|
block(e);
|
|
58
81
|
},
|
|
59
82
|
});
|
|
60
|
-
return (_jsxs("span", {
|
|
83
|
+
return (_jsxs("span", { ref: box, "data-slot": "locked", onAnimationEnd: (e) => {
|
|
84
|
+
if (e.target === e.currentTarget)
|
|
85
|
+
setShaking(false);
|
|
86
|
+
}, "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] })] }));
|
|
61
87
|
}
|
|
62
88
|
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 };
|
|
@@ -4,9 +4,16 @@ import { cn } from "../lib/utils.js";
|
|
|
4
4
|
import { useNow } from "./relative-time.js";
|
|
5
5
|
import { motion, useSettled } from "../lib/motion.js";
|
|
6
6
|
import { runLiveness, STALE_AFTER, useStatusLabel } from "../lib/status.js";
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* @motion live:live A running or waiting dot pulses only while the run has been heard from within the allowed
|
|
9
|
+
* silence. Past it, or when the time can't be read, the dot is still rather than claiming the run is alive, and under
|
|
10
|
+
* reduced motion it is still.
|
|
11
|
+
*/
|
|
8
12
|
const liveMotion = motion("status-dot", "live", "live");
|
|
9
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* @motion pop:settle The dot pops into its final colour when a run it was showing as unfinished settles while it is
|
|
15
|
+
* on screen. A dot that is already settled when it appears is drawn in its colour at once.
|
|
16
|
+
*/
|
|
10
17
|
const settleMotion = motion("status-dot", "settle", "pop");
|
|
11
18
|
const dotColor = {
|
|
12
19
|
idle: "bg-faint-foreground/60",
|
package/dist/blocks/step-list.js
CHANGED
|
@@ -5,12 +5,25 @@ import { CircleSlash, Hand, LoaderCircle } from "lucide-react";
|
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
6
|
import { motion, useArrived, useSettled } from "../lib/motion.js";
|
|
7
7
|
import { useStatusLabel } from "../lib/status.js";
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* @motion mark:mark A step's tick or cross is drawn along its path when the step settles while the list is on screen.
|
|
10
|
+
* Steps that were already settled when the list appeared show their mark at once.
|
|
11
|
+
*/
|
|
9
12
|
const markMotion = motion("step-list", "mark", "mark");
|
|
10
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* @motion fill:rail The line leading to a step fills in when that step finishes while the list is on screen, never
|
|
15
|
+
* ahead of it. Lines for steps that were already finished when the list appeared are full at once.
|
|
16
|
+
*/
|
|
11
17
|
const railMotion = motion("step-list", "rail", "fill");
|
|
12
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* @motion arrive:arrive A step rises in when the app adds it after the list first drew. The steps the list was drawn
|
|
20
|
+
* with appear still.
|
|
21
|
+
*/
|
|
13
22
|
const arriveMotion = motion("step-list", "arrive", "arrive");
|
|
23
|
+
/**
|
|
24
|
+
* @motion live:working A step that is running shows a spinner that turns until the app reports it finished or failed.
|
|
25
|
+
* Under reduced motion the spinner is still, and the step's status is still written in words.
|
|
26
|
+
*/
|
|
14
27
|
const workingMotion = motion("step-list", "working", "live", { dur: 1000 });
|
|
15
28
|
/** A tick, drawn along its path when `draw` is above zero (and again each time it changes). */
|
|
16
29
|
function Tick({ draw }) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
3
|
import { motion } from "../lib/motion.js";
|
|
4
|
+
/**
|
|
5
|
+
* @motion live:caret While the text is still streaming in, a caret blinks at its end. It goes when streaming stops,
|
|
6
|
+
* and under reduced motion it is a still bar.
|
|
7
|
+
*/
|
|
4
8
|
const caretMotion = motion("streaming-text", "caret", "live", { dur: 1000 });
|
|
5
9
|
/**
|
|
6
10
|
* Text that is still arriving. Shows a caret while `streaming`, and announces
|
|
@@ -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
|
|
@@ -56,12 +56,15 @@ const thumbnail = (a) => (a.previewUrl && (a.mediaType ?? "").startsWith("image/
|
|
|
56
56
|
/**
|
|
57
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
|
|
58
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.
|
|
59
62
|
*/
|
|
60
|
-
function AttachmentChip({ attachment: a, onRemove, className, }) {
|
|
63
|
+
function AttachmentChip({ attachment: a, onRemove, className, style, }) {
|
|
61
64
|
const t = useStrings("attachmentPreview", attachmentPreviewStrings);
|
|
62
65
|
const fmt = useFormat();
|
|
63
66
|
const thumb = thumbnail(a);
|
|
64
|
-
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 cd-fill", style: { ...progressMotion, width: `${a.progress * 100}%` } })] }));
|
|
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}%` } })] }));
|
|
65
68
|
}
|
|
66
69
|
/**
|
|
67
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
|
|
@@ -6,6 +6,10 @@ 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
8
|
import { motion } from "../lib/motion.js";
|
|
9
|
+
/**
|
|
10
|
+
* @motion reveal:open The text view opens to its full height when the person asks to see the card as text, and the
|
|
11
|
+
* chevron turns with it; closing reverses both. Under reduced motion it snaps open or shut.
|
|
12
|
+
*/
|
|
9
13
|
const turnMotion = motion("card-registry", "open", "reveal");
|
|
10
14
|
/** CardRegistry's fixed words; override them under `cardRegistry` in LocaleProvider's `strings`. */
|
|
11
15
|
export const cardRegistryStrings = {
|
|
@@ -46,13 +50,13 @@ function Card({ name, props, summary, }) {
|
|
|
46
50
|
const cards = React.useContext(Registry);
|
|
47
51
|
const def = cards[name];
|
|
48
52
|
if (!def) {
|
|
49
|
-
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 })] }));
|
|
53
|
+
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 })] }));
|
|
50
54
|
}
|
|
51
55
|
const problem = def.check(props);
|
|
52
56
|
if (problem) {
|
|
53
|
-
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 })] }));
|
|
57
|
+
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 })] }));
|
|
54
58
|
}
|
|
55
|
-
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) }) }));
|
|
59
|
+
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) }) }));
|
|
56
60
|
}
|
|
57
61
|
/* ---------------------------------------------------------------- small checking helpers */
|
|
58
62
|
export const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
package/dist/chat/citation.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ declare function SourceDetail({ source, }: {
|
|
|
28
28
|
* An inline [n]. Opens the source in a popover on click or Enter, so evidence is
|
|
29
29
|
* one step away without leaving the thread. A source the message doesn't list
|
|
30
30
|
* renders as a plain bracket rather than a guess.
|
|
31
|
+
*
|
|
32
|
+
* @motion tint:tint A source chip's border and text colour change when the pointer is over it, and a ring fades in
|
|
33
|
+
* when a citation number or chip takes keyboard focus.
|
|
31
34
|
*/
|
|
32
35
|
declare function Citation({ source, marker, }: {
|
|
33
36
|
/** The source this marker points to; without it the marker says it's unknown. */
|
package/dist/chat/citation.js
CHANGED
|
@@ -48,13 +48,16 @@ function SourceDetail({ source, }) {
|
|
|
48
48
|
* An inline [n]. Opens the source in a popover on click or Enter, so evidence is
|
|
49
49
|
* one step away without leaving the thread. A source the message doesn't list
|
|
50
50
|
* renders as a plain bracket rather than a guess.
|
|
51
|
+
*
|
|
52
|
+
* @motion tint:tint A source chip's border and text colour change when the pointer is over it, and a ring fades in
|
|
53
|
+
* when a citation number or chip takes keyboard focus.
|
|
51
54
|
*/
|
|
52
55
|
function Citation({ source, marker, }) {
|
|
53
56
|
const t = useStrings("citation", citationStrings);
|
|
54
57
|
if (!source) {
|
|
55
|
-
return _jsxs("sup", { className: "text-faint-foreground", children: ["[", marker ?? "?", "]"] });
|
|
58
|
+
return _jsxs("sup", { "data-slot": "citation", className: "text-faint-foreground", children: ["[", marker ?? "?", "]"] });
|
|
56
59
|
}
|
|
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 cd-tint outline-none focus-visible:ring-[3px] focus-visible:ring-ring/80", source.provisional
|
|
60
|
+
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
61
|
? "border border-dashed border-provisional/60 text-provisional hover:bg-muted"
|
|
59
62
|
: "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
63
|
}
|
package/dist/chat/composer.js
CHANGED
|
@@ -8,12 +8,25 @@ import { EntityChip, useEntityKinds } from "./entity-chip.js";
|
|
|
8
8
|
import { AttachmentChip } from "./attachment-preview.js";
|
|
9
9
|
import { useStrings } from "../blocks/locale.js";
|
|
10
10
|
import { motion, useArrived, useLeaving } from "../lib/motion.js";
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* @motion pop:chip A mention or attachment chip pops in when the person adds one while writing, and a chip they
|
|
13
|
+
* remove pops out. Chips that were there when the composer first drew, such as a restored draft, appear still.
|
|
14
|
+
*/
|
|
12
15
|
const chipMotion = motion("composer", "chip", "pop");
|
|
13
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* @motion overlay:menu The mention and command menu rises a little and fades in above the field when the person types
|
|
18
|
+
* the character that opens it, and goes at once when it closes. Under reduced motion it is a short fade.
|
|
19
|
+
*/
|
|
14
20
|
const menuMotion = motion("composer", "menu", "overlay", { variant: "slide" });
|
|
15
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* @motion press:send Send and Stop shrink a little while they are held down and return when let go. A Send that is
|
|
23
|
+
* disabled never moves, and under reduced motion neither changes size.
|
|
24
|
+
*/
|
|
16
25
|
const sendMotion = motion("composer", "send", "press", { amp: "0.9" });
|
|
26
|
+
/**
|
|
27
|
+
* @motion live:sending While the app reports that the message is being sent, a spinner turns in place of Send's
|
|
28
|
+
* arrow. It stops when the app says sending is over, and under reduced motion the spinner is still.
|
|
29
|
+
*/
|
|
17
30
|
const sendingMotion = motion("composer", "sending", "live", { dur: 1000 });
|
|
18
31
|
/** Composer's fixed words; override them under `composer` in LocaleProvider's `strings`. */
|
|
19
32
|
export const composerStrings = {
|
|
@@ -218,7 +231,7 @@ function Composer({ value, onChange, onSubmit, onStop, state = "ready", scope, o
|
|
|
218
231
|
e.preventDefault();
|
|
219
232
|
choose(item);
|
|
220
233
|
}, onMouseMove: () => setActive(i), className: "flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-sm aria-selected:bg-accent", children: item.type === "command" ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "font-mono text-xs text-accent-text", children: ["/", item.command.name] }), item.command.args && _jsx("span", { className: "font-mono text-xs text-faint-foreground", children: item.command.args }), _jsx("span", { className: "ms-auto truncate text-xs text-muted-foreground", children: item.command.description })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex size-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: Icon ? _jsx(Icon, { className: "size-3.5" }) : _jsx("span", { className: "text-xs", children: "@" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: item.option.label }), (item.option.description ?? item.option.entity?.name) !== item.option.label && (item.option.description ?? item.option.entity?.name) && (_jsx("span", { className: "block truncate font-mono text-2xs text-faint-foreground", children: item.option.description ?? item.option.entity?.name }))] })] })) })] }, item.type === "command" ? item.command.id : item.option.id));
|
|
221
|
-
}) })), _jsxs("div", { className: cn("rounded-2xl border bg-card shadow-raised cd-tint focus-within:border-border-strong focus-within:shadow-float", locked && "bg-muted shadow-none"), children: [(references.length > 0 || attachments.length > 0 || referenceMotion.leaving.length > 0 || attachmentMotion.leaving.length > 0) && (_jsxs("div", { className: "flex flex-wrap gap-1.5 px-3 pt-3", children: [references.map((r) => (_jsx(
|
|
234
|
+
}) })), _jsxs("div", { className: cn("rounded-2xl border bg-card shadow-raised cd-tint focus-within:border-border-strong focus-within:shadow-float", locked && "bg-muted shadow-none"), children: [(references.length > 0 || attachments.length > 0 || referenceMotion.leaving.length > 0 || attachmentMotion.leaving.length > 0) && (_jsxs("div", { className: "flex flex-wrap gap-1.5 px-3 pt-3", children: [references.map((r) => (_jsx(EntityChip, { entity: r, onRemove: (e) => onReferencesChange?.(references.filter((x) => x !== e)), style: chipMotion, className: cn(referenceArrived(referenceKey(r)) && "cd-pop") }, referenceKey(r)))), referenceMotion.leaving.map((r) => (_jsx(LeavingChip, { onDone: () => referenceMotion.left(r), children: _jsx(EntityChip, { entity: r }) }, `leaving:${referenceKey(r)}`))), attachments.map((a) => (_jsx(AttachmentChip, { attachment: a, onRemove: onRemoveAttachment, style: chipMotion, className: cn(attachmentArrived(a.id) && "cd-pop") }, a.id))), attachmentMotion.leaving.map((a) => (_jsx(LeavingChip, { onDone: () => attachmentMotion.left(a), children: _jsx(AttachmentChip, { attachment: a }) }, `leaving:${a.id}`)))] })), _jsx("label", { htmlFor: id, className: "sr-only", children: label }), _jsx("textarea", { ref: textarea, id: id, rows: 1, value: value, disabled: Boolean(locked), placeholder: locked ? undefined : placeholder, onChange: (e) => {
|
|
222
235
|
onChange(e.target.value);
|
|
223
236
|
requestAnimationFrame(syncTrigger);
|
|
224
237
|
}, onKeyDown: onKeyDown, onKeyUp: (e) => {
|
|
@@ -15,6 +15,11 @@ export declare const contextMeterStrings: {
|
|
|
15
15
|
* Counts are the service's own token counts; the ring warns past 80%. When a
|
|
16
16
|
* new count arrives the ring sweeps to it and changes colour at the warning
|
|
17
17
|
* line; it draws its first value in place, with no sweep from zero.
|
|
18
|
+
*
|
|
19
|
+
* @motion fill:fill The ring moves from the old measured value to the new one when the count changes while the meter is
|
|
20
|
+
* on screen, and changes colour at the warning line. The first value is drawn in place, never swept from empty.
|
|
21
|
+
* @motion value:value The percentage beside the ring crossfades to the new figure in the quick duration when the count
|
|
22
|
+
* changes while the meter is on screen; it never counts through values that were not measured.
|
|
18
23
|
*/
|
|
19
24
|
declare function ContextMeter({ segments, limit, className, }: {
|
|
20
25
|
/** What fills the context window, by kind, in tokens. */
|
|
@@ -4,9 +4,13 @@ import * as React from "react";
|
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
6
6
|
import { useFormat, useStrings } from "../blocks/locale.js";
|
|
7
|
-
import { motion } from "../lib/motion.js";
|
|
7
|
+
import { motion, useSettled } from "../lib/motion.js";
|
|
8
8
|
/** The ring moving to a new measured value, after the first one was drawn in place. */
|
|
9
9
|
const fillMotion = motion("context-meter", "fill", "fill");
|
|
10
|
+
/** The percentage changing in place. */
|
|
11
|
+
const valueMotion = motion("context-meter", "value", "value");
|
|
12
|
+
/** Every new figure counts as a change worth showing. */
|
|
13
|
+
const changed = { settled: () => true, over: () => false };
|
|
10
14
|
/** ContextMeter's fixed words; override them under `contextMeter` in LocaleProvider's `strings`. */
|
|
11
15
|
export const contextMeterStrings = {
|
|
12
16
|
/** A token count in thousands; `{n}` is already formatted. */
|
|
@@ -36,6 +40,11 @@ function k(n, format, thousands) {
|
|
|
36
40
|
* Counts are the service's own token counts; the ring warns past 80%. When a
|
|
37
41
|
* new count arrives the ring sweeps to it and changes colour at the warning
|
|
38
42
|
* line; it draws its first value in place, with no sweep from zero.
|
|
43
|
+
*
|
|
44
|
+
* @motion fill:fill The ring moves from the old measured value to the new one when the count changes while the meter is
|
|
45
|
+
* on screen, and changes colour at the warning line. The first value is drawn in place, never swept from empty.
|
|
46
|
+
* @motion value:value The percentage beside the ring crossfades to the new figure in the quick duration when the count
|
|
47
|
+
* changes while the meter is on screen; it never counts through values that were not measured.
|
|
39
48
|
*/
|
|
40
49
|
function ContextMeter({ segments, limit, className, }) {
|
|
41
50
|
const t = useStrings("contextMeter", contextMeterStrings);
|
|
@@ -49,7 +58,9 @@ function ContextMeter({ segments, limit, className, }) {
|
|
|
49
58
|
// The sweep is a transition, so it runs only for a value that changed after the first one was drawn.
|
|
50
59
|
const [drawn, setDrawn] = React.useState(false);
|
|
51
60
|
React.useEffect(() => setDrawn(true), []);
|
|
52
|
-
|
|
61
|
+
const percent = fmt.number(Math.round(pct * 100) / 100, { style: "percent" });
|
|
62
|
+
const percentChanges = useSettled(percent, changed);
|
|
63
|
+
return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { "data-slot": "context-meter", "aria-label": t("label", { used: tokens(used), limit: tokens(limit) }), "data-warn": warn ? "" : undefined, className: cn("inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-2xs tabular-nums outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80", warn ? "text-warning-text" : "text-muted-foreground", className), children: [_jsxs("svg", { viewBox: "0 0 18 18", className: "size-4 -rotate-90", "aria-hidden": true, children: [_jsx("circle", { cx: "9", cy: "9", r: r, fill: "none", stroke: "var(--muted)", strokeWidth: "2.5" }), _jsx("circle", { "data-slot": "context-meter-ring", cx: "9", cy: "9", r: r, fill: "none", stroke: warn ? "var(--warning)" : "var(--primary)", strokeWidth: "2.5", strokeDasharray: `${pct * c} ${c}`, strokeLinecap: "round", style: fillMotion, className: cn(drawn && "cd-fill") })] }), percentChanges > 0 ? (_jsx("span", { style: valueMotion, className: "cd-value", children: percent }, percentChanges)) : (percent)] }), _jsxs(PopoverContent, { align: "start", className: "w-72 p-3", children: [_jsxs("p", { className: "flex items-baseline justify-between text-sm font-medium", children: [t("context"), " ", _jsx("span", { className: "text-xs font-normal text-muted-foreground tabular-nums", children: t("usage", { used: tokens(used), limit: tokens(limit) }) })] }), _jsx("div", { "aria-hidden": true, className: "mt-2 flex h-2 overflow-hidden rounded-full bg-muted", children: segments.map((s) => (_jsx("span", { style: { width: `${(s.tokens / limit) * 100}%`, background: kindColor[s.kind] } }, s.label))) }), _jsx("ul", { className: "mt-3 flex flex-col gap-1.5", children: [...segments]
|
|
53
64
|
.sort((a, b) => b.tokens - a.tokens)
|
|
54
65
|
.map((s) => (_jsxs("li", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { "aria-hidden": true, className: "size-2 rounded-xs", style: { background: kindColor[s.kind] } }), _jsx("span", { className: "flex-1", children: s.label }), _jsx("span", { className: "text-muted-foreground tabular-nums", children: tokens(s.tokens) })] }, s.label))) }), warn && _jsx("p", { className: "mt-3 text-xs text-warning-text", children: t("nearlyFull") })] })] }));
|
|
55
66
|
}
|
|
@@ -43,8 +43,11 @@ declare function useEntityKinds(): (kind: string) => EntityKindInfo;
|
|
|
43
43
|
* so "Account" the table and "Account" the report never read alike. The
|
|
44
44
|
* identifier is the label unless a friendlier one is given; then the
|
|
45
45
|
* identifier is on hover and for screen readers.
|
|
46
|
+
*
|
|
47
|
+
* @motion tint:tint A chip that can be pressed changes its border and background when the pointer is over it, and a
|
|
48
|
+
* ring fades in on keyboard focus. A chip that can't be pressed does not change.
|
|
46
49
|
*/
|
|
47
|
-
declare function EntityChip({ entity, onOpen, onRemove, className, }: {
|
|
50
|
+
declare function EntityChip({ entity, onOpen, onRemove, className, style, }: {
|
|
48
51
|
/** The thing to show: its kind, identifier and optional label. */
|
|
49
52
|
entity: EntityRef;
|
|
50
53
|
/** Makes the chip a button that opens the thing. */
|
|
@@ -52,5 +55,7 @@ declare function EntityChip({ entity, onOpen, onRemove, className, }: {
|
|
|
52
55
|
/** Adds a remove button, e.g. in the composer. */
|
|
53
56
|
onRemove?: ((entity: EntityRef) => void) | undefined;
|
|
54
57
|
className?: string | undefined;
|
|
58
|
+
/** Inline style for the chip's outer element, such as a motion chain from `motion()`. */
|
|
59
|
+
style?: React.CSSProperties | undefined;
|
|
55
60
|
}): React.JSX.Element;
|
|
56
61
|
export { EntityChip, EntityKindsProvider, useEntityKinds };
|
package/dist/chat/entity-chip.js
CHANGED
|
@@ -71,8 +71,11 @@ function useEntityKinds() {
|
|
|
71
71
|
* so "Account" the table and "Account" the report never read alike. The
|
|
72
72
|
* identifier is the label unless a friendlier one is given; then the
|
|
73
73
|
* identifier is on hover and for screen readers.
|
|
74
|
+
*
|
|
75
|
+
* @motion tint:tint A chip that can be pressed changes its border and background when the pointer is over it, and a
|
|
76
|
+
* ring fades in on keyboard focus. A chip that can't be pressed does not change.
|
|
74
77
|
*/
|
|
75
|
-
function EntityChip({ entity, onOpen, onRemove, className, }) {
|
|
78
|
+
function EntityChip({ entity, onOpen, onRemove, className, style, }) {
|
|
76
79
|
const { Icon, noun } = useEntityKinds()(entity.kind);
|
|
77
80
|
const t = useStrings("entityChip", entityChipStrings);
|
|
78
81
|
const shown = entity.label ?? entity.name;
|
|
@@ -81,6 +84,6 @@ function EntityChip({ entity, onOpen, onRemove, className, }) {
|
|
|
81
84
|
: t("title", { noun, name: entity.name });
|
|
82
85
|
const body = (_jsxs(_Fragment, { children: [_jsx(Icon, { "aria-hidden": true, className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: shown }), _jsxs("span", { className: "sr-only", children: [" (", title, ")"] })] }));
|
|
83
86
|
const base = "inline-flex max-w-full items-center gap-1 rounded-sm border bg-card px-1.5 py-px align-baseline font-mono text-[0.92em] leading-[1.35] text-foreground";
|
|
84
|
-
return (_jsxs("span", { "data-slot": "entity-chip", "data-kind": entity.kind, title: title, className: cn("inline-flex max-w-full min-w-0 items-center sm:max-w-64", className), children: [onOpen ? (_jsx("button", { type: "button", onClick: () => onOpen(entity), className: cn(base, "cursor-pointer cd-tint hover:border-border-strong hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80 outline-none"), children: body })) : (_jsx("span", { className: base, children: body })), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: shown }), onClick: () => onRemove(entity), className: "relative after:absolute after:-inset-1 -ms-px rounded-e-sm border border-s-0 bg-card px-1 py-px text-faint-foreground hover:text-foreground", children: "\u00D7" }))] }));
|
|
87
|
+
return (_jsxs("span", { "data-slot": "entity-chip", "data-kind": entity.kind, title: title, style: style, className: cn("inline-flex max-w-full min-w-0 items-center sm:max-w-64", className), children: [onOpen ? (_jsx("button", { type: "button", onClick: () => onOpen(entity), className: cn(base, "cursor-pointer cd-tint hover:border-border-strong hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/80 outline-none"), children: body })) : (_jsx("span", { className: base, children: body })), onRemove && (_jsx("button", { type: "button", "aria-label": t("remove", { name: shown }), onClick: () => onRemove(entity), className: "relative after:absolute after:-inset-1 -ms-px rounded-e-sm border border-s-0 bg-card px-1 py-px text-faint-foreground hover:text-foreground", children: "\u00D7" }))] }));
|
|
85
88
|
}
|
|
86
89
|
export { EntityChip, EntityKindsProvider, useEntityKinds };
|
|
@@ -6,6 +6,10 @@ import { cn } from "../lib/utils.js";
|
|
|
6
6
|
import { useStrings } from "../blocks/locale.js";
|
|
7
7
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
8
8
|
import { motion } from "../lib/motion.js";
|
|
9
|
+
/**
|
|
10
|
+
* @motion reveal:open The folded turns open to their full height when the person presses the summary, with its
|
|
11
|
+
* chevron turning, and close the same way. Under reduced motion they snap open or shut.
|
|
12
|
+
*/
|
|
9
13
|
const turnMotion = motion("folded-turns", "open", "reveal");
|
|
10
14
|
/** FoldedTurns' fixed words; override them under `foldedTurns` in LocaleProvider's `strings`. */
|
|
11
15
|
export const foldedTurnsStrings = {
|
|
@@ -7,7 +7,10 @@ import { useCopy } from "../lib/use-copy.js";
|
|
|
7
7
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
8
8
|
import { useStrings } from "../blocks/locale.js";
|
|
9
9
|
import { motion } from "../lib/motion.js";
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* @motion pop:tick After the person copies, a tick (or a cross, if the browser refused the clipboard) pops in on the
|
|
12
|
+
* button once the browser has answered. Nothing shows before the answer, and under reduced motion it fades in.
|
|
13
|
+
*/
|
|
11
14
|
const tickMotion = motion("message-actions", "tick", "pop");
|
|
12
15
|
/** MessageActions' fixed words; override them under `messageActions` in LocaleProvider's `strings`. */
|
|
13
16
|
export const messageActionsStrings = {
|
package/dist/chat/message.js
CHANGED
|
@@ -12,8 +12,15 @@ import { Reasoning } from "./reasoning.js";
|
|
|
12
12
|
import { ThreadEvent } from "./thread-event.js";
|
|
13
13
|
import { useStrings } from "../blocks/locale.js";
|
|
14
14
|
import { motion } from "../lib/motion.js";
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* @motion arrive:arrive A message rises in when it is added to a Thread after the thread first drew, whether it is
|
|
17
|
+
* the person's or the agent's. Messages loaded with the thread, and a Message outside a Thread, are drawn still.
|
|
18
|
+
*/
|
|
16
19
|
const arriveMotion = motion("message", "arrive", "arrive");
|
|
20
|
+
/**
|
|
21
|
+
* @motion live:caret While a reply is still streaming, a caret blinks at the end of its text. It goes when the reply
|
|
22
|
+
* finishes or is interrupted, and under reduced motion it is a still bar.
|
|
23
|
+
*/
|
|
17
24
|
const caretMotion = motion("message", "caret", "live", { dur: 1000 });
|
|
18
25
|
/** Message's fixed words; override them under `message` in LocaleProvider's `strings`. */
|
|
19
26
|
export const messageStrings = {
|
package/dist/chat/payload.d.ts
CHANGED
|
@@ -16,6 +16,9 @@ export declare const payloadStrings: {
|
|
|
16
16
|
*
|
|
17
17
|
* Code is split into tokens (keywords, strings, comments…) rendered as spans of text, never as HTML. They're coloured
|
|
18
18
|
* when `highlight` is set, or anywhere under `data-syntax="highlight"` (set it on `<html>` for the whole app).
|
|
19
|
+
*
|
|
20
|
+
* @motion tint:tint The copy button's background and text colour change when the pointer is over it, and a ring fades
|
|
21
|
+
* in when it takes keyboard focus.
|
|
19
22
|
*/
|
|
20
23
|
declare function Payload({ language, text, title, bare, highlight, className, }: {
|
|
21
24
|
/** Shown as the label, e.g. json, sql, python. Never executed or highlighted as HTML. */
|
package/dist/chat/payload.js
CHANGED
|
@@ -32,6 +32,9 @@ const languageLabel = (lang, t) => {
|
|
|
32
32
|
*
|
|
33
33
|
* Code is split into tokens (keywords, strings, comments…) rendered as spans of text, never as HTML. They're coloured
|
|
34
34
|
* when `highlight` is set, or anywhere under `data-syntax="highlight"` (set it on `<html>` for the whole app).
|
|
35
|
+
*
|
|
36
|
+
* @motion tint:tint The copy button's background and text colour change when the pointer is over it, and a ring fades
|
|
37
|
+
* in when it takes keyboard focus.
|
|
35
38
|
*/
|
|
36
39
|
function Payload({ language, text, title, bare = false, highlight, className, }) {
|
|
37
40
|
const { state, copy } = useCopy();
|
|
@@ -113,6 +113,11 @@ export type PermissionAnswer = {
|
|
|
113
113
|
* nothing else is ever counted as a denial. While the decision waits, its shield pulses slowly; a press on inert
|
|
114
114
|
* space outside it (the gesture that would dismiss a dialog) nudges it instead, so the person sees it is still
|
|
115
115
|
* waiting. Both stop once the answer is being sent.
|
|
116
|
+
*
|
|
117
|
+
* @motion attention:waiting While the decision waits, a ring pulses out from the shield three times and then is still.
|
|
118
|
+
* It stops once the answer is being sent, and under calm it is one ring that fades.
|
|
119
|
+
* @motion attention:nudge A press on inert space outside the prompt rings it once, so the person sees it is still waiting.
|
|
120
|
+
* @motion refuse:nudge Under expressive the same press also shakes the prompt a little; nothing is dismissed.
|
|
116
121
|
*/
|
|
117
122
|
declare function PermissionPrompt({ request, mode, suggestions, editable, onAllow, onDeny, busy, className, }: {
|
|
118
123
|
/** The waiting request. */
|