@imperosoft/cris-webui-components 1.3.0 → 1.5.0

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/index.js CHANGED
@@ -1134,6 +1134,7 @@ function CrisOfflinePage({
1134
1134
  });
1135
1135
  const processorHost = processorHostProp ?? (typeof window !== "undefined" ? window.location.hostname : "localhost");
1136
1136
  const isError = connectionStatus === "error" || hasSeenError && connectionStatus !== "connected";
1137
+ const isWsTransport = (0, import_cris_webui_ch5_core6.getActiveTransport)() === "ws";
1137
1138
  (0, import_react3.useEffect)(() => {
1138
1139
  if (connectionStatus === "error") {
1139
1140
  setHasSeenError(true);
@@ -1182,6 +1183,7 @@ function CrisOfflinePage({
1182
1183
  return () => clearTimeout(timer);
1183
1184
  }, [retryCountdown, retryConnection]);
1184
1185
  (0, import_react3.useEffect)(() => {
1186
+ if (isWsTransport) return;
1185
1187
  if (isError && isDeployedOnProcessor && loginRedirectDelay > 0 && !returnedFromLogin) {
1186
1188
  const timer = setTimeout(() => {
1187
1189
  try {
@@ -1192,7 +1194,7 @@ function CrisOfflinePage({
1192
1194
  }, loginRedirectDelay);
1193
1195
  return () => clearTimeout(timer);
1194
1196
  }
1195
- }, [isError, isDeployedOnProcessor, loginUrl, loginRedirectDelay, returnedFromLogin]);
1197
+ }, [isError, isDeployedOnProcessor, loginUrl, loginRedirectDelay, returnedFromLogin, isWsTransport]);
1196
1198
  const envInfo = {
1197
1199
  userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "N/A",
1198
1200
  hasCrComLib: typeof window !== "undefined" && typeof window.CrComLib !== "undefined",
@@ -1213,9 +1215,9 @@ function CrisOfflinePage({
1213
1215
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: `cris-offline-page absolute inset-0 bg-gray-900 flex flex-col items-center justify-center overflow-auto py-8 ${className}`, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "text-center max-w-4xl px-4", children: [
1214
1216
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mb-8", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: `w-16 h-16 mx-auto rounded-full flex items-center justify-center ${isError ? "bg-red-500/20" : "bg-yellow-500/20"}`, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: `w-8 h-8 rounded-full ${isError ? "bg-red-500" : "bg-yellow-500 animate-pulse"}` }) }) }),
1215
1217
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "text-2xl font-bold text-white mb-2", children: isError ? isTimeoutError ? "Processor Unreachable" : "Connection Error" : "Connecting..." }),
1216
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-gray-400", children: isError ? isDeployedOnProcessor ? returnedFromLogin ? "Connection failed after login. Please try again." : "Redirecting to login page..." : isTimeoutError ? "Could not connect to the processor. Check the IP address and network connection." : "WebSocket connection failed. SSL certificate may need to be accepted." : "Establishing connection to control system" }),
1217
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-8", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: `px-4 py-2 rounded-full text-sm font-medium ${isError ? "bg-red-500/20 text-red-400" : "bg-yellow-500/20 text-yellow-400"}`, children: isError ? isTimeoutError ? "Connection Timeout" : isDevMode ? "Dev Mode - Auth Error" : isVC4 ? "VC4 - Not Authorized" : "Auth Error" : "Connecting" }) }),
1218
- isError && !isDeployedOnProcessor && isTimeoutError && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-[2em] bg-orange-500/10 border border-orange-500/30 rounded-xl", style: { overflow: "visible", padding: "1.5em 1.5em 2em 1.5em" }, children: [
1218
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-gray-400", children: isError ? isWsTransport ? "Control system is not responding. Reconnecting automatically..." : isDeployedOnProcessor ? returnedFromLogin ? "Connection failed after login. Please try again." : "Redirecting to login page..." : isTimeoutError ? "Could not connect to the processor. Check the IP address and network connection." : "WebSocket connection failed. SSL certificate may need to be accepted." : "Establishing connection to control system" }),
1219
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-8", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: `px-4 py-2 rounded-full text-sm font-medium ${isError ? "bg-red-500/20 text-red-400" : "bg-yellow-500/20 text-yellow-400"}`, children: isError ? isWsTransport ? "Reconnecting" : isTimeoutError ? "Connection Timeout" : isDevMode ? "Dev Mode - Auth Error" : isVC4 ? "VC4 - Not Authorized" : "Auth Error" : "Connecting" }) }),
1220
+ isError && !isWsTransport && !isDeployedOnProcessor && isTimeoutError && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-[2em] bg-orange-500/10 border border-orange-500/30 rounded-xl", style: { overflow: "visible", padding: "1.5em 1.5em 2em 1.5em" }, children: [
1219
1221
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-orange-400 font-medium", style: { fontSize: "1.2em", marginBottom: "1em" }, children: [
1220
1222
  "The processor at ",
1221
1223
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-white", children: processorHost }),
@@ -1236,7 +1238,24 @@ function CrisOfflinePage({
1236
1238
  }
1237
1239
  ) })
1238
1240
  ] }),
1239
- isError && !isDeployedOnProcessor && !isTimeoutError && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-[2em] bg-blue-500/10 border border-blue-500/30 rounded-xl", style: { overflow: "visible", padding: "1.5em 1.5em 2em 1.5em" }, children: retryCountdown !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "text-center", style: { overflow: "visible" }, children: [
1241
+ isError && isWsTransport && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-[2em] bg-orange-500/10 border border-orange-500/30 rounded-xl", style: { overflow: "visible", padding: "1.5em 1.5em 2em 1.5em" }, children: [
1242
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-orange-400 font-medium", style: { fontSize: "1.2em", marginBottom: "1em" }, children: [
1243
+ "The control system at ",
1244
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-white", children: processorHost }),
1245
+ " is not responding."
1246
+ ] }),
1247
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-gray-400", style: { fontSize: "1em", marginBottom: "1em" }, children: "It may be restarting (e.g. a program update). The connection retries automatically." }),
1248
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex gap-[1em] justify-center", style: { overflow: "visible" }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1249
+ "button",
1250
+ {
1251
+ onClick: retryConnection,
1252
+ className: "bg-orange-600 hover:bg-orange-500 text-white font-medium rounded-xl transition-colors",
1253
+ style: { padding: "1em 2em", fontSize: "1.2em", whiteSpace: "nowrap", overflow: "visible" },
1254
+ children: "Retry Now"
1255
+ }
1256
+ ) })
1257
+ ] }),
1258
+ isError && !isWsTransport && !isDeployedOnProcessor && !isTimeoutError && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mt-[2em] bg-blue-500/10 border border-blue-500/30 rounded-xl", style: { overflow: "visible", padding: "1.5em 1.5em 2em 1.5em" }, children: retryCountdown !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "text-center", style: { overflow: "visible" }, children: [
1240
1259
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-blue-400 font-medium", style: { fontSize: "1.4em", marginBottom: "1em" }, children: [
1241
1260
  "Retrying connection in ",
1242
1261
  retryCountdown,
@@ -1275,7 +1294,7 @@ function CrisOfflinePage({
1275
1294
  ] }),
1276
1295
  certPageOpened && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-gray-400", style: { fontSize: "1em", marginTop: "1em" }, children: "Connection will auto-retry when you return to this page." })
1277
1296
  ] }) }),
1278
- isError && isDeployedOnProcessor && returnedFromLogin && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-[2em] bg-red-500/10 border border-red-500/30 rounded-xl", style: { overflow: "visible", padding: "1.5em 1.5em 2em 1.5em" }, children: [
1297
+ isError && !isWsTransport && isDeployedOnProcessor && returnedFromLogin && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-[2em] bg-red-500/10 border border-red-500/30 rounded-xl", style: { overflow: "visible", padding: "1.5em 1.5em 2em 1.5em" }, children: [
1279
1298
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-red-400 font-medium", style: { fontSize: "1.2em", marginBottom: "1em" }, children: "Connection failed after authentication." }),
1280
1299
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-gray-400", style: { fontSize: "1em", marginBottom: "1em" }, children: "This may be due to an expired session or WebSocket connection issue." }),
1281
1300
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex gap-[1em] justify-center flex-wrap", style: { overflow: "visible" }, children: [
@@ -2702,7 +2721,7 @@ function DspIoPage({
2702
2721
  const row = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2703
2722
  "div",
2704
2723
  {
2705
- className: "flex h-full items-stretch gap-[0.3em] px-[0.5em] py-[0.5em]",
2724
+ className: `flex h-full items-stretch gap-[0.3em] px-[0.5em] py-[0.5em]${bare ? "" : " w-max mx-auto"}`,
2706
2725
  style: { minWidth: "min-content" },
2707
2726
  children: sections.map((sec, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col h-full", children: [
2708
2727
  hasHeaders && // Header band: spans this group's strips. The 1px separator lives only
@@ -2762,6 +2781,7 @@ var DSP_CLASS_DEFAULTS = {
2762
2781
  faderThumb: "bg-[#4f5152] rounded",
2763
2782
  mute: "w-full h-full rounded-lg flex items-center justify-center transition-colors bg-[#4f5152] text-white",
2764
2783
  muteActive: "bg-[#dc2626] text-white",
2784
+ mixerBackplate: "bg-[#282C34]",
2765
2785
  mixerCorner: "sticky z-30 flex items-center justify-center bg-[#282C34] text-[#4f5152] text-[1.1em] font-bold border border-black/30",
2766
2786
  mixerChrome: "sticky flex items-center justify-center text-center bg-[#282C34] text-white leading-tight border border-black/30",
2767
2787
  cell: "z-10 flex items-center justify-center border border-black/30",
@@ -2856,15 +2876,42 @@ var import_cris_webui_ch5_core13 = require("@imperosoft/cris-webui-ch5-core");
2856
2876
  // src/components/dsp/DspMixer.tsx
2857
2877
  var import_react7 = require("react");
2858
2878
  var import_jsx_runtime16 = require("react/jsx-runtime");
2859
- var GROUP_W = "2.6em";
2860
- var COMMON_W = "9em";
2861
- var CHAN_W = "3em";
2862
- var GROUP_H = "2.2em";
2863
- var COMMON_H = "2.4em";
2864
- var CHAN_H = "2.1em";
2865
- var CELL_W = "7.5em";
2866
- var ROW_H = "4.5em";
2879
+ var DEFAULT_GROUP_W = "2.6em";
2880
+ var DEFAULT_COMMON_W = "9em";
2881
+ var DEFAULT_CHAN_W = "3em";
2882
+ var DEFAULT_GROUP_H = "2.2em";
2883
+ var DEFAULT_COMMON_H = "2.4em";
2884
+ var DEFAULT_CHAN_H = "2.1em";
2885
+ var DEFAULT_CELL_W = "7.5em";
2886
+ var DEFAULT_ROW_H = "4.5em";
2867
2887
  var DRAG_FACTOR = 0.6;
2888
+ function resolveGeometry(o, inputCount, outputCount) {
2889
+ const groupW = o?.groupW ?? DEFAULT_GROUP_W;
2890
+ const commonW = o?.commonW ?? DEFAULT_COMMON_W;
2891
+ const chanW = o?.chanW ?? DEFAULT_CHAN_W;
2892
+ const groupH = o?.groupH ?? DEFAULT_GROUP_H;
2893
+ const commonH = o?.commonH ?? DEFAULT_COMMON_H;
2894
+ const chanH = o?.chanH ?? DEFAULT_CHAN_H;
2895
+ const cellW = o?.cellW ?? DEFAULT_CELL_W;
2896
+ const rowH = o?.rowH ?? DEFAULT_ROW_H;
2897
+ return {
2898
+ groupW,
2899
+ commonW,
2900
+ chanW,
2901
+ groupH,
2902
+ commonH,
2903
+ chanH,
2904
+ cellW,
2905
+ rowH,
2906
+ topLink: groupH,
2907
+ topChan: `calc(${groupH} + ${commonH})`,
2908
+ leftLink: groupW,
2909
+ leftChan: `calc(${groupW} + ${commonW})`,
2910
+ z: { corner: 40, group: 30, link: 25, chan: 20 },
2911
+ inputCount,
2912
+ outputCount
2913
+ };
2914
+ }
2868
2915
  function CrosspointOnIcon({ icon }) {
2869
2916
  if (typeof icon !== "string") return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_jsx_runtime16.Fragment, { children: icon });
2870
2917
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
@@ -2878,7 +2925,17 @@ function CrosspointOnIcon({ icon }) {
2878
2925
  }
2879
2926
  );
2880
2927
  }
2881
- function DspMixer({ status, oid, send, cls, icons }) {
2928
+ function DspMixer({
2929
+ status,
2930
+ oid,
2931
+ send,
2932
+ cls,
2933
+ icons,
2934
+ geometry,
2935
+ renderInputHeader,
2936
+ renderOutputHeader,
2937
+ renderCorner
2938
+ }) {
2882
2939
  const scrollRef = (0, import_react7.useRef)(null);
2883
2940
  const startRef = (0, import_react7.useRef)(null);
2884
2941
  const movedRef = (0, import_react7.useRef)(false);
@@ -2928,13 +2985,167 @@ function DspMixer({ status, oid, send, cls, icons }) {
2928
2985
  if (inputs.length === 0 || outputs.length === 0) {
2929
2986
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: cls.message, children: "Sin crosspoints" });
2930
2987
  }
2988
+ const geom = resolveGeometry(geometry, inputs.length, outputs.length);
2931
2989
  const inAxis = buildMixerAxisGrouped(inputs, linksFor(status?.ln, "in"), groupsFor(status?.dg, "in"));
2932
2990
  const outAxis = buildMixerAxisGrouped(outputs, linksFor(status?.ln, "out"), groupsFor(status?.dg, "out"));
2933
- const topLink = GROUP_H;
2934
- const topChan = `calc(${GROUP_H} + ${COMMON_H})`;
2935
- const leftLink = GROUP_W;
2936
- const leftChan = `calc(${GROUP_W} + ${COMMON_W})`;
2937
- const Z = { corner: 40, group: 30, link: 25, chan: 20 };
2991
+ const { topLink, topChan, leftLink, leftChan, z: Z } = geom;
2992
+ const defaultInputHeader = () => inAxis.flatMap((it, ii) => {
2993
+ const col = 4 + ii;
2994
+ const nodes = [];
2995
+ if (it.groupStart) {
2996
+ nodes.push(
2997
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2998
+ "div",
2999
+ {
3000
+ className: cls.mixerGroup,
3001
+ style: { top: 0, zIndex: Z.group, gridRow: 1, gridColumn: `${col} / span ${it.groupSpan}` },
3002
+ title: it.groupName,
3003
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2 px-[0.2em]", children: it.groupName })
3004
+ },
3005
+ `igrp:${it.id}`
3006
+ )
3007
+ );
3008
+ } else if (it.groupName === void 0) {
3009
+ nodes.push(
3010
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3011
+ "div",
3012
+ {
3013
+ className: cls.mixerChrome,
3014
+ style: { top: 0, zIndex: Z.group, gridRow: 1, gridColumn: col }
3015
+ },
3016
+ `ifill:${it.id}`
3017
+ )
3018
+ );
3019
+ }
3020
+ if (it.linkCommon !== void 0) {
3021
+ if (it.linkStart) {
3022
+ nodes.push(
3023
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3024
+ "div",
3025
+ {
3026
+ className: `${cls.mixerChrome} px-[0.2em] font-semibold`,
3027
+ style: { top: topLink, zIndex: Z.link, gridRow: 2, gridColumn: `${col} / span ${it.linkSpan}` },
3028
+ title: it.linkCommon,
3029
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2", children: it.linkCommon })
3030
+ },
3031
+ `ilnk:${it.id}`
3032
+ )
3033
+ );
3034
+ }
3035
+ nodes.push(
3036
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3037
+ "div",
3038
+ {
3039
+ className: `${cls.mixerChrome} px-[0.2em]`,
3040
+ style: { top: topChan, zIndex: Z.chan, gridRow: 3, gridColumn: col },
3041
+ title: it.channelLabel,
3042
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-1", children: it.channelLabel })
3043
+ },
3044
+ `ichn:${it.id}`
3045
+ )
3046
+ );
3047
+ } else {
3048
+ nodes.push(
3049
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3050
+ "div",
3051
+ {
3052
+ className: cls.mixerChrome,
3053
+ style: { top: topLink, zIndex: Z.link, gridRow: 2, gridColumn: col }
3054
+ },
3055
+ `ilf:${it.id}`
3056
+ ),
3057
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3058
+ "div",
3059
+ {
3060
+ className: `${cls.mixerChrome} px-[0.2em]`,
3061
+ style: { top: topChan, zIndex: Z.chan, gridRow: 3, gridColumn: col },
3062
+ title: it.channelLabel,
3063
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2", children: it.channelLabel })
3064
+ },
3065
+ `ichn:${it.id}`
3066
+ )
3067
+ );
3068
+ }
3069
+ return nodes;
3070
+ });
3071
+ const defaultOutputHeader = () => outAxis.flatMap((it, oo) => {
3072
+ const row = 4 + oo;
3073
+ const nodes = [];
3074
+ if (it.groupStart) {
3075
+ nodes.push(
3076
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3077
+ "div",
3078
+ {
3079
+ className: cls.mixerGroupV,
3080
+ style: {
3081
+ left: 0,
3082
+ zIndex: Z.group,
3083
+ gridColumn: 1,
3084
+ gridRow: `${row} / span ${it.groupSpan}`,
3085
+ writingMode: "vertical-rl",
3086
+ transform: "rotate(180deg)"
3087
+ },
3088
+ title: it.groupName,
3089
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2", children: it.groupName })
3090
+ },
3091
+ `ogrp:${it.id}`
3092
+ )
3093
+ );
3094
+ } else if (it.groupName === void 0) {
3095
+ nodes.push(
3096
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3097
+ "div",
3098
+ {
3099
+ className: cls.mixerChrome,
3100
+ style: { left: 0, zIndex: Z.group, gridColumn: 1, gridRow: row }
3101
+ },
3102
+ `ofill:${it.id}`
3103
+ )
3104
+ );
3105
+ }
3106
+ if (it.linkCommon !== void 0) {
3107
+ if (it.linkStart) {
3108
+ nodes.push(
3109
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3110
+ "div",
3111
+ {
3112
+ className: `${cls.mixerChrome} justify-start px-[0.4em] font-semibold`,
3113
+ style: { left: leftLink, zIndex: Z.link, gridColumn: 2, gridRow: `${row} / span ${it.linkSpan}` },
3114
+ title: it.linkCommon,
3115
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2 text-left", children: it.linkCommon })
3116
+ },
3117
+ `olnk:${it.id}`
3118
+ )
3119
+ );
3120
+ }
3121
+ nodes.push(
3122
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3123
+ "div",
3124
+ {
3125
+ className: cls.mixerChrome,
3126
+ style: { left: leftChan, zIndex: Z.chan, gridColumn: 3, gridRow: row },
3127
+ title: it.channelLabel,
3128
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-1", children: it.channelLabel })
3129
+ },
3130
+ `ochn:${it.id}`
3131
+ )
3132
+ );
3133
+ } else {
3134
+ nodes.push(
3135
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3136
+ "div",
3137
+ {
3138
+ className: `${cls.mixerChrome} justify-start px-[0.4em]`,
3139
+ style: { left: leftLink, zIndex: Z.chan, gridColumn: "2 / span 2", gridRow: row },
3140
+ title: it.channelLabel,
3141
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2 text-left", children: it.channelLabel })
3142
+ },
3143
+ `ochn:${it.id}`
3144
+ )
3145
+ );
3146
+ }
3147
+ return nodes;
3148
+ });
2938
3149
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2939
3150
  "div",
2940
3151
  {
@@ -2943,15 +3154,15 @@ function DspMixer({ status, oid, send, cls, icons }) {
2943
3154
  onPointerMove,
2944
3155
  onPointerUp,
2945
3156
  onPointerCancel: onPointerUp,
2946
- className: "w-full h-full overflow-auto no-scrollbar relative touch-none select-none cursor-grab",
3157
+ className: "w-full h-full overflow-auto no-scrollbar relative touch-none select-none cursor-grab flex",
2947
3158
  children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
2948
3159
  "div",
2949
3160
  {
2950
- className: "grid",
3161
+ className: "grid m-auto",
2951
3162
  style: {
2952
3163
  // cols: [out group][out link][out channel][inputs…] rows: [in group][in link][in channel][outputs…]
2953
- gridTemplateColumns: `${GROUP_W} ${COMMON_W} ${CHAN_W} repeat(${inputs.length}, ${CELL_W})`,
2954
- gridTemplateRows: `${GROUP_H} ${COMMON_H} ${CHAN_H} repeat(${outputs.length}, ${ROW_H})`,
3164
+ gridTemplateColumns: `${geom.groupW} ${geom.commonW} ${geom.chanW} repeat(${inputs.length}, ${geom.cellW})`,
3165
+ gridTemplateRows: `${geom.groupH} ${geom.commonH} ${geom.chanH} repeat(${outputs.length}, ${geom.rowH})`,
2955
3166
  minWidth: "min-content"
2956
3167
  },
2957
3168
  children: [
@@ -2959,7 +3170,7 @@ function DspMixer({ status, oid, send, cls, icons }) {
2959
3170
  "div",
2960
3171
  {
2961
3172
  "aria-hidden": true,
2962
- className: "sticky bg-[#282C34]",
3173
+ className: `sticky ${cls.mixerBackplate}`,
2963
3174
  style: { top: 0, zIndex: 15, gridColumn: "1 / -1", gridRow: "1 / span 3" }
2964
3175
  }
2965
3176
  ),
@@ -2967,7 +3178,7 @@ function DspMixer({ status, oid, send, cls, icons }) {
2967
3178
  "div",
2968
3179
  {
2969
3180
  "aria-hidden": true,
2970
- className: "sticky bg-[#282C34]",
3181
+ className: `sticky ${cls.mixerBackplate}`,
2971
3182
  style: { left: 0, zIndex: 15, gridColumn: "1 / span 3", gridRow: "1 / -1" }
2972
3183
  }
2973
3184
  ),
@@ -2976,166 +3187,11 @@ function DspMixer({ status, oid, send, cls, icons }) {
2976
3187
  {
2977
3188
  className: cls.mixerCorner,
2978
3189
  style: { top: 0, left: 0, zIndex: Z.corner, gridColumn: "1 / span 3", gridRow: "1 / span 3" },
2979
- children: "OUT \\ IN"
3190
+ children: renderCorner ? renderCorner(geom) : "OUT \\ IN"
2980
3191
  }
2981
3192
  ),
2982
- inAxis.flatMap((it, ii) => {
2983
- const col = 4 + ii;
2984
- const nodes = [];
2985
- if (it.groupStart) {
2986
- nodes.push(
2987
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2988
- "div",
2989
- {
2990
- className: cls.mixerGroup,
2991
- style: { top: 0, zIndex: Z.group, gridRow: 1, gridColumn: `${col} / span ${it.groupSpan}` },
2992
- title: it.groupName,
2993
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2 px-[0.2em]", children: it.groupName })
2994
- },
2995
- `igrp:${it.id}`
2996
- )
2997
- );
2998
- } else if (it.groupName === void 0) {
2999
- nodes.push(
3000
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3001
- "div",
3002
- {
3003
- className: cls.mixerChrome,
3004
- style: { top: 0, zIndex: Z.group, gridRow: 1, gridColumn: col }
3005
- },
3006
- `ifill:${it.id}`
3007
- )
3008
- );
3009
- }
3010
- if (it.linkCommon !== void 0) {
3011
- if (it.linkStart) {
3012
- nodes.push(
3013
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3014
- "div",
3015
- {
3016
- className: `${cls.mixerChrome} px-[0.2em] font-semibold`,
3017
- style: { top: topLink, zIndex: Z.link, gridRow: 2, gridColumn: `${col} / span ${it.linkSpan}` },
3018
- title: it.linkCommon,
3019
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2", children: it.linkCommon })
3020
- },
3021
- `ilnk:${it.id}`
3022
- )
3023
- );
3024
- }
3025
- nodes.push(
3026
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3027
- "div",
3028
- {
3029
- className: `${cls.mixerChrome} px-[0.2em]`,
3030
- style: { top: topChan, zIndex: Z.chan, gridRow: 3, gridColumn: col },
3031
- title: it.channelLabel,
3032
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-1", children: it.channelLabel })
3033
- },
3034
- `ichn:${it.id}`
3035
- )
3036
- );
3037
- } else {
3038
- nodes.push(
3039
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3040
- "div",
3041
- {
3042
- className: cls.mixerChrome,
3043
- style: { top: topLink, zIndex: Z.link, gridRow: 2, gridColumn: col }
3044
- },
3045
- `ilf:${it.id}`
3046
- ),
3047
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3048
- "div",
3049
- {
3050
- className: `${cls.mixerChrome} px-[0.2em]`,
3051
- style: { top: topChan, zIndex: Z.chan, gridRow: 3, gridColumn: col },
3052
- title: it.channelLabel,
3053
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2", children: it.channelLabel })
3054
- },
3055
- `ichn:${it.id}`
3056
- )
3057
- );
3058
- }
3059
- return nodes;
3060
- }),
3061
- outAxis.flatMap((it, oo) => {
3062
- const row = 4 + oo;
3063
- const nodes = [];
3064
- if (it.groupStart) {
3065
- nodes.push(
3066
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3067
- "div",
3068
- {
3069
- className: cls.mixerGroupV,
3070
- style: {
3071
- left: 0,
3072
- zIndex: Z.group,
3073
- gridColumn: 1,
3074
- gridRow: `${row} / span ${it.groupSpan}`,
3075
- writingMode: "vertical-rl",
3076
- transform: "rotate(180deg)"
3077
- },
3078
- title: it.groupName,
3079
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2", children: it.groupName })
3080
- },
3081
- `ogrp:${it.id}`
3082
- )
3083
- );
3084
- } else if (it.groupName === void 0) {
3085
- nodes.push(
3086
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3087
- "div",
3088
- {
3089
- className: cls.mixerChrome,
3090
- style: { left: 0, zIndex: Z.group, gridColumn: 1, gridRow: row }
3091
- },
3092
- `ofill:${it.id}`
3093
- )
3094
- );
3095
- }
3096
- if (it.linkCommon !== void 0) {
3097
- if (it.linkStart) {
3098
- nodes.push(
3099
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3100
- "div",
3101
- {
3102
- className: `${cls.mixerChrome} justify-start px-[0.4em] font-semibold`,
3103
- style: { left: leftLink, zIndex: Z.link, gridColumn: 2, gridRow: `${row} / span ${it.linkSpan}` },
3104
- title: it.linkCommon,
3105
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2 text-left", children: it.linkCommon })
3106
- },
3107
- `olnk:${it.id}`
3108
- )
3109
- );
3110
- }
3111
- nodes.push(
3112
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3113
- "div",
3114
- {
3115
- className: cls.mixerChrome,
3116
- style: { left: leftChan, zIndex: Z.chan, gridColumn: 3, gridRow: row },
3117
- title: it.channelLabel,
3118
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-1", children: it.channelLabel })
3119
- },
3120
- `ochn:${it.id}`
3121
- )
3122
- );
3123
- } else {
3124
- nodes.push(
3125
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3126
- "div",
3127
- {
3128
- className: `${cls.mixerChrome} justify-start px-[0.4em]`,
3129
- style: { left: leftLink, zIndex: Z.chan, gridColumn: "2 / span 2", gridRow: row },
3130
- title: it.channelLabel,
3131
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[1.1em] line-clamp-2 text-left", children: it.channelLabel })
3132
- },
3133
- `ochn:${it.id}`
3134
- )
3135
- );
3136
- }
3137
- return nodes;
3138
- }),
3193
+ renderInputHeader ? renderInputHeader(inAxis, geom) : defaultInputHeader(),
3194
+ renderOutputHeader ? renderOutputHeader(outAxis, geom) : defaultOutputHeader(),
3139
3195
  outAxis.map(
3140
3196
  (orow, oo) => inAxis.map((icol, ii) => {
3141
3197
  const on = isOn(orow.id, icol.id);
@@ -3162,12 +3218,34 @@ function DspMixer({ status, oid, send, cls, icons }) {
3162
3218
 
3163
3219
  // src/components/dsp/CrisViewDspMixer.tsx
3164
3220
  var import_jsx_runtime17 = require("react/jsx-runtime");
3165
- function CrisViewDspMixer({ oid, classes, icons, className }) {
3221
+ function CrisViewDspMixer({
3222
+ oid,
3223
+ classes,
3224
+ icons,
3225
+ className,
3226
+ geometry,
3227
+ renderInputHeader,
3228
+ renderOutputHeader,
3229
+ renderCorner
3230
+ }) {
3166
3231
  const status = (0, import_cris_webui_ch5_core13.useCustomObject)(oid, { subscribe: true });
3167
3232
  const send = (0, import_cris_webui_ch5_core13.useCustomObjectSend)();
3168
3233
  const cls = resolveDspClasses(classes);
3169
3234
  const ic = resolveDspIcons(icons);
3170
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: `w-full h-full ${className ?? ""}`, children: status === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: cls.message, children: "Conectando\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DspMixer, { status, oid, send, cls, icons: ic }) });
3235
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: `w-full h-full ${className ?? ""}`, children: status === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: cls.message, children: "Conectando\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3236
+ DspMixer,
3237
+ {
3238
+ status,
3239
+ oid,
3240
+ send,
3241
+ cls,
3242
+ icons: ic,
3243
+ geometry,
3244
+ renderInputHeader,
3245
+ renderOutputHeader,
3246
+ renderCorner
3247
+ }
3248
+ ) });
3171
3249
  }
3172
3250
 
3173
3251
  // src/components/dsp/DspPresets.tsx
@@ -3237,6 +3315,7 @@ function DspPresets({
3237
3315
 
3238
3316
  // src/components/dsp/CrisViewDspFull.tsx
3239
3317
  var import_jsx_runtime19 = require("react/jsx-runtime");
3318
+ var lastTabByOid = /* @__PURE__ */ new Map();
3240
3319
  function CrisViewDspFull({
3241
3320
  oid,
3242
3321
  presets,
@@ -3249,8 +3328,13 @@ function CrisViewDspFull({
3249
3328
  className,
3250
3329
  initialTab = "in"
3251
3330
  }) {
3252
- const safeInitial = initialTab === "mix" && skipCrosspoints ? "in" : initialTab;
3253
- const [tab, setTab] = (0, import_react9.useState)(safeInitial);
3331
+ const remembered = lastTabByOid.get(oid) ?? initialTab;
3332
+ const safeInitial = remembered === "mix" && skipCrosspoints ? "in" : remembered;
3333
+ const [tab, setTabState] = (0, import_react9.useState)(safeInitial);
3334
+ const setTab = (t) => {
3335
+ lastTabByOid.set(oid, t);
3336
+ setTabState(t);
3337
+ };
3254
3338
  const status = (0, import_cris_webui_ch5_core14.useCustomObject)(oid, { subscribe: true });
3255
3339
  const send = (0, import_cris_webui_ch5_core14.useCustomObjectSend)();
3256
3340
  const cls = resolveDspClasses(classes);