@moises.ai/design-system 3.11.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moises.ai/design-system",
3
- "version": "3.11.2",
3
+ "version": "3.11.3",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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
+ }
@@ -1,8 +1,8 @@
1
1
  import { Flex, Text } from '@radix-ui/themes'
2
2
  import { Popover } from 'radix-ui'
3
3
  import { useCallback, useState } from 'react'
4
+ import { InputLevelMeter } from '../InputLevelMeter/InputLevelMeter'
4
5
  import { Select } from '../Select/Select'
5
- import { Slider } from '../Slider/Slider'
6
6
  import styles from './RecordSettingsPopover.module.css'
7
7
 
8
8
  export function RecordSettingsPopover({
@@ -18,11 +18,14 @@ export function RecordSettingsPopover({
18
18
  }) {
19
19
  const [opened, setOpened] = useState(false)
20
20
 
21
- const handleOpenChange = useCallback((open) => {
22
- if (!open || !isRecord) {
23
- setOpened(open)
24
- }
25
- }, [])
21
+ const handleOpenChange = useCallback(
22
+ (open) => {
23
+ if (!open || !isRecord) {
24
+ setOpened(open)
25
+ }
26
+ },
27
+ [open, isRecord],
28
+ )
26
29
 
27
30
  const handleContextMenu = useCallback(() => {
28
31
  e.preventDefault()
@@ -69,11 +72,7 @@ export function RecordSettingsPopover({
69
72
  </Text>
70
73
 
71
74
  {opened && (
72
- <Slider
73
- value={inputLevel}
74
- max={30}
75
- style={{ paddingTop: '8px', paddingBottom: '8px' }}
76
- />
75
+ <InputLevelMeter linear={inputLevel} showHandler={false} />
77
76
  )}
78
77
  </Flex>
79
78
  </Flex>
@@ -9,8 +9,8 @@ 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,
@@ -32,14 +32,13 @@ export const TrackHeader = memo(
32
32
  menuOptions,
33
33
  showPan = true,
34
34
  showVolumeControls = true,
35
- isGrouped = false,
36
35
  height = 81,
37
36
  compact = false,
38
37
  isActive = false,
39
38
  instrumentOptions,
40
39
  deviceOptions,
41
40
  channelOptions,
42
- inputLevel,
41
+ inputLevel: inputLevel,
43
42
 
44
43
  // State
45
44
  volume,
@@ -213,9 +212,10 @@ export const TrackHeader = memo(
213
212
  <div {...DND_PROTECTION} className={styles.trackControls}>
214
213
  {!compact && showVolumeControls && (
215
214
  <>
216
- <HorizontalVolume
215
+ <InputLevelMeter
216
+ linear={inputLevel}
217
217
  volume={volume}
218
- onChangeValue={onVolumeChange}
218
+ onVolumeChange={onVolumeChange}
219
219
  />
220
220
  {showPan && (
221
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) => <TrackHeader {...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 = {
package/src/index.jsx CHANGED
@@ -87,6 +87,7 @@ export { Countdown } from './components/Countdown/Countdown'
87
87
  export { DataTable } from './components/DataTable/DataTable'
88
88
  export { DropdownButton } from './components/DropdownButton/DropdownButton'
89
89
  export { DropdownMenu } from './components/DropdownMenu/DropdownMenu'
90
+ export { EmptyState } from './components/EmptyState/EmptyState'
90
91
  export {
91
92
  Extension,
92
93
  useExtension,
@@ -100,6 +101,7 @@ export { GroupButtons } from './components/GroupButtons/GroupButtons'
100
101
  export { HeaderPanel } from './components/HeaderPanel/HeaderPanel'
101
102
  export { HorizontalVolume } from './components/HorizontalVolume/HorizontalVolume'
102
103
  export { IconButton } from './components/IconButton/IconButton'
104
+ export { InputLevelMeter } from './components/InputLevelMeter/InputLevelMeter'
103
105
  export { InstrumentSelector } from './components/InstrumentSelector/InstrumentSelector'
104
106
  export { Knob } from './components/Knob/Knob'
105
107
  export { ListCards } from './components/ListCards/ListCards'
@@ -132,15 +134,16 @@ export { Text } from './components/Text/Text'
132
134
  export { TextArea } from './components/TextArea/TextArea'
133
135
  export { TextField } from './components/TextField/TextField'
134
136
  export { Theme } from './components/theme/Theme'
135
- export { ToastProvider, useToast } from './components/ToastProvider/ToastProvider'
136
137
  export { ThumbnailPicker } from './components/ThumbnailPicker/ThumbnailPicker'
138
+ export {
139
+ ToastProvider,
140
+ useToast,
141
+ } from './components/ToastProvider/ToastProvider'
137
142
  export { Tooltip } from './components/Tooltip/Tooltip'
138
143
  export { TooltipWithInfoIcon } from './components/TooltipWithInfoIcon/TooltipWithInfoIcon'
139
144
  export { TrackControlButton } from './components/TrackControlButton'
145
+ export { TrackControlsToggle } from './components/TrackControlsToggle/TrackControlsToggle'
140
146
  export { TrackHeader } from './components/TrackHeader/TrackHeader'
141
147
  export { useForm } from './components/useForm/useForm'
142
148
  export { VoiceConversionForm } from './components/VoiceConversionForm/VoiceConversionForm'
143
149
  export { Waveform } from './components/VoiceConversionForm/Waveform/Waveform'
144
-
145
- export { EmptyState } from './components/EmptyState/EmptyState'
146
- export { TrackControlsToggle } from './components/TrackControlsToggle/TrackControlsToggle'