@hidemikimura/chit-ui 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +148 -3
- package/dist/chit-ui.iife.min.js +218 -99
- package/dist/chit-ui.iife.min.js.map +1 -1
- package/dist/chit-ui.min.js +209 -90
- package/dist/chit-ui.min.js.map +1 -1
- package/dist/types/chit-ui.d.ts +64 -4
- package/dist/types/controllers/composer-controller.d.ts +4 -0
- package/dist/types/controllers/drag-controller.d.ts +118 -0
- package/dist/types/controllers/scroll-lock-controller.d.ts +37 -0
- package/dist/types/controllers/viewport-controller.d.ts +38 -0
- package/dist/types/events.d.ts +1 -0
- package/dist/types/global.d.ts +7 -1
- package/dist/types/i18n/labels.d.ts +8 -0
- package/dist/types/index.d.ts +162 -0
- package/dist/types/render/message-list.d.ts +0 -1
- package/dist/types/theme/default-theme.d.ts +8 -0
- package/dist/types/types.d.ts +40 -0
- package/package.json +1 -1
- package/src/chit-ui.js +261 -4
- package/src/controllers/composer-controller.js +2 -0
- package/src/controllers/drag-controller.js +319 -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/events.js +1 -0
- package/src/global.d.ts +7 -1
- package/src/i18n/labels.js +6 -0
- package/src/index.js +34 -0
- package/src/render/launcher.js +9 -1
- package/src/render/message-list.js +40 -10
- package/src/render/panel.js +17 -1
- package/src/styles/composer.css.js +18 -0
- package/src/styles/launcher.css.js +13 -0
- package/src/styles/message.css.js +60 -15
- package/src/styles/panel.css.js +30 -3
- package/src/theme/default-theme.js +16 -0
- package/src/types.js +18 -0
|
@@ -102,7 +102,8 @@ export const messageStyles = css`
|
|
|
102
102
|
*/
|
|
103
103
|
|
|
104
104
|
[part~='bubble']::after,
|
|
105
|
-
[part~='typing']::after
|
|
105
|
+
[part~='typing']::after,
|
|
106
|
+
[part~='loading']::after {
|
|
106
107
|
content: '';
|
|
107
108
|
display: none;
|
|
108
109
|
position: absolute;
|
|
@@ -118,9 +119,11 @@ export const messageStyles = css`
|
|
|
118
119
|
*/
|
|
119
120
|
@supports (clip-path: path('M 0 0 Z')) {
|
|
120
121
|
:host([data-bubble-tail='top']) [part~='bubble']::after,
|
|
121
|
-
:host([data-bubble-tail='bottom']) [part~='bubble']::after,
|
|
122
122
|
:host([data-bubble-tail='top']) [part~='typing']::after,
|
|
123
|
-
:host([data-bubble-tail='
|
|
123
|
+
:host([data-bubble-tail='top']) [part~='loading']::after,
|
|
124
|
+
:host([data-bubble-tail='bottom']) [part~='bubble']::after,
|
|
125
|
+
:host([data-bubble-tail='bottom']) [part~='typing']::after,
|
|
126
|
+
:host([data-bubble-tail='bottom']) [part~='loading']::after {
|
|
124
127
|
display: block;
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -129,15 +132,18 @@ export const messageStyles = css`
|
|
|
129
132
|
* bubble carries one cue about who is talking, not two.
|
|
130
133
|
*/
|
|
131
134
|
:host([data-bubble-tail='top']) [part~='bubble'],
|
|
132
|
-
:host([data-bubble-tail='bottom']) [part~='bubble'],
|
|
133
135
|
:host([data-bubble-tail='top']) [part~='typing'],
|
|
134
|
-
:host([data-bubble-tail='
|
|
136
|
+
:host([data-bubble-tail='top']) [part~='loading'],
|
|
137
|
+
:host([data-bubble-tail='bottom']) [part~='bubble'],
|
|
138
|
+
:host([data-bubble-tail='bottom']) [part~='typing'],
|
|
139
|
+
:host([data-bubble-tail='bottom']) [part~='loading'] {
|
|
135
140
|
border-radius: var(--chit-bubble-radius);
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
/* Their side: the leaf points left, its root inside the bubble. */
|
|
139
144
|
:host([data-bubble-tail]) [part~='message-assistant'] [part~='bubble']::after,
|
|
140
|
-
:host([data-bubble-tail]) [part~='typing']::after
|
|
145
|
+
:host([data-bubble-tail]) [part~='typing']::after,
|
|
146
|
+
:host([data-bubble-tail]) [part~='loading']::after {
|
|
141
147
|
right: calc(100% - var(--_chit-tail-root));
|
|
142
148
|
background: var(--chit-color-assistant-bg);
|
|
143
149
|
}
|
|
@@ -150,17 +156,20 @@ export const messageStyles = css`
|
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
:host([data-bubble-tail='top']) [part~='bubble']::after,
|
|
153
|
-
:host([data-bubble-tail='top']) [part~='typing']::after
|
|
159
|
+
:host([data-bubble-tail='top']) [part~='typing']::after,
|
|
160
|
+
:host([data-bubble-tail='top']) [part~='loading']::after {
|
|
154
161
|
top: var(--_chit-tail-offset);
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
:host([data-bubble-tail='bottom']) [part~='bubble']::after,
|
|
158
|
-
:host([data-bubble-tail='bottom']) [part~='typing']::after
|
|
165
|
+
:host([data-bubble-tail='bottom']) [part~='typing']::after,
|
|
166
|
+
:host([data-bubble-tail='bottom']) [part~='loading']::after {
|
|
159
167
|
bottom: var(--_chit-tail-offset);
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
:host([data-bubble-tail='bottom']) [part~='message-assistant'] [part~='bubble']::after,
|
|
163
|
-
:host([data-bubble-tail='bottom']) [part~='typing']::after
|
|
171
|
+
:host([data-bubble-tail='bottom']) [part~='typing']::after,
|
|
172
|
+
:host([data-bubble-tail='bottom']) [part~='loading']::after {
|
|
164
173
|
transform: scaleY(-1);
|
|
165
174
|
}
|
|
166
175
|
|
|
@@ -228,24 +237,57 @@ export const messageStyles = css`
|
|
|
228
237
|
}
|
|
229
238
|
}
|
|
230
239
|
|
|
231
|
-
[part~='typing']
|
|
240
|
+
[part~='typing'],
|
|
241
|
+
[part~='loading'] {
|
|
232
242
|
position: relative;
|
|
233
243
|
align-self: flex-start;
|
|
234
244
|
/* Indented past the speaker's icon, when the theme gives them one. */
|
|
235
245
|
margin-inline-start: var(--_chit-speaker-gutter, 0px);
|
|
236
246
|
padding: 0.7em 0.9em;
|
|
247
|
+
/*
|
|
248
|
+
* Stated rather than left to the font's idea of "normal", so that both
|
|
249
|
+
* indicators are the same height whatever the page's typeface does, and
|
|
250
|
+
* so the spinner has a line box it is known to fit inside.
|
|
251
|
+
*/
|
|
252
|
+
line-height: 1.5;
|
|
237
253
|
border-radius: var(--chit-bubble-radius);
|
|
238
254
|
border-bottom-left-radius: 4px;
|
|
239
255
|
background: var(--chit-color-assistant-bg);
|
|
240
256
|
color: var(--chit-color-assistant-text);
|
|
241
257
|
}
|
|
242
258
|
|
|
243
|
-
|
|
259
|
+
/*
|
|
260
|
+
* The wait is laid out inline, like the typing bubble it stands in for. A
|
|
261
|
+
* flex box has no line box of its own, so dots or a spinner alone would sit
|
|
262
|
+
* in a noticeably shorter bubble than one holding a line of text.
|
|
263
|
+
*/
|
|
264
|
+
[part~='loading'] > * + * {
|
|
265
|
+
margin-inline-start: 0.5em;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
[part~='loading'] [part~='spinner'] {
|
|
269
|
+
display: inline-block;
|
|
270
|
+
/*
|
|
271
|
+
* Centred on the line rather than sitting on the baseline: at 1.1em it
|
|
272
|
+
* then fits inside the line box the text would have made on its own, so
|
|
273
|
+
* a spinner bubble is exactly as tall as a typing bubble.
|
|
274
|
+
*/
|
|
275
|
+
vertical-align: middle;
|
|
276
|
+
color: var(--chit-color-system-text);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
[part~='loading-text'] {
|
|
280
|
+
font-size: 0.9em;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
[part~='typing'] .dots,
|
|
284
|
+
[part~='loading'] .dots {
|
|
244
285
|
display: inline-flex;
|
|
245
286
|
gap: 0.25em;
|
|
246
287
|
}
|
|
247
288
|
|
|
248
|
-
[part~='typing'] .dots i
|
|
289
|
+
[part~='typing'] .dots i,
|
|
290
|
+
[part~='loading'] .dots i {
|
|
249
291
|
width: 0.4em;
|
|
250
292
|
height: 0.4em;
|
|
251
293
|
border-radius: 50%;
|
|
@@ -254,10 +296,12 @@ export const messageStyles = css`
|
|
|
254
296
|
animation: chit-typing 1.2s ease-in-out infinite;
|
|
255
297
|
}
|
|
256
298
|
|
|
257
|
-
[part~='typing'] .dots i:nth-child(2)
|
|
299
|
+
[part~='typing'] .dots i:nth-child(2),
|
|
300
|
+
[part~='loading'] .dots i:nth-child(2) {
|
|
258
301
|
animation-delay: 0.15s;
|
|
259
302
|
}
|
|
260
|
-
[part~='typing'] .dots i:nth-child(3)
|
|
303
|
+
[part~='typing'] .dots i:nth-child(3),
|
|
304
|
+
[part~='loading'] .dots i:nth-child(3) {
|
|
261
305
|
animation-delay: 0.3s;
|
|
262
306
|
}
|
|
263
307
|
|
|
@@ -299,7 +343,8 @@ export const messageStyles = css`
|
|
|
299
343
|
|
|
300
344
|
@media (prefers-reduced-motion: reduce) {
|
|
301
345
|
[part~='cursor'],
|
|
302
|
-
[part~='typing'] .dots i
|
|
346
|
+
[part~='typing'] .dots i,
|
|
347
|
+
[part~='loading'] .dots i {
|
|
303
348
|
animation: none;
|
|
304
349
|
}
|
|
305
350
|
}
|
package/src/styles/panel.css.js
CHANGED
|
@@ -8,8 +8,14 @@ export const panelStyles = css`
|
|
|
8
8
|
flex-direction: column;
|
|
9
9
|
width: var(--chit-panel-width);
|
|
10
10
|
height: var(--chit-panel-height);
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/*
|
|
12
|
+
* The room left between the panel's own corner and the far edge. Doubling
|
|
13
|
+
* the offset instead would assume the panel is centred between two equal
|
|
14
|
+
* gaps, which stops being true the moment it is dragged: moving it up
|
|
15
|
+
* would have made it shorter on the way.
|
|
16
|
+
*/
|
|
17
|
+
max-width: calc(100vw - var(--chit-panel-offset-x) - 8px);
|
|
18
|
+
max-height: calc(100vh - var(--chit-panel-offset-y) - 8px);
|
|
13
19
|
overflow: hidden;
|
|
14
20
|
border-radius: var(--chit-panel-radius);
|
|
15
21
|
background-color: var(--chit-color-bg);
|
|
@@ -50,12 +56,18 @@ export const panelStyles = css`
|
|
|
50
56
|
* A phone gets the whole screen. dvh rather than vh so the browser chrome
|
|
51
57
|
* collapsing does not leave a gap, and the safe-area insets keep the header
|
|
52
58
|
* clear of the notch.
|
|
59
|
+
*
|
|
60
|
+
* While a keyboard is up, the two --_chit-vv-* properties carry what is
|
|
61
|
+
* actually on screen (ViewportController fills them in from
|
|
62
|
+
* visualViewport); dvh knows nothing about a keyboard and would leave the
|
|
63
|
+
* composer behind it.
|
|
53
64
|
*/
|
|
54
65
|
:host([data-device='mobile']) [part~='panel'],
|
|
55
66
|
:host([data-device='mobile'][data-panel-position]) [part~='panel'] {
|
|
56
67
|
inset: 0;
|
|
68
|
+
top: var(--_chit-vv-top, 0px);
|
|
57
69
|
width: 100vw;
|
|
58
|
-
height: 100dvh;
|
|
70
|
+
height: var(--_chit-vv-height, 100dvh);
|
|
59
71
|
max-width: none;
|
|
60
72
|
max-height: none;
|
|
61
73
|
border-radius: 0;
|
|
@@ -82,6 +94,21 @@ export const panelStyles = css`
|
|
|
82
94
|
* and space-between pushes the image to one edge and the title to the
|
|
83
95
|
* middle, away from each other.
|
|
84
96
|
*/
|
|
97
|
+
[part~='header'][data-draggable] {
|
|
98
|
+
cursor: grab;
|
|
99
|
+
touch-action: none;
|
|
100
|
+
user-select: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[part~='header'][data-draggable]:active {
|
|
104
|
+
cursor: grabbing;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
[part~='header'][data-draggable]:focus-visible {
|
|
108
|
+
outline: 2px solid var(--chit-color-accent);
|
|
109
|
+
outline-offset: -2px;
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
[part~='header-title'] {
|
|
86
113
|
display: inline-flex;
|
|
87
114
|
min-width: 0;
|
|
@@ -23,6 +23,7 @@ const CLOSED_PC = {
|
|
|
23
23
|
position: 'bottom-right',
|
|
24
24
|
offset: { x: 24, y: 24 },
|
|
25
25
|
radius: 30,
|
|
26
|
+
draggable: false,
|
|
26
27
|
image: null,
|
|
27
28
|
label: null,
|
|
28
29
|
component: null,
|
|
@@ -72,6 +73,9 @@ export const defaultTheme = {
|
|
|
72
73
|
offset: { x: 24, y: 24 },
|
|
73
74
|
radius: 16,
|
|
74
75
|
launcher: /** @type {const} */ ('hidden'),
|
|
76
|
+
// Off by default: a widget that slides around under the thumb on a first
|
|
77
|
+
// touch would be startling, and most pages want it where they put it.
|
|
78
|
+
draggable: false,
|
|
75
79
|
header: { visible: true, title: null, logo: null, home: false },
|
|
76
80
|
background: { image: null },
|
|
77
81
|
colors: {
|
|
@@ -95,6 +99,18 @@ export const defaultTheme = {
|
|
|
95
99
|
// enough to show who is talking, and a tail is a strong look to impose on
|
|
96
100
|
// a widget that has to sit in someone else's page.
|
|
97
101
|
bubble: { radius: 14, tail: /** @type {const} */ ('none') },
|
|
102
|
+
// The wait for an answer. `auto` is opt-in: a widget that starts showing
|
|
103
|
+
// a wait on its own would be guessing that every submit is followed by a
|
|
104
|
+
// round trip, which is not true of a form or a scenario branch.
|
|
105
|
+
loading: {
|
|
106
|
+
auto: false,
|
|
107
|
+
// A spinner rather than the typing dots: the same three dots would say
|
|
108
|
+
// "someone is writing to you", which is not what a wait on a server is.
|
|
109
|
+
// `'dots'` is there for the cases where it really is a person.
|
|
110
|
+
style: /** @type {const} */ ('spinner'),
|
|
111
|
+
text: null,
|
|
112
|
+
timeout: 0,
|
|
113
|
+
},
|
|
98
114
|
// Who is talking. Nothing by default: a widget that invents a name and a
|
|
99
115
|
// face for the consumer's support desk would be guessing.
|
|
100
116
|
speaker: {
|
package/src/types.js
CHANGED
|
@@ -52,12 +52,25 @@
|
|
|
52
52
|
* @property {number} [radius]
|
|
53
53
|
* @property {string | null} [image] Icon image URL; null clears the default.
|
|
54
54
|
* @property {string | null} [label]
|
|
55
|
+
* @property {boolean} [draggable] Let the reader move it around the viewport.
|
|
55
56
|
* @property {(new () => HTMLElement) | string | null} [component] A component that draws the whole launcher.
|
|
56
57
|
* @property {Record<string, unknown>} [props] Properties written to that component.
|
|
57
58
|
* @property {{ background?: string, text?: string, shadow?: string }} [colors]
|
|
58
59
|
* @property {Animation & { idle?: 'none' | 'pulse' | 'bounce' }} [animation]
|
|
59
60
|
*/
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The wait between sending and the answer arriving.
|
|
64
|
+
*
|
|
65
|
+
* @typedef {'dots' | 'spinner' | 'text'} LoadingStyle
|
|
66
|
+
*
|
|
67
|
+
* @typedef {Object} LoadingTheme
|
|
68
|
+
* @property {boolean} [auto] Show it from `chat-submit` until the next message from the other side.
|
|
69
|
+
* @property {LoadingStyle} [style] What it looks like.
|
|
70
|
+
* @property {string | null} [text] Wording beside the animation, or on its own for `'text'`.
|
|
71
|
+
* @property {number} [timeout] ms after which it gives up on its own; 0 leaves it to the consumer.
|
|
72
|
+
*/
|
|
73
|
+
|
|
61
74
|
/**
|
|
62
75
|
* Who is talking, as the theme describes them. A message that carries its own
|
|
63
76
|
* `name` or `avatar` wins; `null` on the message hides what the theme set.
|
|
@@ -94,11 +107,13 @@
|
|
|
94
107
|
* @property {Offset} [offset]
|
|
95
108
|
* @property {number} [radius]
|
|
96
109
|
* @property {'hidden' | 'visible'} [launcher] Keep the launcher visible while open.
|
|
110
|
+
* @property {boolean} [draggable] Let the reader move the panel by its title bar.
|
|
97
111
|
* @property {{ visible?: boolean, title?: string | null, logo?: string | null, home?: boolean }} [header] The title bar: whether it is there at all, its text, its image, and whether it offers a way back to the start.
|
|
98
112
|
* @property {{ image?: string | null }} [background]
|
|
99
113
|
* @property {OpenColors} [colors]
|
|
100
114
|
* @property {{ radius?: number, tail?: BubbleTail }} [bubble] Bubble corner radius, and where the tail points from.
|
|
101
115
|
* @property {{ assistant?: SpeakerTheme, user?: SpeakerTheme }} [speaker] Default name and icon per side.
|
|
116
|
+
* @property {LoadingTheme} [loading] The wait for an answer.
|
|
102
117
|
* @property {Animation & { scroll?: 'smooth' | 'instant' }} [animation]
|
|
103
118
|
* @property {{ maxRows?: number, placeholder?: string | null, attach?: boolean, accept?: string, multiple?: boolean }} [input] Composer: rows, placeholder, and the attach button.
|
|
104
119
|
*/
|
|
@@ -122,6 +137,7 @@
|
|
|
122
137
|
* @property {Position} position
|
|
123
138
|
* @property {{ x: number, y: number }} offset
|
|
124
139
|
* @property {number} radius
|
|
140
|
+
* @property {boolean} draggable
|
|
125
141
|
* @property {string | null} image
|
|
126
142
|
* @property {string | null} label
|
|
127
143
|
* @property {(new () => HTMLElement) | string | null} component
|
|
@@ -136,11 +152,13 @@
|
|
|
136
152
|
* @property {{ x: number, y: number }} offset
|
|
137
153
|
* @property {number} radius
|
|
138
154
|
* @property {'hidden' | 'visible'} launcher
|
|
155
|
+
* @property {boolean} draggable
|
|
139
156
|
* @property {{ visible: boolean, title: string | null, logo: string | null, home: boolean }} header
|
|
140
157
|
* @property {{ image: string | null }} background
|
|
141
158
|
* @property {Required<OpenColors>} colors
|
|
142
159
|
* @property {{ radius: number, tail: BubbleTail }} bubble
|
|
143
160
|
* @property {{ assistant: Required<SpeakerTheme>, user: Required<SpeakerTheme> }} speaker
|
|
161
|
+
* @property {Required<LoadingTheme>} loading
|
|
144
162
|
* @property {{ enter: Effect, exit: Effect, duration: number, scroll: 'smooth' | 'instant' }} animation
|
|
145
163
|
* @property {{ maxRows: number, placeholder: string | null, attach: boolean, accept: string, multiple: boolean }} input
|
|
146
164
|
*
|