@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 +25 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +290 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +289 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -361,6 +361,8 @@ var defaultTheme = {
|
|
|
361
361
|
// Selected: brighter cyan
|
|
362
362
|
selectedTrackControlsBackground: "#d9e9ff",
|
|
363
363
|
// Light blue background for selected track controls
|
|
364
|
+
selectedTrackBackground: "#e8f0fe",
|
|
365
|
+
// Light blue tint for selected track waveform area
|
|
364
366
|
timeColor: "#000",
|
|
365
367
|
timescaleBackgroundColor: "#fff",
|
|
366
368
|
playheadColor: "#f00",
|
|
@@ -437,6 +439,8 @@ var darkTheme = {
|
|
|
437
439
|
// Brighter amber background when selected
|
|
438
440
|
selectedTrackControlsBackground: "#2a2218",
|
|
439
441
|
// Dark warm brown for selected track controls
|
|
442
|
+
selectedTrackBackground: "#e8c090",
|
|
443
|
+
// Amber for selected track waveform area (matches selected clip)
|
|
440
444
|
timeColor: "#d8c0a8",
|
|
441
445
|
// Warm amber for timescale text
|
|
442
446
|
timescaleBackgroundColor: "#1a1612",
|
|
@@ -2958,7 +2962,12 @@ var ChannelContainer = styled25.div.attrs((props) => ({
|
|
|
2958
2962
|
}
|
|
2959
2963
|
}))`
|
|
2960
2964
|
position: relative;
|
|
2961
|
-
background: ${(props) =>
|
|
2965
|
+
background: ${(props) => {
|
|
2966
|
+
if (props.$isSelected) {
|
|
2967
|
+
return props.theme.selectedTrackBackground || props.$backgroundColor || "transparent";
|
|
2968
|
+
}
|
|
2969
|
+
return props.$backgroundColor || "transparent";
|
|
2970
|
+
}};
|
|
2962
2971
|
height: 100%;
|
|
2963
2972
|
`;
|
|
2964
2973
|
var Track = ({
|
|
@@ -2971,7 +2980,7 @@ var Track = ({
|
|
|
2971
2980
|
hasClipHeaders = false,
|
|
2972
2981
|
onClick,
|
|
2973
2982
|
trackId,
|
|
2974
|
-
isSelected
|
|
2983
|
+
isSelected = false
|
|
2975
2984
|
}) => {
|
|
2976
2985
|
const { waveHeight } = usePlaylistInfo();
|
|
2977
2986
|
return /* @__PURE__ */ jsx29(
|
|
@@ -2987,6 +2996,7 @@ var Track = ({
|
|
|
2987
2996
|
{
|
|
2988
2997
|
$backgroundColor: backgroundColor,
|
|
2989
2998
|
$offset: offset,
|
|
2999
|
+
$isSelected: isSelected,
|
|
2990
3000
|
onClick,
|
|
2991
3001
|
"data-track-id": trackId,
|
|
2992
3002
|
children
|
|
@@ -3369,6 +3379,282 @@ var TrackMenu = ({ items: itemsProp }) => {
|
|
|
3369
3379
|
] });
|
|
3370
3380
|
};
|
|
3371
3381
|
|
|
3382
|
+
// src/components/SegmentedVUMeter.tsx
|
|
3383
|
+
import React21, { useMemo as useMemo5 } from "react";
|
|
3384
|
+
import styled34 from "styled-components";
|
|
3385
|
+
import { normalizedToDb } from "@waveform-playlist/core";
|
|
3386
|
+
import { jsx as jsx36, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3387
|
+
var DEFAULT_COLOR_STOPS = [
|
|
3388
|
+
{ dB: 2, color: "#ff0000" },
|
|
3389
|
+
{ dB: -1, color: "#e74c3c" },
|
|
3390
|
+
{ dB: -3, color: "#e67e22" },
|
|
3391
|
+
{ dB: -6, color: "#f1c40f" },
|
|
3392
|
+
{ dB: -12, color: "#2ecc71" },
|
|
3393
|
+
{ dB: -20, color: "#27ae60" },
|
|
3394
|
+
{ dB: -30, color: "#5dade2" },
|
|
3395
|
+
{ dB: -50, color: "#85c1e9" }
|
|
3396
|
+
];
|
|
3397
|
+
var INACTIVE_OPACITY = 0.15;
|
|
3398
|
+
var INACTIVE_COLOR = "rgba(128, 128, 128, 0.2)";
|
|
3399
|
+
var PEAK_COLOR = "#ffffff";
|
|
3400
|
+
function getDefaultLabels(channelCount) {
|
|
3401
|
+
if (channelCount === 1) return ["M"];
|
|
3402
|
+
if (channelCount === 2) return ["L", "R"];
|
|
3403
|
+
return Array.from({ length: channelCount }, (_, i) => String(i + 1));
|
|
3404
|
+
}
|
|
3405
|
+
function getColorForDb(dB, colorStops) {
|
|
3406
|
+
if (colorStops.length === 0) return INACTIVE_COLOR;
|
|
3407
|
+
for (const stop of colorStops) {
|
|
3408
|
+
if (dB >= stop.dB) {
|
|
3409
|
+
return stop.color;
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
return colorStops[colorStops.length - 1].color;
|
|
3413
|
+
}
|
|
3414
|
+
function computeThresholds(segmentCount, dBRange) {
|
|
3415
|
+
const safeCount = Math.max(2, segmentCount);
|
|
3416
|
+
const [minDb, maxDb] = dBRange;
|
|
3417
|
+
const step = (maxDb - minDb) / (safeCount - 1);
|
|
3418
|
+
return Array.from({ length: safeCount }, (_, i) => maxDb - i * step);
|
|
3419
|
+
}
|
|
3420
|
+
function formatDbLabel(dB) {
|
|
3421
|
+
return Math.round(dB).toString();
|
|
3422
|
+
}
|
|
3423
|
+
var MeterContainer = styled34.div`
|
|
3424
|
+
display: inline-flex;
|
|
3425
|
+
flex-direction: ${(props) => props.$orientation === "horizontal" ? "column" : "row"};
|
|
3426
|
+
gap: 4px;
|
|
3427
|
+
font-family: 'Courier New', monospace;
|
|
3428
|
+
`;
|
|
3429
|
+
var ChannelColumn = styled34.div`
|
|
3430
|
+
display: flex;
|
|
3431
|
+
flex-direction: ${(props) => props.$orientation === "horizontal" ? "row" : "column"};
|
|
3432
|
+
align-items: center;
|
|
3433
|
+
gap: 4px;
|
|
3434
|
+
`;
|
|
3435
|
+
var SegmentStack = styled34.div`
|
|
3436
|
+
display: flex;
|
|
3437
|
+
flex-direction: ${(props) => props.$orientation === "horizontal" ? "row" : "column"};
|
|
3438
|
+
`;
|
|
3439
|
+
var Segment = styled34.div.attrs((props) => ({
|
|
3440
|
+
style: {
|
|
3441
|
+
width: `${props.$width}px`,
|
|
3442
|
+
height: `${props.$height}px`,
|
|
3443
|
+
...props.$orientation === "horizontal" ? { marginRight: `${props.$gap}px` } : { marginBottom: `${props.$gap}px` },
|
|
3444
|
+
backgroundColor: props.$isPeak ? PEAK_COLOR : props.$active || props.$coloredInactive ? props.$color : INACTIVE_COLOR,
|
|
3445
|
+
opacity: props.$isPeak || props.$active ? 1 : props.$coloredInactive ? INACTIVE_OPACITY : 1,
|
|
3446
|
+
boxShadow: props.$active || props.$isPeak ? `0 0 4px ${props.$isPeak ? PEAK_COLOR : props.$color}40` : "none"
|
|
3447
|
+
}
|
|
3448
|
+
}))`
|
|
3449
|
+
border-radius: 1px;
|
|
3450
|
+
`;
|
|
3451
|
+
var DEFAULT_LABEL_COLOR = "#888";
|
|
3452
|
+
var ChannelLabel = styled34.div`
|
|
3453
|
+
color: ${(props) => props.$labelColor};
|
|
3454
|
+
font-size: 10px;
|
|
3455
|
+
text-align: center;
|
|
3456
|
+
user-select: none;
|
|
3457
|
+
`;
|
|
3458
|
+
var ScaleColumn = styled34.div`
|
|
3459
|
+
display: flex;
|
|
3460
|
+
flex-direction: column;
|
|
3461
|
+
position: relative;
|
|
3462
|
+
min-width: 28px;
|
|
3463
|
+
`;
|
|
3464
|
+
var ScaleLabel = styled34.div.attrs((props) => ({
|
|
3465
|
+
style: {
|
|
3466
|
+
top: `${props.$top}px`,
|
|
3467
|
+
color: props.$labelColor
|
|
3468
|
+
}
|
|
3469
|
+
}))`
|
|
3470
|
+
position: absolute;
|
|
3471
|
+
left: 50%;
|
|
3472
|
+
font-size: 9px;
|
|
3473
|
+
font-family: 'Courier New', monospace;
|
|
3474
|
+
white-space: nowrap;
|
|
3475
|
+
transform: translate(-50%, -50%);
|
|
3476
|
+
user-select: none;
|
|
3477
|
+
`;
|
|
3478
|
+
var HorizontalScaleWrapper = styled34.div`
|
|
3479
|
+
display: flex;
|
|
3480
|
+
flex-direction: row;
|
|
3481
|
+
align-items: center;
|
|
3482
|
+
gap: 4px;
|
|
3483
|
+
`;
|
|
3484
|
+
var ScaleRow = styled34.div`
|
|
3485
|
+
display: flex;
|
|
3486
|
+
flex-direction: row;
|
|
3487
|
+
position: relative;
|
|
3488
|
+
min-height: 16px;
|
|
3489
|
+
`;
|
|
3490
|
+
var ScaleLabelHorizontal = styled34.div.attrs((props) => ({
|
|
3491
|
+
style: {
|
|
3492
|
+
left: `${props.$left}px`,
|
|
3493
|
+
color: props.$labelColor
|
|
3494
|
+
}
|
|
3495
|
+
}))`
|
|
3496
|
+
position: absolute;
|
|
3497
|
+
top: 50%;
|
|
3498
|
+
font-size: 9px;
|
|
3499
|
+
font-family: 'Courier New', monospace;
|
|
3500
|
+
white-space: nowrap;
|
|
3501
|
+
transform: translate(-50%, -50%);
|
|
3502
|
+
user-select: none;
|
|
3503
|
+
`;
|
|
3504
|
+
var SegmentedVUMeterInner = ({
|
|
3505
|
+
levels,
|
|
3506
|
+
peakLevels,
|
|
3507
|
+
channelLabels,
|
|
3508
|
+
orientation = "vertical",
|
|
3509
|
+
segmentCount = 24,
|
|
3510
|
+
dBRange = [-50, 5],
|
|
3511
|
+
showScale = true,
|
|
3512
|
+
colorStops = DEFAULT_COLOR_STOPS,
|
|
3513
|
+
segmentWidth = 20,
|
|
3514
|
+
segmentHeight = 8,
|
|
3515
|
+
segmentGap = 2,
|
|
3516
|
+
coloredInactive = false,
|
|
3517
|
+
labelColor,
|
|
3518
|
+
className
|
|
3519
|
+
}) => {
|
|
3520
|
+
const labels = channelLabels ?? getDefaultLabels(levels.length);
|
|
3521
|
+
const resolvedLabelColor = labelColor ?? DEFAULT_LABEL_COLOR;
|
|
3522
|
+
const channelCount = levels.length;
|
|
3523
|
+
if (process.env.NODE_ENV !== "production" && peakLevels != null && peakLevels.length !== channelCount) {
|
|
3524
|
+
console.warn(
|
|
3525
|
+
`[waveform-playlist] SegmentedVUMeter: peakLevels length (${peakLevels.length}) does not match levels length (${channelCount})`
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
3528
|
+
const isMultiChannel = channelCount >= 2;
|
|
3529
|
+
const segmentTotalHeight = segmentHeight + segmentGap;
|
|
3530
|
+
const [dBMin, dBMax] = dBRange;
|
|
3531
|
+
const thresholds = useMemo5(
|
|
3532
|
+
() => computeThresholds(segmentCount, [dBMin, dBMax]),
|
|
3533
|
+
[segmentCount, dBMin, dBMax]
|
|
3534
|
+
);
|
|
3535
|
+
const scaleLabels = useMemo5(() => {
|
|
3536
|
+
const totalSize = segmentCount * segmentTotalHeight - segmentGap;
|
|
3537
|
+
const minDb = dBMin;
|
|
3538
|
+
const maxDb = dBMax;
|
|
3539
|
+
let minSpacing;
|
|
3540
|
+
if (orientation === "horizontal") {
|
|
3541
|
+
minSpacing = 35;
|
|
3542
|
+
} else {
|
|
3543
|
+
minSpacing = Math.max(14, segmentTotalHeight * 2);
|
|
3544
|
+
}
|
|
3545
|
+
const labelCount = Math.max(2, Math.floor(totalSize / minSpacing));
|
|
3546
|
+
const labels2 = [];
|
|
3547
|
+
for (let i = 0; i < labelCount; i++) {
|
|
3548
|
+
const t = i / (labelCount - 1);
|
|
3549
|
+
const position = t * totalSize;
|
|
3550
|
+
const db = orientation === "horizontal" ? minDb + t * (maxDb - minDb) : maxDb - t * (maxDb - minDb);
|
|
3551
|
+
labels2.push({ position, label: formatDbLabel(db) });
|
|
3552
|
+
}
|
|
3553
|
+
return labels2;
|
|
3554
|
+
}, [orientation, segmentCount, segmentTotalHeight, segmentGap, dBMin, dBMax]);
|
|
3555
|
+
const renderThresholds = useMemo5(
|
|
3556
|
+
() => orientation === "horizontal" ? [...thresholds].reverse() : thresholds,
|
|
3557
|
+
[thresholds, orientation]
|
|
3558
|
+
);
|
|
3559
|
+
const renderChannel = (channelIndex) => {
|
|
3560
|
+
const level = levels[channelIndex];
|
|
3561
|
+
const levelDb = normalizedToDb(level);
|
|
3562
|
+
const peakDb = peakLevels != null ? normalizedToDb(peakLevels[channelIndex]) : null;
|
|
3563
|
+
let peakSegmentIndex = -1;
|
|
3564
|
+
if (peakDb != null) {
|
|
3565
|
+
let minDist = Infinity;
|
|
3566
|
+
for (let i = 0; i < renderThresholds.length; i++) {
|
|
3567
|
+
const dist = Math.abs(renderThresholds[i] - peakDb);
|
|
3568
|
+
if (dist < minDist) {
|
|
3569
|
+
minDist = dist;
|
|
3570
|
+
peakSegmentIndex = i;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
return /* @__PURE__ */ jsxs12(ChannelColumn, { $orientation: orientation, "data-channel": true, children: [
|
|
3575
|
+
orientation === "horizontal" && /* @__PURE__ */ jsx36(ChannelLabel, { $labelColor: resolvedLabelColor, children: labels[channelIndex] }),
|
|
3576
|
+
/* @__PURE__ */ jsx36(SegmentStack, { $orientation: orientation, children: renderThresholds.map((threshold, segIdx) => {
|
|
3577
|
+
const active = levelDb >= threshold;
|
|
3578
|
+
const isPeak = segIdx === peakSegmentIndex;
|
|
3579
|
+
const color = getColorForDb(threshold, colorStops);
|
|
3580
|
+
return /* @__PURE__ */ jsx36(
|
|
3581
|
+
Segment,
|
|
3582
|
+
{
|
|
3583
|
+
$width: orientation === "horizontal" ? segmentHeight : segmentWidth,
|
|
3584
|
+
$height: orientation === "horizontal" ? segmentWidth : segmentHeight,
|
|
3585
|
+
$gap: segmentGap,
|
|
3586
|
+
$active: active,
|
|
3587
|
+
$color: color,
|
|
3588
|
+
$isPeak: isPeak,
|
|
3589
|
+
$orientation: orientation,
|
|
3590
|
+
$coloredInactive: coloredInactive,
|
|
3591
|
+
"data-segment": true,
|
|
3592
|
+
...isPeak ? { "data-peak": true } : {}
|
|
3593
|
+
},
|
|
3594
|
+
segIdx
|
|
3595
|
+
);
|
|
3596
|
+
}) }),
|
|
3597
|
+
orientation === "vertical" && /* @__PURE__ */ jsx36(ChannelLabel, { $labelColor: resolvedLabelColor, children: labels[channelIndex] })
|
|
3598
|
+
] }, channelIndex);
|
|
3599
|
+
};
|
|
3600
|
+
const renderScale = () => {
|
|
3601
|
+
if (orientation === "horizontal") {
|
|
3602
|
+
const totalWidth = segmentCount * segmentTotalHeight - segmentGap;
|
|
3603
|
+
return /* @__PURE__ */ jsxs12(HorizontalScaleWrapper, { children: [
|
|
3604
|
+
/* @__PURE__ */ jsx36(ChannelLabel, { $labelColor: resolvedLabelColor, style: { visibility: "hidden" }, children: "L" }),
|
|
3605
|
+
/* @__PURE__ */ jsx36(ScaleRow, { style: { width: `${totalWidth}px` }, children: scaleLabels.map(({ position, label }, i) => /* @__PURE__ */ jsx36(ScaleLabelHorizontal, { $left: position, $labelColor: resolvedLabelColor, children: label }, i)) })
|
|
3606
|
+
] });
|
|
3607
|
+
}
|
|
3608
|
+
const totalHeight = segmentCount * segmentTotalHeight - segmentGap;
|
|
3609
|
+
return /* @__PURE__ */ jsx36(ScaleColumn, { style: { height: `${totalHeight}px` }, children: scaleLabels.map(({ position, label }, i) => /* @__PURE__ */ jsx36(ScaleLabel, { $top: position, $labelColor: resolvedLabelColor, children: label }, i)) });
|
|
3610
|
+
};
|
|
3611
|
+
if (isMultiChannel) {
|
|
3612
|
+
if (orientation === "horizontal") {
|
|
3613
|
+
return /* @__PURE__ */ jsxs12(
|
|
3614
|
+
MeterContainer,
|
|
3615
|
+
{
|
|
3616
|
+
className,
|
|
3617
|
+
$orientation: orientation,
|
|
3618
|
+
"data-meter-orientation": orientation,
|
|
3619
|
+
children: [
|
|
3620
|
+
Array.from({ length: channelCount }, (_, i) => renderChannel(i)),
|
|
3621
|
+
showScale && renderScale()
|
|
3622
|
+
]
|
|
3623
|
+
}
|
|
3624
|
+
);
|
|
3625
|
+
}
|
|
3626
|
+
const midpoint = Math.ceil(channelCount / 2);
|
|
3627
|
+
const leftChannels = Array.from({ length: midpoint }, (_, i) => i);
|
|
3628
|
+
const rightChannels = Array.from({ length: channelCount - midpoint }, (_, i) => midpoint + i);
|
|
3629
|
+
return /* @__PURE__ */ jsxs12(
|
|
3630
|
+
MeterContainer,
|
|
3631
|
+
{
|
|
3632
|
+
className,
|
|
3633
|
+
$orientation: orientation,
|
|
3634
|
+
"data-meter-orientation": orientation,
|
|
3635
|
+
children: [
|
|
3636
|
+
leftChannels.map(renderChannel),
|
|
3637
|
+
showScale && renderScale(),
|
|
3638
|
+
rightChannels.map(renderChannel)
|
|
3639
|
+
]
|
|
3640
|
+
}
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
return /* @__PURE__ */ jsxs12(
|
|
3644
|
+
MeterContainer,
|
|
3645
|
+
{
|
|
3646
|
+
className,
|
|
3647
|
+
$orientation: orientation,
|
|
3648
|
+
"data-meter-orientation": orientation,
|
|
3649
|
+
children: [
|
|
3650
|
+
renderChannel(0),
|
|
3651
|
+
showScale && renderScale()
|
|
3652
|
+
]
|
|
3653
|
+
}
|
|
3654
|
+
);
|
|
3655
|
+
};
|
|
3656
|
+
var SegmentedVUMeter = React21.memo(SegmentedVUMeterInner);
|
|
3657
|
+
|
|
3372
3658
|
// src/utils/conversions.ts
|
|
3373
3659
|
function samplesToSeconds(samples, sampleRate) {
|
|
3374
3660
|
return samples / sampleRate;
|
|
@@ -3433,6 +3719,7 @@ export {
|
|
|
3433
3719
|
PlayoutProvider,
|
|
3434
3720
|
ScreenReaderOnly,
|
|
3435
3721
|
ScrollViewportProvider,
|
|
3722
|
+
SegmentedVUMeter,
|
|
3436
3723
|
Selection,
|
|
3437
3724
|
SelectionTimeInputs,
|
|
3438
3725
|
Slider,
|