Code formatting
This commit is contained in:
11
frontend/.eslintrc.cjs
Normal file
11
frontend/.eslintrc.cjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'@nuxtjs/eslint-config-typescript',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/no-multiple-template-root': 'off',
|
||||||
|
},
|
||||||
|
}
|
||||||
7
frontend/.prettierignore
Normal file
7
frontend/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
dist
|
||||||
|
.data
|
||||||
|
*.log
|
||||||
|
package-lock.json
|
||||||
13
frontend/.prettierrc
Normal file
13
frontend/.prettierrc
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"htmlWhitespaceSensitivity": "ignore",
|
||||||
|
"singleAttributePerLine": false
|
||||||
|
}
|
||||||
@@ -22,6 +22,14 @@
|
|||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 31.8% 91.4%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
--sidebar-background: 0 0% 98%;
|
||||||
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
|
--sidebar-border: 220 13% 91%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -42,6 +50,14 @@
|
|||||||
--border: 217.2 32.6% 17.5%;
|
--border: 217.2 32.6% 17.5%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 217.2 32.6% 17.5%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
--sidebar-background: 240 5.9% 10%;
|
||||||
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 240 3.7% 15.9%;
|
||||||
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
136
frontend/components/AppSidebar.vue
Normal file
136
frontend/components/AppSidebar.vue
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
SidebarRail,
|
||||||
|
} from '@/components/ui/sidebar'
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||||
|
import { LayoutGrid, Boxes, Settings, Home, ChevronRight, Database, Layers } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
title: 'Home',
|
||||||
|
url: '/',
|
||||||
|
icon: Home,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Setup',
|
||||||
|
icon: Settings,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Apps',
|
||||||
|
url: '/setup/apps',
|
||||||
|
icon: LayoutGrid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Objects',
|
||||||
|
url: '/setup/objects',
|
||||||
|
icon: Boxes,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Runtime',
|
||||||
|
icon: Database,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'My Apps',
|
||||||
|
url: '/app',
|
||||||
|
icon: Layers,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Sidebar collapsible="icon">
|
||||||
|
<SidebarHeader>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton size="lg" as-child>
|
||||||
|
<NuxtLink to="/">
|
||||||
|
<div
|
||||||
|
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground"
|
||||||
|
>
|
||||||
|
<LayoutGrid class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-0.5 leading-none">
|
||||||
|
<span class="font-semibold">Neo Platform</span>
|
||||||
|
<span class="text-xs">Multi-tenant CRM</span>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarHeader>
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>Application</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
<template v-for="item in menuItems" :key="item.title">
|
||||||
|
<!-- Simple menu item -->
|
||||||
|
<SidebarMenuItem v-if="!item.items">
|
||||||
|
<SidebarMenuButton as-child>
|
||||||
|
<NuxtLink :to="item.url">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
|
||||||
|
<!-- Collapsible menu item with submenu -->
|
||||||
|
<Collapsible v-else as-child :default-open="false" class="group/collapsible">
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<CollapsibleTrigger as-child>
|
||||||
|
<SidebarMenuButton tooltip="{item.title}">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<ChevronRight
|
||||||
|
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||||
|
/>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
|
||||||
|
<SidebarMenuSubButton as-child>
|
||||||
|
<NuxtLink :to="subItem.url">
|
||||||
|
<component :is="subItem.icon" />
|
||||||
|
<span>{{ subItem.title }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
</template>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
<SidebarFooter>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton>
|
||||||
|
<span class="text-sm text-muted-foreground">Logged in as user</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarFooter>
|
||||||
|
<SidebarRail />
|
||||||
|
</Sidebar>
|
||||||
|
</template>
|
||||||
@@ -54,9 +54,7 @@ const handleLogin = async () => {
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleLogin" class="flex flex-col gap-6">
|
<form @submit.prevent="handleLogin" class="flex flex-col gap-6">
|
||||||
<div class="flex flex-col items-center gap-2 text-center">
|
<div class="flex flex-col items-center gap-2 text-center">
|
||||||
<h1 class="text-2xl font-bold">
|
<h1 class="text-2xl font-bold">Login to your account</h1>
|
||||||
Login to your account
|
|
||||||
</h1>
|
|
||||||
<p class="text-balance text-sm text-muted-foreground">
|
<p class="text-balance text-sm text-muted-foreground">
|
||||||
Enter your credentials below to login
|
Enter your credentials below to login
|
||||||
</p>
|
</p>
|
||||||
@@ -69,54 +67,28 @@ const handleLogin = async () => {
|
|||||||
<div class="grid gap-6">
|
<div class="grid gap-6">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="tenantId">Tenant ID</Label>
|
<Label for="tenantId">Tenant ID</Label>
|
||||||
<Input
|
<Input id="tenantId" v-model="tenantId" type="text" placeholder="123" required />
|
||||||
id="tenantId"
|
|
||||||
v-model="tenantId"
|
|
||||||
type="text"
|
|
||||||
placeholder="123"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="email">Email</Label>
|
<Label for="email">Email</Label>
|
||||||
<Input
|
<Input id="email" v-model="email" type="email" placeholder="m@example.com" required />
|
||||||
id="email"
|
|
||||||
v-model="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="m@example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Label for="password">Password</Label>
|
<Label for="password">Password</Label>
|
||||||
<a
|
<a href="#" class="ml-auto text-sm underline-offset-4 hover:underline">
|
||||||
href="#"
|
|
||||||
class="ml-auto text-sm underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input id="password" v-model="password" type="password" required />
|
||||||
id="password"
|
|
||||||
v-model="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="submit" class="w-full" :disabled="loading">
|
||||||
type="submit"
|
|
||||||
class="w-full"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
{{ loading ? 'Logging in...' : 'Login' }}
|
{{ loading ? 'Logging in...' : 'Login' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center text-sm">
|
<div class="text-center text-sm">
|
||||||
Don't have an account?
|
Don't have an account?
|
||||||
<NuxtLink to="/register" class="underline underline-offset-4">
|
<NuxtLink to="/register" class="underline underline-offset-4">Sign up</NuxtLink>
|
||||||
Sign up
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
13
frontend/components/ui/breadcrumb/Breadcrumb.vue
Normal file
13
frontend/components/ui/breadcrumb/Breadcrumb.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav aria-label="breadcrumb" :class="props.class">
|
||||||
|
<slot />
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
22
frontend/components/ui/breadcrumb/BreadcrumbEllipsis.vue
Normal file
22
frontend/components/ui/breadcrumb/BreadcrumbEllipsis.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { MoreHorizontal } from 'lucide-vue-next'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<MoreHorizontal class="h-4 w-4" />
|
||||||
|
</slot>
|
||||||
|
<span class="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
14
frontend/components/ui/breadcrumb/BreadcrumbItem.vue
Normal file
14
frontend/components/ui/breadcrumb/BreadcrumbItem.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li :class="cn('inline-flex items-center gap-1.5', props.class)">
|
||||||
|
<slot />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
20
frontend/components/ui/breadcrumb/BreadcrumbLink.vue
Normal file
20
frontend/components/ui/breadcrumb/BreadcrumbLink.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PrimitiveProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { Primitive } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), {
|
||||||
|
as: 'a',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn('transition-colors hover:text-foreground', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
21
frontend/components/ui/breadcrumb/BreadcrumbList.vue
Normal file
21
frontend/components/ui/breadcrumb/BreadcrumbList.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ol
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ol>
|
||||||
|
</template>
|
||||||
19
frontend/components/ui/breadcrumb/BreadcrumbPage.vue
Normal file
19
frontend/components/ui/breadcrumb/BreadcrumbPage.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
:class="cn('font-normal text-foreground', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
17
frontend/components/ui/breadcrumb/BreadcrumbSeparator.vue
Normal file
17
frontend/components/ui/breadcrumb/BreadcrumbSeparator.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { ChevronRight } from 'lucide-vue-next'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li role="presentation" aria-hidden="true" :class="cn('[&>svg]:size-3.5', props.class)">
|
||||||
|
<slot>
|
||||||
|
<ChevronRight />
|
||||||
|
</slot>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
7
frontend/components/ui/breadcrumb/index.ts
Normal file
7
frontend/components/ui/breadcrumb/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { default as Breadcrumb } from './Breadcrumb.vue'
|
||||||
|
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue'
|
||||||
|
export { default as BreadcrumbItem } from './BreadcrumbItem.vue'
|
||||||
|
export { default as BreadcrumbLink } from './BreadcrumbLink.vue'
|
||||||
|
export { default as BreadcrumbList } from './BreadcrumbList.vue'
|
||||||
|
export { default as BreadcrumbPage } from './BreadcrumbPage.vue'
|
||||||
|
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue'
|
||||||
@@ -1,21 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { PrimitiveProps } from 'reka-ui'
|
||||||
import type { ButtonVariants } from "."
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { Primitive } from "reka-ui"
|
import type { ButtonVariants } from '.'
|
||||||
import { cn } from "@/lib/utils"
|
import { Primitive } from 'reka-ui'
|
||||||
import { buttonVariants } from "."
|
import { cn } from '@/lib/utils'
|
||||||
|
import { buttonVariants } from '.'
|
||||||
|
|
||||||
interface Props {
|
interface Props extends PrimitiveProps {
|
||||||
variant?: ButtonVariants["variant"]
|
variant?: ButtonVariants['variant']
|
||||||
size?: ButtonVariants["size"]
|
size?: ButtonVariants['size']
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
as?: any
|
|
||||||
asChild?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
as: "button",
|
as: 'button',
|
||||||
asChild: false,
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,36 @@
|
|||||||
import type { VariantProps } from "class-variance-authority"
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
import { cva } from "class-variance-authority"
|
import { cva } from 'class-variance-authority'
|
||||||
|
|
||||||
export { default as Button } from "./Button.vue"
|
export { default as Button } from './Button.vue'
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
export const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||||
destructive:
|
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary:
|
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
"default": "h-9 px-4 py-2",
|
default: 'h-9 px-4 py-2',
|
||||||
"xs": "h-7 rounded px-2",
|
xs: 'h-7 rounded px-2',
|
||||||
"sm": "h-8 rounded-md px-3 text-xs",
|
sm: 'h-8 rounded-md px-3 text-xs',
|
||||||
"lg": "h-10 rounded-md px-8",
|
lg: 'h-10 rounded-md px-8',
|
||||||
"icon": "h-9 w-9",
|
icon: 'h-9 w-9',
|
||||||
"icon-sm": "size-8",
|
'icon-sm': 'size-8',
|
||||||
"icon-lg": "size-10",
|
'icon-lg': 'size-10',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default",
|
size: 'default',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :class="cn('rounded-xl border bg-card text-card-foreground shadow', props.class)">
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'rounded-xl border bg-card text-card-foreground shadow',
|
|
||||||
props.class,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h3
|
<h3 :class="cn('font-semibold leading-none tracking-tight', props.class)">
|
||||||
:class="
|
|
||||||
cn('font-semibold leading-none tracking-tight', props.class)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</h3>
|
</h3>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export { default as Card } from "./Card.vue"
|
export { default as Card } from './Card.vue'
|
||||||
export { default as CardContent } from "./CardContent.vue"
|
export { default as CardContent } from './CardContent.vue'
|
||||||
export { default as CardDescription } from "./CardDescription.vue"
|
export { default as CardDescription } from './CardDescription.vue'
|
||||||
export { default as CardFooter } from "./CardFooter.vue"
|
export { default as CardFooter } from './CardFooter.vue'
|
||||||
export { default as CardHeader } from "./CardHeader.vue"
|
export { default as CardHeader } from './CardHeader.vue'
|
||||||
export { default as CardTitle } from "./CardTitle.vue"
|
export { default as CardTitle } from './CardTitle.vue'
|
||||||
|
|||||||
15
frontend/components/ui/collapsible/Collapsible.vue
Normal file
15
frontend/components/ui/collapsible/Collapsible.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'reka-ui'
|
||||||
|
import { CollapsibleRoot, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<CollapsibleRootProps>()
|
||||||
|
const emits = defineEmits<CollapsibleRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CollapsibleRoot v-slot="{ open }" v-bind="forwarded">
|
||||||
|
<slot :open="open" />
|
||||||
|
</CollapsibleRoot>
|
||||||
|
</template>
|
||||||
15
frontend/components/ui/collapsible/CollapsibleContent.vue
Normal file
15
frontend/components/ui/collapsible/CollapsibleContent.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CollapsibleContentProps } from 'reka-ui'
|
||||||
|
import { CollapsibleContent } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<CollapsibleContentProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CollapsibleContent
|
||||||
|
v-bind="props"
|
||||||
|
class="overflow-hidden transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CollapsibleContent>
|
||||||
|
</template>
|
||||||
12
frontend/components/ui/collapsible/CollapsibleTrigger.vue
Normal file
12
frontend/components/ui/collapsible/CollapsibleTrigger.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CollapsibleTriggerProps } from 'reka-ui'
|
||||||
|
import { CollapsibleTrigger } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<CollapsibleTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CollapsibleTrigger v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</template>
|
||||||
3
frontend/components/ui/collapsible/index.ts
Normal file
3
frontend/components/ui/collapsible/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as Collapsible } from './Collapsible.vue'
|
||||||
|
export { default as CollapsibleContent } from './CollapsibleContent.vue'
|
||||||
|
export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue'
|
||||||
@@ -1,24 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from '@vueuse/core'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
defaultValue?: string | number
|
defaultValue?: string | number
|
||||||
modelValue?: string | number
|
modelValue?: string | number
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
(e: "update:modelValue", payload: string | number): void
|
(e: 'update:modelValue', payload: string | number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const modelValue = useVModel(props, "modelValue", emits, {
|
const modelValue = useVModel(props, 'modelValue', emits, {
|
||||||
passive: true,
|
passive: true,
|
||||||
defaultValue: props.defaultValue,
|
defaultValue: props.defaultValue,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<input v-model="modelValue" :class="cn('flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)">
|
<input
|
||||||
|
v-model="modelValue"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as Input } from "./Input.vue"
|
export { default as Input } from './Input.vue'
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
import { Label } from "reka-ui"
|
import { Label } from 'reka-ui'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
for?: string
|
for?: string
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -20,7 +20,7 @@ const delegatedProps = reactiveOmit(props, "class")
|
|||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||||
props.class,
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as Label } from "./Label.vue"
|
export { default as Label } from './Label.vue'
|
||||||
|
|||||||
15
frontend/components/ui/select/Select.vue
Normal file
15
frontend/components/ui/select/Select.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectRootEmits, SelectRootProps } from 'reka-ui'
|
||||||
|
import { SelectRoot, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<SelectRootProps>()
|
||||||
|
const emits = defineEmits<SelectRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectRoot v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</SelectRoot>
|
||||||
|
</template>
|
||||||
54
frontend/components/ui/select/SelectContent.vue
Normal file
54
frontend/components/ui/select/SelectContent.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectContentEmits, SelectContentProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { SelectContent, SelectPortal, SelectViewport, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { SelectScrollDownButton, SelectScrollUpButton } from '.'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||||
|
{
|
||||||
|
position: 'popper',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const emits = defineEmits<SelectContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectPortal>
|
||||||
|
<SelectContent
|
||||||
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
position === 'popper' &&
|
||||||
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectViewport
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-1',
|
||||||
|
position === 'popper' &&
|
||||||
|
'h-[--reka-select-trigger-height] w-full min-w-[--reka-select-trigger-width]'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectViewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectContent>
|
||||||
|
</SelectPortal>
|
||||||
|
</template>
|
||||||
17
frontend/components/ui/select/SelectGroup.vue
Normal file
17
frontend/components/ui/select/SelectGroup.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectGroupProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { SelectGroup } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectGroup :class="cn('p-1 w-full', props.class)" v-bind="delegatedProps">
|
||||||
|
<slot />
|
||||||
|
</SelectGroup>
|
||||||
|
</template>
|
||||||
36
frontend/components/ui/select/SelectItem.vue
Normal file
36
frontend/components/ui/select/SelectItem.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectItemProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { Check } from 'lucide-vue-next'
|
||||||
|
import { SelectItem, SelectItemIndicator, SelectItemText, useForwardProps } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectItem
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectItemIndicator>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</SelectItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectItemText>
|
||||||
|
<slot />
|
||||||
|
</SelectItemText>
|
||||||
|
</SelectItem>
|
||||||
|
</template>
|
||||||
12
frontend/components/ui/select/SelectItemText.vue
Normal file
12
frontend/components/ui/select/SelectItemText.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectItemTextProps } from 'reka-ui'
|
||||||
|
import { SelectItemText } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<SelectItemTextProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectItemText v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</SelectItemText>
|
||||||
|
</template>
|
||||||
14
frontend/components/ui/select/SelectLabel.vue
Normal file
14
frontend/components/ui/select/SelectLabel.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectLabelProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { SelectLabel } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectLabel :class="cn('px-2 py-1.5 text-sm font-semibold', props.class)">
|
||||||
|
<slot />
|
||||||
|
</SelectLabel>
|
||||||
|
</template>
|
||||||
25
frontend/components/ui/select/SelectScrollDownButton.vue
Normal file
25
frontend/components/ui/select/SelectScrollDownButton.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectScrollDownButtonProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { ChevronDown } from 'lucide-vue-next'
|
||||||
|
import { SelectScrollDownButton, useForwardProps } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectScrollDownButton
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronDown />
|
||||||
|
</slot>
|
||||||
|
</SelectScrollDownButton>
|
||||||
|
</template>
|
||||||
25
frontend/components/ui/select/SelectScrollUpButton.vue
Normal file
25
frontend/components/ui/select/SelectScrollUpButton.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectScrollUpButtonProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { ChevronUp } from 'lucide-vue-next'
|
||||||
|
import { SelectScrollUpButton, useForwardProps } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectScrollUpButton
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronUp />
|
||||||
|
</slot>
|
||||||
|
</SelectScrollUpButton>
|
||||||
|
</template>
|
||||||
15
frontend/components/ui/select/SelectSeparator.vue
Normal file
15
frontend/components/ui/select/SelectSeparator.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectSeparatorProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { SelectSeparator } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
|
||||||
|
</template>
|
||||||
31
frontend/components/ui/select/SelectTrigger.vue
Normal file
31
frontend/components/ui/select/SelectTrigger.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectTriggerProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { ChevronDown } from 'lucide-vue-next'
|
||||||
|
import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectTrigger
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<SelectIcon as-child>
|
||||||
|
<ChevronDown class="w-4 h-4 opacity-50 shrink-0" />
|
||||||
|
</SelectIcon>
|
||||||
|
</SelectTrigger>
|
||||||
|
</template>
|
||||||
12
frontend/components/ui/select/SelectValue.vue
Normal file
12
frontend/components/ui/select/SelectValue.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SelectValueProps } from 'reka-ui'
|
||||||
|
import { SelectValue } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<SelectValueProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectValue v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</SelectValue>
|
||||||
|
</template>
|
||||||
11
frontend/components/ui/select/index.ts
Normal file
11
frontend/components/ui/select/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export { default as Select } from './Select.vue'
|
||||||
|
export { default as SelectContent } from './SelectContent.vue'
|
||||||
|
export { default as SelectGroup } from './SelectGroup.vue'
|
||||||
|
export { default as SelectItem } from './SelectItem.vue'
|
||||||
|
export { default as SelectItemText } from './SelectItemText.vue'
|
||||||
|
export { default as SelectLabel } from './SelectLabel.vue'
|
||||||
|
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue'
|
||||||
|
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'
|
||||||
|
export { default as SelectSeparator } from './SelectSeparator.vue'
|
||||||
|
export { default as SelectTrigger } from './SelectTrigger.vue'
|
||||||
|
export { default as SelectValue } from './SelectValue.vue'
|
||||||
27
frontend/components/ui/separator/Separator.vue
Normal file
27
frontend/components/ui/separator/Separator.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SeparatorProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { Separator } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
|
||||||
|
orientation: 'horizontal',
|
||||||
|
decorative: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Separator
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'shrink-0 bg-border',
|
||||||
|
props.orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
1
frontend/components/ui/separator/index.ts
Normal file
1
frontend/components/ui/separator/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Separator } from './Separator.vue'
|
||||||
15
frontend/components/ui/sheet/Sheet.vue
Normal file
15
frontend/components/ui/sheet/Sheet.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<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-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</DialogRoot>
|
||||||
|
</template>
|
||||||
12
frontend/components/ui/sheet/SheetClose.vue
Normal file
12
frontend/components/ui/sheet/SheetClose.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogCloseProps } from 'reka-ui'
|
||||||
|
import { DialogClose } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DialogCloseProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogClose v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</DialogClose>
|
||||||
|
</template>
|
||||||
53
frontend/components/ui/sheet/SheetContent.vue
Normal file
53
frontend/components/ui/sheet/SheetContent.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import type { SheetVariants } from '.'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { X } from 'lucide-vue-next'
|
||||||
|
import {
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { sheetVariants } from '.'
|
||||||
|
|
||||||
|
interface SheetContentProps extends DialogContentProps {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
side?: SheetVariants['side']
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<SheetContentProps>()
|
||||||
|
|
||||||
|
const emits = defineEmits<DialogContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class', 'side')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay
|
||||||
|
class="fixed inset-0 z-50 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(sheetVariants({ side }), props.class)"
|
||||||
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<DialogClose
|
||||||
|
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
|
||||||
|
>
|
||||||
|
<X class="w-4 h-4" />
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</template>
|
||||||
20
frontend/components/ui/sheet/SheetDescription.vue
Normal file
20
frontend/components/ui/sheet/SheetDescription.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogDescriptionProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DialogDescription } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogDescription
|
||||||
|
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogDescription>
|
||||||
|
</template>
|
||||||
12
frontend/components/ui/sheet/SheetFooter.vue
Normal file
12
frontend/components/ui/sheet/SheetFooter.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
12
frontend/components/ui/sheet/SheetHeader.vue
Normal file
12
frontend/components/ui/sheet/SheetHeader.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
20
frontend/components/ui/sheet/SheetTitle.vue
Normal file
20
frontend/components/ui/sheet/SheetTitle.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTitleProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DialogTitle } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogTitle
|
||||||
|
:class="cn('text-lg font-semibold text-foreground', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogTitle>
|
||||||
|
</template>
|
||||||
12
frontend/components/ui/sheet/SheetTrigger.vue
Normal file
12
frontend/components/ui/sheet/SheetTrigger.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTriggerProps } from 'reka-ui'
|
||||||
|
import { DialogTrigger } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DialogTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogTrigger v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</DialogTrigger>
|
||||||
|
</template>
|
||||||
32
frontend/components/ui/sheet/index.ts
Normal file
32
frontend/components/ui/sheet/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
|
||||||
|
export { default as Sheet } from './Sheet.vue'
|
||||||
|
export { default as SheetClose } from './SheetClose.vue'
|
||||||
|
export { default as SheetContent } from './SheetContent.vue'
|
||||||
|
export { default as SheetDescription } from './SheetDescription.vue'
|
||||||
|
export { default as SheetFooter } from './SheetFooter.vue'
|
||||||
|
export { default as SheetHeader } from './SheetHeader.vue'
|
||||||
|
export { default as SheetTitle } from './SheetTitle.vue'
|
||||||
|
export { default as SheetTrigger } from './SheetTrigger.vue'
|
||||||
|
|
||||||
|
export const sheetVariants = cva(
|
||||||
|
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
side: {
|
||||||
|
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||||
|
bottom:
|
||||||
|
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||||
|
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||||
|
right:
|
||||||
|
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
side: 'right',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type SheetVariants = VariantProps<typeof sheetVariants>
|
||||||
92
frontend/components/ui/sidebar/Sidebar.vue
Normal file
92
frontend/components/ui/sidebar/Sidebar.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SidebarProps } from '.'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
||||||
|
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SidebarProps>(), {
|
||||||
|
side: 'left',
|
||||||
|
variant: 'sidebar',
|
||||||
|
collapsible: 'offcanvas',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="collapsible === 'none'"
|
||||||
|
:class="
|
||||||
|
cn('flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground', props.class)
|
||||||
|
"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
|
||||||
|
<SheetContent
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
data-mobile="true"
|
||||||
|
:side="side"
|
||||||
|
class="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||||
|
:style="{
|
||||||
|
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex h-full w-full flex-col">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="group peer hidden md:block"
|
||||||
|
:data-state="state"
|
||||||
|
:data-collapsible="state === 'collapsed' ? collapsible : ''"
|
||||||
|
:data-variant="variant"
|
||||||
|
:data-side="side"
|
||||||
|
>
|
||||||
|
<!-- This is what handles the sidebar gap on desktop -->
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear',
|
||||||
|
'group-data-[collapsible=offcanvas]:w-0',
|
||||||
|
'group-data-[side=right]:rotate-180',
|
||||||
|
variant === 'floating' || variant === 'inset'
|
||||||
|
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
|
||||||
|
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex',
|
||||||
|
side === 'left'
|
||||||
|
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||||
|
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||||
|
// Adjust the padding for floating and inset variants.
|
||||||
|
variant === 'floating' || variant === 'inset'
|
||||||
|
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+_2px)]'
|
||||||
|
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
class="flex h-full w-full flex-col text-sidebar-foreground bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
22
frontend/components/ui/sidebar/SidebarContent.vue
Normal file
22
frontend/components/ui/sidebar/SidebarContent.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-sidebar="content"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
14
frontend/components/ui/sidebar/SidebarFooter.vue
Normal file
14
frontend/components/ui/sidebar/SidebarFooter.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div data-sidebar="footer" :class="cn('flex flex-col gap-2 p-2', props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
14
frontend/components/ui/sidebar/SidebarGroup.vue
Normal file
14
frontend/components/ui/sidebar/SidebarGroup.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div data-sidebar="group" :class="cn('relative flex w-full min-w-0 flex-col p-2', props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
30
frontend/components/ui/sidebar/SidebarGroupAction.vue
Normal file
30
frontend/components/ui/sidebar/SidebarGroupAction.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { Primitive } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<
|
||||||
|
PrimitiveProps & {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-sidebar="group-action"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'after:absolute after:-inset-2 after:md:hidden',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
14
frontend/components/ui/sidebar/SidebarGroupContent.vue
Normal file
14
frontend/components/ui/sidebar/SidebarGroupContent.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div data-sidebar="group-content" :class="cn('w-full text-sm', props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
29
frontend/components/ui/sidebar/SidebarGroupLabel.vue
Normal file
29
frontend/components/ui/sidebar/SidebarGroupLabel.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { Primitive } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<
|
||||||
|
PrimitiveProps & {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-sidebar="group-label"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
14
frontend/components/ui/sidebar/SidebarHeader.vue
Normal file
14
frontend/components/ui/sidebar/SidebarHeader.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div data-sidebar="header" :class="cn('flex flex-col gap-2 p-2', props.class)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
23
frontend/components/ui/sidebar/SidebarInput.vue
Normal file
23
frontend/components/ui/sidebar/SidebarInput.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Input
|
||||||
|
data-sidebar="input"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Input>
|
||||||
|
</template>
|
||||||
22
frontend/components/ui/sidebar/SidebarInset.vue
Normal file
22
frontend/components/ui/sidebar/SidebarInset.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'relative flex min-h-svh flex-1 flex-col bg-background',
|
||||||
|
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
14
frontend/components/ui/sidebar/SidebarMenu.vue
Normal file
14
frontend/components/ui/sidebar/SidebarMenu.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul data-sidebar="menu" :class="cn('flex w-full min-w-0 flex-col gap-1', props.class)">
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
41
frontend/components/ui/sidebar/SidebarMenuAction.vue
Normal file
41
frontend/components/ui/sidebar/SidebarMenuAction.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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 & {
|
||||||
|
showOnHover?: boolean
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
>(),
|
||||||
|
{
|
||||||
|
as: 'button',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-sidebar="menu-action"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
'after:absolute after:-inset-2 after:md:hidden',
|
||||||
|
'peer-data-[size=sm]/menu-button:top-1',
|
||||||
|
'peer-data-[size=default]/menu-button:top-1.5',
|
||||||
|
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
showOnHover &&
|
||||||
|
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
27
frontend/components/ui/sidebar/SidebarMenuBadge.vue
Normal file
27
frontend/components/ui/sidebar/SidebarMenuBadge.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none',
|
||||||
|
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
||||||
|
'peer-data-[size=sm]/menu-button:top-1',
|
||||||
|
'peer-data-[size=default]/menu-button:top-1.5',
|
||||||
|
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
49
frontend/components/ui/sidebar/SidebarMenuButton.vue
Normal file
49
frontend/components/ui/sidebar/SidebarMenuButton.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import type { SidebarMenuButtonProps } from './SidebarMenuButtonChild.vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import SidebarMenuButtonChild from './SidebarMenuButtonChild.vue'
|
||||||
|
import { useSidebar } from './utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<
|
||||||
|
SidebarMenuButtonProps & {
|
||||||
|
tooltip?: string | Component
|
||||||
|
}
|
||||||
|
>(),
|
||||||
|
{
|
||||||
|
as: 'button',
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { isMobile, state } = useSidebar()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'tooltip')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }">
|
||||||
|
<slot />
|
||||||
|
</SidebarMenuButtonChild>
|
||||||
|
|
||||||
|
<Tooltip v-else>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }">
|
||||||
|
<slot />
|
||||||
|
</SidebarMenuButtonChild>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" align="center" :hidden="state !== 'collapsed' || isMobile">
|
||||||
|
<template v-if="typeof tooltip === 'string'">
|
||||||
|
{{ tooltip }}
|
||||||
|
</template>
|
||||||
|
<component :is="tooltip" v-else />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
35
frontend/components/ui/sidebar/SidebarMenuButtonChild.vue
Normal file
35
frontend/components/ui/sidebar/SidebarMenuButtonChild.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import type { SidebarMenuButtonVariants } from '.'
|
||||||
|
import { Primitive } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { sidebarMenuButtonVariants } from '.'
|
||||||
|
|
||||||
|
export interface SidebarMenuButtonProps extends PrimitiveProps {
|
||||||
|
variant?: SidebarMenuButtonVariants['variant']
|
||||||
|
size?: SidebarMenuButtonVariants['size']
|
||||||
|
isActive?: boolean
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SidebarMenuButtonProps>(), {
|
||||||
|
as: 'button',
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-sidebar="menu-button"
|
||||||
|
:data-size="size"
|
||||||
|
:data-active="isActive"
|
||||||
|
:class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
14
frontend/components/ui/sidebar/SidebarMenuItem.vue
Normal file
14
frontend/components/ui/sidebar/SidebarMenuItem.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li data-sidebar="menu-item" :class="cn('group/menu-item relative', props.class)">
|
||||||
|
<slot />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
30
frontend/components/ui/sidebar/SidebarMenuSkeleton.vue
Normal file
30
frontend/components/ui/sidebar/SidebarMenuSkeleton.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
showIcon?: boolean
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const width = computed(() => {
|
||||||
|
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-sidebar="menu-skeleton"
|
||||||
|
:class="cn('rounded-md h-8 flex gap-2 px-2 items-center', props.class)"
|
||||||
|
>
|
||||||
|
<Skeleton v-if="showIcon" class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
|
||||||
|
|
||||||
|
<Skeleton
|
||||||
|
class="h-4 flex-1 max-w-[--skeleton-width]"
|
||||||
|
data-sidebar="menu-skeleton-text"
|
||||||
|
:style="{ '--skeleton-width': width }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
23
frontend/components/ui/sidebar/SidebarMenuSub.vue
Normal file
23
frontend/components/ui/sidebar/SidebarMenuSub.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
42
frontend/components/ui/sidebar/SidebarMenuSubButton.vue
Normal file
42
frontend/components/ui/sidebar/SidebarMenuSubButton.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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 & {
|
||||||
|
size?: 'sm' | 'md'
|
||||||
|
isActive?: boolean
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
>(),
|
||||||
|
{
|
||||||
|
as: 'a',
|
||||||
|
size: 'md',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-sidebar="menu-sub-button"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:data-size="size"
|
||||||
|
:data-active="isActive"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
|
||||||
|
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||||
|
size === 'sm' && 'text-xs',
|
||||||
|
size === 'md' && 'text-sm',
|
||||||
|
'group-data-[collapsible=icon]:hidden',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
7
frontend/components/ui/sidebar/SidebarMenuSubItem.vue
Normal file
7
frontend/components/ui/sidebar/SidebarMenuSubItem.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li>
|
||||||
|
<slot />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
96
frontend/components/ui/sidebar/SidebarProvider.vue
Normal file
96
frontend/components/ui/sidebar/SidebarProvider.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes, Ref } from 'vue'
|
||||||
|
import { defaultDocument, useEventListener, useMediaQuery, useVModel } from '@vueuse/core'
|
||||||
|
import { TooltipProvider } from 'reka-ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
provideSidebarContext,
|
||||||
|
SIDEBAR_COOKIE_MAX_AGE,
|
||||||
|
SIDEBAR_COOKIE_NAME,
|
||||||
|
SIDEBAR_KEYBOARD_SHORTCUT,
|
||||||
|
SIDEBAR_WIDTH,
|
||||||
|
SIDEBAR_WIDTH_ICON,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
defaultOpen?: boolean
|
||||||
|
open?: boolean
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
|
||||||
|
open: undefined,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
'update:open': [open: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery('(max-width: 768px)')
|
||||||
|
const openMobile = ref(false)
|
||||||
|
|
||||||
|
const open = useVModel(props, 'open', emits, {
|
||||||
|
defaultValue: props.defaultOpen ?? false,
|
||||||
|
passive: (props.open === undefined) as false,
|
||||||
|
}) as Ref<boolean>
|
||||||
|
|
||||||
|
function setOpen(value: boolean) {
|
||||||
|
open.value = value // emits('update:open', value)
|
||||||
|
|
||||||
|
// This sets the cookie to keep the sidebar state.
|
||||||
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOpenMobile(value: boolean) {
|
||||||
|
openMobile.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to toggle the sidebar.
|
||||||
|
function toggleSidebar() {
|
||||||
|
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEventListener('keydown', (event: KeyboardEvent) => {
|
||||||
|
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
||||||
|
event.preventDefault()
|
||||||
|
toggleSidebar()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||||
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||||
|
const state = computed(() => (open.value ? 'expanded' : 'collapsed'))
|
||||||
|
|
||||||
|
provideSidebarContext({
|
||||||
|
state,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
isMobile,
|
||||||
|
openMobile,
|
||||||
|
setOpenMobile,
|
||||||
|
toggleSidebar,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipProvider :delay-duration="0">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'--sidebar-width': SIDEBAR_WIDTH,
|
||||||
|
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||||
|
}"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
34
frontend/components/ui/sidebar/SidebarRail.vue
Normal file
34
frontend/components/ui/sidebar/SidebarRail.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useSidebar } from './utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { toggleSidebar } = useSidebar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
data-sidebar="rail"
|
||||||
|
aria-label="Toggle Sidebar"
|
||||||
|
:tabindex="-1"
|
||||||
|
title="Toggle Sidebar"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
||||||
|
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
|
||||||
|
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||||
|
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
|
||||||
|
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
||||||
|
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
15
frontend/components/ui/sidebar/SidebarSeparator.vue
Normal file
15
frontend/components/ui/sidebar/SidebarSeparator.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Separator data-sidebar="separator" :class="cn('mx-2 w-auto bg-sidebar-border', props.class)">
|
||||||
|
<slot />
|
||||||
|
</Separator>
|
||||||
|
</template>
|
||||||
26
frontend/components/ui/sidebar/SidebarTrigger.vue
Normal file
26
frontend/components/ui/sidebar/SidebarTrigger.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { PanelLeft } from 'lucide-vue-next'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useSidebar } from './utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { toggleSidebar } = useSidebar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
data-sidebar="trigger"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
:class="cn('h-7 w-7', props.class)"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
>
|
||||||
|
<PanelLeft />
|
||||||
|
<span class="sr-only">Toggle Sidebar</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
60
frontend/components/ui/sidebar/index.ts
Normal file
60
frontend/components/ui/sidebar/index.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
|
||||||
|
export interface SidebarProps {
|
||||||
|
side?: 'left' | 'right'
|
||||||
|
variant?: 'sidebar' | 'floating' | 'inset'
|
||||||
|
collapsible?: 'offcanvas' | 'icon' | 'none'
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
|
||||||
|
export { default as Sidebar } from './Sidebar.vue'
|
||||||
|
export { default as SidebarContent } from './SidebarContent.vue'
|
||||||
|
export { default as SidebarFooter } from './SidebarFooter.vue'
|
||||||
|
export { default as SidebarGroup } from './SidebarGroup.vue'
|
||||||
|
export { default as SidebarGroupAction } from './SidebarGroupAction.vue'
|
||||||
|
export { default as SidebarGroupContent } from './SidebarGroupContent.vue'
|
||||||
|
export { default as SidebarGroupLabel } from './SidebarGroupLabel.vue'
|
||||||
|
export { default as SidebarHeader } from './SidebarHeader.vue'
|
||||||
|
export { default as SidebarInput } from './SidebarInput.vue'
|
||||||
|
export { default as SidebarInset } from './SidebarInset.vue'
|
||||||
|
export { default as SidebarMenu } from './SidebarMenu.vue'
|
||||||
|
export { default as SidebarMenuAction } from './SidebarMenuAction.vue'
|
||||||
|
export { default as SidebarMenuBadge } from './SidebarMenuBadge.vue'
|
||||||
|
export { default as SidebarMenuButton } from './SidebarMenuButton.vue'
|
||||||
|
export { default as SidebarMenuItem } from './SidebarMenuItem.vue'
|
||||||
|
export { default as SidebarMenuSkeleton } from './SidebarMenuSkeleton.vue'
|
||||||
|
export { default as SidebarMenuSub } from './SidebarMenuSub.vue'
|
||||||
|
export { default as SidebarMenuSubButton } from './SidebarMenuSubButton.vue'
|
||||||
|
export { default as SidebarMenuSubItem } from './SidebarMenuSubItem.vue'
|
||||||
|
export { default as SidebarProvider } from './SidebarProvider.vue'
|
||||||
|
export { default as SidebarRail } from './SidebarRail.vue'
|
||||||
|
export { default as SidebarSeparator } from './SidebarSeparator.vue'
|
||||||
|
export { default as SidebarTrigger } from './SidebarTrigger.vue'
|
||||||
|
|
||||||
|
export { useSidebar } from './utils'
|
||||||
|
|
||||||
|
export const sidebarMenuButtonVariants = cva(
|
||||||
|
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||||
|
outline:
|
||||||
|
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-8 text-sm',
|
||||||
|
sm: 'h-7 text-xs',
|
||||||
|
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants>
|
||||||
19
frontend/components/ui/sidebar/utils.ts
Normal file
19
frontend/components/ui/sidebar/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { ComputedRef, Ref } from 'vue'
|
||||||
|
import { createContext } from 'reka-ui'
|
||||||
|
|
||||||
|
export const SIDEBAR_COOKIE_NAME = 'sidebar_state'
|
||||||
|
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||||
|
export const SIDEBAR_WIDTH = '16rem'
|
||||||
|
export const SIDEBAR_WIDTH_MOBILE = '18rem'
|
||||||
|
export const SIDEBAR_WIDTH_ICON = '3rem'
|
||||||
|
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
|
||||||
|
|
||||||
|
export const [useSidebar, provideSidebarContext] = createContext<{
|
||||||
|
state: ComputedRef<'expanded' | 'collapsed'>
|
||||||
|
open: Ref<boolean>
|
||||||
|
setOpen: (value: boolean) => void
|
||||||
|
isMobile: Ref<boolean>
|
||||||
|
openMobile: Ref<boolean>
|
||||||
|
setOpenMobile: (value: boolean) => void
|
||||||
|
toggleSidebar: () => void
|
||||||
|
}>('Sidebar')
|
||||||
14
frontend/components/ui/skeleton/Skeleton.vue
Normal file
14
frontend/components/ui/skeleton/Skeleton.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface SkeletonProps {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<SkeletonProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('animate-pulse rounded-md bg-primary/10', props.class)" />
|
||||||
|
</template>
|
||||||
1
frontend/components/ui/skeleton/index.ts
Normal file
1
frontend/components/ui/skeleton/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Skeleton } from './Skeleton.vue'
|
||||||
15
frontend/components/ui/tooltip/Tooltip.vue
Normal file
15
frontend/components/ui/tooltip/Tooltip.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TooltipRootEmits, TooltipRootProps } from 'reka-ui'
|
||||||
|
import { TooltipRoot, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<TooltipRootProps>()
|
||||||
|
const emits = defineEmits<TooltipRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipRoot v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</TooltipRoot>
|
||||||
|
</template>
|
||||||
40
frontend/components/ui/tooltip/TooltipContent.vue
Normal file
40
frontend/components/ui/tooltip/TooltipContent.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||||
|
{
|
||||||
|
sideOffset: 4,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emits = defineEmits<TooltipContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipPortal>
|
||||||
|
<TooltipContent
|
||||||
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</template>
|
||||||
12
frontend/components/ui/tooltip/TooltipProvider.vue
Normal file
12
frontend/components/ui/tooltip/TooltipProvider.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TooltipProviderProps } from 'reka-ui'
|
||||||
|
import { TooltipProvider } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<TooltipProviderProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipProvider v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
12
frontend/components/ui/tooltip/TooltipTrigger.vue
Normal file
12
frontend/components/ui/tooltip/TooltipTrigger.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TooltipTriggerProps } from 'reka-ui'
|
||||||
|
import { TooltipTrigger } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<TooltipTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TooltipTrigger v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</TooltipTrigger>
|
||||||
|
</template>
|
||||||
4
frontend/components/ui/tooltip/index.ts
Normal file
4
frontend/components/ui/tooltip/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as Tooltip } from './Tooltip.vue'
|
||||||
|
export { default as TooltipContent } from './TooltipContent.vue'
|
||||||
|
export { default as TooltipProvider } from './TooltipProvider.vue'
|
||||||
|
export { default as TooltipTrigger } from './TooltipTrigger.vue'
|
||||||
66
frontend/layouts/default.vue
Normal file
66
frontend/layouts/default.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import AppSidebar from '@/components/AppSidebar.vue'
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from '@/components/ui/breadcrumb'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => {
|
||||||
|
const paths = route.path.split('/').filter(Boolean)
|
||||||
|
return paths.map((path, index) => ({
|
||||||
|
name: path.charAt(0).toUpperCase() + path.slice(1),
|
||||||
|
path: '/' + paths.slice(0, index + 1).join('/'),
|
||||||
|
isLast: index === paths.length - 1,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SidebarProvider>
|
||||||
|
<AppSidebar />
|
||||||
|
<SidebarInset>
|
||||||
|
<header
|
||||||
|
class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 px-4">
|
||||||
|
<SidebarTrigger class="-ml-1" />
|
||||||
|
<Separator orientation="vertical" class="mr-2 data-[orientation=vertical]:h-4" />
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink as-child>
|
||||||
|
<NuxtLink to="/">Home</NuxtLink>
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<template v-if="breadcrumbs.length > 0">
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<template v-for="(crumb, index) in breadcrumbs" :key="crumb.path">
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage v-if="crumb.isLast">
|
||||||
|
{{ crumb.name }}
|
||||||
|
</BreadcrumbPage>
|
||||||
|
<BreadcrumbLink v-else as-child>
|
||||||
|
<NuxtLink :to="crumb.path">{{ crumb.name }}</NuxtLink>
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator v-if="!crumb.isLast" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</SidebarInset>
|
||||||
|
</SidebarProvider>
|
||||||
|
</template>
|
||||||
@@ -16,7 +16,7 @@ export default defineNuxtConfig({
|
|||||||
* @link https://nuxt.com/docs/api/nuxt-config#alias
|
* @link https://nuxt.com/docs/api/nuxt-config#alias
|
||||||
* @default "@/components/ui"
|
* @default "@/components/ui"
|
||||||
*/
|
*/
|
||||||
componentDir: '@/components/ui'
|
componentDir: '@/components/ui',
|
||||||
},
|
},
|
||||||
|
|
||||||
colorMode: {
|
colorMode: {
|
||||||
@@ -48,11 +48,7 @@ export default defineNuxtConfig({
|
|||||||
hmr: {
|
hmr: {
|
||||||
clientPort: 3001,
|
clientPort: 3001,
|
||||||
},
|
},
|
||||||
allowedHosts: [
|
allowedHosts: ['jupiter.routebox.co', 'localhost', '127.0.0.1'],
|
||||||
'jupiter.routebox.co',
|
|
||||||
'localhost',
|
|
||||||
'127.0.0.1',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
4306
frontend/package-lock.json
generated
4306
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,10 @@
|
|||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "eslint --ext .ts,.js,.vue .",
|
||||||
|
"lint:fix": "eslint --ext .ts,.js,.vue . --fix",
|
||||||
|
"format": "prettier --write \"**/*.{js,ts,vue,json,css,scss,md}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.11.4",
|
"@nuxtjs/tailwindcss": "^6.11.4",
|
||||||
@@ -27,9 +30,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/color-mode": "^3.3.2",
|
"@nuxtjs/color-mode": "^3.3.2",
|
||||||
|
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ const fetchRecord = async () => {
|
|||||||
objectApiName.value = page.objectApiName
|
objectApiName.value = page.objectApiName
|
||||||
|
|
||||||
// Then fetch the record
|
// Then fetch the record
|
||||||
record.value = await api.get(`/runtime/objects/${objectApiName.value}/records/${recordId.value}`)
|
record.value = await api.get(
|
||||||
|
`/runtime/objects/${objectApiName.value}/records/${recordId.value}`
|
||||||
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e.message
|
error.value = e.message
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-background">
|
<NuxtLayout name="default">
|
||||||
<header class="border-b">
|
|
||||||
<div class="container mx-auto px-4 py-4">
|
|
||||||
<h1 class="text-2xl font-bold">Neo Platform</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="container mx-auto px-4 py-8">
|
|
||||||
<div class="text-center space-y-6">
|
<div class="text-center space-y-6">
|
||||||
<h2 class="text-4xl font-bold">Welcome to Neo Platform</h2>
|
<h2 class="text-4xl font-bold">Welcome to Neo Platform</h2>
|
||||||
<p class="text-muted-foreground text-lg">
|
<p class="text-muted-foreground text-lg">
|
||||||
@@ -26,6 +20,5 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</NuxtLayout>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import LoginForm from '@/components/LoginForm.vue'
|
|||||||
<div class="flex flex-col gap-4 p-6 md:p-10">
|
<div class="flex flex-col gap-4 p-6 md:p-10">
|
||||||
<div class="flex justify-center gap-2 md:justify-start">
|
<div class="flex justify-center gap-2 md:justify-start">
|
||||||
<NuxtLink to="/" class="flex items-center gap-2 font-medium">
|
<NuxtLink to="/" class="flex items-center gap-2 font-medium">
|
||||||
<div class="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
|
<div
|
||||||
|
class="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md"
|
||||||
|
>
|
||||||
<LayoutGrid class="size-4" />
|
<LayoutGrid class="size-4" />
|
||||||
</div>
|
</div>
|
||||||
Neo Platform
|
Neo Platform
|
||||||
@@ -25,7 +27,7 @@ import LoginForm from '@/components/LoginForm.vue'
|
|||||||
src="https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=1200&auto=format&fit=crop&q=80"
|
src="https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=1200&auto=format&fit=crop&q=80"
|
||||||
alt="Login cover"
|
alt="Login cover"
|
||||||
class="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
class="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -19,13 +19,7 @@
|
|||||||
<form @submit.prevent="handleRegister" class="space-y-4">
|
<form @submit.prevent="handleRegister" class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="tenantId">Tenant ID</Label>
|
<Label for="tenantId">Tenant ID</Label>
|
||||||
<Input
|
<Input id="tenantId" v-model="tenantId" type="text" required placeholder="123" />
|
||||||
id="tenantId"
|
|
||||||
v-model="tenantId"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder="123"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -55,30 +49,16 @@
|
|||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="firstName">First Name</Label>
|
<Label for="firstName">First Name</Label>
|
||||||
<Input
|
<Input id="firstName" v-model="firstName" type="text" placeholder="John" />
|
||||||
id="firstName"
|
|
||||||
v-model="firstName"
|
|
||||||
type="text"
|
|
||||||
placeholder="John"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="lastName">Last Name</Label>
|
<Label for="lastName">Last Name</Label>
|
||||||
<Input
|
<Input id="lastName" v-model="lastName" type="text" placeholder="Doe" />
|
||||||
id="lastName"
|
|
||||||
v-model="lastName"
|
|
||||||
type="text"
|
|
||||||
placeholder="Doe"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" :disabled="loading" class="w-full">
|
||||||
type="submit"
|
|
||||||
:disabled="loading"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
{{ loading ? 'Creating account...' : 'Create Account' }}
|
{{ loading ? 'Creating account...' : 'Create Account' }}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -86,9 +66,7 @@
|
|||||||
<CardFooter class="flex justify-center">
|
<CardFooter class="flex justify-center">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
<NuxtLink to="/login" class="text-primary hover:underline font-medium">
|
<NuxtLink to="/login" class="text-primary hover:underline font-medium">Login</NuxtLink>
|
||||||
Login
|
|
||||||
</NuxtLink>
|
|
||||||
</p>
|
</p>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-background">
|
<div class="min-h-screen bg-background">
|
||||||
<header class="border-b">
|
<NuxtLayout name="default">
|
||||||
<div class="container mx-auto px-4 py-4">
|
|
||||||
<NuxtLink to="/" class="text-xl font-bold">Neo Platform</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
<div v-if="loading" class="text-center py-12">Loading...</div>
|
<div v-if="loading" class="text-center py-12">Loading...</div>
|
||||||
<div v-else-if="error" class="text-destructive">Error: {{ error }}</div>
|
<div v-else-if="error" class="text-destructive">Error: {{ error }}</div>
|
||||||
@@ -20,11 +16,7 @@
|
|||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h2 class="text-2xl font-semibold mb-4">Pages</h2>
|
<h2 class="text-2xl font-semibold mb-4">Pages</h2>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div v-for="page in app.pages" :key="page.id" class="p-4 border rounded-lg bg-card">
|
||||||
v-for="page in app.pages"
|
|
||||||
:key="page.id"
|
|
||||||
class="p-4 border rounded-lg bg-card"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-semibold">{{ page.label }}</h3>
|
<h3 class="font-semibold">{{ page.label }}</h3>
|
||||||
@@ -38,6 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-background">
|
<div class="min-h-screen bg-background">
|
||||||
<header class="border-b">
|
<NuxtLayout name="default">
|
||||||
<div class="container mx-auto px-4 py-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<NuxtLink to="/" class="text-xl font-bold">Neo Platform</NuxtLink>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<NuxtLink
|
|
||||||
to="/setup/apps"
|
|
||||||
class="text-sm text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
Apps
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/setup/objects"
|
|
||||||
class="text-sm text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
Objects
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
<div v-if="loading" class="text-center py-12">Loading...</div>
|
<div v-if="loading" class="text-center py-12">Loading...</div>
|
||||||
<div v-else-if="error" class="text-destructive">Error: {{ error }}</div>
|
<div v-else-if="error" class="text-destructive">Error: {{ error }}</div>
|
||||||
@@ -92,7 +72,9 @@
|
|||||||
class="p-6 border rounded-lg hover:border-primary transition-colors bg-card"
|
class="p-6 border rounded-lg hover:border-primary transition-colors bg-card"
|
||||||
>
|
>
|
||||||
<h3 class="text-xl font-semibold mb-2">{{ app.label }}</h3>
|
<h3 class="text-xl font-semibold mb-2">{{ app.label }}</h3>
|
||||||
<p class="text-sm text-muted-foreground mb-4">{{ app.description || 'No description' }}</p>
|
<p class="text-sm text-muted-foreground mb-4">
|
||||||
|
{{ app.description || 'No description' }}
|
||||||
|
</p>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="text-muted-foreground">{{ app.pages?.length || 0 }} pages</span>
|
<span class="text-muted-foreground">{{ app.pages?.length || 0 }} pages</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,6 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-background">
|
<div class="min-h-screen bg-background">
|
||||||
<header class="border-b">
|
<NuxtLayout name="default">
|
||||||
<div class="container mx-auto px-4 py-4">
|
|
||||||
<NuxtLink to="/" class="text-xl font-bold">Neo Platform</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
<div v-if="loading" class="text-center py-12">Loading...</div>
|
<div v-if="loading" class="text-center py-12">Loading...</div>
|
||||||
<div v-else-if="error" class="text-destructive">Error: {{ error }}</div>
|
<div v-else-if="error" class="text-destructive">Error: {{ error }}</div>
|
||||||
@@ -33,10 +29,16 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 text-xs">
|
<div class="flex gap-2 text-xs">
|
||||||
<span v-if="field.isRequired" class="px-2 py-1 bg-destructive/10 text-destructive rounded">
|
<span
|
||||||
|
v-if="field.isRequired"
|
||||||
|
class="px-2 py-1 bg-destructive/10 text-destructive rounded"
|
||||||
|
>
|
||||||
Required
|
Required
|
||||||
</span>
|
</span>
|
||||||
<span v-if="field.isUnique" class="px-2 py-1 bg-primary/10 text-primary rounded">
|
<span
|
||||||
|
v-if="field.isUnique"
|
||||||
|
class="px-2 py-1 bg-primary/10 text-primary rounded"
|
||||||
|
>
|
||||||
Unique
|
Unique
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-background">
|
<div class="min-h-screen bg-background">
|
||||||
<header class="border-b">
|
<NuxtLayout name="default">
|
||||||
<div class="container mx-auto px-4 py-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<NuxtLink to="/" class="text-xl font-bold">Neo Platform</NuxtLink>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<NuxtLink
|
|
||||||
to="/setup/apps"
|
|
||||||
class="text-sm text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
Apps
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/setup/objects"
|
|
||||||
class="text-sm text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
Objects
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
<div v-if="loading" class="text-center py-12">Loading...</div>
|
<div v-if="loading" class="text-center py-12">Loading...</div>
|
||||||
<div v-else-if="error" class="text-destructive">Error: {{ error }}</div>
|
<div v-else-if="error" class="text-destructive">Error: {{ error }}</div>
|
||||||
@@ -109,7 +89,9 @@
|
|||||||
System
|
System
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted-foreground mb-4">{{ obj.description || 'No description' }}</p>
|
<p class="text-sm text-muted-foreground mb-4">
|
||||||
|
{{ obj.description || 'No description' }}
|
||||||
|
</p>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="text-muted-foreground">{{ obj.fields?.length || 0 }} fields</span>
|
<span class="text-muted-foreground">{{ obj.fields?.length || 0 }} fields</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,6 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default {
|
|||||||
'./plugins/**/*.{js,ts}',
|
'./plugins/**/*.{js,ts}',
|
||||||
'./app.vue',
|
'./app.vue',
|
||||||
],
|
],
|
||||||
darkMode: 'class',
|
darkMode: ['class', 'class'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
@@ -40,6 +40,16 @@ export default {
|
|||||||
DEFAULT: 'hsl(var(--card))',
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: 'hsl(var(--card-foreground))',
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
},
|
},
|
||||||
|
sidebar: {
|
||||||
|
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||||
|
foreground: 'hsl(var(--sidebar-foreground))',
|
||||||
|
primary: 'hsl(var(--sidebar-primary))',
|
||||||
|
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||||
|
accent: 'hsl(var(--sidebar-accent))',
|
||||||
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||||
|
border: 'hsl(var(--sidebar-border))',
|
||||||
|
ring: 'hsl(var(--sidebar-ring))',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: 'var(--radius)',
|
lg: 'var(--radius)',
|
||||||
|
|||||||
Reference in New Issue
Block a user