@hidemikimura/chit-ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +206 -9
- package/dist/chit-ui.iife.min.js +287 -60
- package/dist/chit-ui.iife.min.js.map +1 -1
- package/dist/chit-ui.min.js +287 -60
- package/dist/chit-ui.min.js.map +1 -1
- package/dist/types/bundle.d.ts +1 -0
- package/dist/types/chit-ui.d.ts +25 -0
- package/dist/types/events.d.ts +2 -0
- package/dist/types/global.d.ts +2 -0
- package/dist/types/i18n/labels.d.ts +8 -0
- package/dist/types/render/composer.d.ts +0 -1
- package/dist/types/theme/default-theme.d.ts +22 -1
- package/dist/types/themes/green.d.ts +16 -0
- package/dist/types/themes/index.d.ts +1 -0
- package/dist/types/types.d.ts +65 -4
- package/package.json +6 -1
- package/src/bundle.js +1 -0
- package/src/chit-ui.js +37 -0
- package/src/events.js +2 -0
- package/src/global.d.ts +2 -0
- package/src/i18n/labels.js +6 -0
- package/src/render/composer.js +58 -0
- package/src/render/message.js +13 -4
- package/src/render/panel.js +56 -10
- package/src/styles/composer.css.js +39 -0
- package/src/styles/host.css.js +2 -0
- package/src/styles/message.css.js +117 -3
- package/src/styles/panel.css.js +21 -2
- package/src/theme/default-theme.js +21 -2
- package/src/theme/theme-to-css.js +5 -0
- package/src/themes/green.js +47 -0
- package/src/themes/index.js +15 -0
- package/src/types.js +20 -6
package/dist/types/bundle.d.ts
CHANGED
package/dist/types/chit-ui.d.ts
CHANGED
|
@@ -213,6 +213,18 @@ export class ChitUI extends LitElement {
|
|
|
213
213
|
handleKeydown(event: KeyboardEvent): void;
|
|
214
214
|
/** @param {boolean} started */
|
|
215
215
|
handleComposition(started: boolean): void;
|
|
216
|
+
/**
|
|
217
|
+
* Report files the reader picked with the attach button.
|
|
218
|
+
*
|
|
219
|
+
* Nothing is uploaded, previewed or added to the conversation here: the
|
|
220
|
+
* widget does not own `messages` and has nowhere to send bytes.
|
|
221
|
+
*
|
|
222
|
+
* @param {File[]} files
|
|
223
|
+
* @returns {void}
|
|
224
|
+
*/
|
|
225
|
+
handleAttach(files: File[]): void;
|
|
226
|
+
/** Open the file picker from code, as the attach button does. */
|
|
227
|
+
openAttach(): void;
|
|
216
228
|
/**
|
|
217
229
|
* Render a consumer-supplied typing bubble.
|
|
218
230
|
*
|
|
@@ -234,6 +246,18 @@ export class ChitUI extends LitElement {
|
|
|
234
246
|
toggleFromUser(): Promise<boolean>;
|
|
235
247
|
/** Called by the close button and by Esc. */
|
|
236
248
|
closeFromUser(): Promise<boolean>;
|
|
249
|
+
/**
|
|
250
|
+
* Ask to start the conversation over.
|
|
251
|
+
*
|
|
252
|
+
* The widget only announces it: `messages` belongs to the consumer, so
|
|
253
|
+
* clearing it, replaying a scenario or asking for confirmation first are all
|
|
254
|
+
* theirs to do. Cancelling the event is not offered for the same reason —
|
|
255
|
+
* there is nothing here to cancel.
|
|
256
|
+
*
|
|
257
|
+
* @param {Trigger} [trigger='api']
|
|
258
|
+
* @returns {void}
|
|
259
|
+
*/
|
|
260
|
+
home(trigger?: Trigger): void;
|
|
237
261
|
/** @override */
|
|
238
262
|
override willUpdate(): void;
|
|
239
263
|
/** @override */
|
|
@@ -249,3 +273,4 @@ import type { Labels } from './i18n/labels.js';
|
|
|
249
273
|
import type { ChatState } from './types.js';
|
|
250
274
|
import type { Device } from './types.js';
|
|
251
275
|
import type { ResolvedTheme } from './types.js';
|
|
276
|
+
import type { Trigger } from './types.js';
|
package/dist/types/events.d.ts
CHANGED
package/dist/types/global.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface ChitUIEventMap {
|
|
|
15
15
|
'chat-message-click': CustomEvent<{ message: Message; target: Element; originalEvent: MouseEvent }>;
|
|
16
16
|
'chat-scroll-top': CustomEvent<Record<string, never>>;
|
|
17
17
|
'chat-breakpoint-change': CustomEvent<{ device: Device }>;
|
|
18
|
+
'chat-home': CustomEvent<{ trigger: Trigger }>;
|
|
19
|
+
'chat-attach': CustomEvent<{ files: File[] }>;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
declare global {
|
|
@@ -33,10 +33,18 @@ export type Labels = {
|
|
|
33
33
|
* Close button.
|
|
34
34
|
*/
|
|
35
35
|
close: string;
|
|
36
|
+
/**
|
|
37
|
+
* Home button: back to the start of the conversation.
|
|
38
|
+
*/
|
|
39
|
+
home: string;
|
|
36
40
|
/**
|
|
37
41
|
* Send button.
|
|
38
42
|
*/
|
|
39
43
|
send: string;
|
|
44
|
+
/**
|
|
45
|
+
* Attach button beside the composer.
|
|
46
|
+
*/
|
|
47
|
+
attach: string;
|
|
40
48
|
/**
|
|
41
49
|
* Composer textarea.
|
|
42
50
|
*/
|
|
@@ -20,9 +20,10 @@ export namespace defaultTheme {
|
|
|
20
20
|
let radius: number;
|
|
21
21
|
let launcher: "hidden";
|
|
22
22
|
namespace header {
|
|
23
|
+
let visible: boolean;
|
|
23
24
|
let title: null;
|
|
24
25
|
let logo: null;
|
|
25
|
-
let
|
|
26
|
+
let home: boolean;
|
|
26
27
|
}
|
|
27
28
|
namespace background {
|
|
28
29
|
let image: null;
|
|
@@ -58,6 +59,23 @@ export namespace defaultTheme {
|
|
|
58
59
|
import inputPlaceholder = PALETTE.muted;
|
|
59
60
|
export { inputPlaceholder };
|
|
60
61
|
}
|
|
62
|
+
namespace bubble {
|
|
63
|
+
let radius_1: number;
|
|
64
|
+
export { radius_1 as radius };
|
|
65
|
+
export let tail: "none";
|
|
66
|
+
}
|
|
67
|
+
namespace speaker {
|
|
68
|
+
namespace assistant {
|
|
69
|
+
let name: null;
|
|
70
|
+
let avatar: null;
|
|
71
|
+
}
|
|
72
|
+
namespace user {
|
|
73
|
+
let name_1: null;
|
|
74
|
+
export { name_1 as name };
|
|
75
|
+
let avatar_1: null;
|
|
76
|
+
export { avatar_1 as avatar };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
61
79
|
namespace animation {
|
|
62
80
|
let enter: "scale";
|
|
63
81
|
let exit: "fade";
|
|
@@ -67,6 +85,9 @@ export namespace defaultTheme {
|
|
|
67
85
|
namespace input {
|
|
68
86
|
let maxRows: number;
|
|
69
87
|
let placeholder: null;
|
|
88
|
+
let attach: boolean;
|
|
89
|
+
let accept: string;
|
|
90
|
+
let multiple: boolean;
|
|
70
91
|
}
|
|
71
92
|
}
|
|
72
93
|
namespace hidden {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** @import { Theme } from '../types.js' */
|
|
2
|
+
/**
|
|
3
|
+
* A green messenger look: a pale blue-grey conversation background, white
|
|
4
|
+
* bubbles for them, bright green for you, tails on the top corner.
|
|
5
|
+
*
|
|
6
|
+
* This is Chit UI's own palette in that familiar shape. It carries no other
|
|
7
|
+
* product's logo, wordmark or artwork, and the greens are Chit UI's own: the
|
|
8
|
+
* deep green is dark enough that white text and the send icon clear WCAG AA
|
|
9
|
+
* on it, which the bright green of the bubbles could not do.
|
|
10
|
+
* `test/theme.test.js` re-checks every pair here, so a later tweak cannot
|
|
11
|
+
* quietly drop below AA.
|
|
12
|
+
*
|
|
13
|
+
* @type {Theme}
|
|
14
|
+
*/
|
|
15
|
+
export const greenTheme: Theme;
|
|
16
|
+
import type { Theme } from '../types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { greenTheme } from "./green.js";
|
package/dist/types/types.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ export type Effect = "fade" | "scale" | "slide" | "none";
|
|
|
33
33
|
* every other file can `@import` from one place and so `tsc` can emit .d.ts.
|
|
34
34
|
*/
|
|
35
35
|
export type Position = "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
36
|
+
/**
|
|
37
|
+
* Public types for Chit UI. This module has no runtime value; it exists so that
|
|
38
|
+
* every other file can `@import` from one place and so `tsc` can emit .d.ts.
|
|
39
|
+
*/
|
|
40
|
+
export type BubbleTail = "none" | "top" | "bottom";
|
|
36
41
|
export type MessageBase = {
|
|
37
42
|
/**
|
|
38
43
|
* Unique within the array; used as the diffing key.
|
|
@@ -40,8 +45,14 @@ export type MessageBase = {
|
|
|
40
45
|
id: string;
|
|
41
46
|
role: Role;
|
|
42
47
|
time?: string | Date | undefined;
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Overrides the theme's speaker name; null hides it.
|
|
50
|
+
*/
|
|
51
|
+
name?: string | null | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Overrides the theme's speaker image; null hides it.
|
|
54
|
+
*/
|
|
55
|
+
avatar?: string | null | undefined;
|
|
45
56
|
status?: MessageStatus | undefined;
|
|
46
57
|
streaming?: boolean | undefined;
|
|
47
58
|
/**
|
|
@@ -108,6 +119,20 @@ export type ClosedTheme = {
|
|
|
108
119
|
idle?: "none" | "pulse" | "bounce";
|
|
109
120
|
}) | undefined;
|
|
110
121
|
};
|
|
122
|
+
/**
|
|
123
|
+
* Who is talking, as the theme describes them. A message that carries its own
|
|
124
|
+
* `name` or `avatar` wins; `null` on the message hides what the theme set.
|
|
125
|
+
*/
|
|
126
|
+
export type SpeakerTheme = {
|
|
127
|
+
/**
|
|
128
|
+
* Shown above the bubble.
|
|
129
|
+
*/
|
|
130
|
+
name?: string | null | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Image URL for the speaker's icon.
|
|
133
|
+
*/
|
|
134
|
+
avatar?: string | null | undefined;
|
|
135
|
+
};
|
|
111
136
|
export type OpenColors = {
|
|
112
137
|
background?: string | undefined;
|
|
113
138
|
text?: string | undefined;
|
|
@@ -141,21 +166,45 @@ export type OpenTheme = {
|
|
|
141
166
|
* Keep the launcher visible while open.
|
|
142
167
|
*/
|
|
143
168
|
launcher?: "hidden" | "visible" | undefined;
|
|
169
|
+
/**
|
|
170
|
+
* The title bar: whether it is there at all, its text, its image, and whether it offers a way back to the start.
|
|
171
|
+
*/
|
|
144
172
|
header?: {
|
|
173
|
+
visible?: boolean;
|
|
145
174
|
title?: string | null;
|
|
146
175
|
logo?: string | null;
|
|
147
|
-
|
|
176
|
+
home?: boolean;
|
|
148
177
|
} | undefined;
|
|
149
178
|
background?: {
|
|
150
179
|
image?: string | null;
|
|
151
180
|
} | undefined;
|
|
152
181
|
colors?: OpenColors | undefined;
|
|
182
|
+
/**
|
|
183
|
+
* Bubble corner radius, and where the tail points from.
|
|
184
|
+
*/
|
|
185
|
+
bubble?: {
|
|
186
|
+
radius?: number;
|
|
187
|
+
tail?: BubbleTail;
|
|
188
|
+
} | undefined;
|
|
189
|
+
/**
|
|
190
|
+
* Default name and icon per side.
|
|
191
|
+
*/
|
|
192
|
+
speaker?: {
|
|
193
|
+
assistant?: SpeakerTheme;
|
|
194
|
+
user?: SpeakerTheme;
|
|
195
|
+
} | undefined;
|
|
153
196
|
animation?: (Animation & {
|
|
154
197
|
scroll?: "smooth" | "instant";
|
|
155
198
|
}) | undefined;
|
|
199
|
+
/**
|
|
200
|
+
* Composer: rows, placeholder, and the attach button.
|
|
201
|
+
*/
|
|
156
202
|
input?: {
|
|
157
203
|
maxRows?: number;
|
|
158
204
|
placeholder?: string | null;
|
|
205
|
+
attach?: boolean;
|
|
206
|
+
accept?: string;
|
|
207
|
+
multiple?: boolean;
|
|
159
208
|
} | undefined;
|
|
160
209
|
};
|
|
161
210
|
export type Theme = {
|
|
@@ -223,14 +272,23 @@ export type ResolvedOpen = {
|
|
|
223
272
|
radius: number;
|
|
224
273
|
launcher: "hidden" | "visible";
|
|
225
274
|
header: {
|
|
275
|
+
visible: boolean;
|
|
226
276
|
title: string | null;
|
|
227
277
|
logo: string | null;
|
|
228
|
-
|
|
278
|
+
home: boolean;
|
|
229
279
|
};
|
|
230
280
|
background: {
|
|
231
281
|
image: string | null;
|
|
232
282
|
};
|
|
233
283
|
colors: Required<OpenColors>;
|
|
284
|
+
bubble: {
|
|
285
|
+
radius: number;
|
|
286
|
+
tail: BubbleTail;
|
|
287
|
+
};
|
|
288
|
+
speaker: {
|
|
289
|
+
assistant: Required<SpeakerTheme>;
|
|
290
|
+
user: Required<SpeakerTheme>;
|
|
291
|
+
};
|
|
234
292
|
animation: {
|
|
235
293
|
enter: Effect;
|
|
236
294
|
exit: Effect;
|
|
@@ -240,6 +298,9 @@ export type ResolvedOpen = {
|
|
|
240
298
|
input: {
|
|
241
299
|
maxRows: number;
|
|
242
300
|
placeholder: string | null;
|
|
301
|
+
attach: boolean;
|
|
302
|
+
accept: string;
|
|
303
|
+
multiple: boolean;
|
|
243
304
|
};
|
|
244
305
|
};
|
|
245
306
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hidemikimura/chit-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lit-based chat widget UI: launcher, panel, themable, any HTML or Lit component as a message",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hidemi Kimura",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./dist/types/element.d.ts",
|
|
17
17
|
"default": "./src/element.js"
|
|
18
18
|
},
|
|
19
|
+
"./themes.js": {
|
|
20
|
+
"types": "./dist/types/themes/index.d.ts",
|
|
21
|
+
"default": "./src/themes/index.js"
|
|
22
|
+
},
|
|
19
23
|
"./dist/*": "./dist/*",
|
|
20
24
|
"./package.json": "./package.json"
|
|
21
25
|
},
|
|
@@ -23,6 +27,7 @@
|
|
|
23
27
|
"src",
|
|
24
28
|
"dist",
|
|
25
29
|
"README.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
26
31
|
"LICENSE"
|
|
27
32
|
],
|
|
28
33
|
"sideEffects": [
|
package/src/bundle.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// against, so consumers of the bundle define their message components with the
|
|
4
4
|
// very same Lit instance.
|
|
5
5
|
export * from './index.js';
|
|
6
|
+
export * from './themes/index.js';
|
|
6
7
|
export { LitElement, html, css, svg, nothing, render } from 'lit';
|
|
7
8
|
export { repeat } from 'lit/directives/repeat.js';
|
|
8
9
|
export { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
package/src/chit-ui.js
CHANGED
|
@@ -387,6 +387,27 @@ export class ChitUI extends LitElement {
|
|
|
387
387
|
else this.#composer.onCompositionEnd();
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Report files the reader picked with the attach button.
|
|
392
|
+
*
|
|
393
|
+
* Nothing is uploaded, previewed or added to the conversation here: the
|
|
394
|
+
* widget does not own `messages` and has nowhere to send bytes.
|
|
395
|
+
*
|
|
396
|
+
* @param {File[]} files
|
|
397
|
+
* @returns {void}
|
|
398
|
+
*/
|
|
399
|
+
handleAttach(files) {
|
|
400
|
+
emit(this, Events.ATTACH, { files });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Open the file picker from code, as the attach button does. */
|
|
404
|
+
openAttach() {
|
|
405
|
+
const field = /** @type {HTMLInputElement | null} */ (
|
|
406
|
+
this.renderRoot.querySelector('[part~="attach-input"]')
|
|
407
|
+
);
|
|
408
|
+
field?.click();
|
|
409
|
+
}
|
|
410
|
+
|
|
390
411
|
// --- internal ---------------------------------------------------------
|
|
391
412
|
|
|
392
413
|
/**
|
|
@@ -442,6 +463,21 @@ export class ChitUI extends LitElement {
|
|
|
442
463
|
return this.#go('closed', 'user');
|
|
443
464
|
}
|
|
444
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Ask to start the conversation over.
|
|
468
|
+
*
|
|
469
|
+
* The widget only announces it: `messages` belongs to the consumer, so
|
|
470
|
+
* clearing it, replaying a scenario or asking for confirmation first are all
|
|
471
|
+
* theirs to do. Cancelling the event is not offered for the same reason —
|
|
472
|
+
* there is nothing here to cancel.
|
|
473
|
+
*
|
|
474
|
+
* @param {Trigger} [trigger='api']
|
|
475
|
+
* @returns {void}
|
|
476
|
+
*/
|
|
477
|
+
home(trigger = 'api') {
|
|
478
|
+
emit(this, Events.HOME, { trigger });
|
|
479
|
+
}
|
|
480
|
+
|
|
445
481
|
/**
|
|
446
482
|
* @param {ChatState} to
|
|
447
483
|
* @param {Trigger} trigger
|
|
@@ -493,6 +529,7 @@ export class ChitUI extends LitElement {
|
|
|
493
529
|
this.dataset.launcherPosition = theme.closed.position;
|
|
494
530
|
this.dataset.panelPosition = theme.open.position;
|
|
495
531
|
this.dataset.idle = theme.closed.animation.idle;
|
|
532
|
+
this.dataset.bubbleTail = theme.open.bubble.tail;
|
|
496
533
|
}
|
|
497
534
|
|
|
498
535
|
/** @override */
|
package/src/events.js
CHANGED
package/src/global.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface ChitUIEventMap {
|
|
|
15
15
|
'chat-message-click': CustomEvent<{ message: Message; target: Element; originalEvent: MouseEvent }>;
|
|
16
16
|
'chat-scroll-top': CustomEvent<Record<string, never>>;
|
|
17
17
|
'chat-breakpoint-change': CustomEvent<{ device: Device }>;
|
|
18
|
+
'chat-home': CustomEvent<{ trigger: Trigger }>;
|
|
19
|
+
'chat-attach': CustomEvent<{ files: File[] }>;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
declare global {
|
package/src/i18n/labels.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* @property {string} panel Accessible name of the dialog.
|
|
7
7
|
* @property {string} conversation Accessible name of the message log.
|
|
8
8
|
* @property {string} close Close button.
|
|
9
|
+
* @property {string} home Home button: back to the start of the conversation.
|
|
9
10
|
* @property {string} send Send button.
|
|
11
|
+
* @property {string} attach Attach button beside the composer.
|
|
10
12
|
* @property {string} input Composer textarea.
|
|
11
13
|
* @property {string} typing Announced while the other side is typing.
|
|
12
14
|
* @property {string} toLatest "Jump to newest" button.
|
|
@@ -22,7 +24,9 @@ const BUILT_IN = {
|
|
|
22
24
|
panel: 'チャット',
|
|
23
25
|
conversation: '会話',
|
|
24
26
|
close: 'チャットを閉じる',
|
|
27
|
+
home: '最初に戻る',
|
|
25
28
|
send: '送信',
|
|
29
|
+
attach: '画像や動画を添付',
|
|
26
30
|
input: 'メッセージを入力',
|
|
27
31
|
typing: '入力中',
|
|
28
32
|
toLatest: '最新へ',
|
|
@@ -35,7 +39,9 @@ const BUILT_IN = {
|
|
|
35
39
|
panel: 'Chat',
|
|
36
40
|
conversation: 'Conversation',
|
|
37
41
|
close: 'Close chat',
|
|
42
|
+
home: 'Back to the start',
|
|
38
43
|
send: 'Send',
|
|
44
|
+
attach: 'Attach an image or video',
|
|
39
45
|
input: 'Type a message',
|
|
40
46
|
typing: 'Typing',
|
|
41
47
|
toLatest: 'Jump to latest',
|
package/src/render/composer.js
CHANGED
|
@@ -4,6 +4,62 @@ import { live } from 'lit/directives/live.js';
|
|
|
4
4
|
|
|
5
5
|
/** @import { ChitUI } from '../chit-ui.js' */
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The attach button and the file input it opens.
|
|
9
|
+
*
|
|
10
|
+
* The widget owns the picking and nothing else: it hands the chosen files to
|
|
11
|
+
* the consumer through `chat-attach` and forgets them. Uploading them, showing
|
|
12
|
+
* a preview and adding a message are all things only the consumer can do, and
|
|
13
|
+
* a half-done version here would be in the way of the real one.
|
|
14
|
+
*
|
|
15
|
+
* The input is reset after every pick so that choosing the same file twice in
|
|
16
|
+
* a row still reports the second time.
|
|
17
|
+
*
|
|
18
|
+
* @param {ChitUI} host
|
|
19
|
+
* @param {boolean} disabled
|
|
20
|
+
* @returns {import('lit').TemplateResult}
|
|
21
|
+
*/
|
|
22
|
+
function renderAttach(host, disabled) {
|
|
23
|
+
const labels = host.currentLabels;
|
|
24
|
+
const input = host.currentTheme.open.input;
|
|
25
|
+
|
|
26
|
+
return html`
|
|
27
|
+
<button
|
|
28
|
+
part="attach-button"
|
|
29
|
+
type="button"
|
|
30
|
+
aria-label=${labels.attach}
|
|
31
|
+
title=${labels.attach}
|
|
32
|
+
?disabled=${disabled}
|
|
33
|
+
@click=${() => host.openAttach()}
|
|
34
|
+
>
|
|
35
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
36
|
+
<path
|
|
37
|
+
fill="none"
|
|
38
|
+
stroke="currentColor"
|
|
39
|
+
stroke-width="2"
|
|
40
|
+
stroke-linecap="round"
|
|
41
|
+
stroke-linejoin="round"
|
|
42
|
+
d="M20 11.5 12.2 19.3a4.6 4.6 0 0 1-6.5-6.5l7.8-7.8a3 3 0 0 1 4.3 4.3l-7.8 7.8a1.5 1.5 0 0 1-2.1-2.1l7.1-7.1"
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
</button>
|
|
46
|
+
<input
|
|
47
|
+
part="attach-input"
|
|
48
|
+
type="file"
|
|
49
|
+
tabindex="-1"
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
accept=${input.accept}
|
|
52
|
+
?multiple=${input.multiple}
|
|
53
|
+
@change=${(/** @type {Event} */ event) => {
|
|
54
|
+
const field = /** @type {HTMLInputElement} */ (event.target);
|
|
55
|
+
const files = Array.from(field.files ?? []);
|
|
56
|
+
field.value = '';
|
|
57
|
+
if (files.length) host.handleAttach(files);
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
7
63
|
/**
|
|
8
64
|
* The composer: a textarea that grows with its content, and a send button.
|
|
9
65
|
*
|
|
@@ -32,6 +88,8 @@ export function renderComposer(host) {
|
|
|
32
88
|
}}
|
|
33
89
|
>
|
|
34
90
|
<slot name="composer">
|
|
91
|
+
${theme.input.attach ? renderAttach(host, disabled) : nothing}
|
|
92
|
+
|
|
35
93
|
<slot name="input-before"></slot>
|
|
36
94
|
|
|
37
95
|
<textarea
|
package/src/render/message.js
CHANGED
|
@@ -45,6 +45,13 @@ export function renderMessage(host, message) {
|
|
|
45
45
|
|
|
46
46
|
const meta = message.time || message.status;
|
|
47
47
|
|
|
48
|
+
// The theme says who is talking; a message overrides either half. `null` on
|
|
49
|
+
// the message hides what the theme set, which is how a run of replies shows
|
|
50
|
+
// the icon only on the first of them.
|
|
51
|
+
const speaker = host.currentTheme.open.speaker[message.role];
|
|
52
|
+
const avatar = message.avatar === undefined ? speaker.avatar : message.avatar;
|
|
53
|
+
const name = message.name === undefined ? speaker.name : message.name;
|
|
54
|
+
|
|
48
55
|
return html`
|
|
49
56
|
<article
|
|
50
57
|
part="message message-${message.role}"
|
|
@@ -53,11 +60,13 @@ export function renderMessage(host, message) {
|
|
|
53
60
|
?data-streaming=${!!message.streaming}
|
|
54
61
|
aria-busy=${message.streaming ? 'true' : 'false'}
|
|
55
62
|
>
|
|
56
|
-
${
|
|
57
|
-
? html`<img part="avatar" src=${
|
|
58
|
-
:
|
|
63
|
+
${avatar
|
|
64
|
+
? html`<img part="avatar" src=${avatar} alt="" loading="lazy" />`
|
|
65
|
+
: speaker.avatar
|
|
66
|
+
? html`<span part="avatar" data-placeholder aria-hidden="true"></span>`
|
|
67
|
+
: nothing}
|
|
59
68
|
<div class="body">
|
|
60
|
-
${
|
|
69
|
+
${name ? html`<span part="name">${name}</span>` : nothing}
|
|
61
70
|
<div part="bubble">
|
|
62
71
|
<div part="message-content" data-content=${contentField(message)}>${renderContent(host, message)}</div>
|
|
63
72
|
${message.streaming ? html`<span part="cursor" aria-hidden="true"></span>` : nothing}
|
package/src/render/panel.js
CHANGED
|
@@ -15,7 +15,7 @@ import { renderComposer } from './composer.js';
|
|
|
15
15
|
export function renderPanel(host) {
|
|
16
16
|
const open = host.currentTheme.open;
|
|
17
17
|
const labels = host.currentLabels;
|
|
18
|
-
const title = open.header
|
|
18
|
+
const title = open.header.title ?? labels.panel;
|
|
19
19
|
|
|
20
20
|
return html`
|
|
21
21
|
<section
|
|
@@ -26,16 +26,67 @@ export function renderPanel(host) {
|
|
|
26
26
|
tabindex="-1"
|
|
27
27
|
@keydown=${(/** @type {KeyboardEvent} */ event) => onKeydown(host, event)}
|
|
28
28
|
>
|
|
29
|
+
${open.header.visible ? renderHeader(host, title) : nothing}
|
|
30
|
+
|
|
31
|
+
${renderMessageList(host)} ${renderComposer(host)}
|
|
32
|
+
|
|
33
|
+
<slot name="footer"></slot>
|
|
34
|
+
</section>
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The title bar.
|
|
40
|
+
*
|
|
41
|
+
* It can be left out entirely (`open.header.visible = false`), which is what a
|
|
42
|
+
* widget embedded in a host that already draws its own title bar wants — LINE
|
|
43
|
+
* LIFF, for one — rather than two stacked titles. The dialog keeps its
|
|
44
|
+
* accessible name either way: that name is read, not seen.
|
|
45
|
+
*
|
|
46
|
+
* Note that hiding it takes the close button with it. Esc and the launcher
|
|
47
|
+
* still close the panel, and the host's own chrome usually supplies the rest.
|
|
48
|
+
*
|
|
49
|
+
* @param {ChitUI} host
|
|
50
|
+
* @param {string} title
|
|
51
|
+
* @returns {import('lit').TemplateResult}
|
|
52
|
+
*/
|
|
53
|
+
function renderHeader(host, title) {
|
|
54
|
+
const open = host.currentTheme.open;
|
|
55
|
+
const labels = host.currentLabels;
|
|
56
|
+
|
|
57
|
+
return html`
|
|
29
58
|
<header part="header">
|
|
30
59
|
<slot name="header">
|
|
31
60
|
<slot name="header-title">
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
61
|
+
<span part="header-title">
|
|
62
|
+
${open.header.logo
|
|
63
|
+
? html`<img part="header-logo" src=${open.header.logo} alt="" />`
|
|
64
|
+
: nothing}
|
|
65
|
+
<span part="header-heading">${title}</span>
|
|
66
|
+
</span>
|
|
36
67
|
</slot>
|
|
37
68
|
<span part="header-actions">
|
|
38
69
|
<slot name="header-actions"></slot>
|
|
70
|
+
${open.header.home
|
|
71
|
+
? html`<button
|
|
72
|
+
part="home-button"
|
|
73
|
+
type="button"
|
|
74
|
+
aria-label=${labels.home}
|
|
75
|
+
title=${labels.home}
|
|
76
|
+
@click=${() => host.home('user')}
|
|
77
|
+
>
|
|
78
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
79
|
+
<path
|
|
80
|
+
fill="none"
|
|
81
|
+
stroke="currentColor"
|
|
82
|
+
stroke-width="2"
|
|
83
|
+
stroke-linejoin="round"
|
|
84
|
+
stroke-linecap="round"
|
|
85
|
+
d="M4 11.2 12 4l8 7.2M6.5 9.8V19h11V9.8M10 19v-4.6h4V19"
|
|
86
|
+
/>
|
|
87
|
+
</svg>
|
|
88
|
+
</button>`
|
|
89
|
+
: nothing}
|
|
39
90
|
<button
|
|
40
91
|
part="close-button"
|
|
41
92
|
type="button"
|
|
@@ -55,11 +106,6 @@ export function renderPanel(host) {
|
|
|
55
106
|
</span>
|
|
56
107
|
</slot>
|
|
57
108
|
</header>
|
|
58
|
-
|
|
59
|
-
${renderMessageList(host)} ${renderComposer(host)}
|
|
60
|
-
|
|
61
|
-
<slot name="footer"></slot>
|
|
62
|
-
</section>
|
|
63
109
|
`;
|
|
64
110
|
}
|
|
65
111
|
|
|
@@ -55,6 +55,45 @@ export const composerStyles = css`
|
|
|
55
55
|
font-weight: 600;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/* Out of sight, but still clickable from the button beside it. */
|
|
59
|
+
[part~='attach-input'] {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[part~='attach-button'] {
|
|
64
|
+
display: grid;
|
|
65
|
+
flex: none;
|
|
66
|
+
place-items: center;
|
|
67
|
+
width: 2.4em;
|
|
68
|
+
height: 2.4em;
|
|
69
|
+
padding: 0;
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
background: transparent;
|
|
73
|
+
color: var(--chit-color-system-text);
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
transition: opacity 120ms ease;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
[part~='attach-button'] svg {
|
|
79
|
+
width: 1.3em;
|
|
80
|
+
height: 1.3em;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[part~='attach-button']:hover:not(:disabled) {
|
|
84
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[part~='attach-button']:focus-visible {
|
|
88
|
+
outline: 2px solid var(--chit-color-accent);
|
|
89
|
+
outline-offset: 2px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
[part~='attach-button']:disabled {
|
|
93
|
+
opacity: 0.4;
|
|
94
|
+
cursor: default;
|
|
95
|
+
}
|
|
96
|
+
|
|
58
97
|
[part~='send-button'] {
|
|
59
98
|
display: grid;
|
|
60
99
|
flex: none;
|