route breadcrumbs, improve sidebar, basic folder layout
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -25,6 +25,7 @@
|
|||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.57",
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
@@ -210,6 +211,8 @@
|
|||||||
|
|
||||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||||
|
|
||||||
|
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.57", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-I1CIObdPBIL/9v75KKoyHWNhq+qqN6ef8+iJY4AVpHLtnRu0Vbp6K0TKcoYZ70U+EgiL6krEbFdcjK3+fwpfHQ=="],
|
||||||
|
|
||||||
"@iconify/collections": ["@iconify/collections@1.0.566", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-rilPX42tJve3z/HUQdZBrMLH/d8OZlMw+D+nMsxsiC/Miyws/WGq8DtV2w9jGGZBd6HJ+VQ3prIIV7UWhV+2ug=="],
|
"@iconify/collections": ["@iconify/collections@1.0.566", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-rilPX42tJve3z/HUQdZBrMLH/d8OZlMw+D+nMsxsiC/Miyws/WGq8DtV2w9jGGZBd6HJ+VQ3prIIV7UWhV+2ug=="],
|
||||||
|
|
||||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||||
|
|||||||
@@ -2,22 +2,28 @@
|
|||||||
import { Icon } from '#components';
|
import { Icon } from '#components';
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
CollapsibleContent,
|
||||||
|
} from '@/components/ui/collapsible';
|
||||||
|
|
||||||
const items = [
|
const route = useRoute();
|
||||||
|
|
||||||
|
const warrens = [
|
||||||
{
|
{
|
||||||
title: 'Home',
|
title: 'Thyr',
|
||||||
url: '/',
|
url: '/warrens/Thyr',
|
||||||
icon: h(Icon, { name: 'lucide:home' }),
|
icon: h(Icon, { name: 'lucide:folder-root' }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'About',
|
title: 'Serc',
|
||||||
url: '/about',
|
url: '/warrens/Serc',
|
||||||
icon: h(Icon, { name: 'lucide:inbox' }),
|
icon: h(Icon, { name: 'lucide:folder-root' }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
@@ -26,18 +32,59 @@ const items = [
|
|||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Application</SidebarGroupLabel>
|
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem v-for="item in items" :key="item.title">
|
<Collapsible class="group/collapsible" :default-open="true">
|
||||||
<SidebarMenuButton as-child :tooltip="item.title">
|
<SidebarMenuItem>
|
||||||
<NuxtLink :to="item.url">
|
<NuxtLink to="/warrens" as-child>
|
||||||
<component :is="item.icon"></component>
|
<SidebarMenuButton
|
||||||
<span>{{ item.title }}</span>
|
tooltip="Your Warrens"
|
||||||
|
:is-active="route.path === '/warrens'"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:folder-tree" />
|
||||||
|
<span>Your Warrens</span>
|
||||||
|
<CollapsibleTrigger
|
||||||
|
as-child
|
||||||
|
@click="preventDefault"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="lucide:chevron-right"
|
||||||
|
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||||
|
/>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</SidebarMenuButton>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
<SidebarMenuSubItem
|
||||||
|
v-for="warren in warrens"
|
||||||
|
:key="warren.title"
|
||||||
|
>
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
as-child
|
||||||
|
:tooltip="warren.title"
|
||||||
|
:is-active="
|
||||||
|
warren.url === route.path
|
||||||
|
"
|
||||||
|
class="transition-all"
|
||||||
|
>
|
||||||
|
<NuxtLink :to="warren.url">
|
||||||
|
<component
|
||||||
|
:is="warren.icon"
|
||||||
|
></component>
|
||||||
|
<span>{{ warren.title }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
<SidebarFooter>
|
||||||
|
<SidebarUser />
|
||||||
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
24
components/DirectoryEntry.vue
Normal file
24
components/DirectoryEntry.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DirectoryEntryType } from '~/types';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const { name, entryType } = defineProps<{
|
||||||
|
name: string;
|
||||||
|
entryType: DirectoryEntryType;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const iconName = entryType === 'file' ? 'lucide:file' : 'lucide:folder';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button class="w-36 h-12" variant="outline" size="lg">
|
||||||
|
<NuxtLink
|
||||||
|
class="flex flex-row items-center gap-1.5"
|
||||||
|
:to="joinPaths(route.path, name)"
|
||||||
|
>
|
||||||
|
<Icon :name="iconName" />
|
||||||
|
<span>{{ name }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
35
components/DirectoryList.vue
Normal file
35
components/DirectoryList.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import type { DirectoryEntryType } from '~/types';
|
||||||
|
const items: Array<{ name: string; entryType: DirectoryEntryType }> = [
|
||||||
|
/* {
|
||||||
|
name: 'File A',
|
||||||
|
entryType: 'file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'File B',
|
||||||
|
entryType: 'file',
|
||||||
|
}, */
|
||||||
|
{
|
||||||
|
name: 'Directory A',
|
||||||
|
entryType: 'directory',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Directory B',
|
||||||
|
entryType: 'directory',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollArea class="w-full h-full">
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<DirectoryEntry
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.name"
|
||||||
|
:name="item.name"
|
||||||
|
:entry-type="item.entryType"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</template>
|
||||||
111
components/SidebarUser.vue
Normal file
111
components/SidebarUser.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuButton,
|
||||||
|
useSidebar,
|
||||||
|
} from '@/components/ui/sidebar';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||||
|
|
||||||
|
const { isMobile } = useSidebar();
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '409',
|
||||||
|
email: 'user@example.com',
|
||||||
|
avatar: 'https://cdn.discordapp.com/avatars/285424924049276939/0368b00056c416cae689ab1434c0aac0.webp',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
tooltip="Settings"
|
||||||
|
>
|
||||||
|
<Avatar class="h-8 w-8 rounded-lg">
|
||||||
|
<AvatarImage :src="user.avatar" />
|
||||||
|
<AvatarFallback class="rounded-lg"
|
||||||
|
>A</AvatarFallback
|
||||||
|
>
|
||||||
|
</Avatar>
|
||||||
|
<div
|
||||||
|
class="grid flex-1 text-left text-sm leading-tight"
|
||||||
|
>
|
||||||
|
<span class="truncate font-semibold">{{
|
||||||
|
user.name
|
||||||
|
}}</span>
|
||||||
|
<span class="truncate text-xs">{{
|
||||||
|
user.email
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
name="lucide:chevrons-up-down"
|
||||||
|
class="ml-auto size-4"
|
||||||
|
/>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
class="w-[--reka-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
|
align="end"
|
||||||
|
:side="isMobile ? 'bottom' : 'right'"
|
||||||
|
:side-offset="4"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel class="p-0 font-normal">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"
|
||||||
|
>
|
||||||
|
<Avatar class="h-8 w-8 rounded-lg">
|
||||||
|
<AvatarImage
|
||||||
|
:src="user.avatar"
|
||||||
|
:alt="user.name"
|
||||||
|
/>
|
||||||
|
<AvatarFallback class="rounded-lg">
|
||||||
|
CN
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div
|
||||||
|
class="grid flex-1 text-left text-sm leading-tight"
|
||||||
|
>
|
||||||
|
<span class="truncate font-semibold">{{
|
||||||
|
user.name
|
||||||
|
}}</span>
|
||||||
|
<span class="truncate text-xs">{{
|
||||||
|
user.email
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Icon name="lucide:user" />
|
||||||
|
Account
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Icon name="lucide:settings" />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Icon name="lucide:door-open" />
|
||||||
|
Log out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</template>
|
||||||
18
components/ui/avatar/Avatar.vue
Normal file
18
components/ui/avatar/Avatar.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { AvatarRoot } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarRoot
|
||||||
|
data-slot="avatar"
|
||||||
|
:class="cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AvatarRoot>
|
||||||
|
</template>
|
||||||
20
components/ui/avatar/AvatarFallback.vue
Normal file
20
components/ui/avatar/AvatarFallback.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { AvatarFallback, type AvatarFallbackProps } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<AvatarFallbackProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarFallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('bg-muted flex size-full items-center justify-center rounded-full', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AvatarFallback>
|
||||||
|
</template>
|
||||||
16
components/ui/avatar/AvatarImage.vue
Normal file
16
components/ui/avatar/AvatarImage.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AvatarImageProps } from 'reka-ui'
|
||||||
|
import { AvatarImage } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<AvatarImageProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarImage
|
||||||
|
data-slot="avatar-image"
|
||||||
|
v-bind="props"
|
||||||
|
class="aspect-square size-full"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</AvatarImage>
|
||||||
|
</template>
|
||||||
3
components/ui/avatar/index.ts
Normal file
3
components/ui/avatar/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as Avatar } from './Avatar.vue'
|
||||||
|
export { default as AvatarFallback } from './AvatarFallback.vue'
|
||||||
|
export { default as AvatarImage } from './AvatarImage.vue'
|
||||||
19
components/ui/collapsible/Collapsible.vue
Normal file
19
components/ui/collapsible/Collapsible.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'reka-ui'
|
||||||
|
import { CollapsibleRoot, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<CollapsibleRootProps>()
|
||||||
|
const emits = defineEmits<CollapsibleRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CollapsibleRoot
|
||||||
|
v-slot="{ open }"
|
||||||
|
data-slot="collapsible"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot :open="open" />
|
||||||
|
</CollapsibleRoot>
|
||||||
|
</template>
|
||||||
14
components/ui/collapsible/CollapsibleContent.vue
Normal file
14
components/ui/collapsible/CollapsibleContent.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CollapsibleContent, type CollapsibleContentProps } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<CollapsibleContentProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CollapsibleContent>
|
||||||
|
</template>
|
||||||
14
components/ui/collapsible/CollapsibleTrigger.vue
Normal file
14
components/ui/collapsible/CollapsibleTrigger.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CollapsibleTrigger, type CollapsibleTriggerProps } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<CollapsibleTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</template>
|
||||||
3
components/ui/collapsible/index.ts
Normal file
3
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'
|
||||||
17
components/ui/dropdown-menu/DropdownMenu.vue
Normal file
17
components/ui/dropdown-menu/DropdownMenu.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DropdownMenuRoot, type DropdownMenuRootEmits, type DropdownMenuRootProps, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuRootProps>()
|
||||||
|
const emits = defineEmits<DropdownMenuRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuRoot
|
||||||
|
data-slot="dropdown-menu"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuRoot>
|
||||||
|
</template>
|
||||||
38
components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
38
components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { Check } from 'lucide-vue-next'
|
||||||
|
import {
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
type DropdownMenuCheckboxItemEmits,
|
||||||
|
type DropdownMenuCheckboxItemProps,
|
||||||
|
DropdownMenuItemIndicator,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class=" cn(
|
||||||
|
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Check class="size-4" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</template>
|
||||||
36
components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
36
components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import {
|
||||||
|
DropdownMenuContent,
|
||||||
|
type DropdownMenuContentEmits,
|
||||||
|
type DropdownMenuContentProps,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||||
|
{
|
||||||
|
sideOffset: 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuContent
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</template>
|
||||||
14
components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
14
components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuGroupProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuGroup
|
||||||
|
data-slot="dropdown-menu-group"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</template>
|
||||||
30
components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
30
components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<DropdownMenuItemProps & {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
inset?: boolean
|
||||||
|
variant?: 'default' | 'destructive'
|
||||||
|
}>(), {
|
||||||
|
variant: 'default',
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'inset', 'variant', 'class')
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuItem
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
:data-inset="inset ? '' : undefined"
|
||||||
|
:data-variant="variant"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn(`focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`, props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</template>
|
||||||
22
components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
22
components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class', 'inset')
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuLabel
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
:data-inset="inset ? '' : undefined"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
</template>
|
||||||
22
components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
22
components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
type DropdownMenuRadioGroupEmits,
|
||||||
|
type DropdownMenuRadioGroupProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuRadioGroupProps>()
|
||||||
|
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuRadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</template>
|
||||||
39
components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
39
components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { Circle } from 'lucide-vue-next'
|
||||||
|
import {
|
||||||
|
DropdownMenuItemIndicator,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
type DropdownMenuRadioItemEmits,
|
||||||
|
type DropdownMenuRadioItemProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const emits = defineEmits<DropdownMenuRadioItemEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(
|
||||||
|
`focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Circle class="size-2 fill-current" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</template>
|
||||||
23
components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
23
components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import {
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
type DropdownMenuSeparatorProps,
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSeparatorProps & {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSeparator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
17
components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
17
components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
19
components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
19
components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
DropdownMenuSub,
|
||||||
|
type DropdownMenuSubEmits,
|
||||||
|
type DropdownMenuSubProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSubProps>()
|
||||||
|
const emits = defineEmits<DropdownMenuSubEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSub data-slot="dropdown-menu-sub" v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</template>
|
||||||
28
components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
28
components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import {
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
type DropdownMenuSubContentEmits,
|
||||||
|
type DropdownMenuSubContentProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</template>
|
||||||
30
components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
30
components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { ChevronRight } from 'lucide-vue-next'
|
||||||
|
import {
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
type DropdownMenuSubTriggerProps,
|
||||||
|
useForwardProps,
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class', 'inset')
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn(
|
||||||
|
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<ChevronRight class="ml-auto size-4" />
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
</template>
|
||||||
16
components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
16
components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DropdownMenuTrigger, type DropdownMenuTriggerProps, useForwardProps } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuTriggerProps>()
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(props)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</template>
|
||||||
16
components/ui/dropdown-menu/index.ts
Normal file
16
components/ui/dropdown-menu/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export { default as DropdownMenu } from './DropdownMenu.vue'
|
||||||
|
|
||||||
|
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'
|
||||||
|
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
|
||||||
|
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'
|
||||||
|
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
|
||||||
|
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'
|
||||||
|
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'
|
||||||
|
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'
|
||||||
|
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
|
||||||
|
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'
|
||||||
|
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
|
||||||
|
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
|
||||||
|
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
|
||||||
|
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
|
||||||
|
export { DropdownMenuPortal } from 'reka-ui'
|
||||||
33
components/ui/scroll-area/ScrollArea.vue
Normal file
33
components/ui/scroll-area/ScrollArea.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import {
|
||||||
|
ScrollAreaCorner,
|
||||||
|
ScrollAreaRoot,
|
||||||
|
type ScrollAreaRootProps,
|
||||||
|
ScrollAreaViewport,
|
||||||
|
} from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import ScrollBar from './ScrollBar.vue'
|
||||||
|
|
||||||
|
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollAreaRoot
|
||||||
|
data-slot="scroll-area"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('relative', props.class)"
|
||||||
|
>
|
||||||
|
<ScrollAreaViewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ScrollAreaViewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaCorner />
|
||||||
|
</ScrollAreaRoot>
|
||||||
|
</template>
|
||||||
31
components/ui/scroll-area/ScrollBar.vue
Normal file
31
components/ui/scroll-area/ScrollBar.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { ScrollAreaScrollbar, type ScrollAreaScrollbarProps, ScrollAreaThumb } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(), {
|
||||||
|
orientation: 'vertical',
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="
|
||||||
|
cn('flex touch-none p-px transition-colors select-none',
|
||||||
|
orientation === 'vertical'
|
||||||
|
&& 'h-full w-2.5 border-l border-l-transparent',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
&& 'h-2.5 flex-col border-t border-t-transparent',
|
||||||
|
props.class)"
|
||||||
|
>
|
||||||
|
<ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
class="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaScrollbar>
|
||||||
|
</template>
|
||||||
2
components/ui/scroll-area/index.ts
Normal file
2
components/ui/scroll-area/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as ScrollArea } from './ScrollArea.vue'
|
||||||
|
export { default as ScrollBar } from './ScrollBar.vue'
|
||||||
@@ -8,12 +8,15 @@ import {
|
|||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from '@/components/ui/breadcrumb';
|
} from '@/components/ui/breadcrumb';
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => getBreadcrumbs(route.path));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<main>
|
<main class="w-full grow">
|
||||||
<header
|
<header
|
||||||
class="h-16 flex items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"
|
class="h-16 flex items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"
|
||||||
>
|
>
|
||||||
@@ -22,15 +25,29 @@ import {
|
|||||||
<Separator orientation="vertical" class="mr-2 !h-4" />
|
<Separator orientation="vertical" class="mr-2 !h-4" />
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem class="hidden md:block">
|
<template
|
||||||
<BreadcrumbLink href="#">
|
v-for="(crumb, i) in breadcrumbs"
|
||||||
Bread
|
:key="i"
|
||||||
</BreadcrumbLink>
|
>
|
||||||
</BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbSeparator class="hidden md:block" />
|
<NuxtLink
|
||||||
<BreadcrumbItem>
|
v-if="i < breadcrumbs.length - 1"
|
||||||
<BreadcrumbPage>Crumb</BreadcrumbPage>
|
:to="crumb.href"
|
||||||
</BreadcrumbItem>
|
as-child
|
||||||
|
>
|
||||||
|
<BreadcrumbLink>
|
||||||
|
{{ crumb.name }}
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</NuxtLink>
|
||||||
|
<BreadcrumbPage v-else>{{
|
||||||
|
crumb.name
|
||||||
|
}}</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator
|
||||||
|
v-if="i < breadcrumbs.length - 1"
|
||||||
|
class="hidden md:block"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
icon: {
|
icon: {
|
||||||
mode: 'svg',
|
mode: 'svg',
|
||||||
|
serverBundle: 'local',
|
||||||
|
provider: 'iconify',
|
||||||
},
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.57",
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"prettier": "^3.6.2"
|
"prettier": "^3.6.2"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<p>/</p>
|
||||||
<h1>/</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
3
pages/warrens/[...path].vue
Normal file
3
pages/warrens/[...path].vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<DirectoryList />
|
||||||
|
</template>
|
||||||
28
pages/warrens/index.vue
Normal file
28
pages/warrens/index.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
|
||||||
|
const warrens = ['Thyr', 'Serc'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollArea class="w-full h-full">
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<Button
|
||||||
|
v-for="(warren, i) in warrens"
|
||||||
|
:key="i"
|
||||||
|
class="w-36 h-12"
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
as-child
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
class="flex flex-row items-center gap-1.5"
|
||||||
|
:to="`/warrens/${warren}`"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:folder-root" />
|
||||||
|
{{ warren }}
|
||||||
|
</NuxtLink>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</template>
|
||||||
6
types/index.ts
Normal file
6
types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type DirectoryEntryType = 'file' | 'directory';
|
||||||
|
|
||||||
|
export type BreadcrumbData = {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
39
utils/index.ts
Normal file
39
utils/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { BreadcrumbData } from '~/types';
|
||||||
|
|
||||||
|
export function getBreadcrumbs(path: string): BreadcrumbData[] {
|
||||||
|
const crumbs = path
|
||||||
|
.split('/')
|
||||||
|
.filter((v) => v.length > 0)
|
||||||
|
.map((v) => ({
|
||||||
|
name: v,
|
||||||
|
href: '#',
|
||||||
|
}));
|
||||||
|
|
||||||
|
crumbs.unshift({ name: '/', href: '/' });
|
||||||
|
|
||||||
|
for (let i = 1; i < crumbs.length; i++) {
|
||||||
|
crumbs[i].name = decodeURI(crumbs[i].name);
|
||||||
|
crumbs[i].href =
|
||||||
|
'/' +
|
||||||
|
path
|
||||||
|
.split('/')
|
||||||
|
.slice(1, i + 1)
|
||||||
|
.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return crumbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preventDefault(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinPaths(base: string, other: string): string {
|
||||||
|
if (!base.endsWith('/')) {
|
||||||
|
base += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return base + other;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user