@adia-ai/web-modules 0.0.4

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +119 -0
  3. package/chat/chat-shell/chat-shell.a2ui.json +149 -0
  4. package/chat/chat-shell/chat-shell.css +10 -0
  5. package/chat/chat-shell/chat-shell.js +297 -0
  6. package/chat/chat-shell/chat-shell.yaml +119 -0
  7. package/chat/chat-shell/css/chat-shell.empty.css +12 -0
  8. package/chat/chat-shell/css/chat-shell.layout.css +60 -0
  9. package/chat/chat-shell/css/chat-shell.markdown.css +74 -0
  10. package/chat/chat-shell/css/chat-shell.messages.css +87 -0
  11. package/chat/chat-shell/css/chat-shell.streaming.css +30 -0
  12. package/chat/chat-shell/css/chat-shell.tokens.css +95 -0
  13. package/chat/index.js +1 -0
  14. package/editor/editor-shell/css/editor-shell.layout.css +171 -0
  15. package/editor/editor-shell/css/editor-shell.tokens.css +28 -0
  16. package/editor/editor-shell/editor-shell.a2ui.json +73 -0
  17. package/editor/editor-shell/editor-shell.css +6 -0
  18. package/editor/editor-shell/editor-shell.js +56 -0
  19. package/editor/editor-shell/editor-shell.yaml +59 -0
  20. package/editor/index.js +1 -0
  21. package/index.js +14 -0
  22. package/package.json +48 -0
  23. package/runtime/a2ui-root/a2ui-root.a2ui.json +125 -0
  24. package/runtime/a2ui-root/a2ui-root.js +191 -0
  25. package/runtime/a2ui-root/a2ui-root.yaml +87 -0
  26. package/runtime/gen-root/gen-root.a2ui.json +72 -0
  27. package/runtime/gen-root/gen-root.css +83 -0
  28. package/runtime/gen-root/gen-root.js +136 -0
  29. package/runtime/gen-root/gen-root.yaml +43 -0
  30. package/runtime/index.js +2 -0
  31. package/shell/admin-shell/admin-shell.a2ui.json +129 -0
  32. package/shell/admin-shell/admin-shell.css +14 -0
  33. package/shell/admin-shell/admin-shell.js +261 -0
  34. package/shell/admin-shell/admin-shell.yaml +89 -0
  35. package/shell/admin-shell/css/admin-shell.collapsed.css +86 -0
  36. package/shell/admin-shell/css/admin-shell.helpers.css +42 -0
  37. package/shell/admin-shell/css/admin-shell.main.css +182 -0
  38. package/shell/admin-shell/css/admin-shell.shell.css +48 -0
  39. package/shell/admin-shell/css/admin-shell.sidebar.css +165 -0
  40. package/shell/admin-shell/css/admin-shell.templates.css +215 -0
  41. package/shell/admin-shell/css/admin-shell.tokens.css +119 -0
  42. package/shell/index.js +1 -0
@@ -0,0 +1,297 @@
1
+ import { UIElement } from '../../../web-components/core/element.js';
2
+ import { renderMarkdown } from '../../../web-components/core/markdown.js';
3
+ import { streamChat } from '../../../a2ui/compose/llm/adapters/index.js';
4
+
5
+ function escapeHTML(s) {
6
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
7
+ }
8
+
9
+ let msgId = 0;
10
+
11
+ /**
12
+ * <chat-shell> — LLM-streaming chat module.
13
+ *
14
+ * Behavior-only orchestrator: stamps no HTML of its own.
15
+ * The author writes the structure; chat-shell wires the behaviors.
16
+ *
17
+ * Structure:
18
+ * <chat-shell proxy-url="/api/chat" model="claude-sonnet-4-20250514">
19
+ * <header>
20
+ * <span data-chat-name>Claude</span>
21
+ * <span data-chat-status></span>
22
+ * </header>
23
+ * <section data-chat-messages>
24
+ * <empty-state-ui data-chat-empty icon="chat-circle"
25
+ * heading="Hello!" description="Ask me anything.">
26
+ * </empty-state-ui>
27
+ * </section>
28
+ * <footer>
29
+ * <chat-input-ui data-chat-input placeholder="Message..."></chat-input-ui>
30
+ * </footer>
31
+ * </chat-shell>
32
+ *
33
+ * Two modes:
34
+ * 1. Built-in: set proxy-url + model, chat-shell streams via LLM adapters
35
+ * 2. External: listen for 'submit', call your API, use appendChunk() etc.
36
+ *
37
+ * Events: submit, chunk, thinking, done, error, abort, clear
38
+ */
39
+ class ChatShell extends UIElement {
40
+ static properties = {
41
+ streaming: { type: Boolean, default: false, reflect: true },
42
+ provider: { type: String, default: '', reflect: true },
43
+ model: { type: String, default: '', reflect: true },
44
+ system: { type: String, default: '', reflect: true },
45
+ proxyUrl: { type: String, default: '', attribute: 'proxy-url', reflect: true },
46
+ thinking: { type: Boolean, default: false, reflect: true },
47
+ };
48
+
49
+ static template = () => null;
50
+
51
+ #messages = [];
52
+ #abortController = null;
53
+ #messagesEl = null;
54
+ #inputEl = null;
55
+ #emptyEl = null;
56
+ #statusEl = null;
57
+ #apiKey = '';
58
+
59
+ // ── Accessors ──
60
+
61
+ get messages() { return [...this.#messages]; }
62
+
63
+ get conversation() {
64
+ return this.#messages
65
+ .filter(m => m.role === 'user' || m.role === 'assistant')
66
+ .map(m => ({ role: m.role, content: m.content }));
67
+ }
68
+
69
+ set conversation(msgs) {
70
+ this.clear();
71
+ for (const m of msgs) {
72
+ this.appendMessage({ role: m.role, content: m.content, render: true });
73
+ }
74
+ }
75
+
76
+ set apiKey(key) { this.#apiKey = key; }
77
+
78
+ // ── Lifecycle ──
79
+
80
+ connected() {
81
+ this.#messagesEl = this.querySelector('[data-chat-messages]') || this.querySelector('section');
82
+ this.#inputEl = this.querySelector('[data-chat-input]') || this.querySelector('chat-input-ui');
83
+ this.#emptyEl = this.querySelector('[data-chat-empty]');
84
+ this.#statusEl = this.querySelector('[data-chat-status]');
85
+
86
+ this.#inputEl?.addEventListener('submit', this.#onSubmit);
87
+ }
88
+
89
+ disconnected() {
90
+ this.#inputEl?.removeEventListener('submit', this.#onSubmit);
91
+ this.abort();
92
+ }
93
+
94
+ // ── Events ──
95
+
96
+ #emit(name, detail) {
97
+ this.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }));
98
+ }
99
+
100
+ #onSubmit = (e) => {
101
+ if (this.streaming) return;
102
+ const { text, model } = e.detail || {};
103
+ if (!text) return;
104
+ this.#inputEl.clear();
105
+
106
+ this.#emit('submit', { text, model });
107
+
108
+ // If proxy-url or apiKey is set, auto-send
109
+ if (this.proxyUrl || this.#apiKey) {
110
+ this.send(text, model ? { model } : {});
111
+ }
112
+ };
113
+
114
+ // ── Message management ──
115
+
116
+ appendMessage({ role, content = '', render: renderMd = false }) {
117
+ const id = `msg_${++msgId}`;
118
+ this.#messages.push({ id, role, content });
119
+
120
+ const el = document.createElement('div');
121
+ el.setAttribute('data-role', role);
122
+ el.setAttribute('data-id', id);
123
+
124
+ if (role === 'user') {
125
+ el.innerHTML = `<div data-bubble>${escapeHTML(content)}</div>`;
126
+ } else if (role === 'assistant') {
127
+ const rendered = renderMd && content ? renderMarkdown(content) : escapeHTML(content);
128
+ el.innerHTML = `
129
+ <span data-avatar>AI</span>
130
+ <div data-bubble><div data-content>${rendered}</div>${!renderMd ? '<span data-cursor></span>' : ''}</div>
131
+ `;
132
+ } else if (role === 'error') {
133
+ el.innerHTML = `<icon-ui name="warning"></icon-ui><span>${escapeHTML(content)}</span>`;
134
+ }
135
+
136
+ this.#messagesEl?.appendChild(el);
137
+ this.#scrollToBottom();
138
+ this.#emit('message', { id, role, content });
139
+ return el;
140
+ }
141
+
142
+ appendChunk(text) {
143
+ const last = this.#messages[this.#messages.length - 1];
144
+ if (!last || last.role !== 'assistant') return;
145
+ last.content += text;
146
+
147
+ const contentEl = this.#messagesEl?.querySelector('[data-role]:last-child [data-content]');
148
+ if (contentEl) {
149
+ contentEl.insertAdjacentText('beforeend', text);
150
+ }
151
+ this.#scrollToBottom();
152
+ }
153
+
154
+ deleteMessage(id) {
155
+ const idx = this.#messages.findIndex(m => m.id === id);
156
+ if (idx === -1) return;
157
+ this.#messages.splice(idx, 1);
158
+ this.#messagesEl?.querySelector(`[data-id="${id}"]`)?.remove();
159
+ }
160
+
161
+ clear() {
162
+ this.#messages.length = 0;
163
+ if (this.#messagesEl) {
164
+ // Preserve empty state element
165
+ const empty = this.#emptyEl;
166
+ this.#messagesEl.innerHTML = '';
167
+ if (empty) this.#messagesEl.appendChild(empty);
168
+ }
169
+ this.#emit('clear');
170
+ }
171
+
172
+ // ── Streaming control ──
173
+
174
+ startStreaming() {
175
+ this.streaming = true;
176
+ if (this.#inputEl) this.#inputEl.disabled = true;
177
+ if (this.#statusEl) this.#statusEl.textContent = 'Typing...';
178
+ }
179
+
180
+ stopStreaming() {
181
+ this.streaming = false;
182
+ if (this.#inputEl) this.#inputEl.disabled = false;
183
+ if (this.#statusEl) this.#statusEl.textContent = '';
184
+
185
+ // Remove cursor
186
+ this.#messagesEl?.querySelector('[data-role]:last-child [data-cursor]')?.remove();
187
+
188
+ // Render markdown in last assistant bubble
189
+ const last = this.#messages[this.#messages.length - 1];
190
+ if (last?.role === 'assistant' && last.content) {
191
+ const contentEl = this.#messagesEl?.querySelector('[data-role]:last-child [data-content]');
192
+ if (contentEl) {
193
+ contentEl.innerHTML = renderMarkdown(last.content);
194
+ // Upgrade code blocks to code-ui
195
+ this.#upgradeCodeBlocks(contentEl);
196
+ }
197
+ }
198
+
199
+ this.#inputEl?.focus();
200
+ }
201
+
202
+ abort() {
203
+ if (this.#abortController) {
204
+ this.#abortController.abort();
205
+ this.#abortController = null;
206
+ this.#emit('abort');
207
+ }
208
+ if (this.streaming) this.stopStreaming();
209
+ }
210
+
211
+ // ── LLM send ──
212
+
213
+ async send(text, opts = {}) {
214
+ const model = opts.model || this.model || this.#inputEl?.model;
215
+ if (!model) throw new Error('No model specified');
216
+
217
+ this.appendMessage({ role: 'user', content: text });
218
+ this.appendMessage({ role: 'assistant', content: '' });
219
+ this.startStreaming();
220
+
221
+ this.#abortController = new AbortController();
222
+
223
+ try {
224
+ const streamOpts = {
225
+ provider: this.provider || undefined,
226
+ apiKey: this.#apiKey || undefined,
227
+ model,
228
+ system: this.system || undefined,
229
+ proxyUrl: this.proxyUrl || undefined,
230
+ thinking: this.thinking || undefined,
231
+ messages: this.conversation.slice(0, -1), // exclude empty assistant
232
+ signal: this.#abortController.signal,
233
+ ...opts,
234
+ };
235
+
236
+ for await (const chunk of streamChat(streamOpts)) {
237
+ if (chunk.type === 'text') {
238
+ this.appendChunk(chunk.text);
239
+ this.#emit('chunk', { text: chunk.text, snapshot: chunk.snapshot });
240
+ } else if (chunk.type === 'thinking') {
241
+ this.#emit('thinking', { text: chunk.text });
242
+ } else if (chunk.type === 'done') {
243
+ this.#emit('done', { text: chunk.text, usage: chunk.usage, stopReason: chunk.stopReason });
244
+ } else if (chunk.type === 'error') {
245
+ this.appendMessage({ role: 'error', content: chunk.error.message });
246
+ this.#emit('error', { error: chunk.error });
247
+ }
248
+ }
249
+ } catch (err) {
250
+ if (err.name !== 'AbortError') {
251
+ this.appendMessage({ role: 'error', content: err.message });
252
+ this.#emit('error', { error: err });
253
+ }
254
+ }
255
+
256
+ this.#abortController = null;
257
+ this.stopStreaming();
258
+ }
259
+
260
+ // ── Export / import ──
261
+
262
+ export() {
263
+ return {
264
+ messages: this.#messages.map(m => ({ role: m.role, content: m.content })),
265
+ model: this.model,
266
+ system: this.system,
267
+ };
268
+ }
269
+
270
+ import(data) {
271
+ if (data.model) this.model = data.model;
272
+ if (data.system) this.system = data.system;
273
+ if (data.messages) this.conversation = data.messages;
274
+ }
275
+
276
+ // ── Private ──
277
+
278
+ #scrollToBottom() {
279
+ const el = this.#messagesEl;
280
+ if (el) requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; });
281
+ }
282
+
283
+ #upgradeCodeBlocks(container) {
284
+ for (const pre of container.querySelectorAll('pre')) {
285
+ const code = pre.querySelector('code');
286
+ if (!code) continue;
287
+ const lang = code.getAttribute('data-lang') || '';
288
+ const codeN = document.createElement('code-ui');
289
+ if (lang) codeN.setAttribute('language', lang);
290
+ codeN.textContent = code.textContent;
291
+ pre.replaceWith(codeN);
292
+ }
293
+ }
294
+ }
295
+
296
+ customElements.define('chat-shell', ChatShell);
297
+ export { ChatShell };
@@ -0,0 +1,119 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: ChatShell
3
+ tag: chat-shell
4
+ component: ChatShell
5
+ category: container
6
+ version: 1
7
+ description: |
8
+ Behavior-only chat orchestrator (LLM-streaming module). Author supplies the
9
+ DOM structure via [data-chat-messages], [data-chat-input], [data-chat-empty],
10
+ [data-chat-status] elements; chat-shell wires message streaming, markdown
11
+ rendering, code-block upgrades, and an LLM integration path via proxy-url
12
+ (or via external submit).
13
+
14
+ props:
15
+ streaming:
16
+ type: boolean
17
+ default: false
18
+ reflect: true
19
+ description: Active streaming indicator; toggled while a response is being received.
20
+
21
+ provider:
22
+ type: string
23
+ default: ""
24
+ reflect: true
25
+ description: LLM provider name (anthropic | openai | google | stub).
26
+
27
+ model:
28
+ type: string
29
+ default: ""
30
+ reflect: true
31
+ description: Model identifier.
32
+
33
+ system:
34
+ type: string
35
+ default: ""
36
+ reflect: true
37
+ description: System prompt prepended to conversations.
38
+
39
+ proxyUrl:
40
+ type: string
41
+ default: ""
42
+ reflect: true
43
+ attribute: proxy-url
44
+ description: API proxy endpoint for LLM calls; enables self-contained chat without external wiring.
45
+
46
+ thinking:
47
+ type: boolean
48
+ default: false
49
+ reflect: true
50
+ description: Enable Anthropic extended-thinking mode.
51
+
52
+ events:
53
+ submit:
54
+ description: Fired on user message submit (before LLM call begins).
55
+ detail:
56
+ text: string
57
+ model: string
58
+
59
+ chunk:
60
+ description: Fired for each streaming chunk.
61
+ detail:
62
+ text: string
63
+ snapshot: string
64
+
65
+ thinking:
66
+ description: Fired when the model emits extended-thinking content.
67
+ detail:
68
+ text: string
69
+
70
+ done:
71
+ description: Fired when a response completes.
72
+ detail:
73
+ text: string
74
+ usage: object
75
+ stopReason: string
76
+
77
+ error:
78
+ description: Fired on any LLM / network error.
79
+ detail:
80
+ error: Error
81
+
82
+ abort:
83
+ description: Fired when the user aborts an in-flight request.
84
+
85
+ clear:
86
+ description: Fired when the conversation is cleared.
87
+
88
+ message:
89
+ description: Fired after each message (user or assistant) is appended.
90
+ detail:
91
+ id: string
92
+ role: string
93
+ content: string
94
+
95
+ slots:
96
+ default:
97
+ description: >-
98
+ Author provides the structural DOM. Expected markers —
99
+ [data-chat-messages] (message list), [data-chat-input] (input surface),
100
+ [data-chat-empty] (empty state), [data-chat-status] (streaming indicator).
101
+
102
+ states:
103
+ - name: idle
104
+ description: No active request.
105
+ - name: streaming
106
+ attribute: streaming
107
+ description: An LLM request is in-flight; [data-chat-status] visible.
108
+
109
+ traits: []
110
+
111
+ a2ui:
112
+ rules:
113
+ - chat-shell is a behavior wrapper; don't nest col-ui/row-ui inside it directly —
114
+ author the structural DOM using [data-chat-*] markers instead.
115
+
116
+ keywords: [chat-shell, chat, llm, streaming, conversation, agent]
117
+ synonyms:
118
+ chat: [conversation, messages, thread]
119
+ related: [ChatInput, Code]
@@ -0,0 +1,12 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ chat-shell — Empty state
3
+ ═══════════════════════════════════════════════════════════════ */
4
+
5
+ chat-shell [data-chat-empty] {
6
+ margin: auto;
7
+ }
8
+
9
+ chat-shell[streaming] [data-chat-empty],
10
+ chat-shell:has([data-role]) [data-chat-empty] {
11
+ display: none;
12
+ }
@@ -0,0 +1,60 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ chat-shell — Root layout
3
+ ═══════════════════════════════════════════════════════════════ */
4
+
5
+ chat-shell {
6
+ box-sizing: border-box;
7
+ display: flex;
8
+ flex-direction: column;
9
+ height: 100%;
10
+ overflow: hidden;
11
+ border: var(--chat-border);
12
+ border-radius: var(--chat-radius);
13
+ background: var(--chat-bg);
14
+ }
15
+
16
+ /* Header */
17
+ chat-shell > header {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: var(--chat-header-gap);
21
+ min-height: var(--chat-header-height);
22
+ padding: 0 var(--chat-header-px);
23
+ border-bottom: var(--chat-header-border);
24
+ flex-shrink: 0;
25
+ }
26
+
27
+ chat-shell > header [data-chat-name] {
28
+ font-weight: var(--chat-weight-semibold);
29
+ font-size: var(--chat-header-name-font);
30
+ }
31
+
32
+ chat-shell > header [data-chat-status] {
33
+ font-size: var(--chat-header-status-font);
34
+ color: var(--chat-header-status-fg);
35
+ margin-inline-start: auto;
36
+ }
37
+
38
+ /* Messages scroll container */
39
+ chat-shell > [data-chat-messages],
40
+ chat-shell > section {
41
+ flex: 1;
42
+ overflow-y: auto;
43
+ padding: var(--chat-messages-py) var(--chat-messages-px);
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: var(--chat-messages-gap);
47
+ min-height: 0;
48
+ }
49
+
50
+ /* Footer */
51
+ chat-shell > footer {
52
+ flex-shrink: 0;
53
+ display: flex;
54
+ align-items: center;
55
+ padding: var(--chat-footer-py) var(--chat-footer-px);
56
+ }
57
+
58
+ chat-shell > footer chat-input-ui {
59
+ flex: 1;
60
+ }
@@ -0,0 +1,74 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ chat-shell — Markdown prose inside bubbles
3
+ ═══════════════════════════════════════════════════════════════ */
4
+
5
+ chat-shell [data-bubble] [data-content] {
6
+ white-space: normal;
7
+ }
8
+
9
+ chat-shell [data-bubble] [data-content] p {
10
+ margin: 0 0 0.5em;
11
+ }
12
+
13
+ chat-shell [data-bubble] [data-content] p:last-child {
14
+ margin-bottom: 0;
15
+ }
16
+
17
+ chat-shell [data-bubble] [data-content] strong {
18
+ font-weight: var(--chat-weight-semibold);
19
+ }
20
+
21
+ chat-shell [data-bubble] [data-content] code {
22
+ background: var(--chat-code-bg);
23
+ padding: 0.1em 0.15em;
24
+ border-radius: var(--chat-code-inline-radius);
25
+ font-family: var(--chat-code-inline-family);
26
+ font-size: 0.875em;
27
+ }
28
+
29
+ chat-shell [data-bubble] [data-content] pre {
30
+ background: var(--chat-code-bg);
31
+ border-radius: var(--chat-code-radius);
32
+ padding: var(--chat-code-block-px);
33
+ overflow-x: auto;
34
+ margin: var(--chat-code-block-my) 0;
35
+ font-size: var(--chat-code-block-font);
36
+ line-height: 1.5;
37
+ }
38
+
39
+ chat-shell [data-bubble] [data-content] pre code {
40
+ background: none;
41
+ padding: 0;
42
+ border-radius: 0;
43
+ font-size: inherit;
44
+ }
45
+
46
+ chat-shell [data-bubble] [data-content] ul,
47
+ chat-shell [data-bubble] [data-content] ol {
48
+ margin: 0.25em 0;
49
+ padding-inline-start: 1.25em;
50
+ }
51
+
52
+ chat-shell [data-bubble] [data-content] li {
53
+ margin-bottom: 0.15em;
54
+ }
55
+
56
+ chat-shell [data-bubble] [data-content] a {
57
+ color: inherit;
58
+ text-decoration: underline;
59
+ text-underline-offset: 2px;
60
+ }
61
+
62
+ chat-shell [data-bubble] [data-content] h1,
63
+ chat-shell [data-bubble] [data-content] h2,
64
+ chat-shell [data-bubble] [data-content] h3,
65
+ chat-shell [data-bubble] [data-content] h4 {
66
+ font-weight: var(--chat-weight-semibold);
67
+ margin: 0.75em 0 0.25em;
68
+ }
69
+
70
+ chat-shell [data-bubble] [data-content] h1:first-child,
71
+ chat-shell [data-bubble] [data-content] h2:first-child,
72
+ chat-shell [data-bubble] [data-content] h3:first-child {
73
+ margin-top: 0;
74
+ }
@@ -0,0 +1,87 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ chat-shell — Message bubbles
3
+ ═══════════════════════════════════════════════════════════════ */
4
+
5
+ /* Message row */
6
+ chat-shell [data-role] {
7
+ display: flex;
8
+ gap: var(--chat-header-gap);
9
+ align-items: flex-end;
10
+ max-width: var(--chat-message-max-width);
11
+ }
12
+
13
+ chat-shell [data-role="user"] {
14
+ align-self: flex-end;
15
+ flex-direction: row-reverse;
16
+ }
17
+
18
+ chat-shell [data-role="assistant"] {
19
+ align-self: flex-start;
20
+ }
21
+
22
+ /* Avatar */
23
+ chat-shell [data-role] [data-avatar] {
24
+ width: var(--chat-avatar-size);
25
+ height: var(--chat-avatar-size);
26
+ border-radius: var(--chat-avatar-radius);
27
+ background: var(--chat-avatar-bg);
28
+ color: var(--chat-avatar-fg);
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ font-size: var(--chat-avatar-font);
33
+ font-weight: var(--chat-avatar-weight);
34
+ flex-shrink: 0;
35
+ }
36
+
37
+ /* Bubble */
38
+ chat-shell [data-role] [data-bubble] {
39
+ padding: var(--chat-assistant-py) var(--chat-assistant-px);
40
+ border-radius: var(--chat-assistant-radius);
41
+ font-size: var(--chat-font-size);
42
+ line-height: var(--chat-line-height);
43
+ white-space: pre-wrap;
44
+ word-break: break-word;
45
+ }
46
+
47
+ chat-shell [data-role] [data-bubble] > :first-child { margin-block-start: 0; }
48
+ chat-shell [data-role] [data-bubble] > :last-child { margin-block-end: 0; }
49
+
50
+ /* User bubble */
51
+ chat-shell [data-role="user"] [data-bubble] {
52
+ background: var(--chat-user-bg);
53
+ color: var(--chat-user-fg);
54
+ padding: var(--chat-user-py) var(--chat-user-px);
55
+ border-radius: var(--chat-user-radius);
56
+ border-bottom-right-radius: var(--chat-user-tail-radius);
57
+ }
58
+
59
+ /* Assistant bubble */
60
+ chat-shell [data-role="assistant"] [data-bubble] {
61
+ background: var(--chat-assistant-bg);
62
+ color: var(--chat-assistant-fg);
63
+ border-bottom-left-radius: var(--chat-assistant-tail-radius);
64
+ }
65
+
66
+ /* Error message */
67
+ chat-shell [data-role="error"] {
68
+ align-self: center;
69
+ max-width: none;
70
+ display: flex;
71
+ align-items: center;
72
+ gap: var(--chat-error-gap);
73
+ color: var(--chat-error-fg);
74
+ font-size: var(--chat-error-font);
75
+ }
76
+
77
+ /* Message actions (copy, regenerate) */
78
+ chat-shell [data-role] [data-actions] {
79
+ display: flex;
80
+ gap: var(--chat-actions-gap);
81
+ opacity: 0;
82
+ transition: opacity var(--chat-actions-duration) var(--chat-actions-easing);
83
+ }
84
+
85
+ chat-shell [data-role]:hover [data-actions] {
86
+ opacity: 1;
87
+ }
@@ -0,0 +1,30 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ chat-shell — Streaming states (cursor, thinking)
3
+ ═══════════════════════════════════════════════════════════════ */
4
+
5
+ /* Blinking cursor during streaming */
6
+ chat-shell [data-cursor] {
7
+ display: inline-block;
8
+ width: var(--chat-cursor-width);
9
+ height: 1em;
10
+ background: var(--chat-cursor-color);
11
+ margin-inline-start: 1px;
12
+ vertical-align: text-bottom;
13
+ animation: chat-shell-blink var(--chat-cursor-speed) step-end infinite;
14
+ }
15
+
16
+ @keyframes chat-shell-blink {
17
+ 50% { opacity: 0; }
18
+ }
19
+
20
+ @media (prefers-reduced-motion: reduce) {
21
+ chat-shell [data-cursor] {
22
+ animation: none;
23
+ opacity: 0.7;
24
+ }
25
+ }
26
+
27
+ /* Thinking indicator */
28
+ chat-shell [data-role="assistant"][data-thinking] [data-bubble] {
29
+ color: var(--chat-thinking-fg);
30
+ }