@moises.ai/design-system 4.19.6 → 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.
@@ -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,
@@ -5,7 +5,7 @@ export const ArrowRightIcon = ({
5
5
  ...props
6
6
  }) => (
7
7
  <svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" fill="none" {...props} aria-label="Arrow Right Icon">
8
- <path d="M13.1429 19L20 12L13.1429 5M18.8571 12L4 12" stroke="white" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
8
+ <path d="M13.1429 19L20 12L13.1429 5M18.8571 12L4 12" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
9
9
  </svg>
10
10
  )
11
11
 
@@ -137,10 +137,9 @@
137
137
  }
138
138
  }
139
139
 
140
- .menuItem[data-disabled] {
140
+ .menuItem[data-disabled],
141
+ .menuItemDisabledClickable {
141
142
  color: var(--neutral-alpha-7);
142
- cursor: not-allowed;
143
- pointer-events: none;
144
143
 
145
144
  & .label,
146
145
  & .secondaryTextBottom,
@@ -159,6 +158,15 @@
159
158
  }
160
159
  }
161
160
 
161
+ .menuItem[data-disabled] {
162
+ cursor: not-allowed;
163
+ pointer-events: none;
164
+ }
165
+
166
+ .menuItemDisabledClickable {
167
+ cursor: pointer;
168
+ }
169
+
162
170
  .menuSeparator {
163
171
  height: 1px;
164
172
  background-color: var(--neutral-alpha-3);
@@ -298,6 +306,10 @@
298
306
  background-color: var(--neutral-alpha-3);
299
307
  }
300
308
 
309
+ .menuItem.menuItemDisabledClickable[data-highlighted] {
310
+ background-color: transparent;
311
+ }
312
+
301
313
  .menuSubContent {
302
314
  min-width: 242px;
303
315
  }
@@ -18,6 +18,11 @@ export const createRenderItem = (MenuPrimitives) => {
18
18
  closeMenu,
19
19
  showSelectedItemBackground = true,
20
20
  ) => {
21
+ const isDisabled = Boolean(opt.disabled)
22
+ const allowClickWhenDisabled = Boolean(opt.allowClickWhenDisabled)
23
+ const isRadixDisabled = isDisabled && !allowClickWhenDisabled
24
+ const isDisabledClickable = isDisabled && allowClickWhenDisabled
25
+
21
26
  switch (opt.type) {
22
27
  case 'custom':
23
28
  return (
@@ -27,13 +32,17 @@ export const createRenderItem = (MenuPrimitives) => {
27
32
  styles.menuItem,
28
33
  styles.menuCustomRender,
29
34
  styles[`menuItem-size-${size}`],
35
+ {
36
+ [styles.menuItemDisabledClickable]: isDisabledClickable,
37
+ },
30
38
  )}
31
39
  onSelect={(event) => {
32
40
  if (!closeOnSelect) {
33
41
  event.preventDefault()
34
42
  }
35
43
  }}
36
- disabled={opt.disabled}
44
+ disabled={isRadixDisabled}
45
+ aria-disabled={isDisabledClickable || undefined}
37
46
  asChild
38
47
  >
39
48
  {opt.customRender(closeMenu)}
@@ -104,8 +113,9 @@ export const createRenderItem = (MenuPrimitives) => {
104
113
  styles.menuItem,
105
114
  styles[`menuItem-size-${size}`],
106
115
  {
107
- [styles.red]: opt.color === 'red' && !opt.disabled,
108
- [styles.cyan]: opt.color === 'cyan' && !opt.disabled,
116
+ [styles.red]: opt.color === 'red' && !isDisabled,
117
+ [styles.cyan]: opt.color === 'cyan' && !isDisabled,
118
+ [styles.menuItemDisabledClickable]: isDisabledClickable,
109
119
  },
110
120
  )}
111
121
  onSelect={(event) => {
@@ -115,7 +125,8 @@ export const createRenderItem = (MenuPrimitives) => {
115
125
  opt.onClick?.()
116
126
  onSelectionChange?.(opt)
117
127
  }}
118
- disabled={opt.disabled}
128
+ disabled={isRadixDisabled}
129
+ aria-disabled={isDisabledClickable || undefined}
119
130
  >
120
131
  {opt.icon && <span className={styles.leftIcon}>{opt.icon}</span>}
121
132
  <div className={styles.labelContainer}>
@@ -163,11 +174,13 @@ export const createRenderItem = (MenuPrimitives) => {
163
174
  [styles.menuItemSelected]:
164
175
  opt.checked && showSelectedItemBackground,
165
176
  [styles[`menuItem-size-${size}`]]: size,
166
- [styles.red]: opt.color === 'red' && !opt.disabled,
167
- [styles.cyan]: opt.color === 'cyan' && !opt.disabled,
177
+ [styles.red]: opt.color === 'red' && !isDisabled,
178
+ [styles.cyan]: opt.color === 'cyan' && !isDisabled,
179
+ [styles.menuItemDisabledClickable]: isDisabledClickable,
168
180
  },
169
181
  )}
170
- disabled={opt.disabled}
182
+ disabled={isRadixDisabled}
183
+ aria-disabled={isDisabledClickable || undefined}
171
184
  >
172
185
  <span className={styles.checkboxIndicatorSlot}>
173
186
  <MenuPrimitives.ItemIndicator