@camstack/ui-library 1.2.163 → 1.2.165
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/composites/confirm-dialog.d.ts +52 -7
- package/dist/generated/system-hooks.d.ts +4 -0
- package/dist/hooks/use-stationary-overlay.d.ts +22 -2
- package/dist/index.cjs +344 -79
- package/dist/index.js +340 -80
- package/package.json +2 -2
|
@@ -1,13 +1,58 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
export type DialogVariant = 'danger' | 'warning' | 'default';
|
|
3
|
+
export interface ConfirmOptions {
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly confirmLabel?: string;
|
|
7
|
+
readonly cancelLabel?: string;
|
|
8
|
+
readonly variant?: DialogVariant;
|
|
9
|
+
}
|
|
10
|
+
export interface AlertOptions {
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly acknowledgeLabel?: string;
|
|
14
|
+
readonly variant?: DialogVariant;
|
|
15
|
+
}
|
|
16
|
+
export type PromptInputType = 'text' | 'password';
|
|
17
|
+
export interface PromptOptions {
|
|
18
|
+
readonly title: string;
|
|
19
|
+
/** Explanatory paragraph above the field. */
|
|
20
|
+
readonly message?: string;
|
|
21
|
+
/** Label rendered against the input. Defaults to the title. */
|
|
22
|
+
readonly label?: string;
|
|
23
|
+
readonly defaultValue?: string;
|
|
24
|
+
/** `password` masks the value and opts out of autofill/remember. */
|
|
25
|
+
readonly inputType?: PromptInputType;
|
|
26
|
+
readonly placeholder?: string;
|
|
27
|
+
readonly confirmLabel?: string;
|
|
28
|
+
readonly cancelLabel?: string;
|
|
29
|
+
/**
|
|
30
|
+
* A field the operator reads/copies rather than edits (the clipboard
|
|
31
|
+
* fallback). Rendered unmasked, selected on open, with no validation.
|
|
32
|
+
*/
|
|
33
|
+
readonly readOnly?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Returns an error message to REFUSE the value and keep the dialog open, or
|
|
36
|
+
* `null` to accept. Runs on submit — the dialog is the validation surface, so
|
|
37
|
+
* a refusal never needs a second `alert`.
|
|
38
|
+
*/
|
|
39
|
+
readonly validate?: (value: string) => string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface DialogContextValue {
|
|
42
|
+
readonly confirm: (options: ConfirmOptions) => Promise<boolean>;
|
|
43
|
+
readonly alert: (options: AlertOptions) => Promise<void>;
|
|
44
|
+
readonly prompt: (options: PromptOptions) => Promise<string | null>;
|
|
8
45
|
}
|
|
9
46
|
export declare function useConfirm(): (options: ConfirmOptions) => Promise<boolean>;
|
|
47
|
+
/** An error/notice with a single acknowledge. Replaces `window.alert`. */
|
|
48
|
+
export declare function useAlert(): (options: AlertOptions) => Promise<void>;
|
|
49
|
+
/** One text input with OK/Cancel. Replaces `window.prompt`. */
|
|
50
|
+
export declare function usePrompt(): (options: PromptOptions) => Promise<string | null>;
|
|
10
51
|
export declare function ConfirmDialogProvider({ children }: {
|
|
11
52
|
children: ReactNode;
|
|
12
53
|
}): import("react").JSX.Element;
|
|
13
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Promotes `el` to the top layer. jsdom has `<dialog>` but no `showModal`, so
|
|
56
|
+
* the attribute is the fallback — see the file docblock.
|
|
57
|
+
*/
|
|
58
|
+
export declare function openTopLayer(el: HTMLDialogElement): void;
|
|
@@ -1039,6 +1039,8 @@ export declare const useNotificationRulesUpdateRule: typeof trpc.notificationRul
|
|
|
1039
1039
|
export declare const useNotificationRulesDeleteRule: typeof trpc.notificationRules.deleteRule.useMutation;
|
|
1040
1040
|
/** Generated alias around `trpc.notificationRules.setRuleEnabled.useMutation`. */
|
|
1041
1041
|
export declare const useNotificationRulesSetRuleEnabled: typeof trpc.notificationRules.setRuleEnabled.useMutation;
|
|
1042
|
+
/** Generated alias around `trpc.notificationRules.getClipRetentionAsks.useQuery`. */
|
|
1043
|
+
export declare const useNotificationRulesGetClipRetentionAsks: typeof trpc.notificationRules.getClipRetentionAsks.useQuery;
|
|
1042
1044
|
/** Generated alias around `trpc.notificationRules.listDeviceMutes.useQuery`. */
|
|
1043
1045
|
export declare const useNotificationRulesListDeviceMutes: typeof trpc.notificationRules.listDeviceMutes.useQuery;
|
|
1044
1046
|
/** Generated alias around `trpc.notificationRules.setDeviceMuted.useMutation`. */
|
|
@@ -1443,6 +1445,8 @@ export declare const usePipelineRunnerGetCameraMetrics: typeof trpc.pipelineRunn
|
|
|
1443
1445
|
export declare const usePipelineRunnerGetAllCameraMetrics: typeof trpc.pipelineRunner.getAllCameraMetrics.useQuery;
|
|
1444
1446
|
/** Generated alias around `trpc.pipelineRunner.getLocalCameras.useQuery`. */
|
|
1445
1447
|
export declare const usePipelineRunnerGetLocalCameras: typeof trpc.pipelineRunner.getLocalCameras.useQuery;
|
|
1448
|
+
/** Generated alias around `trpc.pipelineRunner.requestOccupancyBurst.useMutation`. */
|
|
1449
|
+
export declare const usePipelineRunnerRequestOccupancyBurst: typeof trpc.pipelineRunner.requestOccupancyBurst.useMutation;
|
|
1446
1450
|
/** Generated alias around `trpc.pipelineRunner.getNativeCrop.useQuery`. */
|
|
1447
1451
|
export declare const usePipelineRunnerGetNativeCrop: typeof trpc.pipelineRunner.getNativeCrop.useQuery;
|
|
1448
1452
|
/** Generated alias around `trpc.pipelineRunner.runDetailSubtree.useMutation`. */
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { StationaryOverlayBox } from '@camstack/types';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The ONE route this hook needs.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately NOT the device proxy. The proxy exposes only the caps BOUND to
|
|
6
|
+
* a device, and `zone-analytics` is not one of them — on device 617 the
|
|
7
|
+
* bindings are `pipeline-analytics` and `motion-zones`. Reading the census
|
|
8
|
+
* through `dev?.zoneAnalytics?.…` therefore resolved to `undefined`, the hook
|
|
9
|
+
* returned an empty census, and the overlay drew nothing while reporting
|
|
10
|
+
* nothing: an unreachable cap was indistinguishable from an empty lot. The cap
|
|
11
|
+
* ROUTE is mounted system-wide and takes the `deviceId` as input, which is how
|
|
12
|
+
* every other caller reaches it.
|
|
13
|
+
*/
|
|
14
|
+
export interface UseStationaryOverlayTrpc {
|
|
15
|
+
readonly zoneAnalytics: {
|
|
16
|
+
readonly getCurrentSnapshot: {
|
|
17
|
+
readonly query: (input: {
|
|
18
|
+
deviceId: number;
|
|
19
|
+
}) => Promise<unknown>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
3
23
|
interface SnapshotStationary {
|
|
4
24
|
readonly id: string;
|
|
5
25
|
readonly className: string;
|
|
@@ -22,5 +42,5 @@ interface SnapshotStationary {
|
|
|
22
42
|
* reads as a real detection in the wrong place.
|
|
23
43
|
*/
|
|
24
44
|
export declare function toStationaryOverlayBox(row: SnapshotStationary): StationaryOverlayBox | null;
|
|
25
|
-
export declare function useStationaryOverlay(trpc:
|
|
45
|
+
export declare function useStationaryOverlay(trpc: UseStationaryOverlayTrpc, deviceId: number | null): readonly StationaryOverlayBox[] | undefined;
|
|
26
46
|
export {};
|
package/dist/index.cjs
CHANGED
|
@@ -6091,6 +6091,8 @@ var useNotificationRulesUpdateRule = trpc.notificationRules.updateRule.useMutati
|
|
|
6091
6091
|
var useNotificationRulesDeleteRule = trpc.notificationRules.deleteRule.useMutation;
|
|
6092
6092
|
/** Generated alias around `trpc.notificationRules.setRuleEnabled.useMutation`. */
|
|
6093
6093
|
var useNotificationRulesSetRuleEnabled = trpc.notificationRules.setRuleEnabled.useMutation;
|
|
6094
|
+
/** Generated alias around `trpc.notificationRules.getClipRetentionAsks.useQuery`. */
|
|
6095
|
+
var useNotificationRulesGetClipRetentionAsks = trpc.notificationRules.getClipRetentionAsks.useQuery;
|
|
6094
6096
|
/** Generated alias around `trpc.notificationRules.listDeviceMutes.useQuery`. */
|
|
6095
6097
|
var useNotificationRulesListDeviceMutes = trpc.notificationRules.listDeviceMutes.useQuery;
|
|
6096
6098
|
/** Generated alias around `trpc.notificationRules.setDeviceMuted.useMutation`. */
|
|
@@ -6495,6 +6497,8 @@ var usePipelineRunnerGetCameraMetrics = trpc.pipelineRunner.getCameraMetrics.use
|
|
|
6495
6497
|
var usePipelineRunnerGetAllCameraMetrics = trpc.pipelineRunner.getAllCameraMetrics.useQuery;
|
|
6496
6498
|
/** Generated alias around `trpc.pipelineRunner.getLocalCameras.useQuery`. */
|
|
6497
6499
|
var usePipelineRunnerGetLocalCameras = trpc.pipelineRunner.getLocalCameras.useQuery;
|
|
6500
|
+
/** Generated alias around `trpc.pipelineRunner.requestOccupancyBurst.useMutation`. */
|
|
6501
|
+
var usePipelineRunnerRequestOccupancyBurst = trpc.pipelineRunner.requestOccupancyBurst.useMutation;
|
|
6498
6502
|
/** Generated alias around `trpc.pipelineRunner.getNativeCrop.useQuery`. */
|
|
6499
6503
|
var usePipelineRunnerGetNativeCrop = trpc.pipelineRunner.getNativeCrop.useQuery;
|
|
6500
6504
|
/** Generated alias around `trpc.pipelineRunner.runDetailSubtree.useMutation`. */
|
|
@@ -11269,82 +11273,340 @@ function EventKindGlyph({ iconId, size = 16, color, className }) {
|
|
|
11269
11273
|
}
|
|
11270
11274
|
//#endregion
|
|
11271
11275
|
//#region src/composites/confirm-dialog.tsx
|
|
11276
|
+
/**
|
|
11277
|
+
* The ONE modal dialog of the product — confirm, alert and prompt.
|
|
11278
|
+
*
|
|
11279
|
+
* ## Why all three live here
|
|
11280
|
+
*
|
|
11281
|
+
* The admin UI used to reach for `window.confirm` / `window.alert` /
|
|
11282
|
+
* `window.prompt` wherever a `useConfirm()` call was inconvenient. A native
|
|
11283
|
+
* dialog is wrong on three counts, and the operator hit the worst one on
|
|
11284
|
+
* Identity → Users: resetting a password opened `window.prompt`, which
|
|
11285
|
+
* **cannot mask its input**. A new password was typed in clear text, into a
|
|
11286
|
+
* box the browser may offer to remember. On top of that a native dialog
|
|
11287
|
+
* matches none of the product's visual language and the host can neither style
|
|
11288
|
+
* nor test it.
|
|
11289
|
+
*
|
|
11290
|
+
* So the fix is not "a password modal" — a second dialog component is the
|
|
11291
|
+
* defect, not the cure. The provider that already served every confirm in the
|
|
11292
|
+
* admin UI (and in the viewer, which documents its own copy as "deliberately
|
|
11293
|
+
* the same one `@camstack/ui-library` exposes to the admin UI") grew two more
|
|
11294
|
+
* intents. One provider, one backdrop, one visual language, one set of
|
|
11295
|
+
* keyboard rules.
|
|
11296
|
+
*
|
|
11297
|
+
* ## The three intents are NOT one control
|
|
11298
|
+
*
|
|
11299
|
+
* - `confirm` — a destructive yes/no. It names what is destroyed, and for the
|
|
11300
|
+
* `danger` variant the destructive button is the NON-default: opening focus
|
|
11301
|
+
* lands on Cancel, so a reflexive Enter cancels.
|
|
11302
|
+
* - `alert` — an error/notice with a single acknowledge.
|
|
11303
|
+
* - `prompt` — one text input with OK/Cancel. `inputType: 'password'` masks it
|
|
11304
|
+
* (and opts out of autofill/remember); `readOnly` turns it into a readable,
|
|
11305
|
+
* select-all-on-open field, which is the shape the copy-to-clipboard
|
|
11306
|
+
* fallback needs — a password box would be actively wrong there.
|
|
11307
|
+
*
|
|
11308
|
+
* ## Why the surface is a native `<dialog>`
|
|
11309
|
+
*
|
|
11310
|
+
* These dialogs open OVER surfaces that are themselves native `<dialog>`s
|
|
11311
|
+
* opened with `showModal()` — the event detail panel, the similar-groups
|
|
11312
|
+
* modal. A `showModal()` dialog is promoted to the TOP LAYER and everything
|
|
11313
|
+
* outside it is inert, so a plain `fixed inset-0` overlay rendered at the app
|
|
11314
|
+
* root would be drawn behind that backdrop and could not be clicked at all.
|
|
11315
|
+
* Confirming a track delete from the detail panel would have been impossible.
|
|
11316
|
+
* So this surface is a `<dialog>` of its own: the top layer stacks, the newest
|
|
11317
|
+
* `showModal()` wins, and the confirm sits above the panel that asked for it.
|
|
11318
|
+
*
|
|
11319
|
+
* jsdom implements `<dialog>` but not `showModal()`, so the open falls back to
|
|
11320
|
+
* the `open` attribute there. Everything this component does — focus, keys,
|
|
11321
|
+
* resolution — is identical on both paths; only the top-layer promotion, which
|
|
11322
|
+
* jsdom cannot model at all, is missing. The fallback is asserted below.
|
|
11323
|
+
*
|
|
11324
|
+
* ## Focus, and why there is NO focus trap
|
|
11325
|
+
*
|
|
11326
|
+
* A `showModal()` dialog already scopes the tab ring to itself, and the
|
|
11327
|
+
* surfaces underneath (`SlideOverPanel`) implement no trap of their own. A
|
|
11328
|
+
* second, hand-rolled focus trap on top of that is how a page becomes
|
|
11329
|
+
* unescapable, so this component deliberately does not trap. What it does do:
|
|
11330
|
+
*
|
|
11331
|
+
* - focuses the input (prompt) or the DEFAULT button on open,
|
|
11332
|
+
* - restores focus to the previously focused element on close,
|
|
11333
|
+
* - handles Escape/Enter from a `keydown` on the dialog itself and calls
|
|
11334
|
+
* `stopPropagation()`. React attaches its listeners at the root container, so
|
|
11335
|
+
* stopping there keeps the event from reaching the `window` Escape listener
|
|
11336
|
+
* of whatever modal is underneath: one Escape closes ONE dialog.
|
|
11337
|
+
*/
|
|
11272
11338
|
var ConfirmContext = createSharedContext("camstack:confirm-dialog", null);
|
|
11273
|
-
function
|
|
11339
|
+
function useDialogContext$1() {
|
|
11274
11340
|
const ctx = (0, react$1.useContext)(ConfirmContext);
|
|
11275
11341
|
if (!ctx) throw new Error("useConfirm must be used within ConfirmDialogProvider");
|
|
11276
|
-
return ctx
|
|
11342
|
+
return ctx;
|
|
11343
|
+
}
|
|
11344
|
+
function useConfirm() {
|
|
11345
|
+
return useDialogContext$1().confirm;
|
|
11346
|
+
}
|
|
11347
|
+
/** An error/notice with a single acknowledge. Replaces `window.alert`. */
|
|
11348
|
+
function useAlert() {
|
|
11349
|
+
return useDialogContext$1().alert;
|
|
11350
|
+
}
|
|
11351
|
+
/** One text input with OK/Cancel. Replaces `window.prompt`. */
|
|
11352
|
+
function usePrompt() {
|
|
11353
|
+
return useDialogContext$1().prompt;
|
|
11354
|
+
}
|
|
11355
|
+
var VARIANT_STYLES$2 = {
|
|
11356
|
+
danger: { button: "bg-red-500 hover:bg-red-600 text-white" },
|
|
11357
|
+
warning: { button: "bg-orange-500 hover:bg-orange-600 text-white" },
|
|
11358
|
+
default: { button: "bg-primary hover:bg-primary/90 text-white" }
|
|
11359
|
+
};
|
|
11360
|
+
var CANCEL_CLASS = "px-3 py-1.5 text-xs font-medium rounded-md border border-border text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors";
|
|
11361
|
+
function variantOf(state) {
|
|
11362
|
+
if (state.kind === "prompt") return "default";
|
|
11363
|
+
return state.options.variant ?? "default";
|
|
11364
|
+
}
|
|
11365
|
+
/**
|
|
11366
|
+
* Whether the affirmative button is the DEFAULT (initially focused) control.
|
|
11367
|
+
* A `danger` confirm says no: the destructive action must be deliberate.
|
|
11368
|
+
*/
|
|
11369
|
+
function affirmativeIsDefault(state) {
|
|
11370
|
+
return variantOf(state) !== "danger";
|
|
11277
11371
|
}
|
|
11278
11372
|
function ConfirmDialogProvider({ children }) {
|
|
11279
11373
|
const [dialog, setDialog] = (0, react$1.useState)(null);
|
|
11280
|
-
const confirm = (0, react$1.useCallback)((options) => {
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11285
|
-
|
|
11374
|
+
const confirm = (0, react$1.useCallback)((options) => new Promise((resolve) => {
|
|
11375
|
+
setDialog({
|
|
11376
|
+
kind: "confirm",
|
|
11377
|
+
options,
|
|
11378
|
+
resolve
|
|
11379
|
+
});
|
|
11380
|
+
}), []);
|
|
11381
|
+
const alert = (0, react$1.useCallback)((options) => new Promise((resolve) => {
|
|
11382
|
+
setDialog({
|
|
11383
|
+
kind: "alert",
|
|
11384
|
+
options,
|
|
11385
|
+
resolve: () => resolve()
|
|
11286
11386
|
});
|
|
11387
|
+
}), []);
|
|
11388
|
+
const prompt = (0, react$1.useCallback)((options) => new Promise((resolve) => {
|
|
11389
|
+
setDialog({
|
|
11390
|
+
kind: "prompt",
|
|
11391
|
+
options,
|
|
11392
|
+
resolve
|
|
11393
|
+
});
|
|
11394
|
+
}), []);
|
|
11395
|
+
const close = (0, react$1.useCallback)(() => setDialog(null), []);
|
|
11396
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ConfirmContext.Provider, {
|
|
11397
|
+
value: {
|
|
11398
|
+
confirm,
|
|
11399
|
+
alert,
|
|
11400
|
+
prompt
|
|
11401
|
+
},
|
|
11402
|
+
children: [children, dialog && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogSurface, {
|
|
11403
|
+
state: dialog,
|
|
11404
|
+
onClose: close
|
|
11405
|
+
}, dialogKey(dialog))]
|
|
11406
|
+
});
|
|
11407
|
+
}
|
|
11408
|
+
/**
|
|
11409
|
+
* A key that changes per OPENED dialog, so a second dialog opened while one is
|
|
11410
|
+
* showing remounts the surface (fresh input value, fresh focus) instead of
|
|
11411
|
+
* inheriting the previous one's state.
|
|
11412
|
+
*/
|
|
11413
|
+
function dialogKey(state) {
|
|
11414
|
+
return `${state.kind}:${state.options.title}`;
|
|
11415
|
+
}
|
|
11416
|
+
/**
|
|
11417
|
+
* Promotes `el` to the top layer. jsdom has `<dialog>` but no `showModal`, so
|
|
11418
|
+
* the attribute is the fallback — see the file docblock.
|
|
11419
|
+
*/
|
|
11420
|
+
function openTopLayer(el) {
|
|
11421
|
+
if (typeof el.showModal === "function") el.showModal();
|
|
11422
|
+
else el.setAttribute("open", "");
|
|
11423
|
+
}
|
|
11424
|
+
function closeTopLayer(el) {
|
|
11425
|
+
if (!el.open) return;
|
|
11426
|
+
if (typeof el.close === "function") el.close();
|
|
11427
|
+
else el.removeAttribute("open");
|
|
11428
|
+
}
|
|
11429
|
+
function DialogSurface({ state, onClose }) {
|
|
11430
|
+
const dialogRef = (0, react$1.useRef)(null);
|
|
11431
|
+
const inputRef = (0, react$1.useRef)(null);
|
|
11432
|
+
const defaultButtonRef = (0, react$1.useRef)(null);
|
|
11433
|
+
const settled = (0, react$1.useRef)(false);
|
|
11434
|
+
const [value, setValue] = (0, react$1.useState)(state.kind === "prompt" ? state.options.defaultValue ?? "" : "");
|
|
11435
|
+
const [error, setError] = (0, react$1.useState)(null);
|
|
11436
|
+
(0, react$1.useEffect)(() => {
|
|
11437
|
+
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
11438
|
+
const el = dialogRef.current;
|
|
11439
|
+
if (el) openTopLayer(el);
|
|
11440
|
+
const input = inputRef.current;
|
|
11441
|
+
if (input) {
|
|
11442
|
+
input.focus();
|
|
11443
|
+
if (state.kind === "prompt" && state.options.readOnly === true) input.select();
|
|
11444
|
+
} else defaultButtonRef.current?.focus();
|
|
11445
|
+
return () => {
|
|
11446
|
+
if (el) closeTopLayer(el);
|
|
11447
|
+
previous?.focus();
|
|
11448
|
+
};
|
|
11287
11449
|
}, []);
|
|
11288
|
-
const
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
11294
|
-
|
|
11295
|
-
};
|
|
11296
|
-
const
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11450
|
+
const cancel = (0, react$1.useCallback)(() => {
|
|
11451
|
+
if (settled.current) return;
|
|
11452
|
+
settled.current = true;
|
|
11453
|
+
if (state.kind === "confirm") state.resolve(false);
|
|
11454
|
+
else if (state.kind === "prompt") state.resolve(null);
|
|
11455
|
+
else state.resolve();
|
|
11456
|
+
onClose();
|
|
11457
|
+
}, [state, onClose]);
|
|
11458
|
+
const accept = (0, react$1.useCallback)(() => {
|
|
11459
|
+
if (settled.current) return;
|
|
11460
|
+
if (state.kind === "confirm") {
|
|
11461
|
+
settled.current = true;
|
|
11462
|
+
state.resolve(true);
|
|
11463
|
+
onClose();
|
|
11464
|
+
return;
|
|
11465
|
+
}
|
|
11466
|
+
if (state.kind === "alert") {
|
|
11467
|
+
settled.current = true;
|
|
11468
|
+
state.resolve();
|
|
11469
|
+
onClose();
|
|
11470
|
+
return;
|
|
11471
|
+
}
|
|
11472
|
+
if (state.options.readOnly !== true) {
|
|
11473
|
+
const problem = state.options.validate?.(value) ?? null;
|
|
11474
|
+
if (problem !== null) {
|
|
11475
|
+
setError(problem);
|
|
11476
|
+
inputRef.current?.focus();
|
|
11477
|
+
return;
|
|
11478
|
+
}
|
|
11479
|
+
}
|
|
11480
|
+
settled.current = true;
|
|
11481
|
+
state.resolve(state.options.readOnly === true ? null : value);
|
|
11482
|
+
onClose();
|
|
11483
|
+
}, [
|
|
11484
|
+
state,
|
|
11485
|
+
onClose,
|
|
11486
|
+
value
|
|
11487
|
+
]);
|
|
11488
|
+
const onKeyDown = (0, react$1.useCallback)((e) => {
|
|
11489
|
+
if (e.key === "Escape") {
|
|
11490
|
+
e.stopPropagation();
|
|
11491
|
+
e.preventDefault();
|
|
11492
|
+
cancel();
|
|
11493
|
+
return;
|
|
11494
|
+
}
|
|
11495
|
+
if (e.key !== "Enter") return;
|
|
11496
|
+
if (e.target === inputRef.current) return;
|
|
11497
|
+
if (!affirmativeIsDefault(state)) return;
|
|
11498
|
+
e.stopPropagation();
|
|
11499
|
+
e.preventDefault();
|
|
11500
|
+
accept();
|
|
11501
|
+
}, [
|
|
11502
|
+
state,
|
|
11503
|
+
cancel,
|
|
11504
|
+
accept
|
|
11505
|
+
]);
|
|
11506
|
+
const onSubmit = (0, react$1.useCallback)((e) => {
|
|
11507
|
+
e.preventDefault();
|
|
11508
|
+
e.stopPropagation();
|
|
11509
|
+
accept();
|
|
11510
|
+
}, [accept]);
|
|
11511
|
+
const style = VARIANT_STYLES$2[variantOf(state)];
|
|
11512
|
+
const titleId = "camstack-dialog-title";
|
|
11513
|
+
const body = state.kind === "prompt" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PromptBody, {
|
|
11514
|
+
options: state.options,
|
|
11515
|
+
value,
|
|
11516
|
+
error,
|
|
11517
|
+
inputRef,
|
|
11518
|
+
onChange: (next) => {
|
|
11519
|
+
setValue(next);
|
|
11520
|
+
setError(null);
|
|
11521
|
+
}
|
|
11522
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11523
|
+
className: "mt-2 text-xs text-foreground-subtle leading-relaxed",
|
|
11524
|
+
children: state.options.message
|
|
11525
|
+
});
|
|
11526
|
+
const footer = state.kind === "alert" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11527
|
+
type: "button",
|
|
11528
|
+
ref: defaultButtonRef,
|
|
11529
|
+
onClick: accept,
|
|
11530
|
+
className: `px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${style.button}`,
|
|
11531
|
+
children: state.options.acknowledgeLabel ?? "OK"
|
|
11532
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11533
|
+
type: "button",
|
|
11534
|
+
ref: affirmativeIsDefault(state) ? null : defaultButtonRef,
|
|
11535
|
+
onClick: cancel,
|
|
11536
|
+
className: CANCEL_CLASS,
|
|
11537
|
+
children: state.options.cancelLabel ?? "Cancel"
|
|
11538
|
+
}), !(state.kind === "prompt" && state.options.readOnly === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11539
|
+
type: state.kind === "prompt" ? "submit" : "button",
|
|
11540
|
+
ref: affirmativeIsDefault(state) ? defaultButtonRef : null,
|
|
11541
|
+
onClick: state.kind === "prompt" ? void 0 : accept,
|
|
11542
|
+
className: `px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${style.button}`,
|
|
11543
|
+
children: state.options.confirmLabel ?? (state.kind === "prompt" ? "OK" : "Confirm")
|
|
11544
|
+
})] });
|
|
11545
|
+
const inner = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11546
|
+
className: "px-5 pt-5 pb-4",
|
|
11547
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
11548
|
+
id: titleId,
|
|
11549
|
+
className: "text-sm font-semibold text-foreground",
|
|
11550
|
+
children: state.options.title
|
|
11551
|
+
}), body]
|
|
11552
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
11553
|
+
className: "flex justify-end gap-2 px-5 py-3 border-t border-border bg-surface-hover/30",
|
|
11554
|
+
children: footer
|
|
11555
|
+
})] });
|
|
11556
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
|
|
11557
|
+
ref: dialogRef,
|
|
11558
|
+
"aria-modal": "true",
|
|
11559
|
+
"aria-labelledby": titleId,
|
|
11560
|
+
onKeyDown,
|
|
11561
|
+
onCancel: (e) => {
|
|
11562
|
+
e.preventDefault();
|
|
11563
|
+
cancel();
|
|
11300
11564
|
},
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
button: "bg-orange-500 hover:bg-orange-600 text-white"
|
|
11565
|
+
onClick: (e) => {
|
|
11566
|
+
if (e.target === e.currentTarget) cancel();
|
|
11304
11567
|
},
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
const style = dialog ? variantStyles[dialog.variant ?? "default"] : variantStyles.default;
|
|
11311
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ConfirmContext.Provider, {
|
|
11312
|
-
value: { confirm },
|
|
11313
|
-
children: [children, dialog && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11314
|
-
className: "fixed inset-0 z-50 flex items-center justify-center",
|
|
11315
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
11316
|
-
className: "absolute inset-0 bg-black/50 backdrop-blur-sm",
|
|
11317
|
-
onClick: handleCancel
|
|
11318
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11319
|
-
className: "relative z-10 w-full max-w-sm mx-4 rounded-xl border border-border bg-surface shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150",
|
|
11320
|
-
onClick: (e) => e.stopPropagation(),
|
|
11321
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11322
|
-
className: "px-5 pt-5 pb-4",
|
|
11323
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
11324
|
-
className: "text-sm font-semibold text-foreground",
|
|
11325
|
-
children: dialog.title
|
|
11326
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11327
|
-
className: "mt-2 text-xs text-foreground-subtle leading-relaxed",
|
|
11328
|
-
children: dialog.message
|
|
11329
|
-
})]
|
|
11330
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11331
|
-
className: "flex justify-end gap-2 px-5 py-3 border-t border-border bg-surface-hover/30",
|
|
11332
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11333
|
-
type: "button",
|
|
11334
|
-
onClick: handleCancel,
|
|
11335
|
-
className: "px-3 py-1.5 text-xs font-medium rounded-md border border-border text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors",
|
|
11336
|
-
children: dialog.cancelLabel ?? "Cancel"
|
|
11337
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11338
|
-
type: "button",
|
|
11339
|
-
onClick: handleConfirm,
|
|
11340
|
-
className: `px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${style.button}`,
|
|
11341
|
-
children: dialog.confirmLabel ?? "Confirm"
|
|
11342
|
-
})]
|
|
11343
|
-
})]
|
|
11344
|
-
})]
|
|
11345
|
-
})]
|
|
11568
|
+
className: "fixed left-1/2 top-1/2 z-50 m-0 w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-xl border border-border bg-surface p-0 text-foreground shadow-2xl backdrop:bg-black/50 backdrop:backdrop-blur-sm [&:not([open])]:hidden",
|
|
11569
|
+
children: state.kind === "prompt" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("form", {
|
|
11570
|
+
onSubmit,
|
|
11571
|
+
children: inner
|
|
11572
|
+
}) : inner
|
|
11346
11573
|
});
|
|
11347
11574
|
}
|
|
11575
|
+
function PromptBody({ options, value, error, inputRef, onChange }) {
|
|
11576
|
+
const readOnly = options.readOnly === true;
|
|
11577
|
+
const isPassword = !readOnly && options.inputType === "password";
|
|
11578
|
+
const inputId = "camstack-dialog-input";
|
|
11579
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
11580
|
+
options.message !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11581
|
+
className: "mt-2 text-xs text-foreground-subtle leading-relaxed",
|
|
11582
|
+
children: options.message
|
|
11583
|
+
}),
|
|
11584
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
11585
|
+
htmlFor: inputId,
|
|
11586
|
+
className: "mt-3 block text-[11px] font-medium text-foreground-subtle",
|
|
11587
|
+
children: options.label ?? options.title
|
|
11588
|
+
}),
|
|
11589
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
11590
|
+
id: inputId,
|
|
11591
|
+
ref: inputRef,
|
|
11592
|
+
type: isPassword ? "password" : "text",
|
|
11593
|
+
value,
|
|
11594
|
+
readOnly,
|
|
11595
|
+
placeholder: options.placeholder,
|
|
11596
|
+
onChange: (e) => onChange(e.target.value),
|
|
11597
|
+
onFocus: readOnly ? (e) => e.currentTarget.select() : void 0,
|
|
11598
|
+
autoComplete: isPassword ? "new-password" : "off",
|
|
11599
|
+
autoCorrect: "off",
|
|
11600
|
+
autoCapitalize: "off",
|
|
11601
|
+
spellCheck: false,
|
|
11602
|
+
className: "mt-1 w-full rounded-md border border-border bg-surface px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary"
|
|
11603
|
+
}),
|
|
11604
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11605
|
+
className: "mt-1.5 text-[11px] text-red-400",
|
|
11606
|
+
children: error
|
|
11607
|
+
})
|
|
11608
|
+
] });
|
|
11609
|
+
}
|
|
11348
11610
|
//#endregion
|
|
11349
11611
|
//#region src/contexts/device-context.tsx
|
|
11350
11612
|
/**
|
|
@@ -27977,13 +28239,18 @@ function StationaryBoxes({ boxes }) {
|
|
|
27977
28239
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: boxes.map((b) => {
|
|
27978
28240
|
const color = getClassColor(b.className);
|
|
27979
28241
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
27980
|
-
|
|
28242
|
+
"data-census-box": "",
|
|
28243
|
+
className: "absolute",
|
|
27981
28244
|
style: {
|
|
27982
28245
|
left: `${String(b.x1 * 100)}%`,
|
|
27983
28246
|
top: `${String(b.y1 * 100)}%`,
|
|
27984
28247
|
width: `${String((b.x2 - b.x1) * 100)}%`,
|
|
27985
28248
|
height: `${String((b.y2 - b.y1) * 100)}%`,
|
|
27986
|
-
borderColor: color
|
|
28249
|
+
borderColor: color,
|
|
28250
|
+
borderWidth: _camstack_types.STATIONARY_OVERLAY_STYLE.borderWidth,
|
|
28251
|
+
borderStyle: _camstack_types.STATIONARY_OVERLAY_STYLE.dashed ? "dashed" : "solid",
|
|
28252
|
+
borderRadius: _camstack_types.STATIONARY_OVERLAY_STYLE.borderRadius,
|
|
28253
|
+
opacity: _camstack_types.STATIONARY_OVERLAY_STYLE.opacity
|
|
27987
28254
|
},
|
|
27988
28255
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
27989
28256
|
className: "absolute -top-5 left-0 px-1 text-[10px] font-medium rounded-sm whitespace-nowrap",
|
|
@@ -27994,19 +28261,12 @@ function StationaryBoxes({ boxes }) {
|
|
|
27994
28261
|
children: [
|
|
27995
28262
|
b.label ?? b.className,
|
|
27996
28263
|
" · ",
|
|
27997
|
-
formatParkedFor(now - b.becameStationaryAt)
|
|
28264
|
+
(0, _camstack_types.formatParkedFor)(now - b.becameStationaryAt)
|
|
27998
28265
|
]
|
|
27999
28266
|
})
|
|
28000
28267
|
}, b.id);
|
|
28001
28268
|
}) });
|
|
28002
28269
|
}
|
|
28003
|
-
/** "3 h", "12 min", "45 s" — coarse on purpose: nobody reads a parked car to
|
|
28004
|
-
* the second, and a ticking label redraws the overlay for nothing. */
|
|
28005
|
-
function formatParkedFor(ms) {
|
|
28006
|
-
if (ms < 6e4) return `${String(Math.max(0, Math.round(ms / 1e3)))} s`;
|
|
28007
|
-
if (ms < 36e5) return `${String(Math.round(ms / 6e4))} min`;
|
|
28008
|
-
return `${String(Math.round(ms / 36e5))} h`;
|
|
28009
|
-
}
|
|
28010
28270
|
function DetectionBoxes({ detections, frameWidth, frameHeight }) {
|
|
28011
28271
|
if (frameWidth === 0 || frameHeight === 0) return null;
|
|
28012
28272
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: detections.map((d) => {
|
|
@@ -49767,18 +50027,17 @@ function toStationaryOverlayBox(row) {
|
|
|
49767
50027
|
};
|
|
49768
50028
|
}
|
|
49769
50029
|
function useStationaryOverlay(trpc, deviceId) {
|
|
49770
|
-
const
|
|
49771
|
-
return (0, _tanstack_react_query.useQuery)({
|
|
50030
|
+
const query = (0, _tanstack_react_query.useQuery)({
|
|
49772
50031
|
queryKey: [
|
|
49773
50032
|
"zone-analytics",
|
|
49774
50033
|
deviceId,
|
|
49775
50034
|
"stationaryOverlay"
|
|
49776
50035
|
],
|
|
49777
|
-
enabled: deviceId !== null
|
|
50036
|
+
enabled: deviceId !== null,
|
|
49778
50037
|
refetchInterval: STATIONARY_POLL_MS,
|
|
49779
50038
|
staleTime: STATIONARY_POLL_MS / 2,
|
|
49780
50039
|
queryFn: async () => {
|
|
49781
|
-
const rows = (await
|
|
50040
|
+
const rows = (await trpc.zoneAnalytics.getCurrentSnapshot.query({ deviceId }))?.stationaryObjects ?? [];
|
|
49782
50041
|
const out = [];
|
|
49783
50042
|
for (const r of rows) {
|
|
49784
50043
|
const box = toStationaryOverlayBox(r);
|
|
@@ -49786,7 +50045,8 @@ function useStationaryOverlay(trpc, deviceId) {
|
|
|
49786
50045
|
}
|
|
49787
50046
|
return out;
|
|
49788
50047
|
}
|
|
49789
|
-
})
|
|
50048
|
+
});
|
|
50049
|
+
return query.isError ? void 0 : query.data;
|
|
49790
50050
|
}
|
|
49791
50051
|
//#endregion
|
|
49792
50052
|
//#region src/hooks/turn-server-cache.ts
|
|
@@ -50798,6 +51058,7 @@ exports.nextSort = nextSort;
|
|
|
50798
51058
|
exports.nextSortDirection = nextSortDirection;
|
|
50799
51059
|
exports.nextTableSort = nextTableSort;
|
|
50800
51060
|
exports.normalizeForSearch = normalizeForSearch;
|
|
51061
|
+
exports.openTopLayer = openTopLayer;
|
|
50801
51062
|
exports.overrideEntityIdFromLink = overrideEntityIdFromLink;
|
|
50802
51063
|
exports.parseRecordedServerMessage = parseRecordedServerMessage;
|
|
50803
51064
|
exports.providerIcons = providerIcons;
|
|
@@ -50882,6 +51143,7 @@ exports.useAlarmPanelArm = useAlarmPanelArm;
|
|
|
50882
51143
|
exports.useAlarmPanelDisarm = useAlarmPanelDisarm;
|
|
50883
51144
|
exports.useAlarmPanelGetStatus = useAlarmPanelGetStatus;
|
|
50884
51145
|
exports.useAlarmPanelTrigger = useAlarmPanelTrigger;
|
|
51146
|
+
exports.useAlert = useAlert;
|
|
50885
51147
|
exports.useAlertsDismiss = useAlertsDismiss;
|
|
50886
51148
|
exports.useAlertsEmit = useAlertsEmit;
|
|
50887
51149
|
exports.useAlertsGetUnreadCount = useAlertsGetUnreadCount;
|
|
@@ -51390,6 +51652,7 @@ exports.useNotificationRulesCreateRule = useNotificationRulesCreateRule;
|
|
|
51390
51652
|
exports.useNotificationRulesCreateSnooze = useNotificationRulesCreateSnooze;
|
|
51391
51653
|
exports.useNotificationRulesDeleteRule = useNotificationRulesDeleteRule;
|
|
51392
51654
|
exports.useNotificationRulesGetAlarmConfig = useNotificationRulesGetAlarmConfig;
|
|
51655
|
+
exports.useNotificationRulesGetClipRetentionAsks = useNotificationRulesGetClipRetentionAsks;
|
|
51393
51656
|
exports.useNotificationRulesGetConditionCatalog = useNotificationRulesGetConditionCatalog;
|
|
51394
51657
|
exports.useNotificationRulesGetHistory = useNotificationRulesGetHistory;
|
|
51395
51658
|
exports.useNotificationRulesGetRule = useNotificationRulesGetRule;
|
|
@@ -51601,6 +51864,7 @@ exports.usePipelineRunnerGetParkedTrackFrame = usePipelineRunnerGetParkedTrackFr
|
|
|
51601
51864
|
exports.usePipelineRunnerParkTrackFrame = usePipelineRunnerParkTrackFrame;
|
|
51602
51865
|
exports.usePipelineRunnerReleaseParkedTrackFrames = usePipelineRunnerReleaseParkedTrackFrames;
|
|
51603
51866
|
exports.usePipelineRunnerReportMotion = usePipelineRunnerReportMotion;
|
|
51867
|
+
exports.usePipelineRunnerRequestOccupancyBurst = usePipelineRunnerRequestOccupancyBurst;
|
|
51604
51868
|
exports.usePipelineRunnerRunDetailSubtree = usePipelineRunnerRunDetailSubtree;
|
|
51605
51869
|
exports.usePipelineRunnerRunStatelessStep = usePipelineRunnerRunStatelessStep;
|
|
51606
51870
|
exports.usePlateGalleryAssignPlate = usePlateGalleryAssignPlate;
|
|
@@ -51631,6 +51895,7 @@ exports.usePrivacyMaskGetOptions = usePrivacyMaskGetOptions;
|
|
|
51631
51895
|
exports.usePrivacyMaskGetStatus = usePrivacyMaskGetStatus;
|
|
51632
51896
|
exports.usePrivacyMaskSetAudioEnabled = usePrivacyMaskSetAudioEnabled;
|
|
51633
51897
|
exports.usePrivacyMaskSetMask = usePrivacyMaskSetMask;
|
|
51898
|
+
exports.usePrompt = usePrompt;
|
|
51634
51899
|
exports.usePtzAutotrackGetSettings = usePtzAutotrackGetSettings;
|
|
51635
51900
|
exports.usePtzAutotrackGetStatus = usePtzAutotrackGetStatus;
|
|
51636
51901
|
exports.usePtzAutotrackSetEnabled = usePtzAutotrackSetEnabled;
|