@camstack/ui-library 1.2.50 → 1.2.51

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.
@@ -0,0 +1,281 @@
1
+ const require_index = require("./index.cjs");
2
+ const require_MaskShapeCanvas = require("./MaskShapeCanvas-BByN3jvt.cjs");
3
+ let react = require("react");
4
+ let react_jsx_runtime = require("react/jsx-runtime");
5
+ //#region src/composites/cap-settings/SceneMonitorEditor.tsx
6
+ 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";
7
+ 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";
8
+ 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";
9
+ var TONE_CLASS = {
10
+ alarm: "border-danger/50 bg-danger/15 text-danger",
11
+ ok: "border-success/50 bg-success/15 text-success",
12
+ unknown: "border-border bg-surface text-foreground-subtle"
13
+ };
14
+ /** A scene without at least one reference cannot judge anything, so the
15
+ * similarity threshold only becomes meaningful after the first capture. */
16
+ var DEFAULT_THRESHOLD = .8;
17
+ var DEFAULT_HYSTERESIS = 3;
18
+ function SceneMonitorEditor({ deviceId }) {
19
+ const dev = require_index.useDeviceProxy$1(require_index.useSystem$1().trpcClient, deviceId);
20
+ const pushed = require_index.useDeviceStateSlice$1(dev?.state.sceneMonitor);
21
+ const snapshot = require_index.useDeviceSnapshotImage$1(deviceId, { width: 640 });
22
+ const [fetched, setFetched] = (0, react.useState)(null);
23
+ const [unsupported, setUnsupported] = (0, react.useState)(false);
24
+ const [busy, setBusy] = (0, react.useState)(null);
25
+ const [error, setError] = (0, react.useState)(null);
26
+ const [selectedId, setSelectedId] = (0, react.useState)(null);
27
+ const [drafting, setDrafting] = (0, react.useState)(false);
28
+ const [draftRoi, setDraftRoi] = (0, react.useState)(null);
29
+ const [draftLabel, setDraftLabel] = (0, react.useState)("");
30
+ const mounted = (0, react.useRef)(true);
31
+ (0, react.useEffect)(() => {
32
+ mounted.current = true;
33
+ return () => {
34
+ mounted.current = false;
35
+ };
36
+ }, []);
37
+ const reload = (0, react.useCallback)(async () => {
38
+ if (!dev) return;
39
+ try {
40
+ const status = await dev.sceneMonitor?.listScenes({});
41
+ if (!mounted.current) return;
42
+ if (status) setFetched(status);
43
+ } catch (err) {
44
+ if (!mounted.current) return;
45
+ if (require_index.isAbsentProvider$1(err)) setUnsupported(true);
46
+ else setError(err instanceof Error ? err.message : String(err));
47
+ }
48
+ }, [dev]);
49
+ (0, react.useEffect)(() => {
50
+ setFetched(null);
51
+ setUnsupported(false);
52
+ setError(null);
53
+ reload();
54
+ }, [reload]);
55
+ const monitors = pushed?.monitors ?? fetched?.monitors ?? [];
56
+ const now = Date.now();
57
+ const run = (0, react.useCallback)(async (key, fn) => {
58
+ setBusy(key);
59
+ setError(null);
60
+ try {
61
+ await fn();
62
+ await reload();
63
+ } catch (err) {
64
+ if (mounted.current) setError(err instanceof Error ? err.message : String(err));
65
+ } finally {
66
+ if (mounted.current) setBusy(null);
67
+ }
68
+ }, [reload]);
69
+ const items = (0, react.useMemo)(() => {
70
+ const existing = monitors.map((m) => ({
71
+ id: m.id,
72
+ shape: m.roi,
73
+ label: m.label,
74
+ enabled: m.enabled
75
+ }));
76
+ return draftRoi !== null ? [...existing, {
77
+ id: "__draft__",
78
+ shape: draftRoi,
79
+ label: "New scene"
80
+ }] : existing;
81
+ }, [monitors, draftRoi]);
82
+ const onShapeChange = (0, react.useCallback)((id, shape) => {
83
+ if (shape.kind !== "rect") return;
84
+ if (id === "__draft__") {
85
+ setDraftRoi(shape);
86
+ return;
87
+ }
88
+ run(`roi-${String(id)}`, async () => dev?.sceneMonitor?.updateScene({
89
+ monitorId: String(id),
90
+ patch: { roi: shape }
91
+ }));
92
+ }, [dev, run]);
93
+ const onDrawComplete = (0, react.useCallback)((shape) => {
94
+ if (shape.kind !== "rect") return;
95
+ setDraftRoi(shape);
96
+ }, []);
97
+ const createScene = (0, react.useCallback)(async () => {
98
+ if (draftRoi === null || draftLabel.trim().length === 0) return;
99
+ await run("create", async () => {
100
+ await dev?.sceneMonitor?.createScene({
101
+ label: draftLabel.trim(),
102
+ roi: draftRoi,
103
+ check: {
104
+ mode: "similarity",
105
+ threshold: DEFAULT_THRESHOLD,
106
+ hysteresisCount: DEFAULT_HYSTERESIS
107
+ }
108
+ });
109
+ });
110
+ setDraftRoi(null);
111
+ setDraftLabel("");
112
+ setDrafting(false);
113
+ }, [
114
+ dev,
115
+ draftRoi,
116
+ draftLabel,
117
+ run
118
+ ]);
119
+ if (!dev) return null;
120
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
121
+ className: "flex flex-col gap-3",
122
+ children: unsupported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
123
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
124
+ children: "Scene monitoring is not available for this camera."
125
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
127
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
128
+ 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, in daylight and again at night."
129
+ }),
130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
131
+ className: "relative overflow-hidden rounded-md border border-border",
132
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_MaskShapeCanvas.MaskShapeCanvas, {
133
+ items,
134
+ supportedShapes: ["rect"],
135
+ selectedId: selectedId ?? (draftRoi !== null ? "__draft__" : null),
136
+ onSelect: (id) => setSelectedId(id === null ? null : String(id)),
137
+ onShapeChange,
138
+ onDrawComplete,
139
+ drawingKind: drafting && draftRoi === null ? "rect" : null,
140
+ backdrop: snapshot.src !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
141
+ src: snapshot.src,
142
+ alt: "",
143
+ className: "h-full w-full object-contain",
144
+ draggable: false
145
+ }) : null
146
+ })
147
+ }),
148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
149
+ className: "flex flex-wrap items-center gap-2",
150
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
151
+ type: "button",
152
+ className: drafting ? BTN_PRIMARY : BTN_NEUTRAL,
153
+ "aria-pressed": drafting,
154
+ onClick: () => {
155
+ setDrafting((v) => !v);
156
+ setDraftRoi(null);
157
+ },
158
+ disabled: busy !== null,
159
+ children: drafting ? "Cancel" : "+ New scene"
160
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
161
+ type: "button",
162
+ className: BTN_NEUTRAL,
163
+ onClick: snapshot.refresh,
164
+ disabled: busy !== null,
165
+ children: "Refresh frame"
166
+ })]
167
+ }),
168
+ drafting && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
169
+ className: "flex flex-col gap-2 rounded-md border border-border bg-surface p-2",
170
+ children: [
171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
172
+ className: "text-[10px] text-foreground-subtle",
173
+ 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."
174
+ }),
175
+ draftRoi !== null && require_index.roiTooLarge$1(draftRoi) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
176
+ className: "text-[11px] leading-relaxed text-warning",
177
+ 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."
178
+ }),
179
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
180
+ className: "flex items-center gap-2",
181
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
182
+ type: "text",
183
+ value: draftLabel,
184
+ onChange: (e) => setDraftLabel(e.target.value),
185
+ placeholder: "e.g. Immondizia prelevata",
186
+ className: "flex-1 rounded-md border border-border bg-surface px-2 py-1 text-[12px] text-foreground"
187
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
188
+ type: "button",
189
+ className: BTN_PRIMARY,
190
+ onClick: () => void createScene(),
191
+ disabled: draftRoi === null || draftLabel.trim().length === 0 || busy !== null,
192
+ children: "Create"
193
+ })]
194
+ })
195
+ ]
196
+ }),
197
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
198
+ className: "text-[11px] leading-relaxed text-danger",
199
+ children: error
200
+ }),
201
+ monitors.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
202
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
203
+ children: "No scenes on this camera yet."
204
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
205
+ className: "flex flex-col gap-2",
206
+ children: monitors.map((m) => {
207
+ const badge = require_index.badgeFor$1(m);
208
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
209
+ className: `flex flex-col gap-1.5 rounded-md border p-2 ${selectedId === m.id ? "border-primary/50" : "border-border"}`,
210
+ children: [
211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
212
+ className: "flex items-center gap-2",
213
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
214
+ type: "button",
215
+ className: "flex-1 text-left text-[12px] font-medium text-foreground",
216
+ onClick: () => setSelectedId(m.id),
217
+ children: m.label
218
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
219
+ className: `rounded border px-1.5 py-0.5 text-[10px] font-medium ${TONE_CLASS[badge.tone]}`,
220
+ children: badge.text
221
+ })]
222
+ }),
223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
224
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
225
+ children: require_index.statusLine$1(m, now)
226
+ }),
227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
228
+ className: `${require_index.TEXT_HINT} tabular-nums`,
229
+ children: require_index.coverageLine$1(m)
230
+ }),
231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
232
+ className: "flex flex-wrap items-center gap-1.5",
233
+ children: [
234
+ require_index.SCENE_CAPTURE_VARIANTS$1.map((variant) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
235
+ type: "button",
236
+ className: BTN_NEUTRAL,
237
+ disabled: busy !== null,
238
+ title: `Take a fresh photo now and store it as the ${require_index.variantLabel$1(variant).toLowerCase()} reference`,
239
+ onClick: () => void run(`cap-${m.id}-${variant}`, async () => dev.sceneMonitor?.captureReference({
240
+ monitorId: m.id,
241
+ condition: variant
242
+ })),
243
+ children: [
244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.Camera, { className: "mr-1 inline h-3 w-3" }),
245
+ "Capture ",
246
+ require_index.variantLabel$1(variant).toLowerCase()
247
+ ]
248
+ }, variant)),
249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
250
+ type: "button",
251
+ className: BTN_PRIMARY,
252
+ disabled: busy !== null || m.states.length === 0,
253
+ title: "Clear the flag and re-photograph what normal looks like right now",
254
+ onClick: () => void run(`reset-${m.id}`, async () => dev.sceneMonitor?.resetScene({ monitorId: m.id })),
255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.RotateCcw, { className: "mr-1 inline h-3 w-3" }), "Reset"]
256
+ }),
257
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
258
+ type: "button",
259
+ className: BTN_NEUTRAL,
260
+ disabled: busy !== null,
261
+ onClick: () => void run(`check-${m.id}`, async () => dev.sceneMonitor?.recheckNow({ monitorId: m.id })),
262
+ children: "Check now"
263
+ }),
264
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
265
+ type: "button",
266
+ className: BTN_DANGER,
267
+ disabled: busy !== null,
268
+ onClick: () => void run(`del-${m.id}`, async () => dev.sceneMonitor?.deleteScene({ monitorId: m.id })),
269
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.Trash2, { className: "h-3 w-3" })
270
+ })
271
+ ]
272
+ })
273
+ ]
274
+ }, m.id);
275
+ })
276
+ })
277
+ ] })
278
+ });
279
+ }
280
+ //#endregion
281
+ exports.SceneMonitorEditor = SceneMonitorEditor;
@@ -0,0 +1,281 @@
1
+ import { SCENE_CAPTURE_VARIANTS, TEXT_HINT, badgeFor, coverageLine, isAbsentProvider, o as Camera, r as RotateCcw, roiTooLarge, statusLine, t as Trash2, useDeviceProxy, useDeviceSnapshotImage, useDeviceStateSlice, useSystem, variantLabel } from "./index.js";
2
+ import { MaskShapeCanvas } from "./MaskShapeCanvas-DI4BY7W2.js";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/composites/cap-settings/SceneMonitorEditor.tsx
6
+ 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";
7
+ 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";
8
+ 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";
9
+ var TONE_CLASS = {
10
+ alarm: "border-danger/50 bg-danger/15 text-danger",
11
+ ok: "border-success/50 bg-success/15 text-success",
12
+ unknown: "border-border bg-surface text-foreground-subtle"
13
+ };
14
+ /** A scene without at least one reference cannot judge anything, so the
15
+ * similarity threshold only becomes meaningful after the first capture. */
16
+ var DEFAULT_THRESHOLD = .8;
17
+ var DEFAULT_HYSTERESIS = 3;
18
+ function SceneMonitorEditor({ deviceId }) {
19
+ const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
20
+ const pushed = useDeviceStateSlice(dev?.state.sceneMonitor);
21
+ const snapshot = useDeviceSnapshotImage(deviceId, { width: 640 });
22
+ const [fetched, setFetched] = useState(null);
23
+ const [unsupported, setUnsupported] = useState(false);
24
+ const [busy, setBusy] = useState(null);
25
+ const [error, setError] = useState(null);
26
+ const [selectedId, setSelectedId] = useState(null);
27
+ const [drafting, setDrafting] = useState(false);
28
+ const [draftRoi, setDraftRoi] = useState(null);
29
+ const [draftLabel, setDraftLabel] = useState("");
30
+ const mounted = useRef(true);
31
+ useEffect(() => {
32
+ mounted.current = true;
33
+ return () => {
34
+ mounted.current = false;
35
+ };
36
+ }, []);
37
+ const reload = useCallback(async () => {
38
+ if (!dev) return;
39
+ try {
40
+ const status = await dev.sceneMonitor?.listScenes({});
41
+ if (!mounted.current) return;
42
+ if (status) setFetched(status);
43
+ } catch (err) {
44
+ if (!mounted.current) return;
45
+ if (isAbsentProvider(err)) setUnsupported(true);
46
+ else setError(err instanceof Error ? err.message : String(err));
47
+ }
48
+ }, [dev]);
49
+ useEffect(() => {
50
+ setFetched(null);
51
+ setUnsupported(false);
52
+ setError(null);
53
+ reload();
54
+ }, [reload]);
55
+ const monitors = pushed?.monitors ?? fetched?.monitors ?? [];
56
+ const now = Date.now();
57
+ const run = useCallback(async (key, fn) => {
58
+ setBusy(key);
59
+ setError(null);
60
+ try {
61
+ await fn();
62
+ await reload();
63
+ } catch (err) {
64
+ if (mounted.current) setError(err instanceof Error ? err.message : String(err));
65
+ } finally {
66
+ if (mounted.current) setBusy(null);
67
+ }
68
+ }, [reload]);
69
+ const items = useMemo(() => {
70
+ const existing = monitors.map((m) => ({
71
+ id: m.id,
72
+ shape: m.roi,
73
+ label: m.label,
74
+ enabled: m.enabled
75
+ }));
76
+ return draftRoi !== null ? [...existing, {
77
+ id: "__draft__",
78
+ shape: draftRoi,
79
+ label: "New scene"
80
+ }] : existing;
81
+ }, [monitors, draftRoi]);
82
+ const onShapeChange = useCallback((id, shape) => {
83
+ if (shape.kind !== "rect") return;
84
+ if (id === "__draft__") {
85
+ setDraftRoi(shape);
86
+ return;
87
+ }
88
+ run(`roi-${String(id)}`, async () => dev?.sceneMonitor?.updateScene({
89
+ monitorId: String(id),
90
+ patch: { roi: shape }
91
+ }));
92
+ }, [dev, run]);
93
+ const onDrawComplete = useCallback((shape) => {
94
+ if (shape.kind !== "rect") return;
95
+ setDraftRoi(shape);
96
+ }, []);
97
+ const createScene = useCallback(async () => {
98
+ if (draftRoi === null || draftLabel.trim().length === 0) return;
99
+ await run("create", async () => {
100
+ await dev?.sceneMonitor?.createScene({
101
+ label: draftLabel.trim(),
102
+ roi: draftRoi,
103
+ check: {
104
+ mode: "similarity",
105
+ threshold: DEFAULT_THRESHOLD,
106
+ hysteresisCount: DEFAULT_HYSTERESIS
107
+ }
108
+ });
109
+ });
110
+ setDraftRoi(null);
111
+ setDraftLabel("");
112
+ setDrafting(false);
113
+ }, [
114
+ dev,
115
+ draftRoi,
116
+ draftLabel,
117
+ run
118
+ ]);
119
+ if (!dev) return null;
120
+ return /* @__PURE__ */ jsx("div", {
121
+ className: "flex flex-col gap-3",
122
+ children: unsupported ? /* @__PURE__ */ jsx("p", {
123
+ className: `${TEXT_HINT} leading-relaxed`,
124
+ children: "Scene monitoring is not available for this camera."
125
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
126
+ /* @__PURE__ */ jsx("p", {
127
+ className: `${TEXT_HINT} leading-relaxed`,
128
+ 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, in daylight and again at night."
129
+ }),
130
+ /* @__PURE__ */ jsx("div", {
131
+ className: "relative overflow-hidden rounded-md border border-border",
132
+ children: /* @__PURE__ */ jsx(MaskShapeCanvas, {
133
+ items,
134
+ supportedShapes: ["rect"],
135
+ selectedId: selectedId ?? (draftRoi !== null ? "__draft__" : null),
136
+ onSelect: (id) => setSelectedId(id === null ? null : String(id)),
137
+ onShapeChange,
138
+ onDrawComplete,
139
+ drawingKind: drafting && draftRoi === null ? "rect" : null,
140
+ backdrop: snapshot.src !== null ? /* @__PURE__ */ jsx("img", {
141
+ src: snapshot.src,
142
+ alt: "",
143
+ className: "h-full w-full object-contain",
144
+ draggable: false
145
+ }) : null
146
+ })
147
+ }),
148
+ /* @__PURE__ */ jsxs("div", {
149
+ className: "flex flex-wrap items-center gap-2",
150
+ children: [/* @__PURE__ */ jsx("button", {
151
+ type: "button",
152
+ className: drafting ? BTN_PRIMARY : BTN_NEUTRAL,
153
+ "aria-pressed": drafting,
154
+ onClick: () => {
155
+ setDrafting((v) => !v);
156
+ setDraftRoi(null);
157
+ },
158
+ disabled: busy !== null,
159
+ children: drafting ? "Cancel" : "+ New scene"
160
+ }), /* @__PURE__ */ jsx("button", {
161
+ type: "button",
162
+ className: BTN_NEUTRAL,
163
+ onClick: snapshot.refresh,
164
+ disabled: busy !== null,
165
+ children: "Refresh frame"
166
+ })]
167
+ }),
168
+ drafting && /* @__PURE__ */ jsxs("div", {
169
+ className: "flex flex-col gap-2 rounded-md border border-border bg-surface p-2",
170
+ children: [
171
+ /* @__PURE__ */ jsx("p", {
172
+ className: "text-[10px] text-foreground-subtle",
173
+ 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."
174
+ }),
175
+ draftRoi !== null && roiTooLarge(draftRoi) && /* @__PURE__ */ jsx("p", {
176
+ className: "text-[11px] leading-relaxed text-warning",
177
+ 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."
178
+ }),
179
+ /* @__PURE__ */ jsxs("div", {
180
+ className: "flex items-center gap-2",
181
+ children: [/* @__PURE__ */ jsx("input", {
182
+ type: "text",
183
+ value: draftLabel,
184
+ onChange: (e) => setDraftLabel(e.target.value),
185
+ placeholder: "e.g. Immondizia prelevata",
186
+ className: "flex-1 rounded-md border border-border bg-surface px-2 py-1 text-[12px] text-foreground"
187
+ }), /* @__PURE__ */ jsx("button", {
188
+ type: "button",
189
+ className: BTN_PRIMARY,
190
+ onClick: () => void createScene(),
191
+ disabled: draftRoi === null || draftLabel.trim().length === 0 || busy !== null,
192
+ children: "Create"
193
+ })]
194
+ })
195
+ ]
196
+ }),
197
+ error !== null && /* @__PURE__ */ jsx("p", {
198
+ className: "text-[11px] leading-relaxed text-danger",
199
+ children: error
200
+ }),
201
+ monitors.length === 0 ? /* @__PURE__ */ jsx("p", {
202
+ className: `${TEXT_HINT} leading-relaxed`,
203
+ children: "No scenes on this camera yet."
204
+ }) : /* @__PURE__ */ jsx("ul", {
205
+ className: "flex flex-col gap-2",
206
+ children: monitors.map((m) => {
207
+ const badge = badgeFor(m);
208
+ return /* @__PURE__ */ jsxs("li", {
209
+ className: `flex flex-col gap-1.5 rounded-md border p-2 ${selectedId === m.id ? "border-primary/50" : "border-border"}`,
210
+ children: [
211
+ /* @__PURE__ */ jsxs("div", {
212
+ className: "flex items-center gap-2",
213
+ children: [/* @__PURE__ */ jsx("button", {
214
+ type: "button",
215
+ className: "flex-1 text-left text-[12px] font-medium text-foreground",
216
+ onClick: () => setSelectedId(m.id),
217
+ children: m.label
218
+ }), /* @__PURE__ */ jsx("span", {
219
+ className: `rounded border px-1.5 py-0.5 text-[10px] font-medium ${TONE_CLASS[badge.tone]}`,
220
+ children: badge.text
221
+ })]
222
+ }),
223
+ /* @__PURE__ */ jsx("p", {
224
+ className: `${TEXT_HINT} leading-relaxed`,
225
+ children: statusLine(m, now)
226
+ }),
227
+ /* @__PURE__ */ jsx("p", {
228
+ className: `${TEXT_HINT} tabular-nums`,
229
+ children: coverageLine(m)
230
+ }),
231
+ /* @__PURE__ */ jsxs("div", {
232
+ className: "flex flex-wrap items-center gap-1.5",
233
+ children: [
234
+ SCENE_CAPTURE_VARIANTS.map((variant) => /* @__PURE__ */ jsxs("button", {
235
+ type: "button",
236
+ className: BTN_NEUTRAL,
237
+ disabled: busy !== null,
238
+ title: `Take a fresh photo now and store it as the ${variantLabel(variant).toLowerCase()} reference`,
239
+ onClick: () => void run(`cap-${m.id}-${variant}`, async () => dev.sceneMonitor?.captureReference({
240
+ monitorId: m.id,
241
+ condition: variant
242
+ })),
243
+ children: [
244
+ /* @__PURE__ */ jsx(Camera, { className: "mr-1 inline h-3 w-3" }),
245
+ "Capture ",
246
+ variantLabel(variant).toLowerCase()
247
+ ]
248
+ }, variant)),
249
+ /* @__PURE__ */ jsxs("button", {
250
+ type: "button",
251
+ className: BTN_PRIMARY,
252
+ disabled: busy !== null || m.states.length === 0,
253
+ title: "Clear the flag and re-photograph what normal looks like right now",
254
+ onClick: () => void run(`reset-${m.id}`, async () => dev.sceneMonitor?.resetScene({ monitorId: m.id })),
255
+ children: [/* @__PURE__ */ jsx(RotateCcw, { className: "mr-1 inline h-3 w-3" }), "Reset"]
256
+ }),
257
+ /* @__PURE__ */ jsx("button", {
258
+ type: "button",
259
+ className: BTN_NEUTRAL,
260
+ disabled: busy !== null,
261
+ onClick: () => void run(`check-${m.id}`, async () => dev.sceneMonitor?.recheckNow({ monitorId: m.id })),
262
+ children: "Check now"
263
+ }),
264
+ /* @__PURE__ */ jsx("button", {
265
+ type: "button",
266
+ className: BTN_DANGER,
267
+ disabled: busy !== null,
268
+ onClick: () => void run(`del-${m.id}`, async () => dev.sceneMonitor?.deleteScene({ monitorId: m.id })),
269
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3 w-3" })
270
+ })
271
+ ]
272
+ })
273
+ ]
274
+ }, m.id);
275
+ })
276
+ })
277
+ ] })
278
+ });
279
+ }
280
+ //#endregion
281
+ export { SceneMonitorEditor };
@@ -1,2 +1,43 @@
1
- import { CapSettingsComponentProps } from './index';
2
- export declare function SceneMonitorEditor({ deviceId }: CapSettingsComponentProps): import("react").JSX.Element | null;
1
+ /**
2
+ * SceneMonitorEditor — the per-camera half of the Scenes surface.
3
+ *
4
+ * **Imported directly, not resolved as a widget.** It briefly lived behind the
5
+ * host widget `host/scene-monitor-editor`, mounted as a "Scenes" tab on one
6
+ * camera's detail page. That put a standing question about the property ("is
7
+ * the bin still out") one camera deep, where the operator's actual question —
8
+ * "which of my scenes have tripped" — could not be asked at all. The overview
9
+ * is now the top-level `/scenes` page; this component is what that page opens
10
+ * once a camera is chosen, in a dialog. The widget id and its `deviceConfig`
11
+ * declaration are both gone, in the same commit, because
12
+ * `scripts/check-host-widget-resolves.ts` checks both directions.
13
+ *
14
+ * It renders BARE — no `WidgetPanel` — because its only caller supplies a
15
+ * dialog header, and two stacked titles read as a layout bug.
16
+ *
17
+ * ## The flow, in the order the operator meets it
18
+ *
19
+ * 1. **Draw a region on the still frame.** A rect over a SNAPSHOT, not the live
20
+ * player: a scene is defined against a photograph, and drawing on a moving
21
+ * picture makes it impossible to see whether the box actually contains the
22
+ * bin. The editor starts in draw mode and warns above ~40 % of the frame,
23
+ * because a full-frame scene is dominated by weather, sky and parked cars —
24
+ * it will not work, and it will look broken rather than misconfigured.
25
+ * 2. **Capture the initial state, per lighting variant.** Two buttons, Daylight
26
+ * and Night (IR), each forcing a fresh snapshot. The variant is DETECTED
27
+ * from the frame and shown rather than asked; the buttons exist so the
28
+ * operator can say "capture this as the night reference" while standing
29
+ * outside at 22:00.
30
+ * 3. **Read the coverage line.** "Daylight ✓ · Night (IR) ✗ — reports 'can't
31
+ * tell' in night (ir)". This feature's failure mode is silence that reads as
32
+ * "nothing has happened", and that one line is the difference between a
33
+ * five-minute fix and a session of guessing.
34
+ * 4. **Reset.** Clears the flag AND re-captures the baseline for the current
35
+ * light, because the bin never goes back in exactly the same spot.
36
+ *
37
+ * Live state arrives through the cap's `status.kind: 'push'` runtime slice, so
38
+ * the card updates itself without polling.
39
+ */
40
+ export interface SceneMonitorEditorProps {
41
+ readonly deviceId: number;
42
+ }
43
+ export declare function SceneMonitorEditor({ deviceId }: SceneMonitorEditorProps): import("react").JSX.Element | null;