@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/dist/kit.mjs ADDED
@@ -0,0 +1,2315 @@
1
+ /* @mp-consulting/homebridge-ui-kit v1.3.0
2
+ Brand design system for Homebridge plugins
3
+ https://github.com/mp-consulting/homebridge-ui-kit */
4
+
5
+ // src/js/env.js
6
+ var root = typeof window !== "undefined" ? window : globalThis;
7
+ function doc() {
8
+ return typeof document !== "undefined" ? document : root.document;
9
+ }
10
+ function hbClient() {
11
+ return root.homebridge || (typeof homebridge !== "undefined" ? homebridge : void 0);
12
+ }
13
+
14
+ // src/js/util.js
15
+ var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
16
+ function escapeHtml(value) {
17
+ return String(value == null ? "" : value).replace(/[&<>"']/g, (c) => ESCAPES[c]);
18
+ }
19
+ function safeUrl(url) {
20
+ if (typeof url !== "string") {
21
+ return null;
22
+ }
23
+ return /^https?:\/\//i.test(url.trim()) ? url.trim() : null;
24
+ }
25
+ var uidSeq = 0;
26
+ function uid(prefix) {
27
+ uidSeq += 1;
28
+ return (prefix || "mp") + "-" + uidSeq;
29
+ }
30
+ function toElement(target) {
31
+ if (!target) {
32
+ return null;
33
+ }
34
+ if (typeof target === "string") {
35
+ const d = doc();
36
+ return d ? d.querySelector(target) : null;
37
+ }
38
+ return target;
39
+ }
40
+ function errorMessage(err, fallback) {
41
+ if (typeof err === "string" && err) {
42
+ return err;
43
+ }
44
+ return err && err.message || fallback;
45
+ }
46
+ var liveRegion = null;
47
+ function announce(message, politeness) {
48
+ const d = doc();
49
+ if (!d || !d.body || typeof d.createElement !== "function") {
50
+ return;
51
+ }
52
+ if (!liveRegion || !liveRegion.isConnected) {
53
+ liveRegion = d.createElement("div");
54
+ liveRegion.className = "mp-sr-only";
55
+ liveRegion.setAttribute("data-mp-live", "");
56
+ liveRegion.setAttribute("aria-atomic", "true");
57
+ d.body.appendChild(liveRegion);
58
+ }
59
+ liveRegion.setAttribute("aria-live", politeness === "assertive" ? "assertive" : "polite");
60
+ liveRegion.textContent = "";
61
+ const text = String(message == null ? "" : message);
62
+ setTimeout(() => {
63
+ liveRegion.textContent = text;
64
+ }, 30);
65
+ }
66
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
67
+ function focusables(el) {
68
+ return Array.from(el.querySelectorAll(FOCUSABLE)).filter((n) => !n.hidden && !n.closest("[hidden]"));
69
+ }
70
+ function focusElement(el) {
71
+ if (!el || typeof el.focus !== "function") {
72
+ return;
73
+ }
74
+ if (!el.matches(FOCUSABLE) && !el.hasAttribute("tabindex")) {
75
+ el.setAttribute("tabindex", "-1");
76
+ el.classList.add("mp-focus-target");
77
+ }
78
+ try {
79
+ el.focus({ preventScroll: true });
80
+ } catch {
81
+ el.focus();
82
+ }
83
+ }
84
+ function textOf(el) {
85
+ return el ? String(el.textContent || "").replace(/\s+/g, " ").trim() : "";
86
+ }
87
+
88
+ // src/js/core.js
89
+ function badge(badgeClass, dotClass, label) {
90
+ return '<span class="badge ' + badgeClass + '">' + (dotClass ? '<span class="mp-status ' + dotClass + '" aria-hidden="true"></span>' : "") + escapeHtml(label) + "</span>";
91
+ }
92
+ var FOOTER_LINKS = [
93
+ {
94
+ key: "github",
95
+ text: "GitHub",
96
+ paths: [
97
+ "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"
98
+ ]
99
+ },
100
+ {
101
+ key: "npm",
102
+ text: "npm",
103
+ paths: [
104
+ "M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5l2.404.961L10.404 2zm3.564 1.426L5.596 5 8 5.961 14.154 3.5zm3.25 1.7-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464z"
105
+ ]
106
+ },
107
+ {
108
+ key: "changelog",
109
+ text: "Changelog",
110
+ paths: [
111
+ "M8.515 1.019A7 7 0 0 0 8 1V0a8 8 0 0 1 .589.022zm2.004.45a7 7 0 0 0-.985-.299l.219-.976q.576.129 1.126.342zm1.37.71a7 7 0 0 0-.439-.27l.493-.87a8 8 0 0 1 .979.654l-.615.789a7 7 0 0 0-.418-.302zm1.834 1.79a7 7 0 0 0-.653-.796l.724-.69q.406.429.747.91zm.744 1.352a7 7 0 0 0-.214-.468l.893-.45a8 8 0 0 1 .45 1.088l-.95.313a7 7 0 0 0-.179-.483m.53 2.507a7 7 0 0 0-.1-1.025l.985-.17q.1.58.116 1.17zm-.131 1.538q.05-.254.081-.51l.993.123a8 8 0 0 1-.23 1.155l-.964-.267q.069-.247.12-.501m-.952 2.379q.276-.436.486-.908l.914.405q-.24.54-.555 1.038zm-.964 1.205q.183-.183.35-.378l.758.653a8 8 0 0 1-.401.432z",
112
+ "M8 1a7 7 0 1 0 4.95 11.95l.707.707A8.001 8.001 0 1 1 8 0z",
113
+ "M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5"
114
+ ]
115
+ }
116
+ ];
117
+ function icon(paths) {
118
+ return '<svg class="mp-footer-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false">' + paths.map((d) => '<path d="' + d + '"/>').join("") + "</svg>";
119
+ }
120
+ var StatusBadge = {
121
+ online: (label) => badge("bg-success-subtle text-success-emphasis", "mp-status-online", label || "Online"),
122
+ offline: (label) => badge("bg-danger-subtle text-danger-emphasis", "mp-status-offline", label || "Offline"),
123
+ checking: (label) => badge("bg-secondary-subtle text-secondary-emphasis", "mp-status-checking", label || "Checking…"),
124
+ disabled: (label) => badge("bg-secondary", null, label || "Disabled")
125
+ };
126
+ var EmptyState = {
127
+ render(opts) {
128
+ opts = opts || {};
129
+ const iconClass = opts.iconClass || "bi bi-inbox";
130
+ const title = opts.title || "No items";
131
+ const hint = opts.hint || "";
132
+ return '<div class="mp-empty-state"><i class="' + escapeHtml(iconClass) + ' mp-empty-state-icon" aria-hidden="true"></i><p class="mp-empty-state-title">' + escapeHtml(title) + "</p>" + (hint ? '<p class="mp-empty-state-hint">' + escapeHtml(hint) + "</p>" : "") + "</div>";
133
+ }
134
+ };
135
+ var Loading = {
136
+ render(message) {
137
+ message = message || "Loading…";
138
+ return '<div class="mp-loading" role="status" aria-live="polite"><div class="spinner-border spinner-border-sm text-secondary" aria-hidden="true"></div><span>' + escapeHtml(message) + "</span></div>";
139
+ }
140
+ };
141
+ var HEADING = "h1, h2, h3, h4, h5, h6, [data-mp-focus]";
142
+ var View = {
143
+ show(id, opts) {
144
+ opts = opts || {};
145
+ let shown = null;
146
+ let changed = false;
147
+ let previous = false;
148
+ doc().querySelectorAll(".mp-view").forEach((v) => {
149
+ const active = v.id === id;
150
+ const was = v.classList.contains("active");
151
+ if (active) {
152
+ shown = v;
153
+ changed = !was;
154
+ } else if (was) {
155
+ previous = true;
156
+ }
157
+ v.classList.toggle("active", active);
158
+ });
159
+ if (!shown || !changed || typeof shown.querySelector !== "function") {
160
+ return shown;
161
+ }
162
+ const heading = shown.querySelector(HEADING);
163
+ if (opts.focus === void 0 ? previous : opts.focus) {
164
+ focusElement(heading || shown);
165
+ }
166
+ if (opts.announce === void 0 ? previous : opts.announce) {
167
+ const label = typeof opts.announce === "string" ? opts.announce : shown.getAttribute("aria-label") || textOf(heading);
168
+ if (label) {
169
+ announce(label);
170
+ }
171
+ }
172
+ return shown;
173
+ }
174
+ };
175
+ var Footer = {
176
+ render(opts) {
177
+ opts = opts || {};
178
+ const el = toElement(opts.target || ".mp-footer");
179
+ if (!el) {
180
+ return;
181
+ }
182
+ const links = [];
183
+ FOOTER_LINKS.forEach((link) => {
184
+ const url = safeUrl(opts[link.key]);
185
+ if (!url) {
186
+ return;
187
+ }
188
+ links.push('<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + icon(link.paths) + link.text + "</a>");
189
+ });
190
+ el.innerHTML = links.join('<span class="mp-footer-sep" aria-hidden="true">|</span>');
191
+ }
192
+ };
193
+
194
+ // src/js/ai.js
195
+ var SPARKLE = '<svg class="mp-ai-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 0c.4 3.9 2.1 5.6 6 6-3.9.4-5.6 2.1-6 6-.4-3.9-2.1-5.6-6-6 3.9-.4 5.6-2.1 6-6z"/><path d="M13.5 10.5c.15 1.5.85 2.2 2.5 2.5-1.65.3-2.35 1-2.5 2.5-.15-1.5-.85-2.2-2.5-2.5 1.65-.3 2.35-1 2.5-2.5z"/></svg>';
196
+ var CARET = '<span class="mp-ai-caret" aria-hidden="true"></span>';
197
+ var DEFAULT_NOTE = "Generated by the Assistant. Check it before acting on it.";
198
+ var DEFAULT_ERROR = "The Assistant could not answer. Please try again.";
199
+ var LCS_LIMIT = 4e6;
200
+ var aiSeq = 0;
201
+ function newRequestId() {
202
+ aiSeq += 1;
203
+ return "mpai-" + Date.now().toString(36) + "-" + aiSeq.toString(36) + "-" + Math.random().toString(36).slice(2, 8);
204
+ }
205
+ function eventData(ev) {
206
+ if (ev && ev.data !== void 0) {
207
+ return ev.data;
208
+ }
209
+ if (ev && ev.detail !== void 0) {
210
+ return ev.detail;
211
+ }
212
+ return ev;
213
+ }
214
+ function abortError(reason) {
215
+ if (reason != null) {
216
+ return reason;
217
+ }
218
+ var message = "The Assistant request was cancelled";
219
+ if (typeof DOMException === "function") {
220
+ try {
221
+ return new DOMException(message, "AbortError");
222
+ } catch (e) {
223
+ }
224
+ }
225
+ var err = new Error(message);
226
+ err.name = "AbortError";
227
+ return err;
228
+ }
229
+ function notifyCancel(hb, requestId) {
230
+ try {
231
+ Promise.resolve(hb.request("/ai/cancel", { requestId })).catch(function() {
232
+ });
233
+ } catch (e) {
234
+ }
235
+ }
236
+ function aiRequest(path, body, opts) {
237
+ opts = opts || {};
238
+ var hb = hbClient();
239
+ if (!hb || typeof hb.request !== "function") {
240
+ return withCancel(Promise.reject(new Error("MpKit.ai needs the Homebridge plugin UI (window.homebridge)")));
241
+ }
242
+ var signal = opts.signal;
243
+ if (signal && signal.aborted) {
244
+ return withCancel(Promise.reject(abortError(signal.reason)));
245
+ }
246
+ var requestId = newRequestId();
247
+ var payload = {};
248
+ Object.keys(body || {}).forEach(function(k) {
249
+ payload[k] = body[k];
250
+ });
251
+ payload.requestId = requestId;
252
+ var settled = false;
253
+ var listeners = [];
254
+ if (typeof hb.addEventListener === "function") {
255
+ var on = function(type, fn) {
256
+ var handler = function(ev) {
257
+ var data = eventData(ev);
258
+ if (!settled && data && data.requestId === requestId) {
259
+ fn(data);
260
+ }
261
+ };
262
+ hb.addEventListener(type, handler);
263
+ listeners.push([type, handler]);
264
+ };
265
+ if (typeof opts.onChunk === "function") {
266
+ on("ai:chunk", function(d) {
267
+ if (typeof d.delta === "string" && d.delta) {
268
+ opts.onChunk(d.delta, d);
269
+ }
270
+ });
271
+ }
272
+ if (typeof opts.onDone === "function") {
273
+ on("ai:done", function(d) {
274
+ opts.onDone(d);
275
+ });
276
+ }
277
+ if (typeof opts.onError === "function") {
278
+ on("ai:error", function(d) {
279
+ opts.onError(d.message || DEFAULT_ERROR, d);
280
+ });
281
+ }
282
+ }
283
+ var rejectCancelled;
284
+ var cancelled = new Promise(function(resolve, reject) {
285
+ rejectCancelled = reject;
286
+ });
287
+ cancelled.catch(function() {
288
+ });
289
+ function onAbort() {
290
+ cancel(signal.reason);
291
+ }
292
+ function cleanup() {
293
+ settled = true;
294
+ if (signal && typeof signal.removeEventListener === "function") {
295
+ signal.removeEventListener("abort", onAbort);
296
+ }
297
+ if (typeof hb.removeEventListener !== "function") {
298
+ return;
299
+ }
300
+ listeners.forEach(function(l) {
301
+ hb.removeEventListener(l[0], l[1]);
302
+ });
303
+ listeners = [];
304
+ }
305
+ function cancel(reason) {
306
+ if (settled) {
307
+ return false;
308
+ }
309
+ cleanup();
310
+ if (opts.notifyServer !== false) {
311
+ notifyCancel(hb, requestId);
312
+ }
313
+ rejectCancelled(abortError(reason));
314
+ return true;
315
+ }
316
+ if (signal && typeof signal.addEventListener === "function") {
317
+ signal.addEventListener("abort", onAbort);
318
+ }
319
+ var pending;
320
+ try {
321
+ pending = Promise.resolve(hb.request(path, payload));
322
+ } catch (err) {
323
+ pending = Promise.reject(err);
324
+ }
325
+ var result = Promise.race([pending, cancelled]).then(function(value) {
326
+ cleanup();
327
+ return value;
328
+ }, function(err) {
329
+ cleanup();
330
+ throw err;
331
+ });
332
+ return withCancel(result, cancel, requestId);
333
+ }
334
+ function withCancel(promise, cancel, requestId) {
335
+ promise.cancel = cancel || function() {
336
+ return false;
337
+ };
338
+ promise.requestId = requestId || null;
339
+ return promise;
340
+ }
341
+ function inlineMarkdown(text) {
342
+ return String(text).split(/(`[^`\n]+`)/).map(function(part, i) {
343
+ if (i % 2 === 1) {
344
+ return "<code>" + escapeHtml(part.slice(1, -1)) + "</code>";
345
+ }
346
+ return escapeHtml(part).replace(/\*\*([^*\n]+?)\*\*|__([^_\n]+?)__/g, function(m, a, b) {
347
+ return "<strong>" + (a || b) + "</strong>";
348
+ });
349
+ }).join("");
350
+ }
351
+ var UL_ITEM = /^\s*[-*+]\s+(.*)$/;
352
+ var OL_ITEM = /^\s*\d+[.)]\s+(.*)$/;
353
+ var HEADING2 = /^\s*#{1,6}\s+(.*)$/;
354
+ var FENCE = /^\s*(```|~~~)/;
355
+ function markdown(text) {
356
+ var lines = String(text == null ? "" : text).replace(/\r\n?/g, "\n").split("\n");
357
+ var out = [];
358
+ var para = [];
359
+ var i = 0;
360
+ function flush() {
361
+ if (para.length) {
362
+ out.push("<p>" + para.map(inlineMarkdown).join("<br>") + "</p>");
363
+ para = [];
364
+ }
365
+ }
366
+ while (i < lines.length) {
367
+ var line = lines[i];
368
+ var fence = FENCE.exec(line);
369
+ if (fence) {
370
+ flush();
371
+ var code = [];
372
+ i += 1;
373
+ while (i < lines.length && lines[i].trim().indexOf(fence[1]) !== 0) {
374
+ code.push(lines[i]);
375
+ i += 1;
376
+ }
377
+ i += 1;
378
+ out.push("<pre><code>" + escapeHtml(code.join("\n")) + "</code></pre>");
379
+ continue;
380
+ }
381
+ var listRe = UL_ITEM.test(line) ? UL_ITEM : OL_ITEM.test(line) ? OL_ITEM : null;
382
+ if (listRe) {
383
+ flush();
384
+ var tag = listRe === UL_ITEM ? "ul" : "ol";
385
+ var items = [];
386
+ while (i < lines.length && listRe.test(lines[i])) {
387
+ items.push("<li>" + inlineMarkdown(listRe.exec(lines[i])[1]) + "</li>");
388
+ i += 1;
389
+ }
390
+ out.push("<" + tag + ">" + items.join("") + "</" + tag + ">");
391
+ continue;
392
+ }
393
+ var heading = HEADING2.exec(line);
394
+ if (heading) {
395
+ flush();
396
+ out.push("<p><strong>" + inlineMarkdown(heading[1]) + "</strong></p>");
397
+ } else if (!line.trim()) {
398
+ flush();
399
+ } else {
400
+ para.push(line);
401
+ }
402
+ i += 1;
403
+ }
404
+ flush();
405
+ return out.join("");
406
+ }
407
+ function withCaret(html) {
408
+ if (/<\/p>$/.test(html)) {
409
+ return html.slice(0, -4) + CARET + "</p>";
410
+ }
411
+ if (/<\/li><\/[ou]l>$/.test(html)) {
412
+ return html.slice(0, -10) + CARET + html.slice(-10);
413
+ }
414
+ return html + CARET;
415
+ }
416
+ function toLines(value) {
417
+ if (value == null) {
418
+ return [];
419
+ }
420
+ var text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
421
+ text = text.replace(/\r\n?/g, "\n");
422
+ if (text === "") {
423
+ return [];
424
+ }
425
+ if (text.charAt(text.length - 1) === "\n") {
426
+ text = text.slice(0, -1);
427
+ }
428
+ return text.split("\n");
429
+ }
430
+ function diffLines(before, after) {
431
+ var a = toLines(before);
432
+ var b = toLines(after);
433
+ var out = [];
434
+ var start = 0;
435
+ while (start < a.length && start < b.length && a[start] === b[start]) {
436
+ out.push({ type: "same", text: a[start] });
437
+ start += 1;
438
+ }
439
+ var endA = a.length;
440
+ var endB = b.length;
441
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
442
+ endA -= 1;
443
+ endB -= 1;
444
+ }
445
+ var x = a.slice(start, endA);
446
+ var y = b.slice(start, endB);
447
+ var n = x.length;
448
+ var m = y.length;
449
+ var i;
450
+ var j;
451
+ if (n * m > LCS_LIMIT) {
452
+ x.forEach(function(t) {
453
+ out.push({ type: "del", text: t });
454
+ });
455
+ y.forEach(function(t) {
456
+ out.push({ type: "add", text: t });
457
+ });
458
+ } else {
459
+ var w = m + 1;
460
+ var dp = new Int32Array((n + 1) * w);
461
+ for (i = n - 1; i >= 0; i -= 1) {
462
+ for (j = m - 1; j >= 0; j -= 1) {
463
+ dp[i * w + j] = x[i] === y[j] ? dp[(i + 1) * w + j + 1] + 1 : Math.max(dp[(i + 1) * w + j], dp[i * w + j + 1]);
464
+ }
465
+ }
466
+ i = 0;
467
+ j = 0;
468
+ while (i < n && j < m) {
469
+ if (x[i] === y[j]) {
470
+ out.push({ type: "same", text: x[i] });
471
+ i += 1;
472
+ j += 1;
473
+ } else if (dp[(i + 1) * w + j] >= dp[i * w + j + 1]) {
474
+ out.push({ type: "del", text: x[i] });
475
+ i += 1;
476
+ } else {
477
+ out.push({ type: "add", text: y[j] });
478
+ j += 1;
479
+ }
480
+ }
481
+ for (; i < n; i += 1) {
482
+ out.push({ type: "del", text: x[i] });
483
+ }
484
+ for (; j < m; j += 1) {
485
+ out.push({ type: "add", text: y[j] });
486
+ }
487
+ }
488
+ for (i = endA; i < a.length; i += 1) {
489
+ out.push({ type: "same", text: a[i] });
490
+ }
491
+ return out;
492
+ }
493
+ var DIFF_SIGN = { add: "+", del: "−", same: " " };
494
+ var DIFF_SR = { add: "Added: ", del: "Removed: ", same: "" };
495
+ function diffLineHtml(line) {
496
+ return '<span class="mp-ai-diff-line is-' + line.type + '"><span class="mp-ai-diff-sign" aria-hidden="true">' + DIFF_SIGN[line.type] + "</span>" + (DIFF_SR[line.type] ? '<span class="mp-ai-sr-only">' + DIFF_SR[line.type] + "</span>" : "") + escapeHtml(line.text) + "</span>";
497
+ }
498
+ function bind(el, type, fn) {
499
+ var store2 = el.__mpAiListeners || (el.__mpAiListeners = {});
500
+ if (store2[type]) {
501
+ el.removeEventListener(type, store2[type]);
502
+ }
503
+ store2[type] = fn;
504
+ el.addEventListener(type, fn);
505
+ }
506
+ function actionTarget(ev) {
507
+ var t = ev && ev.target;
508
+ return t && typeof t.closest === "function" ? t.closest("[data-mp-ai-action]") : null;
509
+ }
510
+ function badgeHtml(label) {
511
+ return '<span class="mp-ai-badge">' + SPARKLE + escapeHtml(label || "Assistant") + "</span>";
512
+ }
513
+ function thinkingHtml(label) {
514
+ return '<span class="mp-ai-thinking" role="status">' + escapeHtml(label || "Thinking…") + "</span>";
515
+ }
516
+ var edgeEl = null;
517
+ var ai = {
518
+ /** GET-style status of the Assistant: { enabled, provider, model, capabilities }. */
519
+ status: function() {
520
+ var hb = hbClient();
521
+ if (!hb || typeof hb.request !== "function") {
522
+ return Promise.reject(new Error("MpKit.ai needs the Homebridge plugin UI (window.homebridge)"));
523
+ }
524
+ return Promise.resolve(hb.request("/ai/status"));
525
+ },
526
+ /**
527
+ * Explains a device/plugin error. body: { error, context?, device? } → { text, usage }
528
+ * opts: { onChunk, onDone, onError, signal, notifyServer = true }; the
529
+ * returned promise has .cancel() and .requestId.
530
+ */
531
+ explain: function(body, opts) {
532
+ return aiRequest("/ai/explain", body, opts);
533
+ },
534
+ /** Free-form question. body: { prompt, context? } → { text, usage } */
535
+ ask: function(body, opts) {
536
+ return aiRequest("/ai/ask", body, opts);
537
+ },
538
+ /** Generates plugin config. body: { schema, request, current? } → { config, explanation, usage } */
539
+ config: function(body, opts) {
540
+ return aiRequest("/ai/config", body, opts);
541
+ },
542
+ markdown,
543
+ diffLines,
544
+ /**
545
+ * Returns HTML for an Assistant button (sparkle icon + escaped label).
546
+ * opts: { label = 'Ask Assistant', id, className, size: 'sm' | 'lg', title }
547
+ */
548
+ renderButton: function(opts) {
549
+ opts = opts || {};
550
+ var cls = "mp-ai-button" + (opts.size === "sm" || opts.size === "lg" ? " mp-ai-button-" + opts.size : "") + (opts.className ? " " + escapeHtml(opts.className) : "");
551
+ return '<button type="button" class="' + cls + '"' + (opts.id ? ' id="' + escapeHtml(opts.id) + '"' : "") + (opts.title ? ' title="' + escapeHtml(opts.title) + '"' : "") + ">" + SPARKLE + "<span>" + escapeHtml(opts.label || "Ask Assistant") + "</span></button>";
552
+ },
553
+ /** Returns HTML for a small "Assistant" badge. */
554
+ renderBadge: badgeHtml,
555
+ /** Returns HTML for a pulsing "Thinking…" indicator (a status live region). */
556
+ renderThinking: thinkingHtml,
557
+ /**
558
+ * Renders an answer panel into el and returns a controller for streaming:
559
+ * const answer = MpKit.ai.renderAnswer(el);
560
+ * const res = await MpKit.ai.ask({ prompt }, { onChunk: answer.append });
561
+ * answer.done(res.text);
562
+ * opts: { label = 'Assistant', title, text, streaming = true, note }
563
+ * The text is rendered as escaped, safe markdown.
564
+ */
565
+ renderAnswer: function(el, opts) {
566
+ opts = opts || {};
567
+ var note = opts.note === void 0 ? DEFAULT_NOTE : opts.note;
568
+ el.innerHTML = '<section class="mp-ai-panel" aria-label="' + escapeHtml(opts.label || "Assistant") + ' answer"><div class="mp-ai-panel-header">' + badgeHtml(opts.label) + (opts.title ? '<p class="mp-ai-panel-title">' + escapeHtml(opts.title) + "</p>" : "") + '</div><div class="mp-ai-panel-body mp-ai-prose"></div><p class="mp-ai-error" role="alert" hidden></p>' + (note ? '<p class="mp-ai-panel-note">' + escapeHtml(note) + "</p>" : "") + "</section>";
569
+ var panel = el.querySelector(".mp-ai-panel");
570
+ var body = el.querySelector(".mp-ai-panel-body");
571
+ var errorEl = el.querySelector(".mp-ai-error");
572
+ var text = opts.text == null ? "" : String(opts.text);
573
+ var streaming = opts.streaming !== false;
574
+ function paint() {
575
+ var html = markdown(text);
576
+ if (streaming) {
577
+ html = text ? withCaret(html) : thinkingHtml();
578
+ }
579
+ body.innerHTML = html;
580
+ panel.classList.toggle("is-streaming", streaming);
581
+ panel.setAttribute("aria-busy", streaming ? "true" : "false");
582
+ }
583
+ var controller = {
584
+ append: function(delta) {
585
+ text += delta == null ? "" : String(delta);
586
+ paint();
587
+ return controller;
588
+ },
589
+ setText: function(value) {
590
+ text = value == null ? "" : String(value);
591
+ paint();
592
+ return controller;
593
+ },
594
+ done: function(result) {
595
+ var final = result && typeof result === "object" ? result.text : result;
596
+ if (typeof final === "string" && final) {
597
+ text = final;
598
+ }
599
+ streaming = false;
600
+ paint();
601
+ return controller;
602
+ },
603
+ error: function(err) {
604
+ streaming = false;
605
+ paint();
606
+ errorEl.textContent = errorMessage(err, DEFAULT_ERROR);
607
+ errorEl.hidden = false;
608
+ return controller;
609
+ },
610
+ getText: function() {
611
+ return text;
612
+ }
613
+ };
614
+ paint();
615
+ return controller;
616
+ },
617
+ /**
618
+ * Renders a line diff with Apply / Reject buttons into el.
619
+ * before/after may be strings or JSON-serialisable values (pretty-printed).
620
+ * onApply / onReject receive { before, after, lines }; they may return a
621
+ * promise — if it rejects, the buttons are re-enabled and the error shown.
622
+ * opts: { before, after, onApply, onReject, title, applyLabel, rejectLabel }
623
+ * Returns { lines, added, removed }.
624
+ */
625
+ renderDiff: function(el, opts) {
626
+ opts = opts || {};
627
+ var lines = diffLines(opts.before, opts.after);
628
+ var added = lines.filter(function(l) {
629
+ return l.type === "add";
630
+ }).length;
631
+ var removed = lines.filter(function(l) {
632
+ return l.type === "del";
633
+ }).length;
634
+ var hasActions = typeof opts.onApply === "function" || typeof opts.onReject === "function";
635
+ var title = opts.title || "Suggested changes";
636
+ el.innerHTML = '<div class="mp-ai-diff"><div class="mp-ai-diff-header">' + badgeHtml(opts.label) + '<span class="mp-ai-diff-title">' + escapeHtml(title) + '</span><span class="mp-ai-diff-stats"><span class="mp-ai-diff-stat-add">+' + added + '<span class="mp-ai-sr-only"> added</span></span><span class="mp-ai-diff-stat-del">−' + removed + '<span class="mp-ai-sr-only"> removed</span></span></span></div><pre class="mp-ai-diff-body" tabindex="0" aria-label="' + escapeHtml(title) + '"><code>' + (added || removed ? lines.map(diffLineHtml).join("") : '<span class="mp-ai-diff-line">No changes</span>') + "</code></pre>" + (hasActions ? '<div class="mp-ai-diff-actions"><span class="mp-ai-diff-status" role="status"></span><button type="button" class="mp-ai-action" data-mp-ai-action="reject">' + escapeHtml(opts.rejectLabel || "Reject") + '</button><button type="button" class="mp-ai-action mp-ai-action-primary" data-mp-ai-action="apply">' + escapeHtml(opts.applyLabel || "Apply") + "</button></div>" : "") + "</div>";
637
+ var result = { lines, added, removed };
638
+ if (!hasActions) {
639
+ return result;
640
+ }
641
+ var root2 = el.querySelector(".mp-ai-diff");
642
+ var statusEl = el.querySelector(".mp-ai-diff-status");
643
+ var applyBtn = el.querySelector('[data-mp-ai-action="apply"]');
644
+ var rejectBtn = el.querySelector('[data-mp-ai-action="reject"]');
645
+ var settled = false;
646
+ var busy = false;
647
+ function setDisabled(disabled) {
648
+ applyBtn.disabled = disabled;
649
+ rejectBtn.disabled = disabled;
650
+ }
651
+ bind(el, "click", function(ev) {
652
+ var btn = actionTarget(ev);
653
+ if (!btn || settled || busy) {
654
+ return;
655
+ }
656
+ var action = btn.getAttribute("data-mp-ai-action");
657
+ if (action !== "apply" && action !== "reject") {
658
+ return;
659
+ }
660
+ var cb = action === "apply" ? opts.onApply : opts.onReject;
661
+ busy = true;
662
+ setDisabled(true);
663
+ statusEl.textContent = "";
664
+ Promise.resolve().then(function() {
665
+ return typeof cb === "function" ? cb({ before: opts.before, after: opts.after, lines }) : void 0;
666
+ }).then(function() {
667
+ busy = false;
668
+ settled = true;
669
+ root2.classList.add(action === "apply" ? "is-applied" : "is-rejected");
670
+ statusEl.textContent = action === "apply" ? "Applied" : "Rejected";
671
+ }, function(err) {
672
+ busy = false;
673
+ setDisabled(false);
674
+ statusEl.textContent = errorMessage(err, "Something went wrong");
675
+ });
676
+ });
677
+ return result;
678
+ },
679
+ /**
680
+ * Renders a chat (log + input) into el. Each message is sent with
681
+ * MpKit.ai.ask({ prompt, context }) and streamed into the log; the context is
682
+ * opts.context plus the recent conversation. Pass opts.onSend(prompt,
683
+ * { onChunk, history }) to use another transport; it may resolve with a
684
+ * string or { text }.
685
+ * opts: { label = 'Assistant', context, placeholder, emptyText, onSend }
686
+ * onSend also receives ctx.signal (an AbortSignal) for chat.cancel().
687
+ * Returns { send(text?), cancel(), clear(), messages() }.
688
+ */
689
+ renderChat: function(el, opts) {
690
+ opts = opts || {};
691
+ var inputId = uid("mp-ai-chat-input");
692
+ el.innerHTML = '<div class="mp-ai-chat"><div class="mp-ai-chat-header">' + badgeHtml(opts.label) + '</div><div class="mp-ai-chat-log" role="log" aria-live="polite" aria-label="' + escapeHtml(opts.label || "Assistant") + ' conversation"></div><form class="mp-ai-chat-form"><label class="mp-ai-sr-only" for="' + inputId + '">Message</label><textarea class="mp-ai-chat-input" id="' + inputId + '" rows="1" placeholder="' + escapeHtml(opts.placeholder || "Ask the Assistant…") + '"></textarea><button type="submit" class="mp-ai-button">' + SPARKLE + "<span>Send</span></button></form></div>";
693
+ var log = el.querySelector(".mp-ai-chat-log");
694
+ var input = el.querySelector(".mp-ai-chat-input");
695
+ var sendBtn = el.querySelector(".mp-ai-chat-form .mp-ai-button");
696
+ var messages = [];
697
+ var busy = false;
698
+ function messageHtml(m) {
699
+ if (m.role === "user") {
700
+ return '<div class="mp-ai-chat-msg is-user"><div class="mp-ai-chat-bubble"><span class="mp-ai-sr-only">You: </span>' + escapeHtml(m.text).replace(/\n/g, "<br>") + "</div></div>";
701
+ }
702
+ var html = markdown(m.text);
703
+ if (m.streaming) {
704
+ html = m.text ? withCaret(html) : thinkingHtml();
705
+ }
706
+ if (m.error) {
707
+ html += '<p class="mp-ai-error" role="alert">' + escapeHtml(m.error) + "</p>";
708
+ }
709
+ if (m.stopped) {
710
+ html += '<p class="mp-ai-panel-note">Stopped</p>';
711
+ }
712
+ return '<div class="mp-ai-chat-msg is-assistant"><div class="mp-ai-chat-bubble mp-ai-prose"><span class="mp-ai-sr-only">' + escapeHtml(opts.label || "Assistant") + ": </span>" + html + "</div></div>";
713
+ }
714
+ function paint() {
715
+ log.innerHTML = messages.length ? messages.map(messageHtml).join("") : '<p class="mp-ai-chat-empty">' + escapeHtml(opts.emptyText || "Ask the Assistant a question.") + "</p>";
716
+ log.setAttribute("aria-busy", busy ? "true" : "false");
717
+ log.scrollTop = log.scrollHeight;
718
+ sendBtn.disabled = busy;
719
+ sendBtn.setAttribute("aria-busy", busy ? "true" : "false");
720
+ }
721
+ function defaultSend(prompt, ctx) {
722
+ var transcript = ctx.history.slice(-10).map(function(m) {
723
+ return (m.role === "user" ? "User" : "Assistant") + ": " + m.text;
724
+ }).join("\n");
725
+ var context = [opts.context, transcript ? "Conversation so far:\n" + transcript : ""].filter(Boolean).join("\n\n");
726
+ var body = { prompt };
727
+ if (context) {
728
+ body.context = context;
729
+ }
730
+ return ai.ask(body, { onChunk: ctx.onChunk, signal: ctx.signal });
731
+ }
732
+ var controller = null;
733
+ function send(text) {
734
+ var prompt = String(text == null ? input.value : text).trim();
735
+ if (!prompt || busy) {
736
+ return Promise.resolve(null);
737
+ }
738
+ if (text == null) {
739
+ input.value = "";
740
+ }
741
+ var history = messages.filter(function(m) {
742
+ return !m.error && !m.stopped;
743
+ }).map(function(m) {
744
+ return { role: m.role, text: m.text };
745
+ });
746
+ var reply = { role: "assistant", text: "", streaming: true };
747
+ messages.push({ role: "user", text: prompt }, reply);
748
+ busy = true;
749
+ paint();
750
+ var handler = typeof opts.onSend === "function" ? opts.onSend : defaultSend;
751
+ controller = typeof AbortController === "function" ? new AbortController() : null;
752
+ var signal = controller ? controller.signal : void 0;
753
+ return Promise.resolve().then(function() {
754
+ return handler(prompt, {
755
+ history,
756
+ signal,
757
+ onChunk: function(delta) {
758
+ reply.text += delta == null ? "" : String(delta);
759
+ paint();
760
+ }
761
+ });
762
+ }).then(function(res) {
763
+ var final = res && typeof res === "object" ? res.text : res;
764
+ if (typeof final === "string" && final) {
765
+ reply.text = final;
766
+ }
767
+ }, function(err) {
768
+ if (err && err.name === "AbortError" || signal && signal.aborted) {
769
+ reply.stopped = true;
770
+ } else {
771
+ reply.error = errorMessage(err, DEFAULT_ERROR);
772
+ }
773
+ }).then(function() {
774
+ reply.streaming = false;
775
+ busy = false;
776
+ controller = null;
777
+ paint();
778
+ return reply.error || reply.stopped ? null : reply.text;
779
+ });
780
+ }
781
+ bind(el, "submit", function(ev) {
782
+ ev.preventDefault();
783
+ send();
784
+ });
785
+ bind(el, "keydown", function(ev) {
786
+ var t = ev.target;
787
+ if (ev.key === "Enter" && !ev.shiftKey && !ev.isComposing && t === input) {
788
+ ev.preventDefault();
789
+ send();
790
+ }
791
+ });
792
+ paint();
793
+ return {
794
+ send,
795
+ /** Stops the reply in progress (aborts the signal passed to onSend / ask). */
796
+ cancel: function() {
797
+ if (!controller) {
798
+ return false;
799
+ }
800
+ controller.abort();
801
+ return true;
802
+ },
803
+ clear: function() {
804
+ messages = [];
805
+ paint();
806
+ },
807
+ messages: function() {
808
+ return messages.map(function(m) {
809
+ return { role: m.role, text: m.text, error: m.error };
810
+ });
811
+ }
812
+ };
813
+ },
814
+ /**
815
+ * Shows (default) or hides a fixed glow around the page edges, e.g. while
816
+ * the Assistant works. Returns the element.
817
+ */
818
+ edgeGlow: function(show2) {
819
+ if (show2 === false) {
820
+ if (edgeEl) {
821
+ edgeEl.hidden = true;
822
+ }
823
+ return edgeEl;
824
+ }
825
+ if (!edgeEl) {
826
+ edgeEl = doc().createElement("div");
827
+ edgeEl.className = "mp-ai-edge-glow";
828
+ edgeEl.setAttribute("aria-hidden", "true");
829
+ doc().body.appendChild(edgeEl);
830
+ }
831
+ edgeEl.hidden = false;
832
+ return edgeEl;
833
+ }
834
+ };
835
+
836
+ // src/js/theme.js
837
+ var THEME_STORAGE_KEY = "mp-kit-theme";
838
+ var QUERY = "(prefers-color-scheme: dark)";
839
+ function normalizeTheme(value) {
840
+ if (typeof value !== "string") {
841
+ return null;
842
+ }
843
+ const v = value.trim().toLowerCase();
844
+ if (v === "dark" || v === "dark-mode") {
845
+ return "dark";
846
+ }
847
+ if (v === "light" || v === "light-mode") {
848
+ return "light";
849
+ }
850
+ if (v === "auto" || v === "system") {
851
+ return "auto";
852
+ }
853
+ return null;
854
+ }
855
+ function resolveTheme(preference, systemDark) {
856
+ return preference === "dark" || preference !== "light" && !!systemDark ? "dark" : "light";
857
+ }
858
+ function themeFromSettings(settings) {
859
+ if (!settings || typeof settings !== "object") {
860
+ return null;
861
+ }
862
+ return normalizeTheme(settings.colorScheme) || normalizeTheme(settings.theme) || normalizeTheme(settings.lightingMode);
863
+ }
864
+ function mediaQuery() {
865
+ try {
866
+ return typeof root.matchMedia === "function" ? root.matchMedia(QUERY) : null;
867
+ } catch {
868
+ return null;
869
+ }
870
+ }
871
+ function store(key, value) {
872
+ if (!key) {
873
+ return;
874
+ }
875
+ try {
876
+ root.localStorage.setItem(key, value);
877
+ } catch {
878
+ }
879
+ }
880
+ function stored(key) {
881
+ if (!key) {
882
+ return null;
883
+ }
884
+ try {
885
+ return normalizeTheme(root.localStorage.getItem(key));
886
+ } catch {
887
+ return null;
888
+ }
889
+ }
890
+ async function homebridgeTheme() {
891
+ const hb = hbClient();
892
+ if (!hb) {
893
+ return null;
894
+ }
895
+ if (typeof hb.getUserSettings === "function") {
896
+ try {
897
+ const pref = themeFromSettings(await hb.getUserSettings());
898
+ if (pref) {
899
+ return pref;
900
+ }
901
+ } catch {
902
+ }
903
+ }
904
+ if (typeof hb.userCurrentLightingMode === "function") {
905
+ try {
906
+ const pref = normalizeTheme(await hb.userCurrentLightingMode());
907
+ if (pref) {
908
+ return pref;
909
+ }
910
+ } catch {
911
+ }
912
+ }
913
+ return null;
914
+ }
915
+ function applyTheme(resolved, opts) {
916
+ opts = opts || {};
917
+ const d = doc();
918
+ const target = opts.target || d && d.documentElement;
919
+ if (!target) {
920
+ return;
921
+ }
922
+ const dark = resolved === "dark";
923
+ if (opts.attribute !== false) {
924
+ target.setAttribute("data-bs-theme", dark ? "dark" : "light");
925
+ }
926
+ if (opts.className !== false) {
927
+ target.classList.toggle("mp-theme-dark", dark);
928
+ }
929
+ }
930
+ var Theme = {
931
+ /**
932
+ * Applies the theme now (stored/system preference), then the Homebridge
933
+ * user's setting once it arrives, and follows system changes while the
934
+ * preference is 'auto'.
935
+ * opts: { target = <html>, theme ('light'|'dark'|'auto': skip Homebridge),
936
+ * homebridge = true, storageKey = 'mp-kit-theme' (false: none),
937
+ * attribute = true, className = true, onChange(resolved, preference) }
938
+ */
939
+ init(opts) {
940
+ opts = opts || {};
941
+ const key = opts.storageKey === void 0 ? THEME_STORAGE_KEY : opts.storageKey;
942
+ const explicit = normalizeTheme(opts.theme);
943
+ let preference = explicit || stored(key) || "auto";
944
+ let current = null;
945
+ let pinned = !!explicit;
946
+ let destroyed = false;
947
+ const media = mediaQuery();
948
+ function update() {
949
+ const resolved = resolveTheme(preference, media && media.matches);
950
+ applyTheme(resolved, opts);
951
+ store(key, preference);
952
+ if (resolved !== current) {
953
+ const first = current === null;
954
+ current = resolved;
955
+ if (!first && typeof opts.onChange === "function") {
956
+ opts.onChange(resolved, preference);
957
+ }
958
+ }
959
+ }
960
+ function onMedia() {
961
+ if (!destroyed && preference === "auto") {
962
+ update();
963
+ }
964
+ }
965
+ if (media) {
966
+ if (typeof media.addEventListener === "function") {
967
+ media.addEventListener("change", onMedia);
968
+ } else if (typeof media.addListener === "function") {
969
+ media.addListener(onMedia);
970
+ }
971
+ }
972
+ update();
973
+ const controller = {
974
+ /** Resolves with the resolved theme once the Homebridge setting was applied. */
975
+ ready: null,
976
+ preference: () => preference,
977
+ resolved: () => current,
978
+ /** Overrides the preference ('light' | 'dark' | 'auto'). */
979
+ set(mode) {
980
+ preference = normalizeTheme(mode) || "auto";
981
+ pinned = true;
982
+ update();
983
+ return current;
984
+ },
985
+ destroy() {
986
+ destroyed = true;
987
+ if (!media) {
988
+ return;
989
+ }
990
+ if (typeof media.removeEventListener === "function") {
991
+ media.removeEventListener("change", onMedia);
992
+ } else if (typeof media.removeListener === "function") {
993
+ media.removeListener(onMedia);
994
+ }
995
+ }
996
+ };
997
+ controller.ready = (pinned || opts.homebridge === false ? Promise.resolve(null) : homebridgeTheme()).then((pref) => {
998
+ if (pref && !pinned && !destroyed) {
999
+ preference = pref;
1000
+ update();
1001
+ }
1002
+ return current;
1003
+ });
1004
+ return controller;
1005
+ },
1006
+ apply: applyTheme,
1007
+ resolve: resolveTheme,
1008
+ normalize: normalizeTheme,
1009
+ fromSettings: themeFromSettings
1010
+ };
1011
+
1012
+ // src/js/tabs.js
1013
+ function panelId(tab) {
1014
+ const ref = tab.getAttribute("data-mp-view") || tab.getAttribute("aria-controls") || tab.getAttribute("data-bs-target") || tab.getAttribute("href") || "";
1015
+ const id = ref.charAt(0) === "#" ? ref.slice(1) : ref;
1016
+ return /^[A-Za-z][\w:.-]*$/.test(id) ? id : null;
1017
+ }
1018
+ function isRtl(el) {
1019
+ const holder = el.closest("[dir]");
1020
+ if (holder) {
1021
+ return holder.getAttribute("dir").toLowerCase() === "rtl";
1022
+ }
1023
+ try {
1024
+ const view = el.ownerDocument.defaultView;
1025
+ return view.getComputedStyle(el).direction === "rtl";
1026
+ } catch {
1027
+ return false;
1028
+ }
1029
+ }
1030
+ var Tabs = {
1031
+ /**
1032
+ * Enhances a tab bar. el: .mp-tabs element or selector.
1033
+ * opts: { activation: 'auto' | 'manual', onChange(tab, index, panel) }
1034
+ * Tabs reference their panel with data-mp-view (an .mp-view id, shown with
1035
+ * MpKit.View.show), aria-controls, data-bs-target or href="#id".
1036
+ * Returns { select(indexOrTab), selected(), tabs(), destroy() }.
1037
+ */
1038
+ init(el, opts) {
1039
+ opts = opts || {};
1040
+ const root2 = typeof el === "string" ? doc().querySelector(el) : el;
1041
+ if (!root2) {
1042
+ return null;
1043
+ }
1044
+ const tabs = Array.from(root2.querySelectorAll('.nav-link, [role="tab"]'));
1045
+ const vertical = root2.getAttribute("aria-orientation") === "vertical";
1046
+ root2.setAttribute("role", "tablist");
1047
+ tabs.forEach((tab) => {
1048
+ if (tab.parentElement !== root2 && tab.parentElement.tagName === "LI") {
1049
+ tab.parentElement.setAttribute("role", "presentation");
1050
+ }
1051
+ tab.setAttribute("role", "tab");
1052
+ if (!tab.id) {
1053
+ tab.id = uid("mp-tab");
1054
+ }
1055
+ const id = panelId(tab);
1056
+ const panel = id && root2.ownerDocument.getElementById(id);
1057
+ if (panel) {
1058
+ tab.setAttribute("aria-controls", id);
1059
+ panel.setAttribute("role", "tabpanel");
1060
+ if (!panel.hasAttribute("aria-labelledby")) {
1061
+ panel.setAttribute("aria-labelledby", tab.id);
1062
+ }
1063
+ }
1064
+ });
1065
+ let current = -1;
1066
+ function panelOf(tab) {
1067
+ const id = tab.getAttribute("aria-controls");
1068
+ return id ? root2.ownerDocument.getElementById(id) : null;
1069
+ }
1070
+ function select(target, fromUser) {
1071
+ const index = typeof target === "number" ? target : tabs.indexOf(target);
1072
+ if (index < 0 || index >= tabs.length) {
1073
+ return;
1074
+ }
1075
+ const changed = index !== current;
1076
+ tabs.forEach((tab, i) => {
1077
+ const on = i === index;
1078
+ tab.setAttribute("aria-selected", on ? "true" : "false");
1079
+ tab.setAttribute("tabindex", on ? "0" : "-1");
1080
+ tab.classList.toggle("active", on);
1081
+ const panel2 = panelOf(tab);
1082
+ if (!panel2 || panel2.classList.contains("mp-view")) {
1083
+ return;
1084
+ }
1085
+ if (panel2.classList.contains("tab-pane")) {
1086
+ panel2.classList.toggle("active", on);
1087
+ panel2.classList.toggle("show", on);
1088
+ } else {
1089
+ panel2.hidden = !on;
1090
+ }
1091
+ });
1092
+ const panel = panelOf(tabs[index]);
1093
+ if (panel && panel.classList.contains("mp-view")) {
1094
+ View.show(panel.id, { focus: false, announce: false });
1095
+ }
1096
+ current = index;
1097
+ if (changed && fromUser && typeof opts.onChange === "function") {
1098
+ opts.onChange(tabs[index], index, panel);
1099
+ }
1100
+ }
1101
+ function onClick2(ev) {
1102
+ const tab = ev.target.closest('[role="tab"]');
1103
+ if (!tab || !root2.contains(tab)) {
1104
+ return;
1105
+ }
1106
+ if (tab.tagName === "A") {
1107
+ ev.preventDefault();
1108
+ }
1109
+ select(tab, true);
1110
+ }
1111
+ function onKeydown2(ev) {
1112
+ const tab = ev.target.closest('[role="tab"]');
1113
+ if (!tab || !root2.contains(tab)) {
1114
+ return;
1115
+ }
1116
+ const enabled = tabs.filter((t) => !t.disabled && t.getAttribute("aria-disabled") !== "true");
1117
+ const pos = enabled.indexOf(tab);
1118
+ const rtl = !vertical && isRtl(root2);
1119
+ const next = vertical ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
1120
+ const prev = vertical ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
1121
+ let to = null;
1122
+ if (ev.key === next) {
1123
+ to = enabled[(pos + 1) % enabled.length];
1124
+ } else if (ev.key === prev) {
1125
+ to = enabled[(pos - 1 + enabled.length) % enabled.length];
1126
+ } else if (ev.key === "Home") {
1127
+ to = enabled[0];
1128
+ } else if (ev.key === "End") {
1129
+ to = enabled[enabled.length - 1];
1130
+ } else if ((ev.key === "Enter" || ev.key === " ") && opts.activation === "manual") {
1131
+ ev.preventDefault();
1132
+ select(tab, true);
1133
+ return;
1134
+ }
1135
+ if (!to) {
1136
+ return;
1137
+ }
1138
+ ev.preventDefault();
1139
+ if (opts.activation === "manual") {
1140
+ tabs.forEach((t) => t.setAttribute("tabindex", t === to ? "0" : "-1"));
1141
+ } else {
1142
+ select(to, true);
1143
+ }
1144
+ to.focus();
1145
+ }
1146
+ root2.addEventListener("click", onClick2);
1147
+ root2.addEventListener("keydown", onKeydown2);
1148
+ const initial = tabs.findIndex((t) => t.classList.contains("active") || t.getAttribute("aria-selected") === "true");
1149
+ select(initial < 0 ? 0 : initial, false);
1150
+ return {
1151
+ select: (target) => select(target, true),
1152
+ selected: () => current,
1153
+ tabs: () => tabs.slice(),
1154
+ destroy() {
1155
+ root2.removeEventListener("click", onClick2);
1156
+ root2.removeEventListener("keydown", onKeydown2);
1157
+ }
1158
+ };
1159
+ }
1160
+ };
1161
+
1162
+ // src/js/toast.js
1163
+ var TYPES = ["success", "error", "warning", "info"];
1164
+ var DURATION = { success: 5e3, info: 5e3, warning: 7e3, error: 8e3 };
1165
+ var ICON = { success: "✓", info: "i", warning: "!", error: "✕" };
1166
+ var container = null;
1167
+ function ensureContainer(d) {
1168
+ if (!container || !container.isConnected) {
1169
+ container = d.createElement("div");
1170
+ container.className = "mp-toast-container";
1171
+ d.body.appendChild(container);
1172
+ }
1173
+ return container;
1174
+ }
1175
+ function show(type, message, opts) {
1176
+ if (TYPES.indexOf(type) < 0) {
1177
+ type = "info";
1178
+ }
1179
+ opts = typeof opts === "string" ? { title: opts } : opts || {};
1180
+ const text = String(message == null ? "" : message);
1181
+ const hb = hbClient();
1182
+ if (!opts.local && hb && hb.toast && typeof hb.toast[type] === "function") {
1183
+ hb.toast[type](text, opts.title);
1184
+ return { close() {
1185
+ }, element: null, native: true };
1186
+ }
1187
+ const d = doc();
1188
+ if (!d || !d.body) {
1189
+ return { close() {
1190
+ }, element: null, native: false };
1191
+ }
1192
+ const el = d.createElement("div");
1193
+ el.className = "mp-toast mp-toast-" + type;
1194
+ el.setAttribute("role", type === "error" || type === "warning" ? "alert" : "status");
1195
+ el.setAttribute("aria-atomic", "true");
1196
+ el.innerHTML = '<span class="mp-toast-icon" aria-hidden="true">' + ICON[type] + '</span><div class="mp-toast-body">' + (opts.title ? '<p class="mp-toast-title">' + escapeHtml(opts.title) + "</p>" : "") + '<p class="mp-toast-message">' + escapeHtml(text) + '</p></div><button type="button" class="mp-toast-close" aria-label="Close">×</button>';
1197
+ ensureContainer(d).appendChild(el);
1198
+ let timer = null;
1199
+ let closed = false;
1200
+ const duration = opts.duration === void 0 ? DURATION[type] : Number(opts.duration) || 0;
1201
+ function close() {
1202
+ if (closed) {
1203
+ return;
1204
+ }
1205
+ closed = true;
1206
+ clearTimeout(timer);
1207
+ el.remove();
1208
+ }
1209
+ function arm() {
1210
+ clearTimeout(timer);
1211
+ if (duration > 0) {
1212
+ timer = setTimeout(close, duration);
1213
+ }
1214
+ }
1215
+ el.addEventListener("mouseenter", () => clearTimeout(timer));
1216
+ el.addEventListener("mouseleave", arm);
1217
+ el.addEventListener("focusin", () => clearTimeout(timer));
1218
+ el.addEventListener("focusout", arm);
1219
+ el.querySelector(".mp-toast-close").addEventListener("click", close);
1220
+ arm();
1221
+ return { close, element: el, native: false };
1222
+ }
1223
+ var Toast = {
1224
+ show,
1225
+ success: (message, opts) => show("success", message, opts),
1226
+ error: (message, opts) => show("error", message, opts),
1227
+ warning: (message, opts) => show("warning", message, opts),
1228
+ info: (message, opts) => show("info", message, opts)
1229
+ };
1230
+
1231
+ // src/js/dialog.js
1232
+ function confirm(opts) {
1233
+ opts = typeof opts === "string" ? { message: opts } : opts || {};
1234
+ const d = doc();
1235
+ if (!d || !d.body) {
1236
+ return Promise.resolve(false);
1237
+ }
1238
+ const titleId = uid("mp-dialog-title");
1239
+ const messageId = uid("mp-dialog-message");
1240
+ const danger = !!opts.danger;
1241
+ const backdrop = d.createElement("div");
1242
+ backdrop.className = "mp-dialog-backdrop";
1243
+ backdrop.innerHTML = '<div class="mp-dialog' + (danger ? " mp-dialog-danger" : "") + '" role="alertdialog" aria-modal="true" aria-labelledby="' + titleId + '"' + (opts.message ? ' aria-describedby="' + messageId + '"' : "") + '><h2 class="mp-dialog-title" id="' + titleId + '">' + escapeHtml(opts.title || "Are you sure?") + "</h2>" + (opts.message ? '<p class="mp-dialog-message" id="' + messageId + '">' + escapeHtml(opts.message) + "</p>" : "") + '<div class="mp-dialog-actions"><button type="button" class="btn btn-outline-secondary" data-mp-dialog="cancel">' + escapeHtml(opts.cancelLabel || "Cancel") + '</button><button type="button" class="btn ' + (danger ? "btn-danger" : "btn-primary") + '" data-mp-dialog="confirm">' + escapeHtml(opts.confirmLabel || "Confirm") + "</button></div></div>";
1244
+ const dialog = backdrop.firstChild;
1245
+ const previous = d.activeElement;
1246
+ const inerted = Array.from(d.body.children).filter((n) => !n.hasAttribute("inert") && !n.hasAttribute("data-mp-live") && !n.classList.contains("mp-toast-container"));
1247
+ inerted.forEach((n) => n.setAttribute("inert", ""));
1248
+ d.body.appendChild(backdrop);
1249
+ d.body.classList.add("mp-dialog-open");
1250
+ return new Promise((resolve) => {
1251
+ function finish(result) {
1252
+ d.removeEventListener("keydown", onKey, true);
1253
+ backdrop.remove();
1254
+ inerted.forEach((n) => n.removeAttribute("inert"));
1255
+ if (!d.querySelector(".mp-dialog-backdrop")) {
1256
+ d.body.classList.remove("mp-dialog-open");
1257
+ }
1258
+ if (previous && typeof previous.focus === "function" && previous.isConnected) {
1259
+ try {
1260
+ previous.focus({ preventScroll: true });
1261
+ } catch {
1262
+ previous.focus();
1263
+ }
1264
+ }
1265
+ resolve(result);
1266
+ }
1267
+ function onKey(ev) {
1268
+ if (ev.key === "Escape") {
1269
+ ev.preventDefault();
1270
+ ev.stopPropagation();
1271
+ finish(false);
1272
+ } else if (ev.key === "Tab") {
1273
+ const items = focusables(dialog);
1274
+ if (!items.length) {
1275
+ return;
1276
+ }
1277
+ const first = items[0];
1278
+ const last = items[items.length - 1];
1279
+ const active = d.activeElement;
1280
+ if (ev.shiftKey && (active === first || !dialog.contains(active))) {
1281
+ ev.preventDefault();
1282
+ last.focus();
1283
+ } else if (!ev.shiftKey && (active === last || !dialog.contains(active))) {
1284
+ ev.preventDefault();
1285
+ first.focus();
1286
+ }
1287
+ }
1288
+ }
1289
+ backdrop.addEventListener("click", (ev) => {
1290
+ const action = ev.target.closest && ev.target.closest("[data-mp-dialog]");
1291
+ if (action) {
1292
+ finish(action.getAttribute("data-mp-dialog") === "confirm");
1293
+ } else if (ev.target === backdrop) {
1294
+ finish(false);
1295
+ }
1296
+ });
1297
+ d.addEventListener("keydown", onKey, true);
1298
+ dialog.querySelector('[data-mp-dialog="' + (danger ? "cancel" : "confirm") + '"]').focus();
1299
+ });
1300
+ }
1301
+
1302
+ // src/js/form.js
1303
+ function copyText(text) {
1304
+ const value = String(text == null ? "" : text);
1305
+ const nav = root.navigator;
1306
+ if (nav && nav.clipboard && typeof nav.clipboard.writeText === "function") {
1307
+ return nav.clipboard.writeText(value).then(() => true, () => legacyCopy(value));
1308
+ }
1309
+ return Promise.resolve(legacyCopy(value));
1310
+ }
1311
+ function legacyCopy(value) {
1312
+ const d = doc();
1313
+ if (!d || !d.body || typeof d.execCommand !== "function") {
1314
+ return false;
1315
+ }
1316
+ const area = d.createElement("textarea");
1317
+ area.value = value;
1318
+ area.setAttribute("readonly", "");
1319
+ area.className = "mp-sr-only";
1320
+ d.body.appendChild(area);
1321
+ area.select();
1322
+ let ok = false;
1323
+ try {
1324
+ ok = d.execCommand("copy");
1325
+ } catch {
1326
+ ok = false;
1327
+ }
1328
+ area.remove();
1329
+ return !!ok;
1330
+ }
1331
+ function attrs(map) {
1332
+ return Object.keys(map).map((k) => {
1333
+ const v = map[k];
1334
+ if (v === void 0 || v === null || v === false) {
1335
+ return "";
1336
+ }
1337
+ return v === true ? " " + k : " " + k + '="' + escapeHtml(v) + '"';
1338
+ }).join("");
1339
+ }
1340
+ var CopyButton = {
1341
+ /** opts: { text, target (selector), label = 'Copy', copiedLabel = 'Copied', className, size: 'sm' } */
1342
+ render(opts) {
1343
+ opts = opts || {};
1344
+ const cls = "btn btn-outline-secondary mp-copy-btn" + (opts.size === "sm" ? " btn-sm" : "") + (opts.className ? " " + opts.className : "");
1345
+ return '<button type="button"' + attrs({
1346
+ class: cls,
1347
+ "data-mp-copy": opts.target ? void 0 : String(opts.text == null ? "" : opts.text),
1348
+ "data-mp-copy-target": opts.target,
1349
+ "data-mp-copied-label": opts.copiedLabel || "Copied",
1350
+ "aria-label": opts.ariaLabel
1351
+ }) + ">" + escapeHtml(opts.label || "Copy") + "</button>";
1352
+ }
1353
+ };
1354
+ function copyFromButton(btn) {
1355
+ let text = btn.getAttribute("data-mp-copy");
1356
+ const targetSel = btn.getAttribute("data-mp-copy-target");
1357
+ if (targetSel) {
1358
+ const target = toElement(targetSel);
1359
+ if (!target) {
1360
+ return Promise.resolve(false);
1361
+ }
1362
+ text = "value" in target && target.tagName !== "BUTTON" ? target.value : target.textContent;
1363
+ }
1364
+ return copyText(text).then((ok) => {
1365
+ const label = btn.getAttribute("data-mp-copied-label") || "Copied";
1366
+ if (!btn.__mpLabel) {
1367
+ btn.__mpLabel = btn.textContent;
1368
+ }
1369
+ btn.textContent = ok ? label : "Copy failed";
1370
+ btn.classList.toggle("is-copied", ok);
1371
+ announce(ok ? "Copied to clipboard" : "Could not copy");
1372
+ clearTimeout(btn.__mpCopyTimer);
1373
+ btn.__mpCopyTimer = setTimeout(() => {
1374
+ btn.textContent = btn.__mpLabel;
1375
+ btn.classList.remove("is-copied");
1376
+ }, 2e3);
1377
+ return ok;
1378
+ });
1379
+ }
1380
+ function toggleReveal(btn) {
1381
+ const input = doc().getElementById(btn.getAttribute("data-mp-reveal"));
1382
+ if (!input) {
1383
+ return;
1384
+ }
1385
+ const show2 = input.type === "password";
1386
+ input.type = show2 ? "text" : "password";
1387
+ btn.setAttribute("aria-pressed", show2 ? "true" : "false");
1388
+ btn.textContent = show2 ? btn.getAttribute("data-mp-hide-label") || "Hide" : btn.getAttribute("data-mp-show-label") || "Show";
1389
+ }
1390
+ function normaliseOptions(options) {
1391
+ return (options || []).map((o) => o && typeof o === "object" ? o : { value: o, label: String(o) });
1392
+ }
1393
+ function fieldParts(opts, kind) {
1394
+ const id = opts.id || uid("mp-field");
1395
+ const helpId = opts.help ? id + "-help" : null;
1396
+ const errorId = opts.error ? id + "-error" : null;
1397
+ const describedBy = [helpId, errorId].filter(Boolean).join(" ") || void 0;
1398
+ const label = '<label class="' + (kind === "check" ? "form-check-label" : "form-label") + '" for="' + escapeHtml(id) + '">' + escapeHtml(opts.label || opts.name || "") + (opts.required ? '<span class="mp-required" aria-hidden="true"> *</span>' : "") + "</label>";
1399
+ const help = opts.help ? '<div class="form-text" id="' + escapeHtml(helpId) + '">' + escapeHtml(opts.help) + "</div>" : "";
1400
+ const error = opts.error ? '<div class="invalid-feedback d-block" id="' + escapeHtml(errorId) + '">' + escapeHtml(opts.error) + "</div>" : "";
1401
+ return { id, describedBy, label, help, error };
1402
+ }
1403
+ function field(opts) {
1404
+ opts = opts || {};
1405
+ const type = opts.type || "text";
1406
+ const check = type === "checkbox" || type === "switch";
1407
+ const p = fieldParts(opts, check ? "check" : "field");
1408
+ const common = {
1409
+ id: p.id,
1410
+ name: opts.name,
1411
+ required: !!opts.required,
1412
+ disabled: !!opts.disabled,
1413
+ "aria-describedby": p.describedBy,
1414
+ "aria-invalid": opts.error ? "true" : void 0
1415
+ };
1416
+ const invalid = opts.error ? " is-invalid" : "";
1417
+ if (check) {
1418
+ return '<div class="form-check' + (type === "switch" ? " form-switch" : "") + ' mb-3 mp-field"><input' + attrs(Object.assign({
1419
+ class: "form-check-input" + invalid,
1420
+ type: "checkbox",
1421
+ role: type === "switch" ? "switch" : void 0,
1422
+ checked: !!(opts.checked !== void 0 ? opts.checked : opts.value)
1423
+ }, common)) + ">" + p.label + p.help + p.error + "</div>";
1424
+ }
1425
+ let control;
1426
+ if (type === "select") {
1427
+ control = "<select" + attrs(Object.assign({ class: "form-select" + invalid, multiple: !!opts.multiple }, common)) + ">" + normaliseOptions(opts.options).map((o) => {
1428
+ const selected = Array.isArray(opts.value) ? opts.value.map(String).indexOf(String(o.value)) >= 0 : String(opts.value) === String(o.value);
1429
+ return "<option" + attrs({ value: String(o.value), selected, disabled: !!o.disabled }) + ">" + escapeHtml(o.label) + "</option>";
1430
+ }).join("") + "</select>";
1431
+ } else if (type === "textarea") {
1432
+ control = "<textarea" + attrs(Object.assign({
1433
+ class: "form-control" + invalid,
1434
+ rows: opts.rows || 3,
1435
+ placeholder: opts.placeholder,
1436
+ readonly: !!opts.readonly
1437
+ }, common)) + ">" + escapeHtml(opts.value == null ? "" : opts.value) + "</textarea>";
1438
+ } else {
1439
+ control = "<input" + attrs(Object.assign({
1440
+ class: "form-control" + invalid,
1441
+ type,
1442
+ value: opts.value == null ? void 0 : String(opts.value),
1443
+ placeholder: opts.placeholder,
1444
+ readonly: !!opts.readonly,
1445
+ min: opts.min,
1446
+ max: opts.max,
1447
+ step: opts.step,
1448
+ pattern: opts.pattern,
1449
+ autocomplete: opts.autocomplete,
1450
+ inputmode: opts.inputmode
1451
+ }, common)) + ">";
1452
+ }
1453
+ return '<div class="mb-3 mp-field">' + p.label + control + p.help + p.error + "</div>";
1454
+ }
1455
+ function secret(opts) {
1456
+ opts = opts || {};
1457
+ const p = fieldParts(opts, "field");
1458
+ const input = "<input" + attrs({
1459
+ class: "form-control mp-secret-input" + (opts.error ? " is-invalid" : ""),
1460
+ type: "password",
1461
+ id: p.id,
1462
+ name: opts.name,
1463
+ value: opts.value == null ? void 0 : String(opts.value),
1464
+ placeholder: opts.placeholder,
1465
+ required: !!opts.required,
1466
+ disabled: !!opts.disabled,
1467
+ readonly: !!opts.readonly,
1468
+ autocomplete: opts.autocomplete || "off",
1469
+ spellcheck: "false",
1470
+ autocapitalize: "off",
1471
+ "aria-describedby": p.describedBy,
1472
+ "aria-invalid": opts.error ? "true" : void 0
1473
+ }) + ">";
1474
+ const label = escapeHtml(opts.label || opts.name || "value");
1475
+ const reveal = opts.reveal === false ? "" : '<button type="button" class="btn btn-outline-secondary mp-reveal-btn" data-mp-reveal="' + escapeHtml(p.id) + '" aria-controls="' + escapeHtml(p.id) + '" aria-pressed="false" aria-label="Show ' + label + '">Show</button>';
1476
+ const copy = opts.copy === false ? "" : CopyButton.render({ target: "#" + p.id, ariaLabel: "Copy " + label });
1477
+ return '<div class="mb-3 mp-field">' + p.label + '<div class="input-group' + (opts.error ? " has-validation" : "") + '">' + input + reveal + copy + "</div>" + p.help + p.error + "</div>";
1478
+ }
1479
+ function setPath(obj, path, value) {
1480
+ const keys = path.split(".");
1481
+ let node = obj;
1482
+ keys.slice(0, -1).forEach((k) => {
1483
+ if (!node[k] || typeof node[k] !== "object") {
1484
+ node[k] = {};
1485
+ }
1486
+ node = node[k];
1487
+ });
1488
+ node[keys[keys.length - 1]] = value;
1489
+ }
1490
+ function getPath(obj, path) {
1491
+ return path.split(".").reduce((node, k) => node == null ? void 0 : node[k], obj);
1492
+ }
1493
+ function controls(form) {
1494
+ return Array.from(form.querySelectorAll("input[name], select[name], textarea[name]")).filter((el) => !el.disabled && !el.hasAttribute("data-mp-ignore") && ["button", "submit", "reset", "file", "image"].indexOf(el.type) < 0);
1495
+ }
1496
+ function values(form) {
1497
+ form = toElement(form);
1498
+ const out = {};
1499
+ const els = controls(form);
1500
+ const counts = {};
1501
+ els.forEach((el) => {
1502
+ if (el.type === "checkbox") {
1503
+ counts[el.name] = (counts[el.name] || 0) + 1;
1504
+ }
1505
+ });
1506
+ els.forEach((el) => {
1507
+ const name = el.name;
1508
+ if (el.type === "checkbox") {
1509
+ if (counts[name] > 1) {
1510
+ const list = getPath(out, name) || [];
1511
+ if (el.checked) {
1512
+ list.push(el.value);
1513
+ }
1514
+ setPath(out, name, list);
1515
+ } else {
1516
+ setPath(out, name, el.checked);
1517
+ }
1518
+ } else if (el.type === "radio") {
1519
+ if (el.checked) {
1520
+ setPath(out, name, el.value);
1521
+ } else if (getPath(out, name) === void 0) {
1522
+ setPath(out, name, null);
1523
+ }
1524
+ } else if (el.type === "number" || el.type === "range") {
1525
+ setPath(out, name, el.value === "" ? null : Number(el.value));
1526
+ } else if (el.tagName === "SELECT" && el.multiple) {
1527
+ setPath(out, name, Array.from(el.selectedOptions).map((o) => o.value));
1528
+ } else {
1529
+ setPath(out, name, el.value);
1530
+ }
1531
+ });
1532
+ return out;
1533
+ }
1534
+ function fill(form, data) {
1535
+ form = toElement(form);
1536
+ data = data || {};
1537
+ controls(form).forEach((el) => {
1538
+ const v = getPath(data, el.name);
1539
+ if (v === void 0) {
1540
+ return;
1541
+ }
1542
+ if (el.type === "checkbox") {
1543
+ el.checked = Array.isArray(v) ? v.map(String).indexOf(el.value) >= 0 : !!v;
1544
+ } else if (el.type === "radio") {
1545
+ el.checked = String(v) === el.value;
1546
+ } else if (el.tagName === "SELECT" && el.multiple) {
1547
+ const list = (Array.isArray(v) ? v : [v]).map(String);
1548
+ Array.from(el.options).forEach((o) => {
1549
+ o.selected = list.indexOf(o.value) >= 0;
1550
+ });
1551
+ } else {
1552
+ el.value = v == null ? "" : String(v);
1553
+ }
1554
+ });
1555
+ return form;
1556
+ }
1557
+ function isPlainObject(v) {
1558
+ return !!v && typeof v === "object" && !Array.isArray(v);
1559
+ }
1560
+ function mergeConfig(base, patch) {
1561
+ const out = Object.assign({}, base);
1562
+ Object.keys(patch || {}).forEach((k) => {
1563
+ out[k] = isPlainObject(out[k]) && isPlainObject(patch[k]) ? mergeConfig(out[k], patch[k]) : patch[k];
1564
+ });
1565
+ return out;
1566
+ }
1567
+ function track(form, opts) {
1568
+ opts = opts || {};
1569
+ form = toElement(form);
1570
+ const d = form.ownerDocument;
1571
+ let baseline = JSON.stringify(values(form));
1572
+ let baselineValues = values(form);
1573
+ let dirty = false;
1574
+ let busy = false;
1575
+ let bar = null;
1576
+ if (opts.bar !== false) {
1577
+ bar = d.createElement("div");
1578
+ bar.className = "mp-savebar";
1579
+ bar.setAttribute("role", "region");
1580
+ bar.setAttribute("aria-label", "Unsaved changes");
1581
+ bar.hidden = true;
1582
+ bar.innerHTML = '<span class="mp-savebar-message" role="status">' + escapeHtml(opts.message || "You have unsaved changes") + '</span><span class="mp-savebar-actions"><button type="button" class="btn btn-sm btn-outline-secondary" data-mp-save="discard">' + escapeHtml(opts.discardLabel || "Discard") + '</button><button type="button" class="btn btn-sm btn-primary" data-mp-save="save">' + escapeHtml(opts.saveLabel || "Save") + "</button></span>";
1583
+ const host = opts.bar && opts.bar !== true ? toElement(opts.bar) : null;
1584
+ if (host) {
1585
+ host.appendChild(bar);
1586
+ } else {
1587
+ form.insertAdjacentElement("afterend", bar);
1588
+ }
1589
+ }
1590
+ function setDirty(next) {
1591
+ if (next === dirty) {
1592
+ return;
1593
+ }
1594
+ dirty = next;
1595
+ if (bar) {
1596
+ bar.hidden = !dirty;
1597
+ }
1598
+ form.classList.toggle("is-dirty", dirty);
1599
+ if (typeof opts.onDirtyChange === "function") {
1600
+ opts.onDirtyChange(dirty);
1601
+ }
1602
+ }
1603
+ function check() {
1604
+ setDirty(JSON.stringify(values(form)) !== baseline);
1605
+ }
1606
+ function reset() {
1607
+ baselineValues = values(form);
1608
+ baseline = JSON.stringify(baselineValues);
1609
+ setDirty(false);
1610
+ }
1611
+ function setBusy(next) {
1612
+ busy = next;
1613
+ if (!bar) {
1614
+ return;
1615
+ }
1616
+ bar.setAttribute("aria-busy", next ? "true" : "false");
1617
+ bar.querySelectorAll("button").forEach((b) => {
1618
+ b.disabled = next;
1619
+ });
1620
+ }
1621
+ async function defaultSave(vals) {
1622
+ const hb = hbClient();
1623
+ if (!hb || typeof hb.updatePluginConfig !== "function") {
1624
+ return null;
1625
+ }
1626
+ const blocks = typeof hb.getPluginConfig === "function" ? await hb.getPluginConfig() || [] : [];
1627
+ let config = typeof opts.toConfig === "function" ? await opts.toConfig(vals, blocks) : [mergeConfig(blocks[0] || {}, vals)].concat(blocks.slice(1));
1628
+ if (!Array.isArray(config)) {
1629
+ config = [config];
1630
+ }
1631
+ await hb.updatePluginConfig(config);
1632
+ if (opts.persist !== false && typeof hb.savePluginConfig === "function") {
1633
+ await hb.savePluginConfig();
1634
+ }
1635
+ return config;
1636
+ }
1637
+ async function save() {
1638
+ if (busy) {
1639
+ return false;
1640
+ }
1641
+ const vals = values(form);
1642
+ setBusy(true);
1643
+ try {
1644
+ const custom = typeof opts.save === "function";
1645
+ const result = custom ? await opts.save(vals) : await defaultSave(vals);
1646
+ const saved = custom || result !== null;
1647
+ reset();
1648
+ if (saved) {
1649
+ if (opts.toast !== false) {
1650
+ Toast.success(opts.savedMessage || "Settings saved");
1651
+ }
1652
+ if (typeof opts.onSaved === "function") {
1653
+ opts.onSaved(result);
1654
+ }
1655
+ }
1656
+ return saved;
1657
+ } catch (err) {
1658
+ if (opts.toast !== false) {
1659
+ Toast.error(errorMessage(err, "Could not save the settings"));
1660
+ }
1661
+ if (typeof opts.onError === "function") {
1662
+ opts.onError(err);
1663
+ }
1664
+ throw err;
1665
+ } finally {
1666
+ setBusy(false);
1667
+ }
1668
+ }
1669
+ function discard() {
1670
+ fill(form, baselineValues);
1671
+ check();
1672
+ }
1673
+ function onBarClick(ev) {
1674
+ const btn = ev.target.closest("[data-mp-save]");
1675
+ if (!btn) {
1676
+ return;
1677
+ }
1678
+ if (btn.getAttribute("data-mp-save") === "save") {
1679
+ save().catch(() => {
1680
+ });
1681
+ } else {
1682
+ discard();
1683
+ }
1684
+ }
1685
+ function onSubmit(ev) {
1686
+ ev.preventDefault();
1687
+ save().catch(() => {
1688
+ });
1689
+ }
1690
+ form.addEventListener("input", check);
1691
+ form.addEventListener("change", check);
1692
+ if (opts.submit !== false) {
1693
+ form.addEventListener("submit", onSubmit);
1694
+ }
1695
+ if (bar) {
1696
+ bar.addEventListener("click", onBarClick);
1697
+ }
1698
+ return {
1699
+ bar,
1700
+ isDirty: () => dirty,
1701
+ values: () => values(form),
1702
+ save,
1703
+ discard,
1704
+ /** Takes the current values as the new saved state. */
1705
+ reset,
1706
+ /** Re-checks after programmatic changes. */
1707
+ check,
1708
+ destroy() {
1709
+ form.removeEventListener("input", check);
1710
+ form.removeEventListener("change", check);
1711
+ form.removeEventListener("submit", onSubmit);
1712
+ if (bar) {
1713
+ bar.remove();
1714
+ }
1715
+ }
1716
+ };
1717
+ }
1718
+ var Form = { field, secret, values, fill, track };
1719
+
1720
+ // src/js/delegates.js
1721
+ var ACTIVATABLE = '.mp-device-card[role="button"], [data-mp-activate][role="button"]';
1722
+ function onClick(ev) {
1723
+ const t = ev.target;
1724
+ if (!t || typeof t.closest !== "function") {
1725
+ return;
1726
+ }
1727
+ const copy = t.closest("[data-mp-copy], [data-mp-copy-target]");
1728
+ if (copy) {
1729
+ copyFromButton(copy);
1730
+ return;
1731
+ }
1732
+ const reveal = t.closest("[data-mp-reveal]");
1733
+ if (reveal) {
1734
+ toggleReveal(reveal);
1735
+ }
1736
+ }
1737
+ function onKeydown(ev) {
1738
+ if (ev.defaultPrevented || ev.key !== "Enter" && ev.key !== " ") {
1739
+ return;
1740
+ }
1741
+ const t = ev.target;
1742
+ if (!t || typeof t.matches !== "function" || !t.matches(ACTIVATABLE)) {
1743
+ return;
1744
+ }
1745
+ ev.preventDefault();
1746
+ t.click();
1747
+ }
1748
+ function installDelegates() {
1749
+ const d = doc();
1750
+ if (!d || typeof d.addEventListener !== "function" || d.__mpKitDelegates) {
1751
+ return;
1752
+ }
1753
+ d.__mpKitDelegates = true;
1754
+ d.addEventListener("click", onClick);
1755
+ d.addEventListener("keydown", onKeydown);
1756
+ }
1757
+
1758
+ // src/js/devices.js
1759
+ var Skeleton = {
1760
+ /** opts: { lines = 3, avatar = false, className } */
1761
+ render(opts) {
1762
+ opts = opts || {};
1763
+ const lines = Math.max(1, opts.lines || 3);
1764
+ let html = "";
1765
+ for (let i = 0; i < lines; i += 1) {
1766
+ html += '<span class="mp-skeleton mp-skeleton-text"' + (i === lines - 1 && lines > 1 ? ' style="width:60%"' : "") + "></span>";
1767
+ }
1768
+ return '<div class="mp-skeleton-group' + (opts.className ? " " + escapeHtml(opts.className) : "") + '" aria-hidden="true">' + (opts.avatar ? '<span class="mp-skeleton mp-skeleton-circle"></span>' : "") + '<span class="mp-skeleton-lines">' + html + "</span></div>";
1769
+ }
1770
+ };
1771
+ var STATUS = {
1772
+ online: StatusBadge.online,
1773
+ offline: StatusBadge.offline,
1774
+ checking: StatusBadge.checking,
1775
+ disabled: StatusBadge.disabled
1776
+ };
1777
+ function defaultItem(item, index, interactive) {
1778
+ const name = item.name || item.displayName || item.id || "Device " + (index + 1);
1779
+ const status = STATUS[item.status] ? STATUS[item.status](item.statusLabel) : "";
1780
+ return '<div class="card mp-device-card"' + (interactive ? ' role="button" tabindex="0"' : "") + '><div class="card-body mp-device-card-body">' + (item.iconClass ? '<i class="' + escapeHtml(item.iconClass) + ' mp-device-card-icon" aria-hidden="true"></i>' : "") + '<div class="mp-device-card-text"><div class="mp-device-card-name">' + escapeHtml(name) + "</div>" + (item.subtitle ? '<div class="mp-device-card-subtitle">' + escapeHtml(item.subtitle) + "</div>" : "") + "</div>" + status + "</div></div>";
1781
+ }
1782
+ function defaultFilter(keys) {
1783
+ return (item, query) => keys.some((k) => {
1784
+ const v = item && item[k];
1785
+ return v != null && String(v).toLowerCase().indexOf(query) >= 0;
1786
+ });
1787
+ }
1788
+ var DeviceList = {
1789
+ /**
1790
+ * Renders a device list into el and returns a controller.
1791
+ * opts: { items = [], loading = false, label = 'Devices',
1792
+ * renderItem(item, index) → HTML (default: an .mp-device-card with
1793
+ * name/displayName, subtitle, iconClass and status badge),
1794
+ * search = true | { placeholder, label }, keys = ['name', 'subtitle', 'id'],
1795
+ * filter(item, query), onActivate(item, index, event),
1796
+ * empty: { iconClass, title, hint }, noResults: { title, hint },
1797
+ * skeletonCount = 3 }
1798
+ * With onActivate the default cards are role="button" (Enter / Space work).
1799
+ * Returns { setItems(items), setLoading(bool), setQuery(q), items(), visible(), destroy() }.
1800
+ */
1801
+ render(el, opts) {
1802
+ opts = opts || {};
1803
+ el = toElement(el);
1804
+ const label = opts.label || "Devices";
1805
+ const searchOpts = opts.search === false ? null : typeof opts.search === "object" ? opts.search : {};
1806
+ const searchId = uid("mp-device-search");
1807
+ const filter = typeof opts.filter === "function" ? opts.filter : defaultFilter(opts.keys || ["name", "displayName", "subtitle", "id"]);
1808
+ const interactive = typeof opts.onActivate === "function";
1809
+ let items = Array.isArray(opts.items) ? opts.items.slice() : [];
1810
+ let loading = !!opts.loading;
1811
+ let query = "";
1812
+ let visible = [];
1813
+ el.innerHTML = '<div class="mp-device-list">' + (searchOpts ? '<div class="mp-device-list-search"><label class="mp-sr-only" for="' + searchId + '">' + escapeHtml(searchOpts.label || "Search " + label.toLowerCase()) + '</label><input type="search" class="form-control" id="' + searchId + '" autocomplete="off" placeholder="' + escapeHtml(searchOpts.placeholder || "Search…") + '"></div>' : "") + '<p class="mp-sr-only" role="status" aria-live="polite" aria-atomic="true"></p><div class="mp-device-list-items" role="list" aria-label="' + escapeHtml(label) + '"></div></div>';
1814
+ const list = el.querySelector(".mp-device-list-items");
1815
+ const status = el.querySelector('.mp-device-list [role="status"]');
1816
+ const input = searchOpts ? el.querySelector('input[type="search"]') : null;
1817
+ function announce2(text) {
1818
+ if (status.textContent !== text) {
1819
+ status.textContent = text;
1820
+ }
1821
+ }
1822
+ function paint() {
1823
+ list.setAttribute("aria-busy", loading ? "true" : "false");
1824
+ if (input) {
1825
+ input.disabled = loading && !items.length;
1826
+ }
1827
+ if (loading && !items.length) {
1828
+ const n = opts.skeletonCount || 3;
1829
+ let html = "";
1830
+ for (let i = 0; i < n; i += 1) {
1831
+ html += '<div class="card mp-device-card mp-device-card-skeleton" role="listitem"><div class="card-body">' + Skeleton.render({ lines: 2, avatar: true }) + "</div></div>";
1832
+ }
1833
+ list.innerHTML = html;
1834
+ announce2("Loading " + label.toLowerCase() + "…");
1835
+ visible = [];
1836
+ return;
1837
+ }
1838
+ visible = query ? items.filter((item) => filter(item, query)) : items.slice();
1839
+ if (!items.length) {
1840
+ list.innerHTML = '<div role="listitem">' + EmptyState.render(Object.assign({ title: "No " + label.toLowerCase() + " found" }, opts.empty)) + "</div>";
1841
+ announce2("No " + label.toLowerCase());
1842
+ return;
1843
+ }
1844
+ if (!visible.length) {
1845
+ const nr = opts.noResults || {};
1846
+ list.innerHTML = '<div role="listitem">' + EmptyState.render({
1847
+ iconClass: nr.iconClass || "bi bi-search",
1848
+ title: nr.title || "No matches for “" + query + "”",
1849
+ hint: nr.hint || "Try a different search"
1850
+ }) + "</div>";
1851
+ announce2("No matches");
1852
+ return;
1853
+ }
1854
+ list.innerHTML = visible.map((item) => {
1855
+ const index = items.indexOf(item);
1856
+ const html = typeof opts.renderItem === "function" ? opts.renderItem(item, index) : defaultItem(item, index, interactive);
1857
+ return '<div role="listitem" class="mp-device-list-item" data-mp-index="' + index + '">' + html + "</div>";
1858
+ }).join("");
1859
+ announce2(query ? visible.length + " of " + items.length + " " + label.toLowerCase() : "");
1860
+ }
1861
+ function onInput() {
1862
+ query = input.value.trim().toLowerCase();
1863
+ paint();
1864
+ }
1865
+ function onClick2(ev) {
1866
+ if (!interactive) {
1867
+ return;
1868
+ }
1869
+ const row = ev.target.closest("[data-mp-index]");
1870
+ if (!row || !list.contains(row)) {
1871
+ return;
1872
+ }
1873
+ const control = ev.target.closest('a, button, input, select, textarea, label, [role="button"]');
1874
+ if (control && row.contains(control) && !control.classList.contains("mp-device-card")) {
1875
+ return;
1876
+ }
1877
+ const index = Number(row.getAttribute("data-mp-index"));
1878
+ opts.onActivate(items[index], index, ev);
1879
+ }
1880
+ if (input) {
1881
+ input.addEventListener("input", onInput);
1882
+ }
1883
+ list.addEventListener("click", onClick2);
1884
+ paint();
1885
+ return {
1886
+ setItems(next) {
1887
+ items = Array.isArray(next) ? next.slice() : [];
1888
+ loading = false;
1889
+ paint();
1890
+ },
1891
+ setLoading(next) {
1892
+ loading = !!next;
1893
+ paint();
1894
+ },
1895
+ setQuery(q) {
1896
+ query = String(q == null ? "" : q).trim().toLowerCase();
1897
+ if (input) {
1898
+ input.value = q == null ? "" : String(q);
1899
+ }
1900
+ paint();
1901
+ },
1902
+ items: () => items.slice(),
1903
+ visible: () => visible.slice(),
1904
+ /** Text announced to screen readers for the current state. */
1905
+ status: () => textOf(status),
1906
+ destroy() {
1907
+ if (input) {
1908
+ input.removeEventListener("input", onInput);
1909
+ }
1910
+ list.removeEventListener("click", onClick2);
1911
+ el.innerHTML = "";
1912
+ }
1913
+ };
1914
+ }
1915
+ };
1916
+
1917
+ // src/js/log.js
1918
+ var ANSI = /[\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
1919
+ function stripAnsi(text) {
1920
+ return String(text == null ? "" : text).replace(ANSI, "");
1921
+ }
1922
+ var NEAR_BOTTOM = 8;
1923
+ var LogViewer = {
1924
+ /**
1925
+ * Creates a log viewer in el.
1926
+ * opts: { maxLines = 1000, label = 'Log', live = 'off' | 'polite',
1927
+ * autoscroll = true, wrap = false, lineClass(line) → extra class,
1928
+ * jumpLabel = 'Jump to latest' }
1929
+ * Returns { append(textOrLines), clear(), lines(), scrollToBottom(), isFollowing(), destroy(), element }.
1930
+ */
1931
+ create(el, opts) {
1932
+ opts = opts || {};
1933
+ el = toElement(el);
1934
+ const d = el.ownerDocument;
1935
+ const maxLines = opts.maxLines > 0 ? opts.maxLines : 1e3;
1936
+ el.innerHTML = '<div class="mp-log' + (opts.wrap ? " mp-log-wrap" : "") + '"><div class="mp-log-body" role="log" tabindex="0"></div><button type="button" class="btn btn-sm btn-primary mp-log-jump" hidden></button></div>';
1937
+ const body = el.querySelector(".mp-log-body");
1938
+ const jump = el.querySelector(".mp-log-jump");
1939
+ body.setAttribute("aria-label", opts.label || "Log");
1940
+ body.setAttribute("aria-live", opts.live === "polite" ? "polite" : "off");
1941
+ jump.textContent = opts.jumpLabel || "Jump to latest";
1942
+ let follow = opts.autoscroll !== false;
1943
+ let lines = [];
1944
+ function atBottom() {
1945
+ return body.scrollHeight - body.scrollTop - body.clientHeight <= NEAR_BOTTOM;
1946
+ }
1947
+ function scrollToBottom() {
1948
+ body.scrollTop = body.scrollHeight;
1949
+ follow = opts.autoscroll !== false;
1950
+ jump.hidden = true;
1951
+ }
1952
+ function onScroll() {
1953
+ if (opts.autoscroll === false) {
1954
+ return;
1955
+ }
1956
+ follow = atBottom();
1957
+ if (follow) {
1958
+ jump.hidden = true;
1959
+ }
1960
+ }
1961
+ function append(input) {
1962
+ const incoming = (Array.isArray(input) ? input : String(input == null ? "" : input).split(/\r?\n/)).map(stripAnsi);
1963
+ if (!Array.isArray(input) && incoming.length > 1 && incoming[incoming.length - 1] === "") {
1964
+ incoming.pop();
1965
+ }
1966
+ if (!incoming.length) {
1967
+ return;
1968
+ }
1969
+ const stick = follow && opts.autoscroll !== false;
1970
+ const frag = d.createDocumentFragment();
1971
+ incoming.forEach((line) => {
1972
+ const row = d.createElement("div");
1973
+ const extra = typeof opts.lineClass === "function" ? opts.lineClass(line) : "";
1974
+ row.className = "mp-log-line" + (extra ? " " + extra : "");
1975
+ row.textContent = line;
1976
+ frag.appendChild(row);
1977
+ });
1978
+ body.appendChild(frag);
1979
+ lines = lines.concat(incoming);
1980
+ const excess = lines.length - maxLines;
1981
+ if (excess > 0) {
1982
+ lines = lines.slice(excess);
1983
+ for (let i = 0; i < excess && body.firstChild; i += 1) {
1984
+ body.removeChild(body.firstChild);
1985
+ }
1986
+ }
1987
+ if (stick) {
1988
+ body.scrollTop = body.scrollHeight;
1989
+ } else if (opts.autoscroll !== false) {
1990
+ jump.hidden = false;
1991
+ }
1992
+ }
1993
+ body.addEventListener("scroll", onScroll);
1994
+ jump.addEventListener("click", () => {
1995
+ scrollToBottom();
1996
+ body.focus();
1997
+ });
1998
+ return {
1999
+ element: body,
2000
+ append,
2001
+ clear() {
2002
+ lines = [];
2003
+ body.textContent = "";
2004
+ jump.hidden = true;
2005
+ },
2006
+ lines: () => lines.slice(),
2007
+ scrollToBottom,
2008
+ isFollowing: () => follow,
2009
+ destroy() {
2010
+ body.removeEventListener("scroll", onScroll);
2011
+ el.innerHTML = "";
2012
+ }
2013
+ };
2014
+ },
2015
+ stripAnsi
2016
+ };
2017
+
2018
+ // src/js/pairing.js
2019
+ function formatPin(code) {
2020
+ const digits = String(code == null ? "" : code).replace(/\D/g, "");
2021
+ return digits.length === 8 ? digits.slice(0, 3) + "-" + digits.slice(3, 5) + "-" + digits.slice(5) : "";
2022
+ }
2023
+ function safeImageSrc(src) {
2024
+ if (typeof src !== "string") {
2025
+ return null;
2026
+ }
2027
+ const s = src.trim();
2028
+ if (safeUrl(s)) {
2029
+ return s;
2030
+ }
2031
+ if (/^data:image\/(png|gif|jpeg|webp|svg\+xml)[;,]/i.test(s)) {
2032
+ return s;
2033
+ }
2034
+ if (/^(\.{0,2}\/|[\w-]+(\/|\.))/.test(s) && !/^[a-z][\w+.-]*:/i.test(s)) {
2035
+ return s;
2036
+ }
2037
+ return null;
2038
+ }
2039
+ function pinHtml(opts) {
2040
+ opts = opts || {};
2041
+ const formatted = formatPin(opts.pin) || escapeHtml(opts.pin || "");
2042
+ const spoken = String(opts.pin == null ? "" : opts.pin).replace(/\D/g, "").split("").join(" ");
2043
+ const id = uid("mp-pin");
2044
+ const src = safeImageSrc(opts.qrSrc);
2045
+ const label = opts.label || "HomeKit setup code";
2046
+ return '<figure class="mp-pin" aria-labelledby="' + id + '"><div class="mp-pin-label-box"><span class="mp-pin-label" id="' + id + '">' + escapeHtml(label) + '</span><span class="mp-pin-code" aria-hidden="true">' + formatted + '</span><span class="mp-sr-only">' + escapeHtml(spoken || formatted) + "</span></div>" + (src ? '<img class="mp-pin-qr" src="' + escapeHtml(src) + '" alt="' + escapeHtml(opts.qrAlt || "QR code for " + label) + '">' : "") + '<span class="mp-pin-qr-slot"></span>' + (opts.copy === false ? "" : '<figcaption class="mp-pin-actions">' + CopyButton.render({ text: formatted, label: opts.copyLabel || "Copy code", size: "sm" }) + "</figcaption>") + "</figure>";
2047
+ }
2048
+ var Pairing = {
2049
+ formatPin,
2050
+ /**
2051
+ * HTML for a HomeKit setup-code block (XXX-XX-XXX, read digit by digit by
2052
+ * screen readers) with an optional QR image and a Copy button.
2053
+ * opts: { pin, label = 'HomeKit setup code', qrSrc (http(s), path or
2054
+ * data:image/*), qrAlt, copy = true, copyLabel }
2055
+ * The kit does not generate QR codes: pass an image, or an SVG element to renderPin().
2056
+ */
2057
+ pin: pinHtml,
2058
+ /** Renders pin() into el; opts.qr may be an SVG/IMG element (appended) or an image URL. */
2059
+ renderPin(el, opts) {
2060
+ opts = opts || {};
2061
+ el = toElement(el);
2062
+ const qr = opts.qr;
2063
+ el.innerHTML = pinHtml(Object.assign({}, opts, { qrSrc: typeof qr === "string" ? qr : opts.qrSrc }));
2064
+ if (qr && typeof qr === "object" && qr.nodeType === 1) {
2065
+ const slot = el.querySelector(".mp-pin-qr-slot");
2066
+ if (qr.tagName.toLowerCase() === "svg") {
2067
+ qr.setAttribute("role", "img");
2068
+ if (!qr.getAttribute("aria-label")) {
2069
+ qr.setAttribute("aria-label", opts.qrAlt || "QR code for " + (opts.label || "HomeKit setup code"));
2070
+ }
2071
+ }
2072
+ qr.classList.add("mp-pin-qr");
2073
+ slot.appendChild(qr);
2074
+ }
2075
+ return el.querySelector(".mp-pin");
2076
+ }
2077
+ };
2078
+ var STATE_TEXT = {
2079
+ waiting: "Waiting for you to sign in…",
2080
+ polling: "Waiting for authorization…",
2081
+ success: "Signed in.",
2082
+ error: "Sign-in failed.",
2083
+ expired: "The code expired. Start again to get a new one."
2084
+ };
2085
+ var Auth = {
2086
+ /**
2087
+ * Renders a device-code / OAuth step card into el:
2088
+ * 1. open the sign-in URL, 2. enter the code (with Copy), then a status
2089
+ * line that follows the polling state.
2090
+ * opts: { title = 'Sign in', url, code, instructions, openLabel,
2091
+ * poll() → true | { done, error, interval } (called every `interval` ms
2092
+ * until done; a rejection or { error } shows the error),
2093
+ * interval = 5000, expiresIn (seconds), onSuccess(result), onError(err),
2094
+ * onRetry() (shows a Try again button on error/expiry), successMessage }
2095
+ * Returns { setState(state, message), state(), start(), stop(), destroy(), element }.
2096
+ */
2097
+ deviceCode(el, opts) {
2098
+ opts = opts || {};
2099
+ el = toElement(el);
2100
+ const titleId = uid("mp-auth-title");
2101
+ const url = safeUrl(opts.url);
2102
+ el.innerHTML = '<section class="card mp-auth-card" aria-labelledby="' + titleId + '"><div class="card-body"><h3 class="mp-auth-title" id="' + titleId + '">' + escapeHtml(opts.title || "Sign in") + "</h3>" + (opts.instructions ? '<p class="mp-auth-instructions">' + escapeHtml(opts.instructions) + "</p>" : "") + '<ol class="mp-auth-steps">' + (url ? '<li><span class="mp-auth-step-text">Open the sign-in page</span><a class="btn btn-primary btn-sm mp-auth-open" href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(opts.openLabel || "Open sign-in page") + '<span class="mp-sr-only"> (opens in a new tab)</span></a></li>' : "") + (opts.code ? '<li><span class="mp-auth-step-text">Enter this code</span><span class="mp-auth-code-row"><code class="mp-auth-code">' + escapeHtml(opts.code) + "</code>" + CopyButton.render({ text: opts.code, label: "Copy", size: "sm", ariaLabel: "Copy code" }) + "</span></li>" : "") + '</ol><p class="mp-auth-status" role="status" aria-live="polite"><span class="mp-auth-status-icon" aria-hidden="true"></span><span class="mp-auth-status-text"></span></p><button type="button" class="btn btn-outline-secondary btn-sm mp-auth-retry" hidden>Try again</button></div></section>';
2103
+ const card = el.querySelector(".mp-auth-card");
2104
+ const statusEl = el.querySelector(".mp-auth-status");
2105
+ const statusText = el.querySelector(".mp-auth-status-text");
2106
+ const retry = el.querySelector(".mp-auth-retry");
2107
+ let state = null;
2108
+ let timer = null;
2109
+ let expiry = null;
2110
+ let running = false;
2111
+ let interval = opts.interval > 0 ? opts.interval : 5e3;
2112
+ function setState(next, message) {
2113
+ state = STATE_TEXT[next] ? next : "waiting";
2114
+ ["waiting", "polling", "success", "error", "expired"].forEach((s) => card.classList.toggle("is-" + s, s === state));
2115
+ statusText.textContent = message || state === "success" && opts.successMessage || STATE_TEXT[state];
2116
+ statusEl.setAttribute("role", state === "error" || state === "expired" ? "alert" : "status");
2117
+ retry.hidden = !((state === "error" || state === "expired") && typeof opts.onRetry === "function");
2118
+ if (state === "success" || state === "error" || state === "expired") {
2119
+ stop();
2120
+ }
2121
+ return controller;
2122
+ }
2123
+ function stop() {
2124
+ running = false;
2125
+ clearTimeout(timer);
2126
+ clearTimeout(expiry);
2127
+ }
2128
+ function schedule() {
2129
+ if (!running) {
2130
+ return;
2131
+ }
2132
+ timer = setTimeout(tick, interval);
2133
+ }
2134
+ function tick() {
2135
+ if (!running) {
2136
+ return;
2137
+ }
2138
+ Promise.resolve().then(() => opts.poll()).then((res) => {
2139
+ if (!running) {
2140
+ return;
2141
+ }
2142
+ if (res && typeof res === "object" && res.error) {
2143
+ throw res.error;
2144
+ }
2145
+ if (res && typeof res === "object" && res.interval > 0) {
2146
+ interval = res.interval;
2147
+ }
2148
+ if (res === true || res && typeof res === "object" && res.done) {
2149
+ setState("success");
2150
+ if (typeof opts.onSuccess === "function") {
2151
+ opts.onSuccess(res);
2152
+ }
2153
+ } else {
2154
+ schedule();
2155
+ }
2156
+ }).catch((err) => {
2157
+ if (!running) {
2158
+ return;
2159
+ }
2160
+ setState("error", err && err.message || (typeof err === "string" ? err : ""));
2161
+ if (typeof opts.onError === "function") {
2162
+ opts.onError(err);
2163
+ }
2164
+ });
2165
+ }
2166
+ function start() {
2167
+ if (typeof opts.poll !== "function") {
2168
+ return controller;
2169
+ }
2170
+ stop();
2171
+ running = true;
2172
+ setState("polling");
2173
+ if (opts.expiresIn > 0) {
2174
+ expiry = setTimeout(() => setState("expired"), opts.expiresIn * 1e3);
2175
+ }
2176
+ schedule();
2177
+ return controller;
2178
+ }
2179
+ retry.addEventListener("click", () => {
2180
+ setState("waiting");
2181
+ opts.onRetry();
2182
+ });
2183
+ const controller = {
2184
+ element: card,
2185
+ setState,
2186
+ state: () => state,
2187
+ start,
2188
+ stop,
2189
+ destroy() {
2190
+ stop();
2191
+ el.innerHTML = "";
2192
+ }
2193
+ };
2194
+ setState("waiting");
2195
+ if (typeof opts.poll === "function" && opts.autoStart !== false) {
2196
+ start();
2197
+ }
2198
+ return controller;
2199
+ }
2200
+ };
2201
+ var Steps = {
2202
+ /**
2203
+ * Renders an .mp-steps progress list into el and shows one panel at a time.
2204
+ * opts: { steps: [{ title, panel (selector/element) }], current = 0,
2205
+ * label = 'Progress', onChange(index, step), focus = true }
2206
+ * Returns { go(index), next(), prev(), current(), destroy() }.
2207
+ */
2208
+ create(el, opts) {
2209
+ opts = opts || {};
2210
+ el = toElement(el);
2211
+ const steps = (opts.steps || []).map((s) => typeof s === "string" ? { title: s } : s);
2212
+ let current = -1;
2213
+ function paint() {
2214
+ el.innerHTML = '<ol class="mp-steps" aria-label="' + escapeHtml(opts.label || "Progress") + '">' + steps.map((s, i) => {
2215
+ const st = i < current ? "complete" : i === current ? "current" : "upcoming";
2216
+ return '<li class="mp-step is-' + st + '"' + (st === "current" ? ' aria-current="step"' : "") + '><span class="mp-step-marker" aria-hidden="true">' + (st === "complete" ? "✓" : i + 1) + '</span><span class="mp-step-title">' + escapeHtml(s.title) + "</span>" + (st === "complete" ? '<span class="mp-sr-only"> (completed)</span>' : "") + "</li>";
2217
+ }).join("") + "</ol>";
2218
+ }
2219
+ function go(index, initial) {
2220
+ if (index < 0 || index >= steps.length || index === current) {
2221
+ return current;
2222
+ }
2223
+ current = index;
2224
+ paint();
2225
+ steps.forEach((s, i) => {
2226
+ const panel = toElement(s.panel);
2227
+ if (panel) {
2228
+ panel.hidden = i !== index;
2229
+ }
2230
+ });
2231
+ if (!initial) {
2232
+ const panel = toElement(steps[index].panel);
2233
+ if (panel && opts.focus !== false) {
2234
+ focusElement(panel.querySelector("h1, h2, h3, h4, h5, h6, [data-mp-focus]") || panel);
2235
+ }
2236
+ announce("Step " + (index + 1) + " of " + steps.length + ": " + steps[index].title);
2237
+ if (typeof opts.onChange === "function") {
2238
+ opts.onChange(index, steps[index]);
2239
+ }
2240
+ }
2241
+ return current;
2242
+ }
2243
+ go(Math.min(Math.max(opts.current || 0, 0), Math.max(steps.length - 1, 0)), true);
2244
+ return {
2245
+ go: (i) => go(i),
2246
+ next: () => go(current + 1),
2247
+ prev: () => go(current - 1),
2248
+ current: () => current,
2249
+ destroy() {
2250
+ el.innerHTML = "";
2251
+ }
2252
+ };
2253
+ }
2254
+ };
2255
+
2256
+ // src/js/index.js
2257
+ var version = "1.3.0";
2258
+ var MpKit = {
2259
+ version,
2260
+ escapeHtml,
2261
+ announce,
2262
+ StatusBadge,
2263
+ EmptyState,
2264
+ Loading,
2265
+ View,
2266
+ Footer,
2267
+ Tabs,
2268
+ Theme,
2269
+ Toast,
2270
+ confirm,
2271
+ Form,
2272
+ CopyButton,
2273
+ copy: copyText,
2274
+ DeviceList,
2275
+ Skeleton,
2276
+ LogViewer,
2277
+ stripAnsi,
2278
+ Pairing,
2279
+ Auth,
2280
+ Steps,
2281
+ ai
2282
+ };
2283
+ installDelegates();
2284
+ var index_default = MpKit;
2285
+ export {
2286
+ Auth,
2287
+ CopyButton,
2288
+ DeviceList,
2289
+ EmptyState,
2290
+ Footer,
2291
+ Form,
2292
+ Loading,
2293
+ LogViewer,
2294
+ MpKit,
2295
+ Pairing,
2296
+ Skeleton,
2297
+ StatusBadge,
2298
+ Steps,
2299
+ Tabs,
2300
+ Theme,
2301
+ Toast,
2302
+ View,
2303
+ ai,
2304
+ announce,
2305
+ confirm,
2306
+ copyText as copy,
2307
+ index_default as default,
2308
+ diffLines,
2309
+ escapeHtml,
2310
+ formatPin,
2311
+ markdown,
2312
+ safeUrl,
2313
+ stripAnsi,
2314
+ version
2315
+ };