@moises.ai/design-system 4.17.3 → 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.3",
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",
@@ -1,4 +1,4 @@
1
- import { Flex, Text, Avatar, ScrollArea } from '@radix-ui/themes'
1
+ import { Flex, Text, Avatar, ScrollArea, Skeleton } from '@radix-ui/themes'
2
2
  import { useDroppable } from '@dnd-kit/core'
3
3
  import styles from './SetlistList.module.css'
4
4
  import classNames from 'classnames'
@@ -145,6 +145,7 @@ export const SetlistItem = ({
145
145
  onDragLeave,
146
146
  onDragOver,
147
147
  onDrop,
148
+ isLoading = false,
148
149
  }) => {
149
150
  const [isHovering, setIsHovering] = useState(false)
150
151
 
@@ -210,9 +211,11 @@ export const SetlistItem = ({
210
211
  [styles.collapsedInner]: collapsed,
211
212
  })}
212
213
  >
213
- {avatar && (
214
+ {(avatar || isLoading) && (
214
215
  <div className={styles.avatarSetlist}>
215
- {typeof avatar === 'string' ? (
216
+ {isLoading ? (
217
+ <Skeleton className={styles.avatarSetlistSkeleton} />
218
+ ) : typeof avatar === 'string' ? (
216
219
  <Avatar
217
220
  src={avatar}
218
221
  fallback={<SetlistIcon width={16} height={16} />}
@@ -226,28 +229,37 @@ export const SetlistItem = ({
226
229
  )}
227
230
  <Flex
228
231
  direction="column"
232
+ gap={isLoading ? '1' : '0'}
229
233
  className={classNames(styles.textWrapper, {
230
234
  [styles.textWrapperHidden]: collapsed,
231
235
  })}
232
236
  >
233
- {text && (
234
- <Text
235
- size="2"
236
- weight="regular"
237
- className={styles.textNewSetlist}
238
- truncate
239
- >
240
- {text}
241
- </Text>
242
- )}
243
- <Text
244
- size="1"
245
- weight="regular"
246
- className={styles.textSubtitle}
247
- truncate
248
- >
249
- {subtitle}
250
- </Text>
237
+ {(text || isLoading) &&
238
+ (isLoading ? (
239
+ <Skeleton className={styles.textNewSetlistSkeleton} />
240
+ ) : (
241
+ <Text
242
+ size="2"
243
+ weight="regular"
244
+ className={styles.textNewSetlist}
245
+ truncate
246
+ >
247
+ {text}
248
+ </Text>
249
+ ))}
250
+ {(subtitle || isLoading) &&
251
+ (isLoading ? (
252
+ <Skeleton className={styles.textSubtitleSkeleton} />
253
+ ) : (
254
+ <Text
255
+ size="1"
256
+ weight="regular"
257
+ className={styles.textSubtitle}
258
+ truncate
259
+ >
260
+ {subtitle}
261
+ </Text>
262
+ ))}
251
263
  </Flex>
252
264
  </Flex>
253
265
  {dropdownMenuOptions &&
@@ -621,10 +633,11 @@ export const SetlistList = ({
621
633
  moises={setlist.moises}
622
634
  text={setlist.label}
623
635
  subtitle={setlist.subtitle}
636
+ isLoading={setlist.isLoading}
624
637
  dropdownMenuOptions={setlist.dropdownMenuOptions}
625
638
  collapsed={itemCollapsed}
626
639
  isMobile={isMobile}
627
- className={classNames({
640
+ className={classNames(className, {
628
641
  [styles.collapsedStackItem]: showCollapsedStack,
629
642
  [styles.collapsedTransition]: collapsed && showCollapsedStack,
630
643
  [styles.staggerItem]: isStaggerActive,
@@ -92,6 +92,12 @@
92
92
  z-index: 1;
93
93
  }
94
94
 
95
+ .avatarSetlistSkeleton {
96
+ width: 100%;
97
+ height: 100%;
98
+ border-radius: 4px;
99
+ }
100
+
95
101
  .avatarClickable {
96
102
  cursor: pointer;
97
103
  }
@@ -128,6 +134,12 @@
128
134
  color: var(--neutral-alpha-11);
129
135
  }
130
136
 
137
+ .textNewSetlistSkeleton {
138
+ height: 14px;
139
+ width: 126px;
140
+ border-radius: 4px;
141
+ }
142
+
131
143
  .collapsed .textNewSetlist {
132
144
  opacity: 0;
133
145
  }
@@ -140,6 +152,12 @@
140
152
  white-space: nowrap;
141
153
  }
142
154
 
155
+ .textSubtitleSkeleton {
156
+ height: 12px;
157
+ width: 95px;
158
+ border-radius: 4px;
159
+ }
160
+
143
161
  .collapsed .textSubtitle {
144
162
  opacity: 0;
145
163
  }
@@ -236,6 +236,28 @@ export const Empty = {
236
236
  },
237
237
  }
238
238
 
239
+ const loadingSetlists = [
240
+ { id: 'loading-1', isLoading: true },
241
+ { id: 'loading-2', isLoading: true },
242
+ { id: 'loading-3', isLoading: true },
243
+ ]
244
+
245
+ export const Loading = {
246
+ render: (args) => (
247
+ <SetlistList
248
+ {...args}
249
+ setlists={loadingSetlists}
250
+ onNewSetlistClick={() => {
251
+ args.onNewSetlistClick?.()
252
+ }}
253
+ />
254
+ ),
255
+ args: {
256
+ collapsed: false,
257
+ isMobile: false,
258
+ },
259
+ }
260
+
239
261
  const trackRows = [
240
262
  {
241
263
  id: 'track-1',
@@ -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,