@moises.ai/design-system 4.16.1 → 4.16.2

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.16.1",
3
+ "version": "4.16.2",
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,203 @@
1
+ import classNames from 'classnames'
2
+ import { Slider as SliderRadix } from 'radix-ui'
3
+ import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
4
+ import { Tooltip } from '../Tooltip/Tooltip'
5
+ import styles from './SliderLibrary.module.css'
6
+
7
+ /**
8
+ * @typedef {Object} SliderLibraryProps
9
+ * @property {number} value - Currently selected value (controlled).
10
+ * @property {(value: number) => void} [onValueChange] - Fires whenever the user drags,
11
+ * keys the thumb or scrolls over the slider. Receives a single number (not an array).
12
+ * @property {number} [min=0] - Minimum allowed value.
13
+ * @property {number} [max=100] - Maximum allowed value.
14
+ * @property {number} [step=1] - Increment step. Used by keyboard, drag and wheel.
15
+ * @property {boolean} [disabled=false] - Disables interaction and dims the slider.
16
+ * @property {boolean} [showTooltip=true] - Renders the DS Tooltip below the thumb on
17
+ * hover/focus/drag with the current value.
18
+ * @property {(value: number) => React.ReactNode} [formatValue] - Formats the value for
19
+ * display in the tooltip. Defaults to the raw number.
20
+ * @property {string} [aria-label] - Accessible label, recommended when no visual label exists.
21
+ * @property {string} [className] - Extra class on the root.
22
+ */
23
+
24
+ /**
25
+ * Minimalist horizontal slider used in Library player surfaces.
26
+ *
27
+ * Visuals (from Figma):
28
+ * - 4px track with `--neutral-alpha-3` background.
29
+ * - Range fill in `--aqua-alpha-8` (cyan).
30
+ * - 18x10 white thumb with subtle border, rounded-full.
31
+ * - DS `Tooltip` anchored below the thumb shows the current value, following the thumb during drag.
32
+ *
33
+ * Behavior:
34
+ * - Drag: pointer/touch. Tooltip stays open and follows the thumb continuously.
35
+ * - Keyboard (via Radix Slider primitives): arrows/Home/End/PageUp/PageDown.
36
+ * - Wheel: scrolling over the slider increments/decrements by `step`.
37
+ * - Focus ring: appears only when the thumb is focused via keyboard, never on click/drag.
38
+ *
39
+ * @example
40
+ * const [volume, setVolume] = useState(50)
41
+ * <SliderLibrary
42
+ * value={volume}
43
+ * onValueChange={setVolume}
44
+ * formatValue={(v) => `${v}%`}
45
+ * aria-label="Volume"
46
+ * />
47
+ *
48
+ * @type {React.ForwardRefExoticComponent<SliderLibraryProps & React.RefAttributes<HTMLSpanElement>>}
49
+ */
50
+ export const SliderLibrary = forwardRef(function SliderLibrary(
51
+ {
52
+ value,
53
+ onValueChange,
54
+ min = 0,
55
+ max = 100,
56
+ step = 1,
57
+ disabled = false,
58
+ showTooltip = true,
59
+ formatValue,
60
+ className,
61
+ ...props
62
+ },
63
+ ref,
64
+ ) {
65
+ const ariaLabel = props['aria-label']
66
+ const internalRef = useRef(null)
67
+ const lastInteractionRef = useRef(null)
68
+ const [keyboardFocus, setKeyboardFocus] = useState(false)
69
+ const [isHovering, setIsHovering] = useState(false)
70
+ const [isDragging, setIsDragging] = useState(false)
71
+
72
+ const stateRef = useRef({ value, min, max, step, disabled, onValueChange })
73
+ stateRef.current = { value, min, max, step, disabled, onValueChange }
74
+
75
+ useEffect(() => {
76
+ const onKey = () => {
77
+ lastInteractionRef.current = 'keyboard'
78
+ }
79
+ const onPointer = () => {
80
+ lastInteractionRef.current = 'pointer'
81
+ }
82
+ window.addEventListener('keydown', onKey, true)
83
+ window.addEventListener('pointerdown', onPointer, true)
84
+ return () => {
85
+ window.removeEventListener('keydown', onKey, true)
86
+ window.removeEventListener('pointerdown', onPointer, true)
87
+ }
88
+ }, [])
89
+
90
+ useEffect(() => {
91
+ const node = internalRef.current
92
+ if (!node) return undefined
93
+
94
+ const handleWheel = (event) => {
95
+ const state = stateRef.current
96
+ if (state.disabled) return
97
+ event.preventDefault()
98
+ const direction = event.deltaY < 0 ? 1 : -1
99
+ const next = Math.min(
100
+ state.max,
101
+ Math.max(state.min, state.value + direction * state.step),
102
+ )
103
+ if (next !== state.value) state.onValueChange?.(next)
104
+ }
105
+
106
+ node.addEventListener('wheel', handleWheel, { passive: false })
107
+ return () => node.removeEventListener('wheel', handleWheel)
108
+ }, [])
109
+
110
+ useEffect(() => {
111
+ if (!isDragging) return undefined
112
+ const onUp = () => setIsDragging(false)
113
+ window.addEventListener('pointerup', onUp)
114
+ window.addEventListener('pointercancel', onUp)
115
+ return () => {
116
+ window.removeEventListener('pointerup', onUp)
117
+ window.removeEventListener('pointercancel', onUp)
118
+ }
119
+ }, [isDragging])
120
+
121
+ useEffect(() => {
122
+ if (!isDragging) return undefined
123
+ const style = document.createElement('style')
124
+ style.textContent = '* { cursor: ew-resize !important; }'
125
+ document.head.appendChild(style)
126
+ return () => {
127
+ if (style.parentNode) style.parentNode.removeChild(style)
128
+ }
129
+ }, [isDragging])
130
+
131
+ const setRefs = useCallback(
132
+ (node) => {
133
+ internalRef.current = node
134
+ if (typeof ref === 'function') ref(node)
135
+ else if (ref) ref.current = node
136
+ },
137
+ [ref],
138
+ )
139
+
140
+ const handleRootPointerDown = () => {
141
+ if (!disabled) setIsDragging(true)
142
+ }
143
+
144
+ const handleThumbFocus = () => {
145
+ if (lastInteractionRef.current === 'keyboard') setKeyboardFocus(true)
146
+ }
147
+ const handleThumbBlur = () => setKeyboardFocus(false)
148
+ const handleThumbPointerDown = () => setKeyboardFocus(false)
149
+ const handleThumbPointerEnter = () => setIsHovering(true)
150
+ const handleThumbPointerLeave = () => setIsHovering(false)
151
+
152
+ const displayValue = formatValue ? formatValue(value) : value
153
+ const tooltipOpen =
154
+ !disabled && (isHovering || isDragging || keyboardFocus)
155
+
156
+ const thumb = (
157
+ <SliderRadix.Thumb
158
+ className={styles.thumb}
159
+ data-focus-visible={keyboardFocus || undefined}
160
+ aria-label={ariaLabel}
161
+ onFocus={handleThumbFocus}
162
+ onBlur={handleThumbBlur}
163
+ onPointerDown={handleThumbPointerDown}
164
+ onPointerEnter={handleThumbPointerEnter}
165
+ onPointerLeave={handleThumbPointerLeave}
166
+ />
167
+ )
168
+
169
+ return (
170
+ <SliderRadix.Root
171
+ ref={setRefs}
172
+ className={classNames(styles.root, className)}
173
+ value={[value]}
174
+ onValueChange={(next) => onValueChange?.(next[0])}
175
+ min={min}
176
+ max={max}
177
+ step={step}
178
+ disabled={disabled}
179
+ onPointerDown={handleRootPointerDown}
180
+ {...props}
181
+ >
182
+ <SliderRadix.Track className={styles.track}>
183
+ <SliderRadix.Range className={styles.range} />
184
+ </SliderRadix.Track>
185
+ {showTooltip ? (
186
+ <Tooltip
187
+ content={displayValue}
188
+ open={tooltipOpen}
189
+ delayDuration={0}
190
+ side="bottom"
191
+ sideOffset={8}
192
+ updatePositionStrategy="always"
193
+ >
194
+ {thumb}
195
+ </Tooltip>
196
+ ) : (
197
+ thumb
198
+ )}
199
+ </SliderRadix.Root>
200
+ )
201
+ })
202
+
203
+ SliderLibrary.displayName = 'SliderLibrary'
@@ -0,0 +1,53 @@
1
+ .root {
2
+ position: relative;
3
+ display: flex;
4
+ align-items: center;
5
+ width: 100%;
6
+ min-width: 0;
7
+ height: 19px;
8
+ user-select: none;
9
+ touch-action: none;
10
+ }
11
+
12
+ .root[data-disabled] {
13
+ opacity: 0.5;
14
+ cursor: not-allowed;
15
+ }
16
+
17
+ .track {
18
+ position: relative;
19
+ flex-grow: 1;
20
+ height: 4px;
21
+ background: var(--neutral-alpha-3, rgba(221, 234, 248, 0.08));
22
+ border-radius: 99px;
23
+ overflow: hidden;
24
+ }
25
+
26
+ .range {
27
+ position: absolute;
28
+ height: 100%;
29
+ background: var(--aqua-alpha-8, rgba(0, 216, 230, 0.84));
30
+ border-radius: 99px;
31
+ }
32
+
33
+ .thumb {
34
+ position: relative;
35
+ display: block;
36
+ width: 18px;
37
+ height: 10px;
38
+ background: var(--white, #fff);
39
+ border: none;
40
+ border-radius: 99px;
41
+ box-shadow: 0 0 0 1px #2e31354d;
42
+ outline: none;
43
+ cursor: ew-resize;
44
+ }
45
+
46
+ .thumb[data-focus-visible] {
47
+ outline: 2px solid var(--neutral-alpha-8, rgba(217, 237, 255, 0.36));
48
+ outline-offset: 2px;
49
+ }
50
+
51
+ .root[data-disabled] .thumb {
52
+ cursor: not-allowed;
53
+ }
@@ -0,0 +1,100 @@
1
+ import { useState } from 'react'
2
+ import { SliderLibrary } from './SliderLibrary'
3
+
4
+ export default {
5
+ title: 'Components/SliderLibrary',
6
+ component: SliderLibrary,
7
+ parameters: {
8
+ layout: 'centered',
9
+ },
10
+ argTypes: {
11
+ value: { control: { type: 'number' } },
12
+ min: { control: { type: 'number' } },
13
+ max: { control: { type: 'number' } },
14
+ step: { control: { type: 'number' } },
15
+ disabled: { control: { type: 'boolean' } },
16
+ },
17
+ decorators: [
18
+ (Story) => (
19
+ <div
20
+ style={{
21
+ width: 280,
22
+ padding: 24,
23
+ background: '#1d1d1d',
24
+ borderRadius: 8,
25
+ }}
26
+ >
27
+ <Story />
28
+ </div>
29
+ ),
30
+ ],
31
+ }
32
+
33
+ export const Default = {
34
+ render: () => {
35
+ const [value, setValue] = useState(50)
36
+ return (
37
+ <SliderLibrary
38
+ value={value}
39
+ onValueChange={setValue}
40
+ formatValue={(v) => `${v}%`}
41
+ aria-label="Volume"
42
+ />
43
+ )
44
+ },
45
+ }
46
+
47
+ export const Disabled = {
48
+ render: () => (
49
+ <SliderLibrary
50
+ value={75}
51
+ onValueChange={() => {}}
52
+ disabled
53
+ aria-label="Volume (disabled)"
54
+ />
55
+ ),
56
+ }
57
+
58
+ export const CustomRange = {
59
+ name: 'Custom range (-12 to 12 dB)',
60
+ render: () => {
61
+ const [value, setValue] = useState(0)
62
+ return (
63
+ <SliderLibrary
64
+ value={value}
65
+ onValueChange={setValue}
66
+ min={-12}
67
+ max={12}
68
+ step={0.5}
69
+ formatValue={(v) => `${v.toFixed(1)} dB`}
70
+ aria-label="Gain"
71
+ />
72
+ )
73
+ },
74
+ }
75
+
76
+ export const WithoutTooltip = {
77
+ render: () => {
78
+ const [value, setValue] = useState(50)
79
+ return (
80
+ <SliderLibrary
81
+ value={value}
82
+ onValueChange={setValue}
83
+ showTooltip={false}
84
+ aria-label="Volume"
85
+ />
86
+ )
87
+ },
88
+ }
89
+
90
+ export const WithLabel = {
91
+ render: () => {
92
+ const [value, setValue] = useState(30)
93
+ return (
94
+ <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
95
+ <span style={{ color: '#fff', fontSize: 14 }}>Volume</span>
96
+ <SliderLibrary value={value} onValueChange={setValue} />
97
+ </label>
98
+ )
99
+ },
100
+ }
@@ -0,0 +1 @@
1
+ export { SliderLibrary } from './SliderLibrary'
package/src/index.jsx CHANGED
@@ -130,6 +130,7 @@ export { Shell } from './components/Shell/Shell'
130
130
  export { Sidebar } from './components/Sidebar/Sidebar'
131
131
  export { SidebarSection } from './components/Sidebar/SidebarSection/SidebarSection'
132
132
  export { Slider } from './components/Slider/Slider'
133
+ export { SliderLibrary } from './components/SliderLibrary/SliderLibrary'
133
134
  export { StemGenerationForm } from './components/StemGenerationForm/StemGenerationForm'
134
135
  export { GenerationStatus } from './components/StemSeparationForm/GenerationStatus/GenerationStatus'
135
136
  export { StemSeparationForm } from './components/StemSeparationForm/StemSeparationForm'