@mp-consulting/homebridge-ui-kit 1.2.1 → 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.d.mts ADDED
@@ -0,0 +1,37 @@
1
+ /* @mp-consulting/homebridge-ui-kit v1.3.0 — declarations for the ES module (dist/kit.mjs).
2
+ The ES module does not set the MpKit global. */
3
+
4
+ import type { MpKitApi } from './kit.js';
5
+
6
+ export type * from './kit.js';
7
+
8
+ export declare const MpKit: MpKitApi;
9
+ export default MpKit;
10
+
11
+ export declare const version: MpKitApi['version'];
12
+ export declare const escapeHtml: MpKitApi['escapeHtml'];
13
+ export declare function safeUrl(url: unknown): string | null;
14
+ export declare const announce: MpKitApi['announce'];
15
+ export declare const StatusBadge: MpKitApi['StatusBadge'];
16
+ export declare const EmptyState: MpKitApi['EmptyState'];
17
+ export declare const Loading: MpKitApi['Loading'];
18
+ export declare const View: MpKitApi['View'];
19
+ export declare const Footer: MpKitApi['Footer'];
20
+ export declare const Tabs: MpKitApi['Tabs'];
21
+ export declare const Theme: MpKitApi['Theme'];
22
+ export declare const Toast: MpKitApi['Toast'];
23
+ export declare const confirm: MpKitApi['confirm'];
24
+ export declare const Form: MpKitApi['Form'];
25
+ export declare const CopyButton: MpKitApi['CopyButton'];
26
+ export declare const copy: MpKitApi['copy'];
27
+ export declare const DeviceList: MpKitApi['DeviceList'];
28
+ export declare const Skeleton: MpKitApi['Skeleton'];
29
+ export declare const LogViewer: MpKitApi['LogViewer'];
30
+ export declare const stripAnsi: MpKitApi['stripAnsi'];
31
+ export declare const Pairing: MpKitApi['Pairing'];
32
+ export declare const Auth: MpKitApi['Auth'];
33
+ export declare const Steps: MpKitApi['Steps'];
34
+ export declare const formatPin: MpKitApi['Pairing']['formatPin'];
35
+ export declare const ai: MpKitApi['ai'];
36
+ export declare const markdown: MpKitApi['ai']['markdown'];
37
+ export declare const diffLines: MpKitApi['ai']['diffLines'];
package/dist/kit.d.ts ADDED
@@ -0,0 +1,513 @@
1
+ /* @mp-consulting/homebridge-ui-kit v1.3.0 — TypeScript declarations.
2
+ Classic script (dist/kit.js): `MpKit` is a global (also `window.MpKit`).
3
+ Reference it with: /// <reference types="@mp-consulting/homebridge-ui-kit/kit.js" />
4
+ or: import type {} from '@mp-consulting/homebridge-ui-kit/kit.js';
5
+ ES module (dist/kit.mjs): see kit.d.mts. */
6
+
7
+ export type Target = string | Element;
8
+ export type ResolvedTheme = 'light' | 'dark';
9
+ export type ThemePreference = ResolvedTheme | 'auto';
10
+
11
+ // ── Core ──
12
+
13
+ export interface StatusBadgeApi {
14
+ online(label?: string): string;
15
+ offline(label?: string): string;
16
+ checking(label?: string): string;
17
+ disabled(label?: string): string;
18
+ }
19
+
20
+ export interface EmptyStateOptions {
21
+ iconClass?: string;
22
+ title?: string;
23
+ hint?: string;
24
+ }
25
+
26
+ export interface ViewShowOptions {
27
+ /** Move focus to the view's heading. Default: only when switching from another view. */
28
+ focus?: boolean;
29
+ /** Announce the view (true/false or a custom message). Default as for focus. */
30
+ announce?: boolean | string;
31
+ }
32
+
33
+ export interface FooterOptions {
34
+ github?: string;
35
+ npm?: string;
36
+ changelog?: string;
37
+ target?: Target;
38
+ }
39
+
40
+ // ── Theme ──
41
+
42
+ export interface ThemeOptions {
43
+ target?: Element;
44
+ theme?: ThemePreference;
45
+ homebridge?: boolean;
46
+ storageKey?: string | false;
47
+ attribute?: boolean;
48
+ className?: boolean;
49
+ onChange?(resolved: ResolvedTheme, preference: ThemePreference): void;
50
+ }
51
+
52
+ export interface ThemeController {
53
+ /** Resolves with the resolved theme once the Homebridge setting was applied. */
54
+ ready: Promise<ResolvedTheme | null>;
55
+ preference(): ThemePreference;
56
+ resolved(): ResolvedTheme;
57
+ set(mode: ThemePreference): ResolvedTheme;
58
+ destroy(): void;
59
+ }
60
+
61
+ export interface ThemeApi {
62
+ init(opts?: ThemeOptions): ThemeController;
63
+ apply(resolved: ResolvedTheme, opts?: Pick<ThemeOptions, 'target' | 'attribute' | 'className'>): void;
64
+ resolve(preference: ThemePreference | null | undefined, systemDark: boolean): ResolvedTheme;
65
+ normalize(value: unknown): ThemePreference | null;
66
+ fromSettings(settings: unknown): ThemePreference | null;
67
+ }
68
+
69
+ // ── Tabs ──
70
+
71
+ export interface TabsOptions {
72
+ activation?: 'auto' | 'manual';
73
+ onChange?(tab: HTMLElement, index: number, panel: HTMLElement | null): void;
74
+ }
75
+
76
+ export interface TabsController {
77
+ select(target: number | HTMLElement): void;
78
+ selected(): number;
79
+ tabs(): HTMLElement[];
80
+ destroy(): void;
81
+ }
82
+
83
+ // ── Feedback ──
84
+
85
+ export type ToastType = 'success' | 'error' | 'warning' | 'info';
86
+
87
+ export interface ToastOptions {
88
+ title?: string;
89
+ /** Milliseconds; 0 keeps the toast until it is closed. */
90
+ duration?: number;
91
+ /** Always use the kit's toast, even inside Homebridge. */
92
+ local?: boolean;
93
+ }
94
+
95
+ export interface ToastHandle {
96
+ close(): void;
97
+ element: HTMLElement | null;
98
+ /** True when Homebridge's own toast was used. */
99
+ native: boolean;
100
+ }
101
+
102
+ export interface ToastApi {
103
+ show(type: ToastType, message: string, opts?: ToastOptions | string): ToastHandle;
104
+ success(message: string, opts?: ToastOptions | string): ToastHandle;
105
+ error(message: string, opts?: ToastOptions | string): ToastHandle;
106
+ warning(message: string, opts?: ToastOptions | string): ToastHandle;
107
+ info(message: string, opts?: ToastOptions | string): ToastHandle;
108
+ }
109
+
110
+ export interface ConfirmOptions {
111
+ title?: string;
112
+ message?: string;
113
+ confirmLabel?: string;
114
+ cancelLabel?: string;
115
+ danger?: boolean;
116
+ }
117
+
118
+ // ── Forms ──
119
+
120
+ export interface FieldOption {
121
+ value: string | number;
122
+ label: string;
123
+ disabled?: boolean;
124
+ }
125
+
126
+ export interface FieldOptions {
127
+ name?: string;
128
+ label?: string;
129
+ type?: 'text' | 'number' | 'email' | 'password' | 'url' | 'tel' | 'search' | 'date' | 'time' | 'color' | 'range'
130
+ | 'textarea' | 'select' | 'checkbox' | 'switch' | (string & {});
131
+ id?: string;
132
+ value?: unknown;
133
+ placeholder?: string;
134
+ help?: string;
135
+ error?: string;
136
+ required?: boolean;
137
+ disabled?: boolean;
138
+ readonly?: boolean;
139
+ min?: number | string;
140
+ max?: number | string;
141
+ step?: number | string;
142
+ pattern?: string;
143
+ autocomplete?: string;
144
+ inputmode?: string;
145
+ rows?: number;
146
+ multiple?: boolean;
147
+ options?: Array<FieldOption | string | number>;
148
+ checked?: boolean;
149
+ }
150
+
151
+ export interface SecretOptions extends FieldOptions {
152
+ copy?: boolean;
153
+ reveal?: boolean;
154
+ }
155
+
156
+ export interface CopyButtonOptions {
157
+ text?: string;
158
+ /** Selector of an element whose value (inputs) or text is copied. */
159
+ target?: string;
160
+ label?: string;
161
+ copiedLabel?: string;
162
+ ariaLabel?: string;
163
+ className?: string;
164
+ size?: 'sm';
165
+ }
166
+
167
+ export type PluginConfigBlock = Record<string, any>;
168
+ export type FormValues = Record<string, any>;
169
+
170
+ export interface TrackOptions {
171
+ toConfig?(values: FormValues, blocks: PluginConfigBlock[]):
172
+ PluginConfigBlock | PluginConfigBlock[] | Promise<PluginConfigBlock | PluginConfigBlock[]>;
173
+ save?(values: FormValues): unknown;
174
+ persist?: boolean;
175
+ bar?: boolean | Target;
176
+ submit?: boolean;
177
+ message?: string;
178
+ saveLabel?: string;
179
+ discardLabel?: string;
180
+ savedMessage?: string;
181
+ toast?: boolean;
182
+ onDirtyChange?(dirty: boolean): void;
183
+ onSaved?(config: unknown): void;
184
+ onError?(err: unknown): void;
185
+ }
186
+
187
+ export interface FormTracker {
188
+ bar: HTMLElement | null;
189
+ isDirty(): boolean;
190
+ values(): FormValues;
191
+ /** Resolves true when saved (to Homebridge or a custom save), false outside Homebridge. */
192
+ save(): Promise<boolean>;
193
+ discard(): void;
194
+ reset(): void;
195
+ check(): void;
196
+ destroy(): void;
197
+ }
198
+
199
+ export interface FormApi {
200
+ field(opts: FieldOptions): string;
201
+ secret(opts: SecretOptions): string;
202
+ values(form: Target): FormValues;
203
+ fill<T extends Target>(form: T, data: FormValues): T extends string ? Element : T;
204
+ track(form: Target, opts?: TrackOptions): FormTracker;
205
+ }
206
+
207
+ export interface CopyButtonApi {
208
+ render(opts?: CopyButtonOptions): string;
209
+ }
210
+
211
+ // ── Lists, skeletons, logs ──
212
+
213
+ export type DeviceStatus = 'online' | 'offline' | 'checking' | 'disabled';
214
+
215
+ export interface DeviceItem {
216
+ id?: string | number;
217
+ name?: string;
218
+ displayName?: string;
219
+ subtitle?: string;
220
+ iconClass?: string;
221
+ status?: DeviceStatus;
222
+ statusLabel?: string;
223
+ [key: string]: unknown;
224
+ }
225
+
226
+ export interface DeviceListOptions<T = DeviceItem> {
227
+ items?: T[];
228
+ loading?: boolean;
229
+ label?: string;
230
+ renderItem?(item: T, index: number): string;
231
+ search?: boolean | { placeholder?: string; label?: string };
232
+ keys?: string[];
233
+ filter?(item: T, query: string): boolean;
234
+ onActivate?(item: T, index: number, event: Event): void;
235
+ empty?: EmptyStateOptions;
236
+ noResults?: EmptyStateOptions;
237
+ skeletonCount?: number;
238
+ }
239
+
240
+ export interface DeviceListController<T = DeviceItem> {
241
+ setItems(items: T[]): void;
242
+ setLoading(loading: boolean): void;
243
+ setQuery(query: string): void;
244
+ items(): T[];
245
+ visible(): T[];
246
+ status(): string;
247
+ destroy(): void;
248
+ }
249
+
250
+ export interface SkeletonOptions {
251
+ lines?: number;
252
+ avatar?: boolean;
253
+ className?: string;
254
+ }
255
+
256
+ export interface LogViewerOptions {
257
+ maxLines?: number;
258
+ label?: string;
259
+ live?: 'off' | 'polite';
260
+ autoscroll?: boolean;
261
+ wrap?: boolean;
262
+ lineClass?(line: string): string | undefined | null;
263
+ jumpLabel?: string;
264
+ }
265
+
266
+ export interface LogViewerController {
267
+ element: HTMLElement;
268
+ append(text: string | string[]): void;
269
+ clear(): void;
270
+ lines(): string[];
271
+ scrollToBottom(): void;
272
+ isFollowing(): boolean;
273
+ destroy(): void;
274
+ }
275
+
276
+ // ── Pairing / sign-in / steps ──
277
+
278
+ export interface PinOptions {
279
+ pin: string | number;
280
+ label?: string;
281
+ qrSrc?: string;
282
+ qrAlt?: string;
283
+ copy?: boolean;
284
+ copyLabel?: string;
285
+ }
286
+
287
+ export interface RenderPinOptions extends PinOptions {
288
+ /** An SVG/IMG element to mount, or an image URL. */
289
+ qr?: Element | string;
290
+ }
291
+
292
+ export type AuthState = 'waiting' | 'polling' | 'success' | 'error' | 'expired';
293
+ export type PollResult = boolean | null | undefined | { done?: boolean; error?: unknown; interval?: number; [key: string]: unknown };
294
+
295
+ export interface DeviceCodeOptions {
296
+ title?: string;
297
+ url?: string;
298
+ code?: string;
299
+ instructions?: string;
300
+ openLabel?: string;
301
+ poll?(): PollResult | Promise<PollResult>;
302
+ interval?: number;
303
+ expiresIn?: number;
304
+ autoStart?: boolean;
305
+ successMessage?: string;
306
+ onSuccess?(result: PollResult): void;
307
+ onError?(err: unknown): void;
308
+ onRetry?(): void;
309
+ }
310
+
311
+ export interface DeviceCodeController {
312
+ element: HTMLElement;
313
+ setState(state: AuthState, message?: string): DeviceCodeController;
314
+ state(): AuthState;
315
+ start(): DeviceCodeController;
316
+ stop(): void;
317
+ destroy(): void;
318
+ }
319
+
320
+ export interface StepDefinition {
321
+ title: string;
322
+ panel?: Target;
323
+ }
324
+
325
+ export interface StepsOptions {
326
+ steps: Array<StepDefinition | string>;
327
+ current?: number;
328
+ label?: string;
329
+ focus?: boolean;
330
+ onChange?(index: number, step: StepDefinition): void;
331
+ }
332
+
333
+ export interface StepsController {
334
+ go(index: number): number;
335
+ next(): number;
336
+ prev(): number;
337
+ current(): number;
338
+ destroy(): void;
339
+ }
340
+
341
+ // ── Assistant (AI) ──
342
+
343
+ export interface AiStatus {
344
+ enabled: boolean;
345
+ provider?: string;
346
+ model?: string;
347
+ capabilities?: Record<string, unknown>;
348
+ [key: string]: unknown;
349
+ }
350
+
351
+ export interface AiUsage {
352
+ inputTokens?: number;
353
+ outputTokens?: number;
354
+ [key: string]: unknown;
355
+ }
356
+
357
+ export interface AiTextResult {
358
+ text: string;
359
+ usage?: AiUsage;
360
+ [key: string]: unknown;
361
+ }
362
+
363
+ export interface AiConfigResult {
364
+ config: unknown;
365
+ explanation?: string;
366
+ usage?: AiUsage;
367
+ [key: string]: unknown;
368
+ }
369
+
370
+ export interface AiRequestOptions {
371
+ onChunk?(delta: string, event: { requestId: string; delta: string; [key: string]: unknown }): void;
372
+ onDone?(event: { requestId: string; [key: string]: unknown }): void;
373
+ onError?(message: string, event: { requestId: string; message?: string; [key: string]: unknown }): void;
374
+ /** Cancels the request when aborted. */
375
+ signal?: AbortSignal;
376
+ /** Send a best-effort /ai/cancel request when cancelled (default true). */
377
+ notifyServer?: boolean;
378
+ }
379
+
380
+ /** A pending Assistant request: a promise that can be cancelled. */
381
+ export interface AiRequest<T> extends Promise<T> {
382
+ /** Cancels the request; returns false if it had already settled. */
383
+ cancel(reason?: unknown): boolean;
384
+ requestId: string | null;
385
+ }
386
+
387
+ export interface AiButtonOptions {
388
+ label?: string;
389
+ id?: string;
390
+ className?: string;
391
+ size?: 'sm' | 'lg';
392
+ title?: string;
393
+ }
394
+
395
+ export interface AiAnswerOptions {
396
+ label?: string;
397
+ title?: string;
398
+ text?: string;
399
+ streaming?: boolean;
400
+ /** Footnote; '' hides it. */
401
+ note?: string;
402
+ }
403
+
404
+ export interface AiAnswerController {
405
+ append(delta: string): AiAnswerController;
406
+ setText(text: string): AiAnswerController;
407
+ done(result?: string | { text?: string }): AiAnswerController;
408
+ error(err: unknown): AiAnswerController;
409
+ getText(): string;
410
+ }
411
+
412
+ export interface DiffLine {
413
+ type: 'add' | 'del' | 'same';
414
+ text: string;
415
+ }
416
+
417
+ export interface AiDiffChange {
418
+ before: unknown;
419
+ after: unknown;
420
+ lines: DiffLine[];
421
+ }
422
+
423
+ export interface AiDiffOptions {
424
+ before?: unknown;
425
+ after?: unknown;
426
+ title?: string;
427
+ label?: string;
428
+ applyLabel?: string;
429
+ rejectLabel?: string;
430
+ onApply?(change: AiDiffChange): unknown;
431
+ onReject?(change: AiDiffChange): unknown;
432
+ }
433
+
434
+ export interface AiChatMessage {
435
+ role: 'user' | 'assistant';
436
+ text: string;
437
+ error?: string;
438
+ }
439
+
440
+ export interface AiChatOptions {
441
+ label?: string;
442
+ context?: string;
443
+ placeholder?: string;
444
+ emptyText?: string;
445
+ onSend?(prompt: string, ctx: {
446
+ onChunk(delta: string): void;
447
+ history: Array<{ role: 'user' | 'assistant'; text: string }>;
448
+ signal?: AbortSignal;
449
+ }): unknown;
450
+ }
451
+
452
+ export interface AiChatController {
453
+ send(text?: string): Promise<string | null>;
454
+ cancel(): boolean;
455
+ clear(): void;
456
+ messages(): AiChatMessage[];
457
+ }
458
+
459
+ export interface AiApi {
460
+ status(): Promise<AiStatus>;
461
+ explain(body: { error: string; context?: string; device?: unknown }, opts?: AiRequestOptions): AiRequest<AiTextResult>;
462
+ ask(body: { prompt: string; context?: string }, opts?: AiRequestOptions): AiRequest<AiTextResult>;
463
+ config(body: { schema: unknown; request: string; current?: unknown }, opts?: AiRequestOptions): AiRequest<AiConfigResult>;
464
+ markdown(text: string | null | undefined): string;
465
+ diffLines(before: unknown, after: unknown): DiffLine[];
466
+ renderButton(opts?: AiButtonOptions): string;
467
+ renderBadge(label?: string): string;
468
+ renderThinking(label?: string): string;
469
+ renderAnswer(el: Element, opts?: AiAnswerOptions): AiAnswerController;
470
+ renderDiff(el: Element, opts?: AiDiffOptions): { lines: DiffLine[]; added: number; removed: number };
471
+ renderChat(el: Element, opts?: AiChatOptions): AiChatController;
472
+ edgeGlow(show?: boolean): HTMLElement | null;
473
+ }
474
+
475
+ // ── The MpKit object ──
476
+
477
+ export interface MpKitApi {
478
+ version: string;
479
+ escapeHtml(value: unknown): string;
480
+ announce(message: string, politeness?: 'polite' | 'assertive'): void;
481
+ StatusBadge: StatusBadgeApi;
482
+ EmptyState: { render(opts?: EmptyStateOptions): string };
483
+ Loading: { render(message?: string): string };
484
+ View: { show(id: string, opts?: ViewShowOptions): HTMLElement | null };
485
+ Footer: { render(opts?: FooterOptions): void };
486
+ Tabs: { init(el: Target, opts?: TabsOptions): TabsController | null };
487
+ Theme: ThemeApi;
488
+ Toast: ToastApi;
489
+ confirm(opts?: ConfirmOptions | string): Promise<boolean>;
490
+ Form: FormApi;
491
+ CopyButton: CopyButtonApi;
492
+ copy(text: string): Promise<boolean>;
493
+ DeviceList: { render<T = DeviceItem>(el: Target, opts?: DeviceListOptions<T>): DeviceListController<T> };
494
+ Skeleton: { render(opts?: SkeletonOptions): string };
495
+ LogViewer: { create(el: Target, opts?: LogViewerOptions): LogViewerController; stripAnsi(text: string): string };
496
+ stripAnsi(text: string): string;
497
+ Pairing: {
498
+ formatPin(code: string | number): string;
499
+ pin(opts: PinOptions): string;
500
+ renderPin(el: Target, opts: RenderPinOptions): HTMLElement | null;
501
+ };
502
+ Auth: { deviceCode(el: Target, opts?: DeviceCodeOptions): DeviceCodeController };
503
+ Steps: { create(el: Target, opts: StepsOptions): StepsController };
504
+ ai: AiApi;
505
+ }
506
+
507
+ declare global {
508
+ // eslint-disable-next-line no-var
509
+ var MpKit: MpKitApi;
510
+ interface Window {
511
+ MpKit: MpKitApi;
512
+ }
513
+ }