panelui-web 0.5.0 → 0.7.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.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Label — the name of a field, and the line it sits on.
3
+ *
4
+ * ```tsx
5
+ * <Label htmlFor="email" required>Email</Label>
6
+ * <Input id="email" type="email" required />
7
+ *
8
+ * <Label htmlFor="password">
9
+ * <Label.Text>Password</Label.Text>
10
+ * <Label.Action><a href="/reset">Forgot it?</a></Label.Action>
11
+ * </Label>
12
+ * ```
13
+ *
14
+ * ## One line, three things on it
15
+ *
16
+ * A field's label is rarely only its name. It says whether the field must be
17
+ * filled, it carries a hint (a limit, a format, "Optional"), and it often has
18
+ * something at the far end: a link to reset a password, a count of what is
19
+ * left. Putting those on the label's own line, rather than in lines of their
20
+ * own round the field, keeps a form to one row per field, and keeps the hint
21
+ * next to the name it qualifies.
22
+ *
23
+ * ## The control decides, where it can
24
+ *
25
+ * A label wrapped round its control, such as a checkbox, reads that control's
26
+ * state: it fades when the control is disabled and turns to the destructive
27
+ * colour when the control is invalid, with nothing passed. A label tied to its
28
+ * control by `htmlFor` cannot see it, so it takes `invalid` and `disabled`.
29
+ *
30
+ * ## It renders on the server
31
+ *
32
+ * The state reaches the parts through data attributes and CSS rather than
33
+ * through context, so a label has no client JavaScript of its own.
34
+ */
35
+ import type { ComponentProps } from 'react';
36
+ export interface LabelProps extends ComponentProps<'label'> {
37
+ /**
38
+ * Marks the field as one that must be filled. The mark is for the eye; set
39
+ * `required` on the control as well, which is what a screen reader announces.
40
+ */
41
+ required?: boolean;
42
+ /** Shows "Optional" after the text, for a form where most fields are required. */
43
+ optional?: boolean;
44
+ /**
45
+ * Turns the label to the destructive colour. Needed when the label is tied
46
+ * to its control by `htmlFor`; a label wrapped round its control reads it.
47
+ */
48
+ invalid?: boolean;
49
+ /** Fades the label. As with `invalid`, a wrapped control is read on its own. */
50
+ disabled?: boolean;
51
+ }
52
+ /**
53
+ * The label element. A string child is the label's text; for more, compose
54
+ * `Label.Text`, `Label.Hint` and `Label.Action`.
55
+ */
56
+ declare function LabelRoot({ required, optional, invalid, disabled, className, children, ...props }: LabelProps): import("react").JSX.Element;
57
+ declare namespace LabelRoot {
58
+ var displayName: string;
59
+ }
60
+ export interface LabelTextProps extends ComponentProps<'span'> {
61
+ }
62
+ /** The field's name. It truncates rather than pushing the action off the line. */
63
+ declare function LabelText({ className, children, ...props }: LabelTextProps): import("react").JSX.Element;
64
+ declare namespace LabelText {
65
+ var displayName: string;
66
+ }
67
+ export interface LabelHintProps extends ComponentProps<'span'> {
68
+ }
69
+ /**
70
+ * A quieter word or two after the name: a limit, a format, a unit. On an
71
+ * invalid field it takes the destructive colour, so it can say what is wrong.
72
+ */
73
+ declare function LabelHint({ className, ...props }: LabelHintProps): import("react").JSX.Element;
74
+ declare namespace LabelHint {
75
+ var displayName: string;
76
+ }
77
+ export interface LabelActionProps extends ComponentProps<'span'> {
78
+ }
79
+ /** Pushed to the far end of the line: a link, a count, a small button. */
80
+ declare function LabelAction({ className, ...props }: LabelActionProps): import("react").JSX.Element;
81
+ declare namespace LabelAction {
82
+ var displayName: string;
83
+ }
84
+ export declare const Label: typeof LabelRoot & {
85
+ Text: typeof LabelText;
86
+ Hint: typeof LabelHint;
87
+ Action: typeof LabelAction;
88
+ };
89
+ export { LabelText, LabelHint, LabelAction };
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { tv } from 'tailwind-variants';
3
+ const labelVariants = tv({
4
+ slots: {
5
+ root: [
6
+ 'group/label flex w-full min-w-0 items-center gap-2 text-sm font-medium leading-5 text-foreground select-none',
7
+ // A control wrapped in the label tells it how it is.
8
+ 'has-disabled:cursor-not-allowed has-disabled:opacity-50 has-[[aria-invalid=true]]:text-destructive',
9
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-invalid:text-destructive',
10
+ ],
11
+ // The required mark is drawn after the text by CSS, so it follows the name
12
+ // however the label is composed, and is not read out as "star".
13
+ text: [
14
+ 'flex min-w-0 items-baseline',
15
+ "group-data-required/label:after:ms-0.5 group-data-required/label:after:text-destructive group-data-required/label:after:content-['*']",
16
+ ],
17
+ textInner: 'min-w-0 truncate',
18
+ hint: [
19
+ 'shrink-0 font-normal text-muted-foreground',
20
+ // An invalid field's hint is the reason, so it takes the colour too.
21
+ 'group-data-invalid/label:text-destructive group-has-[[aria-invalid=true]]/label:text-destructive',
22
+ ],
23
+ action: [
24
+ 'order-last ms-auto flex shrink-0 items-center gap-1.5 font-normal text-muted-foreground',
25
+ '[&_a]:text-foreground [&_a]:underline-offset-4 [&_a]:outline-none [&_a:hover]:underline [&_a:focus-visible]:underline',
26
+ 'group-data-invalid/label:text-destructive',
27
+ ],
28
+ },
29
+ });
30
+ /**
31
+ * The label element. A string child is the label's text; for more, compose
32
+ * `Label.Text`, `Label.Hint` and `Label.Action`.
33
+ */
34
+ function LabelRoot({ required = false, optional = false, invalid = false, disabled = false, className, children, ...props }) {
35
+ const slots = labelVariants();
36
+ const plain = typeof children === 'string' || typeof children === 'number';
37
+ return (_jsxs("label", { "data-slot": "label", "data-required": required ? '' : undefined, "data-invalid": invalid ? '' : undefined, "data-disabled": disabled ? '' : undefined, className: slots.root({ className }), ...props, children: [plain ? _jsx(LabelText, { children: children }) : children, optional ? _jsx(LabelHint, { children: "Optional" }) : null] }));
38
+ }
39
+ LabelRoot.displayName = 'Label';
40
+ /** The field's name. It truncates rather than pushing the action off the line. */
41
+ function LabelText({ className, children, ...props }) {
42
+ const slots = labelVariants();
43
+ return (_jsx("span", { "data-slot": "label-text", className: slots.text({ className }), ...props, children: _jsx("span", { className: slots.textInner(), children: children }) }));
44
+ }
45
+ LabelText.displayName = 'Label.Text';
46
+ /**
47
+ * A quieter word or two after the name: a limit, a format, a unit. On an
48
+ * invalid field it takes the destructive colour, so it can say what is wrong.
49
+ */
50
+ function LabelHint({ className, ...props }) {
51
+ return _jsx("span", { "data-slot": "label-hint", className: labelVariants().hint({ className }), ...props });
52
+ }
53
+ LabelHint.displayName = 'Label.Hint';
54
+ /** Pushed to the far end of the line: a link, a count, a small button. */
55
+ function LabelAction({ className, ...props }) {
56
+ return _jsx("span", { "data-slot": "label-action", className: labelVariants().action({ className }), ...props });
57
+ }
58
+ LabelAction.displayName = 'Label.Action';
59
+ export const Label = Object.assign(LabelRoot, {
60
+ Text: LabelText,
61
+ Hint: LabelHint,
62
+ Action: LabelAction,
63
+ });
64
+ export { LabelText, LabelHint, LabelAction };
@@ -0,0 +1,161 @@
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ export type MessageFrom = 'user' | 'assistant';
3
+ export interface MessageProps extends ComponentProps<'div'> {
4
+ /** Who wrote it. A person's turn is a bubble on the trailing side; the assistant's is prose. */
5
+ from?: MessageFrom;
6
+ /** Still being written: a caret follows the last word and the turn is marked busy. */
7
+ streaming?: boolean;
8
+ }
9
+ /** One turn. Put `Message.Avatar` and `Message.Content` in it. */
10
+ declare function MessageRoot({ from: fromProp, streaming, className, ...props }: MessageProps): import("react").JSX.Element;
11
+ declare namespace MessageRoot {
12
+ var displayName: string;
13
+ }
14
+ export interface MessageGroupProps extends ComponentProps<'div'> {
15
+ /** Who wrote every turn in it. */
16
+ from?: MessageFrom;
17
+ }
18
+ /**
19
+ * Turns in a row from the same side, set closer together. Give the avatar to
20
+ * the first and an empty `Message.Avatar` to the rest, so they line up.
21
+ */
22
+ declare function MessageGroup({ from, className, ...props }: MessageGroupProps): import("react").JSX.Element;
23
+ declare namespace MessageGroup {
24
+ var displayName: string;
25
+ }
26
+ export interface MessageAvatarProps extends ComponentProps<'div'> {
27
+ }
28
+ /** Who it is from, beside the turn's first line. Leave it empty to keep the space. */
29
+ declare function MessageAvatar({ className, ...props }: MessageAvatarProps): import("react").JSX.Element;
30
+ declare namespace MessageAvatar {
31
+ var displayName: string;
32
+ }
33
+ export interface MessageContentProps extends ComponentProps<'div'> {
34
+ }
35
+ /** The column with the header, the bubble, the attachments and the actions. */
36
+ declare function MessageContent({ className, ...props }: MessageContentProps): import("react").JSX.Element;
37
+ declare namespace MessageContent {
38
+ var displayName: string;
39
+ }
40
+ export interface MessageHeaderProps extends ComponentProps<'div'> {
41
+ }
42
+ /** A line above the turn: a name in `<strong>`, a time, a model. */
43
+ declare function MessageHeader({ className, ...props }: MessageHeaderProps): import("react").JSX.Element;
44
+ declare namespace MessageHeader {
45
+ var displayName: string;
46
+ }
47
+ export interface MessageBubbleProps extends ComponentProps<'div'> {
48
+ }
49
+ /** The surface of a person's turn. On the assistant's it draws nothing, and the text is the page's. */
50
+ declare function MessageBubble({ className, ...props }: MessageBubbleProps): import("react").JSX.Element;
51
+ declare namespace MessageBubble {
52
+ var displayName: string;
53
+ }
54
+ export interface MessageBubbleContentProps extends ComponentProps<'div'> {
55
+ }
56
+ /**
57
+ * The text. Paragraphs, lists, inline code, code blocks and headings inside
58
+ * it are set as prose, so rendered Markdown can go straight in.
59
+ */
60
+ declare function MessageBubbleContent({ className, children, ...props }: MessageBubbleContentProps): import("react").JSX.Element;
61
+ declare namespace MessageBubbleContent {
62
+ var displayName: string;
63
+ }
64
+ export interface MessageFooterProps extends ComponentProps<'div'> {
65
+ }
66
+ /** A line under the turn: sent, read, a model and how long it took. */
67
+ declare function MessageFooter({ className, ...props }: MessageFooterProps): import("react").JSX.Element;
68
+ declare namespace MessageFooter {
69
+ var displayName: string;
70
+ }
71
+ export interface MessageActionsProps extends ComponentProps<'div'> {
72
+ /**
73
+ * `always`, the default, for the latest answer. `hover` for earlier ones:
74
+ * hidden until the turn is hovered or focused, and always shown on touch.
75
+ */
76
+ reveal?: 'always' | 'hover';
77
+ }
78
+ /** The row of actions under a turn. */
79
+ declare function MessageActions({ reveal, className, ...props }: MessageActionsProps): import("react").JSX.Element;
80
+ declare namespace MessageActions {
81
+ var displayName: string;
82
+ }
83
+ export interface MessageActionProps extends Omit<ComponentProps<'button'>, 'children'> {
84
+ /** Said by a screen reader and shown in the tooltip. Required, since the button is an icon. */
85
+ label: string;
86
+ /** The icon. Optional when `copy` is set, which draws its own. */
87
+ children?: ReactNode;
88
+ /**
89
+ * Text to put on the clipboard when pressed. The icon turns to a check for
90
+ * a moment, and the label to "Copied".
91
+ */
92
+ copy?: string | (() => string);
93
+ /** For a toggle such as a thumbs-up: whether it is on. */
94
+ pressed?: boolean;
95
+ }
96
+ /** An icon button with a tooltip: copy, retry, rate, share. */
97
+ declare function MessageAction({ label, children, copy, pressed, onClick, className, ...props }: MessageActionProps): import("react").JSX.Element;
98
+ declare namespace MessageAction {
99
+ var displayName: string;
100
+ }
101
+ export interface MessageBranchProps extends Omit<ComponentProps<'div'>, 'onChange'> {
102
+ /** How many versions of the answer there are. */
103
+ count: number;
104
+ /** Which one is shown, from 0. */
105
+ index: number;
106
+ /** Called with the index of the version asked for. */
107
+ onIndexChange: (index: number) => void;
108
+ }
109
+ /** "‹ 2 / 3 ›": steps between the versions of an answer that was retried or edited. */
110
+ declare function MessageBranch({ count, index, onIndexChange, className, ...props }: MessageBranchProps): import("react").JSX.Element;
111
+ declare namespace MessageBranch {
112
+ var displayName: string;
113
+ }
114
+ export interface MessageStatusProps extends ComponentProps<'div'> {
115
+ }
116
+ /**
117
+ * What the assistant is doing before its first word: "Thinking", "Reading 4
118
+ * files". Announced as it changes. Replace it with the text once that arrives.
119
+ */
120
+ declare function MessageStatus({ className, ...props }: MessageStatusProps): import("react").JSX.Element;
121
+ declare namespace MessageStatus {
122
+ var displayName: string;
123
+ }
124
+ export interface MessageAttachmentsProps extends ComponentProps<'div'> {
125
+ }
126
+ /** The files sent with a turn, above its bubble. */
127
+ declare function MessageAttachments({ className, ...props }: MessageAttachmentsProps): import("react").JSX.Element;
128
+ declare namespace MessageAttachments {
129
+ var displayName: string;
130
+ }
131
+ export interface MessageAttachmentProps extends Omit<ComponentProps<'div'>, 'children'> {
132
+ /** The file's name. */
133
+ name: string;
134
+ /** A picture to show in place of the file's card. */
135
+ src?: string;
136
+ /** A line under the name: its type and size. */
137
+ meta?: string;
138
+ /** The glyph on a file's card. A page outline when left out. */
139
+ icon?: ReactNode;
140
+ }
141
+ /** One sent file: a picture, or a card with a glyph, the name and its size. */
142
+ declare function MessageAttachment({ name, src, meta, icon, className, ...props }: MessageAttachmentProps): import("react").JSX.Element;
143
+ declare namespace MessageAttachment {
144
+ var displayName: string;
145
+ }
146
+ export declare const Message: typeof MessageRoot & {
147
+ Group: typeof MessageGroup;
148
+ Avatar: typeof MessageAvatar;
149
+ Content: typeof MessageContent;
150
+ Header: typeof MessageHeader;
151
+ Bubble: typeof MessageBubble;
152
+ BubbleContent: typeof MessageBubbleContent;
153
+ Footer: typeof MessageFooter;
154
+ Actions: typeof MessageActions;
155
+ Action: typeof MessageAction;
156
+ Branch: typeof MessageBranch;
157
+ Status: typeof MessageStatus;
158
+ Attachments: typeof MessageAttachments;
159
+ Attachment: typeof MessageAttachment;
160
+ };
161
+ export { MessageGroup, MessageAvatar, MessageContent, MessageHeader, MessageBubble, MessageBubbleContent, MessageFooter, MessageActions, MessageAction, MessageBranch, MessageStatus, MessageAttachments, MessageAttachment, };
@@ -0,0 +1,295 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * Message — one turn of a conversation with an assistant.
5
+ *
6
+ * ```tsx
7
+ * <Message from="user">
8
+ * <Message.Content>
9
+ * <Message.Bubble>
10
+ * <Message.BubbleContent>What changed in the Q3 numbers?</Message.BubbleContent>
11
+ * </Message.Bubble>
12
+ * </Message.Content>
13
+ * </Message>
14
+ *
15
+ * <Message from="assistant">
16
+ * <Message.Content>
17
+ * <Message.Bubble>
18
+ * <Message.BubbleContent>Revenue rose 12%…</Message.BubbleContent>
19
+ * </Message.Bubble>
20
+ * <Message.Actions>
21
+ * <Message.Action label="Copy" copy={text} />
22
+ * </Message.Actions>
23
+ * </Message.Content>
24
+ * </Message>
25
+ * ```
26
+ *
27
+ * ## Two kinds of turn, drawn differently
28
+ *
29
+ * What the person writes is short and is theirs, so it sits in a bubble on
30
+ * the trailing side, the way a sent message does. What the assistant writes is
31
+ * long, has paragraphs, lists and code, and is the thing being read, so it is
32
+ * not boxed at all: it runs the full width as prose, and the page is its
33
+ * surface. A bubble round a page of text only narrows it.
34
+ *
35
+ * ## Actions under the answer
36
+ *
37
+ * Copy, retry, rate and the switcher between versions of an answer sit in a
38
+ * row under it. On the latest answer they stay in view, because that is the
39
+ * one they are for. On earlier ones, `reveal="hover"` keeps them out of the
40
+ * way until the turn is hovered or focused, and on a touch screen, where
41
+ * there is no hover, they always show.
42
+ *
43
+ * ## While it is being written
44
+ *
45
+ * `streaming` puts a caret after the last word and marks the turn busy, so a
46
+ * screen reader waits for it to finish. Before the first word arrives,
47
+ * `Message.Status` says what the assistant is doing, as a line that shimmers.
48
+ */
49
+ import { Tooltip } from '@base-ui/react/tooltip';
50
+ import { createContext, useContext, useEffect, useRef, useState, } from 'react';
51
+ import { tv } from 'tailwind-variants';
52
+ const messageVariants = tv({
53
+ slots: {
54
+ root: 'group/message flex w-full gap-3',
55
+ group: 'flex w-full flex-col gap-1.5',
56
+ avatar: 'flex w-8 shrink-0 justify-center pt-0.5 empty:invisible',
57
+ content: 'flex min-w-0 flex-col gap-1.5',
58
+ header: 'flex items-baseline gap-2 text-xs text-muted-foreground [&_strong]:font-medium [&_strong]:text-foreground',
59
+ bubble: 'min-w-0',
60
+ bubbleContent: [
61
+ 'min-w-0 text-[0.9375rem] leading-7 text-foreground [overflow-wrap:anywhere]',
62
+ // Prose: what an answer is usually made of.
63
+ '[&_p]:my-3 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0',
64
+ '[&_ul]:my-3 [&_ul]:list-disc [&_ul]:ps-5 [&_ol]:my-3 [&_ol]:list-decimal [&_ol]:ps-5 [&_li]:my-1 [&_li]:ps-1 [&_li::marker]:text-muted-foreground',
65
+ // Links in the text; a component's own link inside it keeps its own look.
66
+ '[&_strong]:font-semibold [&_a:not([data-slot])]:underline [&_a:not([data-slot])]:underline-offset-4',
67
+ '[&_:not(pre)>code]:rounded-md [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.8125rem]',
68
+ '[&_pre]:my-3 [&_pre]:overflow-x-auto [&_pre]:rounded-xl [&_pre]:border [&_pre]:bg-muted/50 [&_pre]:p-3.5 [&_pre]:font-mono [&_pre]:text-[0.8125rem] [&_pre]:leading-6',
69
+ '[&_h3]:mt-5 [&_h3]:mb-2 [&_h3]:font-semibold',
70
+ ],
71
+ // The caret is a soft block that breathes rather than blinks. Written out
72
+ // twice: after the text itself, and inside its last block.
73
+ caret: "after:ms-0.5 after:inline-block after:h-[1.05em] after:w-[0.5em] after:translate-y-[0.18em] after:rounded-[3px] after:bg-foreground after:content-[''] after:animate-pulse motion-reduce:after:animate-none",
74
+ caretLast: "[&>:last-child]:after:ms-0.5 [&>:last-child]:after:inline-block [&>:last-child]:after:h-[1.05em] [&>:last-child]:after:w-[0.5em] [&>:last-child]:after:translate-y-[0.18em] [&>:last-child]:after:rounded-[3px] [&>:last-child]:after:bg-foreground [&>:last-child]:after:content-[''] [&>:last-child]:after:animate-pulse motion-reduce:[&>:last-child]:after:animate-none",
75
+ footer: 'flex items-center gap-2 text-xs text-muted-foreground',
76
+ actions: [
77
+ '-ms-1.5 flex items-center gap-0.5 text-muted-foreground',
78
+ 'transition-opacity duration-200 motion-reduce:transition-none',
79
+ ],
80
+ action: [
81
+ 'inline-flex size-8 cursor-pointer items-center justify-center rounded-lg outline-none',
82
+ 'transition-[background-color,color,scale] duration-150 hover:bg-muted hover:text-foreground active:scale-90',
83
+ 'focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none',
84
+ 'disabled:pointer-events-none disabled:opacity-40',
85
+ 'aria-pressed:text-foreground [&_svg]:size-4 [&_svg]:shrink-0',
86
+ ],
87
+ // Copied: the check is drawn in the success colour, then the icon comes back.
88
+ actionDone: 'text-[var(--success)] hover:text-[var(--success)]',
89
+ tooltip: [
90
+ 'z-50 rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background shadow-md',
91
+ 'origin-(--transform-origin) transition-[opacity,scale] duration-150 data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:opacity-0',
92
+ 'motion-reduce:transition-none',
93
+ ],
94
+ branch: 'flex items-center text-xs tabular-nums text-muted-foreground',
95
+ branchButton: [
96
+ 'inline-flex size-7 cursor-pointer items-center justify-center rounded-lg outline-none transition-colors',
97
+ 'hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring',
98
+ 'disabled:pointer-events-none disabled:opacity-35 [&_svg]:size-4',
99
+ ],
100
+ status: [
101
+ 'inline-flex w-fit items-center gap-2 text-[0.9375rem] leading-7',
102
+ // A highlight sweeping across muted text: working, without a spinner.
103
+ 'bg-[linear-gradient(90deg,var(--muted-foreground)_0%,var(--muted-foreground)_40%,var(--foreground)_50%,var(--muted-foreground)_60%,var(--muted-foreground)_100%)] bg-[length:250%_100%] bg-clip-text text-transparent',
104
+ 'animate-[message-shimmer_2.2s_linear_infinite] motion-reduce:animate-none motion-reduce:bg-none motion-reduce:text-muted-foreground',
105
+ ],
106
+ attachments: 'flex flex-wrap gap-2',
107
+ attachment: 'flex items-center gap-2.5 overflow-hidden rounded-xl border bg-background text-xs',
108
+ attachmentImage: 'size-20 object-cover',
109
+ attachmentFile: 'h-14 max-w-60 py-2 pe-3.5 ps-2',
110
+ attachmentGlyph: 'flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground [&_svg]:size-4',
111
+ attachmentName: 'block truncate font-medium text-foreground',
112
+ attachmentMeta: 'block truncate text-muted-foreground',
113
+ },
114
+ variants: {
115
+ from: {
116
+ user: {
117
+ root: 'flex-row-reverse',
118
+ content: 'max-w-[min(80%,36rem)] items-end',
119
+ header: 'justify-end',
120
+ // A squared corner on the sender's side, at the top, where the turn starts.
121
+ bubble: 'rounded-[1.25rem] rounded-se-md bg-muted px-4 py-2.5',
122
+ bubbleContent: 'leading-6 [&_p]:my-2',
123
+ footer: 'flex-row-reverse',
124
+ attachments: 'justify-end',
125
+ },
126
+ assistant: {
127
+ content: 'flex-1',
128
+ },
129
+ },
130
+ reveal: {
131
+ always: {},
132
+ // Earlier answers: out of the way until the turn is hovered or focused,
133
+ // and always there on a touch screen, which has no hover.
134
+ hover: {
135
+ actions: 'opacity-0 group-hover/message:opacity-100 group-focus-within/message:opacity-100 has-data-popup-open:opacity-100 pointer-coarse:opacity-100',
136
+ },
137
+ },
138
+ },
139
+ defaultVariants: { from: 'assistant', reveal: 'always' },
140
+ });
141
+ const MessageContext = createContext({ from: 'assistant', streaming: false });
142
+ const GroupContext = createContext(null);
143
+ /** One turn. Put `Message.Avatar` and `Message.Content` in it. */
144
+ function MessageRoot({ from: fromProp, streaming = false, className, ...props }) {
145
+ const group = useContext(GroupContext);
146
+ const from = fromProp ?? group?.from ?? 'assistant';
147
+ return (_jsx(MessageContext.Provider, { value: { from, streaming }, children: _jsx("div", { "data-slot": "message", "data-from": from, "data-streaming": streaming ? '' : undefined, "aria-busy": streaming || undefined, className: messageVariants({ from }).root({ className }), ...props }) }));
148
+ }
149
+ MessageRoot.displayName = 'Message';
150
+ /**
151
+ * Turns in a row from the same side, set closer together. Give the avatar to
152
+ * the first and an empty `Message.Avatar` to the rest, so they line up.
153
+ */
154
+ function MessageGroup({ from = 'assistant', className, ...props }) {
155
+ return (_jsx(GroupContext.Provider, { value: { from }, children: _jsx("div", { "data-slot": "message-group", className: messageVariants().group({ className }), ...props }) }));
156
+ }
157
+ MessageGroup.displayName = 'Message.Group';
158
+ /** Who it is from, beside the turn's first line. Leave it empty to keep the space. */
159
+ function MessageAvatar({ className, ...props }) {
160
+ return _jsx("div", { "data-slot": "message-avatar", className: messageVariants().avatar({ className }), ...props });
161
+ }
162
+ MessageAvatar.displayName = 'Message.Avatar';
163
+ /** The column with the header, the bubble, the attachments and the actions. */
164
+ function MessageContent({ className, ...props }) {
165
+ const { from } = useContext(MessageContext);
166
+ return _jsx("div", { "data-slot": "message-content", className: messageVariants({ from }).content({ className }), ...props });
167
+ }
168
+ MessageContent.displayName = 'Message.Content';
169
+ /** A line above the turn: a name in `<strong>`, a time, a model. */
170
+ function MessageHeader({ className, ...props }) {
171
+ const { from } = useContext(MessageContext);
172
+ return _jsx("div", { "data-slot": "message-header", className: messageVariants({ from }).header({ className }), ...props });
173
+ }
174
+ MessageHeader.displayName = 'Message.Header';
175
+ /** The surface of a person's turn. On the assistant's it draws nothing, and the text is the page's. */
176
+ function MessageBubble({ className, ...props }) {
177
+ const { from } = useContext(MessageContext);
178
+ return _jsx("div", { "data-slot": "message-bubble", className: messageVariants({ from }).bubble({ className }), ...props });
179
+ }
180
+ MessageBubble.displayName = 'Message.Bubble';
181
+ /**
182
+ * The text. Paragraphs, lists, inline code, code blocks and headings inside
183
+ * it are set as prose, so rendered Markdown can go straight in.
184
+ */
185
+ function MessageBubbleContent({ className, children, ...props }) {
186
+ const { from, streaming } = useContext(MessageContext);
187
+ const slots = messageVariants({ from });
188
+ // The caret follows the last word: after the text itself, or inside the last block.
189
+ const plain = typeof children === 'string' || typeof children === 'number';
190
+ const caret = streaming ? (plain ? slots.caret() : slots.caretLast()) : undefined;
191
+ return (_jsx("div", { "data-slot": "message-bubble-content", className: slots.bubbleContent({ className: [caret, className] }), ...props, children: children }));
192
+ }
193
+ MessageBubbleContent.displayName = 'Message.BubbleContent';
194
+ /** A line under the turn: sent, read, a model and how long it took. */
195
+ function MessageFooter({ className, ...props }) {
196
+ const { from } = useContext(MessageContext);
197
+ return _jsx("div", { "data-slot": "message-footer", className: messageVariants({ from }).footer({ className }), ...props });
198
+ }
199
+ MessageFooter.displayName = 'Message.Footer';
200
+ /** The row of actions under a turn. */
201
+ function MessageActions({ reveal, className, ...props }) {
202
+ return (_jsx(Tooltip.Provider, { delay: 400, closeDelay: 0, children: _jsx("div", { role: "toolbar", "aria-label": "Message actions", "data-slot": "message-actions", className: messageVariants({ reveal }).actions({ className }), ...props }) }));
203
+ }
204
+ MessageActions.displayName = 'Message.Actions';
205
+ /** An icon button with a tooltip: copy, retry, rate, share. */
206
+ function MessageAction({ label, children, copy, pressed, onClick, className, ...props }) {
207
+ const [copied, setCopied] = useState(false);
208
+ const timer = useRef(undefined);
209
+ useEffect(() => () => clearTimeout(timer.current), []);
210
+ const slots = messageVariants();
211
+ const press = (event) => {
212
+ onClick?.(event);
213
+ if (event.defaultPrevented || copy === undefined)
214
+ return;
215
+ const text = typeof copy === 'function' ? copy() : copy;
216
+ // A browser can refuse the clipboard by throwing or by rejecting; either way the check still shows.
217
+ try {
218
+ void navigator.clipboard?.writeText(text).catch(() => { });
219
+ }
220
+ catch { }
221
+ setCopied(true);
222
+ clearTimeout(timer.current);
223
+ timer.current = setTimeout(() => setCopied(false), 1600);
224
+ };
225
+ const shown = copied ? 'Copied' : label;
226
+ return (_jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { type: "button", "aria-label": shown, "aria-pressed": pressed, "data-slot": "message-action", className: slots.action({ className: [copied && slots.actionDone(), className] }), onClick: press, ...props, children: copied ? _jsx(CheckGlyph, {}) : (children ?? (copy !== undefined ? _jsx(CopyGlyph, {}) : null)) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Positioner, { side: "bottom", sideOffset: 6, className: "z-50", children: _jsx(Tooltip.Popup, { className: slots.tooltip(), children: shown }) }) })] }));
227
+ }
228
+ MessageAction.displayName = 'Message.Action';
229
+ /** "‹ 2 / 3 ›": steps between the versions of an answer that was retried or edited. */
230
+ function MessageBranch({ count, index, onIndexChange, className, ...props }) {
231
+ const slots = messageVariants();
232
+ return (_jsxs("div", { role: "group", "aria-label": "Versions", "data-slot": "message-branch", className: slots.branch({ className }), ...props, children: [_jsx("button", { type: "button", "aria-label": "Previous version", className: slots.branchButton(), disabled: index <= 0, onClick: () => onIndexChange(index - 1), children: _jsx(ChevronGlyph, { flip: true }) }), _jsxs("span", { "aria-live": "polite", className: "min-w-9 text-center", children: [index + 1, " / ", count] }), _jsx("button", { type: "button", "aria-label": "Next version", className: slots.branchButton(), disabled: index >= count - 1, onClick: () => onIndexChange(index + 1), children: _jsx(ChevronGlyph, {}) })] }));
233
+ }
234
+ MessageBranch.displayName = 'Message.Branch';
235
+ /**
236
+ * What the assistant is doing before its first word: "Thinking", "Reading 4
237
+ * files". Announced as it changes. Replace it with the text once that arrives.
238
+ */
239
+ function MessageStatus({ className, ...props }) {
240
+ return (_jsxs(_Fragment, { children: [_jsx("style", { href: "panelui-message-shimmer", precedence: "default", children: '@keyframes message-shimmer{from{background-position:100% 0}to{background-position:-150% 0}}' }), _jsx("div", { role: "status", "data-slot": "message-status", className: messageVariants().status({ className }), ...props })] }));
241
+ }
242
+ MessageStatus.displayName = 'Message.Status';
243
+ /** The files sent with a turn, above its bubble. */
244
+ function MessageAttachments({ className, ...props }) {
245
+ const { from } = useContext(MessageContext);
246
+ return (_jsx("div", { "data-slot": "message-attachments", className: messageVariants({ from }).attachments({ className }), ...props }));
247
+ }
248
+ MessageAttachments.displayName = 'Message.Attachments';
249
+ /** One sent file: a picture, or a card with a glyph, the name and its size. */
250
+ function MessageAttachment({ name, src, meta, icon, className, ...props }) {
251
+ const slots = messageVariants();
252
+ if (src) {
253
+ return (_jsx("div", { "data-slot": "message-attachment", className: slots.attachment({ className }), ...props, children: _jsx("img", { src: src, alt: name, className: slots.attachmentImage() }) }));
254
+ }
255
+ return (_jsxs("div", { "data-slot": "message-attachment", className: slots.attachment({ className: [slots.attachmentFile(), className] }), ...props, children: [_jsx("span", { className: slots.attachmentGlyph(), children: icon ?? _jsx(FileGlyph, {}) }), _jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: slots.attachmentName(), children: name }), meta ? _jsx("span", { className: slots.attachmentMeta(), children: meta }) : null] })] }));
256
+ }
257
+ MessageAttachment.displayName = 'Message.Attachment';
258
+ /* -------------------------------------------------------------------------- */
259
+ /* Glyphs */
260
+ /* -------------------------------------------------------------------------- */
261
+ const STROKE = {
262
+ fill: 'none',
263
+ stroke: 'currentColor',
264
+ strokeWidth: 2,
265
+ strokeLinecap: 'round',
266
+ strokeLinejoin: 'round',
267
+ };
268
+ function CopyGlyph() {
269
+ return (_jsxs("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", ...STROKE, children: [_jsx("rect", { x: "8", y: "8", width: "13", height: "13", rx: "3" }), _jsx("path", { d: "M16 8V6a3 3 0 0 0-3-3H6a3 3 0 0 0-3 3v7a3 3 0 0 0 3 3h2" })] }));
270
+ }
271
+ function CheckGlyph() {
272
+ return (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", ...STROKE, children: _jsx("path", { d: "M5 12.5l4.5 4.5L19 7.5" }) }));
273
+ }
274
+ function ChevronGlyph({ flip = false }) {
275
+ return (_jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", ...STROKE, className: flip ? '-scale-x-100 rtl:scale-x-100' : 'rtl:-scale-x-100', children: _jsx("path", { d: "M9 6l6 6-6 6" }) }));
276
+ }
277
+ function FileGlyph() {
278
+ return (_jsxs("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", ...STROKE, children: [_jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }), _jsx("path", { d: "M14 3v5h5" })] }));
279
+ }
280
+ export const Message = Object.assign(MessageRoot, {
281
+ Group: MessageGroup,
282
+ Avatar: MessageAvatar,
283
+ Content: MessageContent,
284
+ Header: MessageHeader,
285
+ Bubble: MessageBubble,
286
+ BubbleContent: MessageBubbleContent,
287
+ Footer: MessageFooter,
288
+ Actions: MessageActions,
289
+ Action: MessageAction,
290
+ Branch: MessageBranch,
291
+ Status: MessageStatus,
292
+ Attachments: MessageAttachments,
293
+ Attachment: MessageAttachment,
294
+ });
295
+ export { MessageGroup, MessageAvatar, MessageContent, MessageHeader, MessageBubble, MessageBubbleContent, MessageFooter, MessageActions, MessageAction, MessageBranch, MessageStatus, MessageAttachments, MessageAttachment, };