@moises.ai/design-system 4.15.11 → 4.16.1

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.15.11",
3
+ "version": "4.16.1",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -0,0 +1,162 @@
1
+ import { forwardRef, useMemo } from 'react'
2
+ import classNames from 'classnames'
3
+ import { Text } from '../Text/Text'
4
+ import { Tooltip } from '../Tooltip/Tooltip'
5
+ import { InfoIcon } from '../../icons'
6
+ import { InstrumentSelectCard } from './InstrumentSelectCard'
7
+ import styles from './InstrumentSelect.module.css'
8
+
9
+ /**
10
+ * @typedef {Object} InstrumentSelectItem
11
+ * @property {string} value - Unique identifier for the item.
12
+ * @property {React.ReactNode} [text] - Item label (alias: `label`, `name`).
13
+ * @property {React.ReactNode} [label] - Alias for `text`.
14
+ * @property {React.ReactNode} [name] - Alias for `text`.
15
+ * @property {React.ComponentType<{ width?: number, height?: number }> | React.ReactNode} [Icon] - Icon component or pre-rendered JSX (alias: `icon`).
16
+ * @property {React.ComponentType<{ width?: number, height?: number }> | React.ReactNode} [icon] - Alias for `Icon`.
17
+ * @property {boolean} [locked] - Marks the item as paywalled (alias: `isLocked`). The lock icon is rendered. Click still fires `onSelect`.
18
+ * @property {boolean} [isLocked] - Alias for `locked`.
19
+ * @property {boolean} [disabled] - Disables the item without showing the lock icon.
20
+ */
21
+
22
+ /**
23
+ * @typedef {string | string[]} InstrumentSelectValue
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} InstrumentSelectProps
28
+ * @property {string} [title] - Optional header title rendered above the grid.
29
+ * @property {React.ReactNode} [tooltip] - Optional tooltip content. Renders an `InfoIcon` with a `Tooltip` next to the title.
30
+ * @property {InstrumentSelectItem[]} items - Items to render as cards.
31
+ * @property {InstrumentSelectValue} [value] - Currently selected value(s). Use a string for single-select and a string[] for multi-select. The component is fully controlled — the parent owns selection state and decides toggle/replace/paywall behavior in `onSelect`.
32
+ * @property {InstrumentSelectValue} [selectedValue] - Alias for `value` (backwards-compat with `InstrumentSelector`).
33
+ * @property {(item: InstrumentSelectItem) => void} [onSelect] - Fires for every card click, including locked items. The parent decides whether to update `value`, open a paywall, etc.
34
+ * @property {number | { initial?: string | number, sm?: string | number, md?: string | number, lg?: string | number }} [columns=2] - Number of grid columns. Accepts a number or a Radix-style responsive object (only `initial` is honored).
35
+ * @property {string} [className]
36
+ */
37
+
38
+ /**
39
+ * Computes whether a given item value is selected, supporting both string (single) and
40
+ * string[] (multi) value shapes.
41
+ *
42
+ * @param {InstrumentSelectValue | undefined} value
43
+ * @param {string} itemValue
44
+ * @returns {boolean}
45
+ */
46
+ function isItemSelected(value, itemValue) {
47
+ if (value == null) return false
48
+ if (Array.isArray(value)) return value.includes(itemValue)
49
+ return value === itemValue
50
+ }
51
+
52
+ /**
53
+ * Instrument selector list with optional title and tooltip header.
54
+ *
55
+ * The component is purely presentational — it does NOT track selection state, does NOT
56
+ * toggle/replace values, and does NOT handle paywalls. The parent owns all of that:
57
+ *
58
+ * - Pass `value` to mark items as selected (string for single, string[] for multi).
59
+ * - `onSelect(item)` fires for every click; the parent decides what to do.
60
+ *
61
+ * Layout: 2-column responsive grid by default with 4px gap. Cards have no minimum width
62
+ * and shrink to fit narrow containers (the parent owns the available width).
63
+ *
64
+ * @example
65
+ * // Single select
66
+ * const [value, setValue] = useState('drums')
67
+ * <InstrumentSelect
68
+ * items={INSTRUMENTS}
69
+ * value={value}
70
+ * onSelect={(item) => setValue(item.value)}
71
+ * />
72
+ *
73
+ * @example
74
+ * // Multi select with paywall — parent owns toggle + paywall logic
75
+ * const [stems, setStems] = useState([])
76
+ * <InstrumentSelect
77
+ * title="Music Stems"
78
+ * tooltip="Select the stems to separate"
79
+ * items={STEMS}
80
+ * value={stems}
81
+ * onSelect={(item) => {
82
+ * if (item.locked) return openPaywall(item)
83
+ * setStems((curr) => curr.includes(item.value)
84
+ * ? curr.filter((v) => v !== item.value)
85
+ * : [...curr, item.value])
86
+ * }}
87
+ * />
88
+ *
89
+ * @type {React.ForwardRefExoticComponent<InstrumentSelectProps & React.RefAttributes<HTMLDivElement>>}
90
+ */
91
+ export const InstrumentSelect = forwardRef(function InstrumentSelect(
92
+ {
93
+ title,
94
+ tooltip,
95
+ items = [],
96
+ value,
97
+ selectedValue,
98
+ onSelect,
99
+ columns = 2,
100
+ className,
101
+ ...props
102
+ },
103
+ ref,
104
+ ) {
105
+ const resolvedValue = value ?? selectedValue
106
+
107
+ const cols = useMemo(() => {
108
+ if (typeof columns === 'number') return columns
109
+ if (columns && typeof columns === 'object') {
110
+ const initial = columns.initial
111
+ const parsed = typeof initial === 'string' ? parseInt(initial, 10) : initial
112
+ return Number.isFinite(parsed) ? parsed : 2
113
+ }
114
+ return 2
115
+ }, [columns])
116
+
117
+ const gridStyle = useMemo(
118
+ () => ({ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }),
119
+ [cols],
120
+ )
121
+
122
+ return (
123
+ <div ref={ref} className={classNames(styles.root, className)} {...props}>
124
+ {title && (
125
+ <div className={styles.header}>
126
+ <Text size="2" weight="medium" className={styles.title}>
127
+ {title}
128
+ </Text>
129
+ {tooltip && (
130
+ <Tooltip content={tooltip}>
131
+ <InfoIcon width={16} height={16} className={styles.infoIcon} />
132
+ </Tooltip>
133
+ )}
134
+ </div>
135
+ )}
136
+
137
+ <div className={styles.grid} style={gridStyle} role="group" aria-label={title}>
138
+ {items.map((item) => {
139
+ const itemText = item.text ?? item.label ?? item.name
140
+ const ItemIcon = item.Icon ?? item.icon
141
+ const itemLocked = item.locked ?? item.isLocked ?? false
142
+ const itemDisabled = item.disabled ?? false
143
+
144
+ return (
145
+ <InstrumentSelectCard
146
+ key={item.value}
147
+ value={item.value}
148
+ text={itemText}
149
+ Icon={ItemIcon}
150
+ selected={isItemSelected(resolvedValue, item.value)}
151
+ locked={itemLocked}
152
+ disabled={itemDisabled}
153
+ onClick={() => onSelect?.(item)}
154
+ />
155
+ )
156
+ })}
157
+ </div>
158
+ </div>
159
+ )
160
+ })
161
+
162
+ InstrumentSelect.displayName = 'InstrumentSelect'
@@ -0,0 +1,147 @@
1
+ /* ----- Root + Header ----- */
2
+
3
+ .root {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: var(--space-2, 8px);
7
+ width: 100%;
8
+ }
9
+
10
+ .header {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: var(--space-2, 8px);
14
+ }
15
+
16
+ .title {
17
+ font-family: var(--default-font-family, 'Articulat', helvetica, arial, sans-serif);
18
+ color: var(--neutral-alpha-12);
19
+ }
20
+
21
+ .infoIcon {
22
+ flex-shrink: 0;
23
+ }
24
+
25
+ .infoIcon path {
26
+ fill: var(--neutral-alpha-7);
27
+ transition: fill 0.18s ease;
28
+ }
29
+
30
+ .infoIcon:hover path {
31
+ fill: var(--neutral-alpha-9);
32
+ }
33
+
34
+ /* ----- Grid ----- */
35
+
36
+ .grid {
37
+ display: grid;
38
+ grid-template-columns: repeat(2, minmax(0, 1fr));
39
+ gap: 4px;
40
+ width: 100%;
41
+ }
42
+
43
+ /* ----- Card ----- */
44
+
45
+ .card {
46
+ position: relative;
47
+ display: flex;
48
+ align-items: center;
49
+ gap: var(--space-3, 12px);
50
+ min-width: 0;
51
+ padding: 10px 8px 10px 12px;
52
+ border: 1px solid var(--neutral-alpha-4, rgba(211, 237, 248, 0.11));
53
+ border-radius: var(--radius-3, 6px);
54
+ background: transparent;
55
+ color: var(--neutral-alpha-12);
56
+ font-family: var(--default-font-family, 'Articulat', helvetica, arial, sans-serif);
57
+ font-size: 14px;
58
+ line-height: 20px;
59
+ text-align: left;
60
+ cursor: pointer;
61
+ user-select: none;
62
+ box-sizing: border-box;
63
+ transition:
64
+ background-color 0.18s ease,
65
+ border-color 0.18s ease,
66
+ color 0.18s ease;
67
+ }
68
+
69
+ .card:focus-visible {
70
+ outline: 2px solid var(--neutral-alpha-8);
71
+ outline-offset: 2px;
72
+ }
73
+
74
+ /* hover (native + forced via data-state='hover' for documentation/Storybook) */
75
+ .card[data-state='default']:hover,
76
+ .card[data-state='hover'] {
77
+ border-color: var(--neutral-alpha-6, rgba(214, 235, 253, 0.19));
78
+ background: var(--neutral-alpha-2, rgba(216, 244, 246, 0.04));
79
+ }
80
+
81
+ /* checked */
82
+ .card[data-state='checked'] {
83
+ border-color: var(--aqua-9, #00dae8);
84
+ background: var(--neutral-alpha-2, rgba(216, 244, 246, 0.04));
85
+ }
86
+
87
+ /* disabled / locked — same border/background as default; text + lock are muted, icon stays */
88
+ .card[data-state='disabled'] {
89
+ cursor: default;
90
+ }
91
+
92
+ .card[data-state='disabled'] .cardText,
93
+ .card[data-state='disabled'] .lockIcon {
94
+ color: var(--neutral-alpha-5);
95
+ }
96
+
97
+ /* ----- Card slots ----- */
98
+
99
+ .cardIcon {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ flex-shrink: 0;
104
+ width: 16px;
105
+ height: 16px;
106
+ color: var(--neutral-alpha-7);
107
+ }
108
+
109
+ .cardIcon svg {
110
+ width: 16px;
111
+ height: 16px;
112
+ }
113
+
114
+ .card[data-state='checked'] .cardIcon {
115
+ color: var(--aqua-9, #00dae8);
116
+ }
117
+
118
+ .cardText {
119
+ flex: 1 1 auto;
120
+ min-width: 0;
121
+ font-weight: 400;
122
+ letter-spacing: 0;
123
+ color: var(--neutral-alpha-12);
124
+ overflow: hidden;
125
+ text-overflow: ellipsis;
126
+ white-space: nowrap;
127
+ }
128
+
129
+ /*
130
+ * When the lock is present, reserve space for it so the text truncates with ellipsis
131
+ * BEFORE reaching the lock icon. Lock geometry: width 12px + right offset 11px from the
132
+ * card edge - existing card padding-right 8px = ~16px buffer needed (with 1px breathing room).
133
+ */
134
+ .card[data-locked] .cardText {
135
+ margin-right: 16px;
136
+ }
137
+
138
+ .lockIcon {
139
+ position: absolute;
140
+ top: 50%;
141
+ right: 11px;
142
+ width: 12px;
143
+ height: 12px;
144
+ transform: translateY(-50%);
145
+ color: var(--neutral-alpha-5);
146
+ pointer-events: none;
147
+ }
@@ -0,0 +1,243 @@
1
+ import { useState } from 'react'
2
+ import { InstrumentSelect } from './InstrumentSelect'
3
+ import { InstrumentSelectCard } from './InstrumentSelectCard'
4
+ import {
5
+ AcousticGuitarIcon,
6
+ BassIcon,
7
+ DrumsIcon,
8
+ ElectricGuitarIcon,
9
+ KeysIcon,
10
+ PianoIcon,
11
+ StringsIcon,
12
+ VocalIcon,
13
+ VocalsBackgroundIcon,
14
+ WindIcon,
15
+ } from '../../icons'
16
+
17
+ const INSTRUMENTS = [
18
+ { value: 'vocals', text: 'Vocals', Icon: VocalIcon },
19
+ { value: 'lead-vocals', text: 'Lead Vocals', Icon: VocalsBackgroundIcon },
20
+ { value: 'guitars', text: 'Guitars', Icon: ElectricGuitarIcon },
21
+ { value: 'acoustic-guitar', text: 'Acoustic Guitar', Icon: AcousticGuitarIcon },
22
+ { value: 'bass', text: 'Bass', Icon: BassIcon },
23
+ { value: 'piano', text: 'Piano', Icon: PianoIcon },
24
+ { value: 'keys', text: 'Keys', Icon: KeysIcon },
25
+ { value: 'strings', text: 'Strings', Icon: StringsIcon },
26
+ { value: 'drums', text: 'Drums', Icon: DrumsIcon },
27
+ { value: 'wind', text: 'Wind', Icon: WindIcon },
28
+ ]
29
+
30
+ const STEMS_WITH_LOCKED = [
31
+ { value: 'vocals', text: 'Vocals', Icon: VocalIcon },
32
+ { value: 'lead-vocals', text: 'Lead Vocals', Icon: VocalsBackgroundIcon, locked: true },
33
+ { value: 'guitars', text: 'Guitars', Icon: ElectricGuitarIcon },
34
+ { value: 'rhythm-guitars', text: 'Rhythm Guitars', Icon: ElectricGuitarIcon, locked: true },
35
+ { value: 'bass', text: 'Bass', Icon: BassIcon },
36
+ { value: 'solo-guitars', text: 'Solo Guitars', Icon: ElectricGuitarIcon, locked: true },
37
+ { value: 'piano', text: 'Piano', Icon: PianoIcon },
38
+ { value: 'keys', text: 'Keys', Icon: KeysIcon },
39
+ { value: 'drums', text: 'Drums', Icon: DrumsIcon },
40
+ { value: 'wind', text: 'Wind', Icon: WindIcon, locked: true },
41
+ ]
42
+
43
+ export default {
44
+ title: 'Components/InstrumentSelect',
45
+ component: InstrumentSelect,
46
+ parameters: {
47
+ layout: 'centered',
48
+ },
49
+ tags: ['autodocs'],
50
+ argTypes: {
51
+ columns: { control: { type: 'number', min: 1, max: 4 } },
52
+ title: { control: 'text' },
53
+ tooltip: { control: 'text' },
54
+ },
55
+ decorators: [
56
+ (Story) => (
57
+ <div style={{ width: 340 }}>
58
+ <Story />
59
+ </div>
60
+ ),
61
+ ],
62
+ }
63
+
64
+ /* ---------- Single Select ---------- */
65
+
66
+ export const SingleSelect = {
67
+ render: (args) => {
68
+ const [value, setValue] = useState('drums')
69
+ return (
70
+ <InstrumentSelect
71
+ {...args}
72
+ value={value}
73
+ onSelect={(item) => setValue(item.value)}
74
+ />
75
+ )
76
+ },
77
+ args: {
78
+ items: INSTRUMENTS.slice(0, 6),
79
+ },
80
+ }
81
+
82
+ /* ---------- Multi Select ---------- */
83
+
84
+ export const MultiSelect = {
85
+ render: (args) => {
86
+ const [values, setValues] = useState(['vocals', 'guitars'])
87
+ return (
88
+ <InstrumentSelect
89
+ {...args}
90
+ value={values}
91
+ onSelect={(item) => {
92
+ setValues((curr) =>
93
+ curr.includes(item.value)
94
+ ? curr.filter((v) => v !== item.value)
95
+ : [...curr, item.value],
96
+ )
97
+ }}
98
+ />
99
+ )
100
+ },
101
+ args: {
102
+ items: INSTRUMENTS.slice(0, 6),
103
+ },
104
+ }
105
+
106
+ /* ---------- With Title ---------- */
107
+
108
+ export const WithTitle = {
109
+ render: (args) => {
110
+ const [value, setValue] = useState('drums')
111
+ return (
112
+ <InstrumentSelect
113
+ {...args}
114
+ value={value}
115
+ onSelect={(item) => setValue(item.value)}
116
+ />
117
+ )
118
+ },
119
+ args: {
120
+ title: 'Music Stems',
121
+ items: INSTRUMENTS.slice(0, 6),
122
+ },
123
+ }
124
+
125
+ /* ---------- With Title And Tooltip ---------- */
126
+
127
+ export const WithTitleAndTooltip = {
128
+ render: (args) => {
129
+ const [value, setValue] = useState('drums')
130
+ return (
131
+ <InstrumentSelect
132
+ {...args}
133
+ value={value}
134
+ onSelect={(item) => setValue(item.value)}
135
+ />
136
+ )
137
+ },
138
+ args: {
139
+ title: 'Music Stems',
140
+ tooltip: 'Choose the instrument that best matches what you want to generate.',
141
+ items: INSTRUMENTS.slice(0, 6),
142
+ },
143
+ }
144
+
145
+ /* ---------- With Locked Items (multi-select with paywall) ---------- */
146
+
147
+ export const WithLockedItems = {
148
+ render: (args) => {
149
+ const [values, setValues] = useState(['vocals', 'guitars'])
150
+ return (
151
+ <InstrumentSelect
152
+ {...args}
153
+ value={values}
154
+ onSelect={(item) => {
155
+ if (item.locked) {
156
+ // eslint-disable-next-line no-alert
157
+ alert(`Open paywall for: ${item.text}`)
158
+ return
159
+ }
160
+ setValues((curr) =>
161
+ curr.includes(item.value)
162
+ ? curr.filter((v) => v !== item.value)
163
+ : [...curr, item.value],
164
+ )
165
+ }}
166
+ />
167
+ )
168
+ },
169
+ args: {
170
+ title: 'Music Stems',
171
+ tooltip: 'Some stems are part of the Premium plan.',
172
+ items: STEMS_WITH_LOCKED,
173
+ },
174
+ }
175
+
176
+ /* ---------- Card states (forced) ---------- */
177
+
178
+ export const CardStates = () => (
179
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 162px)', gap: 4 }}>
180
+ <InstrumentSelectCard value="vocals" text="Default" Icon={VocalIcon} state="default" />
181
+ <InstrumentSelectCard value="vocals" text="Hover" Icon={VocalIcon} state="hover" />
182
+ <InstrumentSelectCard value="vocals" text="Checked" Icon={VocalIcon} state="checked" />
183
+ <InstrumentSelectCard
184
+ value="vocals"
185
+ text="Disabled"
186
+ Icon={VocalIcon}
187
+ state="disabled"
188
+ locked
189
+ />
190
+ </div>
191
+ )
192
+
193
+ CardStates.parameters = {
194
+ docs: {
195
+ description: {
196
+ story:
197
+ 'The four visual states of `InstrumentSelectCard`: default, hover, checked, and disabled (locked).',
198
+ },
199
+ },
200
+ }
201
+
202
+ /* ---------- Columns ---------- */
203
+
204
+ export const OneColumn = {
205
+ render: (args) => {
206
+ const [value, setValue] = useState('drums')
207
+ return (
208
+ <InstrumentSelect
209
+ {...args}
210
+ value={value}
211
+ onSelect={(item) => setValue(item.value)}
212
+ />
213
+ )
214
+ },
215
+ args: {
216
+ columns: 1,
217
+ items: INSTRUMENTS.slice(0, 6),
218
+ },
219
+ }
220
+
221
+ export const ThreeColumns = {
222
+ render: (args) => {
223
+ const [value, setValue] = useState('drums')
224
+ return (
225
+ <InstrumentSelect
226
+ {...args}
227
+ value={value}
228
+ onSelect={(item) => setValue(item.value)}
229
+ />
230
+ )
231
+ },
232
+ args: {
233
+ columns: 3,
234
+ items: INSTRUMENTS,
235
+ },
236
+ decorators: [
237
+ (Story) => (
238
+ <div style={{ width: 540 }}>
239
+ <Story />
240
+ </div>
241
+ ),
242
+ ],
243
+ }
@@ -0,0 +1,85 @@
1
+ import { forwardRef } from 'react'
2
+ import classNames from 'classnames'
3
+ import { LockIcon } from '../../icons'
4
+ import styles from './InstrumentSelect.module.css'
5
+
6
+ /**
7
+ * @typedef {'default' | 'hover' | 'checked' | 'disabled'} InstrumentSelectCardState
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} InstrumentSelectCardProps
12
+ * @property {string} value - Card value identifier (used by parent list for selection).
13
+ * @property {React.ReactNode} [text] - Card label text.
14
+ * @property {React.ComponentType<{ width?: number, height?: number }> | React.ReactNode} [Icon] - Icon as a component or pre-rendered JSX element.
15
+ * @property {boolean} [selected=false] - Whether the card is currently selected. Drives the `checked` visual state.
16
+ * @property {boolean} [locked=false] - Marks the card as locked (paywalled). Renders the lock icon and applies the disabled visual state. `onClick` still fires so consumers can trigger a paywall flow.
17
+ * @property {boolean} [disabled=false] - Marks the card as disabled without showing the lock icon.
18
+ * @property {InstrumentSelectCardState} [state] - Force a specific visual state (overrides `selected`/`locked`/`disabled`). Primarily for documentation/Storybook.
19
+ * @property {(event: React.MouseEvent<HTMLButtonElement>) => void} [onClick]
20
+ * @property {string} [className]
21
+ */
22
+
23
+ /**
24
+ * Individual instrument card with four visual states (default, hover, checked, disabled).
25
+ *
26
+ * Native `<button>` with `aria-pressed` for selection (works for both single and multi-select)
27
+ * and `aria-disabled` for locked/disabled items. The HTML `disabled` attribute is intentionally
28
+ * NOT used so that locked items remain clickable (e.g. to trigger paywall flows).
29
+ *
30
+ * @example
31
+ * <InstrumentSelectCard value="vocals" text="Vocals" Icon={VocalIcon} selected />
32
+ *
33
+ * @example
34
+ * // Locked item (clickable, shows lock icon at the right edge)
35
+ * <InstrumentSelectCard value="hifi" text="HiFi" Icon={HiFiIcon} locked onClick={openPaywall} />
36
+ *
37
+ * @type {React.ForwardRefExoticComponent<InstrumentSelectCardProps & React.RefAttributes<HTMLButtonElement>>}
38
+ */
39
+ export const InstrumentSelectCard = forwardRef(function InstrumentSelectCard(
40
+ {
41
+ value,
42
+ text,
43
+ Icon,
44
+ selected = false,
45
+ locked = false,
46
+ disabled = false,
47
+ state,
48
+ onClick,
49
+ className,
50
+ ...props
51
+ },
52
+ ref,
53
+ ) {
54
+ const isBlocked = locked || disabled
55
+ const computedState = state ?? (isBlocked ? 'disabled' : selected ? 'checked' : 'default')
56
+
57
+ const renderIcon = () => {
58
+ if (!Icon) return null
59
+ if (typeof Icon === 'function') {
60
+ return <Icon width={16} height={16} />
61
+ }
62
+ return Icon
63
+ }
64
+
65
+ return (
66
+ <button
67
+ ref={ref}
68
+ type="button"
69
+ data-state={computedState}
70
+ data-value={value}
71
+ data-locked={locked || undefined}
72
+ aria-pressed={selected}
73
+ aria-disabled={isBlocked || undefined}
74
+ onClick={onClick}
75
+ className={classNames(styles.card, className)}
76
+ {...props}
77
+ >
78
+ {Icon && <span className={styles.cardIcon}>{renderIcon()}</span>}
79
+ {text != null && <span className={styles.cardText}>{text}</span>}
80
+ {locked && <LockIcon width={12} height={12} className={styles.lockIcon} />}
81
+ </button>
82
+ )
83
+ })
84
+
85
+ InstrumentSelectCard.displayName = 'InstrumentSelectCard'
@@ -0,0 +1,2 @@
1
+ export { InstrumentSelect } from './InstrumentSelect'
2
+ export { InstrumentSelectCard } from './InstrumentSelectCard'
@@ -2,6 +2,14 @@ import { RadioCards as RadioCardsRadix, Flex, Text } from '@radix-ui/themes'
2
2
  import styles from './InstrumentSelector.module.css'
3
3
  import classNames from 'classnames'
4
4
 
5
+ /**
6
+ * @deprecated Use `InstrumentSelect` instead. `InstrumentSelector` will be removed in a
7
+ * future release. The new component supports both single and multi-select (auto-inferred
8
+ * from the value type), exposes a `title` and `tooltip` header, and matches the updated
9
+ * Figma design (cyan border on selected). All existing props are accepted as aliases.
10
+ *
11
+ * @see InstrumentSelect
12
+ */
5
13
  export const InstrumentSelector = ({
6
14
  items = [],
7
15
  defaultValue,
@@ -3,6 +3,7 @@
3
3
  align-items: center;
4
4
  gap: 2px;
5
5
  padding: 6px 12px;
6
+ max-width: 232px;
6
7
  text-align: center;
7
8
  z-index: 9999;
8
9
  animation-duration: 0.15s;
@@ -85,7 +86,8 @@
85
86
  }
86
87
 
87
88
  .TooltipText {
88
- white-space: nowrap;
89
+ white-space: normal;
90
+ overflow-wrap: anywhere;
89
91
  }
90
92
 
91
93
  .TooltipShortcut {
package/src/index.jsx CHANGED
@@ -102,6 +102,9 @@ export { HeaderPanel } from './components/HeaderPanel/HeaderPanel'
102
102
  export { HorizontalVolume } from './components/HorizontalVolume/HorizontalVolume'
103
103
  export { IconButton } from './components/IconButton/IconButton'
104
104
  export { InputLevelMeter } from './components/InputLevelMeter/InputLevelMeter'
105
+ export { InstrumentSelect } from './components/InstrumentSelect/InstrumentSelect'
106
+ export { InstrumentSelectCard } from './components/InstrumentSelect/InstrumentSelectCard'
107
+ /** @deprecated Use `InstrumentSelect` instead. */
105
108
  export { InstrumentSelector } from './components/InstrumentSelector/InstrumentSelector'
106
109
  export { Knob } from './components/Knob/Knob'
107
110
  export { ListCards } from './components/ListCards/ListCards'