@camstack/ui-library 1.2.291 → 1.2.293

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 (88) hide show
  1. package/dist/{MaskShapeCanvas-DI4BY7W2.js → MaskShapeCanvas-C9cJlxvG.js} +3 -1
  2. package/dist/{MaskShapeCanvas-BByN3jvt.cjs → MaskShapeCanvas-NCNwu9_5.cjs} +3 -1
  3. package/dist/{MotionZonesSettings-CiYjd-yl.js → MotionZonesSettings-Bv8XzCa3.js} +45 -48
  4. package/dist/{MotionZonesSettings-BK9R9NIr.cjs → MotionZonesSettings-CPRH536-.cjs} +44 -47
  5. package/dist/{PrivacyMaskSettings-DuXUCUev.cjs → PrivacyMaskSettings-BUWvVF_n.cjs} +33 -42
  6. package/dist/{PrivacyMaskSettings-BmRdUrPp.js → PrivacyMaskSettings-BlkbESYj.js} +34 -43
  7. package/dist/{SceneMonitorEditor-COgAjtq5.cjs → SceneMonitorEditor-D_jkzQ2o.cjs} +189 -86
  8. package/dist/{SceneMonitorEditor-J4Ix_0tu.js → SceneMonitorEditor-i3-nXAEU.js} +190 -87
  9. package/dist/composites/addon-global-settings-form.d.ts +0 -12
  10. package/dist/composites/cap-settings/AutotrackSection.d.ts +0 -16
  11. package/dist/composites/cap-settings/CameraGridLayoutEditor.d.ts +0 -21
  12. package/dist/composites/cap-settings/ConsumablesPanel.d.ts +0 -18
  13. package/dist/composites/cap-settings/MaskShapeCanvas.d.ts +0 -102
  14. package/dist/composites/cap-settings/MotionZonesSettings.d.ts +0 -38
  15. package/dist/composites/cap-settings/SceneReferenceGallery.d.ts +0 -38
  16. package/dist/composites/cap-settings/scene-monitor-copy.d.ts +12 -6
  17. package/dist/composites/clips/clip-source-state.d.ts +3 -2
  18. package/dist/composites/clips/clip-thumbnail-fetch.d.ts +2 -1
  19. package/dist/composites/clips/clip-tile-media.d.ts +3 -2
  20. package/dist/composites/clips/clip-view-state.d.ts +2 -1
  21. package/dist/composites/confirm-dialog.d.ts +0 -62
  22. package/dist/composites/data-table.d.ts +2 -11
  23. package/dist/composites/detection-canvas.d.ts +0 -9
  24. package/dist/composites/detection-result-tree.d.ts +0 -12
  25. package/dist/composites/device-controls/container-primary-hero.d.ts +0 -17
  26. package/dist/composites/device-controls/humidifier-control.d.ts +0 -17
  27. package/dist/composites/device-controls/lawn-mower-control.d.ts +2 -1
  28. package/dist/composites/device-controls/pet-feeder-panel.d.ts +0 -18
  29. package/dist/composites/device-controls/script-automation-controls.d.ts +2 -1
  30. package/dist/composites/device-controls/sensor-hero-card.d.ts +0 -18
  31. package/dist/composites/device-controls/update-control.d.ts +0 -13
  32. package/dist/composites/device-controls/vacuum-control.d.ts +2 -1
  33. package/dist/composites/device-controls/valve-control.d.ts +2 -1
  34. package/dist/composites/device-controls/weather-control.d.ts +2 -1
  35. package/dist/composites/device-item/actions.d.ts +0 -22
  36. package/dist/composites/device-item/helpers.d.ts +2 -1
  37. package/dist/composites/device-item/index.d.ts +0 -9
  38. package/dist/composites/device-list/batch-actions-bar.d.ts +0 -11
  39. package/dist/composites/device-list/filter-chips.d.ts +0 -13
  40. package/dist/composites/device-list/page-size-select.d.ts +2 -1
  41. package/dist/composites/device-list/url-state.d.ts +2 -3
  42. package/dist/composites/device-meta.d.ts +14 -7
  43. package/dist/composites/device-picker/DevicePicker.d.ts +1 -1
  44. package/dist/composites/device-picker/device-filter-bar.d.ts +2 -1
  45. package/dist/composites/device-picker/device-parent.d.ts +2 -1
  46. package/dist/composites/device-picker/device-selection-rows.d.ts +6 -1
  47. package/dist/composites/device-selector-picker.d.ts +25 -1
  48. package/dist/composites/device-step-matrix.d.ts +0 -26
  49. package/dist/composites/error-box.d.ts +0 -15
  50. package/dist/composites/event-stream.d.ts +2 -1
  51. package/dist/composites/extensions/extensions-view-state.d.ts +2 -1
  52. package/dist/composites/hls-quality.d.ts +3 -2
  53. package/dist/composites/index.d.ts +0 -2
  54. package/dist/composites/notification-targets-field.d.ts +2 -2
  55. package/dist/composites/pipeline-matrix-shared.d.ts +2 -1
  56. package/dist/composites/provider-entries/AddProviderEntryDialog.d.ts +1 -1
  57. package/dist/composites/provider-entries/ProviderEntryPills.d.ts +1 -1
  58. package/dist/composites/ptz-overlay.d.ts +0 -19
  59. package/dist/composites/qr-code.d.ts +0 -14
  60. package/dist/composites/scope-picker.d.ts +2 -1
  61. package/dist/composites/step-inspector/layered-settings-form.d.ts +0 -11
  62. package/dist/composites/step-inspector/step-inspector-panel.d.ts +0 -6
  63. package/dist/composites/stream-broker-selector.d.ts +0 -21
  64. package/dist/composites/training/TrainingBaseModelPicker.d.ts +0 -10
  65. package/dist/composites/training/retrain-review-badge.d.ts +2 -1
  66. package/dist/composites/webrtc-diagnostics/endpoint-test.d.ts +2 -1
  67. package/dist/composites/webrtc-diagnostics/nat-mapping.d.ts +2 -1
  68. package/dist/composites/webrtc-diagnostics/session-run.d.ts +2 -1
  69. package/dist/composites/webrtc-diagnostics/session-verdict.d.ts +2 -1
  70. package/dist/contexts/ui-text.d.ts +49 -0
  71. package/dist/generated/system-hooks.d.ts +6 -0
  72. package/dist/i18n/addon-i18n.d.ts +46 -0
  73. package/dist/i18n/index.d.ts +2 -0
  74. package/dist/i18n/use-addon-t.d.ts +57 -0
  75. package/dist/index.cjs +3290 -2356
  76. package/dist/index.d.ts +2 -0
  77. package/dist/index.js +3277 -2356
  78. package/dist/lib/format-last-seen.d.ts +15 -1
  79. package/dist/lib/phase-config.d.ts +9 -3
  80. package/dist/lib/pipeline-mirror.d.ts +2 -1
  81. package/package.json +1 -1
  82. package/dist/composites/data-table/data-table-header.d.ts +0 -11
  83. package/dist/composites/data-table/data-table-pagination.d.ts +0 -10
  84. package/dist/composites/data-table/data-table-row.d.ts +0 -11
  85. package/dist/composites/data-table/data-table.d.ts +0 -3
  86. package/dist/composites/data-table/index.d.ts +0 -2
  87. package/dist/composites/data-table/types.d.ts +0 -34
  88. package/dist/composites/data-table-layout.d.ts +0 -31
@@ -1,9 +1,47 @@
1
1
  import { t as Settings2 } from "./settings-2-jT1iBqHD.js";
2
- import { SCENE_CAPTURE_VARIANTS, TEXT_HINT, badgeFor, coverageLine, dt as RotateCcw, hubPath, isAbsentProvider, k as Trash2, roiTooLarge, statusLine, un as ImageOff, useDeviceProxy, useDeviceSnapshotImage, useDeviceStateSlice, useSystem, variantLabel, vr as Camera } from "./index.js";
3
- import { MaskShapeCanvas } from "./MaskShapeCanvas-DI4BY7W2.js";
2
+ import { SCENE_CAPTURE_VARIANTS, TEXT_HINT, UiTrans, badgeFor, coverageLine, dt as RotateCcw, hubPath, isAbsentProvider, k as Trash2, roiTooLarge, statusLine, un as ImageOff, useDeviceProxy, useDeviceSnapshotImage, useDeviceStateSlice, useSystem, useUiText, variantLabel, vr as Camera } from "./index.js";
3
+ import { MaskShapeCanvas } from "./MaskShapeCanvas-C9cJlxvG.js";
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/composites/cap-settings/SceneReferenceGallery.tsx
7
+ /**
8
+ * SceneReferenceGallery — the photographs a scene is actually judging against,
9
+ * grouped by lighting variant, each individually deletable.
10
+ *
11
+ * ## Why it has to exist
12
+ *
13
+ * A scene's whole answer is "does this region still look like the photograph".
14
+ * Until now the operator could see a COUNT of references and never the
15
+ * references themselves, which makes the two failure modes indistinguishable
16
+ * from the outside: a blurry capture taken while a van was in the way poisons a
17
+ * condition forever, and from a cosine score there is no way to tell which one
18
+ * it was. `deleteReference` shipped with the cap for exactly this and had no
19
+ * caller.
20
+ *
21
+ * ## The image
22
+ *
23
+ * A bare `<img>` against the `event-media` data plane
24
+ * (`/addon/pipeline-analytics/event-media/m/<mediaId>` — D482), which is
25
+ * `access: 'authenticated'` — that accepts the `camstack_session` cookie, and a
26
+ * cookie is the only credential an `<img>` can send. Same mechanism, and the
27
+ * same reasoning, as `use-device-snapshot`; no bearer-token blob fetch is
28
+ * needed and no second copy of the auth plumbing is introduced here.
29
+ *
30
+ * `SceneReference.thumbnailMediaId` finally IS a media row id (D482). It was
31
+ * named `…MediaId` while holding a composite MediaStore key
32
+ * (`scene:<ownerId>:thumbnailSmall:<ts>`), which this component read
33
+ * POSITIONALLY for its owner id — a name that described the right design
34
+ * beside a value that did not, which is the shape of leftover this repo has
35
+ * been bitten by three times. The tile now addresses the row.
36
+ *
37
+ * **A missing blob is normal, not exceptional.** Reference thumbnails written
38
+ * before the `ownerKind: 'scene'` fix carried an `ownerId` no event row backs,
39
+ * which made them eligible for the orphan sweep — so on an existing install
40
+ * some of these ids resolve to nothing. That must degrade to a dated
41
+ * placeholder: the reference is still real, still being scored against, and
42
+ * still deletable. A gallery that blanks or throws over a missing picture would
43
+ * take the delete button away exactly when it is needed.
44
+ */
7
45
  /** The hub-proxied prefix for the pipeline-analytics event-media data plane. */
8
46
  var SCENE_MEDIA_BASE = hubPath("/addon/pipeline-analytics/event-media");
9
47
  /**
@@ -51,12 +89,13 @@ function totalReferences(monitor) {
51
89
  }
52
90
  var BTN_TINY = "rounded border border-danger/50 bg-danger/10 px-1 py-0.5 text-[10px] font-medium text-danger hover:bg-danger/20 disabled:opacity-40 transition-colors";
53
91
  function SceneReferenceGallery({ monitor, disabled, onDelete }) {
92
+ const t = useUiText();
54
93
  const groups = groupReferences(monitor);
55
94
  const total = totalReferences(monitor);
56
95
  const [confirming, setConfirming] = useState(null);
57
96
  if (groups.length === 0) return /* @__PURE__ */ jsx("p", {
58
97
  className: `${TEXT_HINT} leading-relaxed`,
59
- children: "No photographs captured yet. Press “Capture daylight” to take the one this scene will judge everything against."
98
+ children: t("uiLibrary.sceneReferenceGallery.empty", "No photographs captured yet. Press “Capture daylight” to take the one this scene will judge everything against.")
60
99
  });
61
100
  return /* @__PURE__ */ jsx("div", {
62
101
  className: "flex flex-col gap-2",
@@ -65,7 +104,7 @@ function SceneReferenceGallery({ monitor, disabled, onDelete }) {
65
104
  children: [/* @__PURE__ */ jsxs("p", {
66
105
  className: "text-[10px] font-semibold uppercase tracking-wide text-foreground-subtle",
67
106
  children: [
68
- variantLabel(group.condition),
107
+ variantLabel(group.condition, t),
69
108
  " · ",
70
109
  group.tiles.length
71
110
  ]
@@ -87,7 +126,10 @@ function SceneReferenceGallery({ monitor, disabled, onDelete }) {
87
126
  type: "button",
88
127
  className: BTN_TINY,
89
128
  disabled,
90
- "aria-label": `Delete the ${variantLabel(group.condition).toLowerCase()} photograph from ${formatCapturedAt(tile.reference.capturedAt)}`,
129
+ "aria-label": t("uiLibrary.sceneReferenceGallery.deleteLabel", "Delete the {{variant}} photograph from {{date}}", {
130
+ variant: variantLabel(group.condition, t).toLowerCase(),
131
+ date: formatCapturedAt(tile.reference.capturedAt)
132
+ }),
91
133
  onClick: () => {
92
134
  if (isLastOverall && !pending) {
93
135
  setConfirming(key);
@@ -99,11 +141,11 @@ function SceneReferenceGallery({ monitor, disabled, onDelete }) {
99
141
  index: tile.index
100
142
  });
101
143
  },
102
- children: [/* @__PURE__ */ jsx(Trash2, { className: "mr-1 inline h-3 w-3" }), pending ? "Confirm" : "Delete"]
144
+ children: [/* @__PURE__ */ jsx(Trash2, { className: "mr-1 inline h-3 w-3" }), pending ? t("uiLibrary.sceneReferenceGallery.confirm", "Confirm") : t("uiLibrary.sceneReferenceGallery.delete", "Delete")]
103
145
  }),
104
146
  pending && /* @__PURE__ */ jsx("p", {
105
147
  className: "text-[10px] leading-snug text-warning",
106
- children: "This is the only photograph this scene has. Delete it and the scene no longer knows what normal looks like — it stops judging until you capture again."
148
+ children: t("uiLibrary.sceneReferenceGallery.lastWarning", "This is the only photograph this scene has. Delete it and the scene no longer knows what normal looks like — it stops judging until you capture again.")
107
149
  })
108
150
  ]
109
151
  }, key);
@@ -113,14 +155,15 @@ function SceneReferenceGallery({ monitor, disabled, onDelete }) {
113
155
  });
114
156
  }
115
157
  function ReferenceThumb({ reference }) {
158
+ const t = useUiText();
116
159
  const [failed, setFailed] = useState(false);
117
160
  const mediaId = reference.thumbnailMediaId;
118
161
  if (mediaId === void 0 || failed) return /* @__PURE__ */ jsxs("div", {
119
162
  className: "flex h-16 w-full flex-col items-center justify-center gap-0.5 rounded bg-surface text-foreground-subtle",
120
- title: "The picture for this reference is no longer stored. The reference itself is intact and still being scored against.",
163
+ title: t("uiLibrary.sceneReferenceGallery.missingPicture", "The picture for this reference is no longer stored. The reference itself is intact and still being scored against."),
121
164
  children: [/* @__PURE__ */ jsx(ImageOff, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
122
165
  className: "text-[9px]",
123
- children: "no picture"
166
+ children: t("uiLibrary.sceneReferenceGallery.noPicture", "no picture")
124
167
  })]
125
168
  });
126
169
  return /* @__PURE__ */ jsx("img", {
@@ -147,15 +190,19 @@ var GROUP_TITLE = "text-[10px] font-semibold uppercase tracking-wide text-foregr
147
190
  /** The two engine-visible interval floors, in the units the operator thinks in. */
148
191
  var MIN_INTERVAL_SEC = 5;
149
192
  var MAX_INTERVAL_SEC = 3600;
150
- var TRIGGER_HINT = {
151
- periodic: "Only on the clock. Predictable, and it notices a change even on a camera nothing ever moves in front of.",
152
- "on-motion": "Only after the camera has been still again for the quiet window below. Cheapest, but a change nobody walks past is never seen.",
153
- both: "Both — the clock is the safety net that lets the motion path miss. This is the right answer for almost every scene."
154
- };
155
- var UNCOVERED_HINT = {
156
- skip: "Sit that light out. No verdict, no alarm, no flag touched — it picks itself back up when a light it knows returns. This is why the night capture is optional.",
157
- "judge-anyway": "Compare against the references you do have. Only correct when the two lights genuinely look alike (a floodlit driveway, an indoor lamp) — against a real infrared frame it will report a change that did not happen."
158
- };
193
+ function triggerHint(mode, t) {
194
+ switch (mode) {
195
+ case "periodic": return t("uiLibrary.sceneSettingsForm.triggerHintPeriodic", "Only on the clock. Predictable, and it notices a change even on a camera nothing ever moves in front of.");
196
+ case "on-motion": return t("uiLibrary.sceneSettingsForm.triggerHintOnMotion", "Only after the camera has been still again for the quiet window below. Cheapest, but a change nobody walks past is never seen.");
197
+ case "both": return t("uiLibrary.sceneSettingsForm.triggerHintBoth", "Both — the clock is the safety net that lets the motion path miss. This is the right answer for almost every scene.");
198
+ }
199
+ }
200
+ function uncoveredHint(policy, t) {
201
+ switch (policy) {
202
+ case "skip": return t("uiLibrary.sceneSettingsForm.uncoveredHintSkip", "Sit that light out. No verdict, no alarm, no flag touched — it picks itself back up when a light it knows returns. This is why the night capture is optional.");
203
+ case "judge-anyway": return t("uiLibrary.sceneSettingsForm.uncoveredHintJudge", "Compare against the references you do have. Only correct when the two lights genuinely look alike (a floodlit driveway, an indoor lamp) — against a real infrared frame it will report a change that did not happen.");
204
+ }
205
+ }
159
206
  function draftOf(monitor) {
160
207
  return {
161
208
  label: monitor.label,
@@ -168,6 +215,7 @@ function draftOf(monitor) {
168
215
  };
169
216
  }
170
217
  function SceneSettingsForm({ monitor, disabled, onPatch }) {
218
+ const t = useUiText();
171
219
  const [draft, setDraft] = useState(() => draftOf(monitor));
172
220
  const [editing, setEditing] = useState(false);
173
221
  useEffect(() => {
@@ -196,13 +244,13 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
196
244
  children: [
197
245
  /* @__PURE__ */ jsx("p", {
198
246
  className: GROUP_TITLE,
199
- children: "What it watches"
247
+ children: t("uiLibrary.sceneSettingsForm.whatItWatches", "What it watches")
200
248
  }),
201
249
  /* @__PURE__ */ jsxs("label", {
202
250
  className: "flex flex-col gap-1",
203
251
  children: [/* @__PURE__ */ jsx("span", {
204
252
  className: LABEL,
205
- children: "Name"
253
+ children: t("uiLibrary.sceneSettingsForm.name", "Name")
206
254
  }), /* @__PURE__ */ jsx("input", {
207
255
  type: "text",
208
256
  value: draft.label,
@@ -229,18 +277,18 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
229
277
  onChange: (e) => commit({ enabled: e.target.checked })
230
278
  }), /* @__PURE__ */ jsx("span", {
231
279
  className: LABEL,
232
- children: "Watching"
280
+ children: t("uiLibrary.sceneSettingsForm.watching", "Watching")
233
281
  })]
234
282
  }),
235
283
  /* @__PURE__ */ jsx("p", {
236
284
  className: TEXT_HINT,
237
- children: "Turned off, the scene keeps its region, its references and its flag — it simply stops checking. Nothing is lost by pausing one for a while."
285
+ children: t("uiLibrary.sceneSettingsForm.watchingHint", "Turned off, the scene keeps its region, its references and its flag — it simply stops checking. Nothing is lost by pausing one for a while.")
238
286
  }),
239
287
  /* @__PURE__ */ jsxs("label", {
240
288
  className: "flex flex-col gap-1",
241
289
  children: [/* @__PURE__ */ jsx("span", {
242
290
  className: LABEL,
243
- children: "When to look"
291
+ children: t("uiLibrary.sceneSettingsForm.whenToLook", "When to look")
244
292
  }), /* @__PURE__ */ jsxs("select", {
245
293
  value: monitor.triggerMode,
246
294
  disabled,
@@ -252,28 +300,28 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
252
300
  children: [
253
301
  /* @__PURE__ */ jsx("option", {
254
302
  value: "both",
255
- children: "On the clock and after motion settles"
303
+ children: t("uiLibrary.sceneSettingsForm.triggerBoth", "On the clock and after motion settles")
256
304
  }),
257
305
  /* @__PURE__ */ jsx("option", {
258
306
  value: "periodic",
259
- children: "On the clock only"
307
+ children: t("uiLibrary.sceneSettingsForm.triggerPeriodic", "On the clock only")
260
308
  }),
261
309
  /* @__PURE__ */ jsx("option", {
262
310
  value: "on-motion",
263
- children: "After motion settles only"
311
+ children: t("uiLibrary.sceneSettingsForm.triggerOnMotion", "After motion settles only")
264
312
  })
265
313
  ]
266
314
  })]
267
315
  }),
268
316
  /* @__PURE__ */ jsx("p", {
269
317
  className: TEXT_HINT,
270
- children: TRIGGER_HINT[monitor.triggerMode]
318
+ children: triggerHint(monitor.triggerMode, t)
271
319
  }),
272
320
  /* @__PURE__ */ jsxs("label", {
273
321
  className: "flex flex-col gap-1",
274
322
  children: [/* @__PURE__ */ jsx("span", {
275
323
  className: LABEL,
276
- children: "Check every (seconds)"
324
+ children: t("uiLibrary.sceneSettingsForm.checkEvery", "Check every (seconds)")
277
325
  }), /* @__PURE__ */ jsx("input", {
278
326
  type: "number",
279
327
  min: MIN_INTERVAL_SEC,
@@ -295,7 +343,7 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
295
343
  }),
296
344
  /* @__PURE__ */ jsx("p", {
297
345
  className: TEXT_HINT,
298
- children: "60 s suits “is the bin still out”. Every check costs one photograph and one encode, so going below about 30 s buys latency you almost never need and multiplies the cost by the number of scenes you own."
346
+ children: t("uiLibrary.sceneSettingsForm.intervalHint", "60 s suits “is the bin still out”. Every check costs one photograph and one encode, so going below about 30 s buys latency you almost never need and multiplies the cost by the number of scenes you own.")
299
347
  })
300
348
  ]
301
349
  }),
@@ -304,16 +352,18 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
304
352
  children: [
305
353
  /* @__PURE__ */ jsx("p", {
306
354
  className: GROUP_TITLE,
307
- children: "How sure it has to be"
355
+ children: t("uiLibrary.sceneSettingsForm.howSure", "How sure it has to be")
308
356
  }),
309
357
  monitor.check.mode === "similarity" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("label", {
310
358
  className: "flex flex-col gap-1",
311
- children: [/* @__PURE__ */ jsxs("span", {
359
+ children: [/* @__PURE__ */ jsx("span", {
312
360
  className: LABEL,
313
- children: ["Match threshold — ", /* @__PURE__ */ jsx("span", {
314
- className: "tabular-nums",
315
- children: draft.threshold.toFixed(2)
316
- })]
361
+ children: /* @__PURE__ */ jsx(UiTrans, {
362
+ uiKey: "uiLibrary.sceneSettingsForm.matchThreshold",
363
+ fallback: "Match threshold — <0>{{value}}</0>",
364
+ components: [/* @__PURE__ */ jsx("span", { className: "tabular-nums" }, "value")],
365
+ values: { value: draft.threshold.toFixed(2) }
366
+ })
317
367
  }), /* @__PURE__ */ jsx("input", {
318
368
  type: "range",
319
369
  min: .3,
@@ -337,13 +387,13 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
337
387
  })]
338
388
  }), /* @__PURE__ */ jsx("p", {
339
389
  className: "text-[10px] text-foreground-subtle",
340
- children: "How closely the region still has to resemble the photograph to count as unchanged. Higher means fussier: at 0.95 a passing cloud can read as a change; at 0.60 the bin can be taken and the scene will shrug. 0.80 is a sensible start — if it cries wolf, lower it; if it misses the thing you care about, raise it."
390
+ children: t("uiLibrary.sceneSettingsForm.thresholdHint", "How closely the region still has to resemble the photograph to count as unchanged. Higher means fussier: at 0.95 a passing cloud can read as a change; at 0.60 the bin can be taken and the scene will shrug. 0.80 is a sensible start — if it cries wolf, lower it; if it misses the thing you care about, raise it.")
341
391
  })] }),
342
392
  /* @__PURE__ */ jsxs("label", {
343
393
  className: "flex flex-col gap-1",
344
394
  children: [/* @__PURE__ */ jsx("span", {
345
395
  className: LABEL,
346
- children: "Agreeing checks before it changes its mind"
396
+ children: t("uiLibrary.sceneSettingsForm.agreeingChecks", "Agreeing checks before it changes its mind")
347
397
  }), /* @__PURE__ */ jsx("input", {
348
398
  type: "number",
349
399
  min: 1,
@@ -365,13 +415,13 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
365
415
  }),
366
416
  /* @__PURE__ */ jsx("p", {
367
417
  className: TEXT_HINT,
368
- children: "One bad photograph must never be able to flip a scene. 3 agreeing checks at 60 s is about four minutes of latency and is what makes the answer trustworthy — leaving a state costs exactly the same as entering one."
418
+ children: t("uiLibrary.sceneSettingsForm.agreeingHint", "One bad photograph must never be able to flip a scene. 3 agreeing checks at 60 s is about four minutes of latency and is what makes the answer trustworthy — leaving a state costs exactly the same as entering one.")
369
419
  }),
370
420
  /* @__PURE__ */ jsxs("label", {
371
421
  className: "flex flex-col gap-1",
372
422
  children: [/* @__PURE__ */ jsx("span", {
373
423
  className: LABEL,
374
- children: "Quiet for (seconds) before a check counts"
424
+ children: t("uiLibrary.sceneSettingsForm.quietFor", "Quiet for (seconds) before a check counts")
375
425
  }), /* @__PURE__ */ jsx("input", {
376
426
  type: "number",
377
427
  min: 0,
@@ -393,13 +443,13 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
393
443
  }),
394
444
  /* @__PURE__ */ jsx("p", {
395
445
  className: TEXT_HINT,
396
- children: "A check taken while something is moving is a photograph of the van parked in front of the bin, so it is discarded rather than merely delayed. 0 disables the gate."
446
+ children: t("uiLibrary.sceneSettingsForm.quietHint", "A check taken while something is moving is a photograph of the van parked in front of the bin, so it is discarded rather than merely delayed. 0 disables the gate.")
397
447
  }),
398
448
  /* @__PURE__ */ jsxs("label", {
399
449
  className: "flex flex-col gap-1",
400
450
  children: [/* @__PURE__ */ jsx("span", {
401
451
  className: LABEL,
402
- children: "Minimum gap between counted checks (seconds)"
452
+ children: t("uiLibrary.sceneSettingsForm.minGap", "Minimum gap between counted checks (seconds)")
403
453
  }), /* @__PURE__ */ jsx("input", {
404
454
  type: "number",
405
455
  min: 0,
@@ -421,7 +471,7 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
421
471
  }),
422
472
  /* @__PURE__ */ jsx("p", {
423
473
  className: TEXT_HINT,
424
- children: "Spreads the agreeing checks over real time instead of letting three adjacent polls inside one long occlusion be mistaken for three independent opinions."
474
+ children: t("uiLibrary.sceneSettingsForm.minGapHint", "Spreads the agreeing checks over real time instead of letting three adjacent polls inside one long occlusion be mistaken for three independent opinions.")
425
475
  })
426
476
  ]
427
477
  }),
@@ -430,13 +480,13 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
430
480
  children: [
431
481
  /* @__PURE__ */ jsx("p", {
432
482
  className: GROUP_TITLE,
433
- children: "Lighting"
483
+ children: t("uiLibrary.sceneSettingsForm.lighting", "Lighting")
434
484
  }),
435
485
  /* @__PURE__ */ jsxs("label", {
436
486
  className: "flex flex-col gap-1",
437
487
  children: [/* @__PURE__ */ jsx("span", {
438
488
  className: LABEL,
439
- children: "When a light has no reference of its own"
489
+ children: t("uiLibrary.sceneSettingsForm.noReference", "When a light has no reference of its own")
440
490
  }), /* @__PURE__ */ jsxs("select", {
441
491
  value: monitor.onUncoveredCondition,
442
492
  disabled,
@@ -447,16 +497,16 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
447
497
  },
448
498
  children: [/* @__PURE__ */ jsx("option", {
449
499
  value: "skip",
450
- children: "Skip the check (recommended)"
500
+ children: t("uiLibrary.sceneSettingsForm.uncoveredSkip", "Skip the check (recommended)")
451
501
  }), /* @__PURE__ */ jsx("option", {
452
502
  value: "judge-anyway",
453
- children: "Judge against the other light anyway"
503
+ children: t("uiLibrary.sceneSettingsForm.uncoveredJudge", "Judge against the other light anyway")
454
504
  })]
455
505
  })]
456
506
  }),
457
507
  /* @__PURE__ */ jsx("p", {
458
508
  className: TEXT_HINT,
459
- children: UNCOVERED_HINT[monitor.onUncoveredCondition]
509
+ children: uncoveredHint(monitor.onUncoveredCondition, t)
460
510
  })
461
511
  ]
462
512
  }),
@@ -465,13 +515,13 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
465
515
  children: [
466
516
  /* @__PURE__ */ jsx("p", {
467
517
  className: GROUP_TITLE,
468
- children: "What you get told"
518
+ children: t("uiLibrary.sceneSettingsForm.whatYouGetTold", "What you get told")
469
519
  }),
470
520
  /* @__PURE__ */ jsxs("label", {
471
521
  className: "flex flex-col gap-1",
472
522
  children: [/* @__PURE__ */ jsx("span", {
473
523
  className: LABEL,
474
- children: "Notifications follow"
524
+ children: t("uiLibrary.sceneSettingsForm.notificationsFollow", "Notifications follow")
475
525
  }), /* @__PURE__ */ jsxs("select", {
476
526
  value: monitor.emit,
477
527
  disabled,
@@ -482,16 +532,16 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
482
532
  },
483
533
  children: [/* @__PURE__ */ jsx("option", {
484
534
  value: "latched",
485
- children: "The flag — told once, still visible at breakfast"
535
+ children: t("uiLibrary.sceneSettingsForm.emitLatched", "The flag — told once, still visible at breakfast")
486
536
  }), /* @__PURE__ */ jsx("option", {
487
537
  value: "live",
488
- children: "The live state — follows the region as it changes"
538
+ children: t("uiLibrary.sceneSettingsForm.emitLive", "The live state — follows the region as it changes")
489
539
  })]
490
540
  })]
491
541
  }),
492
542
  /* @__PURE__ */ jsx("p", {
493
543
  className: TEXT_HINT,
494
- children: "“Is the bin gone” is a flag: you want to be told once and still see it later. “Is the gate open” is a live state."
544
+ children: t("uiLibrary.sceneSettingsForm.emitHint", "“Is the bin gone” is a flag: you want to be told once and still see it later. “Is the gate open” is a live state.")
495
545
  }),
496
546
  /* @__PURE__ */ jsxs("label", {
497
547
  className: "flex items-center gap-2",
@@ -502,12 +552,12 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
502
552
  onChange: (e) => commit({ autoRestore: e.target.checked })
503
553
  }), /* @__PURE__ */ jsx("span", {
504
554
  className: LABEL,
505
- children: "Clear the flag by itself when things look normal again"
555
+ children: t("uiLibrary.sceneSettingsForm.autoRestore", "Clear the flag by itself when things look normal again")
506
556
  })]
507
557
  }),
508
558
  /* @__PURE__ */ jsx("p", {
509
559
  className: TEXT_HINT,
510
- children: "Off by default, so an alarm never silently clears itself. The “back to normal” moment is recorded either way, so an automation can still react to it."
560
+ children: t("uiLibrary.sceneSettingsForm.autoRestoreHint", "Off by default, so an alarm never silently clears itself. The “back to normal” moment is recorded either way, so an automation can still react to it.")
511
561
  })
512
562
  ]
513
563
  }),
@@ -516,7 +566,7 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
516
566
  children: [
517
567
  /* @__PURE__ */ jsx("p", {
518
568
  className: GROUP_TITLE,
519
- children: "AI double-check"
569
+ children: t("uiLibrary.sceneSettingsForm.aiDoubleCheck", "AI double-check")
520
570
  }),
521
571
  /* @__PURE__ */ jsxs("label", {
522
572
  className: "flex items-center gap-2",
@@ -530,18 +580,18 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
530
580
  } } : { confirm: null })
531
581
  }), /* @__PURE__ */ jsx("span", {
532
582
  className: LABEL,
533
- children: "Ask a vision model before changing the answer"
583
+ children: t("uiLibrary.sceneSettingsForm.askVision", "Ask a vision model before changing the answer")
534
584
  })]
535
585
  }),
536
586
  /* @__PURE__ */ jsxs("label", {
537
587
  className: "flex flex-col gap-1",
538
588
  children: [/* @__PURE__ */ jsx("span", {
539
589
  className: LABEL,
540
- children: "The question to ask"
590
+ children: t("uiLibrary.sceneSettingsForm.question", "The question to ask")
541
591
  }), /* @__PURE__ */ jsx("input", {
542
592
  type: "text",
543
593
  value: draft.confirmPrompt,
544
- placeholder: "e.g. Is the wheelie bin still there?",
594
+ placeholder: t("uiLibrary.sceneSettingsForm.questionPlaceholder", "e.g. Is the wheelie bin still there?"),
545
595
  disabled,
546
596
  className: FIELD,
547
597
  onFocus: () => setEditing(true),
@@ -559,13 +609,13 @@ function SceneSettingsForm({ monitor, disabled, onPatch }) {
559
609
  }
560
610
  })]
561
611
  }),
562
- /* @__PURE__ */ jsxs("p", {
612
+ /* @__PURE__ */ jsx("p", {
563
613
  className: TEXT_HINT,
564
- children: [
565
- "The model is asked ",
566
- /* @__PURE__ */ jsx("strong", { children: "only" }),
567
- " when the region has already changed enough to flip the answer — roughly twice a week for a bin, not once a minute. It reasons about the object rather than the pixels, so rain, shadows and a repositioned bin stop counting as the thing being taken. If it cannot be reached, the change is held rather than announced."
568
- ]
614
+ children: /* @__PURE__ */ jsx(UiTrans, {
615
+ uiKey: "uiLibrary.sceneSettingsForm.confirmHint",
616
+ fallback: "The model is asked <0>only</0> when the region has already changed enough to flip the answer — roughly twice a week for a bin, not once a minute. It reasons about the object rather than the pixels, so rain, shadows and a repositioned bin stop counting as the thing being taken. If it cannot be reached, the change is held rather than announced.",
617
+ components: [/* @__PURE__ */ jsx("strong", {}, "only")]
618
+ })
569
619
  })
570
620
  ]
571
621
  })
@@ -588,6 +638,55 @@ function defaultConfirm(prompt) {
588
638
  }
589
639
  //#endregion
590
640
  //#region src/composites/cap-settings/SceneMonitorEditor.tsx
641
+ /**
642
+ * SceneMonitorEditor — the per-camera half of the Scenes surface.
643
+ *
644
+ * **Imported directly, not resolved as a widget.** It briefly lived behind the
645
+ * host widget `host/scene-monitor-editor`, mounted as a "Scenes" tab on one
646
+ * camera's detail page. That put a standing question about the property ("is
647
+ * the bin still out") one camera deep, where the operator's actual question —
648
+ * "which of my scenes have tripped" — could not be asked at all. The overview
649
+ * is now the top-level `/scenes` page; this component is what that page opens
650
+ * once a camera is chosen, in a dialog. The widget id and its `deviceConfig`
651
+ * declaration are both gone, in the same commit, because
652
+ * `scripts/check-host-widget-resolves.ts` checks both directions.
653
+ *
654
+ * It renders BARE — no `WidgetPanel` — because its only caller supplies a
655
+ * dialog header, and two stacked titles read as a layout bug.
656
+ *
657
+ * ## The flow, in the order the operator meets it
658
+ *
659
+ * 1. **Draw a region on the still frame.** A rect over a SNAPSHOT, not the live
660
+ * player: a scene is defined against a photograph, and drawing on a moving
661
+ * picture makes it impossible to see whether the box actually contains the
662
+ * bin. The editor starts in draw mode and warns above ~40 % of the frame,
663
+ * because a full-frame scene is dominated by weather, sky and parked cars —
664
+ * it will not work, and it will look broken rather than misconfigured.
665
+ * 2. **Capture the initial state, per lighting variant.** Two buttons, Daylight
666
+ * and Night (IR), each forcing a fresh snapshot. The variant is DETECTED
667
+ * from the frame and shown rather than asked; the buttons exist so the
668
+ * operator can say "capture this as the night reference" while standing
669
+ * outside at 22:00 — which is exactly why **Night (IR) is labelled
670
+ * optional**. Almost nobody does that, and a scene captured only in daylight
671
+ * is a configuration, not a defect: under the default
672
+ * `onUncoveredCondition: 'skip'` the infrared hours are simply sat out.
673
+ * 3. **Read the coverage line.** "Daylight ✓ · Night (IR) ✗ — optional; checks
674
+ * in night (ir) are skipped". This feature's failure mode is silence that
675
+ * reads as "nothing has happened", and that one line is the difference
676
+ * between a five-minute fix and a session of guessing — but it must say what
677
+ * HAPPENS, not complain. It used to end "reports 'can't tell'", which made a
678
+ * correctly configured scene look broken every night.
679
+ * 4. **Reset.** Clears the flag AND re-captures the baseline for the current
680
+ * light, because the bin never goes back in exactly the same spot.
681
+ * 5. **Settings.** Every knob the model has actually carried since the provider
682
+ * shipped — how often it looks, how closely it has to match, how many
683
+ * agreeing checks a change costs, what a missing light means, which boolean
684
+ * drives notifications, and whether a vision model adjudicates a flip. None
685
+ * of it was reachable before; see `SceneSettingsForm`.
686
+ *
687
+ * Live state arrives through the cap's `status.kind: 'push'` runtime slice, so
688
+ * the card updates itself without polling.
689
+ */
591
690
  var BTN_NEUTRAL = "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors";
592
691
  var BTN_PRIMARY = "rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors";
593
692
  var BTN_DANGER = "rounded-md border border-danger/50 bg-danger/10 px-2 py-1 text-[11px] font-medium text-danger hover:bg-danger/20 disabled:opacity-40 transition-colors";
@@ -601,6 +700,7 @@ var TONE_CLASS = {
601
700
  var DEFAULT_THRESHOLD = .8;
602
701
  var DEFAULT_HYSTERESIS = 3;
603
702
  function SceneMonitorEditor({ deviceId }) {
703
+ const t = useUiText();
604
704
  const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
605
705
  const pushed = useDeviceStateSlice(dev?.state.sceneMonitor);
606
706
  const snapshot = useDeviceSnapshotImage(deviceId, { width: 640 });
@@ -664,9 +764,13 @@ function SceneMonitorEditor({ deviceId }) {
664
764
  return draftRoi !== null ? [...existing, {
665
765
  id: "__draft__",
666
766
  shape: draftRoi,
667
- label: "New scene"
767
+ label: t("uiLibrary.sceneMonitor.newSceneLabel", "New scene")
668
768
  }] : existing;
669
- }, [monitors, draftRoi]);
769
+ }, [
770
+ monitors,
771
+ draftRoi,
772
+ t
773
+ ]);
670
774
  const onShapeChange = useCallback((id, shape) => {
671
775
  if (shape.kind !== "rect") return;
672
776
  if (id === "__draft__") {
@@ -709,11 +813,11 @@ function SceneMonitorEditor({ deviceId }) {
709
813
  className: "flex flex-col gap-3",
710
814
  children: unsupported ? /* @__PURE__ */ jsx("p", {
711
815
  className: `${TEXT_HINT} leading-relaxed`,
712
- children: "Scene monitoring is not available for this camera."
816
+ children: t("uiLibrary.sceneMonitor.unsupported", "Scene monitoring is not available for this camera.")
713
817
  }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
714
818
  /* @__PURE__ */ jsx("p", {
715
819
  className: `${TEXT_HINT} leading-relaxed`,
716
- children: "A scene watches one region of this camera and tells you when it stops looking like the picture you captured. Draw a box around the thing you care about — a wheelie bin, a gate, a parking space — then capture what “normal” looks like. Daylight is enough to start: the night reference is optional, and without it the scene simply sits the infrared hours out instead of reporting a problem."
820
+ children: t("uiLibrary.sceneMonitor.intro", "A scene watches one region of this camera and tells you when it stops looking like the picture you captured. Draw a box around the thing you care about — a wheelie bin, a gate, a parking space — then capture what “normal” looks like. Daylight is enough to start: the night reference is optional, and without it the scene simply sits the infrared hours out instead of reporting a problem.")
717
821
  }),
718
822
  /* @__PURE__ */ jsx("div", {
719
823
  className: "relative overflow-hidden rounded-md border border-border",
@@ -744,13 +848,13 @@ function SceneMonitorEditor({ deviceId }) {
744
848
  setDraftRoi(null);
745
849
  },
746
850
  disabled: busy !== null,
747
- children: drafting ? "Cancel" : "+ New scene"
851
+ children: drafting ? t("uiLibrary.sceneMonitor.cancel", "Cancel") : t("uiLibrary.sceneMonitor.newScene", "+ New scene")
748
852
  }), /* @__PURE__ */ jsx("button", {
749
853
  type: "button",
750
854
  className: BTN_NEUTRAL,
751
855
  onClick: snapshot.refresh,
752
856
  disabled: busy !== null,
753
- children: "Refresh frame"
857
+ children: t("uiLibrary.sceneMonitor.refresh", "Refresh frame")
754
858
  })]
755
859
  }),
756
860
  drafting && /* @__PURE__ */ jsxs("div", {
@@ -758,11 +862,11 @@ function SceneMonitorEditor({ deviceId }) {
758
862
  children: [
759
863
  /* @__PURE__ */ jsx("p", {
760
864
  className: "text-[10px] text-foreground-subtle",
761
- children: draftRoi === null ? "Click on the frame to drop a box, then drag its corner to fit it around the thing you want watched." : "Name it, then create. You capture what “normal” looks like next."
865
+ children: draftRoi === null ? t("uiLibrary.sceneMonitor.draftHint", "Click on the frame to drop a box, then drag its corner to fit it around the thing you want watched.") : t("uiLibrary.sceneMonitor.nameHint", "Name it, then create. You capture what “normal” looks like next.")
762
866
  }),
763
867
  draftRoi !== null && roiTooLarge(draftRoi) && /* @__PURE__ */ jsx("p", {
764
868
  className: "text-[11px] leading-relaxed text-warning",
765
- children: "That box covers most of the frame. A scene that big is dominated by the weather, the sky and passing cars — it will quietly stop noticing the thing you actually care about. Draw it tighter."
869
+ children: t("uiLibrary.sceneMonitor.tooLarge", "That box covers most of the frame. A scene that big is dominated by the weather, the sky and passing cars — it will quietly stop noticing the thing you actually care about. Draw it tighter.")
766
870
  }),
767
871
  /* @__PURE__ */ jsxs("div", {
768
872
  className: "flex items-center gap-2",
@@ -770,14 +874,14 @@ function SceneMonitorEditor({ deviceId }) {
770
874
  type: "text",
771
875
  value: draftLabel,
772
876
  onChange: (e) => setDraftLabel(e.target.value),
773
- placeholder: "e.g. Immondizia prelevata",
877
+ placeholder: t("uiLibrary.sceneMonitor.placeholder", "e.g. Immondizia prelevata"),
774
878
  className: "flex-1 rounded-md border border-border bg-surface px-2 py-1 text-[12px] text-foreground"
775
879
  }), /* @__PURE__ */ jsx("button", {
776
880
  type: "button",
777
881
  className: BTN_PRIMARY,
778
882
  onClick: () => void createScene(),
779
883
  disabled: draftRoi === null || draftLabel.trim().length === 0 || busy !== null,
780
- children: "Create"
884
+ children: t("uiLibrary.sceneMonitor.create", "Create")
781
885
  })]
782
886
  })
783
887
  ]
@@ -788,11 +892,11 @@ function SceneMonitorEditor({ deviceId }) {
788
892
  }),
789
893
  monitors.length === 0 ? /* @__PURE__ */ jsx("p", {
790
894
  className: `${TEXT_HINT} leading-relaxed`,
791
- children: "No scenes on this camera yet."
895
+ children: t("uiLibrary.sceneMonitor.empty", "No scenes on this camera yet.")
792
896
  }) : /* @__PURE__ */ jsx("ul", {
793
897
  className: "flex flex-col gap-2",
794
898
  children: monitors.map((m) => {
795
- const badge = badgeFor(m);
899
+ const badge = badgeFor(m, t);
796
900
  return /* @__PURE__ */ jsxs("li", {
797
901
  className: `flex flex-col gap-1.5 rounded-md border p-2 ${selectedId === m.id ? "border-primary/50" : "border-border"}`,
798
902
  children: [
@@ -810,11 +914,11 @@ function SceneMonitorEditor({ deviceId }) {
810
914
  }),
811
915
  /* @__PURE__ */ jsx("p", {
812
916
  className: `${TEXT_HINT} leading-relaxed`,
813
- children: statusLine(m, now)
917
+ children: statusLine(m, now, t)
814
918
  }),
815
919
  /* @__PURE__ */ jsx("p", {
816
920
  className: `${TEXT_HINT} tabular-nums`,
817
- children: coverageLine(m)
921
+ children: coverageLine(m, t)
818
922
  }),
819
923
  /* @__PURE__ */ jsx(SceneReferenceGallery, {
820
924
  monitor: m,
@@ -834,18 +938,17 @@ function SceneMonitorEditor({ deviceId }) {
834
938
  type: "button",
835
939
  className: BTN_NEUTRAL,
836
940
  disabled: busy !== null,
837
- title: optional ? "Optional. Without a night reference the checks in infrared are simply skipped — the scene keeps working in daylight and nothing is reported as broken." : `Take a fresh photo now and store it as the ${variantLabel(variant).toLowerCase()} reference`,
941
+ title: optional ? t("uiLibrary.sceneMonitor.optionalTitle", "Optional. Without a night reference the checks in infrared are simply skipped — the scene keeps working in daylight and nothing is reported as broken.") : t("uiLibrary.sceneMonitor.captureTitle", "Take a fresh photo now and store it as the {{variant}} reference", { variant: variantLabel(variant, t).toLowerCase() }),
838
942
  onClick: () => void run(`cap-${m.id}-${variant}`, async () => dev.sceneMonitor?.captureReference({
839
943
  monitorId: m.id,
840
944
  condition: variant
841
945
  })),
842
946
  children: [
843
947
  /* @__PURE__ */ jsx(Camera, { className: "mr-1 inline h-3 w-3" }),
844
- "Capture ",
845
- variantLabel(variant).toLowerCase(),
948
+ t("uiLibrary.sceneMonitor.capture", "Capture {{variant}}", { variant: variantLabel(variant, t).toLowerCase() }),
846
949
  optional && /* @__PURE__ */ jsx("span", {
847
950
  className: "ml-1 text-foreground-subtle/70",
848
- children: "(optional)"
951
+ children: t("uiLibrary.sceneMonitor.optional", "(optional)")
849
952
  })
850
953
  ]
851
954
  }, variant);
@@ -854,25 +957,25 @@ function SceneMonitorEditor({ deviceId }) {
854
957
  type: "button",
855
958
  className: BTN_PRIMARY,
856
959
  disabled: busy !== null || m.states.length === 0,
857
- title: "Clear the flag and re-photograph what normal looks like right now",
960
+ title: t("uiLibrary.sceneMonitor.resetTitle", "Clear the flag and re-photograph what normal looks like right now"),
858
961
  onClick: () => void run(`reset-${m.id}`, async () => dev.sceneMonitor?.resetScene({ monitorId: m.id })),
859
- children: [/* @__PURE__ */ jsx(RotateCcw, { className: "mr-1 inline h-3 w-3" }), "Reset"]
962
+ children: [/* @__PURE__ */ jsx(RotateCcw, { className: "mr-1 inline h-3 w-3" }), t("uiLibrary.sceneMonitor.reset", "Reset")]
860
963
  }),
861
964
  /* @__PURE__ */ jsx("button", {
862
965
  type: "button",
863
966
  className: BTN_NEUTRAL,
864
967
  disabled: busy !== null,
865
968
  onClick: () => void run(`check-${m.id}`, async () => dev.sceneMonitor?.recheckNow({ monitorId: m.id })),
866
- children: "Check now"
969
+ children: t("uiLibrary.sceneMonitor.checkNow", "Check now")
867
970
  }),
868
971
  /* @__PURE__ */ jsxs("button", {
869
972
  type: "button",
870
973
  className: tuningId === m.id ? BTN_PRIMARY : BTN_NEUTRAL,
871
974
  "aria-pressed": tuningId === m.id,
872
- "aria-label": `Settings for ${m.label}`,
975
+ "aria-label": t("uiLibrary.sceneMonitor.settingsFor", "Settings for {{label}}", { label: m.label }),
873
976
  disabled: busy !== null,
874
977
  onClick: () => setTuningId((id) => id === m.id ? null : m.id),
875
- children: [/* @__PURE__ */ jsx(Settings2, { className: "mr-1 inline h-3 w-3" }), "Settings"]
978
+ children: [/* @__PURE__ */ jsx(Settings2, { className: "mr-1 inline h-3 w-3" }), t("uiLibrary.sceneMonitor.settings", "Settings")]
876
979
  }),
877
980
  /* @__PURE__ */ jsx("button", {
878
981
  type: "button",