@moises.ai/design-system 3.5.7 → 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 +3956 -3766
- package/package.json +3 -2
- package/src/components/Extension/Extension.jsx +317 -0
- package/src/components/Extension/Extension.stories.jsx +368 -0
- package/src/components/SetlistList/SetlistList.jsx +23 -0
- package/src/components/SetlistList/SetlistList.module.css +8 -3
- package/src/components/useForm/useForm.jsx +204 -0
- package/src/components/useForm/useForm.stories.jsx +459 -0
- package/src/index.jsx +3 -0
|
@@ -151,7 +151,9 @@ export const SetlistList = ({
|
|
|
151
151
|
}) => {
|
|
152
152
|
const [openSetlistMenuId, setOpenSetlistMenuId] = useState(null)
|
|
153
153
|
const [isHoveredWhenCollapsed, setIsHoveredWhenCollapsed] = useState(false)
|
|
154
|
+
const [isFullyCollapsed, setIsFullyCollapsed] = useState(false)
|
|
154
155
|
const hoverTimeoutRef = useRef(null)
|
|
156
|
+
const collapseCenterTimeoutRef = useRef(null)
|
|
155
157
|
const collapsedItemHeight = 44
|
|
156
158
|
const collapsedVisibleOffset = 8
|
|
157
159
|
const maxCollapsedItems = 4
|
|
@@ -180,6 +182,26 @@ export const SetlistList = ({
|
|
|
180
182
|
}, 180)
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (showCollapsedStack) {
|
|
187
|
+
collapseCenterTimeoutRef.current = setTimeout(() => {
|
|
188
|
+
setIsFullyCollapsed(true)
|
|
189
|
+
collapseCenterTimeoutRef.current = null
|
|
190
|
+
}, 200)
|
|
191
|
+
} else {
|
|
192
|
+
setIsFullyCollapsed(false)
|
|
193
|
+
if (collapseCenterTimeoutRef.current) {
|
|
194
|
+
clearTimeout(collapseCenterTimeoutRef.current)
|
|
195
|
+
collapseCenterTimeoutRef.current = null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return () => {
|
|
199
|
+
if (collapseCenterTimeoutRef.current) {
|
|
200
|
+
clearTimeout(collapseCenterTimeoutRef.current)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}, [showCollapsedStack])
|
|
204
|
+
|
|
183
205
|
useEffect(() => {
|
|
184
206
|
return () => {
|
|
185
207
|
if (hoverTimeoutRef.current) {
|
|
@@ -260,6 +282,7 @@ export const SetlistList = ({
|
|
|
260
282
|
direction="column"
|
|
261
283
|
className={classNames(styles.setlistsContent, {
|
|
262
284
|
[styles.collapsedStack]: showCollapsedStack,
|
|
285
|
+
[styles.collapsedStackCentered]: isFullyCollapsed,
|
|
263
286
|
[styles.collapsedMask]: collapsed,
|
|
264
287
|
})}
|
|
265
288
|
>
|
|
@@ -194,22 +194,27 @@
|
|
|
194
194
|
}
|
|
195
195
|
.collapsedStack {
|
|
196
196
|
gap: 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.collapsedStackCentered {
|
|
197
200
|
align-items: center;
|
|
198
201
|
}
|
|
199
202
|
.collapsedStackItem {
|
|
200
203
|
position: relative;
|
|
201
|
-
justify-content: flex-start;
|
|
204
|
+
/* justify-content: flex-start; */
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
.collapsedMask {
|
|
205
208
|
position: relative;
|
|
206
209
|
overflow: hidden;
|
|
207
210
|
padding-top: 2px;
|
|
211
|
+
display: flex;
|
|
212
|
+
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
.collapsedStack.collapsedMask {
|
|
215
|
+
/* .collapsedStack.collapsedMask {
|
|
211
216
|
padding-top: 12px;
|
|
212
|
-
}
|
|
217
|
+
} */
|
|
213
218
|
|
|
214
219
|
.collapsedTransition {
|
|
215
220
|
transition: margin-top 360ms ease-in-out;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
+
import { Text, Flex } from '../../index'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('zod').ZodObject} ZodSchema
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {'checkbox' | 'switch'} ToggleFieldType
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} TextFieldProps
|
|
14
|
+
* @property {string} value - Current field value.
|
|
15
|
+
* @property {(e: Event | string) => void} onChange - Change handler that accepts
|
|
16
|
+
* an input event or a raw value (for Select-like components).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} ToggleFieldProps
|
|
21
|
+
* @property {boolean} checked - Current checked state.
|
|
22
|
+
* @property {(checked: boolean) => void} onCheckedChange - Callback fired when the checked state changes.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} FormResult
|
|
27
|
+
* @property {boolean} isValid - Whether the current values pass schema validation.
|
|
28
|
+
* @property {Object} data - Parsed data when valid, raw values when invalid.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} FieldProps
|
|
33
|
+
* @property {string} name - Field name matching a key in the schema.
|
|
34
|
+
* @property {React.ReactNode} children - The form control to render.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} UseFormReturn
|
|
39
|
+
* @property {((name: string, type?: undefined) => TextFieldProps) & ((name: string, type: ToggleFieldType) => ToggleFieldProps)} connectField
|
|
40
|
+
* Returns spread-ready props for a form control. Pass `'checkbox'` or `'switch'`
|
|
41
|
+
* as the second argument for toggle controls.
|
|
42
|
+
* @property {React.FC<FieldProps>} Field - Wrapper that renders its children with
|
|
43
|
+
* an inline validation error message below.
|
|
44
|
+
* @property {(name: string) => React.ReactNode | null} error - Returns the error
|
|
45
|
+
* message element for a field, or `null` if the field is valid.
|
|
46
|
+
* @property {Object} values - Current form values object (reactive).
|
|
47
|
+
* @property {(name: string, value: *) => void} set - Imperatively set a single field value.
|
|
48
|
+
* @property {(vals?: Object) => void} reset - Reset all values and errors.
|
|
49
|
+
* Pass an object to reset to specific values, or omit for schema defaults.
|
|
50
|
+
* @property {() => FormResult} getValues - Validate the form and return
|
|
51
|
+
* `{ isValid, data }`. Sets field errors on failure.
|
|
52
|
+
* @property {(cb: (values: Object) => void) => () => void} watchValues
|
|
53
|
+
* Subscribe to value changes. Returns an unsubscribe function.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/** @param {{ name: string, errorsRef: React.MutableRefObject<Object>, children: React.ReactNode }} props */
|
|
57
|
+
function FieldView({ name, errorsRef, children }) {
|
|
58
|
+
const msg = errorsRef.current[name]
|
|
59
|
+
return (
|
|
60
|
+
<Flex direction="column" gap="1">
|
|
61
|
+
{children}
|
|
62
|
+
{msg && <Text size="1" color="red">{msg}</Text>}
|
|
63
|
+
</Flex>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lightweight form hook driven by a Zod schema.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} options
|
|
71
|
+
* @param {ZodSchema} options.schema - Zod object schema used for default values and validation.
|
|
72
|
+
* @returns {UseFormReturn}
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* const schema = z.object({
|
|
76
|
+
* name: z.string().min(2, 'Too short').default(''),
|
|
77
|
+
* agree: z.boolean().refine(v => v, 'Required').default(false),
|
|
78
|
+
* })
|
|
79
|
+
*
|
|
80
|
+
* function MyForm() {
|
|
81
|
+
* const { connectField, Field, getValues } = useForm({ schema })
|
|
82
|
+
*
|
|
83
|
+
* return (
|
|
84
|
+
* <>
|
|
85
|
+
* <Field name="name">
|
|
86
|
+
* <TextField placeholder="Name" {...connectField('name')} />
|
|
87
|
+
* </Field>
|
|
88
|
+
* <Field name="agree">
|
|
89
|
+
* <Checkbox {...connectField('agree', 'checkbox')} />
|
|
90
|
+
* </Field>
|
|
91
|
+
* <Button onClick={() => {
|
|
92
|
+
* const { isValid, data } = getValues()
|
|
93
|
+
* if (isValid) console.log(data)
|
|
94
|
+
* }}>Save</Button>
|
|
95
|
+
* </>
|
|
96
|
+
* )
|
|
97
|
+
* }
|
|
98
|
+
*/
|
|
99
|
+
export function useForm({ schema }) {
|
|
100
|
+
const [values, setValues] = useState(() => schema.parse({}))
|
|
101
|
+
const [errors, setErrors] = useState({})
|
|
102
|
+
const errorsRef = useRef(errors)
|
|
103
|
+
errorsRef.current = errors
|
|
104
|
+
const listenersRef = useRef([])
|
|
105
|
+
|
|
106
|
+
const mountedRef = useRef(false)
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!mountedRef.current) {
|
|
109
|
+
mountedRef.current = true
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
for (const cb of listenersRef.current) cb(values)
|
|
113
|
+
}, [values])
|
|
114
|
+
|
|
115
|
+
/** @type {(name: string, value: *) => void} */
|
|
116
|
+
const set = useCallback((name, value) => {
|
|
117
|
+
setValues((prev) => ({ ...prev, [name]: value }))
|
|
118
|
+
setErrors((prev) => ({ ...prev, [name]: null }))
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns spread-ready props for a form control.
|
|
123
|
+
*
|
|
124
|
+
* - Default (no type): returns `{ value, onChange }` for text inputs and selects.
|
|
125
|
+
* - `'checkbox'` / `'switch'`: returns `{ checked, onCheckedChange }`.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} name - Field name matching a key in the schema.
|
|
128
|
+
* @param {ToggleFieldType} [type] - Control type hint.
|
|
129
|
+
* @returns {TextFieldProps | ToggleFieldProps}
|
|
130
|
+
*/
|
|
131
|
+
const connectField = (name, type) => {
|
|
132
|
+
const val = values[name]
|
|
133
|
+
const update = (v) => set(name, v)
|
|
134
|
+
if (type === 'checkbox' || type === 'switch') {
|
|
135
|
+
return { checked: !!val, onCheckedChange: update }
|
|
136
|
+
}
|
|
137
|
+
return { value: val ?? '', onChange: (e) => update(e?.target?.value ?? e) }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validate the current values against the schema.
|
|
142
|
+
*
|
|
143
|
+
* @returns {FormResult} `{ isValid: true, data }` on success, or
|
|
144
|
+
* `{ isValid: false, data }` on failure (with field errors set).
|
|
145
|
+
*/
|
|
146
|
+
const getValues = () => {
|
|
147
|
+
const result = schema.safeParse(values)
|
|
148
|
+
if (result.success) {
|
|
149
|
+
setErrors({})
|
|
150
|
+
return { isValid: true, data: result.data }
|
|
151
|
+
}
|
|
152
|
+
const fieldErrors = {}
|
|
153
|
+
for (const issue of result.error.issues) {
|
|
154
|
+
const name = issue.path.join('.')
|
|
155
|
+
if (!fieldErrors[name]) fieldErrors[name] = issue.message
|
|
156
|
+
}
|
|
157
|
+
setErrors(fieldErrors)
|
|
158
|
+
return { isValid: false, data: values }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Subscribe to value changes. The callback fires after every state
|
|
163
|
+
* update (skipping the initial mount).
|
|
164
|
+
*
|
|
165
|
+
* @param {(values: Object) => void} cb - Listener called with the latest values.
|
|
166
|
+
* @returns {() => void} Unsubscribe function (safe to return from `useEffect`).
|
|
167
|
+
*/
|
|
168
|
+
const watchValues = useCallback((cb) => {
|
|
169
|
+
listenersRef.current.push(cb)
|
|
170
|
+
return () => {
|
|
171
|
+
listenersRef.current = listenersRef.current.filter((l) => l !== cb)
|
|
172
|
+
}
|
|
173
|
+
}, [])
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns the error message element for a field, or `null` if valid.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} name - Field name.
|
|
179
|
+
* @returns {React.ReactNode | null}
|
|
180
|
+
*/
|
|
181
|
+
const error = (name) => {
|
|
182
|
+
const msg = errors[name]
|
|
183
|
+
if (!msg) return null
|
|
184
|
+
return <Text size="1" color="red">{msg}</Text>
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Reset all values and clear errors.
|
|
189
|
+
*
|
|
190
|
+
* @param {Object} [vals] - Optional values to reset to. Defaults to schema defaults.
|
|
191
|
+
*/
|
|
192
|
+
const reset = (vals) => {
|
|
193
|
+
setValues(vals ?? schema.parse({}))
|
|
194
|
+
setErrors({})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** @type {React.FC<FieldProps>} */
|
|
198
|
+
const Field = useCallback(
|
|
199
|
+
(props) => <FieldView {...props} errorsRef={errorsRef} />,
|
|
200
|
+
[]
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return { connectField, Field, error, values, set, reset, getValues, watchValues }
|
|
204
|
+
}
|