This commit is contained in:
Simon Pocrnjič
2026-01-18 18:21:41 +01:00
parent 28f28be1b8
commit cc4c07717e
33 changed files with 1440 additions and 579 deletions
+88 -88
View File
@@ -1,118 +1,118 @@
<script setup>
import { ref, reactive, nextTick } from 'vue';
import DialogModal from './DialogModal.vue';
import InputError from './InputError.vue';
import PrimaryButton from './PrimaryButton.vue';
import SecondaryButton from './SecondaryButton.vue';
import TextInput from './TextInput.vue';
import { ref, reactive, nextTick } from "vue";
import DialogModal from "./DialogModal.vue";
import InputError from "./InputError.vue";
import PrimaryButton from "./PrimaryButton.vue";
import SecondaryButton from "./SecondaryButton.vue";
import { Input } from "@/Components/ui/input";
const emit = defineEmits(['confirmed']);
const emit = defineEmits(["confirmed"]);
defineProps({
title: {
type: String,
default: 'Confirm Password',
},
content: {
type: String,
default: 'For your security, please confirm your password to continue.',
},
button: {
type: String,
default: 'Confirm',
},
title: {
type: String,
default: "Confirm Password",
},
content: {
type: String,
default: "For your security, please confirm your password to continue.",
},
button: {
type: String,
default: "Confirm",
},
});
const confirmingPassword = ref(false);
const form = reactive({
password: '',
error: '',
processing: false,
password: "",
error: "",
processing: false,
});
const passwordInput = ref(null);
const startConfirmingPassword = () => {
axios.get(route('password.confirmation')).then(response => {
if (response.data.confirmed) {
emit('confirmed');
} else {
confirmingPassword.value = true;
axios.get(route("password.confirmation")).then((response) => {
if (response.data.confirmed) {
emit("confirmed");
} else {
confirmingPassword.value = true;
setTimeout(() => passwordInput.value.focus(), 250);
}
});
setTimeout(() => passwordInput.value.focus(), 250);
}
});
};
const confirmPassword = () => {
form.processing = true;
form.processing = true;
axios.post(route('password.confirm'), {
password: form.password,
}).then(() => {
form.processing = false;
axios
.post(route("password.confirm"), {
password: form.password,
})
.then(() => {
form.processing = false;
closeModal();
nextTick().then(() => emit('confirmed'));
}).catch(error => {
form.processing = false;
form.error = error.response.data.errors.password[0];
passwordInput.value.focus();
closeModal();
nextTick().then(() => emit("confirmed"));
})
.catch((error) => {
form.processing = false;
form.error = error.response.data.errors.password[0];
passwordInput.value.focus();
});
};
const closeModal = () => {
confirmingPassword.value = false;
form.password = '';
form.error = '';
confirmingPassword.value = false;
form.password = "";
form.error = "";
};
</script>
<template>
<span>
<span @click="startConfirmingPassword">
<slot />
</span>
<DialogModal :show="confirmingPassword" @close="closeModal">
<template #title>
{{ title }}
</template>
<template #content>
{{ content }}
<div class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="confirmPassword"
/>
<InputError :message="form.error" class="mt-2" />
</div>
</template>
<template #footer>
<SecondaryButton @click="closeModal">
Cancel
</SecondaryButton>
<PrimaryButton
class="ms-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="confirmPassword"
>
{{ button }}
</PrimaryButton>
</template>
</DialogModal>
<span>
<span @click="startConfirmingPassword">
<slot />
</span>
<DialogModal :show="confirmingPassword" @close="closeModal">
<template #title>
{{ title }}
</template>
<template #content>
{{ content }}
<div class="mt-4">
<Input
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="confirmPassword"
/>
<InputError :message="form.error" class="mt-2" />
</div>
</template>
<template #footer>
<SecondaryButton @click="closeModal"> Cancel </SecondaryButton>
<PrimaryButton
class="ms-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="confirmPassword"
>
{{ button }}
</PrimaryButton>
</template>
</DialogModal>
</span>
</template>
@@ -1,70 +0,0 @@
<script setup lang="ts">
import type { LucideIcon } from "lucide-vue-next";
import { ChevronRight } from "lucide-vue-next";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/Components/ui/collapsible";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/Components/ui/sidebar";
defineProps<{
items: {
title: string;
url: string;
icon?: LucideIcon;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
}>();
</script>
<template>
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
<Collapsible
v-for="item in items"
:key="item.title"
as-child
:default-open="item.isActive"
class="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger as-child>
<SidebarMenuButton :tooltip="item.title">
<component :is="item.icon" v-if="item.icon" />
<span>{{ item.title }}</span>
<ChevronRight
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
/>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
<SidebarMenuSubButton as-child>
<a :href="subItem.url">
<span>{{ subItem.title }}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroup>
</template>
@@ -7,12 +7,7 @@ import { router } from "@inertiajs/vue3";
import CreateDialog from "../Dialogs/CreateDialog.vue";
import UpdateDialog from "../Dialogs/UpdateDialog.vue";
import SectionTitle from "../SectionTitle.vue";
import {
FormControl,
FormItem,
FormLabel,
FormMessage,
} from "@/Components/ui/form";
import { FormControl, FormItem, FormLabel, FormMessage } from "@/Components/ui/form";
import { Input } from "@/Components/ui/input";
import {
Select,
@@ -97,7 +92,7 @@ watch(
country: a.country || "",
post_code: a.post_code || a.postal_code || "",
city: a.city || "",
type_id: a.type_id ?? (props.types?.[0]?.id ?? null),
type_id: a.type_id ?? props.types?.[0]?.id ?? null,
description: a.description || "",
});
return;
@@ -108,52 +103,51 @@ watch(
{ immediate: true }
);
watch(() => props.show, (val) => {
if (val && props.edit && props.id) {
const a = props.person.addresses?.find((x) => x.id === props.id);
if (a) {
form.setValues({
address: a.address || "",
country: a.country || "",
post_code: a.post_code || a.postal_code || "",
city: a.city || "",
type_id: a.type_id ?? (props.types?.[0]?.id ?? null),
description: a.description || "",
});
watch(
() => props.show,
(val) => {
if (val && props.edit && props.id) {
const a = props.person.addresses?.find((x) => x.id === props.id);
if (a) {
form.setValues({
address: a.address || "",
country: a.country || "",
post_code: a.post_code || a.postal_code || "",
city: a.city || "",
type_id: a.type_id ?? props.types?.[0]?.id ?? null,
description: a.description || "",
});
}
} else if (val && !props.edit) {
resetForm();
}
} else if (val && !props.edit) {
resetForm();
}
});
);
const create = async () => {
processing.value = true;
const { values } = form;
router.post(
route("person.address.create", props.person),
values,
{
preserveScroll: true,
onSuccess: () => {
processing.value = false;
close();
resetForm();
},
onError: (errors) => {
Object.keys(errors).forEach((field) => {
const errorMessages = Array.isArray(errors[field])
? errors[field]
: [errors[field]];
form.setFieldError(field, errorMessages[0]);
});
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
}
);
router.post(route("person.address.create", props.person), values, {
preserveScroll: true,
onSuccess: () => {
processing.value = false;
close();
resetForm();
},
onError: (errors) => {
Object.keys(errors).forEach((field) => {
const errorMessages = Array.isArray(errors[field])
? errors[field]
: [errors[field]];
form.setFieldError(field, errorMessages[0]);
});
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
});
};
const update = async () => {
@@ -223,7 +217,12 @@ const onConfirm = () => {
<FormItem>
<FormLabel>Naslov</FormLabel>
<FormControl>
<Input type="text" placeholder="Naslov" autocomplete="street-address" v-bind="componentField" />
<Input
type="text"
placeholder="Naslov"
autocomplete="street-address"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -233,7 +232,12 @@ const onConfirm = () => {
<FormItem>
<FormLabel>Država</FormLabel>
<FormControl>
<Input type="text" placeholder="Država" autocomplete="country" v-bind="componentField" />
<Input
type="text"
placeholder="Država"
autocomplete="country"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -243,7 +247,12 @@ const onConfirm = () => {
<FormItem>
<FormLabel>Poštna številka</FormLabel>
<FormControl>
<Input type="text" placeholder="Poštna številka" autocomplete="postal-code" v-bind="componentField" />
<Input
type="text"
placeholder="Poštna številka"
autocomplete="postal-code"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -253,7 +262,22 @@ const onConfirm = () => {
<FormItem>
<FormLabel>Mesto</FormLabel>
<FormControl>
<Input type="text" placeholder="Mesto" autocomplete="address-level2" v-bind="componentField" />
<Input
type="text"
placeholder="Mesto"
autocomplete="address-level2"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="description">
<FormItem>
<FormLabel>Opis</FormLabel>
<FormControl>
<Input type="text" placeholder="Opis" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
@@ -6,12 +6,7 @@ import * as z from "zod";
import { router } from "@inertiajs/vue3";
import UpdateDialog from "../Dialogs/UpdateDialog.vue";
import SectionTitle from "../SectionTitle.vue";
import {
FormControl,
FormItem,
FormLabel,
FormMessage,
} from "@/Components/ui/form";
import { FormControl, FormItem, FormLabel, FormMessage } from "@/Components/ui/form";
import { Input } from "@/Components/ui/input";
import {
Select,
@@ -85,7 +80,7 @@ const hydrate = () => {
country: a.country || "",
post_code: a.post_code || a.postal_code || "",
city: a.city || "",
type_id: a.type_id ?? (props.types?.[0]?.id ?? null),
type_id: a.type_id ?? props.types?.[0]?.id ?? null,
description: a.description || "",
});
return;
@@ -94,10 +89,17 @@ const hydrate = () => {
resetForm();
};
watch(() => props.id, () => hydrate(), { immediate: true });
watch(() => props.show, (v) => {
if (v) hydrate();
});
watch(
() => props.id,
() => hydrate(),
{ immediate: true }
);
watch(
() => props.show,
(v) => {
if (v) hydrate();
}
);
const update = async () => {
processing.value = true;
@@ -157,7 +159,12 @@ const onConfirm = () => {
<FormItem>
<FormLabel>Naslov</FormLabel>
<FormControl>
<Input type="text" placeholder="Naslov" autocomplete="street-address" v-bind="componentField" />
<Input
type="text"
placeholder="Naslov"
autocomplete="street-address"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -167,7 +174,12 @@ const onConfirm = () => {
<FormItem>
<FormLabel>Država</FormLabel>
<FormControl>
<Input type="text" placeholder="Država" autocomplete="country" v-bind="componentField" />
<Input
type="text"
placeholder="Država"
autocomplete="country"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -177,7 +189,12 @@ const onConfirm = () => {
<FormItem>
<FormLabel>Poštna številka</FormLabel>
<FormControl>
<Input type="text" placeholder="Poštna številka" autocomplete="postal-code" v-bind="componentField" />
<Input
type="text"
placeholder="Poštna številka"
autocomplete="postal-code"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -187,7 +204,22 @@ const onConfirm = () => {
<FormItem>
<FormLabel>Mesto</FormLabel>
<FormControl>
<Input type="text" placeholder="Mesto" autocomplete="address-level2" v-bind="componentField" />
<Input
type="text"
placeholder="Mesto"
autocomplete="address-level2"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="description">
<FormItem>
<FormLabel>Opis</FormLabel>
<FormControl>
<Input type="text" placeholder="Opis" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
@@ -24,9 +24,9 @@ const handleDelete = (id, label) => emit("delete", id, label);
<template>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<Card class="p-2 gap-1" v-for="address in person.addresses" :key="address.id">
<div class="flex items-center justify-between mb-2">
<div class="flex flex-wrap gap-2">
<Card class="p-2 gap-0" v-for="address in person.addresses" :key="address.id">
<div class="flex items-center justify-between">
<div class="flex flex-wrap gap-1">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"
>
@@ -61,13 +61,16 @@ const handleDelete = (id, label) => emit("delete", id, label);
</DropdownMenu>
</div>
</div>
<p class="text-sm font-medium text-gray-900 leading-relaxed p-1">
<p class="font-medium text-gray-900 leading-relaxed p-1">
{{
address.post_code && address.city
? `${address.address}, ${address.post_code} ${address.city}`
: address.address
}}
</p>
<p class="text-sm text-muted-foreground p-1" v-if="address.description">
{{ address.description }}
</p>
</Card>
<button
v-if="edit"
@@ -27,9 +27,9 @@ const handleDelete = (id, label) => emit("delete", id, label);
<template>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<template v-if="getEmails(person).length">
<Card class="p-2 gap-1" v-for="(email, idx) in getEmails(person)" :key="idx">
<div class="flex items-center justify-between mb-2" v-if="edit">
<div class="flex flex-wrap gap-2">
<Card class="p-2 gap-0" v-for="(email, idx) in getEmails(person)" :key="idx">
<div class="flex items-center justify-between" v-if="edit">
<div class="flex flex-wrap gap-1">
<span
v-if="email?.label"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800"
@@ -69,7 +69,7 @@ const handleDelete = (id, label) => emit("delete", id, label);
</div>
</div>
<div class="p-1">
<p class="text-sm font-medium text-gray-900 leading-relaxed">
<p class="font-medium text-gray-900 leading-relaxed">
{{ email?.value || email?.email || email?.address || "-" }}
</p>
<p
@@ -30,9 +30,9 @@ const handleSms = (phone) => emit("sms", phone);
<template>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<template v-if="getPhones(person).length">
<Card class="p-2 gap-1" v-for="phone in getPhones(person)" :key="phone.id">
<div class="flex items-center justify-between mb-2">
<div class="flex flex-wrap gap-2">
<Card class="p-2 gap-0" v-for="phone in getPhones(person)" :key="phone.id">
<div class="flex items-center justify-between">
<div class="flex flex-wrap gap-1">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"
>
@@ -79,9 +79,12 @@ const handleSms = (phone) => emit("sms", phone);
</DropdownMenu>
</div>
</div>
<p class="text-sm font-medium text-gray-900 leading-relaxed p-1">
<p class="font-medium leading-relaxed p-1">
{{ phone.nu }}
</p>
<p class="text-sm text-muted-foreground p-1" v-if="phone.description">
{{ phone.description }}
</p>
</Card>
</template>
<button
@@ -6,12 +6,7 @@ import * as z from "zod";
import { router } from "@inertiajs/vue3";
import CreateDialog from "../Dialogs/CreateDialog.vue";
import SectionTitle from "../SectionTitle.vue";
import {
FormControl,
FormItem,
FormLabel,
FormMessage,
} from "@/Components/ui/form";
import { FormControl, FormItem, FormLabel, FormMessage } from "@/Components/ui/form";
import { Input } from "@/Components/ui/input";
import {
Select,
@@ -101,29 +96,25 @@ const create = async () => {
processing.value = true;
const { values } = form;
router.post(
route("person.phone.create", props.person),
values,
{
preserveScroll: true,
onSuccess: () => {
close();
resetForm();
},
onError: (errors) => {
Object.keys(errors).forEach((field) => {
const errorMessages = Array.isArray(errors[field])
? errors[field]
: [errors[field]];
form.setFieldError(field, errorMessages[0]);
});
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
}
);
router.post(route("person.phone.create", props.person), values, {
preserveScroll: true,
onSuccess: () => {
close();
resetForm();
},
onError: (errors) => {
Object.keys(errors).forEach((field) => {
const errorMessages = Array.isArray(errors[field])
? errors[field]
: [errors[field]];
form.setFieldError(field, errorMessages[0]);
});
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
});
};
const onSubmit = form.handleSubmit(() => {
@@ -150,7 +141,12 @@ const onSubmit = form.handleSubmit(() => {
<FormItem>
<FormLabel>Številka</FormLabel>
<FormControl>
<Input type="text" placeholder="Številka telefona" autocomplete="tel" v-bind="componentField" />
<Input
type="text"
placeholder="Številka telefona"
autocomplete="tel"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -166,7 +162,11 @@ const onSubmit = form.handleSubmit(() => {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem v-for="option in countryOptions" :key="option.value" :value="option.value">
<SelectItem
v-for="option in countryOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectItem>
</SelectContent>
@@ -204,7 +204,11 @@ const onSubmit = form.handleSubmit(() => {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem v-for="option in phoneTypeOptions" :key="option.value" :value="option.value">
<SelectItem
v-for="option in phoneTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectItem>
</SelectContent>
@@ -213,6 +217,16 @@ const onSubmit = form.handleSubmit(() => {
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="description">
<FormItem>
<FormLabel>Opis</FormLabel>
<FormControl>
<Input type="text" placeholder="Opis" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="validated">
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
@@ -6,12 +6,7 @@ import * as z from "zod";
import { router } from "@inertiajs/vue3";
import UpdateDialog from "../Dialogs/UpdateDialog.vue";
import SectionTitle from "../SectionTitle.vue";
import {
FormControl,
FormItem,
FormLabel,
FormMessage,
} from "@/Components/ui/form";
import { FormControl, FormItem, FormLabel, FormMessage } from "@/Components/ui/form";
import { Input } from "@/Components/ui/input";
import {
Select,
@@ -108,7 +103,7 @@ function hydrateFromProps() {
form.setValues({
nu: p.nu || "",
country_code: p.country_code ?? 386,
type_id: p.type_id ?? (props.types?.[0]?.id ?? null),
type_id: p.type_id ?? props.types?.[0]?.id ?? null,
description: p.description || "",
validated: !!p.validated,
phone_type: p.phone_type ?? null,
@@ -119,8 +114,17 @@ function hydrateFromProps() {
resetForm();
}
watch(() => props.id, () => hydrateFromProps(), { immediate: true });
watch(() => props.show, (val) => { if (val) hydrateFromProps(); });
watch(
() => props.id,
() => hydrateFromProps(),
{ immediate: true }
);
watch(
() => props.show,
(val) => {
if (val) hydrateFromProps();
}
);
const update = async () => {
processing.value = true;
@@ -175,7 +179,12 @@ const onSubmit = form.handleSubmit(() => {
<FormItem>
<FormLabel>Številka</FormLabel>
<FormControl>
<Input type="text" placeholder="Številka telefona" autocomplete="tel" v-bind="componentField" />
<Input
type="text"
placeholder="Številka telefona"
autocomplete="tel"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -191,7 +200,11 @@ const onSubmit = form.handleSubmit(() => {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem v-for="option in countryOptions" :key="option.value" :value="option.value">
<SelectItem
v-for="option in countryOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectItem>
</SelectContent>
@@ -229,7 +242,11 @@ const onSubmit = form.handleSubmit(() => {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem v-for="option in phoneTypeOptions" :key="option.value" :value="option.value">
<SelectItem
v-for="option in phoneTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</SelectItem>
</SelectContent>
@@ -238,6 +255,16 @@ const onSubmit = form.handleSubmit(() => {
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="description">
<FormItem>
<FormLabel>Opis</FormLabel>
<FormControl>
<Input type="text" placeholder="Opis" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="validated">
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
@@ -0,0 +1,20 @@
<script setup>
import { cn } from "@/lib/utils";
import { fieldVariants } from ".";
const props = defineProps({
class: { type: null, required: false },
orientation: { type: null, required: false },
});
</script>
<template>
<div
role="group"
data-slot="field"
:data-orientation="orientation"
:class="cn(fieldVariants({ orientation }), props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,21 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="field-content"
:class="
cn(
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
props.class,
)
"
>
<slot />
</div>
</template>
@@ -0,0 +1,23 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<p
data-slot="field-description"
:class="
cn(
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
props.class,
)
"
>
<slot />
</p>
</template>
@@ -0,0 +1,43 @@
<script setup>
import { computed } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
errors: { type: Array, required: false },
});
const content = computed(() => {
if (!props.errors || props.errors.length === 0) return null;
if (props.errors.length === 1 && props.errors[0]?.message) {
return props.errors[0].message;
}
return props.errors.some((e) => e?.message) ? props.errors : null;
});
</script>
<template>
<div
v-if="$slots.default || content"
role="alert"
data-slot="field-error"
:class="cn('text-destructive text-sm font-normal', props.class)"
>
<slot v-if="$slots.default" />
<template v-else-if="typeof content === 'string'">
{{ content }}
</template>
<ul
v-else-if="Array.isArray(content)"
class="ml-4 flex list-disc flex-col gap-1"
>
<li v-for="(error, index) in content" :key="index">
{{ error?.message }}
</li>
</ul>
</div>
</template>
@@ -0,0 +1,21 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="field-group"
:class="
cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
props.class,
)
"
>
<slot />
</div>
</template>
@@ -0,0 +1,24 @@
<script setup>
import { cn } from "@/lib/utils";
import { Label } from '@/Components/ui/label';
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<Label
data-slot="field-label"
:class="
cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&_>[data-slot=field]]:p-3',
'has-[[data-state=checked]]:bg-primary/5 has-[[data-state=checked]]:border-primary dark:has-[[data-state=checked]]:bg-primary/10',
props.class,
)
"
>
<slot />
</Label>
</template>
@@ -0,0 +1,25 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
variant: { type: String, required: false },
});
</script>
<template>
<legend
data-slot="field-legend"
:data-variant="variant"
:class="
cn(
'mb-3 font-medium',
'data-[variant=legend]:text-base',
'data-[variant=label]:text-sm',
props.class,
)
"
>
<slot />
</legend>
</template>
@@ -0,0 +1,30 @@
<script setup>
import { cn } from "@/lib/utils";
import { Separator } from '@/Components/ui/separator';
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="field-separator"
:data-content="!!$slots.default"
:class="
cn(
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
props.class,
)
"
>
<Separator class="absolute inset-0 top-1/2" />
<span
v-if="$slots.default"
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
<slot />
</span>
</div>
</template>
@@ -0,0 +1,22 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<fieldset
data-slot="field-set"
:class="
cn(
'flex flex-col gap-6',
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
props.class,
)
"
>
<slot />
</fieldset>
</template>
@@ -0,0 +1,21 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="field-label"
:class="
cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
props.class,
)
"
>
<slot />
</div>
</template>
+36
View File
@@ -0,0 +1,36 @@
import { cva } from "class-variance-authority";
export const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
},
);
export { default as Field } from "./Field.vue";
export { default as FieldContent } from "./FieldContent.vue";
export { default as FieldDescription } from "./FieldDescription.vue";
export { default as FieldError } from "./FieldError.vue";
export { default as FieldGroup } from "./FieldGroup.vue";
export { default as FieldLabel } from "./FieldLabel.vue";
export { default as FieldLegend } from "./FieldLegend.vue";
export { default as FieldSeparator } from "./FieldSeparator.vue";
export { default as FieldSet } from "./FieldSet.vue";
export { default as FieldTitle } from "./FieldTitle.vue";
@@ -36,6 +36,7 @@ const props = defineProps({
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([