@djangocfg/widget-chat 0.1.1
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/LICENSE +21 -0
- package/README.md +124 -0
- package/package.json +95 -0
- package/src/composer/composer.tsx +100 -0
- package/src/composer/context.ts +33 -0
- package/src/composer/controls/button.tsx +104 -0
- package/src/composer/controls/language-menu.tsx +123 -0
- package/src/composer/controls/send.tsx +83 -0
- package/src/composer/controls/think.tsx +50 -0
- package/src/composer/controls/tools.tsx +130 -0
- package/src/composer/controls/tooltip.tsx +10 -0
- package/src/composer/controls/voice.tsx +266 -0
- package/src/composer/field/field.tsx +140 -0
- package/src/composer/field/input.tsx +214 -0
- package/src/composer/field/keys.ts +44 -0
- package/src/composer/field/use-autosize.ts +134 -0
- package/src/composer/focus-field.ts +27 -0
- package/src/composer/index.ts +86 -0
- package/src/composer/stories/field.tsx +112 -0
- package/src/composer/stories/harness.tsx +127 -0
- package/src/composer/tools/menu.tsx +129 -0
- package/src/composer/tools/rows.tsx +180 -0
- package/src/composer/tools/use-menu-nav.ts +66 -0
- package/src/composer/tools/use-recent-tools.ts +30 -0
- package/src/composer/tray/banner.tsx +157 -0
- package/src/composer/tray/command-progress.tsx +116 -0
- package/src/composer/tray/picked-tools.tsx +104 -0
- package/src/composer/tray/queue.tsx +202 -0
- package/src/composer/tray/use-replace-text.ts +58 -0
- package/src/composer/use-draft-persistence.ts +79 -0
- package/src/content/index.ts +16 -0
- package/src/content/media-content.tsx +38 -0
- package/src/content/response.tsx +221 -0
- package/src/core/adapter.ts +29 -0
- package/src/core/capabilities.ts +286 -0
- package/src/core/chunks.ts +66 -0
- package/src/core/content/incomplete-math.ts +118 -0
- package/src/core/content/index.ts +9 -0
- package/src/core/draft-storage.ts +62 -0
- package/src/core/events.ts +88 -0
- package/src/core/focus/index.ts +10 -0
- package/src/core/focus/roving.ts +56 -0
- package/src/core/group-parts.ts +80 -0
- package/src/core/index.ts +34 -0
- package/src/core/links/index.ts +33 -0
- package/src/core/links/prose.ts +191 -0
- package/src/core/links/scheme.ts +98 -0
- package/src/core/links/tool-id.ts +26 -0
- package/src/core/media/allowed-src.ts +138 -0
- package/src/core/media/block.ts +254 -0
- package/src/core/media/index.ts +22 -0
- package/src/core/media/kinds.ts +85 -0
- package/src/core/outbound.ts +86 -0
- package/src/core/parts.ts +169 -0
- package/src/core/queue/index.ts +15 -0
- package/src/core/queue/queue.ts +100 -0
- package/src/core/recall/boundary.ts +77 -0
- package/src/core/recall/from-transcript.ts +47 -0
- package/src/core/recall/index.ts +23 -0
- package/src/core/recall/ladder.ts +171 -0
- package/src/core/reload-decision.ts +36 -0
- package/src/core/scroll/index.ts +17 -0
- package/src/core/scroll/missed.ts +106 -0
- package/src/core/scroll/sent.ts +54 -0
- package/src/core/text-replace.ts +20 -0
- package/src/core/tool-name.ts +31 -0
- package/src/core/tool-picks-set.ts +48 -0
- package/src/core/tool-picks.ts +142 -0
- package/src/core/tool-prompt.ts +47 -0
- package/src/core/tools/capability.ts +34 -0
- package/src/core/tools/catalogue.ts +109 -0
- package/src/core/tools/index.ts +17 -0
- package/src/core/tools/layout.ts +44 -0
- package/src/core/tools/nav.ts +32 -0
- package/src/core/tools/rank.ts +60 -0
- package/src/core/tools/recent.ts +39 -0
- package/src/core/transcript.ts +301 -0
- package/src/core/voice/earcons.ts +58 -0
- package/src/core/voice/index.ts +26 -0
- package/src/core/voice/language-search.ts +62 -0
- package/src/core/voice/languages.ts +267 -0
- package/src/core/voice/resolve-language.ts +93 -0
- package/src/core/voice/session.ts +196 -0
- package/src/core/voice/web-speech.ts +170 -0
- package/src/i18n/index.ts +87 -0
- package/src/i18n/locales/ar.ts +97 -0
- package/src/i18n/locales/da.ts +90 -0
- package/src/i18n/locales/de.ts +90 -0
- package/src/i18n/locales/en.ts +90 -0
- package/src/i18n/locales/es.ts +90 -0
- package/src/i18n/locales/fr.ts +90 -0
- package/src/i18n/locales/it.ts +90 -0
- package/src/i18n/locales/ja.ts +83 -0
- package/src/i18n/locales/ko.ts +83 -0
- package/src/i18n/locales/nl.ts +90 -0
- package/src/i18n/locales/no.ts +90 -0
- package/src/i18n/locales/pl.ts +96 -0
- package/src/i18n/locales/ptBR.ts +90 -0
- package/src/i18n/locales/ru.ts +96 -0
- package/src/i18n/locales/sv.ts +90 -0
- package/src/i18n/locales/tr.ts +89 -0
- package/src/i18n/locales/zh.ts +83 -0
- package/src/i18n/plural.ts +62 -0
- package/src/i18n/types.ts +257 -0
- package/src/index.ts +18 -0
- package/src/primitives/index.ts +43 -0
- package/src/primitives/message.tsx +85 -0
- package/src/primitives/part-registry.tsx +44 -0
- package/src/primitives/stories-harness.tsx +118 -0
- package/src/primitives/suggestions.tsx +112 -0
- package/src/primitives/tool-call.tsx +145 -0
- package/src/primitives/tooltip.tsx +48 -0
- package/src/primitives/transcript.tsx +285 -0
- package/src/primitives/use-dismiss.ts +100 -0
- package/src/primitives/use-missed-count.ts +30 -0
- package/src/primitives/use-overflows.ts +69 -0
- package/src/primitives/use-return-on-send.ts +29 -0
- package/src/primitives/use-roving-focus.ts +69 -0
- package/src/primitives/use-select-all-transcript.ts +111 -0
- package/src/primitives/use-stick-to-bottom.ts +83 -0
- package/src/primitives/use-turn-activity.ts +25 -0
- package/src/store/context.ts +28 -0
- package/src/store/hooks.ts +215 -0
- package/src/store/index.ts +31 -0
- package/src/store/provider.tsx +97 -0
- package/src/store/store.ts +394 -0
- package/src/store/subscribers.ts +55 -0
- package/src/store/surfaces.ts +174 -0
- package/src/store/use-draft.ts +168 -0
- package/src/store/use-queue.ts +61 -0
- package/src/store/use-recall.ts +264 -0
- package/src/store/use-reload-on-revision.ts +67 -0
- package/src/store/use-speech-language.ts +84 -0
- package/src/store/use-voice.ts +116 -0
- package/src/styles/block.css +128 -0
- package/src/styles/composer.css +449 -0
- package/src/styles/index.css +47 -0
- package/src/styles/markdown.css +368 -0
- package/src/styles/surface.css +102 -0
- package/src/styles/tool.css +128 -0
- package/src/styles/turn-mark.css +97 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +69 -0
- package/src/testing/mock-tools.ts +101 -0
- package/src/testing/mock-voice.ts +81 -0
- package/src/ui/chat-composer.tsx +286 -0
- package/src/ui/chat-reply-chip.tsx +42 -0
- package/src/ui/chat-suggestions.tsx +87 -0
- package/src/ui/chat-transcript.tsx +157 -0
- package/src/ui/chat.tsx +197 -0
- package/src/ui/collapsible-body.tsx +180 -0
- package/src/ui/collapsible-row.tsx +81 -0
- package/src/ui/day-divider.tsx +49 -0
- package/src/ui/index.ts +54 -0
- package/src/ui/jump-to-latest.tsx +60 -0
- package/src/ui/link-chip.tsx +73 -0
- package/src/ui/message-actions.tsx +163 -0
- package/src/ui/message-quote.tsx +47 -0
- package/src/ui/message.tsx +295 -0
- package/src/ui/pending.tsx +85 -0
- package/src/ui/renderers/data.tsx +63 -0
- package/src/ui/renderers/reasoning.tsx +73 -0
- package/src/ui/renderers/tool.tsx +176 -0
- package/src/ui/turn-mark.tsx +31 -0
- package/src/utils/day-heading.ts +46 -0
- package/src/utils/error-message.ts +10 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/markdown-text.ts +175 -0
- package/src/widgets/index.ts +14 -0
- package/src/widgets/media.tsx +239 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@djangocfg/widget-chat` — the CSS the package's own markup needs to work.
|
|
3
|
+
*
|
|
4
|
+
* Import once, anywhere in a host's stylesheet:
|
|
5
|
+
*
|
|
6
|
+
* @import "@djangocfg/widget-chat/styles";
|
|
7
|
+
*
|
|
8
|
+
* ## What belongs here, and what does not
|
|
9
|
+
*
|
|
10
|
+
* This file carries rules that are part of the COMPONENTS' contract — layout
|
|
11
|
+
* that the markup would be broken without, and that no host would ever want
|
|
12
|
+
* differently. It carries no product VALUES: not the type scale, not the
|
|
13
|
+
* reading width, not a colour. Those stay the host's, declared as `--chat-*`
|
|
14
|
+
* tokens; a host defines them alongside its own theme.
|
|
15
|
+
*
|
|
16
|
+
* The dividing question is "would a second consumer want this changed?" — and
|
|
17
|
+
* it is about the VALUE, not about the rule. Which class consumes which token
|
|
18
|
+
* is not a decision anyone would take differently; it is wiring, and it belongs
|
|
19
|
+
* beside the markup it wires, or it breaks the next time that markup moves.
|
|
20
|
+
* Every `var()` here therefore carries a fallback, so a host that declares no
|
|
21
|
+
* tokens still gets a readable surface rather than a collapsed one.
|
|
22
|
+
*
|
|
23
|
+
* ## Why CSS at all, in a package that ships none
|
|
24
|
+
*
|
|
25
|
+
* Because `:has()` is the only mechanism that expresses some of these rules,
|
|
26
|
+
* and it has no JavaScript equivalent that is not a resize observer writing a
|
|
27
|
+
* class —
|
|
28
|
+
* a measure-and-render cycle for a question the browser has already answered.
|
|
29
|
+
* Everything else the package needs is a Tailwind class on its own markup.
|
|
30
|
+
*
|
|
31
|
+
* The package is consumed as SOURCE (`"main": "./src/index.ts"`), so a host's
|
|
32
|
+
* Tailwind entry can import this directly; there is no build step to teach.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Split by SUBSYSTEM, one file each, because they answer to different owners:
|
|
37
|
+
* the composer's rules are layout the markup breaks without, the markdown
|
|
38
|
+
* surface styles what the renderer emits, and the block rhythm belongs to the
|
|
39
|
+
* transcript. A single file made "is this rule still used?" a question about
|
|
40
|
+
* 513 lines.
|
|
41
|
+
*/
|
|
42
|
+
@import "./surface.css";
|
|
43
|
+
@import "./turn-mark.css";
|
|
44
|
+
@import "./composer.css";
|
|
45
|
+
@import "./markdown.css";
|
|
46
|
+
@import "./block.css";
|
|
47
|
+
@import "./tool.css";
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MARKDOWN surface — how a rendered message reads.
|
|
3
|
+
*
|
|
4
|
+
* These style markup the PACKAGE renders (`streamdown` output inside a
|
|
5
|
+
* message), so they ship with it: a second consumer that mounted the transcript
|
|
6
|
+
* and got un-gutter'd lists, a doubled code background and document-scale
|
|
7
|
+
* headings would be inheriting a bug, not exercising a choice.
|
|
8
|
+
*
|
|
9
|
+
* What stays with the HOST is what a product decides — the type SCALE, the
|
|
10
|
+
* reading COLUMN, and the `.chat-surface`/`.chat-column` classes it puts on its
|
|
11
|
+
* own elements. Consumed here as variables WITH FALLBACKS, so this degrades to
|
|
12
|
+
* sensible relative sizing when a host declares none.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Zero-specificity by design.
|
|
17
|
+
*
|
|
18
|
+
* Everything below is `:where()`, so every rule has ZERO specificity: a
|
|
19
|
+
* component that sets its own value always wins, and these are defaults rather
|
|
20
|
+
* than overrides. That matters because the agent's markdown is rendered by
|
|
21
|
+
* `streamdown`, and fighting a library with specificity is how a stylesheet
|
|
22
|
+
* becomes unmaintainable.
|
|
23
|
+
*
|
|
24
|
+
* The through-line is that a message is not a DOCUMENT. Document defaults —
|
|
25
|
+
* heavy code chips, big headings, generous block margins — are calibrated for a
|
|
26
|
+
* page the reader arrives at, not for a paragraph inside a conversation.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/* Monospace, INSIDE a sentence.
|
|
30
|
+
*
|
|
31
|
+
* The default here is a chip: background, padding, radius. At prose weight that
|
|
32
|
+
* reads heavier than the words around it, so a sentence with three identifiers
|
|
33
|
+
* in it turns into a row of buttons. Keeping the tint but cutting the padding
|
|
34
|
+
* and softening the ground keeps it identifiable as code without letting it
|
|
35
|
+
* outrank the sentence carrying it. */
|
|
36
|
+
.chat-surface :where(code, kbd, samp) {
|
|
37
|
+
font-size: var(--chat-text-code, 0.92em);
|
|
38
|
+
border-radius: 0.25rem;
|
|
39
|
+
padding: 0.05em 0.3em;
|
|
40
|
+
background: color-mix(in oklab, currentColor 8%, transparent);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* A fenced block is the opposite case: it IS the content, so it keeps its own
|
|
44
|
+
* ground and padding. Reset the inline rule rather than restate it — a `code`
|
|
45
|
+
* inside a `pre` matched the selector above and got a second background. */
|
|
46
|
+
/* A fenced block scrolls rather than spilling out of the column, for the same
|
|
47
|
+
* reason a table does. The guards that hold prose do not reach it:
|
|
48
|
+
* `break-words` is scoped to the user bubble and code arrives from the model,
|
|
49
|
+
* and nothing wraps a long line inside a `pre`.
|
|
50
|
+
*
|
|
51
|
+
* What spills is not the block's own width — the `pre` measures at the column
|
|
52
|
+
* either way. Its CONTENT overflows it, and with `overflow-x: visible` that
|
|
53
|
+
* paints past the edge and grows the transcript's scroll area instead: measured
|
|
54
|
+
* live, one 400-character line took the scroller from 1218 to 3434 while every
|
|
55
|
+
* element in the row still reported 768. So the whole transcript scrolls
|
|
56
|
+
* sideways with no element admitting it did. */
|
|
57
|
+
.chat-surface :where(pre) {
|
|
58
|
+
font-size: var(--chat-text-code, 0.92em);
|
|
59
|
+
border-radius: 0.5rem;
|
|
60
|
+
line-height: 1.5;
|
|
61
|
+
overflow-x: auto;
|
|
62
|
+
}
|
|
63
|
+
.chat-surface :where(pre code, pre kbd, pre samp) {
|
|
64
|
+
padding: 0;
|
|
65
|
+
background: transparent;
|
|
66
|
+
font-size: inherit;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Headings stay close to the body. An `h1` at document scale inside a bubble
|
|
70
|
+
* reads as a shout, and the agent emits them freely. The steps are deliberately
|
|
71
|
+
* small — the job is to mark structure, not to build a title page. */
|
|
72
|
+
.chat-surface :where(h1, h2, h3, h4, h5, h6) {
|
|
73
|
+
font-size: calc(var(--chat-text, 1em) * 1.08);
|
|
74
|
+
line-height: 1.35;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
margin-block: 1.2em 0.5em;
|
|
77
|
+
}
|
|
78
|
+
.chat-surface :where(h1:first-child, h2:first-child, h3:first-child) {
|
|
79
|
+
margin-block-start: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Lists need their marker gutter back.
|
|
83
|
+
*
|
|
84
|
+
* Measured on a live app: `padding-left: 0`, so the bullets rendered
|
|
85
|
+
* OUTSIDE the text column and the items did not line up with the paragraphs
|
|
86
|
+
* above them. `1.4em` is a marker plus a space at the current size, and it
|
|
87
|
+
* scales with the type because it is an em. */
|
|
88
|
+
.chat-surface :where(ul, ol) {
|
|
89
|
+
padding-inline-start: 1.4em;
|
|
90
|
+
}
|
|
91
|
+
.chat-surface :where(li) {
|
|
92
|
+
margin-block: 0.15em;
|
|
93
|
+
}
|
|
94
|
+
.chat-surface :where(li > ul, li > ol) {
|
|
95
|
+
margin-block: 0.15em;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Block rhythm. One value, so paragraphs, lists, quotes and code blocks are
|
|
99
|
+
* separated by the same distance and the message reads as one column rather
|
|
100
|
+
* than as stacked cards. */
|
|
101
|
+
.chat-surface :where(p, ul, ol, pre, blockquote, table) {
|
|
102
|
+
margin-block: 0.7em;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* A notice is a UI row that happens to be a <p>, so the prose rhythm above
|
|
106
|
+
* spaces it against a sibling whose parent already sets a gap. Keyed on the
|
|
107
|
+
* element's own `data-slot` rather than a class, so it cannot be lost by a
|
|
108
|
+
* caller passing `className`, and in `:where()` so it only REMOVES. */
|
|
109
|
+
.chat-surface :where(p[data-slot="notice"]) {
|
|
110
|
+
margin-block: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* The opt-out for UI rendered INSIDE the surface that is not prose.
|
|
114
|
+
*
|
|
115
|
+
* A popover, a tooltip or a menu portalled into `.chat-surface` inherits the
|
|
116
|
+
* block rhythm above, which spaces its rows like paragraphs — three equal gaps
|
|
117
|
+
* where the design has a heading, a subtitle and a detail line. A consumer
|
|
118
|
+
* cannot win that with a utility class: `:where()` scores zero, so the two tie
|
|
119
|
+
* on specificity and this sheet loads later.
|
|
120
|
+
*
|
|
121
|
+
* `.chat-plain` states the exemption where the rhythm is defined, so a reader
|
|
122
|
+
* of either sees both. It also resets the type scale, because the same surface
|
|
123
|
+
* sets a prose line-height that a dense card must not inherit.
|
|
124
|
+
*
|
|
125
|
+
* Wrapped in `:where()` for the same reason everything else here is: this must
|
|
126
|
+
* REMOVE the prose rhythm, not impose a rhythm of its own. Written as a plain
|
|
127
|
+
* class it scored (0,2,0) and beat the card's own `mt-*` utilities — the rows
|
|
128
|
+
* collapsed against each other and no spacing set on them had any effect. */
|
|
129
|
+
.chat-surface :where(.chat-plain) :where(p, ul, ol, pre, blockquote, table) {
|
|
130
|
+
margin-block: 0;
|
|
131
|
+
}
|
|
132
|
+
.chat-surface :where(.chat-plain) {
|
|
133
|
+
font-size: 0.875rem;
|
|
134
|
+
line-height: 1.4;
|
|
135
|
+
}
|
|
136
|
+
/* No leading gap at the top of a message, and none trailing at the bottom —
|
|
137
|
+
* the bubble's own padding owns those edges. Scoped to a first/last child of
|
|
138
|
+
* ANY container rather than of the surface, because each message renders inside
|
|
139
|
+
* its own wrapper; but `> *` on that wrapper only, so a nested list's first item
|
|
140
|
+
* keeps its rhythm. */
|
|
141
|
+
.chat-surface :where(* > p:first-child, * > ul:first-child, * > ol:first-child, * > pre:first-child) {
|
|
142
|
+
margin-block-start: 0;
|
|
143
|
+
}
|
|
144
|
+
.chat-surface :where(* > p:last-child, * > ul:last-child, * > ol:last-child, * > pre:last-child) {
|
|
145
|
+
margin-block-end: 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* A quote is marked by a RULE, not by a filled box: it is an aside inside a
|
|
149
|
+
* message that already sits in a bubble, and a second surface inside the first
|
|
150
|
+
* reads as a nested card. */
|
|
151
|
+
.chat-surface :where(blockquote) {
|
|
152
|
+
padding-inline-start: 0.9em;
|
|
153
|
+
border-inline-start: 2px solid color-mix(in oklab, currentColor 20%, transparent);
|
|
154
|
+
color: color-mix(in oklab, currentColor 75%, transparent);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Tables scroll rather than widen the column. A wide table is the one markdown
|
|
158
|
+
* element that can break the reading column outright, and a horizontal scrollbar
|
|
159
|
+
* on the table beats one on the page. */
|
|
160
|
+
.chat-surface :where(table) {
|
|
161
|
+
display: block;
|
|
162
|
+
overflow-x: auto;
|
|
163
|
+
border-collapse: collapse;
|
|
164
|
+
font-size: var(--chat-text-small, 0.8em);
|
|
165
|
+
}
|
|
166
|
+
.chat-surface :where(th, td) {
|
|
167
|
+
padding: 0.35em 0.6em;
|
|
168
|
+
border: 1px solid color-mix(in oklab, currentColor 15%, transparent);
|
|
169
|
+
text-align: start;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* A separator between thoughts, not a page break. */
|
|
173
|
+
.chat-surface :where(hr) {
|
|
174
|
+
margin-block: 1.2em;
|
|
175
|
+
border: 0;
|
|
176
|
+
border-block-start: 1px solid color-mix(in oklab, currentColor 15%, transparent);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* An inline chip — a file the agent named, or a bare url it wrote. Both are the
|
|
180
|
+
* sibling of inline code above, and both follow the same three rules for the
|
|
181
|
+
* same reasons, so the rules live on ONE class and each kind adds only what is
|
|
182
|
+
* genuinely its own.
|
|
183
|
+
*
|
|
184
|
+
* Sized in `em` so it tracks the bubble's text instead of pinning itself to a
|
|
185
|
+
* rem the surface knows nothing about. Coloured by INHERITANCE, never by a
|
|
186
|
+
* named token: the user's bubble has no primary fill in dark theme, so a
|
|
187
|
+
* `--primary-foreground` resolved to near-black against light prose and the
|
|
188
|
+
* reference all but vanished. And the ground is mixed FROM that inherited
|
|
189
|
+
* colour, so it tints toward whichever direction the theme runs — a fixed white
|
|
190
|
+
* wash shows on a dark bubble and disappears on a light one.
|
|
191
|
+
*
|
|
192
|
+
* The class exists so the component carries none of this in a class list: the
|
|
193
|
+
* rule belongs with the prose it sits in, beside the code chip it matches. */
|
|
194
|
+
.chat-surface .chat-chip {
|
|
195
|
+
/* Neither a bare `button` nor an `a` inherits type and colour from its
|
|
196
|
+
* context, so the three lines a UA stylesheet would otherwise decide are
|
|
197
|
+
* stated here. For the file chip this is the whole reason it is a plain
|
|
198
|
+
* button: the shared `Button` gives these AND four other things a word
|
|
199
|
+
* inside a sentence must not have. For the link chip it is what strips the
|
|
200
|
+
* default underline and link colour. */
|
|
201
|
+
font: inherit;
|
|
202
|
+
color: inherit;
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
text-decoration: none;
|
|
205
|
+
font-size: var(--chat-text-code, 0.92em);
|
|
206
|
+
/* An inline-flex box exports its FIRST item's baseline, and the first item
|
|
207
|
+
* here is the icon — an SVG, which has no text baseline to give. That is why
|
|
208
|
+
* the label rode visibly higher than the words on either side, and why no
|
|
209
|
+
* amount of padding or line-height fixes it: none of them change WHICH child
|
|
210
|
+
* the baseline comes from.
|
|
211
|
+
*
|
|
212
|
+
* `align-items: baseline` makes the label the donor instead, and
|
|
213
|
+
* `align-self: center` on the icon (below) re-centres the icon against it.
|
|
214
|
+
* Measured against the neighbouring prose, chip bottom minus text bottom:
|
|
215
|
+
*
|
|
216
|
+
* line-height 1.4, align center → -3.50 (the reported defect)
|
|
217
|
+
* line-height inherit → -3.50, and 24.29px tall: `inherit`
|
|
218
|
+
* takes the multiplier, not the height
|
|
219
|
+
* line-height 1, align center → -3.50 height changed, drift did not
|
|
220
|
+
* align-items: baseline → 0.00
|
|
221
|
+
*
|
|
222
|
+
* `baseline-source: last` reaches the same result and is not used: it is the
|
|
223
|
+
* newer property, unsupported in Safari, and buys nothing here.
|
|
224
|
+
*
|
|
225
|
+
* `normal` rather than a number: the font's own line box is what the label
|
|
226
|
+
* needs to sit in, and every value tried moved the box relative to the text
|
|
227
|
+
* around it (`1` → -1.28, `1.29` → +0.98, `inherit` → +3.28, `normal` →
|
|
228
|
+
* -0.50). The residue is the difference between two DIFFERENT type sizes'
|
|
229
|
+
* bottom edges, not a baseline offset, so it cannot reach zero and does not
|
|
230
|
+
* need to. */
|
|
231
|
+
line-height: normal;
|
|
232
|
+
align-items: baseline;
|
|
233
|
+
border-radius: 0.25rem;
|
|
234
|
+
/* Horizontal padding only, and the vertical breathing room comes from the
|
|
235
|
+
* shadow below instead.
|
|
236
|
+
*
|
|
237
|
+
* Vertical padding cannot be used here: on a flex container the baseline is
|
|
238
|
+
* measured from the padding box, so every form of it drags the label off the
|
|
239
|
+
* line. Measured, drift against the neighbouring text:
|
|
240
|
+
*
|
|
241
|
+
* padding-block: 2px → +2.00
|
|
242
|
+
* padding-block + matching negative margin → +2.00 (margin is not counted)
|
|
243
|
+
* padding-block + vertical-align offset → +4.00 (they ADD, not cancel)
|
|
244
|
+
* padding-block on the label instead → +0.72
|
|
245
|
+
* box-shadow spread → 0.00
|
|
246
|
+
*/
|
|
247
|
+
padding: 0 0.3em;
|
|
248
|
+
/* Horizontal only, and exactly the shadow's spread: the ground below paints
|
|
249
|
+
* OUTSIDE the box, so without this it laps over the words on either side and
|
|
250
|
+
* the chip reads as touching them. Inline margin does not affect the
|
|
251
|
+
* baseline (measured: drift stays 0.00), which is why the axis that could not
|
|
252
|
+
* take padding can take this. */
|
|
253
|
+
margin-inline: 0.18em;
|
|
254
|
+
background: color-mix(in oklab, currentColor 10%, transparent);
|
|
255
|
+
/* The ground, grown evenly on all four sides. A shadow paints outside the
|
|
256
|
+
* border box without joining layout, so the chip reads as having padding
|
|
257
|
+
* while its baseline, its height and the paragraph's line box are all
|
|
258
|
+
* untouched. Same colour as the background, so the two are one shape. */
|
|
259
|
+
box-shadow: 0 0 0 0.18em color-mix(in oklab, currentColor 10%, transparent);
|
|
260
|
+
}
|
|
261
|
+
/* The icon must NOT sit on the baseline it just handed to the label — a glyph
|
|
262
|
+
* aligned that way hangs its whole body above the line. Centring it against the
|
|
263
|
+
* label is the counterpart to `align-items: baseline` above.
|
|
264
|
+
*
|
|
265
|
+
* The size is an em for the same reason the type is: a fixed 16px leaves the
|
|
266
|
+
* icon behind the moment a surface raises its text. */
|
|
267
|
+
.chat-surface .chat-chip > svg {
|
|
268
|
+
align-self: center;
|
|
269
|
+
width: 1em;
|
|
270
|
+
height: 1em;
|
|
271
|
+
}
|
|
272
|
+
/* A host-supplied typed icon arrives as a SPAN wrapping the svg, sized in
|
|
273
|
+
* pixels inline. Both have to be overridden here: the pixel size does not track
|
|
274
|
+
* the surface's type, and the span would donate the baseline the label owns. */
|
|
275
|
+
.chat-surface .chat-chip > span:where([aria-hidden="true"], [role="img"]) {
|
|
276
|
+
align-self: center;
|
|
277
|
+
width: 1em !important;
|
|
278
|
+
height: 1em !important;
|
|
279
|
+
}
|
|
280
|
+
.chat-surface .chat-chip > span:where([aria-hidden="true"], [role="img"]) > svg {
|
|
281
|
+
width: 100%;
|
|
282
|
+
height: 100%;
|
|
283
|
+
display: block;
|
|
284
|
+
}
|
|
285
|
+
/* Both surfaces move together, and INSTANTLY. A hover lighting only the
|
|
286
|
+
* background shows the shadow as a separate unlit ring; a transition covering
|
|
287
|
+
* only `background-color` eases one half while the ring snaps, which separates
|
|
288
|
+
* the two halves of one shape. Do not add a transition without covering both. */
|
|
289
|
+
.chat-surface .chat-chip:where(:hover) {
|
|
290
|
+
background: color-mix(in oklab, currentColor 18%, transparent);
|
|
291
|
+
box-shadow: 0 0 0 0.18em color-mix(in oklab, currentColor 18%, transparent);
|
|
292
|
+
}
|
|
293
|
+
/* An ordinary link the agent wrote, where the author chose the label.
|
|
294
|
+
*
|
|
295
|
+
* A host overriding the renderer's `a` component opts out of the library's
|
|
296
|
+
* default anchor, which is where the class, `target` and `rel` came from. With
|
|
297
|
+
* none of them a link is indistinguishable from the prose around it, so this
|
|
298
|
+
* states them rather than leaving the host's fallback bare.
|
|
299
|
+
*
|
|
300
|
+
* `overflow-wrap` because a url too long for the column otherwise widens the
|
|
301
|
+
* bubble instead of wrapping. */
|
|
302
|
+
.chat-surface .chat-link {
|
|
303
|
+
text-decoration: underline;
|
|
304
|
+
text-underline-offset: 0.15em;
|
|
305
|
+
overflow-wrap: anywhere;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* WHICH SIDE a link sits on is read from the DOM, not passed down.
|
|
309
|
+
*
|
|
310
|
+
* The accent is right against assistant prose and wrong inside a user bubble,
|
|
311
|
+
* whose saturated ground already spends the accent — so the link needs the
|
|
312
|
+
* bubble's own foreground there instead.
|
|
313
|
+
*
|
|
314
|
+
* That decision cannot travel as a prop. `components` is memoized ONCE per
|
|
315
|
+
* screen (`Response` re-parses every message when the object's identity
|
|
316
|
+
* changes), so a per-message value threaded through it would either be stale or
|
|
317
|
+
* cost a full re-parse per render. The message wrapper already publishes
|
|
318
|
+
* `data-role`, which is a fact about the DOM the link is standing in and needs
|
|
319
|
+
* no plumbing at all.
|
|
320
|
+
*
|
|
321
|
+
* Assistant is the default rather than a mirrored rule: an unknown or absent
|
|
322
|
+
* role reads as prose, which is the safe direction — an accent that fails to
|
|
323
|
+
* apply is a dull link, while a foreground that fails to apply is invisible
|
|
324
|
+
* text on a saturated ground. */
|
|
325
|
+
.chat-surface .chat-link,
|
|
326
|
+
.chat-surface .chat-link-chip {
|
|
327
|
+
color: var(--primary);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.chat-surface [data-role="user"] .chat-link,
|
|
331
|
+
.chat-surface [data-role="user"] .chat-link-chip {
|
|
332
|
+
color: inherit;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* A url label carries a domain AND a path, so the ceiling is generous; the
|
|
336
|
+
* component still elides the middle, and this is only the backstop. */
|
|
337
|
+
.chat-surface .chat-link-chip {
|
|
338
|
+
max-width: 20rem;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* A rendered diagram, and the source shown when one cannot be drawn.
|
|
342
|
+
*
|
|
343
|
+
* The SVG carries no dimensions of its own worth honouring — the renderer sizes
|
|
344
|
+
* it to the graph, which is routinely wider than the column. Constraining the
|
|
345
|
+
* element and letting the child scale is what keeps a wide diagram from
|
|
346
|
+
* widening the bubble, the same rule every other wide block here follows. */
|
|
347
|
+
.chat-surface .chat-diagram {
|
|
348
|
+
display: flex;
|
|
349
|
+
justify-content: center;
|
|
350
|
+
overflow-x: auto;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.chat-surface .chat-diagram > svg {
|
|
354
|
+
max-width: 100%;
|
|
355
|
+
height: auto;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* The fallback is the agent's own source, so it reads as a listing rather than
|
|
359
|
+
* as an error: the same ground and rhythm as a code block, because that is what
|
|
360
|
+
* it is. Colour comes from `currentColor` and cannot be wrong in either theme. */
|
|
361
|
+
.chat-surface .chat-diagram-source {
|
|
362
|
+
overflow-x: auto;
|
|
363
|
+
padding: 0.75rem;
|
|
364
|
+
border-radius: var(--radius, 0.5rem);
|
|
365
|
+
background: color-mix(in oklab, currentColor 6%, transparent);
|
|
366
|
+
font-size: 0.8125em;
|
|
367
|
+
white-space: pre;
|
|
368
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.chat-surface` and `.chat-column` — the two classes `ChatTranscript` puts on
|
|
3
|
+
* its own root, and so the two this package must make work.
|
|
4
|
+
*
|
|
5
|
+
* They were the host's for as long as the VALUES were the argument. They are
|
|
6
|
+
* not: what a transcript reads at and how wide it is allowed to get are still
|
|
7
|
+
* the product's answers, and they still arrive as `--chat-*` tokens the host
|
|
8
|
+
* declares. What lives here is only the WIRING — which class consumes which
|
|
9
|
+
* token — which is not a decision at all, and which broke as soon as the
|
|
10
|
+
* markup moved into this package while the rules stayed behind.
|
|
11
|
+
*
|
|
12
|
+
* Every token carries a fallback, so the components work in a host that
|
|
13
|
+
* declares none: a workbench, a test, a second consumer. A missing token must
|
|
14
|
+
* degrade to a readable default, never to zero.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The type. Chat is the one surface in an app that is sustained READING
|
|
19
|
+
* rather than scanning a control, so it does not inherit the control scale.
|
|
20
|
+
*/
|
|
21
|
+
.chat-surface {
|
|
22
|
+
font-size: var(--chat-text, 1rem);
|
|
23
|
+
line-height: var(--chat-leading, 1.65);
|
|
24
|
+
/* NORMAL, and that is the whole decision. A design system's negative tracking
|
|
25
|
+
* is tuned for LABELS — a chip, a menu row, something taken in at a glance,
|
|
26
|
+
* where tightening buys density. Prose is the opposite job: the reader needs
|
|
27
|
+
* the word shapes open. Measured against the two surfaces this one is judged
|
|
28
|
+
* beside, both set prose at `normal` and neither tightens it.
|
|
29
|
+
*
|
|
30
|
+
* Declared rather than left alone, because a token like
|
|
31
|
+
* `--letter-spacing-base` is set once high in the tree on a control-sized
|
|
32
|
+
* element — so it resolves THERE and would arrive here as fixed pixels
|
|
33
|
+
* computed for a size this surface does not use. */
|
|
34
|
+
letter-spacing: normal;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A fling that lands on the bottom edge must not hand its leftover momentum to
|
|
39
|
+
* the page behind, which then bounces back into this box.
|
|
40
|
+
*/
|
|
41
|
+
.chat-surface {
|
|
42
|
+
overscroll-behavior: contain;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The reading column, applied to the CONTENTS of a full-width surface.
|
|
47
|
+
*
|
|
48
|
+
* Every direct child is centred and capped, so the scroller keeps its scrollbar
|
|
49
|
+
* at the pane's edge while the transcript stays readable on a wide display. A
|
|
50
|
+
* host puts the same class on its composer wrapper — one rule for both, because
|
|
51
|
+
* a transcript and a composer at different widths disagree visibly about where
|
|
52
|
+
* the conversation is.
|
|
53
|
+
*
|
|
54
|
+
* A CSS rule rather than a wrapper element per row: the transcript renders its
|
|
55
|
+
* rows as direct children, and threading a container through it would make the
|
|
56
|
+
* primitive responsible for page layout it has no business knowing about.
|
|
57
|
+
*/
|
|
58
|
+
/*
|
|
59
|
+
* The scrollbar-side inset, shared by the transcript and the composer so the
|
|
60
|
+
* two never disagree about where the column ends.
|
|
61
|
+
*
|
|
62
|
+
* A token because the right answer differs by device: a desktop needs only
|
|
63
|
+
* enough to clear a real scrollbar, while a phone overlays its own and wants
|
|
64
|
+
* the inset to match the screen's other margins — reply text sat against the
|
|
65
|
+
* bare edge at 0px while the header beside it kept 20.
|
|
66
|
+
*/
|
|
67
|
+
.chat-inset-r {
|
|
68
|
+
padding-right: var(--chat-edge-r, 1rem);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.chat-column > * {
|
|
72
|
+
margin-inline: auto;
|
|
73
|
+
width: 100%;
|
|
74
|
+
max-width: var(--chat-column, 48rem);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The row a jump landed on, marked just long enough to be found.
|
|
79
|
+
*
|
|
80
|
+
* An animation rather than a persistent state: the mark answers "which row did
|
|
81
|
+
* I mean", and a highlight that stayed would become a selection nobody chose.
|
|
82
|
+
* Removed and re-added by `ChatSurfaces.revealMessage`, so a second jump to the
|
|
83
|
+
* same row flashes again.
|
|
84
|
+
*/
|
|
85
|
+
.chat-revealed {
|
|
86
|
+
animation: chat-reveal 1.2s ease-out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@keyframes chat-reveal {
|
|
90
|
+
from {
|
|
91
|
+
background: color-mix(in oklab, var(--primary) 18%, transparent);
|
|
92
|
+
}
|
|
93
|
+
to {
|
|
94
|
+
background: transparent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@media (prefers-reduced-motion: reduce) {
|
|
99
|
+
.chat-revealed {
|
|
100
|
+
animation-duration: 0.01ms;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool calls — a summary that opens.
|
|
3
|
+
*
|
|
4
|
+
* Form adapted from vercel/ai-elements `packages/elements/src/tool.tsx`
|
|
5
|
+
* (Apache-2.0, submodule @ 6a9d5b1): card shell, header-as-trigger, rotating
|
|
6
|
+
* chevron. The disclosure RULE is ours — they collapse unconditionally, which
|
|
7
|
+
* renders a running call as a stalled row.
|
|
8
|
+
*
|
|
9
|
+
* Here rather than in the class list because every rule below is appearance.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* Spacing is not here: `block.css` owns the rhythm for every block-level part,
|
|
13
|
+
* because a tool call and a card sitting next to each other need one rule, not
|
|
14
|
+
* two that agree until someone edits one of them. */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The running marker TUMBLES, reusing `turn-mark-tumble` — one motion for every
|
|
18
|
+
* place this app says "still going".
|
|
19
|
+
*
|
|
20
|
+
* A square, never a circle, and that is the reference's measured point: the
|
|
21
|
+
* corners are what make rotation visible, and at this size a turning circle is
|
|
22
|
+
* indistinguishable from a still one. A ring would be worse still — it promises
|
|
23
|
+
* progress, and no tool here knows its own duration.
|
|
24
|
+
*/
|
|
25
|
+
.chat-tool-dot {
|
|
26
|
+
inline-size: 0.5rem;
|
|
27
|
+
block-size: 0.5rem;
|
|
28
|
+
border-radius: 999px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Bigger while it turns: at 8px the corners travel too few pixels for the
|
|
32
|
+
* rotation to register, which is the whole reason this is a square. */
|
|
33
|
+
.chat-tool-dot[data-running] {
|
|
34
|
+
inline-size: 0.625rem;
|
|
35
|
+
block-size: 0.625rem;
|
|
36
|
+
border-radius: 0.1rem;
|
|
37
|
+
animation: turn-mark-tumble 1.6s linear infinite;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A running call is legible as a BLOCK, not as an 8px glyph.
|
|
42
|
+
*
|
|
43
|
+
* The marker alone put the entire signal in the smallest element on the row —
|
|
44
|
+
* at a glance a running call and a finished one were the same card. The border
|
|
45
|
+
* carries it at the size of the thing it describes; the marker then confirms
|
|
46
|
+
* what the border already said.
|
|
47
|
+
*
|
|
48
|
+
* Border colour only: no ground, no shadow, nothing that shifts layout when a
|
|
49
|
+
* call completes.
|
|
50
|
+
*/
|
|
51
|
+
.chat-tool[data-state="input-streaming"],
|
|
52
|
+
.chat-tool[data-state="input-available"] {
|
|
53
|
+
border-color: color-mix(in oklab, var(--primary) 45%, var(--border));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* The status word earns the accent for the same reason — "Running" printed in
|
|
57
|
+
* the same grey as "Done" states the fact and hides it. */
|
|
58
|
+
.chat-tool[data-state="input-streaming"] .chat-tool-status,
|
|
59
|
+
.chat-tool[data-state="input-available"] .chat-tool-status {
|
|
60
|
+
color: var(--primary);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@media (prefers-reduced-motion: reduce) {
|
|
64
|
+
/* Still, but not invisible — the accent colour alone carries "running". */
|
|
65
|
+
.chat-tool-dot[data-running] {
|
|
66
|
+
animation: none;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* The header is the trigger, so it must read as pressable across its width. */
|
|
71
|
+
.chat-tool-header {
|
|
72
|
+
display: flex;
|
|
73
|
+
width: 100%;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: space-between;
|
|
76
|
+
gap: 0.75rem;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
text-align: start;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Transform only: rotation costs no layout, so the header cannot shift. */
|
|
82
|
+
.chat-tool-chevron {
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
transition: transform 150ms ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.chat-tool[data-open] .chat-tool-chevron {
|
|
88
|
+
transform: rotate(180deg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (prefers-reduced-motion: reduce) {
|
|
92
|
+
.chat-tool-chevron {
|
|
93
|
+
transition: none;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Tool output is the one surface here guaranteed to carry machine-generated strings —
|
|
99
|
+
* paths, hashes, base64, a datamarked transcript whose words are joined by a
|
|
100
|
+
* marker rune. None carry a break opportunity, so the box grows until the text
|
|
101
|
+
* leaves it.
|
|
102
|
+
*
|
|
103
|
+
* `anywhere`, not `break-all`: `break-all` chops ordinary prose too, and a
|
|
104
|
+
* result is usually both. Same property as the long-url rule in `markdown.css`.
|
|
105
|
+
*
|
|
106
|
+
* `min-width: 0` is load-bearing: a grid item's automatic minimum size is its
|
|
107
|
+
* content, so an unbreakable token widens the track before wrapping applies.
|
|
108
|
+
*/
|
|
109
|
+
.chat-tool-input,
|
|
110
|
+
.chat-tool-output {
|
|
111
|
+
min-width: 0;
|
|
112
|
+
overflow-wrap: anywhere;
|
|
113
|
+
white-space: pre-wrap;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Collapsing solves the common case, not the reader who OPENS a call that
|
|
117
|
+
* returned five tool descriptions. Capped so no result buries the answer, tall
|
|
118
|
+
* enough that an ordinary one needs no scrolling. */
|
|
119
|
+
.chat-tool-output {
|
|
120
|
+
max-height: 18rem;
|
|
121
|
+
overflow-y: auto;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Never capped: an error is short, and it is why the turn went as it did. */
|
|
125
|
+
.chat-tool-output[data-error] {
|
|
126
|
+
max-height: none;
|
|
127
|
+
color: var(--destructive, hsl(0 67% 60%));
|
|
128
|
+
}
|