@camstack/ui-library 1.0.1 → 1.0.3

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,384 @@
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
+ /**
6
+ * @license lucide-react v0.576.0 - ISC
7
+ *
8
+ * This source code is licensed under the ISC license.
9
+ * See the LICENSE file in the root directory of this source tree.
10
+ */
11
+ var Hexagon = require_index.createLucideIcon("hexagon", [["path", {
12
+ d: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",
13
+ key: "yt0hxn"
14
+ }]]);
15
+ //#endregion
16
+ //#region src/composites/cap-settings/PrivacyMaskSettings.tsx
17
+ /**
18
+ * PrivacyMaskSettings — management surface for the on-camera
19
+ * `privacy-mask` capability.
20
+ *
21
+ * The cap was reshaped from a boolean cell-grid into a SHAPE model: up to
22
+ * `maxRegions` zones, each a rectangle or a free polygon (normalized 0..1
23
+ * of the camera frame). This component owns the draft zone list and edits
24
+ * it via the shared `MaskShapeCanvas` drawing-plane, painted over the live
25
+ * frame only while editing.
26
+ *
27
+ * Structure:
28
+ * - The editable SHAPES are painted over the live frame by the shared
29
+ * `MaskShapeCanvas`, registered as a player-overlay layer ONLY while
30
+ * "Edit mask" is toggled on (OFF by default — nothing draws on mount).
31
+ * - Every CONTROL lives in the panel: edit-mode toggle, master enable,
32
+ * Add rect / Add polygon (gated by `supportedShapes` + `maxRegions`,
33
+ * they arm the canvas' draw mode), active-count display, Revert, Save.
34
+ *
35
+ * Registered as host widget `host/privacy-mask-grid` (see
36
+ * `widgets/host-widgets.ts`) so the D14 device-config cap framework mounts
37
+ * it on the Image tab. Self-gates with `isAbsentProvider` to a friendly
38
+ * "not supported" message when the camera has no privacy-mask cap.
39
+ */
40
+ var PRIVACY_MASK_OVERLAY_ORDER = 120;
41
+ var OVERLAY_ID = "privacy-mask";
42
+ 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";
43
+ 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";
44
+ /** Narrow the full MaskShape union to a privacy-mask shape (rect|polygon). */
45
+ function toPrivacyMaskShape(shape) {
46
+ if (shape.kind === "rect" || shape.kind === "polygon") return shape;
47
+ return null;
48
+ }
49
+ /** Next free 0-based integer id not already used by a draft region. */
50
+ function nextRegionId(regions) {
51
+ const used = new Set(regions.map((r) => r.id));
52
+ let id = 0;
53
+ while (used.has(id)) id += 1;
54
+ return id;
55
+ }
56
+ /** Structural equality for a draft vs. committed region list (order-sensitive). */
57
+ function regionsEqual(a, b) {
58
+ if (a.length !== b.length) return false;
59
+ for (let i = 0; i < a.length; i += 1) if (JSON.stringify(a[i]) !== JSON.stringify(b[i])) return false;
60
+ return true;
61
+ }
62
+ function PrivacyMaskSettings({ deviceId }) {
63
+ const dev = require_index.useDeviceProxy$1(require_index.useSystem$1().trpcClient, deviceId);
64
+ const [options, setOptions] = (0, react.useState)(null);
65
+ const [unsupported, setUnsupported] = (0, react.useState)(false);
66
+ const [draft, setDraft] = (0, react.useState)(null);
67
+ const [committed, setCommitted] = (0, react.useState)(null);
68
+ const [saving, setSaving] = (0, react.useState)(false);
69
+ const [editing, setEditing] = (0, react.useState)(false);
70
+ const [selectedId, setSelectedId] = (0, react.useState)(null);
71
+ const [drawingKind, setDrawingKind] = (0, react.useState)(null);
72
+ const seededRef = (0, react.useRef)(false);
73
+ (0, react.useEffect)(() => {
74
+ if (!dev) return void 0;
75
+ let cancelled = false;
76
+ seededRef.current = false;
77
+ setOptions(null);
78
+ setUnsupported(false);
79
+ setDraft(null);
80
+ setCommitted(null);
81
+ setEditing(false);
82
+ setSelectedId(null);
83
+ setDrawingKind(null);
84
+ (async () => {
85
+ try {
86
+ const opts = await dev.privacyMask?.getOptions({});
87
+ if (cancelled) return;
88
+ if (!opts) throw new Error("device proxy not ready");
89
+ setOptions(opts);
90
+ if (seededRef.current) return;
91
+ const status = await dev.privacyMask?.getStatus({});
92
+ if (cancelled) return;
93
+ if (!status) throw new Error("device proxy not ready");
94
+ seededRef.current = true;
95
+ const seed = {
96
+ enabled: status.enabled,
97
+ regions: status.regions
98
+ };
99
+ setCommitted(seed);
100
+ setDraft(seed);
101
+ } catch (err) {
102
+ if (cancelled) return;
103
+ if (require_index.isAbsentProvider$1(err)) setUnsupported(true);
104
+ else console.error("Privacy Mask load failed", err);
105
+ }
106
+ })();
107
+ return () => {
108
+ cancelled = true;
109
+ };
110
+ }, [dev]);
111
+ const dirty = (0, react.useMemo)(() => draft !== null && committed !== null && (draft.enabled !== committed.enabled || !regionsEqual(draft.regions, committed.regions)), [draft, committed]);
112
+ const activeCount = draft ? draft.regions.length : 0;
113
+ const maxRegions = options ? options.maxRegions : 0;
114
+ const maxRegionsRef = (0, react.useRef)(0);
115
+ (0, react.useEffect)(() => {
116
+ maxRegionsRef.current = maxRegions;
117
+ }, [maxRegions]);
118
+ const atCapacity = maxRegions > 0 && activeCount >= maxRegions;
119
+ const toggleEnabled = (0, react.useCallback)(() => {
120
+ setDraft((prev) => prev ? {
121
+ ...prev,
122
+ enabled: !prev.enabled
123
+ } : prev);
124
+ }, []);
125
+ const onSelect = (0, react.useCallback)((id) => {
126
+ setSelectedId(typeof id === "number" ? id : null);
127
+ }, []);
128
+ const items = (0, react.useMemo)(() => draft ? draft.regions.map((r) => ({
129
+ id: r.id,
130
+ shape: r.shape,
131
+ enabled: r.enabled,
132
+ label: `Zone ${String(r.id)}`
133
+ })) : [], [draft]);
134
+ const onShapeChange = (0, react.useCallback)((id, shape) => {
135
+ const next = toPrivacyMaskShape(shape);
136
+ if (!next) return;
137
+ setDraft((prev) => prev ? {
138
+ ...prev,
139
+ regions: prev.regions.map((r) => r.id === id ? {
140
+ ...r,
141
+ shape: next
142
+ } : r)
143
+ } : prev);
144
+ }, []);
145
+ const onDrawComplete = (0, react.useCallback)((shape) => {
146
+ const next = toPrivacyMaskShape(shape);
147
+ if (!next) return;
148
+ setDrawingKind(null);
149
+ setDraft((prev) => {
150
+ if (!prev) return prev;
151
+ const limit = maxRegionsRef.current;
152
+ if (limit > 0 && prev.regions.length >= limit) return prev;
153
+ const id = nextRegionId(prev.regions);
154
+ const region = {
155
+ id,
156
+ enabled: true,
157
+ shape: next
158
+ };
159
+ setSelectedId(id);
160
+ return {
161
+ ...prev,
162
+ regions: [...prev.regions, region]
163
+ };
164
+ });
165
+ }, []);
166
+ const armDraw = (0, react.useCallback)((kind) => {
167
+ setEditing(true);
168
+ setSelectedId(null);
169
+ setDrawingKind(kind);
170
+ }, []);
171
+ const selectRegion = (0, react.useCallback)((id) => {
172
+ setEditing(true);
173
+ setDrawingKind(null);
174
+ setSelectedId(id);
175
+ }, []);
176
+ const deleteRegion = (0, react.useCallback)((id) => {
177
+ setSelectedId((cur) => cur === id ? null : cur);
178
+ setDraft((prev) => prev ? {
179
+ ...prev,
180
+ regions: prev.regions.filter((r) => r.id !== id)
181
+ } : prev);
182
+ }, []);
183
+ const revert = (0, react.useCallback)(() => {
184
+ setDrawingKind(null);
185
+ setSelectedId(null);
186
+ if (committed) setDraft({
187
+ enabled: committed.enabled,
188
+ regions: committed.regions
189
+ });
190
+ }, [committed]);
191
+ const save = (0, react.useCallback)(async () => {
192
+ if (!dev || !draft) return;
193
+ setSaving(true);
194
+ try {
195
+ await dev.privacyMask?.setMask({ patch: {
196
+ enabled: draft.enabled,
197
+ regions: [...draft.regions]
198
+ } });
199
+ const fresh = await dev.privacyMask?.getStatus({});
200
+ if (fresh) {
201
+ const norm = {
202
+ enabled: fresh.enabled,
203
+ regions: fresh.regions
204
+ };
205
+ setCommitted(norm);
206
+ setDraft(norm);
207
+ }
208
+ } catch (err) {
209
+ console.error("Privacy Mask save failed", err);
210
+ } finally {
211
+ setSaving(false);
212
+ }
213
+ }, [dev, draft]);
214
+ const toggleEditing = (0, react.useCallback)(() => {
215
+ setEditing((v) => {
216
+ if (v) {
217
+ setDrawingKind(null);
218
+ setSelectedId(null);
219
+ }
220
+ return !v;
221
+ });
222
+ }, []);
223
+ const supportedShapes = options?.supportedShapes ?? [];
224
+ require_index.usePlayerOverlayLayer((0, react.useMemo)(() => editing && !unsupported && options && draft ? {
225
+ id: OVERLAY_ID,
226
+ order: PRIVACY_MASK_OVERLAY_ORDER,
227
+ node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_MaskShapeCanvas.MaskShapeCanvas, {
228
+ transparent: true,
229
+ items,
230
+ supportedShapes,
231
+ polygonVertices: options.polygonVertices,
232
+ selectedId,
233
+ onSelect,
234
+ onShapeChange,
235
+ onDrawComplete,
236
+ drawingKind
237
+ })
238
+ } : null, [
239
+ editing,
240
+ unsupported,
241
+ options,
242
+ draft,
243
+ items,
244
+ supportedShapes,
245
+ selectedId,
246
+ onSelect,
247
+ onShapeChange,
248
+ onDrawComplete,
249
+ drawingKind
250
+ ]));
251
+ const supportsRect = options?.supportedShapes.includes("rect") ?? false;
252
+ const supportsPolygon = options?.supportedShapes.includes("polygon") ?? false;
253
+ const noShapes = options !== null && (options.maxRegions <= 0 || options.supportedShapes.length === 0);
254
+ const ready = !unsupported && !noShapes && options !== null && draft !== null;
255
+ if (!dev) return null;
256
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.WidgetPanel, {
257
+ title: "Privacy Mask",
258
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.EyeOff, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
259
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
260
+ className: "flex flex-col gap-3",
261
+ children: unsupported || noShapes ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
262
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
263
+ children: "This camera doesn't support an on-board privacy mask."
264
+ }) : !ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
265
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
266
+ children: "Loading the camera's privacy mask…"
267
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
269
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
270
+ children: [
271
+ "Toggle ",
272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
273
+ className: "text-foreground",
274
+ children: "Edit mask"
275
+ }),
276
+ " to draw blanked-out zones on the live frame, then ",
277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
278
+ className: "text-foreground",
279
+ children: "Save"
280
+ }),
281
+ " to push them to the camera. Drag a rectangle to move, its corner to resize; drag polygon vertices, click an edge midpoint to add one, or right-click a vertex to remove it."
282
+ ]
283
+ }),
284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
285
+ className: "flex items-center gap-2 flex-wrap",
286
+ children: [
287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
288
+ type: "button",
289
+ onClick: toggleEditing,
290
+ disabled: saving,
291
+ "aria-pressed": editing,
292
+ className: editing ? BTN_PRIMARY : BTN_NEUTRAL,
293
+ children: editing ? "Done editing" : "Edit mask"
294
+ }),
295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
296
+ type: "button",
297
+ onClick: toggleEnabled,
298
+ disabled: saving,
299
+ "aria-pressed": draft.enabled,
300
+ className: draft.enabled ? BTN_PRIMARY : BTN_NEUTRAL,
301
+ children: draft.enabled ? "Mask on" : "Mask off"
302
+ }),
303
+ supportsRect && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
304
+ type: "button",
305
+ onClick: () => armDraw("rect"),
306
+ disabled: saving || atCapacity,
307
+ "aria-pressed": drawingKind === "rect",
308
+ className: drawingKind === "rect" ? BTN_PRIMARY : BTN_NEUTRAL,
309
+ title: atCapacity ? "Maximum zones reached" : "Add a rectangle zone",
310
+ children: "+ Rect"
311
+ }),
312
+ supportsPolygon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
313
+ type: "button",
314
+ onClick: () => armDraw("polygon"),
315
+ disabled: saving || atCapacity,
316
+ "aria-pressed": drawingKind === "polygon",
317
+ className: drawingKind === "polygon" ? BTN_PRIMARY : BTN_NEUTRAL,
318
+ title: atCapacity ? "Maximum zones reached" : "Add a polygon zone",
319
+ children: "+ Polygon"
320
+ }),
321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
322
+ className: `${require_index.TEXT_HINT} ml-1 tabular-nums`,
323
+ children: [
324
+ activeCount,
325
+ " / ",
326
+ maxRegions,
327
+ " zones"
328
+ ]
329
+ }),
330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "flex-1" }),
331
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
332
+ type: "button",
333
+ onClick: revert,
334
+ disabled: saving || !dirty,
335
+ className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
336
+ children: "Revert"
337
+ }),
338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
339
+ type: "button",
340
+ onClick: () => void save(),
341
+ disabled: saving || !dirty,
342
+ className: BTN_PRIMARY,
343
+ children: saving ? "Saving…" : "Save"
344
+ })
345
+ ]
346
+ }),
347
+ activeCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
348
+ className: "flex flex-col gap-1",
349
+ children: draft.regions.map((r) => {
350
+ const selected = selectedId === r.id;
351
+ const ShapeIcon = r.shape.kind === "polygon" ? Hexagon : require_index.Square;
352
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
353
+ className: `flex items-center gap-2 rounded-md border px-2 py-1 transition-colors ${selected ? "border-primary/50 bg-primary/10" : "border-border bg-surface"}`,
354
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
355
+ type: "button",
356
+ onClick: () => selectRegion(r.id),
357
+ disabled: saving,
358
+ className: "flex flex-1 items-center gap-2 text-left text-[11px] font-medium text-foreground-subtle hover:text-foreground disabled:opacity-40 transition-colors",
359
+ children: [
360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShapeIcon, { className: "h-3.5 w-3.5 shrink-0" }),
361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["Zone ", r.id] }),
362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
363
+ className: "text-foreground-faint capitalize",
364
+ children: r.shape.kind
365
+ })
366
+ ]
367
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
368
+ type: "button",
369
+ onClick: () => deleteRegion(r.id),
370
+ disabled: saving,
371
+ "aria-label": `Delete zone ${String(r.id)}`,
372
+ title: "Delete zone",
373
+ className: "inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-surface text-foreground-subtle hover:border-red-400/40 hover:bg-red-500/10 hover:text-red-400 disabled:opacity-40 transition-colors",
374
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.Trash2, { className: "h-3.5 w-3.5" })
375
+ })]
376
+ }, r.id);
377
+ })
378
+ }) : null
379
+ ] })
380
+ })
381
+ });
382
+ }
383
+ //#endregion
384
+ exports.PrivacyMaskSettings = PrivacyMaskSettings;
@@ -118,6 +118,10 @@ export { WidgetSlot } from './widget-slot';
118
118
  export type { WidgetSlotProps } from './widget-slot';
119
119
  export { ScopePicker, validateScopes } from './scope-picker';
120
120
  export type { ScopeAccess } from './scope-picker';
121
- export { PtzPanel, ConsumablesPanel, AutotrackSection, MotionZonesSettings, MaskShapeCanvas, RecordingPanel, } from './cap-settings';
122
- export type { CapSettingsComponentProps, MaskShapeItem, MaskShapeCanvasProps } from './cap-settings';
121
+ export { PtzPanel } from './cap-settings/PtzPanel';
122
+ export { ConsumablesPanel } from './cap-settings/ConsumablesPanel';
123
+ export { AutotrackSection } from './cap-settings/AutotrackSection';
124
+ export { RecordingPanel } from './cap-settings/RecordingPanel';
125
+ export type { CapSettingsComponentProps } from './cap-settings/index';
126
+ export type { MaskShapeItem, MaskShapeCanvasProps } from './cap-settings/MaskShapeCanvas';
123
127
  export * from './widget-panel.js';