chore: initial poc

This commit is contained in:
zernonia 2024-04-16 23:51:40 +08:00
parent fbe14a20c1
commit 0d868693e0
18 changed files with 842 additions and 1 deletions

View File

@ -8,7 +8,7 @@ export interface NavItem {
}
export type SidebarNavItem = NavItem & {
items: SidebarNavItem[]
items?: SidebarNavItem[]
}
export type NavItemWithChildren = NavItem & {
@ -134,6 +134,16 @@ export const docsConfig: DocsConfig = {
},
],
},
{
title: 'Auto Form',
items: [
{
title: 'Test',
href: '/docs/components/auto-form',
items: [],
},
],
},
{
title: 'Components',
items: [

View File

@ -38,6 +38,13 @@ export const Index = {
component: () => import("../src/lib/registry/default/example/AspectRatioDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/AspectRatioDemo.vue"],
},
"AutoForm": {
name: "AutoForm",
type: "components:example",
registryDependencies: ["button","form","input","toast"],
component: () => import("../src/lib/registry/default/example/AutoForm.vue").then((m) => m.default),
files: ["../src/lib/registry/default/example/AutoForm.vue"],
},
"AvatarDemo": {
name: "AvatarDemo",
type: "components:example",
@ -1278,6 +1285,13 @@ export const Index = {
component: () => import("../src/lib/registry/new-york/example/AspectRatioDemo.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/AspectRatioDemo.vue"],
},
"AutoForm": {
name: "AutoForm",
type: "components:example",
registryDependencies: ["button","form","input","toast"],
component: () => import("../src/lib/registry/new-york/example/AutoForm.vue").then((m) => m.default),
files: ["../src/lib/registry/new-york/example/AutoForm.vue"],
},
"AvatarDemo": {
name: "AvatarDemo",
type: "components:example",

View File

@ -0,0 +1,7 @@
---
title: Auto Form
description: Building forms with VeeValidate and Zod.
primitive: https://vee-validate.logaretm.com/v4/guide/overview/
---
<ComponentPreview name="AutoForm" />

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { h } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/lib/registry/default/ui/button'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/lib/registry/default/ui/form'
import { Input } from '@/lib/registry/default/ui/input'
import { toast } from '@/lib/registry/default/ui/toast'
const formSchema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
}))
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
toast({
title: 'You submitted the following values:',
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
})
})
</script>
<template>
<form class="w-2/3 space-y-6" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="username">
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input type="text" placeholder="shadcn" v-bind="componentField" />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit">
Submit
</Button>
</form>
</template>

View File

@ -0,0 +1,178 @@
<script setup lang="ts">
import { h } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import AutoFormField from '../ui/auto-form/AutoFormField.vue'
import { Button } from '@/lib/registry/new-york/ui/button'
import { toast } from '@/lib/registry/new-york/ui/toast'
import { AutoForm, getBaseSchema, getBaseType } from '@/lib/registry/new-york/ui/auto-form'
enum Sports {
Football = 'Football/Soccer',
Basketball = 'Basketball',
Baseball = 'Baseball',
Hockey = 'Hockey (Ice)',
None = 'I don\'t like sports',
}
const schema = z.object({
username: z
.string({
required_error: 'Username is required.',
})
.min(2, {
message: 'Username must be at least 2 characters.',
}),
password: z
.string({
required_error: 'Password is required.',
})
.describe('Your secure password')
.min(8, {
message: 'Password must be at least 8 characters.',
}),
favouriteNumber: z.coerce
.number({
invalid_type_error: 'Favourite number must be a number.',
})
.min(1, {
message: 'Favourite number must be at least 1.',
})
.max(10, {
message: 'Favourite number must be at most 10.',
})
.default(1)
.optional(),
acceptTerms: z
.boolean()
.describe('Accept terms and conditions.')
.refine(value => value, {
message: 'You must accept the terms and conditions.',
path: ['acceptTerms'],
}),
sendMeMails: z.boolean().optional(),
birthday: z.coerce.date().optional(),
color: z.enum(['red', 'green', 'blue']).optional(),
// Another enum example
marshmallows: z
.enum(['not many', 'a few', 'a lot', 'too many'])
.describe('How many marshmallows fit in your mouth?'),
// Native enum example
sports: z.nativeEnum(Sports).describe('What is your favourite sport?'),
bio: z
.string()
.min(10, {
message: 'Bio must be at least 10 characters.',
})
.max(160, {
message: 'Bio must not be longer than 30 characters.',
})
.optional(),
customParent: z.string().optional(),
file: z.string().optional().describe('Text file'),
subObject: z.object({
subField: z.string().optional().default('Sub Field'),
numberField: z.number().optional().default(1),
subSubObject: z
.object({
subSubField: z.string().default('Sub Sub Field'),
})
.describe('Sub Sub Object Description'),
}),
optionalSubObject: z
.object({
optionalSubField: z.string(),
otherOptionalSubField: z.string(),
})
.optional(),
})
const formSchema = toTypedSchema(schema)
const { handleSubmit } = useForm({
validationSchema: formSchema,
})
const onSubmit = handleSubmit((values) => {
toast({
title: 'You submitted the following values:',
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
})
})
</script>
<template>
<AutoForm
class="w-2/3 space-y-6"
:schema="schema"
:field-config="{
username: {
description: 'Test description',
inputProps: {
id: '123',
},
},
password: {
inputProps: {
id: '345',
type: 'password',
},
},
acceptTerms: {
description: 'this is custom',
},
sendMeMails: {
component: 'switch',
},
color: {
enumProps: {
options: ['red', 'green', 'blue'],
placeholder: 'Choose a color',
},
},
marshmallows: {
component: 'radio',
},
bio: {
component: 'textarea',
},
subObject: {
},
}"
@submit="onSubmit"
>
<template #username="componentField">
<div class="w-1/3">
<AutoFormField v-bind="{ ...componentField, name: 'username' }" />
</div>
</template>
<template #password="componentField">
<div class="w-1/3">
<AutoFormField v-bind="{ ...componentField, name: 'password' }" />
</div>
</template>
<Button type="submit">
Submit
</Button>
</AutoForm>
</template>

View File

@ -0,0 +1,63 @@
<script setup lang="ts" generic="T extends ZodRawShape">
import { computed } from 'vue'
import type { ZodAny, ZodObject, ZodRawShape } from 'zod'
import { getBaseType, getDefaultValueInZodStack } from './utils'
import type { Config } from './interface'
import AutoFormField from './AutoFormField.vue'
const props = defineProps<{
schema: ZodObject<T>
fieldConfig?: {
[key in keyof T]?: Config
}
}>()
const shapes = computed(() => {
// @ts-expect-error ignore {} not assignable to object
const val: {
[key in keyof T]: {
type: string
default: any
required?: boolean
options?: string[]
schema?: ZodAny
}
} = {}
const shape = props.schema.shape
Object.keys(shape).forEach((name) => {
const item = shape[name] as ZodAny
let options = 'values' in item._def ? item._def.values as string[] : undefined
if (!Array.isArray(options) && typeof options === 'object')
options = Object.values(options)
val[name as keyof T] = {
type: getBaseType(item),
default: getDefaultValueInZodStack(item),
options,
required: !['ZodOptional', 'ZodNullable'].includes(item._def.typeName),
schema: item,
}
})
return val
})
</script>
<template>
<form>
<template v-for="(shape, key) of shapes" :key="key">
<slot
:shape="shape"
:name="key.toString()"
:config="fieldConfig?.[key as keyof typeof fieldConfig] as Config"
>
<AutoFormField
:config="fieldConfig?.[key as keyof typeof fieldConfig] as Config"
:name="key.toString()"
:shape="shape"
/>
</slot>
</template>
<slot :shapes="shapes" />
</form>
</template>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import type { ZodAny } from 'zod'
import type { Config } from './interface'
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from './constant'
defineProps<{
name: string
shape: {
type: string
required?: boolean
options?: any[]
schema?: ZodAny
}
config?: Config
}>()
</script>
<template>
<component
:is="INPUT_COMPONENTS[DEFAULT_ZOD_HANDLERS[shape.type]] "
:name="name"
:required="shape.required"
:options="shape.options"
:config="config"
>
<slot />
</component>
<!-- <FieldInput
v-if="shape.type === ZodFirstPartyTypeKind.ZodString"
:name="name"
:required="shape.required"
:config="config"
/>
<FieldInput
v-else-if="shape.type === ZodFirstPartyTypeKind.ZodNumber"
:name="name"
:required="shape.required"
type="number"
:config="config"
/>
<FieldBoolean
v-else-if="shape.type === ZodFirstPartyTypeKind.ZodBoolean"
:name="name"
:required="shape.required"
:config="config"
/>
<FieldDate
v-else-if="shape.type === ZodFirstPartyTypeKind.ZodDate"
:name="name"
:required="shape.required"
:config="config"
/>
<FieldEnum
v-else-if="shape.type === ZodFirstPartyTypeKind.ZodEnum"
:name="name"
:required="shape.required"
:config="config"
:options="shape.options"
/> -->
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { FormLabel } from '@/lib/registry/new-york/ui/form'
defineProps<{
required?: boolean
}>()
</script>
<template>
<FormLabel>
<slot />
<span v-if="required" class="text-destructive"> *</span>
</FormLabel>
</template>

View File

@ -0,0 +1,6 @@
<script setup lang="ts">
</script>
<template>
obkect
</template>

View File

@ -0,0 +1,31 @@
import FieldNumber from './fields/Number.vue'
import FieldInput from './fields/Input.vue'
import FieldBoolean from './fields/Boolean.vue'
import FieldDate from './fields/Date.vue'
import FieldEnum from './fields/Enum.vue'
export const INPUT_COMPONENTS = {
date: FieldDate,
select: FieldEnum,
radio: FieldEnum,
checkbox: FieldBoolean,
switch: FieldBoolean,
textarea: FieldInput,
number: FieldNumber,
string: FieldInput,
}
/**
* Define handlers for specific Zod types.
* You can expand this object to support more types.
*/
export const DEFAULT_ZOD_HANDLERS: {
[key: string]: keyof typeof INPUT_COMPONENTS
} = {
ZodString: 'string',
ZodBoolean: 'checkbox',
ZodDate: 'date',
ZodEnum: 'select',
ZodNativeEnum: 'select',
ZodNumber: 'number',
}

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import { beautifyObjectName } from '../utils'
import type { Config } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Switch } from '@/lib/registry/new-york/ui/switch'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
defineProps<{
name: string
required?: boolean
config?: Config
}>()
</script>
<template>
<FormField v-slot="{ componentField }" :name="name">
<FormItem>
<div class="space-y-0 mb-3 flex items-center gap-3">
<FormControl>
<Switch v-if="config?.component === 'switch'" v-bind="{ ...componentField }" />
<Checkbox v-else v-bind="{ ...componentField }" />
</FormControl>
<AutoFormLabel :required="required">
{{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel>
</div>
<FormDescription v-if="config?.description">
{{ config.description }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</template>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { CalendarIcon } from '@radix-icons/vue'
import { beautifyObjectName } from '../utils'
import type { Config } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Calendar } from '@/lib/registry/new-york/ui/calendar'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/registry/new-york/ui/popover'
import { cn } from '@/lib/utils'
defineProps<{
name: string
required?: boolean
config?: Config
}>()
const df = new DateFormatter('en-US', {
dateStyle: 'long',
})
</script>
<template>
<FormField v-slot="{ componentField }" :name="name">
<FormItem>
<AutoFormLabel :required="required">
{{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel>
<FormControl>
<div>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-full justify-start text-left font-normal',
!componentField.modelValue && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
{{ componentField.modelValue ? df.format(componentField.modelValue.toDate(getLocalTimeZone())) : "Pick a date" }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar initial-focus v-bind="{ ...componentField }" />
</PopoverContent>
</Popover>
</div>
</FormControl>
<FormDescription v-if="config?.description">
{{ config.description }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</template>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { computed } from 'vue'
import { beautifyObjectName } from '../utils'
import type { Config } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/lib/registry/new-york/ui/select'
import { Label } from '@/lib/registry/new-york/ui/label'
import { RadioGroup, RadioGroupItem } from '@/lib/registry/new-york/ui/radio-group'
const props = defineProps<{
name: string
required?: boolean
options?: string[]
config?: Config
}>()
const computedOptions = computed(() => props.config?.enumProps?.options || props.options)
</script>
<template>
<FormField v-slot="{ componentField }" :name="name">
<FormItem>
<AutoFormLabel :required="required">
{{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel>
<FormControl>
<RadioGroup v-if="config?.component === 'radio'" :orientation="'vertical'" v-bind="{ ...componentField }">
<div v-for="(option, index) in computedOptions" :key="option" class="mb-2 flex items-center gap-3 space-y-0">
<RadioGroupItem :id="`${option}-${index}`" :value="option" />
<Label :for="`${option}-${index}`">{{ beautifyObjectName(option) }}</Label>
</div>
</RadioGroup>
<Select v-else v-bind="{ ...componentField }">
<SelectTrigger class="w-full">
<SelectValue :placeholder="config?.enumProps?.placeholder" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="option in computedOptions" :key="option" :value="option">
{{ beautifyObjectName(option) }}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription v-if="config?.description">
{{ config.description }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</template>

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { beautifyObjectName } from '../utils'
import type { Config } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input'
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
defineOptions({
inheritAttrs: false,
})
defineProps<{
name: string
required?: boolean
config?: Config
}>()
</script>
<template>
<FormField v-slot="{ componentField }" :name="name">
<FormItem>
<AutoFormLabel :required="required">
{{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel>
<FormControl>
<Textarea v-if="config?.component === 'textarea'" type="text" v-bind="{ ...$attrs, ...componentField, ...config?.inputProps }" />
<Input v-else type="text" v-bind="{ ...$attrs, ...componentField, ...config?.inputProps }" />
</FormControl>
<FormDescription v-if="config?.description">
{{ config.description }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</template>

View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import { beautifyObjectName } from '../utils'
import type { Config } from '../interface'
import AutoFormLabel from '../AutoFormLabel.vue'
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/lib/registry/new-york/ui/form'
import { Input } from '@/lib/registry/new-york/ui/input'
defineOptions({
inheritAttrs: false,
})
defineProps<{
name: string
required?: boolean
config?: Config
}>()
</script>
<template>
<FormField v-slot="{ componentField }" :name="name">
<FormItem>
<AutoFormLabel :required="required">
{{ config?.label || beautifyObjectName(name) }}
</AutoFormLabel>
<FormControl>
<Input type="number" v-bind="{ ...$attrs, ...componentField, ...config?.inputProps }" />
</FormControl>
<FormDescription v-if="config?.description">
{{ config.description }}
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</template>

View File

@ -0,0 +1,2 @@
export { default as AutoForm } from './AutoForm.vue'
export { getObjectFormSchema, getBaseSchema, getBaseType, getDefaultValues } from './utils'

View File

@ -0,0 +1,55 @@
import type { InputHTMLAttributes, SelectHTMLAttributes } from 'vue'
import type * as z from 'zod'
import type { INPUT_COMPONENTS } from './constant'
export interface Config {
label?: string
description?: string
component?: keyof typeof INPUT_COMPONENTS
inputProps?: InputHTMLAttributes
enumProps?: SelectHTMLAttributes & { options?: any[] }
}
export type FieldConfig<SchemaType extends z.infer<z.ZodObject<any, any>>> = {
// If SchemaType.key is an object, create a nested FieldConfig, otherwise FieldConfigItem
[Key in keyof SchemaType]?: SchemaType[Key] extends object
? FieldConfig<z.infer<SchemaType[Key]>>
: FieldConfig<z.infer<SchemaType[Key]>> // TODO;
}
export enum DependencyType {
DISABLES,
REQUIRES,
HIDES,
SETS_OPTIONS,
}
interface BaseDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> {
sourceField: keyof SchemaType
type: DependencyType
targetField: keyof SchemaType
when: (sourceFieldValue: any, targetFieldValue: any) => boolean
}
export type ValueDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
BaseDependency<SchemaType> & {
type:
| DependencyType.DISABLES
| DependencyType.REQUIRES
| DependencyType.HIDES
}
export type EnumValues = readonly [string, ...string[]]
export type OptionsDependency<
SchemaType extends z.infer<z.ZodObject<any, any>>,
> = BaseDependency<SchemaType> & {
type: DependencyType.SETS_OPTIONS
// Partial array of values from sourceField that will trigger the dependency
options: EnumValues
}
export type Dependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
| ValueDependency<SchemaType>
| OptionsDependency<SchemaType>

View File

@ -0,0 +1,129 @@
import { ZodFirstPartyTypeKind, type z } from 'zod'
import type { FieldConfig } from './interface'
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
export type ZodObjectOrWrapped =
| z.ZodObject<any, any>
| z.ZodEffects<z.ZodObject<any, any>>
/**
* Beautify a camelCase string.
* e.g. "myString" -> "My String"
*/
export function beautifyObjectName(string: string) {
// if numbers only return the string
let output = string.replace(/([A-Z])/g, ' $1')
output = output.charAt(0).toUpperCase() + output.slice(1)
return output
}
/**
* Get the lowest level Zod type.
* This will unpack optionals, refinements, etc.
*/
export function getBaseSchema<
ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny,
>(schema: ChildType | z.ZodEffects<ChildType>): ChildType | null {
if (!schema)
return null
if ('innerType' in schema._def)
return getBaseSchema(schema._def.innerType as ChildType)
if ('schema' in schema._def)
return getBaseSchema(schema._def.schema as ChildType)
return schema as ChildType
}
/**
* Get the type name of the lowest level Zod type.
* This will unpack optionals, refinements, etc.
*/
export function getBaseType(schema: z.ZodAny) {
const baseSchema = getBaseSchema(schema)
return baseSchema ? baseSchema._def.typeName : ''
}
/**
* Search for a "ZodDefault" in the Zod stack and return its value.
*/
export function getDefaultValueInZodStack(schema: z.ZodAny): any {
const typedSchema = schema as unknown as z.ZodDefault<
z.ZodNumber | z.ZodString
>
if (typedSchema._def.typeName === 'ZodDefault')
return typedSchema._def.defaultValue()
if ('innerType' in typedSchema._def) {
return getDefaultValueInZodStack(
typedSchema._def.innerType as unknown as z.ZodAny,
)
}
if ('schema' in typedSchema._def) {
return getDefaultValueInZodStack(
(typedSchema._def as any).schema as z.ZodAny,
)
}
return undefined
}
/**
* Get all default values from a Zod schema.
*/
export function getDefaultValues<Schema extends z.ZodObject<any, any>>(
schema: Schema,
fieldConfig?: FieldConfig<z.infer<Schema>>,
) {
if (!schema)
return null
const { shape } = schema
type DefaultValuesType = Partial<z.infer<Schema>>
const defaultValues = {} as DefaultValuesType
if (!shape)
return defaultValues
for (const key of Object.keys(shape)) {
const item = shape[key] as z.ZodAny
// @ts-expect-error weird
if (getBaseType(item) === ZodFirstPartyTypeKind.ZodObject) {
const defaultItems = getDefaultValues(
getBaseSchema(item) as unknown as z.ZodObject<any, any>,
fieldConfig?.[key] as FieldConfig<z.infer<Schema>>,
)
if (defaultItems !== null) {
for (const defaultItemKey of Object.keys(defaultItems)) {
const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType
defaultValues[pathKey] = defaultItems[defaultItemKey]
}
}
}
else {
let defaultValue = getDefaultValueInZodStack(item)
if (
(defaultValue === null || defaultValue === '')
&& fieldConfig?.[key]?.inputProps
) {
defaultValue = (fieldConfig?.[key]?.inputProps as unknown as any)
.defaultValue
}
if (defaultValue !== undefined)
defaultValues[key as keyof DefaultValuesType] = defaultValue
}
}
return defaultValues
}
export function getObjectFormSchema(
schema: ZodObjectOrWrapped,
): z.ZodObject<any, any> {
if (schema?._def.typeName === 'ZodEffects') {
const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>
return getObjectFormSchema(typedSchema._def.schema)
}
return schema as z.ZodObject<any, any>
}