@hidemikimura/chit-ui 0.3.0 → 0.3.2
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 +39 -0
- package/README.md +53 -3
- package/dist/chit-ui.iife.min.js +44 -20
- package/dist/chit-ui.iife.min.js.map +1 -1
- package/dist/chit-ui.min.js +44 -20
- package/dist/chit-ui.min.js.map +1 -1
- package/dist/types/controllers/scroll-lock-controller.d.ts +37 -0
- package/dist/types/controllers/viewport-controller.d.ts +38 -0
- package/dist/types/index.d.ts +162 -0
- package/package.json +2 -1
- package/skills/README.md +34 -0
- package/skills/chit-ui/SKILL.md +148 -0
- package/skills/chit-ui/references/api.md +189 -0
- package/skills/chit-ui/references/recipes.md +204 -0
- package/src/chit-ui.js +6 -0
- package/src/controllers/scroll-controller.js +21 -1
- package/src/controllers/scroll-lock-controller.js +185 -0
- package/src/controllers/viewport-controller.js +102 -0
- package/src/index.js +34 -0
- package/src/styles/composer.css.js +18 -0
- package/src/styles/panel.css.js +7 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a touch that lands on the open panel from scrolling the page behind
|
|
3
|
+
* it.
|
|
4
|
+
*
|
|
5
|
+
* `overscroll-behavior: contain` is on the message list already, and it does
|
|
6
|
+
* the job for the case it covers: a list that can scroll, dragged past its
|
|
7
|
+
* own end. It has nothing to say about the cases that actually leak. A short
|
|
8
|
+
* conversation does not overflow, so the list is not a scroll container at
|
|
9
|
+
* all and the touch goes straight through to the document — which is what a
|
|
10
|
+
* reader on an iPhone sees as the page sliding around underneath a chat that
|
|
11
|
+
* looks like it should be holding still. The same happens for a touch that
|
|
12
|
+
* starts on the title bar or beside the input.
|
|
13
|
+
*
|
|
14
|
+
* So the rule is decided here instead: a touch on the panel may scroll
|
|
15
|
+
* something inside the panel, and otherwise it does nothing. On the first
|
|
16
|
+
* move of each gesture the controller looks along the touch's own path —
|
|
17
|
+
* `composedPath`, so a scroller inside a consumer's component counts too —
|
|
18
|
+
* for something that can still travel in the direction the finger is going.
|
|
19
|
+
* Finding one, it stands aside for the rest of the gesture; finding none, it
|
|
20
|
+
* cancels the move, and iOS then takes the whole gesture as "not a scroll".
|
|
21
|
+
*
|
|
22
|
+
* Two touches are left alone: that is a pinch, and zooming is the reader's.
|
|
23
|
+
* A move that is no longer cancelable is left alone too — the browser has
|
|
24
|
+
* already committed to scrolling, and `overscroll-behavior` is what keeps
|
|
25
|
+
* that inside the list.
|
|
26
|
+
*
|
|
27
|
+
* @implements {ReactiveController}
|
|
28
|
+
*/
|
|
29
|
+
export class ScrollLockController implements ReactiveController {
|
|
30
|
+
/** @param {LitElement} host */
|
|
31
|
+
constructor(host: LitElement);
|
|
32
|
+
hostUpdated(): void;
|
|
33
|
+
hostDisconnected(): void;
|
|
34
|
+
#private;
|
|
35
|
+
}
|
|
36
|
+
import type { ReactiveController } from 'lit';
|
|
37
|
+
import type { LitElement } from 'lit';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the full-screen panel inside the part of the screen the reader can
|
|
3
|
+
* actually see.
|
|
4
|
+
*
|
|
5
|
+
* On a phone the panel is `position: fixed` across the whole viewport, and
|
|
6
|
+
* that viewport is not what is on screen once a keyboard slides up: the
|
|
7
|
+
* layout viewport keeps its full height, so the composer — and with it the
|
|
8
|
+
* last few messages — ends up behind the keyboard. iOS then scrolls the page
|
|
9
|
+
* to chase the focused field, which moves the fixed panel out of frame
|
|
10
|
+
* instead of helping. `dvh` is no use here either; it tracks the browser's
|
|
11
|
+
* own chrome collapsing, not the keyboard.
|
|
12
|
+
*
|
|
13
|
+
* `visualViewport` is the one thing that does report the visible box, so the
|
|
14
|
+
* panel is pinned to it: its top to `offsetTop`, its height to `height`. The
|
|
15
|
+
* composer lands just above the keyboard, and the conversation gets whatever
|
|
16
|
+
* is left.
|
|
17
|
+
*
|
|
18
|
+
* A pinch is left alone. Zooming is how a reader gets a closer look, and a
|
|
19
|
+
* panel that re-fitted itself to the magnified box would take that away by
|
|
20
|
+
* reflowing the text back to the same apparent size.
|
|
21
|
+
*
|
|
22
|
+
* @implements {ReactiveController}
|
|
23
|
+
*/
|
|
24
|
+
export class ViewportController implements ReactiveController {
|
|
25
|
+
/**
|
|
26
|
+
* @param {LitElement} host
|
|
27
|
+
* @param {{ enabled: () => boolean }} options
|
|
28
|
+
*/
|
|
29
|
+
constructor(host: LitElement, { enabled }: {
|
|
30
|
+
enabled: () => boolean;
|
|
31
|
+
});
|
|
32
|
+
hostConnected(): void;
|
|
33
|
+
hostDisconnected(): void;
|
|
34
|
+
hostUpdated(): void;
|
|
35
|
+
#private;
|
|
36
|
+
}
|
|
37
|
+
import type { ReactiveController } from 'lit';
|
|
38
|
+
import type { LitElement } from 'lit';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,166 @@
|
|
|
1
1
|
/// <reference path="./global.d.ts" />
|
|
2
2
|
export { ChitUI };
|
|
3
3
|
export { Events } from "./events.js";
|
|
4
|
+
/**
|
|
5
|
+
* The public types, re-exported so a consumer can write
|
|
6
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
7
|
+
* into `dist/types/` for them.
|
|
8
|
+
*/
|
|
9
|
+
export type Role = import("./types.js").Role;
|
|
10
|
+
/**
|
|
11
|
+
* The public types, re-exported so a consumer can write
|
|
12
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
13
|
+
* into `dist/types/` for them.
|
|
14
|
+
*/
|
|
15
|
+
export type MessageStatus = import("./types.js").MessageStatus;
|
|
16
|
+
/**
|
|
17
|
+
* The public types, re-exported so a consumer can write
|
|
18
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
19
|
+
* into `dist/types/` for them.
|
|
20
|
+
*/
|
|
21
|
+
export type ChatState = import("./types.js").ChatState;
|
|
22
|
+
/**
|
|
23
|
+
* The public types, re-exported so a consumer can write
|
|
24
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
25
|
+
* into `dist/types/` for them.
|
|
26
|
+
*/
|
|
27
|
+
export type Trigger = import("./types.js").Trigger;
|
|
28
|
+
/**
|
|
29
|
+
* The public types, re-exported so a consumer can write
|
|
30
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
31
|
+
* into `dist/types/` for them.
|
|
32
|
+
*/
|
|
33
|
+
export type Device = import("./types.js").Device;
|
|
34
|
+
/**
|
|
35
|
+
* The public types, re-exported so a consumer can write
|
|
36
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
37
|
+
* into `dist/types/` for them.
|
|
38
|
+
*/
|
|
39
|
+
export type Effect = import("./types.js").Effect;
|
|
40
|
+
/**
|
|
41
|
+
* The public types, re-exported so a consumer can write
|
|
42
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
43
|
+
* into `dist/types/` for them.
|
|
44
|
+
*/
|
|
45
|
+
export type Position = import("./types.js").Position;
|
|
46
|
+
/**
|
|
47
|
+
* The public types, re-exported so a consumer can write
|
|
48
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
49
|
+
* into `dist/types/` for them.
|
|
50
|
+
*/
|
|
51
|
+
export type BubbleTail = import("./types.js").BubbleTail;
|
|
52
|
+
/**
|
|
53
|
+
* The public types, re-exported so a consumer can write
|
|
54
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
55
|
+
* into `dist/types/` for them.
|
|
56
|
+
*/
|
|
57
|
+
export type LoadingStyle = import("./types.js").LoadingStyle;
|
|
58
|
+
/**
|
|
59
|
+
* The public types, re-exported so a consumer can write
|
|
60
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
61
|
+
* into `dist/types/` for them.
|
|
62
|
+
*/
|
|
63
|
+
export type MessageBase = import("./types.js").MessageBase;
|
|
64
|
+
/**
|
|
65
|
+
* The public types, re-exported so a consumer can write
|
|
66
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
67
|
+
* into `dist/types/` for them.
|
|
68
|
+
*/
|
|
69
|
+
export type TextMessage = import("./types.js").TextMessage;
|
|
70
|
+
/**
|
|
71
|
+
* The public types, re-exported so a consumer can write
|
|
72
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
73
|
+
* into `dist/types/` for them.
|
|
74
|
+
*/
|
|
75
|
+
export type HtmlMessage = import("./types.js").HtmlMessage;
|
|
76
|
+
/**
|
|
77
|
+
* The public types, re-exported so a consumer can write
|
|
78
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
79
|
+
* into `dist/types/` for them.
|
|
80
|
+
*/
|
|
81
|
+
export type TemplateMessage = import("./types.js").TemplateMessage;
|
|
82
|
+
/**
|
|
83
|
+
* The public types, re-exported so a consumer can write
|
|
84
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
85
|
+
* into `dist/types/` for them.
|
|
86
|
+
*/
|
|
87
|
+
export type ElementMessage = import("./types.js").ElementMessage;
|
|
88
|
+
/**
|
|
89
|
+
* The public types, re-exported so a consumer can write
|
|
90
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
91
|
+
* into `dist/types/` for them.
|
|
92
|
+
*/
|
|
93
|
+
export type ComponentMessage = import("./types.js").ComponentMessage;
|
|
94
|
+
/**
|
|
95
|
+
* The public types, re-exported so a consumer can write
|
|
96
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
97
|
+
* into `dist/types/` for them.
|
|
98
|
+
*/
|
|
99
|
+
export type Message = import("./types.js").Message;
|
|
100
|
+
/**
|
|
101
|
+
* The public types, re-exported so a consumer can write
|
|
102
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
103
|
+
* into `dist/types/` for them.
|
|
104
|
+
*/
|
|
105
|
+
export type Animation = import("./types.js").Animation;
|
|
106
|
+
/**
|
|
107
|
+
* The public types, re-exported so a consumer can write
|
|
108
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
109
|
+
* into `dist/types/` for them.
|
|
110
|
+
*/
|
|
111
|
+
export type Offset = import("./types.js").Offset;
|
|
112
|
+
/**
|
|
113
|
+
* The public types, re-exported so a consumer can write
|
|
114
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
115
|
+
* into `dist/types/` for them.
|
|
116
|
+
*/
|
|
117
|
+
export type ClosedTheme = import("./types.js").ClosedTheme;
|
|
118
|
+
/**
|
|
119
|
+
* The public types, re-exported so a consumer can write
|
|
120
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
121
|
+
* into `dist/types/` for them.
|
|
122
|
+
*/
|
|
123
|
+
export type OpenColors = import("./types.js").OpenColors;
|
|
124
|
+
/**
|
|
125
|
+
* The public types, re-exported so a consumer can write
|
|
126
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
127
|
+
* into `dist/types/` for them.
|
|
128
|
+
*/
|
|
129
|
+
export type SpeakerTheme = import("./types.js").SpeakerTheme;
|
|
130
|
+
/**
|
|
131
|
+
* The public types, re-exported so a consumer can write
|
|
132
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
133
|
+
* into `dist/types/` for them.
|
|
134
|
+
*/
|
|
135
|
+
export type LoadingTheme = import("./types.js").LoadingTheme;
|
|
136
|
+
/**
|
|
137
|
+
* The public types, re-exported so a consumer can write
|
|
138
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
139
|
+
* into `dist/types/` for them.
|
|
140
|
+
*/
|
|
141
|
+
export type OpenTheme = import("./types.js").OpenTheme;
|
|
142
|
+
/**
|
|
143
|
+
* The public types, re-exported so a consumer can write
|
|
144
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
145
|
+
* into `dist/types/` for them.
|
|
146
|
+
*/
|
|
147
|
+
export type Theme = import("./types.js").Theme;
|
|
148
|
+
/**
|
|
149
|
+
* The public types, re-exported so a consumer can write
|
|
150
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
151
|
+
* into `dist/types/` for them.
|
|
152
|
+
*/
|
|
153
|
+
export type ResolvedClosed = import("./types.js").ResolvedClosed;
|
|
154
|
+
/**
|
|
155
|
+
* The public types, re-exported so a consumer can write
|
|
156
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
157
|
+
* into `dist/types/` for them.
|
|
158
|
+
*/
|
|
159
|
+
export type ResolvedOpen = import("./types.js").ResolvedOpen;
|
|
160
|
+
/**
|
|
161
|
+
* The public types, re-exported so a consumer can write
|
|
162
|
+
* `import type { Message } from '@hidemikimura/chit-ui'` instead of reaching
|
|
163
|
+
* into `dist/types/` for them.
|
|
164
|
+
*/
|
|
165
|
+
export type ResolvedTheme = import("./types.js").ResolvedTheme;
|
|
4
166
|
import { ChitUI } from './chit-ui.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hidemikimura/chit-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"src",
|
|
28
28
|
"dist",
|
|
29
|
+
"skills",
|
|
29
30
|
"README.md",
|
|
30
31
|
"CHANGELOG.md",
|
|
31
32
|
"LICENSE"
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# AI 向けの skill
|
|
2
|
+
|
|
3
|
+
`chit-ui/` は、Chit UI を組み込む人の AI コーディング支援(Claude Code、Cursor、
|
|
4
|
+
その他 skill を読めるツール)向けの説明書です。npm パッケージにも同梱しているので、
|
|
5
|
+
`node_modules/@hidemikimura/chit-ui/skills/chit-ui/` からそのまま参照できます。
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
chit-ui/
|
|
9
|
+
├── SKILL.md 境界・最小の組み込み・間違えやすいところ
|
|
10
|
+
└── references/
|
|
11
|
+
├── api.md 全プロパティ・イベント・スロット・パーツ・CSS 変数・テーマ
|
|
12
|
+
└── recipes.md 用途別の書き方(ストリーミング、シナリオ型、添付、LIFF ほか)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 使い方
|
|
16
|
+
|
|
17
|
+
Claude Code なら、プロジェクトの `.claude/skills/` に置くか、シンボリックリンクを張ります。
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
mkdir -p .claude/skills
|
|
21
|
+
ln -s ../../node_modules/@hidemikimura/chit-ui/skills/chit-ui .claude/skills/chit-ui
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
skill を読み込まないツールでも、`SKILL.md` と `references/` を渡せばそのまま使えます。
|
|
25
|
+
|
|
26
|
+
## 書くときの約束
|
|
27
|
+
|
|
28
|
+
人間向けの説明は README.md とドキュメントサイト(`site/`)が正本で、ここはその要約では
|
|
29
|
+
ありません。**AI が間違えやすいところ**を優先して書きます。`messages` の所有者が利用者側で
|
|
30
|
+
あること、`push` では描画されないこと、`text` と `html` の使い分け、`typing` と `loading`
|
|
31
|
+
が排他であること、といった「知らないと自然に踏む地雷」が中心です。
|
|
32
|
+
|
|
33
|
+
API を変えたら、README.md・`site/`・`skills/` の 3 つを一緒に更新してください。
|
|
34
|
+
`references/api.md` の冒頭には対象バージョンを書いています。
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chit-ui
|
|
3
|
+
description: Chit UI(@hidemikimura/chit-ui)でチャットウィジェットを組み込む・直すときに必ず使う。Lit 製のチャット UI を Web ページやアプリに載せる、吹き出しやランチャーの見た目を変える、発言にカスタムコンポーネントを差し込む、送信イベントをバックエンドにつなぐ、`<chit-ui>` というタグや chat-submit / chat-attach / chat-move といったイベントがコードに出てきた、といった場面で参照する。チャットウィジェット・チャット UI・問い合わせウィジェットを新しく作る相談にも使う。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Chit UI
|
|
7
|
+
|
|
8
|
+
Lit で書かれたチャットウィジェットのカスタム要素。ページの隅に丸いランチャーを置き、
|
|
9
|
+
押すとパネルが開く。スマホ幅では全画面になる。
|
|
10
|
+
|
|
11
|
+
**このライブラリが持たないもの**: 通信、状態管理、発言の保存。`messages` 配列の所有者は
|
|
12
|
+
利用者側で、ライブラリはそれを読んで描くだけ。書き換えることは一度もない。送信も
|
|
13
|
+
「送信された」というイベントを出すところまでで、そこから先は利用者のコードが決める。
|
|
14
|
+
この境界を踏み外した設計(ライブラリが勝手に発言を足す、通信を始める)にしないこと。
|
|
15
|
+
|
|
16
|
+
## 最小の組み込み
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@hidemikimura/chit-ui/dist/chit-ui.min.js"></script>
|
|
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
|
+
chat.messages = [...chat.messages, { id: crypto.randomUUID(), role: 'user', text }];
|
|
29
|
+
chat.busy = true;
|
|
30
|
+
chat.typing = true;
|
|
31
|
+
|
|
32
|
+
const reply = await yourBackend(text);
|
|
33
|
+
|
|
34
|
+
chat.typing = false;
|
|
35
|
+
chat.busy = false;
|
|
36
|
+
chat.messages = [...chat.messages,
|
|
37
|
+
{ id: crypto.randomUUID(), role: 'assistant', text: reply }];
|
|
38
|
+
});
|
|
39
|
+
</script>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
npm から使うときは `npm install @hidemikimura/chit-ui lit` して `import '@hidemikimura/chit-ui';`。
|
|
43
|
+
タグ名を自分で決めるなら `import { ChitUI } from '@hidemikimura/chit-ui/element.js';` を
|
|
44
|
+
使って `customElements.define()` する。型は
|
|
45
|
+
`import type { Message, Theme } from '@hidemikimura/chit-ui';`。
|
|
46
|
+
|
|
47
|
+
**npm 版と単一バンドル(dist/chit-ui.min.js)を同じページで混ぜない。** Lit が二重に
|
|
48
|
+
読み込まれる。`window.ChitUI` を作る IIFE 版(`dist/chit-ui.iife.min.js`)もある。
|
|
49
|
+
|
|
50
|
+
## 間違えやすいところ
|
|
51
|
+
|
|
52
|
+
**`messages` は必ず新しい配列を代入する。** `chat.messages.push(...)` は描画されない
|
|
53
|
+
(Lit が同一参照を変更なしとみなす)。`[...chat.messages, next]` の形で代入する。
|
|
54
|
+
|
|
55
|
+
**`id` は必須で、安定していること。** 差分描画のキーになる。毎回採番し直すと全部の
|
|
56
|
+
吹き出しが作り直され、コンポーネント発言のインスタンスも捨てられる。
|
|
57
|
+
|
|
58
|
+
**人が書いた文章は `text`、自分で組み立てた HTML だけ `html`。** `text` は Lit が
|
|
59
|
+
テキストノードとして描くのでエスケープが構造的に保証され、改行もそのまま出る。
|
|
60
|
+
`html` を使うなら `chat.sanitize = (html) => DOMPurify.sanitize(html)` を必ず設定する。
|
|
61
|
+
バックエンドや LLM の出力をそのまま `html` に入れるのは事故のもと。
|
|
62
|
+
|
|
63
|
+
**`typing` と `loading` は排他。** 一方を `true` にすると他方が `false` になる。
|
|
64
|
+
相手が機械なら「入力中」より `loading`(スピナーや文言)が正しい。
|
|
65
|
+
|
|
66
|
+
**`chat-submit` は中止できる。** `event.preventDefault()` すると入力欄は空にならず、
|
|
67
|
+
`open.loading.auto` の自動ローディングも始まらない。
|
|
68
|
+
|
|
69
|
+
**スマホ幅では `open.width` / `open.height` は無視される**(常に全画面)。
|
|
70
|
+
`open.draggable` もスマホでは効かない。
|
|
71
|
+
|
|
72
|
+
**`open.header.visible: false` にすると閉じるボタンも消える。** LINE LIFF のように
|
|
73
|
+
外側がタイトルを持つ画面向け。Esc とランチャーでは閉じられる。
|
|
74
|
+
|
|
75
|
+
## 発言の 5 形式
|
|
76
|
+
|
|
77
|
+
`text` / `html` / `template`(Lit の `TemplateResult`)/ `element`(`HTMLElement`)/
|
|
78
|
+
`component`(コンストラクタかタグ名 + `props`)のいずれか 1 つを持たせる。
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
{ id: 'x', role: 'assistant', component: 'order-card', props: { orderId: '1001' } }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`component` は同じ `id` である限りインスタンスが再利用され、変わった `props` だけが
|
|
85
|
+
再代入される。発言の中のボタンは個別にリスナーを付けるより
|
|
86
|
+
`chat-message-click`(`detail: { message, target, originalEvent }`)で一括して受けるほうが楽。
|
|
87
|
+
`target` はコンポーネントの Shadow DOM 内まで辿れている。
|
|
88
|
+
|
|
89
|
+
`role` は `'user'` / `'assistant'` / `'system'`。`system` は中央寄せの連絡行で、
|
|
90
|
+
名前もアイコンも出ない。
|
|
91
|
+
|
|
92
|
+
## テーマ
|
|
93
|
+
|
|
94
|
+
部分指定でよく、指定しなかった項目は既定値で埋まる。`null` を渡すと既定値を消せる。
|
|
95
|
+
実行中に差し替えれば即反映される(ダークモードの切り替えなど)。
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
chat.theme = {
|
|
99
|
+
breakpoint: 768,
|
|
100
|
+
closed: { pc: { size: 64, image: '/icon.png' }, mobile: { size: 56 }, draggable: true },
|
|
101
|
+
open: {
|
|
102
|
+
width: 380, height: 600,
|
|
103
|
+
colors: { accent: '#2563eb' },
|
|
104
|
+
bubble: { radius: 18, tail: 'top' },
|
|
105
|
+
header: { title: 'サポート', logo: '/logo.png', home: true },
|
|
106
|
+
loading: { auto: true, style: 'spinner' },
|
|
107
|
+
input: { attach: true, placeholder: 'ご質問をどうぞ' },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
付属テーマは `import { greenTheme } from '@hidemikimura/chit-ui/themes.js'`
|
|
113
|
+
(単一バンドルなら `window.ChitUI.greenTheme`)。重ねて調整できる。
|
|
114
|
+
|
|
115
|
+
細かい色だけ変えたいなら CSS のほうが早い。テーマの値は CSS カスタムプロパティになり、
|
|
116
|
+
ページ側の指定が勝つ。
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
chit-ui { --chit-color-accent: #d81b60; --chit-launcher-size: 72px; }
|
|
120
|
+
chit-ui::part(bubble) { font-size: 15px; }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--_chit-` で始まる変数は内部用。触らない。
|
|
124
|
+
|
|
125
|
+
## よく使う API
|
|
126
|
+
|
|
127
|
+
| 種類 | もの |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| プロパティ | `messages` `state` `theme` `typing` `loading` `busy` `value` `inputDisabled` `inputHidden` `maxLength` `sanitize` `locale` `labels` `dragOffset` |
|
|
130
|
+
| メソッド | `open()` `close()` `toggle()` `submit(text?)` `focusInput()` `scrollToBottom()` `getMessageElement(id)` `home()` `openAttach()` `resetPosition()` |
|
|
131
|
+
| イベント | `chat-submit` `chat-open` `chat-close` `chat-state-change` `chat-input` `chat-message-click` `chat-message-render` `chat-scroll-top` `chat-home` `chat-attach` `chat-move` `chat-breakpoint-change` |
|
|
132
|
+
|
|
133
|
+
状態遷移のメソッドはアニメーション完了で resolve する Promise を返す(中止されたら `false`)。
|
|
134
|
+
すべてのイベントは `bubbles` と `composed` が立っているので `document` でも拾える。
|
|
135
|
+
|
|
136
|
+
網羅した表(全プロパティ・全イベントの `detail`・スロット・CSS パーツ・CSS 変数・
|
|
137
|
+
テーマの全項目)は `references/api.md`。用途別の書き方(ストリーミング、シナリオ型、
|
|
138
|
+
未読バッジ、LIFF、添付の送信)は `references/recipes.md`。
|
|
139
|
+
|
|
140
|
+
## 作るときの順序
|
|
141
|
+
|
|
142
|
+
1. `chat-submit` を受けて自分の発言を積み、返事を積むところまで作る。ここが本体。
|
|
143
|
+
2. 待っている間の表示を決める(`busy` + `loading`、または `open.loading.auto`)。
|
|
144
|
+
3. 見た目はテーマ、細部は CSS カスタムプロパティと `::part()`。
|
|
145
|
+
4. 必要なら発言の中身を `component` にして、操作は `chat-message-click` で受ける。
|
|
146
|
+
|
|
147
|
+
会話の保存・復元、未読管理、ログインの有無といった判断はすべて利用者側のコードに置く。
|
|
148
|
+
ライブラリに寄せようとしないこと。
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Chit UI API リファレンス
|
|
2
|
+
|
|
3
|
+
`@hidemikimura/chit-ui` 0.3.2 時点。パーツ名・プロパティ名・イベント名・スロット名・
|
|
4
|
+
CSS カスタムプロパティ名は公開 API で、変更はメジャーバージョンでのみ行う。
|
|
5
|
+
|
|
6
|
+
## プロパティ
|
|
7
|
+
|
|
8
|
+
| プロパティ | 型 | 既定 | 内容 |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| `state` | `'closed' \| 'open' \| 'hidden'` | `'closed'` | 現在の状態 |
|
|
11
|
+
| `theme` | `Theme` | `{}` | テーマ(部分指定可) |
|
|
12
|
+
| `messages` | `Message[]` | `[]` | 描画する発言。必ず新しい配列を代入する |
|
|
13
|
+
| `typing` | `boolean \| { html }` | `false` | 相手が入力中の表示。`loading` とは排他 |
|
|
14
|
+
| `loading` | `boolean` | `false` | 応答待ちの表示。`typing` とは排他。属性にも反映される |
|
|
15
|
+
| `busy` | `boolean` | `false` | 送信中。入力をロックする |
|
|
16
|
+
| `inputDisabled` | `boolean` | `false` | 入力欄を見せたままロック |
|
|
17
|
+
| `inputHidden` | `boolean` | `false` | 入力欄を消す(`submit(text)` は使える) |
|
|
18
|
+
| `value` | `string` | `''` | 入力欄の内容 |
|
|
19
|
+
| `placeholder` | `string` | テーマの値 | 入力欄のプレースホルダー |
|
|
20
|
+
| `sendOnEnter` | `boolean` | `true` | Enter で送信するか |
|
|
21
|
+
| `maxLength` | `number` | なし | 入力の上限(コードポイント単位) |
|
|
22
|
+
| `focusOnOpen` | `'auto' \| 'always' \| 'never'` | `'auto'` | 開いたときの自動フォーカス。`auto` は PC のみ |
|
|
23
|
+
| `sanitize` | `(html: string) => string` | なし | `html` 発言のサニタイズ |
|
|
24
|
+
| `formatTime` | `(time: Date) => string` | `HH:mm` | 時刻表示 |
|
|
25
|
+
| `messageStyles` | `string` | `''` | 発言の中に適用する追加 CSS |
|
|
26
|
+
| `locale` | `string` | `<html lang>` | ラベルと時刻の言語(`ja` / `en`) |
|
|
27
|
+
| `labels` | `Partial<Labels>` | なし | UI 文字列の上書き |
|
|
28
|
+
| `dragOffset` | `{ x, y } \| null` | `null` | ドラッグで生じたずれ。読み書きできる |
|
|
29
|
+
|
|
30
|
+
読み取り専用: `renderedState` `device`(`'pc' \| 'mobile'`)`hasUnseen` `canSend`
|
|
31
|
+
`currentTheme` `currentLabels` `resolvedLocale`。
|
|
32
|
+
|
|
33
|
+
## メソッド
|
|
34
|
+
|
|
35
|
+
| メソッド | 内容 |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `open()` `close()` `hide()` `show()` `toggle()` | 状態遷移。アニメーション完了で resolve する `Promise<boolean>`(中止なら `false`) |
|
|
38
|
+
| `submit(text?)` | `chat-submit` を発火。省略時は入力欄の内容 |
|
|
39
|
+
| `focusInput()` `clearInput()` | 入力欄の操作 |
|
|
40
|
+
| `scrollToBottom({ smooth })` | 最新へスクロール。省略時はテーマの設定に従う |
|
|
41
|
+
| `getMessageElement(id)` | その発言のコンテナ DOM(未描画なら `null`) |
|
|
42
|
+
| `home()` | `chat-home` を発火(ホームボタンと同じ) |
|
|
43
|
+
| `openAttach()` | 添付のファイル選択を開く |
|
|
44
|
+
| `resetPosition()` | ドラッグで動かした位置を忘れる |
|
|
45
|
+
|
|
46
|
+
## イベント
|
|
47
|
+
|
|
48
|
+
すべて `CustomEvent`。`bubbles` と `composed` が立っているので `document` でも拾える。
|
|
49
|
+
|
|
50
|
+
| イベント | タイミング | `detail` | 中止 |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| `chat-submit` | 送信された | `{ text }` | ○ |
|
|
53
|
+
| `chat-before-open` / `chat-before-close` | 遷移の直前 | `{ from, trigger }` | ○ |
|
|
54
|
+
| `chat-open` / `chat-close` | 遷移アニメーション完了 | `{ from, trigger }` | |
|
|
55
|
+
| `chat-hide` / `chat-show` | 非表示になった / 戻った | `{ from, to }` | |
|
|
56
|
+
| `chat-state-change` | `state` が変わった直後 | `{ from, to, trigger }` | |
|
|
57
|
+
| `chat-input` | 入力欄が編集された | `{ value }` | |
|
|
58
|
+
| `chat-message-render` | 発言が描画・更新された | `{ message, element, instance }` | |
|
|
59
|
+
| `chat-message-click` | 発言の中がクリックされた | `{ message, target, originalEvent }` | ○ |
|
|
60
|
+
| `chat-scroll-top` | 一覧が最上部に達した | `{}` | |
|
|
61
|
+
| `chat-breakpoint-change` | PC / スマホの判定が変わった | `{ device }` | |
|
|
62
|
+
| `chat-home` | ホームボタン、または `home()` | `{ trigger }` | |
|
|
63
|
+
| `chat-attach` | 添付ファイルが選ばれた | `{ files: File[] }` | |
|
|
64
|
+
| `chat-move` | ドラッグまたは矢印キー | `{ target, position, offset, displacement }` | |
|
|
65
|
+
|
|
66
|
+
`trigger` は `'user'`(クリックや Esc)か `'api'`(メソッドやプロパティ代入)。
|
|
67
|
+
|
|
68
|
+
## 発言オブジェクト
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
{
|
|
72
|
+
id: string, // 必須・安定していること
|
|
73
|
+
role: 'user' | 'assistant' | 'system',
|
|
74
|
+
name?: string | null, // テーマの既定を打ち消すなら null
|
|
75
|
+
avatar?: string | null,
|
|
76
|
+
time?: Date | string | number,
|
|
77
|
+
status?: 'sending' | 'sent' | 'error',
|
|
78
|
+
streaming?: boolean, // 末尾にカーソル、aria-busy
|
|
79
|
+
meta?: unknown, // 利用者の自由欄。イベントでそのまま返る
|
|
80
|
+
// 以下のいずれか 1 つ
|
|
81
|
+
text?: string,
|
|
82
|
+
html?: string,
|
|
83
|
+
template?: TemplateResult,
|
|
84
|
+
element?: HTMLElement,
|
|
85
|
+
component?: (new () => HTMLElement) | string,
|
|
86
|
+
props?: Record<string, unknown>, // component のとき
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## テーマ
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
{
|
|
94
|
+
breakpoint?: number, // 既定 768。これ未満をスマホとみなす
|
|
95
|
+
closed?: {
|
|
96
|
+
// pc / mobile に分けて書ける。分けなければ両方に同じ設定
|
|
97
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left',
|
|
98
|
+
offset?: { x: number, y: number },
|
|
99
|
+
size?: number | 'auto',
|
|
100
|
+
radius?: number,
|
|
101
|
+
image?: string | null,
|
|
102
|
+
label?: string | null,
|
|
103
|
+
component?: (new () => HTMLElement) | string,
|
|
104
|
+
props?: Record<string, unknown>,
|
|
105
|
+
colors?: { bg?, text?, shadow? },
|
|
106
|
+
draggable?: boolean, // 既定 false
|
|
107
|
+
animation?: { enter?, exit?, duration? },
|
|
108
|
+
},
|
|
109
|
+
open?: {
|
|
110
|
+
width?: number, height?: number, // スマホでは無視(常に全画面)
|
|
111
|
+
position?: Position, offset?: { x, y },
|
|
112
|
+
radius?: number,
|
|
113
|
+
launcher?: 'hidden' | 'visible',
|
|
114
|
+
draggable?: boolean, // 取っ手はタイトルバー。スマホでは無効
|
|
115
|
+
header?: { visible?: boolean, title?: string | null, logo?: string | null, home?: boolean },
|
|
116
|
+
bubble?: { radius?: number, tail?: 'none' | 'top' | 'bottom' },
|
|
117
|
+
speaker?: { assistant?: { name?, avatar? }, user?: { name?, avatar? } },
|
|
118
|
+
loading?: { auto?: boolean, style?: 'dots' | 'spinner' | 'text', text?: string | null, timeout?: number },
|
|
119
|
+
input?: { maxRows?: number, placeholder?: string | null, attach?: boolean, accept?: string, multiple?: boolean },
|
|
120
|
+
colors?: { bg?, text?, accent?, border?, userBubble?, userText?, assistantBubble?,
|
|
121
|
+
assistantText?, systemText?, headerBg?, headerText?, inputBg?, inputText?,
|
|
122
|
+
inputPlaceholder? },
|
|
123
|
+
bgImage?: string | null,
|
|
124
|
+
animation?: { enter?, exit?, duration?, scroll?: 'smooth' | 'instant' },
|
|
125
|
+
},
|
|
126
|
+
hidden?: { animation?: { exit?, duration? } },
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`animation` の `enter` / `exit` は `'fade' | 'scale' | 'slide' | 'none'`。
|
|
131
|
+
|
|
132
|
+
## スロット
|
|
133
|
+
|
|
134
|
+
| スロット | 差し替え対象 |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `launcher` | 閉じた状態のアイコンの中身 |
|
|
137
|
+
| `header` | パネルヘッダー全体(`header-title` / `header-actions` で部分差し替えも可) |
|
|
138
|
+
| `empty` | 発言が 1 件もないときの表示 |
|
|
139
|
+
| `input-before` / `input-after` | 入力欄の前後 |
|
|
140
|
+
| `composer` | 入力欄全体。差し替えたら `submit(text)` を自分で呼ぶ |
|
|
141
|
+
| `footer` | 入力欄の下(免責事項など) |
|
|
142
|
+
|
|
143
|
+
## CSS パーツ
|
|
144
|
+
|
|
145
|
+
`launcher` `launcher-icon` `launcher-label` `panel` `header` `header-title`
|
|
146
|
+
`header-heading` `header-logo` `header-actions` `home-button` `close-button`
|
|
147
|
+
`messages` `messages-inner` `message` `message-user` `message-assistant`
|
|
148
|
+
`message-system` `bubble` `message-content` `avatar` `name` `meta` `time` `status`
|
|
149
|
+
`cursor` `typing` `loading` `loading-text` `to-latest` `composer` `input`
|
|
150
|
+
`attach-button` `attach-input` `counter` `send-button` `spinner`
|
|
151
|
+
|
|
152
|
+
## CSS カスタムプロパティ(33)
|
|
153
|
+
|
|
154
|
+
色: `--chit-color-bg` `--chit-color-text` `--chit-color-accent` `--chit-color-border`
|
|
155
|
+
`--chit-color-user-bg` `--chit-color-user-text` `--chit-color-assistant-bg`
|
|
156
|
+
`--chit-color-assistant-text` `--chit-color-system-text` `--chit-color-header-bg`
|
|
157
|
+
`--chit-color-header-text` `--chit-color-input-bg` `--chit-color-input-text`
|
|
158
|
+
`--chit-color-input-placeholder`
|
|
159
|
+
|
|
160
|
+
ランチャー: `--chit-launcher-size` `--chit-launcher-radius` `--chit-launcher-offset-x`
|
|
161
|
+
`--chit-launcher-offset-y` `--chit-launcher-bg` `--chit-launcher-text`
|
|
162
|
+
`--chit-launcher-shadow` `--chit-launcher-image`
|
|
163
|
+
|
|
164
|
+
吹き出し: `--chit-bubble-radius`
|
|
165
|
+
|
|
166
|
+
パネル: `--chit-panel-width` `--chit-panel-height` `--chit-panel-radius`
|
|
167
|
+
`--chit-panel-offset-x` `--chit-panel-offset-y` `--chit-panel-shadow`
|
|
168
|
+
`--chit-panel-bg-image`
|
|
169
|
+
|
|
170
|
+
全体: `--chit-font-family` `--chit-font-size` `--chit-z-index`
|
|
171
|
+
|
|
172
|
+
アニメーションの長さは CSS 変数にしていない(遷移の完了を JavaScript 側が知る必要が
|
|
173
|
+
あるため、テーマの `animation.duration` が唯一の設定箇所)。
|
|
174
|
+
|
|
175
|
+
## エントリポイント
|
|
176
|
+
|
|
177
|
+
| 指定 | 中身 |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `@hidemikimura/chit-ui` | `<chit-ui>` を登録する。型もここから読める |
|
|
180
|
+
| `@hidemikimura/chit-ui/element.js` | 登録しない。`ChitUI` クラスだけ |
|
|
181
|
+
| `@hidemikimura/chit-ui/themes.js` | 付属テーマ(`greenTheme`) |
|
|
182
|
+
| `dist/chit-ui.min.js` | Lit 同梱の ESM 単一バンドル |
|
|
183
|
+
| `dist/chit-ui.iife.min.js` | 同上の IIFE 版。`window.ChitUI` |
|
|
184
|
+
|
|
185
|
+
## 対応ブラウザ
|
|
186
|
+
|
|
187
|
+
Chrome / Edge / Firefox / Safari の最新 2 バージョンと iOS Safari 16 以降。
|
|
188
|
+
吹き出しのしっぽだけ `clip-path: path()` を使っていて、非対応の環境ではしっぽが出ない
|
|
189
|
+
(他の機能には影響しない)。
|