@hidemikimura/chit-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +440 -0
  3. package/dist/chit-ui.iife.min.js +964 -0
  4. package/dist/chit-ui.iife.min.js.map +1 -0
  5. package/dist/chit-ui.min.js +964 -0
  6. package/dist/chit-ui.min.js.map +1 -0
  7. package/dist/types/bundle.d.ts +7 -0
  8. package/dist/types/chit-ui.d.ts +251 -0
  9. package/dist/types/controllers/breakpoint-controller.d.ts +29 -0
  10. package/dist/types/controllers/composer-controller.d.ts +54 -0
  11. package/dist/types/controllers/scroll-controller.d.ts +44 -0
  12. package/dist/types/controllers/state-controller.d.ts +61 -0
  13. package/dist/types/controllers/theme-controller.d.ts +47 -0
  14. package/dist/types/element.d.ts +1 -0
  15. package/dist/types/events.d.ts +28 -0
  16. package/dist/types/global.d.ts +25 -0
  17. package/dist/types/i18n/labels.d.ts +68 -0
  18. package/dist/types/index.d.ts +4 -0
  19. package/dist/types/render/composer.d.ts +15 -0
  20. package/dist/types/render/content.d.ts +75 -0
  21. package/dist/types/render/launcher.d.ts +18 -0
  22. package/dist/types/render/message-list.d.ts +17 -0
  23. package/dist/types/render/message.d.ts +14 -0
  24. package/dist/types/render/panel.d.ts +10 -0
  25. package/dist/types/styles/adopted-sheet.d.ts +20 -0
  26. package/dist/types/styles/composer.css.d.ts +1 -0
  27. package/dist/types/styles/content.css.d.ts +9 -0
  28. package/dist/types/styles/host.css.d.ts +9 -0
  29. package/dist/types/styles/launcher.css.d.ts +1 -0
  30. package/dist/types/styles/message.css.d.ts +1 -0
  31. package/dist/types/styles/panel.css.d.ts +1 -0
  32. package/dist/types/theme/default-theme.d.ts +97 -0
  33. package/dist/types/theme/merge-theme.d.ts +33 -0
  34. package/dist/types/theme/theme-to-css.d.ts +24 -0
  35. package/dist/types/types.d.ts +268 -0
  36. package/package.json +71 -0
  37. package/src/bundle.js +11 -0
  38. package/src/chit-ui.js +548 -0
  39. package/src/controllers/breakpoint-controller.js +82 -0
  40. package/src/controllers/composer-controller.js +215 -0
  41. package/src/controllers/scroll-controller.js +252 -0
  42. package/src/controllers/state-controller.js +316 -0
  43. package/src/controllers/theme-controller.js +75 -0
  44. package/src/element.js +4 -0
  45. package/src/events.js +33 -0
  46. package/src/global.d.ts +25 -0
  47. package/src/i18n/labels.js +72 -0
  48. package/src/index.js +9 -0
  49. package/src/render/composer.js +72 -0
  50. package/src/render/content.js +211 -0
  51. package/src/render/launcher.js +64 -0
  52. package/src/render/message-list.js +67 -0
  53. package/src/render/message.js +82 -0
  54. package/src/render/panel.js +77 -0
  55. package/src/styles/adopted-sheet.js +72 -0
  56. package/src/styles/composer.css.js +108 -0
  57. package/src/styles/content.css.js +119 -0
  58. package/src/styles/host.css.js +160 -0
  59. package/src/styles/launcher.css.js +139 -0
  60. package/src/styles/message.css.js +192 -0
  61. package/src/styles/panel.css.js +126 -0
  62. package/src/theme/default-theme.js +111 -0
  63. package/src/theme/merge-theme.js +100 -0
  64. package/src/theme/theme-to-css.js +94 -0
  65. package/src/types.js +143 -0
@@ -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 {};