@camstack/ui-library 1.2.48 → 1.2.50

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.
package/dist/index.cjs CHANGED
@@ -13119,6 +13119,10 @@ var contentVariants = cva("fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-
13119
13119
  * header, tab strip, footer — stays put at every height. The dialog
13120
13120
  * keeps its own padding either way, so `sheet-on-mobile`'s
13121
13121
  * safe-area bottom padding still applies to the footer.
13122
+ *
13123
+ * `body` also takes a DEFINITE height below `md` — see the
13124
+ * `scroll: 'body'` compound variants. That is not a style choice; the
13125
+ * flex chain does not resolve without it on iOS Safari.
13122
13126
  */
13123
13127
  scroll: {
13124
13128
  content: "overflow-y-auto",
@@ -13144,6 +13148,16 @@ var contentVariants = cva("fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-
13144
13148
  placement: "sheet-on-mobile",
13145
13149
  width: "lg",
13146
13150
  class: "md:w-[calc(100vw-2rem)] md:max-w-lg"
13151
+ },
13152
+ {
13153
+ scroll: "body",
13154
+ placement: "sheet-on-mobile",
13155
+ class: "h-[92dvh] md:h-auto"
13156
+ },
13157
+ {
13158
+ scroll: "body",
13159
+ placement: "center",
13160
+ class: "h-[calc(100dvh-2rem-2*max(env(safe-area-inset-top),env(safe-area-inset-bottom)))] md:h-auto"
13147
13161
  }
13148
13162
  ],
13149
13163
  defaultVariants: {
@@ -13197,6 +13211,12 @@ function DialogHeader({ className, ...props }) {
13197
13211
  * footer out of the dialog instead of scrolling — the exact failure this
13198
13212
  * variant exists to remove. Anything sticky INSIDE a dialog body pins against
13199
13213
  * this element, since it is the scroll container.
13214
+ *
13215
+ * It only bounds a scroll area if every ancestor up to the dialog has a
13216
+ * resolved height. `DialogContent` supplies the top of that chain (see the
13217
+ * `scroll: 'body'` compound variants); an intermediate wrapper between them —
13218
+ * a `<Tabs>` that owns the tab strip, say — must carry `flex-1 min-h-0` too,
13219
+ * or it collapses and takes the body with it.
13200
13220
  */
13201
13221
  function DialogBody({ className, ...props }) {
13202
13222
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -35142,255 +35162,6 @@ function useContextSafe(ctx) {
35142
35162
  return (0, react$1.useContext)(ctx);
35143
35163
  }
35144
35164
  //#endregion
35145
- //#region src/composites/widget-panel.tsx
35146
- function WidgetPanel({ title, icon, headerRight, collapsible = false, defaultOpen = true, bodyClassName = "p-3", children }) {
35147
- const [open, setOpen] = (0, react$1.useState)(defaultOpen);
35148
- const showBody = !collapsible || open;
35149
- const headerInner = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
35150
- className: "flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-foreground",
35151
- children: [icon, title]
35152
- }), collapsible ? open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3.5 w-3.5 text-foreground-subtle" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3.5 w-3.5 text-foreground-subtle" }) : headerRight] });
35153
- const headerClass = `px-4 py-2 flex items-center justify-between ${showBody ? "border-b border-border" : ""}`;
35154
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35155
- className: "rounded-lg border border-border bg-surface overflow-hidden",
35156
- children: [collapsible ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35157
- type: "button",
35158
- onClick: () => setOpen((v) => !v),
35159
- className: `w-full text-left ${headerClass}`,
35160
- children: headerInner
35161
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35162
- className: headerClass,
35163
- children: headerInner
35164
- }), showBody ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35165
- className: bodyClassName,
35166
- children
35167
- }) : null]
35168
- });
35169
- }
35170
- function WidgetMetricCard({ label, icon, children, footer }) {
35171
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35172
- className: "rounded-md border border-border/60 bg-background/40 px-3 py-2",
35173
- children: [
35174
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35175
- className: "mb-1 flex items-center gap-1.5 text-[11px] uppercase tracking-wider text-foreground-subtle",
35176
- children: [icon, label]
35177
- }),
35178
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35179
- className: "text-sm font-medium text-foreground",
35180
- children
35181
- }),
35182
- footer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35183
- className: "mt-0.5 text-[10px] text-foreground-subtle",
35184
- children: footer
35185
- }) : null
35186
- ]
35187
- });
35188
- }
35189
- //#endregion
35190
- //#region src/composites/cap-settings/PtzPanel.tsx
35191
- /**
35192
- * PtzPanel — live pan/tilt/zoom controls for the `ptz` capability. A
35193
- * cap-settings component (ui-library), mounted as a cap-contributed
35194
- * top-level device-detail tab via the cap-UI contribution mechanism
35195
- * (the `ptz` cap declares `ui: { tab: 'ptz', kind: 'static', ... }`).
35196
- *
35197
- * Consolidates the former admin-ui `PTZPanelContent`: `usePTZ` drives
35198
- * the controls, `PTZOverlay` renders them (`mode='panel'`), and an
35199
- * Autofocus toggle is shown only when `getOptions().hasAutofocus`.
35200
- * Autotrack is its own cap-UI contribution (`ptz-autotrack`), mounted
35201
- * independently by the contribution mechanism — not nested here.
35202
- */
35203
- /** Autofocus toggle — only mounted when `hasAutofocus`. Reads the cap's
35204
- * `getStatus().autofocus` and drives `setAutofocus`. */
35205
- function AutofocusToggle({ deviceId }) {
35206
- const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
35207
- const [enabled, setEnabled] = (0, react$1.useState)(null);
35208
- const [busy, setBusy] = (0, react$1.useState)(false);
35209
- (0, react$1.useEffect)(() => {
35210
- if (!dev) return void 0;
35211
- let cancelled = false;
35212
- (async () => {
35213
- try {
35214
- const status = await dev.ptz?.getStatus({});
35215
- if (cancelled || !status) return;
35216
- setEnabled(status.autofocus);
35217
- } catch {}
35218
- })();
35219
- return () => {
35220
- cancelled = true;
35221
- };
35222
- }, [dev]);
35223
- const toggle = (0, react$1.useCallback)(async () => {
35224
- if (!dev?.ptz || enabled === null) return;
35225
- setBusy(true);
35226
- try {
35227
- const next = !enabled;
35228
- await dev.ptz.setAutofocus({ enabled: next });
35229
- setEnabled(next);
35230
- } catch (err) {
35231
- console.error("ptz.setAutofocus failed", err);
35232
- } finally {
35233
- setBusy(false);
35234
- }
35235
- }, [dev, enabled]);
35236
- if (enabled === null) return null;
35237
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35238
- className: "flex items-center justify-between border-t border-border/40 mt-2 pt-2",
35239
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35240
- className: "text-[10.5px] font-semibold text-foreground",
35241
- children: "Autofocus"
35242
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
35243
- type: "button",
35244
- onClick: () => void toggle(),
35245
- disabled: busy,
35246
- "aria-pressed": enabled,
35247
- className: `flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${enabled ? "bg-success/15 text-success hover:bg-success/25" : "bg-surface-elevated/40 text-foreground-subtle hover:bg-surface-hover hover:text-foreground"} disabled:opacity-50`,
35248
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-1.5 rounded-full ${enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), enabled ? "On" : "Off"]
35249
- })]
35250
- });
35251
- }
35252
- function PtzPanel({ deviceId }) {
35253
- const system = useSystem();
35254
- const ready = Number.isFinite(deviceId);
35255
- const controls = usePTZ(system.trpcClient, ready ? deviceId : 0, { enabled: ready });
35256
- if (!ready) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35257
- className: "flex items-center justify-center h-full text-[11px] text-foreground-subtle italic",
35258
- children: [
35259
- "Device #",
35260
- deviceId,
35261
- " not loaded."
35262
- ]
35263
- });
35264
- const hasAutofocus = controls.options?.hasAutofocus === true;
35265
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(WidgetPanel, {
35266
- title: "PTZ",
35267
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Move, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
35268
- bodyClassName: "p-3 flex flex-col overflow-y-auto",
35269
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PTZOverlay, {
35270
- controls,
35271
- mode: "panel"
35272
- }), hasAutofocus && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutofocusToggle, { deviceId })]
35273
- });
35274
- }
35275
- //#endregion
35276
- //#region src/composites/cap-settings/ConsumablesPanel.tsx
35277
- /**
35278
- * ConsumablesPanel — maintenance-items surface for the generic
35279
- * `consumables` capability. A cap-settings component (ui-library),
35280
- * mounted as a cap-contributed top-level device-detail tab via the
35281
- * cap-UI contribution mechanism (the `consumables` cap declares
35282
- * `ui: { kind:'widget', widgetId:'host/consumables-panel', tab:'consumables', topTab:true, ... }`).
35283
- *
35284
- * Reads the cap's pushed runtime-state slice via `useDeviceCapability`
35285
- * and renders one row per item: a fill bar + percentage when `level` is
35286
- * known, else an `ok`/`replace` status pill when `status` is known, else
35287
- * a muted em-dash. A per-item "Replaced" button calls `cap.reset` when
35288
- * the item is `resettable`. The cap is inert until a provider sets
35289
- * items — an empty list renders a muted placeholder.
35290
- */
35291
- /** Remaining-life % at or below this is treated as a warning. */
35292
- var LOW_LEVEL_THRESHOLD = 15;
35293
- function isLowOrReplace(item) {
35294
- return item.status === "replace" || item.level !== null && item.level <= LOW_LEVEL_THRESHOLD;
35295
- }
35296
- /** Format a ms-epoch as a compact local date for the "replaced" hint. */
35297
- function formatResetAt(ms) {
35298
- try {
35299
- return new Date(ms).toLocaleDateString(void 0, {
35300
- year: "numeric",
35301
- month: "short",
35302
- day: "numeric"
35303
- });
35304
- } catch {
35305
- return "";
35306
- }
35307
- }
35308
- function ConsumableRow({ item, busy, canReset, onReset }) {
35309
- const warn = isLowOrReplace(item);
35310
- const barColor = warn ? "bg-warning" : "bg-success";
35311
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35312
- className: `flex flex-col gap-1.5 rounded-md border p-3 ${warn ? "border-warning/40 bg-warning/5" : "border-border/40 bg-surface-elevated/30"}`,
35313
- children: [
35314
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35315
- className: "flex items-center justify-between gap-2",
35316
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35317
- className: `text-xs font-semibold ${warn ? "text-warning" : "text-foreground"}`,
35318
- children: item.label
35319
- }), item.resettable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35320
- type: "button",
35321
- onClick: () => onReset(item.key),
35322
- disabled: busy || !canReset,
35323
- className: "rounded-md border border-border bg-surface px-2 py-0.5 text-[10px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
35324
- children: "Replaced"
35325
- })]
35326
- }),
35327
- item.level !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35328
- className: "flex items-center gap-2",
35329
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35330
- className: "h-1.5 flex-1 overflow-hidden rounded-full bg-surface-elevated/60",
35331
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35332
- className: `h-full rounded-full ${barColor} transition-all`,
35333
- style: { width: `${Math.max(0, Math.min(100, item.level))}%` }
35334
- })
35335
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
35336
- className: "text-[10px] font-medium tabular-nums text-foreground-subtle",
35337
- children: [Math.round(item.level), "%"]
35338
- })]
35339
- }) : item.status !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35340
- className: `inline-flex w-fit items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium ${item.status === "replace" ? "bg-warning/15 text-warning" : "bg-success/15 text-success"}`,
35341
- children: item.status === "replace" ? "Replace" : "OK"
35342
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35343
- className: "text-[10px] text-foreground-subtle",
35344
- children: "—"
35345
- }),
35346
- item.lastResetAt !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
35347
- className: "text-[10px] text-foreground-subtle",
35348
- children: ["Replaced ", formatResetAt(item.lastResetAt)]
35349
- })
35350
- ]
35351
- });
35352
- }
35353
- function ConsumablesPanel({ deviceId }) {
35354
- const system = useSystem();
35355
- const ready = Number.isFinite(deviceId);
35356
- const { cap, slice, available } = useDeviceCapability({
35357
- trpc: system.trpcClient,
35358
- deviceId: ready ? deviceId : null,
35359
- cap: (d) => d.consumables,
35360
- slice: (d) => d.state.consumables
35361
- });
35362
- const [busyKey, setBusyKey] = (0, react$1.useState)(null);
35363
- const onReset = (0, react$1.useCallback)(async (key) => {
35364
- if (!cap) return;
35365
- setBusyKey(key);
35366
- try {
35367
- await cap.reset({ key });
35368
- } catch (err) {
35369
- console.error("consumables.reset failed", err);
35370
- } finally {
35371
- setBusyKey(null);
35372
- }
35373
- }, [cap]);
35374
- const items = slice?.items ?? [];
35375
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetPanel, {
35376
- title: "Consumables",
35377
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Gauge, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
35378
- bodyClassName: "p-3 overflow-y-auto",
35379
- children: items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
35380
- className: "text-[11px] text-foreground-subtle italic leading-relaxed",
35381
- children: "No consumables reported."
35382
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35383
- className: "flex flex-col gap-2",
35384
- children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableRow, {
35385
- item,
35386
- busy: busyKey === item.key,
35387
- canReset: available,
35388
- onReset: (key) => void onReset(key)
35389
- }, item.key))
35390
- })
35391
- });
35392
- }
35393
- //#endregion
35394
35165
  //#region src/hooks/use-device-autotrack.ts
35395
35166
  /**
35396
35167
  * `useDeviceAutotrack` — typed wrapper around the `ptz-autotrack` cap.
@@ -35449,6 +35220,51 @@ function useDeviceAutotrack(deviceId) {
35449
35220
  };
35450
35221
  }
35451
35222
  //#endregion
35223
+ //#region src/composites/widget-panel.tsx
35224
+ function WidgetPanel({ title, icon, headerRight, collapsible = false, defaultOpen = true, bodyClassName = "p-3", children }) {
35225
+ const [open, setOpen] = (0, react$1.useState)(defaultOpen);
35226
+ const showBody = !collapsible || open;
35227
+ const headerInner = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
35228
+ className: "flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-foreground",
35229
+ children: [icon, title]
35230
+ }), collapsible ? open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3.5 w-3.5 text-foreground-subtle" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3.5 w-3.5 text-foreground-subtle" }) : headerRight] });
35231
+ const headerClass = `px-4 py-2 flex items-center justify-between ${showBody ? "border-b border-border" : ""}`;
35232
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35233
+ className: "rounded-lg border border-border bg-surface overflow-hidden",
35234
+ children: [collapsible ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35235
+ type: "button",
35236
+ onClick: () => setOpen((v) => !v),
35237
+ className: `w-full text-left ${headerClass}`,
35238
+ children: headerInner
35239
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35240
+ className: headerClass,
35241
+ children: headerInner
35242
+ }), showBody ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35243
+ className: bodyClassName,
35244
+ children
35245
+ }) : null]
35246
+ });
35247
+ }
35248
+ function WidgetMetricCard({ label, icon, children, footer }) {
35249
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35250
+ className: "rounded-md border border-border/60 bg-background/40 px-3 py-2",
35251
+ children: [
35252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35253
+ className: "mb-1 flex items-center gap-1.5 text-[11px] uppercase tracking-wider text-foreground-subtle",
35254
+ children: [icon, label]
35255
+ }),
35256
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35257
+ className: "text-sm font-medium text-foreground",
35258
+ children
35259
+ }),
35260
+ footer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35261
+ className: "mt-0.5 text-[10px] text-foreground-subtle",
35262
+ children: footer
35263
+ }) : null
35264
+ ]
35265
+ });
35266
+ }
35267
+ //#endregion
35452
35268
  //#region src/composites/cap-settings/AutotrackSection.tsx
35453
35269
  /**
35454
35270
  * Autotrack settings card — rendered inside the PTZ panel when the
@@ -35601,6 +35417,210 @@ function AutotrackSection({ deviceId }) {
35601
35417
  });
35602
35418
  }
35603
35419
  //#endregion
35420
+ //#region src/composites/cap-settings/ConsumablesPanel.tsx
35421
+ /**
35422
+ * ConsumablesPanel — maintenance-items surface for the generic
35423
+ * `consumables` capability. A cap-settings component (ui-library),
35424
+ * mounted as a cap-contributed top-level device-detail tab via the
35425
+ * cap-UI contribution mechanism (the `consumables` cap declares
35426
+ * `ui: { kind:'widget', widgetId:'host/consumables-panel', tab:'consumables', topTab:true, ... }`).
35427
+ *
35428
+ * Reads the cap's pushed runtime-state slice via `useDeviceCapability`
35429
+ * and renders one row per item: a fill bar + percentage when `level` is
35430
+ * known, else an `ok`/`replace` status pill when `status` is known, else
35431
+ * a muted em-dash. A per-item "Replaced" button calls `cap.reset` when
35432
+ * the item is `resettable`. The cap is inert until a provider sets
35433
+ * items — an empty list renders a muted placeholder.
35434
+ */
35435
+ /** Remaining-life % at or below this is treated as a warning. */
35436
+ var LOW_LEVEL_THRESHOLD = 15;
35437
+ function isLowOrReplace(item) {
35438
+ return item.status === "replace" || item.level !== null && item.level <= LOW_LEVEL_THRESHOLD;
35439
+ }
35440
+ /** Format a ms-epoch as a compact local date for the "replaced" hint. */
35441
+ function formatResetAt(ms) {
35442
+ try {
35443
+ return new Date(ms).toLocaleDateString(void 0, {
35444
+ year: "numeric",
35445
+ month: "short",
35446
+ day: "numeric"
35447
+ });
35448
+ } catch {
35449
+ return "";
35450
+ }
35451
+ }
35452
+ function ConsumableRow({ item, busy, canReset, onReset }) {
35453
+ const warn = isLowOrReplace(item);
35454
+ const barColor = warn ? "bg-warning" : "bg-success";
35455
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35456
+ className: `flex flex-col gap-1.5 rounded-md border p-3 ${warn ? "border-warning/40 bg-warning/5" : "border-border/40 bg-surface-elevated/30"}`,
35457
+ children: [
35458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35459
+ className: "flex items-center justify-between gap-2",
35460
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35461
+ className: `text-xs font-semibold ${warn ? "text-warning" : "text-foreground"}`,
35462
+ children: item.label
35463
+ }), item.resettable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35464
+ type: "button",
35465
+ onClick: () => onReset(item.key),
35466
+ disabled: busy || !canReset,
35467
+ className: "rounded-md border border-border bg-surface px-2 py-0.5 text-[10px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
35468
+ children: "Replaced"
35469
+ })]
35470
+ }),
35471
+ item.level !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35472
+ className: "flex items-center gap-2",
35473
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35474
+ className: "h-1.5 flex-1 overflow-hidden rounded-full bg-surface-elevated/60",
35475
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35476
+ className: `h-full rounded-full ${barColor} transition-all`,
35477
+ style: { width: `${Math.max(0, Math.min(100, item.level))}%` }
35478
+ })
35479
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
35480
+ className: "text-[10px] font-medium tabular-nums text-foreground-subtle",
35481
+ children: [Math.round(item.level), "%"]
35482
+ })]
35483
+ }) : item.status !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35484
+ className: `inline-flex w-fit items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium ${item.status === "replace" ? "bg-warning/15 text-warning" : "bg-success/15 text-success"}`,
35485
+ children: item.status === "replace" ? "Replace" : "OK"
35486
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35487
+ className: "text-[10px] text-foreground-subtle",
35488
+ children: "—"
35489
+ }),
35490
+ item.lastResetAt !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
35491
+ className: "text-[10px] text-foreground-subtle",
35492
+ children: ["Replaced ", formatResetAt(item.lastResetAt)]
35493
+ })
35494
+ ]
35495
+ });
35496
+ }
35497
+ function ConsumablesPanel({ deviceId }) {
35498
+ const system = useSystem();
35499
+ const ready = Number.isFinite(deviceId);
35500
+ const { cap, slice, available } = useDeviceCapability({
35501
+ trpc: system.trpcClient,
35502
+ deviceId: ready ? deviceId : null,
35503
+ cap: (d) => d.consumables,
35504
+ slice: (d) => d.state.consumables
35505
+ });
35506
+ const [busyKey, setBusyKey] = (0, react$1.useState)(null);
35507
+ const onReset = (0, react$1.useCallback)(async (key) => {
35508
+ if (!cap) return;
35509
+ setBusyKey(key);
35510
+ try {
35511
+ await cap.reset({ key });
35512
+ } catch (err) {
35513
+ console.error("consumables.reset failed", err);
35514
+ } finally {
35515
+ setBusyKey(null);
35516
+ }
35517
+ }, [cap]);
35518
+ const items = slice?.items ?? [];
35519
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetPanel, {
35520
+ title: "Consumables",
35521
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Gauge, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
35522
+ bodyClassName: "p-3 overflow-y-auto",
35523
+ children: items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
35524
+ className: "text-[11px] text-foreground-subtle italic leading-relaxed",
35525
+ children: "No consumables reported."
35526
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35527
+ className: "flex flex-col gap-2",
35528
+ children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConsumableRow, {
35529
+ item,
35530
+ busy: busyKey === item.key,
35531
+ canReset: available,
35532
+ onReset: (key) => void onReset(key)
35533
+ }, item.key))
35534
+ })
35535
+ });
35536
+ }
35537
+ //#endregion
35538
+ //#region src/composites/cap-settings/PtzPanel.tsx
35539
+ /**
35540
+ * PtzPanel — live pan/tilt/zoom controls for the `ptz` capability. A
35541
+ * cap-settings component (ui-library), mounted as a cap-contributed
35542
+ * top-level device-detail tab via the cap-UI contribution mechanism
35543
+ * (the `ptz` cap declares `ui: { tab: 'ptz', kind: 'static', ... }`).
35544
+ *
35545
+ * Consolidates the former admin-ui `PTZPanelContent`: `usePTZ` drives
35546
+ * the controls, `PTZOverlay` renders them (`mode='panel'`), and an
35547
+ * Autofocus toggle is shown only when `getOptions().hasAutofocus`.
35548
+ * Autotrack is its own cap-UI contribution (`ptz-autotrack`), mounted
35549
+ * independently by the contribution mechanism — not nested here.
35550
+ */
35551
+ /** Autofocus toggle — only mounted when `hasAutofocus`. Reads the cap's
35552
+ * `getStatus().autofocus` and drives `setAutofocus`. */
35553
+ function AutofocusToggle({ deviceId }) {
35554
+ const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
35555
+ const [enabled, setEnabled] = (0, react$1.useState)(null);
35556
+ const [busy, setBusy] = (0, react$1.useState)(false);
35557
+ (0, react$1.useEffect)(() => {
35558
+ if (!dev) return void 0;
35559
+ let cancelled = false;
35560
+ (async () => {
35561
+ try {
35562
+ const status = await dev.ptz?.getStatus({});
35563
+ if (cancelled || !status) return;
35564
+ setEnabled(status.autofocus);
35565
+ } catch {}
35566
+ })();
35567
+ return () => {
35568
+ cancelled = true;
35569
+ };
35570
+ }, [dev]);
35571
+ const toggle = (0, react$1.useCallback)(async () => {
35572
+ if (!dev?.ptz || enabled === null) return;
35573
+ setBusy(true);
35574
+ try {
35575
+ const next = !enabled;
35576
+ await dev.ptz.setAutofocus({ enabled: next });
35577
+ setEnabled(next);
35578
+ } catch (err) {
35579
+ console.error("ptz.setAutofocus failed", err);
35580
+ } finally {
35581
+ setBusy(false);
35582
+ }
35583
+ }, [dev, enabled]);
35584
+ if (enabled === null) return null;
35585
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35586
+ className: "flex items-center justify-between border-t border-border/40 mt-2 pt-2",
35587
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35588
+ className: "text-[10.5px] font-semibold text-foreground",
35589
+ children: "Autofocus"
35590
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
35591
+ type: "button",
35592
+ onClick: () => void toggle(),
35593
+ disabled: busy,
35594
+ "aria-pressed": enabled,
35595
+ className: `flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${enabled ? "bg-success/15 text-success hover:bg-success/25" : "bg-surface-elevated/40 text-foreground-subtle hover:bg-surface-hover hover:text-foreground"} disabled:opacity-50`,
35596
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-1.5 rounded-full ${enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), enabled ? "On" : "Off"]
35597
+ })]
35598
+ });
35599
+ }
35600
+ function PtzPanel({ deviceId }) {
35601
+ const system = useSystem();
35602
+ const ready = Number.isFinite(deviceId);
35603
+ const controls = usePTZ(system.trpcClient, ready ? deviceId : 0, { enabled: ready });
35604
+ if (!ready) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35605
+ className: "flex items-center justify-center h-full text-[11px] text-foreground-subtle italic",
35606
+ children: [
35607
+ "Device #",
35608
+ deviceId,
35609
+ " not loaded."
35610
+ ]
35611
+ });
35612
+ const hasAutofocus = controls.options?.hasAutofocus === true;
35613
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(WidgetPanel, {
35614
+ title: "PTZ",
35615
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Move, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
35616
+ bodyClassName: "p-3 flex flex-col overflow-y-auto",
35617
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PTZOverlay, {
35618
+ controls,
35619
+ mode: "panel"
35620
+ }), hasAutofocus && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutofocusToggle, { deviceId })]
35621
+ });
35622
+ }
35623
+ //#endregion
35604
35624
  //#region src/composites/cap-settings/recording-config-form.ts
35605
35625
  /** Every weekday — the `days` of a band that covers the whole week. */
35606
35626
  var ALL_WEEKDAYS = [
@@ -37507,12 +37527,17 @@ function RecordingPanel({ deviceId }) {
37507
37527
  })]
37508
37528
  });
37509
37529
  }
37530
+ //#endregion
37531
+ //#region src/widgets/host-widgets.ts
37532
+ var MotionZonesSettingsLazy = (0, react$1.lazy)(() => Promise.resolve().then(() => require("./MotionZonesSettings-BP9p0pFB.cjs")).then((m) => ({ default: m.MotionZonesSettings })));
37533
+ var SceneMonitorEditorLazy = (0, react$1.lazy)(() => Promise.resolve().then(() => require("./SceneMonitorEditor-BXwXLLch.cjs")).then((m) => ({ default: m.SceneMonitorEditor })));
37510
37534
  /** Host widgets — ui-library React components embeddable as a
37511
37535
  * `type:'widget'` ConfigField. `WidgetSlot` resolves these BEFORE the
37512
37536
  * Module-Federation `WidgetRegistry`. ids are namespaced `host/<name>`. */
37513
37537
  var HOST_WIDGETS = {
37514
- "host/motion-zones-grid": (0, react$1.lazy)(() => Promise.resolve().then(() => require("./MotionZonesSettings-BP9p0pFB.cjs")).then((m) => ({ default: m.MotionZonesSettings }))),
37538
+ "host/motion-zones-grid": MotionZonesSettingsLazy,
37515
37539
  "host/privacy-mask-grid": (0, react$1.lazy)(() => Promise.resolve().then(() => require("./PrivacyMaskSettings-Oo7gyB4E.cjs")).then((m) => ({ default: m.PrivacyMaskSettings }))),
37540
+ "host/scene-monitor-editor": SceneMonitorEditorLazy,
37516
37541
  "host/ptz-panel": PtzPanel,
37517
37542
  "host/consumables-panel": ConsumablesPanel,
37518
37543
  "host/ptz-autotrack": AutotrackSection,
@@ -48562,6 +48587,7 @@ exports.COMMIT_DEDUPE_TOLERANCE_MS = COMMIT_DEDUPE_TOLERANCE_MS;
48562
48587
  exports.COMMIT_DEDUPE_WINDOW_MS = COMMIT_DEDUPE_WINDOW_MS;
48563
48588
  exports.CONTROL_CAP_NAMES = CONTROL_CAP_NAMES;
48564
48589
  exports.CONTROL_FILLS = CONTROL_FILLS;
48590
+ exports.Camera = Camera;
48565
48591
  exports.CameraStreamPlayer = CameraStreamPlayer;
48566
48592
  exports.Card = Card;
48567
48593
  exports.Checkbox = Checkbox;
@@ -48713,6 +48739,7 @@ exports.RadialGauge = RadialGauge;
48713
48739
  exports.RecordedPlaybackProvider = RecordedPlaybackProvider;
48714
48740
  exports.RecordingPanel = RecordingPanel;
48715
48741
  exports.ResponseLog = ResponseLog;
48742
+ exports.RotateCcw = RotateCcw;
48716
48743
  exports.SECTION_BODY = SECTION_BODY;
48717
48744
  exports.SECTION_CARD = SECTION_CARD;
48718
48745
  exports.SECTION_HEADER = SECTION_HEADER;
@@ -49193,12 +49220,14 @@ exports.useDeviceProxy = useDeviceProxy;
49193
49220
  exports.useDeviceProxy$1 = useDeviceProxy;
49194
49221
  exports.useDeviceSnapshot = useDeviceSnapshot;
49195
49222
  exports.useDeviceSnapshotImage = useDeviceSnapshotImage;
49223
+ exports.useDeviceSnapshotImage$1 = useDeviceSnapshotImage;
49196
49224
  exports.useDeviceState = useDeviceState;
49197
49225
  exports.useDeviceStateGetAllSnapshots = useDeviceStateGetAllSnapshots;
49198
49226
  exports.useDeviceStateGetCapSlice = useDeviceStateGetCapSlice;
49199
49227
  exports.useDeviceStateGetSnapshot = useDeviceStateGetSnapshot;
49200
49228
  exports.useDeviceStateSetCapSlice = useDeviceStateSetCapSlice;
49201
49229
  exports.useDeviceStateSlice = useDeviceStateSlice;
49230
+ exports.useDeviceStateSlice$1 = useDeviceStateSlice;
49202
49231
  exports.useDeviceStatusGetStatus = useDeviceStatusGetStatus;
49203
49232
  exports.useDeviceWebrtc = useDeviceWebrtc;
49204
49233
  exports.useDevices = useDevices;