@moises.ai/design-system 3.11.1 → 3.11.3
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 +7944 -7734
- package/package.json +1 -1
- package/src/components/InputLevelMeter/InputLevelMeter.jsx +205 -0
- package/src/components/InputLevelMeter/InputLevelMeter.module.css +111 -0
- package/src/components/InputLevelMeter/InputLevelMeter.stories.jsx +27 -0
- package/src/components/InputLevelMeter/useSimulatedInputLevel.js +33 -0
- package/src/components/TrackHeader/RecordSettingsPopover.jsx +82 -0
- package/src/components/TrackHeader/RecordSettingsPopover.module.css +13 -0
- package/src/components/TrackHeader/TrackHeader.jsx +48 -40
- package/src/components/TrackHeader/TrackHeader.stories.jsx +28 -1
- package/src/index.jsx +7 -4
package/package.json
CHANGED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { memo, useCallback, useRef, useState } from 'react'
|
|
2
|
+
import styles from './InputLevelMeter.module.css'
|
|
3
|
+
|
|
4
|
+
const SEGMENT_MIN = 5
|
|
5
|
+
const SEGMENT_RANGE = 25
|
|
6
|
+
const UNITY_GAIN_POS = 0.667
|
|
7
|
+
const MAX_BOOST_DB = 6
|
|
8
|
+
|
|
9
|
+
function linearToMeterLevel(linear) {
|
|
10
|
+
const peakDb = 20 * Math.log10(Math.max(linear, 1e-6))
|
|
11
|
+
return Math.max(0, Math.min(30, (peakDb + 60) / 2))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function clampLevel(level) {
|
|
15
|
+
if (!Number.isFinite(level)) return 0
|
|
16
|
+
return Math.max(0, Math.min(30, level))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function clampPeakDb(peakDb) {
|
|
20
|
+
if (!Number.isFinite(peakDb)) return Number.NEGATIVE_INFINITY
|
|
21
|
+
return Math.max(-96, peakDb)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getNormalizedLevel(level) {
|
|
25
|
+
return Math.max(
|
|
26
|
+
0,
|
|
27
|
+
Math.min(100, ((clampLevel(level) - SEGMENT_MIN) / SEGMENT_RANGE) * 100),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getPeakHoldColor(peakPct) {
|
|
32
|
+
if (peakPct >= 88) return 'rgba(254, 78, 84, 0.89)'
|
|
33
|
+
if (peakPct >= 69) return 'rgb(255, 197, 61)'
|
|
34
|
+
return '#00dae8'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function gainToFaderPosition(gain) {
|
|
38
|
+
if (gain <= 0) return 0
|
|
39
|
+
const db = 20 * Math.log10(gain)
|
|
40
|
+
if (db <= -96) return 0
|
|
41
|
+
if (db <= 0) return ((db + 96) / 96) * UNITY_GAIN_POS
|
|
42
|
+
return (
|
|
43
|
+
UNITY_GAIN_POS +
|
|
44
|
+
(Math.min(db, MAX_BOOST_DB) / MAX_BOOST_DB) * (1 - UNITY_GAIN_POS)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function faderPositionToGain(pos) {
|
|
49
|
+
if (pos <= 0) return 0
|
|
50
|
+
let db
|
|
51
|
+
if (pos <= UNITY_GAIN_POS) {
|
|
52
|
+
db = (pos / UNITY_GAIN_POS) * 96 - 96
|
|
53
|
+
} else {
|
|
54
|
+
db = ((pos - UNITY_GAIN_POS) / (1 - UNITY_GAIN_POS)) * MAX_BOOST_DB
|
|
55
|
+
}
|
|
56
|
+
return 10 ** (db / 20)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function MeterRow({ level }) {
|
|
60
|
+
const pct = getNormalizedLevel(level)
|
|
61
|
+
return (
|
|
62
|
+
<div className={styles.barRow}>
|
|
63
|
+
<div className={styles.barTrack} />
|
|
64
|
+
<div
|
|
65
|
+
className={styles.barFill}
|
|
66
|
+
style={{
|
|
67
|
+
width: `${pct}%`,
|
|
68
|
+
backgroundSize: pct > 0 ? `${10000 / pct}% 100%` : '100% 100%',
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const InputLevelMeter = memo(function InputLevelMeter({
|
|
76
|
+
linear = [0, 0],
|
|
77
|
+
volume = 1,
|
|
78
|
+
onVolumeChange,
|
|
79
|
+
showHandler = true,
|
|
80
|
+
showMeter = true,
|
|
81
|
+
}) {
|
|
82
|
+
const [linearL, linearR] = linear
|
|
83
|
+
const leftLevel = clampLevel(linearToMeterLevel(linearL ?? 0))
|
|
84
|
+
const rightLevel = clampLevel(linearToMeterLevel(linearR ?? 0))
|
|
85
|
+
|
|
86
|
+
const [isPressed, setIsPressed] = useState(false)
|
|
87
|
+
const isPressedRef = useRef(false)
|
|
88
|
+
const meterRef = useRef(null)
|
|
89
|
+
const peakHoldRef = useRef(0)
|
|
90
|
+
|
|
91
|
+
const faderPercent = Math.max(
|
|
92
|
+
0,
|
|
93
|
+
Math.min(100, gainToFaderPosition(volume) * 100),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const leftNormalized = getNormalizedLevel(leftLevel)
|
|
97
|
+
const rightNormalized = getNormalizedLevel(rightLevel)
|
|
98
|
+
const currentPeakPct = Math.max(leftNormalized, rightNormalized)
|
|
99
|
+
|
|
100
|
+
if (currentPeakPct > peakHoldRef.current) {
|
|
101
|
+
peakHoldRef.current = currentPeakPct
|
|
102
|
+
}
|
|
103
|
+
if (currentPeakPct <= 0) {
|
|
104
|
+
peakHoldRef.current = 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const peakHoldPct = peakHoldRef.current
|
|
108
|
+
const peakHoldColor = getPeakHoldColor(peakHoldPct)
|
|
109
|
+
const showPeakHold = peakHoldPct > 0
|
|
110
|
+
|
|
111
|
+
const handlePointerDown = useCallback(
|
|
112
|
+
(e) => {
|
|
113
|
+
if (!onVolumeChange) return
|
|
114
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
115
|
+
isPressedRef.current = true
|
|
116
|
+
setIsPressed(true)
|
|
117
|
+
const rect = meterRef.current?.getBoundingClientRect()
|
|
118
|
+
if (rect) {
|
|
119
|
+
const pos = Math.max(
|
|
120
|
+
0,
|
|
121
|
+
Math.min(1, (e.clientX - rect.left) / rect.width),
|
|
122
|
+
)
|
|
123
|
+
onVolumeChange([faderPositionToGain(pos)])
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
[onVolumeChange],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const handlePointerMove = useCallback(
|
|
130
|
+
(e) => {
|
|
131
|
+
if (!isPressedRef.current || !onVolumeChange) return
|
|
132
|
+
const rect = meterRef.current?.getBoundingClientRect()
|
|
133
|
+
if (rect) {
|
|
134
|
+
const pos = Math.max(
|
|
135
|
+
0,
|
|
136
|
+
Math.min(1, (e.clientX - rect.left) / rect.width),
|
|
137
|
+
)
|
|
138
|
+
onVolumeChange([faderPositionToGain(pos)])
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
[onVolumeChange],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const handlePointerUp = useCallback(() => {
|
|
145
|
+
isPressedRef.current = false
|
|
146
|
+
setIsPressed(false)
|
|
147
|
+
}, [])
|
|
148
|
+
|
|
149
|
+
const meterClassName = [styles.bars, isPressed ? styles.pressed : '']
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.join(' ')
|
|
152
|
+
|
|
153
|
+
const hasHandler = onVolumeChange && showHandler
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
className={styles.container}
|
|
158
|
+
onMouseLeave={() => {
|
|
159
|
+
if (!isPressed) setIsPressed(false)
|
|
160
|
+
}}
|
|
161
|
+
onPointerDown={handlePointerDown}
|
|
162
|
+
onPointerMove={handlePointerMove}
|
|
163
|
+
onPointerUp={handlePointerUp}
|
|
164
|
+
>
|
|
165
|
+
<div ref={meterRef} className={meterClassName}>
|
|
166
|
+
<MeterRow level={leftLevel} />
|
|
167
|
+
<MeterRow level={rightLevel} />
|
|
168
|
+
|
|
169
|
+
{hasHandler && (
|
|
170
|
+
<div
|
|
171
|
+
className={`${styles.thumb} ${isPressed ? styles.pressed : ''}`}
|
|
172
|
+
style={{ left: `${faderPercent}%` }}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{showPeakHold && (
|
|
177
|
+
<>
|
|
178
|
+
<div
|
|
179
|
+
className={styles.peakDotTop}
|
|
180
|
+
style={{
|
|
181
|
+
left: `min(calc(${peakHoldPct}% + 3px), calc(100% - 3px))`,
|
|
182
|
+
background: peakHoldColor,
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
<div
|
|
186
|
+
className={styles.peakDotBottom}
|
|
187
|
+
style={{
|
|
188
|
+
left: `min(calc(${peakHoldPct}% + 3px), calc(100% - 3px))`,
|
|
189
|
+
background: peakHoldColor,
|
|
190
|
+
}}
|
|
191
|
+
/>
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{showMeter && (
|
|
197
|
+
<div
|
|
198
|
+
className={`${styles.zeroDbDot} ${isPressed ? styles.pressed : ''}`}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
InputLevelMeter.displayName = 'InputLevelMeter'
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 4px;
|
|
6
|
+
align-items: flex-start;
|
|
7
|
+
width: 100%;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.bars {
|
|
11
|
+
position: relative;
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: 1px;
|
|
15
|
+
padding: 2px;
|
|
16
|
+
border-radius: 3px;
|
|
17
|
+
width: 100%;
|
|
18
|
+
background: rgba(216, 244, 246, 0.04);
|
|
19
|
+
transition: background-color 0.1s ease;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.bars.pressed {
|
|
23
|
+
background: rgba(221, 234, 248, 0.08);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.barRow {
|
|
27
|
+
display: inline-grid;
|
|
28
|
+
grid-template-rows: max-content;
|
|
29
|
+
place-items: start;
|
|
30
|
+
width: 100%;
|
|
31
|
+
position: relative;
|
|
32
|
+
line-height: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.barTrack {
|
|
36
|
+
grid-column: 1;
|
|
37
|
+
grid-row: 1;
|
|
38
|
+
height: 3px;
|
|
39
|
+
width: 100%;
|
|
40
|
+
border-radius: 9999px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.barFill {
|
|
44
|
+
grid-column: 1;
|
|
45
|
+
grid-row: 1;
|
|
46
|
+
height: 3px;
|
|
47
|
+
border-radius: 9999px;
|
|
48
|
+
background: linear-gradient(
|
|
49
|
+
270deg,
|
|
50
|
+
rgba(254, 78, 84, 0.894) 12%,
|
|
51
|
+
rgb(255, 197, 61) 12%,
|
|
52
|
+
rgb(255, 197, 61) 31%,
|
|
53
|
+
rgb(0, 218, 232) 31%
|
|
54
|
+
);
|
|
55
|
+
will-change: width;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.thumb {
|
|
59
|
+
position: absolute;
|
|
60
|
+
top: 50%;
|
|
61
|
+
transform: translate(-50%, -50%);
|
|
62
|
+
width: 8px;
|
|
63
|
+
height: 19px;
|
|
64
|
+
border-radius: 9999px;
|
|
65
|
+
background: #edeef0;
|
|
66
|
+
cursor: ew-resize;
|
|
67
|
+
z-index: 2;
|
|
68
|
+
touch-action: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.thumb.pressed {
|
|
72
|
+
background: white;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.peakDot {
|
|
76
|
+
position: absolute;
|
|
77
|
+
width: 1px;
|
|
78
|
+
height: 3px;
|
|
79
|
+
border-radius: 9999px;
|
|
80
|
+
z-index: 1;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.peakDotTop {
|
|
85
|
+
composes: peakDot;
|
|
86
|
+
top: calc(50% - 2px);
|
|
87
|
+
transform: translate(-50%, -50%);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.peakDotBottom {
|
|
91
|
+
composes: peakDot;
|
|
92
|
+
top: calc(50% + 2px);
|
|
93
|
+
transform: translate(-50%, -50%);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.zeroDbDot {
|
|
97
|
+
position: absolute;
|
|
98
|
+
left: 66.7%;
|
|
99
|
+
top: 12px;
|
|
100
|
+
width: 2px;
|
|
101
|
+
height: 2px;
|
|
102
|
+
border-radius: 50%;
|
|
103
|
+
background: rgba(255, 255, 255, 0.12);
|
|
104
|
+
pointer-events: none;
|
|
105
|
+
transform: translateX(-50%);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.zeroDbDot.pressed {
|
|
109
|
+
width: 3px;
|
|
110
|
+
height: 3px;
|
|
111
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { InputLevelMeter } from './InputLevelMeter'
|
|
2
|
+
import { useSimulatedInputLevel } from './useSimulatedInputLevel'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Components/InputLevelMeter',
|
|
6
|
+
component: InputLevelMeter,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<div style={{ padding: '16px', background: '#1d1d1d', width: '200px' }}>
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function AnimatedMeter() {
|
|
21
|
+
const linear = useSimulatedInputLevel()
|
|
22
|
+
return <InputLevelMeter linear={linear} />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Default = {
|
|
26
|
+
render: () => <AnimatedMeter />,
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useSimulatedInputLevel() {
|
|
4
|
+
const [linear, setLinear] = useState([0.5, 0.5])
|
|
5
|
+
const targetRef = useRef([0.5, 0.5])
|
|
6
|
+
const currentRef = useRef([0.5, 0.5])
|
|
7
|
+
const frameRef = useRef(null)
|
|
8
|
+
const tickRef = useRef(0)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
function animate() {
|
|
12
|
+
tickRef.current++
|
|
13
|
+
|
|
14
|
+
if (tickRef.current % 20 === 0) {
|
|
15
|
+
targetRef.current = [Math.random(), Math.random()]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
currentRef.current = currentRef.current.map((cur, i) => {
|
|
19
|
+
const target = targetRef.current[i]
|
|
20
|
+
const speed = cur > target ? 0.15 : 0.08
|
|
21
|
+
return cur + (target - cur) * speed
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
setLinear([...currentRef.current])
|
|
25
|
+
frameRef.current = requestAnimationFrame(animate)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
frameRef.current = requestAnimationFrame(animate)
|
|
29
|
+
return () => cancelAnimationFrame(frameRef.current)
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
return linear
|
|
33
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Flex, Text } from '@radix-ui/themes'
|
|
2
|
+
import { Popover } from 'radix-ui'
|
|
3
|
+
import { useCallback, useState } from 'react'
|
|
4
|
+
import { InputLevelMeter } from '../InputLevelMeter/InputLevelMeter'
|
|
5
|
+
import { Select } from '../Select/Select'
|
|
6
|
+
import styles from './RecordSettingsPopover.module.css'
|
|
7
|
+
|
|
8
|
+
export function RecordSettingsPopover({
|
|
9
|
+
isRecord,
|
|
10
|
+
device,
|
|
11
|
+
deviceOptions,
|
|
12
|
+
onDeviceChange,
|
|
13
|
+
channel,
|
|
14
|
+
channelOptions,
|
|
15
|
+
onChannelChange,
|
|
16
|
+
inputLevel,
|
|
17
|
+
children,
|
|
18
|
+
}) {
|
|
19
|
+
const [opened, setOpened] = useState(false)
|
|
20
|
+
|
|
21
|
+
const handleOpenChange = useCallback(
|
|
22
|
+
(open) => {
|
|
23
|
+
if (!open || !isRecord) {
|
|
24
|
+
setOpened(open)
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
[open, isRecord],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const handleContextMenu = useCallback(() => {
|
|
31
|
+
e.preventDefault()
|
|
32
|
+
setOpened(true)
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Popover.Root open={opened} onOpenChange={handleOpenChange}>
|
|
37
|
+
<Popover.Trigger asChild onContextMenu={handleContextMenu}>
|
|
38
|
+
{children}
|
|
39
|
+
</Popover.Trigger>
|
|
40
|
+
|
|
41
|
+
<Popover.Content width="360px">
|
|
42
|
+
<Flex className={styles.root} direction="column" gap="4">
|
|
43
|
+
<Flex direction="column" gap="1">
|
|
44
|
+
<Text size="1" weight="medium">
|
|
45
|
+
Source
|
|
46
|
+
</Text>
|
|
47
|
+
|
|
48
|
+
{deviceOptions?.length ? (
|
|
49
|
+
<Select
|
|
50
|
+
items={deviceOptions}
|
|
51
|
+
defaultValue={device}
|
|
52
|
+
onChange={onDeviceChange}
|
|
53
|
+
size="1"
|
|
54
|
+
/>
|
|
55
|
+
) : (
|
|
56
|
+
<Text size="1">No device options.</Text>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{channelOptions?.length && (
|
|
60
|
+
<Select
|
|
61
|
+
items={channelOptions}
|
|
62
|
+
defaultValue={channel}
|
|
63
|
+
onChange={onChannelChange}
|
|
64
|
+
size="1"
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
</Flex>
|
|
68
|
+
|
|
69
|
+
<Flex direction="column" gap="1">
|
|
70
|
+
<Text size="1" weight="medium">
|
|
71
|
+
Input Level
|
|
72
|
+
</Text>
|
|
73
|
+
|
|
74
|
+
{opened && (
|
|
75
|
+
<InputLevelMeter linear={inputLevel} showHandler={false} />
|
|
76
|
+
)}
|
|
77
|
+
</Flex>
|
|
78
|
+
</Flex>
|
|
79
|
+
</Popover.Content>
|
|
80
|
+
</Popover.Root>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
width: 200px;
|
|
3
|
+
padding: var(--space-3);
|
|
4
|
+
border-radius: var(--radius-4, 8px);
|
|
5
|
+
border: 1px solid
|
|
6
|
+
var(--colors-neutral-neutral-alpha-3, rgba(221, 234, 248, 0.08));
|
|
7
|
+
background: var(--panel-solid, #18191b);
|
|
8
|
+
|
|
9
|
+
/* Shadows/shadow-5 */
|
|
10
|
+
box-shadow:
|
|
11
|
+
0 12px 32px -16px var(--overlays-black-alpha-5, rgba(0, 0, 0, 0.3)),
|
|
12
|
+
0 12px 60px 0 var(--overlays-black-alpha-3, rgba(0, 0, 0, 0.15));
|
|
13
|
+
}
|
|
@@ -9,29 +9,36 @@ import {
|
|
|
9
9
|
Box,
|
|
10
10
|
DropdownMenu,
|
|
11
11
|
Flex,
|
|
12
|
-
HorizontalVolume,
|
|
13
12
|
IconButton,
|
|
13
|
+
InputLevelMeter,
|
|
14
14
|
PanControl,
|
|
15
15
|
Text,
|
|
16
16
|
Tooltip,
|
|
17
17
|
TrackControlsToggle,
|
|
18
18
|
} from '../../index'
|
|
19
|
+
import { RecordSettingsPopover } from './RecordSettingsPopover'
|
|
19
20
|
import styles from './TrackHeader.module.css'
|
|
20
21
|
|
|
22
|
+
const DND_PROTECTION = {
|
|
23
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
24
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
25
|
+
onTouchStart: (e) => e.stopPropagation(),
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
export const TrackHeader = memo(
|
|
22
29
|
({
|
|
23
30
|
// Config
|
|
24
31
|
title,
|
|
25
32
|
menuOptions,
|
|
26
|
-
instrumentOptions,
|
|
27
|
-
onInstrumentChange,
|
|
28
|
-
instrumentSelected,
|
|
29
33
|
showPan = true,
|
|
30
34
|
showVolumeControls = true,
|
|
31
|
-
isGrouped = false,
|
|
32
35
|
height = 81,
|
|
33
36
|
compact = false,
|
|
34
37
|
isActive = false,
|
|
38
|
+
instrumentOptions,
|
|
39
|
+
deviceOptions,
|
|
40
|
+
channelOptions,
|
|
41
|
+
inputLevel: inputLevel,
|
|
35
42
|
|
|
36
43
|
// State
|
|
37
44
|
volume,
|
|
@@ -41,6 +48,9 @@ export const TrackHeader = memo(
|
|
|
41
48
|
isAutoMuted,
|
|
42
49
|
isSolo,
|
|
43
50
|
isOpen,
|
|
51
|
+
instrumentSelected,
|
|
52
|
+
device,
|
|
53
|
+
channel,
|
|
44
54
|
|
|
45
55
|
// State change listeners
|
|
46
56
|
onVolumeChange,
|
|
@@ -49,6 +59,9 @@ export const TrackHeader = memo(
|
|
|
49
59
|
onRecordChange,
|
|
50
60
|
onSoloChange,
|
|
51
61
|
onOpenChange,
|
|
62
|
+
onInstrumentChange,
|
|
63
|
+
onDeviceChange,
|
|
64
|
+
onChannelChange,
|
|
52
65
|
|
|
53
66
|
// Children (having content means group/takelanes)
|
|
54
67
|
children,
|
|
@@ -94,9 +107,7 @@ export const TrackHeader = memo(
|
|
|
94
107
|
style={{ minWidth: 0, overflow: 'hidden' }}
|
|
95
108
|
>
|
|
96
109
|
<IconButton
|
|
97
|
-
|
|
98
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
99
|
-
onTouchStart={(e) => e.stopPropagation()}
|
|
110
|
+
{...DND_PROTECTION}
|
|
100
111
|
variant="ghost"
|
|
101
112
|
size="1"
|
|
102
113
|
onClick={handleToggleOpen}
|
|
@@ -142,39 +153,41 @@ export const TrackHeader = memo(
|
|
|
142
153
|
</Flex>
|
|
143
154
|
|
|
144
155
|
<Flex
|
|
156
|
+
{...DND_PROTECTION}
|
|
145
157
|
direction="row"
|
|
146
|
-
gap="
|
|
158
|
+
gap="2px"
|
|
147
159
|
align="center"
|
|
148
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
149
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
150
|
-
onTouchStart={(e) => e.stopPropagation()}
|
|
151
160
|
>
|
|
152
|
-
<
|
|
161
|
+
<RecordSettingsPopover
|
|
162
|
+
isRecord={isRecord}
|
|
163
|
+
deviceOptions={deviceOptions}
|
|
164
|
+
device={device}
|
|
165
|
+
onDeviceChange={onDeviceChange}
|
|
166
|
+
channelOptions={channelOptions}
|
|
167
|
+
channel={channel}
|
|
168
|
+
onChannelChange={onChannelChange}
|
|
169
|
+
inputLevel={inputLevel}
|
|
170
|
+
>
|
|
153
171
|
<TrackControlsToggle
|
|
154
172
|
type="record"
|
|
155
173
|
selected={isRecord}
|
|
156
|
-
onClick={onRecordChange}
|
|
174
|
+
onClick={() => onRecordChange?.()}
|
|
157
175
|
/>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
/>
|
|
170
|
-
</Flex>
|
|
176
|
+
</RecordSettingsPopover>
|
|
177
|
+
<TrackControlsToggle
|
|
178
|
+
type={isMuted ? 'mute' : isAutoMuted ? 'autoMute' : 'mute'}
|
|
179
|
+
selected={isMuted || (!isSolo && isAutoMuted)}
|
|
180
|
+
onClick={onMutedChange}
|
|
181
|
+
/>
|
|
182
|
+
<TrackControlsToggle
|
|
183
|
+
type="solo"
|
|
184
|
+
selected={isSolo}
|
|
185
|
+
onClick={onSoloChange}
|
|
186
|
+
/>
|
|
171
187
|
</Flex>
|
|
172
188
|
|
|
173
189
|
{menuOptions && (
|
|
174
|
-
<Box
|
|
175
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
176
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
177
|
-
onTouchStart={(e) => e.stopPropagation()}>
|
|
190
|
+
<Box {...DND_PROTECTION}>
|
|
178
191
|
<DropdownMenu
|
|
179
192
|
trigger={
|
|
180
193
|
<IconButton
|
|
@@ -193,21 +206,16 @@ export const TrackHeader = memo(
|
|
|
193
206
|
/>
|
|
194
207
|
</Box>
|
|
195
208
|
)}
|
|
196
|
-
|
|
197
209
|
</Flex>
|
|
198
210
|
</div>
|
|
199
211
|
|
|
200
|
-
<div
|
|
201
|
-
className={styles.trackControls}
|
|
202
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
203
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
204
|
-
onTouchStart={(e) => e.stopPropagation()}
|
|
205
|
-
>
|
|
212
|
+
<div {...DND_PROTECTION} className={styles.trackControls}>
|
|
206
213
|
{!compact && showVolumeControls && (
|
|
207
214
|
<>
|
|
208
|
-
<
|
|
215
|
+
<InputLevelMeter
|
|
216
|
+
linear={inputLevel}
|
|
209
217
|
volume={volume}
|
|
210
|
-
|
|
218
|
+
onVolumeChange={onVolumeChange}
|
|
211
219
|
/>
|
|
212
220
|
{showPan && (
|
|
213
221
|
<PanControl
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { BassIcon, DrumsIcon, ElectricGuitarIcon, KeysIcon } from '../../icons'
|
|
3
|
+
import { useSimulatedInputLevel } from '../InputLevelMeter/useSimulatedInputLevel'
|
|
3
4
|
import { TrackHeader } from './TrackHeader'
|
|
4
5
|
export default {
|
|
5
6
|
title: 'Components/TrackHeader',
|
|
@@ -130,7 +131,33 @@ export const Default = {
|
|
|
130
131
|
instrumentOptions,
|
|
131
132
|
instrumentSelected: instrumentOptions[0],
|
|
132
133
|
},
|
|
133
|
-
render: (args) =>
|
|
134
|
+
render: (args) => {
|
|
135
|
+
const inputLevel = useSimulatedInputLevel()
|
|
136
|
+
const [isRecord, setIsRecord] = useState(false)
|
|
137
|
+
const [isMuted, setIsMuted] = useState(args.isMuted)
|
|
138
|
+
const [isSolo, setIsSolo] = useState(args.isSolo)
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<TrackHeader
|
|
142
|
+
{...args}
|
|
143
|
+
deviceOptions={[
|
|
144
|
+
{ value: 'mic', label: 'Built-in Microphone' },
|
|
145
|
+
{ value: 'interface', label: 'Audio Interface' },
|
|
146
|
+
]}
|
|
147
|
+
channelOptions={[
|
|
148
|
+
{ value: '1', label: 'Channel 1' },
|
|
149
|
+
{ value: '2', label: 'Channel 2' },
|
|
150
|
+
]}
|
|
151
|
+
isRecord={isRecord}
|
|
152
|
+
onRecordChange={() => setIsRecord((v) => !v)}
|
|
153
|
+
isMuted={isMuted}
|
|
154
|
+
onMutedChange={() => setIsMuted((v) => !v)}
|
|
155
|
+
isSolo={isSolo}
|
|
156
|
+
onSoloChange={() => setIsSolo((v) => !v)}
|
|
157
|
+
inputLevel={inputLevel}
|
|
158
|
+
/>
|
|
159
|
+
)
|
|
160
|
+
},
|
|
134
161
|
}
|
|
135
162
|
|
|
136
163
|
export const Group = {
|