Dashboard final version, TODO: update main sidebar menu
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from "@/Components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { computed, HTMLAttributes } from "vue";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
loading?: boolean;
|
||||
padding?: "default" | "none" | "tight";
|
||||
hover?: boolean; // subtle hover style
|
||||
clickable?: boolean; // adds cursor + focus ring
|
||||
disabled?: boolean;
|
||||
class?: HTMLAttributes["class"];
|
||||
headerClass?: HTMLAttributes["class"];
|
||||
bodyClass?: HTMLAttributes["class"];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
// Emit click for consumers if clickable
|
||||
const emit = defineEmits<{ (e: "click", ev: MouseEvent): void }>();
|
||||
|
||||
const wrapperClasses = computed(() => {
|
||||
const base = "relative transition-colors";
|
||||
const hover = props.hover ? "hover:bg-muted/50" : "";
|
||||
const clickable =
|
||||
props.clickable && !props.disabled
|
||||
? "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
: "";
|
||||
const disabled = props.disabled ? "opacity-60 pointer-events-none" : "";
|
||||
return [base, hover, clickable, disabled].filter(Boolean).join(" ");
|
||||
});
|
||||
|
||||
const paddingClasses = computed(() => {
|
||||
switch (props.padding) {
|
||||
case "none":
|
||||
return "p-0";
|
||||
case "tight":
|
||||
return "p-3 sm:p-4";
|
||||
default:
|
||||
return "p-4 sm:p-6";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
:class="cn(wrapperClasses, props.class)"
|
||||
@click="props.clickable && emit('click', $event)"
|
||||
>
|
||||
<!-- Header Slot / Fallback -->
|
||||
<CardHeader
|
||||
v-if="title || description || $slots.header"
|
||||
:class="cn('space-y-1', headerClass)"
|
||||
>
|
||||
<template v-if="$slots.header">
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<CardTitle v-if="title">{{ title }}</CardTitle>
|
||||
<CardDescription v-if="description">{{ description }}</CardDescription>
|
||||
</template>
|
||||
</CardHeader>
|
||||
|
||||
<!-- Loading Skeleton -->
|
||||
<div v-if="loading" class="animate-pulse space-y-3 px-4 py-4">
|
||||
<div class="h-4 w-1/3 rounded bg-muted" />
|
||||
<div class="h-3 w-1/2 rounded bg-muted" />
|
||||
<div class="h-32 rounded bg-muted" />
|
||||
</div>
|
||||
|
||||
<!-- Content Slot -->
|
||||
<CardContent v-else :class="cn(paddingClasses, bodyClass)">
|
||||
<slot />
|
||||
</CardContent>
|
||||
|
||||
<!-- Footer Slot -->
|
||||
<CardFooter v-if="$slots.footer" class="border-t px-4 py-3 sm:px-6">
|
||||
<slot name="footer" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user