@falcondev-oss/nuxt-layers-base 0.40.3 → 0.41.1
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/app/app.tsx +14 -0
- package/app/assets/css/base.css +6 -0
- package/app/components/layout/LayoutAuth.tsx +22 -0
- package/app/components/layout/LayoutNavbar.tsx +193 -0
- package/app/components/layout/LayoutPage.tsx +183 -0
- package/app/components/layout/LayoutSidebar.tsx +167 -0
- package/app/components/modals/ConfirmModal.tsx +80 -0
- package/app/components/modals/EntityCrudModal.tsx +124 -0
- package/app/components/overlay/modal/ModalActions.tsx +36 -0
- package/app/components/overlay/modal/ModalConfirm.tsx +46 -0
- package/app/components/overlay/modal/ModalRadioGroup.tsx +62 -0
- package/app/components/u/UActions.tsx +30 -0
- package/app/components/u/UAuthForm.tsx +30 -0
- package/app/components/u/UCustomApp.tsx +33 -0
- package/app/components/u/UField.tsx +211 -0
- package/app/components/u/UForm.tsx +133 -0
- package/app/components/u/UImageWithFallback.tsx +47 -0
- package/app/components/u/UInputDatePicker.tsx +94 -0
- package/app/components/u/UInputDateRangePicker.tsx +143 -0
- package/app/components/u/UInputDurationMinutes.tsx +106 -0
- package/app/components/u/UInputFile.tsx +132 -0
- package/app/components/u/URibbonCard.tsx +54 -0
- package/app/components/u/URibbonSection.tsx +36 -0
- package/app/components/u/USwitchCard.tsx +64 -0
- package/app/components/u/UToggleButton.tsx +46 -0
- package/app/components/util/Define.tsx +21 -0
- package/app/components/util/SlotRef.tsx +21 -0
- package/app/components/util/Sync.tsx +37 -0
- package/app/components/util/TemplateText.tsx +31 -0
- package/app/composables/confirm.ts +1 -1
- package/app/error.tsx +24 -0
- package/app/utils/define-setup-component.ts +39 -5
- package/nuxt.config.ts +8 -0
- package/package.json +4 -3
- package/app/app.vue +0 -3
- package/app/components/layout/LayoutAuth.vue +0 -13
- package/app/components/layout/LayoutNavbar.vue +0 -140
- package/app/components/layout/LayoutPage.vue +0 -174
- package/app/components/layout/LayoutSidebar.vue +0 -117
- package/app/components/modals/ConfirmModal.vue +0 -65
- package/app/components/modals/EntityCrudModal.vue +0 -109
- package/app/components/overlay/modal/ModalActions.vue +0 -25
- package/app/components/overlay/modal/ModalConfirm.vue +0 -25
- package/app/components/overlay/modal/ModalRadioGroup.vue +0 -37
- package/app/components/u/UActions.vue +0 -23
- package/app/components/u/UAuthForm.vue +0 -36
- package/app/components/u/UCustomApp.vue +0 -23
- package/app/components/u/UField.vue +0 -191
- package/app/components/u/UForm.vue +0 -96
- package/app/components/u/UImageWithFallback.vue +0 -35
- package/app/components/u/UInputDatePicker.vue +0 -79
- package/app/components/u/UInputDateRangePicker.vue +0 -100
- package/app/components/u/UInputDurationMinutes.vue +0 -90
- package/app/components/u/UInputFile.vue +0 -127
- package/app/components/u/URibbonCard.vue +0 -43
- package/app/components/u/URibbonSection.vue +0 -29
- package/app/components/u/USwitchCard.vue +0 -51
- package/app/components/util/Define.vue +0 -13
- package/app/components/util/SlotRef.vue +0 -17
- package/app/components/util/Sync.vue +0 -24
- package/app/components/util/TemplateText.vue +0 -20
- package/app/error.vue +0 -15
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { FormSchema, FormSourceValues, FormSubmitValues } from '@falcondev-oss/form-core'
|
|
2
|
+
import type { DefaultError, UseQueryOptions } from '@tanstack/vue-query'
|
|
3
|
+
import type { If, IsNever } from 'type-fest'
|
|
4
|
+
import type { VNode } from 'vue'
|
|
5
|
+
import { useForm } from '@falcondev-oss/form-vue'
|
|
6
|
+
import { useQuery } from '@tanstack/vue-query'
|
|
7
|
+
import modalTheme from '#build/ui/modal'
|
|
8
|
+
import { Sync, UButton, UForm, UModal } from '#components'
|
|
9
|
+
|
|
10
|
+
export default defineSetupComponent(
|
|
11
|
+
<
|
|
12
|
+
Schema extends FormSchema,
|
|
13
|
+
QueryData extends NonNullable<FormSourceValues<Schema>>,
|
|
14
|
+
Select extends QueryData = QueryData,
|
|
15
|
+
Id extends string | null = string | null,
|
|
16
|
+
>(_: {
|
|
17
|
+
props: {
|
|
18
|
+
id: Id
|
|
19
|
+
name: string
|
|
20
|
+
schema: Schema
|
|
21
|
+
getQueryOptions: (
|
|
22
|
+
id: Id,
|
|
23
|
+
) => UseQueryOptions<If<IsNever<Select>, QueryData, any>, DefaultError, Select>
|
|
24
|
+
newValues: NonNullable<FormSourceValues<Schema>>
|
|
25
|
+
mutate: (values: FormSubmitValues<Schema> & { id: Id }) => Promise<unknown>
|
|
26
|
+
}
|
|
27
|
+
emits: {
|
|
28
|
+
close: () => void
|
|
29
|
+
}
|
|
30
|
+
slots: {
|
|
31
|
+
default: (props: {
|
|
32
|
+
form: ReturnType<typeof useForm<Schema>>
|
|
33
|
+
data: Select | undefined
|
|
34
|
+
}) => VNode[]
|
|
35
|
+
}
|
|
36
|
+
}) =>
|
|
37
|
+
options(_, {
|
|
38
|
+
name: 'EntityCrudModal',
|
|
39
|
+
props: ['id', 'name', 'schema', 'getQueryOptions', 'newValues', 'mutate'],
|
|
40
|
+
emits: ['close'],
|
|
41
|
+
setup: (props, { emit, slots }) => {
|
|
42
|
+
const toast = useToast()
|
|
43
|
+
const appConfig = useAppConfig()
|
|
44
|
+
|
|
45
|
+
const { data, isLoading } = useQuery(computed(() => props.getQueryOptions(props.id)))
|
|
46
|
+
|
|
47
|
+
const form = useForm({
|
|
48
|
+
schema: props.schema,
|
|
49
|
+
sourceValues: () => {
|
|
50
|
+
if (isLoading.value) return
|
|
51
|
+
|
|
52
|
+
return data.value ?? props.newValues
|
|
53
|
+
},
|
|
54
|
+
async submit({ values }) {
|
|
55
|
+
await props.mutate({
|
|
56
|
+
...values,
|
|
57
|
+
id: props.id,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
emit('close')
|
|
61
|
+
toast.add({
|
|
62
|
+
title: `${props.name} ${props.id ? 'aktualisiert' : 'erstellt'}`,
|
|
63
|
+
color: 'success',
|
|
64
|
+
icon: 'lucide:check',
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const formActionsTeleportId = useId()
|
|
70
|
+
const closeHandle = ref<(() => void) | null>(null)
|
|
71
|
+
|
|
72
|
+
return () => (
|
|
73
|
+
<UModal
|
|
74
|
+
close={false}
|
|
75
|
+
title={`${props.name} ${props.id ? 'bearbeiten' : 'erstellen'}`}
|
|
76
|
+
dismissible={false}
|
|
77
|
+
v-slots={vSlots(UModal, {
|
|
78
|
+
actions: () => [
|
|
79
|
+
<UButton
|
|
80
|
+
class={modalTheme.slots.close}
|
|
81
|
+
icon={appConfig.ui.icons.close}
|
|
82
|
+
color="neutral"
|
|
83
|
+
variant="ghost"
|
|
84
|
+
aria-label="Schließen"
|
|
85
|
+
loadingAuto
|
|
86
|
+
onClick={() => {
|
|
87
|
+
closeHandle.value?.()
|
|
88
|
+
}}
|
|
89
|
+
/>,
|
|
90
|
+
],
|
|
91
|
+
body: ({ close }) => [
|
|
92
|
+
<Sync
|
|
93
|
+
input={close}
|
|
94
|
+
output={closeHandle.value}
|
|
95
|
+
onUpdate:output={(value: (() => void) | null) => {
|
|
96
|
+
closeHandle.value = value
|
|
97
|
+
}}
|
|
98
|
+
/>,
|
|
99
|
+
<UForm
|
|
100
|
+
class="flex flex-col gap-2"
|
|
101
|
+
form={form}
|
|
102
|
+
submitLabel={props.id ? 'Speichern' : 'Erstellen'}
|
|
103
|
+
actionsTeleportTo={`#${formActionsTeleportId}`}
|
|
104
|
+
v-slots={{
|
|
105
|
+
default: () => slots.default?.({ form, data: data.value }) ?? [],
|
|
106
|
+
}}
|
|
107
|
+
/>,
|
|
108
|
+
],
|
|
109
|
+
footer: () => [
|
|
110
|
+
<div id={formActionsTeleportId} class="flex w-full justify-end gap-2">
|
|
111
|
+
<UButton
|
|
112
|
+
color="neutral"
|
|
113
|
+
variant="soft"
|
|
114
|
+
label="Abbrechen"
|
|
115
|
+
onClick={() => emit('close')}
|
|
116
|
+
/>
|
|
117
|
+
</div>,
|
|
118
|
+
],
|
|
119
|
+
})}
|
|
120
|
+
/>
|
|
121
|
+
)
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ButtonProps } from '@nuxt/ui'
|
|
2
|
+
import { UButton, UModal } from '#components'
|
|
3
|
+
|
|
4
|
+
export default defineSetupComponent(
|
|
5
|
+
(_: {
|
|
6
|
+
props: {
|
|
7
|
+
actions: ButtonProps[]
|
|
8
|
+
title: string
|
|
9
|
+
description?: string
|
|
10
|
+
}
|
|
11
|
+
}) =>
|
|
12
|
+
options(_, {
|
|
13
|
+
name: 'ModalActions',
|
|
14
|
+
props: ['actions', 'title', 'description'],
|
|
15
|
+
emits: [],
|
|
16
|
+
setup: (props) => () => (
|
|
17
|
+
<UModal
|
|
18
|
+
title={props.title}
|
|
19
|
+
description={props.description}
|
|
20
|
+
ui={{ content: 'max-w-fit', footer: 'flex flex-col gap-4' }}
|
|
21
|
+
v-slots={vSlots(UModal, {
|
|
22
|
+
footer: () =>
|
|
23
|
+
props.actions.map((action) => (
|
|
24
|
+
<UButton
|
|
25
|
+
key={action.label}
|
|
26
|
+
variant="subtle"
|
|
27
|
+
class="w-full justify-center"
|
|
28
|
+
size="xl"
|
|
29
|
+
{...action}
|
|
30
|
+
/>
|
|
31
|
+
)),
|
|
32
|
+
})}
|
|
33
|
+
/>
|
|
34
|
+
),
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { UButton, UModal } from '#components'
|
|
2
|
+
|
|
3
|
+
export default defineSetupComponent(
|
|
4
|
+
(_: {
|
|
5
|
+
props: {
|
|
6
|
+
title: string
|
|
7
|
+
description: string
|
|
8
|
+
submitLabel: string
|
|
9
|
+
}
|
|
10
|
+
emits: {
|
|
11
|
+
close: (confirmed: boolean) => void
|
|
12
|
+
}
|
|
13
|
+
}) =>
|
|
14
|
+
options(_, {
|
|
15
|
+
name: 'ModalConfirm',
|
|
16
|
+
props: ['title', 'description', 'submitLabel'],
|
|
17
|
+
emits: ['close'],
|
|
18
|
+
setup:
|
|
19
|
+
(props, { emit }) =>
|
|
20
|
+
() => (
|
|
21
|
+
<UModal
|
|
22
|
+
close={{ onClick: () => emit('close', false) }}
|
|
23
|
+
title={props.title}
|
|
24
|
+
description={props.description}
|
|
25
|
+
class="max-w-sm"
|
|
26
|
+
v-slots={vSlots(UModal, {
|
|
27
|
+
footer: () => [
|
|
28
|
+
<div class="flex w-full justify-end gap-2">
|
|
29
|
+
<UButton
|
|
30
|
+
variant="ghost"
|
|
31
|
+
color="neutral"
|
|
32
|
+
label="Abbrechen"
|
|
33
|
+
onClick={() => emit('close', false)}
|
|
34
|
+
/>
|
|
35
|
+
<UButton
|
|
36
|
+
label={props.submitLabel}
|
|
37
|
+
color="error"
|
|
38
|
+
onClick={() => emit('close', true)}
|
|
39
|
+
/>
|
|
40
|
+
</div>,
|
|
41
|
+
],
|
|
42
|
+
})}
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { GetItemValue, NestedItem, RadioGroupItem } from '#ui/types'
|
|
2
|
+
import { UButton, UModal, URadioGroup } from '#components'
|
|
3
|
+
|
|
4
|
+
export default defineSetupComponent(
|
|
5
|
+
<T extends Extract<RadioGroupItem, object>>(_: {
|
|
6
|
+
props: {
|
|
7
|
+
items: T[]
|
|
8
|
+
defaultValue: GetItemValue<T[], 'value', NestedItem<T>>
|
|
9
|
+
title: string
|
|
10
|
+
submitButtonLabel: string
|
|
11
|
+
description?: string
|
|
12
|
+
}
|
|
13
|
+
emits: {
|
|
14
|
+
close: (value: T['value'] | undefined) => void
|
|
15
|
+
}
|
|
16
|
+
}) =>
|
|
17
|
+
options(_, {
|
|
18
|
+
name: 'ModalRadioGroup',
|
|
19
|
+
props: ['items', 'defaultValue', 'title', 'submitButtonLabel', 'description'],
|
|
20
|
+
emits: ['close'],
|
|
21
|
+
setup: (props, { emit }) => {
|
|
22
|
+
// `RadioGroupItem['value']` is `any` in `@nuxt/ui`, so neither the picked value nor what
|
|
23
|
+
// the radio group reports back can be typed any tighter — hence the disables below
|
|
24
|
+
const selectedValue = ref<T['value'] | undefined>(props.defaultValue)
|
|
25
|
+
|
|
26
|
+
return () => (
|
|
27
|
+
<UModal
|
|
28
|
+
title={props.title}
|
|
29
|
+
description={props.description}
|
|
30
|
+
v-slots={vSlots(UModal, {
|
|
31
|
+
body: () => [
|
|
32
|
+
<div class="flex flex-col gap-4">
|
|
33
|
+
<URadioGroup
|
|
34
|
+
// eslint-disable-next-line ts/no-unsafe-assignment
|
|
35
|
+
modelValue={selectedValue.value}
|
|
36
|
+
onUpdate:modelValue={(value) => {
|
|
37
|
+
selectedValue.value = value
|
|
38
|
+
}}
|
|
39
|
+
variant="table"
|
|
40
|
+
items={props.items}
|
|
41
|
+
/>
|
|
42
|
+
<div class="flex w-full justify-end gap-4">
|
|
43
|
+
<UButton
|
|
44
|
+
label="Abbrechen"
|
|
45
|
+
variant="ghost"
|
|
46
|
+
color="neutral"
|
|
47
|
+
onClick={() => emit('close', undefined)}
|
|
48
|
+
/>
|
|
49
|
+
<UButton
|
|
50
|
+
label={props.submitButtonLabel}
|
|
51
|
+
// eslint-disable-next-line ts/no-unsafe-argument
|
|
52
|
+
onClick={() => emit('close', selectedValue.value)}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</div>,
|
|
56
|
+
],
|
|
57
|
+
})}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ButtonProps } from '@nuxt/ui'
|
|
2
|
+
import { UButton } from '#components'
|
|
3
|
+
|
|
4
|
+
function getActionKey(action: ButtonProps): string | undefined {
|
|
5
|
+
return action.label || (action.icon as string | undefined)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default defineSetupComponent(
|
|
9
|
+
(_: {
|
|
10
|
+
props: {
|
|
11
|
+
actions?: ButtonProps[]
|
|
12
|
+
defaults?: Partial<ButtonProps>
|
|
13
|
+
}
|
|
14
|
+
}) =>
|
|
15
|
+
options(_, {
|
|
16
|
+
name: 'UActions',
|
|
17
|
+
props: ['actions', 'defaults'],
|
|
18
|
+
emits: [],
|
|
19
|
+
setup: (props) => () =>
|
|
20
|
+
props.actions ? (
|
|
21
|
+
<div class="flex gap-4">
|
|
22
|
+
{props.actions.map((action) => (
|
|
23
|
+
<UButton key={getActionKey(action)} {...{ ...props.defaults, ...action }} />
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
) : (
|
|
27
|
+
<div />
|
|
28
|
+
),
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthFormField,
|
|
3
|
+
AuthFormProps,
|
|
4
|
+
AuthFormSlots,
|
|
5
|
+
FormSchema,
|
|
6
|
+
FormSubmitEvent,
|
|
7
|
+
InferInput,
|
|
8
|
+
InferOutput,
|
|
9
|
+
} from '@nuxt/ui'
|
|
10
|
+
import type { Reactive } from 'vue'
|
|
11
|
+
import NuxtUIAuthForm from '@nuxt/ui/components/AuthForm.vue'
|
|
12
|
+
|
|
13
|
+
export default defineSetupComponent(
|
|
14
|
+
<T extends FormSchema = FormSchema<object>, F extends AuthFormField = AuthFormField>(_: {
|
|
15
|
+
props: Omit<AuthFormProps<T, F>, 'onSubmit'> & {
|
|
16
|
+
onSubmit?: (event: FormSubmitEvent<InferOutput<T>>) => void | Promise<void>
|
|
17
|
+
}
|
|
18
|
+
// nothing is read here, so every prop reaches `NuxtUIAuthForm` as an inherited attribute
|
|
19
|
+
propKeys: never
|
|
20
|
+
slots: AuthFormSlots<Reactive<InferInput<T>>, F>
|
|
21
|
+
}) =>
|
|
22
|
+
options(_, {
|
|
23
|
+
name: 'UAuthForm',
|
|
24
|
+
props: [],
|
|
25
|
+
emits: [],
|
|
26
|
+
setup:
|
|
27
|
+
(_props, { slots }) =>
|
|
28
|
+
() => <NuxtUIAuthForm v-slots={slots} />,
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AppProps } from '@nuxt/ui'
|
|
2
|
+
import { de } from '@nuxt/ui/locale'
|
|
3
|
+
import { useForwardProps } from 'reka-ui'
|
|
4
|
+
import { Suspense } from 'vue'
|
|
5
|
+
import { NuxtLayout, NuxtLoadingIndicator, NuxtPage, UApp } from '#components'
|
|
6
|
+
|
|
7
|
+
export default defineSetupComponent(
|
|
8
|
+
(_: {
|
|
9
|
+
props: {
|
|
10
|
+
app?: AppProps
|
|
11
|
+
}
|
|
12
|
+
}) =>
|
|
13
|
+
options(_, {
|
|
14
|
+
name: 'UCustomApp',
|
|
15
|
+
props: ['app'],
|
|
16
|
+
emits: [],
|
|
17
|
+
setup: (props) => {
|
|
18
|
+
const forwarded = useForwardProps(props)
|
|
19
|
+
const config = useRuntimeConfig()
|
|
20
|
+
|
|
21
|
+
return () => (
|
|
22
|
+
<UApp locale={de} {...forwarded.value.app} nonce={config.public.projectId}>
|
|
23
|
+
<NuxtLoadingIndicator color="var(--color-brand-secondary, var(--color-brand-primary))" />
|
|
24
|
+
<Suspense>
|
|
25
|
+
<NuxtLayout>
|
|
26
|
+
<NuxtPage />
|
|
27
|
+
</NuxtLayout>
|
|
28
|
+
</Suspense>
|
|
29
|
+
</UApp>
|
|
30
|
+
)
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { FormField } from '@falcondev-oss/form-core'
|
|
2
|
+
import type { FormFieldProps, FormFieldSlots } from '@nuxt/ui'
|
|
3
|
+
import type { VNode, WritableComputedRef } from 'vue'
|
|
4
|
+
import { useForwardProps } from 'reka-ui'
|
|
5
|
+
import * as R from 'remeda'
|
|
6
|
+
import { DevOnly, UFormField, UIcon, UPopover } from '#components'
|
|
7
|
+
import { mergeSlotClass } from '../../utils/ui'
|
|
8
|
+
|
|
9
|
+
type InputProps<T> = {
|
|
10
|
+
'modelValue': T
|
|
11
|
+
'onUpdate:modelValue': (value: T) => void
|
|
12
|
+
'onBlur': () => void
|
|
13
|
+
'disabled': boolean
|
|
14
|
+
'loading': boolean
|
|
15
|
+
'modelModifiers': { nullable: true }
|
|
16
|
+
'placeholder'?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderErrors(errors: string[]) {
|
|
20
|
+
if (errors.length === 1) return <p>{errors.join('\n')}</p>
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<ul>
|
|
24
|
+
{errors.map((error, index) => (
|
|
25
|
+
<li key={error + index} class="list-inside">
|
|
26
|
+
{error}
|
|
27
|
+
</li>
|
|
28
|
+
))}
|
|
29
|
+
</ul>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default defineSetupComponent(
|
|
34
|
+
<T,>(_: {
|
|
35
|
+
props: FormFieldProps & {
|
|
36
|
+
field: FormField<T>
|
|
37
|
+
errorInline?: boolean
|
|
38
|
+
errorPreferPlaceholder?: boolean
|
|
39
|
+
}
|
|
40
|
+
slots: {
|
|
41
|
+
default: (slot: {
|
|
42
|
+
bind: InputProps<T>
|
|
43
|
+
model: WritableComputedRef<T>
|
|
44
|
+
field: FormField<T>
|
|
45
|
+
}) => VNode[]
|
|
46
|
+
} & Omit<FormFieldSlots, 'default'>
|
|
47
|
+
}) =>
|
|
48
|
+
options(_, {
|
|
49
|
+
name: 'UField',
|
|
50
|
+
props: [
|
|
51
|
+
'as',
|
|
52
|
+
'name',
|
|
53
|
+
'errorPattern',
|
|
54
|
+
'label',
|
|
55
|
+
'description',
|
|
56
|
+
'help',
|
|
57
|
+
'error',
|
|
58
|
+
'hint',
|
|
59
|
+
'size',
|
|
60
|
+
'required',
|
|
61
|
+
'eagerValidation',
|
|
62
|
+
'validateOnInputDelay',
|
|
63
|
+
'orientation',
|
|
64
|
+
'class',
|
|
65
|
+
'ui',
|
|
66
|
+
'field',
|
|
67
|
+
'errorInline',
|
|
68
|
+
'errorPreferPlaceholder',
|
|
69
|
+
],
|
|
70
|
+
emits: [],
|
|
71
|
+
setup: (props, { slots }) => {
|
|
72
|
+
const forwardedProps = useForwardProps(props)
|
|
73
|
+
|
|
74
|
+
const isOverMaxLength = computed(() => {
|
|
75
|
+
const field = forwardedProps.value.field
|
|
76
|
+
|
|
77
|
+
return field.schema.maxLength === undefined || field.value == null
|
|
78
|
+
? false
|
|
79
|
+
: (field.value as string | number)?.toString().length > field.schema.maxLength
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const formFieldProps = computed<FormFieldProps>(() => {
|
|
83
|
+
const { field, ...rest } = forwardedProps.value
|
|
84
|
+
|
|
85
|
+
const hint =
|
|
86
|
+
field.schema.maxLength === undefined ||
|
|
87
|
+
field.schema.maxLength === field.schema.minLength
|
|
88
|
+
? undefined
|
|
89
|
+
: `${(field.value as string | number | null)?.toString().length ?? 0}/${field.schema.maxLength}`
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
required: field.schema.required,
|
|
93
|
+
label: field.schema.title,
|
|
94
|
+
description: field.schema.description,
|
|
95
|
+
hint,
|
|
96
|
+
...R.omitBy(rest, (v) => v === undefined),
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const bind = computed(() => {
|
|
101
|
+
const field = forwardedProps.value.field
|
|
102
|
+
|
|
103
|
+
const placeholder = (field.errors && field.errors[0]) || field.schema.default?.toString()
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
'modelValue': field.value,
|
|
107
|
+
'onUpdate:modelValue': (value) => field.handleChange(value),
|
|
108
|
+
'onBlur': () => field.handleBlur(),
|
|
109
|
+
'disabled': field.disabled,
|
|
110
|
+
'loading': field.isPending,
|
|
111
|
+
placeholder,
|
|
112
|
+
'modelModifiers': { nullable: true },
|
|
113
|
+
} satisfies InputProps<T>
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const model = computed({
|
|
117
|
+
get() {
|
|
118
|
+
return forwardedProps.value.field.value
|
|
119
|
+
},
|
|
120
|
+
set(value: T) {
|
|
121
|
+
forwardedProps.value.field.handleChange(value)
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return () => {
|
|
126
|
+
const field = props.field
|
|
127
|
+
/** the error is only worth showing where it can't be read off the placeholder */
|
|
128
|
+
const showErrors =
|
|
129
|
+
!!field.errors && (props.errorPreferPlaceholder ? String(field.value).length > 0 : true)
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<UFormField
|
|
133
|
+
{...formFieldProps.value}
|
|
134
|
+
ui={{
|
|
135
|
+
...formFieldProps.value.ui,
|
|
136
|
+
hint: mergeSlotClass(
|
|
137
|
+
formFieldProps.value.ui?.hint,
|
|
138
|
+
isOverMaxLength.value ? 'text-error' : '',
|
|
139
|
+
),
|
|
140
|
+
error: mergeSlotClass(formFieldProps.value.ui?.error, 'mt-1!'),
|
|
141
|
+
}}
|
|
142
|
+
error={!!field.errors}
|
|
143
|
+
class={['u-field', props.class]}
|
|
144
|
+
v-slots={vSlots(UFormField, {
|
|
145
|
+
default: () =>
|
|
146
|
+
slots.default?.({
|
|
147
|
+
bind: bind.value,
|
|
148
|
+
model,
|
|
149
|
+
field: forwardedProps.value.field,
|
|
150
|
+
}) ?? [
|
|
151
|
+
<DevOnly>
|
|
152
|
+
<p class="font-black text-red-500">UField missing slot</p>
|
|
153
|
+
</DevOnly>,
|
|
154
|
+
],
|
|
155
|
+
|
|
156
|
+
...(props.errorInline && showErrors
|
|
157
|
+
? {
|
|
158
|
+
error: () => [
|
|
159
|
+
renderErrors(field.errors!),
|
|
160
|
+
...(typeof props.error === 'string' ? [<>{props.error}</>] : []),
|
|
161
|
+
],
|
|
162
|
+
}
|
|
163
|
+
: {
|
|
164
|
+
hint: ({ hint }) => [
|
|
165
|
+
<span class="flex items-center gap-1.5">
|
|
166
|
+
{showErrors ? (
|
|
167
|
+
<UPopover
|
|
168
|
+
mode="hover"
|
|
169
|
+
openDelay={0}
|
|
170
|
+
ui={{
|
|
171
|
+
content: 'bg-error-50 ring-error-200! rounded py-1 px-2',
|
|
172
|
+
}}
|
|
173
|
+
v-slots={vSlots(UPopover, {
|
|
174
|
+
content: () => [
|
|
175
|
+
<div class="max-w-sm text-xs text-(--ui-color-neutral-800)">
|
|
176
|
+
{renderErrors(field.errors!)}
|
|
177
|
+
</div>,
|
|
178
|
+
],
|
|
179
|
+
})}
|
|
180
|
+
>
|
|
181
|
+
<UIcon name="lucide:circle-alert" class="text-error" />
|
|
182
|
+
</UPopover>
|
|
183
|
+
) : null}
|
|
184
|
+
|
|
185
|
+
{hint}
|
|
186
|
+
</span>,
|
|
187
|
+
],
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
...(field.schema.examples && {
|
|
191
|
+
help: () => [
|
|
192
|
+
Array.isArray(field.schema.examples) ? (
|
|
193
|
+
<ul>
|
|
194
|
+
{field.schema.examples.map((example, index) => (
|
|
195
|
+
<li key={index}>{example}</li>
|
|
196
|
+
))}
|
|
197
|
+
</ul>
|
|
198
|
+
) : (
|
|
199
|
+
<p>{field.schema.examples}</p>
|
|
200
|
+
),
|
|
201
|
+
],
|
|
202
|
+
}),
|
|
203
|
+
|
|
204
|
+
...R.omit(slots, ['default']),
|
|
205
|
+
})}
|
|
206
|
+
/>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
)
|