@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.4 → 5.0.0-beta.41

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 (121) hide show
  1. package/CONTRIBUTING.md +9 -17
  2. package/config.schema.json +60 -0
  3. package/dist/account/ownership.js +16 -16
  4. package/dist/account/ownership.js.map +1 -1
  5. package/dist/account/persistence.js +24 -74
  6. package/dist/account/persistence.js.map +1 -1
  7. package/dist/account/temporary-authentication.js +5 -3
  8. package/dist/account/temporary-authentication.js.map +1 -1
  9. package/dist/configuration.js +50 -13
  10. package/dist/configuration.js.map +1 -1
  11. package/dist/device/member-evidence.js +11 -4
  12. package/dist/device/member-evidence.js.map +1 -1
  13. package/dist/device/member-trust.js +14 -32
  14. package/dist/device/member-trust.js.map +1 -1
  15. package/dist/device/snapshot.js +6 -18
  16. package/dist/device/snapshot.js.map +1 -1
  17. package/dist/diagnostics.js +966 -337
  18. package/dist/diagnostics.js.map +1 -1
  19. package/dist/homekit/adapter.js +4 -1
  20. package/dist/homekit/adapter.js.map +1 -1
  21. package/dist/homekit/adapters/battery.js +3 -3
  22. package/dist/homekit/adapters/battery.js.map +1 -1
  23. package/dist/homekit/adapters/camera-controls.js +15 -37
  24. package/dist/homekit/adapters/camera-controls.js.map +1 -1
  25. package/dist/homekit/adapters/camera-streaming.js +74 -48
  26. package/dist/homekit/adapters/camera-streaming.js.map +1 -1
  27. package/dist/homekit/adapters/contact.js +5 -5
  28. package/dist/homekit/adapters/contact.js.map +1 -1
  29. package/dist/homekit/adapters/doorbell.js +3 -15
  30. package/dist/homekit/adapters/doorbell.js.map +1 -1
  31. package/dist/homekit/adapters/lock.js +72 -221
  32. package/dist/homekit/adapters/lock.js.map +1 -1
  33. package/dist/homekit/adapters/motion.js +3 -3
  34. package/dist/homekit/adapters/motion.js.map +1 -1
  35. package/dist/homekit/adapters/package-sensor.js +63 -0
  36. package/dist/homekit/adapters/package-sensor.js.map +1 -0
  37. package/dist/homekit/adapters/registry.js +2 -0
  38. package/dist/homekit/adapters/registry.js.map +1 -1
  39. package/dist/homekit/adapters/security-system.js +106 -250
  40. package/dist/homekit/adapters/security-system.js.map +1 -1
  41. package/dist/homekit/adapters/siren.js +5 -33
  42. package/dist/homekit/adapters/siren.js.map +1 -1
  43. package/dist/homekit/adapters/smart-light.js +19 -217
  44. package/dist/homekit/adapters/smart-light.js.map +1 -1
  45. package/dist/homekit/coverage-matrix.js +8 -67
  46. package/dist/homekit/coverage-matrix.js.map +1 -1
  47. package/dist/homekit/device-control.js +249 -13
  48. package/dist/homekit/device-control.js.map +1 -1
  49. package/dist/homekit/reconciler.js +25 -9
  50. package/dist/homekit/reconciler.js.map +1 -1
  51. package/dist/media/last-successful-image.js +8 -23
  52. package/dist/media/last-successful-image.js.map +1 -1
  53. package/dist/media/live-preview.js +217 -0
  54. package/dist/media/live-preview.js.map +1 -0
  55. package/dist/media/live-stream.js +689 -160
  56. package/dist/media/live-stream.js.map +1 -1
  57. package/dist/media/recording.js +41 -118
  58. package/dist/media/recording.js.map +1 -1
  59. package/dist/media/snapshot.js +22 -17
  60. package/dist/media/snapshot.js.map +1 -1
  61. package/dist/media/station-live-sessions.js +40 -51
  62. package/dist/media/station-live-sessions.js.map +1 -1
  63. package/dist/platform.js +61 -10
  64. package/dist/platform.js.map +1 -1
  65. package/dist/runtime/channel.js +227 -40
  66. package/dist/runtime/channel.js.map +1 -1
  67. package/dist/runtime/device-settings.js +88 -0
  68. package/dist/runtime/device-settings.js.map +1 -0
  69. package/dist/runtime/owner.js +30 -5
  70. package/dist/runtime/owner.js.map +1 -1
  71. package/dist/runtime/sdk-client.js +21 -17
  72. package/dist/runtime/sdk-client.js.map +1 -1
  73. package/dist/runtime/tracker.js +8 -38
  74. package/dist/runtime/tracker.js.map +1 -1
  75. package/dist/storage.js +1 -1
  76. package/dist/storage.js.map +1 -1
  77. package/dist/ui/dashboard.js +36 -16
  78. package/dist/ui/dashboard.js.map +1 -1
  79. package/dist/ui/runtime-channel-client.js +155 -184
  80. package/dist/ui/runtime-channel-client.js.map +1 -1
  81. package/dist/ui/server.js +94 -41
  82. package/dist/ui/server.js.map +1 -1
  83. package/homebridge-ui/public/app.css +531 -239
  84. package/homebridge-ui/public/assets/icons/auto_fix_high.svg +1 -0
  85. package/homebridge-ui/public/assets/icons/battery_0.svg +1 -0
  86. package/homebridge-ui/public/assets/icons/battery_1.svg +1 -0
  87. package/homebridge-ui/public/assets/icons/battery_2.svg +1 -0
  88. package/homebridge-ui/public/assets/icons/battery_3.svg +1 -0
  89. package/homebridge-ui/public/assets/icons/battery_4.svg +1 -0
  90. package/homebridge-ui/public/assets/icons/battery_5.svg +1 -0
  91. package/homebridge-ui/public/assets/icons/battery_6.svg +1 -0
  92. package/homebridge-ui/public/assets/icons/battery_full.svg +1 -0
  93. package/homebridge-ui/public/assets/icons/contact_support.svg +1 -0
  94. package/homebridge-ui/public/assets/icons/delete.svg +1 -0
  95. package/homebridge-ui/public/assets/icons/download.svg +1 -0
  96. package/homebridge-ui/public/assets/icons/extension_off.svg +1 -0
  97. package/homebridge-ui/public/assets/icons/info.svg +1 -0
  98. package/homebridge-ui/public/assets/icons/key_off.svg +1 -0
  99. package/homebridge-ui/public/assets/icons/keyboard_arrow_left.svg +1 -0
  100. package/homebridge-ui/public/assets/icons/keyboard_arrow_right.svg +1 -0
  101. package/homebridge-ui/public/assets/icons/keyboard_double_arrow_left.svg +1 -0
  102. package/homebridge-ui/public/assets/icons/keyboard_double_arrow_right.svg +1 -0
  103. package/homebridge-ui/public/assets/icons/movie_off.svg +1 -0
  104. package/homebridge-ui/public/assets/icons/play_arrow.svg +1 -0
  105. package/homebridge-ui/public/assets/icons/refresh.svg +1 -0
  106. package/homebridge-ui/public/assets/icons/satellite_alt.svg +1 -0
  107. package/homebridge-ui/public/assets/icons/search_off.svg +1 -0
  108. package/homebridge-ui/public/assets/icons/sensors_off.svg +1 -0
  109. package/homebridge-ui/public/assets/icons/settings_photo_camera.svg +1 -0
  110. package/homebridge-ui/public/assets/icons/solar_power.svg +1 -0
  111. package/homebridge-ui/public/assets/icons/stop.svg +1 -0
  112. package/homebridge-ui/public/assets/icons/videocam_off.svg +1 -0
  113. package/homebridge-ui/public/i18n/en.json +161 -82
  114. package/homebridge-ui/public/i18n/fr.json +162 -83
  115. package/homebridge-ui/public/index.html +224 -96
  116. package/homebridge-ui/public/js/app.js +821 -435
  117. package/homebridge-ui/public/js/dashboard.js +148 -31
  118. package/i18n/runtime/en.json +10 -3
  119. package/package.json +10 -11
  120. package/homebridge-ui/public/js/legacy-settings.js +0 -37
  121. package/homebridge-ui/public/js/profile-wizard.js +0 -61
@@ -107,6 +107,11 @@ body {
107
107
  sans-serif;
108
108
  -webkit-font-smoothing: antialiased;
109
109
  text-rendering: optimizeLegibility;
110
+ accent-color: var(--accent);
111
+ }
112
+
113
+ ::selection {
114
+ background: color-mix(in srgb, var(--accent) 28%, transparent);
110
115
  }
111
116
 
112
117
  .shell {
@@ -263,7 +268,8 @@ h2 {
263
268
  color: var(--muted);
264
269
  }
265
270
 
266
- .diagnostics-panel select {
271
+ .diagnostics-panel select,
272
+ .wizard-setting select {
267
273
  width: 100%;
268
274
  min-height: 44px;
269
275
  margin-top: 8px;
@@ -274,7 +280,9 @@ h2 {
274
280
  padding: 8px 12px;
275
281
  }
276
282
 
277
- .diagnostics-actions {
283
+ .diagnostics-actions:not([hidden]) {
284
+ display: flex;
285
+ gap: 8px;
278
286
  margin-top: 4px;
279
287
  }
280
288
 
@@ -290,42 +298,25 @@ h2 {
290
298
  padding: clamp(16px, 4vw, 24px);
291
299
  }
292
300
 
293
- .diagnostics-question,
294
- .diagnostics-direct,
295
- .diagnostics-frequency,
296
- .diagnostics-match {
301
+ .diagnostics-question {
297
302
  display: grid;
298
303
  gap: 16px;
299
304
  }
300
305
 
301
- .diagnostics-question > strong,
302
- .diagnostics-frequency > strong,
303
- .diagnostics-match > strong {
304
- font-size: clamp(17px, 4vw, 21px);
305
- line-height: 1.35;
306
- letter-spacing: -0.01em;
307
- text-wrap: balance;
308
- }
309
-
310
- .diagnostics-match p {
311
- margin-bottom: 0;
312
- }
313
-
314
- .diagnostics-choice-actions {
315
- display: grid;
316
- grid-template-columns: repeat(2, minmax(0, 1fr));
317
- gap: 8px;
306
+ .diagnostics-question > h3,
307
+ .diagnostics-guidance > h3 {
308
+ margin: 0;
318
309
  }
319
310
 
320
- .diagnostics-choice-actions button {
321
- width: 100%;
322
- padding-inline: 16px;
311
+ .diagnostics-guidance > h3 {
312
+ font-size: inherit;
323
313
  }
324
314
 
325
- button.diagnostics-secondary-action {
326
- border: 1px solid var(--line-strong);
327
- background: transparent;
328
- color: var(--ink);
315
+ .diagnostics-question > h3 {
316
+ font-size: clamp(17px, 4vw, 21px);
317
+ line-height: 1.35;
318
+ letter-spacing: -0.01em;
319
+ text-wrap: balance;
329
320
  }
330
321
 
331
322
  button.diagnostics-quiet-action {
@@ -366,103 +357,83 @@ button.diagnostics-quiet-action {
366
357
  text-transform: uppercase;
367
358
  }
368
359
 
369
- .diagnostics-actions button {
370
- width: 100%;
360
+ .diagnostics-actions [data-diagnostics-reproduction] {
361
+ flex: 1 1 auto;
371
362
  }
372
363
 
364
+ .diagnostics-result h2,
373
365
  .diagnostics-result p {
374
366
  margin-bottom: 0;
375
367
  }
376
368
 
377
- .diagnostics-privacy {
369
+ .diagnostics-note {
370
+ margin-bottom: 0;
378
371
  color: var(--muted);
379
372
  font-size: 12px;
380
373
  }
381
374
 
382
- .diagnostics-result {
375
+ .diagnostics-result[open] {
383
376
  display: grid;
377
+ max-width: 560px;
384
378
  gap: 8px;
385
379
  }
386
380
 
387
381
  /*
388
- * The two steps of the handoff share one shape, so the sequence reads as a sequence: same height, same
389
- * gap, same badge, and a colour each. The download is a button because it acts here; the report is an
390
- * anchor because it navigates to GitHub.
382
+ * The download is a button because it acts here; the two ways to report are anchors because they navigate to
383
+ * GitHub, side by side because either one finishes the handoff.
391
384
  */
392
385
  .diagnostics-result [data-diagnostics-export],
393
- .diagnostics-result [data-diagnostics-issue] {
386
+ .diagnostics-handoff a {
394
387
  display: flex;
395
- width: 100%;
396
388
  min-height: 44px;
397
389
  align-items: center;
398
390
  justify-content: center;
399
- gap: 10px;
400
- margin-top: 8px;
401
391
  border-radius: var(--radius-control);
392
+ padding: 8px 12px;
402
393
  font-weight: 600;
394
+ text-align: center;
403
395
  text-decoration: none;
404
396
  }
405
397
 
406
398
  /*
407
- * The step number is generated content rather than an element, because translation assigns `textContent`
408
- * on the control and would remove any child it had.
399
+ * The dialog sits in the top layer, which adds nothing to the page height Homebridge sizes its iframe by, so the
400
+ * stage keeps room for it rather than leaving it to scroll inside a sliver.
409
401
  */
410
- .diagnostics-result [data-diagnostics-export]::before,
411
- .diagnostics-result [data-diagnostics-issue]::before {
412
- display: inline-flex;
413
- width: 22px;
414
- height: 22px;
415
- flex: none;
416
- align-items: center;
417
- justify-content: center;
418
- border-radius: 50%;
419
- font-size: 12px;
420
- font-variant-numeric: tabular-nums;
421
- font-weight: 700;
402
+ .diagnostics-stage:has(> .diagnostics-result[open]) {
403
+ min-height: 320px;
422
404
  }
423
405
 
424
- /* Each badge inverts its own control's pair, so the digit keeps its contrast on either background. */
425
- .diagnostics-result [data-diagnostics-export]::before {
426
- background: var(--on-accent);
427
- color: var(--accent);
428
- content: '1';
429
- }
430
-
431
- /*
432
- * Step two is the same progression as step one and is filled differently rather than coloured
433
- * differently: a success colour would claim the task is complete, and colour alone must not be what
434
- * tells two controls apart.
435
- */
436
- .diagnostics-result [data-diagnostics-issue] {
437
- border: 1px solid var(--accent);
438
- background: transparent;
439
- color: var(--accent-ink);
406
+ .diagnostics-handoff:not([hidden]) {
407
+ display: grid;
408
+ grid-template-columns: 1fr 1fr;
409
+ gap: 8px;
410
+ margin-top: 8px;
440
411
  }
441
412
 
442
- .diagnostics-result [data-diagnostics-issue]::before {
413
+ .diagnostics-handoff [data-diagnostics-issue] {
443
414
  background: var(--accent);
444
415
  color: var(--on-accent);
445
- content: '2';
446
416
  }
447
417
 
448
- /* Without an archive there is nothing to attach, so the step is present and not yet a link. */
449
- .diagnostics-result a[data-diagnostics-issue][aria-disabled='true'] {
450
- border-color: var(--line);
451
- color: var(--ink-disabled);
452
- cursor: default;
418
+ .diagnostics-handoff [data-diagnostics-existing-issue] {
419
+ border: 1px solid var(--line-strong);
420
+ color: var(--ink);
421
+ }
422
+
423
+ .diagnostics-result [data-diagnostics-result-status]:empty {
424
+ display: none;
453
425
  }
454
426
 
455
- .diagnostics-result a[data-diagnostics-issue][aria-disabled='true']::before {
456
- background: var(--ink-disabled);
457
- color: var(--panel);
427
+ .diagnostics-result [data-diagnostics-download-again],
428
+ .diagnostics-result [data-diagnostics-start-another] {
429
+ justify-self: center;
458
430
  }
459
431
 
460
432
  /*
461
- * The opening screen: every area at once, three to a row.
433
+ * The opening screen: every area at once, three to a row, each label over its one-line summary.
462
434
  *
463
- * Each tile carries a Material Symbols outline glyph (Apache-2.0) as a mask rather than an element, because
464
- * translation assigns `textContent` on the tile and would remove any child it had. A mask also lets the glyph
465
- * take the tile's own colour in every state instead of being baked into an image. The data URI is inline, so
435
+ * Each tile carries a Material Symbols outline glyph (Apache-2.0) as a mask rather than an element. A mask lets
436
+ * the glyph take the tile's own colour in every state instead of being baked into an image. The data URI is inline, so
466
437
  * the panel renders the same on a host with no route to the internet.
467
438
  */
468
439
  .diagnostics-tiles {
@@ -479,7 +450,7 @@ button.diagnostics-quiet-action {
479
450
  align-items: center;
480
451
  justify-content: center;
481
452
  gap: 8px;
482
- padding: 10px 6px;
453
+ padding: 12px 8px;
483
454
  border: 1px solid var(--line-strong);
484
455
  background: transparent;
485
456
  color: var(--ink);
@@ -501,6 +472,12 @@ button.diagnostics-quiet-action {
501
472
  mask-size: contain;
502
473
  }
503
474
 
475
+ .diagnostics-tiles small {
476
+ color: var(--muted);
477
+ font-size: 11px;
478
+ font-weight: 400;
479
+ }
480
+
504
481
  .diagnostics-tiles button:hover:not(:disabled) {
505
482
  border-color: var(--accent);
506
483
  background: var(--accent-soft);
@@ -535,107 +512,6 @@ button.diagnostics-quiet-action {
535
512
  mask-image: url('data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%20-960%20960%20960%22%3E%3Cpath%20d=%22M513.5-254.5Q528-269%20528-290t-14.5-35.5Q499-340%20478-340t-35.5%2014.5Q428-311%20428-290t14.5%2035.5Q457-240%20478-240t35.5-14.5ZM442-394h74q0-33%207.5-52t42.5-52q26-26%2041-49.5t15-56.5q0-56-41-86t-97-30q-57%200-92.5%2030T342-618l66%2026q5-18%2022.5-39t53.5-21q32%200%2048%2017.5t16%2038.5q0%2020-12%2037.5T506-526q-44%2039-54%2059t-10%2073Zm38%20314q-83%200-156-31.5T197-197q-54-54-85.5-127T80-480q0-83%2031.5-156T197-763q54-54%20127-85.5T480-880q83%200%20156%2031.5T763-763q54%2054%2085.5%20127T880-480q0%2083-31.5%20156T763-197q-54%2054-127%2085.5T480-80Zm0-80q134%200%20227-93t93-227q0-134-93-227t-227-93q-134%200-227%2093t-93%20227q0%20134%2093%20227t227%2093Zm0-320Z%22/%3E%3C/svg%3E');
536
513
  }
537
514
 
538
- /* The same grid as the areas, so picking hardware reads as the same kind of act as picking an area. */
539
- .diagnostics-device-list {
540
- display: grid;
541
- max-height: 264px;
542
- grid-template-columns: repeat(3, minmax(0, 1fr));
543
- gap: 8px;
544
- overflow-y: auto;
545
- margin: 8px 0 12px;
546
- }
547
-
548
- .diagnostics-device-list button {
549
- display: flex;
550
- min-height: 104px;
551
- flex-direction: column;
552
- align-items: center;
553
- justify-content: center;
554
- gap: 6px;
555
- padding: 8px 6px;
556
- border: 1px solid var(--line-strong);
557
- background: transparent;
558
- color: var(--ink);
559
- font-size: 11px;
560
- font-weight: 600;
561
- line-height: 1.2;
562
- overflow-wrap: break-word;
563
- text-align: center;
564
- }
565
-
566
- .diagnostics-device-list img {
567
- width: 24px;
568
- height: 24px;
569
- flex: none;
570
- }
571
-
572
- /*
573
- * The device's own artwork, contained rather than cropped and at half the tile.
574
- *
575
- * `contain` is how the devices view draws the same image, because it is a cutout of the hardware and not a
576
- * scene: cropping it to fill cuts the device out of its own picture. Half the tile keeps the name it belongs
577
- * to from competing with it. That view's darkening filter is not carried over — it exists to seat the image
578
- * under white text on a coloured tile, and these tiles read dark text on the panel.
579
- */
580
- .diagnostics-device-list img.device-photo {
581
- width: 100%;
582
- max-width: 100%;
583
- height: auto;
584
- max-height: 50%;
585
- object-fit: contain;
586
- }
587
-
588
- .diagnostics-device-list button:hover {
589
- border-color: var(--accent);
590
- background: var(--accent-soft);
591
- }
592
-
593
- /* Pressed is the selection, so it is the border and the fill that say so rather than the colour alone. */
594
- .diagnostics-device-list button[aria-pressed='true'] {
595
- border-color: var(--accent);
596
- border-width: 2px;
597
- background: var(--accent-soft);
598
- color: var(--accent-ink);
599
- }
600
-
601
- .diagnostics-step-hint {
602
- margin: 4px 0 0;
603
- color: var(--muted);
604
- font-size: 12px;
605
- }
606
-
607
- .diagnostics-local-note {
608
- margin: 12px 0 0;
609
- color: var(--ink-secondary);
610
- font-size: 12px;
611
- }
612
-
613
- .diagnostics-result button {
614
- width: 100%;
615
- }
616
-
617
- .diagnostics-manifest {
618
- margin-top: 12px;
619
- border-block: 1px solid var(--line);
620
- padding-block: 8px;
621
- font-size: 12px;
622
- font-variant-numeric: tabular-nums;
623
- }
624
-
625
- .diagnostics-manifest ul {
626
- margin: 8px 0;
627
- padding-left: 20px;
628
- }
629
-
630
- .diagnostics-review-confirm {
631
- display: flex;
632
- align-items: flex-start;
633
- gap: 8px;
634
- margin-top: 12px;
635
- font-size: 12px;
636
- cursor: pointer;
637
- }
638
-
639
515
  .advanced-fields {
640
516
  display: grid;
641
517
  grid-template-columns: 1fr;
@@ -670,6 +546,12 @@ button.diagnostics-quiet-action {
670
546
  font: inherit;
671
547
  }
672
548
 
549
+ .advanced-fields input[type='checkbox'] {
550
+ justify-self: start;
551
+ width: auto;
552
+ min-height: 0;
553
+ }
554
+
673
555
  .advanced-fields small,
674
556
  .advanced-device-help {
675
557
  color: var(--muted);
@@ -755,6 +637,7 @@ button.diagnostics-quiet-action {
755
637
  border: 1px solid var(--line-strong);
756
638
  border-radius: var(--radius-control);
757
639
  background: var(--panel);
640
+ color: var(--ink);
758
641
  padding: 0;
759
642
  }
760
643
 
@@ -789,38 +672,84 @@ button.diagnostics-quiet-action {
789
672
  transform 150ms var(--ease-out);
790
673
  }
791
674
 
675
+ .dashboard-action[data-collecting],
676
+ .dashboard-action:has(.diagnostics-name) {
677
+ width: auto;
678
+ grid-auto-flow: column;
679
+ gap: 8px;
680
+ padding: 0 12px;
681
+ }
682
+
792
683
  .dashboard-action[data-collecting] {
793
684
  border-color: var(--accent);
794
685
  background: color-mix(in srgb, var(--accent) 16%, var(--panel));
795
686
  }
796
687
 
688
+ /*
689
+ * The dashboard's diagnostics action is named in words at rest, and a capture under way replaces that name
690
+ * beside the icon, so neither rests on the icon or on colour alone.
691
+ */
692
+ .diagnostics-name,
693
+ .diagnostics-capturing {
694
+ font-size: 12px;
695
+ font-weight: 600;
696
+ }
697
+
698
+ .diagnostics-capturing,
699
+ .dashboard-action[data-collecting] .diagnostics-name {
700
+ display: none;
701
+ }
702
+
703
+ .dashboard-action[data-collecting] .diagnostics-capturing {
704
+ display: inline;
705
+ }
706
+
707
+ /* A problem the dashboard shows: an alert ring, and a dot so the state does not rest on colour alone. */
708
+ .dashboard-action[data-attention] {
709
+ border-color: var(--alert);
710
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--alert) 24%, transparent);
711
+ }
712
+
713
+ .dashboard-action[data-attention]::before {
714
+ content: '';
715
+ position: absolute;
716
+ top: -5px;
717
+ right: -5px;
718
+ width: 8px;
719
+ height: 8px;
720
+ border: 2px solid var(--panel);
721
+ border-radius: var(--radius-pill);
722
+ background: var(--alert);
723
+ }
724
+
797
725
  .dashboard-action:hover::after,
798
726
  .dashboard-action:focus-visible::after {
799
727
  opacity: 1;
800
728
  transform: translateY(0);
801
729
  }
802
730
 
803
- /*
804
- * The way into a diagnostic run, standing where a status pill used to. A pill saying "Connected" was decoration on
805
- * the healthy path and a dead end on every other; this appears only where a run would explain something.
806
- */
807
- .dashboard-diagnose {
808
- flex: 0 0 auto;
809
- /* Buttons are full width by default here, and this one sits inline beside the title. */
810
- width: auto;
811
- border: 1px solid var(--alert);
812
- border-radius: var(--radius-pill);
813
- background: color-mix(in srgb, var(--alert) 16%, var(--panel));
731
+ .blocking-dialog {
732
+ max-width: 420px;
733
+ border: 0;
734
+ border-radius: var(--radius-panel);
735
+ background: var(--panel);
736
+ box-shadow: var(--shadow-overlay);
814
737
  color: var(--ink);
815
- padding: 6px 12px;
816
- font: inherit;
817
- font-size: 12px;
818
- font-weight: 600;
819
- cursor: pointer;
738
+ padding: 24px;
739
+ }
740
+
741
+ .blocking-dialog::backdrop {
742
+ background: rgb(0 0 0 / 30%);
743
+ backdrop-filter: blur(5px);
820
744
  }
821
745
 
822
- .dashboard-diagnose:hover {
823
- background: color-mix(in srgb, var(--alert) 26%, var(--panel));
746
+ .blocking-dialog h2 {
747
+ margin: 0 0 8px;
748
+ }
749
+
750
+ .blocking-dialog p {
751
+ margin: 0 0 20px;
752
+ color: var(--ink-secondary);
824
753
  }
825
754
 
826
755
  .device-group + .device-group {
@@ -899,7 +828,7 @@ button.diagnostics-quiet-action {
899
828
  padding: 12px;
900
829
  }
901
830
 
902
- .device-tile:not([data-device-class='camera']) /*
831
+ /*
903
832
  * A device's settings are a page rather than a face on the tile, so they are read at the page's scale and have as
904
833
  * much room as the device needs. The tile is the summary and the way in; nothing about its size bounds this.
905
834
  */
@@ -944,7 +873,7 @@ button.diagnostics-quiet-action {
944
873
  }
945
874
  }
946
875
 
947
- .diagnostic-panel img {
876
+ .device-tile:not([data-device-class='camera']) .diagnostic-panel img {
948
877
  display: none;
949
878
  }
950
879
 
@@ -962,7 +891,7 @@ button.diagnostics-quiet-action {
962
891
  width: 8px;
963
892
  height: 8px;
964
893
  border: 2px solid var(--tile-bg);
965
- border-radius: 50%;
894
+ border-radius: var(--radius-pill);
966
895
  background: var(--alert);
967
896
  pointer-events: none;
968
897
  }
@@ -1368,6 +1297,127 @@ button.diagnostics-quiet-action {
1368
1297
  font-weight: 700;
1369
1298
  }
1370
1299
 
1300
+ /*
1301
+ * The guard-mode map: four HomeKit states against nine station modes, one pairing per row.
1302
+ *
1303
+ * A table because the content is one, which also gives each select its own row label without a second element
1304
+ * beside it. Row rules and no box: the panel's own surface encloses it, and the one control in the row is the
1305
+ * select, which carries the only border here.
1306
+ */
1307
+ .arming-setting {
1308
+ color: var(--muted);
1309
+ font-size: 11px;
1310
+ }
1311
+
1312
+ .arming-setting > summary {
1313
+ display: grid;
1314
+ grid-template-columns: minmax(0, 1fr) 12px;
1315
+ align-items: center;
1316
+ gap: 4px 8px;
1317
+ cursor: pointer;
1318
+ }
1319
+
1320
+ /* `display: grid` on a summary drops the native marker, so the affordance is drawn back in as a caret. */
1321
+ .arming-setting > summary::marker,
1322
+ .arming-setting > summary::-webkit-details-marker {
1323
+ content: '';
1324
+ display: none;
1325
+ }
1326
+
1327
+ .arming-setting > summary::after {
1328
+ content: '';
1329
+ grid-row: 1 / span 2;
1330
+ grid-column: 2;
1331
+ width: 6px;
1332
+ height: 6px;
1333
+ border-right: 1.5px solid var(--muted);
1334
+ border-bottom: 1.5px solid var(--muted);
1335
+ transform: rotate(45deg);
1336
+ transition: transform 140ms var(--ease-out);
1337
+ }
1338
+
1339
+ .arming-setting[open] > summary::after {
1340
+ transform: rotate(-135deg);
1341
+ }
1342
+
1343
+ /* The map as one line, which is the answer most households only ever read. */
1344
+ .arming-summary {
1345
+ overflow: hidden;
1346
+ color: var(--muted);
1347
+ font-size: 10px;
1348
+ text-overflow: ellipsis;
1349
+ white-space: nowrap;
1350
+ }
1351
+
1352
+ .arming-setting[open] > summary {
1353
+ margin-bottom: 8px;
1354
+ }
1355
+
1356
+ .arming-table {
1357
+ width: 100%;
1358
+ border-collapse: collapse;
1359
+ }
1360
+
1361
+ .arming-table thead th {
1362
+ padding: 0 0 4px;
1363
+ border-bottom: 1px solid var(--line-strong);
1364
+ color: var(--muted);
1365
+ font-size: 10px;
1366
+ font-weight: 600;
1367
+ letter-spacing: 0.04em;
1368
+ text-align: left;
1369
+ text-transform: uppercase;
1370
+ }
1371
+
1372
+ .arming-table tbody tr + tr th,
1373
+ .arming-table tbody tr + tr td {
1374
+ border-top: 1px solid var(--line);
1375
+ }
1376
+
1377
+ .arming-table th[scope='row'] {
1378
+ width: 38%;
1379
+ padding: 4px 8px 4px 0;
1380
+ color: var(--ink);
1381
+ font-size: 11px;
1382
+ font-weight: 600;
1383
+ text-align: left;
1384
+ }
1385
+
1386
+ .arming-table th[scope='row'] label {
1387
+ cursor: pointer;
1388
+ }
1389
+
1390
+ .arming-table td {
1391
+ padding: 4px 0;
1392
+ }
1393
+
1394
+ .arming-table select {
1395
+ width: 100%;
1396
+ min-height: 28px;
1397
+ border: 1px solid var(--line-strong);
1398
+ border-radius: var(--radius-control);
1399
+ background: var(--panel);
1400
+ color: var(--ink);
1401
+ padding: 4px 8px;
1402
+ font-size: 11px;
1403
+ }
1404
+
1405
+ /* The changed row carries the weight, so a pending map reads as which pairing moved rather than as a heavier table. */
1406
+ .arming-setting .snapshot-help {
1407
+ margin-top: 8px;
1408
+ }
1409
+
1410
+ .arming-table select.preference-control-changed {
1411
+ border-color: var(--accent);
1412
+ font-weight: 700;
1413
+ }
1414
+
1415
+ @media (pointer: coarse) {
1416
+ .arming-table select {
1417
+ min-height: 44px;
1418
+ }
1419
+ }
1420
+
1371
1421
  .preference-grid .preference-changed .snapshot-description {
1372
1422
  font-weight: 400;
1373
1423
  }
@@ -1469,7 +1519,7 @@ button.diagnostics-quiet-action {
1469
1519
  left: 2px;
1470
1520
  width: 14px;
1471
1521
  height: 14px;
1472
- border-radius: 50%;
1522
+ border-radius: var(--radius-pill);
1473
1523
  background: var(--panel);
1474
1524
  box-shadow: 0 1px 2px rgb(0 0 0 / 22%);
1475
1525
  transition: transform 140ms var(--ease-out);
@@ -1541,8 +1591,7 @@ button.diagnostics-quiet-action {
1541
1591
  cursor: pointer;
1542
1592
  }
1543
1593
 
1544
- .first-setup-confirmation input,
1545
- .diagnostics-review-confirm input {
1594
+ .first-setup-confirmation input {
1546
1595
  appearance: none;
1547
1596
  display: grid;
1548
1597
  place-content: center;
@@ -1557,8 +1606,7 @@ button.diagnostics-quiet-action {
1557
1606
  padding: 0;
1558
1607
  }
1559
1608
 
1560
- .first-setup-confirmation input::before,
1561
- .diagnostics-review-confirm input::before {
1609
+ .first-setup-confirmation input::before {
1562
1610
  content: '';
1563
1611
  width: 8px;
1564
1612
  height: 4px;
@@ -1567,14 +1615,12 @@ button.diagnostics-quiet-action {
1567
1615
  transform: rotate(-45deg) scale(0);
1568
1616
  }
1569
1617
 
1570
- .first-setup-confirmation input:checked,
1571
- .diagnostics-review-confirm input:checked {
1618
+ .first-setup-confirmation input:checked {
1572
1619
  border-color: var(--accent);
1573
1620
  background: var(--accent);
1574
1621
  }
1575
1622
 
1576
- .first-setup-confirmation input:checked::before,
1577
- .diagnostics-review-confirm input:checked::before {
1623
+ .first-setup-confirmation input:checked::before {
1578
1624
  transform: rotate(-45deg) scale(1);
1579
1625
  }
1580
1626
 
@@ -1722,7 +1768,6 @@ button {
1722
1768
  text-align: center;
1723
1769
  }
1724
1770
 
1725
- /* One setting per row, so a control that needs width gets it and nothing sits half-read beside it. */
1726
1771
  /*
1727
1772
  * Two lists with a column of transfer buttons between them, sharing the parent's rows so the three columns line up.
1728
1773
  *
@@ -1860,11 +1905,11 @@ button {
1860
1905
  transform 100ms var(--ease-out);
1861
1906
  }
1862
1907
 
1863
- button:hover:not(:disabled) {
1908
+ button:hover:not(:disabled, .wizard-live-play) {
1864
1909
  background: color-mix(in srgb, var(--accent) 88%, var(--on-accent));
1865
1910
  }
1866
1911
 
1867
- button:active:not(:disabled, .device-open-control) {
1912
+ button:active:not(:disabled, .device-open-control, .wizard-live-play) {
1868
1913
  transform: scale(0.97);
1869
1914
  }
1870
1915
 
@@ -1874,7 +1919,6 @@ button:disabled {
1874
1919
  cursor: not-allowed;
1875
1920
  }
1876
1921
 
1877
- button.diagnostics-secondary-action:hover:not(:disabled),
1878
1922
  .dashboard-page-back:hover:not(:disabled),
1879
1923
  .warm-up-controls button:hover:not(:disabled),
1880
1924
  .warm-up-column button:hover:not(:disabled),
@@ -1897,6 +1941,21 @@ button.diagnostics-quiet-action:hover:not(:disabled) {
1897
1941
  background: transparent;
1898
1942
  }
1899
1943
 
1944
+ .wizard-map-device:hover:not(:disabled, [aria-current='step']) {
1945
+ background: color-mix(in srgb, var(--ink) 5%, transparent);
1946
+ }
1947
+
1948
+ .wizard-map-device[aria-current='step']:hover {
1949
+ background: color-mix(in srgb, var(--accent) 16%, transparent);
1950
+ }
1951
+
1952
+ /* The frame is black in both themes, so the icon's fill is a fixed deep teal the white glyph reads against; the
1953
+ button itself covers the whole frame and stays transparent. */
1954
+ .wizard-live-play:hover:not(:disabled) img,
1955
+ .wizard-live-play:focus-visible img {
1956
+ background: color-mix(in srgb, var(--accent) 55%, #000000);
1957
+ }
1958
+
1900
1959
  /*
1901
1960
  * The tile is the dashboard's primary target and artwork covers everything but its edge, so the edge carries the
1902
1961
  * hover. A step in `--line` alone was too quiet to register as feedback; the accent in the edge is not.
@@ -1906,8 +1965,7 @@ button.diagnostics-quiet-action:hover:not(:disabled) {
1906
1965
  }
1907
1966
 
1908
1967
  .warm-up-controls button:disabled,
1909
- .warm-up-column button:disabled,
1910
- button.diagnostics-secondary-action:disabled {
1968
+ .warm-up-column button:disabled {
1911
1969
  border-color: var(--line);
1912
1970
  background: var(--panel);
1913
1971
  color: var(--ink-disabled);
@@ -1961,11 +2019,11 @@ input:focus-visible {
1961
2019
  grid-template-columns: repeat(2, minmax(0, 1fr));
1962
2020
  }
1963
2021
 
1964
- .diagnostics-device-list {
1965
- grid-template-columns: repeat(2, minmax(0, 1fr));
2022
+ /* The row keeps its three actions on one line by leaving the diagnostics action to its icon. */
2023
+ .diagnostics-name {
2024
+ display: none;
1966
2025
  }
1967
2026
 
1968
-
1969
2027
  .status-row {
1970
2028
  display: grid;
1971
2029
  }
@@ -1979,11 +2037,11 @@ input:focus-visible {
1979
2037
  }
1980
2038
 
1981
2039
  .legacy-notice {
1982
- display: grid;
2040
+ grid-template-columns: auto minmax(0, 1fr);
1983
2041
  }
1984
2042
 
1985
2043
  .legacy-notice button {
1986
- width: 100%;
2044
+ grid-column: 1 / -1;
1987
2045
  }
1988
2046
  }
1989
2047
 
@@ -2030,22 +2088,18 @@ input:focus-visible {
2030
2088
  }
2031
2089
 
2032
2090
  @media (max-width: 520px) {
2033
- .advanced-fields {
2034
- grid-template-columns: 1fr;
2035
- }
2036
-
2037
2091
  .diagnostics-actions button {
2038
2092
  width: 100%;
2039
2093
  }
2040
2094
 
2041
- .diagnostics-choice-actions {
2042
- display: grid;
2043
- grid-template-columns: repeat(2, minmax(0, 1fr));
2095
+ /* Two actions side by side leave each a sliver of a label at this width, so they stack. */
2096
+ .diagnostics-handoff:not([hidden]) {
2097
+ grid-template-columns: 1fr;
2044
2098
  }
2045
2099
 
2046
- .diagnostics-choice-actions button {
2047
- width: 100%;
2048
- min-width: 0;
2100
+ .diagnostics-choice-actions {
2101
+ display: grid;
2102
+ gap: 8px;
2049
2103
  }
2050
2104
 
2051
2105
  .device-grid {
@@ -2080,7 +2134,6 @@ input:focus-visible {
2080
2134
  gap: 4px;
2081
2135
  padding: 32px 16px 8px;
2082
2136
  }
2083
-
2084
2137
  }
2085
2138
 
2086
2139
  .shell[data-theme='dark'] {
@@ -2132,3 +2185,242 @@ input:focus-visible {
2132
2185
  .shell[data-theme='dark'] .eufy-logo .logo-dark {
2133
2186
  display: block;
2134
2187
  }
2188
+
2189
+ /* Sized by its container alone, never by a picture, and never selected or dragged by a click on it. One 16:9 frame
2190
+ split down the middle: the camera's left half beside the right half of the same view as HomeKit receives it. */
2191
+ .wizard-live {
2192
+ position: relative;
2193
+ aspect-ratio: 16 / 9;
2194
+ min-width: 0;
2195
+ overflow: hidden;
2196
+ border-radius: var(--radius-card);
2197
+ background: #000000;
2198
+ user-select: none;
2199
+ -webkit-user-select: none;
2200
+ -webkit-touch-callout: none;
2201
+ }
2202
+
2203
+ .wizard-live figure {
2204
+ position: absolute;
2205
+ inset: 0;
2206
+ margin: 0;
2207
+ clip-path: inset(0 50% 0 0);
2208
+ }
2209
+
2210
+ .wizard-live figure + figure {
2211
+ clip-path: inset(0 0 0 50%);
2212
+ }
2213
+
2214
+ /* Until the HomeKit side has a picture, the camera's picture shows whole, with no split and no labels. */
2215
+ .wizard-live:has(figure + figure > img[hidden]) figure {
2216
+ clip-path: none;
2217
+ }
2218
+
2219
+ .wizard-live:has(figure + figure > img[hidden]) :is(.wizard-live-label, figure + figure),
2220
+ .wizard-live:has(figure + figure > img[hidden])::after {
2221
+ display: none;
2222
+ }
2223
+
2224
+ .wizard-live::after {
2225
+ position: absolute;
2226
+ inset: 0 auto 0 50%;
2227
+ width: 2px;
2228
+ margin-left: -1px;
2229
+ background: rgb(255 255 255 / 80%);
2230
+ content: '';
2231
+ pointer-events: none;
2232
+ }
2233
+
2234
+ .wizard-live figure > img {
2235
+ display: block;
2236
+ width: 100%;
2237
+ height: 100%;
2238
+ object-fit: contain;
2239
+ pointer-events: none;
2240
+ }
2241
+
2242
+ /* The whole box plays and stops; the pill is the icon. */
2243
+ .wizard-live-play {
2244
+ position: absolute;
2245
+ z-index: 1;
2246
+ inset: 0;
2247
+ display: grid;
2248
+ place-items: center;
2249
+ padding: 0;
2250
+ border: 0;
2251
+ background: transparent;
2252
+ cursor: pointer;
2253
+ }
2254
+
2255
+ .wizard-live-play img {
2256
+ box-sizing: border-box;
2257
+ width: 72px;
2258
+ height: 52px;
2259
+ padding: 8px 18px;
2260
+ border-radius: var(--radius-card);
2261
+ background: rgb(0 0 0 / 70%);
2262
+ }
2263
+
2264
+ /* Stop stays out of the picture until the pointer or the keyboard is on the video. */
2265
+ .wizard-live-stop {
2266
+ opacity: 0;
2267
+ transition: opacity 150ms var(--ease-out);
2268
+ }
2269
+
2270
+ .wizard-live:hover .wizard-live-stop,
2271
+ .wizard-live-stop:focus-visible {
2272
+ opacity: 1;
2273
+ }
2274
+
2275
+ @media (hover: none) {
2276
+ .wizard-live-stop {
2277
+ opacity: 1;
2278
+ }
2279
+ }
2280
+
2281
+ /* The reason the box has no picture, centred and large; below the spinner or the play button when either shows. */
2282
+ .wizard-live-status {
2283
+ position: absolute;
2284
+ inset: 0;
2285
+ display: grid;
2286
+ place-items: center;
2287
+ margin: 0;
2288
+ pointer-events: none;
2289
+ padding: 24px;
2290
+ color: var(--tile-scene-ink);
2291
+ font-size: 1.5rem;
2292
+ font-weight: 600;
2293
+ text-align: center;
2294
+ background: rgb(0 0 0 / 45%);
2295
+ }
2296
+
2297
+ .wizard-live:has(.wizard-live-spinner:not([hidden]), [data-wizard-live-play]:not([hidden])) .wizard-live-status {
2298
+ padding-top: 120px;
2299
+ }
2300
+
2301
+ .wizard-live-status:empty {
2302
+ display: none;
2303
+ }
2304
+
2305
+ /* Over the picture, so showing it does not move the play and stop buttons under the pointer. */
2306
+ .wizard-live-label {
2307
+ position: absolute;
2308
+ inset: 0 50% auto 0;
2309
+ margin: 0;
2310
+ padding: 8px 12px;
2311
+ color: var(--tile-scene-ink);
2312
+ background: linear-gradient(rgb(0 0 0 / 70%), transparent);
2313
+ }
2314
+
2315
+ .wizard-live figure + figure .wizard-live-label {
2316
+ inset: 0 0 auto 50%;
2317
+ text-align: right;
2318
+ }
2319
+
2320
+ .wizard-nav {
2321
+ display: flex;
2322
+ justify-content: flex-end;
2323
+ }
2324
+
2325
+ .wizard-map {
2326
+ display: flex;
2327
+ flex-wrap: wrap;
2328
+ gap: 12px;
2329
+ padding: 12px;
2330
+ border: 1px solid var(--line);
2331
+ border-radius: var(--radius-card);
2332
+ background: var(--panel);
2333
+ }
2334
+
2335
+ .wizard-map-group {
2336
+ flex: 1 1 180px;
2337
+ }
2338
+
2339
+ .wizard-map-base {
2340
+ margin: 0 0 4px;
2341
+ font-weight: 600;
2342
+ }
2343
+
2344
+ .wizard-map-members {
2345
+ display: flex;
2346
+ flex-direction: column;
2347
+ gap: 4px;
2348
+ padding-left: 12px;
2349
+ border-left: 2px solid var(--line-strong);
2350
+ }
2351
+
2352
+ .wizard-map-device {
2353
+ padding: 4px 8px;
2354
+ border: 0;
2355
+ border-radius: var(--radius-sm);
2356
+ background: transparent;
2357
+ color: inherit;
2358
+ text-align: left;
2359
+ cursor: pointer;
2360
+ }
2361
+
2362
+ /* A HomeBase heading is a button and a standalone heading is not: the plain one takes the button's size. */
2363
+ p.wizard-map-base {
2364
+ display: flex;
2365
+ align-items: center;
2366
+ min-height: 44px;
2367
+ padding: 4px 8px;
2368
+ }
2369
+
2370
+ .wizard-map-device[aria-current='step'] {
2371
+ background: color-mix(in srgb, var(--accent) 16%, transparent);
2372
+ color: var(--accent-ink);
2373
+ font-weight: 600;
2374
+ }
2375
+
2376
+ .wizard-live figure + figure .wizard-live-spinner {
2377
+ left: 75%;
2378
+ }
2379
+
2380
+ .wizard-card {
2381
+ padding: 12px 16px;
2382
+ border: 1px solid var(--line);
2383
+ border-radius: var(--radius-card);
2384
+ background: var(--panel);
2385
+ }
2386
+
2387
+ .wizard-card h3 {
2388
+ margin: 0 0 4px;
2389
+ }
2390
+
2391
+ .wizard-live-spinner {
2392
+ position: absolute;
2393
+ inset: 50% auto auto 50%;
2394
+ width: 48px;
2395
+ height: 48px;
2396
+ margin: -24px 0 0 -24px;
2397
+ border: 4px solid rgb(255 255 255 / 30%);
2398
+ border-top-color: var(--tile-scene-ink);
2399
+ border-radius: var(--radius-pill);
2400
+ animation: wizard-live-spin 0.9s linear infinite;
2401
+ }
2402
+
2403
+ @keyframes wizard-live-spin {
2404
+ to {
2405
+ transform: rotate(360deg);
2406
+ }
2407
+ }
2408
+
2409
+ @media (prefers-reduced-motion: reduce) {
2410
+ .wizard-live-spinner {
2411
+ animation-duration: 2.4s;
2412
+ }
2413
+ }
2414
+
2415
+ .wizard-setting {
2416
+ display: flex;
2417
+ align-items: center;
2418
+ justify-content: space-between;
2419
+ gap: 12px;
2420
+ }
2421
+
2422
+ .wizard-setting select {
2423
+ width: auto;
2424
+ min-width: 160px;
2425
+ margin-top: 0;
2426
+ }