@hidemikimura/chit-ui 0.1.0 → 0.3.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 +91 -0
- package/README.md +314 -11
- package/dist/chit-ui.iife.min.js +416 -94
- package/dist/chit-ui.iife.min.js.map +1 -1
- package/dist/chit-ui.min.js +407 -85
- package/dist/chit-ui.min.js.map +1 -1
- package/dist/types/bundle.d.ts +1 -0
- package/dist/types/chit-ui.d.ts +89 -4
- package/dist/types/controllers/composer-controller.d.ts +4 -0
- package/dist/types/controllers/drag-controller.d.ts +118 -0
- package/dist/types/events.d.ts +3 -0
- package/dist/types/global.d.ts +9 -1
- package/dist/types/i18n/labels.d.ts +16 -0
- package/dist/types/render/composer.d.ts +0 -1
- package/dist/types/render/message-list.d.ts +0 -1
- package/dist/types/theme/default-theme.d.ts +30 -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 +105 -4
- package/package.json +6 -1
- package/src/bundle.js +1 -0
- package/src/chit-ui.js +292 -4
- package/src/controllers/composer-controller.js +2 -0
- package/src/controllers/drag-controller.js +319 -0
- package/src/events.js +3 -0
- package/src/global.d.ts +9 -1
- package/src/i18n/labels.js +12 -0
- package/src/render/composer.js +58 -0
- package/src/render/launcher.js +9 -1
- package/src/render/message-list.js +40 -10
- package/src/render/message.js +13 -4
- package/src/render/panel.js +73 -11
- package/src/styles/composer.css.js +39 -0
- package/src/styles/host.css.js +2 -0
- package/src/styles/launcher.css.js +13 -0
- package/src/styles/message.css.js +168 -9
- package/src/styles/panel.css.js +44 -4
- package/src/theme/default-theme.js +37 -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 +38 -6
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);
|
|
@@ -76,18 +82,48 @@ export const panelStyles = css`
|
|
|
76
82
|
color: var(--chit-color-header-text);
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
/*
|
|
86
|
+
* The image and the title are one flex item. A slot is display: contents,
|
|
87
|
+
* so without this wrapper its children become items of the header itself
|
|
88
|
+
* and space-between pushes the image to one edge and the title to the
|
|
89
|
+
* middle, away from each other.
|
|
90
|
+
*/
|
|
91
|
+
[part~='header'][data-draggable] {
|
|
92
|
+
cursor: grab;
|
|
93
|
+
touch-action: none;
|
|
94
|
+
user-select: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
[part~='header'][data-draggable]:active {
|
|
98
|
+
cursor: grabbing;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
[part~='header'][data-draggable]:focus-visible {
|
|
102
|
+
outline: 2px solid var(--chit-color-accent);
|
|
103
|
+
outline-offset: -2px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
[part~='header-title'] {
|
|
107
|
+
display: inline-flex;
|
|
108
|
+
min-width: 0;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: 0.5em;
|
|
111
|
+
}
|
|
112
|
+
|
|
79
113
|
[part~='header-heading'] {
|
|
114
|
+
overflow: hidden;
|
|
80
115
|
font-size: 1.05em;
|
|
81
116
|
font-weight: 600;
|
|
117
|
+
text-overflow: ellipsis;
|
|
118
|
+
white-space: nowrap;
|
|
82
119
|
}
|
|
83
120
|
|
|
84
121
|
[part~='header-logo'] {
|
|
122
|
+
flex: none;
|
|
85
123
|
width: 1.75em;
|
|
86
124
|
height: 1.75em;
|
|
87
|
-
margin-inline-end: 0.5em;
|
|
88
125
|
border-radius: 50%;
|
|
89
126
|
object-fit: cover;
|
|
90
|
-
vertical-align: middle;
|
|
91
127
|
}
|
|
92
128
|
|
|
93
129
|
[part~='header-actions'] {
|
|
@@ -96,6 +132,7 @@ export const panelStyles = css`
|
|
|
96
132
|
gap: 0.25em;
|
|
97
133
|
}
|
|
98
134
|
|
|
135
|
+
[part~='home-button'],
|
|
99
136
|
[part~='close-button'] {
|
|
100
137
|
display: grid;
|
|
101
138
|
place-items: center;
|
|
@@ -109,15 +146,18 @@ export const panelStyles = css`
|
|
|
109
146
|
cursor: pointer;
|
|
110
147
|
}
|
|
111
148
|
|
|
149
|
+
[part~='home-button']:hover,
|
|
112
150
|
[part~='close-button']:hover {
|
|
113
151
|
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
114
152
|
}
|
|
115
153
|
|
|
154
|
+
[part~='home-button']:focus-visible,
|
|
116
155
|
[part~='close-button']:focus-visible {
|
|
117
156
|
outline: 2px solid var(--chit-color-accent);
|
|
118
157
|
outline-offset: 1px;
|
|
119
158
|
}
|
|
120
159
|
|
|
160
|
+
[part~='home-button'] svg,
|
|
121
161
|
[part~='close-button'] svg {
|
|
122
162
|
width: 1.1em;
|
|
123
163
|
height: 1.1em;
|
|
@@ -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,7 +73,10 @@ export const defaultTheme = {
|
|
|
72
73
|
offset: { x: 24, y: 24 },
|
|
73
74
|
radius: 16,
|
|
74
75
|
launcher: /** @type {const} */ ('hidden'),
|
|
75
|
-
|
|
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,
|
|
79
|
+
header: { visible: true, title: null, logo: null, home: false },
|
|
76
80
|
background: { image: null },
|
|
77
81
|
colors: {
|
|
78
82
|
background: PALETTE.surface,
|
|
@@ -91,6 +95,28 @@ export const defaultTheme = {
|
|
|
91
95
|
inputText: PALETTE.ink,
|
|
92
96
|
inputPlaceholder: PALETTE.muted,
|
|
93
97
|
},
|
|
98
|
+
// No tail by default: the squared-off corner on the speaker's side is
|
|
99
|
+
// enough to show who is talking, and a tail is a strong look to impose on
|
|
100
|
+
// a widget that has to sit in someone else's page.
|
|
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
|
+
},
|
|
114
|
+
// Who is talking. Nothing by default: a widget that invents a name and a
|
|
115
|
+
// face for the consumer's support desk would be guessing.
|
|
116
|
+
speaker: {
|
|
117
|
+
assistant: { name: null, avatar: null },
|
|
118
|
+
user: { name: null, avatar: null },
|
|
119
|
+
},
|
|
94
120
|
animation: {
|
|
95
121
|
enter: /** @type {const} */ ('scale'),
|
|
96
122
|
exit: /** @type {const} */ ('fade'),
|
|
@@ -100,7 +126,16 @@ export const defaultTheme = {
|
|
|
100
126
|
// ScrollController for why.
|
|
101
127
|
scroll: /** @type {const} */ ('smooth'),
|
|
102
128
|
},
|
|
103
|
-
|
|
129
|
+
// `attach` is off by default: a widget that offers to take files when
|
|
130
|
+
// nothing is listening for them would be promising something it cannot
|
|
131
|
+
// keep. `accept` and `multiple` go straight to the file input.
|
|
132
|
+
input: {
|
|
133
|
+
maxRows: 5,
|
|
134
|
+
placeholder: null,
|
|
135
|
+
attach: false,
|
|
136
|
+
accept: 'image/*,video/*',
|
|
137
|
+
multiple: false,
|
|
138
|
+
},
|
|
104
139
|
},
|
|
105
140
|
hidden: {
|
|
106
141
|
animation: { exit: /** @type {const} */ ('fade'), duration: 150 },
|
|
@@ -76,7 +76,12 @@ export function themeToVariables(theme) {
|
|
|
76
76
|
'--chit-color-input-text': c.inputText,
|
|
77
77
|
'--chit-color-input-placeholder': c.inputPlaceholder,
|
|
78
78
|
|
|
79
|
+
'--chit-bubble-radius': px(open.bubble.radius),
|
|
80
|
+
|
|
79
81
|
'--_chit-input-max-rows': String(open.input.maxRows),
|
|
82
|
+
// Internal: the width the speaker's icon takes up, so the typing
|
|
83
|
+
// indicator lines up with the bubbles it belongs to.
|
|
84
|
+
'--_chit-speaker-gutter': open.speaker.assistant.avatar ? '2.5em' : '0px',
|
|
80
85
|
};
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @import { Theme } from '../types.js' */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A green messenger look: a pale blue-grey conversation background, white
|
|
7
|
+
* bubbles for them, bright green for you, tails on the top corner.
|
|
8
|
+
*
|
|
9
|
+
* This is Chit UI's own palette in that familiar shape. It carries no other
|
|
10
|
+
* product's logo, wordmark or artwork, and the greens are Chit UI's own: the
|
|
11
|
+
* deep green is dark enough that white text and the send icon clear WCAG AA
|
|
12
|
+
* on it, which the bright green of the bubbles could not do.
|
|
13
|
+
* `test/theme.test.js` re-checks every pair here, so a later tweak cannot
|
|
14
|
+
* quietly drop below AA.
|
|
15
|
+
*
|
|
16
|
+
* @type {Theme}
|
|
17
|
+
*/
|
|
18
|
+
export const greenTheme = {
|
|
19
|
+
closed: {
|
|
20
|
+
colors: {
|
|
21
|
+
background: '#0a6b36',
|
|
22
|
+
text: '#ffffff',
|
|
23
|
+
shadow: '0 4px 12px rgba(16, 42, 26, 0.32)',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
open: {
|
|
27
|
+
radius: 12,
|
|
28
|
+
colors: {
|
|
29
|
+
background: '#d3ddea',
|
|
30
|
+
text: '#16212c',
|
|
31
|
+
accent: '#0a6b36',
|
|
32
|
+
border: '#b6c5d6',
|
|
33
|
+
shadow: '0 8px 32px rgba(16, 32, 48, 0.28)',
|
|
34
|
+
headerBackground: '#ffffff',
|
|
35
|
+
headerText: '#16212c',
|
|
36
|
+
userBubble: '#9ce26c',
|
|
37
|
+
userText: '#14240c',
|
|
38
|
+
assistantBubble: '#ffffff',
|
|
39
|
+
assistantText: '#16212c',
|
|
40
|
+
systemText: '#37475a',
|
|
41
|
+
inputBackground: '#ffffff',
|
|
42
|
+
inputText: '#16212c',
|
|
43
|
+
inputPlaceholder: '#5c6370',
|
|
44
|
+
},
|
|
45
|
+
bubble: { radius: 18, tail: 'top' },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Themes the library ships with.
|
|
5
|
+
*
|
|
6
|
+
* A preset is an ordinary `Theme` object, so it is assigned like any other
|
|
7
|
+
* theme and customised by spreading over it:
|
|
8
|
+
*
|
|
9
|
+
* import { greenTheme } from '@hidemikimura/chit-ui/themes.js';
|
|
10
|
+
* el.theme = { ...greenTheme, open: { ...greenTheme.open, width: 420 } };
|
|
11
|
+
*
|
|
12
|
+
* Importing this module does not register the element or pull the widget in;
|
|
13
|
+
* it is data only.
|
|
14
|
+
*/
|
|
15
|
+
export { greenTheme } from './green.js';
|
package/src/types.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @typedef {'pc' | 'mobile'} Device
|
|
12
12
|
* @typedef {'fade' | 'scale' | 'slide' | 'none'} Effect
|
|
13
13
|
* @typedef {'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'} Position
|
|
14
|
+
* @typedef {'none' | 'top' | 'bottom'} BubbleTail
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -18,8 +19,8 @@
|
|
|
18
19
|
* @property {string} id Unique within the array; used as the diffing key.
|
|
19
20
|
* @property {Role} role
|
|
20
21
|
* @property {string | Date} [time]
|
|
21
|
-
* @property {string} [name]
|
|
22
|
-
* @property {string} [avatar]
|
|
22
|
+
* @property {string | null} [name] Overrides the theme's speaker name; null hides it.
|
|
23
|
+
* @property {string | null} [avatar] Overrides the theme's speaker image; null hides it.
|
|
23
24
|
* @property {MessageStatus} [status]
|
|
24
25
|
* @property {boolean} [streaming]
|
|
25
26
|
* @property {unknown} [meta] Never touched by the library.
|
|
@@ -51,12 +52,34 @@
|
|
|
51
52
|
* @property {number} [radius]
|
|
52
53
|
* @property {string | null} [image] Icon image URL; null clears the default.
|
|
53
54
|
* @property {string | null} [label]
|
|
55
|
+
* @property {boolean} [draggable] Let the reader move it around the viewport.
|
|
54
56
|
* @property {(new () => HTMLElement) | string | null} [component] A component that draws the whole launcher.
|
|
55
57
|
* @property {Record<string, unknown>} [props] Properties written to that component.
|
|
56
58
|
* @property {{ background?: string, text?: string, shadow?: string }} [colors]
|
|
57
59
|
* @property {Animation & { idle?: 'none' | 'pulse' | 'bounce' }} [animation]
|
|
58
60
|
*/
|
|
59
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
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Who is talking, as the theme describes them. A message that carries its own
|
|
76
|
+
* `name` or `avatar` wins; `null` on the message hides what the theme set.
|
|
77
|
+
*
|
|
78
|
+
* @typedef {Object} SpeakerTheme
|
|
79
|
+
* @property {string | null} [name] Shown above the bubble.
|
|
80
|
+
* @property {string | null} [avatar] Image URL for the speaker's icon.
|
|
81
|
+
*/
|
|
82
|
+
|
|
60
83
|
/**
|
|
61
84
|
* @typedef {Object} OpenColors
|
|
62
85
|
* @property {string} [background]
|
|
@@ -84,11 +107,15 @@
|
|
|
84
107
|
* @property {Offset} [offset]
|
|
85
108
|
* @property {number} [radius]
|
|
86
109
|
* @property {'hidden' | 'visible'} [launcher] Keep the launcher visible while open.
|
|
87
|
-
* @property {
|
|
110
|
+
* @property {boolean} [draggable] Let the reader move the panel by its title bar.
|
|
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.
|
|
88
112
|
* @property {{ image?: string | null }} [background]
|
|
89
113
|
* @property {OpenColors} [colors]
|
|
114
|
+
* @property {{ radius?: number, tail?: BubbleTail }} [bubble] Bubble corner radius, and where the tail points from.
|
|
115
|
+
* @property {{ assistant?: SpeakerTheme, user?: SpeakerTheme }} [speaker] Default name and icon per side.
|
|
116
|
+
* @property {LoadingTheme} [loading] The wait for an answer.
|
|
90
117
|
* @property {Animation & { scroll?: 'smooth' | 'instant' }} [animation]
|
|
91
|
-
* @property {{ maxRows?: number, placeholder?: string | null }} [input]
|
|
118
|
+
* @property {{ maxRows?: number, placeholder?: string | null, attach?: boolean, accept?: string, multiple?: boolean }} [input] Composer: rows, placeholder, and the attach button.
|
|
92
119
|
*/
|
|
93
120
|
|
|
94
121
|
/**
|
|
@@ -110,6 +137,7 @@
|
|
|
110
137
|
* @property {Position} position
|
|
111
138
|
* @property {{ x: number, y: number }} offset
|
|
112
139
|
* @property {number} radius
|
|
140
|
+
* @property {boolean} draggable
|
|
113
141
|
* @property {string | null} image
|
|
114
142
|
* @property {string | null} label
|
|
115
143
|
* @property {(new () => HTMLElement) | string | null} component
|
|
@@ -124,11 +152,15 @@
|
|
|
124
152
|
* @property {{ x: number, y: number }} offset
|
|
125
153
|
* @property {number} radius
|
|
126
154
|
* @property {'hidden' | 'visible'} launcher
|
|
127
|
-
* @property {
|
|
155
|
+
* @property {boolean} draggable
|
|
156
|
+
* @property {{ visible: boolean, title: string | null, logo: string | null, home: boolean }} header
|
|
128
157
|
* @property {{ image: string | null }} background
|
|
129
158
|
* @property {Required<OpenColors>} colors
|
|
159
|
+
* @property {{ radius: number, tail: BubbleTail }} bubble
|
|
160
|
+
* @property {{ assistant: Required<SpeakerTheme>, user: Required<SpeakerTheme> }} speaker
|
|
161
|
+
* @property {Required<LoadingTheme>} loading
|
|
130
162
|
* @property {{ enter: Effect, exit: Effect, duration: number, scroll: 'smooth' | 'instant' }} animation
|
|
131
|
-
* @property {{ maxRows: number, placeholder: string | null }} input
|
|
163
|
+
* @property {{ maxRows: number, placeholder: string | null, attach: boolean, accept: string, multiple: boolean }} input
|
|
132
164
|
*
|
|
133
165
|
* @typedef {Object} ResolvedTheme
|
|
134
166
|
* @property {number} breakpoint
|