@moises.ai/design-system 4.19.7 → 4.19.8

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.19.7",
3
+ "version": "4.19.8",
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 React from 'react'
1
+ import React, { useRef, useState } from 'react'
2
2
  import {
3
3
  Box,
4
4
  Flex,
@@ -7,7 +7,8 @@ import {
7
7
  Tooltip,
8
8
  Separator,
9
9
  } from '@radix-ui/themes'
10
- import { InfoIcon } from '../../icons'
10
+ import { Select as SelectPrimitive } from 'radix-ui'
11
+ import { CheckIcon, InfoIcon } from '../../icons'
11
12
  import styles from './Select.module.css'
12
13
  import classNames from 'classnames'
13
14
 
@@ -22,17 +23,56 @@ export const Select = ({
22
23
  size = '2',
23
24
  variant = 'soft',
24
25
  disabled = false,
26
+ allowClickWhenDisabled = false,
25
27
  readOnly = false,
26
28
  maxHeight = 'auto',
27
29
  showSelectIndicator = true,
28
30
  maxWidth = '100%',
29
31
  placeholder = null,
32
+ onFocus,
33
+ onOpenChange,
30
34
  ...props
31
35
  }) => {
32
36
  const resolvedValue = value === null ? '' : value
33
37
  const resolvedDefaultValue =
34
38
  defaultValue === null ? undefined : defaultValue
35
39
 
40
+ const isDisabled = Boolean(disabled)
41
+ const isDisabledClickable = isDisabled && Boolean(allowClickWhenDisabled)
42
+ const isRadixDisabled = (isDisabled && !allowClickWhenDisabled) || readOnly
43
+
44
+ const [open, setOpen] = useState(false)
45
+ const suppressCloseRef = useRef(false)
46
+
47
+ const handleOpenChange = (nextOpen) => {
48
+ if (!isDisabledClickable) {
49
+ onOpenChange?.(nextOpen)
50
+ return
51
+ }
52
+
53
+ if (nextOpen) {
54
+ setOpen(true)
55
+ onOpenChange?.(true)
56
+ // Keep menu open while onFocus runs (e.g. alert/paywall steals focus).
57
+ suppressCloseRef.current = true
58
+ setTimeout(() => {
59
+ onFocus?.()
60
+ setTimeout(() => {
61
+ suppressCloseRef.current = false
62
+ }, 50)
63
+ }, 0)
64
+ return
65
+ }
66
+
67
+ if (suppressCloseRef.current) {
68
+ setOpen(true)
69
+ return
70
+ }
71
+
72
+ setOpen(false)
73
+ onOpenChange?.(false)
74
+ }
75
+
36
76
  return (
37
77
  <Box width="100%">
38
78
  {title && (
@@ -53,8 +93,10 @@ export const Select = ({
53
93
  defaultValue={resolvedDefaultValue}
54
94
  value={resolvedValue}
55
95
  className={classNames(className)}
56
- disabled={disabled || readOnly}
57
96
  {...props}
97
+ disabled={isRadixDisabled}
98
+ open={isDisabledClickable ? open : props.open}
99
+ onOpenChange={handleOpenChange}
58
100
  >
59
101
  <SelectRadix.Trigger
60
102
  variant={variant}
@@ -64,6 +106,7 @@ export const Select = ({
64
106
  className={classNames(styles.selectTrigger, {
65
107
  [styles.ghost]: variant === 'ghost',
66
108
  [styles.soft]: variant === 'soft',
109
+ [styles.triggerDisabled]: isDisabledClickable,
67
110
  })}
68
111
  />
69
112
  <SelectRadix.Content
@@ -72,52 +115,86 @@ export const Select = ({
72
115
  style={{ height: maxHeight }}
73
116
  >
74
117
  <SelectRadix.Group>
75
- {items.map((item) =>
76
- item.type === 'separator' ? (
77
- <Separator
78
- key={item.value}
79
- my="2"
80
- style={{
81
- width: '100%',
82
- background: 'var(--neutral-alpha-4)',
83
- }}
84
- />
85
- ) : (
86
- <SelectRadix.Item
118
+ {items.map((item) => {
119
+ if (item.type === 'separator') {
120
+ return (
121
+ <Separator
122
+ key={item.value}
123
+ my="2"
124
+ style={{
125
+ width: '100%',
126
+ background: 'var(--neutral-alpha-4)',
127
+ }}
128
+ />
129
+ )
130
+ }
131
+
132
+ const itemDisabled =
133
+ isDisabledClickable || Boolean(item.disabled) || readOnly
134
+
135
+ return (
136
+ <SelectPrimitive.Item
87
137
  key={item.value}
88
138
  value={item.value}
89
- className={classNames(styles.selectItem, {
139
+ className={classNames('rt-SelectItem', styles.selectItem, {
90
140
  [styles.hideIndicator]: !showSelectIndicator,
91
141
  [styles.size1]: size === '1',
92
142
  [styles.size2]: size === '2',
93
143
  })}
94
- disabled={disabled || readOnly}
144
+ disabled={itemDisabled}
95
145
  >
96
- <span className={styles.itemLabel}>
97
- <Text size={size} style={{ color: 'var(--neutral-12)' }}>
98
- {item.label}
99
- </Text>
100
- {item.aiDetected && (
101
- <span className={styles.aiDetected} aria-hidden>
102
-
103
- </span>
104
- )}
105
- {item.extra && (
146
+ <SelectPrimitive.ItemIndicator className="rt-SelectItemIndicator">
147
+ <CheckIcon
148
+ className="rt-SelectItemIndicatorIcon"
149
+ width={16}
150
+ height={16}
151
+ />
152
+ </SelectPrimitive.ItemIndicator>
153
+ <SelectPrimitive.ItemText className={styles.itemText}>
154
+ <span className={styles.itemLabel}>
106
155
  <Text
107
156
  size={size}
108
- className={styles.extra}
109
157
  style={{
110
- color: 'var(--neutral-alpha-9)',
158
+ color: itemDisabled
159
+ ? 'var(--neutral-alpha-9)'
160
+ : 'var(--neutral-12)',
111
161
  }}
112
162
  >
113
- {' '}
114
- {item.extra}
163
+ {item.label}
115
164
  </Text>
116
- )}
117
- </span>
118
- </SelectRadix.Item>
119
- ),
120
- )}
165
+ {item.aiDetected && (
166
+ <span className={styles.aiDetected} aria-hidden>
167
+
168
+ </span>
169
+ )}
170
+ {item.extra && (
171
+ <Text
172
+ size={size}
173
+ className={styles.extra}
174
+ style={{
175
+ color: 'var(--neutral-alpha-9)',
176
+ }}
177
+ >
178
+ {' '}
179
+ {item.extra}
180
+ </Text>
181
+ )}
182
+ </span>
183
+ </SelectPrimitive.ItemText>
184
+ {item.rightIcon && (
185
+ <span className={styles.rightSlot}>
186
+ <span
187
+ className={classNames(styles.rightIcon, {
188
+ [styles.rightIconDisabled]: itemDisabled,
189
+ })}
190
+ >
191
+ {item.rightIcon}
192
+ </span>
193
+ </span>
194
+ )}
195
+ </SelectPrimitive.Item>
196
+ )
197
+ })}
121
198
  </SelectRadix.Group>
122
199
  </SelectRadix.Content>
123
200
  </SelectRadix.Root>
@@ -75,7 +75,7 @@
75
75
  .selectTrigger.soft {
76
76
  background-color: var(--neutral-alpha-2) ;
77
77
 
78
- &:hover:not([disabled]) {
78
+ &:hover:not([disabled]):not(.triggerDisabled) {
79
79
  background-color: var(--neutral-alpha-3);
80
80
  }
81
81
  }
@@ -92,7 +92,7 @@
92
92
  .selectTrigger.ghost {
93
93
  background-color: transparent;
94
94
 
95
- &:hover:not([disabled]) {
95
+ &:hover:not([disabled]):not(.triggerDisabled) {
96
96
  background-color: var(--neutral-alpha-2);
97
97
  }
98
98
  }
@@ -102,6 +102,8 @@
102
102
 
103
103
  /* List items */
104
104
  .selectItem {
105
+ width: 100%;
106
+ padding-right: 12px;
105
107
  &:hover {
106
108
  background-color: var(--neutral-alpha-2);
107
109
  }
@@ -115,19 +117,31 @@
115
117
  /* Disabled styles */
116
118
  .selectTrigger.soft {
117
119
  color: var(--white);
118
- &:disabled {
120
+ &:disabled,
121
+ &.triggerDisabled {
119
122
  color: var(--neutral-alpha-9);
120
123
  background-color: var(--neutral-alpha-2);
121
- }}
124
+ }
125
+ }
122
126
 
123
127
  .selectTrigger.ghost {
124
128
  color: var(--white);
125
- &:disabled {
129
+ &:disabled,
130
+ &.triggerDisabled {
126
131
  color: var(--neutral-alpha-9);
127
132
  background-color: transparent;
128
133
  }
129
134
  }
130
135
 
136
+ .selectTrigger.soft.triggerDisabled[aria-expanded='true'],
137
+ .selectTrigger.ghost.triggerDisabled[aria-expanded='true'] {
138
+ background-color: var(--neutral-alpha-2);
139
+ }
140
+
141
+ .selectTrigger.ghost.triggerDisabled[aria-expanded='true'] {
142
+ background-color: transparent;
143
+ }
144
+
131
145
  .hideIndicator :global(.rt-SelectItemIndicator) {
132
146
  display: none;
133
147
  }
@@ -147,8 +161,44 @@
147
161
  padding: 12px 15px;
148
162
  }
149
163
 
164
+ .itemText {
165
+ flex: 1;
166
+ min-width: 0;
167
+ }
168
+
150
169
  .itemLabel {
151
170
  line-height: 1;
171
+ min-width: 0;
172
+ }
173
+
174
+ .rightSlot {
175
+ margin-left: auto;
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 8px;
179
+ flex-shrink: 0;
180
+ height: 18px;
181
+ color: var(--neutral-alpha-11);
182
+ }
183
+
184
+ .rightIcon {
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ color: var(--neutral-alpha-11);
189
+ }
190
+
191
+ .rightIcon > svg {
192
+ width: 14px;
193
+ height: 14px;
194
+ }
195
+
196
+ .rightIconDisabled {
197
+ color: var(--neutral-alpha-7);
198
+ }
199
+
200
+ .rightIconDisabled > svg {
201
+ color: var(--neutral-alpha-7);
152
202
  }
153
203
 
154
204
  .aiDetected {
@@ -1,6 +1,8 @@
1
1
  import { Flex } from '@radix-ui/themes'
2
2
  import { Select } from './Select'
3
3
  import { useState } from 'react'
4
+ import { LockIcon, StarIcon } from '../../icons'
5
+ import { GearIcon } from '@radix-ui/react-icons'
4
6
 
5
7
  export default {
6
8
  title: 'Components/Select',
@@ -41,6 +43,8 @@ import { Select } from '@moises.ai/design-system'
41
43
  items: {
42
44
  value: string, // Value of the option (required)
43
45
  label: string, // Label of the option (required)
46
+ disabled?: boolean, // Disables this option (still visible in the menu)
47
+ rightIcon?: ReactNode, // Optional icon displayed on the right side
44
48
  aiDetected?: boolean, // Shows ✦ icon before label if true (optional)
45
49
  extra?: string | ReactNode, // Extra text or element after label (optional)
46
50
  }[], // List of items to display in the select
@@ -52,11 +56,13 @@ import { Select } from '@moises.ai/design-system'
52
56
  size?: '1' | '2' | '3', // Size variant of the select (default: '2')
53
57
  variant?: 'soft' | 'ghost', // Visual style variant (default: 'soft')
54
58
  disabled?: boolean, // Whether the select is disabled (default: false)
59
+ allowClickWhenDisabled?: boolean, // Keeps disabled look, opens menu with disabled options, fires onFocus on open (e.g. paywall)
55
60
  className?: string, // Additional CSS class name
56
61
  readOnly?: boolean, // Whether the select is read-only (default: false)
57
62
  maxHeight?: string, // Max height of the dropdown (e.g. '200px')
58
63
  showSelectIndicator?: boolean, // Show the default Radix select indicator (default: false)
59
64
  maxWidth?: string, // Max width of the select (e.g. '300px')
65
+ onFocus?: () => void, // Called when opening while disabled + allowClickWhenDisabled
60
66
  }
61
67
  \`\`\`
62
68
  `,
@@ -81,6 +87,12 @@ import { Select } from '@moises.ai/design-system'
81
87
  description: 'Whether the select is disabled',
82
88
  defaultValue: false,
83
89
  },
90
+ allowClickWhenDisabled: {
91
+ control: 'boolean',
92
+ description:
93
+ 'When true with disabled, keeps disabled look, opens menu with disabled options, and fires onFocus on open',
94
+ defaultValue: false,
95
+ },
84
96
  showSelectIndicator: {
85
97
  control: 'boolean',
86
98
  description: 'Whether to show the default Radix select indicator',
@@ -191,6 +203,64 @@ export const Disabled = {
191
203
  decorators: [withFlexContainer],
192
204
  }
193
205
 
206
+ export const AllowClickWhenDisabled = {
207
+ render: () => {
208
+ const [lastAction, setLastAction] = useState('none yet')
209
+
210
+ return (
211
+ <Flex direction="column" gap="2" width="300px">
212
+ <Select
213
+ title="Instrument"
214
+ info="Looks disabled, opens menu and fires onFocus"
215
+ defaultValue="bass"
216
+ size="2"
217
+ disabled
218
+ allowClickWhenDisabled
219
+ items={[
220
+ { value: 'bass', label: 'Bass' },
221
+ { value: 'guitar', label: 'Guitar' },
222
+ { value: 'piano', label: 'Piano' },
223
+ ]}
224
+ onFocus={() => {
225
+ setLastAction('activated (focus)')
226
+ alert('paywall opened')
227
+ }}
228
+ />
229
+ <div style={{ fontSize: 14, color: '#4b5563' }}>
230
+ Last action: {lastAction}
231
+ </div>
232
+ </Flex>
233
+ )
234
+ },
235
+ }
236
+
237
+ export const WithRightIcon = {
238
+ args: {
239
+ title: 'Instrument',
240
+ defaultValue: 'bass',
241
+ size: '2',
242
+ items: [
243
+ {
244
+ value: 'bass',
245
+ label: 'Bass',
246
+ rightIcon: <LockIcon width={14} height={14} />,
247
+ },
248
+ {
249
+ value: 'guitar',
250
+ label: 'Guitar',
251
+ rightIcon: <StarIcon width={14} height={14} />,
252
+ },
253
+ {
254
+ value: 'piano',
255
+ label: 'Piano',
256
+ rightIcon: <GearIcon width={14} height={14} />,
257
+ },
258
+ { value: 'drums', label: 'Drums' },
259
+ ],
260
+ },
261
+ decorators: [withFlexContainer],
262
+ }
263
+
194
264
  export const DisabledGhost = {
195
265
  args: {
196
266
  title: 'Instrument',
@@ -12,6 +12,7 @@ export const TextField = ({
12
12
  className,
13
13
  disabled,
14
14
  readOnly,
15
+ allowClickWhenDisabled = false,
15
16
  size = '2',
16
17
  Icon,
17
18
  focusRing = false,
@@ -23,6 +24,7 @@ export const TextField = ({
23
24
  aiDetected = false,
24
25
  textAlign = 'left',
25
26
  style,
27
+ onFocus,
26
28
  ...props
27
29
  }) => {
28
30
  // legacy Icon prop is kept for backwards compatibility.
@@ -31,6 +33,8 @@ export const TextField = ({
31
33
  (Icon ? <Icon width={16} height={16} className={styles.icon} /> : null)
32
34
 
33
35
  const chars = String(value ?? '').length
36
+ const isDisabled = Boolean(disabled)
37
+ const isDisabledClickable = isDisabled && Boolean(allowClickWhenDisabled)
34
38
 
35
39
  return (
36
40
  <Flex direction="column" width="100%" height="100%" gap="1">
@@ -42,39 +46,61 @@ export const TextField = ({
42
46
  {info && <TooltipWithInfoIcon content={info} />}
43
47
  </Flex>
44
48
  )}
45
- <TextFieldRadix.Root
46
- size={size}
47
- value={value}
48
- onChange={onChange}
49
- placeholder={placeholder}
50
- className={classNames(styles.TextField, className, {
51
- [styles.Disabled]: disabled,
52
- [styles.ReadOnly]: readOnly,
53
- [styles.focusRing]: focusRing || error,
54
- [styles.errorText]: error,
55
- [styles[`size${size}`]]: size,
56
- [styles.alignCenter]: textAlign === 'center',
57
- [styles.alignLeft]: textAlign === 'left',
58
- })}
59
- style={{
60
- ...(aiDetected && { '--ai-chars': chars }),
61
- ...style,
62
- }}
63
- readOnly={readOnly}
64
- disabled={disabled}
65
- autoFocus={autoFocus}
66
- {...props}
67
- >
68
- {resolvedStartSlot && (
69
- <TextFieldRadix.Slot>{resolvedStartSlot}</TextFieldRadix.Slot>
70
- )}
71
- {aiDetected && (
72
- <span className={styles.aiDetected} aria-hidden>
73
- <span className={styles.aiDetectedMark}>✦</span>
74
- </span>
49
+ <div className={styles.fieldWrapper}>
50
+ <TextFieldRadix.Root
51
+ size={size}
52
+ value={value}
53
+ onChange={isDisabledClickable ? undefined : onChange}
54
+ placeholder={placeholder}
55
+ className={classNames(styles.TextField, className, {
56
+ [styles.Disabled]: isDisabled,
57
+ [styles.ReadOnly]: readOnly && !isDisabled,
58
+ [styles.focusRing]: focusRing || error,
59
+ [styles.errorText]: error,
60
+ [styles[`size${size}`]]: size,
61
+ [styles.alignCenter]: textAlign === 'center',
62
+ [styles.alignLeft]: textAlign === 'left',
63
+ })}
64
+ style={{
65
+ ...(aiDetected && { '--ai-chars': chars }),
66
+ ...style,
67
+ }}
68
+ autoFocus={autoFocus}
69
+ {...props}
70
+ onFocus={isDisabledClickable ? undefined : onFocus}
71
+ readOnly={readOnly}
72
+ disabled={isDisabled}
73
+ >
74
+ {resolvedStartSlot && (
75
+ <TextFieldRadix.Slot>{resolvedStartSlot}</TextFieldRadix.Slot>
76
+ )}
77
+ {aiDetected && (
78
+ <span className={styles.aiDetected} aria-hidden>
79
+ <span className={styles.aiDetectedMark}>✦</span>
80
+ </span>
81
+ )}
82
+ {endSlot && <div className={styles.endSlot}>{endSlot}</div>}
83
+ </TextFieldRadix.Root>
84
+ {isDisabledClickable && (
85
+ <button
86
+ type="button"
87
+ className={styles.disabledClickOverlay}
88
+ aria-label={title || placeholder || 'Activate'}
89
+ tabIndex={0}
90
+ onPointerDown={(event) => {
91
+ // Prevent focus so dialogs/alerts don't restore focus and re-trigger.
92
+ event.preventDefault()
93
+ onFocus?.(event)
94
+ }}
95
+ onKeyDown={(event) => {
96
+ if (event.key === 'Enter' || event.key === ' ') {
97
+ event.preventDefault()
98
+ onFocus?.(event)
99
+ }
100
+ }}
101
+ />
75
102
  )}
76
- {endSlot && <div className={styles.endSlot}>{endSlot}</div>}
77
- </TextFieldRadix.Root>
103
+ </div>
78
104
  {secondaryText && (
79
105
  <Text
80
106
  size="1"
@@ -77,6 +77,27 @@
77
77
  cursor: default;
78
78
  }
79
79
 
80
+ .fieldWrapper {
81
+ position: relative;
82
+ width: 100%;
83
+ }
84
+
85
+ .disabledClickOverlay {
86
+ position: absolute;
87
+ inset: 0;
88
+ z-index: 1;
89
+ margin: 0;
90
+ padding: 0;
91
+ border: none;
92
+ border-radius: inherit;
93
+ background: transparent;
94
+ cursor: default;
95
+ }
96
+
97
+ .disabledClickOverlay:focus {
98
+ outline: none;
99
+ }
100
+
80
101
  .ReadOnly {
81
102
  background-color: var(--neutral-alpha-2) !important;
82
103
  color: var(--neutral-12);
@@ -66,6 +66,15 @@ const MyComponent = () => (
66
66
  defaultValue: { summary: 'false' },
67
67
  },
68
68
  },
69
+ allowClickWhenDisabled: {
70
+ control: 'boolean',
71
+ description:
72
+ 'When true with disabled, keeps the disabled look and blocks typing, but still allows focus (e.g. paywall via onFocus)',
73
+ table: {
74
+ type: { summary: 'boolean' },
75
+ defaultValue: { summary: 'false' },
76
+ },
77
+ },
69
78
  readOnly: {
70
79
  control: 'boolean',
71
80
  description: 'Whether the input is read-only',
@@ -238,6 +247,32 @@ export const Disabled = {
238
247
  },
239
248
  }
240
249
 
250
+ export const AllowClickWhenDisabled = {
251
+ render: (args) => {
252
+ const [lastAction, setLastAction] = React.useState('none yet')
253
+
254
+ return (
255
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
256
+ <TextField
257
+ {...args}
258
+ disabled
259
+ allowClickWhenDisabled
260
+ value="Looks disabled, click to activate"
261
+ onFocus={() => {
262
+ setLastAction('activated (focus)')
263
+ action('activated (focus)')()
264
+ alert('paywall opened')
265
+ }}
266
+ />
267
+ <div style={{ fontSize: 14, color: '#4b5563' }}>Last action: {lastAction}</div>
268
+ </div>
269
+ )
270
+ },
271
+ args: {
272
+ ...Default.args,
273
+ },
274
+ }
275
+
241
276
  export const ReadOnly = {
242
277
  args: {
243
278
  ...Default.args,