@hidemikimura/chit-ui 0.1.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +440 -0
  3. package/dist/chit-ui.iife.min.js +964 -0
  4. package/dist/chit-ui.iife.min.js.map +1 -0
  5. package/dist/chit-ui.min.js +964 -0
  6. package/dist/chit-ui.min.js.map +1 -0
  7. package/dist/types/bundle.d.ts +7 -0
  8. package/dist/types/chit-ui.d.ts +251 -0
  9. package/dist/types/controllers/breakpoint-controller.d.ts +29 -0
  10. package/dist/types/controllers/composer-controller.d.ts +54 -0
  11. package/dist/types/controllers/scroll-controller.d.ts +44 -0
  12. package/dist/types/controllers/state-controller.d.ts +61 -0
  13. package/dist/types/controllers/theme-controller.d.ts +47 -0
  14. package/dist/types/element.d.ts +1 -0
  15. package/dist/types/events.d.ts +28 -0
  16. package/dist/types/global.d.ts +25 -0
  17. package/dist/types/i18n/labels.d.ts +68 -0
  18. package/dist/types/index.d.ts +4 -0
  19. package/dist/types/render/composer.d.ts +15 -0
  20. package/dist/types/render/content.d.ts +75 -0
  21. package/dist/types/render/launcher.d.ts +18 -0
  22. package/dist/types/render/message-list.d.ts +17 -0
  23. package/dist/types/render/message.d.ts +14 -0
  24. package/dist/types/render/panel.d.ts +10 -0
  25. package/dist/types/styles/adopted-sheet.d.ts +20 -0
  26. package/dist/types/styles/composer.css.d.ts +1 -0
  27. package/dist/types/styles/content.css.d.ts +9 -0
  28. package/dist/types/styles/host.css.d.ts +9 -0
  29. package/dist/types/styles/launcher.css.d.ts +1 -0
  30. package/dist/types/styles/message.css.d.ts +1 -0
  31. package/dist/types/styles/panel.css.d.ts +1 -0
  32. package/dist/types/theme/default-theme.d.ts +97 -0
  33. package/dist/types/theme/merge-theme.d.ts +33 -0
  34. package/dist/types/theme/theme-to-css.d.ts +24 -0
  35. package/dist/types/types.d.ts +268 -0
  36. package/package.json +71 -0
  37. package/src/bundle.js +11 -0
  38. package/src/chit-ui.js +548 -0
  39. package/src/controllers/breakpoint-controller.js +82 -0
  40. package/src/controllers/composer-controller.js +215 -0
  41. package/src/controllers/scroll-controller.js +252 -0
  42. package/src/controllers/state-controller.js +316 -0
  43. package/src/controllers/theme-controller.js +75 -0
  44. package/src/element.js +4 -0
  45. package/src/events.js +33 -0
  46. package/src/global.d.ts +25 -0
  47. package/src/i18n/labels.js +72 -0
  48. package/src/index.js +9 -0
  49. package/src/render/composer.js +72 -0
  50. package/src/render/content.js +211 -0
  51. package/src/render/launcher.js +64 -0
  52. package/src/render/message-list.js +67 -0
  53. package/src/render/message.js +82 -0
  54. package/src/render/panel.js +77 -0
  55. package/src/styles/adopted-sheet.js +72 -0
  56. package/src/styles/composer.css.js +108 -0
  57. package/src/styles/content.css.js +119 -0
  58. package/src/styles/host.css.js +160 -0
  59. package/src/styles/launcher.css.js +139 -0
  60. package/src/styles/message.css.js +192 -0
  61. package/src/styles/panel.css.js +126 -0
  62. package/src/theme/default-theme.js +111 -0
  63. package/src/theme/merge-theme.js +100 -0
  64. package/src/theme/theme-to-css.js +94 -0
  65. package/src/types.js +143 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hidemi Kimura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,440 @@
1
+ # Chit UI
2
+
3
+ Web ページに載せるチャットウィジェットの **UI だけ** を提供する、Lit ベースの
4
+ JavaScript ライブラリです。通信も会話ロジックも、発言の中に置いたボタンの挙動も持ちません。
5
+ 持つのは、見た目・状態・入力・イベント通知の 4 つだけです。
6
+
7
+ - カスタム要素 1 つ(`<chit-ui>`)。Shadow DOM に閉じているので埋め込み先の CSS と干渉しません
8
+ - 閉じた状態(ランチャー)・開いた状態(パネル)・非表示の 3 状態
9
+ - 状態ごとのテーマ。閉じた状態は PC とスマホで別々に指定できます
10
+ - 発言の中身は HTML 文字列でも Lit テンプレートでも `LitElement` を継承したコンポーネントでも可
11
+ - 依存は Lit のみ。単一バンドルは gzip 約 19 KB
12
+
13
+ ## はじめに
14
+
15
+ ### `<script>` 1 本で使う
16
+
17
+ ```html
18
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@hidemikimura/chit-ui/dist/chit-ui.min.js"></script>
19
+
20
+ <chit-ui id="chat"></chit-ui>
21
+
22
+ <script type="module">
23
+ const chat = document.getElementById('chat');
24
+
25
+ chat.addEventListener('chat-submit', async (event) => {
26
+ const text = event.detail.text;
27
+
28
+ // 発言を積むのはあなたのコード。ライブラリは messages を書き換えません。
29
+ // text は自動でエスケープされ、改行もそのまま表示されます
30
+ chat.messages = [...chat.messages, { id: crypto.randomUUID(), role: 'user', text }];
31
+
32
+ chat.busy = true;
33
+ chat.typing = true;
34
+
35
+ const reply = await yourBackend(text); // 通信もあなたのコード
36
+
37
+ chat.typing = false;
38
+ chat.messages = [...chat.messages,
39
+ { id: crypto.randomUUID(), role: 'assistant', html: reply }];
40
+ chat.busy = false;
41
+ });
42
+ </script>
43
+ ```
44
+
45
+ ### npm から使う
46
+
47
+ ```sh
48
+ npm install @hidemikimura/chit-ui lit
49
+ ```
50
+
51
+ ```js
52
+ import '@hidemikimura/chit-ui'; // <chit-ui> を登録する
53
+ ```
54
+
55
+ タグ名を自分で決めたい場合は、登録しないエントリを使います。
56
+
57
+ ```js
58
+ import { ChitUI } from '@hidemikimura/chit-ui/element.js';
59
+ customElements.define('my-chat', ChitUI);
60
+ ```
61
+
62
+ 単一バンドル(`dist/chit-ui.min.js`)は Lit を同梱しています。ESM 版のほかに
63
+ `window.ChitUI` を作る IIFE 版(`dist/chit-ui.iife.min.js`)もあります。npm 版と
64
+ 単一バンドル版を同じページで混ぜると Lit が二重に読み込まれるので、どちらか一方にしてください。
65
+
66
+ ## 発言の中身
67
+
68
+ 1 つの発言には、次の 5 つのうち **ちょうど 1 つ** を指定します。
69
+
70
+ | 指定 | 型 | 向いている場面 |
71
+ | --- | --- | --- |
72
+ | `text` | 文字列 | 人が入力した文章。エスケープされ、改行が保たれます |
73
+ | `html` | 文字列 | サーバーから返ってきた HTML をそのまま出す |
74
+ | `template` | Lit の `TemplateResult` | 利用者側が Lit で書いていて、プロパティやイベントをその場で束縛したい |
75
+ | `component` + `props` | `LitElement` 継承クラス、またはタグ名 | テンプレートを書かずにコンポーネントを渡したい |
76
+ | `element` | `HTMLElement` | 要素の生成と寿命を自分で完全に管理したい |
77
+
78
+ ```js
79
+ import { html } from 'lit';
80
+ import { OrderCard } from './order-card.js'; // class OrderCard extends LitElement
81
+
82
+ chat.messages = [
83
+ { id: 'm1', role: 'user', text: '領収証を再発行したいです\n宛名も変えたいです' },
84
+ { id: 'm2', role: 'assistant', html: '<p>注文履歴から発行できます。</p>' },
85
+ { id: 'm3', role: 'assistant', template: html`<order-card .order=${order} @select=${onSelect}></order-card>` },
86
+ { id: 'm4', role: 'assistant', component: OrderCard, props: { order } },
87
+ { id: 'm5', role: 'assistant', element: myCardElement },
88
+ ];
89
+ ```
90
+
91
+ ### 発言オブジェクト
92
+
93
+ | フィールド | 必須 | 内容 |
94
+ | --- | --- | --- |
95
+ | `id` | ○ | 差分描画のキー。配列内で一意にしてください |
96
+ | `role` | ○ | `user`(右寄せ)/ `assistant`(左寄せ)/ `system`(中央の連絡行) |
97
+ | 中身 | ○ | `text` / `html` / `template` / `element` / `component` のいずれか 1 つ |
98
+ | `props` | | `component` に渡すプロパティ |
99
+ | `time` | | ISO 文字列または `Date`。`formatTime` で表示を差し替えられます |
100
+ | `name` / `avatar` | | 発言者名とアバター画像 URL |
101
+ | `status` | | `sending` / `sent` / `error` |
102
+ | `streaming` | | `true` の間はカーソルを出し、スクロールを追従させます |
103
+ | `meta` | | ライブラリは触りません。イベントでそのまま返ってきます |
104
+
105
+ ### 人が入力した文章は `text` で
106
+
107
+ `text` はテキストノードとして描画されるので、HTML として解釈されません。エスケープ漏れが
108
+ 起こりようがなく、`sanitize` も不要です。改行は `white-space: pre-wrap` で保たれるので、
109
+ Shift+Enter で入力された複数行がそのまま表示されます。
110
+
111
+ ```js
112
+ chat.addEventListener('chat-submit', (event) => {
113
+ chat.messages = [...chat.messages,
114
+ { id: crypto.randomUUID(), role: 'user', text: event.detail.text }];
115
+ });
116
+ ```
117
+
118
+ `chat-submit` の `text` は入力されたそのままで、前後の空白や改行も削りません。整形が必要なら
119
+ 利用者側で行ってください。
120
+
121
+ `html` 形式には `pre-wrap` を当てていません。マークアップ自身の改行やインデントが
122
+ 空行として見えてしまうためです。HTML の中で改行を見せたい場合は `<br>` を使うか、
123
+ その要素に `white-space: pre-wrap` を当ててください。
124
+
125
+ ### コンポーネントを渡すときの注意
126
+
127
+ `component` にクラスを渡す場合、そのクラスは `customElements.define()` 済みである必要が
128
+ あります。ライブラリが勝手にタグ名を付けて登録することはしません(その名前がページ全体で
129
+ 占有されてしまうため)。未登録のクラスを渡すと例外になります。
130
+
131
+ コンポーネントは自分の Shadow DOM を持つので、ライブラリの既定スタイルは中まで届きません。
132
+ 一方でテーマの CSS カスタムプロパティは継承されるので、`var(--chit-color-accent)` などを
133
+ 参照すればウィジェットと色を揃えられます。
134
+
135
+ ### `template` はひとつの「形」に値を差し込む
136
+
137
+ Lit がノードを再利用するのは、テンプレートの形(タグ構造)が同じで値だけが変わったときです。
138
+ 別のテンプレートリテラルは別のテンプレートとして扱われ、DOM が作り直されます。
139
+ ストリーミングのように同じ発言を何度も更新する場合は、関数に切り出してください。
140
+
141
+ ```js
142
+ const line = (text) => html`<p>${text}</p>`; // 形はひとつ
143
+
144
+ chat.messages = [{ id: 'm2', role: 'assistant', template: line(partial), streaming: true }];
145
+ ```
146
+
147
+ ### 崩れ防止
148
+
149
+ どんな HTML を入れてもウィジェット側のレイアウトが崩れないよう、発言の中身には次の制約が
150
+ 掛かります。
151
+
152
+ - 画像・動画・iframe・SVG・canvas は吹き出しの幅に収まります
153
+ - `table` と `pre` は吹き出しを広げず、内部で横スクロールします
154
+ - 長い URL や英単語は折り返されます
155
+ - 発言の直下に置かれた `position: fixed` / `absolute` は無効化されます
156
+ - `contain: layout paint` により、描画が吹き出しの外へ漏れません
157
+
158
+ 発言の中に独自のスタイルを当てたい場合は、`messageStyles` プロパティに CSS 文字列を渡すか、
159
+ `html` の中に `<style>` を含めてください。`messageStyles` は既定スタイルより後に適用されます。
160
+
161
+ ## セキュリティ
162
+
163
+ `html` で渡した文字列は **サニタイズせずにそのまま描画します**。利用者や第三者が書いた内容が
164
+ 混ざる場合は、`sanitize` に DOMPurify などを渡してください。
165
+
166
+ ```js
167
+ chat.sanitize = (html) => DOMPurify.sanitize(html);
168
+ ```
169
+
170
+ `sanitize` は `html` 文字列にのみ適用されます。`text` は構造的にエスケープされているので
171
+ 不要で、`template` / `element` / `component` はあなたが組み立てたものとして扱い、素通しします。
172
+
173
+ 人が入力した文章をそのまま表示するだけなら、`html` ではなく `text` を使えばこの問題自体が
174
+ 起きません。
175
+
176
+ ## 状態
177
+
178
+ ```js
179
+ chat.state; // 'closed' | 'open' | 'hidden'
180
+ chat.state = 'open'; // 代入でも遷移します
181
+ await chat.open(); // アニメーション完了で resolve する Promise
182
+ ```
183
+
184
+ | 状態 | 表示 | ユーザー操作 | コード |
185
+ | --- | --- | --- | --- |
186
+ | `closed` | ランチャー | クリック / Enter / Space → `open` | `open()`, `hide()` |
187
+ | `open` | パネル | 閉じるボタン / Esc → `closed` | `close()`, `hide()` |
188
+ | `hidden` | 何も描画しない | なし | `show()` → `closed`、`open()` → `open` |
189
+
190
+ `state` は代入した瞬間に変わりますが、DOM は退場アニメーションが終わるまで前の状態を
191
+ 表示し続けます。いま描画されている状態は `renderedState`(およびホストの
192
+ `data-rendered-state` 属性)で読めます。
193
+
194
+ 遷移は `chat-before-open` / `chat-before-close` を `preventDefault()` すると中止できます。
195
+
196
+ ## テーマ
197
+
198
+ ```js
199
+ chat.theme = {
200
+ breakpoint: 768, // これ未満の幅をスマホとみなす
201
+ closed: {
202
+ pc: { size: 64, image: '/icon.png', offset: { x: 24, y: 24 } },
203
+ mobile: { size: 56, offset: { x: 16, y: 16 } },
204
+ // size は px、または 'auto'(中身に決めさせる)
205
+ },
206
+ open: {
207
+ width: 380, height: 600,
208
+ colors: { accent: '#2563eb', userBubble: '#2563eb' },
209
+ header: { title: 'サポート' },
210
+ },
211
+ };
212
+ ```
213
+
214
+ - 部分指定で構いません。指定しなかった項目は既定テーマで埋まります
215
+ - `null` を渡すと既定値を消せます(`image: null` で既定アイコンを外す、など)
216
+ - `closed` を `pc` / `mobile` に分けなければ両方に同じ設定が使われます。`pc` だけ指定した
217
+ 場合、スマホは `pc` を引き継ぎつつサイズとオフセットだけ電話向けの既定値になります
218
+ - スマホ幅では `open` は常に全画面です(`width` / `height` は無視されます)
219
+ - 実行中に差し替えると即座に反映されます(ダークモードの切り替えなど)
220
+
221
+ ### 閉じた状態を自分で描く
222
+
223
+ `closed.component` に `LitElement` を継承したコンポーネント(またはタグ名)を渡すと、
224
+ 閉じた状態の見た目をまるごと差し替えられます。**大きさはコンポーネント側が決めます。**
225
+
226
+ ```js
227
+ import { PillLauncher } from './pill-launcher.js'; // class PillLauncher extends LitElement
228
+
229
+ chat.theme = {
230
+ closed: {
231
+ component: PillLauncher,
232
+ props: { unread: 3, online: true },
233
+ label: 'サポートに相談', // ボタンの読み上げ名に使われます
234
+ },
235
+ };
236
+ ```
237
+
238
+ このとき外側のボタンは、幅・高さ・背景・影・角丸・余白をすべて手放します。ひとつの箱を
239
+ 2 者がスタイルすると、はみ出しや切り取りが起きるためです。ボタンであること自体は残るので、
240
+ キーボード操作とスクリーンリーダーからの見え方は変わりません。読み上げ名は `closed.label`、
241
+ なければ既定の文言になります。
242
+
243
+ `props` を差し替えると、同じコンポーネントインスタンスに書き込まれます(作り直されません)。
244
+ 未読バッジのように状態を持つランチャーは、この経路で更新してください。
245
+
246
+ ```js
247
+ chat.theme = { ...chat.theme, closed: { ...chat.theme.closed, props: { unread: 4 } } };
248
+ ```
249
+
250
+ `launcher` スロットとの使い分けは、ウィジェットの丸いボタンを残すかどうかです。スロットは
251
+ 既定の円の**中に**あなたの中身を置き、`component` は円ごと置き換えます。スロットを使いつつ
252
+ 大きさだけ中身に決めさせたい場合は `closed.size` に `'auto'` を指定します。
253
+
254
+ ### スクロールの追従
255
+
256
+ 新しい発言が届いたとき、最下部にいる読み手の画面はアニメーションで追従します。
257
+ `open.animation.scroll` に `'instant'` を指定すると即座に移動します。
258
+
259
+ ```js
260
+ chat.theme = { open: { animation: { scroll: 'instant' } } };
261
+ ```
262
+
263
+ ひとつの発言が伸びていく場合(ストリーミング、画像の読み込み)は設定によらず即時です。
264
+ 数十ミリ秒ごとにアニメーションをやり直すと表示が文字に追いつかないうえ、アニメーション中の
265
+ スクロール位置が「最下部にいない」と読めてしまい、追従そのものが止まるためです。
266
+ `prefers-reduced-motion` が有効な環境でも即時になります。
267
+
268
+ テーマの値は CSS カスタムプロパティになるので、ページ側の CSS からも上書きできます。
269
+ こちらが優先されます。
270
+
271
+ ```css
272
+ chit-ui {
273
+ --chit-color-accent: #d81b60;
274
+ --chit-launcher-size: 72px;
275
+ }
276
+ ```
277
+
278
+ 公開しているカスタムプロパティは次の 32 個です。
279
+
280
+ 色: `--chit-color-bg` `--chit-color-text` `--chit-color-accent` `--chit-color-border`
281
+ `--chit-color-user-bg` `--chit-color-user-text` `--chit-color-assistant-bg`
282
+ `--chit-color-assistant-text` `--chit-color-system-text` `--chit-color-header-bg`
283
+ `--chit-color-header-text` `--chit-color-input-bg` `--chit-color-input-text`
284
+ `--chit-color-input-placeholder`
285
+
286
+ ランチャー: `--chit-launcher-size` `--chit-launcher-radius` `--chit-launcher-offset-x`
287
+ `--chit-launcher-offset-y` `--chit-launcher-bg` `--chit-launcher-text`
288
+ `--chit-launcher-shadow` `--chit-launcher-image`
289
+
290
+ パネル: `--chit-panel-width` `--chit-panel-height` `--chit-panel-radius`
291
+ `--chit-panel-offset-x` `--chit-panel-offset-y` `--chit-panel-shadow`
292
+ `--chit-panel-bg-image`
293
+
294
+ 全体: `--chit-font-family` `--chit-font-size` `--chit-z-index`
295
+
296
+ アニメーションの長さは CSS 変数にしていません。遷移の完了を JavaScript 側が知る必要があり、
297
+ テーマの `animation.duration` が唯一の設定箇所になるためです。
298
+
299
+ `--_chit-` で始まる変数は内部用なので、変更しないでください。
300
+
301
+ ## プロパティ
302
+
303
+ | プロパティ | 型 | 既定 | 内容 |
304
+ | --- | --- | --- | --- |
305
+ | `state` | `'closed' \| 'open' \| 'hidden'` | `'closed'` | 現在の状態 |
306
+ | `theme` | `Theme` | `{}` | テーマ(部分指定可) |
307
+ | `messages` | `Message[]` | `[]` | 描画する発言 |
308
+ | `typing` | `boolean \| { html }` | `false` | 相手が入力中の表示 |
309
+ | `busy` | `boolean` | `false` | 送信中。入力をロックします |
310
+ | `inputDisabled` / `inputHidden` | `boolean` | `false` | 入力欄の無効化 / 非表示 |
311
+ | `value` | `string` | `''` | 入力欄の内容 |
312
+ | `placeholder` | `string` | テーマの値 | 入力欄のプレースホルダー |
313
+ | `sendOnEnter` | `boolean` | `true` | Enter で送信するか |
314
+ | `maxLength` | `number` | なし | 入力文字数の上限(コードポイント単位) |
315
+ | `focusOnOpen` | `'auto' \| 'always' \| 'never'` | `'auto'` | 開いたときの自動フォーカス。`auto` は PC のみ |
316
+ | `sanitize` | `(html) => string` | なし | `html` 発言のサニタイズ |
317
+ | `formatTime` | `(time: Date) => string` | `HH:mm` | 時刻表示 |
318
+ | `messageStyles` | `string` | `''` | 発言の中に適用する追加 CSS |
319
+ | `locale` | `string` | `<html lang>` | ラベルと時刻の言語 |
320
+ | `labels` | `Partial<Labels>` | なし | UI 文字列の上書き |
321
+
322
+ 読み取り専用: `renderedState`、`device`(`'pc' \| 'mobile'`)、`hasUnseen`、`canSend`、
323
+ `currentTheme`、`currentLabels`、`resolvedLocale`。
324
+
325
+ ## メソッド
326
+
327
+ | メソッド | 内容 |
328
+ | --- | --- |
329
+ | `open()` / `close()` / `hide()` / `show()` / `toggle()` | 状態遷移。アニメーション完了で resolve する Promise を返します(中止された場合は `false`) |
330
+ | `submit(text?)` | `chat-submit` を発火します。省略時は入力欄の内容 |
331
+ | `focusInput()` / `clearInput()` | 入力欄の操作 |
332
+ | `scrollToBottom({ smooth })` | 最新の発言までスクロール。`smooth` 省略時はテーマの設定に従う |
333
+ | `getMessageElement(id)` | その発言のコンテナ DOM(未描画なら `null`) |
334
+
335
+ ## イベント
336
+
337
+ すべて `chat-` プレフィックス付きの `CustomEvent` で、`bubbles` と `composed` が立っている
338
+ ので `document` でも拾えます。
339
+
340
+ | イベント | タイミング | `detail` | 中止 |
341
+ | --- | --- | --- | --- |
342
+ | `chat-submit` | 送信された | `{ text }` | ○ |
343
+ | `chat-before-open` / `chat-before-close` | 遷移の直前 | `{ from, trigger }` | ○ |
344
+ | `chat-open` / `chat-close` | 遷移アニメーション完了 | `{ from, trigger }` | |
345
+ | `chat-hide` / `chat-show` | 非表示になった / 戻った | `{ from, to }` | |
346
+ | `chat-state-change` | `state` が変わった直後 | `{ from, to, trigger }` | |
347
+ | `chat-input` | 入力欄が編集された | `{ value }` | |
348
+ | `chat-message-render` | 発言が描画・更新された | `{ message, element, instance }` | |
349
+ | `chat-message-click` | 発言の中がクリックされた | `{ message, target, originalEvent }` | ○ |
350
+ | `chat-scroll-top` | 一覧が最上部に達した | `{}` | |
351
+ | `chat-breakpoint-change` | PC / スマホの判定が変わった | `{ device }` | |
352
+
353
+ `trigger` は `'user'`(クリックや Esc)か `'api'`(メソッドやプロパティ代入)です。
354
+
355
+ ### 発言の中のボタンを動かす
356
+
357
+ 発言 HTML の中のボタンは、`chat-message-click` で一括して受けるのが簡単です。
358
+ `target` はコンポーネントの Shadow DOM 内でクリックされた要素まで辿って返ります。
359
+
360
+ ```js
361
+ chat.addEventListener('chat-message-click', (event) => {
362
+ const action = event.detail.target.closest('[data-action]')?.dataset.action;
363
+ if (action === 'open-orders') showOrders(event.detail.message.meta);
364
+ });
365
+ ```
366
+
367
+ 描画のたびに個別のリスナーを付けたい場合は `chat-message-render` を使います。
368
+
369
+ ## スロット
370
+
371
+ | スロット | 差し替え対象 |
372
+ | --- | --- |
373
+ | `launcher` | 閉じた状態のアイコンの中身(未読バッジなどもここで) |
374
+ | `header` | パネルヘッダー全体。`header-title` / `header-actions` で部分差し替えも可 |
375
+ | `empty` | 発言が 1 件もないときの表示 |
376
+ | `input-before` / `input-after` | 入力欄の前後(添付ボタンなど) |
377
+ | `composer` | 入力欄全体。差し替えた場合は `submit(text)` を自分で呼びます |
378
+ | `footer` | 入力欄の下(免責事項など) |
379
+
380
+ ## CSS パーツ
381
+
382
+ `::part()` で外部からスタイルを当てられます。
383
+
384
+ `launcher` `launcher-icon` `launcher-label` `panel` `header` `header-heading` `header-logo`
385
+ `header-actions` `close-button` `messages` `messages-inner` `message` `message-user`
386
+ `message-assistant` `message-system` `bubble` `message-content` `avatar` `name` `meta` `time`
387
+ `status` `cursor` `typing` `to-latest` `composer` `input` `counter` `send-button` `spinner`
388
+
389
+ パーツ名・プロパティ名・イベント名・スロット名・CSS カスタムプロパティ名は公開 API として
390
+ 扱い、変更はメジャーバージョンでのみ行います。
391
+
392
+ ## アクセシビリティ
393
+
394
+ - ランチャーは `button`、パネルは `role="dialog"`、メッセージ一覧は `role="log"` +
395
+ `aria-live="polite"`
396
+ - Esc で閉じ、開閉に合わせてフォーカスをランチャーとパネルの間で移します
397
+ - ストリーミング中の発言は `aria-busy="true"` なので、1 文字ずつ読み上げられません
398
+ - `prefers-reduced-motion` を尊重します
399
+ - 既定テーマの文字色と背景色の組み合わせはすべて WCAG AA(4.5:1)以上です
400
+ - `npm run audit:a11y` で axe-core による監査を実行できます(6 状態、違反 0 を維持)
401
+
402
+ ## ブラウザ
403
+
404
+ Chrome / Edge / Firefox / Safari の最新 2 バージョンと iOS Safari 16 以降。IE は非対応です。
405
+
406
+ ## 開発
407
+
408
+ ```sh
409
+ npm install
410
+ npm run dev # demo/ を開いて動作確認
411
+ npm run typecheck # JSDoc の型チェック
412
+ npm test # Web Test Runner(Chromium / WebKit)
413
+ npm run audit:a11y # axe-core による監査
414
+ npm run build # 型定義と単一バンドルを dist/ に出力
415
+ npm run check # 上記をまとめて
416
+ ```
417
+
418
+ ### リリース前の手動チェック
419
+
420
+ IME の実挙動は合成イベントでは再現しきれないため、リリース前に実機で確認します。
421
+
422
+ - macOS Safari + 日本語 IME: 変換確定の Enter で誤送信しないこと
423
+ - iOS Safari: パネルが全画面になり、キーボード表示中も入力欄が隠れないこと
424
+ - Android Chrome: 変換中の Enter で誤送信しないこと
425
+
426
+ ## 公開
427
+
428
+ ```sh
429
+ npm login
430
+ npm run check
431
+ npm publish
432
+ ```
433
+
434
+ `package.json` の `publishConfig.access` が `public` なので、`--access public` を付ける必要は
435
+ ありません。スコープ付きパッケージは既定で非公開扱いになり、その場合 npm の無料プランでは
436
+ `402 Payment Required` になります。
437
+
438
+ ## ライセンス
439
+
440
+ MIT