@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.
- package/LICENSE +21 -0
- package/README.md +440 -0
- package/dist/chit-ui.iife.min.js +964 -0
- package/dist/chit-ui.iife.min.js.map +1 -0
- package/dist/chit-ui.min.js +964 -0
- package/dist/chit-ui.min.js.map +1 -0
- package/dist/types/bundle.d.ts +7 -0
- package/dist/types/chit-ui.d.ts +251 -0
- package/dist/types/controllers/breakpoint-controller.d.ts +29 -0
- package/dist/types/controllers/composer-controller.d.ts +54 -0
- package/dist/types/controllers/scroll-controller.d.ts +44 -0
- package/dist/types/controllers/state-controller.d.ts +61 -0
- package/dist/types/controllers/theme-controller.d.ts +47 -0
- package/dist/types/element.d.ts +1 -0
- package/dist/types/events.d.ts +28 -0
- package/dist/types/global.d.ts +25 -0
- package/dist/types/i18n/labels.d.ts +68 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/render/composer.d.ts +15 -0
- package/dist/types/render/content.d.ts +75 -0
- package/dist/types/render/launcher.d.ts +18 -0
- package/dist/types/render/message-list.d.ts +17 -0
- package/dist/types/render/message.d.ts +14 -0
- package/dist/types/render/panel.d.ts +10 -0
- package/dist/types/styles/adopted-sheet.d.ts +20 -0
- package/dist/types/styles/composer.css.d.ts +1 -0
- package/dist/types/styles/content.css.d.ts +9 -0
- package/dist/types/styles/host.css.d.ts +9 -0
- package/dist/types/styles/launcher.css.d.ts +1 -0
- package/dist/types/styles/message.css.d.ts +1 -0
- package/dist/types/styles/panel.css.d.ts +1 -0
- package/dist/types/theme/default-theme.d.ts +97 -0
- package/dist/types/theme/merge-theme.d.ts +33 -0
- package/dist/types/theme/theme-to-css.d.ts +24 -0
- package/dist/types/types.d.ts +268 -0
- package/package.json +71 -0
- package/src/bundle.js +11 -0
- package/src/chit-ui.js +548 -0
- package/src/controllers/breakpoint-controller.js +82 -0
- package/src/controllers/composer-controller.js +215 -0
- package/src/controllers/scroll-controller.js +252 -0
- package/src/controllers/state-controller.js +316 -0
- package/src/controllers/theme-controller.js +75 -0
- package/src/element.js +4 -0
- package/src/events.js +33 -0
- package/src/global.d.ts +25 -0
- package/src/i18n/labels.js +72 -0
- package/src/index.js +9 -0
- package/src/render/composer.js +72 -0
- package/src/render/content.js +211 -0
- package/src/render/launcher.js +64 -0
- package/src/render/message-list.js +67 -0
- package/src/render/message.js +82 -0
- package/src/render/panel.js +77 -0
- package/src/styles/adopted-sheet.js +72 -0
- package/src/styles/composer.css.js +108 -0
- package/src/styles/content.css.js +119 -0
- package/src/styles/host.css.js +160 -0
- package/src/styles/launcher.css.js +139 -0
- package/src/styles/message.css.js +192 -0
- package/src/styles/panel.css.js +126 -0
- package/src/theme/default-theme.js +111 -0
- package/src/theme/merge-theme.js +100 -0
- package/src/theme/theme-to-css.js +94 -0
- package/src/types.js +143 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { css } from 'lit';
|
|
3
|
+
|
|
4
|
+
export const panelStyles = css`
|
|
5
|
+
[part~='panel'] {
|
|
6
|
+
position: absolute;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
width: var(--chit-panel-width);
|
|
10
|
+
height: var(--chit-panel-height);
|
|
11
|
+
max-width: calc(100vw - var(--chit-panel-offset-x) * 2);
|
|
12
|
+
max-height: calc(100vh - var(--chit-panel-offset-y) * 2);
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
border-radius: var(--chit-panel-radius);
|
|
15
|
+
background-color: var(--chit-color-bg);
|
|
16
|
+
background-image: var(--chit-panel-bg-image);
|
|
17
|
+
background-size: cover;
|
|
18
|
+
background-position: center;
|
|
19
|
+
box-shadow: var(--chit-panel-shadow);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* --- placement -------------------------------------------------------- */
|
|
23
|
+
|
|
24
|
+
:host([data-panel-position='bottom-right']) [part~='panel'] {
|
|
25
|
+
right: var(--chit-panel-offset-x);
|
|
26
|
+
bottom: var(--chit-panel-offset-y);
|
|
27
|
+
transform-origin: bottom right;
|
|
28
|
+
}
|
|
29
|
+
:host([data-panel-position='bottom-left']) [part~='panel'] {
|
|
30
|
+
left: var(--chit-panel-offset-x);
|
|
31
|
+
bottom: var(--chit-panel-offset-y);
|
|
32
|
+
transform-origin: bottom left;
|
|
33
|
+
}
|
|
34
|
+
:host([data-panel-position='top-right']) [part~='panel'] {
|
|
35
|
+
right: var(--chit-panel-offset-x);
|
|
36
|
+
top: var(--chit-panel-offset-y);
|
|
37
|
+
transform-origin: top right;
|
|
38
|
+
}
|
|
39
|
+
:host([data-panel-position='top-left']) [part~='panel'] {
|
|
40
|
+
left: var(--chit-panel-offset-x);
|
|
41
|
+
top: var(--chit-panel-offset-y);
|
|
42
|
+
transform-origin: top left;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[part~='panel']:focus {
|
|
46
|
+
outline: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* A phone gets the whole screen. dvh rather than vh so the browser chrome
|
|
51
|
+
* collapsing does not leave a gap, and the safe-area insets keep the header
|
|
52
|
+
* clear of the notch.
|
|
53
|
+
*/
|
|
54
|
+
:host([data-device='mobile']) [part~='panel'],
|
|
55
|
+
:host([data-device='mobile'][data-panel-position]) [part~='panel'] {
|
|
56
|
+
inset: 0;
|
|
57
|
+
width: 100vw;
|
|
58
|
+
height: 100dvh;
|
|
59
|
+
max-width: none;
|
|
60
|
+
max-height: none;
|
|
61
|
+
border-radius: 0;
|
|
62
|
+
padding-top: env(safe-area-inset-top);
|
|
63
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
64
|
+
overscroll-behavior: contain;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[part~='header'] {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex: none;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: space-between;
|
|
72
|
+
gap: 0.5em;
|
|
73
|
+
padding: 0.875em 1em;
|
|
74
|
+
border-bottom: 1px solid var(--chit-color-border);
|
|
75
|
+
background: var(--chit-color-header-bg);
|
|
76
|
+
color: var(--chit-color-header-text);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[part~='header-heading'] {
|
|
80
|
+
font-size: 1.05em;
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[part~='header-logo'] {
|
|
85
|
+
width: 1.75em;
|
|
86
|
+
height: 1.75em;
|
|
87
|
+
margin-inline-end: 0.5em;
|
|
88
|
+
border-radius: 50%;
|
|
89
|
+
object-fit: cover;
|
|
90
|
+
vertical-align: middle;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
[part~='header-actions'] {
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: 0.25em;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[part~='close-button'] {
|
|
100
|
+
display: grid;
|
|
101
|
+
place-items: center;
|
|
102
|
+
width: 2em;
|
|
103
|
+
height: 2em;
|
|
104
|
+
padding: 0;
|
|
105
|
+
border: 0;
|
|
106
|
+
border-radius: 50%;
|
|
107
|
+
background: transparent;
|
|
108
|
+
color: inherit;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
[part~='close-button']:hover {
|
|
113
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
[part~='close-button']:focus-visible {
|
|
117
|
+
outline: 2px solid var(--chit-color-accent);
|
|
118
|
+
outline-offset: 1px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
[part~='close-button'] svg {
|
|
122
|
+
width: 1.1em;
|
|
123
|
+
height: 1.1em;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
`;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @import { ResolvedClosed } from '../types.js' */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Neutral light palette. Every text/background pair here clears WCAG AA
|
|
7
|
+
* (4.5:1); `test/theme.test.js` re-checks the ratios so a future tweak cannot
|
|
8
|
+
* quietly drop below it.
|
|
9
|
+
*/
|
|
10
|
+
const PALETTE = {
|
|
11
|
+
accent: '#2563eb',
|
|
12
|
+
ink: '#1f2328',
|
|
13
|
+
muted: '#5c6370',
|
|
14
|
+
surface: '#ffffff',
|
|
15
|
+
raised: '#f1f3f5',
|
|
16
|
+
line: '#e4e4e7',
|
|
17
|
+
onAccent: '#ffffff',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** @type {ResolvedClosed} */
|
|
21
|
+
const CLOSED_PC = {
|
|
22
|
+
size: 60,
|
|
23
|
+
position: 'bottom-right',
|
|
24
|
+
offset: { x: 24, y: 24 },
|
|
25
|
+
radius: 30,
|
|
26
|
+
image: null,
|
|
27
|
+
label: null,
|
|
28
|
+
component: null,
|
|
29
|
+
props: {},
|
|
30
|
+
colors: {
|
|
31
|
+
background: PALETTE.accent,
|
|
32
|
+
text: PALETTE.onAccent,
|
|
33
|
+
shadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
|
|
34
|
+
},
|
|
35
|
+
animation: { enter: 'scale', exit: 'scale', duration: 200, idle: 'none' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What a phone gets when the theme does not say. Only the measurements differ:
|
|
40
|
+
* a thumb needs a slightly smaller target closer to the screen edge.
|
|
41
|
+
*
|
|
42
|
+
* @type {Partial<ResolvedClosed>}
|
|
43
|
+
*/
|
|
44
|
+
const CLOSED_MOBILE_DIFF = {
|
|
45
|
+
size: 56,
|
|
46
|
+
offset: { x: 16, y: 16 },
|
|
47
|
+
radius: 28,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The library's own theme. A user theme is merged over this, so everything
|
|
52
|
+
* here is what you get when the user says nothing at all.
|
|
53
|
+
*
|
|
54
|
+
* `closed` carries both device buckets; `mergeTheme` narrows it to one.
|
|
55
|
+
*/
|
|
56
|
+
export const defaultTheme = {
|
|
57
|
+
breakpoint: 768,
|
|
58
|
+
zIndex: 2147483000,
|
|
59
|
+
font: {
|
|
60
|
+
family:
|
|
61
|
+
"system-ui, -apple-system, 'Segoe UI', 'Hiragino Sans', 'Noto Sans JP', sans-serif",
|
|
62
|
+
size: 14,
|
|
63
|
+
},
|
|
64
|
+
closed: {
|
|
65
|
+
pc: CLOSED_PC,
|
|
66
|
+
mobile: /** @type {ResolvedClosed} */ ({ ...CLOSED_PC, ...CLOSED_MOBILE_DIFF }),
|
|
67
|
+
},
|
|
68
|
+
open: {
|
|
69
|
+
width: 380,
|
|
70
|
+
height: 600,
|
|
71
|
+
position: /** @type {const} */ ('bottom-right'),
|
|
72
|
+
offset: { x: 24, y: 24 },
|
|
73
|
+
radius: 16,
|
|
74
|
+
launcher: /** @type {const} */ ('hidden'),
|
|
75
|
+
header: { title: null, logo: null, avatar: null },
|
|
76
|
+
background: { image: null },
|
|
77
|
+
colors: {
|
|
78
|
+
background: PALETTE.surface,
|
|
79
|
+
text: PALETTE.ink,
|
|
80
|
+
accent: PALETTE.accent,
|
|
81
|
+
border: PALETTE.line,
|
|
82
|
+
shadow: '0 8px 32px rgba(0, 0, 0, 0.24)',
|
|
83
|
+
headerBackground: PALETTE.surface,
|
|
84
|
+
headerText: PALETTE.ink,
|
|
85
|
+
userBubble: PALETTE.accent,
|
|
86
|
+
userText: PALETTE.onAccent,
|
|
87
|
+
assistantBubble: PALETTE.raised,
|
|
88
|
+
assistantText: PALETTE.ink,
|
|
89
|
+
systemText: PALETTE.muted,
|
|
90
|
+
inputBackground: PALETTE.surface,
|
|
91
|
+
inputText: PALETTE.ink,
|
|
92
|
+
inputPlaceholder: PALETTE.muted,
|
|
93
|
+
},
|
|
94
|
+
animation: {
|
|
95
|
+
enter: /** @type {const} */ ('scale'),
|
|
96
|
+
exit: /** @type {const} */ ('fade'),
|
|
97
|
+
duration: 150,
|
|
98
|
+
// How the conversation follows a new message. Growth within a message
|
|
99
|
+
// (a streamed reply, an image loading) is always instant; see
|
|
100
|
+
// ScrollController for why.
|
|
101
|
+
scroll: /** @type {const} */ ('smooth'),
|
|
102
|
+
},
|
|
103
|
+
input: { maxRows: 5, placeholder: null },
|
|
104
|
+
},
|
|
105
|
+
hidden: {
|
|
106
|
+
animation: { exit: /** @type {const} */ ('fade'), duration: 150 },
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** The measurements a phone falls back to when only `closed.pc` was given. */
|
|
111
|
+
export const mobileClosedDefaults = CLOSED_MOBILE_DIFF;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { defaultTheme, mobileClosedDefaults } from './default-theme.js';
|
|
3
|
+
|
|
4
|
+
/** @import { Theme, ClosedTheme, Device, ResolvedTheme } from '../types.js' */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {value is Record<string, unknown>}
|
|
9
|
+
*/
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Merge `override` onto `base`, recursing into plain objects.
|
|
16
|
+
*
|
|
17
|
+
* `undefined` means "say nothing" and leaves the base value alone; `null` means
|
|
18
|
+
* "clear it", so `{ image: null }` removes a default icon rather than being
|
|
19
|
+
* ignored. There are no arrays anywhere in a theme, so a plain recursive merge
|
|
20
|
+
* is enough.
|
|
21
|
+
*
|
|
22
|
+
* @template {Record<string, any>} T
|
|
23
|
+
* @param {T} base
|
|
24
|
+
* @param {Record<string, any> | undefined} override
|
|
25
|
+
* @returns {T}
|
|
26
|
+
*/
|
|
27
|
+
export function mergeTheme(base, override) {
|
|
28
|
+
if (!isPlainObject(override)) return base;
|
|
29
|
+
|
|
30
|
+
/** @type {Record<string, any>} */
|
|
31
|
+
const out = { ...base };
|
|
32
|
+
for (const [key, value] of Object.entries(override)) {
|
|
33
|
+
if (value === undefined) continue;
|
|
34
|
+
const current = out[key];
|
|
35
|
+
out[key] = isPlainObject(value) && isPlainObject(current) ? mergeTheme(current, value) : value;
|
|
36
|
+
}
|
|
37
|
+
return /** @type {T} */ (out);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Decide which `closed` settings this device gets.
|
|
42
|
+
*
|
|
43
|
+
* Three shapes are allowed, and this is where they collapse into one:
|
|
44
|
+
* no pc/mobile keys at all (the same settings serve both), both given, or only
|
|
45
|
+
* one given. When only `pc` is given the phone inherits it but keeps the
|
|
46
|
+
* default phone measurements, so a custom icon carries over without a
|
|
47
|
+
* desktop-sized button landing on a phone.
|
|
48
|
+
*
|
|
49
|
+
* @param {ClosedTheme | { pc?: ClosedTheme, mobile?: ClosedTheme } | undefined} closed
|
|
50
|
+
* @param {Device} device
|
|
51
|
+
* @returns {ClosedTheme}
|
|
52
|
+
*/
|
|
53
|
+
function resolveClosed(closed, device) {
|
|
54
|
+
const perDevice = /** @type {{ pc?: ClosedTheme, mobile?: ClosedTheme }} */ (closed ?? {});
|
|
55
|
+
const split = 'pc' in perDevice || 'mobile' in perDevice;
|
|
56
|
+
|
|
57
|
+
if (!split) {
|
|
58
|
+
const shared = /** @type {ClosedTheme} */ (closed ?? {});
|
|
59
|
+
return mergeTheme(defaultTheme.closed[device], shared);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pc = mergeTheme(defaultTheme.closed.pc, perDevice.pc);
|
|
63
|
+
if (device === 'pc') return pc;
|
|
64
|
+
|
|
65
|
+
if (perDevice.mobile) return mergeTheme(defaultTheme.closed.mobile, perDevice.mobile);
|
|
66
|
+
return mergeTheme(pc, mobileClosedDefaults);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fill every gap in a user theme and narrow it to one device.
|
|
71
|
+
*
|
|
72
|
+
* @param {Theme | undefined} theme
|
|
73
|
+
* @param {Device} device
|
|
74
|
+
* @returns {ResolvedTheme}
|
|
75
|
+
*/
|
|
76
|
+
export function resolveTheme(theme, device) {
|
|
77
|
+
const user = theme ?? {};
|
|
78
|
+
const closed = resolveClosed(user.closed, device);
|
|
79
|
+
|
|
80
|
+
return /** @type {ResolvedTheme} */ ({
|
|
81
|
+
breakpoint: user.breakpoint ?? defaultTheme.breakpoint,
|
|
82
|
+
zIndex: user.zIndex ?? defaultTheme.zIndex,
|
|
83
|
+
font: mergeTheme(defaultTheme.font, user.font),
|
|
84
|
+
closed: mergeTheme(defaultTheme.closed[device], closed),
|
|
85
|
+
open: mergeTheme(defaultTheme.open, user.open),
|
|
86
|
+
hidden: mergeTheme(defaultTheme.hidden, user.hidden),
|
|
87
|
+
device,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The breakpoint a theme asks for, readable before the full resolve (the
|
|
93
|
+
* device is not known until the breakpoint is).
|
|
94
|
+
*
|
|
95
|
+
* @param {Theme | undefined} theme
|
|
96
|
+
* @returns {number}
|
|
97
|
+
*/
|
|
98
|
+
export function breakpointOf(theme) {
|
|
99
|
+
return theme?.breakpoint ?? defaultTheme.breakpoint;
|
|
100
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @import { ResolvedTheme } from '../types.js' */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {number} value
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
const px = (value) => `${value}px`;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A CSS `url()` for an image, or `none`. Quotes and backslashes are escaped so
|
|
13
|
+
* a filename with an apostrophe cannot break out of the declaration.
|
|
14
|
+
*
|
|
15
|
+
* @param {string | null | undefined} value
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function url(value) {
|
|
19
|
+
if (!value) return 'none';
|
|
20
|
+
return `url("${value.replace(/[\\"]/g, '\\$&')}")`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Turn a resolved theme into the custom properties the stylesheets read.
|
|
25
|
+
*
|
|
26
|
+
* Only values CSS can act on are here. Positions, animation effects, text and
|
|
27
|
+
* images that belong in markup are applied as attributes or drawn by the
|
|
28
|
+
* render functions instead.
|
|
29
|
+
*
|
|
30
|
+
* Animation durations are deliberately absent: the transition code has to know
|
|
31
|
+
* when an animation ends, so the theme is the single source of truth for them
|
|
32
|
+
* and it writes the duration onto the element as it starts. A custom property
|
|
33
|
+
* here would look like a knob that does nothing.
|
|
34
|
+
*
|
|
35
|
+
* @param {ResolvedTheme} theme
|
|
36
|
+
* @returns {Record<string, string>}
|
|
37
|
+
*/
|
|
38
|
+
export function themeToVariables(theme) {
|
|
39
|
+
const { closed, open } = theme;
|
|
40
|
+
const c = open.colors;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
'--chit-z-index': String(theme.zIndex),
|
|
44
|
+
'--chit-font-family': theme.font.family,
|
|
45
|
+
'--chit-font-size': px(theme.font.size),
|
|
46
|
+
|
|
47
|
+
'--chit-launcher-size': closed.size === 'auto' ? 'auto' : px(closed.size),
|
|
48
|
+
'--chit-launcher-offset-x': px(closed.offset.x),
|
|
49
|
+
'--chit-launcher-offset-y': px(closed.offset.y),
|
|
50
|
+
'--chit-launcher-radius': px(closed.radius),
|
|
51
|
+
'--chit-launcher-image': url(closed.image),
|
|
52
|
+
'--chit-launcher-bg': closed.colors.background,
|
|
53
|
+
'--chit-launcher-text': closed.colors.text,
|
|
54
|
+
'--chit-launcher-shadow': closed.colors.shadow,
|
|
55
|
+
|
|
56
|
+
'--chit-panel-width': px(open.width),
|
|
57
|
+
'--chit-panel-height': px(open.height),
|
|
58
|
+
'--chit-panel-offset-x': px(open.offset.x),
|
|
59
|
+
'--chit-panel-offset-y': px(open.offset.y),
|
|
60
|
+
'--chit-panel-radius': px(open.radius),
|
|
61
|
+
'--chit-panel-bg-image': url(open.background.image),
|
|
62
|
+
'--chit-panel-shadow': c.shadow,
|
|
63
|
+
|
|
64
|
+
'--chit-color-bg': c.background,
|
|
65
|
+
'--chit-color-text': c.text,
|
|
66
|
+
'--chit-color-accent': c.accent,
|
|
67
|
+
'--chit-color-border': c.border,
|
|
68
|
+
'--chit-color-header-bg': c.headerBackground,
|
|
69
|
+
'--chit-color-header-text': c.headerText,
|
|
70
|
+
'--chit-color-user-bg': c.userBubble,
|
|
71
|
+
'--chit-color-user-text': c.userText,
|
|
72
|
+
'--chit-color-assistant-bg': c.assistantBubble,
|
|
73
|
+
'--chit-color-assistant-text': c.assistantText,
|
|
74
|
+
'--chit-color-system-text': c.systemText,
|
|
75
|
+
'--chit-color-input-bg': c.inputBackground,
|
|
76
|
+
'--chit-color-input-text': c.inputText,
|
|
77
|
+
'--chit-color-input-placeholder': c.inputPlaceholder,
|
|
78
|
+
|
|
79
|
+
'--_chit-input-max-rows': String(open.input.maxRows),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The stylesheet text for a resolved theme.
|
|
85
|
+
*
|
|
86
|
+
* @param {ResolvedTheme} theme
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function themeToCss(theme) {
|
|
90
|
+
const declarations = Object.entries(themeToVariables(theme))
|
|
91
|
+
.map(([name, value]) => ` ${name}: ${value};`)
|
|
92
|
+
.join('\n');
|
|
93
|
+
return `:host {\n${declarations}\n}\n`;
|
|
94
|
+
}
|
package/src/types.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public types for Chit UI. This module has no runtime value; it exists so that
|
|
5
|
+
* every other file can `@import` from one place and so `tsc` can emit .d.ts.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {'user' | 'assistant' | 'system'} Role
|
|
8
|
+
* @typedef {'sending' | 'sent' | 'error'} MessageStatus
|
|
9
|
+
* @typedef {'closed' | 'open' | 'hidden'} ChatState
|
|
10
|
+
* @typedef {'user' | 'api'} Trigger
|
|
11
|
+
* @typedef {'pc' | 'mobile'} Device
|
|
12
|
+
* @typedef {'fade' | 'scale' | 'slide' | 'none'} Effect
|
|
13
|
+
* @typedef {'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'} Position
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} MessageBase
|
|
18
|
+
* @property {string} id Unique within the array; used as the diffing key.
|
|
19
|
+
* @property {Role} role
|
|
20
|
+
* @property {string | Date} [time]
|
|
21
|
+
* @property {string} [name]
|
|
22
|
+
* @property {string} [avatar]
|
|
23
|
+
* @property {MessageStatus} [status]
|
|
24
|
+
* @property {boolean} [streaming]
|
|
25
|
+
* @property {unknown} [meta] Never touched by the library.
|
|
26
|
+
*
|
|
27
|
+
* @typedef {MessageBase & { text: string }} TextMessage
|
|
28
|
+
* @typedef {MessageBase & { html: string }} HtmlMessage
|
|
29
|
+
* @typedef {MessageBase & { template: import('lit').TemplateResult }} TemplateMessage
|
|
30
|
+
* @typedef {MessageBase & { element: HTMLElement }} ElementMessage
|
|
31
|
+
* @typedef {MessageBase & { component: (new () => HTMLElement) | string, props?: Record<string, unknown> }} ComponentMessage
|
|
32
|
+
* @typedef {TextMessage | HtmlMessage | TemplateMessage | ElementMessage | ComponentMessage} Message
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} Animation
|
|
37
|
+
* @property {Effect} [enter]
|
|
38
|
+
* @property {Effect} [exit]
|
|
39
|
+
* @property {number} [duration] ms
|
|
40
|
+
*
|
|
41
|
+
* @typedef {Object} Offset
|
|
42
|
+
* @property {number} [x]
|
|
43
|
+
* @property {number} [y]
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} ClosedTheme
|
|
48
|
+
* @property {number | 'auto'} [size] Launcher edge length in px, or 'auto' to let its content decide.
|
|
49
|
+
* @property {Position} [position]
|
|
50
|
+
* @property {Offset} [offset]
|
|
51
|
+
* @property {number} [radius]
|
|
52
|
+
* @property {string | null} [image] Icon image URL; null clears the default.
|
|
53
|
+
* @property {string | null} [label]
|
|
54
|
+
* @property {(new () => HTMLElement) | string | null} [component] A component that draws the whole launcher.
|
|
55
|
+
* @property {Record<string, unknown>} [props] Properties written to that component.
|
|
56
|
+
* @property {{ background?: string, text?: string, shadow?: string }} [colors]
|
|
57
|
+
* @property {Animation & { idle?: 'none' | 'pulse' | 'bounce' }} [animation]
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} OpenColors
|
|
62
|
+
* @property {string} [background]
|
|
63
|
+
* @property {string} [text]
|
|
64
|
+
* @property {string} [accent]
|
|
65
|
+
* @property {string} [border]
|
|
66
|
+
* @property {string} [shadow]
|
|
67
|
+
* @property {string} [headerBackground]
|
|
68
|
+
* @property {string} [headerText]
|
|
69
|
+
* @property {string} [userBubble]
|
|
70
|
+
* @property {string} [userText]
|
|
71
|
+
* @property {string} [assistantBubble]
|
|
72
|
+
* @property {string} [assistantText]
|
|
73
|
+
* @property {string} [systemText]
|
|
74
|
+
* @property {string} [inputBackground]
|
|
75
|
+
* @property {string} [inputText]
|
|
76
|
+
* @property {string} [inputPlaceholder]
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @typedef {Object} OpenTheme
|
|
81
|
+
* @property {number} [width] Ignored on mobile (always full screen).
|
|
82
|
+
* @property {number} [height] Ignored on mobile (always full screen).
|
|
83
|
+
* @property {Position} [position]
|
|
84
|
+
* @property {Offset} [offset]
|
|
85
|
+
* @property {number} [radius]
|
|
86
|
+
* @property {'hidden' | 'visible'} [launcher] Keep the launcher visible while open.
|
|
87
|
+
* @property {{ title?: string | null, logo?: string | null, avatar?: string | null }} [header]
|
|
88
|
+
* @property {{ image?: string | null }} [background]
|
|
89
|
+
* @property {OpenColors} [colors]
|
|
90
|
+
* @property {Animation & { scroll?: 'smooth' | 'instant' }} [animation]
|
|
91
|
+
* @property {{ maxRows?: number, placeholder?: string | null }} [input]
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @typedef {Object} Theme
|
|
96
|
+
* @property {number} [breakpoint] Widths below this are treated as mobile.
|
|
97
|
+
* @property {number} [zIndex]
|
|
98
|
+
* @property {{ family?: string, size?: number }} [font]
|
|
99
|
+
* @property {ClosedTheme | { pc?: ClosedTheme, mobile?: ClosedTheme }} [closed]
|
|
100
|
+
* @property {OpenTheme} [open]
|
|
101
|
+
* @property {{ animation?: { exit?: Effect, duration?: number } }} [hidden]
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A theme with every gap filled in and `closed` narrowed to the device in use.
|
|
106
|
+
* This is what the render functions and the CSS generator read.
|
|
107
|
+
*
|
|
108
|
+
* @typedef {Object} ResolvedClosed
|
|
109
|
+
* @property {number | 'auto'} size
|
|
110
|
+
* @property {Position} position
|
|
111
|
+
* @property {{ x: number, y: number }} offset
|
|
112
|
+
* @property {number} radius
|
|
113
|
+
* @property {string | null} image
|
|
114
|
+
* @property {string | null} label
|
|
115
|
+
* @property {(new () => HTMLElement) | string | null} component
|
|
116
|
+
* @property {Record<string, unknown>} props
|
|
117
|
+
* @property {{ background: string, text: string, shadow: string }} colors
|
|
118
|
+
* @property {{ enter: Effect, exit: Effect, duration: number, idle: 'none' | 'pulse' | 'bounce' }} animation
|
|
119
|
+
*
|
|
120
|
+
* @typedef {Object} ResolvedOpen
|
|
121
|
+
* @property {number} width
|
|
122
|
+
* @property {number} height
|
|
123
|
+
* @property {Position} position
|
|
124
|
+
* @property {{ x: number, y: number }} offset
|
|
125
|
+
* @property {number} radius
|
|
126
|
+
* @property {'hidden' | 'visible'} launcher
|
|
127
|
+
* @property {{ title: string | null, logo: string | null, avatar: string | null }} header
|
|
128
|
+
* @property {{ image: string | null }} background
|
|
129
|
+
* @property {Required<OpenColors>} colors
|
|
130
|
+
* @property {{ enter: Effect, exit: Effect, duration: number, scroll: 'smooth' | 'instant' }} animation
|
|
131
|
+
* @property {{ maxRows: number, placeholder: string | null }} input
|
|
132
|
+
*
|
|
133
|
+
* @typedef {Object} ResolvedTheme
|
|
134
|
+
* @property {number} breakpoint
|
|
135
|
+
* @property {number} zIndex
|
|
136
|
+
* @property {{ family: string, size: number }} font
|
|
137
|
+
* @property {ResolvedClosed} closed
|
|
138
|
+
* @property {ResolvedOpen} open
|
|
139
|
+
* @property {{ animation: { exit: Effect, duration: number } }} hidden
|
|
140
|
+
* @property {Device} device Which bucket `closed` was narrowed to.
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
export {};
|