@moises.ai/design-system 4.17.4 → 4.17.5

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.17.4",
3
+ "version": "4.17.5",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -17,6 +17,8 @@ import styles from './SliderLibrary.module.css'
17
17
  * hover/focus/drag with the current value.
18
18
  * @property {(value: number) => React.ReactNode} [formatValue] - Formats the value for
19
19
  * display in the tooltip. Defaults to the raw number.
20
+ * @property {(value: number) => void} [onThumbDoubleClick] - Fires when the thumb is
21
+ * double-clicked. Receives the current value.
20
22
  * @property {string} [aria-label] - Accessible label, recommended when no visual label exists.
21
23
  * @property {string} [className] - Extra class on the root.
22
24
  */
@@ -57,6 +59,7 @@ export const SliderLibrary = forwardRef(function SliderLibrary(
57
59
  disabled = false,
58
60
  showTooltip = true,
59
61
  formatValue,
62
+ onThumbDoubleClick,
60
63
  className,
61
64
  ...props
62
65
  },
@@ -148,6 +151,9 @@ export const SliderLibrary = forwardRef(function SliderLibrary(
148
151
  const handleThumbPointerDown = () => setKeyboardFocus(false)
149
152
  const handleThumbPointerEnter = () => setIsHovering(true)
150
153
  const handleThumbPointerLeave = () => setIsHovering(false)
154
+ const handleThumbDoubleClick = () => {
155
+ if (!disabled) onThumbDoubleClick?.(value)
156
+ }
151
157
 
152
158
  const displayValue = formatValue ? formatValue(value) : value
153
159
  const tooltipOpen =
@@ -163,6 +169,7 @@ export const SliderLibrary = forwardRef(function SliderLibrary(
163
169
  onPointerDown={handleThumbPointerDown}
164
170
  onPointerEnter={handleThumbPointerEnter}
165
171
  onPointerLeave={handleThumbPointerLeave}
172
+ onDoubleClick={onThumbDoubleClick ? handleThumbDoubleClick : undefined}
166
173
  />
167
174
  )
168
175
 
@@ -98,3 +98,25 @@ export const WithLabel = {
98
98
  )
99
99
  },
100
100
  }
101
+
102
+ export const ThumbDoubleClick = {
103
+ name: 'Thumb double-click',
104
+ render: () => {
105
+ const defaultValue = 0
106
+ const [value, setValue] = useState(35)
107
+ return (
108
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
109
+ <SliderLibrary
110
+ value={value}
111
+ onValueChange={setValue}
112
+ onThumbDoubleClick={() => setValue(defaultValue)}
113
+ formatValue={(v) => `${v}%`}
114
+ aria-label="Volume"
115
+ />
116
+ <span style={{ color: '#a1a1a1', fontSize: 12 }}>
117
+ Double-click the thumb to reset to {defaultValue}%
118
+ </span>
119
+ </div>
120
+ )
121
+ },
122
+ }
@@ -0,0 +1,114 @@
1
+ import classNames from 'classnames'
2
+ import { Popover as PopoverRadix } from 'radix-ui'
3
+ import { memo, useCallback, useState } from 'react'
4
+ import { ScrollArea } from '../../index'
5
+ import { Theme } from '../theme/Theme'
6
+ import styles from './ToolsPopover.module.css'
7
+
8
+ export const ToolsPopover = memo(
9
+ ({
10
+ trigger,
11
+ children,
12
+ className,
13
+ contentClassName,
14
+ triggerClassName,
15
+ disabled = false,
16
+ maxHeight,
17
+ sideOffset = 5,
18
+ matchTriggerWidth = false,
19
+ wrapCloseMenu,
20
+ modal = false,
21
+ ...props
22
+ }) => {
23
+ const isControlled = props.open !== undefined
24
+ const [internalOpen, setInternalOpen] = useState(false)
25
+
26
+ const open = isControlled ? props.open : internalOpen
27
+ const onOpenChange = useCallback(
28
+ (value) => {
29
+ if (!isControlled) setInternalOpen(value)
30
+ props?.onOpenChange?.(value)
31
+ },
32
+ [isControlled, props?.onOpenChange],
33
+ )
34
+
35
+ const baseClose = useCallback(() => onOpenChange(false), [onOpenChange])
36
+ const closePopover = wrapCloseMenu ? wrapCloseMenu(baseClose) : baseClose
37
+
38
+ const contentStyle = {
39
+ ...(matchTriggerWidth && {
40
+ width: 'var(--radix-popover-trigger-width)',
41
+ boxSizing: 'border-box',
42
+ }),
43
+ ...(maxHeight && { height: maxHeight }),
44
+ }
45
+
46
+ const handlePanelKeyDown = useCallback((event) => {
47
+ if (event.key === 'Tab') {
48
+ event.stopPropagation()
49
+ }
50
+ }, [])
51
+
52
+ const handleOpenAutoFocus = useCallback((event) => {
53
+ event.preventDefault()
54
+ const panel = event.currentTarget.querySelector('[data-tools-popover-panel]')
55
+ panel?.focus({ preventScroll: true })
56
+ }, [])
57
+
58
+ const renderPanel = () => (
59
+ <div
60
+ className={styles.contentPanel}
61
+ data-tools-popover-panel
62
+ tabIndex={-1}
63
+ onKeyDown={handlePanelKeyDown}
64
+ >
65
+ {typeof children === 'function' ? children(closePopover) : children}
66
+ </div>
67
+ )
68
+
69
+ const popoverContent = (
70
+ <Theme asChild>
71
+ <PopoverRadix.Content
72
+ className={classNames(styles.popoverContent, className, contentClassName)}
73
+ side={props.side}
74
+ align={props.align}
75
+ style={contentStyle}
76
+ sideOffset={sideOffset}
77
+ alignOffset={0}
78
+ onOpenAutoFocus={handleOpenAutoFocus}
79
+ onCloseAutoFocus={(event) => {
80
+ event.preventDefault()
81
+ }}
82
+ onClick={(event) => event.stopPropagation()}
83
+ >
84
+ {maxHeight ? (
85
+ <ScrollArea scrollbars="vertical" style={{ height: maxHeight }}>
86
+ {renderPanel()}
87
+ </ScrollArea>
88
+ ) : (
89
+ renderPanel()
90
+ )}
91
+ </PopoverRadix.Content>
92
+ </Theme>
93
+ )
94
+
95
+ return (
96
+ <PopoverRadix.Root
97
+ {...props}
98
+ modal={modal}
99
+ open={disabled ? false : open}
100
+ onOpenChange={disabled ? undefined : onOpenChange}
101
+ >
102
+ <PopoverRadix.Trigger asChild className={triggerClassName} disabled={disabled}>
103
+ {trigger}
104
+ </PopoverRadix.Trigger>
105
+
106
+ <PopoverRadix.Portal className={styles.popoverPortal}>
107
+ {popoverContent}
108
+ </PopoverRadix.Portal>
109
+ </PopoverRadix.Root>
110
+ )
111
+ },
112
+ )
113
+
114
+ ToolsPopover.displayName = 'ToolsPopover'
@@ -0,0 +1,88 @@
1
+ .popoverPortal {
2
+ border-radius: 8px;
3
+ border: 1px solid var(--neutral-alpha-3);
4
+ background: #18191b;
5
+ box-shadow: 0 12px 32px -16px rgba(0, 0, 0, 0.3),
6
+ 0 12px 60px 0 rgba(0, 0, 0, 0.15);
7
+ }
8
+
9
+ .popoverContent {
10
+ border-radius: 8px;
11
+ width: fit-content;
12
+ background-color: var(--panel-solid);
13
+ padding: 12px 16px 16px 16px;
14
+ box-shadow: 0 12px 32px -16px rgba(0, 0, 0, 0.3),
15
+ 0 12px 60px 0 rgba(0, 0, 0, 0.15);
16
+ border: 1px solid var(--neutral-alpha-3);
17
+ animation-duration: 400ms;
18
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
19
+ will-change: transform, opacity;
20
+ }
21
+
22
+ .popoverContent[data-side='top'] {
23
+ animation-name: slideDownAndFade;
24
+ }
25
+
26
+ .popoverContent[data-side='right'] {
27
+ animation-name: slideLeftAndFade;
28
+ }
29
+
30
+ .popoverContent[data-side='bottom'] {
31
+ animation-name: slideUpAndFade;
32
+ }
33
+
34
+ .popoverContent[data-side='left'] {
35
+ animation-name: slideRightAndFade;
36
+ }
37
+
38
+ @keyframes slideUpAndFade {
39
+ from {
40
+ opacity: 0;
41
+ transform: translateY(2px);
42
+ }
43
+
44
+ to {
45
+ opacity: 1;
46
+ transform: translateY(0);
47
+ }
48
+ }
49
+
50
+ @keyframes slideRightAndFade {
51
+ from {
52
+ opacity: 0;
53
+ transform: translateX(-2px);
54
+ }
55
+
56
+ to {
57
+ opacity: 1;
58
+ transform: translateX(0);
59
+ }
60
+ }
61
+
62
+ @keyframes slideDownAndFade {
63
+ from {
64
+ opacity: 0;
65
+ transform: translateY(-2px);
66
+ }
67
+
68
+ to {
69
+ opacity: 1;
70
+ transform: translateY(0);
71
+ }
72
+ }
73
+
74
+ @keyframes slideLeftAndFade {
75
+ from {
76
+ opacity: 0;
77
+ transform: translateX(2px);
78
+ }
79
+
80
+ to {
81
+ opacity: 1;
82
+ transform: translateX(0);
83
+ }
84
+ }
85
+
86
+ .contentPanel {
87
+ outline: none;
88
+ }
@@ -0,0 +1,139 @@
1
+ import { Flex, Switch, Text } from '@radix-ui/themes'
2
+ import { useState } from 'react'
3
+ import { MetronomeIcon } from '../../icons/MetronomeIcon'
4
+ import { Button } from '../Button/Button'
5
+ import { IconButton } from '../IconButton/IconButton'
6
+ import { ToolsPopover } from './ToolsPopover'
7
+
8
+ export default {
9
+ title: 'Components/ToolsPopover',
10
+ component: ToolsPopover,
11
+ parameters: {
12
+ layout: 'centered',
13
+ docs: {
14
+ description: {
15
+ component: `
16
+ ToolsPopover is a Popover wrapper with controlled open state, styled panel content, and focus management.
17
+
18
+ Pass any single React element as \`trigger\` — it is forwarded to \`Popover.Trigger\` via \`asChild\`.
19
+
20
+ Use this instead of \`ToolsDropdown\` when the panel contains interactive content (forms, switches, sliders) that should stay open while the user interacts with it.
21
+ `,
22
+ },
23
+ },
24
+ },
25
+ tags: ['autodocs'],
26
+ argTypes: {
27
+ trigger: {
28
+ description: 'Any single React element used as the popover trigger',
29
+ table: { type: { summary: 'React.ReactElement' } },
30
+ },
31
+ modal: {
32
+ control: 'boolean',
33
+ table: { type: { summary: 'boolean' }, defaultValue: { summary: false } },
34
+ },
35
+ children: {
36
+ description: 'Popover content or render prop receiving closePopover',
37
+ table: { type: { summary: 'React.ReactNode | (closePopover) => React.ReactNode' } },
38
+ },
39
+ },
40
+ }
41
+
42
+ export const Default = {
43
+ render: () => (
44
+ <ToolsPopover trigger={<Button variant="soft">Settings</Button>}>
45
+ <Text size="2">Tool settings panel</Text>
46
+ </ToolsPopover>
47
+ ),
48
+ }
49
+
50
+ export const WithIconButton = {
51
+ render: () => (
52
+ <ToolsPopover trigger={<IconButton><MetronomeIcon width={16} height={16} /></IconButton>}>
53
+ <Flex direction="column" gap="2">
54
+ <Text size="2">BPM: 120</Text>
55
+ <Text size="2">Time signature: 4/4</Text>
56
+ </Flex>
57
+ </ToolsPopover>
58
+ ),
59
+ }
60
+
61
+ export const WithInteractiveContent = {
62
+ render: () => {
63
+ const [enabled, setEnabled] = useState(false)
64
+
65
+ return (
66
+ <ToolsPopover trigger={<Button variant="ghost">Chords</Button>}>
67
+ {(closePopover) => (
68
+ <Flex direction="column" gap="3" width="220px">
69
+ <Text size="2">Show chord diagrams</Text>
70
+ <Switch size="2" checked={enabled} onCheckedChange={setEnabled} />
71
+ <Button
72
+ variant="soft"
73
+ onClick={() => {
74
+ closePopover()
75
+ alert('Saved')
76
+ }}
77
+ >
78
+ Save
79
+ </Button>
80
+ </Flex>
81
+ )}
82
+ </ToolsPopover>
83
+ )
84
+ },
85
+ }
86
+
87
+ export const ControlledOpen = {
88
+ render: () => {
89
+ const [isOpen, setIsOpen] = useState(false)
90
+
91
+ return (
92
+ <Flex direction="column" gap="4" align="center">
93
+ <Flex gap="2">
94
+ <button type="button" onClick={() => setIsOpen(true)}>
95
+ Open
96
+ </button>
97
+ <button type="button" onClick={() => setIsOpen(false)}>
98
+ Close
99
+ </button>
100
+ </Flex>
101
+
102
+ <ToolsPopover
103
+ open={isOpen}
104
+ onOpenChange={setIsOpen}
105
+ trigger={<Button variant="outline">Controlled</Button>}
106
+ >
107
+ <Text size="2">Controlled popover content</Text>
108
+ </ToolsPopover>
109
+
110
+ <Text size="1">
111
+ Status: <strong>{isOpen ? 'Open' : 'Closed'}</strong>
112
+ </Text>
113
+ </Flex>
114
+ )
115
+ },
116
+ }
117
+
118
+ export const CustomElement = {
119
+ render: () => (
120
+ <ToolsPopover
121
+ trigger={
122
+ <button
123
+ type="button"
124
+ style={{
125
+ padding: '8px 12px',
126
+ borderRadius: 4,
127
+ border: '1px solid var(--neutral-alpha-6)',
128
+ background: 'transparent',
129
+ cursor: 'pointer',
130
+ }}
131
+ >
132
+ Custom trigger
133
+ </button>
134
+ }
135
+ >
136
+ <Text size="2">Any element can be the trigger</Text>
137
+ </ToolsPopover>
138
+ ),
139
+ }
package/src/index.jsx CHANGED
@@ -55,6 +55,7 @@ export { DataTable } from './components/DataTable/DataTable'
55
55
  export { DropdownButton } from './components/DropdownButton/DropdownButton'
56
56
  export { DropdownMenu } from './components/DropdownMenu/DropdownMenu'
57
57
  export { ToolsDropdown } from './components/ToolsDropdown/ToolsDropdown'
58
+ export { ToolsPopover } from './components/ToolsPopover/ToolsPopover'
58
59
  export { EmptyState } from './components/EmptyState/EmptyState'
59
60
  export {
60
61
  Extension,