@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moises.ai/design-system",
3
- "version": "3.5.6",
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
@@ -14,6 +14,11 @@
14
14
  transition-delay: 100ms;
15
15
  }
16
16
 
17
+ .upgradeItem:focus-visible {
18
+ outline: 2px solid var(--neutral-alpha-8);
19
+ outline-offset: -3px;
20
+ }
21
+
17
22
  .upgradeItem:hover {
18
23
  background-color: var(--neutral-alpha-2);
19
24
  }
@@ -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 }