@matter-server/dashboard 1.2.4 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +15 -2
  2. package/dist/esm/pages/components/node-details.d.ts.map +1 -1
  3. package/dist/esm/pages/components/node-details.js +14 -1
  4. package/dist/esm/pages/components/node-details.js.map +1 -1
  5. package/dist/esm/pages/matter-cluster-view.d.ts +1 -0
  6. package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
  7. package/dist/esm/pages/matter-cluster-view.js +8 -0
  8. package/dist/esm/pages/matter-cluster-view.js.map +1 -1
  9. package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -1
  10. package/dist/esm/pages/matter-dashboard-app.js +1 -0
  11. package/dist/esm/pages/matter-dashboard-app.js.map +1 -1
  12. package/dist/esm/pages/network/network-details.d.ts.map +1 -1
  13. package/dist/esm/pages/network/network-details.js +20 -7
  14. package/dist/esm/pages/network/network-details.js.map +1 -1
  15. package/dist/esm/pages/network/network-types.d.ts +2 -0
  16. package/dist/esm/pages/network/network-types.d.ts.map +1 -1
  17. package/dist/esm/pages/network/network-utils.d.ts +16 -0
  18. package/dist/esm/pages/network/network-utils.d.ts.map +1 -1
  19. package/dist/esm/pages/network/network-utils.js +33 -0
  20. package/dist/esm/pages/network/network-utils.js.map +1 -1
  21. package/dist/esm/pages/network/thread-graph.d.ts.map +1 -1
  22. package/dist/esm/pages/network/thread-graph.js +13 -0
  23. package/dist/esm/pages/network/thread-graph.js.map +1 -1
  24. package/dist/esm/util/pairing-code.d.ts +18 -0
  25. package/dist/esm/util/pairing-code.d.ts.map +1 -0
  26. package/dist/esm/util/pairing-code.js +24 -0
  27. package/dist/esm/util/pairing-code.js.map +6 -0
  28. package/dist/web/js/{attribute-write-dialog-fBF9_jCK.js → attribute-write-dialog-yHLoNOkW.js} +1 -1
  29. package/dist/web/js/{command-invoke-dialog-DWMBX1gN.js → command-invoke-dialog-ByzPtgEb.js} +1 -1
  30. package/dist/web/js/{commission-node-dialog-BptZBF2k.js → commission-node-dialog-BlT9WOki.js} +5 -5
  31. package/dist/web/js/{commission-node-existing-yUyavzOc.js → commission-node-existing-DMPiGpS5.js} +1 -1
  32. package/dist/web/js/{commission-node-thread-Cb5v-CtI.js → commission-node-thread-D3X-Not8.js} +1 -1
  33. package/dist/web/js/{commission-node-wifi-R0fGOeQ-.js → commission-node-wifi-3sLvZVvY.js} +1 -1
  34. package/dist/web/js/{dialog-box-DLCbBR3S.js → dialog-box-DXneB7f7.js} +1 -1
  35. package/dist/web/js/{fabric-label-dialog-tW-AnWBx.js → fabric-label-dialog-DoBL1t3o.js} +1 -1
  36. package/dist/web/js/main.js +1 -1
  37. package/dist/web/js/{matter-dashboard-app-BGWRh_7q.js → matter-dashboard-app-mMXFHv8v.js} +1787 -17
  38. package/dist/web/js/{node-acl-add-dialog-DMKY5qsl.js → node-acl-add-dialog-DJrFkAp9.js} +1 -1
  39. package/dist/web/js/{node-binding-dialog-bephut8j.js → node-binding-dialog-CQt-LbiG.js} +1 -1
  40. package/dist/web/js/{node-label-dialog-CHDD9Bk1.js → node-label-dialog-D1CyUaCS.js} +1 -1
  41. package/dist/web/js/{settings-dialog-BT_YDlXZ.js → settings-dialog-B9ctg2Ji.js} +1 -1
  42. package/package.json +8 -5
  43. package/src/pages/components/node-details.ts +14 -1
  44. package/src/pages/matter-cluster-view.ts +9 -0
  45. package/src/pages/matter-dashboard-app.ts +3 -0
  46. package/src/pages/network/network-details.ts +22 -7
  47. package/src/pages/network/network-types.ts +2 -0
  48. package/src/pages/network/network-utils.ts +45 -0
  49. package/src/pages/network/thread-graph.ts +17 -0
  50. package/src/util/pairing-code.ts +32 -0
@@ -1,4 +1,4 @@
1
- import { i, c, a as clientContext, n, r, b as i$1, P as Privilege, v as nodeIdKey, x as aclCapacity, s as showAlertDialog, y as targetServerClusters, z as clusters, B as AuthMode, C as addAclEntry, E as PRIVILEGE_NAMES, d as b, F as mdiClose, G as getDeviceName, h as handleAsync, g as t } from './matter-dashboard-app-BGWRh_7q.js';
1
+ import { i, c, a as clientContext, n, r, b as i$1, P as Privilege, v as nodeIdKey, x as aclCapacity, s as showAlertDialog, y as targetServerClusters, z as clusters, B as AuthMode, C as addAclEntry, E as PRIVILEGE_NAMES, d as b, F as mdiClose, G as getDeviceName, h as handleAsync, g as t } from './matter-dashboard-app-mMXFHv8v.js';
2
2
  import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
3
3
  import './main.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { i, c, a as clientContext, n, r, b as i$1, v as nodeIdKey, z as clusters, s as showAlertDialog, H as targetAclCapacityForBinding, I as addBinding, J as bindableClusters, A, d as b, G as getDeviceName, K as getEndpointDeviceTypes, h as handleAsync, g as t } from './matter-dashboard-app-BGWRh_7q.js';
1
+ import { i, c, a as clientContext, n, r, b as i$1, v as nodeIdKey, z as clusters, s as showAlertDialog, H as targetAclCapacityForBinding, I as addBinding, J as bindableClusters, A, d as b, G as getDeviceName, K as getEndpointDeviceTypes, h as handleAsync, g as t } from './matter-dashboard-app-mMXFHv8v.js';
2
2
  import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
3
3
  import './main.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { r, n, g as t, b as i, M as MAX_NODE_LABEL_LENGTH, d as b, w as writeNodeLabel, s as showAlertDialog } from './matter-dashboard-app-BGWRh_7q.js';
1
+ import { r, n, g as t, b as i, M as MAX_NODE_LABEL_LENGTH, d as b, w as writeNodeLabel, s as showAlertDialog } from './matter-dashboard-app-mMXFHv8v.js';
2
2
  import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
3
3
  import './main.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { i, c, a as clientContext, t as tickContext, r, e, b as i$1, f as fireAndForget, s as showAlertDialog, d as b, A, h as handleAsync, g as t, n, D as DevModeService, m as mdiWifi, j as mdiPlus, k as mdiPencil, l as mdiDelete, o as mdiEyeOff, p as mdiEye, q as mdiAccessPoint } from './matter-dashboard-app-BGWRh_7q.js';
1
+ import { i, c, a as clientContext, t as tickContext, r, e, b as i$1, f as fireAndForget, s as showAlertDialog, d as b, A, h as handleAsync, g as t, n, D as DevModeService, m as mdiWifi, j as mdiPlus, k as mdiPencil, l as mdiDelete, o as mdiEyeOff, p as mdiEye, q as mdiAccessPoint } from './matter-dashboard-app-mMXFHv8v.js';
2
2
  import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
3
3
  import './main.js';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matter-server/dashboard",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Dashboard for OHF Matter Server",
5
5
  "homepage": "https://github.com/matter-js/matterjs-server",
6
6
  "bugs": {
@@ -28,27 +28,30 @@
28
28
  "generate": "tsx scripts/generate-descriptions.ts",
29
29
  "build": "npm run generate && nacho-build && npm run bundle",
30
30
  "build-clean": "npm run generate && nacho-build --clean && npm run bundle",
31
- "bundle": "rollup -c"
31
+ "bundle": "rollup -c",
32
+ "test": "matter-test --force-exit"
32
33
  },
33
34
  "dependencies": {
34
35
  "@lit/context": "^1.1.6",
35
36
  "@material/web": "^2.4.1",
36
- "@matter-server/custom-clusters": "1.2.4",
37
- "@matter-server/ws-client": "1.2.4",
37
+ "@matter-server/custom-clusters": "1.2.5",
38
+ "@matter-server/ws-client": "1.2.5",
38
39
  "@mdi/js": "^7.4.47",
39
40
  "lit": "^3.3.3",
41
+ "qrcode-generator": "^2.0.4",
40
42
  "tslib": "^2.8.1",
41
43
  "vis-network": "^10.1.0"
42
44
  },
43
45
  "devDependencies": {
44
46
  "@babel/preset-env": "^8.0.2",
45
- "@matter/main": "0.17.5-alpha.0-20260711-a60245c75",
47
+ "@matter/main": "0.17.5",
46
48
  "@rollup/plugin-babel": "^7.1.0",
47
49
  "@rollup/plugin-commonjs": "^29.0.3",
48
50
  "@rollup/plugin-json": "^6.1.0",
49
51
  "@rollup/plugin-node-resolve": "^16.0.3",
50
52
  "@rollup/plugin-terser": "^1.0.0",
51
53
  "@rollup/plugin-typescript": "^12.3.0",
54
+ "@types/qrcode-generator": "^0.0.16",
52
55
  "rollup": "^4.62.2",
53
56
  "rollup-plugin-copy": "^3.5.0",
54
57
  "serve": "^14.2.6"
@@ -26,6 +26,7 @@ import "../camera-overlay.js";
26
26
  import { getDeviceIcon } from "../../util/device-icons.js";
27
27
  import { getEndpointDeviceTypes } from "../../util/endpoints.js";
28
28
  import { ICD_CLUSTER_ID, icdBadge } from "../../util/icd.js";
29
+ import { formatManualPairingCode, renderPairingQrCodeDataUri } from "../../util/pairing-code.js";
29
30
  import { bindingContext } from "./context.js";
30
31
 
31
32
  /** Map updateState values to user-friendly labels */
@@ -320,9 +321,21 @@ export class NodeDetails extends LitElement {
320
321
  }
321
322
  try {
322
323
  const shareCode = await this.client.openCommissioningWindow(this.node!.node_id);
324
+ const manualCode = formatManualPairingCode(shareCode.setup_manual_code);
325
+ const qrDataUri = renderPairingQrCodeDataUri(shareCode.setup_qr_code);
323
326
  showAlertDialog({
324
327
  title: "Share device",
325
- text: `Setup code: ${shareCode.setup_manual_code}`,
328
+ text: html`
329
+ <div style="display:flex;flex-direction:column;align-items:center;gap:16px;">
330
+ <img src=${qrDataUri} alt="Commissioning QR code" style="width:200px;height:200px;" />
331
+ <div style="text-align:center;">
332
+ <div style="font-size:0.8rem;color:var(--md-sys-color-on-surface-variant);">Setup code</div>
333
+ <div style="font-size:1.4rem;font-family:monospace;letter-spacing:0.05em;">
334
+ ${manualCode}
335
+ </div>
336
+ </div>
337
+ </div>
338
+ `,
326
339
  });
327
340
  } catch (err: any) {
328
341
  showAlertDialog({
@@ -377,6 +377,7 @@ class MatterClusterView extends LitElement {
377
377
  // Reset per-attribute refresh state when navigating to a different cluster/endpoint.
378
378
  if (changedProperties.has("cluster") || changedProperties.has("endpoint")) {
379
379
  this._refreshState = {};
380
+ this._scrollCommandPanelIntoView();
380
381
  }
381
382
 
382
383
  // After render, find and configure the cluster commands component
@@ -391,6 +392,14 @@ class MatterClusterView extends LitElement {
391
392
  }
392
393
  }
393
394
 
395
+ // Route change resets scroll to top; for clusters with a command panel land on the panel instead.
396
+ private _scrollCommandPanelIntoView() {
397
+ requestAnimationFrame(() => {
398
+ const container = this.shadowRoot?.getElementById("cluster-commands-container");
399
+ container?.scrollIntoView({ block: "start", behavior: "auto" });
400
+ });
401
+ }
402
+
394
403
  private _goBack() {
395
404
  history.back();
396
405
  }
@@ -64,6 +64,9 @@ class MatterDashboardApp extends LitElement {
64
64
  const hash = location.hash.substring(1);
65
65
  const pathParts = hash.split("/");
66
66
 
67
+ // Single scroll container: without this a route change keeps the previous view's offset.
68
+ window.scrollTo(0, 0);
69
+
67
70
  // Reset initial selected node
68
71
  this._initialSelectedNodeId = null;
69
72
 
@@ -37,12 +37,16 @@ import {
37
37
  formatThreadVersion,
38
38
  getDeviceName,
39
39
  getNetworkType,
40
+ DIAGNOSTIC_MESH_NODE_EXPLANATION,
41
+ EXTERNAL_ROUTER_CAPABLE_NOTE,
42
+ EXTERNAL_THREAD_DEVICE_CASES,
40
43
  getNodeConnectionsFromPairs,
41
44
  getRoutableDestinationsCount,
42
45
  getSignalColorFromRssi,
43
46
  getThreadChannel,
44
47
  getThreadExtendedAddressHex,
45
48
  getThreadRole,
49
+ getThreadRoleDescription,
46
50
  getThreadRoleName,
47
51
  getThreadVersion,
48
52
  getWiFiDiagnostics,
@@ -237,6 +241,7 @@ export class NetworkDetails extends LitElement {
237
241
  <span class="label">Role:</span>
238
242
  <span class="value">${getThreadRoleName(threadRole)}</span>
239
243
  </div>
244
+ <p class="hint-text inline-note">${getThreadRoleDescription(threadRole)}</p>
240
245
  ${threadVersion !== undefined
241
246
  ? html`
242
247
  <div class="info-row">
@@ -482,10 +487,7 @@ export class NetworkDetails extends LitElement {
482
487
 
483
488
  <md-divider></md-divider>
484
489
  <div class="section">
485
- <p class="hint-text">
486
- This node was inferred from Border Router diagnostics (Route64 / child table) and is not
487
- commissioned to this fabric, so no device details are available.
488
- </p>
490
+ <p class="hint-text">${DIAGNOSTIC_MESH_NODE_EXPLANATION}</p>
489
491
  </div>
490
492
  `;
491
493
  }
@@ -504,6 +506,9 @@ export class NetworkDetails extends LitElement {
504
506
  <span class="label">Type:</span>
505
507
  <span class="value">${unknown.isRouter ? "Router (external)" : "End Device (external)"}</span>
506
508
  </div>
509
+ ${unknown.isRouter
510
+ ? html`<p class="hint-text inline-note">${EXTERNAL_ROUTER_CAPABLE_NOTE}</p>`
511
+ : nothing}
507
512
  <div class="info-row">
508
513
  <span class="label">Extended Address:</span>
509
514
  <span class="value mono">${unknown.extAddressHex}</span>
@@ -539,10 +544,12 @@ export class NetworkDetails extends LitElement {
539
544
  <md-divider></md-divider>
540
545
  <div class="section">
541
546
  <p class="hint-text">
542
- This device appears in Thread neighbor tables but is not commissioned to this fabric. It may be a
543
- Thread Border Router whose Thread radio MAC differs from its MeshCoP border-agent ID (common with
544
- Apple and Aqara), or a device from another Matter ecosystem.
547
+ This device appears in a commissioned node's Thread neighbor table but is not part of this fabric.
548
+ It may be:
545
549
  </p>
550
+ <ul class="hint-text">
551
+ ${EXTERNAL_THREAD_DEVICE_CASES.map(c => html`<li>${c}</li>`)}
552
+ </ul>
546
553
  </div>
547
554
  `;
548
555
  }
@@ -1828,6 +1835,14 @@ export class NetworkDetails extends LitElement {
1828
1835
  margin: 0;
1829
1836
  }
1830
1837
 
1838
+ .hint-text.inline-note {
1839
+ margin-top: 6px;
1840
+ margin-bottom: 4px;
1841
+ padding-left: 10px;
1842
+ border-left: 2px solid var(--md-sys-color-outline-variant, #ccc);
1843
+ font-size: 0.72rem;
1844
+ }
1845
+
1831
1846
  .connected-nodes-list {
1832
1847
  display: flex;
1833
1848
  flex-direction: column;
@@ -35,6 +35,8 @@ export interface NetworkGraphNode {
35
35
  shape: "image";
36
36
  networkType: NetworkType;
37
37
  threadRole?: ThreadRoutingRole;
38
+ /** Hover tooltip (vis.js): role or unknown/external explanation. */
39
+ title?: string;
38
40
  /** Whether the node is offline */
39
41
  offline?: boolean;
40
42
  /** Whether this is an unknown/external device */
@@ -150,6 +150,51 @@ export function getThreadRoleName(role: number | undefined): string {
150
150
  }
151
151
  }
152
152
 
153
+ /** Tooltip-friendly long form of {@link getThreadRoleName}. */
154
+ export function getThreadRoleDescription(role: number | undefined): string {
155
+ switch (role) {
156
+ case 2:
157
+ return "Sleepy End Device: keeps its radio off while idle to save battery and reaches the mesh only through a parent router. The links shown for it can include stale router-table entries for Thread addresses it no longer uses.";
158
+ case 3:
159
+ return "End Device: a leaf node that reaches the mesh through a parent router and does not route for other nodes. The links shown for it can include stale router-table entries for old or unused Thread addresses.";
160
+ case 4:
161
+ return "REED (Router-Eligible End Device): currently acts as an end device but can be promoted to a full Router when the mesh needs more routing capacity.";
162
+ case 5:
163
+ return "Router: forwards traffic for other nodes in the Thread mesh.";
164
+ case 6:
165
+ return "Leader: the elected node that manages router assignments for the Thread network.";
166
+ case 0:
167
+ case 1:
168
+ return "Thread routing role is unassigned or unspecified.";
169
+ default:
170
+ return "Thread routing role could not be determined.";
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Cases a Thread node shown as unknown/external may actually be. These devices are
176
+ * inferred from a commissioned node's neighbor table; the code cannot tell the cases
177
+ * apart, so the copy enumerates the possibilities rather than asserting one.
178
+ */
179
+ export const EXTERNAL_THREAD_DEVICE_CASES = [
180
+ "a device commissioned to another Matter fabric on the same Thread network",
181
+ "a native (non-Matter) Thread accessory",
182
+ "a Border Router whose Thread radio MAC differs from its MeshCoP border-agent ID (common with Apple and Aqara)",
183
+ "a stale neighbor entry for a device that has left",
184
+ ] as const;
185
+
186
+ /** Single-sentence form of {@link EXTERNAL_THREAD_DEVICE_CASES} for plain-text tooltips. */
187
+ export const EXTERNAL_THREAD_DEVICE_EXPLANATION = `Seen in a commissioned node's Thread neighbor table but not part of this fabric. It may be ${EXTERNAL_THREAD_DEVICE_CASES.slice(0, -1).join(", ")}, or ${EXTERNAL_THREAD_DEVICE_CASES[EXTERNAL_THREAD_DEVICE_CASES.length - 1]}.`;
188
+
189
+ /**
190
+ * `isRouter` for an external neighbor is derived from rx-on-when-idle, so it means
191
+ * router-capable (mains-powered), not a confirmed routing role.
192
+ */
193
+ export const EXTERNAL_ROUTER_CAPABLE_NOTE = `"Router" here means the neighbor advertised rx-on-when-idle (mains-powered / router-capable), not a confirmed routing role.`;
194
+
195
+ export const DIAGNOSTIC_MESH_NODE_EXPLANATION =
196
+ "Inferred from Border Router diagnostics (Route64 / child table) and not commissioned to this fabric, so no device details are available.";
197
+
153
198
  /**
154
199
  * Gets WiFi security type name.
155
200
  */
@@ -27,6 +27,9 @@ import {
27
27
  buildRloc16Map,
28
28
  buildThreadEdgePairs,
29
29
  decodeMeshcopStateBitmap,
30
+ DIAGNOSTIC_MESH_NODE_EXPLANATION,
31
+ EXTERNAL_ROUTER_CAPABLE_NOTE,
32
+ EXTERNAL_THREAD_DEVICE_EXPLANATION,
30
33
  findDiagnosticMeshNodes,
31
34
  findUnknownDevices,
32
35
  makeDiagnosticRloc16Resolver,
@@ -36,6 +39,7 @@ import {
36
39
  getNetworkType,
37
40
  getThreadExtendedAddressHex,
38
41
  getThreadRole,
42
+ getThreadRoleDescription,
39
43
  mergeDiagnosticEdges,
40
44
  signalLevelToColor,
41
45
  stripMdnsHostname,
@@ -340,6 +344,7 @@ export class ThreadGraph extends BaseNetworkGraph {
340
344
  return {
341
345
  id: nodeId,
342
346
  label: getDeviceName(node),
347
+ title: getThreadRoleDescription(threadRole),
343
348
  image: createNodeIconDataUrl(node, threadRole, false, isOffline),
344
349
  shape: "image" as const,
345
350
  networkType: "thread" as const,
@@ -401,9 +406,16 @@ export class ThreadGraph extends BaseNetworkGraph {
401
406
  const decodedState = decodeMeshcopStateBitmap(device.stateBitmapHex);
402
407
  const isLeader = decodedState?.threadRoleValue === 3;
403
408
  const isPrimaryBbr = decodedState?.bbr === true && decodedState.bbrFunction === "primary";
409
+ const brRole = new Array<string>();
410
+ if (isLeader) brRole.push("currently the Thread Leader");
411
+ if (isPrimaryBbr) brRole.push("Primary Backbone Border Router (BBR)");
412
+ const brTitle =
413
+ "Thread Border Router bridging the Thread mesh to the IP network" +
414
+ (brRole.length > 0 ? ` — ${brRole.join("; ")}.` : ".");
404
415
  graphNodes.push({
405
416
  id: device.id,
406
417
  label,
418
+ title: brTitle,
407
419
  image: createBorderRouterIconDataUrl(isSelected, isLeader, isPrimaryBbr),
408
420
  shape: "image" as const,
409
421
  networkType: "thread" as const,
@@ -415,9 +427,13 @@ export class ThreadGraph extends BaseNetworkGraph {
415
427
  const typeLabel =
416
428
  diagNode?.vendorName !== undefined ? `${baseType} (${diagNode.vendorName})` : baseType;
417
429
  const suffix = device.networkName !== undefined ? `\n${device.networkName}` : "";
430
+ const title = device.isRouter
431
+ ? `${EXTERNAL_THREAD_DEVICE_EXPLANATION} ${EXTERNAL_ROUTER_CAPABLE_NOTE}`
432
+ : EXTERNAL_THREAD_DEVICE_EXPLANATION;
418
433
  graphNodes.push({
419
434
  id: device.id,
420
435
  label: `${typeLabel} [${device.extAddressHex.slice(-8)}]${suffix}`,
436
+ title,
421
437
  image: createUnknownDeviceIconDataUrl(device.isRouter, isSelected),
422
438
  shape: "image" as const,
423
439
  networkType: "thread" as const,
@@ -485,6 +501,7 @@ export class ThreadGraph extends BaseNetworkGraph {
485
501
  graphNodes.push({
486
502
  id: meshNode.id,
487
503
  label,
504
+ title: DIAGNOSTIC_MESH_NODE_EXPLANATION,
488
505
  image: createUnknownDeviceIconDataUrl(meshNode.isRouter, meshNode.id === this._selectedNodeId),
489
506
  shape: "image" as const,
490
507
  networkType: "thread" as const,
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import qrcode from "qrcode-generator";
8
+
9
+ /**
10
+ * Format a Matter manual pairing code for display. The 11-digit short code is
11
+ * grouped as `XXXX-XXX-XXXX` per the Matter spec; codes of any other length are
12
+ * returned stripped to their digits, or unchanged when they contain no digits.
13
+ */
14
+ export function formatManualPairingCode(code: string): string {
15
+ const digits = code.replace(/\D/g, "");
16
+ if (digits.length === 11) {
17
+ return `${digits.slice(0, 4)}-${digits.slice(4, 7)}-${digits.slice(7, 11)}`;
18
+ }
19
+ return digits || code;
20
+ }
21
+
22
+ /**
23
+ * Render a Matter QR pairing payload (the `MT:...` string) as an SVG data URI.
24
+ * The generated SVG carries an opaque white background, so the code stays
25
+ * scannable regardless of the surrounding theme.
26
+ */
27
+ export function renderPairingQrCodeDataUri(qrPayload: string, cellSize = 5, margin = 4): string {
28
+ const qr = qrcode(0, "M");
29
+ qr.addData(qrPayload);
30
+ qr.make();
31
+ return `data:image/svg+xml,${encodeURIComponent(qr.createSvgTag(cellSize, margin))}`;
32
+ }