Initial
This commit is contained in:
+1
-99
@@ -1,99 +1 @@
|
|||||||
import stylistic from '@stylistic/eslint-plugin';
|
export default [];
|
||||||
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 }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
Generated
+846
-8
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -28,6 +28,7 @@
|
|||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"sass-embedded": "^1.100.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"typescript-eslint": "^8.23.0",
|
"typescript-eslint": "^8.23.0",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"@inertiajs/vue3": "^3.0.0",
|
"@inertiajs/vue3": "^3.0.0",
|
||||||
"@laravel/passkeys": "^0.2.0",
|
"@laravel/passkeys": "^0.2.0",
|
||||||
"@lucide/vue": "^1.17.0",
|
"@lucide/vue": "^1.17.0",
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -46,9 +48,11 @@
|
|||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
|
"vite-plugin-vuetify": "^2.1.3",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-input-otp": "^0.3.2",
|
"vue-input-otp": "^0.3.2",
|
||||||
"vue-sonner": "^2.0.0"
|
"vue-sonner": "^2.0.0",
|
||||||
|
"vuetify": "^4.1.2"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
||||||
|
|||||||
+27
-23
@@ -1,33 +1,37 @@
|
|||||||
|
import '../css/app.css';
|
||||||
|
|
||||||
import { createInertiaApp } from '@inertiajs/vue3';
|
import { createInertiaApp } from '@inertiajs/vue3';
|
||||||
import { initializeTheme } from '@/composables/useAppearance';
|
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||||
import AppLayout from '@/layouts/AppLayout.vue';
|
import { createApp, h } from 'vue';
|
||||||
import AuthLayout from '@/layouts/AuthLayout.vue';
|
import type {DefineComponent} from 'vue';
|
||||||
import SettingsLayout from '@/layouts/settings/Layout.vue';
|
import MainLayout from '@/layouts/MainLayout.vue';
|
||||||
import { initializeFlashToast } from '@/lib/flashToast';
|
import vuetify from '@/plugins/vuetify';
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||||
|
|
||||||
createInertiaApp({
|
createInertiaApp({
|
||||||
title: (title) => (title ? `${title} - ${appName}` : appName),
|
title: (title) => (title ? `${title} - ${appName}` : appName),
|
||||||
layout: (name) => {
|
|
||||||
switch (true) {
|
resolve: (name) => {
|
||||||
case name === 'Welcome':
|
const page = resolvePageComponent(
|
||||||
return null;
|
`./pages/${name}.vue`,
|
||||||
case name.startsWith('auth/'):
|
import.meta.glob<DefineComponent>('./pages/**/*.vue'),
|
||||||
return AuthLayout;
|
);
|
||||||
case name.startsWith('settings/'):
|
page.then((module) => {
|
||||||
return [AppLayout, SettingsLayout];
|
module.default.layout ??= MainLayout;
|
||||||
default:
|
});
|
||||||
return AppLayout;
|
|
||||||
}
|
return page;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setup({ el, App, props, plugin }) {
|
||||||
|
return createApp({ render: () => h(App, props) })
|
||||||
|
.use(plugin)
|
||||||
|
.use(vuetify)
|
||||||
|
.mount(el);
|
||||||
|
},
|
||||||
|
|
||||||
progress: {
|
progress: {
|
||||||
color: '#4B5563',
|
color: '#C0392B',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// This will set light / dark mode on page load...
|
|
||||||
initializeTheme();
|
|
||||||
|
|
||||||
// This will listen for flash toast data from the server...
|
|
||||||
initializeFlashToast();
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
Reference in New Issue
Block a user