Teren-app/resources/js/Components/app/ui/card/AppCard.vue

93 lines
2.5 KiB
Vue

<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>