@waveform-playlist/ui-components 9.5.2 → 10.1.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.d.mts CHANGED
@@ -77,6 +77,7 @@ interface WaveformPlaylistTheme {
77
77
  selectedWaveOutlineColor: WaveformColor;
78
78
  selectedWaveFillColor: WaveformColor;
79
79
  selectedTrackControlsBackground: string;
80
+ selectedTrackBackground: string;
80
81
  timeColor: string;
81
82
  timescaleBackgroundColor: string;
82
83
  playheadColor: string;
@@ -681,6 +682,29 @@ interface TrackMenuProps {
681
682
  }
682
683
  declare const TrackMenu: react__default.FC<TrackMenuProps>;
683
684
 
685
+ interface ColorStop {
686
+ dB: number;
687
+ color: string;
688
+ }
689
+ interface SegmentedVUMeterProps {
690
+ levels: number[];
691
+ peakLevels?: number[];
692
+ channelLabels?: string[];
693
+ orientation?: 'vertical' | 'horizontal';
694
+ segmentCount?: number;
695
+ dBRange?: [number, number];
696
+ showScale?: boolean;
697
+ colorStops?: ColorStop[];
698
+ segmentWidth?: number;
699
+ segmentHeight?: number;
700
+ segmentGap?: number;
701
+ coloredInactive?: boolean;
702
+ /** Color for scale labels and channel labels. Defaults to '#888'. */
703
+ labelColor?: string;
704
+ className?: string;
705
+ }
706
+ declare const SegmentedVUMeter: react__default.NamedExoticComponent<SegmentedVUMeterProps>;
707
+
684
708
  type SnapTo = 'bar' | 'beat' | 'off';
685
709
  type ScaleMode = 'beats' | 'temporal';
686
710
  interface BeatsAndBarsContextValue {
@@ -897,4 +921,4 @@ declare const BaseSlider: styled_components_dist_types.IStyledComponentBase<"web
897
921
  ref?: ((instance: HTMLInputElement | null) => void | react.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof react.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | react.RefObject<HTMLInputElement> | null | undefined;
898
922
  }>, never>, never>> & string;
899
923
 
900
- export { AudioPosition, type AudioPositionProps, AutomaticScrollCheckbox, type AutomaticScrollCheckboxProps, BaseButton, BaseCheckbox, BaseCheckboxLabel, BaseCheckboxWrapper, BaseControlButton, BaseInput, BaseInputSmall, BaseLabel, BaseSelect, BaseSelectSmall, BaseSlider, type BeatsAndBarsContextValue, BeatsAndBarsProvider, type BeatsAndBarsProviderProps, Button, ButtonGroup, CLIP_BOUNDARY_WIDTH, CLIP_BOUNDARY_WIDTH_TOUCH, CLIP_HEADER_HEIGHT, Channel, type ChannelProps, Clip, ClipBoundary, type ClipBoundaryProps, ClipHeader, ClipHeaderPresentational, type ClipHeaderPresentationalProps, type ClipHeaderProps, type ClipProps, ClipViewportOriginProvider, CloseButton, Controls$1 as Controls, DevicePixelRatioProvider, DotsIcon, type DragHandleProps, FadeOverlay, type FadeOverlayProps, type GradientStop, Header, InlineLabel, LoopRegion, LoopRegionMarkers, type LoopRegionMarkersProps, type LoopRegionProps, MasterVolumeControl, type MasterVolumeControlProps, PianoRollChannel, type PianoRollChannelProps, Playhead, type PlayheadProps, PlayheadWithMarker, Playlist, PlaylistErrorBoundary, type PlaylistErrorBoundaryProps, PlaylistInfoContext, type PlaylistProps, PlayoutProvider, type PrecomputedTickData, type RenderPlayheadFunction, type ScaleMode, ScreenReaderOnly, type ScrollViewport, ScrollViewportProvider, Selection, type SelectionProps, SelectionTimeInputs, type SelectionTimeInputsProps, Slider, SliderWrapper, SmartChannel, type SmartChannelProps, SmartScale, type SmartScaleProps, type SnapTo, SpectrogramChannel, type SpectrogramChannelProps, SpectrogramLabels, type SpectrogramLabelsProps, type SpectrogramWorkerCanvasApi, StyledPlaylist, StyledTimeScale, type TimeFormat, TimeFormatSelect, type TimeFormatSelectProps, TimeInput, type TimeInputProps, TimeScale, type TimeScaleProps, TimescaleLoopRegion, type TimescaleLoopRegionProps, Track, TrackControlsContext, TrackMenu, type TrackMenuItem, type TrackMenuProps, type TrackProps, TrashIcon, VolumeDownIcon, VolumeUpIcon, type WaveformColor, type WaveformDrawMode, type WaveformGradient, type WaveformPlaylistTheme, darkTheme, defaultTheme, formatTime, getScaleInfo, isWaveformGradient, parseTime, pixelsToSamples, pixelsToSeconds, samplesToPixels, samplesToSeconds, secondsToPixels, secondsToSamples, useBeatsAndBars, useClipViewportOrigin, useDevicePixelRatio, usePlaylistInfo, usePlayoutStatus, usePlayoutStatusUpdate, useScrollViewport, useScrollViewportSelector, useTheme, useTrackControls, useVisibleChunkIndices, waveformColorToCss };
924
+ export { AudioPosition, type AudioPositionProps, AutomaticScrollCheckbox, type AutomaticScrollCheckboxProps, BaseButton, BaseCheckbox, BaseCheckboxLabel, BaseCheckboxWrapper, BaseControlButton, BaseInput, BaseInputSmall, BaseLabel, BaseSelect, BaseSelectSmall, BaseSlider, type BeatsAndBarsContextValue, BeatsAndBarsProvider, type BeatsAndBarsProviderProps, Button, ButtonGroup, CLIP_BOUNDARY_WIDTH, CLIP_BOUNDARY_WIDTH_TOUCH, CLIP_HEADER_HEIGHT, Channel, type ChannelProps, Clip, ClipBoundary, type ClipBoundaryProps, ClipHeader, ClipHeaderPresentational, type ClipHeaderPresentationalProps, type ClipHeaderProps, type ClipProps, ClipViewportOriginProvider, CloseButton, type ColorStop, Controls$1 as Controls, DevicePixelRatioProvider, DotsIcon, type DragHandleProps, FadeOverlay, type FadeOverlayProps, type GradientStop, Header, InlineLabel, LoopRegion, LoopRegionMarkers, type LoopRegionMarkersProps, type LoopRegionProps, MasterVolumeControl, type MasterVolumeControlProps, PianoRollChannel, type PianoRollChannelProps, Playhead, type PlayheadProps, PlayheadWithMarker, Playlist, PlaylistErrorBoundary, type PlaylistErrorBoundaryProps, PlaylistInfoContext, type PlaylistProps, PlayoutProvider, type PrecomputedTickData, type RenderPlayheadFunction, type ScaleMode, ScreenReaderOnly, type ScrollViewport, ScrollViewportProvider, SegmentedVUMeter, type SegmentedVUMeterProps, Selection, type SelectionProps, SelectionTimeInputs, type SelectionTimeInputsProps, Slider, SliderWrapper, SmartChannel, type SmartChannelProps, SmartScale, type SmartScaleProps, type SnapTo, SpectrogramChannel, type SpectrogramChannelProps, SpectrogramLabels, type SpectrogramLabelsProps, type SpectrogramWorkerCanvasApi, StyledPlaylist, StyledTimeScale, type TimeFormat, TimeFormatSelect, type TimeFormatSelectProps, TimeInput, type TimeInputProps, TimeScale, type TimeScaleProps, TimescaleLoopRegion, type TimescaleLoopRegionProps, Track, TrackControlsContext, TrackMenu, type TrackMenuItem, type TrackMenuProps, type TrackProps, TrashIcon, VolumeDownIcon, VolumeUpIcon, type WaveformColor, type WaveformDrawMode, type WaveformGradient, type WaveformPlaylistTheme, darkTheme, defaultTheme, formatTime, getScaleInfo, isWaveformGradient, parseTime, pixelsToSamples, pixelsToSeconds, samplesToPixels, samplesToSeconds, secondsToPixels, secondsToSamples, useBeatsAndBars, useClipViewportOrigin, useDevicePixelRatio, usePlaylistInfo, usePlayoutStatus, usePlayoutStatusUpdate, useScrollViewport, useScrollViewportSelector, useTheme, useTrackControls, useVisibleChunkIndices, waveformColorToCss };
package/dist/index.d.ts CHANGED
@@ -77,6 +77,7 @@ interface WaveformPlaylistTheme {
77
77
  selectedWaveOutlineColor: WaveformColor;
78
78
  selectedWaveFillColor: WaveformColor;
79
79
  selectedTrackControlsBackground: string;
80
+ selectedTrackBackground: string;
80
81
  timeColor: string;
81
82
  timescaleBackgroundColor: string;
82
83
  playheadColor: string;
@@ -681,6 +682,29 @@ interface TrackMenuProps {
681
682
  }
682
683
  declare const TrackMenu: react__default.FC<TrackMenuProps>;
683
684
 
685
+ interface ColorStop {
686
+ dB: number;
687
+ color: string;
688
+ }
689
+ interface SegmentedVUMeterProps {
690
+ levels: number[];
691
+ peakLevels?: number[];
692
+ channelLabels?: string[];
693
+ orientation?: 'vertical' | 'horizontal';
694
+ segmentCount?: number;
695
+ dBRange?: [number, number];
696
+ showScale?: boolean;
697
+ colorStops?: ColorStop[];
698
+ segmentWidth?: number;
699
+ segmentHeight?: number;
700
+ segmentGap?: number;
701
+ coloredInactive?: boolean;
702
+ /** Color for scale labels and channel labels. Defaults to '#888'. */
703
+ labelColor?: string;
704
+ className?: string;
705
+ }
706
+ declare const SegmentedVUMeter: react__default.NamedExoticComponent<SegmentedVUMeterProps>;
707
+
684
708
  type SnapTo = 'bar' | 'beat' | 'off';
685
709
  type ScaleMode = 'beats' | 'temporal';
686
710
  interface BeatsAndBarsContextValue {
@@ -897,4 +921,4 @@ declare const BaseSlider: styled_components_dist_types.IStyledComponentBase<"web
897
921
  ref?: ((instance: HTMLInputElement | null) => void | react.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof react.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | react.RefObject<HTMLInputElement> | null | undefined;
898
922
  }>, never>, never>> & string;
899
923
 
900
- export { AudioPosition, type AudioPositionProps, AutomaticScrollCheckbox, type AutomaticScrollCheckboxProps, BaseButton, BaseCheckbox, BaseCheckboxLabel, BaseCheckboxWrapper, BaseControlButton, BaseInput, BaseInputSmall, BaseLabel, BaseSelect, BaseSelectSmall, BaseSlider, type BeatsAndBarsContextValue, BeatsAndBarsProvider, type BeatsAndBarsProviderProps, Button, ButtonGroup, CLIP_BOUNDARY_WIDTH, CLIP_BOUNDARY_WIDTH_TOUCH, CLIP_HEADER_HEIGHT, Channel, type ChannelProps, Clip, ClipBoundary, type ClipBoundaryProps, ClipHeader, ClipHeaderPresentational, type ClipHeaderPresentationalProps, type ClipHeaderProps, type ClipProps, ClipViewportOriginProvider, CloseButton, Controls$1 as Controls, DevicePixelRatioProvider, DotsIcon, type DragHandleProps, FadeOverlay, type FadeOverlayProps, type GradientStop, Header, InlineLabel, LoopRegion, LoopRegionMarkers, type LoopRegionMarkersProps, type LoopRegionProps, MasterVolumeControl, type MasterVolumeControlProps, PianoRollChannel, type PianoRollChannelProps, Playhead, type PlayheadProps, PlayheadWithMarker, Playlist, PlaylistErrorBoundary, type PlaylistErrorBoundaryProps, PlaylistInfoContext, type PlaylistProps, PlayoutProvider, type PrecomputedTickData, type RenderPlayheadFunction, type ScaleMode, ScreenReaderOnly, type ScrollViewport, ScrollViewportProvider, Selection, type SelectionProps, SelectionTimeInputs, type SelectionTimeInputsProps, Slider, SliderWrapper, SmartChannel, type SmartChannelProps, SmartScale, type SmartScaleProps, type SnapTo, SpectrogramChannel, type SpectrogramChannelProps, SpectrogramLabels, type SpectrogramLabelsProps, type SpectrogramWorkerCanvasApi, StyledPlaylist, StyledTimeScale, type TimeFormat, TimeFormatSelect, type TimeFormatSelectProps, TimeInput, type TimeInputProps, TimeScale, type TimeScaleProps, TimescaleLoopRegion, type TimescaleLoopRegionProps, Track, TrackControlsContext, TrackMenu, type TrackMenuItem, type TrackMenuProps, type TrackProps, TrashIcon, VolumeDownIcon, VolumeUpIcon, type WaveformColor, type WaveformDrawMode, type WaveformGradient, type WaveformPlaylistTheme, darkTheme, defaultTheme, formatTime, getScaleInfo, isWaveformGradient, parseTime, pixelsToSamples, pixelsToSeconds, samplesToPixels, samplesToSeconds, secondsToPixels, secondsToSamples, useBeatsAndBars, useClipViewportOrigin, useDevicePixelRatio, usePlaylistInfo, usePlayoutStatus, usePlayoutStatusUpdate, useScrollViewport, useScrollViewportSelector, useTheme, useTrackControls, useVisibleChunkIndices, waveformColorToCss };
924
+ export { AudioPosition, type AudioPositionProps, AutomaticScrollCheckbox, type AutomaticScrollCheckboxProps, BaseButton, BaseCheckbox, BaseCheckboxLabel, BaseCheckboxWrapper, BaseControlButton, BaseInput, BaseInputSmall, BaseLabel, BaseSelect, BaseSelectSmall, BaseSlider, type BeatsAndBarsContextValue, BeatsAndBarsProvider, type BeatsAndBarsProviderProps, Button, ButtonGroup, CLIP_BOUNDARY_WIDTH, CLIP_BOUNDARY_WIDTH_TOUCH, CLIP_HEADER_HEIGHT, Channel, type ChannelProps, Clip, ClipBoundary, type ClipBoundaryProps, ClipHeader, ClipHeaderPresentational, type ClipHeaderPresentationalProps, type ClipHeaderProps, type ClipProps, ClipViewportOriginProvider, CloseButton, type ColorStop, Controls$1 as Controls, DevicePixelRatioProvider, DotsIcon, type DragHandleProps, FadeOverlay, type FadeOverlayProps, type GradientStop, Header, InlineLabel, LoopRegion, LoopRegionMarkers, type LoopRegionMarkersProps, type LoopRegionProps, MasterVolumeControl, type MasterVolumeControlProps, PianoRollChannel, type PianoRollChannelProps, Playhead, type PlayheadProps, PlayheadWithMarker, Playlist, PlaylistErrorBoundary, type PlaylistErrorBoundaryProps, PlaylistInfoContext, type PlaylistProps, PlayoutProvider, type PrecomputedTickData, type RenderPlayheadFunction, type ScaleMode, ScreenReaderOnly, type ScrollViewport, ScrollViewportProvider, SegmentedVUMeter, type SegmentedVUMeterProps, Selection, type SelectionProps, SelectionTimeInputs, type SelectionTimeInputsProps, Slider, SliderWrapper, SmartChannel, type SmartChannelProps, SmartScale, type SmartScaleProps, type SnapTo, SpectrogramChannel, type SpectrogramChannelProps, SpectrogramLabels, type SpectrogramLabelsProps, type SpectrogramWorkerCanvasApi, StyledPlaylist, StyledTimeScale, type TimeFormat, TimeFormatSelect, type TimeFormatSelectProps, TimeInput, type TimeInputProps, TimeScale, type TimeScaleProps, TimescaleLoopRegion, type TimescaleLoopRegionProps, Track, TrackControlsContext, TrackMenu, type TrackMenuItem, type TrackMenuProps, type TrackProps, TrashIcon, VolumeDownIcon, VolumeUpIcon, type WaveformColor, type WaveformDrawMode, type WaveformGradient, type WaveformPlaylistTheme, darkTheme, defaultTheme, formatTime, getScaleInfo, isWaveformGradient, parseTime, pixelsToSamples, pixelsToSeconds, samplesToPixels, samplesToSeconds, secondsToPixels, secondsToSamples, useBeatsAndBars, useClipViewportOrigin, useDevicePixelRatio, usePlaylistInfo, usePlayoutStatus, usePlayoutStatusUpdate, useScrollViewport, useScrollViewportSelector, useTheme, useTrackControls, useVisibleChunkIndices, waveformColorToCss };
package/dist/index.js CHANGED
@@ -74,6 +74,7 @@ __export(index_exports, {
74
74
  PlayoutProvider: () => PlayoutProvider,
75
75
  ScreenReaderOnly: () => ScreenReaderOnly,
76
76
  ScrollViewportProvider: () => ScrollViewportProvider,
77
+ SegmentedVUMeter: () => SegmentedVUMeter,
77
78
  Selection: () => Selection,
78
79
  SelectionTimeInputs: () => SelectionTimeInputs,
79
80
  Slider: () => Slider,
@@ -484,6 +485,8 @@ var defaultTheme = {
484
485
  // Selected: brighter cyan
485
486
  selectedTrackControlsBackground: "#d9e9ff",
486
487
  // Light blue background for selected track controls
488
+ selectedTrackBackground: "#e8f0fe",
489
+ // Light blue tint for selected track waveform area
487
490
  timeColor: "#000",
488
491
  timescaleBackgroundColor: "#fff",
489
492
  playheadColor: "#f00",
@@ -560,6 +563,8 @@ var darkTheme = {
560
563
  // Brighter amber background when selected
561
564
  selectedTrackControlsBackground: "#2a2218",
562
565
  // Dark warm brown for selected track controls
566
+ selectedTrackBackground: "#e8c090",
567
+ // Amber for selected track waveform area (matches selected clip)
563
568
  timeColor: "#d8c0a8",
564
569
  // Warm amber for timescale text
565
570
  timescaleBackgroundColor: "#1a1612",
@@ -3062,7 +3067,12 @@ var ChannelContainer = import_styled_components26.default.div.attrs((props) => (
3062
3067
  }
3063
3068
  }))`
3064
3069
  position: relative;
3065
- background: ${(props) => props.$backgroundColor || "transparent"};
3070
+ background: ${(props) => {
3071
+ if (props.$isSelected) {
3072
+ return props.theme.selectedTrackBackground || props.$backgroundColor || "transparent";
3073
+ }
3074
+ return props.$backgroundColor || "transparent";
3075
+ }};
3066
3076
  height: 100%;
3067
3077
  `;
3068
3078
  var Track = ({
@@ -3075,7 +3085,7 @@ var Track = ({
3075
3085
  hasClipHeaders = false,
3076
3086
  onClick,
3077
3087
  trackId,
3078
- isSelected: _isSelected = false
3088
+ isSelected = false
3079
3089
  }) => {
3080
3090
  const { waveHeight } = usePlaylistInfo();
3081
3091
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
@@ -3091,6 +3101,7 @@ var Track = ({
3091
3101
  {
3092
3102
  $backgroundColor: backgroundColor,
3093
3103
  $offset: offset,
3104
+ $isSelected: isSelected,
3094
3105
  onClick,
3095
3106
  "data-track-id": trackId,
3096
3107
  children
@@ -3473,6 +3484,282 @@ var TrackMenu = ({ items: itemsProp }) => {
3473
3484
  ] });
3474
3485
  };
3475
3486
 
3487
+ // src/components/SegmentedVUMeter.tsx
3488
+ var import_react30 = __toESM(require("react"));
3489
+ var import_styled_components35 = __toESM(require("styled-components"));
3490
+ var import_core8 = require("@waveform-playlist/core");
3491
+ var import_jsx_runtime36 = require("react/jsx-runtime");
3492
+ var DEFAULT_COLOR_STOPS = [
3493
+ { dB: 2, color: "#ff0000" },
3494
+ { dB: -1, color: "#e74c3c" },
3495
+ { dB: -3, color: "#e67e22" },
3496
+ { dB: -6, color: "#f1c40f" },
3497
+ { dB: -12, color: "#2ecc71" },
3498
+ { dB: -20, color: "#27ae60" },
3499
+ { dB: -30, color: "#5dade2" },
3500
+ { dB: -50, color: "#85c1e9" }
3501
+ ];
3502
+ var INACTIVE_OPACITY = 0.15;
3503
+ var INACTIVE_COLOR = "rgba(128, 128, 128, 0.2)";
3504
+ var PEAK_COLOR = "#ffffff";
3505
+ function getDefaultLabels(channelCount) {
3506
+ if (channelCount === 1) return ["M"];
3507
+ if (channelCount === 2) return ["L", "R"];
3508
+ return Array.from({ length: channelCount }, (_, i) => String(i + 1));
3509
+ }
3510
+ function getColorForDb(dB, colorStops) {
3511
+ if (colorStops.length === 0) return INACTIVE_COLOR;
3512
+ for (const stop of colorStops) {
3513
+ if (dB >= stop.dB) {
3514
+ return stop.color;
3515
+ }
3516
+ }
3517
+ return colorStops[colorStops.length - 1].color;
3518
+ }
3519
+ function computeThresholds(segmentCount, dBRange) {
3520
+ const safeCount = Math.max(2, segmentCount);
3521
+ const [minDb, maxDb] = dBRange;
3522
+ const step = (maxDb - minDb) / (safeCount - 1);
3523
+ return Array.from({ length: safeCount }, (_, i) => maxDb - i * step);
3524
+ }
3525
+ function formatDbLabel(dB) {
3526
+ return Math.round(dB).toString();
3527
+ }
3528
+ var MeterContainer = import_styled_components35.default.div`
3529
+ display: inline-flex;
3530
+ flex-direction: ${(props) => props.$orientation === "horizontal" ? "column" : "row"};
3531
+ gap: 4px;
3532
+ font-family: 'Courier New', monospace;
3533
+ `;
3534
+ var ChannelColumn = import_styled_components35.default.div`
3535
+ display: flex;
3536
+ flex-direction: ${(props) => props.$orientation === "horizontal" ? "row" : "column"};
3537
+ align-items: center;
3538
+ gap: 4px;
3539
+ `;
3540
+ var SegmentStack = import_styled_components35.default.div`
3541
+ display: flex;
3542
+ flex-direction: ${(props) => props.$orientation === "horizontal" ? "row" : "column"};
3543
+ `;
3544
+ var Segment = import_styled_components35.default.div.attrs((props) => ({
3545
+ style: {
3546
+ width: `${props.$width}px`,
3547
+ height: `${props.$height}px`,
3548
+ ...props.$orientation === "horizontal" ? { marginRight: `${props.$gap}px` } : { marginBottom: `${props.$gap}px` },
3549
+ backgroundColor: props.$isPeak ? PEAK_COLOR : props.$active || props.$coloredInactive ? props.$color : INACTIVE_COLOR,
3550
+ opacity: props.$isPeak || props.$active ? 1 : props.$coloredInactive ? INACTIVE_OPACITY : 1,
3551
+ boxShadow: props.$active || props.$isPeak ? `0 0 4px ${props.$isPeak ? PEAK_COLOR : props.$color}40` : "none"
3552
+ }
3553
+ }))`
3554
+ border-radius: 1px;
3555
+ `;
3556
+ var DEFAULT_LABEL_COLOR = "#888";
3557
+ var ChannelLabel = import_styled_components35.default.div`
3558
+ color: ${(props) => props.$labelColor};
3559
+ font-size: 10px;
3560
+ text-align: center;
3561
+ user-select: none;
3562
+ `;
3563
+ var ScaleColumn = import_styled_components35.default.div`
3564
+ display: flex;
3565
+ flex-direction: column;
3566
+ position: relative;
3567
+ min-width: 28px;
3568
+ `;
3569
+ var ScaleLabel = import_styled_components35.default.div.attrs((props) => ({
3570
+ style: {
3571
+ top: `${props.$top}px`,
3572
+ color: props.$labelColor
3573
+ }
3574
+ }))`
3575
+ position: absolute;
3576
+ left: 50%;
3577
+ font-size: 9px;
3578
+ font-family: 'Courier New', monospace;
3579
+ white-space: nowrap;
3580
+ transform: translate(-50%, -50%);
3581
+ user-select: none;
3582
+ `;
3583
+ var HorizontalScaleWrapper = import_styled_components35.default.div`
3584
+ display: flex;
3585
+ flex-direction: row;
3586
+ align-items: center;
3587
+ gap: 4px;
3588
+ `;
3589
+ var ScaleRow = import_styled_components35.default.div`
3590
+ display: flex;
3591
+ flex-direction: row;
3592
+ position: relative;
3593
+ min-height: 16px;
3594
+ `;
3595
+ var ScaleLabelHorizontal = import_styled_components35.default.div.attrs((props) => ({
3596
+ style: {
3597
+ left: `${props.$left}px`,
3598
+ color: props.$labelColor
3599
+ }
3600
+ }))`
3601
+ position: absolute;
3602
+ top: 50%;
3603
+ font-size: 9px;
3604
+ font-family: 'Courier New', monospace;
3605
+ white-space: nowrap;
3606
+ transform: translate(-50%, -50%);
3607
+ user-select: none;
3608
+ `;
3609
+ var SegmentedVUMeterInner = ({
3610
+ levels,
3611
+ peakLevels,
3612
+ channelLabels,
3613
+ orientation = "vertical",
3614
+ segmentCount = 24,
3615
+ dBRange = [-50, 5],
3616
+ showScale = true,
3617
+ colorStops = DEFAULT_COLOR_STOPS,
3618
+ segmentWidth = 20,
3619
+ segmentHeight = 8,
3620
+ segmentGap = 2,
3621
+ coloredInactive = false,
3622
+ labelColor,
3623
+ className
3624
+ }) => {
3625
+ const labels = channelLabels ?? getDefaultLabels(levels.length);
3626
+ const resolvedLabelColor = labelColor ?? DEFAULT_LABEL_COLOR;
3627
+ const channelCount = levels.length;
3628
+ if (process.env.NODE_ENV !== "production" && peakLevels != null && peakLevels.length !== channelCount) {
3629
+ console.warn(
3630
+ `[waveform-playlist] SegmentedVUMeter: peakLevels length (${peakLevels.length}) does not match levels length (${channelCount})`
3631
+ );
3632
+ }
3633
+ const isMultiChannel = channelCount >= 2;
3634
+ const segmentTotalHeight = segmentHeight + segmentGap;
3635
+ const [dBMin, dBMax] = dBRange;
3636
+ const thresholds = (0, import_react30.useMemo)(
3637
+ () => computeThresholds(segmentCount, [dBMin, dBMax]),
3638
+ [segmentCount, dBMin, dBMax]
3639
+ );
3640
+ const scaleLabels = (0, import_react30.useMemo)(() => {
3641
+ const totalSize = segmentCount * segmentTotalHeight - segmentGap;
3642
+ const minDb = dBMin;
3643
+ const maxDb = dBMax;
3644
+ let minSpacing;
3645
+ if (orientation === "horizontal") {
3646
+ minSpacing = 35;
3647
+ } else {
3648
+ minSpacing = Math.max(14, segmentTotalHeight * 2);
3649
+ }
3650
+ const labelCount = Math.max(2, Math.floor(totalSize / minSpacing));
3651
+ const labels2 = [];
3652
+ for (let i = 0; i < labelCount; i++) {
3653
+ const t = i / (labelCount - 1);
3654
+ const position = t * totalSize;
3655
+ const db = orientation === "horizontal" ? minDb + t * (maxDb - minDb) : maxDb - t * (maxDb - minDb);
3656
+ labels2.push({ position, label: formatDbLabel(db) });
3657
+ }
3658
+ return labels2;
3659
+ }, [orientation, segmentCount, segmentTotalHeight, segmentGap, dBMin, dBMax]);
3660
+ const renderThresholds = (0, import_react30.useMemo)(
3661
+ () => orientation === "horizontal" ? [...thresholds].reverse() : thresholds,
3662
+ [thresholds, orientation]
3663
+ );
3664
+ const renderChannel = (channelIndex) => {
3665
+ const level = levels[channelIndex];
3666
+ const levelDb = (0, import_core8.normalizedToDb)(level);
3667
+ const peakDb = peakLevels != null ? (0, import_core8.normalizedToDb)(peakLevels[channelIndex]) : null;
3668
+ let peakSegmentIndex = -1;
3669
+ if (peakDb != null) {
3670
+ let minDist = Infinity;
3671
+ for (let i = 0; i < renderThresholds.length; i++) {
3672
+ const dist = Math.abs(renderThresholds[i] - peakDb);
3673
+ if (dist < minDist) {
3674
+ minDist = dist;
3675
+ peakSegmentIndex = i;
3676
+ }
3677
+ }
3678
+ }
3679
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(ChannelColumn, { $orientation: orientation, "data-channel": true, children: [
3680
+ orientation === "horizontal" && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ChannelLabel, { $labelColor: resolvedLabelColor, children: labels[channelIndex] }),
3681
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(SegmentStack, { $orientation: orientation, children: renderThresholds.map((threshold, segIdx) => {
3682
+ const active = levelDb >= threshold;
3683
+ const isPeak = segIdx === peakSegmentIndex;
3684
+ const color = getColorForDb(threshold, colorStops);
3685
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
3686
+ Segment,
3687
+ {
3688
+ $width: orientation === "horizontal" ? segmentHeight : segmentWidth,
3689
+ $height: orientation === "horizontal" ? segmentWidth : segmentHeight,
3690
+ $gap: segmentGap,
3691
+ $active: active,
3692
+ $color: color,
3693
+ $isPeak: isPeak,
3694
+ $orientation: orientation,
3695
+ $coloredInactive: coloredInactive,
3696
+ "data-segment": true,
3697
+ ...isPeak ? { "data-peak": true } : {}
3698
+ },
3699
+ segIdx
3700
+ );
3701
+ }) }),
3702
+ orientation === "vertical" && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ChannelLabel, { $labelColor: resolvedLabelColor, children: labels[channelIndex] })
3703
+ ] }, channelIndex);
3704
+ };
3705
+ const renderScale = () => {
3706
+ if (orientation === "horizontal") {
3707
+ const totalWidth = segmentCount * segmentTotalHeight - segmentGap;
3708
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(HorizontalScaleWrapper, { children: [
3709
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ChannelLabel, { $labelColor: resolvedLabelColor, style: { visibility: "hidden" }, children: "L" }),
3710
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ScaleRow, { style: { width: `${totalWidth}px` }, children: scaleLabels.map(({ position, label }, i) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ScaleLabelHorizontal, { $left: position, $labelColor: resolvedLabelColor, children: label }, i)) })
3711
+ ] });
3712
+ }
3713
+ const totalHeight = segmentCount * segmentTotalHeight - segmentGap;
3714
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ScaleColumn, { style: { height: `${totalHeight}px` }, children: scaleLabels.map(({ position, label }, i) => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ScaleLabel, { $top: position, $labelColor: resolvedLabelColor, children: label }, i)) });
3715
+ };
3716
+ if (isMultiChannel) {
3717
+ if (orientation === "horizontal") {
3718
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3719
+ MeterContainer,
3720
+ {
3721
+ className,
3722
+ $orientation: orientation,
3723
+ "data-meter-orientation": orientation,
3724
+ children: [
3725
+ Array.from({ length: channelCount }, (_, i) => renderChannel(i)),
3726
+ showScale && renderScale()
3727
+ ]
3728
+ }
3729
+ );
3730
+ }
3731
+ const midpoint = Math.ceil(channelCount / 2);
3732
+ const leftChannels = Array.from({ length: midpoint }, (_, i) => i);
3733
+ const rightChannels = Array.from({ length: channelCount - midpoint }, (_, i) => midpoint + i);
3734
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3735
+ MeterContainer,
3736
+ {
3737
+ className,
3738
+ $orientation: orientation,
3739
+ "data-meter-orientation": orientation,
3740
+ children: [
3741
+ leftChannels.map(renderChannel),
3742
+ showScale && renderScale(),
3743
+ rightChannels.map(renderChannel)
3744
+ ]
3745
+ }
3746
+ );
3747
+ }
3748
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
3749
+ MeterContainer,
3750
+ {
3751
+ className,
3752
+ $orientation: orientation,
3753
+ "data-meter-orientation": orientation,
3754
+ children: [
3755
+ renderChannel(0),
3756
+ showScale && renderScale()
3757
+ ]
3758
+ }
3759
+ );
3760
+ };
3761
+ var SegmentedVUMeter = import_react30.default.memo(SegmentedVUMeterInner);
3762
+
3476
3763
  // src/utils/conversions.ts
3477
3764
  function samplesToSeconds(samples, sampleRate) {
3478
3765
  return samples / sampleRate;
@@ -3538,6 +3825,7 @@ function secondsToPixels2(seconds, samplesPerPixel, sampleRate) {
3538
3825
  PlayoutProvider,
3539
3826
  ScreenReaderOnly,
3540
3827
  ScrollViewportProvider,
3828
+ SegmentedVUMeter,
3541
3829
  Selection,
3542
3830
  SelectionTimeInputs,
3543
3831
  Slider,