@moises.ai/design-system 4.18.1 → 4.18.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.
@@ -22,6 +22,9 @@ export const PanControl = ({
22
22
  disabled,
23
23
  minValue = -0.65,
24
24
  maxValue = 0.65,
25
+ showTooltip = true,
26
+ tooltipDelayDuration = 0,
27
+ tooltipCloseDelay = 500,
25
28
  }) => {
26
29
  const containerRef = useRef(null)
27
30
  const [isDragging, setIsDragging] = useState(false)
@@ -30,17 +33,31 @@ export const PanControl = ({
30
33
  const startYRef = useRef(0)
31
34
  const pendingNormRef = useRef(value)
32
35
  const rafRef = useRef(0)
36
+ const openTimeoutRef = useRef(null)
37
+ const closeTimeoutRef = useRef(null)
38
+ const tooltipTimingRef = useRef({
39
+ tooltipDelayDuration,
40
+ tooltipCloseDelay,
41
+ })
42
+ tooltipTimingRef.current = { tooltipDelayDuration, tooltipCloseDelay }
33
43
 
34
44
  const [knobFocused, setKnobFocused] = useState(false)
35
- const [hoverOpen, setHoverOpen] = useState(false)
45
+ const [isHovering, setIsHovering] = useState(false)
36
46
 
37
47
  useEffect(() => {
38
48
  if (disabled) {
39
- setHoverOpen(false)
49
+ setIsHovering(false)
40
50
  setKnobFocused(false)
41
51
  }
42
52
  }, [disabled])
43
53
 
54
+ useEffect(() => {
55
+ return () => {
56
+ if (openTimeoutRef.current != null) clearTimeout(openTimeoutRef.current)
57
+ if (closeTimeoutRef.current != null) clearTimeout(closeTimeoutRef.current)
58
+ }
59
+ }, [])
60
+
44
61
  useEffect(() => {
45
62
  if (isDragging) {
46
63
  const style = document.createElement('style')
@@ -187,8 +204,107 @@ export const PanControl = ({
187
204
  const displayNorm = clampNorm(denormalize(value, minValue, maxValue))
188
205
  const ariaPanPct = Math.round(displayNorm * 100)
189
206
 
207
+ const clearOpenTimeout = () => {
208
+ if (openTimeoutRef.current != null) {
209
+ clearTimeout(openTimeoutRef.current)
210
+ openTimeoutRef.current = null
211
+ }
212
+ }
213
+
214
+ const clearCloseTimeout = () => {
215
+ if (closeTimeoutRef.current != null) {
216
+ clearTimeout(closeTimeoutRef.current)
217
+ closeTimeoutRef.current = null
218
+ }
219
+ }
220
+
221
+ const handlePointerEnter = () => {
222
+ if (disabled) return
223
+ clearCloseTimeout()
224
+ clearOpenTimeout()
225
+ const { tooltipDelayDuration: delay } = tooltipTimingRef.current
226
+ if (delay <= 0) {
227
+ setIsHovering(true)
228
+ return
229
+ }
230
+ openTimeoutRef.current = setTimeout(() => {
231
+ openTimeoutRef.current = null
232
+ setIsHovering(true)
233
+ }, delay)
234
+ }
235
+
236
+ const handlePointerLeave = () => {
237
+ clearOpenTimeout()
238
+ const { tooltipCloseDelay: delay } = tooltipTimingRef.current
239
+ if (delay <= 0) {
240
+ setIsHovering(false)
241
+ return
242
+ }
243
+ closeTimeoutRef.current = setTimeout(() => {
244
+ closeTimeoutRef.current = null
245
+ setIsHovering(false)
246
+ }, delay)
247
+ }
248
+
190
249
  const tooltipOpen =
191
- !disabled && (isDragging || knobFocused || hoverOpen)
250
+ !disabled && (isDragging || knobFocused || isHovering)
251
+
252
+ const pan = (
253
+ <div
254
+ role="slider"
255
+ tabIndex={disabled ? -1 : 0}
256
+ aria-label="Pan"
257
+ aria-valuemin={-100}
258
+ aria-valuemax={100}
259
+ aria-valuenow={ariaPanPct}
260
+ aria-valuetext={panValueText}
261
+ aria-disabled={disabled}
262
+ className={classNames(
263
+ styles.pan,
264
+ isDragging && styles.active,
265
+ disabled && styles.disabled,
266
+ )}
267
+ onPointerDown={onPointerDown}
268
+ onPointerMove={onPointerMove}
269
+ onPointerUp={onPointerUp}
270
+ onPointerCancel={onPointerUp}
271
+ onPointerEnter={handlePointerEnter}
272
+ onPointerLeave={handlePointerLeave}
273
+ onFocus={() => setKnobFocused(true)}
274
+ onBlur={() => setKnobFocused(false)}
275
+ onKeyDown={handleKnobKeyDown}
276
+ >
277
+ <svg width={20} height={20} viewBox="0 0 20 20" fill="none">
278
+ <circle cx={KNOB_CENTER_X} cy={KNOB_CENTER_Y} r={10} />
279
+ <circle
280
+ cx={KNOB_CENTER_X}
281
+ cy={KNOB_CENTER_Y}
282
+ r={KNOB_RADIUS}
283
+ stroke="#D3EDF8"
284
+ strokeOpacity={0.113725}
285
+ />
286
+ {value !== 0 && (
287
+ <path
288
+ d={arcPath}
289
+ stroke={arcColor}
290
+ strokeLinecap="round"
291
+ strokeWidth={1}
292
+ fill="none"
293
+ />
294
+ )}
295
+ <g
296
+ transform={`rotate(${pointerRotation} ${KNOB_CENTER_X} ${KNOB_CENTER_Y})`}
297
+ >
298
+ <path
299
+ d="M10 0.5 L10 6"
300
+ stroke={pointerColor}
301
+ strokeLinecap="round"
302
+ strokeWidth={1}
303
+ />
304
+ </g>
305
+ </svg>
306
+ </div>
307
+ )
192
308
 
193
309
  return (
194
310
  <div
@@ -198,70 +314,20 @@ export const PanControl = ({
198
314
  onClick={handleClick}
199
315
  style={{ cursor: disabled ? 'default' : 'ns-resize' }}
200
316
  >
201
- <Tooltip
202
- content={panValueText}
203
- open={tooltipOpen}
204
- onOpenChange={(next) => {
205
- if (!disabled) setHoverOpen(next)
206
- }}
207
- side="bottom"
208
- align="center"
209
- sideOffset={3}
210
- delayDuration={0}
211
- >
212
- <div
213
- role="slider"
214
- tabIndex={disabled ? -1 : 0}
215
- aria-label="Pan"
216
- aria-valuemin={-100}
217
- aria-valuemax={100}
218
- aria-valuenow={ariaPanPct}
219
- aria-valuetext={panValueText}
220
- aria-disabled={disabled}
221
- className={classNames(
222
- styles.pan,
223
- isDragging && styles.active,
224
- disabled && styles.disabled,
225
- )}
226
- onPointerDown={onPointerDown}
227
- onPointerMove={onPointerMove}
228
- onPointerUp={onPointerUp}
229
- onPointerCancel={onPointerUp}
230
- onFocus={() => setKnobFocused(true)}
231
- onBlur={() => setKnobFocused(false)}
232
- onKeyDown={handleKnobKeyDown}
317
+ {showTooltip ? (
318
+ <Tooltip
319
+ content={panValueText}
320
+ open={tooltipOpen}
321
+ delayDuration={tooltipDelayDuration}
322
+ side="bottom"
323
+ align="center"
324
+ sideOffset={3}
233
325
  >
234
- <svg width={20} height={20} viewBox="0 0 20 20" fill="none">
235
- <circle cx={KNOB_CENTER_X} cy={KNOB_CENTER_Y} r={10} />
236
- <circle
237
- cx={KNOB_CENTER_X}
238
- cy={KNOB_CENTER_Y}
239
- r={KNOB_RADIUS}
240
- stroke="#D3EDF8"
241
- strokeOpacity={0.113725}
242
- />
243
- {value !== 0 && (
244
- <path
245
- d={arcPath}
246
- stroke={arcColor}
247
- strokeLinecap="round"
248
- strokeWidth={1}
249
- fill="none"
250
- />
251
- )}
252
- <g
253
- transform={`rotate(${pointerRotation} ${KNOB_CENTER_X} ${KNOB_CENTER_Y})`}
254
- >
255
- <path
256
- d="M10 0.5 L10 6"
257
- stroke={pointerColor}
258
- strokeLinecap="round"
259
- strokeWidth={1}
260
- />
261
- </g>
262
- </svg>
263
- </div>
264
- </Tooltip>
326
+ {pan}
327
+ </Tooltip>
328
+ ) : (
329
+ pan
330
+ )}
265
331
  </div>
266
332
  )
267
333
  }
@@ -1,6 +1,6 @@
1
1
  import { PanControl } from './PanControl'
2
2
  import { useState } from 'react'
3
- import { Flex } from '@radix-ui/themes'
3
+ import { Flex, Text } from '@radix-ui/themes'
4
4
 
5
5
  export default {
6
6
  title: 'Components/PanControl',
@@ -20,13 +20,24 @@ export default {
20
20
  ' <PanControl\n' +
21
21
  ' value={panValue}\n' +
22
22
  ' onChange={setPanValue}\n' +
23
+ ' tooltipDelayDuration={0}\n' +
24
+ ' tooltipCloseDelay={500}\n' +
23
25
  ' />\n' +
24
26
  ' )\n' +
25
27
  '}\n' +
26
- '```',
28
+ '```\n\n' +
29
+ '## Tooltip\n\n' +
30
+ 'Shows the current pan value on hover/focus/drag. Use `tooltipDelayDuration` and `tooltipCloseDelay` to control open/close timing, or `showTooltip={false}` to hide it.',
27
31
  },
28
32
  },
29
33
  },
34
+ args: {
35
+ showTooltip: true,
36
+ tooltipDelayDuration: 0,
37
+ tooltipCloseDelay: 500,
38
+ minValue: -0.65,
39
+ maxValue: 0.65,
40
+ },
30
41
  argTypes: {
31
42
  value: {
32
43
  description:
@@ -75,15 +86,35 @@ export default {
75
86
  defaultValue: { summary: 0.65 },
76
87
  },
77
88
  },
89
+ showTooltip: {
90
+ control: 'boolean',
91
+ description: 'Shows the pan value tooltip on hover/focus/drag',
92
+ table: {
93
+ type: { summary: 'boolean' },
94
+ defaultValue: { summary: true },
95
+ },
96
+ },
97
+ tooltipDelayDuration: {
98
+ control: 'number',
99
+ description: 'Ms before the tooltip opens on hover',
100
+ table: {
101
+ type: { summary: 'number' },
102
+ defaultValue: { summary: 0 },
103
+ },
104
+ },
105
+ tooltipCloseDelay: {
106
+ control: 'number',
107
+ description: 'Ms the tooltip stays open after the pointer leaves',
108
+ table: {
109
+ type: { summary: 'number' },
110
+ defaultValue: { summary: 500 },
111
+ },
112
+ },
78
113
  },
79
114
  tags: ['autodocs'],
80
115
  }
81
116
 
82
117
  export const Default = {
83
- args: {
84
- minValue: -0.65,
85
- maxValue: 0.65,
86
- },
87
118
  render: (args) => {
88
119
  const [value, setValue] = useState(0)
89
120
  return <PanControl {...args} value={value} onChange={setValue} />
@@ -98,3 +129,41 @@ export const Disabled = {
98
129
  )
99
130
  },
100
131
  }
132
+
133
+ export const WithoutTooltip = {
134
+ args: {
135
+ showTooltip: false,
136
+ },
137
+ render: (args) => {
138
+ const [value, setValue] = useState(0)
139
+ return <PanControl {...args} value={value} onChange={setValue} />
140
+ },
141
+ }
142
+
143
+ export const TooltipTiming = {
144
+ name: 'Tooltip timing',
145
+ args: {
146
+ tooltipDelayDuration: 200,
147
+ tooltipCloseDelay: 500,
148
+ },
149
+ render: (args) => {
150
+ const [value, setValue] = useState(0)
151
+ return (
152
+ <Flex direction="column" align="center" gap="2">
153
+ <PanControl {...args} value={value} onChange={setValue} />
154
+ <Text size="1" color="gray">
155
+ Opens after {args.tooltipDelayDuration}ms · closes{' '}
156
+ {args.tooltipCloseDelay}ms after leave
157
+ </Text>
158
+ </Flex>
159
+ )
160
+ },
161
+ parameters: {
162
+ docs: {
163
+ description: {
164
+ story:
165
+ 'Adjust `tooltipDelayDuration` (open) and `tooltipCloseDelay` (close grace) via controls.',
166
+ },
167
+ },
168
+ },
169
+ }
@@ -15,6 +15,10 @@ import styles from './SliderLibrary.module.css'
15
15
  * @property {boolean} [disabled=false] - Disables interaction and dims the slider.
16
16
  * @property {boolean} [showTooltip=true] - Renders the DS Tooltip below the thumb on
17
17
  * hover/focus/drag with the current value.
18
+ * @property {number} [tooltipDelayDuration=0] - Time in ms from hover enter until the
19
+ * tooltip opens. Drag and keyboard focus open immediately.
20
+ * @property {number} [tooltipCloseDelay=500] - Time in ms the tooltip stays open after
21
+ * the pointer leaves the thumb (or re-enters within this window).
18
22
  * @property {(value: number) => React.ReactNode} [formatValue] - Formats the value for
19
23
  * display in the tooltip. Defaults to the raw number.
20
24
  * @property {(value: number) => void} [onThumbDoubleClick] - Fires when the thumb is
@@ -58,6 +62,8 @@ export const SliderLibrary = forwardRef(function SliderLibrary(
58
62
  step = 1,
59
63
  disabled = false,
60
64
  showTooltip = true,
65
+ tooltipDelayDuration = 0,
66
+ tooltipCloseDelay = 500,
61
67
  formatValue,
62
68
  onThumbDoubleClick,
63
69
  className,
@@ -68,6 +74,8 @@ export const SliderLibrary = forwardRef(function SliderLibrary(
68
74
  const ariaLabel = props['aria-label']
69
75
  const internalRef = useRef(null)
70
76
  const lastInteractionRef = useRef(null)
77
+ const openTimeoutRef = useRef(null)
78
+ const closeTimeoutRef = useRef(null)
71
79
  const [keyboardFocus, setKeyboardFocus] = useState(false)
72
80
  const [isHovering, setIsHovering] = useState(false)
73
81
  const [isDragging, setIsDragging] = useState(false)
@@ -75,8 +83,21 @@ export const SliderLibrary = forwardRef(function SliderLibrary(
75
83
  const stateRef = useRef({ value, min, max, step, disabled, onValueChange })
76
84
  stateRef.current = { value, min, max, step, disabled, onValueChange }
77
85
 
86
+ const tooltipTimingRef = useRef({
87
+ tooltipDelayDuration,
88
+ tooltipCloseDelay,
89
+ })
90
+ tooltipTimingRef.current = { tooltipDelayDuration, tooltipCloseDelay }
91
+
78
92
  const wheelRef = useRef({ accum: 0, axis: null, lastTime: 0 })
79
93
 
94
+ useEffect(() => {
95
+ return () => {
96
+ if (openTimeoutRef.current != null) clearTimeout(openTimeoutRef.current)
97
+ if (closeTimeoutRef.current != null) clearTimeout(closeTimeoutRef.current)
98
+ }
99
+ }, [])
100
+
80
101
  useEffect(() => {
81
102
  const onKey = () => {
82
103
  lastInteractionRef.current = 'keyboard'
@@ -181,13 +202,50 @@ export const SliderLibrary = forwardRef(function SliderLibrary(
181
202
  if (!disabled) setIsDragging(true)
182
203
  }
183
204
 
205
+ const clearOpenTimeout = () => {
206
+ if (openTimeoutRef.current != null) {
207
+ clearTimeout(openTimeoutRef.current)
208
+ openTimeoutRef.current = null
209
+ }
210
+ }
211
+
212
+ const clearCloseTimeout = () => {
213
+ if (closeTimeoutRef.current != null) {
214
+ clearTimeout(closeTimeoutRef.current)
215
+ closeTimeoutRef.current = null
216
+ }
217
+ }
218
+
184
219
  const handleThumbFocus = () => {
185
220
  if (lastInteractionRef.current === 'keyboard') setKeyboardFocus(true)
186
221
  }
187
222
  const handleThumbBlur = () => setKeyboardFocus(false)
188
223
  const handleThumbPointerDown = () => setKeyboardFocus(false)
189
- const handleThumbPointerEnter = () => setIsHovering(true)
190
- const handleThumbPointerLeave = () => setIsHovering(false)
224
+ const handleThumbPointerEnter = () => {
225
+ clearCloseTimeout()
226
+ clearOpenTimeout()
227
+ const { tooltipDelayDuration: delay } = tooltipTimingRef.current
228
+ if (delay <= 0) {
229
+ setIsHovering(true)
230
+ return
231
+ }
232
+ openTimeoutRef.current = setTimeout(() => {
233
+ openTimeoutRef.current = null
234
+ setIsHovering(true)
235
+ }, delay)
236
+ }
237
+ const handleThumbPointerLeave = () => {
238
+ clearOpenTimeout()
239
+ const { tooltipCloseDelay: delay } = tooltipTimingRef.current
240
+ if (delay <= 0) {
241
+ setIsHovering(false)
242
+ return
243
+ }
244
+ closeTimeoutRef.current = setTimeout(() => {
245
+ closeTimeoutRef.current = null
246
+ setIsHovering(false)
247
+ }, delay)
248
+ }
191
249
  const handleThumbDoubleClick = () => {
192
250
  if (!disabled) onThumbDoubleClick?.(value)
193
251
  }
@@ -230,7 +288,7 @@ export const SliderLibrary = forwardRef(function SliderLibrary(
230
288
  <Tooltip
231
289
  content={displayValue}
232
290
  open={tooltipOpen}
233
- delayDuration={0}
291
+ delayDuration={tooltipDelayDuration}
234
292
  side="bottom"
235
293
  sideOffset={8}
236
294
  updatePositionStrategy="always"
@@ -7,12 +7,36 @@ export default {
7
7
  parameters: {
8
8
  layout: 'centered',
9
9
  },
10
+ args: {
11
+ tooltipDelayDuration: 0,
12
+ tooltipCloseDelay: 500,
13
+ showTooltip: true,
14
+ },
10
15
  argTypes: {
11
16
  value: { control: { type: 'number' } },
12
17
  min: { control: { type: 'number' } },
13
18
  max: { control: { type: 'number' } },
14
19
  step: { control: { type: 'number' } },
15
20
  disabled: { control: { type: 'boolean' } },
21
+ showTooltip: {
22
+ control: { type: 'boolean' },
23
+ description: 'Shows the value tooltip on hover/focus/drag.',
24
+ },
25
+ tooltipDelayDuration: {
26
+ control: { type: 'number' },
27
+ description: 'Ms before the tooltip opens on hover.',
28
+ table: {
29
+ defaultValue: { summary: 0 },
30
+ },
31
+ },
32
+ tooltipCloseDelay: {
33
+ control: { type: 'number' },
34
+ description:
35
+ 'Ms the tooltip stays open after the pointer leaves the thumb.',
36
+ table: {
37
+ defaultValue: { summary: 500 },
38
+ },
39
+ },
16
40
  },
17
41
  decorators: [
18
42
  (Story) => (
@@ -31,10 +55,11 @@ export default {
31
55
  }
32
56
 
33
57
  export const Default = {
34
- render: () => {
58
+ render: (args) => {
35
59
  const [value, setValue] = useState(50)
36
60
  return (
37
61
  <SliderLibrary
62
+ {...args}
38
63
  value={value}
39
64
  onValueChange={setValue}
40
65
  formatValue={(v) => `${v}%`}
@@ -45,8 +70,9 @@ export const Default = {
45
70
  }
46
71
 
47
72
  export const Disabled = {
48
- render: () => (
73
+ render: (args) => (
49
74
  <SliderLibrary
75
+ {...args}
50
76
  value={75}
51
77
  onValueChange={() => {}}
52
78
  disabled
@@ -57,10 +83,11 @@ export const Disabled = {
57
83
 
58
84
  export const CustomRange = {
59
85
  name: 'Custom range (-12 to 12 dB)',
60
- render: () => {
86
+ render: (args) => {
61
87
  const [value, setValue] = useState(0)
62
88
  return (
63
89
  <SliderLibrary
90
+ {...args}
64
91
  value={value}
65
92
  onValueChange={setValue}
66
93
  min={-12}
@@ -74,26 +101,63 @@ export const CustomRange = {
74
101
  }
75
102
 
76
103
  export const WithoutTooltip = {
77
- render: () => {
104
+ args: {
105
+ showTooltip: false,
106
+ },
107
+ render: (args) => {
78
108
  const [value, setValue] = useState(50)
79
109
  return (
80
110
  <SliderLibrary
111
+ {...args}
81
112
  value={value}
82
113
  onValueChange={setValue}
83
- showTooltip={false}
84
114
  aria-label="Volume"
85
115
  />
86
116
  )
87
117
  },
88
118
  }
89
119
 
120
+ export const TooltipTiming = {
121
+ name: 'Tooltip timing',
122
+ args: {
123
+ tooltipDelayDuration: 200,
124
+ tooltipCloseDelay: 500,
125
+ },
126
+ render: (args) => {
127
+ const [value, setValue] = useState(50)
128
+ return (
129
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
130
+ <SliderLibrary
131
+ {...args}
132
+ value={value}
133
+ onValueChange={setValue}
134
+ formatValue={(v) => `${v}%`}
135
+ aria-label="Volume"
136
+ />
137
+ <span style={{ color: '#a1a1a1', fontSize: 12 }}>
138
+ Opens after {args.tooltipDelayDuration}ms · closes {args.tooltipCloseDelay}ms
139
+ after leave
140
+ </span>
141
+ </div>
142
+ )
143
+ },
144
+ parameters: {
145
+ docs: {
146
+ description: {
147
+ story:
148
+ 'Adjust `tooltipDelayDuration` (open) and `tooltipCloseDelay` (close grace) via controls.',
149
+ },
150
+ },
151
+ },
152
+ }
153
+
90
154
  export const WithLabel = {
91
- render: () => {
155
+ render: (args) => {
92
156
  const [value, setValue] = useState(30)
93
157
  return (
94
158
  <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
95
159
  <span style={{ color: '#fff', fontSize: 14 }}>Volume</span>
96
- <SliderLibrary value={value} onValueChange={setValue} />
160
+ <SliderLibrary {...args} value={value} onValueChange={setValue} />
97
161
  </label>
98
162
  )
99
163
  },
@@ -101,12 +165,13 @@ export const WithLabel = {
101
165
 
102
166
  export const ThumbDoubleClick = {
103
167
  name: 'Thumb double-click',
104
- render: () => {
168
+ render: (args) => {
105
169
  const defaultValue = 0
106
170
  const [value, setValue] = useState(35)
107
171
  return (
108
172
  <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
109
173
  <SliderLibrary
174
+ {...args}
110
175
  value={value}
111
176
  onValueChange={setValue}
112
177
  onThumbDoubleClick={() => setValue(defaultValue)}