@camstack/ui-library 1.2.179 → 1.2.181
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/cap-settings/camera-grid-layout.d.ts +14 -2
- package/dist/index.cjs +96 -78
- package/dist/index.js +96 -78
- package/package.json +1 -1
|
@@ -32,13 +32,25 @@ export interface GridProfileOffer {
|
|
|
32
32
|
readonly profile: 'high' | 'mid' | 'low';
|
|
33
33
|
readonly offered: boolean;
|
|
34
34
|
readonly missingSources: readonly number[];
|
|
35
|
+
/**
|
|
36
|
+
* The canvas this profile composes onto, `WxH`, or empty when it is not
|
|
37
|
+
* offered. DERIVED — the operator no longer types a resolution, so this is
|
|
38
|
+
* the only place the grid's actual size is ever stated.
|
|
39
|
+
*/
|
|
40
|
+
readonly canvas: string;
|
|
41
|
+
/**
|
|
42
|
+
* Whether this profile is PUBLISHED, of the ones the grid could serve.
|
|
43
|
+
*
|
|
44
|
+
* A grid's `high` composes its sources' `high`, so on a 4K fleet it is a 4K
|
|
45
|
+
* canvas built from 4K decodes — an opt-in, not something a viewer's adaptive
|
|
46
|
+
* should be handed by climbing to the top rung it can see.
|
|
47
|
+
*/
|
|
48
|
+
readonly published: boolean;
|
|
35
49
|
}
|
|
36
50
|
export interface GridLayoutView {
|
|
37
51
|
readonly instanceId: string;
|
|
38
52
|
readonly deviceId: number;
|
|
39
53
|
readonly name: string;
|
|
40
|
-
readonly width: number;
|
|
41
|
-
readonly height: number;
|
|
42
54
|
readonly fps: number;
|
|
43
55
|
readonly cells: readonly GridLayoutCell[];
|
|
44
56
|
readonly profiles: readonly GridProfileOffer[];
|
package/dist/index.cjs
CHANGED
|
@@ -16641,7 +16641,9 @@ function readProfileOffer(value) {
|
|
|
16641
16641
|
return {
|
|
16642
16642
|
profile,
|
|
16643
16643
|
offered,
|
|
16644
|
-
missingSources
|
|
16644
|
+
missingSources,
|
|
16645
|
+
canvas: typeof value["canvas"] === "string" ? value["canvas"] : "",
|
|
16646
|
+
published: typeof value["published"] === "boolean" ? value["published"] : offered
|
|
16645
16647
|
};
|
|
16646
16648
|
}
|
|
16647
16649
|
/**
|
|
@@ -16655,8 +16657,8 @@ function readProfileOffer(value) {
|
|
|
16655
16657
|
function parseGridView(raw) {
|
|
16656
16658
|
if (raw === null) return null;
|
|
16657
16659
|
if (!isRecord(raw)) return "unreadable";
|
|
16658
|
-
const { instanceId, deviceId, name,
|
|
16659
|
-
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof
|
|
16660
|
+
const { instanceId, deviceId, name, fps, cells, profiles } = raw;
|
|
16661
|
+
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
|
|
16660
16662
|
const parsed = [];
|
|
16661
16663
|
for (const entry of cells) {
|
|
16662
16664
|
const cell = readCell(entry);
|
|
@@ -16673,8 +16675,6 @@ function parseGridView(raw) {
|
|
|
16673
16675
|
instanceId,
|
|
16674
16676
|
deviceId,
|
|
16675
16677
|
name,
|
|
16676
|
-
width,
|
|
16677
|
-
height,
|
|
16678
16678
|
fps,
|
|
16679
16679
|
cells: parsed,
|
|
16680
16680
|
profiles: offers
|
|
@@ -16888,7 +16888,7 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16888
16888
|
setLoaded("unreadable");
|
|
16889
16889
|
});
|
|
16890
16890
|
}, [dev, reload]);
|
|
16891
|
-
const save = (0, react$1.useCallback)(async () => {
|
|
16891
|
+
const save = (0, react$1.useCallback)(async (extra) => {
|
|
16892
16892
|
const layout = dev?.cameraGridLayout;
|
|
16893
16893
|
if (draft === null || !layout) return;
|
|
16894
16894
|
setSaving(true);
|
|
@@ -16896,10 +16896,9 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16896
16896
|
try {
|
|
16897
16897
|
const outcome = parseGridView(await layout.saveLayout({
|
|
16898
16898
|
name: draft.name,
|
|
16899
|
-
width: draft.width,
|
|
16900
|
-
height: draft.height,
|
|
16901
16899
|
fps: draft.fps,
|
|
16902
|
-
cells: draft.cells.map((cell) => ({ ...cell }))
|
|
16900
|
+
cells: draft.cells.map((cell) => ({ ...cell })),
|
|
16901
|
+
...extra?.publishedProfiles !== void 0 ? { publishedProfiles: [...extra.publishedProfiles] } : {}
|
|
16903
16902
|
}));
|
|
16904
16903
|
if (outcome !== null && outcome !== "unreadable") {
|
|
16905
16904
|
setLoaded(outcome);
|
|
@@ -16920,7 +16919,27 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16920
16919
|
} : cell)
|
|
16921
16920
|
});
|
|
16922
16921
|
}, []);
|
|
16923
|
-
|
|
16922
|
+
/**
|
|
16923
|
+
* The shape the grid will actually come out as.
|
|
16924
|
+
*
|
|
16925
|
+
* The canvas is no longer a field: each profile derives its own from the
|
|
16926
|
+
* cells and the sources' dimensions, and the server reports what it came out
|
|
16927
|
+
* as. Drawing at that aspect is the only way the editor shows the truth —
|
|
16928
|
+
* before, it drew at whatever two numbers had been typed, which is exactly
|
|
16929
|
+
* how a layout could look right here and wrong on screen.
|
|
16930
|
+
*
|
|
16931
|
+
* 16:9 while nothing is offered yet: a grid with no cells has no shape, and
|
|
16932
|
+
* an empty editor has to be drawn at something.
|
|
16933
|
+
*/
|
|
16934
|
+
const canvasAspect = (0, react$1.useMemo)(() => {
|
|
16935
|
+
const offered = loaded === void 0 || loaded === null || loaded === "unreadable" ? [] : loaded.profiles;
|
|
16936
|
+
for (const row of offered) {
|
|
16937
|
+
if (!row.offered) continue;
|
|
16938
|
+
const [width, height] = row.canvas.split("x").map(Number);
|
|
16939
|
+
if (width !== void 0 && height !== void 0 && width > 0 && height > 0) return width / height;
|
|
16940
|
+
}
|
|
16941
|
+
return 16 / 9;
|
|
16942
|
+
}, [loaded]);
|
|
16924
16943
|
if (loaded === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetPanel, {
|
|
16925
16944
|
title: "Grid layout",
|
|
16926
16945
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Grid2x2, { className: "h-3.5 w-3.5" }),
|
|
@@ -17031,66 +17050,48 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
17031
17050
|
}),
|
|
17032
17051
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17033
17052
|
className: "grid grid-cols-4 gap-2 text-[10.5px]",
|
|
17034
|
-
children: [
|
|
17035
|
-
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
|
|
17054
|
-
|
|
17055
|
-
|
|
17056
|
-
|
|
17057
|
-
|
|
17058
|
-
|
|
17059
|
-
|
|
17060
|
-
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
|
|
17065
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
17066
|
-
|
|
17067
|
-
disabled: draft.cells.length >= MAX_CELLS,
|
|
17068
|
-
onClick: () => {
|
|
17069
|
-
const first = cameras.find((camera) => camera.id !== deviceId);
|
|
17070
|
-
if (!first) return;
|
|
17071
|
-
setDraft({
|
|
17072
|
-
...draft,
|
|
17073
|
-
cells: [...draft.cells, {
|
|
17074
|
-
deviceId: first.id,
|
|
17075
|
-
source: FULL_FRAME,
|
|
17076
|
-
cell: clampNormalizedRect({
|
|
17077
|
-
x: .1,
|
|
17078
|
-
y: .1,
|
|
17079
|
-
width: .4,
|
|
17080
|
-
height: .4
|
|
17081
|
-
})
|
|
17082
|
-
}]
|
|
17083
|
-
});
|
|
17084
|
-
setSelected(draft.cells.length);
|
|
17085
|
-
},
|
|
17086
|
-
className: "flex items-center justify-center gap-1 self-end rounded border border-border px-2 py-1 uppercase",
|
|
17087
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), " Cell"]
|
|
17088
|
-
})
|
|
17089
|
-
]
|
|
17053
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabelledNumber, {
|
|
17054
|
+
label: "FPS",
|
|
17055
|
+
value: draft.fps,
|
|
17056
|
+
onChange: (fps) => {
|
|
17057
|
+
setDraft({
|
|
17058
|
+
...draft,
|
|
17059
|
+
fps
|
|
17060
|
+
});
|
|
17061
|
+
}
|
|
17062
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17063
|
+
type: "button",
|
|
17064
|
+
disabled: draft.cells.length >= MAX_CELLS,
|
|
17065
|
+
onClick: () => {
|
|
17066
|
+
const first = cameras.find((camera) => camera.id !== deviceId);
|
|
17067
|
+
if (!first) return;
|
|
17068
|
+
setDraft({
|
|
17069
|
+
...draft,
|
|
17070
|
+
cells: [...draft.cells, {
|
|
17071
|
+
deviceId: first.id,
|
|
17072
|
+
source: FULL_FRAME,
|
|
17073
|
+
cell: clampNormalizedRect({
|
|
17074
|
+
x: .1,
|
|
17075
|
+
y: .1,
|
|
17076
|
+
width: .4,
|
|
17077
|
+
height: .4
|
|
17078
|
+
})
|
|
17079
|
+
}]
|
|
17080
|
+
});
|
|
17081
|
+
setSelected(draft.cells.length);
|
|
17082
|
+
},
|
|
17083
|
+
className: "flex items-center justify-center gap-1 self-end rounded border border-border px-2 py-1 uppercase",
|
|
17084
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), " Cell"]
|
|
17085
|
+
})]
|
|
17090
17086
|
}),
|
|
17091
17087
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridProfileReport, {
|
|
17092
17088
|
offers: loaded.profiles,
|
|
17093
|
-
cameras
|
|
17089
|
+
cameras,
|
|
17090
|
+
onToggle: (profile) => {
|
|
17091
|
+
if (draft === null) return;
|
|
17092
|
+
const current = loaded.profiles.filter((row) => row.published).map((row) => row.profile);
|
|
17093
|
+
save({ publishedProfiles: current.includes(profile) ? current.filter((entry) => entry !== profile) : [...current, profile] });
|
|
17094
|
+
}
|
|
17094
17095
|
}),
|
|
17095
17096
|
draft.cells[selected] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CellInspector, {
|
|
17096
17097
|
cell: draft.cells[selected],
|
|
@@ -17120,7 +17121,7 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
17120
17121
|
* Read-only on purpose: what is on offer is decided by the source cameras'
|
|
17121
17122
|
* assignments, and a control here would be a switch that writes nothing (D62).
|
|
17122
17123
|
*/
|
|
17123
|
-
function GridProfileReport({ offers, cameras }) {
|
|
17124
|
+
function GridProfileReport({ offers, cameras, onToggle }) {
|
|
17124
17125
|
const nameOf = (deviceId) => cameras.find((camera) => camera.id === deviceId)?.name ?? `#${String(deviceId)}`;
|
|
17125
17126
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17126
17127
|
className: "flex flex-col gap-1 rounded border border-border p-2 text-[10.5px]",
|
|
@@ -17129,16 +17130,33 @@ function GridProfileReport({ offers, cameras }) {
|
|
|
17129
17130
|
children: "Published profiles"
|
|
17130
17131
|
}), offers.map((offer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17131
17132
|
className: "flex items-baseline gap-2",
|
|
17132
|
-
children: [
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17138
|
-
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
17133
|
+
children: [
|
|
17134
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17135
|
+
type: "checkbox",
|
|
17136
|
+
checked: offer.published,
|
|
17137
|
+
disabled: !offer.offered,
|
|
17138
|
+
"aria-label": `publish ${offer.profile}`,
|
|
17139
|
+
onChange: () => {
|
|
17140
|
+
onToggle(offer.profile);
|
|
17141
|
+
}
|
|
17142
|
+
}),
|
|
17143
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17144
|
+
className: "w-10 uppercase",
|
|
17145
|
+
children: offer.profile
|
|
17146
|
+
}),
|
|
17147
|
+
offer.offered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
17148
|
+
className: "text-foreground-subtle",
|
|
17149
|
+
children: [
|
|
17150
|
+
offer.canvas,
|
|
17151
|
+
" —",
|
|
17152
|
+
" ",
|
|
17153
|
+
offer.published ? `composed from every source’s ${offer.profile}` : "serviceable, not published"
|
|
17154
|
+
]
|
|
17155
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17156
|
+
className: "text-danger",
|
|
17157
|
+
children: offer.missingSources.length === 0 ? "not offered — this grid has no cells yet" : `not offered — no ${offer.profile} on ${offer.missingSources.map(nameOf).join(", ")}`
|
|
17158
|
+
})
|
|
17159
|
+
]
|
|
17142
17160
|
}, offer.profile))]
|
|
17143
17161
|
});
|
|
17144
17162
|
}
|
package/dist/index.js
CHANGED
|
@@ -16617,7 +16617,9 @@ function readProfileOffer(value) {
|
|
|
16617
16617
|
return {
|
|
16618
16618
|
profile,
|
|
16619
16619
|
offered,
|
|
16620
|
-
missingSources
|
|
16620
|
+
missingSources,
|
|
16621
|
+
canvas: typeof value["canvas"] === "string" ? value["canvas"] : "",
|
|
16622
|
+
published: typeof value["published"] === "boolean" ? value["published"] : offered
|
|
16621
16623
|
};
|
|
16622
16624
|
}
|
|
16623
16625
|
/**
|
|
@@ -16631,8 +16633,8 @@ function readProfileOffer(value) {
|
|
|
16631
16633
|
function parseGridView(raw) {
|
|
16632
16634
|
if (raw === null) return null;
|
|
16633
16635
|
if (!isRecord(raw)) return "unreadable";
|
|
16634
|
-
const { instanceId, deviceId, name,
|
|
16635
|
-
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof
|
|
16636
|
+
const { instanceId, deviceId, name, fps, cells, profiles } = raw;
|
|
16637
|
+
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
|
|
16636
16638
|
const parsed = [];
|
|
16637
16639
|
for (const entry of cells) {
|
|
16638
16640
|
const cell = readCell(entry);
|
|
@@ -16649,8 +16651,6 @@ function parseGridView(raw) {
|
|
|
16649
16651
|
instanceId,
|
|
16650
16652
|
deviceId,
|
|
16651
16653
|
name,
|
|
16652
|
-
width,
|
|
16653
|
-
height,
|
|
16654
16654
|
fps,
|
|
16655
16655
|
cells: parsed,
|
|
16656
16656
|
profiles: offers
|
|
@@ -16864,7 +16864,7 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16864
16864
|
setLoaded("unreadable");
|
|
16865
16865
|
});
|
|
16866
16866
|
}, [dev, reload]);
|
|
16867
|
-
const save = useCallback(async () => {
|
|
16867
|
+
const save = useCallback(async (extra) => {
|
|
16868
16868
|
const layout = dev?.cameraGridLayout;
|
|
16869
16869
|
if (draft === null || !layout) return;
|
|
16870
16870
|
setSaving(true);
|
|
@@ -16872,10 +16872,9 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16872
16872
|
try {
|
|
16873
16873
|
const outcome = parseGridView(await layout.saveLayout({
|
|
16874
16874
|
name: draft.name,
|
|
16875
|
-
width: draft.width,
|
|
16876
|
-
height: draft.height,
|
|
16877
16875
|
fps: draft.fps,
|
|
16878
|
-
cells: draft.cells.map((cell) => ({ ...cell }))
|
|
16876
|
+
cells: draft.cells.map((cell) => ({ ...cell })),
|
|
16877
|
+
...extra?.publishedProfiles !== void 0 ? { publishedProfiles: [...extra.publishedProfiles] } : {}
|
|
16879
16878
|
}));
|
|
16880
16879
|
if (outcome !== null && outcome !== "unreadable") {
|
|
16881
16880
|
setLoaded(outcome);
|
|
@@ -16896,7 +16895,27 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16896
16895
|
} : cell)
|
|
16897
16896
|
});
|
|
16898
16897
|
}, []);
|
|
16899
|
-
|
|
16898
|
+
/**
|
|
16899
|
+
* The shape the grid will actually come out as.
|
|
16900
|
+
*
|
|
16901
|
+
* The canvas is no longer a field: each profile derives its own from the
|
|
16902
|
+
* cells and the sources' dimensions, and the server reports what it came out
|
|
16903
|
+
* as. Drawing at that aspect is the only way the editor shows the truth —
|
|
16904
|
+
* before, it drew at whatever two numbers had been typed, which is exactly
|
|
16905
|
+
* how a layout could look right here and wrong on screen.
|
|
16906
|
+
*
|
|
16907
|
+
* 16:9 while nothing is offered yet: a grid with no cells has no shape, and
|
|
16908
|
+
* an empty editor has to be drawn at something.
|
|
16909
|
+
*/
|
|
16910
|
+
const canvasAspect = useMemo(() => {
|
|
16911
|
+
const offered = loaded === void 0 || loaded === null || loaded === "unreadable" ? [] : loaded.profiles;
|
|
16912
|
+
for (const row of offered) {
|
|
16913
|
+
if (!row.offered) continue;
|
|
16914
|
+
const [width, height] = row.canvas.split("x").map(Number);
|
|
16915
|
+
if (width !== void 0 && height !== void 0 && width > 0 && height > 0) return width / height;
|
|
16916
|
+
}
|
|
16917
|
+
return 16 / 9;
|
|
16918
|
+
}, [loaded]);
|
|
16900
16919
|
if (loaded === void 0) return /* @__PURE__ */ jsx(WidgetPanel, {
|
|
16901
16920
|
title: "Grid layout",
|
|
16902
16921
|
icon: /* @__PURE__ */ jsx(Grid2x2, { className: "h-3.5 w-3.5" }),
|
|
@@ -17007,66 +17026,48 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
17007
17026
|
}),
|
|
17008
17027
|
/* @__PURE__ */ jsxs("div", {
|
|
17009
17028
|
className: "grid grid-cols-4 gap-2 text-[10.5px]",
|
|
17010
|
-
children: [
|
|
17011
|
-
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17034
|
-
|
|
17035
|
-
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
/* @__PURE__ */
|
|
17042
|
-
|
|
17043
|
-
disabled: draft.cells.length >= MAX_CELLS,
|
|
17044
|
-
onClick: () => {
|
|
17045
|
-
const first = cameras.find((camera) => camera.id !== deviceId);
|
|
17046
|
-
if (!first) return;
|
|
17047
|
-
setDraft({
|
|
17048
|
-
...draft,
|
|
17049
|
-
cells: [...draft.cells, {
|
|
17050
|
-
deviceId: first.id,
|
|
17051
|
-
source: FULL_FRAME,
|
|
17052
|
-
cell: clampNormalizedRect({
|
|
17053
|
-
x: .1,
|
|
17054
|
-
y: .1,
|
|
17055
|
-
width: .4,
|
|
17056
|
-
height: .4
|
|
17057
|
-
})
|
|
17058
|
-
}]
|
|
17059
|
-
});
|
|
17060
|
-
setSelected(draft.cells.length);
|
|
17061
|
-
},
|
|
17062
|
-
className: "flex items-center justify-center gap-1 self-end rounded border border-border px-2 py-1 uppercase",
|
|
17063
|
-
children: [/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }), " Cell"]
|
|
17064
|
-
})
|
|
17065
|
-
]
|
|
17029
|
+
children: [/* @__PURE__ */ jsx(LabelledNumber, {
|
|
17030
|
+
label: "FPS",
|
|
17031
|
+
value: draft.fps,
|
|
17032
|
+
onChange: (fps) => {
|
|
17033
|
+
setDraft({
|
|
17034
|
+
...draft,
|
|
17035
|
+
fps
|
|
17036
|
+
});
|
|
17037
|
+
}
|
|
17038
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
17039
|
+
type: "button",
|
|
17040
|
+
disabled: draft.cells.length >= MAX_CELLS,
|
|
17041
|
+
onClick: () => {
|
|
17042
|
+
const first = cameras.find((camera) => camera.id !== deviceId);
|
|
17043
|
+
if (!first) return;
|
|
17044
|
+
setDraft({
|
|
17045
|
+
...draft,
|
|
17046
|
+
cells: [...draft.cells, {
|
|
17047
|
+
deviceId: first.id,
|
|
17048
|
+
source: FULL_FRAME,
|
|
17049
|
+
cell: clampNormalizedRect({
|
|
17050
|
+
x: .1,
|
|
17051
|
+
y: .1,
|
|
17052
|
+
width: .4,
|
|
17053
|
+
height: .4
|
|
17054
|
+
})
|
|
17055
|
+
}]
|
|
17056
|
+
});
|
|
17057
|
+
setSelected(draft.cells.length);
|
|
17058
|
+
},
|
|
17059
|
+
className: "flex items-center justify-center gap-1 self-end rounded border border-border px-2 py-1 uppercase",
|
|
17060
|
+
children: [/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }), " Cell"]
|
|
17061
|
+
})]
|
|
17066
17062
|
}),
|
|
17067
17063
|
/* @__PURE__ */ jsx(GridProfileReport, {
|
|
17068
17064
|
offers: loaded.profiles,
|
|
17069
|
-
cameras
|
|
17065
|
+
cameras,
|
|
17066
|
+
onToggle: (profile) => {
|
|
17067
|
+
if (draft === null) return;
|
|
17068
|
+
const current = loaded.profiles.filter((row) => row.published).map((row) => row.profile);
|
|
17069
|
+
save({ publishedProfiles: current.includes(profile) ? current.filter((entry) => entry !== profile) : [...current, profile] });
|
|
17070
|
+
}
|
|
17070
17071
|
}),
|
|
17071
17072
|
draft.cells[selected] !== void 0 && /* @__PURE__ */ jsx(CellInspector, {
|
|
17072
17073
|
cell: draft.cells[selected],
|
|
@@ -17096,7 +17097,7 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
17096
17097
|
* Read-only on purpose: what is on offer is decided by the source cameras'
|
|
17097
17098
|
* assignments, and a control here would be a switch that writes nothing (D62).
|
|
17098
17099
|
*/
|
|
17099
|
-
function GridProfileReport({ offers, cameras }) {
|
|
17100
|
+
function GridProfileReport({ offers, cameras, onToggle }) {
|
|
17100
17101
|
const nameOf = (deviceId) => cameras.find((camera) => camera.id === deviceId)?.name ?? `#${String(deviceId)}`;
|
|
17101
17102
|
return /* @__PURE__ */ jsxs("div", {
|
|
17102
17103
|
className: "flex flex-col gap-1 rounded border border-border p-2 text-[10.5px]",
|
|
@@ -17105,16 +17106,33 @@ function GridProfileReport({ offers, cameras }) {
|
|
|
17105
17106
|
children: "Published profiles"
|
|
17106
17107
|
}), offers.map((offer) => /* @__PURE__ */ jsxs("div", {
|
|
17107
17108
|
className: "flex items-baseline gap-2",
|
|
17108
|
-
children: [
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17109
|
+
children: [
|
|
17110
|
+
/* @__PURE__ */ jsx("input", {
|
|
17111
|
+
type: "checkbox",
|
|
17112
|
+
checked: offer.published,
|
|
17113
|
+
disabled: !offer.offered,
|
|
17114
|
+
"aria-label": `publish ${offer.profile}`,
|
|
17115
|
+
onChange: () => {
|
|
17116
|
+
onToggle(offer.profile);
|
|
17117
|
+
}
|
|
17118
|
+
}),
|
|
17119
|
+
/* @__PURE__ */ jsx("span", {
|
|
17120
|
+
className: "w-10 uppercase",
|
|
17121
|
+
children: offer.profile
|
|
17122
|
+
}),
|
|
17123
|
+
offer.offered ? /* @__PURE__ */ jsxs("span", {
|
|
17124
|
+
className: "text-foreground-subtle",
|
|
17125
|
+
children: [
|
|
17126
|
+
offer.canvas,
|
|
17127
|
+
" —",
|
|
17128
|
+
" ",
|
|
17129
|
+
offer.published ? `composed from every source’s ${offer.profile}` : "serviceable, not published"
|
|
17130
|
+
]
|
|
17131
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
17132
|
+
className: "text-danger",
|
|
17133
|
+
children: offer.missingSources.length === 0 ? "not offered — this grid has no cells yet" : `not offered — no ${offer.profile} on ${offer.missingSources.map(nameOf).join(", ")}`
|
|
17134
|
+
})
|
|
17135
|
+
]
|
|
17118
17136
|
}, offer.profile))]
|
|
17119
17137
|
});
|
|
17120
17138
|
}
|