@camstack/addon-model-studio 1.1.323 → 1.1.324

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.
Files changed (19) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/{MotionZonesSettings-CBVJdmZ_.mjs → MotionZonesSettings-BFSzpDlc.mjs} +2 -2
  3. package/dist/{PrivacyMaskSettings-CFuZa1xW.mjs → PrivacyMaskSettings-bzbS1q77.mjs} +4 -4
  4. package/dist/{SceneMonitorEditor-DHYl0WUF.mjs → SceneMonitorEditor-5jtlWR_n.mjs} +3 -3
  5. package/dist/_stub.js +10 -10
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-DiTB72W6.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-DMLYtwXQ.mjs} +3 -3
  7. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D8h8qfbd.mjs +26 -0
  8. package/dist/{hostInit-BL6ZxXXr.mjs → hostInit-BQhIepOe.mjs} +2 -2
  9. package/dist/model-studio.addon.js +285 -3
  10. package/dist/model-studio.addon.mjs +285 -3
  11. package/dist/{player-overlays-CDEmtjZg.mjs → player-overlays-DwnklNvS.mjs} +1 -1
  12. package/dist/remoteEntry.js +1 -1
  13. package/dist/{responsive-B_pKmFhb.mjs → responsive-DFhiWOST.mjs} +1 -1
  14. package/dist/{scene-monitor-copy-KV9Ti6_0.mjs → scene-monitor-copy-DVaiZyk_.mjs} +1 -1
  15. package/dist/{square-ByW1TPIi.mjs → square-CL1GZCDy.mjs} +1 -1
  16. package/dist/{trash-2-9gFkA-Zu.mjs → trash-2-Bs4Zqar1.mjs} +1 -1
  17. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-B880BvK0.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-B3z4_zyV.mjs} +1 -1
  18. package/package.json +1 -1
  19. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B3TmOWWR.mjs +0 -26
@@ -5850,7 +5850,7 @@ var ZodIssueCode = {
5850
5850
  var ZodFirstPartyTypeKind;
5851
5851
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5852
5852
  //#endregion
5853
- //#region ../types/dist/sleep-6eG9C8Xl.mjs
5853
+ //#region ../types/dist/sleep-63gM_1JR.mjs
5854
5854
  /**
5855
5855
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5856
5856
  * window to float samples (D455).
@@ -9211,6 +9211,7 @@ var CAP_NAMES = Object.freeze([
9211
9211
  "recording-signal",
9212
9212
  "records",
9213
9213
  "remote",
9214
+ "remote-layout-components",
9214
9215
  "running-state",
9215
9216
  "scene-monitor",
9216
9217
  "script-runner",
@@ -58816,6 +58817,30 @@ object({
58816
58817
  at: number()
58817
58818
  }).nullable()
58818
58819
  });
58820
+ var RemoteButtonShapeSchema = _enum([
58821
+ "rect",
58822
+ "rounded",
58823
+ "round",
58824
+ "pill"
58825
+ ]);
58826
+ var RemoteButtonToneSchema = _enum([
58827
+ "neutral",
58828
+ "accent",
58829
+ "danger",
58830
+ "red",
58831
+ "green",
58832
+ "yellow",
58833
+ "blue"
58834
+ ]);
58835
+ var RemoteGroupFrameSchema = _enum([
58836
+ "none",
58837
+ "panel",
58838
+ "divider",
58839
+ "dpad",
58840
+ "rocker-v",
58841
+ "rocker-h",
58842
+ "keypad"
58843
+ ]);
58819
58844
  var RemoteLayoutButtonSchema = object({
58820
58845
  id: string().min(1).max(40),
58821
58846
  /** 0-based grid position. */
@@ -58825,12 +58850,94 @@ var RemoteLayoutButtonSchema = object({
58825
58850
  colSpan: number().int().min(1).max(24),
58826
58851
  icon: string().min(1).max(60).optional(),
58827
58852
  label: string().min(1).max(40).optional(),
58828
- role: RemoteRoleSchema
58853
+ role: RemoteRoleSchema,
58854
+ /** Absent = the plain rendering every layout had before shapes existed. */
58855
+ shape: RemoteButtonShapeSchema.optional(),
58856
+ tone: RemoteButtonToneSchema.optional(),
58857
+ /** The group this button belongs to; it must lie inside that group's rectangle. */
58858
+ groupId: string().min(1).max(40).optional()
58859
+ });
58860
+ /**
58861
+ * A rectangle of the grid that moves as ONE thing and is drawn with a frame: a
58862
+ * titled panel, a section divider, a D-pad ring. `componentId` names the palette
58863
+ * entry it came from (informational: the layout is self-contained).
58864
+ */
58865
+ var RemoteLayoutGroupSchema = object({
58866
+ id: string().min(1).max(40),
58867
+ componentId: string().min(1).max(80),
58868
+ frame: RemoteGroupFrameSchema,
58869
+ row: number().int().min(0).max(24),
58870
+ col: number().int().min(0).max(24),
58871
+ rowSpan: number().int().min(1).max(24),
58872
+ colSpan: number().int().min(1).max(24),
58873
+ title: string().min(1).max(40).optional()
58829
58874
  });
58875
+ function rectsOverlap(a, b) {
58876
+ return a.row < b.row + b.rowSpan && b.row < a.row + a.rowSpan && a.col < b.col + b.colSpan && b.col < a.col + a.colSpan;
58877
+ }
58878
+ function rectContains(outer, inner) {
58879
+ return inner.row >= outer.row && inner.col >= outer.col && inner.row + inner.rowSpan <= outer.row + outer.rowSpan && inner.col + inner.colSpan <= outer.col + outer.colSpan;
58880
+ }
58881
+ /**
58882
+ * The group rules of a layout: inside the grid, no two groups overlap, a
58883
+ * member lies inside its group, a divider holds nothing, and a button that is
58884
+ * not a member never sits inside someone else's rectangle (a group moves as
58885
+ * one thing, so a stray key under it would be left behind).
58886
+ */
58887
+ function refineGroups(grid, ctx) {
58888
+ const groups = grid.groups ?? [];
58889
+ const byId = /* @__PURE__ */ new Map();
58890
+ const fail = (message, path) => {
58891
+ ctx.addIssue({
58892
+ code: "custom",
58893
+ message,
58894
+ path
58895
+ });
58896
+ };
58897
+ groups.forEach((group, index) => {
58898
+ if (byId.has(group.id)) fail(`group id "${group.id}" is used twice`, [
58899
+ "groups",
58900
+ index,
58901
+ "id"
58902
+ ]);
58903
+ byId.set(group.id, group);
58904
+ if (group.row + group.rowSpan > grid.rows || group.col + group.colSpan > grid.columns) fail(`group "${group.id}" leaves the grid`, ["groups", index]);
58905
+ for (let other = 0; other < index; other += 1) {
58906
+ const earlier = groups[other];
58907
+ if (earlier !== void 0 && rectsOverlap(earlier, group)) {
58908
+ fail(`groups "${earlier.id}" and "${group.id}" overlap`, ["groups", index]);
58909
+ return;
58910
+ }
58911
+ }
58912
+ });
58913
+ grid.buttons.forEach((button, index) => {
58914
+ if (button.groupId !== void 0) {
58915
+ const group = byId.get(button.groupId);
58916
+ if (group === void 0) {
58917
+ fail(`button "${button.id}" names the unknown group "${button.groupId}"`, [
58918
+ "buttons",
58919
+ index,
58920
+ "groupId"
58921
+ ]);
58922
+ return;
58923
+ }
58924
+ if (group.frame === "divider") fail(`a divider group ("${group.id}") holds no buttons`, [
58925
+ "buttons",
58926
+ index,
58927
+ "groupId"
58928
+ ]);
58929
+ if (!rectContains(group, button)) fail(`button "${button.id}" lies outside its group "${group.id}"`, ["buttons", index]);
58930
+ return;
58931
+ }
58932
+ const foreign = groups.find((group) => rectsOverlap(group, button));
58933
+ if (foreign !== void 0) fail(`button "${button.id}" lies inside group "${foreign.id}" but is not a member`, ["buttons", index]);
58934
+ });
58935
+ }
58830
58936
  object({
58831
58937
  columns: number().int().min(1).max(24),
58832
58938
  rows: number().int().min(1).max(24),
58833
- buttons: array(RemoteLayoutButtonSchema).max(200)
58939
+ buttons: array(RemoteLayoutButtonSchema).max(200),
58940
+ groups: array(RemoteLayoutGroupSchema).max(50).optional()
58834
58941
  }).superRefine((grid, ctx) => {
58835
58942
  const ids = /* @__PURE__ */ new Set();
58836
58943
  const taken = /* @__PURE__ */ new Map();
@@ -58867,6 +58974,7 @@ object({
58867
58974
  taken.set(cell, button.id);
58868
58975
  }
58869
58976
  });
58977
+ refineGroups(grid, ctx);
58870
58978
  });
58871
58979
  var RemoteBoundCommandSchema = object({
58872
58980
  role: RemoteRoleSchema,
@@ -58998,6 +59106,91 @@ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSche
58998
59106
  kind: "mutation",
58999
59107
  auth: "admin"
59000
59108
  });
59109
+ var RemoteComponentCategorySchema = _enum([
59110
+ "power",
59111
+ "navigation",
59112
+ "volume",
59113
+ "numbers",
59114
+ "media",
59115
+ "colour",
59116
+ "apps",
59117
+ "layout"
59118
+ ]);
59119
+ /** `<namespace>:<name>`, both lowercase slugs — `core:dpad`, `<addonId>:<name>`. */
59120
+ var COMPONENT_ID_RE = /^[a-z0-9][a-z0-9_.-]*:[a-z0-9][a-z0-9_-]*$/;
59121
+ var RemoteComponentButtonSchema = object({
59122
+ /** Joined to the instance id when the component is inserted. */
59123
+ idSuffix: string().regex(/^[a-z0-9][a-z0-9_-]{0,19}$/, { message: "a short lowercase slug" }),
59124
+ row: number().int().min(0).max(24),
59125
+ col: number().int().min(0).max(24),
59126
+ rowSpan: number().int().min(1).max(24),
59127
+ colSpan: number().int().min(1).max(24),
59128
+ role: RemoteRoleSchema,
59129
+ icon: string().min(1).max(60).optional(),
59130
+ label: string().min(1).max(40).optional(),
59131
+ shape: RemoteButtonShapeSchema.optional(),
59132
+ tone: RemoteButtonToneSchema.optional()
59133
+ });
59134
+ var RemoteLayoutComponentSchema = object({
59135
+ id: string().max(80).regex(COMPONENT_ID_RE, { message: "<namespace>:<name>, lowercase" }),
59136
+ name: string().min(1).max(60),
59137
+ nameI18n: object({
59138
+ en: string().min(1).max(60),
59139
+ it: string().min(1).max(60)
59140
+ }).optional(),
59141
+ /** A display icon key, resolved by the UI like a button icon. */
59142
+ icon: string().min(1).max(60),
59143
+ category: RemoteComponentCategorySchema,
59144
+ size: object({
59145
+ rows: number().int().min(1).max(24),
59146
+ cols: number().int().min(1).max(24)
59147
+ }),
59148
+ /** The frame drawn around the whole component, or null for loose buttons. */
59149
+ group: object({
59150
+ frame: RemoteGroupFrameSchema,
59151
+ title: string().min(1).max(40).optional()
59152
+ }).nullable(),
59153
+ buttons: array(RemoteComponentButtonSchema).max(40)
59154
+ }).superRefine((component, ctx) => {
59155
+ const suffixes = /* @__PURE__ */ new Set();
59156
+ component.buttons.forEach((button, index) => {
59157
+ if (suffixes.has(button.idSuffix)) ctx.addIssue({
59158
+ code: "custom",
59159
+ message: `button "${button.idSuffix}" is listed twice`,
59160
+ path: [
59161
+ "buttons",
59162
+ index,
59163
+ "idSuffix"
59164
+ ]
59165
+ });
59166
+ suffixes.add(button.idSuffix);
59167
+ if (button.row + button.rowSpan > component.size.rows || button.col + button.colSpan > component.size.cols) {
59168
+ ctx.addIssue({
59169
+ code: "custom",
59170
+ message: `button "${button.idSuffix}" leaves the component`,
59171
+ path: ["buttons", index]
59172
+ });
59173
+ return;
59174
+ }
59175
+ for (let other = 0; other < index; other += 1) {
59176
+ const earlier = component.buttons[other];
59177
+ if (earlier !== void 0 && rectsOverlap(earlier, button)) {
59178
+ ctx.addIssue({
59179
+ code: "custom",
59180
+ message: `buttons "${earlier.idSuffix}" and "${button.idSuffix}" overlap`,
59181
+ path: ["buttons", index]
59182
+ });
59183
+ return;
59184
+ }
59185
+ }
59186
+ });
59187
+ if (component.group?.frame === "divider" && component.buttons.length > 0) ctx.addIssue({
59188
+ code: "custom",
59189
+ message: "a divider component holds no buttons",
59190
+ path: ["buttons"]
59191
+ });
59192
+ });
59193
+ method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
59001
59194
  DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _void(), {
59002
59195
  kind: "mutation",
59003
59196
  auth: "protected"
@@ -67933,6 +68126,87 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
67933
68126
  kind: "mutation",
67934
68127
  auth: "admin"
67935
68128
  });
68129
+ var btn = (idSuffix, row, col, init) => ({
68130
+ idSuffix,
68131
+ row,
68132
+ col,
68133
+ rowSpan: 1,
68134
+ colSpan: 1,
68135
+ ...init
68136
+ });
68137
+ function rocker(id, name, it, frame, title, buttons, size) {
68138
+ return {
68139
+ id,
68140
+ name,
68141
+ nameI18n: {
68142
+ en: name,
68143
+ it
68144
+ },
68145
+ icon: frame === "rocker-v" ? "chevrons-up-down" : "chevrons-left-right",
68146
+ category: id.includes("channel") || id.includes("volume") ? "volume" : "navigation",
68147
+ size,
68148
+ group: title === null ? { frame } : {
68149
+ frame,
68150
+ title
68151
+ },
68152
+ buttons
68153
+ };
68154
+ }
68155
+ btn("power", 0, 0, {
68156
+ role: "power",
68157
+ shape: "round",
68158
+ tone: "danger"
68159
+ }), btn("up", 0, 1, { role: "up" }), btn("left", 1, 0, { role: "left" }), btn("ok", 1, 1, {
68160
+ role: "ok",
68161
+ shape: "round",
68162
+ tone: "accent"
68163
+ }), btn("right", 1, 2, { role: "right" }), btn("down", 2, 1, { role: "down" }), rocker("core:rocker-volume-v", "Volume rocker", "Bilanciere volume", "rocker-v", "VOL", [btn("up", 0, 0, {
68164
+ role: "volume_up",
68165
+ shape: "pill"
68166
+ }), btn("down", 1, 0, {
68167
+ role: "volume_down",
68168
+ shape: "pill"
68169
+ })], {
68170
+ rows: 2,
68171
+ cols: 1
68172
+ }), rocker("core:rocker-channel-v", "Channel rocker", "Bilanciere canali", "rocker-v", "CH", [btn("up", 0, 0, {
68173
+ role: "channel_up",
68174
+ shape: "pill"
68175
+ }), btn("down", 1, 0, {
68176
+ role: "channel_down",
68177
+ shape: "pill"
68178
+ })], {
68179
+ rows: 2,
68180
+ cols: 1
68181
+ }), rocker("core:rocker-h", "Minus / plus rocker", "Bilanciere meno / più", "rocker-h", null, [btn("minus", 0, 0, {
68182
+ role: "custom:minus",
68183
+ label: "−",
68184
+ shape: "pill"
68185
+ }), btn("plus", 0, 1, {
68186
+ role: "custom:plus",
68187
+ label: "+",
68188
+ shape: "pill"
68189
+ })], {
68190
+ rows: 1,
68191
+ cols: 2
68192
+ }), [...Array.from({ length: 9 }, (_, i) => btn(`d${i + 1}`, Math.floor(i / 3), i % 3, { role: `digit_${i + 1}` })), btn("d0", 3, 1, { role: "digit_0" })], btn("red", 0, 0, {
68193
+ role: "custom:color_red",
68194
+ tone: "red"
68195
+ }), btn("green", 0, 1, {
68196
+ role: "custom:color_green",
68197
+ tone: "green"
68198
+ }), btn("yellow", 0, 2, {
68199
+ role: "custom:color_yellow",
68200
+ tone: "yellow"
68201
+ }), btn("blue", 0, 3, {
68202
+ role: "custom:color_blue",
68203
+ tone: "blue"
68204
+ }), btn("rewind", 0, 0, { role: "rewind" }), btn("play", 0, 1, { role: "play" }), btn("pause", 0, 2, { role: "pause" }), btn("forward", 0, 3, { role: "fast_forward" }), btn("stop", 0, 4, { role: "stop" }), [
68205
+ 1,
68206
+ 2,
68207
+ 3,
68208
+ 4
68209
+ ].map((n) => btn(`app${n}`, 0, n - 1, { role: `custom:app_slot_${n}` })), btn("back", 0, 0, { role: "back" }), btn("home", 0, 1, { role: "home" }), btn("menu", 0, 2, { role: "menu" });
67936
68210
  discriminatedUnion("kind", [object({
67937
68211
  kind: literal("raw"),
67938
68212
  timings: array(number().int()),
@@ -75895,6 +76169,13 @@ Object.freeze({
75895
76169
  access: "create",
75896
76170
  role: "admin"
75897
76171
  },
76172
+ "remoteLayoutComponents.listComponents": {
76173
+ capName: "remote-layout-components",
76174
+ capScope: "system",
76175
+ addonId: null,
76176
+ access: "view",
76177
+ role: "admin"
76178
+ },
75898
76179
  "runningState.getStatus": {
75899
76180
  capName: "running-state",
75900
76181
  capScope: "device",
@@ -82071,6 +82352,7 @@ var SYSTEM_CAP_VOCABULARY = {
82071
82352
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
82072
82353
  records: system(INTERNAL),
82073
82354
  remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
82355
+ "remote-layout-components": system("the palette of the remote template editor — read-only data, never actuated"),
82074
82356
  "server-management": system(CLUSTER),
82075
82357
  "settings-store": system(INTERNAL),
82076
82358
  "smtp-provider": system(INTERNAL),
@@ -5824,7 +5824,7 @@ var ZodIssueCode = {
5824
5824
  var ZodFirstPartyTypeKind;
5825
5825
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5826
5826
  //#endregion
5827
- //#region ../types/dist/sleep-6eG9C8Xl.mjs
5827
+ //#region ../types/dist/sleep-63gM_1JR.mjs
5828
5828
  /**
5829
5829
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5830
5830
  * window to float samples (D455).
@@ -9185,6 +9185,7 @@ var CAP_NAMES = Object.freeze([
9185
9185
  "recording-signal",
9186
9186
  "records",
9187
9187
  "remote",
9188
+ "remote-layout-components",
9188
9189
  "running-state",
9189
9190
  "scene-monitor",
9190
9191
  "script-runner",
@@ -58790,6 +58791,30 @@ object({
58790
58791
  at: number()
58791
58792
  }).nullable()
58792
58793
  });
58794
+ var RemoteButtonShapeSchema = _enum([
58795
+ "rect",
58796
+ "rounded",
58797
+ "round",
58798
+ "pill"
58799
+ ]);
58800
+ var RemoteButtonToneSchema = _enum([
58801
+ "neutral",
58802
+ "accent",
58803
+ "danger",
58804
+ "red",
58805
+ "green",
58806
+ "yellow",
58807
+ "blue"
58808
+ ]);
58809
+ var RemoteGroupFrameSchema = _enum([
58810
+ "none",
58811
+ "panel",
58812
+ "divider",
58813
+ "dpad",
58814
+ "rocker-v",
58815
+ "rocker-h",
58816
+ "keypad"
58817
+ ]);
58793
58818
  var RemoteLayoutButtonSchema = object({
58794
58819
  id: string().min(1).max(40),
58795
58820
  /** 0-based grid position. */
@@ -58799,12 +58824,94 @@ var RemoteLayoutButtonSchema = object({
58799
58824
  colSpan: number().int().min(1).max(24),
58800
58825
  icon: string().min(1).max(60).optional(),
58801
58826
  label: string().min(1).max(40).optional(),
58802
- role: RemoteRoleSchema
58827
+ role: RemoteRoleSchema,
58828
+ /** Absent = the plain rendering every layout had before shapes existed. */
58829
+ shape: RemoteButtonShapeSchema.optional(),
58830
+ tone: RemoteButtonToneSchema.optional(),
58831
+ /** The group this button belongs to; it must lie inside that group's rectangle. */
58832
+ groupId: string().min(1).max(40).optional()
58833
+ });
58834
+ /**
58835
+ * A rectangle of the grid that moves as ONE thing and is drawn with a frame: a
58836
+ * titled panel, a section divider, a D-pad ring. `componentId` names the palette
58837
+ * entry it came from (informational: the layout is self-contained).
58838
+ */
58839
+ var RemoteLayoutGroupSchema = object({
58840
+ id: string().min(1).max(40),
58841
+ componentId: string().min(1).max(80),
58842
+ frame: RemoteGroupFrameSchema,
58843
+ row: number().int().min(0).max(24),
58844
+ col: number().int().min(0).max(24),
58845
+ rowSpan: number().int().min(1).max(24),
58846
+ colSpan: number().int().min(1).max(24),
58847
+ title: string().min(1).max(40).optional()
58803
58848
  });
58849
+ function rectsOverlap(a, b) {
58850
+ return a.row < b.row + b.rowSpan && b.row < a.row + a.rowSpan && a.col < b.col + b.colSpan && b.col < a.col + a.colSpan;
58851
+ }
58852
+ function rectContains(outer, inner) {
58853
+ return inner.row >= outer.row && inner.col >= outer.col && inner.row + inner.rowSpan <= outer.row + outer.rowSpan && inner.col + inner.colSpan <= outer.col + outer.colSpan;
58854
+ }
58855
+ /**
58856
+ * The group rules of a layout: inside the grid, no two groups overlap, a
58857
+ * member lies inside its group, a divider holds nothing, and a button that is
58858
+ * not a member never sits inside someone else's rectangle (a group moves as
58859
+ * one thing, so a stray key under it would be left behind).
58860
+ */
58861
+ function refineGroups(grid, ctx) {
58862
+ const groups = grid.groups ?? [];
58863
+ const byId = /* @__PURE__ */ new Map();
58864
+ const fail = (message, path) => {
58865
+ ctx.addIssue({
58866
+ code: "custom",
58867
+ message,
58868
+ path
58869
+ });
58870
+ };
58871
+ groups.forEach((group, index) => {
58872
+ if (byId.has(group.id)) fail(`group id "${group.id}" is used twice`, [
58873
+ "groups",
58874
+ index,
58875
+ "id"
58876
+ ]);
58877
+ byId.set(group.id, group);
58878
+ if (group.row + group.rowSpan > grid.rows || group.col + group.colSpan > grid.columns) fail(`group "${group.id}" leaves the grid`, ["groups", index]);
58879
+ for (let other = 0; other < index; other += 1) {
58880
+ const earlier = groups[other];
58881
+ if (earlier !== void 0 && rectsOverlap(earlier, group)) {
58882
+ fail(`groups "${earlier.id}" and "${group.id}" overlap`, ["groups", index]);
58883
+ return;
58884
+ }
58885
+ }
58886
+ });
58887
+ grid.buttons.forEach((button, index) => {
58888
+ if (button.groupId !== void 0) {
58889
+ const group = byId.get(button.groupId);
58890
+ if (group === void 0) {
58891
+ fail(`button "${button.id}" names the unknown group "${button.groupId}"`, [
58892
+ "buttons",
58893
+ index,
58894
+ "groupId"
58895
+ ]);
58896
+ return;
58897
+ }
58898
+ if (group.frame === "divider") fail(`a divider group ("${group.id}") holds no buttons`, [
58899
+ "buttons",
58900
+ index,
58901
+ "groupId"
58902
+ ]);
58903
+ if (!rectContains(group, button)) fail(`button "${button.id}" lies outside its group "${group.id}"`, ["buttons", index]);
58904
+ return;
58905
+ }
58906
+ const foreign = groups.find((group) => rectsOverlap(group, button));
58907
+ if (foreign !== void 0) fail(`button "${button.id}" lies inside group "${foreign.id}" but is not a member`, ["buttons", index]);
58908
+ });
58909
+ }
58804
58910
  object({
58805
58911
  columns: number().int().min(1).max(24),
58806
58912
  rows: number().int().min(1).max(24),
58807
- buttons: array(RemoteLayoutButtonSchema).max(200)
58913
+ buttons: array(RemoteLayoutButtonSchema).max(200),
58914
+ groups: array(RemoteLayoutGroupSchema).max(50).optional()
58808
58915
  }).superRefine((grid, ctx) => {
58809
58916
  const ids = /* @__PURE__ */ new Set();
58810
58917
  const taken = /* @__PURE__ */ new Map();
@@ -58841,6 +58948,7 @@ object({
58841
58948
  taken.set(cell, button.id);
58842
58949
  }
58843
58950
  });
58951
+ refineGroups(grid, ctx);
58844
58952
  });
58845
58953
  var RemoteBoundCommandSchema = object({
58846
58954
  role: RemoteRoleSchema,
@@ -58972,6 +59080,91 @@ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSche
58972
59080
  kind: "mutation",
58973
59081
  auth: "admin"
58974
59082
  });
59083
+ var RemoteComponentCategorySchema = _enum([
59084
+ "power",
59085
+ "navigation",
59086
+ "volume",
59087
+ "numbers",
59088
+ "media",
59089
+ "colour",
59090
+ "apps",
59091
+ "layout"
59092
+ ]);
59093
+ /** `<namespace>:<name>`, both lowercase slugs — `core:dpad`, `<addonId>:<name>`. */
59094
+ var COMPONENT_ID_RE = /^[a-z0-9][a-z0-9_.-]*:[a-z0-9][a-z0-9_-]*$/;
59095
+ var RemoteComponentButtonSchema = object({
59096
+ /** Joined to the instance id when the component is inserted. */
59097
+ idSuffix: string().regex(/^[a-z0-9][a-z0-9_-]{0,19}$/, { message: "a short lowercase slug" }),
59098
+ row: number().int().min(0).max(24),
59099
+ col: number().int().min(0).max(24),
59100
+ rowSpan: number().int().min(1).max(24),
59101
+ colSpan: number().int().min(1).max(24),
59102
+ role: RemoteRoleSchema,
59103
+ icon: string().min(1).max(60).optional(),
59104
+ label: string().min(1).max(40).optional(),
59105
+ shape: RemoteButtonShapeSchema.optional(),
59106
+ tone: RemoteButtonToneSchema.optional()
59107
+ });
59108
+ var RemoteLayoutComponentSchema = object({
59109
+ id: string().max(80).regex(COMPONENT_ID_RE, { message: "<namespace>:<name>, lowercase" }),
59110
+ name: string().min(1).max(60),
59111
+ nameI18n: object({
59112
+ en: string().min(1).max(60),
59113
+ it: string().min(1).max(60)
59114
+ }).optional(),
59115
+ /** A display icon key, resolved by the UI like a button icon. */
59116
+ icon: string().min(1).max(60),
59117
+ category: RemoteComponentCategorySchema,
59118
+ size: object({
59119
+ rows: number().int().min(1).max(24),
59120
+ cols: number().int().min(1).max(24)
59121
+ }),
59122
+ /** The frame drawn around the whole component, or null for loose buttons. */
59123
+ group: object({
59124
+ frame: RemoteGroupFrameSchema,
59125
+ title: string().min(1).max(40).optional()
59126
+ }).nullable(),
59127
+ buttons: array(RemoteComponentButtonSchema).max(40)
59128
+ }).superRefine((component, ctx) => {
59129
+ const suffixes = /* @__PURE__ */ new Set();
59130
+ component.buttons.forEach((button, index) => {
59131
+ if (suffixes.has(button.idSuffix)) ctx.addIssue({
59132
+ code: "custom",
59133
+ message: `button "${button.idSuffix}" is listed twice`,
59134
+ path: [
59135
+ "buttons",
59136
+ index,
59137
+ "idSuffix"
59138
+ ]
59139
+ });
59140
+ suffixes.add(button.idSuffix);
59141
+ if (button.row + button.rowSpan > component.size.rows || button.col + button.colSpan > component.size.cols) {
59142
+ ctx.addIssue({
59143
+ code: "custom",
59144
+ message: `button "${button.idSuffix}" leaves the component`,
59145
+ path: ["buttons", index]
59146
+ });
59147
+ return;
59148
+ }
59149
+ for (let other = 0; other < index; other += 1) {
59150
+ const earlier = component.buttons[other];
59151
+ if (earlier !== void 0 && rectsOverlap(earlier, button)) {
59152
+ ctx.addIssue({
59153
+ code: "custom",
59154
+ message: `buttons "${earlier.idSuffix}" and "${button.idSuffix}" overlap`,
59155
+ path: ["buttons", index]
59156
+ });
59157
+ return;
59158
+ }
59159
+ }
59160
+ });
59161
+ if (component.group?.frame === "divider" && component.buttons.length > 0) ctx.addIssue({
59162
+ code: "custom",
59163
+ message: "a divider component holds no buttons",
59164
+ path: ["buttons"]
59165
+ });
59166
+ });
59167
+ method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
58975
59168
  DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _void(), {
58976
59169
  kind: "mutation",
58977
59170
  auth: "protected"
@@ -67907,6 +68100,87 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
67907
68100
  kind: "mutation",
67908
68101
  auth: "admin"
67909
68102
  });
68103
+ var btn = (idSuffix, row, col, init) => ({
68104
+ idSuffix,
68105
+ row,
68106
+ col,
68107
+ rowSpan: 1,
68108
+ colSpan: 1,
68109
+ ...init
68110
+ });
68111
+ function rocker(id, name, it, frame, title, buttons, size) {
68112
+ return {
68113
+ id,
68114
+ name,
68115
+ nameI18n: {
68116
+ en: name,
68117
+ it
68118
+ },
68119
+ icon: frame === "rocker-v" ? "chevrons-up-down" : "chevrons-left-right",
68120
+ category: id.includes("channel") || id.includes("volume") ? "volume" : "navigation",
68121
+ size,
68122
+ group: title === null ? { frame } : {
68123
+ frame,
68124
+ title
68125
+ },
68126
+ buttons
68127
+ };
68128
+ }
68129
+ btn("power", 0, 0, {
68130
+ role: "power",
68131
+ shape: "round",
68132
+ tone: "danger"
68133
+ }), btn("up", 0, 1, { role: "up" }), btn("left", 1, 0, { role: "left" }), btn("ok", 1, 1, {
68134
+ role: "ok",
68135
+ shape: "round",
68136
+ tone: "accent"
68137
+ }), btn("right", 1, 2, { role: "right" }), btn("down", 2, 1, { role: "down" }), rocker("core:rocker-volume-v", "Volume rocker", "Bilanciere volume", "rocker-v", "VOL", [btn("up", 0, 0, {
68138
+ role: "volume_up",
68139
+ shape: "pill"
68140
+ }), btn("down", 1, 0, {
68141
+ role: "volume_down",
68142
+ shape: "pill"
68143
+ })], {
68144
+ rows: 2,
68145
+ cols: 1
68146
+ }), rocker("core:rocker-channel-v", "Channel rocker", "Bilanciere canali", "rocker-v", "CH", [btn("up", 0, 0, {
68147
+ role: "channel_up",
68148
+ shape: "pill"
68149
+ }), btn("down", 1, 0, {
68150
+ role: "channel_down",
68151
+ shape: "pill"
68152
+ })], {
68153
+ rows: 2,
68154
+ cols: 1
68155
+ }), rocker("core:rocker-h", "Minus / plus rocker", "Bilanciere meno / più", "rocker-h", null, [btn("minus", 0, 0, {
68156
+ role: "custom:minus",
68157
+ label: "−",
68158
+ shape: "pill"
68159
+ }), btn("plus", 0, 1, {
68160
+ role: "custom:plus",
68161
+ label: "+",
68162
+ shape: "pill"
68163
+ })], {
68164
+ rows: 1,
68165
+ cols: 2
68166
+ }), [...Array.from({ length: 9 }, (_, i) => btn(`d${i + 1}`, Math.floor(i / 3), i % 3, { role: `digit_${i + 1}` })), btn("d0", 3, 1, { role: "digit_0" })], btn("red", 0, 0, {
68167
+ role: "custom:color_red",
68168
+ tone: "red"
68169
+ }), btn("green", 0, 1, {
68170
+ role: "custom:color_green",
68171
+ tone: "green"
68172
+ }), btn("yellow", 0, 2, {
68173
+ role: "custom:color_yellow",
68174
+ tone: "yellow"
68175
+ }), btn("blue", 0, 3, {
68176
+ role: "custom:color_blue",
68177
+ tone: "blue"
68178
+ }), btn("rewind", 0, 0, { role: "rewind" }), btn("play", 0, 1, { role: "play" }), btn("pause", 0, 2, { role: "pause" }), btn("forward", 0, 3, { role: "fast_forward" }), btn("stop", 0, 4, { role: "stop" }), [
68179
+ 1,
68180
+ 2,
68181
+ 3,
68182
+ 4
68183
+ ].map((n) => btn(`app${n}`, 0, n - 1, { role: `custom:app_slot_${n}` })), btn("back", 0, 0, { role: "back" }), btn("home", 0, 1, { role: "home" }), btn("menu", 0, 2, { role: "menu" });
67910
68184
  discriminatedUnion("kind", [object({
67911
68185
  kind: literal("raw"),
67912
68186
  timings: array(number().int()),
@@ -75869,6 +76143,13 @@ Object.freeze({
75869
76143
  access: "create",
75870
76144
  role: "admin"
75871
76145
  },
76146
+ "remoteLayoutComponents.listComponents": {
76147
+ capName: "remote-layout-components",
76148
+ capScope: "system",
76149
+ addonId: null,
76150
+ access: "view",
76151
+ role: "admin"
76152
+ },
75872
76153
  "runningState.getStatus": {
75873
76154
  capName: "running-state",
75874
76155
  capScope: "device",
@@ -82045,6 +82326,7 @@ var SYSTEM_CAP_VOCABULARY = {
82045
82326
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
82046
82327
  records: system(INTERNAL),
82047
82328
  remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
82329
+ "remote-layout-components": system("the palette of the remote template editor — read-only data, never actuated"),
82048
82330
  "server-management": system(CLUSTER),
82049
82331
  "settings-store": system(INTERNAL),
82050
82332
  "smtp-provider": system(INTERNAL),