@camstack/ui-library 1.2.265 → 1.2.267

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.
@@ -16,8 +16,10 @@ export type { BatteryBadgeProps, BatteryStatusLike } from "./battery-badge";
16
16
  export { BatteryBadge } from "./battery-badge";
17
17
  export type { NetworkLinkBadgeProps, NetworkLinkStatusLike } from "./network-link-badge";
18
18
  export { NetworkLinkBadge } from "./network-link-badge";
19
- export { NODE_COLOR_IDS, assignNodeColorId, nodeColorClass, nodeTextColorClass } from "./node-color";
19
+ export { NODE_COLOR_IDS, assignNodeColorId, nodeColorClass, nodeSoftBgClass, nodeTextColorClass } from "./node-color";
20
20
  export type { NodeColorId } from "./node-color";
21
+ export type { AgentSettingsColorRow } from "./node-color-map";
22
+ export { buildColorByNodeId, colorIdForNode } from "./node-color-map";
21
23
  export type { PipelineRoleId, RoleNodePillProps } from "./role-node-pill";
22
24
  export { RoleNodePill } from "./role-node-pill";
23
25
  export * from "./breadcrumb";
@@ -157,6 +159,11 @@ export { mountAddonPage } from "./mount-addon-page";
157
159
  export type { NodePickerProps } from "./node-picker";
158
160
  export { NodePicker } from "./node-picker";
159
161
  export * from "./page-header";
162
+ export * from "./page-shell";
163
+ export * from "./section-toolbar";
164
+ export * from "./item-form-dialog";
165
+ export * from "./use-item-form-url";
166
+ export * from "./media-grid";
160
167
  export type { PhaseIconProps } from "./phase-icon";
161
168
  export { PhaseIcon } from "./phase-icon";
162
169
  export type { PipelineBuilderProps } from "./pipeline-builder";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ItemFormDialog — shared shell for create/edit of a named list item.
3
+ *
4
+ * Mirrors Notification Center Rules: `Dialog` + `DialogContent` width lg,
5
+ * sheet-on-mobile, scroll body; title + Cancel in the chrome; body and footer
6
+ * slots for the form. Open state is owned by the caller (typically
7
+ * `useItemFormUrl`).
8
+ */
9
+ import type { LucideIcon } from "lucide-react";
10
+ import type { ReactNode, JSX } from "react";
11
+ import { type DialogContentProps } from "../primitives";
12
+ export interface ItemFormDialogProps {
13
+ readonly open: boolean;
14
+ readonly onClose: () => void;
15
+ readonly title: string;
16
+ readonly icon?: LucideIcon;
17
+ readonly error?: string | null;
18
+ readonly width?: DialogContentProps["width"];
19
+ readonly children: ReactNode;
20
+ /** Footer actions (Save, …). Cancel is always in the header. */
21
+ readonly footer?: ReactNode;
22
+ readonly testId?: string;
23
+ }
24
+ export declare function ItemFormDialog({ open, onClose, title, icon: Icon, error, width, children, footer, testId }: ItemFormDialogProps): JSX.Element | null;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * MediaGrid — responsive grid of media tiles (events, frames, crops).
3
+ * Empty and pending are distinct; never fold unanswered into empty (D315).
4
+ */
5
+ import type { ReactNode, JSX } from "react";
6
+ export interface MediaCardProps {
7
+ readonly children: ReactNode;
8
+ readonly selected?: boolean;
9
+ readonly onSelect?: () => void;
10
+ readonly label?: string;
11
+ readonly className?: string;
12
+ readonly testId?: string;
13
+ }
14
+ export declare function MediaCard({ children, selected, onSelect, label, className, testId }: MediaCardProps): JSX.Element;
15
+ export type MediaGridStatus = "pending" | "ready" | "empty";
16
+ export interface MediaGridProps {
17
+ readonly status: MediaGridStatus;
18
+ /** Required when status is `ready`. */
19
+ readonly children?: ReactNode;
20
+ /** Shown when status is pending. */
21
+ readonly pendingMessage?: string;
22
+ /** Shown when status is empty. */
23
+ readonly emptyMessage?: string;
24
+ readonly className?: string;
25
+ readonly testId?: string;
26
+ }
27
+ export declare function MediaGrid({ status, children, pendingMessage, emptyMessage, className, testId }: MediaGridProps): JSX.Element;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * nodeId → palette slot for assignment pills, NodePicker and other cluster surfaces.
3
+ * Reads persisted `color` from listAgentSettings; falls back to assignNodeColorId.
4
+ */
5
+ import { type NodeColorId } from "@camstack/types";
6
+ export interface AgentSettingsColorRow {
7
+ readonly nodeId: string;
8
+ readonly settings: object;
9
+ }
10
+ export declare function buildColorByNodeId(rows: readonly AgentSettingsColorRow[] | undefined): ReadonlyMap<string, NodeColorId>;
11
+ export declare function colorIdForNode(colorByNodeId: ReadonlyMap<string, NodeColorId>, nodeId: string): NodeColorId;
@@ -2,3 +2,4 @@ import { NODE_COLOR_IDS, assignNodeColorId, type NodeColorId } from "@camstack/t
2
2
  export { NODE_COLOR_IDS, assignNodeColorId, type NodeColorId };
3
3
  export declare function nodeColorClass(id: NodeColorId): string;
4
4
  export declare function nodeTextColorClass(id: NodeColorId): string;
5
+ export declare function nodeSoftBgClass(id: NodeColorId): string;
@@ -12,5 +12,7 @@ export interface NodePickerProps {
12
12
  * default hides the picker since there's nothing to choose.
13
13
  */
14
14
  readonly alwaysShow?: boolean;
15
+ /** When true, offline nodes stay enabled and selectable. Default false. */
16
+ readonly allowOffline?: boolean;
15
17
  }
16
- export declare function NodePicker({ selectedNodeId, onSelect, label, hideIcon, alwaysShow }: NodePickerProps): JSX.Element | null;
18
+ export declare function NodePicker({ selectedNodeId, onSelect, label, hideIcon, alwaysShow, allowOffline }: NodePickerProps): JSX.Element | null;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @deprecated Prefer `PageShell` for the full page frame (gutter + optional nav).
3
+ * Kept for title-row-only call sites that already pad themselves.
4
+ */
1
5
  import { type ReactNode, type JSX } from "react";
2
6
  export interface PageHeaderProps {
3
7
  title: string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * PageShell — the one page chrome for admin-ui and addon pages.
3
+ *
4
+ * Gutter `p-2 sm:p-4`, `overflow-x-clip` (never `hidden` — that kills sticky
5
+ * descendants). Optional title row (icon + title + subtitle | actions) and a
6
+ * `nav` slot under it for hub pills / selectors.
7
+ */
8
+ import type { LucideIcon } from "lucide-react";
9
+ import type { ReactNode, JSX } from "react";
10
+ export interface PageShellProps {
11
+ readonly icon?: LucideIcon;
12
+ readonly title?: string;
13
+ readonly subtitle?: ReactNode;
14
+ readonly actions?: ReactNode;
15
+ /** Selector row inside the header, under the title (hub pills, …). */
16
+ readonly nav?: ReactNode;
17
+ readonly className?: string;
18
+ readonly children: ReactNode;
19
+ }
20
+ export declare function PageShell({ icon: Icon, title, subtitle, actions, nav, className, children }: PageShellProps): JSX.Element;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * SectionToolbar — filters / node on the left, Add (or other actions) on the
3
+ * right, inside a SectionCard body. Owns no data.
4
+ */
5
+ import type { ReactNode, JSX } from "react";
6
+ export interface SectionToolbarProps {
7
+ /** Filters, NodePicker, search, … */
8
+ readonly start?: ReactNode;
9
+ /** Add button or other primary actions. */
10
+ readonly end?: ReactNode;
11
+ readonly className?: string;
12
+ readonly testId?: string;
13
+ }
14
+ export declare function SectionToolbar({ start, end, className, testId }: SectionToolbarProps): JSX.Element;
@@ -0,0 +1,21 @@
1
+ export type ItemFormUrlState = {
2
+ readonly mode: "closed";
3
+ } | {
4
+ readonly mode: "new";
5
+ } | {
6
+ readonly mode: "edit";
7
+ readonly id: string;
8
+ };
9
+ export interface UseItemFormUrlOptions {
10
+ /** Query key for create (default `new`). Value is the literal `1` when open. */
11
+ readonly newKey?: string;
12
+ /** Query key for edit id (default `edit`). */
13
+ readonly editKey?: string;
14
+ }
15
+ export interface UseItemFormUrlResult {
16
+ readonly state: ItemFormUrlState;
17
+ readonly openNew: () => void;
18
+ readonly openEdit: (id: string) => void;
19
+ readonly close: () => void;
20
+ }
21
+ export declare function useItemFormUrl(options?: UseItemFormUrlOptions): UseItemFormUrlResult;
package/dist/index.cjs CHANGED
@@ -19723,18 +19723,21 @@ function WidgetPanel({ title, icon, headerRight, collapsible = false, defaultOpe
19723
19723
  }
19724
19724
  function WidgetMetricCard({ label, icon, children, footer }) {
19725
19725
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19726
- className: "rounded-md border border-border/60 bg-background/40 px-3 py-2",
19726
+ className: "min-w-0 rounded-lg border border-border bg-surface p-2 sm:p-2.5",
19727
19727
  children: [
19728
19728
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
19729
- className: "mb-1 flex items-center gap-1.5 text-[11px] uppercase tracking-wider text-foreground-subtle",
19730
- children: [icon, label]
19729
+ className: "mb-0.5 flex min-w-0 items-center gap-1.5 text-[10px] uppercase tracking-wider text-foreground-subtle",
19730
+ children: [icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
19731
+ className: "min-w-0 truncate",
19732
+ children: label
19733
+ })]
19731
19734
  }),
19732
19735
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
19733
- className: "text-sm font-medium text-foreground",
19736
+ className: "truncate text-lg font-semibold leading-tight text-foreground",
19734
19737
  children
19735
19738
  }),
19736
- footer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
19737
- className: "mt-0.5 text-[10px] text-foreground-subtle",
19739
+ footer != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
19740
+ className: "mt-0.5 truncate text-[10px] text-foreground-subtle",
19738
19741
  children: footer
19739
19742
  }) : null
19740
19743
  ]
@@ -25753,7 +25756,9 @@ var contentVariants = cva("relative fixed left-1/2 top-1/2 -translate-x-1/2 -tra
25753
25756
  width: {
25754
25757
  sm: "max-w-sm",
25755
25758
  md: "max-w-md",
25756
- lg: "max-w-lg"
25759
+ lg: "max-w-lg",
25760
+ /** Wide editors (scripts, long forms). */
25761
+ xl: "max-w-4xl"
25757
25762
  },
25758
25763
  /**
25759
25764
  * WHICH box scrolls when the content is taller than the height cap.
@@ -25800,6 +25805,11 @@ var contentVariants = cva("relative fixed left-1/2 top-1/2 -translate-x-1/2 -tra
25800
25805
  width: "lg",
25801
25806
  class: "md:w-[calc(100vw-2rem)] md:max-w-lg"
25802
25807
  },
25808
+ {
25809
+ placement: "sheet-on-mobile",
25810
+ width: "xl",
25811
+ class: "md:w-[calc(100vw-2rem)] md:max-w-4xl"
25812
+ },
25803
25813
  {
25804
25814
  scroll: "body",
25805
25815
  placement: "sheet-on-mobile",
@@ -30918,6 +30928,8 @@ var GRID_GAP = "gap-2 sm:gap-3";
30918
30928
  var LIST_ROW = "px-3 sm:px-4 py-1.5 sm:py-2";
30919
30929
  /** 1-up at L1, 2-up at L2, 4-up at L3 — quick-stat row. */
30920
30930
  var GRID_QUICK_STATS = "grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4";
30931
+ /** Auto-fill media tiles (events, frames, crops) — ~4 across at lg. */
30932
+ var MEDIA_GRID = "grid grid-cols-[repeat(auto-fill,minmax(11rem,1fr))]";
30921
30933
  /** 1-up at L1, 2-up at L2+ — paired panels (motion + detection rules). */
30922
30934
  var GRID_PAIRED = "grid grid-cols-1 lg:grid-cols-2";
30923
30935
  /** Side-by-side layout that stacks below md (canvas + side panel). */
@@ -32829,12 +32841,46 @@ var NODE_TEXT_CLASS = {
32829
32841
  "node-g": "text-node-g",
32830
32842
  "node-h": "text-node-h"
32831
32843
  };
32844
+ /** Soft fill for selected pills / chrome (Tailwind v4 needs full class strings). */
32845
+ var NODE_BG_SOFT_CLASS$1 = {
32846
+ "node-a": "bg-node-a/15",
32847
+ "node-b": "bg-node-b/15",
32848
+ "node-c": "bg-node-c/15",
32849
+ "node-d": "bg-node-d/15",
32850
+ "node-e": "bg-node-e/15",
32851
+ "node-f": "bg-node-f/15",
32852
+ "node-g": "bg-node-g/15",
32853
+ "node-h": "bg-node-h/15"
32854
+ };
32832
32855
  function nodeColorClass(id) {
32833
32856
  return NODE_BG_CLASS[id];
32834
32857
  }
32835
32858
  function nodeTextColorClass(id) {
32836
32859
  return NODE_TEXT_CLASS[id];
32837
32860
  }
32861
+ function nodeSoftBgClass(id) {
32862
+ return NODE_BG_SOFT_CLASS$1[id];
32863
+ }
32864
+ //#endregion
32865
+ //#region src/composites/node-color-map.ts
32866
+ /**
32867
+ * nodeId → palette slot for assignment pills, NodePicker and other cluster surfaces.
32868
+ * Reads persisted `color` from listAgentSettings; falls back to assignNodeColorId.
32869
+ */
32870
+ function readSettingsColor(settings) {
32871
+ for (const [key, value] of Object.entries(settings)) if (key === "color") return value;
32872
+ }
32873
+ function buildColorByNodeId(rows) {
32874
+ const map = /* @__PURE__ */ new Map();
32875
+ for (const row of rows ?? []) {
32876
+ const parsed = _camstack_types.NodeColorIdSchema.safeParse(readSettingsColor(row.settings));
32877
+ map.set(row.nodeId, parsed.success ? parsed.data : (0, _camstack_types.assignNodeColorId)(row.nodeId));
32878
+ }
32879
+ return map;
32880
+ }
32881
+ function colorIdForNode(colorByNodeId, nodeId) {
32882
+ return colorByNodeId.get(nodeId) ?? (0, _camstack_types.assignNodeColorId)(nodeId);
32883
+ }
32838
32884
  //#endregion
32839
32885
  //#region src/composites/role-node-pill.tsx
32840
32886
  var ROLE_SOFT_CLASS = {
@@ -58072,12 +58118,15 @@ function mountAddonPage(PageComponent, options = {}) {
58072
58118
  *
58073
58119
  * Uses `useClusterNodes` hook so every consumer hits the same React-Query
58074
58120
  * cache. Renders a `Server` icon + label + segmented pill bar with one
58075
- * button per online node, hub first.
58121
+ * button per node (hub first), tinted from agent-settings palette colours.
58122
+ * Offline nodes stay listed; they are disabled unless `allowOffline`.
58076
58123
  *
58077
58124
  * Used by admin-ui Addons page, benchmark addon page, pipeline page, etc.
58078
58125
  */
58079
- function NodePicker({ selectedNodeId, onSelect, label = "Node", hideIcon = false, alwaysShow = false }) {
58126
+ function NodePicker({ selectedNodeId, onSelect, label = "Node", hideIcon = false, alwaysShow = false, allowOffline = false }) {
58080
58127
  const { nodes } = useClusterNodes();
58128
+ const settingsQuery = usePipelineOrchestratorListAgentSettings(void 0);
58129
+ const colorByNodeId = (0, react$1.useMemo)(() => buildColorByNodeId(settingsQuery.data), [settingsQuery.data]);
58081
58130
  if (!alwaysShow && nodes.length <= 1) return null;
58082
58131
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58083
58132
  className: "flex items-center gap-1.5",
@@ -58091,12 +58140,14 @@ function NodePicker({ selectedNodeId, onSelect, label = "Node", hideIcon = false
58091
58140
  className: "flex rounded-md border border-border overflow-hidden text-[11px] font-medium",
58092
58141
  children: nodes.map((node, idx) => {
58093
58142
  const isSelected = selectedNodeId === node.id;
58143
+ const colorId = colorIdForNode(colorByNodeId, node.id);
58144
+ const disabled = !allowOffline && !node.isOnline;
58094
58145
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
58095
58146
  type: "button",
58096
58147
  onClick: () => onSelect(node.id),
58097
- disabled: !node.isOnline,
58148
+ disabled,
58098
58149
  title: node.isOnline ? node.name : `${node.name} (offline)`,
58099
- className: cn("px-2.5 py-1 transition-colors", isSelected ? "bg-primary/15 text-primary" : "bg-transparent text-foreground-subtle hover:bg-surface-hover hover:text-foreground", idx > 0 && "border-l border-border", !node.isOnline && "opacity-50 cursor-not-allowed"),
58150
+ className: cn("px-2.5 py-1 transition-colors", isSelected ? cn(nodeSoftBgClass(colorId), nodeTextColorClass(colorId)) : cn("bg-transparent hover:bg-surface-hover", nodeTextColorClass(colorId), !disabled && "opacity-70 hover:opacity-100"), idx > 0 && "border-l border-border", disabled && "opacity-50 cursor-not-allowed"),
58100
58151
  children: node.shortName
58101
58152
  }, node.id);
58102
58153
  })
@@ -58108,20 +58159,227 @@ function NodePicker({ selectedNodeId, onSelect, label = "Node", hideIcon = false
58108
58159
  //#region src/composites/page-header.tsx
58109
58160
  function PageHeader({ title, subtitle, actions, className }) {
58110
58161
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58111
- className: cn("flex flex-col gap-2 mb-3 sm:flex-row sm:items-center sm:justify-between", className),
58112
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
58113
- className: "text-sm font-semibold text-foreground",
58114
- children: title
58115
- }), subtitle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
58116
- className: "text-foreground-subtle text-xs",
58117
- children: subtitle
58118
- })] }), actions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58119
- className: "flex items-center gap-2 flex-wrap",
58162
+ className: cn("mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", className),
58163
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58164
+ className: "min-w-0",
58165
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
58166
+ className: "text-lg font-semibold text-foreground",
58167
+ children: title
58168
+ }), subtitle !== void 0 && subtitle !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
58169
+ className: "mt-0.5 text-xs text-foreground-subtle",
58170
+ children: subtitle
58171
+ })]
58172
+ }), actions !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58173
+ className: "flex flex-wrap items-center gap-2",
58120
58174
  children: actions
58121
58175
  })]
58122
58176
  });
58123
58177
  }
58124
58178
  //#endregion
58179
+ //#region src/composites/page-shell.tsx
58180
+ function PageShell({ icon: Icon, title, subtitle, actions, nav, className, children }) {
58181
+ const hasTitleRow = Boolean(title) || subtitle != null || actions != null;
58182
+ const hasHeader = hasTitleRow || nav != null;
58183
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58184
+ className: cn("space-y-4 overflow-x-clip p-2 sm:p-4", className),
58185
+ children: [hasHeader && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58186
+ className: "space-y-3",
58187
+ children: [hasTitleRow && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58188
+ className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-4",
58189
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58190
+ className: "min-w-0",
58191
+ children: [title !== void 0 && title !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h1", {
58192
+ className: "flex items-center gap-2 text-lg font-semibold text-foreground",
58193
+ children: [Icon !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
58194
+ className: "h-4 w-4 shrink-0 text-foreground-subtle",
58195
+ "aria-hidden": true
58196
+ }), title]
58197
+ }), subtitle != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
58198
+ className: "mt-0.5 text-xs text-foreground-subtle",
58199
+ children: subtitle
58200
+ })]
58201
+ }), actions != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58202
+ className: "flex shrink-0 flex-wrap items-center gap-2",
58203
+ children: actions
58204
+ })]
58205
+ }), nav]
58206
+ }), children]
58207
+ });
58208
+ }
58209
+ //#endregion
58210
+ //#region src/composites/section-toolbar.tsx
58211
+ function SectionToolbar({ start, end, className, testId }) {
58212
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58213
+ className: cn("flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between", className),
58214
+ "data-testid": testId,
58215
+ children: [start != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58216
+ className: "flex min-w-0 flex-1 flex-wrap items-center gap-2",
58217
+ children: start
58218
+ }), end != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58219
+ className: "flex shrink-0 flex-wrap items-center gap-2 sm:justify-end",
58220
+ children: end
58221
+ })]
58222
+ });
58223
+ }
58224
+ //#endregion
58225
+ //#region src/composites/item-form-dialog.tsx
58226
+ function ItemFormDialog({ open, onClose, title, icon: Icon, error, width = "lg", children, footer, testId }) {
58227
+ if (!open) return null;
58228
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
58229
+ open: true,
58230
+ onOpenChange: (next) => !next && onClose(),
58231
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, {
58232
+ width,
58233
+ placement: "sheet-on-mobile",
58234
+ scroll: "body",
58235
+ "data-testid": testId,
58236
+ children: [
58237
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
58238
+ className: "mb-3 flex shrink-0 items-center justify-between gap-2",
58239
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
58240
+ className: "flex min-w-0 items-center gap-2 text-sm font-semibold text-foreground",
58241
+ children: [Icon !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
58242
+ className: "h-4 w-4 shrink-0 text-foreground-subtle",
58243
+ "aria-hidden": true
58244
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
58245
+ className: "truncate",
58246
+ children: title
58247
+ })]
58248
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
58249
+ size: "sm",
58250
+ variant: "ghost",
58251
+ type: "button",
58252
+ onClick: onClose,
58253
+ children: "Cancel"
58254
+ })]
58255
+ }),
58256
+ error != null && error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58257
+ className: "mb-3 shrink-0 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-xs text-danger",
58258
+ children: error
58259
+ }),
58260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogBody, {
58261
+ className: "pr-1",
58262
+ children
58263
+ }),
58264
+ footer != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogFooter, {
58265
+ className: "mt-3 shrink-0",
58266
+ children: footer
58267
+ })
58268
+ ]
58269
+ })
58270
+ });
58271
+ }
58272
+ //#endregion
58273
+ //#region src/hooks/use-url-query.ts
58274
+ /**
58275
+ * Tiny URL query sync for surfaces that cannot assume react-router
58276
+ * (federated addon pages) or that only need a handful of params.
58277
+ *
58278
+ * Reads on mount + `popstate`; writes with `history.replaceState`. Empty /
58279
+ * null values are deleted so the URL stays short.
58280
+ */
58281
+ function readParam(key) {
58282
+ return new URLSearchParams(window.location.search).get(key);
58283
+ }
58284
+ function writeParam(key, value) {
58285
+ const params = new URLSearchParams(window.location.search);
58286
+ if (value === null || value === "") params.delete(key);
58287
+ else params.set(key, value);
58288
+ const qs = params.toString();
58289
+ const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
58290
+ window.history.replaceState(null, "", url);
58291
+ }
58292
+ /**
58293
+ * One query param as React state. `null` means absent.
58294
+ * Changing it replaces the URL without a navigation.
58295
+ */
58296
+ function useUrlQueryParam(key) {
58297
+ const [value, setValue] = (0, react$1.useState)(() => readParam(key));
58298
+ (0, react$1.useEffect)(() => {
58299
+ const onPop = () => setValue(readParam(key));
58300
+ window.addEventListener("popstate", onPop);
58301
+ return () => window.removeEventListener("popstate", onPop);
58302
+ }, [key]);
58303
+ return [value, (0, react$1.useCallback)((next) => {
58304
+ writeParam(key, next);
58305
+ setValue(next);
58306
+ }, [key])];
58307
+ }
58308
+ //#endregion
58309
+ //#region src/composites/use-item-form-url.ts
58310
+ /**
58311
+ * URL ownership for create/edit modals: `?new=1` or `?edit=<id>` (replace).
58312
+ * Mutually exclusive — opening one clears the other.
58313
+ */
58314
+ function useItemFormUrl(options = {}) {
58315
+ const newKey = options.newKey ?? "new";
58316
+ const editKey = options.editKey ?? "edit";
58317
+ const [newParam, setNewParam] = useUrlQueryParam(newKey);
58318
+ const [editParam, setEditParam] = useUrlQueryParam(editKey);
58319
+ return {
58320
+ state: (0, react$1.useMemo)(() => {
58321
+ if (editParam !== null && editParam !== "") return {
58322
+ mode: "edit",
58323
+ id: editParam
58324
+ };
58325
+ if (newParam === "1") return { mode: "new" };
58326
+ return { mode: "closed" };
58327
+ }, [editParam, newParam]),
58328
+ openNew: (0, react$1.useCallback)(() => {
58329
+ setEditParam(null);
58330
+ setNewParam("1");
58331
+ }, [setEditParam, setNewParam]),
58332
+ openEdit: (0, react$1.useCallback)((id) => {
58333
+ setNewParam(null);
58334
+ setEditParam(id);
58335
+ }, [setEditParam, setNewParam]),
58336
+ close: (0, react$1.useCallback)(() => {
58337
+ setNewParam(null);
58338
+ setEditParam(null);
58339
+ }, [setEditParam, setNewParam])
58340
+ };
58341
+ }
58342
+ //#endregion
58343
+ //#region src/composites/media-grid.tsx
58344
+ function MediaCard({ children, selected = false, onSelect, label, className, testId }) {
58345
+ const interactive = onSelect !== void 0;
58346
+ const onKeyDown = (event) => {
58347
+ if (!interactive) return;
58348
+ if (event.key === "Enter" || event.key === " ") {
58349
+ event.preventDefault();
58350
+ onSelect();
58351
+ }
58352
+ };
58353
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58354
+ role: interactive ? "button" : void 0,
58355
+ tabIndex: interactive ? 0 : void 0,
58356
+ "aria-pressed": interactive ? selected : void 0,
58357
+ "aria-label": label,
58358
+ "data-testid": testId,
58359
+ onClick: onSelect,
58360
+ onKeyDown,
58361
+ className: cn("relative aspect-square min-w-0 overflow-hidden rounded-md border bg-surface", selected ? "border-primary ring-1 ring-primary" : "border-border", interactive && "cursor-pointer hover:border-foreground-subtle", className),
58362
+ children
58363
+ });
58364
+ }
58365
+ function MediaGrid({ status, children, pendingMessage = "Loading…", emptyMessage = "Nothing to show", className, testId }) {
58366
+ if (status === "pending") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
58367
+ className: "animate-pulse py-8 text-center text-xs text-foreground-subtle",
58368
+ "data-testid": testId,
58369
+ children: pendingMessage
58370
+ });
58371
+ if (status === "empty") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
58372
+ className: "py-8 text-center text-xs text-foreground-subtle",
58373
+ "data-testid": testId,
58374
+ children: emptyMessage
58375
+ });
58376
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
58377
+ className: cn(MEDIA_GRID, GRID_GAP, className),
58378
+ "data-testid": testId,
58379
+ children
58380
+ });
58381
+ }
58382
+ //#endregion
58125
58383
  //#region src/composites/phase-icon.tsx
58126
58384
  /**
58127
58385
  * PhaseIcon — renders the correct lucide icon for a CameraPhase.
@@ -62065,42 +62323,6 @@ function useZoneEditing() {
62065
62323
  return (0, react$1.useContext)(ZoneEditingContext);
62066
62324
  }
62067
62325
  //#endregion
62068
- //#region src/hooks/use-url-query.ts
62069
- /**
62070
- * Tiny URL query sync for surfaces that cannot assume react-router
62071
- * (federated addon pages) or that only need a handful of params.
62072
- *
62073
- * Reads on mount + `popstate`; writes with `history.replaceState`. Empty /
62074
- * null values are deleted so the URL stays short.
62075
- */
62076
- function readParam(key) {
62077
- return new URLSearchParams(window.location.search).get(key);
62078
- }
62079
- function writeParam(key, value) {
62080
- const params = new URLSearchParams(window.location.search);
62081
- if (value === null || value === "") params.delete(key);
62082
- else params.set(key, value);
62083
- const qs = params.toString();
62084
- const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
62085
- window.history.replaceState(null, "", url);
62086
- }
62087
- /**
62088
- * One query param as React state. `null` means absent.
62089
- * Changing it replaces the URL without a navigation.
62090
- */
62091
- function useUrlQueryParam(key) {
62092
- const [value, setValue] = (0, react$1.useState)(() => readParam(key));
62093
- (0, react$1.useEffect)(() => {
62094
- const onPop = () => setValue(readParam(key));
62095
- window.addEventListener("popstate", onPop);
62096
- return () => window.removeEventListener("popstate", onPop);
62097
- }, [key]);
62098
- return [value, (0, react$1.useCallback)((next) => {
62099
- writeParam(key, next);
62100
- setValue(next);
62101
- }, [key])];
62102
- }
62103
- //#endregion
62104
62326
  //#region src/hooks/use-device-cap-slice.ts
62105
62327
  /**
62106
62328
  * useDeviceCapSlice — generic runtime-state slice reader for an arbitrary
@@ -62802,6 +63024,7 @@ exports.ImageOff = ImageOff;
62802
63024
  exports.ImageSelector = ImageSelector;
62803
63025
  exports.InferenceConfigSelector = InferenceConfigSelector;
62804
63026
  exports.Input = Input;
63027
+ exports.ItemFormDialog = ItemFormDialog;
62805
63028
  exports.KebabMenu = KebabMenu;
62806
63029
  exports.KeyValueList = KeyValueList;
62807
63030
  exports.LIST_ROW = LIST_ROW;
@@ -62816,8 +63039,11 @@ exports.LockInlineControl = LockInlineControl;
62816
63039
  exports.LockPanel = LockPanel;
62817
63040
  exports.LogStream = LogStream;
62818
63041
  exports.LoginForm = LoginForm;
63042
+ exports.MEDIA_GRID = MEDIA_GRID;
62819
63043
  exports.MODE_COLOR = MODE_COLOR;
62820
63044
  exports.MaskShapeCanvas = MaskShapeCanvas;
63045
+ exports.MediaCard = MediaCard;
63046
+ exports.MediaGrid = MediaGrid;
62821
63047
  exports.MediaPlayerHeroCard = MediaPlayerHeroCard;
62822
63048
  exports.MediaPlayerInlineControl = MediaPlayerInlineControl;
62823
63049
  exports.MediaPlayerPanel = MediaPlayerPanel;
@@ -62854,6 +63080,7 @@ exports.PICKER_WINDOW = PICKER_WINDOW;
62854
63080
  exports.PRIORITY = PRIORITY;
62855
63081
  exports.PTZOverlay = PTZOverlay;
62856
63082
  exports.PageHeader = PageHeader;
63083
+ exports.PageShell = PageShell;
62857
63084
  exports.PageSizeSelect = PageSizeSelect;
62858
63085
  exports.Pagination = Pagination;
62859
63086
  exports.PasswordInput = PasswordInput;
@@ -62916,6 +63143,7 @@ exports.ScrollArea = ScrollArea;
62916
63143
  exports.SecretFieldControl = SecretFieldControl;
62917
63144
  exports.SecretRevealProvider = SecretRevealProvider;
62918
63145
  exports.SectionCard = SectionCard;
63146
+ exports.SectionToolbar = SectionToolbar;
62919
63147
  exports.SegmentedChoice = SegmentedChoice;
62920
63148
  exports.Select = Select;
62921
63149
  exports.SelectedChip = SelectedChip;
@@ -62999,6 +63227,7 @@ exports.assignNodeColorId = _camstack_types.assignNodeColorId;
62999
63227
  exports.badgeFor = badgeFor;
63000
63228
  exports.badgeFor$1 = badgeFor;
63001
63229
  exports.bindsToModel = bindsToModel;
63230
+ exports.buildColorByNodeId = buildColorByNodeId;
63002
63231
  exports.buildPeerConnectionConfig = buildPeerConnectionConfig;
63003
63232
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
63004
63233
  exports.catalogHasDraft = catalogHasDraft;
@@ -63011,6 +63240,7 @@ exports.classifySessionVerdict = classifySessionVerdict;
63011
63240
  exports.clearPickerGroup = clearPickerGroup;
63012
63241
  exports.clusterRootModelIds = clusterRootModelIds;
63013
63242
  exports.cn = cn;
63243
+ exports.colorIdForNode = colorIdForNode;
63014
63244
  exports.columnsForContext = columnsForContext;
63015
63245
  exports.compareSrflxMappings = compareSrflxMappings;
63016
63246
  exports.computePageButtons = computePageButtons;
@@ -63103,6 +63333,7 @@ exports.nextSort = nextSort;
63103
63333
  exports.nextSortDirection = nextSortDirection;
63104
63334
  exports.nextTableSort = nextTableSort;
63105
63335
  exports.nodeColorClass = nodeColorClass;
63336
+ exports.nodeSoftBgClass = nodeSoftBgClass;
63106
63337
  exports.nodeTextColorClass = nodeTextColorClass;
63107
63338
  exports.normalizeForSearch = normalizeForSearch;
63108
63339
  exports.offListSelections = offListSelections;
@@ -63625,6 +63856,7 @@ exports.useIntercomStartTalkSession = useIntercomStartTalkSession;
63625
63856
  exports.useIntercomStopSession = useIntercomStopSession;
63626
63857
  exports.useIsMidWidth = useIsMidWidth;
63627
63858
  exports.useIsMobile = useIsMobile;
63859
+ exports.useItemFormUrl = useItemFormUrl;
63628
63860
  exports.useKeychainAdminAcknowledgeReset = useKeychainAdminAcknowledgeReset;
63629
63861
  exports.useKeychainAdminDeleteOrphans = useKeychainAdminDeleteOrphans;
63630
63862
  exports.useKeychainAdminDownloadRecoveryKey = useKeychainAdminDownloadRecoveryKey;