@falcondev-oss/nuxt-layers-base 0.21.0 → 0.23.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/.playground/app/pages/index.vue +156 -150
- package/.playground/app.vue +15 -0
- package/.playground/nuxt.config.ts +10 -0
- package/app/components/u/UField.vue +82 -25
- package/package.json +8 -8
|
@@ -7,12 +7,14 @@ const overlay = useOverlay()
|
|
|
7
7
|
|
|
8
8
|
const form = useForm({
|
|
9
9
|
schema: z.object({
|
|
10
|
-
duration: z.number(),
|
|
11
|
-
dateIso: z.string(),
|
|
10
|
+
duration: z.number().meta({ title: 'Duration' }),
|
|
11
|
+
dateIso: z.string().meta({ title: 'Datum' }),
|
|
12
|
+
text: z.string().max(10).meta({ title: 'Text' }),
|
|
12
13
|
}),
|
|
13
14
|
sourceValues: () => ({
|
|
14
15
|
dateIso: null,
|
|
15
16
|
duration: null,
|
|
17
|
+
text: '',
|
|
16
18
|
}),
|
|
17
19
|
async submit({ values }) {
|
|
18
20
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
@@ -67,86 +69,85 @@ const columns = useTableColumns<typeof data>(
|
|
|
67
69
|
</script>
|
|
68
70
|
|
|
69
71
|
<template>
|
|
70
|
-
<
|
|
71
|
-
:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
userMenu: {
|
|
137
|
-
name: 'Benjamin Canac',
|
|
138
|
-
avatarSrc: 'https://github.com/benjamincanac.png',
|
|
72
|
+
<LayoutSidebar
|
|
73
|
+
:items="[
|
|
74
|
+
{
|
|
75
|
+
label: 'Home',
|
|
76
|
+
icon: 'i-lucide-house',
|
|
77
|
+
active: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: 'Inbox',
|
|
81
|
+
icon: 'i-lucide-inbox',
|
|
82
|
+
badge: '4',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label: 'Contacts',
|
|
86
|
+
icon: 'i-lucide-users',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'Settings',
|
|
90
|
+
icon: 'i-lucide-settings',
|
|
91
|
+
defaultOpen: true,
|
|
92
|
+
children: [
|
|
93
|
+
{
|
|
94
|
+
label: 'General',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: 'Members',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: 'Notifications',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
]"
|
|
105
|
+
:bottom-items="[
|
|
106
|
+
{
|
|
107
|
+
label: 'Home',
|
|
108
|
+
icon: 'i-lucide-house',
|
|
109
|
+
active: true,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
label: 'Inbox',
|
|
113
|
+
icon: 'i-lucide-inbox',
|
|
114
|
+
badge: '4',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: 'Contacts',
|
|
118
|
+
icon: 'i-lucide-users',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
label: 'Settings',
|
|
122
|
+
icon: 'i-lucide-settings',
|
|
123
|
+
defaultOpen: true,
|
|
124
|
+
children: [
|
|
125
|
+
{
|
|
126
|
+
label: 'General',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: 'Members',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
label: 'Notifications',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
139
135
|
},
|
|
136
|
+
]"
|
|
137
|
+
:user-menu="{
|
|
138
|
+
name: 'Benjamin Canac',
|
|
139
|
+
avatarSrc: 'https://github.com/benjamincanac.png',
|
|
140
140
|
}"
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
>
|
|
142
|
+
<LayoutNavbar
|
|
143
|
+
:navbar="{
|
|
143
144
|
title: 'Dashboard',
|
|
144
145
|
ui: {
|
|
145
146
|
root: 'relative',
|
|
146
147
|
title: 'flex-1 absolute inset-0 w-full',
|
|
147
148
|
},
|
|
148
|
-
}
|
|
149
|
-
toolbar
|
|
149
|
+
}"
|
|
150
|
+
:toolbar="{
|
|
150
151
|
items: [
|
|
151
152
|
{
|
|
152
153
|
label: 'General',
|
|
@@ -177,85 +178,90 @@ const columns = useTableColumns<typeof data>(
|
|
|
177
178
|
icon: 'i-lucide-bell',
|
|
178
179
|
},
|
|
179
180
|
],
|
|
180
|
-
},
|
|
181
|
-
}"
|
|
182
|
-
>
|
|
183
|
-
<template #navbar-title>
|
|
184
|
-
<div class="w-full text-center">title</div>
|
|
185
|
-
</template>
|
|
186
|
-
|
|
187
|
-
<UTableCard>
|
|
188
|
-
<UTable :data :columns @select="() => {}" />
|
|
189
|
-
</UTableCard>
|
|
190
|
-
<UCard
|
|
191
|
-
:ui="{
|
|
192
|
-
body: 'flex flex-col gap-4 items-start',
|
|
193
|
-
}"
|
|
194
|
-
>
|
|
195
|
-
<UButton
|
|
196
|
-
label="Confirm"
|
|
197
|
-
variant="subtle"
|
|
198
|
-
@click="
|
|
199
|
-
() => {
|
|
200
|
-
confirm.confirmDestructive({
|
|
201
|
-
title: 'Are you sure?',
|
|
202
|
-
description: 'This action cannot be undone.',
|
|
203
|
-
submitLabel: 'Yes, delete it',
|
|
204
|
-
})
|
|
205
|
-
}
|
|
206
|
-
"
|
|
207
|
-
/>
|
|
208
|
-
<UButton
|
|
209
|
-
label="Actions"
|
|
210
|
-
variant="subtle"
|
|
211
|
-
@click="
|
|
212
|
-
() => {
|
|
213
|
-
overlay.create(LazyOverlayModalActions, {
|
|
214
|
-
defaultOpen: true,
|
|
215
|
-
props: {
|
|
216
|
-
title: 'Actions',
|
|
217
|
-
description: 'Choose an action to perform',
|
|
218
|
-
actions: [
|
|
219
|
-
{
|
|
220
|
-
label: 'Action 1',
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
label: 'Action 2',
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
},
|
|
227
|
-
})
|
|
228
|
-
}
|
|
229
|
-
"
|
|
230
|
-
/>
|
|
231
|
-
</UCard>
|
|
232
|
-
<UCard
|
|
233
|
-
:ui="{
|
|
234
|
-
body: 'flex flex-col gap-4 items-start ',
|
|
235
181
|
}"
|
|
236
182
|
>
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
183
|
+
<template #navbar-title>
|
|
184
|
+
<div class="w-full text-center">title</div>
|
|
185
|
+
</template>
|
|
186
|
+
|
|
187
|
+
<UTableCard>
|
|
188
|
+
<UTable :data :columns @select="() => {}" />
|
|
189
|
+
</UTableCard>
|
|
190
|
+
<UCard
|
|
191
|
+
:ui="{
|
|
192
|
+
body: 'flex flex-col gap-4 items-start',
|
|
242
193
|
}"
|
|
243
194
|
>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
195
|
+
<UButton
|
|
196
|
+
label="Confirm"
|
|
197
|
+
variant="subtle"
|
|
198
|
+
@click="
|
|
199
|
+
() => {
|
|
200
|
+
confirm.confirmDestructive({
|
|
201
|
+
title: 'Are you sure?',
|
|
202
|
+
description: 'This action cannot be undone.',
|
|
203
|
+
submitLabel: 'Yes, delete it',
|
|
204
|
+
})
|
|
205
|
+
}
|
|
251
206
|
"
|
|
207
|
+
/>
|
|
208
|
+
<UButton
|
|
209
|
+
label="Actions"
|
|
210
|
+
variant="subtle"
|
|
211
|
+
@click="
|
|
212
|
+
() => {
|
|
213
|
+
overlay.create(LazyOverlayModalActions, {
|
|
214
|
+
defaultOpen: true,
|
|
215
|
+
props: {
|
|
216
|
+
title: 'Actions',
|
|
217
|
+
description: 'Choose an action to perform',
|
|
218
|
+
actions: [
|
|
219
|
+
{
|
|
220
|
+
label: 'Action 1',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
label: 'Action 2',
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
"
|
|
230
|
+
/>
|
|
231
|
+
</UCard>
|
|
232
|
+
<UCard
|
|
233
|
+
class="max-w-sm"
|
|
234
|
+
:ui="{
|
|
235
|
+
body: 'flex flex-col gap-4 items-start ',
|
|
236
|
+
}"
|
|
237
|
+
>
|
|
238
|
+
<UForm
|
|
239
|
+
:form
|
|
240
|
+
:success-toast="{
|
|
241
|
+
title: 'test',
|
|
242
|
+
description: 'wow',
|
|
243
|
+
}"
|
|
244
|
+
class="flex flex-col gap-4"
|
|
252
245
|
>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
246
|
+
{{ form.data }}
|
|
247
|
+
<UField v-slot="{ props }" :field="form.fields.text.$use()">
|
|
248
|
+
<UInput class="w-full" v-bind="props" />
|
|
249
|
+
</UField>
|
|
250
|
+
<UField
|
|
251
|
+
v-slot="{ props }"
|
|
252
|
+
:field="
|
|
253
|
+
form.fields.dateIso.$use({
|
|
254
|
+
translate: dateValueIsoTranslator(),
|
|
255
|
+
})
|
|
256
|
+
"
|
|
257
|
+
>
|
|
258
|
+
<UInputDatePicker class="w-full" v-bind="props" />
|
|
259
|
+
</UField>
|
|
260
|
+
<UField v-slot="{ props }" :field="form.fields.duration.$use()">
|
|
261
|
+
<UInputDurationMinutes class="w-full" v-bind="props" />
|
|
262
|
+
</UField>
|
|
263
|
+
</UForm>
|
|
264
|
+
</UCard>
|
|
265
|
+
</LayoutNavbar>
|
|
266
|
+
</LayoutSidebar>
|
|
261
267
|
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { de } from '@nuxt/ui/locale'
|
|
3
|
+
import { Settings } from 'luxon'
|
|
4
|
+
|
|
5
|
+
Settings.throwOnInvalid = true
|
|
6
|
+
Settings.defaultLocale = 'de'
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<UCustomApp
|
|
11
|
+
:app="{
|
|
12
|
+
locale: de,
|
|
13
|
+
}"
|
|
14
|
+
/>
|
|
15
|
+
</template>
|
|
@@ -6,5 +6,15 @@ export default defineNuxtConfig({
|
|
|
6
6
|
projectId: 'my-project',
|
|
7
7
|
},
|
|
8
8
|
},
|
|
9
|
+
typescript: {
|
|
10
|
+
tsConfig: {
|
|
11
|
+
vueCompilerOptions: {
|
|
12
|
+
strictTemplates: true,
|
|
13
|
+
strictVModel: false,
|
|
14
|
+
htmlAttributes: ['aria-*'],
|
|
15
|
+
dataAttributes: ['data-*'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
9
19
|
css: ['~/assets/test.css'],
|
|
10
20
|
})
|
|
@@ -1,66 +1,123 @@
|
|
|
1
|
-
<script setup lang="ts" generic="T">
|
|
1
|
+
<script setup lang="ts" generic="T, const Nullable extends boolean = false">
|
|
2
2
|
import type { FormField } from '@falcondev-oss/form-core'
|
|
3
3
|
import type { FormFieldProps, FormFieldSlots } from '@nuxt/ui'
|
|
4
|
-
import type { ModelModifiers } from '@nuxt/ui/runtime/types/input.js'
|
|
5
4
|
import { useForwardProps } from 'reka-ui'
|
|
6
|
-
import
|
|
5
|
+
import * as R from 'remeda'
|
|
7
6
|
|
|
8
|
-
type
|
|
7
|
+
type InputSlotProps<T, Nullable extends boolean> = {
|
|
9
8
|
'modelValue': T
|
|
10
9
|
'onUpdate:modelValue': (value: T) => void
|
|
11
10
|
'onBlur': () => void
|
|
12
11
|
'disabled': boolean
|
|
13
12
|
'loading': boolean
|
|
14
|
-
'modelModifiers'
|
|
13
|
+
'modelModifiers': true extends Nullable ? { nullable: true } : never
|
|
14
|
+
'placeholder'?: string
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const props = defineProps<
|
|
18
18
|
FormFieldProps & {
|
|
19
19
|
field: FormField<T>
|
|
20
|
-
}
|
|
20
|
+
} & { nullable?: Nullable }
|
|
21
21
|
>()
|
|
22
22
|
const slots = defineSlots<
|
|
23
23
|
{
|
|
24
|
-
default: (props:
|
|
24
|
+
default: (slot: { props: InputSlotProps<T, Nullable>; field: FormField<T> }) => any
|
|
25
25
|
} & Omit<FormFieldSlots, 'default'>
|
|
26
26
|
>()
|
|
27
27
|
|
|
28
28
|
const forwardedProps = useForwardProps(props)
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const isOverMaxLength = computed(() => {
|
|
31
|
+
const field = forwardedProps.value.field
|
|
32
|
+
|
|
33
|
+
return field.schema.maxLength === undefined || field.value == null
|
|
34
|
+
? false
|
|
35
|
+
: (field.value as string | number)?.toString().length > field.schema.maxLength
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const formFieldProps = computed<FormFieldProps>(() => {
|
|
31
39
|
const { field, ...rest } = forwardedProps.value
|
|
32
|
-
|
|
40
|
+
|
|
41
|
+
const hint =
|
|
42
|
+
field.schema.maxLength === undefined
|
|
43
|
+
? undefined
|
|
44
|
+
: `${(field.value as string | number | null)?.toString().length ?? 0}/${field.schema.maxLength}`
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
required: field.schema.required,
|
|
48
|
+
label: field.schema.title,
|
|
49
|
+
description: field.schema.description,
|
|
50
|
+
hint,
|
|
51
|
+
...R.omitBy(rest, (v) => v === undefined),
|
|
52
|
+
}
|
|
33
53
|
})
|
|
34
54
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
const inputProps = computed(() => {
|
|
56
|
+
const field = forwardedProps.value.field
|
|
57
|
+
|
|
58
|
+
const placeholder = field.errors && field.errors.join('\n')
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
'modelValue': field.value,
|
|
62
|
+
'onUpdate:modelValue': (value) => field.handleChange(value),
|
|
63
|
+
'onBlur': () => field.handleBlur(),
|
|
64
|
+
'disabled': field.disabled,
|
|
65
|
+
'loading': field.isPending,
|
|
66
|
+
'modelModifiers': (props.nullable === true
|
|
67
|
+
? { nullable: true }
|
|
68
|
+
: undefined) as true extends Nullable ? { nullable: true } : never,
|
|
69
|
+
placeholder,
|
|
70
|
+
} satisfies InputSlotProps<T, Nullable>
|
|
71
|
+
})
|
|
48
72
|
</script>
|
|
49
73
|
|
|
50
74
|
<template>
|
|
51
75
|
<UFormField
|
|
52
76
|
v-bind="formFieldProps"
|
|
53
|
-
:
|
|
77
|
+
:ui="{
|
|
78
|
+
hint: isOverMaxLength ? 'text-error' : '',
|
|
79
|
+
}"
|
|
80
|
+
:error="!!field.errors"
|
|
54
81
|
>
|
|
55
|
-
<
|
|
82
|
+
<template #hint="{ hint }">
|
|
83
|
+
<span class="flex items-center gap-1.5">
|
|
84
|
+
<UPopover
|
|
85
|
+
v-if="!!field.errors"
|
|
86
|
+
mode="hover"
|
|
87
|
+
:delay-duration="0"
|
|
88
|
+
:ui="{
|
|
89
|
+
content: 'bg-error-50 ring-error-200! rounded py-1 px-2',
|
|
90
|
+
}"
|
|
91
|
+
>
|
|
92
|
+
<UIcon name="lucide:circle-alert" class="text-error" />
|
|
93
|
+
<template #content>
|
|
94
|
+
<p class="text-(--ui-color-neutral-800) max-w-sm whitespace-normal text-xs">
|
|
95
|
+
{{ field.errors.join('\n') }}
|
|
96
|
+
</p>
|
|
97
|
+
</template>
|
|
98
|
+
</UPopover>
|
|
99
|
+
|
|
100
|
+
{{ hint }}
|
|
101
|
+
</span>
|
|
102
|
+
</template>
|
|
103
|
+
|
|
104
|
+
<slot v-bind="{ props: inputProps, field: forwardedProps.field }">
|
|
56
105
|
<DevOnly>
|
|
57
106
|
<p class="font-black text-red-500">UField missing slot</p>
|
|
58
107
|
</DevOnly>
|
|
59
108
|
</slot>
|
|
60
109
|
|
|
61
|
-
<template v-for="(_, name) in omit(slots, ['default'])" #[name]="slotData">
|
|
110
|
+
<template v-for="(_, name) in R.omit(slots, ['default'])" #[name]="slotData">
|
|
62
111
|
<!-- @vue-ignore -->
|
|
63
112
|
<slot :name="name" v-bind="slotData || {}" />
|
|
64
113
|
</template>
|
|
65
114
|
</UFormField>
|
|
66
115
|
</template>
|
|
116
|
+
|
|
117
|
+
<style scoped>
|
|
118
|
+
:deep([aria-invalid='true']) {
|
|
119
|
+
&::placeholder {
|
|
120
|
+
color: var(--ui-color-error-400);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</style>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@falcondev-oss/nuxt-layers-base",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.23.0",
|
|
5
5
|
"description": "Nuxt layer with lots of useful helpers and @nuxt/ui components",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:falcondev-oss/nuxt-layers",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"pnpm": "10"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@falcondev-oss/form-core": "^0.
|
|
18
|
-
"@falcondev-oss/form-vue": "^0.
|
|
17
|
+
"@falcondev-oss/form-core": "^0.21.1",
|
|
18
|
+
"@falcondev-oss/form-vue": "^0.21.1",
|
|
19
19
|
"@falcondev-oss/trpc-typed-form-data": "^0.4.1",
|
|
20
20
|
"@falcondev-oss/trpc-vue-query": "^0.5.2",
|
|
21
|
-
"@iconify-json/lucide": "^1.2.
|
|
21
|
+
"@iconify-json/lucide": "^1.2.94",
|
|
22
22
|
"@internationalized/date": "^3.11.0",
|
|
23
23
|
"@nuxt/icon": "^2.2.1",
|
|
24
24
|
"@nuxt/ui": "4.4.0",
|
|
@@ -34,19 +34,19 @@
|
|
|
34
34
|
"consola": "^3.4.2",
|
|
35
35
|
"defu": "^6.1.4",
|
|
36
36
|
"maska": "^3.2.0",
|
|
37
|
-
"reka-ui": "^2.8.
|
|
37
|
+
"reka-ui": "^2.8.2",
|
|
38
38
|
"remeda": "^2.33.6",
|
|
39
39
|
"superjson": "^2.2.6",
|
|
40
|
-
"tailwindcss": "^4.1
|
|
40
|
+
"tailwindcss": "^4.2.1",
|
|
41
41
|
"trpc-nuxt": "^2.0.1",
|
|
42
42
|
"type-fest": "^5.4.4",
|
|
43
|
-
"vue": "^3.5.
|
|
43
|
+
"vue": "^3.5.29",
|
|
44
44
|
"vue-router": "^4.6.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"nuxt": "^4.3.1",
|
|
48
48
|
"typescript": "^5.9.3",
|
|
49
|
-
"vue-tsc": "^3.2.
|
|
49
|
+
"vue-tsc": "^3.2.5",
|
|
50
50
|
"zod": "^4.3.6"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|