@camstack/ui-library 1.1.43 → 1.1.44
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/device-controls/control-registry.d.ts +14 -5
- package/dist/composites/device-step-matrix.d.ts +33 -0
- package/dist/composites/index.d.ts +3 -0
- package/dist/composites/pipeline-matrix-shared.d.ts +69 -0
- package/dist/composites/pipeline-tree-matrix.d.ts +2 -14
- package/dist/index.cjs +527 -252
- package/dist/index.js +527 -254
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -14426,7 +14426,7 @@ function buildDisplayStep(addon, cfg) {
|
|
|
14426
14426
|
function computeEffective(mode, agentDefault, patch) {
|
|
14427
14427
|
if (mode !== "device" || !patch) return agentDefault;
|
|
14428
14428
|
return {
|
|
14429
|
-
enabled:
|
|
14429
|
+
enabled: agentDefault.enabled,
|
|
14430
14430
|
modelId: patch.modelId ?? agentDefault.modelId,
|
|
14431
14431
|
settings: patch.settings ?? agentDefault.settings
|
|
14432
14432
|
};
|
|
@@ -14440,97 +14440,62 @@ function AgentModeEditor({ addon, agentDefault, modelsForFormat, onChangeAgentCo
|
|
|
14440
14440
|
};
|
|
14441
14441
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14442
14442
|
className: "space-y-4 text-xs",
|
|
14443
|
-
children: [
|
|
14444
|
-
|
|
14445
|
-
|
|
14446
|
-
|
|
14447
|
-
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14455
|
-
}),
|
|
14456
|
-
|
|
14457
|
-
|
|
14458
|
-
|
|
14459
|
-
|
|
14460
|
-
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
})] }),
|
|
14464
|
-
(addon.configSchema?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14465
|
-
className: "pt-2 border-t border-border/50 space-y-3",
|
|
14466
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14467
|
-
className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
|
|
14468
|
-
children: "Settings"
|
|
14469
|
-
}), addon.configSchema?.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
|
|
14470
|
-
field,
|
|
14471
|
-
allFields: addon.configSchema ?? [],
|
|
14472
|
-
step: buildDisplayStep(addon, agentDefault),
|
|
14473
|
-
disabled: false,
|
|
14474
|
-
onChange: (next) => update({ settings: { ...next.settings } })
|
|
14475
|
-
}, field.key))]
|
|
14476
|
-
})
|
|
14477
|
-
]
|
|
14443
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14444
|
+
className: "text-[10px] uppercase tracking-widest text-foreground-subtle mb-1",
|
|
14445
|
+
children: "Model"
|
|
14446
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
|
|
14447
|
+
models: modelsForFormat,
|
|
14448
|
+
value: agentDefault.modelId ?? "",
|
|
14449
|
+
onChange: (modelId) => update({ modelId })
|
|
14450
|
+
})] }), (addon.configSchema?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14451
|
+
className: "pt-2 border-t border-border/50 space-y-3",
|
|
14452
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14453
|
+
className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
|
|
14454
|
+
children: "Settings"
|
|
14455
|
+
}), addon.configSchema?.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
|
|
14456
|
+
field,
|
|
14457
|
+
allFields: addon.configSchema ?? [],
|
|
14458
|
+
step: buildDisplayStep(addon, agentDefault),
|
|
14459
|
+
disabled: false,
|
|
14460
|
+
onChange: (next) => update({ settings: { ...next.settings } })
|
|
14461
|
+
}, field.key))]
|
|
14462
|
+
})]
|
|
14478
14463
|
});
|
|
14479
14464
|
}
|
|
14480
14465
|
function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, modelsForFormat, onChangePatch }) {
|
|
14481
14466
|
const effective = computeEffective("device", agentDefault, currentPatch);
|
|
14482
14467
|
const commitPatch = (next) => {
|
|
14483
|
-
const hasAnyField = next.
|
|
14468
|
+
const hasAnyField = next.modelId !== void 0 || next.settings !== void 0;
|
|
14484
14469
|
onChangePatch?.(hasAnyField ? next : null);
|
|
14485
14470
|
};
|
|
14486
|
-
const setEnabled = (state) => {
|
|
14487
|
-
const { enabled: _prev, ...rest } = currentPatch ?? {};
|
|
14488
|
-
commitPatch(state === "inherit" ? rest : {
|
|
14489
|
-
...rest,
|
|
14490
|
-
enabled: state === "on"
|
|
14491
|
-
});
|
|
14492
|
-
};
|
|
14493
14471
|
const setModel = (modelId) => {
|
|
14494
|
-
const { modelId: _prev, ...rest } = currentPatch ?? {};
|
|
14472
|
+
const { enabled: _enabled, modelId: _prev, ...rest } = currentPatch ?? {};
|
|
14495
14473
|
commitPatch(modelId === void 0 ? rest : {
|
|
14496
14474
|
...rest,
|
|
14497
14475
|
modelId
|
|
14498
14476
|
});
|
|
14499
14477
|
};
|
|
14500
14478
|
const setSettings = (settings) => {
|
|
14501
|
-
const { settings: _prev, ...rest } = currentPatch ?? {};
|
|
14479
|
+
const { enabled: _enabled, settings: _prev, ...rest } = currentPatch ?? {};
|
|
14502
14480
|
commitPatch(settings === void 0 ? rest : {
|
|
14503
14481
|
...rest,
|
|
14504
14482
|
settings
|
|
14505
14483
|
});
|
|
14506
14484
|
};
|
|
14507
|
-
const enabledTri = currentPatch?.enabled === void 0 ? "inherit" : currentPatch.enabled ? "on" : "off";
|
|
14508
14485
|
const modelOverridden = currentPatch?.modelId !== void 0;
|
|
14509
14486
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14510
14487
|
className: "space-y-4 text-xs",
|
|
14511
14488
|
children: [
|
|
14512
14489
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14513
|
-
className: "
|
|
14514
|
-
children: [
|
|
14515
|
-
"
|
|
14516
|
-
"
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
children: s
|
|
14523
|
-
}, s)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
14524
|
-
className: "ml-2 text-[10px] text-foreground-subtle",
|
|
14525
|
-
children: [
|
|
14526
|
-
"agent default:",
|
|
14527
|
-
" ",
|
|
14528
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14529
|
-
className: "font-semibold",
|
|
14530
|
-
children: agentDefault.enabled ? "on" : "off"
|
|
14531
|
-
})
|
|
14532
|
-
]
|
|
14533
|
-
})]
|
|
14490
|
+
className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
|
|
14491
|
+
children: [
|
|
14492
|
+
"Provisioning for ",
|
|
14493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14494
|
+
className: "font-semibold text-foreground",
|
|
14495
|
+
children: agentNodeId
|
|
14496
|
+
}),
|
|
14497
|
+
" — model + settings only. Whether this camera runs the step is set by its per-camera gate above."
|
|
14498
|
+
]
|
|
14534
14499
|
}),
|
|
14535
14500
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
14536
14501
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -14601,15 +14566,13 @@ function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, mode
|
|
|
14601
14566
|
className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
|
|
14602
14567
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14603
14568
|
className: "font-semibold text-foreground mb-0.5",
|
|
14604
|
-
children: "Effective
|
|
14569
|
+
children: "Effective provisioning preview"
|
|
14605
14570
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14606
14571
|
className: "font-mono truncate",
|
|
14607
14572
|
children: [
|
|
14608
14573
|
addon.id,
|
|
14609
14574
|
" · model: ",
|
|
14610
|
-
effective.modelId
|
|
14611
|
-
" · ",
|
|
14612
|
-
effective.enabled ? "on" : "off"
|
|
14575
|
+
effective.modelId ?? "Auto"
|
|
14613
14576
|
]
|
|
14614
14577
|
})]
|
|
14615
14578
|
})
|
|
@@ -14634,19 +14597,6 @@ function AgentStepEditor(props) {
|
|
|
14634
14597
|
onChangePatch
|
|
14635
14598
|
});
|
|
14636
14599
|
}
|
|
14637
|
-
//#endregion
|
|
14638
|
-
//#region src/composites/pipeline-tree-matrix.tsx
|
|
14639
|
-
/**
|
|
14640
|
-
* Fixed column geometry (rem). The matrix uses a CSS grid with STATIC track
|
|
14641
|
-
* widths so a cell's content (a status chip, a long model id) can never resize
|
|
14642
|
-
* a column or shift its neighbours — cell content is truncated WITHIN the fixed
|
|
14643
|
-
* track instead. This kills the "table dances when the last column's status
|
|
14644
|
-
* changes" jitter. See `min-w-0` on every grid child, which lets the child
|
|
14645
|
-
* shrink to the track (a grid item's automatic minimum is its content size, so
|
|
14646
|
-
* without this a non-wrapping model id would blow the track wider than its max).
|
|
14647
|
-
*/
|
|
14648
|
-
var STEP_COL_REM = 18;
|
|
14649
|
-
var AGENT_COL_REM = 13;
|
|
14650
14600
|
function flattenTree(nodes) {
|
|
14651
14601
|
const out = [];
|
|
14652
14602
|
const walk = (list, depth) => {
|
|
@@ -14661,6 +14611,20 @@ function flattenTree(nodes) {
|
|
|
14661
14611
|
walk(nodes, 0);
|
|
14662
14612
|
return out;
|
|
14663
14613
|
}
|
|
14614
|
+
/**
|
|
14615
|
+
* Layout decision for the responsive A↔B switch. Pure so it can be unit-tested
|
|
14616
|
+
* in isolation from the DOM.
|
|
14617
|
+
*
|
|
14618
|
+
* Returns `true` (single-node fallback) when there is MORE than one node but
|
|
14619
|
+
* the container is too narrow to show the step column plus at least TWO node
|
|
14620
|
+
* columns side by side. An unmeasured container (`width <= 0`) defaults to the
|
|
14621
|
+
* wide matrix so there is no first-paint flash into the fallback.
|
|
14622
|
+
*/
|
|
14623
|
+
function shouldUseSingleNode(width, nodeCount, stepColPx, agentColPx = 208) {
|
|
14624
|
+
if (nodeCount <= 1) return false;
|
|
14625
|
+
if (width <= 0) return false;
|
|
14626
|
+
return width < stepColPx + agentColPx * 2;
|
|
14627
|
+
}
|
|
14664
14628
|
var SLOT_LABEL = {
|
|
14665
14629
|
detector: {
|
|
14666
14630
|
label: "detector",
|
|
@@ -14711,25 +14675,26 @@ function ClassChips({ inputs, outputs }) {
|
|
|
14711
14675
|
]
|
|
14712
14676
|
});
|
|
14713
14677
|
}
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
"
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
|
|
14726
|
-
|
|
14678
|
+
/** Step label (slot heading + addon name + class chips), shared by both views. */
|
|
14679
|
+
function StepLabel({ node }) {
|
|
14680
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14681
|
+
className: "flex flex-col gap-1 min-w-0 flex-1",
|
|
14682
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlotHeading, { slot: node.slot }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14683
|
+
className: "flex items-center gap-2 flex-wrap",
|
|
14684
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14685
|
+
className: "font-semibold truncate",
|
|
14686
|
+
children: node.addonName
|
|
14687
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClassChips, {
|
|
14688
|
+
inputs: node.inputClasses,
|
|
14689
|
+
outputs: node.outputClasses
|
|
14690
|
+
})]
|
|
14691
|
+
})]
|
|
14727
14692
|
});
|
|
14728
14693
|
}
|
|
14729
14694
|
/**
|
|
14730
|
-
* Cell content.
|
|
14731
|
-
*
|
|
14732
|
-
*
|
|
14695
|
+
* Cell content. Width-stable: the status dot is `shrink-0` (reserved space) and
|
|
14696
|
+
* the model id truncates inside a `min-w-0` flex row, so a longer model id never
|
|
14697
|
+
* widens the cell — it ellipsises instead.
|
|
14733
14698
|
*/
|
|
14734
14699
|
function renderCellContent(state) {
|
|
14735
14700
|
if (state.kind === "enabled") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -14762,31 +14727,41 @@ function renderCellContent(state) {
|
|
|
14762
14727
|
})
|
|
14763
14728
|
]
|
|
14764
14729
|
});
|
|
14730
|
+
if (state.kind === "skip") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
14731
|
+
className: "flex min-w-0 items-center gap-1 text-amber-500",
|
|
14732
|
+
title: "Gated on for this camera, but no compatible model on this node — click to provision",
|
|
14733
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-amber-500 shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14734
|
+
className: "truncate font-medium",
|
|
14735
|
+
children: "⚠ skip"
|
|
14736
|
+
})]
|
|
14737
|
+
});
|
|
14765
14738
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
14766
14739
|
className: "flex min-w-0 items-center gap-1 text-foreground-subtle/70",
|
|
14767
14740
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-muted shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14768
14741
|
className: "truncate",
|
|
14769
|
-
children: "
|
|
14742
|
+
children: "—"
|
|
14770
14743
|
})]
|
|
14771
14744
|
});
|
|
14772
14745
|
}
|
|
14773
14746
|
function cellButtonClass(state, isSelected, extra) {
|
|
14774
|
-
return cn("text-left text-xs hover:bg-muted/40", state.kind === "enabled" && "bg-emerald-500/5", state.kind === "na" && "bg-muted/30", isSelected && "ring-2 ring-primary ring-inset", extra);
|
|
14747
|
+
return cn("text-left text-xs hover:bg-muted/40", state.kind === "enabled" && "bg-emerald-500/5", state.kind === "skip" && "bg-amber-500/5", state.kind === "na" && "bg-muted/30", isSelected && "ring-2 ring-primary ring-inset", extra);
|
|
14775
14748
|
}
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14749
|
+
//#endregion
|
|
14750
|
+
//#region src/composites/pipeline-tree-matrix.tsx
|
|
14751
|
+
var STEP_COL_REM$1 = 18;
|
|
14752
|
+
function QuickToggle({ enabled, onChange, disabled }) {
|
|
14753
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
14754
|
+
type: "button",
|
|
14755
|
+
role: "switch",
|
|
14756
|
+
"aria-checked": enabled,
|
|
14757
|
+
disabled,
|
|
14758
|
+
onClick: (e) => {
|
|
14759
|
+
e.stopPropagation();
|
|
14760
|
+
onChange(!enabled);
|
|
14761
|
+
},
|
|
14762
|
+
className: cn("relative inline-flex h-3.5 w-7 shrink-0 items-center rounded-full transition-colors", enabled ? "bg-emerald-500" : "bg-foreground-subtle/30", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"),
|
|
14763
|
+
title: enabled ? "Click to disable" : "Click to enable",
|
|
14764
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cn("inline-block h-3 w-3 rounded-full bg-white shadow transition-transform", enabled ? "translate-x-3.5" : "translate-x-0.5") })
|
|
14790
14765
|
});
|
|
14791
14766
|
}
|
|
14792
14767
|
function GridRow({ node, depth, agents, getCellState, onCellClick, selectedCell, toggleProps }) {
|
|
@@ -14814,11 +14789,11 @@ function GridRow({ node, depth, agents, getCellState, onCellClick, selectedCell,
|
|
|
14814
14789
|
/**
|
|
14815
14790
|
* Wide layout: horizontally self-scrolling matrix. The scroll lives INSIDE
|
|
14816
14791
|
* this `overflow-auto` box (sticky first column + sticky header row) so the
|
|
14817
|
-
* page body never scrolls sideways. Column tracks are fixed px widths
|
|
14818
|
-
*
|
|
14792
|
+
* page body never scrolls sideways. Column tracks are fixed px widths so the
|
|
14793
|
+
* grid is static regardless of content.
|
|
14819
14794
|
*/
|
|
14820
|
-
function MatrixGrid({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
|
|
14821
|
-
const gridTemplate = `${STEP_COL_REM}rem repeat(${agents.length},
|
|
14795
|
+
function MatrixGrid$1({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
|
|
14796
|
+
const gridTemplate = `${STEP_COL_REM$1}rem repeat(${agents.length}, 13rem)`;
|
|
14822
14797
|
const showToggle = onToggleEnabled !== void 0 && agents.length === 1;
|
|
14823
14798
|
const onlyAgent = agents[0]?.agentNodeId;
|
|
14824
14799
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -14863,9 +14838,7 @@ function MatrixGrid({ rows, agents, getCellState, onCellClick, selectedCell, onT
|
|
|
14863
14838
|
}
|
|
14864
14839
|
/**
|
|
14865
14840
|
* Narrow layout: one card per agent column, each listing every step stacked
|
|
14866
|
-
* vertically.
|
|
14867
|
-
* (click-to-edit, status, toggle) reflow into full-width rows so nothing scrolls
|
|
14868
|
-
* horizontally. Feature-parity with the matrix — same `onCellClick`, same
|
|
14841
|
+
* vertically. Feature-parity with the matrix — same `onCellClick`, same
|
|
14869
14842
|
* `renderCellContent`, same single-agent `onToggleEnabled`.
|
|
14870
14843
|
*/
|
|
14871
14844
|
function StackedCards({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
|
|
@@ -14934,7 +14907,7 @@ function PipelineTreeMatrix({ tree, agents, getCellState, onCellClick, selectedC
|
|
|
14934
14907
|
})
|
|
14935
14908
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14936
14909
|
className: "hidden @2xl/matrix:block",
|
|
14937
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
|
|
14910
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid$1, {
|
|
14938
14911
|
rows,
|
|
14939
14912
|
agents,
|
|
14940
14913
|
getCellState,
|
|
@@ -14946,6 +14919,226 @@ function PipelineTreeMatrix({ tree, agents, getCellState, onCellClick, selectedC
|
|
|
14946
14919
|
});
|
|
14947
14920
|
}
|
|
14948
14921
|
//#endregion
|
|
14922
|
+
//#region src/composites/device-step-matrix.tsx
|
|
14923
|
+
/**
|
|
14924
|
+
* DeviceStepMatrix — the per-camera "Detection pipeline → Steps" surface (#33)
|
|
14925
|
+
* rendered as ONE hierarchical steps×nodes matrix.
|
|
14926
|
+
*
|
|
14927
|
+
* Column 1 (STICKY): the indented step tree. Each step carries its slot +
|
|
14928
|
+
* name + class-flow chips PLUS a per-camera, node-independent GATE control
|
|
14929
|
+
* with THREE states — On / Off / Default (inherit). `package-detection` is
|
|
14930
|
+
* zone-driven, so it shows a "Zones" badge instead of a gate.
|
|
14931
|
+
* Columns 2..N (one per online node): the provisioned model chip (click →
|
|
14932
|
+
* the model/config picker), a clickable "⚠ skip" when the step is gated ON
|
|
14933
|
+
* but the node has no compatible model, or "—" when N/A.
|
|
14934
|
+
*
|
|
14935
|
+
* Responsive, container-measured (never a global media query, so it also works
|
|
14936
|
+
* embedded):
|
|
14937
|
+
* A) wide — sticky step column + node columns in an `overflow-x:auto` box, so
|
|
14938
|
+
* narrow viewports scroll the NODES sideways while col-1 stays pinned and
|
|
14939
|
+
* the page body never scrolls horizontally.
|
|
14940
|
+
* B) narrow — below the width needed for ≥2 node columns, collapse to a
|
|
14941
|
+
* single-node view: a node selector picks ONE node and the matrix renders
|
|
14942
|
+
* `step-tree × that-one-node`.
|
|
14943
|
+
*/
|
|
14944
|
+
/** Wider than the cluster matrix's 18rem: col-1 also hosts the gate control. */
|
|
14945
|
+
var STEP_COL_REM = 21;
|
|
14946
|
+
var STEP_COL_PX = STEP_COL_REM * 16;
|
|
14947
|
+
function activeChoice(gate) {
|
|
14948
|
+
if (!gate.explicit) return "default";
|
|
14949
|
+
return gate.effectiveOn ? "on" : "off";
|
|
14950
|
+
}
|
|
14951
|
+
/**
|
|
14952
|
+
* Tri-state segmented control. `Default` inherits the catalog value (shown in
|
|
14953
|
+
* the caption); `On`/`Off` pin an explicit per-camera override. Touch-friendly
|
|
14954
|
+
* 28px segments.
|
|
14955
|
+
*/
|
|
14956
|
+
function GateControl({ gate, busy, onSet }) {
|
|
14957
|
+
const active = activeChoice(gate);
|
|
14958
|
+
const seg = (choice, label, value, activeClass) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
14959
|
+
type: "button",
|
|
14960
|
+
"aria-pressed": active === choice,
|
|
14961
|
+
disabled: busy,
|
|
14962
|
+
onClick: () => onSet(value),
|
|
14963
|
+
className: cn("flex-1 px-2 py-1 text-[11px] font-medium leading-none transition-colors", "min-h-[26px]", active === choice ? activeClass : "text-foreground-subtle hover:bg-muted/60", busy && "opacity-50 cursor-not-allowed"),
|
|
14964
|
+
children: label
|
|
14965
|
+
});
|
|
14966
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14967
|
+
className: "flex flex-col gap-1",
|
|
14968
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14969
|
+
className: "inline-flex w-fit max-w-full self-start overflow-hidden rounded-md border border-border",
|
|
14970
|
+
children: [
|
|
14971
|
+
seg("default", "Auto", null, "bg-primary/15 text-primary"),
|
|
14972
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
|
|
14973
|
+
seg("on", "On", true, "bg-emerald-500 text-white"),
|
|
14974
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
|
|
14975
|
+
seg("off", "Off", false, "bg-foreground-subtle/70 text-white")
|
|
14976
|
+
]
|
|
14977
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14978
|
+
className: "text-[10px] leading-none text-foreground-subtle",
|
|
14979
|
+
children: active === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
14980
|
+
"= default (",
|
|
14981
|
+
gate.defaultOn ? "on" : "off",
|
|
14982
|
+
")"
|
|
14983
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: ["overridden · default ", gate.defaultOn ? "on" : "off"] })
|
|
14984
|
+
})]
|
|
14985
|
+
});
|
|
14986
|
+
}
|
|
14987
|
+
function ZoneBadge() {
|
|
14988
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14989
|
+
className: "flex flex-col gap-1",
|
|
14990
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14991
|
+
className: "inline-flex w-fit items-center gap-1 rounded-md border border-violet-500/40 bg-violet-500/10 px-2 py-1 text-[11px] font-medium text-violet-400",
|
|
14992
|
+
children: "Zones"
|
|
14993
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14994
|
+
className: "text-[10px] leading-none text-foreground-subtle",
|
|
14995
|
+
children: "driven by package zones"
|
|
14996
|
+
})]
|
|
14997
|
+
});
|
|
14998
|
+
}
|
|
14999
|
+
function StepGateCell({ node, depth, gate, busy, onSet }) {
|
|
15000
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
15001
|
+
className: "sticky left-0 z-10 flex flex-col gap-2 border-b border-border bg-surface px-3 py-2.5 text-xs min-w-0",
|
|
15002
|
+
style: { paddingLeft: `${12 + depth * 14}px` },
|
|
15003
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), gate.kind === "zone-driven" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoneBadge, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GateControl, {
|
|
15004
|
+
gate,
|
|
15005
|
+
busy,
|
|
15006
|
+
onSet
|
|
15007
|
+
})]
|
|
15008
|
+
});
|
|
15009
|
+
}
|
|
15010
|
+
/**
|
|
15011
|
+
* Agents-scoped node selector for the B fallback. Styled like the shared
|
|
15012
|
+
* `NodePicker` but driven by THIS matrix's `agents` set — the cluster-wide
|
|
15013
|
+
* NodePicker can list nodes (e.g. the hub) that own no pipeline column here,
|
|
15014
|
+
* which would let the operator select a node the matrix can't show.
|
|
15015
|
+
*/
|
|
15016
|
+
function NodeSelectorBar({ agents, selected, onSelect }) {
|
|
15017
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
15018
|
+
className: "flex flex-wrap items-center gap-1.5",
|
|
15019
|
+
role: "tablist",
|
|
15020
|
+
"aria-label": "Node",
|
|
15021
|
+
children: [
|
|
15022
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Server, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
|
|
15023
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
15024
|
+
className: "text-[10px] uppercase tracking-wider text-foreground-subtle",
|
|
15025
|
+
children: "Node"
|
|
15026
|
+
}),
|
|
15027
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15028
|
+
className: "flex overflow-hidden rounded-md border border-border text-[11px] font-medium",
|
|
15029
|
+
children: agents.map((a, idx) => {
|
|
15030
|
+
const isSelected = a.agentNodeId === selected;
|
|
15031
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
15032
|
+
type: "button",
|
|
15033
|
+
role: "tab",
|
|
15034
|
+
"aria-selected": isSelected,
|
|
15035
|
+
onClick: () => onSelect(a.agentNodeId),
|
|
15036
|
+
title: `${a.agentNodeId} · ${a.engineLabel}`,
|
|
15037
|
+
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"),
|
|
15038
|
+
children: a.agentNodeId
|
|
15039
|
+
}, a.agentNodeId);
|
|
15040
|
+
})
|
|
15041
|
+
})
|
|
15042
|
+
]
|
|
15043
|
+
});
|
|
15044
|
+
}
|
|
15045
|
+
function MatrixGrid({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, single }) {
|
|
15046
|
+
const rows = (0, react$1.useMemo)(() => flattenTree(tree), [tree]);
|
|
15047
|
+
const gridTemplate = single ? `minmax(0, 1.35fr) repeat(${agents.length}, minmax(0, 1fr))` : `${STEP_COL_REM}rem repeat(${agents.length}, 13rem)`;
|
|
15048
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15049
|
+
className: cn("rounded border border-border", single ? "overflow-hidden" : "overflow-auto"),
|
|
15050
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
15051
|
+
className: cn("grid", single ? "w-full" : "w-max"),
|
|
15052
|
+
style: { gridTemplateColumns: gridTemplate },
|
|
15053
|
+
children: [
|
|
15054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15055
|
+
className: "sticky left-0 top-0 z-20 border-b border-border bg-muted/60 px-3 py-2 text-[10px] uppercase tracking-widest text-foreground-subtle",
|
|
15056
|
+
children: "Step · gate"
|
|
15057
|
+
}),
|
|
15058
|
+
agents.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
15059
|
+
className: "sticky top-0 z-10 min-w-0 border-b border-l border-border bg-muted/60 px-3 py-2",
|
|
15060
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15061
|
+
className: "truncate text-xs font-semibold text-foreground",
|
|
15062
|
+
children: a.agentNodeId
|
|
15063
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
15064
|
+
className: "truncate text-[10px] text-foreground-subtle",
|
|
15065
|
+
children: a.engineLabel
|
|
15066
|
+
})]
|
|
15067
|
+
}, a.agentNodeId)),
|
|
15068
|
+
rows.map(({ node, depth }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixRow, {
|
|
15069
|
+
node,
|
|
15070
|
+
depth,
|
|
15071
|
+
agents,
|
|
15072
|
+
gate: gateFor(node.addonId),
|
|
15073
|
+
getCellState,
|
|
15074
|
+
onCellClick,
|
|
15075
|
+
selectedCell,
|
|
15076
|
+
onSetGate,
|
|
15077
|
+
gateBusy
|
|
15078
|
+
}, node.addonId))
|
|
15079
|
+
]
|
|
15080
|
+
})
|
|
15081
|
+
});
|
|
15082
|
+
}
|
|
15083
|
+
function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selectedCell, onSetGate, gateBusy }) {
|
|
15084
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepGateCell, {
|
|
15085
|
+
node,
|
|
15086
|
+
depth,
|
|
15087
|
+
gate,
|
|
15088
|
+
busy: gateBusy,
|
|
15089
|
+
onSet: (next) => onSetGate(node.addonId, next)
|
|
15090
|
+
}), agents.map((a) => {
|
|
15091
|
+
const state = getCellState(node.addonId, a.agentNodeId);
|
|
15092
|
+
const isSelected = selectedCell?.addonId === node.addonId && selectedCell.agentNodeId === a.agentNodeId;
|
|
15093
|
+
const actionable = state.kind === "enabled" || state.kind === "skip";
|
|
15094
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
15095
|
+
type: "button",
|
|
15096
|
+
disabled: !actionable,
|
|
15097
|
+
onClick: () => onCellClick(node.addonId, a.agentNodeId),
|
|
15098
|
+
className: cellButtonClass(state, isSelected, cn("min-w-0 overflow-hidden border-b border-l border-border px-3 py-2", !actionable && "cursor-default hover:bg-transparent")),
|
|
15099
|
+
children: renderCellContent(state)
|
|
15100
|
+
}, a.agentNodeId);
|
|
15101
|
+
})] });
|
|
15102
|
+
}
|
|
15103
|
+
function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, fallbackNodeId, onFallbackNodeChange }) {
|
|
15104
|
+
const containerRef = (0, react$1.useRef)(null);
|
|
15105
|
+
const [width, setWidth] = (0, react$1.useState)(0);
|
|
15106
|
+
(0, react$1.useLayoutEffect)(() => {
|
|
15107
|
+
const el = containerRef.current;
|
|
15108
|
+
if (!el) return;
|
|
15109
|
+
const measure = () => setWidth(el.getBoundingClientRect().width);
|
|
15110
|
+
measure();
|
|
15111
|
+
const RO = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;
|
|
15112
|
+
RO?.observe(el);
|
|
15113
|
+
return () => RO?.disconnect();
|
|
15114
|
+
}, []);
|
|
15115
|
+
const single = shouldUseSingleNode(width, agents.length, STEP_COL_PX);
|
|
15116
|
+
const selectedNode = (0, react$1.useMemo)(() => {
|
|
15117
|
+
if (agents.length === 0) return null;
|
|
15118
|
+
return agents.find((a) => a.agentNodeId === fallbackNodeId) ?? agents[0];
|
|
15119
|
+
}, [agents, fallbackNodeId]);
|
|
15120
|
+
const shownAgents = single && selectedNode ? [selectedNode] : agents;
|
|
15121
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
15122
|
+
ref: containerRef,
|
|
15123
|
+
className: "w-full space-y-2",
|
|
15124
|
+
children: [single && selectedNode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeSelectorBar, {
|
|
15125
|
+
agents,
|
|
15126
|
+
selected: selectedNode.agentNodeId,
|
|
15127
|
+
onSelect: onFallbackNodeChange
|
|
15128
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
|
|
15129
|
+
tree,
|
|
15130
|
+
agents: shownAgents,
|
|
15131
|
+
gateFor,
|
|
15132
|
+
getCellState,
|
|
15133
|
+
onCellClick,
|
|
15134
|
+
selectedCell,
|
|
15135
|
+
onSetGate,
|
|
15136
|
+
gateBusy,
|
|
15137
|
+
single: single && selectedNode !== null
|
|
15138
|
+
})]
|
|
15139
|
+
});
|
|
15140
|
+
}
|
|
15141
|
+
//#endregion
|
|
14949
15142
|
//#region src/trpc-react.ts
|
|
14950
15143
|
/**
|
|
14951
15144
|
* Shared typed React Query proxy for the backend AppRouter.
|
|
@@ -26533,131 +26726,117 @@ function ControlHeroCard({ trpc, deviceId, features, optimistic }) {
|
|
|
26533
26726
|
}
|
|
26534
26727
|
//#endregion
|
|
26535
26728
|
//#region src/composites/device-controls/control-registry.tsx
|
|
26536
|
-
var COVER_CONTROL = {
|
|
26537
|
-
InlineControl: CoverInlineControl,
|
|
26538
|
-
HeroCard: CoverHeroCard
|
|
26539
|
-
};
|
|
26540
|
-
var DUMMY_CONTROL = {
|
|
26541
|
-
InlineControl: DummyInline,
|
|
26542
|
-
HeroCard: DummyHeroCard
|
|
26543
|
-
};
|
|
26544
|
-
var SENSOR_CONTROL = {
|
|
26545
|
-
InlineControl: SensorInlineControl,
|
|
26546
|
-
HeroCard: SensorHeroCard
|
|
26547
|
-
};
|
|
26548
|
-
var UPDATE_CONTROL = {
|
|
26549
|
-
InlineControl: UpdateInlineControl,
|
|
26550
|
-
HeroCard: UpdateHeroCard
|
|
26551
|
-
};
|
|
26552
|
-
var SWITCH_CONTROL = {
|
|
26553
|
-
InlineControl: SwitchInlineControl,
|
|
26554
|
-
HeroCard: SwitchHeroCard
|
|
26555
|
-
};
|
|
26556
|
-
var LOCK_CONTROL = {
|
|
26557
|
-
InlineControl: LockInlineControl,
|
|
26558
|
-
HeroCard: LockHeroCard
|
|
26559
|
-
};
|
|
26560
|
-
var LIGHT_CONTROL = {
|
|
26561
|
-
InlineControl: LightInlineControl,
|
|
26562
|
-
HeroCard: LightHeroCard
|
|
26563
|
-
};
|
|
26564
|
-
var FAN_CONTROL = {
|
|
26565
|
-
InlineControl: FanInlineControl,
|
|
26566
|
-
HeroCard: FanHeroCard
|
|
26567
|
-
};
|
|
26568
|
-
var THERMOSTAT_CONTROL = {
|
|
26569
|
-
InlineControl: ThermostatInlineControl,
|
|
26570
|
-
HeroCard: ThermostatHeroCard
|
|
26571
|
-
};
|
|
26572
|
-
var CLIMATE_CONTROL = {
|
|
26573
|
-
InlineControl: AcInlineControl,
|
|
26574
|
-
HeroCard: AcHeroCard
|
|
26575
|
-
};
|
|
26576
|
-
var MEDIA_PLAYER_CONTROL = {
|
|
26577
|
-
InlineControl: MediaPlayerInlineControl,
|
|
26578
|
-
HeroCard: MediaPlayerHeroCard
|
|
26579
|
-
};
|
|
26580
|
-
var ALARM_PANEL_CONTROL = {
|
|
26581
|
-
InlineControl: AlarmInlineControl,
|
|
26582
|
-
HeroCard: AlarmHeroCard
|
|
26583
|
-
};
|
|
26584
|
-
var CONTROL_CONTROL = {
|
|
26585
|
-
InlineControl: ControlInlineControl,
|
|
26586
|
-
HeroCard: ControlHeroCard
|
|
26587
|
-
};
|
|
26588
|
-
var VACUUM_CONTROL = {
|
|
26589
|
-
InlineControl: VacuumInlineControl,
|
|
26590
|
-
HeroCard: VacuumHeroCard
|
|
26591
|
-
};
|
|
26592
|
-
var LAWN_MOWER_CONTROL = {
|
|
26593
|
-
InlineControl: LawnMowerInlineControl,
|
|
26594
|
-
HeroCard: LawnMowerHeroCard
|
|
26595
|
-
};
|
|
26596
|
-
var VALVE_CONTROL = {
|
|
26597
|
-
InlineControl: ValveInlineControl,
|
|
26598
|
-
HeroCard: ValveHeroCard
|
|
26599
|
-
};
|
|
26600
|
-
var HUMIDIFIER_CONTROL = {
|
|
26601
|
-
InlineControl: HumidifierInlineControl,
|
|
26602
|
-
HeroCard: HumidifierHeroCard
|
|
26603
|
-
};
|
|
26604
|
-
var WATER_HEATER_CONTROL = {
|
|
26605
|
-
InlineControl: WaterHeaterInlineControl,
|
|
26606
|
-
HeroCard: WaterHeaterHeroCard
|
|
26607
|
-
};
|
|
26608
|
-
var WEATHER_CONTROL = {
|
|
26609
|
-
InlineControl: WeatherInlineControl,
|
|
26610
|
-
HeroCard: WeatherHeroCard
|
|
26611
|
-
};
|
|
26612
|
-
var IMAGE_CONTROL = {
|
|
26613
|
-
InlineControl: ImageInlineControl,
|
|
26614
|
-
HeroCard: ImageHeroCard
|
|
26615
|
-
};
|
|
26616
|
-
var PET_FEEDER_CONTROL = {
|
|
26617
|
-
InlineControl: PetFeederInlineControl,
|
|
26618
|
-
HeroCard: PetFeederHeroCard
|
|
26619
|
-
};
|
|
26620
26729
|
/**
|
|
26621
|
-
*
|
|
26622
|
-
*
|
|
26623
|
-
|
|
26624
|
-
|
|
26625
|
-
|
|
26626
|
-
|
|
26627
|
-
|
|
26628
|
-
|
|
26629
|
-
|
|
26630
|
-
|
|
26631
|
-
|
|
26632
|
-
|
|
26633
|
-
|
|
26634
|
-
|
|
26635
|
-
|
|
26636
|
-
|
|
26637
|
-
|
|
26638
|
-
|
|
26730
|
+
* Presentation binding: the shared `DeviceControlKind` (decided in
|
|
26731
|
+
* `@camstack/types` `resolveDeviceControlKind`) → this platform's web
|
|
26732
|
+
* components. Exhaustive over `DeviceControlKind` — omitting a kind fails the
|
|
26733
|
+
* build here. The type→kind DECISION is single-sourced in `@camstack/types`;
|
|
26734
|
+
* this map only binds a kind to its web widgets.
|
|
26735
|
+
*/
|
|
26736
|
+
var CONTROL_KIND_COMPONENTS = {
|
|
26737
|
+
cover: {
|
|
26738
|
+
InlineControl: CoverInlineControl,
|
|
26739
|
+
HeroCard: CoverHeroCard
|
|
26740
|
+
},
|
|
26741
|
+
valve: {
|
|
26742
|
+
InlineControl: ValveInlineControl,
|
|
26743
|
+
HeroCard: ValveHeroCard
|
|
26744
|
+
},
|
|
26745
|
+
humidifier: {
|
|
26746
|
+
InlineControl: HumidifierInlineControl,
|
|
26747
|
+
HeroCard: HumidifierHeroCard
|
|
26748
|
+
},
|
|
26749
|
+
"water-heater": {
|
|
26750
|
+
InlineControl: WaterHeaterInlineControl,
|
|
26751
|
+
HeroCard: WaterHeaterHeroCard
|
|
26752
|
+
},
|
|
26753
|
+
switch: {
|
|
26754
|
+
InlineControl: SwitchInlineControl,
|
|
26755
|
+
HeroCard: SwitchHeroCard
|
|
26756
|
+
},
|
|
26757
|
+
light: {
|
|
26758
|
+
InlineControl: LightInlineControl,
|
|
26759
|
+
HeroCard: LightHeroCard
|
|
26760
|
+
},
|
|
26761
|
+
fan: {
|
|
26762
|
+
InlineControl: FanInlineControl,
|
|
26763
|
+
HeroCard: FanHeroCard
|
|
26764
|
+
},
|
|
26765
|
+
sensor: {
|
|
26766
|
+
InlineControl: SensorInlineControl,
|
|
26767
|
+
HeroCard: SensorHeroCard
|
|
26768
|
+
},
|
|
26769
|
+
thermostat: {
|
|
26770
|
+
InlineControl: ThermostatInlineControl,
|
|
26771
|
+
HeroCard: ThermostatHeroCard
|
|
26772
|
+
},
|
|
26773
|
+
climate: {
|
|
26774
|
+
InlineControl: AcInlineControl,
|
|
26775
|
+
HeroCard: AcHeroCard
|
|
26776
|
+
},
|
|
26777
|
+
button: {
|
|
26639
26778
|
InlineControl: ButtonControl,
|
|
26640
26779
|
HeroCard: ButtonHeroCard
|
|
26641
26780
|
},
|
|
26642
|
-
|
|
26643
|
-
|
|
26644
|
-
|
|
26645
|
-
|
|
26646
|
-
|
|
26647
|
-
|
|
26648
|
-
|
|
26649
|
-
|
|
26650
|
-
|
|
26651
|
-
|
|
26652
|
-
|
|
26653
|
-
|
|
26654
|
-
|
|
26655
|
-
|
|
26656
|
-
|
|
26657
|
-
|
|
26658
|
-
|
|
26781
|
+
update: {
|
|
26782
|
+
InlineControl: UpdateInlineControl,
|
|
26783
|
+
HeroCard: UpdateHeroCard
|
|
26784
|
+
},
|
|
26785
|
+
lock: {
|
|
26786
|
+
InlineControl: LockInlineControl,
|
|
26787
|
+
HeroCard: LockHeroCard
|
|
26788
|
+
},
|
|
26789
|
+
"media-player": {
|
|
26790
|
+
InlineControl: MediaPlayerInlineControl,
|
|
26791
|
+
HeroCard: MediaPlayerHeroCard
|
|
26792
|
+
},
|
|
26793
|
+
alarm: {
|
|
26794
|
+
InlineControl: AlarmInlineControl,
|
|
26795
|
+
HeroCard: AlarmHeroCard
|
|
26796
|
+
},
|
|
26797
|
+
control: {
|
|
26798
|
+
InlineControl: ControlInlineControl,
|
|
26799
|
+
HeroCard: ControlHeroCard
|
|
26800
|
+
},
|
|
26801
|
+
weather: {
|
|
26802
|
+
InlineControl: WeatherInlineControl,
|
|
26803
|
+
HeroCard: WeatherHeroCard
|
|
26804
|
+
},
|
|
26805
|
+
vacuum: {
|
|
26806
|
+
InlineControl: VacuumInlineControl,
|
|
26807
|
+
HeroCard: VacuumHeroCard
|
|
26808
|
+
},
|
|
26809
|
+
"lawn-mower": {
|
|
26810
|
+
InlineControl: LawnMowerInlineControl,
|
|
26811
|
+
HeroCard: LawnMowerHeroCard
|
|
26812
|
+
},
|
|
26813
|
+
image: {
|
|
26814
|
+
InlineControl: ImageInlineControl,
|
|
26815
|
+
HeroCard: ImageHeroCard
|
|
26816
|
+
},
|
|
26817
|
+
"pet-feeder": {
|
|
26818
|
+
InlineControl: PetFeederInlineControl,
|
|
26819
|
+
HeroCard: PetFeederHeroCard
|
|
26820
|
+
},
|
|
26821
|
+
dummy: {
|
|
26822
|
+
InlineControl: DummyInline,
|
|
26823
|
+
HeroCard: DummyHeroCard
|
|
26824
|
+
}
|
|
26659
26825
|
};
|
|
26660
26826
|
/**
|
|
26827
|
+
* Exhaustive record over DeviceType, DERIVED from the shared decision
|
|
26828
|
+
* (`DEVICE_TYPE_CONTROL_KIND`) composed with the web `CONTROL_KIND_COMPONENTS`
|
|
26829
|
+
* binding — so the type→control mapping can never diverge from the single
|
|
26830
|
+
* source in `@camstack/types`. Preserved as a public export for consumers/tests
|
|
26831
|
+
* that index it by `DeviceType`.
|
|
26832
|
+
*
|
|
26833
|
+
* The `Object.fromEntries` result is a `{ [k: string]: DeviceTypeControl }`;
|
|
26834
|
+
* `Object.values(DeviceType)` (a string enum) yields exactly the enum's members,
|
|
26835
|
+
* so the record is fully populated — the assertion narrows that documented
|
|
26836
|
+
* enum-record projection boundary.
|
|
26837
|
+
*/
|
|
26838
|
+
var DEVICE_TYPE_CONTROL = Object.fromEntries(Object.values(_camstack_types.DeviceType).map((type) => [type, CONTROL_KIND_COMPONENTS[_camstack_types.DEVICE_TYPE_CONTROL_KIND[type]]]));
|
|
26839
|
+
/**
|
|
26661
26840
|
* Per-type FILL flag.
|
|
26662
26841
|
*
|
|
26663
26842
|
* true — range / choice / button controls (cover ▲■▼ triad, thermostat
|
|
@@ -26711,12 +26890,14 @@ var CONTROL_FILLS = {
|
|
|
26711
26890
|
};
|
|
26712
26891
|
var DEVICE_TYPE_VALUES$1 = new Set(Object.values(_camstack_types.DeviceType));
|
|
26713
26892
|
/**
|
|
26714
|
-
* Runtime-string-safe lookup: a device row's `type` crosses the wire
|
|
26715
|
-
*
|
|
26716
|
-
*
|
|
26893
|
+
* Runtime-string-safe lookup: a device row's `type` crosses the wire as a plain
|
|
26894
|
+
* string. The type→control-kind DECISION is single-sourced in `@camstack/types`
|
|
26895
|
+
* (`resolveDeviceControlKind`, null for an unrecognised type); this maps the
|
|
26896
|
+
* resolved kind onto the web components.
|
|
26717
26897
|
*/
|
|
26718
26898
|
function resolveDeviceControl(type) {
|
|
26719
|
-
|
|
26899
|
+
const kind = (0, _camstack_types.resolveDeviceControlKind)(type);
|
|
26900
|
+
return kind ? CONTROL_KIND_COMPONENTS[kind] : null;
|
|
26720
26901
|
}
|
|
26721
26902
|
/**
|
|
26722
26903
|
* Runtime-string-safe alignment lookup for the fixed-width control column.
|
|
@@ -40488,8 +40669,30 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
40488
40669
|
const pc = new RTCPeerConnection({ iceServers: [...iceServers ?? [], { urls: "stun:stun.l.google.com:19302" }] });
|
|
40489
40670
|
const newStream = new MediaStream();
|
|
40490
40671
|
let swappedIn = false;
|
|
40491
|
-
|
|
40492
|
-
|
|
40672
|
+
let warmupStarted = false;
|
|
40673
|
+
let warmupVideo = null;
|
|
40674
|
+
let warmupRvfc;
|
|
40675
|
+
let warmupSafety;
|
|
40676
|
+
const teardownWarmup = () => {
|
|
40677
|
+
if (warmupSafety !== void 0) {
|
|
40678
|
+
clearTimeout(warmupSafety);
|
|
40679
|
+
warmupSafety = void 0;
|
|
40680
|
+
}
|
|
40681
|
+
const v = warmupVideo;
|
|
40682
|
+
warmupVideo = null;
|
|
40683
|
+
if (!v) return;
|
|
40684
|
+
if (warmupRvfc !== void 0 && typeof v.cancelVideoFrameCallback === "function") v.cancelVideoFrameCallback(warmupRvfc);
|
|
40685
|
+
v.removeEventListener("loadeddata", commitSwap);
|
|
40686
|
+
try {
|
|
40687
|
+
v.srcObject = null;
|
|
40688
|
+
} catch {}
|
|
40689
|
+
v.remove();
|
|
40690
|
+
};
|
|
40691
|
+
function commitSwap() {
|
|
40692
|
+
if (swappedIn || !mountedRef.current) {
|
|
40693
|
+
teardownWarmup();
|
|
40694
|
+
return;
|
|
40695
|
+
}
|
|
40493
40696
|
swappedIn = true;
|
|
40494
40697
|
pcRef.current = pc;
|
|
40495
40698
|
if (videoRef.current) {
|
|
@@ -40497,11 +40700,32 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
40497
40700
|
videoRef.current.play().catch(() => {});
|
|
40498
40701
|
}
|
|
40499
40702
|
if (oldPc && oldPc !== pc) oldPc.close();
|
|
40703
|
+
teardownWarmup();
|
|
40704
|
+
}
|
|
40705
|
+
const armSwap = () => {
|
|
40706
|
+
if (warmupStarted || swappedIn || !mountedRef.current) return;
|
|
40707
|
+
warmupStarted = true;
|
|
40708
|
+
if (typeof document === "undefined") {
|
|
40709
|
+
commitSwap();
|
|
40710
|
+
return;
|
|
40711
|
+
}
|
|
40712
|
+
const v = document.createElement("video");
|
|
40713
|
+
v.muted = true;
|
|
40714
|
+
v.playsInline = true;
|
|
40715
|
+
v.autoplay = true;
|
|
40716
|
+
v.style.cssText = "position:absolute;left:-9999px;top:0;width:1px;height:1px;opacity:0;pointer-events:none;";
|
|
40717
|
+
v.srcObject = newStream;
|
|
40718
|
+
warmupVideo = v;
|
|
40719
|
+
(containerRef.current ?? document.body).appendChild(v);
|
|
40720
|
+
v.play().catch(() => {});
|
|
40721
|
+
if (typeof v.requestVideoFrameCallback === "function") warmupRvfc = v.requestVideoFrameCallback(() => commitSwap());
|
|
40722
|
+
v.addEventListener("loadeddata", commitSwap, { once: true });
|
|
40723
|
+
warmupSafety = setTimeout(() => commitSwap(), 1800);
|
|
40500
40724
|
};
|
|
40501
40725
|
pc.ontrack = (event) => {
|
|
40502
40726
|
console.debug("[WebRTC] ontrack (reoffer)", event.track.kind, event.track.readyState);
|
|
40503
40727
|
newStream.addTrack(event.track);
|
|
40504
|
-
|
|
40728
|
+
armSwap();
|
|
40505
40729
|
};
|
|
40506
40730
|
pc.oniceconnectionstatechange = () => {
|
|
40507
40731
|
if (!mountedRef.current) return;
|
|
@@ -40509,10 +40733,11 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
40509
40733
|
console.debug("[WebRTC] ICE state (reoffer):", iceState);
|
|
40510
40734
|
if (iceState === "connected" || iceState === "completed") {
|
|
40511
40735
|
reconnectAttemptsRef.current = 0;
|
|
40512
|
-
|
|
40736
|
+
armSwap();
|
|
40513
40737
|
updateState("playing");
|
|
40514
40738
|
if (videoRef.current?.paused) videoRef.current.play().catch(() => {});
|
|
40515
40739
|
} else if (iceState === "failed" || iceState === "closed") {
|
|
40740
|
+
teardownWarmup();
|
|
40516
40741
|
if (!swappedIn && iceState === "failed") pc.close();
|
|
40517
40742
|
updateState("disconnected");
|
|
40518
40743
|
scheduleReconnect();
|
|
@@ -44590,6 +44815,33 @@ function buildRawTree(data, fetchedAt) {
|
|
|
44590
44815
|
* State). Computed once at module load.
|
|
44591
44816
|
*/
|
|
44592
44817
|
var ALL_DEVICE_CAP_NAMES = Object.freeze([...new Set(_camstack_types.ALL_CAPABILITY_DEFINITIONS.filter((c) => c.scope === "device").map((c) => c.name))].toSorted());
|
|
44818
|
+
/**
|
|
44819
|
+
* The keys of a cap's `runtimeState` object schema, or `[]` when the cap
|
|
44820
|
+
* declares none / it is not an object schema. Read structurally (no Zod
|
|
44821
|
+
* import, no cast) so a self-contained addon's schema instance still works
|
|
44822
|
+
* across the bundle boundary — `ZodObject.shape` is a public getter.
|
|
44823
|
+
*/
|
|
44824
|
+
function runtimeStateKeys(capName) {
|
|
44825
|
+
const rs = _camstack_types.ALL_CAPABILITY_DEFINITIONS.find((c) => c.name === capName)?.runtimeState;
|
|
44826
|
+
if (!rs || typeof rs !== "object") return [];
|
|
44827
|
+
const shape = Reflect.get(rs, "shape");
|
|
44828
|
+
if (!shape || typeof shape !== "object") return [];
|
|
44829
|
+
return Object.keys(shape);
|
|
44830
|
+
}
|
|
44831
|
+
/**
|
|
44832
|
+
* Cold placeholder slice for a bound cap that declares `runtimeState` but
|
|
44833
|
+
* has never written it (e.g. a virtual-doorbell camera that has never rung).
|
|
44834
|
+
* Every declared field is seeded `null` so the tree surfaces the cap's
|
|
44835
|
+
* presence and reads as "never" instead of the cap being OMITTED entirely.
|
|
44836
|
+
* Returns `null` when the cap has no runtime-state schema (nothing to seed).
|
|
44837
|
+
*/
|
|
44838
|
+
function coldRuntimeStateSlice(capName) {
|
|
44839
|
+
const keys = runtimeStateKeys(capName);
|
|
44840
|
+
if (keys.length === 0) return null;
|
|
44841
|
+
const slice = {};
|
|
44842
|
+
for (const key of keys) slice[key] = null;
|
|
44843
|
+
return slice;
|
|
44844
|
+
}
|
|
44593
44845
|
/** Depth at which tree nodes start collapsed. Cap nodes (depth 0) and
|
|
44594
44846
|
* their direct slice keys (depth 1) render expanded; anything nested
|
|
44595
44847
|
* deeper opens on demand. */
|
|
@@ -44636,6 +44888,27 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", onCl
|
|
|
44636
44888
|
(0, react$1.useEffect)(() => {
|
|
44637
44889
|
seededRef.current = false;
|
|
44638
44890
|
}, [deviceId]);
|
|
44891
|
+
const { data: bindings } = trpc.deviceManager.getBindings.useQuery({ deviceId }, { staleTime: 3e4 });
|
|
44892
|
+
(0, react$1.useEffect)(() => {
|
|
44893
|
+
const entries = bindings?.entries;
|
|
44894
|
+
if (!entries || entries.length === 0) return;
|
|
44895
|
+
const now = Date.now();
|
|
44896
|
+
setCapStates((prev) => {
|
|
44897
|
+
let mutated = false;
|
|
44898
|
+
const next = new Map(prev);
|
|
44899
|
+
for (const entry of entries) {
|
|
44900
|
+
if (next.has(entry.capName)) continue;
|
|
44901
|
+
const slice = coldRuntimeStateSlice(entry.capName);
|
|
44902
|
+
if (!slice) continue;
|
|
44903
|
+
next.set(entry.capName, {
|
|
44904
|
+
slice,
|
|
44905
|
+
updatedAt: now
|
|
44906
|
+
});
|
|
44907
|
+
mutated = true;
|
|
44908
|
+
}
|
|
44909
|
+
return mutated ? next : prev;
|
|
44910
|
+
});
|
|
44911
|
+
}, [bindings, deviceId]);
|
|
44639
44912
|
const patchCapState = (0, react$1.useCallback)((capName, slice) => {
|
|
44640
44913
|
setCapStates((prev) => {
|
|
44641
44914
|
const next = new Map(prev);
|
|
@@ -46738,6 +47011,7 @@ exports.DeviceItem = DeviceItem;
|
|
|
46738
47011
|
exports.DeviceList = DeviceList;
|
|
46739
47012
|
exports.DeviceMultiSelectField = DeviceMultiSelectField;
|
|
46740
47013
|
exports.DeviceSelectField = DeviceSelectField;
|
|
47014
|
+
exports.DeviceStepMatrix = DeviceStepMatrix;
|
|
46741
47015
|
exports.Dialog = Dialog;
|
|
46742
47016
|
exports.DialogContent = DialogContent;
|
|
46743
47017
|
exports.DialogDescription = DialogDescription;
|
|
@@ -46971,6 +47245,7 @@ exports.selectedDeviceOptions = selectedDeviceOptions;
|
|
|
46971
47245
|
exports.serializeRecordedCommand = serializeRecordedCommand;
|
|
46972
47246
|
exports.shouldCommit = shouldCommit;
|
|
46973
47247
|
exports.shouldEmit = shouldEmit;
|
|
47248
|
+
exports.shouldUseSingleNode = shouldUseSingleNode;
|
|
46974
47249
|
exports.sortRows = sortRows;
|
|
46975
47250
|
exports.statusIcons = statusIcons;
|
|
46976
47251
|
exports.stripParentNamePrefix = stripParentNamePrefix;
|