Replace PrimeVue with shadcn #5
38 changed files with 1117 additions and 19 deletions
15
app/components/ui/avatar/Avatar.vue
Normal file
15
app/components/ui/avatar/Avatar.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<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>
|
||||
21
app/components/ui/avatar/AvatarFallback.vue
Normal file
21
app/components/ui/avatar/AvatarFallback.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import type { AvatarFallbackProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { AvatarFallback } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<AvatarFallbackProps & { class?: HTMLAttributes["class"] }>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarFallback
|
||||
data-slot="avatar-fallback"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-muted flex size-full items-center justify-center rounded-full', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AvatarFallback>
|
||||
</template>
|
||||
12
app/components/ui/avatar/AvatarImage.vue
Normal file
12
app/components/ui/avatar/AvatarImage.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<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
app/components/ui/avatar/index.ts
Normal file
3
app/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";
|
||||
13
app/components/ui/breadcrumb/Breadcrumb.vue
Normal file
13
app/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" data-slot="breadcrumb" :class="props.class">
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
23
app/components/ui/breadcrumb/BreadcrumbEllipsis.vue
Normal file
23
app/components/ui/breadcrumb/BreadcrumbEllipsis.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<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
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('flex size-9 items-center justify-center', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<MoreHorizontal class="size-4" />
|
||||
</slot>
|
||||
<span class="sr-only">More</span>
|
||||
</span>
|
||||
</template>
|
||||
14
app/components/ui/breadcrumb/BreadcrumbItem.vue
Normal file
14
app/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 data-slot="breadcrumb-item" :class="cn('inline-flex items-center gap-1.5', props.class)">
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
21
app/components/ui/breadcrumb/BreadcrumbLink.vue
Normal file
21
app/components/ui/breadcrumb/BreadcrumbLink.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts" setup>
|
||||
import type { PrimitiveProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { Primitive } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(), {
|
||||
as: "a",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="breadcrumb-link"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn('hover:text-foreground transition-colors', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
17
app/components/ui/breadcrumb/BreadcrumbList.vue
Normal file
17
app/components/ui/breadcrumb/BreadcrumbList.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
:class="cn('text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ol>
|
||||
</template>
|
||||
20
app/components/ui/breadcrumb/BreadcrumbPage.vue
Normal file
20
app/components/ui/breadcrumb/BreadcrumbPage.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
:class="cn('text-foreground font-normal', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
17
app/components/ui/breadcrumb/BreadcrumbSeparator.vue
Normal file
17
app/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 data-slot="breadcrumb-separator" role="presentation" aria-hidden="true" :class="cn('[&>svg]:size-3.5', props.class)">
|
||||
<slot>
|
||||
<ChevronRight />
|
||||
</slot>
|
||||
</li>
|
||||
</template>
|
||||
7
app/components/ui/breadcrumb/index.ts
Normal file
7
app/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";
|
||||
15
app/components/ui/collapsible/Collapsible.vue
Normal file
15
app/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="slotProps" data-slot="collapsible" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</CollapsibleRoot>
|
||||
</template>
|
||||
12
app/components/ui/collapsible/CollapsibleContent.vue
Normal file
12
app/components/ui/collapsible/CollapsibleContent.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import type { CollapsibleContentProps } from "reka-ui";
|
||||
import { CollapsibleContent } from "reka-ui";
|
||||
|
||||
const props = defineProps<CollapsibleContentProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleContent data-slot="collapsible-content" v-bind="props">
|
||||
<slot />
|
||||
</CollapsibleContent>
|
||||
</template>
|
||||
12
app/components/ui/collapsible/CollapsibleTrigger.vue
Normal file
12
app/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 data-slot="collapsible-trigger" v-bind="props">
|
||||
<slot />
|
||||
</CollapsibleTrigger>
|
||||
</template>
|
||||
3
app/components/ui/collapsible/index.ts
Normal file
3
app/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";
|
||||
15
app/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
15
app/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui";
|
||||
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>();
|
||||
const emits = defineEmits<DropdownMenuRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot v-slot="slotProps" data-slot="dropdown-menu" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
37
app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
37
app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { Check } from "lucide-vue-next";
|
||||
import { DropdownMenuCheckboxItem, DropdownMenuItemIndicator, useForwardPropsEmits } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>();
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuCheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<Check class="size-4" />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuCheckboxItem>
|
||||
</template>
|
||||
37
app/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
37
app/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { DropdownMenuContent, DropdownMenuPortal, useForwardPropsEmits } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(), {
|
||||
sideOffset: 4,
|
||||
});
|
||||
const emits = defineEmits<DropdownMenuContentEmits>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
data-slot="dropdown-menu-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</template>
|
||||
12
app/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
12
app/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuGroupProps } from "reka-ui";
|
||||
import { DropdownMenuGroup } from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuGroup data-slot="dropdown-menu-group" v-bind="props">
|
||||
<slot />
|
||||
</DropdownMenuGroup>
|
||||
</template>
|
||||
41
app/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
41
app/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuItemProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { DropdownMenuItem, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
DropdownMenuItemProps & {
|
||||
class?: HTMLAttributes["class"];
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}
|
||||
>(),
|
||||
{
|
||||
variant: "default",
|
||||
}
|
||||
);
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "inset", "variant", "class");
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuItem
|
||||
data-slot="dropdown-menu-item"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
:data-variant="variant"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
23
app/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
23
app/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuLabelProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { DropdownMenuLabel, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"]; inset?: boolean }>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "inset");
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuLabel>
|
||||
</template>
|
||||
15
app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
15
app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui";
|
||||
import { DropdownMenuRadioGroup, useForwardPropsEmits } from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>();
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioGroup data-slot="dropdown-menu-radio-group" v-bind="forwarded">
|
||||
<slot />
|
||||
</DropdownMenuRadioGroup>
|
||||
</template>
|
||||
38
app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
38
app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { Circle } from "lucide-vue-next";
|
||||
import { DropdownMenuItemIndicator, DropdownMenuRadioItem, useForwardPropsEmits } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>();
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<Circle class="size-2 fill-current" />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuRadioItem>
|
||||
</template>
|
||||
23
app/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
23
app/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuSeparatorProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { DropdownMenuSeparator } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes["class"];
|
||||
}
|
||||
>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSeparator
|
||||
data-slot="dropdown-menu-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
|
||||
/>
|
||||
</template>
|
||||
14
app/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
14
app/components/ui/dropdown-menu/DropdownMenuShortcut.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>
|
||||
<span data-slot="dropdown-menu-shortcut" :class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
15
app/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
15
app/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui";
|
||||
import { DropdownMenuSub, useForwardPropsEmits } from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>();
|
||||
const emits = defineEmits<DropdownMenuSubEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</DropdownMenuSub>
|
||||
</template>
|
||||
29
app/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
29
app/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { DropdownMenuSubContent, useForwardPropsEmits } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>();
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuSubContent>
|
||||
</template>
|
||||
29
app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
29
app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubTriggerProps } from "reka-ui";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { ChevronRight } from "lucide-vue-next";
|
||||
import { DropdownMenuSubTrigger, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"]; inset?: boolean }>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "inset");
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
v-bind="forwardedProps"
|
||||
: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>
|
||||
14
app/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
14
app/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { DropdownMenuTriggerProps } from "reka-ui";
|
||||
import { DropdownMenuTrigger, useForwardProps } from "reka-ui";
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>();
|
||||
|
||||
const forwardedProps = useForwardProps(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuTrigger data-slot="dropdown-menu-trigger" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
</template>
|
||||
16
app/components/ui/dropdown-menu/index.ts
Normal file
16
app/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";
|
||||
|
|
@ -1,14 +1,48 @@
|
|||
<script setup lang="ts">
|
||||
import DefaultSidebar from "~/layouts/default/Sidebar.vue";
|
||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar";
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "~/components/ui/breadcrumb";
|
||||
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarProvider>
|
||||
<DefaultSidebar />
|
||||
<main>
|
||||
<SidebarTrigger />
|
||||
<slot />
|
||||
</main>
|
||||
<SidebarInset>
|
||||
<header class="flex h-12 shrink-0 items-center gap-2 border-b px-4">
|
||||
<SidebarTrigger class="-ml-1" />
|
||||
<Separator orientation="vertical" class="mr-2 data-[orientation=vertical]:h-4" />
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem class="hidden md:block">
|
||||
<BreadcrumbLink href="#"> Building Your Application </BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator class="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</header>
|
||||
<main class="flex flex-1 flex-col gap-4 p-4">
|
||||
<slot />
|
||||
</main>
|
||||
<footer class="flex h-12 shrink-0 items-center gap-2 border-b px-4" data-testid="footer">
|
||||
<div v-if="currentYear === 2025" class="bg-muted/50 flex-1 rounded-xl p-2 text-center">Glowing Fiesta 2025</div>
|
||||
<div v-else class="bg-muted/50 flex-1 rounded-xl p-2 text-center">Glowing Fiesta 2025 - {{ currentYear }}</div>
|
||||
</footer>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,113 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronRight,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
BookOpen,
|
||||
HandCoins,
|
||||
LogOut,
|
||||
Settings2,
|
||||
Sparkles,
|
||||
SquareTerminal,
|
||||
} from "lucide-vue-next";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
useSidebar,
|
||||
type SidebarProps,
|
||||
} from "~/components/ui/sidebar";
|
||||
import { HandCoins } from "lucide-vue-next";
|
||||
|
||||
const props = withDefaults(defineProps<SidebarProps>(), {
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/components/ui/collapsible";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "Liviu",
|
||||
email: "x.liviu@gmail.com",
|
||||
avatar: "https://git.burcusel.nl/avatars/bcc51c59d08174c798ba7db59631df6cb820e042679af9098cc03ef68330f486?size=200",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Playground",
|
||||
url: "#",
|
||||
icon: SquareTerminal,
|
||||
isActive: true,
|
||||
items: [
|
||||
{ title: "History", url: "#" },
|
||||
{ title: "Starred", url: "#" },
|
||||
{ title: "Settings", url: "#" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
url: "#",
|
||||
icon: BookOpen,
|
||||
isActive: true,
|
||||
items: [
|
||||
{ title: "Introduction", url: "#" },
|
||||
{ title: "Get Started", url: "#" },
|
||||
{ title: "Tutorials", url: "#" },
|
||||
{ title: "Changelog", url: "#" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: Settings2,
|
||||
isActive: true,
|
||||
items: [
|
||||
{ title: "General", url: "#" },
|
||||
{ title: "Billing", url: "#" },
|
||||
{ title: "Limits", url: "#" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface NavItem {
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: any;
|
||||
isActive?: boolean;
|
||||
items?: { title: string; url: string }[];
|
||||
}
|
||||
|
||||
interface SidebarLayoutProps extends /* @vue-ignore */ SidebarProps {
|
||||
navItems?: NavItem[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SidebarLayoutProps>(), {
|
||||
collapsible: "icon",
|
||||
});
|
||||
|
||||
const navMain = computed(() => props.navItems || data.navMain);
|
||||
|
||||
const { isMobile } = useSidebar();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -30,16 +123,123 @@ const props = withDefaults(defineProps<SidebarProps>(), {
|
|||
<HandCoins class="size-4" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 leading-none">
|
||||
<span class="font-medium">Glowing Fiesta</span>
|
||||
<span class="">v1.0.0</span>
|
||||
<span class="font-bold text-primary">Glowing Fiesta</span>
|
||||
<span>v1.0.0</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent></SidebarContent>
|
||||
<SidebarFooter></SidebarFooter>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<Collapsible
|
||||
v-for="item in navMain"
|
||||
:key="item.title"
|
||||
as-child
|
||||
:default-open="item.isActive"
|
||||
class="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton :tooltip="item.title">
|
||||
<component :is="item.icon" v-if="item.icon" class="text-primary" />
|
||||
<span class="font-bold text-primary">{{ item.title }}</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 text-primary"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
|
||||
<SidebarMenuSubButton as-child>
|
||||
<NuxtLink :to="subItem.url">
|
||||
<span>{{ subItem.title }}</span>
|
||||
</NuxtLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar class="h-8 w-8 rounded-lg">
|
||||
<AvatarImage :src="data.user.avatar" :alt="data.user.name" />
|
||||
<AvatarFallback class="rounded-lg"> LB </AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-medium">{{ data.user.name }}</span>
|
||||
<span class="truncate text-xs">{{ data.user.email }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
class="w-[--reka-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
:side="isMobile ? 'bottom' : 'right'"
|
||||
align="end"
|
||||
: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="data.user.avatar" :alt="data.user.name" />
|
||||
<AvatarFallback class="rounded-lg">LB</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ data.user.name }}</span>
|
||||
<span class="truncate text-xs">{{ data.user.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
|
||||
<SidebarRail></SidebarRail>
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const lastClicked = ref<string>("None");
|
||||
|
||||
const buttonClicked = (variant: string) => {
|
||||
lastClicked.value = variant;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="text-3xl font-bold">Dashboard</h1>
|
||||
<div class="container">
|
||||
<Button size="lg">Testing 1, 2, 3</Button>
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div class="bg-muted/50 aspect-video rounded-xl" />
|
||||
<div class="bg-muted/50 aspect-video rounded-xl flex items-center justify-center gap-2">
|
||||
<span class="text-primary">Last clicked button:</span>
|
||||
<span class="font-bold text-lime-500">{{ lastClicked }}</span>
|
||||
</div>
|
||||
<div class="bg-muted/50 aspect-video rounded-xl" />
|
||||
</div>
|
||||
<div class="bg-muted/50 min-h-screen flex-1 rounded-xl md:min-h-min flex items-center justify-center gap-2">
|
||||
<Button variant="default" @click="buttonClicked('default')">Default</Button>
|
||||
<Button variant="outline" @click="buttonClicked('outline')">Outline</Button>
|
||||
<Button variant="ghost" @click="buttonClicked('ghost')">Ghost</Button>
|
||||
<Button variant="link" @click="buttonClicked('link')">Link</Button>
|
||||
<Button variant="secondary" @click="buttonClicked('secondary')">Secondary</Button>
|
||||
<Button variant="destructive" @click="buttonClicked('destructive')">Destructive</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,38 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { mount, type VueWrapper } from "@vue/test-utils";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import DefaultLayout from "~/layouts/Default.vue";
|
||||
|
||||
describe("Default.vue", () => {
|
||||
const wrapper: VueWrapper = mount(DefaultLayout);
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("loads without crashing", () => {
|
||||
const wrapper = mount(DefaultLayout);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe("Footer", () => {
|
||||
it("footer is displayed", () => {
|
||||
const wrapper = mount(DefaultLayout);
|
||||
const footer = wrapper.find("[data-testid='footer']");
|
||||
expect(footer.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("footer shows only 2025 when current year is 2025", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2025, 0, 1));
|
||||
const wrapper = mount(DefaultLayout);
|
||||
const footer = wrapper.find("[data-testid='footer']");
|
||||
expect(footer.text()).toBe("Glowing Fiesta 2025");
|
||||
});
|
||||
|
||||
it("footer shows range when current year is not 2025", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2069, 0, 1));
|
||||
const wrapper = mount(DefaultLayout);
|
||||
const footer = wrapper.find("[data-testid='footer']");
|
||||
expect(footer.text()).toBe("Glowing Fiesta 2025 - 2069");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
191
tests/layouts/default/Sidebar.test.ts
Normal file
191
tests/layouts/default/Sidebar.test.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import SidebarLayout from "~/layouts/default/Sidebar.vue";
|
||||
import { ref } from "vue";
|
||||
import type * as SidebarUI from "~/components/ui/sidebar";
|
||||
|
||||
const { useSidebarMock } = vi.hoisted(() => ({
|
||||
useSidebarMock: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the UI components and hook
|
||||
vi.mock("~/components/ui/sidebar", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof SidebarUI>();
|
||||
const { ref } = await import("vue");
|
||||
const MockComponent = {
|
||||
template: "<div><slot /></div>",
|
||||
inheritAttrs: false,
|
||||
};
|
||||
|
||||
// Default implementation
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(false),
|
||||
state: ref("expanded"),
|
||||
openMobile: ref(false),
|
||||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
return {
|
||||
...actual,
|
||||
Sidebar: MockComponent,
|
||||
SidebarContent: MockComponent,
|
||||
SidebarFooter: MockComponent,
|
||||
SidebarGroup: MockComponent,
|
||||
SidebarHeader: MockComponent,
|
||||
SidebarMenu: MockComponent,
|
||||
SidebarMenuItem: MockComponent,
|
||||
SidebarMenuButton: MockComponent,
|
||||
SidebarRail: MockComponent,
|
||||
SidebarMenuSub: MockComponent,
|
||||
SidebarMenuSubButton: MockComponent,
|
||||
SidebarMenuSubItem: MockComponent,
|
||||
useSidebar: useSidebarMock,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock other UI components to avoid rendering complexity
|
||||
vi.mock("~/components/ui/dropdown-menu", () => {
|
||||
const MockComponent = { template: "<div><slot /></div>" };
|
||||
return {
|
||||
DropdownMenu: MockComponent,
|
||||
DropdownMenuContent: MockComponent,
|
||||
DropdownMenuGroup: MockComponent,
|
||||
DropdownMenuItem: MockComponent,
|
||||
DropdownMenuLabel: MockComponent,
|
||||
DropdownMenuSeparator: MockComponent,
|
||||
DropdownMenuTrigger: MockComponent,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("~/components/ui/collapsible", () => {
|
||||
const MockComponent = { template: "<div><slot /></div>" };
|
||||
return {
|
||||
Collapsible: MockComponent,
|
||||
CollapsibleContent: MockComponent,
|
||||
CollapsibleTrigger: MockComponent,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("~/components/ui/avatar", () => {
|
||||
const MockComponent = { template: "<div><slot /></div>" };
|
||||
return {
|
||||
Avatar: MockComponent,
|
||||
AvatarFallback: MockComponent,
|
||||
AvatarImage: MockComponent,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/sheet", () => {
|
||||
const MockComponent = { template: "<div><slot /></div>" };
|
||||
return {
|
||||
Sheet: MockComponent,
|
||||
SheetContent: MockComponent,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/sheet/SheetDescription.vue", () => ({
|
||||
default: { template: "<div><slot /></div>" },
|
||||
}));
|
||||
vi.mock("@/components/ui/sheet/SheetHeader.vue", () => ({
|
||||
default: { template: "<div><slot /></div>" },
|
||||
}));
|
||||
vi.mock("lucide-vue-next", () => {
|
||||
const MockIcon = { template: '<svg class="lucide-mock" />' };
|
||||
return {
|
||||
BadgeCheck: MockIcon,
|
||||
Bell: MockIcon,
|
||||
ChevronRight: MockIcon,
|
||||
ChevronsUpDown: MockIcon,
|
||||
CreditCard: MockIcon,
|
||||
BookOpen: MockIcon,
|
||||
HandCoins: MockIcon,
|
||||
LogOut: MockIcon,
|
||||
Settings2: MockIcon,
|
||||
Sparkles: MockIcon,
|
||||
SquareTerminal: MockIcon,
|
||||
};
|
||||
});
|
||||
|
||||
describe("SidebarLayout", () => {
|
||||
it("renders the header correctly", () => {
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(false),
|
||||
state: ref("expanded"),
|
||||
openMobile: ref(false),
|
||||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(SidebarLayout, {
|
||||
props: {
|
||||
collapsible: "icon",
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toContain("Glowing Fiesta");
|
||||
expect(wrapper.text()).toContain("v1.0.0");
|
||||
});
|
||||
|
||||
it("renders navigation groups", () => {
|
||||
const wrapper = mount(SidebarLayout);
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain("Playground");
|
||||
expect(text).toContain("Documentation");
|
||||
expect(text).toContain("Settings");
|
||||
});
|
||||
|
||||
it("renders user information", () => {
|
||||
const wrapper = mount(SidebarLayout);
|
||||
expect(wrapper.text()).toContain("Liviu");
|
||||
expect(wrapper.text()).toContain("x.liviu@gmail.com");
|
||||
});
|
||||
|
||||
it("renders sub-items in navigation", () => {
|
||||
const wrapper = mount(SidebarLayout);
|
||||
const text = wrapper.text();
|
||||
// Checking sub-items of Playground
|
||||
expect(text).toContain("History");
|
||||
expect(text).toContain("Starred");
|
||||
// Checking sub-items of Documentation
|
||||
expect(text).toContain("Introduction");
|
||||
expect(text).toContain("Get Started");
|
||||
});
|
||||
|
||||
it("does not render icon if item.icon is missing", () => {
|
||||
const wrapper = mount(SidebarLayout, {
|
||||
props: {
|
||||
navItems: [
|
||||
{
|
||||
title: "No Icon Item",
|
||||
url: "#",
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("No Icon Item");
|
||||
expect(wrapper.find('[data-testid="sidebar-icon"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("renders correctly in mobile view", () => {
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(true),
|
||||
state: ref("expanded"),
|
||||
openMobile: ref(true),
|
||||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(SidebarLayout);
|
||||
|
||||
// When isMobile is true, it renders a Sheet.
|
||||
// Since we mocked Sheet components as simple divs with slots, the content should still be present.
|
||||
// However, the structure is different.
|
||||
// We can verify that the content is still rendered (passed through the slot).
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain("Glowing Fiesta");
|
||||
expect(text).toContain("Liviu");
|
||||
|
||||
// Check specific specific mock interaction if needed, or just that it doesn't crash
|
||||
// and renders the menu items.
|
||||
expect(text).toContain("Playground");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,52 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, beforeEach } from "vitest";
|
||||
import { mount, type VueWrapper } from "@vue/test-utils";
|
||||
import IndexPage from "~/pages/index.vue";
|
||||
|
||||
describe("pages/index.vue", () => {
|
||||
const wrapper: VueWrapper = mount(IndexPage);
|
||||
let wrapper: VueWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(IndexPage);
|
||||
});
|
||||
|
||||
it("loads without crashing", () => {
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("displays initial state correctly", () => {
|
||||
const displayElement = wrapper.find(".text-lime-500");
|
||||
expect(displayElement.exists()).toBe(true);
|
||||
expect(displayElement.text()).toBe("None");
|
||||
});
|
||||
|
||||
it("updates text when Default button is clicked", async () => {
|
||||
// Find button method 1: by text content inside button elements
|
||||
const buttons = wrapper.findAll("button");
|
||||
const defaultBtn = buttons.find((b) => b.text() === "Default");
|
||||
|
||||
expect(defaultBtn?.exists()).toBe(true);
|
||||
|
||||
await defaultBtn?.trigger("click");
|
||||
expect(wrapper.find(".text-lime-500").text()).toBe("default");
|
||||
});
|
||||
|
||||
it("updates text when other buttons are clicked", async () => {
|
||||
const testCases = [
|
||||
{ label: "Outline", expected: "outline" },
|
||||
{ label: "Ghost", expected: "ghost" },
|
||||
{ label: "Link", expected: "link" },
|
||||
{ label: "Secondary", expected: "secondary" },
|
||||
{ label: "Destructive", expected: "destructive" },
|
||||
];
|
||||
|
||||
for (const { label, expected } of testCases) {
|
||||
const buttons = wrapper.findAll("button");
|
||||
const btn = buttons.find((b) => b.text() === label);
|
||||
|
||||
expect(btn?.exists(), `Button with label ${label} should exist`).toBe(true);
|
||||
await btn?.trigger("click");
|
||||
|
||||
expect(wrapper.find(".text-lime-500").text()).toBe(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ export default defineConfig({
|
|||
// Exclude config files
|
||||
"*.config.*",
|
||||
"assets/icons/**",
|
||||
|
||||
// Exclude UI components
|
||||
"app/components/ui/**",
|
||||
],
|
||||
},
|
||||
name: "GFiesta",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue