Test commit to new origin

This commit is contained in:
Simon Pocrnjič 2025-12-01 19:30:53 +01:00
parent c1ac92efbf
commit c4a78b4632
87 changed files with 2137 additions and 1618 deletions

1127
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build" "build": "vite build",
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
}, },
"devDependencies": { "devDependencies": {
"@inertiajs/vue3": "2.0", "@inertiajs/vue3": "2.0",
@ -11,14 +12,17 @@
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tailwindcss/postcss": "^4.1.16", "@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"axios": "^1.7.4", "axios": "^1.7.4",
"laravel-vite-plugin": "^2.0.1", "laravel-vite-plugin": "^2.0.1",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"tailwindcss": "^4.1.16", "tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite": "^7.1.7", "vite": "^7.1.7",
"vue": "^3.3.13" "vue": "^3.3.13",
"vue-tsc": "^3.1.5"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, ref } from "vue"; import { computed, ref, useAttrs } from "vue";
import { Button } from "@/Components/ui/button"; import { Button } from "@/Components/ui/button";
import { Calendar } from "@/Components/ui/calendar"; import { Calendar } from "@/Components/ui/calendar";
import { import {
@ -13,6 +13,8 @@ import { format } from "date-fns";
import { sl } from "date-fns/locale"; import { sl } from "date-fns/locale";
import { CalendarDate, parseDate } from "@internationalized/date"; import { CalendarDate, parseDate } from "@internationalized/date";
defineOptions({ inheritAttrs: false });
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: [Date, String, null], type: [Date, String, null],
@ -42,6 +44,13 @@ const props = defineProps({
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue"]);
const attrs = useAttrs();
const forwardedAttrs = computed(() => {
const { class: _class, id: _id, ...rest } = attrs;
return rest;
});
const controlId = computed(() => attrs.id ?? props.id);
// Convert string/Date to CalendarDate // Convert string/Date to CalendarDate
const toCalendarDate = (value) => { const toCalendarDate = (value) => {
if (!value) return null; if (!value) return null;
@ -115,13 +124,15 @@ const open = ref(false);
<Popover v-model:open="open"> <Popover v-model:open="open">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button <Button
:id="id" v-bind="forwardedAttrs"
:id="controlId"
variant="outline" variant="outline"
:class=" :class="
cn( cn(
'w-full justify-start text-left font-normal', 'w-full justify-start text-left font-normal',
!calendarDate && 'text-muted-foreground', !calendarDate && 'text-muted-foreground',
error && 'border-red-500 focus:border-red-500 focus:ring-red-500' error && 'border-red-500 focus:border-red-500 focus:ring-red-500',
attrs.class
) )
" "
:disabled="disabled" :disabled="disabled"

View File

@ -0,0 +1,70 @@
<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>

View File

@ -24,7 +24,7 @@ const handleDelete = (id, label) => emit("delete", id, label);
<template> <template>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<Card class="p-2" v-for="address in person.addresses" :key="address.id"> <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 items-center justify-between mb-2">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<span <span
@ -61,7 +61,7 @@ const handleDelete = (id, label) => emit("delete", id, label);
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
<p class="text-sm font-medium text-gray-900 leading-relaxed"> <p class="text-sm font-medium text-gray-900 leading-relaxed p-1">
{{ {{
address.post_code && address.city address.post_code && address.city
? `${address.address}, ${address.post_code} ${address.city}` ? `${address.address}, ${address.post_code} ${address.city}`

View File

@ -27,7 +27,7 @@ const handleDelete = (id, label) => emit("delete", id, label);
<template> <template>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<template v-if="getEmails(person).length"> <template v-if="getEmails(person).length">
<Card class="p-2" v-for="(email, idx) in getEmails(person)" :key="idx"> <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 items-center justify-between mb-2" v-if="edit">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<span <span
@ -68,15 +68,17 @@ const handleDelete = (id, label) => emit("delete", id, label);
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
<p class="text-sm font-medium text-gray-900 leading-relaxed"> <div class="p-1">
{{ email?.value || email?.email || email?.address || "-" }} <p class="text-sm font-medium text-gray-900 leading-relaxed">
</p> {{ email?.value || email?.email || email?.address || "-" }}
<p </p>
v-if="email?.note" <p
class="mt-2 text-xs text-gray-600 whitespace-pre-wrap leading-relaxed" v-if="email?.note"
> class="mt-2 text-xs text-gray-600 whitespace-pre-wrap leading-relaxed"
{{ email.note }} >
</p> {{ email.note }}
</p>
</div>
</Card> </Card>
</template> </template>
<button <button

View File

@ -30,7 +30,7 @@ const handleSms = (phone) => emit("sms", phone);
<template> <template>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<template v-if="getPhones(person).length"> <template v-if="getPhones(person).length">
<Card class="p-2" v-for="phone in getPhones(person)" :key="phone.id"> <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 items-center justify-between mb-2">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<span <span
@ -79,7 +79,9 @@ const handleSms = (phone) => emit("sms", phone);
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
<p class="text-sm font-medium text-gray-900 leading-relaxed">{{ phone.nu }}</p> <p class="text-sm font-medium text-gray-900 leading-relaxed p-1">
{{ phone.nu }}
</p>
</Card> </Card>
</template> </template>
<button <button

View File

@ -12,12 +12,7 @@ import { router, usePage } from "@inertiajs/vue3";
import { useForm, Field as FormField } from "vee-validate"; import { useForm, Field as FormField } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod"; import { toTypedSchema } from "@vee-validate/zod";
import * as z from "zod"; import * as z from "zod";
import { import { FormControl, FormItem, FormLabel, FormMessage } from "@/Components/ui/form";
FormControl,
FormItem,
FormLabel,
FormMessage,
} from "@/Components/ui/form";
import { Input } from "@/Components/ui/input"; import { Input } from "@/Components/ui/input";
import { Textarea } from "@/Components/ui/textarea"; import { Textarea } from "@/Components/ui/textarea";
import { import {
@ -158,8 +153,7 @@ const formSchema = toTypedSchema(
(val) => { (val) => {
const encoding = isGsm7(val) ? "GSM-7" : "UCS-2"; const encoding = isGsm7(val) ? "GSM-7" : "UCS-2";
const maxAllowed = encoding === "GSM-7" ? 640 : 320; const maxAllowed = encoding === "GSM-7" ? 640 : 320;
const count = const count = encoding === "GSM-7" ? gsm7Length(val) : ucs2Length(val);
encoding === "GSM-7" ? gsm7Length(val) : ucs2Length(val);
return count <= maxAllowed; return count <= maxAllowed;
}, },
{ {
@ -196,9 +190,7 @@ const sendersForSelectedProfile = computed(() => {
); );
}); });
const smsEncoding = computed(() => const smsEncoding = computed(() => (isGsm7(form.values.message) ? "GSM-7" : "UCS-2"));
isGsm7(form.values.message) ? "GSM-7" : "UCS-2"
);
const charCount = computed(() => const charCount = computed(() =>
smsEncoding.value === "GSM-7" smsEncoding.value === "GSM-7"
@ -222,13 +214,9 @@ const segments = computed(() => {
const creditsNeeded = computed(() => segments.value); const creditsNeeded = computed(() => segments.value);
const maxAllowed = computed(() => const maxAllowed = computed(() => (smsEncoding.value === "GSM-7" ? 640 : 320));
smsEncoding.value === "GSM-7" ? 640 : 320
);
const remaining = computed(() => const remaining = computed(() => Math.max(0, maxAllowed.value - charCount.value));
Math.max(0, maxAllowed.value - charCount.value)
);
// Truncate message if exceeds limit // Truncate message if exceeds limit
watch( watch(
@ -236,10 +224,7 @@ watch(
(val) => { (val) => {
const limit = maxAllowed.value; const limit = maxAllowed.value;
if (charCount.value > limit) { if (charCount.value > limit) {
form.setFieldValue( form.setFieldValue("message", truncateToLimit(val, limit, smsEncoding.value));
"message",
truncateToLimit(val, limit, smsEncoding.value)
);
} }
} }
); );
@ -248,28 +233,28 @@ watch(
watch( watch(
() => form.values.profile_id, () => form.values.profile_id,
(profileId) => { (profileId) => {
if (!profileId) { if (!profileId) {
form.setFieldValue("sender_id", null); form.setFieldValue("sender_id", null);
return;
}
const prof = (pageSmsProfiles.value || []).find((p) => p.id === profileId);
if (prof?.default_sender_id) {
const inList = sendersForSelectedProfile.value.find(
(s) => s.id === prof.default_sender_id
);
if (inList) {
form.setFieldValue("sender_id", prof.default_sender_id);
return; return;
} }
}
// Auto-select first sender if available const prof = (pageSmsProfiles.value || []).find((p) => p.id === profileId);
if (sendersForSelectedProfile.value.length > 0) { if (prof?.default_sender_id) {
form.setFieldValue("sender_id", sendersForSelectedProfile.value[0].id); const inList = sendersForSelectedProfile.value.find(
} else { (s) => s.id === prof.default_sender_id
form.setFieldValue("sender_id", null); );
} if (inList) {
form.setFieldValue("sender_id", prof.default_sender_id);
return;
}
}
// Auto-select first sender if available
if (sendersForSelectedProfile.value.length > 0) {
form.setFieldValue("sender_id", sendersForSelectedProfile.value[0].id);
} else {
form.setFieldValue("sender_id", null);
}
} }
); );
@ -299,14 +284,10 @@ const buildVarsFromSelectedContract = () => {
type: c.account.type, type: c.account.type,
initial_amount: initial_amount:
c.account.initial_amount ?? c.account.initial_amount ??
(c.account.initial_amount_raw (c.account.initial_amount_raw ? formatEu(c.account.initial_amount_raw) : null),
? formatEu(c.account.initial_amount_raw)
: null),
balance_amount: balance_amount:
c.account.balance_amount ?? c.account.balance_amount ??
(c.account.balance_amount_raw (c.account.balance_amount_raw ? formatEu(c.account.balance_amount_raw) : null),
? formatEu(c.account.balance_amount_raw)
: null),
initial_amount_raw: c.account.initial_amount_raw ?? null, initial_amount_raw: c.account.initial_amount_raw ?? null,
balance_amount_raw: c.account.balance_amount_raw ?? null, balance_amount_raw: c.account.balance_amount_raw ?? null,
}; };
@ -361,16 +342,16 @@ const updateSmsFromSelection = async () => {
watch( watch(
() => form.values.template_id, () => form.values.template_id,
() => { () => {
if (!form.values.template_id) return; if (!form.values.template_id) return;
updateSmsFromSelection(); updateSmsFromSelection();
} }
); );
watch( watch(
() => form.values.contract_uuid, () => form.values.contract_uuid,
() => { () => {
if (!form.values.template_id) return; if (!form.values.template_id) return;
updateSmsFromSelection(); updateSmsFromSelection();
} }
); );
@ -497,11 +478,7 @@ const open = computed({
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem :value="null"></SelectItem> <SelectItem :value="null"></SelectItem>
<SelectItem <SelectItem v-for="p in pageSmsProfiles" :key="p.id" :value="p.id">
v-for="p in pageSmsProfiles"
:key="p.id"
:value="p.id"
>
{{ p.name || "Profil #" + p.id }} {{ p.name || "Profil #" + p.id }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -546,11 +523,7 @@ const open = computed({
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem :value="null"></SelectItem> <SelectItem :value="null"></SelectItem>
<SelectItem <SelectItem v-for="c in contractsForCase" :key="c.uuid" :value="c.uuid">
v-for="c in contractsForCase"
:key="c.uuid"
:value="c.uuid"
>
{{ c.reference || c.uuid }} {{ c.reference || c.uuid }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -574,11 +547,7 @@ const open = computed({
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem :value="null"></SelectItem> <SelectItem :value="null"></SelectItem>
<SelectItem <SelectItem v-for="t in pageSmsTemplates" :key="t.id" :value="t.id">
v-for="t in pageSmsTemplates"
:key="t.id"
:value="t.id"
>
{{ t.name || "Predloga #" + t.id }} {{ t.name || "Predloga #" + t.id }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@ -626,14 +595,13 @@ const open = computed({
</span> </span>
</div> </div>
<p class="text-[11px] text-gray-500 leading-snug"> <p class="text-[11px] text-gray-500 leading-snug">
Dolžina 160 znakov velja samo pri pošiljanju sporočil, ki vsebujejo Dolžina 160 znakov velja samo pri pošiljanju sporočil, ki vsebujejo znake, ki
znake, ki ne zahtevajo enkodiranja. Če npr. želite pošiljati ne zahtevajo enkodiranja. Če npr. želite pošiljati šumnike, ki niso del
šumnike, ki niso del 7-bitne abecede GSM, morate uporabiti Unicode 7-bitne abecede GSM, morate uporabiti Unicode enkodiranje (UCS2). V tem
enkodiranje (UCS2). V tem primeru je največja dolžina enega SMS primeru je največja dolžina enega SMS sporočila 70 znakov (pri daljših
sporočila 70 znakov (pri daljših sporočilih 67 znakov na del), sporočilih 67 znakov na del), medtem ko je pri GSM7 160 znakov (pri daljših
medtem ko je pri GSM7 160 znakov (pri daljših sporočilih 153 sporočilih 153 znakov na del). Razširjeni znaki (^{{ "{" }}}}\\[]~| in )
znakov na del). Razširjeni znaki (^{{ "{" }}}}\\[]~| in ) štejejo štejejo dvojno. Največja dovoljena dolžina po ponudniku: 640 (GSM7) oziroma
dvojno. Največja dovoljena dolžina po ponudniku: 640 (GSM7) oziroma
320 (UCS2) znakov. 320 (UCS2) znakov.
</p> </p>
</div> </div>
@ -641,10 +609,7 @@ const open = computed({
<FormField v-slot="{ value, handleChange }" name="delivery_report"> <FormField v-slot="{ value, handleChange }" name="delivery_report">
<FormItem class="flex flex-row items-start space-x-3 space-y-0"> <FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl> <FormControl>
<Switch <Switch :model-value="value" @update:model-value="handleChange" />
:model-value="value"
@update:model-value="handleChange"
/>
</FormControl> </FormControl>
<div class="space-y-1 leading-none"> <div class="space-y-1 leading-none">
<FormLabel>Zahtevaj poročilo o dostavi</FormLabel> <FormLabel>Zahtevaj poročilo o dostavi</FormLabel>
@ -657,10 +622,7 @@ const open = computed({
<Button variant="outline" @click="closeSmsDialog" :disabled="processing"> <Button variant="outline" @click="closeSmsDialog" :disabled="processing">
Prekliči Prekliči
</Button> </Button>
<Button <Button @click="onSubmit" :disabled="processing || !form.values.message">
@click="onSubmit"
:disabled="processing || !form.values.message"
>
Pošlji Pošlji
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -33,7 +33,7 @@ const handleDelete = (id, label) => emit("delete", id, label);
<template> <template>
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<template v-if="getTRRs(person).length"> <template v-if="getTRRs(person).length">
<Card class="p-2" v-for="(acc, idx) in getTRRs(person)" :key="idx"> <Card class="p-2 gap-2" v-for="(acc, idx) in getTRRs(person)" :key="idx">
<div class="flex items-center justify-between mb-2" v-if="edit"> <div class="flex items-center justify-between mb-2" v-if="edit">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<span <span

View File

@ -0,0 +1,178 @@
<script setup lang="ts">
import type { SidebarProps } from "@/Components/ui/sidebar";
import {
AudioWaveform,
BookOpen,
Bot,
Command,
Frame,
GalleryVerticalEnd,
Map,
PieChart,
Settings2,
SquareTerminal,
} from "lucide-vue-next";
import NavMain from "@/Components/app/ui/layout/NavMain.vue";
import NavProjects from "@/Components/app/ui/layout/NavProjects.vue";
import NavUser from "@/Components/app/ui/layout/NavUser.vue";
import TeamSwitcher from "@/Components/app/ui/layout/TeamSwitcher.vue";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
} from "@/Components/ui/sidebar";
const props = withDefaults(defineProps<SidebarProps>(), {
collapsible: "icon",
});
// This is sample data.
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
teams: [
{
name: "Acme Inc",
logo: GalleryVerticalEnd,
plan: "Enterprise",
},
{
name: "Acme Corp.",
logo: AudioWaveform,
plan: "Startup",
},
{
name: "Evil Corp.",
logo: Command,
plan: "Free",
},
],
navMain: [
{
title: "Playground",
url: "#",
icon: SquareTerminal,
isActive: true,
items: [
{
title: "History",
url: "#",
},
{
title: "Starred",
url: "#",
},
{
title: "Settings",
url: "#",
},
],
},
{
title: "Models",
url: "#",
icon: Bot,
items: [
{
title: "Genesis",
url: "#",
},
{
title: "Explorer",
url: "#",
},
{
title: "Quantum",
url: "#",
},
],
},
{
title: "Documentation",
url: "#",
icon: BookOpen,
items: [
{
title: "Introduction",
url: "#",
},
{
title: "Get Started",
url: "#",
},
{
title: "Tutorials",
url: "#",
},
{
title: "Changelog",
url: "#",
},
],
},
{
title: "Settings",
url: "#",
icon: Settings2,
items: [
{
title: "General",
url: "#",
},
{
title: "Team",
url: "#",
},
{
title: "Billing",
url: "#",
},
{
title: "Limits",
url: "#",
},
],
},
],
projects: [
{
name: "Design Engineering",
url: "#",
icon: Frame,
},
{
name: "Sales & Marketing",
url: "#",
icon: PieChart,
},
{
name: "Travel",
url: "#",
icon: Map,
},
],
};
</script>
<template>
<Sidebar v-bind="props">
<SidebarHeader>
<TeamSwitcher :teams="data.teams" />
</SidebarHeader>
<SidebarContent>
<NavMain :items="data.navMain" />
<NavProjects :projects="data.projects" />
</SidebarContent>
<SidebarFooter>
<NavUser :user="data.user" />
</SidebarFooter>
<SidebarRail />
</Sidebar>
</template>

View File

@ -0,0 +1,70 @@
<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>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import type { LucideIcon } from "lucide-vue-next";
import { Folder, Forward, MoreHorizontal, Trash2 } from "lucide-vue-next";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/Components/ui/sidebar";
defineProps<{
projects: {
name: string;
url: string;
icon: LucideIcon;
}[];
}>();
const { isMobile } = useSidebar();
</script>
<template>
<SidebarGroup class="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem v-for="item in projects" :key="item.name">
<SidebarMenuButton as-child>
<a :href="item.url">
<component :is="item.icon" />
<span>{{ item.name }}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuAction show-on-hover>
<MoreHorizontal />
<span class="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-48 rounded-lg"
:side="isMobile ? 'bottom' : 'right'"
:align="isMobile ? 'end' : 'start'"
>
<DropdownMenuItem>
<Folder class="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward class="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 class="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton class="text-sidebar-foreground/70">
<MoreHorizontal class="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</template>

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} from "lucide-vue-next";
import { Avatar, AvatarFallback, AvatarImage } from "@/Components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/Components/ui/sidebar";
const props = defineProps<{
user: {
name: string;
email: string;
avatar: string;
};
}>();
const { isMobile } = useSidebar();
</script>
<template>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="user.avatar" :alt="user.name" />
<AvatarFallback class="rounded-lg"> CN </AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span>
<span class="truncate text-xs">{{ user.email }}</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--reka-dropdown-menu-trigger-width] min-w-56 rounded-lg"
:side="isMobile ? 'bottom' : 'right'"
align="end"
:side-offset="4"
>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage :src="user.avatar" :alt="user.name" />
<AvatarFallback class="rounded-lg"> CN </AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ user.name }}</span>
<span class="truncate text-xs">{{ user.email }}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</template>

View File

@ -0,0 +1,92 @@
<script setup lang="ts">
import type { Component } from "vue";
import { ChevronsUpDown, Plus } from "lucide-vue-next";
import { ref } from "vue";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/Components/ui/sidebar";
const props = defineProps<{
teams: {
name: string;
logo: Component;
plan: string;
}[];
}>();
const { isMobile } = useSidebar();
const activeTeam = ref(props.teams[0]);
</script>
<template>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
>
<component :is="activeTeam.logo" class="size-4" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">
{{ activeTeam.name }}
</span>
<span class="truncate text-xs">{{ activeTeam.plan }}</span>
</div>
<ChevronsUpDown class="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--reka-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
:side="isMobile ? 'bottom' : 'right'"
:side-offset="4"
>
<DropdownMenuLabel class="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
<DropdownMenuItem
v-for="(team, index) in teams"
:key="team.name"
class="gap-2 p-2"
@click="activeTeam = team"
>
<div class="flex size-6 items-center justify-center rounded-sm border">
<component :is="team.logo" class="size-3.5 shrink-0" />
</div>
{{ team.name }}
<DropdownMenuShortcut>{{ index + 1 }}</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem class="gap-2 p-2">
<div
class="flex size-6 items-center justify-center rounded-md border bg-transparent"
>
<Plus class="size-4" />
</div>
<div class="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { AvatarVariants } from "."
import { AvatarRoot } from "reka-ui"
import { cn } from "@/lib/utils"
import { avatarVariant } from "."
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
size?: AvatarVariants["size"]
shape?: AvatarVariants["shape"]
}>(), {
size: "sm",
shape: "circle",
})
</script>
<template>
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
<slot />
</AvatarRoot>
</template>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { AvatarFallbackProps } from "reka-ui"
import { AvatarFallback } from "reka-ui"
const props = defineProps<AvatarFallbackProps>()
</script>
<template>
<AvatarFallback v-bind="props">
<slot />
</AvatarFallback>
</template>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { AvatarImageProps } from "reka-ui"
import { AvatarImage } from "reka-ui"
const props = defineProps<AvatarImageProps>()
</script>
<template>
<AvatarImage v-bind="props" class="h-full w-full object-cover">
<slot />
</AvatarImage>
</template>

View File

@ -0,0 +1,25 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Avatar } from "./Avatar.vue"
export { default as AvatarFallback } from "./AvatarFallback.vue"
export { default as AvatarImage } from "./AvatarImage.vue"
export const avatarVariant = cva(
"inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden",
{
variants: {
size: {
sm: "h-10 w-10 text-xs",
base: "h-16 w-16 text-2xl",
lg: "h-32 w-32 text-5xl",
},
shape: {
circle: "rounded-full",
square: "rounded-md",
},
},
},
)
export type AvatarVariants = VariantProps<typeof avatarVariant>

View File

@ -0,0 +1,13 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<nav aria-label="breadcrumb" :class="props.class">
<slot />
</nav>
</template>

View File

@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { MoreHorizontal } from "lucide-vue-next"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
role="presentation"
aria-hidden="true"
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="h-4 w-4" />
</slot>
<span class="sr-only">More</span>
</span>
</template>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<li
:class="cn('inline-flex items-center gap-1.5', props.class)"
>
<slot />
</li>
</template>

View File

@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(), {
as: "a",
})
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('transition-colors hover:text-foreground', props.class)"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<ol
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)"
>
<slot />
</ol>
</template>

View File

@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
role="link"
aria-disabled="true"
aria-current="page"
:class="cn('font-normal text-foreground', props.class)"
>
<slot />
</span>
</template>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { ChevronRight } from "lucide-vue-next"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<li
role="presentation"
aria-hidden="true"
:class="cn('[&>svg]:size-3.5', props.class)"
>
<slot>
<ChevronRight />
</slot>
</li>
</template>

View File

@ -0,0 +1,7 @@
export { default as Breadcrumb } from "./Breadcrumb.vue"
export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue"
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue"
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue"
export { default as BreadcrumbList } from "./BreadcrumbList.vue"
export { default as BreadcrumbPage } from "./BreadcrumbPage.vue"
export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue"

View File

@ -1,15 +1,20 @@
<script setup> <script setup lang="ts">
import { Primitive } from "reka-ui"; import type { PrimitiveProps } from "reka-ui"
import { cn } from "@/lib/utils"; import type { HTMLAttributes } from "vue"
import { buttonVariants } from "."; import type { ButtonVariants } from "."
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from "."
const props = defineProps({ interface Props extends PrimitiveProps {
variant: { type: null, required: false }, variant?: ButtonVariants["variant"]
size: { type: null, required: false }, size?: ButtonVariants["size"]
class: { type: null, required: false }, class?: HTMLAttributes["class"]
asChild: { type: Boolean, required: false }, }
as: { type: null, required: false, default: "button" },
}); const props = withDefaults(defineProps<Props>(), {
as: "button",
})
</script> </script>
<template> <template>

View File

@ -0,0 +1,38 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Button } from "./Button.vue"
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
"icon": "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui"
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<CollapsibleRootProps>()
const emits = defineEmits<CollapsibleRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<CollapsibleRoot v-slot="{ open }" v-bind="forwarded">
<slot :open="open" />
</CollapsibleRoot>
</template>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { CollapsibleContentProps } from "reka-ui"
import { CollapsibleContent } from "reka-ui"
const props = defineProps<CollapsibleContentProps>()
</script>
<template>
<CollapsibleContent v-bind="props" class="overflow-hidden transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
<slot />
</CollapsibleContent>
</template>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { CollapsibleTriggerProps } from "reka-ui"
import { CollapsibleTrigger } from "reka-ui"
const props = defineProps<CollapsibleTriggerProps>()
</script>
<template>
<CollapsibleTrigger v-bind="props">
<slot />
</CollapsibleTrigger>
</template>

View File

@ -0,0 +1,3 @@
export { default as Collapsible } from "./Collapsible.vue";
export { default as CollapsibleContent } from "./CollapsibleContent.vue";
export { default as CollapsibleTrigger } from "./CollapsibleTrigger.vue";

View File

@ -0,0 +1,3 @@
export { default as Collapsible } from "./Collapsible.vue"
export { default as CollapsibleContent } from "./CollapsibleContent.vue"
export { default as CollapsibleTrigger } from "./CollapsibleTrigger.vue"

View File

@ -1,15 +1,11 @@
<script setup> <script setup lang="ts">
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui"; import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui"
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps({ const props = defineProps<DropdownMenuRootProps>()
defaultOpen: { type: Boolean, required: false }, const emits = defineEmits<DropdownMenuRootEmits>()
open: { type: Boolean, required: false },
dir: { type: String, required: false },
modal: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits); const forwarded = useForwardPropsEmits(props, emits)
</script> </script>
<template> <template>

View File

@ -1,37 +1,30 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
import { Check } from "lucide-vue-next"; import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import { import {
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuItemIndicator, DropdownMenuItemIndicator,
useForwardPropsEmits, useForwardPropsEmits,
} from "reka-ui"; } from "reka-ui"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
const props = defineProps({ const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
modelValue: { type: [Boolean, String], required: false }, const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["select", "update:modelValue"]);
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits); const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script> </script>
<template> <template>
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
v-bind="forwarded" v-bind="forwarded"
:class=" :class=" cn(
cn( 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', props.class,
props.class, )"
)
"
> >
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator> <DropdownMenuItemIndicator>

View File

@ -1,59 +1,32 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuPortal, DropdownMenuPortal,
useForwardPropsEmits, useForwardPropsEmits,
} from "reka-ui"; } from "reka-ui"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
const props = defineProps({ const props = withDefaults(
forceMount: { type: Boolean, required: false }, defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
loop: { type: Boolean, required: false }, {
side: { type: null, required: false }, sideOffset: 4,
sideOffset: { type: Number, required: false, default: 4 }, },
sideFlip: { type: Boolean, required: false }, )
align: { type: null, required: false }, const emits = defineEmits<DropdownMenuContentEmits>()
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"closeAutoFocus",
]);
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits); const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script> </script>
<template> <template>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent <DropdownMenuContent
v-bind="forwarded" v-bind="forwarded"
:class=" :class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)"
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
> >
<slot /> <slot />
</DropdownMenuContent> </DropdownMenuContent>

View File

@ -1,10 +1,8 @@
<script setup> <script setup lang="ts">
import { DropdownMenuGroup } from "reka-ui"; import type { DropdownMenuGroupProps } from "reka-ui"
import { DropdownMenuGroup } from "reka-ui"
const props = defineProps({ const props = defineProps<DropdownMenuGroupProps>()
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script> </script>
<template> <template>

View File

@ -1,32 +1,25 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { DropdownMenuItemProps } from "reka-ui"
import { DropdownMenuItem, useForwardProps } from "reka-ui"; import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"; import { reactiveOmit } from "@vueuse/core"
import { DropdownMenuItem, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps({ const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
});
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps); const forwardedProps = useForwardProps(delegatedProps)
</script> </script>
<template> <template>
<DropdownMenuItem <DropdownMenuItem
v-bind="forwardedProps" v-bind="forwardedProps"
:class=" :class="cn(
cn( 'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
'relative flex cursor-pointer select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0', inset && 'pl-8',
inset && 'pl-8', props.class,
props.class, )"
)
"
> >
<slot /> <slot />
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -1,26 +1,21 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { DropdownMenuLabelProps } from "reka-ui"
import { DropdownMenuLabel, useForwardProps } from "reka-ui"; import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"; import { reactiveOmit } from "@vueuse/core"
import { DropdownMenuLabel, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps({ const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
inset: { type: Boolean, required: false },
});
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps); const forwardedProps = useForwardProps(delegatedProps)
</script> </script>
<template> <template>
<DropdownMenuLabel <DropdownMenuLabel
v-bind="forwardedProps" v-bind="forwardedProps"
:class=" :class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)
"
> >
<slot /> <slot />
</DropdownMenuLabel> </DropdownMenuLabel>

View File

@ -1,14 +1,14 @@
<script setup> <script setup lang="ts">
import { DropdownMenuRadioGroup, useForwardPropsEmits } from "reka-ui"; import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui"
import {
DropdownMenuRadioGroup,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps({ const props = defineProps<DropdownMenuRadioGroupProps>()
modelValue: { type: String, required: false }, const emits = defineEmits<DropdownMenuRadioGroupEmits>()
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
const emits = defineEmits(["update:modelValue"]);
const forwarded = useForwardPropsEmits(props, emits); const forwarded = useForwardPropsEmits(props, emits)
</script> </script>
<template> <template>

View File

@ -1,38 +1,31 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
import { Circle } from "lucide-vue-next"; import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Circle } from "lucide-vue-next"
import { import {
DropdownMenuItemIndicator, DropdownMenuItemIndicator,
DropdownMenuRadioItem, DropdownMenuRadioItem,
useForwardPropsEmits, useForwardPropsEmits,
} from "reka-ui"; } from "reka-ui"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
const props = defineProps({ const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>()
value: { type: String, required: true },
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["select"]); const emits = defineEmits<DropdownMenuRadioItemEmits>()
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits); const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script> </script>
<template> <template>
<DropdownMenuRadioItem <DropdownMenuRadioItem
v-bind="forwarded" v-bind="forwarded"
:class=" :class="cn(
cn( 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', props.class,
props.class, )"
)
"
> >
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator> <DropdownMenuItemIndicator>

View File

@ -1,20 +1,19 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { DropdownMenuSeparatorProps } from "reka-ui"
import { DropdownMenuSeparator } from "reka-ui"; import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"; import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSeparator,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps({ const props = defineProps<DropdownMenuSeparatorProps & {
asChild: { type: Boolean, required: false }, class?: HTMLAttributes["class"]
as: { type: null, required: false }, }>()
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
</script> </script>
<template> <template>
<DropdownMenuSeparator <DropdownMenuSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
v-bind="delegatedProps"
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
/>
</template> </template>

View File

@ -1,9 +1,10 @@
<script setup> <script setup lang="ts">
import { cn } from "@/lib/utils"; import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps({ const props = defineProps<{
class: { type: null, required: false }, class?: HTMLAttributes["class"]
}); }>()
</script> </script>
<template> <template>

View File

@ -1,13 +1,14 @@
<script setup> <script setup lang="ts">
import { DropdownMenuSub, useForwardPropsEmits } from "reka-ui"; import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui"
import {
DropdownMenuSub,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps({ const props = defineProps<DropdownMenuSubProps>()
defaultOpen: { type: Boolean, required: false }, const emits = defineEmits<DropdownMenuSubEmits>()
open: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits); const forwarded = useForwardPropsEmits(props, emits)
</script> </script>
<template> <template>

View File

@ -1,54 +1,25 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
import { DropdownMenuSubContent, useForwardPropsEmits } from "reka-ui"; import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"; import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSubContent,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps({ const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
forceMount: { type: Boolean, required: false }, const emits = defineEmits<DropdownMenuSubContentEmits>()
loop: { type: Boolean, required: false },
sideOffset: { type: Number, required: false },
sideFlip: { type: Boolean, required: false },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"entryFocus",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits); const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script> </script>
<template> <template>
<DropdownMenuSubContent <DropdownMenuSubContent
v-bind="forwarded" v-bind="forwarded"
:class=" :class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)"
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
> >
<slot /> <slot />
</DropdownMenuSubContent> </DropdownMenuSubContent>

View File

@ -1,31 +1,28 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { DropdownMenuSubTriggerProps } from "reka-ui"
import { ChevronRight } from "lucide-vue-next"; import type { HTMLAttributes } from "vue"
import { DropdownMenuSubTrigger, useForwardProps } from "reka-ui"; import { reactiveOmit } from "@vueuse/core"
import { cn } from "@/lib/utils"; import { ChevronRight } from "lucide-vue-next"
import {
DropdownMenuSubTrigger,
useForwardProps,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps({ const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"] }>()
disabled: { type: Boolean, required: false },
textValue: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps); const forwardedProps = useForwardProps(delegatedProps)
</script> </script>
<template> <template>
<DropdownMenuSubTrigger <DropdownMenuSubTrigger
v-bind="forwardedProps" v-bind="forwardedProps"
:class=" :class="cn(
cn( 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent', props.class,
props.class, )"
)
"
> >
<slot /> <slot />
<ChevronRight class="ml-auto h-4 w-4" /> <ChevronRight class="ml-auto h-4 w-4" />

View File

@ -1,17 +1,14 @@
<script setup> <script setup lang="ts">
import { DropdownMenuTrigger, useForwardProps } from "reka-ui"; import type { DropdownMenuTriggerProps } from "reka-ui"
import { DropdownMenuTrigger, useForwardProps } from "reka-ui"
const props = defineProps({ const props = defineProps<DropdownMenuTriggerProps>()
disabled: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
const forwardedProps = useForwardProps(props); const forwardedProps = useForwardProps(props)
</script> </script>
<template> <template>
<DropdownMenuTrigger class="outline-none cursor-pointer" v-bind="forwardedProps"> <DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
<slot /> <slot />
</DropdownMenuTrigger> </DropdownMenuTrigger>
</template> </template>

View File

@ -0,0 +1,16 @@
export { default as DropdownMenu } from "./DropdownMenu.vue"
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
export { DropdownMenuPortal } from "reka-ui"

View File

@ -1,32 +1,33 @@
<script setup> <script setup lang="ts">
import { useVModel } from "@vueuse/core"; import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"; import { useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils"
const props = defineProps({ const props = defineProps<{
defaultValue: { type: [String, Number], required: false }, defaultValue?: string | number
modelValue: { type: [String, Number], required: false }, modelValue?: string | number
class: { type: null, required: false }, class?: HTMLAttributes["class"]
}); }>()
const emits = defineEmits(["update:modelValue"]); const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, "modelValue", emits, { const modelValue = useVModel(props, "modelValue", emits, {
passive: true, passive: true,
defaultValue: props.defaultValue, defaultValue: props.defaultValue,
}); })
</script> </script>
<template> <template>
<input <input
v-model="modelValue" v-model="modelValue"
data-slot="input" data-slot="input"
:class=" :class="cn(
cn( 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', props.class,
props.class, )"
) >
"
/>
</template> </template>

View File

@ -0,0 +1 @@
export { default as Input } from "./Input.vue"

View File

@ -3,7 +3,7 @@ import { reactiveOmit } from "@vueuse/core";
import { ChevronLeftIcon } from "lucide-vue-next"; import { ChevronLeftIcon } from "lucide-vue-next";
import { PaginationFirst, useForwardProps } from "reka-ui"; import { PaginationFirst, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button'; import { buttonVariants } from '@/Components/ui/button';
const props = defineProps({ const props = defineProps({
asChild: { type: Boolean, required: false }, asChild: { type: Boolean, required: false },

View File

@ -2,7 +2,7 @@
import { reactiveOmit } from "@vueuse/core"; import { reactiveOmit } from "@vueuse/core";
import { PaginationListItem } from "reka-ui"; import { PaginationListItem } from "reka-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button'; import { buttonVariants } from '@/Components/ui/button';
const props = defineProps({ const props = defineProps({
value: { type: Number, required: true }, value: { type: Number, required: true },

View File

@ -3,7 +3,7 @@ import { reactiveOmit } from "@vueuse/core";
import { ChevronRightIcon } from "lucide-vue-next"; import { ChevronRightIcon } from "lucide-vue-next";
import { PaginationLast, useForwardProps } from "reka-ui"; import { PaginationLast, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button'; import { buttonVariants } from '@/Components/ui/button';
const props = defineProps({ const props = defineProps({
asChild: { type: Boolean, required: false }, asChild: { type: Boolean, required: false },

View File

@ -3,7 +3,7 @@ import { reactiveOmit } from "@vueuse/core";
import { ChevronRightIcon } from "lucide-vue-next"; import { ChevronRightIcon } from "lucide-vue-next";
import { PaginationNext, useForwardProps } from "reka-ui"; import { PaginationNext, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button'; import { buttonVariants } from '@/Components/ui/button';
const props = defineProps({ const props = defineProps({
asChild: { type: Boolean, required: false }, asChild: { type: Boolean, required: false },

View File

@ -3,7 +3,7 @@ import { reactiveOmit } from "@vueuse/core";
import { ChevronLeftIcon } from "lucide-vue-next"; import { ChevronLeftIcon } from "lucide-vue-next";
import { PaginationPrev, useForwardProps } from "reka-ui"; import { PaginationPrev, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button'; import { buttonVariants } from '@/Components/ui/button';
const props = defineProps({ const props = defineProps({
asChild: { type: Boolean, required: false }, asChild: { type: Boolean, required: false },

View File

@ -2,7 +2,7 @@
import { reactiveOmit } from "@vueuse/core"; import { reactiveOmit } from "@vueuse/core";
import { RangeCalendarCellTrigger, useForwardProps } from "reka-ui"; import { RangeCalendarCellTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button'; import { buttonVariants } from '@/Components/ui/button';
const props = defineProps({ const props = defineProps({
day: { type: null, required: true }, day: { type: null, required: true },

View File

@ -3,7 +3,7 @@ import { reactiveOmit } from "@vueuse/core";
import { ChevronRight } from "lucide-vue-next"; import { ChevronRight } from "lucide-vue-next";
import { RangeCalendarNext, useForwardProps } from "reka-ui"; import { RangeCalendarNext, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button'; import { buttonVariants } from '@/Components/ui/button';
const props = defineProps({ const props = defineProps({
nextPage: { type: Function, required: false }, nextPage: { type: Function, required: false },

View File

@ -3,7 +3,7 @@ import { reactiveOmit } from "@vueuse/core";
import { ChevronLeft } from "lucide-vue-next"; import { ChevronLeft } from "lucide-vue-next";
import { RangeCalendarPrev, useForwardProps } from "reka-ui"; import { RangeCalendarPrev, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button'; import { buttonVariants } from '@/Components/ui/button';
const props = defineProps({ const props = defineProps({
prevPage: { type: Function, required: false }, prevPage: { type: Function, required: false },

View File

@ -1,17 +1,18 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { SeparatorProps } from "reka-ui"
import { Separator } from "reka-ui"; import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"; import { reactiveOmit } from "@vueuse/core"
import { Separator } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps({ const props = withDefaults(defineProps<
orientation: { type: String, required: false, default: "horizontal" }, SeparatorProps & { class?: HTMLAttributes["class"] }
decorative: { type: Boolean, required: false, default: true }, >(), {
asChild: { type: Boolean, required: false }, orientation: "horizontal",
as: { type: null, required: false }, decorative: true,
class: { type: null, required: false }, })
});
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
</script> </script>
<template> <template>

View File

@ -0,0 +1 @@
export { default as Separator } from "./Separator.vue"

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot
v-slot="slotProps"
data-slot="sheet"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DialogRoot>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogCloseProps } from "reka-ui"
import { DialogClose } from "reka-ui"
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose
data-slot="sheet-close"
v-bind="props"
>
<slot />
</DialogClose>
</template>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import SheetOverlay from "./SheetOverlay.vue"
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes["class"]
side?: "top" | "right" | "bottom" | "left"
}
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SheetContentProps>(), {
side: "right",
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class", "side")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<SheetOverlay />
<DialogContent
data-slot="sheet-content"
:class="cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right'
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left'
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top'
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom'
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
props.class)"
v-bind="{ ...$attrs, ...forwarded }"
>
<slot />
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogDescription
data-slot="sheet-description"
:class="cn('text-muted-foreground text-sm', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogOverlay
data-slot="sheet-overlay"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogOverlay>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogTitle
data-slot="sheet-title"
:class="cn('text-foreground font-semibold', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogTriggerProps } from "reka-ui"
import { DialogTrigger } from "reka-ui"
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger
data-slot="sheet-trigger"
v-bind="props"
>
<slot />
</DialogTrigger>
</template>

View File

@ -0,0 +1,8 @@
export { default as Sheet } from "./Sheet.vue";
export { default as SheetClose } from "./SheetClose.vue";
export { default as SheetContent } from "./SheetContent.vue";
export { default as SheetDescription } from "./SheetDescription.vue";
export { default as SheetFooter } from "./SheetFooter.vue";
export { default as SheetHeader } from "./SheetHeader.vue";
export { default as SheetTitle } from "./SheetTitle.vue";
export { default as SheetTrigger } from "./SheetTrigger.vue";

View File

@ -0,0 +1,8 @@
export { default as Sheet } from "./Sheet.vue"
export { default as SheetClose } from "./SheetClose.vue"
export { default as SheetContent } from "./SheetContent.vue"
export { default as SheetDescription } from "./SheetDescription.vue"
export { default as SheetFooter } from "./SheetFooter.vue"
export { default as SheetHeader } from "./SheetHeader.vue"
export { default as SheetTitle } from "./SheetTitle.vue"
export { default as SheetTrigger } from "./SheetTrigger.vue"

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
interface SkeletonProps {
class?: HTMLAttributes["class"]
}
const props = defineProps<SkeletonProps>()
</script>
<template>
<div
data-slot="skeleton"
:class="cn('animate-pulse rounded-md bg-primary/10', props.class)"
/>
</template>

View File

@ -0,0 +1 @@
export { default as Skeleton } from "./Skeleton.vue";

View File

@ -0,0 +1 @@
export { default as Skeleton } from "./Skeleton.vue"

View File

@ -1,22 +1,19 @@
<script setup> <script setup lang="ts">
import { TooltipRoot, useForwardPropsEmits } from "reka-ui"; import type { TooltipRootEmits, TooltipRootProps } from "reka-ui"
import { TooltipRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps({ const props = defineProps<TooltipRootProps>()
defaultOpen: { type: Boolean, required: false }, const emits = defineEmits<TooltipRootEmits>()
open: { type: Boolean, required: false },
delayDuration: { type: Number, required: false },
disableHoverableContent: { type: Boolean, required: false },
disableClosingTrigger: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
ignoreNonKeyboardFocus: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits); const forwarded = useForwardPropsEmits(props, emits)
</script> </script>
<template> <template>
<TooltipRoot v-bind="forwarded"> <TooltipRoot
<slot /> v-slot="slotProps"
data-slot="tooltip"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</TooltipRoot> </TooltipRoot>
</template> </template>

View File

@ -1,51 +1,34 @@
<script setup> <script setup lang="ts">
import { reactiveOmit } from "@vueuse/core"; import type { TooltipContentEmits, TooltipContentProps } from "reka-ui"
import { TooltipContent, TooltipPortal, useForwardPropsEmits } from "reka-ui"; import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"; import { reactiveOmit } from "@vueuse/core"
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({ defineOptions({
inheritAttrs: false, inheritAttrs: false,
}); })
const props = defineProps({ const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes["class"] }>(), {
forceMount: { type: Boolean, required: false }, sideOffset: 4,
ariaLabel: { type: String, required: false }, })
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false, default: 4 },
align: { type: null, required: false },
alignOffset: { type: Number, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["escapeKeyDown", "pointerDownOutside"]); const emits = defineEmits<TooltipContentEmits>()
const delegatedProps = reactiveOmit(props, "class"); const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script> </script>
<template> <template>
<TooltipPortal> <TooltipPortal>
<TooltipContent <TooltipContent
data-slot="tooltip-content"
v-bind="{ ...forwarded, ...$attrs }" v-bind="{ ...forwarded, ...$attrs }"
:class=" :class="cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance', props.class)"
cn(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
> >
<slot /> <slot />
<TooltipArrow class="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipContent> </TooltipContent>
</TooltipPortal> </TooltipPortal>
</template> </template>

View File

@ -1,14 +1,10 @@
<script setup> <script setup lang="ts">
import { TooltipProvider } from "reka-ui"; import type { TooltipProviderProps } from "reka-ui"
import { TooltipProvider } from "reka-ui"
const props = defineProps({ const props = withDefaults(defineProps<TooltipProviderProps>(), {
delayDuration: { type: Number, required: false }, delayDuration: 0,
skipDelayDuration: { type: Number, required: false }, })
disableHoverableContent: { type: Boolean, required: false },
disableClosingTrigger: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
ignoreNonKeyboardFocus: { type: Boolean, required: false },
});
</script> </script>
<template> <template>

View File

@ -1,15 +1,15 @@
<script setup> <script setup lang="ts">
import { TooltipTrigger } from "reka-ui"; import type { TooltipTriggerProps } from "reka-ui"
import { TooltipTrigger } from "reka-ui"
const props = defineProps({ const props = defineProps<TooltipTriggerProps>()
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
});
</script> </script>
<template> <template>
<TooltipTrigger v-bind="props"> <TooltipTrigger
data-slot="tooltip-trigger"
v-bind="props"
>
<slot /> <slot />
</TooltipTrigger> </TooltipTrigger>
</template> </template>

View File

@ -0,0 +1,4 @@
export { default as Tooltip } from "./Tooltip.vue"
export { default as TooltipContent } from "./TooltipContent.vue"
export { default as TooltipProvider } from "./TooltipProvider.vue"
export { default as TooltipTrigger } from "./TooltipTrigger.vue"

View File

@ -43,9 +43,9 @@ const fmtDateDMY = (v) => {
<template #header> </template> <template #header> </template>
<div class="py-12"> <div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg"> <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="mx-auto max-w-4x1 py-3"> <div class="mx-auto max-w-4x1 py-3">
<div class="pb-3"> <div class="pb-3 px-3">
<SectionTitle> <SectionTitle>
<template #title>Primeri</template> <template #title>Primeri</template>
</SectionTitle> </SectionTitle>

View File

@ -255,7 +255,7 @@ const submitAttachSegment = () => {
</span> </span>
</div> </div>
</div> </div>
<Card class="border-l-4 border-blue-400"> <Card class="border-l-4 border-blue-400 p-0">
<div class="p-3 flex justify-between items-center"> <div class="p-3 flex justify-between items-center">
<SectionTitle> <SectionTitle>
<template #title> <template #title>
@ -271,8 +271,8 @@ const submitAttachSegment = () => {
</div> </div>
<div class="pt-1" :hidden="clientDetails"> <div class="pt-1" :hidden="clientDetails">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card> <Card class="p-0">
<div class="mx-auto max-w-4x1 p-3"> <div class="p-3">
<PersonInfoGrid <PersonInfoGrid
:types="types" :types="types"
:person="client.person" :person="client.person"
@ -285,8 +285,8 @@ const submitAttachSegment = () => {
<!-- Case details --> <!-- Case details -->
<div class="pt-6"> <div class="pt-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card class="border-l-4 border-red-400"> <Card class="border-l-4 border-red-400 p-0">
<div class="mx-auto max-w-4x1 p-3 flex items-center justify-between"> <div class="p-3 flex items-center justify-between">
<SectionTitle> <SectionTitle>
<template #title>{{ client_case.person.full_name }}</template> <template #title>{{ client_case.person.full_name }}</template>
</SectionTitle> </SectionTitle>
@ -309,8 +309,8 @@ const submitAttachSegment = () => {
</div> </div>
<div class="pt-1"> <div class="pt-1">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card> <Card class="p-0">
<div class="mx-auto max-w-4x1 p-3"> <div class="p-3">
<PersonInfoGrid <PersonInfoGrid
:types="types" :types="types"
tab-color="red-600" tab-color="red-600"
@ -326,8 +326,8 @@ const submitAttachSegment = () => {
<!-- Contracts section --> <!-- Contracts section -->
<div class="pt-12"> <div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card> <Card class="p-0">
<div class="mx-auto max-w-4x1"> <div>
<div class="p-3"> <div class="p-3">
<SectionTitle> <SectionTitle>
<template #title> Pogodbe </template> <template #title> Pogodbe </template>
@ -369,9 +369,9 @@ const submitAttachSegment = () => {
<!-- Activities section --> <!-- Activities section -->
<div class="pt-12"> <div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card> <Card class="p-0">
<div class="mx-auto max-w-4x1"> <div>
<div class="flex justify-between p-4"> <div class="flex justify-between p-3">
<SectionTitle> <SectionTitle>
<template #title>Aktivnosti</template> <template #title>Aktivnosti</template>
</SectionTitle> </SectionTitle>
@ -410,8 +410,8 @@ const submitAttachSegment = () => {
<!-- Documents section --> <!-- Documents section -->
<div class="pt-12"> <div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card> <Card class="p-0">
<div class="mx-auto max-w-4x1"> <div>
<div class="p-4"> <div class="p-4">
<SectionTitle> <SectionTitle>
<template #title>Dokumenti</template> <template #title>Dokumenti</template>

View File

@ -132,7 +132,7 @@ function formatDate(value) {
<!-- Header card (matches Client/Show header style) --> <!-- Header card (matches Client/Show header style) -->
<div class="pt-6"> <div class="pt-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card class="border-l-4 border-blue-400"> <Card class="border-l-4 border-blue-400 p-0">
<div class="p-3 flex justify-between items-center"> <div class="p-3 flex justify-between items-center">
<SectionTitle> <SectionTitle>
<template #title> <template #title>
@ -146,8 +146,8 @@ function formatDate(value) {
</div> </div>
<div class="pt-1"> <div class="pt-1">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card> <Card class="p-0">
<div class="mx-auto max-w-4x1 p-3"> <div class="p-3">
<PersonInfoGrid <PersonInfoGrid
:types="types" :types="types"
:person="client.person" :person="client.person"

View File

@ -165,8 +165,8 @@ const fmtCurrency = (v) => {
<template #header> </template> <template #header> </template>
<div class="py-6"> <div class="py-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card> <Card class="p-0">
<CardHeader class="p-5"> <CardHeader>
<CardTitle>Naročniki</CardTitle> <CardTitle>Naročniki</CardTitle>
</CardHeader> </CardHeader>
<CardContent class="p-0"> <CardContent class="p-0">

View File

@ -63,7 +63,7 @@ function applySearch() {
<template #header></template> <template #header></template>
<div class="pt-6"> <div class="pt-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card class="border-l-4 border-blue-400"> <Card class="border-l-4 border-blue-400 p-0!">
<div class="p-3 flex justify-between items-center"> <div class="p-3 flex justify-between items-center">
<SectionTitle> <SectionTitle>
<template #title> <template #title>
@ -77,8 +77,8 @@ function applySearch() {
</div> </div>
<div class="pt-1"> <div class="pt-1">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Card> <Card class="p-0!">
<div class="mx-auto max-w-4x1 p-3"> <div class="p-3">
<PersonInfoGrid <PersonInfoGrid
:types="types" :types="types"
:person="client.person" :person="client.person"

View File

@ -1,620 +0,0 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { computed, ref, onMounted } from "vue";
import { usePage, Link } from "@inertiajs/vue3";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
faUsers,
faUserPlus,
faClipboardList,
faFileLines,
faCloudArrowUp,
faArrowUpRightFromSquare,
} from "@fortawesome/free-solid-svg-icons";
import { faFileContract } from "@fortawesome/free-solid-svg-icons";
const props = defineProps({
kpis: Object,
activities: Array,
trends: Object,
systemHealth: Object,
staleCases: Array,
fieldJobsAssignedToday: Array,
importsInProgress: Array,
activeTemplates: Array,
smsStats: Array,
});
const kpiDefs = [
{ key: "clients_total", label: "Vse stranke", icon: faUsers, route: "client" },
{ key: "clients_new_7d", label: "Nove (7d)", icon: faUserPlus, route: "client" },
{
key: "field_jobs_today",
label: "Terenske danes",
icon: faClipboardList,
route: "fieldjobs.index",
},
{
key: "documents_today",
label: "Dokumenti danes",
icon: faFileLines,
route: "clientCase",
},
{
key: "active_imports",
label: "Aktivni uvozi",
icon: faCloudArrowUp,
route: "imports.index",
},
{
key: "active_contracts",
label: "Aktivne pogodbe",
icon: faFileContract,
route: "clientCase",
},
];
const page = usePage();
// Simple sparkline path generator
function sparkline(values) {
if (!values || !values.length) {
return "";
}
const max = Math.max(...values) || 1;
const h = 24;
const w = 60;
const step = w / (values.length - 1 || 1);
return values
.map(
(v, i) =>
`${i === 0 ? "M" : "L"}${(i * step).toFixed(2)},${(h - (v / max) * h).toFixed(2)}`
)
.join(" ");
}
// Remove single relatedTarget helper and replace with multi-link builder
function buildRelated(a) {
const links = [];
// Only client case link (other routes not defined yet)
if (a.client_case_uuid || a.client_case_id) {
const caseParam = a.client_case_uuid || a.client_case_id;
try {
// Prefer Ziggy when available and force stringification here
const href = String(route("clientCase.show", { client_case: caseParam }));
links.push({
type: "client_case",
label: "Primer",
href,
});
} catch (e) {
// Safe fallback to a best-effort URL to avoid breaking render
links.push({
type: "client_case",
label: "Primer",
href: `/client-cases/${caseParam}`,
});
}
}
return links;
}
const activityItems = computed(() =>
(props.activities || []).map((a) => ({ ...a, links: buildRelated(a) }))
);
// Format stale days label: never negative; '<1 dan' if 0<=value<1; else integer with proper suffix.
function formatStaleDaysLabel(value) {
const num = Number.parseFloat(value);
if (Number.isNaN(num)) {
return "—";
}
if (num < 1) {
return "<1 dan";
}
const whole = Math.floor(num);
return whole === 1 ? "1 dan" : whole + " dni";
}
// Robust time formatter to avoid fixed 02:00:00 (timezone / fallback issues)
function formatJobTime(ts) {
if (!ts) return "";
try {
const d = new Date(ts);
if (isNaN(d.getTime())) return "";
// Show HH:MM (24h) and seconds only if non-zero seconds
const pad = (n) => n.toString().padStart(2, "0");
const h = pad(d.getHours());
const m = pad(d.getMinutes());
const s = d.getSeconds();
return s ? `${h}:${m}:${pad(s)}` : `${h}:${m}`;
} catch (e) {
return "";
}
}
// Safely build a client case href using Ziggy when available, with a plain fallback.
function safeCaseHref(uuid, segment = null) {
if (!uuid) {
return "#";
}
try {
const params = { client_case: uuid };
if (segment != null) {
params.segment = segment;
}
return String(route("clientCase.show", params));
} catch (e) {
return segment != null
? `/client-cases/${uuid}?segment=${segment}`
: `/client-cases/${uuid}`;
}
}
</script>
<template>
<AppLayout title="Nadzorna plošča">
<template #header> </template>
<div class="max-w-7xl mx-auto space-y-10 py-6">
<!-- KPI Cards with trends -->
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
<Link
v-for="k in kpiDefs"
:key="k.key"
:href="route(k.route)"
class="group relative bg-white border rounded-xl shadow-sm px-4 py-5 flex flex-col gap-3 hover:border-indigo-300 hover:shadow transition focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<div class="flex items-center justify-between">
<span
class="inline-flex items-center justify-center h-10 w-10 rounded-md bg-indigo-50 text-indigo-600 group-hover:bg-indigo-100"
>
<FontAwesomeIcon :icon="k.icon" class="w-5 h-5" />
</span>
<span class="text-[11px] text-gray-400 uppercase tracking-wide">{{
k.label
}}</span>
</div>
<div class="flex items-end gap-2">
<span class="text-2xl font-semibold tracking-tight text-gray-900">{{
props.kpis?.[k.key] ?? "—"
}}</span>
<span
class="text-[10px] text-indigo-500 opacity-0 group-hover:opacity-100 transition"
>Odpri </span
>
</div>
<div v-if="trends" class="mt-1 h-6">
<svg
v-if="k.key === 'clients_new_7d'"
:viewBox="'0 0 60 24'"
class="w-full h-6 overflow-visible"
>
<path
:d="sparkline(trends.clients_new)"
fill="none"
class="stroke-indigo-400"
stroke-width="2"
stroke-linejoin="round"
stroke-linecap="round"
/>
</svg>
<svg
v-else-if="k.key === 'documents_today'"
:viewBox="'0 0 60 24'"
class="w-full h-6"
>
<path
:d="sparkline(trends.documents_new)"
fill="none"
class="stroke-emerald-400"
stroke-width="2"
stroke-linejoin="round"
stroke-linecap="round"
/>
</svg>
<svg
v-else-if="k.key === 'field_jobs_today'"
:viewBox="'0 0 60 24'"
class="w-full h-6"
>
<path
:d="sparkline(trends.field_jobs)"
fill="none"
class="stroke-amber-400"
stroke-width="2"
stroke-linejoin="round"
stroke-linecap="round"
/>
</svg>
<svg
v-else-if="k.key === 'active_imports'"
:viewBox="'0 0 60 24'"
class="w-full h-6"
>
<path
:d="sparkline(trends.imports_new)"
fill="none"
class="stroke-fuchsia-400"
stroke-width="2"
stroke-linejoin="round"
stroke-linecap="round"
/>
</svg>
</div>
</Link>
</div>
<div class="grid lg:grid-cols-3 gap-8">
<!-- Activity Feed -->
<div class="lg:col-span-1 space-y-4">
<div class="bg-white border rounded-xl shadow-sm p-5 flex flex-col gap-4">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase">
Aktivnost
</h3>
</div>
<ul class="divide-y divide-gray-100 text-sm" v-if="activities">
<li
v-for="a in activityItems"
:key="a.id"
class="py-2 flex items-start gap-3"
>
<span class="w-2 h-2 mt-2 rounded-full bg-indigo-400" />
<div class="flex-1 min-w-0 space-y-1">
<p class="text-gray-700 line-clamp-2">
{{ a.note || "Dogodek" }}
</p>
<div class="flex flex-wrap items-center gap-2">
<span class="text-[11px] text-gray-400">{{
new Date(a.created_at).toLocaleString()
}}</span>
<Link
v-for="l in a.links"
:key="l.type + l.href"
:href="l.href"
class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 text-indigo-600 hover:bg-indigo-100 font-medium tracking-wide"
>{{ l.label }}</Link
>
</div>
</div>
</li>
<li
v-if="!activities?.length"
class="py-4 text-xs text-gray-500 text-center"
>
Ni zabeleženih aktivnosti.
</li>
</ul>
<ul v-else class="animate-pulse space-y-2">
<li v-for="n in 5" :key="n" class="h-5 bg-gray-100 rounded" />
</ul>
<div class="pt-1 flex justify-between items-center text-[11px]">
<Link
:href="route('dashboard')"
class="inline-flex items-center gap-1 font-medium text-indigo-600 hover:underline"
>Več kmalu
<FontAwesomeIcon :icon="faArrowUpRightFromSquare" class="w-3 h-3"
/></Link>
<span v-if="systemHealth" class="text-gray-400"
>Posodobljeno
{{ new Date(systemHealth.generated_at).toLocaleTimeString() }}</span
>
</div>
</div>
</div>
<!-- Right side panels -->
<div class="lg:col-span-2 space-y-8">
<!-- SMS Overview -->
<div class="bg-white border rounded-xl shadow-sm p-6">
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
SMS stanje
</h3>
<div v-if="props.smsStats?.length" class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider">
<tr>
<th class="px-3 py-2 text-left">Profil</th>
<th class="px-3 py-2 text-left">Bilanca</th>
<th class="px-3 py-2 text-left">Danes (skupaj)</th>
<th class="px-3 py-2 text-left">Sent</th>
<th class="px-3 py-2 text-left">Delivered</th>
<th class="px-3 py-2 text-left">Failed</th>
</tr>
</thead>
<tbody>
<tr
v-for="p in props.smsStats"
:key="p.id"
class="border-t last:border-b"
>
<td class="px-3 py-2">
<span class="font-medium text-gray-900">{{ p.name }}</span>
<span
class="ml-2 text-[11px]"
:class="p.active ? 'text-emerald-600' : 'text-gray-400'"
>{{ p.active ? "Aktiven" : "Neaktiven" }}</span
>
</td>
<td class="px-3 py-2 text-gray-700">
{{ p.balance ?? "—" }}
</td>
<td class="px-3 py-2">{{ p.today?.total ?? 0 }}</td>
<td class="px-3 py-2 text-sky-700">{{ p.today?.sent ?? 0 }}</td>
<td class="px-3 py-2 text-emerald-700">
{{ p.today?.delivered ?? 0 }}
</td>
<td class="px-3 py-2 text-rose-700">{{ p.today?.failed ?? 0 }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="text-sm text-gray-500">Ni podatkov o SMS.</div>
</div>
<!-- System Health -->
<div class="bg-white border rounded-xl shadow-sm p-6">
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
System Health
</h3>
<div
v-if="systemHealth"
class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm"
>
<div class="flex flex-col gap-1">
<span class="text-[11px] uppercase text-gray-400">Queue backlog</span>
<span class="font-semibold text-gray-800">{{
systemHealth.queue_backlog ?? "—"
}}</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-[11px] uppercase text-gray-400">Failed jobs</span>
<span class="font-semibold text-gray-800">{{
systemHealth.failed_jobs ?? "—"
}}</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-[11px] uppercase text-gray-400"
>Last activity (min)</span
>
<span
class="font-semibold text-gray-800"
:title="
systemHealth.last_activity_iso
? new Date(systemHealth.last_activity_iso).toLocaleString()
: ''
"
>{{
Math.max(0, parseInt(systemHealth.last_activity_minutes ?? 0))
}}</span
>
</div>
<div class="flex flex-col gap-1">
<span class="text-[11px] uppercase text-gray-400">Generated</span>
<span class="font-semibold text-gray-800">{{
new Date(systemHealth.generated_at).toLocaleTimeString()
}}</span>
</div>
</div>
<div v-else class="grid sm:grid-cols-4 gap-4 animate-pulse">
<div v-for="n in 4" :key="n" class="h-10 bg-gray-100 rounded" />
</div>
</div>
<!-- Completed Field Jobs Trend (7 dni) -->
<div class="bg-white border rounded-xl shadow-sm p-6">
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
Zaključena terenska dela (7 dni)
</h3>
<div v-if="trends" class="h-24">
<svg viewBox="0 0 140 60" class="w-full h-full">
<defs>
<linearGradient id="fjc" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.35" />
<stop offset="100%" stop-color="#6366f1" stop-opacity="0" />
</linearGradient>
</defs>
<path
v-if="trends.field_jobs_completed"
:d="sparkline(trends.field_jobs_completed)"
stroke="#6366f1"
stroke-width="2"
fill="none"
stroke-linejoin="round"
stroke-linecap="round"
/>
</svg>
<div class="mt-2 flex gap-2 text-[10px] text-gray-400">
<span
v-for="(l, i) in trends.labels"
:key="i"
class="flex-1 truncate text-center"
>{{ l.slice(5) }}</span
>
</div>
</div>
<div v-else class="h-24 animate-pulse bg-gray-100 rounded" />
</div>
<!-- Stale Cases -->
<div class="bg-white border rounded-xl shadow-sm p-6">
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
Stari primeri brez aktivnosti
</h3>
<ul v-if="staleCases" class="divide-y divide-gray-100 text-sm">
<li
v-for="c in staleCases"
:key="c.id"
class="py-2 flex items-center justify-between"
>
<div class="min-w-0">
<Link
v-if="c?.uuid"
:href="safeCaseHref(c.uuid)"
class="text-indigo-600 hover:underline font-medium"
>{{ c.client_ref || c.uuid.slice(0, 8) }}</Link
>
<span v-else class="text-gray-700 font-medium">{{
c.client_ref || "Primer"
}}</span>
<p class="text-[11px] text-gray-400">
Brez aktivnosti:
{{ formatStaleDaysLabel(c.days_without_activity ?? c.days_stale) }}
</p>
</div>
<span class="text-[10px] px-2 py-0.5 rounded bg-amber-50 text-amber-600"
>Stale</span
>
</li>
<li
v-if="!staleCases.length"
class="py-4 text-xs text-gray-500 text-center"
>
Ni starih primerov.
</li>
</ul>
<div v-else class="space-y-2 animate-pulse">
<div v-for="n in 5" :key="n" class="h-5 bg-gray-100 rounded" />
</div>
</div>
<!-- Field Jobs Assigned Today -->
<div class="bg-white border rounded-xl shadow-sm p-6">
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
Današnje dodelitve terenskih
</h3>
<ul v-if="fieldJobsAssignedToday" class="divide-y divide-gray-100 text-sm">
<li
v-for="f in fieldJobsAssignedToday"
:key="f.id"
class="py-2 flex items-start justify-between gap-3"
>
<div class="min-w-0 flex-1">
<p class="text-gray-700 text-sm font-medium">
#{{ f.id }}
<template v-if="f.contract">
·
<Link
v-if="f.contract.client_case_uuid"
:href="
safeCaseHref(f.contract.client_case_uuid, f.contract.segment_id)
"
class="text-indigo-600 hover:underline"
>
{{ f.contract.reference || f.contract.uuid?.slice(0, 8) }}
</Link>
<span v-else class="text-gray-700">{{
f.contract.reference || f.contract.uuid?.slice(0, 8)
}}</span>
<span v-if="f.contract.person_full_name" class="text-gray-500">
{{ f.contract.person_full_name }}
</span>
</template>
</p>
<p class="text-[11px] text-gray-400">
{{ formatJobTime(f.created_at) }}
</p>
</div>
<div class="flex items-center gap-2">
<span
v-if="f.priority"
class="text-[10px] px-2 py-0.5 rounded bg-rose-50 text-rose-600"
>Prioriteta</span
>
</div>
</li>
<li
v-if="!fieldJobsAssignedToday.length"
class="py-4 text-xs text-gray-500 text-center"
>
Ni dodelitev.
</li>
</ul>
<div v-else class="space-y-2 animate-pulse">
<div v-for="n in 5" :key="n" class="h-5 bg-gray-100 rounded" />
</div>
</div>
<!-- Imports In Progress -->
<div class="bg-white border rounded-xl shadow-sm p-6">
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
Uvozi v teku
</h3>
<ul v-if="importsInProgress" class="divide-y divide-gray-100 text-sm">
<li v-for="im in importsInProgress" :key="im.id" class="py-2 space-y-1">
<div class="flex items-center justify-between">
<p class="font-medium text-gray-700 truncate">
{{ im.file_name }}
</p>
<span
class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 text-indigo-600"
>{{ im.status }}</span
>
</div>
<div class="w-full h-2 bg-gray-100 rounded overflow-hidden">
<div
class="h-full bg-indigo-500"
:style="{ width: (im.progress_pct || 0) + '%' }"
></div>
</div>
<p class="text-[10px] text-gray-400">
{{ im.imported_rows }}/{{ im.total_rows }} (veljavnih:
{{ im.valid_rows }}, neveljavnih: {{ im.invalid_rows }})
</p>
</li>
<li
v-if="!importsInProgress.length"
class="py-4 text-xs text-gray-500 text-center"
>
Ni aktivnih uvozov.
</li>
</ul>
<div v-else class="space-y-2 animate-pulse">
<div v-for="n in 4" :key="n" class="h-5 bg-gray-100 rounded" />
</div>
</div>
<!-- Active Document Templates -->
<div class="bg-white border rounded-xl shadow-sm p-6">
<h3 class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4">
Aktivne predloge dokumentov
</h3>
<ul v-if="activeTemplates" class="divide-y divide-gray-100 text-sm">
<li
v-for="t in activeTemplates"
:key="t.id"
class="py-2 flex items-center justify-between"
>
<div class="min-w-0">
<p class="text-gray-700 font-medium truncate">
{{ t.name }}
</p>
<p class="text-[11px] text-gray-400">
v{{ t.version }} · {{ new Date(t.updated_at).toLocaleDateString() }}
</p>
</div>
<Link
:href="route('admin.document-templates.edit', t.id)"
class="text-[10px] px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 hover:bg-indigo-100"
>Uredi</Link
>
</li>
<li
v-if="!activeTemplates.length"
class="py-4 text-xs text-gray-500 text-center"
>
Ni aktivnih predlog.
</li>
</ul>
<div v-else class="space-y-2 animate-pulse">
<div v-for="n in 5" :key="n" class="h-5 bg-gray-100 rounded" />
</div>
</div>
<!-- ...end of right side panels -->
</div>
</div>
</div>
</AppLayout>
</template>

View File

@ -1,11 +1,11 @@
<script setup> <script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import SimpleKpiCard from "./Partials/SimpleKpiCard.vue"; import SimpleKpiCard from "./Partials/SimpleKpiCard.vue";
import ActivityFeed from "./Partials/ActivityFeed.vue"; import ActivityFeed from "./Partials/ActivityFeed.vue";
import SmsOverview from "./Partials/SmsOverview.vue"; import SmsOverview from "./Partials/SmsOverview.vue";
import CompletedFieldJobsTrend from "./Partials/CompletedFieldJobsTrend.vue"; import CompletedFieldJobsTrend from "./Partials/CompletedFieldJobsTrend.vue";
import FieldJobsAssignedToday from "./Partials/FieldJobsAssignedToday.vue"; import FieldJobsAssignedToday from "./Partials/FieldJobsAssignedToday.vue";
import { Users, FileText, Banknote, CalendarCheck } from "lucide-vue-next"; import { Users, FileText, Banknote, CalendarCheck } from "lucide-vue-next";
import AppLayout from "@/Layouts/AppLayout.vue";
const props = defineProps({ const props = defineProps({
kpis: Object, kpis: Object,
@ -41,6 +41,8 @@ const formatBalance = (amount) => {
label="Aktivni stranke" label="Aktivni stranke"
:value="kpis?.active_clients" :value="kpis?.active_clients"
:icon="Users" :icon="Users"
icon-bg="bg-chart-2/10"
icon-color="text-chart-2"
/> />
<SimpleKpiCard <SimpleKpiCard
label="Aktivne pogodbe" label="Aktivne pogodbe"

View File

@ -1,6 +1,7 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin'; import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@ -20,5 +21,9 @@ export default defineConfig({
css: { css: {
postcss: './postcss.config.js', postcss: './postcss.config.js',
}, },
// Default resolution resolve: {
alias: {
'@': path.resolve(__dirname, './resources/js'),
},
},
}); });