@moises.ai/design-system 3.9.21 → 3.9.23

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.9.21",
3
+ "version": "3.9.23",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -119,23 +119,33 @@ function Header({ title, onBack, headerLeft, headerRight, allowClose = true, onC
119
119
  * allowing individual children to manage their own overflow.
120
120
  */
121
121
  function Content({ scrollable = true, style, children }) {
122
+ if (!scrollable) {
123
+ return (
124
+ <Flex
125
+ direction="column"
126
+ gap="3"
127
+ style={{ flex: 1, overflowY: 'hidden', minHeight: 0, ...style }}
128
+ >
129
+ {children}
130
+ </Flex>
131
+ )
132
+ }
133
+
122
134
  return (
123
135
  <div
124
136
  style={{
125
137
  flex: 1,
126
138
  minHeight: 0,
127
- overflowY: scrollable ? 'auto' : 'hidden',
139
+ overflowY: 'auto',
128
140
  display: 'flex',
129
141
  flexDirection: 'column',
130
- ...style,
131
142
  }}
132
143
  >
133
144
  <Flex
134
145
  direction="column"
135
- px="4"
136
- py={scrollable ? '4' : undefined}
146
+ p="4"
137
147
  gap="3"
138
- style={scrollable ? undefined : { flex: 1, minHeight: 0 }}
148
+ style={style}
139
149
  >
140
150
  {children}
141
151
  </Flex>
@@ -158,7 +168,6 @@ function Footer({ children }) {
158
168
  align="center"
159
169
  justify="center"
160
170
  mx="4"
161
- // py="3"
162
171
  >
163
172
  {children}
164
173
  </Flex>
@@ -177,6 +186,7 @@ function Footer({ children }) {
177
186
  * @param {React.ReactNode} [props.headerLeft] - Override for the default back button.
178
187
  * @param {boolean} [props.allowClose=true] - Whether to show the close (x) button.
179
188
  * @param {Function} [props.onClose] - Custom close handler.
189
+ * @param {boolean} [props.keepMounted] - When true, the screen stays mounted when navigating away, preserving state.
180
190
  */
181
191
  function Screen() {
182
192
  return null
@@ -262,8 +272,19 @@ function Navigator({ initialScreen, animated, children }) {
262
272
  const def = screens[current.name]
263
273
  if (!def) return null
264
274
 
275
+ // Stable layers: every stack entry with keepMounted, keyed by stack index so the same
276
+ // component instance stays mounted when navigating (fixes state loss on pop).
277
+ const keepMountedLayers = useMemo(
278
+ () =>
279
+ stack
280
+ .map((entry, stackIndex) => (screens[entry.name]?.keepMounted ? { entry, stackIndex } : null))
281
+ .filter(Boolean),
282
+ [stack, screens],
283
+ )
284
+
265
285
  const opt = (key, fallback) => (key in screenOptions ? screenOptions[key] : (def[key] ?? fallback))
266
286
  const Component = def.component
287
+ const currentHasKeepMounted = def.keepMounted === true
267
288
 
268
289
  const handleSetOptions = useCallback(
269
290
  (opts) => {
@@ -291,11 +312,51 @@ function Navigator({ initialScreen, animated, children }) {
291
312
  />
292
313
  <div
293
314
  ref={wrapperRef}
294
- style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, overflow: 'hidden' }}
315
+ style={{
316
+ position: 'relative',
317
+ display: 'flex',
318
+ flexDirection: 'column',
319
+ flex: 1,
320
+ minHeight: 0,
321
+ overflow: 'hidden',
322
+ }}
295
323
  >
296
- <ScreenContext.Provider value={screenCtx} key={gen}>
297
- <Component />
298
- </ScreenContext.Provider>
324
+ {keepMountedLayers.map(({ entry, stackIndex }) => {
325
+ const layerDef = screens[entry.name]
326
+ if (!layerDef?.component) return null
327
+ const LayerComponent = layerDef.component
328
+ const isCurrent = entry.name === current.name
329
+ const layerCtx = isCurrent
330
+ ? screenCtx
331
+ : { name: entry.name, params: entry.params || {}, setOptions: () => {} }
332
+ return (
333
+ <div
334
+ key={`layer-${stackIndex}`}
335
+ style={isCurrent ? {
336
+ position: 'relative',
337
+ display: 'flex',
338
+ flexDirection: 'column',
339
+ flex: 1,
340
+ minHeight: 0,
341
+ overflow: 'hidden',
342
+ } : {
343
+ position: 'absolute',
344
+ inset: 0,
345
+ visibility: 'hidden',
346
+ pointerEvents: 'none',
347
+ }}
348
+ >
349
+ <ScreenContext.Provider value={layerCtx}>
350
+ <LayerComponent />
351
+ </ScreenContext.Provider>
352
+ </div>
353
+ )
354
+ })}
355
+ {!currentHasKeepMounted && (
356
+ <ScreenContext.Provider value={screenCtx} key={gen}>
357
+ <Component />
358
+ </ScreenContext.Provider>
359
+ )}
299
360
  </div>
300
361
  </div>
301
362
  </NavigationContext.Provider>
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
- import { Badge, Button, Card, Flex, IconButton, Text, Extension, useNavigation, useScreen } from '../../index'
3
2
  import { DotsVerticalIcon } from '../../icons'
3
+ import { Badge, Button, Card, Extension, Flex, IconButton, Text, useNavigation, useScreen } from '../../index'
4
4
 
5
5
  const mockMoises = {
6
6
  extension: { close: () => console.log('extension.close()') },
@@ -365,3 +365,52 @@ export const CustomClose = {
365
365
  </Extension>
366
366
  ),
367
367
  }
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // Navigator — keepMounted preserves state when navigating away
371
+ // ---------------------------------------------------------------------------
372
+
373
+ function KeepMountedHomeScreen() {
374
+ const { push } = useNavigation()
375
+ const [value, setValue] = useState('')
376
+
377
+ return (
378
+ <Extension.Content>
379
+ <Text size="2" color="gray">
380
+ This screen has <code>keepMounted</code>. Type below, then go to Next. When you go back, your text is still here.
381
+ </Text>
382
+ <input
383
+ type="text"
384
+ placeholder="Type something..."
385
+ value={value}
386
+ onChange={(e) => setValue(e.target.value)}
387
+ style={{
388
+ padding: 8,
389
+ borderRadius: 6,
390
+ border: '1px solid var(--gray-a6)',
391
+ }}
392
+ />
393
+ <Button variant="soft" size="2" onClick={() => push('next')}>
394
+ Go to Next
395
+ </Button>
396
+ </Extension.Content>
397
+ )
398
+ }
399
+
400
+ function KeepMountedNextScreen() {
401
+ return (
402
+ <Extension.Content>
403
+ <Text size="2">Press back — the previous screen kept its state because it stayed mounted.</Text>
404
+ </Extension.Content>
405
+ )
406
+ }
407
+
408
+ export const KeepMounted = {
409
+ name: 'Navigator (keepMounted)',
410
+ render: () => (
411
+ <Extension moises={mockMoises} initialScreen="home" animated>
412
+ <Extension.Screen name="home" title="Keep Mounted Demo" component={KeepMountedHomeScreen} keepMounted />
413
+ <Extension.Screen name="next" title="Next" component={KeepMountedNextScreen} />
414
+ </Extension>
415
+ ),
416
+ }
@@ -273,8 +273,26 @@ export const ListCards = ({
273
273
  children,
274
274
  alwaysShowScrollbar = true,
275
275
  style,
276
+ scrollable = true,
276
277
  ...props
277
278
  }) => {
279
+ const content = (
280
+ <Box className={styles.gridContainer} pr="3" style={style}>
281
+ <RadioCards.Root
282
+ defaultValue={undefined}
283
+ className={className}
284
+ style={{ display: 'contents' }}
285
+ {...props}
286
+ >
287
+ {children}
288
+ </RadioCards.Root>
289
+ </Box>
290
+ )
291
+
292
+ if (!scrollable) {
293
+ return content
294
+ }
295
+
278
296
  return (
279
297
  <ScrollArea
280
298
  height="100%"
@@ -282,16 +300,7 @@ export const ListCards = ({
282
300
  scrollbars="vertical"
283
301
  type={alwaysShowScrollbar ? 'auto' : 'hover'}
284
302
  >
285
- <Box className={styles.gridContainer} pr="3" style={style}>
286
- <RadioCards.Root
287
- defaultValue={undefined}
288
- className={className}
289
- style={{ display: 'contents' }}
290
- {...props}
291
- >
292
- {children}
293
- </RadioCards.Root>
294
- </Box>
303
+ {content}
295
304
  </ScrollArea>
296
305
  )
297
306
  }
@@ -1,14 +1,14 @@
1
- import React, { useCallback } from 'react'
2
- import classNames from 'classnames'
3
1
  import {
4
- Flex,
5
2
  Box,
6
3
  Card as CardRadix,
7
- Text,
4
+ Flex,
8
5
  ScrollArea,
6
+ Text,
9
7
  } from '@radix-ui/themes'
10
- import styles from './MultiSelectCards.module.css'
8
+ import classNames from 'classnames'
9
+ import React, { useCallback } from 'react'
11
10
  import { LockIcon } from '../../icons'
11
+ import styles from './MultiSelectCards.module.css'
12
12
 
13
13
  const Card = ({ item, value, onSelect, disabled, isLocked }) => {
14
14
  const handleSelect = useCallback(() => {
@@ -71,12 +71,13 @@ export const MultiSelectCards = ({
71
71
  onSelect,
72
72
  className,
73
73
  disabled = false,
74
+ style,
74
75
  ...props
75
76
  }) => (
76
77
  <ScrollArea height="100%" width="100%" scrollbars="vertical">
77
78
  <Box
78
79
  className={classNames(styles.gridContainer, className)}
79
- pr="3"
80
+ style={{ paddingRight: 'var(--space-3)', ...style }}
80
81
  {...props}
81
82
  >
82
83
  {items.map((item) => (
@@ -204,6 +204,7 @@
204
204
 
205
205
  .setlistsScrollArea {
206
206
  height: 100%;
207
+ max-height: 80vh;
207
208
  padding-right: 6px;
208
209
  }
209
210
 
@@ -223,13 +224,14 @@ sectionTitle {
223
224
  .setlistsContent {
224
225
  /* gap: 4px; */
225
226
  margin: 1px 0;
227
+ /* padding-bottom: 16px; */
226
228
  }
227
229
 
228
230
  .scrollShadow {
229
231
  position: relative;
230
232
  }
231
233
 
232
- .scrollShadow::after {
234
+ /* .scrollShadow::after {
233
235
  content: '';
234
236
  position: sticky;
235
237
  bottom: -2px;
@@ -245,7 +247,7 @@ sectionTitle {
245
247
  transparent 100%
246
248
  );
247
249
  opacity: 0.35;
248
- }
250
+ } */
249
251
 
250
252
  .collapsedStack {
251
253
  gap: 0;
@@ -284,14 +286,13 @@ sectionTitle {
284
286
 
285
287
  .collapsedMask {
286
288
  position: relative;
287
- overflow: hidden;
288
289
  display: flex;
289
- max-height: 80vh;
290
290
  transition: max-height 260ms ease-in-out;
291
291
  }
292
292
 
293
293
  .collapsedMask.collapsedStack,
294
294
  .collapsedMask.collapsedShrinking {
295
+ overflow: hidden;
295
296
  max-height: 120px;
296
297
  }
297
298
 
@@ -12,6 +12,44 @@ import {
12
12
  WidgetIcon,
13
13
  } from '../../icons'
14
14
 
15
+ const SETLIST_TEMPLATES = [
16
+ {
17
+ label: 'Piano Exercises',
18
+ subtitle: 'By Berklee',
19
+ moises: true,
20
+ icon: 'https://storage.googleapis.com/moises-api-assets/setlists/jamsession_cory/cover.png',
21
+ },
22
+ {
23
+ label: 'Bossa Nova Essentials',
24
+ },
25
+ {
26
+ label: 'Band Rehearsal',
27
+ subtitle: 'By Nickyz dsdasdas',
28
+ group: true,
29
+ },
30
+ {
31
+ label: 'Gig Dec 21th',
32
+ icon: <MusicNoteIcon width={16} height={16} />,
33
+ },
34
+ ]
35
+
36
+ const createSetlists = (count) =>
37
+ Array.from({ length: count }, (_, i) => {
38
+ const template = SETLIST_TEMPLATES[i % SETLIST_TEMPLATES.length]
39
+ const id = `setlist-${i}`
40
+ return {
41
+ id,
42
+ label: `${template.label} #${i + 1}`,
43
+ subtitle: template.subtitle,
44
+ moises: template.moises,
45
+ icon: template.icon,
46
+ group: template.group,
47
+ dropdownMenuOptions: [
48
+ { type: 'item', key: 'edit', label: 'Edit', onClick: () => console.log('Edit clicked') },
49
+ ],
50
+ }
51
+ })
52
+
15
53
  export default {
16
54
  title: 'Features/Shell',
17
55
  component: Shell,
@@ -96,225 +134,8 @@ export const Default = {
96
134
  ],
97
135
  selectedProductId: 'library',
98
136
  onProductClick: () => console.log('Product clicked'),
99
- setlists: [
100
- {
101
- id: 'piano-exercises',
102
- label: 'Piano Exercises',
103
- subtitle: 'By Berklee',
104
- moises: true,
105
- icon: 'https://storage.googleapis.com/moises-api-assets/setlists/jamsession_cory/cover.png',
106
- dropdownMenuOptions: [
107
- {
108
- type: 'item',
109
- key: 'edit',
110
- label: 'Edit',
111
- onClick: () => console.log('Edit clicked'),
112
- },
113
- ],
114
- },
115
- {
116
- id: 'bossa-nova',
117
- label: 'Bossa Nova Essentials',
118
- dropdownMenuOptions: [
119
- {
120
- type: 'item',
121
- key: 'edit',
122
- label: 'Edit',
123
- onClick: () => console.log('Edit clicked'),
124
- },
125
- ],
126
- },
127
- {
128
- id: 'band-rehearsal',
129
- label: 'Band Rehearsal',
130
- subtitle: 'By Nickyz dsdasdas',
131
- group: true,
132
- dropdownMenuOptions: [
133
- {
134
- type: 'item',
135
- key: 'edit',
136
- label: 'Edit',
137
- onClick: () => console.log('Edit clicked'),
138
- },
139
- ],
140
- },
141
- {
142
- id: 'gig-dec-21',
143
- label: 'Gig Dec 21th',
144
- icon: <MusicNoteIcon width={16} height={16} />,
145
- dropdownMenuOptions: [
146
- {
147
- type: 'item',
148
- key: 'edit',
149
- label: 'Edit',
150
- onClick: () => console.log('Edit clicked'),
151
- },
152
- ],
153
- },
154
- {
155
- id: 'piano-exercises-1',
156
- label: 'Piano Exercises',
157
- subtitle: 'By Berklee',
158
- moises: true,
159
- icon: 'https://storage.googleapis.com/moises-api-assets/setlists/jamsession_cory/cover.png',
160
- dropdownMenuOptions: [
161
- {
162
- type: 'item',
163
- key: 'edit',
164
- label: 'Edit',
165
- onClick: () => console.log('Edit clicked'),
166
- },
167
- ],
168
- },
169
- {
170
- id: 'bossa-nova-1',
171
- label: 'Bossa Nova Essentials',
172
- dropdownMenuOptions: [
173
- {
174
- type: 'item',
175
- key: 'edit',
176
- label: 'Edit',
177
- onClick: () => console.log('Edit clicked'),
178
- },
179
- ],
180
- },
181
- {
182
- id: 'band-rehearsal-1',
183
- label: 'Band Rehearsal',
184
- subtitle: 'By Nickyz dsdasdas',
185
- group: true,
186
- dropdownMenuOptions: [
187
- {
188
- type: 'item',
189
- key: 'edit',
190
- label: 'Edit',
191
- onClick: () => console.log('Edit clicked'),
192
- },
193
- ],
194
- },
195
- {
196
- id: 'gig-dec-21-1',
197
- label: 'Gig Dec 21th',
198
- icon: <MusicNoteIcon width={16} height={16} />,
199
- dropdownMenuOptions: [
200
- {
201
- type: 'item',
202
- key: 'edit',
203
- label: 'Edit',
204
- onClick: () => console.log('Edit clicked'),
205
- },
206
- ],
207
- },
208
- {
209
- id: 'piano-exercises-2',
210
- label: 'Piano Exercises',
211
- subtitle: 'By Berklee',
212
- moises: true,
213
- icon: 'https://storage.googleapis.com/moises-api-assets/setlists/jamsession_cory/cover.png',
214
- dropdownMenuOptions: [
215
- {
216
- type: 'item',
217
- key: 'edit',
218
- label: 'Edit',
219
- onClick: () => console.log('Edit clicked'),
220
- },
221
- ],
222
- },
223
- {
224
- id: 'bossa-nova-2',
225
- label: 'Bossa Nova Essentials',
226
- dropdownMenuOptions: [
227
- {
228
- type: 'item',
229
- key: 'edit',
230
- label: 'Edit',
231
- onClick: () => console.log('Edit clicked'),
232
- },
233
- ],
234
- },
235
- {
236
- id: 'band-rehearsal-2',
237
- label: 'Band Rehearsal',
238
- subtitle: 'By Nickyz dsdasdas',
239
- group: true,
240
- dropdownMenuOptions: [
241
- {
242
- type: 'item',
243
- key: 'edit',
244
- label: 'Edit',
245
- onClick: () => console.log('Edit clicked'),
246
- },
247
- ],
248
- },
249
- {
250
- id: 'gig-dec-21-2',
251
- label: 'Gig Dec 21th',
252
- icon: <MusicNoteIcon width={16} height={16} />,
253
- dropdownMenuOptions: [
254
- {
255
- type: 'item',
256
- key: 'edit',
257
- label: 'Edit',
258
- onClick: () => console.log('Edit clicked'),
259
- },
260
- ],
261
- },
262
- {
263
- id: 'piano-exercises-3',
264
- label: 'Piano Exercises',
265
- subtitle: 'By Berklee',
266
- moises: true,
267
- icon: 'https://storage.googleapis.com/moises-api-assets/setlists/jamsession_cory/cover.png',
268
- dropdownMenuOptions: [
269
- {
270
- type: 'item',
271
- key: 'edit',
272
- label: 'Edit',
273
- onClick: () => console.log('Edit clicked'),
274
- },
275
- ],
276
- },
277
- {
278
- id: 'bossa-nova-3',
279
- label: 'Bossa Nova Essentials',
280
- dropdownMenuOptions: [
281
- {
282
- type: 'item',
283
- key: 'edit',
284
- label: 'Edit',
285
- onClick: () => console.log('Edit clicked'),
286
- },
287
- ],
288
- },
289
- {
290
- id: 'band-rehearsal-3',
291
- label: 'Band Rehearsal',
292
- subtitle: 'By Nickyz dsdasdas',
293
- group: true,
294
- dropdownMenuOptions: [
295
- {
296
- type: 'item',
297
- key: 'edit',
298
- label: 'Edit',
299
- onClick: () => console.log('Edit clicked'),
300
- },
301
- ],
302
- },
303
- {
304
- id: 'gig-dec-21-3',
305
- label: 'Gig Dec 21th',
306
- icon: <MusicNoteIcon width={16} height={16} />,
307
- dropdownMenuOptions: [
308
- {
309
- type: 'item',
310
- key: 'edit',
311
- label: 'Edit',
312
- onClick: () => console.log('Edit clicked'),
313
- },
314
- ],
315
- },
316
- ],
317
- selectedSetlistId: 'piano-exercises',
137
+ setlists: createSetlists(100),
138
+ selectedSetlistId: 'setlist-0',
318
139
  onSetlistClick: () => console.log('Setlist clicked'),
319
140
  onNewSetlistClick: () => console.log('New setlist clicked'),
320
141
  collapsed: false,
@@ -121,6 +121,7 @@ export function useForm({ schema }) {
121
121
  if (typeof name === 'string') {
122
122
  setValues((prev) => ({ ...prev, [name]: value }))
123
123
  setErrors((prev) => ({ ...prev, [name]: null }))
124
+ return
124
125
  }
125
126
 
126
127
  const values = name