@moises.ai/design-system 3.13.7 → 3.14.0

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": "3.13.7",
3
+ "version": "3.14.0",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,8 +1,17 @@
1
+ import { useLayoutEffect, useRef, useState } from 'react'
1
2
  import { Flex, Callout as CalloutPrimitive } from '@radix-ui/themes'
2
3
  import { InfoCircledIcon } from '@radix-ui/react-icons'
3
- import styles from './Callout.module.css'
4
4
  import classNames from 'classnames'
5
+
6
+ import styles from './Callout.module.css'
5
7
  import { Text } from '../Text/Text'
8
+ import { Button } from '../Button/Button'
9
+ import { IconButton } from '../IconButton/IconButton'
10
+
11
+ const buttonSizeForCallout = (size) => (size === '3' ? '2' : '1')
12
+ const iconButtonSizeForCallout = (size) => (size === '3' ? '2' : '1')
13
+ const iconSizeForCallout = (size) => (size === '3' ? '20px' : '16px')
14
+ const iconHeightForCallout = (size) => (size === '3' ? '24px' : '18px')
6
15
 
7
16
  export const Callout = ({
8
17
  children,
@@ -10,12 +19,59 @@ export const Callout = ({
10
19
  size = '2',
11
20
  color = 'accent',
12
21
  highContrast = false,
13
- Icon,
22
+ Icon = InfoCircledIcon,
14
23
  text,
15
24
  title,
16
- hideIcon,
25
+ hideIcon = false,
26
+ buttonProps,
27
+ iconButtonProps,
17
28
  ...props
18
29
  }) => {
30
+ const hasButton = Boolean(buttonProps)
31
+ const hasIconButton = Boolean(iconButtonProps)
32
+ const hasActions = hasButton || hasIconButton
33
+
34
+ const iconSize = iconSizeForCallout(size)
35
+ const iconHeight = iconHeightForCallout(size)
36
+ const textSize = size === '3' ? '3' : '2'
37
+
38
+ const contentRef = useRef(null)
39
+ const actionsRef = useRef(null)
40
+ const [contentAlign, setContentAlign] = useState('flex-start')
41
+
42
+ useLayoutEffect(() => {
43
+
44
+ if (!hasActions) {
45
+ setContentAlign('flex-start')
46
+ return
47
+ }
48
+
49
+ if (!contentRef.current || !actionsRef.current) return
50
+
51
+ const updateAlign = () => {
52
+ const contentHeight = contentRef.current.offsetHeight
53
+ const actionsHeight = actionsRef.current.offsetHeight
54
+ setContentAlign(contentHeight < actionsHeight ? 'center' : 'flex-start')
55
+ }
56
+
57
+ updateAlign()
58
+
59
+ const observer = new ResizeObserver(updateAlign)
60
+ observer.observe(contentRef.current)
61
+ observer.observe(actionsRef.current)
62
+
63
+ return () => observer.disconnect()
64
+ }, [hasActions])
65
+
66
+ const renderIcon = () => {
67
+ if (hideIcon) return null
68
+
69
+ return (
70
+ <CalloutPrimitive.Icon style={{ height: iconHeight }}>
71
+ <Icon width={iconSize} height={iconSize} />
72
+ </CalloutPrimitive.Icon>
73
+ )
74
+ }
19
75
 
20
76
  return (
21
77
  <CalloutPrimitive.Root
@@ -29,29 +85,53 @@ export const Callout = ({
29
85
  styles[color],
30
86
  highContrast && styles[`${color}-highContrast`],
31
87
  styles[`size${size}`],
88
+ hasButton && !hasIconButton && styles.onlyButton,
89
+ hasIconButton && !hasButton && styles.onlyIconButton,
32
90
  )}
33
91
  {...props}
34
92
  >
35
- {!hideIcon && (
36
- <CalloutPrimitive.Icon>
37
- {Icon ? (
38
- <Icon width={16} height={16} />
39
- ) : (
40
- <InfoCircledIcon width={16} height={16} />
41
- )}
42
- </CalloutPrimitive.Icon>
43
- )}
93
+ <Flex justify="between" gap="3" width="100%">
94
+ <Flex
95
+ direction="row"
96
+ gap="2"
97
+ align={contentAlign}
98
+ width="100%"
99
+ >
100
+ {renderIcon()}
101
+
102
+ <Flex direction="column" gap="1" width="100%" ref={contentRef}>
103
+ {title && (
104
+ <Text size={textSize} weight="bold">
105
+ {title}
106
+ </Text>
107
+ )}
44
108
 
109
+ <Text size={textSize} weight="regular">
110
+ {text || children}
111
+ </Text>
112
+ </Flex>
113
+ </Flex>
45
114
 
115
+ {hasActions && (
116
+ <Flex ref={actionsRef} gap="2" align="center" shrink="0">
117
+ {hasButton && (
118
+ <Button
119
+ size={buttonSizeForCallout(size)}
120
+ {...buttonProps}
121
+ />
122
+ )}
46
123
 
47
- <Flex direction="column" gap="0">
48
- {title && (
49
- <Text size={size} weight="bold">{title}</Text>
124
+ {hasIconButton && (
125
+ <IconButton
126
+ size={iconButtonSizeForCallout(size)}
127
+ {...iconButtonProps}
128
+ />
129
+ )}
130
+ </Flex>
50
131
  )}
51
- <Text size={size} weight="regular">{text || children}</Text>
52
132
  </Flex>
53
133
  </CalloutPrimitive.Root>
54
134
  )
55
135
  }
56
136
 
57
- Callout.displayName = 'Callout'
137
+ Callout.displayName = 'Callout'
@@ -1,7 +1,21 @@
1
- .size1 {
2
- padding: 8px 12px;
1
+ .Callout {
2
+ display: flex;
3
3
  }
4
4
 
5
+ .size1:not(.onlyButton):not(.onlyIconButton) {
6
+ padding: 8px 12px !important;
7
+ }
8
+ .onlyButton.size1, .onlyIconButton.size1 {
9
+ padding: 6px 8px !important;
10
+ }
11
+
12
+ .size1Icon, .size2Icon {
13
+ height: 24px !important;
14
+ }
15
+
16
+ .size3Icon {
17
+ height: 32px !important;
18
+ }
5
19
  .accent {
6
20
  background-color: var(--aqua-alpha-3) !important;
7
21
  color: var(--aqua-alpha-11) !important;
@@ -54,3 +68,13 @@
54
68
  color:var(--info-alpha-12) !important;
55
69
  }
56
70
 
71
+ .text {
72
+ height: 100% !important;
73
+ display: flex;
74
+ align-items: center;
75
+ }
76
+
77
+ .textWithIcon{
78
+ display: flex;
79
+ gap: 8px;
80
+ }
@@ -1,5 +1,5 @@
1
1
  import { Callout } from './Callout'
2
- import { AvatarIcon } from '@radix-ui/react-icons'
2
+ import { AvatarIcon, Cross2Icon } from '@radix-ui/react-icons'
3
3
  import { Link } from '@radix-ui/themes'
4
4
 
5
5
  export default {
@@ -23,6 +23,8 @@ The Callout component is used to display informational, warning, error, or succe
23
23
  - **highContrast** (boolean): Whether to use high contrast (default: false)
24
24
  - **Icon** (React.Component): Custom icon (default: InfoCircledIcon)
25
25
  - **hideIcon** (boolean): Whether to hide the icon (default: false)
26
+ - **buttonProps** (object, optional): Spread onto \`Button\` — \`children\`, \`color\`, \`variant\`, \`onClick\`, etc.
27
+ - **iconButtonProps** (object, optional): Spread onto \`IconButton\` — \`children\`, \`color\`, \`onClick\`, \`aria-label\`, etc.
26
28
  - **className** (string): Additional CSS classes
27
29
 
28
30
  ### Usage
@@ -74,6 +76,16 @@ import { Callout, Link } from '@moises.ai/design-system'
74
76
  control: { type: 'text' },
75
77
  description: 'Callout title (optional)',
76
78
  },
79
+ buttonProps: {
80
+ control: false,
81
+ description:
82
+ 'Props forwarded to Button (set to render a button: children, color, variant, onClick, …)',
83
+ },
84
+ iconButtonProps: {
85
+ control: false,
86
+ description:
87
+ 'Props forwarded to IconButton (set to render an icon button: children, color, onClick, aria-label, …)',
88
+ },
77
89
  },
78
90
  }
79
91
 
@@ -320,4 +332,128 @@ export const WithTitle = {
320
332
  },
321
333
  },
322
334
  },
335
+ }
336
+
337
+ // Button and IconButton via buttonProps / iconButtonProps
338
+ export const WithActions = {
339
+ render: () => (
340
+ <div
341
+ style={{
342
+ display: 'flex',
343
+ flexDirection: 'column',
344
+ gap: '1rem',
345
+ width: '100%',
346
+ maxWidth: 720,
347
+ }}
348
+ >
349
+ <Callout
350
+ text="Callout with Button and IconButton — check onClick handlers in the browser console or alerts."
351
+ buttonProps={{
352
+ children: 'Button',
353
+ variant: 'soft',
354
+ color: 'accent',
355
+ onClick: () => {
356
+ // eslint-disable-next-line no-console -- Storybook demo
357
+ console.log('Callout Button clicked')
358
+ },
359
+ }}
360
+ iconButtonProps={{
361
+ children: <Cross2Icon />,
362
+ variant: 'ghost',
363
+ color: 'accent',
364
+ 'aria-label': 'Dismiss',
365
+ onClick: () => {
366
+ // eslint-disable-next-line no-console -- Storybook demo
367
+ console.log('Callout IconButton clicked')
368
+ },
369
+ }}
370
+ />
371
+ <Callout
372
+ title="Only button"
373
+ text="iconButtonProps omitted — only the primary action is shown."
374
+ color="neutral"
375
+ buttonProps={{
376
+ children: 'Action',
377
+ variant: 'solid',
378
+ color: 'neutral',
379
+ onClick: () => { },
380
+ }}
381
+ />
382
+ <Callout
383
+ text="Only IconButton (e.g. dismiss without primary CTA)."
384
+ color="info"
385
+ iconButtonProps={{
386
+ children: <Cross2Icon />,
387
+ color: 'accent',
388
+ 'aria-label': 'Close',
389
+ onClick: () => { },
390
+ }}
391
+ />
392
+ <Callout
393
+ text="Small (size 1) with both actions."
394
+ size="1"
395
+ buttonProps={{
396
+ children: 'OK',
397
+ onClick: () => { },
398
+ }}
399
+ iconButtonProps={{
400
+ children: <Cross2Icon />,
401
+ 'aria-label': 'Close',
402
+ onClick: () => { },
403
+ }}
404
+ />
405
+ <Callout
406
+ text="Small (size 1) with only iconButton."
407
+ size="1"
408
+ iconButtonProps={{
409
+ children: <Cross2Icon />,
410
+ 'aria-label': 'Close',
411
+ onClick: () => { console.log('IconButton clicked') },
412
+ }}
413
+ />
414
+ <Callout
415
+ text="Small (size 1) with only Button."
416
+ size="1"
417
+ buttonProps={{
418
+ children: 'Action',
419
+ variant: 'solid',
420
+ color: 'neutral',
421
+ onClick: () => { console.log('Button clicked') },
422
+ }}
423
+ />
424
+ <Callout
425
+ text="Small (size 2) with only Button."
426
+ size="2"
427
+ buttonProps={{
428
+ children: 'Action',
429
+ variant: 'solid',
430
+ color: 'neutral',
431
+ onClick: () => { console.log('Button clicked') },
432
+ }}
433
+ />
434
+ <Callout
435
+ text="Large (size 3) with both actions."
436
+ size="3"
437
+ buttonProps={{
438
+ children: 'Continue',
439
+ variant: 'solid',
440
+ color: 'cyan',
441
+ onClick: () => { console.log('Button clicked') },
442
+ }}
443
+ iconButtonProps={{
444
+ children: <Cross2Icon />,
445
+ 'aria-label': 'Dismiss',
446
+ onClick: () => { console.log('IconButton clicked') },
447
+ }}
448
+ />
449
+ </div>
450
+ ),
451
+ parameters: {
452
+ docs: {
453
+ description: {
454
+ story:
455
+ 'Uses `buttonProps` and `iconButtonProps` to render `Button` and `IconButton` with full control over label, color, variant, and handlers.',
456
+ },
457
+ },
458
+ },
323
459
  }
@@ -0,0 +1,58 @@
1
+ import { Flex } from '@radix-ui/themes'
2
+ import classNames from 'classnames'
3
+ import { Text } from '../Text/Text'
4
+ import styles from './VerticalSegmentControl.module.css'
5
+
6
+ export const VerticalSegmentControl = ({
7
+ items,
8
+ selectedType,
9
+ handleTypeChange,
10
+ className,
11
+ 'aria-label': ariaLabel = 'Segmented options',
12
+ ...props
13
+ }) => {
14
+ return (
15
+ <Flex
16
+ direction="column"
17
+ className={classNames(styles.root, className)}
18
+ role="radiogroup"
19
+ aria-label={ariaLabel}
20
+ {...props}
21
+ >
22
+ {items.map((item) => {
23
+ const isSelected = selectedType === item.value
24
+ return (
25
+ <button
26
+ key={item.value}
27
+ type="button"
28
+ role="radio"
29
+ aria-checked={isSelected}
30
+ className={styles.item}
31
+ onClick={() => handleTypeChange(item.value)}
32
+ >
33
+ <div
34
+ className={classNames(styles.surface, isSelected && styles.surfaceSelected)}
35
+ >
36
+ <Text
37
+ size="1"
38
+ weight={isSelected ? 'medium' : 'regular'}
39
+ className={classNames(styles.label, isSelected && styles.labelSelected)}
40
+ >
41
+ {item.label}
42
+ </Text>
43
+ </div>
44
+ <span
45
+ className={classNames(
46
+ styles.indicator,
47
+ isSelected && styles.indicatorSelected,
48
+ )}
49
+ aria-hidden
50
+ />
51
+ </button>
52
+ )
53
+ })}
54
+ </Flex>
55
+ )
56
+ }
57
+
58
+ VerticalSegmentControl.displayName = 'VerticalSegmentControl'
@@ -0,0 +1,72 @@
1
+
2
+ .root {
3
+ gap: 1px;
4
+ width: 98px;
5
+ border-radius: var(--radius-3);
6
+ isolation: isolate;
7
+ }
8
+
9
+ .item {
10
+ position: relative;
11
+ display: flex;
12
+ align-items: stretch;
13
+ width: 100%;
14
+ margin: 0;
15
+ padding: 0;
16
+ border: none;
17
+ background: transparent;
18
+ font: inherit;
19
+ text-align: left;
20
+ cursor: pointer;
21
+ }
22
+
23
+ .item:focus-visible {
24
+ outline: 2px solid var(--neutral-alpha-8);
25
+ outline-offset: 2px;
26
+ border-radius: var(--radius-2);
27
+ }
28
+
29
+ .surface {
30
+ display: flex;
31
+ flex: 1 1 auto;
32
+ align-items: center;
33
+ justify-content: center;
34
+ min-width: 0;
35
+ padding: var(--space-1) var(--space-2);
36
+ border-radius: var(--radius-2);
37
+ background-color: rgba(221, 235, 236, 0.02);
38
+ }
39
+
40
+ .surfaceSelected {
41
+ background-color: var(--neutral-alpha-3);
42
+ box-shadow:
43
+ 0 12px 32px 0 rgba(0, 0, 0, 0.15),
44
+ 0 8px 40px 0 rgba(0, 0, 0, 0.05);
45
+ }
46
+
47
+ .label {
48
+ letter-spacing: 0.04px;
49
+ color: var(--neutral-alpha-10);
50
+ }
51
+
52
+ .labelSelected {
53
+ color: var(--aqua-alpha-11);
54
+ }
55
+
56
+ .indicator {
57
+ position: absolute;
58
+ top: 50%;
59
+ right: var(--space-2);
60
+ width: 1px;
61
+ height: 8px;
62
+ border-radius: var(--radius-full);
63
+ background-color: var(--neutral-alpha-2);
64
+ transform: translateY(-50%);
65
+ }
66
+
67
+ .indicatorSelected {
68
+ background-color: var(--aqua-11);
69
+ box-shadow:
70
+ 0 0 3px 1px var(--aqua-alpha-5),
71
+ 0 0 8px 0 var(--aqua-alpha-9);
72
+ }
@@ -0,0 +1,79 @@
1
+ import { Flex } from '@radix-ui/themes'
2
+ import { useState } from 'react'
3
+ import { VerticalSegmentControl } from './VerticalSegmentControl'
4
+
5
+ const sixItems = [
6
+ { value: '1', label: 'Item 1' },
7
+ { value: '2', label: 'Item 2' },
8
+ { value: '3', label: 'Item 3' },
9
+ { value: '4', label: 'Item 4' },
10
+ { value: '5', label: 'Item 5' },
11
+ { value: '6', label: 'Item 6' },
12
+ ]
13
+
14
+ export default {
15
+ title: 'Components/VerticalSegmentControl',
16
+ component: VerticalSegmentControl,
17
+ parameters: {
18
+ layout: 'centered',
19
+ backgrounds: {
20
+ default: 'dark',
21
+ },
22
+ },
23
+ tags: ['autodocs'],
24
+ argTypes: {
25
+ handleTypeChange: { action: 'changed' },
26
+ },
27
+ }
28
+
29
+ export const Default = {
30
+ args: {
31
+ items: sixItems,
32
+ initialSelectedType: '1',
33
+ 'aria-label': 'Mixer segment',
34
+ },
35
+ render: (args) => {
36
+ const [selectedType, setSelectedType] = useState(args.initialSelectedType)
37
+
38
+ return (
39
+ <Flex p="6" style={{ background: 'var(--neutral-1)' }}>
40
+ <VerticalSegmentControl
41
+ items={args.items}
42
+ selectedType={selectedType}
43
+ handleTypeChange={(value) => {
44
+ setSelectedType(value)
45
+ args.handleTypeChange(value)
46
+ }}
47
+ aria-label={args['aria-label']}
48
+ />
49
+ </Flex>
50
+ )
51
+ },
52
+ }
53
+
54
+ export const CompactList = {
55
+ args: {
56
+ items: [
57
+ { value: 'a', label: 'Preset' },
58
+ { value: 'b', label: 'Model' },
59
+ { value: 'c', label: 'Prompt' },
60
+ ],
61
+ initialSelectedType: 'b',
62
+ },
63
+ render: (args) => {
64
+ const [selectedType, setSelectedType] = useState(args.initialSelectedType)
65
+
66
+ return (
67
+ <Flex p="6" style={{ background: 'var(--neutral-1)' }}>
68
+ <VerticalSegmentControl
69
+ items={args.items}
70
+ selectedType={selectedType}
71
+ handleTypeChange={(value) => {
72
+ setSelectedType(value)
73
+ args.handleTypeChange?.(value)
74
+ }}
75
+ />
76
+ </Flex>
77
+ )
78
+ },
79
+ }
package/src/index.jsx CHANGED
@@ -149,3 +149,5 @@ export { useForm } from './components/useForm/useForm'
149
149
  export { VoiceConversionForm } from './components/VoiceConversionForm/VoiceConversionForm'
150
150
  export { Waveform } from './components/VoiceConversionForm/Waveform/Waveform'
151
151
  export { SpecialDialog } from './components/SpecialDialog/SpecialDialog'
152
+
153
+ export { VerticalSegmentControl } from './components/VerticalSegmentControl/VerticalSegmentControl'