@moises.ai/design-system 3.5.6 → 3.6.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/dist/index.js +4261 -4030
- package/package.json +3 -2
- package/src/components/AdditionalItems/AdditionalItems.jsx +8 -0
- package/src/components/AdditionalItems/AdditionalItems.module.css +5 -0
- package/src/components/DropdownButton/DropdownButton.module.css +64 -0
- package/src/components/Extension/Extension.jsx +317 -0
- package/src/components/Extension/Extension.stories.jsx +368 -0
- package/src/components/ProductsList/ProductsList.jsx +8 -0
- package/src/components/ProductsList/ProductsList.module.css +5 -0
- package/src/components/ProfileMenu/MenuTrigger.jsx +2 -0
- package/src/components/ProfileMenu/ProfileMenu.module.css +5 -0
- package/src/components/Select/Select.jsx +5 -1
- package/src/components/Select/Select.module.css +67 -0
- package/src/components/SetlistList/SetlistList.jsx +57 -5
- package/src/components/SetlistList/SetlistList.module.css +34 -0
- package/src/components/Sidebar/Sidebar.module.css +1 -1
- package/src/components/Sidebar/SidebarSection/SidebarSection.module.css +1 -1
- package/src/components/useForm/useForm.jsx +204 -0
- package/src/components/useForm/useForm.stories.jsx +459 -0
- package/src/index.jsx +3 -0
- package/src/lib/menu/Menu.module.css +69 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moises.ai/design-system",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Design System package based on @radix-ui/themes with custom defaults",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -72,7 +72,8 @@
|
|
|
72
72
|
"react-dom": "^19.0.0",
|
|
73
73
|
"storybook": "^8.6.14",
|
|
74
74
|
"vite": "^5.1.0",
|
|
75
|
-
"vite-plugin-css-injected-by-js": "^3.5.2"
|
|
75
|
+
"vite-plugin-css-injected-by-js": "^3.5.2",
|
|
76
|
+
"zod": "^4.3.6"
|
|
76
77
|
},
|
|
77
78
|
"publishConfig": {
|
|
78
79
|
"access": "public"
|
|
@@ -19,11 +19,19 @@ export const AdditionalItems = ({
|
|
|
19
19
|
<Flex
|
|
20
20
|
key={item.id || index}
|
|
21
21
|
align="center"
|
|
22
|
+
role="button"
|
|
23
|
+
tabIndex={0}
|
|
22
24
|
className={classNames(styles.upgradeItem, {
|
|
23
25
|
[styles.upgradeItemCollapsed]: collapsed,
|
|
24
26
|
[styles.upgradeItemSelected]: isSelected,
|
|
25
27
|
})}
|
|
26
28
|
onClick={item.onClick}
|
|
29
|
+
onKeyDown={(e) => {
|
|
30
|
+
if ((e.key === 'Enter' || e.key === ' ') && item.onClick) {
|
|
31
|
+
e.preventDefault()
|
|
32
|
+
item.onClick()
|
|
33
|
+
}
|
|
34
|
+
}}
|
|
27
35
|
>
|
|
28
36
|
{item.icon && <div className={styles.upgradeIcon}>{item.icon}</div>}
|
|
29
37
|
<div
|
|
@@ -95,6 +95,70 @@
|
|
|
95
95
|
justify-content: center;
|
|
96
96
|
align-items: center;
|
|
97
97
|
gap: 8px;
|
|
98
|
+
|
|
99
|
+
animation-duration: 400ms;
|
|
100
|
+
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
|
101
|
+
will-change: transform, opacity;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.content[data-side='top'] {
|
|
105
|
+
animation-name: slideDownAndFade;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.content[data-side='right'] {
|
|
109
|
+
animation-name: slideLeftAndFade;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.content[data-side='bottom'] {
|
|
113
|
+
animation-name: slideUpAndFade;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.content[data-side='left'] {
|
|
117
|
+
animation-name: slideRightAndFade;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@keyframes slideUpAndFade {
|
|
121
|
+
from {
|
|
122
|
+
opacity: 0;
|
|
123
|
+
transform: translateY(2px);
|
|
124
|
+
}
|
|
125
|
+
to {
|
|
126
|
+
opacity: 1;
|
|
127
|
+
transform: translateY(0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@keyframes slideRightAndFade {
|
|
132
|
+
from {
|
|
133
|
+
opacity: 0;
|
|
134
|
+
transform: translateX(-2px);
|
|
135
|
+
}
|
|
136
|
+
to {
|
|
137
|
+
opacity: 1;
|
|
138
|
+
transform: translateX(0);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@keyframes slideDownAndFade {
|
|
143
|
+
from {
|
|
144
|
+
opacity: 0;
|
|
145
|
+
transform: translateY(-2px);
|
|
146
|
+
}
|
|
147
|
+
to {
|
|
148
|
+
opacity: 1;
|
|
149
|
+
transform: translateY(0);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@keyframes slideLeftAndFade {
|
|
154
|
+
from {
|
|
155
|
+
opacity: 0;
|
|
156
|
+
transform: translateX(2px);
|
|
157
|
+
}
|
|
158
|
+
to {
|
|
159
|
+
opacity: 1;
|
|
160
|
+
transform: translateX(0);
|
|
161
|
+
}
|
|
98
162
|
}
|
|
99
163
|
|
|
100
164
|
.chevron {
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { Children, createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { Flex, Heading, IconButton } from '../../index'
|
|
3
|
+
import { ArrowLeftIcon, Cross1Icon } from '../../icons'
|
|
4
|
+
|
|
5
|
+
const ExtensionContext = createContext(null)
|
|
6
|
+
const NavigationContext = createContext(null)
|
|
7
|
+
const ScreenContext = createContext(null)
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Hooks
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the `moises` object from the nearest `<Extension>` ancestor.
|
|
15
|
+
* @returns {object} The moises extension API instance.
|
|
16
|
+
*/
|
|
17
|
+
export function useExtension() {
|
|
18
|
+
return useContext(ExtensionContext)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Stack-level navigation controls.
|
|
23
|
+
* Works anywhere inside an `<Extension>` with `initialScreen`.
|
|
24
|
+
*/
|
|
25
|
+
export function useNavigation() {
|
|
26
|
+
const ctx = useContext(NavigationContext)
|
|
27
|
+
if (!ctx) throw new Error('useNavigation must be used inside an <Extension> with initialScreen')
|
|
28
|
+
return ctx
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Screen-specific context (name, params, setOptions).
|
|
33
|
+
* Only available inside a component rendered by `<Extension.Screen>`.
|
|
34
|
+
*/
|
|
35
|
+
export function useScreen() {
|
|
36
|
+
const ctx = useContext(ScreenContext)
|
|
37
|
+
if (!ctx) throw new Error('useScreen must be used inside an <Extension.Screen> component')
|
|
38
|
+
return ctx
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Sub-components
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fixed header bar pinned to the top of the extension panel.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} props
|
|
49
|
+
* @param {string} props.title - Heading text displayed in the header.
|
|
50
|
+
* @param {Function} [props.onBack] - Callback fired when the back button is clicked.
|
|
51
|
+
* When provided, an arrow-left `IconButton` is rendered on the left side.
|
|
52
|
+
* @param {React.ReactNode} [props.headerLeft] - Custom left-side content. Replaces the
|
|
53
|
+
* default back button when provided (even as `null`).
|
|
54
|
+
* @param {React.ReactNode} [props.headerRight] - Right-side action content. Takes priority
|
|
55
|
+
* over `children` when both are provided.
|
|
56
|
+
* @param {boolean} [props.allowClose=true] - Whether to show the close (x) button on the right.
|
|
57
|
+
* @param {Function} [props.onClose] - Custom close handler. Defaults to `moises.extension.close()`.
|
|
58
|
+
* @param {React.ReactNode} [props.children] - Action elements rendered between the title and the close button.
|
|
59
|
+
*/
|
|
60
|
+
function Header({ title, onBack, headerLeft, headerRight, allowClose = true, onClose, children }) {
|
|
61
|
+
const moises = useContext(ExtensionContext)
|
|
62
|
+
|
|
63
|
+
const handleClose = onClose || (() => moises?.extension?.close?.())
|
|
64
|
+
const rightContent = headerRight ?? children
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Flex
|
|
68
|
+
align="center"
|
|
69
|
+
gap="1"
|
|
70
|
+
mx="4"
|
|
71
|
+
py="3"
|
|
72
|
+
style={{ borderBottom: '1px solid var(--gray-a4)' }}
|
|
73
|
+
>
|
|
74
|
+
{headerLeft !== undefined ? (
|
|
75
|
+
headerLeft
|
|
76
|
+
) : onBack ? (
|
|
77
|
+
<IconButton variant="ghost" color="gray" size="1" onClick={onBack}>
|
|
78
|
+
<ArrowLeftIcon />
|
|
79
|
+
</IconButton>
|
|
80
|
+
) : null}
|
|
81
|
+
<Flex align="center" gap="2" width="100%" style={{ flex: 2 }}>
|
|
82
|
+
<Heading
|
|
83
|
+
size="3"
|
|
84
|
+
weight="medium"
|
|
85
|
+
style={{ width: '100%', minWidth: 0 }}
|
|
86
|
+
trim="both"
|
|
87
|
+
>
|
|
88
|
+
{title}
|
|
89
|
+
</Heading>
|
|
90
|
+
</Flex>
|
|
91
|
+
{rightContent && (
|
|
92
|
+
<Flex align="center" gap="1" justify="end" style={{ flex: 1, flexShrink: 0 }}>
|
|
93
|
+
{rightContent}
|
|
94
|
+
</Flex>
|
|
95
|
+
)}
|
|
96
|
+
{allowClose && (
|
|
97
|
+
<IconButton variant="ghost" color="gray" size="1" onClick={handleClose}>
|
|
98
|
+
<Cross1Icon />
|
|
99
|
+
</IconButton>
|
|
100
|
+
)}
|
|
101
|
+
</Flex>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Scrollable content area that fills all remaining vertical space.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} props
|
|
109
|
+
* @param {React.ReactNode} props.children - Content to render inside the scrollable area.
|
|
110
|
+
*/
|
|
111
|
+
function Content({ children }) {
|
|
112
|
+
return (
|
|
113
|
+
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
114
|
+
<Flex direction="column" p="4" gap="3">
|
|
115
|
+
{children}
|
|
116
|
+
</Flex>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fixed footer bar pinned to the bottom of the extension panel.
|
|
123
|
+
* Only renders when children are provided.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} props
|
|
126
|
+
* @param {React.ReactNode} [props.children] - Footer content (e.g. action buttons).
|
|
127
|
+
*/
|
|
128
|
+
function Footer({ children }) {
|
|
129
|
+
if (!children) return null
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Flex
|
|
133
|
+
align="center"
|
|
134
|
+
justify="center"
|
|
135
|
+
mx="4"
|
|
136
|
+
py="3"
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</Flex>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Declarative screen registration. Renders nothing — the navigator
|
|
145
|
+
* controls mounting based on the active stack.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} props
|
|
148
|
+
* @param {string} props.name - Unique screen identifier for navigation.
|
|
149
|
+
* @param {string} props.title - Default header title.
|
|
150
|
+
* @param {React.ComponentType} props.component - Component to render when active.
|
|
151
|
+
* @param {React.ReactNode} [props.headerRight] - Default right-side header content.
|
|
152
|
+
* @param {React.ReactNode} [props.headerLeft] - Override for the default back button.
|
|
153
|
+
* @param {boolean} [props.allowClose=true] - Whether to show the close (x) button.
|
|
154
|
+
* @param {Function} [props.onClose] - Custom close handler.
|
|
155
|
+
*/
|
|
156
|
+
function Screen() {
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Navigator (internal)
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
const ANIM_DURATION = 200
|
|
165
|
+
const ANIM_DISTANCE = 80
|
|
166
|
+
|
|
167
|
+
function animateTransition(el, direction) {
|
|
168
|
+
if (!el) return
|
|
169
|
+
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
170
|
+
el.getAnimations().forEach((a) => a.cancel())
|
|
171
|
+
const x = direction === 'forward' ? ANIM_DISTANCE : -ANIM_DISTANCE
|
|
172
|
+
el.animate(
|
|
173
|
+
[
|
|
174
|
+
{ opacity: 0, transform: `translateX(${x}px)` },
|
|
175
|
+
{ opacity: 1, transform: 'translateX(0)' },
|
|
176
|
+
],
|
|
177
|
+
{ duration: reduced ? 150 : ANIM_DURATION, easing: 'ease-out' },
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function Navigator({ initialScreen, animated, children }) {
|
|
182
|
+
const [stack, setStack] = useState([{ name: initialScreen, params: {} }])
|
|
183
|
+
const [screenOptions, setScreenOptions] = useState({})
|
|
184
|
+
const [gen, setGen] = useState(0)
|
|
185
|
+
const stackRef = useRef(stack)
|
|
186
|
+
const wrapperRef = useRef(null)
|
|
187
|
+
stackRef.current = stack
|
|
188
|
+
|
|
189
|
+
const screens = useMemo(() => {
|
|
190
|
+
const map = {}
|
|
191
|
+
Children.forEach(children, (child) => {
|
|
192
|
+
if (child?.type === Screen) map[child.props.name] = child.props
|
|
193
|
+
})
|
|
194
|
+
return map
|
|
195
|
+
}, [children])
|
|
196
|
+
|
|
197
|
+
const navigate = useCallback((updater, dir = null) => {
|
|
198
|
+
if (animated && dir) animateTransition(wrapperRef.current, dir)
|
|
199
|
+
setStack(updater)
|
|
200
|
+
setScreenOptions({})
|
|
201
|
+
setGen((g) => g + 1)
|
|
202
|
+
}, [animated])
|
|
203
|
+
|
|
204
|
+
const push = useCallback(
|
|
205
|
+
(name, params = {}) => navigate((prev) => [...prev, { name, params }], 'forward'),
|
|
206
|
+
[navigate],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
const pop = useCallback(
|
|
210
|
+
() => navigate((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev), 'back'),
|
|
211
|
+
[navigate],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const popTo = useCallback(
|
|
215
|
+
(name) =>
|
|
216
|
+
navigate((prev) => {
|
|
217
|
+
const idx = prev.findIndex((e) => e.name === name)
|
|
218
|
+
if (idx === -1) throw new Error(`Screen "${name}" is not in the stack`)
|
|
219
|
+
return prev.slice(0, idx + 1)
|
|
220
|
+
}, 'back'),
|
|
221
|
+
[navigate],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const reset = useCallback(
|
|
225
|
+
(name) => navigate(() => [{ name: name || initialScreen, params: {} }]),
|
|
226
|
+
[navigate, initialScreen],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
const canGoBack = stack.length > 1
|
|
230
|
+
|
|
231
|
+
const navigation = useMemo(
|
|
232
|
+
() => ({ push, pop, popTo, reset, canGoBack, stack }),
|
|
233
|
+
[push, pop, popTo, reset, canGoBack, stack],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
const current = stack[stack.length - 1]
|
|
237
|
+
const def = screens[current.name]
|
|
238
|
+
if (!def) return null
|
|
239
|
+
|
|
240
|
+
const opt = (key, fallback) => (key in screenOptions ? screenOptions[key] : (def[key] ?? fallback))
|
|
241
|
+
const Component = def.component
|
|
242
|
+
|
|
243
|
+
const handleSetOptions = useCallback(
|
|
244
|
+
(opts) => {
|
|
245
|
+
const top = stackRef.current[stackRef.current.length - 1]
|
|
246
|
+
if (top.name === current.name) setScreenOptions((prev) => ({ ...prev, ...opts }))
|
|
247
|
+
},
|
|
248
|
+
[current.name],
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const screenCtx = useMemo(
|
|
252
|
+
() => ({ name: current.name, params: current.params || {}, setOptions: handleSetOptions }),
|
|
253
|
+
[current.name, current.params, handleSetOptions],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<NavigationContext.Provider value={navigation}>
|
|
258
|
+
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
|
|
259
|
+
<Header
|
|
260
|
+
title={opt('title')}
|
|
261
|
+
onBack={canGoBack ? pop : undefined}
|
|
262
|
+
headerLeft={opt('headerLeft')}
|
|
263
|
+
headerRight={opt('headerRight')}
|
|
264
|
+
allowClose={opt('allowClose', true)}
|
|
265
|
+
onClose={opt('onClose')}
|
|
266
|
+
/>
|
|
267
|
+
<div
|
|
268
|
+
ref={wrapperRef}
|
|
269
|
+
style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, overflow: 'hidden' }}
|
|
270
|
+
>
|
|
271
|
+
<ScreenContext.Provider value={screenCtx} key={gen}>
|
|
272
|
+
<Component />
|
|
273
|
+
</ScreenContext.Provider>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</NavigationContext.Provider>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Extension
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Root layout component for extension panels.
|
|
286
|
+
* Provides the `moises` API via context and renders a full-height flex column.
|
|
287
|
+
*
|
|
288
|
+
* When `initialScreen` is provided, enters navigator mode: scans children for
|
|
289
|
+
* `<Extension.Screen>` elements and manages a navigation stack automatically.
|
|
290
|
+
* Otherwise, passes children through as-is (simple mode).
|
|
291
|
+
*
|
|
292
|
+
* @param {object} props
|
|
293
|
+
* @param {object} props.moises - The moises extension API instance.
|
|
294
|
+
* @param {string} [props.initialScreen] - Activates navigator mode with this screen.
|
|
295
|
+
* @param {boolean} [props.animated] - Enable slide+fade transitions (navigator mode only).
|
|
296
|
+
* @param {React.ReactNode} props.children - Extension sub-components or Screen elements.
|
|
297
|
+
*/
|
|
298
|
+
function Extension({ moises, initialScreen, animated, children }) {
|
|
299
|
+
return (
|
|
300
|
+
<ExtensionContext.Provider value={moises}>
|
|
301
|
+
<Flex direction="column" style={{ height: '100%' }}>
|
|
302
|
+
{initialScreen ? (
|
|
303
|
+
<Navigator initialScreen={initialScreen} animated={animated}>
|
|
304
|
+
{children}
|
|
305
|
+
</Navigator>
|
|
306
|
+
) : children}
|
|
307
|
+
</Flex>
|
|
308
|
+
</ExtensionContext.Provider>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
Extension.Header = Header
|
|
313
|
+
Extension.Content = Content
|
|
314
|
+
Extension.Footer = Footer
|
|
315
|
+
Extension.Screen = Screen
|
|
316
|
+
|
|
317
|
+
export { Extension }
|