GF-4 (#4) Finalized layout
This commit is contained in:
parent
57593b4370
commit
671baaf079
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">
|
<script setup lang="ts">
|
||||||
import DefaultSidebar from "~/layouts/default/Sidebar.vue";
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<DefaultSidebar />
|
<DefaultSidebar />
|
||||||
<main>
|
<SidebarInset>
|
||||||
<SidebarTrigger />
|
<header class="flex h-12 shrink-0 items-center gap-2 border-b px-4">
|
||||||
<slot />
|
<SidebarTrigger class="-ml-1" />
|
||||||
</main>
|
<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>
|
</SidebarProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,113 @@
|
||||||
<script setup lang="ts">
|
<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 {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
useSidebar,
|
||||||
type SidebarProps,
|
type SidebarProps,
|
||||||
} from "~/components/ui/sidebar";
|
} 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",
|
collapsible: "icon",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navMain = computed(() => props.navItems || data.navMain);
|
||||||
|
|
||||||
|
const { isMobile } = useSidebar();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -30,16 +123,123 @@ const props = withDefaults(defineProps<SidebarProps>(), {
|
||||||
<HandCoins class="size-4" />
|
<HandCoins class="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-0.5 leading-none">
|
<div class="flex flex-col gap-0.5 leading-none">
|
||||||
<span class="font-medium">Glowing Fiesta</span>
|
<span class="font-bold text-primary">Glowing Fiesta</span>
|
||||||
<span class="">v1.0.0</span>
|
<span>v1.0.0</span>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarHeader>
|
</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>
|
<SidebarRail></SidebarRail>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,29 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const lastClicked = ref<string>("None");
|
||||||
|
|
||||||
|
const buttonClicked = (variant: string) => {
|
||||||
|
lastClicked.value = variant;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1 class="text-3xl font-bold">Dashboard</h1>
|
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||||
<div class="container">
|
<div class="bg-muted/50 aspect-video rounded-xl" />
|
||||||
<Button size="lg">Testing 1, 2, 3</Button>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,38 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { mount, type VueWrapper } from "@vue/test-utils";
|
import { mount } from "@vue/test-utils";
|
||||||
import DefaultLayout from "~/layouts/Default.vue";
|
import DefaultLayout from "~/layouts/Default.vue";
|
||||||
|
|
||||||
describe("Default.vue", () => {
|
describe("Default.vue", () => {
|
||||||
const wrapper: VueWrapper = mount(DefaultLayout);
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
it("loads without crashing", () => {
|
it("loads without crashing", () => {
|
||||||
|
const wrapper = mount(DefaultLayout);
|
||||||
expect(wrapper.exists()).toBe(true);
|
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 { mount, type VueWrapper } from "@vue/test-utils";
|
||||||
import IndexPage from "~/pages/index.vue";
|
import IndexPage from "~/pages/index.vue";
|
||||||
|
|
||||||
describe("pages/index.vue", () => {
|
describe("pages/index.vue", () => {
|
||||||
const wrapper: VueWrapper = mount(IndexPage);
|
let wrapper: VueWrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = mount(IndexPage);
|
||||||
|
});
|
||||||
|
|
||||||
it("loads without crashing", () => {
|
it("loads without crashing", () => {
|
||||||
expect(wrapper.exists()).toBe(true);
|
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
|
// Exclude config files
|
||||||
"*.config.*",
|
"*.config.*",
|
||||||
"assets/icons/**",
|
"assets/icons/**",
|
||||||
|
|
||||||
|
// Exclude UI components
|
||||||
|
"app/components/ui/**",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
name: "GFiesta",
|
name: "GFiesta",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue