@moises.ai/design-system 4.14.4 → 4.14.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moises.ai/design-system",
3
- "version": "4.14.4",
3
+ "version": "4.14.6",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,4 +1,11 @@
1
- import { memo, useCallback, useEffect, useRef, useState } from 'react'
1
+ import {
2
+ forwardRef,
3
+ memo,
4
+ useCallback,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ } from 'react'
2
9
  import { Tooltip } from '../Tooltip/Tooltip'
3
10
  import styles from './InputLevelMeter.module.css'
4
11
 
@@ -7,6 +14,8 @@ const SEGMENT_RANGE = 25
7
14
  const UNITY_GAIN_POS = 0.667
8
15
  const MAX_BOOST_DB = 6
9
16
  const FADER_KEYBOARD_STEP = 0.02
17
+ const THUMB_DOUBLE_TAP_MS = 400
18
+ const THUMB_DOUBLE_TAP_MAX_PX = 12
10
19
 
11
20
  function linearToMeterLevel(linear) {
12
21
  const peakDb = 20 * Math.log10(Math.max(linear, 1e-6))
@@ -81,10 +90,134 @@ function MeterRow({ level }) {
81
90
  )
82
91
  }
83
92
 
93
+ const MeterThumb = memo(
94
+ forwardRef(function MeterThumb(
95
+ {
96
+ faderPercent,
97
+ isPressed,
98
+ hover,
99
+ volume,
100
+ doubleTapStateRef,
101
+ onVolumeChange,
102
+ onVolumeReset,
103
+ },
104
+ ref,
105
+ ) {
106
+ const [thumbFocused, setThumbFocused] = useState(false)
107
+ const [hoverOpen, setHoverOpen] = useState(false)
108
+
109
+ const tooltipOpen = isPressed || thumbFocused || hoverOpen
110
+
111
+ const handlePointerDownCapture = useCallback(
112
+ (e) => {
113
+ if (!onVolumeReset || e.button !== 0) return
114
+ const now = performance.now()
115
+ const prev = doubleTapStateRef.current
116
+ if (
117
+ prev &&
118
+ now - prev.t < THUMB_DOUBLE_TAP_MS &&
119
+ Math.hypot(e.clientX - prev.x, e.clientY - prev.y) <
120
+ THUMB_DOUBLE_TAP_MAX_PX
121
+ ) {
122
+ e.preventDefault()
123
+ e.stopPropagation()
124
+ doubleTapStateRef.current = null
125
+ onVolumeReset()
126
+ return
127
+ }
128
+ doubleTapStateRef.current = {
129
+ t: now,
130
+ x: e.clientX,
131
+ y: e.clientY,
132
+ }
133
+ },
134
+ [doubleTapStateRef, onVolumeReset],
135
+ )
136
+
137
+ const handleKeyDown = useCallback(
138
+ (e) => {
139
+ if (!onVolumeChange) return
140
+ const pos = gainToFaderPosition(volume)
141
+ let nextPos = pos
142
+ switch (e.key) {
143
+ case 'ArrowRight':
144
+ case 'ArrowUp':
145
+ e.preventDefault()
146
+ nextPos = Math.min(1, pos + FADER_KEYBOARD_STEP)
147
+ onVolumeChange([faderPositionToGain(nextPos)])
148
+ break
149
+ case 'ArrowLeft':
150
+ case 'ArrowDown':
151
+ e.preventDefault()
152
+ nextPos = Math.max(0, pos - FADER_KEYBOARD_STEP)
153
+ onVolumeChange([faderPositionToGain(nextPos)])
154
+ break
155
+ case 'Home':
156
+ e.preventDefault()
157
+ onVolumeChange([faderPositionToGain(0)])
158
+ break
159
+ case 'End':
160
+ e.preventDefault()
161
+ onVolumeChange([faderPositionToGain(1)])
162
+ break
163
+ case 'PageUp':
164
+ e.preventDefault()
165
+ nextPos = Math.min(1, pos + FADER_KEYBOARD_STEP * 4)
166
+ onVolumeChange([faderPositionToGain(nextPos)])
167
+ break
168
+ case 'PageDown':
169
+ e.preventDefault()
170
+ nextPos = Math.max(0, pos - FADER_KEYBOARD_STEP * 4)
171
+ onVolumeChange([faderPositionToGain(nextPos)])
172
+ break
173
+ default:
174
+ break
175
+ }
176
+ },
177
+ [onVolumeChange, volume],
178
+ )
179
+
180
+ const gainDbForAria =
181
+ volume <= 0 ? -96 : 20 * Math.log10(Math.max(volume, 1e-6))
182
+
183
+ return (
184
+ <Tooltip
185
+ content={formatGainDb(volume)}
186
+ open={tooltipOpen}
187
+ onOpenChange={setHoverOpen}
188
+ side="bottom"
189
+ align="center"
190
+ sideOffset={3}
191
+ delayDuration={0}
192
+ >
193
+ <div
194
+ ref={ref}
195
+ role="slider"
196
+ tabIndex={0}
197
+ aria-label="Input gain"
198
+ aria-valuemin={-96}
199
+ aria-valuemax={MAX_BOOST_DB}
200
+ aria-valuenow={Math.round(gainDbForAria * 10) / 10}
201
+ aria-valuetext={formatGainDb(volume)}
202
+ className={`${styles.thumb} ${isPressed ? styles.pressed : hover ? styles.hovered : ''}`}
203
+ style={{ left: `${faderPercent}%` }}
204
+ onPointerDownCapture={handlePointerDownCapture}
205
+ onFocus={() => setThumbFocused(true)}
206
+ onBlur={() => setThumbFocused(false)}
207
+ onKeyDown={handleKeyDown}
208
+ />
209
+ </Tooltip>
210
+ )
211
+ }),
212
+ )
213
+
214
+ MeterThumb.displayName = 'MeterThumb'
215
+
84
216
  export const InputLevelMeter = memo(function InputLevelMeter({
85
217
  linear = [0, 0],
86
218
  volume = 1,
87
219
  onVolumeChange,
220
+ onVolumeReset,
88
221
  showHandler = true,
89
222
  showMeter = true,
90
223
  hover = false,
@@ -97,14 +230,13 @@ export const InputLevelMeter = memo(function InputLevelMeter({
97
230
  const [isPressed, setIsPressed] = useState(false)
98
231
  const isPressedRef = useRef(false)
99
232
  const meterRef = useRef(null)
233
+ const thumbRef = useRef(null)
234
+ const thumbDoubleTapStateRef = useRef(null)
100
235
  const peakHoldRef = useRef(0)
101
236
  const peakTimerRef = useRef(null)
102
237
  const [peakHoldPct, setPeakHoldPct] = useState(0)
103
238
  const [peakRising, setPeakRising] = useState(false)
104
239
 
105
- const [thumbFocused, setThumbFocused] = useState(false)
106
- const [hoverOpen, setHoverOpen] = useState(false)
107
-
108
240
  useEffect(() => {
109
241
  peakHoldRef.current = 0
110
242
  setPeakHoldPct(0)
@@ -151,6 +283,13 @@ export const InputLevelMeter = memo(function InputLevelMeter({
151
283
  const handlePointerDown = useCallback(
152
284
  (e) => {
153
285
  if (!onVolumeChange) return
286
+ if (
287
+ thumbRef.current &&
288
+ e.target instanceof Element &&
289
+ !thumbRef.current.contains(e.target)
290
+ ) {
291
+ thumbDoubleTapStateRef.current = null
292
+ }
154
293
  e.currentTarget.setPointerCapture(e.pointerId)
155
294
  isPressedRef.current = true
156
295
  setIsPressed(true)
@@ -188,49 +327,6 @@ export const InputLevelMeter = memo(function InputLevelMeter({
188
327
  document.body.style.cursor = ''
189
328
  }, [])
190
329
 
191
- const handleThumbKeyDown = useCallback(
192
- (e) => {
193
- if (!onVolumeChange) return
194
- const pos = gainToFaderPosition(volume)
195
- let nextPos = pos
196
- switch (e.key) {
197
- case 'ArrowRight':
198
- case 'ArrowUp':
199
- e.preventDefault()
200
- nextPos = Math.min(1, pos + FADER_KEYBOARD_STEP)
201
- onVolumeChange([faderPositionToGain(nextPos)])
202
- break
203
- case 'ArrowLeft':
204
- case 'ArrowDown':
205
- e.preventDefault()
206
- nextPos = Math.max(0, pos - FADER_KEYBOARD_STEP)
207
- onVolumeChange([faderPositionToGain(nextPos)])
208
- break
209
- case 'Home':
210
- e.preventDefault()
211
- onVolumeChange([faderPositionToGain(0)])
212
- break
213
- case 'End':
214
- e.preventDefault()
215
- onVolumeChange([faderPositionToGain(1)])
216
- break
217
- case 'PageUp':
218
- e.preventDefault()
219
- nextPos = Math.min(1, pos + FADER_KEYBOARD_STEP * 4)
220
- onVolumeChange([faderPositionToGain(nextPos)])
221
- break
222
- case 'PageDown':
223
- e.preventDefault()
224
- nextPos = Math.max(0, pos - FADER_KEYBOARD_STEP * 4)
225
- onVolumeChange([faderPositionToGain(nextPos)])
226
- break
227
- default:
228
- break
229
- }
230
- },
231
- [onVolumeChange, volume],
232
- )
233
-
234
330
  const hasHandler = onVolumeChange && showHandler
235
331
 
236
332
  const meterClassName = [
@@ -241,11 +337,6 @@ export const InputLevelMeter = memo(function InputLevelMeter({
241
337
  .filter(Boolean)
242
338
  .join(' ')
243
339
 
244
- const tooltipOpen = isPressed || thumbFocused || hoverOpen
245
-
246
- const gainDbForAria =
247
- volume <= 0 ? -96 : 20 * Math.log10(Math.max(volume, 1e-6))
248
-
249
340
  return (
250
341
  <div
251
342
  className={styles.container}
@@ -261,30 +352,16 @@ export const InputLevelMeter = memo(function InputLevelMeter({
261
352
  <MeterRow level={rightLevel} />
262
353
 
263
354
  {hasHandler && (
264
- <Tooltip
265
- content={formatGainDb(volume)}
266
- open={tooltipOpen}
267
- onOpenChange={setHoverOpen}
268
- side="bottom"
269
- align="center"
270
- sideOffset={3}
271
- delayDuration={0}
272
- >
273
- <div
274
- role="slider"
275
- tabIndex={0}
276
- aria-label="Input gain"
277
- aria-valuemin={-96}
278
- aria-valuemax={MAX_BOOST_DB}
279
- aria-valuenow={Math.round(gainDbForAria * 10) / 10}
280
- aria-valuetext={formatGainDb(volume)}
281
- className={`${styles.thumb} ${isPressed ? styles.pressed : hover ? styles.hovered : ''}`}
282
- style={{ left: `${faderPercent}%` }}
283
- onFocus={() => setThumbFocused(true)}
284
- onBlur={() => setThumbFocused(false)}
285
- onKeyDown={handleThumbKeyDown}
286
- />
287
- </Tooltip>
355
+ <MeterThumb
356
+ ref={thumbRef}
357
+ faderPercent={faderPercent}
358
+ isPressed={isPressed}
359
+ hover={hover}
360
+ volume={volume}
361
+ doubleTapStateRef={thumbDoubleTapStateRef}
362
+ onVolumeChange={onVolumeChange}
363
+ onVolumeReset={onVolumeReset}
364
+ />
288
365
  )}
289
366
 
290
367
  <div
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef, useCallback } from 'react'
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
2
  import { InputLevelMeter } from './InputLevelMeter'
3
3
  import { useSimulatedInputLevel } from './useSimulatedInputLevel'
4
4
 
@@ -47,12 +47,18 @@ function InteractiveMeter({ initialVolume = 1, ...props }) {
47
47
  linear={linear}
48
48
  volume={volume}
49
49
  onVolumeChange={handleVolumeChange}
50
+ onVolumeReset={() => setVolume(1)}
50
51
  {...props}
51
52
  />
52
53
  <span
53
- style={{ color: 'rgba(255,255,255,0.5)', fontSize: '11px', fontFamily: 'monospace' }}
54
+ style={{
55
+ color: 'rgba(255,255,255,0.5)',
56
+ fontSize: '11px',
57
+ fontFamily: 'monospace',
58
+ }}
54
59
  >
55
- gain: {volume.toFixed(3)} | dB: {volume > 0 ? (20 * Math.log10(volume)).toFixed(1) : '-∞'}
60
+ gain: {volume.toFixed(3)} | dB:{' '}
61
+ {volume > 0 ? (20 * Math.log10(volume)).toFixed(1) : '-∞'}
56
62
  </span>
57
63
  </div>
58
64
  )
@@ -152,13 +158,29 @@ export const HoverState = {
152
158
  render: () => (
153
159
  <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
154
160
  <div>
155
- <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '10px', fontFamily: 'monospace', display: 'block', marginBottom: '4px' }}>
161
+ <span
162
+ style={{
163
+ color: 'rgba(255,255,255,0.4)',
164
+ fontSize: '10px',
165
+ fontFamily: 'monospace',
166
+ display: 'block',
167
+ marginBottom: '4px',
168
+ }}
169
+ >
156
170
  Default
157
171
  </span>
158
172
  <AnimatedMeter />
159
173
  </div>
160
174
  <div>
161
- <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '10px', fontFamily: 'monospace', display: 'block', marginBottom: '4px' }}>
175
+ <span
176
+ style={{
177
+ color: 'rgba(255,255,255,0.4)',
178
+ fontSize: '10px',
179
+ fontFamily: 'monospace',
180
+ display: 'block',
181
+ marginBottom: '4px',
182
+ }}
183
+ >
162
184
  hover=true (parent hovered)
163
185
  </span>
164
186
  <AnimatedMeter hover />
@@ -258,13 +280,15 @@ export const MultipleMeters = {
258
280
  const linear = useSimulatedInputLevel()
259
281
  const [volumes, setVolumes] = useState([1, 0.7, 0.4, 0.1])
260
282
 
261
- const makeHandler = (idx) => ([v]) => {
262
- setVolumes((prev) => {
263
- const next = [...prev]
264
- next[idx] = v
265
- return next
266
- })
267
- }
283
+ const makeHandler =
284
+ (idx) =>
285
+ ([v]) => {
286
+ setVolumes((prev) => {
287
+ const next = [...prev]
288
+ next[idx] = v
289
+ return next
290
+ })
291
+ }
268
292
 
269
293
  const labels = ['Master', 'Vocals', 'Guitar', 'Drums']
270
294
 
@@ -54,6 +54,7 @@ export const TrackHeader = memo(
54
54
 
55
55
  // State change listeners
56
56
  onVolumeChange,
57
+ onVolumeReset,
57
58
  onPanChange,
58
59
  onMutedChange,
59
60
  onRecordChange,
@@ -218,6 +219,7 @@ export const TrackHeader = memo(
218
219
  linear={inputLevel}
219
220
  volume={volume}
220
221
  onVolumeChange={onVolumeChange}
222
+ onVolumeReset={onVolumeReset}
221
223
  />
222
224
  {showPan && (
223
225
  <PanControl
@@ -28,7 +28,7 @@ import { TrackHeader } from '@moises.ai/design-system'
28
28
  import { TrackHeader } from '@moises.ai/design-system'
29
29
 
30
30
  <TrackHeader title="Group Track">
31
- <TrackHeader title="Grouped Track" showPan showVolumeControls volume={0.7} pan={-0.3} isMuted={false} isSolo={false} onVolumeChange={() => {}} onPanChange={() => {}} onMutedChange={() => {}} onSoloChange={() => {}} isGrouped />
31
+ <TrackHeader title="Grouped Track" showPan showVolumeControls volume={0.7} pan={-0.3} isMuted={false} isSolo={false} onVolumeChange={() => {}} onVolumeReset={() => {}} onPanChange={() => {}} onMutedChange={() => {}} onSoloChange={() => {}} isGrouped />
32
32
  </TrackHeader>
33
33
  \`\`\`
34
34
 
@@ -59,6 +59,7 @@ import { TrackHeader } from '@moises.ai/design-system'
59
59
  isMuted: boolean; // Whether the track is muted
60
60
  isSolo: boolean; // Whether the track is solo
61
61
  onVolumeChange: (volume: number) => void; // Callback function to handle volume change
62
+ onVolumeReset: () => void; // Callback function to handle volume reset
62
63
  onPanChange: (pan: number) => void; // Callback function to handle pan change
63
64
  onMutedChange: (muted: boolean) => void; // Callback function to handle muted change
64
65
  onSoloChange: (solo: boolean) => void; // Callback function to handle solo change
@@ -123,10 +124,11 @@ export const Default = {
123
124
  isAutoMuted: false,
124
125
  compact: false,
125
126
  menuOptions,
126
- onVolumeChange: () => { },
127
- onPanChange: () => { },
128
- onMutedChange: () => { },
129
- onSoloChange: () => { },
127
+ onVolumeChange: () => {},
128
+ onVolumeReset: () => {},
129
+ onPanChange: () => {},
130
+ onMutedChange: () => {},
131
+ onSoloChange: () => {},
130
132
  instrumentOptions,
131
133
  instrumentSelected: instrumentOptions[0],
132
134
  },
@@ -177,20 +179,26 @@ export const Group = {
177
179
  isAutoMuted: false,
178
180
  compact: false,
179
181
  menuOptions,
180
- onVolumeChange: () => { },
181
- onPanChange: () => { },
182
- onMutedChange: () => { },
183
- onSoloChange: () => { },
182
+ onVolumeChange: () => {},
183
+ onPanChange: () => {},
184
+ onMutedChange: () => {},
185
+ onSoloChange: () => {},
184
186
  instrumentOptions,
185
187
  instrumentSelected: instrumentOptions[0],
186
- onInstrumentChange: () => { },
188
+ onInstrumentChange: () => {},
187
189
  },
188
190
  render: (args) => {
189
191
  const [isOpen, setIsOpen] = useState(true)
190
192
  const [pan, setPan] = useState(-0.3)
191
193
  const [pan2, setPan2] = useState(-0.3)
192
194
  return (
193
- <TrackHeader {...args} isOpen={isOpen} onOpenChange={setIsOpen} instrumentSelected={args.instrumentSelected} onInstrumentChange={args.onInstrumentChange}>
195
+ <TrackHeader
196
+ {...args}
197
+ isOpen={isOpen}
198
+ onOpenChange={setIsOpen}
199
+ instrumentSelected={args.instrumentSelected}
200
+ onInstrumentChange={args.onInstrumentChange}
201
+ >
194
202
  <TrackHeader
195
203
  title="Grouped Track"
196
204
  isGrouped
@@ -200,10 +208,10 @@ export const Group = {
200
208
  pan={pan}
201
209
  isMuted={false}
202
210
  isSolo={false}
203
- onVolumeChange={() => { }}
211
+ onVolumeChange={() => {}}
204
212
  onPanChange={setPan}
205
- onMutedChange={() => { }}
206
- onSoloChange={() => { }}
213
+ onMutedChange={() => {}}
214
+ onSoloChange={() => {}}
207
215
  menuOptions={menuOptions}
208
216
  instrumentOptions={args.instrumentOptions}
209
217
  instrumentSelected={args.instrumentSelected}
@@ -218,10 +226,10 @@ export const Group = {
218
226
  pan={pan2}
219
227
  isMuted={false}
220
228
  isSolo={false}
221
- onVolumeChange={() => { }}
229
+ onVolumeChange={() => {}}
222
230
  onPanChange={setPan2}
223
- onMutedChange={() => { }}
224
- onSoloChange={() => { }}
231
+ onMutedChange={() => {}}
232
+ onSoloChange={() => {}}
225
233
  menuOptions={menuOptions}
226
234
  instrumentOptions={args.instrumentOptions}
227
235
  instrumentSelected={args.instrumentSelected}
@@ -245,18 +253,21 @@ export const Takelanes = {
245
253
  isSolo: false,
246
254
  compact: false,
247
255
  menuOptions,
248
- onVolumeChange: () => { },
249
- onPanChange: () => { },
250
- onMutedChange: () => { },
251
- onSoloChange: () => { },
252
- onOpenChange: () => { },
256
+ onVolumeChange: () => {},
257
+ onPanChange: () => {},
258
+ onMutedChange: () => {},
259
+ onSoloChange: () => {},
260
+ onOpenChange: () => {},
253
261
  instrumentOptions,
254
262
  instrumentSelected: instrumentOptions[0],
255
- onInstrumentChange: () => { },
256
-
263
+ onInstrumentChange: () => {},
257
264
  },
258
265
  render: (args) => (
259
- <TrackHeader {...args} instrumentSelected={args.instrumentSelected} onInstrumentChange={args.onInstrumentChange}>
266
+ <TrackHeader
267
+ {...args}
268
+ instrumentSelected={args.instrumentSelected}
269
+ onInstrumentChange={args.onInstrumentChange}
270
+ >
260
271
  <TrackHeader
261
272
  title="Take Lane 1"
262
273
  showPan={false}