Basic Laravel project
Some checks failed
linter / quality (push) Successful in 3m37s
tests / ci (push) Failing after 8m4s

This commit is contained in:
2025-08-15 17:55:11 +02:00
commit 0eaf20efa7
249 changed files with 26846 additions and 0 deletions

167
resources/css/app.css Normal file
View File

@@ -0,0 +1,167 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-sans: Instrument Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar-background);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@layer utilities {
body,
html {
--font-sans:
'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
}
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(0 0% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(0 0% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(0 0% 3.9%);
--primary: hsl(0 0% 9%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(0 0% 92.1%);
--secondary-foreground: hsl(0 0% 9%);
--muted: hsl(0 0% 96.1%);
--muted-foreground: hsl(0 0% 45.1%);
--accent: hsl(0 0% 96.1%);
--accent-foreground: hsl(0 0% 9%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 92.8%);
--input: hsl(0 0% 89.8%);
--ring: hsl(0 0% 3.9%);
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
--radius: 0.5rem;
--sidebar-background: hsl(0 0% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(0 0% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(0 0% 94%);
--sidebar-accent-foreground: hsl(0 0% 30%);
--sidebar-border: hsl(0 0% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--sidebar: hsl(0 0% 98%);
}
.dark {
--background: hsl(0 0% 3.9%);
--foreground: hsl(0 0% 98%);
--card: hsl(0 0% 3.9%);
--card-foreground: hsl(0 0% 98%);
--popover: hsl(0 0% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(0 0% 9%);
--secondary: hsl(0 0% 14.9%);
--secondary-foreground: hsl(0 0% 98%);
--muted: hsl(0 0% 16.08%);
--muted-foreground: hsl(0 0% 63.9%);
--accent: hsl(0 0% 14.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 84% 60%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 14.9%);
--input: hsl(0 0% 14.9%);
--ring: hsl(0 0% 83.1%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
--sidebar-background: hsl(0 0% 7%);
--sidebar-foreground: hsl(0 0% 95.9%);
--sidebar-primary: hsl(360, 100%, 100%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(0 0% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(0 0% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--sidebar: hsl(240 5.9% 10%);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

27
resources/js/app.ts Normal file
View File

@@ -0,0 +1,27 @@
import '../css/app.css';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import type { DefineComponent } from 'vue';
import { createApp, h } from 'vue';
import { ZiggyVue } from 'ziggy-js';
import { initializeTheme } from './composables/useAppearance';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
resolve: (name) => resolvePageComponent(`./pages/${name}.vue`, import.meta.glob<DefineComponent>('./pages/**/*.vue')),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue)
.mount(el);
},
progress: {
color: '#4B5563',
},
});
// This will set light / dark mode on page load...
initializeTheme();

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { SidebarInset } from '@/components/ui/sidebar';
import { computed } from 'vue';
interface Props {
variant?: 'header' | 'sidebar';
class?: string;
}
const props = defineProps<Props>();
const className = computed(() => props.class);
</script>
<template>
<SidebarInset v-if="props.variant === 'sidebar'" :class="className">
<slot />
</SidebarInset>
<main v-else class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl" :class="className">
<slot />
</main>
</template>

View File

@@ -0,0 +1,186 @@
<script setup lang="ts">
import AppLogo from '@/components/AppLogo.vue';
import AppLogoIcon from '@/components/AppLogoIcon.vue';
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { NavigationMenu, NavigationMenuItem, NavigationMenuList, navigationMenuTriggerStyle } from '@/components/ui/navigation-menu';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import UserMenuContent from '@/components/UserMenuContent.vue';
import { getInitials } from '@/composables/useInitials';
import type { BreadcrumbItem, NavItem } from '@/types';
import { Link, usePage } from '@inertiajs/vue3';
import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-vue-next';
import { computed } from 'vue';
interface Props {
breadcrumbs?: BreadcrumbItem[];
}
const props = withDefaults(defineProps<Props>(), {
breadcrumbs: () => [],
});
const page = usePage();
const auth = computed(() => page.props.auth);
const isCurrentRoute = computed(() => (url: string) => page.url === url);
const activeItemStyles = computed(
() => (url: string) => (isCurrentRoute.value(url) ? 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100' : ''),
);
const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
href: '/dashboard',
icon: LayoutGrid,
},
];
const rightNavItems: NavItem[] = [
{
title: 'Repository',
href: 'https://github.com/laravel/vue-starter-kit',
icon: Folder,
},
{
title: 'Documentation',
href: 'https://laravel.com/docs/starter-kits#vue',
icon: BookOpen,
},
];
</script>
<template>
<div>
<div class="border-b border-sidebar-border/80">
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
<!-- Mobile Menu -->
<div class="lg:hidden">
<Sheet>
<SheetTrigger :as-child="true">
<Button variant="ghost" size="icon" class="mr-2 h-9 w-9">
<Menu class="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" class="w-[300px] p-6">
<SheetTitle class="sr-only">Navigation Menu</SheetTitle>
<SheetHeader class="flex justify-start text-left">
<AppLogoIcon class="size-6 fill-current text-black dark:text-white" />
</SheetHeader>
<div class="flex h-full flex-1 flex-col justify-between space-y-4 py-6">
<nav class="-mx-3 space-y-1">
<Link
v-for="item in mainNavItems"
:key="item.title"
:href="item.href"
class="flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent"
:class="activeItemStyles(item.href)"
>
<component v-if="item.icon" :is="item.icon" class="h-5 w-5" />
{{ item.title }}
</Link>
</nav>
<div class="flex flex-col space-y-4">
<a
v-for="item in rightNavItems"
:key="item.title"
:href="item.href"
target="_blank"
rel="noopener noreferrer"
class="flex items-center space-x-2 text-sm font-medium"
>
<component v-if="item.icon" :is="item.icon" class="h-5 w-5" />
<span>{{ item.title }}</span>
</a>
</div>
</div>
</SheetContent>
</Sheet>
</div>
<Link :href="route('dashboard')" class="flex items-center gap-x-2">
<AppLogo />
</Link>
<!-- Desktop Menu -->
<div class="hidden h-full lg:flex lg:flex-1">
<NavigationMenu class="ml-10 flex h-full items-stretch">
<NavigationMenuList class="flex h-full items-stretch space-x-2">
<NavigationMenuItem v-for="(item, index) in mainNavItems" :key="index" class="relative flex h-full items-center">
<Link
:class="[navigationMenuTriggerStyle(), activeItemStyles(item.href), 'h-9 cursor-pointer px-3']"
:href="item.href"
>
<component v-if="item.icon" :is="item.icon" class="mr-2 h-4 w-4" />
{{ item.title }}
</Link>
<div
v-if="isCurrentRoute(item.href)"
class="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"
></div>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
<div class="ml-auto flex items-center space-x-2">
<div class="relative flex items-center space-x-1">
<Button variant="ghost" size="icon" class="group h-9 w-9 cursor-pointer">
<Search class="size-5 opacity-80 group-hover:opacity-100" />
</Button>
<div class="hidden space-x-1 lg:flex">
<template v-for="item in rightNavItems" :key="item.title">
<TooltipProvider :delay-duration="0">
<Tooltip>
<TooltipTrigger>
<Button variant="ghost" size="icon" as-child class="group h-9 w-9 cursor-pointer">
<a :href="item.href" target="_blank" rel="noopener noreferrer">
<span class="sr-only">{{ item.title }}</span>
<component :is="item.icon" class="size-5 opacity-80 group-hover:opacity-100" />
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ item.title }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger :as-child="true">
<Button
variant="ghost"
size="icon"
class="relative size-10 w-auto rounded-full p-1 focus-within:ring-2 focus-within:ring-primary"
>
<Avatar class="size-8 overflow-hidden rounded-full">
<AvatarImage v-if="auth.user.avatar" :src="auth.user.avatar" :alt="auth.user.name" />
<AvatarFallback class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white">
{{ getInitials(auth.user?.name) }}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-56">
<UserMenuContent :user="auth.user" />
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<div v-if="props.breadcrumbs.length > 1" class="flex w-full border-b border-sidebar-border/70">
<div class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl">
<Breadcrumbs :breadcrumbs="breadcrumbs" />
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import AppLogoIcon from '@/components/AppLogoIcon.vue';
</script>
<template>
<div class="flex aspect-square size-8 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground">
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
</div>
<div class="ml-1 grid flex-1 text-left text-sm">
<span class="mb-0.5 truncate leading-tight font-semibold">Laravel Starter Kit</span>
</div>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
defineOptions({
inheritAttrs: false,
});
interface Props {
className?: HTMLAttributes['class'];
}
defineProps<Props>();
</script>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" :class="className" v-bind="$attrs">
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z"
/>
</svg>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { SidebarProvider } from '@/components/ui/sidebar';
import { usePage } from '@inertiajs/vue3';
interface Props {
variant?: 'header' | 'sidebar';
}
defineProps<Props>();
const isOpen = usePage().props.sidebarOpen;
</script>
<template>
<div v-if="variant === 'header'" class="flex min-h-screen w-full flex-col">
<slot />
</div>
<SidebarProvider v-else :default-open="isOpen">
<slot />
</SidebarProvider>
</template>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import NavFooter from '@/components/NavFooter.vue';
import NavMain from '@/components/NavMain.vue';
import NavUser from '@/components/NavUser.vue';
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { type NavItem } from '@/types';
import { Link } from '@inertiajs/vue3';
import { LayoutGrid } from 'lucide-vue-next';
import AppLogo from './AppLogo.vue';
const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
href: '/dashboard',
icon: LayoutGrid,
},
];
</script>
<template>
<Sidebar collapsible="icon" variant="inset">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" as-child>
<Link :href="route('dashboard')">
<AppLogo />
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain :items="mainNavItems" />
</SidebarContent>
<SidebarFooter>
<NavUser />
</SidebarFooter>
</Sidebar>
<slot />
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import { SidebarTrigger } from '@/components/ui/sidebar';
import type { BreadcrumbItemType } from '@/types';
withDefaults(
defineProps<{
breadcrumbs?: BreadcrumbItemType[];
}>(),
{
breadcrumbs: () => [],
},
);
</script>
<template>
<header
class="flex h-16 shrink-0 items-center gap-2 border-b border-sidebar-border/70 px-6 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:px-4"
>
<div class="flex items-center gap-2">
<SidebarTrigger class="-ml-1" />
<template v-if="breadcrumbs && breadcrumbs.length > 0">
<Breadcrumbs :breadcrumbs="breadcrumbs" />
</template>
</div>
</header>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { useAppearance } from '@/composables/useAppearance';
import { Monitor, Moon, Sun } from 'lucide-vue-next';
const { appearance, updateAppearance } = useAppearance();
const tabs = [
{ value: 'light', Icon: Sun, label: 'Light' },
{ value: 'dark', Icon: Moon, label: 'Dark' },
{ value: 'system', Icon: Monitor, label: 'System' },
] as const;
</script>
<template>
<div class="inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800">
<button
v-for="{ value, Icon, label } in tabs"
:key="value"
@click="updateAppearance(value)"
:class="[
'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
appearance === value
? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
: 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60',
]"
>
<component :is="Icon" class="-ml-1 h-4 w-4" />
<span class="ml-1.5 text-sm">{{ label }}</span>
</button>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb';
import { Link } from '@inertiajs/vue3';
interface BreadcrumbItemType {
title: string;
href?: string;
}
defineProps<{
breadcrumbs: BreadcrumbItemType[];
}>();
</script>
<template>
<Breadcrumb>
<BreadcrumbList>
<template v-for="(item, index) in breadcrumbs" :key="index">
<BreadcrumbItem>
<template v-if="index === breadcrumbs.length - 1">
<BreadcrumbPage>{{ item.title }}</BreadcrumbPage>
</template>
<template v-else>
<BreadcrumbLink as-child>
<Link :href="item.href ?? '#'">{{ item.title }}</Link>
</BreadcrumbLink>
</template>
</BreadcrumbItem>
<BreadcrumbSeparator v-if="index !== breadcrumbs.length - 1" />
</template>
</BreadcrumbList>
</Breadcrumb>
</template>

View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
// Components
import HeadingSmall from '@/components/HeadingSmall.vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
const passwordInput = ref<HTMLInputElement | null>(null);
const form = useForm({
password: '',
});
const deleteUser = (e: Event) => {
e.preventDefault();
form.delete(route('profile.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value?.focus(),
onFinish: () => form.reset(),
});
};
const closeModal = () => {
form.clearErrors();
form.reset();
};
</script>
<template>
<div class="space-y-6">
<HeadingSmall title="Delete account" description="Delete your account and all of its resources" />
<div class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
<div class="relative space-y-0.5 text-red-600 dark:text-red-100">
<p class="font-medium">Warning</p>
<p class="text-sm">Please proceed with caution, this cannot be undone.</p>
</div>
<Dialog>
<DialogTrigger as-child>
<Button variant="destructive">Delete account</Button>
</DialogTrigger>
<DialogContent>
<form method="POST" class="space-y-6" @submit="deleteUser">
<DialogHeader class="space-y-3">
<DialogTitle>Are you sure you want to delete your account?</DialogTitle>
<DialogDescription>
Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your
password to confirm you would like to permanently delete your account.
</DialogDescription>
</DialogHeader>
<div class="grid gap-2">
<Label for="password" class="sr-only">Password</Label>
<Input id="password" type="password" name="password" ref="passwordInput" v-model="form.password" placeholder="Password" />
<InputError :message="form.errors.password" />
</div>
<DialogFooter class="gap-2">
<DialogClose as-child>
<Button variant="secondary" @click="closeModal"> Cancel </Button>
</DialogClose>
<Button type="submit" variant="destructive" :disabled="form.processing"> Delete account </Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
interface Props {
title: string;
description?: string;
}
defineProps<Props>();
</script>
<template>
<div class="mb-8 space-y-0.5">
<h2 class="text-xl font-semibold tracking-tight">{{ title }}</h2>
<p v-if="description" class="text-sm text-muted-foreground">
{{ description }}
</p>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
interface Props {
title: string;
description?: string;
}
defineProps<Props>();
</script>
<template>
<header>
<h3 class="mb-0.5 text-base font-medium">{{ title }}</h3>
<p v-if="description" class="text-sm text-muted-foreground">
{{ description }}
</p>
</header>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import * as icons from 'lucide-vue-next';
import { computed } from 'vue';
interface Props {
name: string;
class?: string;
size?: number | string;
color?: string;
strokeWidth?: number | string;
}
const props = withDefaults(defineProps<Props>(), {
class: '',
size: 16,
strokeWidth: 2,
});
const className = computed(() => cn('h-4 w-4', props.class));
const icon = computed(() => {
const iconName = props.name.charAt(0).toUpperCase() + props.name.slice(1);
return (icons as Record<string, any>)[iconName];
});
</script>
<template>
<component :is="icon" :class="className" :size="size" :stroke-width="strokeWidth" :color="color" />
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
defineProps<{
message?: string;
}>();
</script>
<template>
<div v-show="message">
<p class="text-sm text-red-600 dark:text-red-500">
{{ message }}
</p>
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { type NavItem } from '@/types';
interface Props {
items: NavItem[];
class?: string;
}
defineProps<Props>();
</script>
<template>
<SidebarGroup :class="`group-data-[collapsible=icon]:p-0 ${$props.class || ''}`">
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem v-for="item in items" :key="item.title">
<SidebarMenuButton class="text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100" as-child>
<a :href="item.href" target="_blank" rel="noopener noreferrer">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { type NavItem } from '@/types';
import { Link, usePage } from '@inertiajs/vue3';
defineProps<{
items: NavItem[];
}>();
const page = usePage();
</script>
<template>
<SidebarGroup class="px-2 py-0">
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem v-for="item in items" :key="item.title">
<SidebarMenuButton as-child :is-active="item.href === page.url" :tooltip="item.title">
<Link :href="item.href">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</template>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import UserInfo from '@/components/UserInfo.vue';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar';
import { type User } from '@/types';
import { usePage } from '@inertiajs/vue3';
import { ChevronUp } from 'lucide-vue-next';
import UserMenuContent from './UserMenuContent.vue';
const page = usePage();
const user = page.props.auth.user as User;
const { isMobile, state } = useSidebar();
</script>
<template>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton size="lg" class="group data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<UserInfo :user="user" />
<ChevronUp class="transition group-data-[state=open]:rotate-180 ml-auto size-4" data-icon />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
:side="isMobile ? 'bottom' : state === 'collapsed' ? 'left' : 'bottom'"
align="end"
:side-offset="4"
>
<UserMenuContent :user="user" />
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue';
const patternId = computed(() => `pattern-${Math.random().toString(36).substring(2, 9)}`);
</script>
<template>
<svg class="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" fill="none">
<defs>
<pattern :id="patternId" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
<path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
</pattern>
</defs>
<rect stroke="none" :fill="`url(#${patternId})`" width="100%" height="100%"></rect>
</svg>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import { Method } from '@inertiajs/core';
import { Link } from '@inertiajs/vue3';
interface Props {
href: string;
tabindex?: number;
method?: Method;
as?: string;
}
defineProps<Props>();
</script>
<template>
<Link
:href="href"
:tabindex="tabindex"
:method="method"
:as="as"
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
>
<slot />
</Link>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useInitials } from '@/composables/useInitials';
import type { User } from '@/types';
import { computed } from 'vue';
interface Props {
user: User;
showEmail?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showEmail: false,
});
const { getInitials } = useInitials();
// Compute whether we should show the avatar image
const showAvatar = computed(() => props.user.avatar && props.user.avatar !== '');
</script>
<template>
<Avatar class="h-8 w-8 overflow-hidden rounded-lg">
<AvatarImage v-if="showAvatar" :src="user.avatar!" :alt="user.name" />
<AvatarFallback class="rounded-lg text-black dark:text-white">
{{ getInitials(user.name) }}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{{ user.name }}</span>
<span v-if="showEmail" class="truncate text-xs text-muted-foreground">{{ user.email }}</span>
</div>
</template>

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import UserInfo from '@/components/UserInfo.vue';
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
import type { User } from '@/types';
import { Link, router } from '@inertiajs/vue3';
import { LogOut, Settings } from 'lucide-vue-next';
interface Props {
user: User;
}
const handleLogout = () => {
router.flushAll();
};
defineProps<Props>();
</script>
<template>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserInfo :user="user" :show-email="true" />
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem :as-child="true">
<Link class="block w-full" :href="route('profile.edit')" prefetch as="button">
<Settings class="mr-2 h-4 w-4" />
Settings
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem :as-child="true">
<Link class="block w-full" method="post" :href="route('logout')" @click="handleLogout" as="button">
<LogOut class="mr-2 h-4 w-4" />
Log out
</Link>
</DropdownMenuItem>
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { AvatarRoot } from 'reka-ui'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<AvatarRoot
data-slot="avatar"
:class="cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', props.class)"
>
<slot />
</AvatarRoot>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { AvatarFallback, type AvatarFallbackProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<AvatarFallbackProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<AvatarFallback
data-slot="avatar-fallback"
v-bind="delegatedProps"
:class="cn('bg-muted flex size-full items-center justify-center rounded-full', props.class)"
>
<slot />
</AvatarFallback>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { AvatarImageProps } from 'reka-ui'
import { AvatarImage } from 'reka-ui'
const props = defineProps<AvatarImageProps>()
</script>
<template>
<AvatarImage
data-slot="avatar-image"
v-bind="props"
class="aspect-square size-full"
>
<slot />
</AvatarImage>
</template>

View File

@@ -0,0 +1,3 @@
export { default as Avatar } from './Avatar.vue'
export { default as AvatarFallback } from './AvatarFallback.vue'
export { default as AvatarImage } from './AvatarImage.vue'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { ChevronRight } from 'lucide-vue-next'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<li
data-slot="breadcrumb-separator"
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

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'reka-ui'
import { type ButtonVariants, buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>
<template>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,36 @@
import { cva, type VariantProps } 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 shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs 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 shadow-xs 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',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View File

@@ -0,0 +1,22 @@
<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="card"
:class="
cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<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="card-action"
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<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="card-content"
:class="cn('px-6', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<p
data-slot="card-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>

View File

@@ -0,0 +1,17 @@
<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="card-footer"
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<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="card-header"
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<h3
data-slot="card-title"
:class="cn('leading-none font-semibold', props.class)"
>
<slot />
</h3>
</template>

View File

@@ -0,0 +1,7 @@
export { default as Card } from './Card.vue'
export { default as CardAction } from './CardAction.vue'
export { default as CardContent } from './CardContent.vue'
export { default as CardDescription } from './CardDescription.vue'
export { default as CardFooter } from './CardFooter.vue'
export { default as CardHeader } from './CardHeader.vue'
export { default as CardTitle } from './CardTitle.vue'

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { Check } from 'lucide-vue-next'
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CheckboxRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CheckboxRoot
data-slot="checkbox"
v-bind="forwarded"
:class="
cn('peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
props.class)"
>
<CheckboxIndicator
data-slot="checkbox-indicator"
class="flex items-center justify-center text-current transition-none"
>
<slot>
<Check class="size-3.5" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>

View File

@@ -0,0 +1 @@
export { default as Checkbox } from './Checkbox.vue'

View File

@@ -0,0 +1,19 @@
<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 }"
data-slot="collapsible"
v-bind="forwarded"
>
<slot :open="open" />
</CollapsibleRoot>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { CollapsibleContent, type CollapsibleContentProps } from 'reka-ui'
const props = defineProps<CollapsibleContentProps>()
</script>
<template>
<CollapsibleContent
data-slot="collapsible-content"
v-bind="props"
>
<slot />
</CollapsibleContent>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { CollapsibleTrigger, type CollapsibleTriggerProps } from 'reka-ui'
const props = defineProps<CollapsibleTriggerProps>()
</script>
<template>
<CollapsibleTrigger
data-slot="collapsible-trigger"
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,17 @@
<script setup lang="ts">
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot
data-slot="dialog"
v-bind="forwarded"
>
<slot />
</DialogRoot>
</template>

View File

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

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { X } from 'lucide-vue-next'
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import DialogOverlay from './DialogOverlay.vue'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay />
<DialogContent
data-slot="dialog-content"
v-bind="forwarded"
:class="
cn(
'bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class,
)"
>
<slot />
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground 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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<X />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogDescription
data-slot="dialog-description"
v-bind="forwardedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</DialogDescription>
</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="dialog-footer"
:class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<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="dialog-header"
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { DialogOverlay, type DialogOverlayProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DialogOverlay
data-slot="dialog-overlay"
v-bind="delegatedProps"
: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)"
>
<slot />
</DialogOverlay>
</template>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { X } from 'lucide-vue-next'
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class,
)
"
v-bind="forwarded"
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
<slot />
<DialogClose
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { DialogTitle, type DialogTitleProps, useForwardProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogTitle
data-slot="dialog-title"
v-bind="forwardedProps"
:class="cn('text-lg leading-none font-semibold', props.class)"
>
<slot />
</DialogTitle>
</template>

View File

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

View File

@@ -0,0 +1,10 @@
export { default as Dialog } from './Dialog.vue'
export { default as DialogClose } from './DialogClose.vue'
export { default as DialogContent } from './DialogContent.vue'
export { default as DialogDescription } from './DialogDescription.vue'
export { default as DialogFooter } from './DialogFooter.vue'
export { default as DialogHeader } from './DialogHeader.vue'
export { default as DialogOverlay } from './DialogOverlay.vue'
export { default as DialogScrollContent } from './DialogScrollContent.vue'
export { default as DialogTitle } from './DialogTitle.vue'
export { default as DialogTrigger } from './DialogTrigger.vue'

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { DropdownMenuRoot, type DropdownMenuRootEmits, type DropdownMenuRootProps, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DropdownMenuRootProps>()
const emits = defineEmits<DropdownMenuRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRoot
data-slot="dropdown-menu"
v-bind="forwarded"
>
<slot />
</DropdownMenuRoot>
</template>

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Check } from 'lucide-vue-next'
import {
DropdownMenuCheckboxItem,
type DropdownMenuCheckboxItemEmits,
type DropdownMenuCheckboxItemProps,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuCheckboxItem
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class=" cn(
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Check class="size-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
DropdownMenuContent,
type DropdownMenuContentEmits,
type DropdownMenuContentProps,
DropdownMenuPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
{
sideOffset: 4,
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground 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 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'reka-ui'
const props = defineProps<DropdownMenuGroupProps>()
</script>
<template>
<DropdownMenuGroup
data-slot="dropdown-menu-group"
v-bind="props"
>
<slot />
</DropdownMenuGroup>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'reka-ui'
const props = withDefaults(defineProps<DropdownMenuItemProps & {
class?: HTMLAttributes['class']
inset?: boolean
variant?: 'default' | 'destructive'
}>(), {
variant: 'default',
})
const delegatedProps = reactiveOmit(props, 'inset', 'variant')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuItem
data-slot="dropdown-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="cn(`focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`, props.class)"
>
<slot />
</DropdownMenuItem>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'reka-ui'
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, 'class', 'inset')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuLabel
data-slot="dropdown-menu-label"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import {
DropdownMenuRadioGroup,
type DropdownMenuRadioGroupEmits,
type DropdownMenuRadioGroupProps,
useForwardPropsEmits,
} from 'reka-ui'
const props = defineProps<DropdownMenuRadioGroupProps>()
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRadioGroup
data-slot="dropdown-menu-radio-group"
v-bind="forwarded"
>
<slot />
</DropdownMenuRadioGroup>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Circle } from 'lucide-vue-next'
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
type DropdownMenuRadioItemEmits,
type DropdownMenuRadioItemProps,
useForwardPropsEmits,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuRadioItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuRadioItem
data-slot="dropdown-menu-radio-item"
v-bind="forwarded"
:class="cn(
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Circle class="size-2 fill-current" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
DropdownMenuSeparator,
type DropdownMenuSeparatorProps,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DropdownMenuSeparatorProps & {
class?: HTMLAttributes['class']
}>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DropdownMenuSeparator
data-slot="dropdown-menu-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
/>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span
data-slot="dropdown-menu-shortcut"
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
>
<slot />
</span>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DropdownMenuSub,
type DropdownMenuSubEmits,
type DropdownMenuSubProps,
useForwardPropsEmits,
} from 'reka-ui'
const props = defineProps<DropdownMenuSubProps>()
const emits = defineEmits<DropdownMenuSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuSub data-slot="dropdown-menu-sub" v-bind="forwarded">
<slot />
</DropdownMenuSub>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
DropdownMenuSubContent,
type DropdownMenuSubContentEmits,
type DropdownMenuSubContentProps,
useForwardPropsEmits,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
>
<slot />
</DropdownMenuSubContent>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import { ChevronRight } from 'lucide-vue-next'
import {
DropdownMenuSubTrigger,
type DropdownMenuSubTriggerProps,
useForwardProps,
} from 'reka-ui'
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, 'class', 'inset')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { DropdownMenuTrigger, type DropdownMenuTriggerProps, useForwardProps } from 'reka-ui'
const props = defineProps<DropdownMenuTriggerProps>()
const forwardedProps = useForwardProps(props)
</script>
<template>
<DropdownMenuTrigger
data-slot="dropdown-menu-trigger"
v-bind="forwardedProps"
>
<slot />
</DropdownMenuTrigger>
</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

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useVModel } from '@vueuse/core'
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes['class']
}>()
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<input
v-model="modelValue"
data-slot="input"
:class="cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex 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]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class,
)"
>
</template>

View File

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

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Label, type LabelProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<Label
data-slot="label"
v-bind="delegatedProps"
:class="
cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
props.class,
)
"
>
<slot />
</Label>
</template>

View File

@@ -0,0 +1 @@
export { default as Label } from './Label.vue'

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import {
NavigationMenuRoot,
type NavigationMenuRootEmits,
type NavigationMenuRootProps,
useForwardPropsEmits,
} from 'reka-ui'
import NavigationMenuViewport from './NavigationMenuViewport.vue'
const props = withDefaults(defineProps<NavigationMenuRootProps & {
class?: HTMLAttributes['class']
viewport?: boolean
}>(), {
viewport: true,
})
const emits = defineEmits<NavigationMenuRootEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'viewport')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<NavigationMenuRoot
data-slot="navigation-menu"
:data-viewport="viewport"
v-bind="forwarded"
:class="cn('group/navigation-menu relative flex max-w-max flex-1 items-center justify-center', props.class)"
>
<slot />
<NavigationMenuViewport v-if="viewport" />
</NavigationMenuRoot>
</template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
NavigationMenuContent,
type NavigationMenuContentEmits,
type NavigationMenuContentProps,
useForwardPropsEmits,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<NavigationMenuContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<NavigationMenuContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<NavigationMenuContent
data-slot="navigation-menu-content"
v-bind="forwarded"
:class="cn(
`data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto`,
`group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none`,
props.class,
)"
>
<slot />
</NavigationMenuContent>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { NavigationMenuIndicator, type NavigationMenuIndicatorProps, useForwardProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<NavigationMenuIndicatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<NavigationMenuIndicator
data-slot="navigation-menu-indicator"
v-bind="forwardedProps"
:class="cn('data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden', props.class)"
>
<div class="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuIndicator>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import { NavigationMenuItem, type NavigationMenuItemProps } from 'reka-ui'
const props = defineProps<NavigationMenuItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<NavigationMenuItem
data-slot="navigation-menu-item"
v-bind="delegatedProps"
:class="cn('relative', props.class)"
>
<slot />
</NavigationMenuItem>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import {
NavigationMenuLink,
type NavigationMenuLinkEmits,
type NavigationMenuLinkProps,
useForwardPropsEmits,
} from 'reka-ui'
const props = defineProps<NavigationMenuLinkProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<NavigationMenuLinkEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<NavigationMenuLink
data-slot="navigation-menu-link"
v-bind="forwarded"
:class="cn(`'data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4'`, props.class)"
>
<slot />
</NavigationMenuLink>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { NavigationMenuList, type NavigationMenuListProps, useForwardProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<NavigationMenuListProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<NavigationMenuList
data-slot="navigation-menu-list"
v-bind="forwardedProps"
:class="
cn(
'group flex flex-1 list-none items-center justify-center gap-1',
props.class,
)
"
>
<slot />
</NavigationMenuList>
</template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { ChevronDown } from 'lucide-vue-next'
import {
NavigationMenuTrigger,
type NavigationMenuTriggerProps,
useForwardProps,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { navigationMenuTriggerStyle } from '.'
const props = defineProps<NavigationMenuTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<NavigationMenuTrigger
data-slot="navigation-menu-trigger"
v-bind="forwardedProps"
:class="cn(navigationMenuTriggerStyle(), 'group', props.class)"
>
<slot />
<ChevronDown
class="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuTrigger>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import {
NavigationMenuViewport,
type NavigationMenuViewportProps,
useForwardProps,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<NavigationMenuViewportProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<div class="absolute top-full left-0 isolate z-50 flex justify-center">
<NavigationMenuViewport
data-slot="navigation-menu-viewport"
v-bind="forwardedProps"
:class="
cn(
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--reka-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--reka-navigation-menu-viewport-width)]',
props.class,
)
"
/>
</div>
</template>

View File

@@ -0,0 +1,14 @@
import { cva } from 'class-variance-authority'
export { default as NavigationMenu } from './NavigationMenu.vue'
export { default as NavigationMenuContent } from './NavigationMenuContent.vue'
export { default as NavigationMenuIndicator } from './NavigationMenuIndicator.vue'
export { default as NavigationMenuItem } from './NavigationMenuItem.vue'
export { default as NavigationMenuLink } from './NavigationMenuLink.vue'
export { default as NavigationMenuList } from './NavigationMenuList.vue'
export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue'
export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue'
export const navigationMenuTriggerStyle = cva(
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1',
)

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import { Separator, type SeparatorProps } from 'reka-ui'
const props = withDefaults(defineProps<
SeparatorProps & { class?: HTMLAttributes['class'] }
>(), {
orientation: 'horizontal',
decorative: true,
})
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<Separator
data-slot="separator-root"
v-bind="delegatedProps"
:class="
cn(
`bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px`,
props.class,
)
"
/>
</template>

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } 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,63 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import { X } from 'lucide-vue-next'
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogPortal,
useForwardPropsEmits,
} from 'reka-ui'
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="{ ...forwarded, ...$attrs }"
>
<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,23 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { DialogDescription, type DialogDescriptionProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</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,23 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { DialogOverlay, type DialogOverlayProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</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,23 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { DialogTitle, type DialogTitleProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DialogTitle
data-slot="sheet-title"
:class="cn('text-foreground font-semibold', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>

Some files were not shown because too many files have changed in this diff Show More