@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 +294 -37
- package/dist/ai.css +63 -16
- package/dist/ai.min.css +3 -0
- package/dist/ai.min.css.map +1 -0
- package/dist/kit.css +689 -21
- package/dist/kit.d.mts +37 -0
- package/dist/kit.d.ts +513 -0
- package/dist/kit.js +1978 -485
- package/dist/kit.min.css +3 -0
- package/dist/kit.min.css.map +1 -0
- package/dist/kit.min.js +13 -0
- package/dist/kit.min.js.map +1 -0
- package/dist/kit.mjs +2315 -0
- package/dist/theme-boot.js +2 -0
- package/package.json +39 -3
- package/scripts/copy-assets.js +81 -15
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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/
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
673
|
+
margin-inline-end: auto;
|
|
627
674
|
color: var(--mp-ai-fg-muted);
|
|
628
675
|
font-size: 0.8125rem;
|
|
629
676
|
}
|
package/dist/ai.min.css
ADDED
|
@@ -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 */
|