Initial
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
2026-07-04 18:24:44 +10:00
parent 3f6d532cf0
commit 0485b85a59
209 changed files with 1429 additions and 7372 deletions
+1 -99
View File
@@ -1,99 +1 @@
import stylistic from '@stylistic/eslint-plugin';
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
import prettier from 'eslint-config-prettier/flat';
import importPlugin from 'eslint-plugin-import';
import vue from 'eslint-plugin-vue';
const controlStatements = [
'if',
'return',
'for',
'while',
'do',
'switch',
'try',
'throw',
];
const paddingAroundControl = [
...controlStatements.flatMap((stmt) => [
{ blankLine: 'always', prev: '*', next: stmt },
{ blankLine: 'always', prev: stmt, next: '*' },
]),
];
export default defineConfigWithVueTs(
vue.configs['flat/essential'],
vueTsConfigs.recommended,
{
plugins: {
import: importPlugin,
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json',
},
node: true,
},
},
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
fixStyle: 'separate-type-imports',
},
],
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import/consistent-type-specifier-style': [
'error',
'prefer-top-level',
],
},
},
{
plugins: {
'@stylistic': stylistic,
},
rules: {
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }],
'@stylistic/padding-line-between-statements': [
'error',
...paddingAroundControl,
],
},
},
{
ignores: [
'vendor',
'node_modules',
'public',
'bootstrap/ssr',
'tailwind.config.js',
'vite.config.ts',
'resources/js/actions/**',
'resources/js/components/ui/*',
'resources/js/routes/**',
'resources/js/wayfinder/**',
],
},
prettier,
{
plugins: {
'@stylistic': stylistic,
},
rules: {
curly: ['error', 'all'],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }],
},
},
);
export default [];
+846 -8
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -28,6 +28,7 @@
"eslint-plugin-vue": "^9.32.0",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"sass-embedded": "^1.100.0",
"typescript": "^5.2.2",
"typescript-eslint": "^8.23.0",
"vite": "^8.0.0",
@@ -38,6 +39,7 @@
"@inertiajs/vue3": "^3.0.0",
"@laravel/passkeys": "^0.2.0",
"@lucide/vue": "^1.17.0",
"@mdi/font": "^7.4.47",
"@vueuse/core": "^12.8.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -46,9 +48,11 @@
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
"tw-animate-css": "^1.2.5",
"vite-plugin-vuetify": "^2.1.3",
"vue": "^3.5.13",
"vue-input-otp": "^0.3.2",
"vue-sonner": "^2.0.0"
"vue-sonner": "^2.0.0",
"vuetify": "^4.1.2"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.9.5",
+28 -24
View File
@@ -1,33 +1,37 @@
import '../css/app.css';
import { createInertiaApp } from '@inertiajs/vue3';
import { initializeTheme } from '@/composables/useAppearance';
import AppLayout from '@/layouts/AppLayout.vue';
import AuthLayout from '@/layouts/AuthLayout.vue';
import SettingsLayout from '@/layouts/settings/Layout.vue';
import { initializeFlashToast } from '@/lib/flashToast';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createApp, h } from 'vue';
import type {DefineComponent} from 'vue';
import MainLayout from '@/layouts/MainLayout.vue';
import vuetify from '@/plugins/vuetify';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
layout: (name) => {
switch (true) {
case name === 'Welcome':
return null;
case name.startsWith('auth/'):
return AuthLayout;
case name.startsWith('settings/'):
return [AppLayout, SettingsLayout];
default:
return AppLayout;
}
},
progress: {
color: '#4B5563',
},
resolve: (name) => {
const page = resolvePageComponent(
`./pages/${name}.vue`,
import.meta.glob<DefineComponent>('./pages/**/*.vue'),
);
page.then((module) => {
module.default.layout ??= MainLayout;
});
// This will set light / dark mode on page load...
initializeTheme();
return page;
},
// This will listen for flash toast data from the server...
initializeFlashToast();
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(vuetify)
.mount(el);
},
progress: {
color: '#C0392B',
},
});
-30
View File
@@ -1,30 +0,0 @@
<script setup lang="ts">
import { AlertCircle } from '@lucide/vue';
import { computed } from 'vue';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
type Props = {
errors: string[];
title?: string;
};
const props = withDefaults(defineProps<Props>(), {
title: 'Something went wrong.',
});
const uniqueErrors = computed(() => Array.from(new Set(props.errors)));
</script>
<template>
<Alert variant="destructive">
<AlertCircle class="size-4" />
<AlertTitle>{{ title }}</AlertTitle>
<AlertDescription>
<ul class="list-inside list-disc text-sm">
<li v-for="(error, index) in uniqueErrors" :key="index">
{{ error }}
</li>
</ul>
</AlertDescription>
</Alert>
</template>
-28
View File
@@ -1,28 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { SidebarInset } from '@/components/ui/sidebar';
import type { AppVariant } from '@/types';
type Props = {
variant?: AppVariant;
class?: string;
};
const props = withDefaults(defineProps<Props>(), {
variant: 'sidebar',
});
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>
-283
View File
@@ -1,283 +0,0 @@
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import { BookOpen, Folder, LayoutGrid, Menu, Search } from '@lucide/vue';
import { computed } from 'vue';
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 { useCurrentUrl } from '@/composables/useCurrentUrl';
import { getInitials } from '@/composables/useInitials';
import { toUrl } from '@/lib/utils';
import { dashboard } from '@/routes';
import type { BreadcrumbItem, NavItem } from '@/types';
type Props = {
breadcrumbs?: BreadcrumbItem[];
};
const props = withDefaults(defineProps<Props>(), {
breadcrumbs: () => [],
});
const page = usePage();
const auth = computed(() => page.props.auth);
const { isCurrentUrl, whenCurrentUrl } = useCurrentUrl();
const activeItemStyles =
'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="
whenCurrentUrl(
item.href,
activeItemStyles,
)
"
>
<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="toUrl(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="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(),
whenCurrentUrl(
item.href,
activeItemStyles,
),
'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="isCurrentUrl(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="toUrl(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>
-16
View File
@@ -1,16 +0,0 @@
<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>
-29
View File
@@ -1,29 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
defineOptions({
inheritAttrs: false,
});
type 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>
-24
View File
@@ -1,24 +0,0 @@
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3';
import { SidebarProvider } from '@/components/ui/sidebar';
import type { AppVariant } from '@/types';
type Props = {
variant?: AppVariant;
};
withDefaults(defineProps<Props>(), {
variant: 'sidebar',
});
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>
-66
View File
@@ -1,66 +0,0 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { BookOpen, FolderGit2, LayoutGrid } from '@lucide/vue';
import AppLogo from '@/components/AppLogo.vue';
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 { dashboard } from '@/routes';
import type { NavItem } from '@/types';
const mainNavItems: NavItem[] = [
{
title: 'Dashboard',
href: dashboard(),
icon: LayoutGrid,
},
];
const footerNavItems: NavItem[] = [
{
title: 'Repository',
href: 'https://github.com/laravel/vue-starter-kit',
icon: FolderGit2,
},
{
title: 'Documentation',
href: 'https://laravel.com/docs/starter-kits#vue',
icon: BookOpen,
},
];
</script>
<template>
<Sidebar collapsible="icon" variant="inset">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" as-child>
<Link :href="dashboard()">
<AppLogo />
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain :items="mainNavItems" />
</SidebarContent>
<SidebarFooter>
<NavFooter :items="footerNavItems" />
<NavUser />
</SidebarFooter>
</Sidebar>
<slot />
</template>
@@ -1,27 +0,0 @@
<script setup lang="ts">
import Breadcrumbs from '@/components/Breadcrumbs.vue';
import { SidebarTrigger } from '@/components/ui/sidebar';
import type { BreadcrumbItem } from '@/types';
withDefaults(
defineProps<{
breadcrumbs?: BreadcrumbItem[];
}>(),
{
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>
@@ -1,33 +0,0 @@
<script setup lang="ts">
import { Monitor, Moon, Sun } from '@lucide/vue';
import { useAppearance } from '@/composables/useAppearance';
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>
-38
View File
@@ -1,38 +0,0 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import type { BreadcrumbItem as BreadcrumbItemType } from '@/types';
type Props = {
breadcrumbs: BreadcrumbItemType[];
};
defineProps<Props>();
</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>
-113
View File
@@ -1,113 +0,0 @@
<script setup lang="ts">
import { Form } from '@inertiajs/vue3';
import { useTemplateRef } from 'vue';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
import PasswordInput from '@/components/PasswordInput.vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
const passwordInput = useTemplateRef('passwordInput');
</script>
<template>
<div class="space-y-6">
<Heading
variant="small"
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" data-test="delete-user-button"
>Delete account</Button
>
</DialogTrigger>
<DialogContent>
<Form
v-bind="ProfileController.destroy.form()"
reset-on-success
@error="() => passwordInput?.focus()"
:options="{
preserveScroll: true,
}"
class="space-y-6"
v-slot="{ errors, processing, reset, clearErrors }"
>
<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
>
<PasswordInput
id="password"
name="password"
ref="passwordInput"
placeholder="Password"
/>
<InputError :message="errors.password" />
</div>
<DialogFooter class="gap-2">
<DialogClose as-child>
<Button
variant="secondary"
@click="
() => {
clearErrors();
reset();
}
"
>
Cancel
</Button>
</DialogClose>
<Button
type="submit"
variant="destructive"
:disabled="processing"
data-test="confirm-delete-user-button"
>
Delete account
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
</div>
</div>
</template>
-28
View File
@@ -1,28 +0,0 @@
<script setup lang="ts">
type Props = {
title: string;
description?: string;
variant?: 'default' | 'small';
};
withDefaults(defineProps<Props>(), {
variant: 'default',
});
</script>
<template>
<header :class="variant === 'small' ? '' : 'mb-8 space-y-0.5'">
<h2
:class="
variant === 'small'
? 'mb-0.5 text-base font-medium'
: 'text-xl font-semibold tracking-tight'
"
>
{{ title }}
</h2>
<p v-if="description" class="text-sm text-muted-foreground">
{{ description }}
</p>
</header>
</template>
-13
View File
@@ -1,13 +0,0 @@
<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>
@@ -1,65 +0,0 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { KeyRound } from '@lucide/vue';
import { destroy } from '@/actions/Laravel/Passkeys/Http/Controllers/PasskeyRegistrationController';
import Heading from '@/components/Heading.vue';
import PasskeyItem from '@/components/PasskeyItem.vue';
import PasskeyRegister from '@/components/PasskeyRegister.vue';
import type { Passkey } from '@/types/auth';
export type Props = {
canManagePasskeys?: boolean;
passkeys?: Passkey[];
};
withDefaults(defineProps<Props>(), {
canManagePasskeys: false,
passkeys: () => [],
});
const handleDelete = (id: number, onError: () => void) => {
router.delete(destroy.url(id), {
preserveScroll: true,
onError,
});
};
const handleRegisterSuccess = () => {
router.reload();
};
</script>
<template>
<div v-if="canManagePasskeys" class="space-y-6">
<Heading
variant="small"
title="Passkeys"
description="Manage your passkeys for passwordless sign-in"
/>
<div class="overflow-hidden rounded-lg border border-border">
<template v-if="passkeys.length">
<PasskeyItem
v-for="passkey in passkeys"
:key="passkey.id"
:passkey="passkey"
@remove="handleDelete"
/>
</template>
<div v-else class="p-8 text-center">
<div
class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-muted"
>
<KeyRound class="h-7 w-7 text-muted-foreground" />
</div>
<p class="font-medium">No passkeys yet</p>
<p class="mt-1 text-sm text-muted-foreground">
Add a passkey to sign in without a password
</p>
</div>
</div>
<PasskeyRegister @success="handleRegisterSuccess" />
</div>
</template>
@@ -1,93 +0,0 @@
<script setup lang="ts">
import { Form } from '@inertiajs/vue3';
import { ShieldCheck } from '@lucide/vue';
import { onUnmounted, ref } from 'vue';
import Heading from '@/components/Heading.vue';
import TwoFactorRecoveryCodes from '@/components/TwoFactorRecoveryCodes.vue';
import TwoFactorSetupModal from '@/components/TwoFactorSetupModal.vue';
import { Button } from '@/components/ui/button';
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
import { disable, enable } from '@/routes/two-factor';
export type Props = {
canManageTwoFactor?: boolean;
requiresConfirmation?: boolean;
twoFactorEnabled?: boolean;
};
withDefaults(defineProps<Props>(), {
canManageTwoFactor: false,
requiresConfirmation: false,
twoFactorEnabled: false,
});
const { hasSetupData, clearTwoFactorAuthData } = useTwoFactorAuth();
const showSetupModal = ref<boolean>(false);
onUnmounted(() => clearTwoFactorAuthData());
</script>
<template>
<div v-if="canManageTwoFactor" class="space-y-6">
<Heading
variant="small"
title="Two-factor authentication"
description="Manage your two-factor authentication settings"
/>
<div
v-if="!twoFactorEnabled"
class="flex flex-col items-start justify-start space-y-4"
>
<p class="text-sm text-muted-foreground">
When you enable two-factor authentication, you will be prompted
for a secure pin during login. This pin can be retrieved from a
TOTP-supported application on your phone.
</p>
<div>
<Button v-if="hasSetupData" @click="showSetupModal = true">
<ShieldCheck />Continue setup
</Button>
<Form
v-else
v-bind="enable.form()"
@success="showSetupModal = true"
#default="{ processing }"
>
<Button type="submit" :disabled="processing">
Enable 2FA
</Button>
</Form>
</div>
</div>
<div v-else class="flex flex-col items-start justify-start space-y-4">
<p class="text-sm text-muted-foreground">
You will be prompted for a secure, random pin during login,
which you can retrieve from the TOTP-supported application on
your phone.
</p>
<div class="relative inline">
<Form v-bind="disable.form()" #default="{ processing }">
<Button
variant="destructive"
type="submit"
:disabled="processing"
>
Disable 2FA
</Button>
</Form>
</div>
<TwoFactorRecoveryCodes />
</div>
<TwoFactorSetupModal
v-model:isOpen="showSetupModal"
:requiresConfirmation="requiresConfirmation"
:twoFactorEnabled="twoFactorEnabled"
/>
</div>
</template>
+34
View File
@@ -0,0 +1,34 @@
<script setup lang="ts">
defineProps<{
light?: boolean;
small?: boolean;
}>();
</script>
<template>
<span :class="['masthead', small && 'masthead-sm', light && 'masthead-light', !light && 'masthead-dark']">
Dredge<span class="masthead-accent">News</span>
</span>
</template>
<style scoped lang="sass">
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&display=swap')
.masthead
font-family: 'Playfair Display', serif
font-weight: 700
font-size: 22px
letter-spacing: -0.5px
.masthead-sm
font-size: 16px
.masthead-light
color: #FFFFFF
.masthead-dark
color: #111111
.masthead-accent
color: #C0392B
</style>
-44
View File
@@ -1,44 +0,0 @@
<script setup lang="ts">
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { toUrl } from '@/lib/utils';
import type { NavItem } from '@/types';
type 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="toUrl(item.href)"
target="_blank"
rel="noopener noreferrer"
>
<component :is="item.icon" />
<span>{{ item.title }}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</template>
-38
View File
@@ -1,38 +0,0 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { useCurrentUrl } from '@/composables/useCurrentUrl';
import type { NavItem } from '@/types';
defineProps<{
items: NavItem[];
}>();
const { isCurrentUrl } = useCurrentUrl();
</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="isCurrentUrl(item.href)"
:tooltip="item.title"
>
<Link :href="item.href">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</template>
-55
View File
@@ -1,55 +0,0 @@
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3';
import { ChevronsUpDown } from '@lucide/vue';
import { computed } from 'vue';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar';
import UserInfo from '@/components/UserInfo.vue';
import UserMenuContent from '@/components/UserMenuContent.vue';
const page = usePage();
const user = computed(() => page.props.auth.user);
const { isMobile, state } = 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"
data-test="sidebar-menu-button"
>
<UserInfo :user="user" />
<ChevronsUpDown class="ml-auto size-4" />
</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>
-95
View File
@@ -1,95 +0,0 @@
<script setup lang="ts">
import { KeyRound, Trash2 } from '@lucide/vue';
import { ref } from 'vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import type { Passkey } from '@/types/auth';
const props = defineProps<{
passkey: Passkey;
}>();
const emit = defineEmits<{
remove: [id: number, onError: () => void];
}>();
const isDeleting = ref(false);
const handleDelete = () => {
isDeleting.value = true;
emit('remove', props.passkey.id, () => {
isDeleting.value = false;
});
};
</script>
<template>
<div class="flex items-center justify-between border-b p-4 last:border-b-0">
<div class="flex items-center gap-4">
<div
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-muted"
>
<KeyRound class="h-5 w-5 text-muted-foreground" />
</div>
<div class="space-y-1">
<div class="flex items-center gap-2.5">
<p class="font-medium tracking-tight">{{ passkey.name }}</p>
<span
v-if="passkey.authenticator"
class="inline-flex items-center gap-1 rounded-md bg-muted px-2 py-0.5 text-[11px] font-medium tracking-wide text-muted-foreground uppercase ring-1 ring-border ring-inset"
>
{{ passkey.authenticator }}
</span>
</div>
<p class="text-sm text-muted-foreground">
Added {{ passkey.created_at_diff }}
<template v-if="passkey.last_used_at_diff">
<span class="mx-1 text-muted-foreground/50">/</span>
Last used {{ passkey.last_used_at_diff }}
</template>
</p>
</div>
</div>
<Dialog>
<DialogTrigger as-child>
<Button
variant="ghost"
size="sm"
class="text-destructive hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 class="h-4 w-4" />
<span class="sr-only">Remove</span>
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Remove passkey</DialogTitle>
<DialogDescription>
Are you sure you want to remove the "{{ passkey.name }}"
passkey? You will no longer be able to use it to sign in.
</DialogDescription>
<DialogFooter class="gap-2">
<DialogClose as-child>
<Button variant="secondary">Cancel</Button>
</DialogClose>
<Button
variant="destructive"
:disabled="isDeleting"
@click="handleDelete"
>
{{ isDeleting ? 'Removing...' : 'Remove passkey' }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>
@@ -1,94 +0,0 @@
<script setup lang="ts">
import { usePasskeyRegister } from '@laravel/passkeys/vue';
import { ref } from 'vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
const emit = defineEmits<{
success: [];
}>();
const getDefaultPasskeyName = () => {
const ua = navigator.userAgent;
const browser = ['Chrome', 'Firefox', 'Safari', 'Edge', 'Opera'].find(
(browser) => new RegExp(browser).test(ua),
);
const os = ['iPhone', 'iPad', 'Android', 'Mac', 'Windows'].find((os) =>
new RegExp(os).test(ua),
);
return [browser, os].filter(Boolean).join(' on ') || '';
};
const name = ref(getDefaultPasskeyName());
const showForm = ref(false);
const { register, isLoading, error, isSupported } = usePasskeyRegister({
onSuccess: () => {
name.value = '';
showForm.value = false;
emit('success');
},
});
const handleSubmit = async (event: Event) => {
event.preventDefault();
if (!name.value.trim()) {
return;
}
await register(name.value);
};
const handleCancel = () => {
showForm.value = false;
name.value = '';
};
</script>
<template>
<div v-if="!isSupported" class="text-sm text-muted-foreground">
Passkeys are not supported in this browser.
</div>
<Button v-else-if="!showForm" variant="outline" @click="showForm = true">
Add passkey
</Button>
<form
v-else
@submit="handleSubmit"
class="space-y-4 rounded-lg border border-border bg-muted/50 p-4"
>
<div class="grid gap-2">
<Label for="passkey-name">Passkey name</Label>
<Input
id="passkey-name"
type="text"
v-model="name"
placeholder="e.g., MacBook Pro, iPhone"
class="mt-1 block w-full border-foreground/20"
autofocus
/>
<p class="text-xs text-muted-foreground">
A name helps you identify this passkey later.
</p>
</div>
<InputError v-if="error" :message="error" />
<div class="flex gap-2">
<Button type="submit" :disabled="isLoading || !name.trim()">
{{ isLoading ? 'Registering...' : 'Register passkey' }}
</Button>
<Button type="button" variant="ghost" @click="handleCancel">
Cancel
</Button>
</div>
</form>
</template>
-73
View File
@@ -1,73 +0,0 @@
<script setup lang="ts">
import type { UrlMethodPair } from '@inertiajs/core';
import { router } from '@inertiajs/vue3';
import { usePasskeyVerify } from '@laravel/passkeys/vue';
import { KeyRound } from '@lucide/vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Spinner } from '@/components/ui/spinner';
type Props = {
routes?: {
options: UrlMethodPair;
submit: UrlMethodPair;
};
label?: string;
loadingLabel?: string;
separator?: string;
};
const props = defineProps<Props>();
const { verify, isLoading, error, isSupported } = usePasskeyVerify({
...(props.routes
? {
routes: {
options: props.routes.options.url,
submit: props.routes.submit.url,
},
}
: {}),
onSuccess: (response) => {
router.visit(response.redirect ?? '/dashboard');
},
});
</script>
<template>
<div v-if="isSupported">
<div class="grid gap-2">
<Button
type="button"
variant="outline"
class="w-full"
@click="verify"
:disabled="isLoading"
>
<Spinner v-if="isLoading" />
<KeyRound v-else class="h-4 w-4" />
{{
isLoading
? (props.loadingLabel ?? 'Authenticating...')
: (props.label ?? 'Sign in with a passkey')
}}
</Button>
<div v-if="error" class="text-center">
<InputError :message="error" />
</div>
</div>
<div class="relative my-6">
<div class="absolute inset-0 flex items-center">
<Separator class="w-full" />
</div>
<div class="relative flex justify-center text-xs uppercase">
<span class="bg-background px-2 text-muted-foreground">
{{ props.separator ?? 'Or continue with email' }}
</span>
</div>
</div>
</div>
</template>
-46
View File
@@ -1,46 +0,0 @@
<script setup lang="ts">
import { Eye, EyeOff } from '@lucide/vue';
import { ref, useTemplateRef } from 'vue';
import type { HTMLAttributes } from 'vue';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
defineOptions({ inheritAttrs: false });
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
const showPassword = ref(false);
const inputRef = useTemplateRef('inputRef');
defineExpose({
$el: inputRef,
focus: () => inputRef.value?.$el?.focus(),
});
</script>
<template>
<div class="relative">
<Input
ref="inputRef"
:type="showPassword ? 'text' : 'password'"
:class="cn('pr-10', props.class)"
v-bind="$attrs"
/>
<button
type="button"
@click="showPassword = !showPassword"
:class="
cn(
'absolute inset-y-0 right-0 flex items-center rounded-r-md px-3 text-muted-foreground hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring focus-visible:outline-none',
)
"
:aria-label="showPassword ? 'Hide password' : 'Show password'"
:tabindex="-1"
>
<EyeOff v-if="showPassword" class="size-4" />
<Eye v-else class="size-4" />
</button>
</div>
</template>
@@ -1,31 +0,0 @@
<script setup lang="ts">
import { useId } from 'vue';
const patternId = `pattern-${useId()}`;
</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>
-25
View File
@@ -1,25 +0,0 @@
<script setup lang="ts">
import type { LinkComponentBaseProps, Method } from '@inertiajs/core';
import { Link } from '@inertiajs/vue3';
type Props = {
href: LinkComponentBaseProps['href'];
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>
@@ -1,123 +0,0 @@
<script setup lang="ts">
import { Form } from '@inertiajs/vue3';
import { Eye, EyeOff, LockKeyhole, RefreshCw } from '@lucide/vue';
import { nextTick, onMounted, ref, useTemplateRef } from 'vue';
import AlertError from '@/components/AlertError.vue';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
import { regenerateRecoveryCodes } from '@/routes/two-factor';
const { recoveryCodesList, fetchRecoveryCodes, errors } = useTwoFactorAuth();
const isRecoveryCodesVisible = ref<boolean>(false);
const recoveryCodeSectionRef = useTemplateRef('recoveryCodeSectionRef');
const toggleRecoveryCodesVisibility = async () => {
if (!isRecoveryCodesVisible.value && !recoveryCodesList.value.length) {
await fetchRecoveryCodes();
}
isRecoveryCodesVisible.value = !isRecoveryCodesVisible.value;
if (isRecoveryCodesVisible.value) {
await nextTick();
recoveryCodeSectionRef.value?.scrollIntoView({ behavior: 'smooth' });
}
};
onMounted(async () => {
if (!recoveryCodesList.value.length) {
await fetchRecoveryCodes();
}
});
</script>
<template>
<Card class="w-full">
<CardHeader>
<CardTitle class="flex gap-3">
<LockKeyhole class="size-4" />2FA recovery codes
</CardTitle>
<CardDescription>
Recovery codes let you regain access if you lose your 2FA
device. Store them in a secure password manager.
</CardDescription>
</CardHeader>
<CardContent>
<div
class="flex flex-col gap-3 select-none sm:flex-row sm:items-center sm:justify-between"
>
<Button @click="toggleRecoveryCodesVisibility" class="w-fit">
<component
:is="isRecoveryCodesVisible ? EyeOff : Eye"
class="size-4"
/>
{{ isRecoveryCodesVisible ? 'Hide' : 'View' }} recovery
codes
</Button>
<Form
v-if="isRecoveryCodesVisible && recoveryCodesList.length"
v-bind="regenerateRecoveryCodes.form()"
method="post"
:options="{ preserveScroll: true }"
@success="fetchRecoveryCodes"
#default="{ processing }"
>
<Button
variant="secondary"
type="submit"
:disabled="processing"
>
<RefreshCw /> Regenerate codes
</Button>
</Form>
</div>
<div
:class="[
'relative overflow-hidden transition-all duration-300',
isRecoveryCodesVisible
? 'h-auto opacity-100'
: 'h-0 opacity-0',
]"
>
<div v-if="errors?.length" class="mt-6">
<AlertError :errors="errors" />
</div>
<div v-else class="mt-3 space-y-3">
<div
ref="recoveryCodeSectionRef"
class="grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm"
>
<div v-if="!recoveryCodesList.length" class="space-y-2">
<div
v-for="n in 8"
:key="n"
class="h-4 animate-pulse rounded bg-muted-foreground/20"
></div>
</div>
<div
v-else
v-for="(code, index) in recoveryCodesList"
:key="index"
>
{{ code }}
</div>
</div>
<p class="text-xs text-muted-foreground select-none">
Each recovery code can be used once to access your
account and will be removed after use. If you need more,
click
<span class="font-bold">Regenerate codes</span> above.
</p>
</div>
</div>
</CardContent>
</Card>
</template>
@@ -1,298 +0,0 @@
<script setup lang="ts">
import { Form } from '@inertiajs/vue3';
import { Check, Copy, ScanLine } from '@lucide/vue';
import { useClipboard } from '@vueuse/core';
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
import AlertError from '@/components/AlertError.vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from '@/components/ui/input-otp';
import { Spinner } from '@/components/ui/spinner';
import { useAppearance } from '@/composables/useAppearance';
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
import { confirm } from '@/routes/two-factor';
import type { TwoFactorConfigContent } from '@/types';
type Props = {
requiresConfirmation: boolean;
twoFactorEnabled: boolean;
};
const { resolvedAppearance } = useAppearance();
const props = defineProps<Props>();
const isOpen = defineModel<boolean>('isOpen');
const { copy, copied } = useClipboard();
const { qrCodeSvg, manualSetupKey, clearSetupData, fetchSetupData, errors } =
useTwoFactorAuth();
const showVerificationStep = ref(false);
const code = ref<string>('');
const pinInputContainerRef = useTemplateRef('pinInputContainerRef');
const modalConfig = computed<TwoFactorConfigContent>(() => {
if (props.twoFactorEnabled) {
return {
title: 'Two-factor authentication enabled',
description:
'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.',
buttonText: 'Close',
};
}
if (showVerificationStep.value) {
return {
title: 'Verify authentication code',
description: 'Enter the 6-digit code from your authenticator app',
buttonText: 'Continue',
};
}
return {
title: 'Enable two-factor authentication',
description:
'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app',
buttonText: 'Continue',
};
});
const handleModalNextStep = () => {
if (props.requiresConfirmation) {
showVerificationStep.value = true;
nextTick(() => {
pinInputContainerRef.value?.querySelector('input')?.focus();
});
return;
}
clearSetupData();
isOpen.value = false;
};
const resetModalState = () => {
if (props.twoFactorEnabled) {
clearSetupData();
}
showVerificationStep.value = false;
code.value = '';
};
watch(
() => isOpen.value,
async (isOpen) => {
if (!isOpen) {
resetModalState();
return;
}
if (!qrCodeSvg.value) {
await fetchSetupData();
}
},
);
</script>
<template>
<Dialog :open="isOpen" @update:open="isOpen = $event">
<DialogContent class="sm:max-w-md">
<DialogHeader class="flex items-center justify-center">
<div
class="mb-3 w-auto rounded-full border border-border bg-card p-0.5 shadow-sm"
>
<div
class="relative overflow-hidden rounded-full border border-border bg-muted p-2.5"
>
<div
class="absolute inset-0 grid grid-cols-5 opacity-50"
>
<div
v-for="i in 5"
:key="`col-${i}`"
class="border-r border-border last:border-r-0"
/>
</div>
<div
class="absolute inset-0 grid grid-rows-5 opacity-50"
>
<div
v-for="i in 5"
:key="`row-${i}`"
class="border-b border-border last:border-b-0"
/>
</div>
<ScanLine
class="relative z-20 size-6 text-foreground"
/>
</div>
</div>
<DialogTitle>{{ modalConfig.title }}</DialogTitle>
<DialogDescription class="text-center">
{{ modalConfig.description }}
</DialogDescription>
</DialogHeader>
<div
class="relative flex w-auto flex-col items-center justify-center space-y-5"
>
<template v-if="!showVerificationStep">
<AlertError v-if="errors?.length" :errors="errors" />
<template v-else>
<div
class="relative mx-auto flex max-w-md items-center overflow-hidden"
>
<div
class="relative mx-auto aspect-square w-64 overflow-hidden rounded-lg border border-border"
>
<div
v-if="!qrCodeSvg"
class="absolute inset-0 z-10 flex aspect-square h-auto w-full animate-pulse items-center justify-center bg-background"
>
<Spinner class="size-6" />
</div>
<div
v-else
class="relative z-10 overflow-hidden border p-5"
>
<div
v-html="qrCodeSvg"
class="flex aspect-square size-full items-center justify-center"
:style="{
filter:
resolvedAppearance === 'dark'
? 'invert(1) brightness(1.5)'
: undefined,
}"
/>
</div>
</div>
</div>
<div class="flex w-full items-center space-x-5">
<Button class="w-full" @click="handleModalNextStep">
{{ modalConfig.buttonText }}
</Button>
</div>
<div
class="relative flex w-full items-center justify-center"
>
<div
class="absolute inset-0 top-1/2 h-px w-full bg-border"
/>
<span class="relative bg-card px-2 py-1"
>or, enter the code manually</span
>
</div>
<div
class="flex w-full items-center justify-center space-x-2"
>
<div
class="flex w-full items-stretch overflow-hidden rounded-xl border border-border"
>
<div
v-if="!manualSetupKey"
class="flex h-full w-full items-center justify-center bg-muted p-3"
>
<Spinner />
</div>
<template v-else>
<input
type="text"
readonly
:value="manualSetupKey"
class="h-full w-full bg-background p-3 text-foreground"
/>
<button
@click="copy(manualSetupKey || '')"
class="relative block h-auto border-l border-border px-3 hover:bg-muted"
>
<Check
v-if="copied"
class="w-4 text-green-500"
/>
<Copy v-else class="w-4" />
</button>
</template>
</div>
</div>
</template>
</template>
<template v-else>
<Form
v-bind="confirm.form()"
error-bag="confirmTwoFactorAuthentication"
reset-on-error
@finish="code = ''"
@success="isOpen = false"
v-slot="{ errors, processing }"
>
<input type="hidden" name="code" :value="code" />
<div
ref="pinInputContainerRef"
class="relative w-full space-y-3"
>
<div
class="flex w-full flex-col items-center justify-center space-y-3 py-2"
>
<InputOTP
id="otp"
v-model="code"
:maxlength="6"
:disabled="processing"
autofocus
>
<InputOTPGroup>
<InputOTPSlot
v-for="index in 6"
:key="index"
:index="index - 1"
/>
</InputOTPGroup>
</InputOTP>
<InputError :message="errors?.code" />
</div>
<div class="flex w-full items-center space-x-5">
<Button
type="button"
variant="outline"
class="w-auto flex-1"
@click="showVerificationStep = false"
:disabled="processing"
>
Back
</Button>
<Button
type="submit"
class="w-auto flex-1"
:disabled="processing || code.length < 6"
>
Confirm
</Button>
</div>
</div>
</Form>
</template>
</div>
</DialogContent>
</Dialog>
</template>
-38
View File
@@ -1,38 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { useInitials } from '@/composables/useInitials';
import type { User } from '@/types';
type 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>
@@ -1,54 +0,0 @@
<script setup lang="ts">
import { Link, router } from '@inertiajs/vue3';
import { LogOut, Settings } from '@lucide/vue';
import {
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import UserInfo from '@/components/UserInfo.vue';
import { logout } from '@/routes';
import { edit } from '@/routes/profile';
import type { User } from '@/types';
type 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 cursor-pointer" :href="edit()" prefetch>
<Settings class="mr-2 h-4 w-4" />
Settings
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem :as-child="true">
<Link
class="block w-full cursor-pointer"
:href="logout()"
@click="handleLogout"
as="button"
data-test="logout-button"
>
<LogOut class="mr-2 h-4 w-4" />
Log out
</Link>
</DropdownMenuItem>
</template>
@@ -1,21 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { AlertVariants } from "."
import { cn } from "@/lib/utils"
import { alertVariants } from "."
const props = defineProps<{
class?: HTMLAttributes["class"]
variant?: AlertVariants["variant"]
}>()
</script>
<template>
<div
data-slot="alert"
:class="cn(alertVariants({ variant }), props.class)"
role="alert"
>
<slot />
</div>
</template>
@@ -1,17 +0,0 @@
<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="alert-description"
:class="cn('text-muted-foreground col-start-2 text-sm [&_p]:leading-relaxed', props.class)"
>
<slot />
</div>
</template>
@@ -1,17 +0,0 @@
<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="alert-title"
:class="cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', props.class)"
>
<slot />
</div>
</template>
-24
View File
@@ -1,24 +0,0 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Alert } from "./Alert.vue"
export { default as AlertDescription } from "./AlertDescription.vue"
export { default as AlertTitle } from "./AlertTitle.vue"
export const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type AlertVariants = VariantProps<typeof alertVariants>
@@ -1,18 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { AvatarRoot } from "reka-ui"
import { cn } from "@/lib/utils"
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>
@@ -1,21 +0,0 @@
<script setup lang="ts">
import type { AvatarFallbackProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { AvatarFallback } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AvatarFallbackProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</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>
@@ -1,16 +0,0 @@
<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>
@@ -1,3 +0,0 @@
export { default as Avatar } from "./Avatar.vue"
export { default as AvatarFallback } from "./AvatarFallback.vue"
export { default as AvatarImage } from "./AvatarImage.vue"
@@ -1,26 +0,0 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import type { BadgeVariants } from "."
import { reactiveOmit } from "@vueuse/core"
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { badgeVariants } from "."
const props = defineProps<PrimitiveProps & {
variant?: BadgeVariants["variant"]
class?: HTMLAttributes["class"]
}>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<Primitive
data-slot="badge"
:class="cn(badgeVariants({ variant }), props.class)"
v-bind="delegatedProps"
>
<slot />
</Primitive>
</template>
-26
View File
@@ -1,26 +0,0 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Badge } from "./Badge.vue"
export const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>
@@ -1,17 +0,0 @@
<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>
@@ -1,23 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { MoreHorizontal } from "@lucide/vue"
import { cn } from "@/lib/utils"
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>
@@ -1,17 +0,0 @@
<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>
@@ -1,21 +0,0 @@
<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
data-slot="breadcrumb-link"
:as="as"
:as-child="asChild"
:class="cn('hover:text-foreground transition-colors', props.class)"
>
<slot />
</Primitive>
</template>
@@ -1,17 +0,0 @@
<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>
@@ -1,20 +0,0 @@
<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>
@@ -1,22 +0,0 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { ChevronRight } from "@lucide/vue"
import { cn } from "@/lib/utils"
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>
@@ -1,7 +0,0 @@
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"
@@ -1,31 +0,0 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import type { ButtonVariants } from "."
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { 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"
:data-variant="variant"
:data-size="size"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>
@@ -1,38 +0,0 @@
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>
-22
View File
@@ -1,22 +0,0 @@
<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>
@@ -1,17 +0,0 @@
<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>
@@ -1,17 +0,0 @@
<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>
@@ -1,17 +0,0 @@
<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>
@@ -1,17 +0,0 @@
<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>
@@ -1,17 +0,0 @@
<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>
@@ -1,17 +0,0 @@
<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>
-7
View File
@@ -1,7 +0,0 @@
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"
@@ -1,35 +0,0 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { Check } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<CheckboxRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CheckboxRoot
v-slot="slotProps"
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="grid place-content-center text-current transition-none"
>
<slot v-bind="slotProps">
<Check class="size-3.5" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>
@@ -1 +0,0 @@
export { default as Checkbox } from "./Checkbox.vue"
@@ -1,19 +0,0 @@
<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="slotProps"
data-slot="collapsible"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</CollapsibleRoot>
</template>
@@ -1,15 +0,0 @@
<script setup lang="ts">
import type { CollapsibleContentProps } from "reka-ui"
import { CollapsibleContent } from "reka-ui"
const props = defineProps<CollapsibleContentProps>()
</script>
<template>
<CollapsibleContent
data-slot="collapsible-content"
v-bind="props"
>
<slot />
</CollapsibleContent>
</template>
@@ -1,15 +0,0 @@
<script setup lang="ts">
import type { CollapsibleTriggerProps } from "reka-ui"
import { CollapsibleTrigger } from "reka-ui"
const props = defineProps<CollapsibleTriggerProps>()
</script>
<template>
<CollapsibleTrigger
data-slot="collapsible-trigger"
v-bind="props"
>
<slot />
</CollapsibleTrigger>
</template>
@@ -1,3 +0,0 @@
export { default as Collapsible } from "./Collapsible.vue"
export { default as CollapsibleContent } from "./CollapsibleContent.vue"
export { default as CollapsibleTrigger } from "./CollapsibleTrigger.vue"
@@ -1,19 +0,0 @@
<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="dialog"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DialogRoot>
</template>
@@ -1,15 +0,0 @@
<script setup lang="ts">
import type { DialogCloseProps } from "reka-ui"
import { DialogClose } from "reka-ui"
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose
data-slot="dialog-close"
v-bind="props"
>
<slot />
</DialogClose>
</template>
@@ -1,53 +0,0 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { X } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import DialogOverlay from "./DialogOverlay.vue"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<DialogContentProps & { class?: HTMLAttributes["class"], showCloseButton?: boolean }>(), {
showCloseButton: true,
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay />
<DialogContent
data-slot="dialog-content"
v-bind="{ ...$attrs, ...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
v-if="showCloseButton"
data-slot="dialog-close"
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>
@@ -1,23 +0,0 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
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>
@@ -1,27 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { DialogClose } from "reka-ui"
import { cn } from "@/lib/utils"
import { Button } from '@/components/ui/button'
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
showCloseButton?: boolean
}>(), {
showCloseButton: false,
})
</script>
<template>
<div
data-slot="dialog-footer"
:class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)"
>
<slot />
<DialogClose v-if="showCloseButton" as-child>
<Button variant="outline">
Close
</Button>
</DialogClose>
</div>
</template>
@@ -1,17 +0,0 @@
<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>
@@ -1,21 +0,0 @@
<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="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>
@@ -1,59 +0,0 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { X } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
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="{ ...$attrs, ...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>
@@ -1,23 +0,0 @@
<script setup lang="ts">
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
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>
@@ -1,15 +0,0 @@
<script setup lang="ts">
import type { DialogTriggerProps } from "reka-ui"
import { DialogTrigger } from "reka-ui"
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger
data-slot="dialog-trigger"
v-bind="props"
>
<slot />
</DialogTrigger>
</template>
@@ -1,10 +0,0 @@
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"
@@ -1,19 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui"
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DropdownMenuRootProps>()
const emits = defineEmits<DropdownMenuRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRoot
v-slot="slotProps"
data-slot="dropdown-menu"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DropdownMenuRoot>
</template>
@@ -1,39 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { Check } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
const delegatedProps = reactiveOmit(props, "class")
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>
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>
@@ -1,39 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
{
sideOffset: 4,
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="{ ...$attrs, ...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>
@@ -1,15 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuGroupProps } from "reka-ui"
import { DropdownMenuGroup } from "reka-ui"
const props = defineProps<DropdownMenuGroupProps>()
</script>
<template>
<DropdownMenuGroup
data-slot="dropdown-menu-group"
v-bind="props"
>
<slot />
</DropdownMenuGroup>
</template>
@@ -1,31 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DropdownMenuItem, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<DropdownMenuItemProps & {
class?: HTMLAttributes["class"]
inset?: boolean
variant?: "default" | "destructive"
}>(), {
variant: "default",
})
const delegatedProps = reactiveOmit(props, "inset", "variant", "class")
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 data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_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>
@@ -1,23 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuLabelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DropdownMenuLabel, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
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>
@@ -1,21 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui"
import {
DropdownMenuRadioGroup,
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>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { Circle } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuRadioItemEmits>()
const delegatedProps = reactiveOmit(props, "class")
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>
<slot name="indicator-icon">
<Circle class="size-2 fill-current" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>
@@ -1,23 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuSeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSeparator,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuSeparatorProps & {
class?: HTMLAttributes["class"]
}>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DropdownMenuSeparator
data-slot="dropdown-menu-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
/>
</template>
@@ -1,17 +0,0 @@
<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>
@@ -1,18 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui"
import {
DropdownMenuSub,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<DropdownMenuSubProps>()
const emits = defineEmits<DropdownMenuSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
<slot v-bind="slotProps" />
</DropdownMenuSub>
</template>
@@ -1,27 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSubContent,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
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>
@@ -1,31 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuSubTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { ChevronRight } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSubTrigger,
useForwardProps,
} from "reka-ui"
import { cn } from "@/lib/utils"
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"
:data-inset="inset ? '' : undefined"
:class="cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground',
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>
@@ -1,17 +0,0 @@
<script setup lang="ts">
import type { DropdownMenuTriggerProps } from "reka-ui"
import { DropdownMenuTrigger, 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>
@@ -1,16 +0,0 @@
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"
@@ -1,28 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { OTPInputEmits, OTPInputProps } from "vue-input-otp"
import { reactiveOmit } from "@vueuse/core"
import { useForwardPropsEmits } from "reka-ui"
import { OTPInput } from "vue-input-otp"
import { cn } from "@/lib/utils"
const props = defineProps<OTPInputProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<OTPInputEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<OTPInput
v-slot="slotProps"
v-bind="forwarded"
:container-class="cn('flex items-center gap-2 has-disabled:opacity-50', props.class)"
data-slot="input-otp"
class="disabled:cursor-not-allowed"
>
<slot v-bind="slotProps" />
</OTPInput>
</template>
@@ -1,22 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<div
data-slot="input-otp-group"
v-bind="forwarded"
:class="cn('flex items-center', props.class)"
>
<slot />
</div>
</template>
@@ -1,21 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { MinusIcon } from "@lucide/vue"
import { useForwardProps } from "reka-ui"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
const forwarded = useForwardProps(props)
</script>
<template>
<div
data-slot="input-otp-separator"
role="separator"
v-bind="forwarded"
>
<slot>
<MinusIcon />
</slot>
</div>
</template>
@@ -1,32 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { useForwardProps } from "reka-ui"
import { computed } from "vue"
import { useVueOTPContext } from "vue-input-otp"
import { cn } from "@/lib/utils"
const props = defineProps<{ index: number, class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardProps(delegatedProps)
const context = useVueOTPContext()
const slot = computed(() => context?.value.slots[props.index])
</script>
<template>
<div
v-bind="forwarded"
data-slot="input-otp-slot"
:data-active="slot?.isActive"
:class="cn('data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]', props.class)"
>
{{ slot?.char }}
<div v-if="slot?.hasFakeCaret" class="pointer-events-none absolute inset-0 flex items-center justify-center">
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
</div>
</template>
@@ -1,4 +0,0 @@
export { default as InputOTP } from "./InputOTP.vue"
export { default as InputOTPGroup } from "./InputOTPGroup.vue"
export { default as InputOTPSeparator } from "./InputOTPSeparator.vue"
export { default as InputOTPSlot } from "./InputOTPSlot.vue"
@@ -1,33 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils"
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 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>
@@ -1 +0,0 @@
export { default as Input } from "./Input.vue"

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