@mp-consulting/homebridge-ui-kit 1.2.2 → 1.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/README.md CHANGED
@@ -9,23 +9,41 @@ Provides shared CSS tokens, Bootstrap 5 overrides, and vanilla JS UI components
9
9
  | File | Description |
10
10
  |------|-------------|
11
11
  | `dist/kit.css` | CSS tokens, Bootstrap overrides, shared component styles and the Assistant (AI) components |
12
- | `dist/kit.js` | Vanilla JS helpers exposed as `window.MpKit` (including `MpKit.ai`) |
12
+ | `dist/kit.js` | Vanilla JS helpers exposed as `window.MpKit` (including `MpKit.ai`) — a classic script |
13
13
  | `dist/ai.css` | Standalone build: tokens + Assistant components only, no Bootstrap overrides (for non-plugin apps such as the Glass UI) |
14
+ | `dist/kit.min.css`, `dist/kit.min.js`, `dist/ai.min.css` | Minified builds of the above, each with a `.map` source map |
15
+ | `dist/theme-boot.js` | ~0.4 kB `<head>` script that applies the light/dark theme before first paint (see [Theme](#theme)) |
16
+ | `dist/kit.d.ts`, `dist/kit.d.mts` | TypeScript declarations for the global and the ES module |
17
+ | `dist/kit.mjs` | ES module: `MpKit` (default export) plus named exports (`escapeHtml`, `markdown`, `diffLines`, `StatusBadge`, …); it does not set a global |
14
18
 
15
- Package entry points: `@mp-consulting/homebridge-ui-kit/dist/*` (and the shortcuts `/kit.css`, `/kit.js`, `/ai.css`).
19
+ Package entry points: `@mp-consulting/homebridge-ui-kit` (the ES module), `@mp-consulting/homebridge-ui-kit/dist/*`
20
+ and the shortcuts `/kit.css`, `/kit.min.css`, `/kit.js`, `/kit.min.js`, `/kit.mjs`, `/ai.css`, `/ai.min.css`.
21
+
22
+ ```js
23
+ // In a bundled app (e.g. the Glass UI) — pure helpers, no global, tree-shakeable
24
+ import { markdown, diffLines, escapeHtml } from '@mp-consulting/homebridge-ui-kit';
25
+ ```
16
26
 
17
27
  ## Integration
18
28
 
19
- ### 1. Install as a dev dependency
29
+ > **Serve everything from the plugin.** The Homebridge UI serves a plugin's custom UI with a
30
+ > content-security policy that only allows **same-origin** scripts and styles
31
+ > (`script-src 'self' 'unsafe-inline'`, `style-src 'self' 'unsafe-inline'`), so CDN `<script>` /
32
+ > `<link>` tags are blocked (a plugin's `customUiCspDomains` can only widen `script-src`, never
33
+ > `style-src`). Install the kit *and* Bootstrap as dev dependencies and let `mp-ui-kit-copy --vendor`
34
+ > copy them into `homebridge-ui/public/lib/`, then reference the local `lib/` paths as shown below.
35
+
36
+ ### 1. Install as dev dependencies
20
37
 
21
38
  ```bash
22
- npm install --save-dev @mp-consulting/homebridge-ui-kit
39
+ npm install --save-dev @mp-consulting/homebridge-ui-kit bootstrap@5.3 bootstrap-icons@1.11
23
40
  ```
24
41
 
25
42
  ### 2. Add the copy script to `package.json`
26
43
 
27
- The package ships a zero-dependency CLI, `mp-ui-kit-copy`, that copies `dist/` into
28
- `homebridge-ui/public/lib/` of the plugin it runs in:
44
+ The package ships a zero-dependency CLI, `mp-ui-kit-copy`, that copies the kit's browser files into
45
+ `homebridge-ui/public/lib/` of the plugin it runs in. With `--vendor` it also copies Bootstrap and
46
+ Bootstrap Icons from your `node_modules`, so the UI needs nothing from a CDN:
29
47
 
30
48
  ```json
31
49
  {
@@ -38,19 +56,27 @@ The package ships a zero-dependency CLI, `mp-ui-kit-copy`, that copies `dist/` i
38
56
 
39
57
  | Option | Effect |
40
58
  |--------|--------|
41
- | *(none)* | Copies `kit.css`, `kit.js` and `ai.css` into `homebridge-ui/public/lib/` |
59
+ | *(none)* | Copies the kit's browser files (`kit.css`, `kit.js`, `ai.css`, their `.min` variants, `kit.mjs`, `theme-boot.js`) into `homebridge-ui/public/lib/`. Source maps and `.d.ts` files are not copied, and the `sourceMappingURL` comments are stripped |
42
60
  | `--dest <dir>` | Copies into `<dir>` instead (relative to the current directory) |
61
+ | `--only <files>` | Copies only these dist files (comma-separated, repeatable), e.g. `--only kit.min.css,kit.min.js,theme-boot.js` |
43
62
  | `--vendor` | Also copies `bootstrap.min.css`, `bootstrap.bundle.min.js`, `bootstrap-icons.min.css` and `fonts/bootstrap-icons.woff(2)` from the plugin's own `node_modules` (packages that are not installed are skipped) and strips their `sourceMappingURL` comments, producing the same `lib/` layout as the plugins' previous `copy:ui-assets` scripts |
44
63
 
64
+ > **Load `kit.css` *or* `ai.css`, never both.** `kit.css` already contains everything in `ai.css`
65
+ > (tokens + Assistant components); loading both duplicates ~20 kB of CSS and can reorder overrides.
66
+ > Plugin UIs load `kit.css`; `ai.css` is for apps that do not use the plugin kit (the Glass UI).
67
+ > The CLI warns when an `.html` page next to the destination folder links both.
68
+
45
69
  Without the CLI, the equivalent is:
46
70
 
47
71
  ```bash
48
- mkdir -p homebridge-ui/public/lib && cp node_modules/@mp-consulting/homebridge-ui-kit/dist/* homebridge-ui/public/lib/
72
+ mkdir -p homebridge-ui/public/lib && cp node_modules/@mp-consulting/homebridge-ui-kit/dist/*.{css,js} homebridge-ui/public/lib/
49
73
  ```
50
74
 
51
75
  ### 3. Reference in `index.html`
52
76
 
53
- The plugin's `homebridge-ui/public/index.html` must be a full HTML document. Load Bootstrap 5.3 (from CDN as below, or `lib/bootstrap.min.css` when you use `--vendor`), then `lib/kit.css` and your styles in `<head>`, and Bootstrap JS + `kit.js` + your app script at the end of `<body>`:
77
+ The plugin's `homebridge-ui/public/index.html` must be a full HTML document. Load Bootstrap, then
78
+ `lib/kit.css` and your styles in `<head>`, and Bootstrap JS + `kit.js` + your app script at the end
79
+ of `<body>` — all from `lib/`:
54
80
 
55
81
  ```html
56
82
  <!DOCTYPE html>
@@ -58,45 +84,62 @@ The plugin's `homebridge-ui/public/index.html` must be a full HTML document. Loa
58
84
  <head>
59
85
  <meta charset="UTF-8">
60
86
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
61
- <script>
62
- (function() {
63
- try {
64
- if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
65
- document.documentElement.dataset.bsTheme = 'dark';
66
- }
67
- } catch(e) {}
68
- })();
69
- </script>
70
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
71
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous">
87
+ <script src="lib/theme-boot.js"></script>
88
+ <link rel="stylesheet" href="lib/bootstrap.min.css">
89
+ <link rel="stylesheet" href="lib/bootstrap-icons.min.css">
72
90
  <link rel="stylesheet" href="lib/kit.css">
73
91
  <link rel="stylesheet" href="styles.css">
74
92
  </head>
75
93
  <body>
76
94
  <!-- your UI here -->
77
95
 
78
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
96
+ <script src="lib/bootstrap.bundle.min.js"></script>
79
97
  <script src="lib/kit.js"></script>
80
98
  <script src="app.js"></script>
81
99
  </body>
82
100
  </html>
83
101
  ```
84
102
 
85
- The `integrity` hashes pin the exact CDN files; update them if you change a version (jsDelivr shows the SRI hash for each file).
103
+ > **Outside Homebridge** (a standalone page or a prototype without that CSP) you can load Bootstrap
104
+ > from a CDN instead. Pin the files with SRI hashes, and update them if you change a version
105
+ > (jsDelivr shows the hash for each file):
106
+ >
107
+ > ```html
108
+ > <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
109
+ > <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous">
110
+ > <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
111
+ > ```
112
+
113
+ #### Theme
86
114
 
87
- The early inline script applies `data-bs-theme="dark"` from the system preference before any CSS loads, preventing a flash of wrong theme. In your app script, also apply the Homebridge user's saved theme setting:
115
+ `lib/theme-boot.js` is a tiny, same-origin script (inline scripts are not needed) that sets
116
+ `data-bs-theme` (and the `.mp-theme-dark` class) on `<html>` before the stylesheets paint, from the
117
+ preference remembered by `MpKit.Theme` or else the system preference — no flash of the wrong theme.
118
+ Then, in your app script, let the kit apply the Homebridge user's setting and follow changes:
88
119
 
89
120
  ```js
90
- try {
91
- const settings = await homebridge.getUserSettings?.();
92
- if (settings?.theme === 'dark') {
93
- document.documentElement.dataset.bsTheme = 'dark';
94
- } else if (settings?.theme === 'light') {
95
- document.documentElement.dataset.bsTheme = 'light';
96
- }
97
- } catch (e) {}
121
+ const theme = MpKit.Theme.init(); // system preference now; Homebridge setting when it arrives
122
+ await theme.ready; // optional: resolves with 'light' | 'dark'
98
123
  ```
99
124
 
125
+ `MpKit.Theme.init(opts)` reads `homebridge.getUserSettings()` (`colorScheme`/`theme`: `light`,
126
+ `dark` or `auto`) and falls back to `homebridge.userCurrentLightingMode()`; with `auto` (or outside
127
+ Homebridge) it follows `prefers-color-scheme` live through a `matchMedia` listener. It remembers the
128
+ preference in `localStorage` (`mp-kit-theme`) for `theme-boot.js`.
129
+
130
+ | Option | Default | Effect |
131
+ |--------|---------|--------|
132
+ | `target` | `<html>` | Element that gets `data-bs-theme` / `.mp-theme-dark` |
133
+ | `theme` | — | Force `'light'`, `'dark'` or `'auto'` and skip the Homebridge lookup |
134
+ | `homebridge` | `true` | `false` skips the Homebridge lookup |
135
+ | `attribute` / `className` | `true` | Set `data-bs-theme` / toggle `.mp-theme-dark` |
136
+ | `storageKey` | `'mp-kit-theme'` | `localStorage` key (`false` disables it) |
137
+ | `onChange(resolved, preference)` | — | Called when the resolved theme changes |
138
+
139
+ The controller has `ready`, `preference()`, `resolved()`, `set('light' | 'dark' | 'auto')` and
140
+ `destroy()`. Pure helpers: `MpKit.Theme.resolve(preference, systemDark)`, `.normalize(value)`,
141
+ `.fromSettings(settings)` and `.apply(resolved, opts)`.
142
+
100
143
  ### 4. Update `.gitignore`
101
144
 
102
145
  ```gitignore
@@ -126,6 +169,21 @@ export default tseslint.config(
126
169
  );
127
170
  ```
128
171
 
172
+ ### 6. TypeScript (optional)
173
+
174
+ The package ships declarations for both builds. For the classic script (`lib/kit.js`), add a
175
+ reference once (e.g. in a `globals.d.ts`) to type the `MpKit` global and `window.MpKit`:
176
+
177
+ ```ts
178
+ /// <reference types="@mp-consulting/homebridge-ui-kit/kit.js" />
179
+ // or: import type {} from '@mp-consulting/homebridge-ui-kit/kit.js';
180
+ ```
181
+
182
+ The ES module entry is typed automatically (`import { markdown } from '@mp-consulting/homebridge-ui-kit'`).
183
+ Option and handle types are exported too, e.g. `import type { AiRequestOptions, ThemeController,
184
+ DeviceItem } from '@mp-consulting/homebridge-ui-kit'`. In JavaScript files, `// @ts-check` plus the
185
+ reference above gives editor completion for every helper.
186
+
129
187
  ## MpKit API
130
188
 
131
189
  ```js
@@ -141,8 +199,32 @@ MpKit.EmptyState.render({ iconClass, title, hint })
141
199
  // Loading placeholder
142
200
  MpKit.Loading.render('Loading...')
143
201
 
144
- // View switching (.mp-view elements)
202
+ // View switching (.mp-view elements). When switching from another view, focus
203
+ // moves to the new view's heading (or [data-mp-focus], or the view) and its
204
+ // aria-label / heading is announced politely. Returns the shown element.
145
205
  MpKit.View.show('viewId')
206
+ MpKit.View.show('viewId', { focus: false, announce: 'Custom message' })
207
+
208
+ // Accessible tabs for a .mp-tabs nav: roles, aria-selected, roving tabindex,
209
+ // Arrow keys / Home / End (mirrored in RTL). Panels come from data-mp-view
210
+ // (an .mp-view id), aria-controls, data-bs-target or href="#id".
211
+ const tabs = MpKit.Tabs.init('#tabs', { activation: 'auto', onChange: (tab, index, panel) => {} });
212
+ tabs.select(1);
213
+
214
+ // Toasts: Homebridge's own toasts inside the plugin UI, a Bootstrap-style
215
+ // fallback (.mp-toast, escaped text, role="status"/"alert") elsewhere
216
+ MpKit.Toast.success('Config saved'); // also .error / .warning / .info
217
+ MpKit.Toast.error('Login failed', 'Account'); // optional title
218
+ MpKit.Toast.info('Local only', { title, duration: 0, local: true }); // 0 = until closed
219
+
220
+ // Accessible confirmation dialog → Promise<boolean> (Escape/backdrop = false;
221
+ // focus is trapped and restored; the danger variant focuses Cancel first)
222
+ if (await MpKit.confirm({ title: 'Remove device?', message: 'This cannot be undone.',
223
+ confirmLabel: 'Remove', danger: true })) { /* … */ }
224
+
225
+ // Screen-reader announcement through a shared, visually hidden live region
226
+ MpKit.announce('Settings saved'); // polite
227
+ MpKit.announce('Connection lost', 'assertive');
146
228
 
147
229
  // Support footer (only http/https URLs are rendered; icons are inline SVG,
148
230
  // so the Bootstrap Icons font is not required)
@@ -152,9 +234,110 @@ MpKit.Footer.render({ github, npm, changelog, target: '.mp-footer' })
152
234
  MpKit.escapeHtml(device.name)
153
235
  ```
154
236
 
237
+ ### Forms
238
+
239
+ ```js
240
+ // Labelled Bootstrap controls (escaped; help/error wired with aria-describedby)
241
+ form.innerHTML = MpKit.Form.field({ name: 'host', label: 'Host', help: 'IP or hostname', required: true })
242
+ + MpKit.Form.field({ name: 'pollInterval', label: 'Poll interval (s)', type: 'number', min: 10, value: 30 })
243
+ + MpKit.Form.field({ name: 'mode', label: 'Mode', type: 'select', options: ['cloud', 'lan'], value: 'lan' })
244
+ + MpKit.Form.field({ name: 'debug', label: 'Debug logging', type: 'switch' })
245
+ // Secret with Show/Hide (aria-pressed) and Copy buttons
246
+ + MpKit.Form.secret({ name: 'auth.token', label: 'API token', value: token });
247
+
248
+ // Copy button for literal text or another element's value/text
249
+ MpKit.CopyButton.render({ text: code, label: 'Copy code' });
250
+ MpKit.CopyButton.render({ target: '#deviceId', size: 'sm' });
251
+ await MpKit.copy('text'); // → true/false (Clipboard API, execCommand fallback)
252
+
253
+ // Read / write named controls (dotted names nest: "auth.token" → { auth: { token } })
254
+ MpKit.Form.fill(form, config);
255
+ const values = MpKit.Form.values(form);
256
+
257
+ // Dirty tracking + sticky Save bar → homebridge.updatePluginConfig + savePluginConfig
258
+ const tracker = MpKit.Form.track(form, {
259
+ // optional: build the config blocks (default: merge values into the first block)
260
+ toConfig: (values, blocks) => [{ ...blocks[0], ...values, platform: 'MyPlatform' }],
261
+ // persist: false → only updatePluginConfig (the user clicks Homebridge's Save)
262
+ });
263
+ tracker.isDirty(); tracker.save(); tracker.discard(); tracker.reset(); tracker.destroy();
264
+ ```
265
+
266
+ The copy and reveal buttons work through document-level delegated listeners the kit installs once,
267
+ so the HTML strings need no extra wiring. Saving shows `MpKit.Toast` feedback (`toast: false` turns
268
+ it off), re-enables the bar on failure and keeps the form dirty. Outside Homebridge (and without a
269
+ custom `save(values)`), saving only resets the dirty state and resolves `false`.
270
+
271
+ ### Device lists, skeletons and logs
272
+
273
+ ```js
274
+ // Searchable device list with loading (skeleton), empty and no-results states.
275
+ const list = MpKit.DeviceList.render('#devices', {
276
+ loading: true, // skeleton cards + aria-busy until setItems()
277
+ label: 'Devices', // list name; search label "Search devices"
278
+ empty: { title: 'No devices found', hint: 'Click Discover to scan your network' },
279
+ onActivate: (device, index) => openDevice(device), // cards become role="button" (Enter/Space)
280
+ // renderItem: (device) => `<div class="card mp-device-card" role="button" tabindex="0">…</div>`,
281
+ // keys: ['name', 'subtitle', 'id'], filter: (device, query) => …, search: false,
282
+ });
283
+ list.setItems([{ id, name, subtitle: '192.168.1.40', status: 'online', iconClass: 'bi bi-lightbulb' }]);
284
+ list.setQuery('lamp'); list.setLoading(true);
285
+
286
+ // Shimmer placeholders (static with prefers-reduced-motion)
287
+ el.innerHTML = MpKit.Skeleton.render({ lines: 3, avatar: true });
288
+
289
+ // Plain-text log: ANSI codes stripped, capped, auto-scrolls unless the user
290
+ // scrolled up (then shows "Jump to latest"); role="log", keyboard scrollable.
291
+ const log = MpKit.LogViewer.create('#log', { maxLines: 1000, label: 'Plugin log' });
292
+ log.append('\u001b[32m[info]\u001b[0m Connected\nSecond line');
293
+ MpKit.stripAnsi(text);
294
+ ```
295
+
296
+ Default device cards show `name` (or `displayName`), `subtitle`, `iconClass` and a `status` badge
297
+ (`online` / `offline` / `checking` / `disabled`). Any `.mp-device-card[role="button"]` on the page —
298
+ yours included — is activated by Enter and Space. The log's live region is off by default
299
+ (a busy log would flood screen readers); pass `live: 'polite'` for low-volume logs.
300
+
301
+ ### Pairing and sign-in
302
+
303
+ ```js
304
+ // HomeKit setup code: XXX-XX-XXX, read digit by digit by screen readers, with
305
+ // a Copy button. The kit does not generate QR codes — pass an image or SVG.
306
+ el.innerHTML = MpKit.Pairing.pin({ pin: '03145154', qrSrc: 'qr.png' }); // http(s), path or data:image/*
307
+ MpKit.Pairing.renderPin(el, { pin, qr: svgElementFromYourServer }); // or mount an <svg>/<img>
308
+ MpKit.Pairing.formatPin('03145154'); // '031-45-154'
309
+
310
+ // OAuth device-code step card: open URL, code with Copy, polling state, success/error
311
+ const signIn = MpKit.Auth.deviceCode('#signin', {
312
+ url: res.verificationUri, code: res.userCode, expiresIn: res.expiresIn,
313
+ poll: () => homebridge.request('/auth/poll'), // true | { done } | { error } | { interval }
314
+ interval: 5000,
315
+ onSuccess: () => MpKit.Steps /* … */, onRetry: () => startAgain(),
316
+ });
317
+ signIn.setState('error', 'Access denied'); // or drive the state yourself (no poll)
318
+
319
+ // Multi-step wizard: .mp-steps progress (aria-current="step") + one visible panel;
320
+ // focus moves to the new panel's heading and "Step 2 of 3: Devices" is announced.
321
+ const wizard = MpKit.Steps.create('#steps', {
322
+ steps: [{ title: 'Account', panel: '#step-login' }, { title: 'Devices', panel: '#step-devices' },
323
+ { title: 'Done', panel: '#step-done' }],
324
+ onChange: (index, step) => {},
325
+ });
326
+ wizard.next(); wizard.prev(); wizard.go(2);
327
+ ```
328
+
155
329
  All text passed to the helpers is HTML-escaped, so it is safe to pass device names or other
156
330
  values that came from the network. To include markup, build the element yourself.
157
331
 
332
+ ### Accessibility and RTL
333
+
334
+ - `--mp-text-subtle` (secondary hints, ≥ 6:1 in both themes) and `--mp-focus-ring` (focus outlines;
335
+ the brand indigo in light mode, `#818cf8` in dark mode where the indigo is only 2.5:1) are the
336
+ contrast-safe tokens the components use. `.mp-sr-only` hides content visually only.
337
+ - The component styles use logical properties (`margin-inline-start`, `padding-inline-end`, …), so
338
+ `dir="rtl"` pages lay out correctly with the regular (LTR) Bootstrap build. Code blocks and diffs
339
+ stay left-to-right.
340
+
158
341
  ## Assistant (AI)
159
342
 
160
343
  The Assistant components give AI features one recognisable look: a rotating
@@ -245,17 +428,62 @@ const chat = MpKit.ai.renderChat(document.getElementById('chat'), {
245
428
  });
246
429
  ```
247
430
 
431
+ #### Cancelling a request
432
+
433
+ Every request accepts an `AbortSignal` and returns a promise with `.cancel()` (and `.requestId`).
434
+ Cancelling rejects the promise with an `AbortError` (or the signal's reason), removes the event
435
+ listeners — late chunks are ignored — and sends a best-effort `/ai/cancel` request with the
436
+ `requestId` so a server that supports it can stop generating (failures are ignored; pass
437
+ `notifyServer: false` to skip it).
438
+
439
+ ```js
440
+ const controller = new AbortController();
441
+ stopBtn.onclick = () => controller.abort();
442
+ try {
443
+ await MpKit.ai.ask({ prompt }, { onChunk: answer.append, signal: controller.signal });
444
+ } catch (e) {
445
+ if (e.name !== 'AbortError') { answer.error(e); }
446
+ }
447
+
448
+ const req = MpKit.ai.explain({ error }); // or keep the promise…
449
+ req.cancel(); // …and cancel it directly
450
+ ```
451
+
452
+ `renderChat()` returns `cancel()` too (the partial reply stays, marked "Stopped"), and passes
453
+ `ctx.signal` to a custom `onSend`.
454
+
248
455
  `MpKit.ai.markdown(text)` and `MpKit.ai.diffLines(before, after)` are exported too.
249
456
  Outside Homebridge (e.g. the Glass UI) load only `dist/ai.css` and use the CSS
250
457
  classes directly.
251
458
 
252
- `examples/ai-preview.html` (repository only) previews every component in light and
253
- dark mode with a mocked `homebridge` object: run `npm run build` and serve the repo
254
- root with any static server.
459
+ `examples/index.html` (repository only; published to GitHub Pages) is a gallery of every
460
+ component — core, forms, feedback, pairing, logs and Assistant — with light/dark and RTL toggles
461
+ and a mocked `homebridge` object; `examples/ai-preview.html` focuses on the Assistant components.
462
+ See [Development](#development) to run them.
255
463
 
256
464
  ## Dark Mode
257
465
 
258
- Dark mode is handled entirely by Bootstrap's `data-bs-theme="dark"` attribute on `<html>`. Do **not** use `@media (prefers-color-scheme: dark)` blocks, `.dark-mode` CSS classes, or custom CSS variable overrides — Bootstrap handles all of this automatically. Use Bootstrap CSS variables (`var(--bs-body-bg)`, `var(--bs-primary)`, etc.) in your custom CSS instead of hardcoded hex values.
466
+ Plugins use Bootstrap's `data-bs-theme="dark"` attribute on `<html>`: Bootstrap and the kit's
467
+ `--mp-*` tokens both follow it. Use Bootstrap CSS variables (`var(--bs-body-bg)`, `var(--bs-primary)`, …)
468
+ and the `--mp-*` tokens in your own CSS instead of hard-coded colours.
469
+
470
+ Hosts whose dark mode is not Bootstrap's attribute (for example an app that toggles `body.dark-mode`,
471
+ such as the Glass UI) add the **`mp-theme-dark`** class to the same element instead of copying the
472
+ kit's dark values:
473
+
474
+ ```js
475
+ document.body.classList.toggle('dark-mode', dark);
476
+ document.body.classList.toggle('mp-theme-dark', dark); // kit tokens follow
477
+ ```
478
+
479
+ Under `.mp-theme-dark` alone the host colours (`--mp-ai-surface`, `--mp-ai-fg`, …) are plain values
480
+ from Bootstrap's dark palette, because the host does not switch Bootstrap's variables; override just
481
+ the ones that differ (e.g. `--mp-ai-surface` for a different card colour). When an element has both
482
+ `data-bs-theme="dark"` and `.mp-theme-dark`, the Bootstrap-variable values win.
483
+
484
+ The kit's CSS is intentionally **not** wrapped in a cascade layer: unlayered styles beat every layered
485
+ style, so with unlayered Bootstrap a `@layer mp-kit` would make Bootstrap override the kit's
486
+ `.btn-primary` and `.mp-tabs` remaps.
259
487
 
260
488
  ## Development
261
489
 
@@ -263,12 +491,41 @@ Dark mode is handled entirely by Bootstrap's `data-bs-theme="dark"` attribute on
263
491
  npm install
264
492
  npm run build # outputs to dist/ (commit the result)
265
493
  npm run build:check # fails if dist/ is out of date (run in CI)
266
- npm test
494
+ npm test # node:test unit tests (jsdom) against dist/
495
+ npm run test:types # type-checks the .d.ts files
267
496
  ```
268
497
 
269
498
  Use the `--mp-*` tokens for brand colors. For text in the brand color use `--mp-primary-text`,
270
499
  which switches to a lighter shade in dark mode so it keeps WCAG AA contrast.
271
500
 
501
+ ### Gallery and visual tests
502
+
503
+ ```bash
504
+ npm run examples # copies dist/ + Bootstrap into examples/lib/ (git-ignored)
505
+ npm run serve # http://localhost:4173/ with the Homebridge UI's CSP
506
+ npx playwright install chromium
507
+ npm run test:visual # screenshots (light, dark, narrow, rtl) + axe-core + keyboard checks
508
+ npm run test:visual:update # refresh the baselines after an intentional visual change
509
+ ```
510
+
511
+ Screenshot baselines live in `e2e/__screenshots__/<platform>/<project>/` because font rendering
512
+ differs per OS. CI compares against the Linux baselines inside the
513
+ `mcr.microsoft.com/playwright:v1.63.0-noble` image; refresh them with the same image (the command is
514
+ in `.github/workflows/node.js.yml`) and keep its tag in step with `@playwright/test`. The axe-core (WCAG 2.2 AA)
515
+ and keyboard checks always run. Append `?static&theme=dark&dir=rtl` to the gallery URL to preview a
516
+ combination.
517
+
518
+ ### Releases
519
+
520
+ `npm run size` checks the gzip size of the minified files against the budgets in
521
+ `scripts/check-size.js` (CI and the publish workflow run it), and CI checks the `npm pack`
522
+ contents. Releases are prepared by release-please (`release-please-config.json`,
523
+ `.release-please-manifest.json`): it keeps a release PR with the next version and changelog from
524
+ conventional commits; merging it creates the GitHub release and then starts `publish.yml` on the
525
+ new tag with a `workflow_dispatch` (npm trusted publishing, which adds provenance automatically). No
526
+ extra secret is needed: the default `GITHUB_TOKEN` may start a `workflow_dispatch`, and a release
527
+ published by hand still runs `publish.yml` through its `release` trigger. Version 1.3.0 was released by hand (its notes predate release-please); release-please takes over from there.
528
+
272
529
  ## License
273
530
 
274
531
  MIT © MP Consulting
package/dist/ai.css CHANGED
@@ -1,9 +1,18 @@
1
- /* @mp-consulting/homebridge-ui-kit v1.2.2 */
1
+ /* @mp-consulting/homebridge-ui-kit v1.3.0 */
2
2
 
3
3
  /* ── Design tokens ──────────────────────────────────────────────────────────
4
4
  CSS custom properties used throughout the design system.
5
5
  Override these in your plugin's styles.css to customize per-plugin.
6
- Light values live on :root; dark values follow Bootstrap's data-bs-theme.
6
+ Light values live on :root. Dark values apply under Bootstrap's
7
+ [data-bs-theme="dark"] or under the selector-agnostic .mp-theme-dark class,
8
+ for hosts whose dark mode does not use Bootstrap's attribute (e.g. a
9
+ `body.dark-mode` app): add .mp-theme-dark next to your own class instead of
10
+ copying these values.
11
+
12
+ The kit's rules are deliberately NOT wrapped in a cascade layer:
13
+ unlayered styles beat every layered style regardless of specificity or
14
+ order, so unlayered Bootstrap would then override the kit's Bootstrap
15
+ remaps (primary button, tabs, spinner) and break them.
7
16
  ────────────────────────────────────────────────────────────────────────── */
8
17
 
9
18
  :root {
@@ -19,10 +28,19 @@
19
28
  --mp-status-offline: #ef4444;
20
29
  --mp-status-checking: #94a3b8;
21
30
 
31
+ /* Accessible text and focus colours (plain values, so they hold contrast
32
+ whether or not the host switches Bootstrap's variables) */
33
+ --mp-text-subtle: #5c636a; /* secondary hints; 6.1:1 on white */
34
+ --mp-focus-ring: #4f46e5; /* focus outlines; 6.3:1 on white */
35
+
22
36
  /* Surfaces */
23
37
  --mp-surface: rgba(0, 0, 0, 0.02);
24
38
  --mp-border: rgba(0, 0, 0, 0.1);
25
39
 
40
+ /* Skeleton shimmer */
41
+ --mp-skeleton-base: rgba(0, 0, 0, 0.08);
42
+ --mp-skeleton-shine: rgba(0, 0, 0, 0.03);
43
+
26
44
  /* Shape */
27
45
  --mp-radius: 0.5rem;
28
46
  --mp-shadow-hover: 0 0.5rem 1rem rgba(0, 0, 0, 0.12);
@@ -75,12 +93,42 @@
75
93
  initial-value: 0deg;
76
94
  }
77
95
 
78
- [data-bs-theme="dark"] {
96
+ /* Dark values that do not depend on Bootstrap. */
97
+ [data-bs-theme="dark"],
98
+ .mp-theme-dark {
79
99
  --mp-primary-text: #818cf8;
100
+ --mp-text-subtle: #a3a9b0; /* 6.5:1 on #212529 */
101
+ --mp-focus-ring: #818cf8; /* 5.2:1 on #212529 (the brand indigo is 2.5:1) */
80
102
  --mp-surface: rgba(255, 255, 255, 0.03);
81
103
  --mp-border: rgba(255, 255, 255, 0.08);
82
104
  --mp-shadow-hover: 0 0.5rem 1rem rgba(0, 0, 0, 0.3);
105
+ --mp-skeleton-base: rgba(255, 255, 255, 0.08);
106
+ --mp-skeleton-shine: rgba(255, 255, 255, 0.16);
83
107
  --mp-ai-text: #d8b4fe; /* 9.6:1 on #212529 */
108
+ --mp-ai-glow-opacity: 0.7;
109
+ --mp-ai-edge-glow-opacity: 0.8;
110
+ --mp-ai-diff-add-bg: rgba(34, 197, 94, 0.16);
111
+ --mp-ai-diff-add-text: #bbf7d0;
112
+ --mp-ai-diff-del-bg: rgba(239, 68, 68, 0.18);
113
+ --mp-ai-diff-del-text: #fecaca;
114
+ }
115
+
116
+ /* .mp-theme-dark alone: the host does not switch Bootstrap's variables, so
117
+ the host colours are plain values (Bootstrap's dark palette). */
118
+ .mp-theme-dark {
119
+ --mp-ai-surface: #212529;
120
+ --mp-ai-fg: #dee2e6;
121
+ --mp-ai-fg-muted: rgba(222, 226, 230, 0.75);
122
+ --mp-ai-bg: transparent;
123
+ --mp-ai-border-color: #495057;
124
+ --mp-ai-success-text: #75b798;
125
+ --mp-ai-danger-text: #ea868f;
126
+ }
127
+
128
+ /* Bootstrap's dark theme: follow its variables, so a plugin that customises
129
+ them is respected. Declared after .mp-theme-dark so it wins when an element
130
+ has both. */
131
+ [data-bs-theme="dark"] {
84
132
  --mp-ai-surface: var(--bs-body-bg, #212529);
85
133
  --mp-ai-fg: var(--bs-body-color, #dee2e6);
86
134
  --mp-ai-fg-muted: var(--bs-secondary-color, rgba(222, 226, 230, 0.75));
@@ -88,12 +136,6 @@
88
136
  --mp-ai-border-color: var(--bs-border-color, #495057);
89
137
  --mp-ai-success-text: var(--bs-success-text-emphasis, #75b798);
90
138
  --mp-ai-danger-text: var(--bs-danger-text-emphasis, #ea868f);
91
- --mp-ai-glow-opacity: 0.7;
92
- --mp-ai-edge-glow-opacity: 0.8;
93
- --mp-ai-diff-add-bg: rgba(34, 197, 94, 0.16);
94
- --mp-ai-diff-add-text: #bbf7d0;
95
- --mp-ai-diff-del-bg: rgba(239, 68, 68, 0.18);
96
- --mp-ai-diff-del-text: #fecaca;
97
139
  }
98
140
 
99
141
  /* ── Assistant (AI) components ──────────────────────────────────────────────
@@ -373,7 +415,7 @@
373
415
  display: inline-block;
374
416
  width: 0.5em;
375
417
  height: 1.1em;
376
- margin-left: 2px;
418
+ margin-inline-start: 2px;
377
419
  vertical-align: text-bottom;
378
420
  border-radius: 1px;
379
421
  background: var(--mp-ai-text);
@@ -399,7 +441,7 @@
399
441
 
400
442
  .mp-ai-prose ul,
401
443
  .mp-ai-prose ol {
402
- padding-left: 1.25rem;
444
+ padding-inline-start: 1.25rem;
403
445
  }
404
446
 
405
447
  .mp-ai-prose code {
@@ -412,7 +454,10 @@
412
454
  font-size: 0.85em;
413
455
  }
414
456
 
457
+ /* Code reads left to right even on an RTL page. */
415
458
  .mp-ai-prose pre {
459
+ direction: ltr;
460
+ unicode-bidi: isolate;
416
461
  padding: 0.625rem 0.75rem;
417
462
  overflow-x: auto;
418
463
  border-radius: 0.375rem;
@@ -483,14 +528,14 @@
483
528
  }
484
529
 
485
530
  .is-user > .mp-ai-chat-bubble {
486
- border-bottom-right-radius: 0.25rem;
531
+ border-end-end-radius: 0.25rem;
487
532
  background: var(--mp-primary, #4f46e5);
488
533
  color: #fff;
489
534
  }
490
535
 
491
536
  .is-assistant > .mp-ai-chat-bubble {
492
537
  border: 1px solid var(--mp-border, rgba(0, 0, 0, 0.1));
493
- border-bottom-left-radius: 0.25rem;
538
+ border-end-start-radius: 0.25rem;
494
539
  background: var(--mp-surface, rgba(0, 0, 0, 0.02));
495
540
  }
496
541
 
@@ -560,7 +605,7 @@
560
605
  .mp-ai-diff-stats {
561
606
  flex: none;
562
607
  display: inline-flex;
563
- margin-left: auto;
608
+ margin-inline-start: auto;
564
609
  gap: 0.5rem;
565
610
  font-family: var(--bs-font-monospace, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
566
611
  }
@@ -574,6 +619,8 @@
574
619
  }
575
620
 
576
621
  .mp-ai-diff-body {
622
+ direction: ltr;
623
+ unicode-bidi: isolate;
577
624
  max-height: 24rem;
578
625
  margin: 0;
579
626
  padding: 0.375rem 0;
@@ -586,7 +633,7 @@
586
633
  .mp-ai-diff-line {
587
634
  display: block;
588
635
  min-width: max-content;
589
- padding-right: 0.75rem;
636
+ padding-inline-end: 0.75rem;
590
637
  white-space: pre;
591
638
  }
592
639
 
@@ -623,7 +670,7 @@
623
670
  }
624
671
 
625
672
  .mp-ai-diff-status {
626
- margin-right: auto;
673
+ margin-inline-end: auto;
627
674
  color: var(--mp-ai-fg-muted);
628
675
  font-size: 0.8125rem;
629
676
  }
@@ -0,0 +1,3 @@
1
+ /* @mp-consulting/homebridge-ui-kit v1.3.0 */
2
+ :root{--mp-primary: #4f46e5;--mp-primary-rgb: 79, 70, 229;--mp-primary-hover: #4338ca;--mp-primary-text: #4f46e5;--mp-status-online: #10b981;--mp-status-online-glow: rgba(16, 185, 129, .35);--mp-status-offline: #ef4444;--mp-status-checking: #94a3b8;--mp-text-subtle: #5c636a;--mp-focus-ring: #4f46e5;--mp-surface: rgba(0, 0, 0, .02);--mp-border: rgba(0, 0, 0, .1);--mp-skeleton-base: rgba(0, 0, 0, .08);--mp-skeleton-shine: rgba(0, 0, 0, .03);--mp-radius: .5rem;--mp-shadow-hover: 0 .5rem 1rem rgba(0, 0, 0, .12);--mp-ai-1: #BC82F3;--mp-ai-2: #F5B9EA;--mp-ai-3: #8D9FFF;--mp-ai-4: #FF6778;--mp-ai-5: #FFBA71;--mp-ai-6: #C686FF;--mp-ai-stops: var(--mp-ai-1), var(--mp-ai-2), var(--mp-ai-3), var(--mp-ai-4), var(--mp-ai-5), var(--mp-ai-6), var(--mp-ai-1);--mp-ai-gradient: conic-gradient(from var(--mp-ai-angle), var(--mp-ai-stops));--mp-ai-gradient-linear: linear-gradient(90deg, var(--mp-ai-stops));--mp-ai-text: #7e22ce;--mp-ai-surface: var(--bs-body-bg, #fff);--mp-ai-fg: var(--bs-body-color, #212529);--mp-ai-fg-muted: var(--bs-secondary-color, #6c757d);--mp-ai-bg: var(--bs-body-bg, transparent);--mp-ai-border-color: var(--bs-border-color, #dee2e6);--mp-ai-success-text: var(--bs-success-text-emphasis, #0a3622);--mp-ai-danger-text: var(--bs-danger-text-emphasis, #b02a37);--mp-ai-border-width: 2px;--mp-ai-glow-blur: 12px;--mp-ai-glow-opacity: .45;--mp-ai-edge-glow-opacity: .55;--mp-ai-duration: 4s;--mp-ai-edge-width: 28px;--mp-ai-diff-add-bg: #dcfce7;--mp-ai-diff-add-text: #14532d;--mp-ai-diff-del-bg: #fee2e2;--mp-ai-diff-del-text: #7f1d1d}@property --mp-ai-angle{syntax: "<angle>"; inherits: false; initial-value: 0deg;}[data-bs-theme=dark],.mp-theme-dark{--mp-primary-text: #818cf8;--mp-text-subtle: #a3a9b0;--mp-focus-ring: #818cf8;--mp-surface: rgba(255, 255, 255, .03);--mp-border: rgba(255, 255, 255, .08);--mp-shadow-hover: 0 .5rem 1rem rgba(0, 0, 0, .3);--mp-skeleton-base: rgba(255, 255, 255, .08);--mp-skeleton-shine: rgba(255, 255, 255, .16);--mp-ai-text: #d8b4fe;--mp-ai-glow-opacity: .7;--mp-ai-edge-glow-opacity: .8;--mp-ai-diff-add-bg: rgba(34, 197, 94, .16);--mp-ai-diff-add-text: #bbf7d0;--mp-ai-diff-del-bg: rgba(239, 68, 68, .18);--mp-ai-diff-del-text: #fecaca}.mp-theme-dark{--mp-ai-surface: #212529;--mp-ai-fg: #dee2e6;--mp-ai-fg-muted: rgba(222, 226, 230, .75);--mp-ai-bg: transparent;--mp-ai-border-color: #495057;--mp-ai-success-text: #75b798;--mp-ai-danger-text: #ea868f}[data-bs-theme=dark]{--mp-ai-surface: var(--bs-body-bg, #212529);--mp-ai-fg: var(--bs-body-color, #dee2e6);--mp-ai-fg-muted: var(--bs-secondary-color, rgba(222, 226, 230, .75));--mp-ai-bg: var(--bs-body-bg, transparent);--mp-ai-border-color: var(--bs-border-color, #495057);--mp-ai-success-text: var(--bs-success-text-emphasis, #75b798);--mp-ai-danger-text: var(--bs-danger-text-emphasis, #ea868f)}@keyframes mp-ai-spin{to{--mp-ai-angle: 360deg}}@keyframes mp-ai-glow-pulse{0%,to{opacity:calc(var(--mp-ai-glow-opacity) * .35)}50%{opacity:var(--mp-ai-glow-opacity)}}@keyframes mp-ai-orb-pulse{0%,to{transform:scale(.8);opacity:.65}50%{transform:scale(1.1);opacity:1}}@keyframes mp-ai-blink{50%{opacity:0}}@keyframes mp-ai-fade-in{0%{opacity:0}}.mp-ai-halo,.mp-ai-button,.mp-ai-badge,.mp-ai-panel{position:relative;isolation:isolate;background:transparent}.mp-ai-halo:before,.mp-ai-button:before,.mp-ai-badge:before,.mp-ai-panel:before{content:"";position:absolute;inset:0;z-index:-1;border:var(--mp-ai-border-width) solid transparent;border-radius:inherit;background:linear-gradient(var(--mp-ai-surface),var(--mp-ai-surface)) padding-box,conic-gradient(from var(--mp-ai-angle),var(--mp-ai-stops)) border-box;pointer-events:none}.mp-ai-halo:after,.mp-ai-button:after,.mp-ai-panel:after{content:"";position:absolute;inset:0;z-index:-2;border-radius:inherit;background:conic-gradient(from var(--mp-ai-angle),var(--mp-ai-stops));filter:blur(var(--mp-ai-glow-blur));opacity:var(--mp-ai-glow-opacity);pointer-events:none;transition:opacity .2s ease}.mp-ai-halo{display:block;border-radius:var(--mp-radius, .5rem)}.mp-ai-halo:before,.mp-ai-halo:after{animation:mp-ai-spin var(--mp-ai-duration) linear infinite}.mp-ai-badge{--mp-ai-border-width: 1.5px;display:inline-flex;align-items:center;gap:.3em;padding:.2em .65em;border-radius:999px;color:var(--mp-ai-text);font-size:.75rem;font-weight:600;line-height:1.2;white-space:nowrap}.mp-ai-icon{width:1em;height:1em;flex-shrink:0;fill:currentColor;color:var(--mp-ai-text)}.mp-ai-button{display:inline-flex;align-items:center;justify-content:center;gap:.4rem;padding:.375rem .95rem;border:0;border-radius:999px;color:var(--mp-ai-fg);font:inherit;font-size:.875rem;font-weight:500;line-height:1.5;text-decoration:none;cursor:pointer}.mp-ai-button:after{opacity:0}.mp-ai-button:hover:before,.mp-ai-button:hover:after,.mp-ai-button:focus-visible:before,.mp-ai-button:focus-visible:after,.mp-ai-button[aria-busy=true]:before,.mp-ai-button[aria-busy=true]:after{animation:mp-ai-spin var(--mp-ai-duration) linear infinite}.mp-ai-button:hover:after,.mp-ai-button:focus-visible:after,.mp-ai-button[aria-busy=true]:after{opacity:var(--mp-ai-glow-opacity)}.mp-ai-button:focus-visible{outline:2px solid var(--mp-ai-text);outline-offset:3px}.mp-ai-button:disabled:not([aria-busy=true]){cursor:not-allowed;opacity:.6}.mp-ai-button-sm{padding:.25rem .7rem;font-size:.8125rem}.mp-ai-button-lg{padding:.55rem 1.25rem;font-size:1rem}.mp-ai-thinking:not(.mp-ai-halo,.mp-ai-button,.mp-ai-badge,.mp-ai-panel){display:inline-flex;align-items:center;gap:.5rem;color:var(--mp-ai-fg-muted);font-size:.875rem}.mp-ai-thinking:not(.mp-ai-halo,.mp-ai-button,.mp-ai-badge,.mp-ai-panel):before{content:"";width:.875em;height:.875em;flex-shrink:0;border-radius:50%;background:conic-gradient(from var(--mp-ai-angle),var(--mp-ai-stops));animation:mp-ai-spin 1.5s linear infinite,mp-ai-orb-pulse 1.4s ease-in-out infinite}.mp-ai-thinking:is(.mp-ai-halo,.mp-ai-button,.mp-ai-panel):before{animation:mp-ai-spin var(--mp-ai-duration) linear infinite}.mp-ai-thinking:is(.mp-ai-halo,.mp-ai-button,.mp-ai-panel):after{animation:mp-ai-spin var(--mp-ai-duration) linear infinite,mp-ai-glow-pulse 1.6s ease-in-out infinite}.mp-ai-panel{display:block;box-sizing:border-box;width:100%;max-width:100%;min-width:0;overflow-wrap:break-word;padding:1rem 1.125rem;border-radius:var(--mp-radius, .5rem);color:var(--mp-ai-fg);font-size:.9375rem}.mp-ai-panel:after{opacity:calc(var(--mp-ai-glow-opacity) * .4)}.mp-ai-panel.is-streaming:before,.mp-ai-panel.is-streaming:after{animation:mp-ai-spin var(--mp-ai-duration) linear infinite}.mp-ai-panel.is-streaming:after{opacity:var(--mp-ai-glow-opacity)}.mp-ai-panel-header{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem .5rem;margin-bottom:.625rem}.mp-ai-panel-header>.mp-ai-badge{flex:none}.mp-ai-panel-title{flex:1 1 12rem;min-width:0;margin:0;font-size:.875rem;font-weight:600;line-height:1.35;overflow-wrap:anywhere}.mp-ai-panel-body{min-width:0}.mp-ai-panel-note{margin:.75rem 0 0;font-size:.75rem;color:var(--mp-ai-fg-muted)}.mp-ai-error{margin:.5rem 0 0;color:var(--mp-ai-danger-text);font-size:.875rem}.mp-ai-caret{display:inline-block;width:.5em;height:1.1em;margin-inline-start:2px;vertical-align:text-bottom;border-radius:1px;background:var(--mp-ai-text);animation:mp-ai-blink 1s steps(1) infinite}.mp-ai-prose>:first-child{margin-top:0}.mp-ai-prose>:last-child{margin-bottom:0}.mp-ai-prose p,.mp-ai-prose ul,.mp-ai-prose ol,.mp-ai-prose pre{margin:0 0 .625rem}.mp-ai-prose ul,.mp-ai-prose ol{padding-inline-start:1.25rem}.mp-ai-prose code{padding:.1em .35em;border-radius:.25rem;background:var(--mp-surface, rgba(0, 0, 0, .04));border:1px solid var(--mp-border, rgba(0, 0, 0, .1));color:inherit;font-family:var(--bs-font-monospace, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);font-size:.85em}.mp-ai-prose pre{direction:ltr;unicode-bidi:isolate;padding:.625rem .75rem;overflow-x:auto;border-radius:.375rem;background:var(--mp-surface, rgba(0, 0, 0, .04));border:1px solid var(--mp-border, rgba(0, 0, 0, .1));font-size:.8125rem}.mp-ai-prose pre code{padding:0;border:0;background:none;font-size:inherit}.mp-ai-chat{display:flex;flex-direction:column;min-height:16rem;max-height:var(--mp-ai-chat-height, 28rem);overflow:hidden;border:1px solid var(--mp-border, rgba(0, 0, 0, .1));border-radius:var(--mp-radius, .5rem);background:var(--mp-ai-surface);color:var(--mp-ai-fg)}.mp-ai-chat-header{display:flex;align-items:center;gap:.5rem;padding:.625rem .875rem;border-bottom:1px solid var(--mp-border, rgba(0, 0, 0, .1))}.mp-ai-chat-log{display:flex;flex:1;flex-direction:column;gap:.75rem;padding:1rem;overflow-y:auto}.mp-ai-chat-empty{margin:auto;color:var(--mp-ai-fg-muted);font-size:.875rem;text-align:center}.mp-ai-chat-msg{display:flex}.mp-ai-chat-msg.is-user{justify-content:flex-end}.mp-ai-chat-bubble{max-width:85%;padding:.5rem .75rem;border-radius:.875rem;font-size:.9rem;overflow-wrap:anywhere}.is-user>.mp-ai-chat-bubble{border-end-end-radius:.25rem;background:var(--mp-primary, #4f46e5);color:#fff}.is-assistant>.mp-ai-chat-bubble{border:1px solid var(--mp-border, rgba(0, 0, 0, .1));border-end-start-radius:.25rem;background:var(--mp-surface, rgba(0, 0, 0, .02))}.mp-ai-chat-form{display:flex;align-items:flex-end;gap:.5rem;padding:.75rem;border-top:1px solid var(--mp-border, rgba(0, 0, 0, .1))}.mp-ai-chat-input{flex:1;min-height:2.375rem;max-height:8rem;padding:.45rem .75rem;resize:vertical;border:1px solid var(--mp-ai-border-color);border-radius:.5rem;background:var(--mp-ai-bg);color:inherit;font:inherit;font-size:.9rem}.mp-ai-chat-input:focus{outline:2px solid var(--mp-ai-text);outline-offset:1px;border-color:transparent}.mp-ai-diff{overflow:hidden;border:1px solid var(--mp-border, rgba(0, 0, 0, .1));border-radius:var(--mp-radius, .5rem);background:var(--mp-ai-surface);color:var(--mp-ai-fg)}.mp-ai-diff-header{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem .5rem;padding:.5rem .75rem;border-bottom:1px solid var(--mp-border, rgba(0, 0, 0, .1));font-size:.8125rem}.mp-ai-diff-header>.mp-ai-badge{flex:none}.mp-ai-diff-title{flex:1 1 12rem;min-width:0;font-weight:500;line-height:1.35;overflow-wrap:anywhere}.mp-ai-diff-stats{flex:none;display:inline-flex;margin-inline-start:auto;gap:.5rem;font-family:var(--bs-font-monospace, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)}.mp-ai-diff-stat-add{color:var(--mp-ai-success-text)}.mp-ai-diff-stat-del{color:var(--mp-ai-danger-text)}.mp-ai-diff-body{direction:ltr;unicode-bidi:isolate;max-height:24rem;margin:0;padding:.375rem 0;overflow:auto;font-family:var(--bs-font-monospace, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);font-size:.8125rem;line-height:1.55}.mp-ai-diff-line{display:block;min-width:max-content;padding-inline-end:.75rem;white-space:pre}.mp-ai-diff-sign{display:inline-block;width:1.75rem;color:var(--mp-ai-fg-muted);text-align:center;-webkit-user-select:none;user-select:none}.mp-ai-diff-line.is-add{background:var(--mp-ai-diff-add-bg);color:var(--mp-ai-diff-add-text)}.mp-ai-diff-line.is-del{background:var(--mp-ai-diff-del-bg);color:var(--mp-ai-diff-del-text)}.mp-ai-diff-line.is-add .mp-ai-diff-sign,.mp-ai-diff-line.is-del .mp-ai-diff-sign{color:inherit}.mp-ai-diff-actions{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.5rem .75rem;border-top:1px solid var(--mp-border, rgba(0, 0, 0, .1))}.mp-ai-diff-status{margin-inline-end:auto;color:var(--mp-ai-fg-muted);font-size:.8125rem}.mp-ai-diff.is-applied .mp-ai-diff-status{color:var(--mp-ai-success-text)}.mp-ai-action{padding:.3rem .85rem;border:1px solid var(--mp-ai-border-color);border-radius:.375rem;background:transparent;color:var(--mp-ai-fg);font:inherit;font-size:.8125rem;font-weight:500;cursor:pointer}.mp-ai-action:hover{background:var(--mp-surface, rgba(0, 0, 0, .04))}.mp-ai-action-primary{border-color:var(--mp-primary, #4f46e5);background:var(--mp-primary, #4f46e5);color:#fff}.mp-ai-action-primary:hover{border-color:var(--mp-primary-hover, #4338ca);background:var(--mp-primary-hover, #4338ca)}.mp-ai-action:focus-visible{outline:2px solid var(--mp-ai-text);outline-offset:2px}.mp-ai-action:disabled{cursor:not-allowed;opacity:.6}.mp-ai-edge-glow{position:fixed;inset:0;z-index:1090;pointer-events:none;animation:mp-ai-fade-in .4s ease-out}.mp-ai-edge-glow[hidden]{display:none}.mp-ai-edge-glow:before,.mp-ai-edge-glow:after{content:"";position:absolute;inset:0;background:conic-gradient(from var(--mp-ai-angle) at 50% 50%,var(--mp-ai-stops));animation:mp-ai-spin var(--mp-ai-duration) linear infinite}.mp-ai-edge-glow:before{padding:3px;-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box exclude,linear-gradient(#000 0 0)}.mp-ai-edge-glow:after{opacity:var(--mp-ai-edge-glow-opacity);-webkit-mask-image:linear-gradient(to right,#000,transparent var(--mp-ai-edge-width),transparent calc(100% - var(--mp-ai-edge-width)),#000),linear-gradient(to bottom,#000,transparent var(--mp-ai-edge-width),transparent calc(100% - var(--mp-ai-edge-width)),#000);mask-image:linear-gradient(to right,#000,transparent var(--mp-ai-edge-width),transparent calc(100% - var(--mp-ai-edge-width)),#000),linear-gradient(to bottom,#000,transparent var(--mp-ai-edge-width),transparent calc(100% - var(--mp-ai-edge-width)),#000)}.mp-ai-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}@media(prefers-reduced-motion:reduce){.mp-ai-halo:before,.mp-ai-halo:after,.mp-ai-button:before,.mp-ai-button:after,.mp-ai-panel:before,.mp-ai-panel:after,.mp-ai-thinking:before,.mp-ai-thinking:after,.mp-ai-edge-glow,.mp-ai-edge-glow:before,.mp-ai-edge-glow:after,.mp-ai-caret{animation:none!important}.mp-ai-button:after{transition:none}}@media(forced-colors:active){.mp-ai-halo,.mp-ai-button,.mp-ai-badge,.mp-ai-panel{border:1px solid CanvasText}.mp-ai-edge-glow{display:none}}
3
+ /*# sourceMappingURL=ai.min.css.map */