@moises.ai/design-system 3.11.20 → 3.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moises.ai/design-system",
3
- "version": "3.11.20",
3
+ "version": "3.12.0",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -6,6 +6,7 @@ const SEGMENT_MIN = 5
6
6
  const SEGMENT_RANGE = 25
7
7
  const UNITY_GAIN_POS = 0.667
8
8
  const MAX_BOOST_DB = 6
9
+ const FADER_KEYBOARD_STEP = 0.02
9
10
 
10
11
  function linearToMeterLevel(linear) {
11
12
  const peakDb = 20 * Math.log10(Math.max(linear, 1e-6))
@@ -101,6 +102,9 @@ export const InputLevelMeter = memo(function InputLevelMeter({
101
102
  const [peakHoldPct, setPeakHoldPct] = useState(0)
102
103
  const [peakRising, setPeakRising] = useState(false)
103
104
 
105
+ const [thumbFocused, setThumbFocused] = useState(false)
106
+ const [hoverOpen, setHoverOpen] = useState(false)
107
+
104
108
  useEffect(() => {
105
109
  peakHoldRef.current = 0
106
110
  setPeakHoldPct(0)
@@ -184,6 +188,49 @@ export const InputLevelMeter = memo(function InputLevelMeter({
184
188
  document.body.style.cursor = ''
185
189
  }, [])
186
190
 
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
+
187
234
  const hasHandler = onVolumeChange && showHandler
188
235
 
189
236
  const meterClassName = [
@@ -194,6 +241,11 @@ export const InputLevelMeter = memo(function InputLevelMeter({
194
241
  .filter(Boolean)
195
242
  .join(' ')
196
243
 
244
+ const tooltipOpen = isPressed || thumbFocused || hoverOpen
245
+
246
+ const gainDbForAria =
247
+ volume <= 0 ? -96 : 20 * Math.log10(Math.max(volume, 1e-6))
248
+
197
249
  return (
198
250
  <div
199
251
  className={styles.container}
@@ -211,15 +263,26 @@ export const InputLevelMeter = memo(function InputLevelMeter({
211
263
  {hasHandler && (
212
264
  <Tooltip
213
265
  content={formatGainDb(volume)}
214
- open={isPressed}
266
+ open={tooltipOpen}
267
+ onOpenChange={setHoverOpen}
215
268
  side="bottom"
216
269
  align="center"
217
- sideOffset={8}
270
+ sideOffset={3}
218
271
  delayDuration={0}
219
272
  >
220
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)}
221
281
  className={`${styles.thumb} ${isPressed ? styles.pressed : hover ? styles.hovered : ''}`}
222
282
  style={{ left: `${faderPercent}%` }}
283
+ onFocus={() => setThumbFocused(true)}
284
+ onBlur={() => setThumbFocused(false)}
285
+ onKeyDown={handleThumbKeyDown}
223
286
  />
224
287
  </Tooltip>
225
288
  )}
@@ -78,6 +78,11 @@
78
78
  background: white;
79
79
  }
80
80
 
81
+ .thumb:focus-visible {
82
+ outline: 2px solid var(--neutral-alpha-8);
83
+ outline-offset: 2px;
84
+ }
85
+
81
86
  .peakDot {
82
87
  position: absolute;
83
88
  width: 1px;
@@ -2,13 +2,14 @@ import styles from './PanControl.module.css'
2
2
  import classNames from 'classnames'
3
3
  import { useRef, useCallback, useMemo, useState, useEffect } from 'react'
4
4
  import { useEventListener } from '../../utils/useEventListener'
5
- import { Tooltip } from '../../index'
5
+ import { Tooltip } from '../Tooltip/Tooltip'
6
6
 
7
7
  const KNOB_CENTER_X = 10
8
8
  const KNOB_CENTER_Y = 10
9
9
  const KNOB_RADIUS = 9.5
10
10
  const START_ANGLE = -Math.PI / 2
11
11
  const STEP_SIZE = 0.01
12
+ const KEYBOARD_STEP = STEP_SIZE * 12
12
13
 
13
14
  const clampNorm = (v) => Math.max(-1, Math.min(1, v))
14
15
  const denormalize = (normalized, minValue, maxValue) =>
@@ -30,6 +31,16 @@ export const PanControl = ({
30
31
  const pendingNormRef = useRef(value)
31
32
  const rafRef = useRef(0)
32
33
 
34
+ const [knobFocused, setKnobFocused] = useState(false)
35
+ const [hoverOpen, setHoverOpen] = useState(false)
36
+
37
+ useEffect(() => {
38
+ if (disabled) {
39
+ setHoverOpen(false)
40
+ setKnobFocused(false)
41
+ }
42
+ }, [disabled])
43
+
33
44
  useEffect(() => {
34
45
  if (isDragging) {
35
46
  const style = document.createElement('style')
@@ -111,6 +122,38 @@ export const PanControl = ({
111
122
  [scheduleEmit],
112
123
  )
113
124
 
125
+ const handleKnobKeyDown = useCallback(
126
+ (e) => {
127
+ if (disabled) return
128
+ let next = value
129
+ switch (e.key) {
130
+ case 'ArrowUp':
131
+ case 'ArrowRight':
132
+ e.preventDefault()
133
+ next = value + KEYBOARD_STEP
134
+ scheduleEmit(next)
135
+ break
136
+ case 'ArrowDown':
137
+ case 'ArrowLeft':
138
+ e.preventDefault()
139
+ next = value - KEYBOARD_STEP
140
+ scheduleEmit(next)
141
+ break
142
+ case 'Home':
143
+ e.preventDefault()
144
+ scheduleEmit(-1)
145
+ break
146
+ case 'End':
147
+ e.preventDefault()
148
+ scheduleEmit(1)
149
+ break
150
+ default:
151
+ break
152
+ }
153
+ },
154
+ [disabled, value, scheduleEmit],
155
+ )
156
+
114
157
  const { arcPath, pointerRotation, arcColor, pointerColor } = useMemo(() => {
115
158
  const actual = denormalize(value, minValue, maxValue)
116
159
  const displayNorm = clampNorm(actual)
@@ -138,6 +181,12 @@ export const PanControl = ({
138
181
  return `Pan: ${Math.abs(pct) < 1 ? 0 : pct}`
139
182
  }, [value, minValue, maxValue])
140
183
 
184
+ const displayNorm = clampNorm(denormalize(value, minValue, maxValue))
185
+ const ariaPanPct = Math.round(displayNorm * 100)
186
+
187
+ const tooltipOpen =
188
+ !disabled && (isDragging || knobFocused || hoverOpen)
189
+
141
190
  return (
142
191
  <div
143
192
  ref={containerRef}
@@ -148,13 +197,24 @@ export const PanControl = ({
148
197
  >
149
198
  <Tooltip
150
199
  content={panValueText}
151
- open={isDragging}
200
+ open={tooltipOpen}
201
+ onOpenChange={(next) => {
202
+ if (!disabled) setHoverOpen(next)
203
+ }}
152
204
  side="bottom"
153
205
  align="center"
154
- sideOffset={10}
206
+ sideOffset={3}
155
207
  delayDuration={0}
156
208
  >
157
209
  <div
210
+ role="slider"
211
+ tabIndex={disabled ? -1 : 0}
212
+ aria-label="Pan"
213
+ aria-valuemin={-100}
214
+ aria-valuemax={100}
215
+ aria-valuenow={ariaPanPct}
216
+ aria-valuetext={panValueText}
217
+ aria-disabled={disabled}
158
218
  className={classNames(
159
219
  styles.pan,
160
220
  isDragging && styles.active,
@@ -164,6 +224,9 @@ export const PanControl = ({
164
224
  onPointerMove={onPointerMove}
165
225
  onPointerUp={onPointerUp}
166
226
  onPointerCancel={onPointerUp}
227
+ onFocus={() => setKnobFocused(true)}
228
+ onBlur={() => setKnobFocused(false)}
229
+ onKeyDown={handleKnobKeyDown}
167
230
  >
168
231
  <svg width={20} height={20} viewBox="0 0 20 20" fill="none">
169
232
  <circle cx={KNOB_CENTER_X} cy={KNOB_CENTER_Y} r={10} />
@@ -37,4 +37,9 @@
37
37
  fill: #DDEAF8;
38
38
  fill-opacity: 0.0784314;
39
39
  }
40
-
40
+
41
+ .pan:focus-visible:not(.disabled) {
42
+ outline: 2px solid var(--neutral-alpha-8);
43
+ outline-offset: 2px;
44
+ border-radius: 50%;
45
+ }
@@ -14,6 +14,26 @@ import {
14
14
  import { useMobileDrawer } from '../../utils/useMobileDrawer'
15
15
  import { MobileDrawerProvider } from '../../contexts/MobileDrawerContext'
16
16
 
17
+ const DEFAULT_MOBILE_BREAKPOINT = 768
18
+
19
+ const getMobileMediaQuery = (mobileBreakpoint) => {
20
+ if (mobileBreakpoint == null) {
21
+ return `(max-width: ${DEFAULT_MOBILE_BREAKPOINT}px)`
22
+ }
23
+
24
+ if (typeof mobileBreakpoint === 'number') {
25
+ return `(max-width: ${mobileBreakpoint}px)`
26
+ }
27
+
28
+ if (typeof mobileBreakpoint === 'string') {
29
+ const trimmed = mobileBreakpoint.trim()
30
+ if (!trimmed) return `(max-width: ${DEFAULT_MOBILE_BREAKPOINT}px)`
31
+ return trimmed.startsWith('(') ? trimmed : `(max-width: ${trimmed})`
32
+ }
33
+
34
+ return `(max-width: ${DEFAULT_MOBILE_BREAKPOINT}px)`
35
+ }
36
+
17
37
  export const Sidebar = ({
18
38
  className,
19
39
  children,
@@ -23,9 +43,12 @@ export const Sidebar = ({
23
43
  onCollapsedChange,
24
44
  onLogoClick,
25
45
  tooltip = 'Expand menu',
46
+ mobileBreakpoint = DEFAULT_MOBILE_BREAKPOINT,
26
47
  ...props
27
48
  }) => {
28
- const { isMobile, isOpen: mobileOpen, open, close } = useMobileDrawer()
49
+ const mobileMediaQuery = getMobileMediaQuery(mobileBreakpoint)
50
+ const { isMobile, isOpen: mobileOpen, open, close } =
51
+ useMobileDrawer(mobileMediaQuery)
29
52
  const [isHovered, setIsHovered] = useState(false)
30
53
  const [internalTemporarilyExpandedByDrag, setInternalTemporarilyExpandedByDrag] =
31
54
  useState(false)
@@ -159,6 +182,7 @@ export const Sidebar = ({
159
182
  <div
160
183
  className={classNames(styles.root, {
161
184
  [styles.rootCollapsed]: effectiveCollapsed,
185
+ [styles.rootMobile]: isMobile,
162
186
  })}
163
187
  {...props}
164
188
  >
@@ -184,8 +208,8 @@ export const Sidebar = ({
184
208
  cursor: effectiveCollapsed || onLogoClick ? 'pointer' : 'default',
185
209
  }}
186
210
  >
187
- <MoisesLogoIcon width={32} height={17} />
188
- <MoisesIcon height={12.269} />
211
+ <MoisesLogoIcon width={32} height={17} />
212
+ <MoisesIcon height={12.269} />
189
213
  </Flex>
190
214
  </button>
191
215
 
@@ -240,35 +264,35 @@ export const Sidebar = ({
240
264
  [styles.headerCollapsed]: effectiveCollapsed,
241
265
  })}
242
266
  >
243
- <button
244
- type="button"
245
- className={styles.logoButton}
246
- onClick={handleLogoClick}
247
- aria-label={desktopLogoAriaLabel}
248
- >
249
- <Flex
250
- align="center"
251
- gap="2"
252
- style={{
253
- cursor: effectiveCollapsed || onLogoClick ? 'pointer' : 'default',
254
- }}
267
+ <button
268
+ type="button"
269
+ className={styles.logoButton}
270
+ onClick={handleLogoClick}
271
+ aria-label={desktopLogoAriaLabel}
255
272
  >
256
- {logoContent}
257
- </Flex>
258
- </button>
259
- <button
260
- type="button"
261
- onClick={handleToggleCollapse}
262
- className={classNames(styles.toggleButton, {
263
- [styles.toggleButtonHidden]: effectiveCollapsed,
264
- })}
265
- aria-label="Collapse menu"
266
- >
267
- <SidebarLeftIcon width={16} height={16} />
268
- </button>
269
- </Flex>
270
- </div>
271
- </Tooltip>
273
+ <Flex
274
+ align="center"
275
+ gap="2"
276
+ style={{
277
+ cursor: effectiveCollapsed || onLogoClick ? 'pointer' : 'default',
278
+ }}
279
+ >
280
+ {logoContent}
281
+ </Flex>
282
+ </button>
283
+ <button
284
+ type="button"
285
+ onClick={handleToggleCollapse}
286
+ className={classNames(styles.toggleButton, {
287
+ [styles.toggleButtonHidden]: effectiveCollapsed,
288
+ })}
289
+ aria-label="Collapse menu"
290
+ >
291
+ <SidebarLeftIcon width={16} height={16} />
292
+ </button>
293
+ </Flex>
294
+ </div>
295
+ </Tooltip>
272
296
  </div>
273
297
  ) : (
274
298
  <div className={styles.headerWrapper}>
@@ -279,33 +303,33 @@ export const Sidebar = ({
279
303
  [styles.headerCollapsed]: effectiveCollapsed,
280
304
  })}
281
305
  >
282
- <button
283
- type="button"
284
- className={styles.logoButton}
285
- onClick={handleLogoClick}
286
- aria-label={desktopLogoAriaLabel}
287
- >
288
- <Flex
289
- align="center"
290
- gap="2"
291
- style={{
292
- cursor: effectiveCollapsed || onLogoClick ? 'pointer' : 'default',
293
- }}
306
+ <button
307
+ type="button"
308
+ className={styles.logoButton}
309
+ onClick={handleLogoClick}
310
+ aria-label={desktopLogoAriaLabel}
294
311
  >
295
- {logoContent}
296
- </Flex>
297
- </button>
298
- <button
299
- type="button"
300
- onClick={handleToggleCollapse}
301
- className={classNames(styles.toggleButton, {
302
- [styles.toggleButtonHidden]: effectiveCollapsed,
303
- })}
304
- aria-label="Collapse menu"
305
- >
306
- <SidebarLeftIcon width={16} height={16} />
307
- </button>
308
- </Flex>
312
+ <Flex
313
+ align="center"
314
+ gap="2"
315
+ style={{
316
+ cursor: effectiveCollapsed || onLogoClick ? 'pointer' : 'default',
317
+ }}
318
+ >
319
+ {logoContent}
320
+ </Flex>
321
+ </button>
322
+ <button
323
+ type="button"
324
+ onClick={handleToggleCollapse}
325
+ className={classNames(styles.toggleButton, {
326
+ [styles.toggleButtonHidden]: effectiveCollapsed,
327
+ })}
328
+ aria-label="Collapse menu"
329
+ >
330
+ <SidebarLeftIcon width={16} height={16} />
331
+ </button>
332
+ </Flex>
309
333
  </div>
310
334
  )}
311
335
  <Flex direction="column" className={styles.content}>
@@ -148,6 +148,106 @@
148
148
  color: var(--neutral-alpha-12);
149
149
  }
150
150
 
151
+ .rootMobile {
152
+ position: absolute;
153
+ width: 0;
154
+ height: 0;
155
+ overflow: visible;
156
+ background: transparent;
157
+ border: none;
158
+ }
159
+
160
+ .rootMobile.rootCollapsed {
161
+ width: 0;
162
+ height: 0;
163
+ }
164
+
165
+ .rootMobile .desktopSidebar {
166
+ display: none;
167
+ }
168
+
169
+ .rootMobile .mobileTopBarWrapper {
170
+ display: block;
171
+ position: fixed;
172
+ top: 0;
173
+ left: 0;
174
+ right: 0;
175
+ height: 64px;
176
+ z-index: 1002;
177
+ background-color: var(--neutral-1);
178
+ border-bottom: 1px solid var(--neutral-3);
179
+ }
180
+
181
+ .rootMobile .mobileTopBar {
182
+ display: flex;
183
+ width: 100%;
184
+ border-bottom: none;
185
+ }
186
+
187
+ .rootMobile .mobileOverlay {
188
+ display: block;
189
+ position: fixed;
190
+ inset: 0;
191
+ z-index: 1000;
192
+ background-color: rgba(0, 0, 0, 0.5);
193
+ opacity: 0;
194
+ pointer-events: none;
195
+ transition: opacity 160ms ease-out;
196
+ }
197
+
198
+ .rootMobile .mobileOverlayOpen {
199
+ opacity: 1;
200
+ pointer-events: auto;
201
+ }
202
+
203
+ .rootMobile .mobileDrawer {
204
+ display: flex;
205
+ flex-direction: column;
206
+ position: fixed;
207
+ top: 64px;
208
+ left: 0;
209
+ right: 0;
210
+ bottom: 0;
211
+ z-index: 1001;
212
+ width: 100%;
213
+ height: calc(100vh - 64px);
214
+ height: calc(100dvh - 64px);
215
+ padding: 16px;
216
+ background-color: var(--neutral-1);
217
+ opacity: 0;
218
+ visibility: hidden;
219
+ pointer-events: none;
220
+ transition: opacity 200ms ease-out, visibility 0s linear 200ms;
221
+ box-sizing: border-box;
222
+ overflow-y: auto;
223
+ overflow-x: hidden;
224
+ }
225
+
226
+ .rootMobile .mobileDrawerOpen {
227
+ opacity: 1;
228
+ visibility: visible;
229
+ pointer-events: auto;
230
+ transition: opacity 200ms ease-out, visibility 0s linear 0s;
231
+ }
232
+
233
+ .rootMobile .mobileDrawerContent {
234
+ display: flex;
235
+ flex-direction: column;
236
+ flex: 1;
237
+ transform: translateY(-12px);
238
+ opacity: 0;
239
+ transition: transform 240ms ease-out, opacity 240ms ease-out;
240
+ will-change: transform, opacity;
241
+ width: 100%;
242
+ min-height: 0;
243
+ height: 100%;
244
+ }
245
+
246
+ .rootMobile .mobileDrawerContentOpen {
247
+ transform: translateY(0);
248
+ opacity: 1;
249
+ }
250
+
151
251
  @media (max-width: 768px) {
152
252
  .root {
153
253
  position: absolute;
@@ -2,8 +2,8 @@
2
2
  box-sizing: border-box;
3
3
  width: 20px;
4
4
  min-width: 20px;
5
+ height: 20px;
5
6
  min-height: 20px;
6
- height: 100%;
7
7
  margin: 0;
8
8
  padding: 2px;
9
9
  border: none;
@@ -7,7 +7,7 @@ export const MinusIcon = ({ width = 16, height = 16, className, ...props }) => (
7
7
  fill="none"
8
8
  className={className} {...props}>
9
9
  <g >
10
- <path d="M3.33325 8H12.6666" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
10
+ <path d="M3.33325 8H12.6666" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
11
11
  </g>
12
12
  </svg>
13
13
  )
@@ -11,12 +11,12 @@ export const Share2Icon = ({
11
11
  fill="none"
12
12
  xmlns="http://www.w3.org/2000/svg"
13
13
  >
14
- <g clip-path="url(#clip0_6624_4145)">
14
+ <g clipPath="url(#clip0_6624_4145)">
15
15
  <path
16
16
  d="M10.1875 2.89583L8.00001 0.708328M8.00001 0.708328L5.81251 2.89583M8.00001 0.708328V10.1875M10.9167 5.08333C12.5275 5.08333 13.8333 6.38916 13.8333 7.99999V12.2917C13.8333 13.9485 12.4902 15.2917 10.8333 15.2917H5.16656C3.50968 15.2917 2.16653 13.9485 2.16656 12.2916L2.16663 7.99999C2.16666 6.38915 3.47251 5.08333 5.08335 5.08333"
17
17
  stroke="#B0B4BA"
18
- stroke-linecap="round"
19
- stroke-linejoin="round"
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
20
  />
21
21
  </g>
22
22
  <defs>