@imperosoft/cris-webui-components 1.1.4 → 1.2.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
@@ -30,10 +30,19 @@ __export(index_exports, {
30
30
  CrisSpinner: () => CrisSpinner,
31
31
  CrisText: () => CrisText,
32
32
  CrisTextInput: () => CrisTextInput,
33
+ CrisViewComm: () => CrisViewComm,
34
+ CrisViewDspFull: () => CrisViewDspFull,
35
+ buildMixerAxis: () => buildMixerAxis,
36
+ clampLevel: () => clampLevel,
37
+ collapseStrips: () => collapseStrips,
38
+ commIndicators: () => commIndicators,
33
39
  configureIcons: () => configureIcons,
34
40
  getIconConfig: () => getIconConfig,
35
41
  getIconFilter: () => getIconFilter,
36
- getIconUrl: () => getIconUrl
42
+ getIconUrl: () => getIconUrl,
43
+ levelToPercent: () => levelToPercent,
44
+ linksFor: () => linksFor,
45
+ normalizeLinked: () => normalizeLinked
37
46
  });
38
47
  module.exports = __toCommonJS(index_exports);
39
48
 
@@ -94,6 +103,7 @@ function isTouchActive() {
94
103
 
95
104
  // src/components/CrisButton.tsx
96
105
  var import_jsx_runtime = require("react/jsx-runtime");
106
+ var MIN_PRESS_VISUAL_MS = 120;
97
107
  function CrisButton({
98
108
  join,
99
109
  joinFeedback,
@@ -126,6 +136,12 @@ function CrisButton({
126
136
  children,
127
137
  onPress,
128
138
  onRelease,
139
+ onTap,
140
+ onLongPress,
141
+ holdMs = 2e3,
142
+ onHoldRepeat,
143
+ repeatMs = 150,
144
+ onHoldProgress,
129
145
  debug = false
130
146
  }) {
131
147
  const debugRef = (0, import_react.useRef)(debug);
@@ -143,6 +159,12 @@ function CrisButton({
143
159
  const touchMovedRef = (0, import_react.useRef)(false);
144
160
  const touchPressTimerRef = (0, import_react.useRef)(null);
145
161
  const touchPressedRef = (0, import_react.useRef)(false);
162
+ const holdTimerRef = (0, import_react.useRef)(null);
163
+ const repeatIntervalRef = (0, import_react.useRef)(null);
164
+ const progressRafRef = (0, import_react.useRef)(null);
165
+ const pressStartRef = (0, import_react.useRef)(0);
166
+ const gestureFiredRef = (0, import_react.useRef)(false);
167
+ const pressVisualTimerRef = (0, import_react.useRef)(null);
146
168
  const feedbackJoin = joinFeedback ?? join;
147
169
  const feedback = (0, import_cris_webui_ch5_core.useDigital)(feedbackJoin ?? 0);
148
170
  const enabledJoin = (0, import_cris_webui_ch5_core.useDigital)(joinEnable ?? 0);
@@ -158,6 +180,12 @@ function CrisButton({
158
180
  setPressed(false);
159
181
  touchingRef.current = false;
160
182
  touchStartedHereRef.current = false;
183
+ if (pressVisualTimerRef.current) {
184
+ clearTimeout(pressVisualTimerRef.current);
185
+ pressVisualTimerRef.current = null;
186
+ }
187
+ clearGestureTimers();
188
+ onHoldProgress?.(0);
161
189
  if (join != null && smartId == null) {
162
190
  log("sending release via dSet(false)");
163
191
  dSet(join, false);
@@ -170,6 +198,22 @@ function CrisButton({
170
198
  clearTimeout(touchPressTimerRef.current);
171
199
  touchPressTimerRef.current = null;
172
200
  }
201
+ if (holdTimerRef.current) {
202
+ clearTimeout(holdTimerRef.current);
203
+ holdTimerRef.current = null;
204
+ }
205
+ if (repeatIntervalRef.current) {
206
+ clearInterval(repeatIntervalRef.current);
207
+ repeatIntervalRef.current = null;
208
+ }
209
+ if (progressRafRef.current != null) {
210
+ cancelAnimationFrame(progressRafRef.current);
211
+ progressRafRef.current = null;
212
+ }
213
+ if (pressVisualTimerRef.current) {
214
+ clearTimeout(pressVisualTimerRef.current);
215
+ pressVisualTimerRef.current = null;
216
+ }
173
217
  if (pressedRef.current && join != null && smartId == null) {
174
218
  log("UNMOUNT RELEASE - component unmounting while pressed");
175
219
  dSet(join, false);
@@ -185,6 +229,44 @@ function CrisButton({
185
229
  } else if (hasControlFeedback && textSelected != null) {
186
230
  currentText = textSelected;
187
231
  }
232
+ const clearGestureTimers = () => {
233
+ if (holdTimerRef.current) {
234
+ clearTimeout(holdTimerRef.current);
235
+ holdTimerRef.current = null;
236
+ }
237
+ if (repeatIntervalRef.current) {
238
+ clearInterval(repeatIntervalRef.current);
239
+ repeatIntervalRef.current = null;
240
+ }
241
+ if (progressRafRef.current != null) {
242
+ cancelAnimationFrame(progressRafRef.current);
243
+ progressRafRef.current = null;
244
+ }
245
+ };
246
+ const armGestures = () => {
247
+ gestureFiredRef.current = false;
248
+ pressStartRef.current = Date.now();
249
+ if (onLongPress) {
250
+ holdTimerRef.current = setTimeout(() => {
251
+ holdTimerRef.current = null;
252
+ gestureFiredRef.current = true;
253
+ onLongPress();
254
+ }, holdMs);
255
+ }
256
+ if (onHoldProgress) {
257
+ const tick = () => {
258
+ const p = Math.min(1, (Date.now() - pressStartRef.current) / holdMs);
259
+ onHoldProgress(p);
260
+ progressRafRef.current = p < 1 ? requestAnimationFrame(tick) : null;
261
+ };
262
+ progressRafRef.current = requestAnimationFrame(tick);
263
+ }
264
+ if (onHoldRepeat) {
265
+ repeatIntervalRef.current = setInterval(() => {
266
+ onHoldRepeat();
267
+ }, repeatMs);
268
+ }
269
+ };
188
270
  const handlePress = () => {
189
271
  log("handlePress called", { suppressKeyClicks, pressedRef: pressedRef.current, isEnabled });
190
272
  if (suppressKeyClicks) {
@@ -197,18 +279,24 @@ function CrisButton({
197
279
  }
198
280
  pressedRef.current = true;
199
281
  setPressed(true);
282
+ pressStartRef.current = Date.now();
283
+ if (pressVisualTimerRef.current) {
284
+ clearTimeout(pressVisualTimerRef.current);
285
+ pressVisualTimerRef.current = null;
286
+ }
200
287
  if (!isEnabled) {
201
288
  log("SKIPPED dSet: not enabled");
202
289
  return;
203
290
  }
204
291
  onPress?.();
292
+ armGestures();
205
293
  if (join != null && smartId == null) {
206
294
  log("SENDING PRESS via dSet(true)");
207
295
  dSet(join, true);
208
296
  }
209
297
  };
210
- const handleRelease = () => {
211
- log("handleRelease called", { suppressKeyClicks, pressedRef: pressedRef.current, isEnabled });
298
+ const handleRelease = (clean = true) => {
299
+ log("handleRelease called", { clean, suppressKeyClicks, pressedRef: pressedRef.current, isEnabled });
212
300
  if (suppressKeyClicks) {
213
301
  log("BLOCKED: suppressKeyClicks");
214
302
  return;
@@ -218,7 +306,17 @@ function CrisButton({
218
306
  return;
219
307
  }
220
308
  pressedRef.current = false;
221
- setPressed(false);
309
+ const heldMs = Date.now() - pressStartRef.current;
310
+ if (heldMs >= MIN_PRESS_VISUAL_MS) {
311
+ setPressed(false);
312
+ } else {
313
+ pressVisualTimerRef.current = setTimeout(() => {
314
+ pressVisualTimerRef.current = null;
315
+ setPressed(false);
316
+ }, MIN_PRESS_VISUAL_MS - heldMs);
317
+ }
318
+ clearGestureTimers();
319
+ onHoldProgress?.(0);
222
320
  if (!isEnabled) {
223
321
  log("SKIPPED dSet: not enabled");
224
322
  return;
@@ -228,6 +326,9 @@ function CrisButton({
228
326
  log("SENDING RELEASE via dSet(false)");
229
327
  dSet(join, false);
230
328
  }
329
+ if (clean && onTap && !gestureFiredRef.current) {
330
+ onTap();
331
+ }
231
332
  };
232
333
  const SCROLL_THRESHOLD = 8;
233
334
  const PRESS_DELAY = 80;
@@ -263,7 +364,7 @@ function CrisButton({
263
364
  cancelPressTimer();
264
365
  if (touchPressedRef.current) {
265
366
  touchPressedRef.current = false;
266
- handleRelease();
367
+ handleRelease(false);
267
368
  log("touchMove: scroll detected after press, releasing");
268
369
  } else {
269
370
  log("touchMove: scroll detected, press cancelled");
@@ -284,11 +385,11 @@ function CrisButton({
284
385
  touchStartedHereRef.current = false;
285
386
  if (touchPressedRef.current) {
286
387
  touchPressedRef.current = false;
287
- handleRelease();
388
+ handleRelease(true);
288
389
  } else {
289
390
  touchPressedRef.current = false;
290
391
  handlePress();
291
- handleRelease();
392
+ handleRelease(true);
292
393
  }
293
394
  };
294
395
  const handleTouchCancel = () => {
@@ -299,7 +400,7 @@ function CrisButton({
299
400
  touchStartedHereRef.current = false;
300
401
  if (touchPressedRef.current) {
301
402
  touchPressedRef.current = false;
302
- handleRelease();
403
+ handleRelease(false);
303
404
  }
304
405
  touchMovedRef.current = false;
305
406
  };
@@ -309,11 +410,11 @@ function CrisButton({
309
410
  };
310
411
  const handleMouseUp = () => {
311
412
  if (isTouchActive() || touchingRef.current) return;
312
- handleRelease();
413
+ handleRelease(true);
313
414
  };
314
415
  const handleMouseLeave = () => {
315
416
  if (isTouchActive() || touchingRef.current) return;
316
- handleRelease();
417
+ handleRelease(false);
317
418
  };
318
419
  if (!isVisible) return null;
319
420
  const classes = [
@@ -489,6 +590,10 @@ function CrisSlider({
489
590
  trackSizePercent = 20,
490
591
  thumbSizePercent = 4,
491
592
  delayMsAfterDragUpdateFeedback = 1e3,
593
+ value,
594
+ onChange,
595
+ onCommit,
596
+ changeThrottleMs = 100,
492
597
  className = "",
493
598
  style,
494
599
  barClassName = "",
@@ -508,15 +613,27 @@ function CrisSlider({
508
613
  const aSet = (0, import_cris_webui_ch5_core3.useJoinsStore)((state) => state.aSet);
509
614
  const [ratioCurrent, setRatioCurrent] = (0, import_react2.useState)(0);
510
615
  const [isDragging, setIsDragging] = (0, import_react2.useState)(false);
616
+ const draggingRef = (0, import_react2.useRef)(false);
511
617
  const isDraggingOrJustAfterRef = (0, import_react2.useRef)(false);
512
618
  const ratioBeforeDragRef = (0, import_react2.useRef)(0);
513
619
  const afterDragTimeoutRef = (0, import_react2.useRef)(null);
514
620
  const touchingRef = (0, import_react2.useRef)(false);
621
+ const changeTimerRef = (0, import_react2.useRef)(null);
622
+ const lastChangeAtRef = (0, import_react2.useRef)(0);
623
+ const latestValueRef = (0, import_react2.useRef)(0);
624
+ const touchStartXRef = (0, import_react2.useRef)(0);
625
+ const touchStartYRef = (0, import_react2.useRef)(0);
626
+ const touchDecidedRef = (0, import_react2.useRef)(false);
627
+ const valueRef = (0, import_react2.useRef)(value);
628
+ valueRef.current = value;
629
+ const analogValueRef = (0, import_react2.useRef)(analogValue);
630
+ analogValueRef.current = analogValue;
515
631
  const isEnabled = joinEnable == null ? true : enabled;
516
632
  const isVisible = joinVisible == null ? true : visible;
517
633
  (0, import_react2.useEffect)(() => {
518
634
  if (!isVisible && isDraggingOrJustAfterRef.current) {
519
635
  setIsDragging(false);
636
+ draggingRef.current = false;
520
637
  isDraggingOrJustAfterRef.current = false;
521
638
  touchingRef.current = false;
522
639
  if (effectiveDigitalJoin != null) {
@@ -526,16 +643,24 @@ function CrisSlider({
526
643
  }, [isVisible, effectiveDigitalJoin, dSet]);
527
644
  (0, import_react2.useEffect)(() => {
528
645
  return () => {
646
+ if (changeTimerRef.current != null) {
647
+ clearTimeout(changeTimerRef.current);
648
+ changeTimerRef.current = null;
649
+ }
650
+ if (afterDragTimeoutRef.current !== null) {
651
+ window.clearTimeout(afterDragTimeoutRef.current);
652
+ afterDragTimeoutRef.current = null;
653
+ }
529
654
  if (isDraggingOrJustAfterRef.current && effectiveDigitalJoin != null) {
530
655
  dSet(effectiveDigitalJoin, false);
531
656
  }
532
657
  };
533
658
  }, [effectiveDigitalJoin, dSet]);
534
659
  const analogToRatio = (0, import_react2.useCallback)(
535
- (value) => {
660
+ (value2) => {
536
661
  const range = maxValue - minValue;
537
662
  if (range <= 0) return 0;
538
- return Math.max(0, Math.min(1, (value - minValue) / range));
663
+ return Math.max(0, Math.min(1, (value2 - minValue) / range));
539
664
  },
540
665
  [minValue, maxValue]
541
666
  );
@@ -547,16 +672,17 @@ function CrisSlider({
547
672
  [minValue, maxValue]
548
673
  );
549
674
  const updateFromFeedback = (0, import_react2.useCallback)(() => {
550
- if (!isDraggingOrJustAfterRef.current && effectiveAnalogJoin != null) {
551
- setRatioCurrent(analogToRatio(analogValue));
552
- }
553
- }, [analogValue, analogToRatio, effectiveAnalogJoin]);
675
+ if (isDraggingOrJustAfterRef.current) return;
676
+ const v = valueRef.current !== void 0 ? valueRef.current : effectiveAnalogJoin != null ? analogValueRef.current : void 0;
677
+ if (v !== void 0) setRatioCurrent(analogToRatio(v));
678
+ }, [analogToRatio, effectiveAnalogJoin]);
554
679
  (0, import_react2.useEffect)(() => {
555
680
  updateFromFeedback();
556
- }, [updateFromFeedback]);
681
+ }, [value, analogValue, updateFromFeedback]);
557
682
  const handleDragStart = (0, import_react2.useCallback)(() => {
558
683
  if (!isEnabled) return;
559
684
  setIsDragging(true);
685
+ draggingRef.current = true;
560
686
  if (effectiveDigitalJoin != null) {
561
687
  dSet(effectiveDigitalJoin, true);
562
688
  }
@@ -570,11 +696,17 @@ function CrisSlider({
570
696
  }
571
697
  }, [isEnabled, effectiveDigitalJoin, dSet, ratioCurrent]);
572
698
  const handleDragEnd = (0, import_react2.useCallback)(() => {
573
- if (!isDragging) return;
699
+ if (!draggingRef.current) return;
700
+ draggingRef.current = false;
574
701
  setIsDragging(false);
575
702
  if (effectiveDigitalJoin != null) {
576
703
  dSet(effectiveDigitalJoin, false);
577
704
  }
705
+ if (changeTimerRef.current != null) {
706
+ clearTimeout(changeTimerRef.current);
707
+ changeTimerRef.current = null;
708
+ }
709
+ onCommit?.(latestValueRef.current);
578
710
  if (delayMsAfterDragUpdateFeedback > 0) {
579
711
  afterDragTimeoutRef.current = window.setTimeout(() => {
580
712
  isDraggingOrJustAfterRef.current = false;
@@ -585,10 +717,10 @@ function CrisSlider({
585
717
  isDraggingOrJustAfterRef.current = false;
586
718
  updateFromFeedback();
587
719
  }
588
- }, [isDragging, effectiveDigitalJoin, dSet, delayMsAfterDragUpdateFeedback, updateFromFeedback]);
720
+ }, [effectiveDigitalJoin, dSet, delayMsAfterDragUpdateFeedback, updateFromFeedback, onCommit]);
589
721
  const handleMove = (0, import_react2.useCallback)(
590
722
  (clientX, clientY, bounds) => {
591
- if (!isDragging) return;
723
+ if (!draggingRef.current) return;
592
724
  let newRatio;
593
725
  if (horizontal) {
594
726
  newRatio = (clientX - bounds.left) / bounds.width;
@@ -597,11 +729,31 @@ function CrisSlider({
597
729
  }
598
730
  newRatio = Math.max(0, Math.min(1, newRatio));
599
731
  setRatioCurrent(newRatio);
732
+ const outValue = ratioToAnalog(newRatio);
733
+ latestValueRef.current = outValue;
600
734
  if (effectiveAnalogJoin != null) {
601
- aSet(effectiveAnalogJoin, ratioToAnalog(newRatio));
735
+ aSet(effectiveAnalogJoin, outValue);
736
+ }
737
+ if (onChange) {
738
+ const now = Date.now();
739
+ const wait = changeThrottleMs - (now - lastChangeAtRef.current);
740
+ if (wait <= 0) {
741
+ if (changeTimerRef.current != null) {
742
+ clearTimeout(changeTimerRef.current);
743
+ changeTimerRef.current = null;
744
+ }
745
+ lastChangeAtRef.current = now;
746
+ onChange(outValue);
747
+ } else if (changeTimerRef.current == null) {
748
+ changeTimerRef.current = window.setTimeout(() => {
749
+ changeTimerRef.current = null;
750
+ lastChangeAtRef.current = Date.now();
751
+ onChange(latestValueRef.current);
752
+ }, wait);
753
+ }
602
754
  }
603
755
  },
604
- [isDragging, horizontal, effectiveAnalogJoin, aSet, ratioToAnalog]
756
+ [horizontal, effectiveAnalogJoin, aSet, ratioToAnalog, onChange, changeThrottleMs]
605
757
  );
606
758
  const handleMouseDown = (event) => {
607
759
  if (isTouchActive() || touchingRef.current) return;
@@ -611,7 +763,7 @@ function CrisSlider({
611
763
  handleMove(event.clientX, event.clientY, bounds);
612
764
  };
613
765
  const handleMouseMove = (event) => {
614
- if (!isDragging) return;
766
+ if (!draggingRef.current) return;
615
767
  const bounds = event.currentTarget.getBoundingClientRect();
616
768
  handleMove(event.clientX, event.clientY, bounds);
617
769
  };
@@ -619,38 +771,58 @@ function CrisSlider({
619
771
  handleDragEnd();
620
772
  };
621
773
  const handleMouseLeave = (event) => {
622
- if (isDragging) {
774
+ if (draggingRef.current) {
623
775
  const bounds = event.currentTarget.getBoundingClientRect();
624
776
  handleMove(event.clientX, event.clientY, bounds);
625
777
  }
626
778
  handleDragEnd();
627
779
  };
780
+ const TOUCH_SLOP = 8;
628
781
  const handleTouchStart = (event) => {
629
782
  touchStart();
630
783
  touchingRef.current = true;
631
- handleDragStart();
632
- const bounds = event.currentTarget.getBoundingClientRect();
633
- const touch = event.touches[0];
634
- handleMove(touch.clientX, touch.clientY, bounds);
784
+ const t = event.touches[0];
785
+ touchStartXRef.current = t.clientX;
786
+ touchStartYRef.current = t.clientY;
787
+ touchDecidedRef.current = false;
635
788
  };
636
789
  const handleTouchMove = (event) => {
637
- if (!isDragging) return;
638
- const bounds = event.currentTarget.getBoundingClientRect();
639
- const touch = event.touches[0];
640
- handleMove(touch.clientX, touch.clientY, bounds);
790
+ const t = event.touches[0];
791
+ if (!t) return;
792
+ if (draggingRef.current) {
793
+ handleMove(t.clientX, t.clientY, event.currentTarget.getBoundingClientRect());
794
+ return;
795
+ }
796
+ if (touchDecidedRef.current) return;
797
+ const dx = Math.abs(t.clientX - touchStartXRef.current);
798
+ const dy = Math.abs(t.clientY - touchStartYRef.current);
799
+ if (dx < TOUCH_SLOP && dy < TOUCH_SLOP) return;
800
+ touchDecidedRef.current = true;
801
+ const along = horizontal ? dx : dy;
802
+ const cross = horizontal ? dy : dx;
803
+ if (along >= cross) {
804
+ handleDragStart();
805
+ handleMove(t.clientX, t.clientY, event.currentTarget.getBoundingClientRect());
806
+ }
641
807
  };
642
- const handleTouchEnd = () => {
808
+ const handleTouchEnd = (event) => {
643
809
  touchEnd();
644
- handleDragEnd();
810
+ if (draggingRef.current) {
811
+ handleDragEnd();
812
+ } else if (!touchDecidedRef.current) {
813
+ const t = event.changedTouches[0];
814
+ if (t) {
815
+ handleDragStart();
816
+ handleMove(t.clientX, t.clientY, event.currentTarget.getBoundingClientRect());
817
+ handleDragEnd();
818
+ }
819
+ }
820
+ touchDecidedRef.current = false;
645
821
  };
646
- const handleTouchCancel = (event) => {
822
+ const handleTouchCancel = () => {
647
823
  touchEnd();
648
- if (isDragging && event.touches.length > 0) {
649
- const bounds = event.currentTarget.getBoundingClientRect();
650
- const touch = event.touches[0];
651
- handleMove(touch.clientX, touch.clientY, bounds);
652
- }
653
- handleDragEnd();
824
+ touchDecidedRef.current = false;
825
+ if (draggingRef.current) handleDragEnd();
654
826
  };
655
827
  if (!isVisible) return null;
656
828
  const containerClasses = [
@@ -678,8 +850,10 @@ function CrisSlider({
678
850
  computedBarStyle.height = `${height}%`;
679
851
  computedBarStyle.top = `${top}%`;
680
852
  }
853
+ const dragLayerStyle = isDragging ? { willChange: "transform" } : {};
681
854
  const computedFillStyle = {
682
855
  ...fillStyle,
856
+ ...dragLayerStyle,
683
857
  position: "absolute"
684
858
  };
685
859
  if (horizontal) {
@@ -701,6 +875,7 @@ function CrisSlider({
701
875
  }
702
876
  const computedThumbStyle = {
703
877
  ...thumbStyle,
878
+ ...dragLayerStyle,
704
879
  position: "absolute"
705
880
  };
706
881
  if (horizontal) {
@@ -724,7 +899,9 @@ function CrisSlider({
724
899
  ...style,
725
900
  position: "relative",
726
901
  cursor: isEnabled ? "pointer" : "default",
727
- touchAction: "none",
902
+ // Let the browser scroll the cross-axis (so a horizontal swipe over a vertical
903
+ // fader scrolls the row); the slider handles drags along its own axis.
904
+ touchAction: horizontal ? "pan-y" : "pan-x",
728
905
  userSelect: "none"
729
906
  },
730
907
  onMouseDown: handleMouseDown,
@@ -2159,6 +2336,654 @@ function CrisCoMatrixListsTie({
2159
2336
  }
2160
2337
  );
2161
2338
  }
2339
+
2340
+ // src/components/CrisViewComm.tsx
2341
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2342
+ var ETH = /* @__PURE__ */ new Set(["tcp", "udp", "ssh", "ws"]);
2343
+ function commIndicators(c) {
2344
+ const isEth = !!c?.kd && ETH.has(c.kd);
2345
+ return {
2346
+ eth: { visible: isEth || c?.kd === "native", on: !!c?.co },
2347
+ serial: { visible: c?.kd === "serial" || isEth && !!c?.so, on: !!c?.al }
2348
+ };
2349
+ }
2350
+ var COMM_DEFAULTS = {
2351
+ root: "flex items-center gap-[0.4em]",
2352
+ dot: "w-[2.2em] h-[2.2em] rounded-full flex items-center justify-center",
2353
+ dotOn: "bg-[#22c55e]",
2354
+ dotOff: "bg-[#dc2626]",
2355
+ icon: ""
2356
+ };
2357
+ function isNode(v) {
2358
+ return v !== void 0 && typeof v !== "string";
2359
+ }
2360
+ function IndicatorDot({ icon, on, iconScale, cls }) {
2361
+ const iconStyle = {
2362
+ width: iconScale,
2363
+ height: iconScale,
2364
+ filter: "brightness(0) invert(1)",
2365
+ opacity: on ? 1 : 0.5
2366
+ };
2367
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: `${cls.dot} ${on ? cls.dotOn : cls.dotOff}`, children: isNode(icon) ? icon : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("img", { className: cls.icon, src: getIconUrl(icon), alt: "", draggable: false, style: iconStyle }) });
2368
+ }
2369
+ function CrisViewComm({ comm, classes, icons, className }) {
2370
+ const cls = {
2371
+ root: classes?.root ?? COMM_DEFAULTS.root,
2372
+ dot: classes?.dot ?? COMM_DEFAULTS.dot,
2373
+ dotOn: classes?.dotOn ?? COMM_DEFAULTS.dotOn,
2374
+ dotOff: classes?.dotOff ?? COMM_DEFAULTS.dotOff,
2375
+ icon: classes?.icon ?? COMM_DEFAULTS.icon
2376
+ };
2377
+ const ethIcon = icons?.ethernet ?? "ind-ethernet";
2378
+ const serialIcon = icons?.serial ?? "rs232";
2379
+ const { eth, serial } = commIndicators(comm);
2380
+ if (!eth.visible && !serial.visible) return null;
2381
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: `${cls.root} ${className ?? ""}`, children: [
2382
+ eth.visible && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IndicatorDot, { icon: ethIcon, on: eth.on, iconScale: "85%", cls }),
2383
+ serial.visible && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IndicatorDot, { icon: serialIcon, on: serial.on, iconScale: "95%", cls })
2384
+ ] });
2385
+ }
2386
+
2387
+ // src/components/dsp/CrisViewDspFull.tsx
2388
+ var import_react9 = require("react");
2389
+ var import_cris_webui_ch5_core11 = require("@imperosoft/cris-webui-ch5-core");
2390
+
2391
+ // src/components/dsp/DspIoPage.tsx
2392
+ var import_react6 = require("react");
2393
+
2394
+ // src/components/dsp/dspModel.ts
2395
+ function levelToPercent(lv) {
2396
+ return Math.round(clampLevel(lv) / 65535 * 100);
2397
+ }
2398
+ function clampLevel(lv) {
2399
+ if (!Number.isFinite(lv)) return 0;
2400
+ return Math.max(0, Math.min(65535, lv));
2401
+ }
2402
+ function normalizeLinked(raw) {
2403
+ return {
2404
+ label: raw.lb ?? "",
2405
+ channels: raw.ch ?? []
2406
+ };
2407
+ }
2408
+ function collapseStrips(channels, links) {
2409
+ const byId = new Map(channels.map((c) => [c.id, c]));
2410
+ const grouped = /* @__PURE__ */ new Set();
2411
+ const groups = [];
2412
+ for (const raw of links ?? []) {
2413
+ const { label, channels: members } = normalizeLinked(raw);
2414
+ if (members.length === 0) continue;
2415
+ members.forEach((id) => grouped.add(id));
2416
+ const primary = members.find((id) => byId.has(id));
2417
+ const ref = primary !== void 0 ? byId.get(primary) : void 0;
2418
+ groups.push({
2419
+ kind: "group",
2420
+ key: `g:${members.join("-")}`,
2421
+ label: label || ref?.lb || `Grupo ${members[0]}`,
2422
+ channels: members,
2423
+ lv: clampLevel(ref?.lv),
2424
+ mt: ref?.mt ?? false
2425
+ });
2426
+ }
2427
+ const singles = channels.filter((c) => !grouped.has(c.id)).map((c) => ({
2428
+ kind: "single",
2429
+ key: `c:${c.id}`,
2430
+ label: c.lb ?? `Canal ${c.id}`,
2431
+ channels: [c.id],
2432
+ lv: clampLevel(c.lv),
2433
+ mt: c.mt
2434
+ }));
2435
+ return [...singles, ...groups];
2436
+ }
2437
+ function linksFor(ln, io) {
2438
+ return io === "in" ? ln?.ip : ln?.op;
2439
+ }
2440
+ function buildMixerAxis(channels, links) {
2441
+ const items = channels.map((c) => ({
2442
+ id: c.id,
2443
+ channelLabel: c.lb ?? `${c.id}`
2444
+ }));
2445
+ const indexById = new Map(channels.map((c, idx) => [c.id, idx]));
2446
+ for (const raw of links ?? []) {
2447
+ const { label, channels: members } = normalizeLinked(raw);
2448
+ if (members.length < 2) continue;
2449
+ const sorted = [...members].sort((a, b) => a - b);
2450
+ const consecutiveNumbers = sorted.every((n, k) => k === 0 || n === sorted[k - 1] + 1);
2451
+ if (!consecutiveNumbers) continue;
2452
+ const idxs = sorted.map((n) => indexById.get(n));
2453
+ if (idxs.some((x) => x === void 0)) continue;
2454
+ const indices = idxs;
2455
+ const consecutiveIdx = indices.every((x, k) => k === 0 || x === indices[k - 1] + 1);
2456
+ if (!consecutiveIdx) continue;
2457
+ const startIdx = indices[0];
2458
+ items[startIdx].groupStart = true;
2459
+ items[startIdx].groupSpan = indices.length;
2460
+ for (const idx of indices) items[idx].groupCommon = label;
2461
+ }
2462
+ return items;
2463
+ }
2464
+
2465
+ // src/components/dsp/DspChannelStrip.tsx
2466
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2467
+ function DspChannelStrip({ strip, io, oid, send, cls, icons }) {
2468
+ const primary = strip.channels[0];
2469
+ const sendLevel = (channel, value, queued) => send(oid, { action: "level.set", io, channel, value, queued });
2470
+ const streamLevel = (value) => {
2471
+ if (primary !== void 0) sendLevel(primary, value, false);
2472
+ };
2473
+ const commitLevel = (value) => {
2474
+ strip.channels.forEach((ch) => sendLevel(ch, value, true));
2475
+ };
2476
+ const toggleMute = () => {
2477
+ const value = !strip.mt;
2478
+ strip.channels.forEach(
2479
+ (channel) => send(oid, { action: "mute.set", io, channel, value, queued: false })
2480
+ );
2481
+ };
2482
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: cls.strip, style: { minWidth: 0 }, children: [
2483
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "w-full h-[4.1em] flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: cls.stripLabel, title: strip.label, children: strip.label }) }),
2484
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: cls.levelReadout, children: [
2485
+ levelToPercent(strip.lv),
2486
+ "%"
2487
+ ] }),
2488
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2489
+ CrisSlider,
2490
+ {
2491
+ value: strip.lv,
2492
+ onChange: streamLevel,
2493
+ onCommit: commitLevel,
2494
+ minValue: 0,
2495
+ maxValue: 65535,
2496
+ thumbSizePercent: 7,
2497
+ className: "flex-1 w-full",
2498
+ barClassName: cls.faderBar,
2499
+ fillClassName: cls.faderFill,
2500
+ thumbClassName: cls.faderThumb
2501
+ }
2502
+ ),
2503
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "w-full h-[3.8em] shrink-0 mt-[0.6em]", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2504
+ CrisButton,
2505
+ {
2506
+ selected: strip.mt,
2507
+ onPress: toggleMute,
2508
+ iconName: icons.muteOff,
2509
+ iconNameActive: icons.muteOn,
2510
+ iconSize: "3.6em",
2511
+ iconStyle: { filter: icons.muteOffFilter },
2512
+ iconStyleActive: { filter: icons.muteOnFilter },
2513
+ className: cls.mute,
2514
+ classActive: cls.muteActive
2515
+ }
2516
+ ) })
2517
+ ] });
2518
+ }
2519
+
2520
+ // src/components/dsp/useHorizontalWheel.ts
2521
+ var import_react5 = require("react");
2522
+ function useHorizontalWheel(ref) {
2523
+ (0, import_react5.useEffect)(() => {
2524
+ const el = ref.current;
2525
+ if (!el) return;
2526
+ const onWheel = (e) => {
2527
+ if (e.deltaY === 0) return;
2528
+ if (el.scrollWidth <= el.clientWidth) return;
2529
+ e.preventDefault();
2530
+ el.scrollLeft += e.deltaY;
2531
+ };
2532
+ el.addEventListener("wheel", onWheel, { passive: false });
2533
+ return () => el.removeEventListener("wheel", onWheel);
2534
+ }, [ref]);
2535
+ }
2536
+
2537
+ // src/components/dsp/DspIoPage.tsx
2538
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2539
+ function DspIoPage({ status, io, oid, send, skip, cls, icons, emptyLabel }) {
2540
+ const scrollRef = (0, import_react6.useRef)(null);
2541
+ useHorizontalWheel(scrollRef);
2542
+ const skipSet = new Set(skip ?? []);
2543
+ const raw = (io === "in" ? status?.ip : status?.op) ?? [];
2544
+ const channels = skipSet.size ? raw.filter((c) => !skipSet.has(c.id)) : raw;
2545
+ const allLinks = linksFor(status?.ln, io);
2546
+ const links = skipSet.size ? allLinks?.filter((g) => (g.ch ?? []).some((id) => !skipSet.has(id))) : allLinks;
2547
+ const strips = collapseStrips(channels, links);
2548
+ if (strips.length === 0) {
2549
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: cls.message, children: emptyLabel });
2550
+ }
2551
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { ref: scrollRef, className: "w-full h-full overflow-x-auto no-scrollbar", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2552
+ "div",
2553
+ {
2554
+ className: "flex h-full items-stretch gap-[0.3em] px-[0.5em] py-[0.5em]",
2555
+ style: { minWidth: "min-content" },
2556
+ children: strips.map((strip) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2557
+ DspChannelStrip,
2558
+ {
2559
+ strip,
2560
+ io,
2561
+ oid,
2562
+ send,
2563
+ cls,
2564
+ icons
2565
+ },
2566
+ strip.key
2567
+ ))
2568
+ }
2569
+ ) });
2570
+ }
2571
+
2572
+ // src/components/dsp/DspMixer.tsx
2573
+ var import_react7 = require("react");
2574
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2575
+ var COMMON_W = "9em";
2576
+ var CHAN_W = "3em";
2577
+ var COMMON_H = "2.4em";
2578
+ var CHAN_H = "2.1em";
2579
+ var CELL_W = "7.5em";
2580
+ var ROW_H = "4.5em";
2581
+ var DRAG_FACTOR = 0.6;
2582
+ function CrosspointOnIcon({ icon }) {
2583
+ if (typeof icon !== "string") return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_jsx_runtime14.Fragment, { children: icon });
2584
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2585
+ "img",
2586
+ {
2587
+ src: getIconUrl(icon),
2588
+ alt: "",
2589
+ draggable: false,
2590
+ className: "pointer-events-none",
2591
+ style: { width: "4.4em", height: "4.4em", filter: "brightness(0) invert(1)" }
2592
+ }
2593
+ );
2594
+ }
2595
+ function DspMixer({ status, oid, send, cls, icons }) {
2596
+ const scrollRef = (0, import_react7.useRef)(null);
2597
+ const startRef = (0, import_react7.useRef)(null);
2598
+ const movedRef = (0, import_react7.useRef)(false);
2599
+ const thresholdRef = (0, import_react7.useRef)(8);
2600
+ const pendingRef = (0, import_react7.useRef)(null);
2601
+ const inputs = status?.ip ?? [];
2602
+ const outputs = status?.op ?? [];
2603
+ const isOn = (output, input) => outputs.find((o) => o.id === output)?.xp?.[input] === true;
2604
+ const onPointerDown = (e) => {
2605
+ const el = scrollRef.current;
2606
+ if (!el) return;
2607
+ movedRef.current = false;
2608
+ thresholdRef.current = parseFloat(getComputedStyle(el).fontSize || "16") * DRAG_FACTOR;
2609
+ startRef.current = { x: e.clientX, y: e.clientY, sl: el.scrollLeft, st: el.scrollTop };
2610
+ const cell = e.target.closest("[data-cell]");
2611
+ pendingRef.current = cell ? { input: Number(cell.dataset.in), output: Number(cell.dataset.out) } : null;
2612
+ el.setPointerCapture(e.pointerId);
2613
+ };
2614
+ const onPointerMove = (e) => {
2615
+ const el = scrollRef.current;
2616
+ const s = startRef.current;
2617
+ if (!el || !s) return;
2618
+ const dx = e.clientX - s.x;
2619
+ const dy = e.clientY - s.y;
2620
+ if (!movedRef.current && Math.hypot(dx, dy) > thresholdRef.current) movedRef.current = true;
2621
+ if (movedRef.current) {
2622
+ el.scrollLeft = s.sl - dx;
2623
+ el.scrollTop = s.st - dy;
2624
+ }
2625
+ };
2626
+ const onPointerUp = (e) => {
2627
+ const el = scrollRef.current;
2628
+ el?.releasePointerCapture?.(e.pointerId);
2629
+ if (!movedRef.current && pendingRef.current) {
2630
+ const { input, output } = pendingRef.current;
2631
+ send(oid, {
2632
+ action: "crosspoint.set",
2633
+ input,
2634
+ output,
2635
+ value: !isOn(output, input),
2636
+ queued: true
2637
+ });
2638
+ }
2639
+ startRef.current = null;
2640
+ pendingRef.current = null;
2641
+ };
2642
+ if (inputs.length === 0 || outputs.length === 0) {
2643
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: cls.message, children: "Sin crosspoints" });
2644
+ }
2645
+ const inAxis = buildMixerAxis(inputs, linksFor(status?.ln, "in"));
2646
+ const outAxis = buildMixerAxis(outputs, linksFor(status?.ln, "out"));
2647
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2648
+ "div",
2649
+ {
2650
+ ref: scrollRef,
2651
+ onPointerDown,
2652
+ onPointerMove,
2653
+ onPointerUp,
2654
+ onPointerCancel: onPointerUp,
2655
+ className: "w-full h-full overflow-auto no-scrollbar relative touch-none select-none cursor-grab",
2656
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2657
+ "div",
2658
+ {
2659
+ className: "grid",
2660
+ style: {
2661
+ // cols: [output common][output channel][inputs…] rows: [in common][in channel][outputs…]
2662
+ gridTemplateColumns: `${COMMON_W} ${CHAN_W} repeat(${inputs.length}, ${CELL_W})`,
2663
+ gridTemplateRows: `${COMMON_H} ${CHAN_H} repeat(${outputs.length}, ${ROW_H})`,
2664
+ minWidth: "min-content"
2665
+ },
2666
+ children: [
2667
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2668
+ "div",
2669
+ {
2670
+ className: cls.mixerCorner,
2671
+ style: { top: 0, left: 0, gridColumn: "1 / span 2", gridRow: "1 / span 2" },
2672
+ children: "OUT \\ IN"
2673
+ }
2674
+ ),
2675
+ inAxis.flatMap((it, ii) => {
2676
+ const col = 3 + ii;
2677
+ if (it.groupCommon !== void 0) {
2678
+ const nodes = [
2679
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2680
+ "div",
2681
+ {
2682
+ className: `${cls.mixerChrome} z-20 px-[0.2em]`,
2683
+ style: { top: 0, gridRow: "1 / span 2", gridColumn: col, alignItems: "flex-end", paddingBottom: "0.3em" },
2684
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "line-clamp-1", children: it.channelLabel })
2685
+ },
2686
+ `ich:${it.id}`
2687
+ )
2688
+ ];
2689
+ if (it.groupStart) {
2690
+ nodes.unshift(
2691
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2692
+ "div",
2693
+ {
2694
+ className: `${cls.mixerChrome} z-25 px-[0.2em] font-semibold`,
2695
+ style: { top: 0, gridRow: 1, gridColumn: `${col} / span ${it.groupSpan}` },
2696
+ title: it.groupCommon,
2697
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "line-clamp-2", children: it.groupCommon })
2698
+ },
2699
+ `icc:${it.id}`
2700
+ )
2701
+ );
2702
+ }
2703
+ return nodes;
2704
+ }
2705
+ return [
2706
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2707
+ "div",
2708
+ {
2709
+ className: `${cls.mixerChrome} z-20 px-[0.2em]`,
2710
+ style: { top: 0, gridRow: "1 / span 2", gridColumn: col, alignItems: "center" },
2711
+ title: it.channelLabel,
2712
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "line-clamp-3", children: it.channelLabel })
2713
+ },
2714
+ `is:${it.id}`
2715
+ )
2716
+ ];
2717
+ }),
2718
+ outAxis.flatMap((it, oo) => {
2719
+ const row = 3 + oo;
2720
+ if (it.groupCommon !== void 0) {
2721
+ const nodes = [
2722
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2723
+ "div",
2724
+ {
2725
+ className: `${cls.mixerChrome} z-20`,
2726
+ style: { left: 0, gridColumn: "1 / span 2", gridRow: row, justifyContent: "flex-end", paddingRight: "0.6em" },
2727
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "line-clamp-1", children: it.channelLabel })
2728
+ },
2729
+ `och:${it.id}`
2730
+ )
2731
+ ];
2732
+ if (it.groupStart) {
2733
+ nodes.unshift(
2734
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2735
+ "div",
2736
+ {
2737
+ className: `${cls.mixerChrome} z-25 justify-start px-[0.4em] font-semibold`,
2738
+ style: { left: 0, gridColumn: 1, gridRow: `${row} / span ${it.groupSpan}` },
2739
+ title: it.groupCommon,
2740
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "line-clamp-2 text-left", children: it.groupCommon })
2741
+ },
2742
+ `occ:${it.id}`
2743
+ )
2744
+ );
2745
+ }
2746
+ return nodes;
2747
+ }
2748
+ return [
2749
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2750
+ "div",
2751
+ {
2752
+ className: `${cls.mixerChrome} z-20 justify-start px-[0.4em]`,
2753
+ style: { left: 0, gridColumn: "1 / span 2", gridRow: row },
2754
+ title: it.channelLabel,
2755
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "line-clamp-2 text-left", children: it.channelLabel })
2756
+ },
2757
+ `os:${it.id}`
2758
+ )
2759
+ ];
2760
+ }),
2761
+ outputs.map(
2762
+ (o, oo) => inputs.map((i, ii) => {
2763
+ const on = isOn(o.id, i.id);
2764
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2765
+ "div",
2766
+ {
2767
+ "data-cell": true,
2768
+ "data-in": i.id,
2769
+ "data-out": o.id,
2770
+ className: `${cls.cell} ${on ? cls.cellOn : cls.cellOff}`,
2771
+ style: { gridColumn: 3 + ii, gridRow: 3 + oo },
2772
+ children: on && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CrosspointOnIcon, { icon: icons.crosspointOn })
2773
+ },
2774
+ `x:${i.id}:${o.id}`
2775
+ );
2776
+ })
2777
+ )
2778
+ ]
2779
+ }
2780
+ )
2781
+ }
2782
+ );
2783
+ }
2784
+
2785
+ // src/components/dsp/DspPresets.tsx
2786
+ var import_react8 = require("react");
2787
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2788
+ var HOLD_MS = 2e3;
2789
+ var SAVED_MS = 1e3;
2790
+ function PresetButton({ num, name, active, onCall, onSave, cls }) {
2791
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2792
+ CrisButton,
2793
+ {
2794
+ onTap: onCall,
2795
+ onLongPress: onSave,
2796
+ holdMs: HOLD_MS,
2797
+ selected: active,
2798
+ className: cls.preset,
2799
+ classActive: cls.presetActive,
2800
+ classPressed: cls.presetPressed,
2801
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "flex items-center justify-center gap-[0.5em]", children: [
2802
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-[1.6em] font-bold leading-none", children: num }),
2803
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-[1.2em] leading-none", children: name })
2804
+ ] })
2805
+ }
2806
+ );
2807
+ }
2808
+ function DspPresets({
2809
+ oid,
2810
+ send,
2811
+ presets,
2812
+ activeDevice,
2813
+ activeLocal,
2814
+ sustainedFeedback = true,
2815
+ cls
2816
+ }) {
2817
+ const [saved, setSaved] = (0, import_react8.useState)(false);
2818
+ const savedTimer = (0, import_react8.useRef)(null);
2819
+ const call = (id) => send(oid, { action: "preset.call", value: id });
2820
+ const save = (id) => {
2821
+ send(oid, { action: "preset.save", value: id });
2822
+ setSaved(true);
2823
+ if (savedTimer.current !== null) clearTimeout(savedTimer.current);
2824
+ savedTimer.current = window.setTimeout(() => setSaved(false), SAVED_MS);
2825
+ };
2826
+ const isActive = (p) => {
2827
+ if (!sustainedFeedback) return false;
2828
+ return p.kind === "local" ? activeLocal === p.id : activeDevice === p.id;
2829
+ };
2830
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: cls.presetWrap, children: [
2831
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: cls.presetLabel, children: "Preset" }),
2832
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "relative flex-1 flex items-stretch gap-[0.3em] mt-[0.25em]", children: [
2833
+ presets.map((p) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2834
+ PresetButton,
2835
+ {
2836
+ num: p.id,
2837
+ name: p.name,
2838
+ active: isActive(p),
2839
+ onCall: () => call(p.id),
2840
+ onSave: p.saveAllowed === false ? void 0 : () => save(p.id),
2841
+ cls
2842
+ },
2843
+ p.id
2844
+ )),
2845
+ saved && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: cls.savedFlash, children: "Saved" })
2846
+ ] })
2847
+ ] });
2848
+ }
2849
+
2850
+ // src/components/dsp/dspClasses.ts
2851
+ var DSP_CLASS_DEFAULTS = {
2852
+ root: "w-full h-full flex flex-col",
2853
+ header: "relative w-full flex items-end justify-center gap-[0.3em] px-[1em] pt-[0.5em]",
2854
+ content: "w-full flex-1 min-h-0",
2855
+ tab: "w-[10.5em] h-[3.25em] rounded-b-none rounded-lg flex items-center justify-center transition-colors bg-[#4f5152] text-white text-[1.4em] font-bold",
2856
+ tabActive: "bg-[#007ca0]",
2857
+ message: "w-full h-full flex items-center justify-center text-gray-400 text-[2em]",
2858
+ presetWrap: "absolute left-[1em] bottom-[0.3em] h-[4.5em] flex flex-col items-center",
2859
+ presetLabel: "text-[#4f5152] text-[1.4em] leading-none",
2860
+ preset: "w-[8em] h-full rounded-lg flex items-center justify-center transition-colors bg-[#4f5152] text-white",
2861
+ presetActive: "bg-[#007ca0]",
2862
+ presetPressed: "bg-[#006080]",
2863
+ savedFlash: "absolute inset-0 flex items-center justify-center rounded bg-[#22c55e] text-white text-[1.2em] font-bold pointer-events-none",
2864
+ strip: "flex flex-col items-center gap-[0.4em] h-full w-[9em] flex-none px-[0.3em]",
2865
+ stripLabel: "text-[#4f5152] text-center leading-tight text-[1.1em] line-clamp-3",
2866
+ levelReadout: "text-[1.1em] text-[#4f5152] leading-none opacity-70 mt-[0.5em]",
2867
+ faderBar: "bg-[#c0c0c0] rounded",
2868
+ faderFill: "bg-[#007ca0] rounded",
2869
+ faderThumb: "bg-[#4f5152] rounded",
2870
+ mute: "w-full h-full rounded-lg flex items-center justify-center transition-colors bg-[#4f5152] text-white",
2871
+ muteActive: "bg-[#dc2626] text-white",
2872
+ mixerCorner: "sticky z-30 flex items-center justify-center bg-[#282C34] text-[#4f5152] text-[1.1em] font-bold border border-black/30",
2873
+ mixerChrome: "sticky flex items-center justify-center text-center bg-[#282C34] text-white text-[1.1em] leading-tight border border-black/30",
2874
+ cell: "z-10 flex items-center justify-center border border-black/30",
2875
+ cellOn: "bg-[#22c55e]",
2876
+ cellOff: "bg-[#4f5152]"
2877
+ };
2878
+ var DSP_ICON_DEFAULTS = {
2879
+ crosspointOn: "audio-volume-ok",
2880
+ muteOff: "audio-volume-high",
2881
+ muteOn: "audio-volume-mute",
2882
+ muteOffFilter: "invert(65%) sepia(70%) saturate(500%) hue-rotate(80deg) brightness(110%) contrast(95%)",
2883
+ muteOnFilter: "brightness(0) invert(1)"
2884
+ };
2885
+ function mergeDefined(defaults4, overrides) {
2886
+ if (!overrides) return { ...defaults4 };
2887
+ const out = { ...defaults4 };
2888
+ Object.keys(overrides).forEach((k) => {
2889
+ const v = overrides[k];
2890
+ if (v !== void 0) out[k] = v;
2891
+ });
2892
+ return out;
2893
+ }
2894
+ function resolveDspClasses(classes) {
2895
+ return mergeDefined(DSP_CLASS_DEFAULTS, classes);
2896
+ }
2897
+ function resolveDspIcons(icons) {
2898
+ return mergeDefined(DSP_ICON_DEFAULTS, icons);
2899
+ }
2900
+
2901
+ // src/components/dsp/CrisViewDspFull.tsx
2902
+ var import_jsx_runtime16 = require("react/jsx-runtime");
2903
+ function CrisViewDspFull({
2904
+ oid,
2905
+ presets,
2906
+ skipShowInput,
2907
+ skipShowOutput,
2908
+ skipCrosspoints = false,
2909
+ sustainedPresetFeedback = true,
2910
+ classes,
2911
+ icons,
2912
+ className,
2913
+ initialTab = "in"
2914
+ }) {
2915
+ const safeInitial = initialTab === "mix" && skipCrosspoints ? "in" : initialTab;
2916
+ const [tab, setTab] = (0, import_react9.useState)(safeInitial);
2917
+ const status = (0, import_cris_webui_ch5_core11.useCustomObject)(oid, { subscribe: true });
2918
+ const send = (0, import_cris_webui_ch5_core11.useCustomObjectSend)();
2919
+ const cls = resolveDspClasses(classes);
2920
+ const ic = resolveDspIcons(icons);
2921
+ const tabs = [
2922
+ { id: "in", label: "Inputs" },
2923
+ ...skipCrosspoints ? [] : [{ id: "mix", label: "Mixer" }],
2924
+ { id: "out", label: "Outputs" }
2925
+ ];
2926
+ const hasPresets = !!presets && presets.length > 0;
2927
+ const activeDevice = status?.pr?.dv ?? status?.ps?.dv;
2928
+ const activeLocal = status?.pr?.lc ?? status?.ps?.lc;
2929
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: `${cls.root} ${className ?? ""}`, children: [
2930
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: cls.header, children: [
2931
+ hasPresets && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2932
+ DspPresets,
2933
+ {
2934
+ oid,
2935
+ send,
2936
+ presets,
2937
+ activeDevice,
2938
+ activeLocal,
2939
+ sustainedFeedback: sustainedPresetFeedback,
2940
+ cls
2941
+ }
2942
+ ),
2943
+ tabs.map((t) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2944
+ CrisButton,
2945
+ {
2946
+ text: t.label,
2947
+ selected: tab === t.id,
2948
+ onPress: () => setTab(t.id),
2949
+ className: cls.tab,
2950
+ classActive: cls.tabActive
2951
+ },
2952
+ t.id
2953
+ )),
2954
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(CrisViewComm, { comm: status?.cm, className: "absolute right-[1em] bottom-[0.3em] text-[1.25em]" })
2955
+ ] }),
2956
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: cls.content, children: status === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: cls.message, children: "Conectando\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
2957
+ tab === "in" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2958
+ DspIoPage,
2959
+ {
2960
+ status,
2961
+ io: "in",
2962
+ oid,
2963
+ send,
2964
+ skip: skipShowInput,
2965
+ cls,
2966
+ icons: ic,
2967
+ emptyLabel: "Sin entradas"
2968
+ }
2969
+ ),
2970
+ tab === "mix" && !skipCrosspoints && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DspMixer, { status, oid, send, cls, icons: ic }),
2971
+ tab === "out" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2972
+ DspIoPage,
2973
+ {
2974
+ status,
2975
+ io: "out",
2976
+ oid,
2977
+ send,
2978
+ skip: skipShowOutput,
2979
+ cls,
2980
+ icons: ic,
2981
+ emptyLabel: "Sin salidas"
2982
+ }
2983
+ )
2984
+ ] }) })
2985
+ ] });
2986
+ }
2162
2987
  // Annotate the CommonJS export names for ESM import in node:
2163
2988
  0 && (module.exports = {
2164
2989
  CrisButton,
@@ -2171,9 +2996,18 @@ function CrisCoMatrixListsTie({
2171
2996
  CrisSpinner,
2172
2997
  CrisText,
2173
2998
  CrisTextInput,
2999
+ CrisViewComm,
3000
+ CrisViewDspFull,
3001
+ buildMixerAxis,
3002
+ clampLevel,
3003
+ collapseStrips,
3004
+ commIndicators,
2174
3005
  configureIcons,
2175
3006
  getIconConfig,
2176
3007
  getIconFilter,
2177
- getIconUrl
3008
+ getIconUrl,
3009
+ levelToPercent,
3010
+ linksFor,
3011
+ normalizeLinked
2178
3012
  });
2179
3013
  //# sourceMappingURL=index.js.map