refactor: clean up component templates and improve code readability across various Vue files by consolidating attributes and removing unnecessary line breaks

This commit is contained in:
Yoga Pangestu 2026-06-25 20:23:48 +07:00
parent cecbf92310
commit 459d568c1a
32 changed files with 324 additions and 598 deletions

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Component } from 'vue';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { Component } from 'vue';
interface LowStockItem { interface LowStockItem {
name: string; name: string;
@ -26,36 +26,23 @@ withDefaults(defineProps<Props>(), {
<template> <template>
<Card> <Card>
<CardHeader <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<div> <div>
<CardTitle class="text-sm font-medium">{{ title }}</CardTitle> <CardTitle class="text-sm font-medium">{{ title }}</CardTitle>
</div> </div>
<component <component v-if="icon" :is="icon" class="size-4" :class="iconColorClass" />
v-if="icon"
:is="icon"
class="size-4"
:class="iconColorClass"
/>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div v-if="items.length > 0" class="space-y-2"> <div v-if="items.length > 0" class="space-y-2">
<div <div v-for="item in items" :key="item.name"
v-for="item in items" class="flex items-center justify-between rounded-md border px-3 py-2">
:key="item.name"
class="flex items-center justify-between rounded-md border px-3 py-2"
>
<span class="text-sm">{{ item.name }}</span> <span class="text-sm">{{ item.name }}</span>
<Badge variant="outline" :class="badgeColorClass"> <Badge variant="outline" :class="badgeColorClass">
{{ item.stock }} {{ item.unit || 'pcs' }} {{ item.stock }} {{ item.unit || 'pcs' }}
</Badge> </Badge>
</div> </div>
</div> </div>
<div <div v-else class="flex h-[80px] items-center justify-center text-sm text-muted-foreground">
v-else
class="flex h-[80px] items-center justify-center text-sm text-muted-foreground"
>
{{ emptyText }} {{ emptyText }}
</div> </div>
</CardContent> </CardContent>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { Component } from 'vue'; import type { Component } from 'vue';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
interface StatItem { interface StatItem {
label: string; label: string;
@ -40,16 +40,11 @@ withDefaults(defineProps<Props>(), {
{{ subLabel }} {{ subLabel }}
</p> </p>
</div> </div>
<div <div class="border-t pt-2 text-center"
class="border-t pt-2 text-center" :class="cols === 2 ? 'grid grid-cols-2 gap-2' : 'grid grid-cols-3 gap-2'">
:class="cols === 2 ? 'grid grid-cols-2 gap-2' : 'grid grid-cols-3 gap-2'"
>
<div v-for="item in items" :key="item.label"> <div v-for="item in items" :key="item.label">
<p class="text-xs text-muted-foreground">{{ item.label }}</p> <p class="text-xs text-muted-foreground">{{ item.label }}</p>
<p <p class="text-sm font-semibold" :class="item.color ?? ''">
class="text-sm font-semibold"
:class="item.color ?? ''"
>
{{ item.value }} {{ item.value }}
</p> </p>
</div> </div>

View File

@ -5,7 +5,6 @@ import { FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table';
import { computed, provide } from 'vue'; import { computed, provide } from 'vue';
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue'; import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import DataTableEmpty from './DataTableEmpty.vue';
import { import {
Table, Table,
TableBody, TableBody,
@ -21,6 +20,7 @@ import type {
DataTablePaginationLink, DataTablePaginationLink,
DataTableSort, DataTableSort,
} from '@/types/data-table'; } from '@/types/data-table';
import DataTableEmpty from './DataTableEmpty.vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -157,11 +157,8 @@ function resolveRowSpan(row: TData, columnId: string): number | undefined {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id"> <TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead <TableHead v-for="header in headerGroup.headers" :key="header.id"
v-for="header in headerGroup.headers" :class="header.column.id === '_row_number' ? ROW_NUMBER_COLUMN_CLASS : undefined">
:key="header.id"
:class="header.column.id === '_row_number' ? ROW_NUMBER_COLUMN_CLASS : undefined"
>
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" <FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
:props="header.getContext()" /> :props="header.getContext()" />
</TableHead> </TableHead>
@ -169,27 +166,17 @@ function resolveRowSpan(row: TData, columnId: string): number | undefined {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<template v-if="table.getRowModel().rows.length"> <template v-if="table.getRowModel().rows.length">
<TableRow <TableRow v-for="row in table.getRowModel().rows" :key="row.id"
v-for="row in table.getRowModel().rows"
:key="row.id"
:class="getRowClassName?.(row.original, row.index)" :class="getRowClassName?.(row.original, row.index)"
:data-state="row.getIsSelected() ? 'selected' : undefined" :data-state="row.getIsSelected() ? 'selected' : undefined">
> <template v-for="cell in row.getVisibleCells()" :key="cell.id">
<template <TableCell v-if="(resolveRowSpan(row.original, cell.column.id) ?? 1) !== 0" :rowspan="(resolveRowSpan(row.original, cell.column.id) ?? 1) > 1
v-for="cell in row.getVisibleCells()"
:key="cell.id"
>
<TableCell
v-if="(resolveRowSpan(row.original, cell.column.id) ?? 1) !== 0"
:rowspan="(resolveRowSpan(row.original, cell.column.id) ?? 1) > 1
? resolveRowSpan(row.original, cell.column.id) ? resolveRowSpan(row.original, cell.column.id)
: undefined" : undefined" :class="[
:class="[
cell.column.id === '_row_number' ? ROW_NUMBER_COLUMN_CLASS : undefined, cell.column.id === '_row_number' ? ROW_NUMBER_COLUMN_CLASS : undefined,
(resolveRowSpan(row.original, cell.column.id) ?? 1) > 1 ? 'align-middle' : undefined, (resolveRowSpan(row.original, cell.column.id) ?? 1) > 1 ? 'align-middle' : undefined,
cell.column.columnDef.meta?.cellClassName, cell.column.columnDef.meta?.cellClassName,
]" ]">
>
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" /> <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell> </TableCell>
</template> </template>
@ -202,26 +189,15 @@ function resolveRowSpan(row: TData, columnId: string): number | undefined {
</Table> </Table>
</div> </div>
<div <div v-if="pagination" class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
v-if="pagination"
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<p class="text-muted-foreground text-sm"> <p class="text-muted-foreground text-sm">
{{ paginationSummary }} {{ paginationSummary }}
</p> </p>
<div <div v-if="paginationLinks?.length && pagination.lastPage > 1"
v-if="paginationLinks?.length && pagination.lastPage > 1" class="flex flex-wrap items-center justify-center gap-1 sm:justify-end">
class="flex flex-wrap items-center justify-center gap-1 sm:justify-end" <Button v-for="link in paginationLinks" :key="`${link.label}-${link.url}`" variant="outline" size="sm"
> :disabled="!link.url || link.active" as-child>
<Button
v-for="link in paginationLinks"
:key="`${link.label}-${link.url}`"
variant="outline"
size="sm"
:disabled="!link.url || link.active"
as-child
>
<Link v-if="link.url" :href="link.url" preserve-scroll> <Link v-if="link.url" :href="link.url" preserve-scroll>
<span v-html="link.label" /> <span v-html="link.label" />
</Link> </Link>

View File

@ -36,12 +36,9 @@ function sortDirection(): 'asc' | 'desc' | null {
<template> <template>
<div :class="cn('flex items-center gap-2', props.class)"> <div :class="cn('flex items-center gap-2', props.class)">
<Button <Button variant="ghost" size="sm"
variant="ghost"
size="sm"
:class="cn('-ml-3 h-8 data-[state=open]:bg-accent', props.class?.includes('justify-end') && 'ml-auto')" :class="cn('-ml-3 h-8 data-[state=open]:bg-accent', props.class?.includes('justify-end') && 'ml-auto')"
@click="sortContext?.onSort(column)" @click="sortContext?.onSort(column)">
>
<span>{{ title }}</span> <span>{{ title }}</span>
<ArrowDown v-if="sortDirection() === 'desc'" class="size-4" /> <ArrowDown v-if="sortDirection() === 'desc'" class="size-4" />
<ArrowUp v-else-if="sortDirection() === 'asc'" class="size-4" /> <ArrowUp v-else-if="sortDirection() === 'asc'" class="size-4" />

View File

@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from 'vue'; import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
import { import {
Empty, Empty,
EmptyDescription, EmptyDescription,
EmptyHeader, EmptyHeader,
EmptyTitle, EmptyTitle,
} from '@/components/ui/empty'; } from '@/components/ui/empty';
import { cn } from '@/lib/utils';
withDefaults( withDefaults(
defineProps<{ defineProps<{

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useMediaQuery } from '@vueuse/core';
import { ListFilter, Search } from '@lucide/vue'; import { ListFilter, Search } from '@lucide/vue';
import { useMediaQuery } from '@vueuse/core';
import { computed } from 'vue'; import { computed } from 'vue';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -66,12 +66,7 @@ const selectSideOffset = computed(() => (isMobile.value ? 4 : 12));
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div class="relative w-full max-w-sm"> <div class="relative w-full max-w-sm">
<Search class="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <Search class="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
<Input <Input v-model="search" type="search" :placeholder="searchPlaceholder ?? 'Cari...'" class="pl-9" />
v-model="search"
type="search"
:placeholder="searchPlaceholder ?? 'Cari...'"
class="pl-9"
/>
</div> </div>
<Popover v-if="filterDefs?.length"> <Popover v-if="filterDefs?.length">
@ -79,25 +74,18 @@ const selectSideOffset = computed(() => (isMobile.value ? 4 : 12));
<Button variant="outline" class="shrink-0"> <Button variant="outline" class="shrink-0">
<ListFilter class="size-4" /> <ListFilter class="size-4" />
Filter Filter
<Badge <Badge v-if="activeFilterCount > 0" variant="secondary"
v-if="activeFilterCount > 0" class="ml-1 size-5 rounded-full px-1 text-xs">
variant="secondary"
class="ml-1 size-5 rounded-full px-1 text-xs"
>
{{ activeFilterCount }} {{ activeFilterCount }}
</Badge> </Badge>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent align="end" class="w-80 space-y-4 p-4" @pointer-down-outside="(event) => {
align="end"
class="w-80 space-y-4 p-4"
@pointer-down-outside="(event) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (target.closest('[data-slot=select-content]') || target.closest('[data-slot=select-trigger]')) { if (target.closest('[data-slot=select-content]') || target.closest('[data-slot=select-trigger]')) {
event.preventDefault(); event.preventDefault();
} }
}" }">
>
<div class="space-y-1"> <div class="space-y-1">
<h4 class="text-sm font-medium"> <h4 class="text-sm font-medium">
Filter Filter
@ -107,26 +95,16 @@ const selectSideOffset = computed(() => (isMobile.value ? 4 : 12));
</p> </p>
</div> </div>
<div <div v-for="filter in filterDefs" :key="filter.key" class="space-y-2">
v-for="filter in filterDefs"
:key="filter.key"
class="space-y-2"
>
<Label :for="`filter-${filter.key}`">{{ filter.label }}</Label> <Label :for="`filter-${filter.key}`">{{ filter.label }}</Label>
<Input <Input v-if="filter.type === 'text'" :id="`filter-${filter.key}`"
v-if="filter.type === 'text'"
:id="`filter-${filter.key}`"
:model-value="filterValue(filter.key)" :model-value="filterValue(filter.key)"
:placeholder="filter.placeholder ?? `Filter ${filter.label.toLowerCase()}`" :placeholder="filter.placeholder ?? `Filter ${filter.label.toLowerCase()}`"
@update:model-value="onFilterChange(filter.key, String($event ?? ''))" @update:model-value="onFilterChange(filter.key, String($event ?? ''))" />
/>
<Select <Select v-else :model-value="filterValue(filter.key) || 'all'"
v-else @update:model-value="onFilterChange(filter.key, String($event ?? ''))">
:model-value="filterValue(filter.key) || 'all'"
@update:model-value="onFilterChange(filter.key, String($event ?? ''))"
>
<SelectTrigger :id="`filter-${filter.key}`" class="w-full"> <SelectTrigger :id="`filter-${filter.key}`" class="w-full">
<SelectValue :placeholder="filter.placeholder ?? 'Semua'" /> <SelectValue :placeholder="filter.placeholder ?? 'Semua'" />
</SelectTrigger> </SelectTrigger>
@ -134,24 +112,14 @@ const selectSideOffset = computed(() => (isMobile.value ? 4 : 12));
<SelectItem value="all"> <SelectItem value="all">
Semua Semua
</SelectItem> </SelectItem>
<SelectItem <SelectItem v-for="option in filter.options" :key="option.value" :value="option.value">
v-for="option in filter.options"
:key="option.value"
:value="option.value"
>
{{ option.label }} {{ option.label }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<Button <Button v-if="activeFilterCount > 0" variant="ghost" size="sm" class="w-full" @click="resetFilters">
v-if="activeFilterCount > 0"
variant="ghost"
size="sm"
class="w-full"
@click="resetFilters"
>
Reset Filter Reset Filter
</Button> </Button>
</PopoverContent> </PopoverContent>

View File

@ -83,13 +83,6 @@ const displayValue = computed({
</script> </script>
<template> <template>
<Input <Input :id="id" v-model="displayValue" inputmode="decimal" autocomplete="off" :placeholder="placeholder ?? '0'"
:id="id" :disabled="disabled" :class="props.class" />
v-model="displayValue"
inputmode="decimal"
autocomplete="off"
:placeholder="placeholder ?? '0'"
:disabled="disabled"
:class="props.class"
/>
</template> </template>

View File

@ -56,34 +56,20 @@ function clearFile() {
</FieldDescription> </FieldDescription>
<div class="space-y-3"> <div class="space-y-3">
<div <div v-if="displayUrl" :class="cn(
v-if="displayUrl"
:class="cn(
'relative overflow-hidden rounded-lg border bg-muted/30', 'relative overflow-hidden rounded-lg border bg-muted/30',
previewClass ?? 'aspect-video max-w-md', previewClass ?? 'aspect-video max-w-md',
)" )">
>
<img :src="displayUrl" :alt="label" class="size-full object-cover"> <img :src="displayUrl" :alt="label" class="size-full object-cover">
<Button <Button v-if="model" type="button" variant="secondary" size="icon" class="absolute top-2 right-2 size-7"
v-if="model" @click="clearFile">
type="button"
variant="secondary"
size="icon"
class="absolute top-2 right-2 size-7"
@click="clearFile"
>
<X class="size-4" /> <X class="size-4" />
</Button> </Button>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Input <Input :id="id" type="file" :accept="accept ?? 'image/*'" class="max-w-md cursor-pointer"
:id="id" @change="onFileChange" />
type="file"
:accept="accept ?? 'image/*'"
class="max-w-md cursor-pointer"
@change="onFileChange"
/>
<ImagePlus v-if="!displayUrl" class="text-muted-foreground size-5 shrink-0" /> <ImagePlus v-if="!displayUrl" class="text-muted-foreground size-5 shrink-0" />
</div> </div>
</div> </div>

View File

@ -101,54 +101,32 @@ onBeforeUnmount(() => {
<div class="space-y-3"> <div class="space-y-3">
<div v-if="visibleExisting.length > 0 || previewUrls.length > 0" class="flex flex-wrap items-center gap-2"> <div v-if="visibleExisting.length > 0 || previewUrls.length > 0" class="flex flex-wrap items-center gap-2">
<button <button v-for="item in visibleExisting" :key="`existing-${item.id}`" type="button"
v-for="item in visibleExisting"
:key="`existing-${item.id}`"
type="button"
class="group relative size-14 overflow-hidden rounded-md border bg-muted/30" class="group relative size-14 overflow-hidden rounded-md border bg-muted/30"
@click="openPreview(item.url)" @click="openPreview(item.url)">
>
<img :src="item.thumb_url" :alt="label" class="size-full object-cover"> <img :src="item.thumb_url" :alt="label" class="size-full object-cover">
<Button <Button type="button" variant="secondary" size="icon"
type="button"
variant="secondary"
size="icon"
class="absolute top-0.5 right-0.5 size-5 opacity-0 transition-opacity group-hover:opacity-100" class="absolute top-0.5 right-0.5 size-5 opacity-0 transition-opacity group-hover:opacity-100"
@click.stop="removeExisting(item.id)" @click.stop="removeExisting(item.id)">
>
<X class="size-3" /> <X class="size-3" />
</Button> </Button>
</button> </button>
<button <button v-for="(url, index) in previewUrls" :key="`new-${index}`" type="button"
v-for="(url, index) in previewUrls"
:key="`new-${index}`"
type="button"
class="group relative size-14 overflow-hidden rounded-md border bg-muted/30" class="group relative size-14 overflow-hidden rounded-md border bg-muted/30"
@click="openPreview(url)" @click="openPreview(url)">
>
<img :src="url" :alt="label" class="size-full object-cover"> <img :src="url" :alt="label" class="size-full object-cover">
<Button <Button type="button" variant="secondary" size="icon"
type="button"
variant="secondary"
size="icon"
class="absolute top-0.5 right-0.5 size-5 opacity-0 transition-opacity group-hover:opacity-100" class="absolute top-0.5 right-0.5 size-5 opacity-0 transition-opacity group-hover:opacity-100"
@click.stop="removeNew(index)" @click.stop="removeNew(index)">
>
<X class="size-3" /> <X class="size-3" />
</Button> </Button>
</button> </button>
</div> </div>
<div v-if="canAddMore" class="flex items-center gap-2"> <div v-if="canAddMore" class="flex items-center gap-2">
<Input <Input :id="id" type="file" accept="image/*" :multiple="maxFiles > 1" class="max-w-md cursor-pointer"
:id="id" @change="onFileChange" />
type="file"
accept="image/*"
:multiple="maxFiles > 1"
class="max-w-md cursor-pointer"
@change="onFileChange"
/>
<ImagePlus class="text-muted-foreground size-5 shrink-0" /> <ImagePlus class="text-muted-foreground size-5 shrink-0" />
</div> </div>
</div> </div>

View File

@ -39,13 +39,6 @@ const displayValue = computed({
</script> </script>
<template> <template>
<Input <Input :id="id" v-model="displayValue" inputmode="numeric" autocomplete="off" :placeholder="placeholder ?? '0'"
:id="id" :disabled="disabled" :class="props.class" />
v-model="displayValue"
inputmode="numeric"
autocomplete="off"
:placeholder="placeholder ?? '0'"
:disabled="disabled"
:class="props.class"
/>
</template> </template>

View File

@ -27,19 +27,10 @@ const displayValue = computed({
<template> <template>
<div class="relative"> <div class="relative">
<span <span class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-sm">
class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-sm"
>
Rp Rp
</span> </span>
<Input <Input :id="id" v-model="displayValue" inputmode="numeric" autocomplete="off" :placeholder="placeholder ?? '0'"
:id="id" :disabled="disabled" :class="cn('pl-9', props.class)" />
v-model="displayValue"
inputmode="numeric"
autocomplete="off"
:placeholder="placeholder ?? '0'"
:disabled="disabled"
:class="cn('pl-9', props.class)"
/>
</div> </div>
</template> </template>

View File

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import DropZone from 'dropzone-vue';
import { computed, ref } from 'vue';
import MediaPreviewDialog from '@/components/media/MediaPreviewDialog.vue'; import MediaPreviewDialog from '@/components/media/MediaPreviewDialog.vue';
import { import {
Field, Field,
@ -8,11 +10,9 @@ import {
} from '@/components/ui/field'; } from '@/components/ui/field';
import type { MediaUploadState } from '@/types/media'; import type { MediaUploadState } from '@/types/media';
import { createMediaUploadState } from '@/types/media'; import { createMediaUploadState } from '@/types/media';
import DropZone from 'dropzone-vue';
import 'dropzone-vue/dist/dropzone-vue.common.css'; import 'dropzone-vue/dist/dropzone-vue.common.css';
import { computed, ref } from 'vue';
const props = withDefaults( withDefaults(
defineProps<{ defineProps<{
id: string; id: string;
label: string; label: string;
@ -51,8 +51,14 @@ type FilePreview = { id: string; file: File; objectUrl: string };
const filePreviews = ref<FilePreview[]>([]); const filePreviews = ref<FilePreview[]>([]);
function formatBytes(bytes: number): string { function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`; if (bytes < 1024) {
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
} }
@ -67,9 +73,13 @@ function onAddedFile(item: { id: string; file: File }) {
function onRemovedFile(item: { id: string; file: File }) { function onRemovedFile(item: { id: string; file: File }) {
const idx = state.value.newFiles.indexOf(item.file); const idx = state.value.newFiles.indexOf(item.file);
if (idx !== -1) state.value.newFiles.splice(idx, 1);
if (idx !== -1) {
state.value.newFiles.splice(idx, 1);
}
const pi = filePreviews.value.findIndex((p) => p.id === item.id); const pi = filePreviews.value.findIndex((p) => p.id === item.id);
if (pi !== -1) { if (pi !== -1) {
URL.revokeObjectURL(filePreviews.value[pi].objectUrl); URL.revokeObjectURL(filePreviews.value[pi].objectUrl);
filePreviews.value.splice(pi, 1); filePreviews.value.splice(pi, 1);
@ -99,8 +109,10 @@ const allPreviews = computed<NormalizedEntry[]>(() => {
sizeLabel: 'Tersimpan', sizeLabel: 'Tersimpan',
onRemove: () => { onRemove: () => {
const idx = state.value.existing.findIndex((e) => e.id === item.id); const idx = state.value.existing.findIndex((e) => e.id === item.id);
if (idx !== -1) { if (idx !== -1) {
state.value.existing.splice(idx, 1); state.value.existing.splice(idx, 1);
if (!state.value.removeIds.includes(item.id)) { if (!state.value.removeIds.includes(item.id)) {
state.value.removeIds.push(item.id); state.value.removeIds.push(item.id);
} }
@ -169,46 +181,24 @@ function triggerDropzonePicker() {
<div class="dropzone-wrapper"> <div class="dropzone-wrapper">
<!-- Dropzone form: hidden when any preview exists to prevent layout corruption --> <!-- Dropzone form: hidden when any preview exists to prevent layout corruption -->
<div <div :class="[
:class="[
'dz-form-host', 'dz-form-host',
{ 'dz-form-hidden': allPreviews.length > 0 }, { 'dz-form-hidden': allPreviews.length > 0 },
]" ]">
> <DropZone ref="dropzoneRef" :max-files="maxFiles" :max-file-size="maxFileSize"
<DropZone :accepted-files="acceptedFiles" :upload-on-drop="false" :clickable="true"
ref="dropzoneRef" dropzone-class-name="dz-box" dropzone-message-class-name="dz-message" @added-file="onAddedFile"
:max-files="maxFiles" @removed-file="onRemovedFile">
:max-file-size="maxFileSize"
:accepted-files="acceptedFiles"
:upload-on-drop="false"
:clickable="true"
dropzone-class-name="dz-box"
dropzone-message-class-name="dz-message"
@added-file="onAddedFile"
@removed-file="onRemovedFile"
>
<template #message> <template #message>
<div class="dz-placeholder"> <div class="dz-placeholder">
<svg <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"
width="22" stroke-linejoin="round">
height="22" <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
/>
<polyline points="17 8 12 3 7 8" /> <polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" /> <line x1="12" y1="3" x2="12" y2="15" />
</svg> </svg>
<span class="dz-placeholder-primary" <span class="dz-placeholder-primary">Klik atau seret file ke sini</span>
>Klik atau seret file ke sini</span
>
<span class="dz-placeholder-secondary"> <span class="dz-placeholder-secondary">
{{ acceptedFiles.join(', ') }} &mdash; Maks. {{ acceptedFiles.join(', ') }} &mdash; Maks.
{{ maxFiles }} file, {{ maxFiles }} file,
@ -220,29 +210,14 @@ function triggerDropzonePicker() {
</div> </div>
<!-- Overlay: shown when any preview exists, triggers file picker on click --> <!-- Overlay: shown when any preview exists, triggers file picker on click -->
<div <div v-if="allPreviews.length > 0" class="dz-overlay" @click="triggerDropzonePicker">
v-if="allPreviews.length > 0" <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none"
class="dz-overlay" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
@click="triggerDropzonePicker"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" /> <polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" /> <line x1="12" y1="3" x2="12" y2="15" />
</svg> </svg>
<span class="dz-placeholder-primary" <span class="dz-placeholder-primary">Klik atau seret file ke sini</span>
>Klik atau seret file ke sini</span
>
<span class="dz-placeholder-secondary"> <span class="dz-placeholder-secondary">
{{ acceptedFiles.join(', ') }} &mdash; Maks. {{ acceptedFiles.join(', ') }} &mdash; Maks.
{{ maxFiles }} file, {{ formatBytes(maxFileSize) }}/file {{ maxFiles }} file, {{ formatBytes(maxFileSize) }}/file
@ -252,36 +227,15 @@ function triggerDropzonePicker() {
<!-- Preview grid (existing + new) --> <!-- Preview grid (existing + new) -->
<div v-if="allPreviews.length > 0" :class="['preview-grid', { 'preview-grid--single': maxFiles === 1 }]"> <div v-if="allPreviews.length > 0" :class="['preview-grid', { 'preview-grid--single': maxFiles === 1 }]">
<div <div v-for="entry in allPreviews" :key="entry.key" class="preview-card">
v-for="entry in allPreviews"
:key="entry.key"
class="preview-card"
>
<!-- Thumbnail --> <!-- Thumbnail -->
<div <div class="preview-thumb" @click="openPreview(entry.previewSrc)">
class="preview-thumb" <img v-if="entry.isImage" :src="entry.thumbSrc" :alt="entry.name" />
@click="openPreview(entry.previewSrc)"
>
<img
v-if="entry.isImage"
:src="entry.thumbSrc"
:alt="entry.name"
/>
<div v-else class="preview-thumb-icon"> <div v-else class="preview-thumb-icon">
<svg <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
width="32" stroke-linejoin="round">
height="32" <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
/>
<polyline points="14 2 14 8 20 8" /> <polyline points="14 2 14 8 20 8" />
</svg> </svg>
</div> </div>
@ -295,23 +249,10 @@ function triggerDropzonePicker() {
</p> </p>
<p class="preview-size">{{ entry.sizeLabel }}</p> <p class="preview-size">{{ entry.sizeLabel }}</p>
</div> </div>
<button <button type="button" class="preview-remove" title="Hapus" @click="entry.onRemove()">
type="button" <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
class="preview-remove" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
title="Hapus" stroke-linejoin="round">
@click="entry.onRemove()"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" /> <line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
@ -322,11 +263,7 @@ function triggerDropzonePicker() {
<FieldError :errors="errors" /> <FieldError :errors="errors" />
<MediaPreviewDialog <MediaPreviewDialog v-model:open="previewOpen" :url="previewUrl" :title="label" />
v-model:open="previewOpen"
:url="previewUrl"
:title="label"
/>
</Field> </Field>
</div> </div>
</template> </template>

View File

@ -20,12 +20,8 @@ defineProps<{
<DialogHeader class="sr-only"> <DialogHeader class="sr-only">
<DialogTitle>{{ title ?? 'Pratinjau Gambar' }}</DialogTitle> <DialogTitle>{{ title ?? 'Pratinjau Gambar' }}</DialogTitle>
</DialogHeader> </DialogHeader>
<img <img v-if="url" :src="url" :alt="title ?? 'Pratinjau gambar'"
v-if="url" class="max-h-[80vh] w-full rounded-md object-contain">
:src="url"
:alt="title ?? 'Pratinjau gambar'"
class="max-h-[80vh] w-full rounded-md object-contain"
>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</template> </template>

View File

@ -22,13 +22,8 @@ function openPreview(url: string) {
<template> <template>
<div v-if="items.length" class="flex items-center gap-1"> <div v-if="items.length" class="flex items-center gap-1">
<button <button v-for="item in visibleItems" :key="item.id" type="button"
v-for="item in visibleItems" class="size-8 overflow-hidden rounded border bg-muted/30" @click="openPreview(item.url)">
:key="item.id"
type="button"
class="size-8 overflow-hidden rounded border bg-muted/30"
@click="openPreview(item.url)"
>
<img :src="item.thumb_url" alt="Foto" class="size-full object-cover"> <img :src="item.thumb_url" alt="Foto" class="size-full object-cover">
</button> </button>
<span v-if="hiddenCount > 0" class="text-muted-foreground text-xs"> <span v-if="hiddenCount > 0" class="text-muted-foreground text-xs">

View File

@ -108,8 +108,7 @@ const handleAddToCart = () => {
<!-- Image Thumbnails --> <!-- Image Thumbnails -->
<div v-if="allImages.length > 1" class="flex gap-2 overflow-x-auto scrollbar-hide pb-1"> <div v-if="allImages.length > 1" class="flex gap-2 overflow-x-auto scrollbar-hide pb-1">
<button v-for="(img, idx) in allImages" :key="img.id" <button v-for="(img, idx) in allImages" :key="img.id" @click="activeImageIndex = idx" :class="[
@click="activeImageIndex = idx" :class="[
'shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 transition-all cursor-pointer', 'shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 transition-all cursor-pointer',
activeImageIndex === idx activeImageIndex === idx
? 'border-amber-500 shadow-md shadow-amber-200/40' ? 'border-amber-500 shadow-md shadow-amber-200/40'
@ -226,6 +225,7 @@ const handleAddToCart = () => {
.scrollbar-hide::-webkit-scrollbar { .scrollbar-hide::-webkit-scrollbar {
display: none; display: none;
} }
.scrollbar-hide { .scrollbar-hide {
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AlertDialogEmits, AlertDialogProps } from "reka-ui" import type { AlertDialogEmits, AlertDialogProps } from "reka-ui";
import { AlertDialogRoot, useForwardPropsEmits } from "reka-ui" import { AlertDialogRoot, useForwardPropsEmits } from "reka-ui";
const props = defineProps<AlertDialogProps>() const props = defineProps<AlertDialogProps>()
const emits = defineEmits<AlertDialogEmits>() const emits = defineEmits<AlertDialogEmits>()

View File

@ -12,14 +12,11 @@ const delegatedProps = reactiveOmit(props, "class")
</script> </script>
<template> <template>
<AlertDialogCancel <AlertDialogCancel v-bind="delegatedProps" :class="cn(
v-bind="delegatedProps"
:class="cn(
buttonVariants({ variant: 'outline' }), buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0', 'mt-2 sm:mt-0',
props.class, props.class,
)" )">
>
<slot /> <slot />
</AlertDialogCancel> </AlertDialogCancel>
</template> </template>

View File

@ -24,20 +24,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template> <template>
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay <AlertDialogOverlay data-slot="alert-dialog-overlay"
data-slot="alert-dialog-overlay" class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 pointer-events-none fixed inset-0 z-[200] bg-black/80" />
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 pointer-events-none fixed inset-0 z-[200] bg-black/80" <AlertDialogContent data-slot="alert-dialog-content" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
/>
<AlertDialogContent
data-slot="alert-dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background 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 pointer-events-auto fixed top-[50%] left-[50%] z-[201] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg', 'bg-background 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 pointer-events-auto fixed top-[50%] left-[50%] z-[201] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class, props.class,
) )
" ">
>
<slot /> <slot />
</AlertDialogContent> </AlertDialogContent>
</AlertDialogPortal> </AlertDialogPortal>

View File

@ -13,11 +13,8 @@ const delegatedProps = reactiveOmit(props, "class")
</script> </script>
<template> <template>
<AlertDialogDescription <AlertDialogDescription data-slot="alert-dialog-description" v-bind="delegatedProps"
data-slot="alert-dialog-description" :class="cn('text-muted-foreground text-sm', props.class)">
v-bind="delegatedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot /> <slot />
</AlertDialogDescription> </AlertDialogDescription>
</template> </template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from "vue" import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes["class"] class?: HTMLAttributes["class"]
@ -8,15 +8,11 @@ const props = defineProps<{
</script> </script>
<template> <template>
<div <div data-slot="alert-dialog-footer" :class="cn(
data-slot="alert-dialog-footer"
:class="
cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
props.class, props.class,
) )
" ">
>
<slot /> <slot />
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from "vue" import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes["class"] class?: HTMLAttributes["class"]
@ -8,10 +8,7 @@ const props = defineProps<{
</script> </script>
<template> <template>
<div <div data-slot="alert-dialog-header" :class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)">
data-slot="alert-dialog-header"
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
>
<slot /> <slot />
</div> </div>
</template> </template>

View File

@ -11,11 +11,8 @@ const delegatedProps = reactiveOmit(props, "class")
</script> </script>
<template> <template>
<AlertDialogTitle <AlertDialogTitle data-slot="alert-dialog-title" v-bind="delegatedProps"
data-slot="alert-dialog-title" :class="cn('text-lg font-semibold', props.class)">
v-bind="delegatedProps"
:class="cn('text-lg font-semibold', props.class)"
>
<slot /> <slot />
</AlertDialogTitle> </AlertDialogTitle>
</template> </template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AlertDialogTriggerProps } from "reka-ui" import type { AlertDialogTriggerProps } from "reka-ui";
import { AlertDialogTrigger } from "reka-ui" import { AlertDialogTrigger } from "reka-ui";
const props = defineProps<AlertDialogTriggerProps>() const props = defineProps<AlertDialogTriggerProps>()
</script> </script>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from "vue" import type { HTMLAttributes } from "vue";
import { AvatarRoot } from "reka-ui" import { AvatarRoot } from "reka-ui";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes["class"] class?: HTMLAttributes["class"]
@ -9,10 +9,8 @@ const props = defineProps<{
</script> </script>
<template> <template>
<AvatarRoot <AvatarRoot data-slot="avatar"
data-slot="avatar" :class="cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', props.class)">
:class="cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', props.class)"
>
<slot /> <slot />
</AvatarRoot> </AvatarRoot>
</template> </template>

View File

@ -11,11 +11,8 @@ const delegatedProps = reactiveOmit(props, "class")
</script> </script>
<template> <template>
<AvatarFallback <AvatarFallback data-slot="avatar-fallback" v-bind="delegatedProps"
data-slot="avatar-fallback" :class="cn('bg-muted flex size-full items-center justify-center rounded-full', props.class)">
v-bind="delegatedProps"
:class="cn('bg-muted flex size-full items-center justify-center rounded-full', props.class)"
>
<slot /> <slot />
</AvatarFallback> </AvatarFallback>
</template> </template>

View File

@ -1,16 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AvatarImageProps } from "reka-ui" import type { AvatarImageProps } from "reka-ui";
import { AvatarImage } from "reka-ui" import { AvatarImage } from "reka-ui";
const props = defineProps<AvatarImageProps>() const props = defineProps<AvatarImageProps>()
</script> </script>
<template> <template>
<AvatarImage <AvatarImage data-slot="avatar-image" v-bind="props" class="aspect-square size-full">
data-slot="avatar-image"
v-bind="props"
class="aspect-square size-full"
>
<slot /> <slot />
</AvatarImage> </AvatarImage>
</template> </template>

View File

@ -16,11 +16,7 @@ const delegatedProps = reactiveOmit(props, "class")
</script> </script>
<template> <template>
<Primitive <Primitive data-slot="badge" :class="cn(badgeVariants({ variant }), props.class)" v-bind="delegatedProps">
data-slot="badge"
:class="cn(badgeVariants({ variant }), props.class)"
v-bind="delegatedProps"
>
<slot /> <slot />
</Primitive> </Primitive>
</template> </template>

View File

@ -18,14 +18,8 @@ const props = withDefaults(defineProps<Props>(), {
</script> </script>
<template> <template>
<Primitive <Primitive data-slot="button" :data-variant="variant" :data-size="size" :as="as" :as-child="asChild"
data-slot="button" :class="cn(buttonVariants({ variant, size }), props.class)">
:data-variant="variant"
:data-size="size"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot /> <slot />
</Primitive> </Primitive>
</template> </template>

View File

@ -49,16 +49,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<div class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none"> <div class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none">
{{ formatter.custom(toDate(date), { month: 'short' }) }} {{ formatter.custom(toDate(date), { month: 'short' }) }}
</div> </div>
<NativeSelect <NativeSelect class="text-xs h-8 pr-6 pl-2 text-transparent relative" :model-value="date.month" @change="(e: Event) => {
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
:model-value="date.month"
@change="(e: Event) => {
placeholder = placeholder.set({ placeholder = placeholder.set({
month: Number((e?.target as any)?.value), month: Number((e?.target as any)?.value),
}) })
}" }">
> <NativeSelectOption v-for="(month) in createYear({ dateObj: date })" :key="month.toString()"
<NativeSelectOption v-for="(month) in createYear({ dateObj: date })" :key="month.toString()" :value="month.month" :selected="date.month === month.month"> :value="month.month" :selected="date.month === month.month">
{{ formatter.custom(toDate(month), { month: 'short' }) }} {{ formatter.custom(toDate(month), { month: 'short' }) }}
</NativeSelectOption> </NativeSelectOption>
</NativeSelect> </NativeSelect>
@ -72,16 +69,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<div class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none"> <div class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none">
{{ formatter.custom(toDate(date), { year: 'numeric' }) }} {{ formatter.custom(toDate(date), { year: 'numeric' }) }}
</div> </div>
<NativeSelect <NativeSelect class="text-xs h-8 pr-6 pl-2 text-transparent relative" :model-value="date.year" @change="(e: Event) => {
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
:model-value="date.year"
@change="(e: Event) => {
placeholder = placeholder.set({ placeholder = placeholder.set({
year: Number((e?.target as any)?.value), year: Number((e?.target as any)?.value),
}) })
}" }">
> <NativeSelectOption v-for="(year) in yearRange" :key="year.toString()" :value="year.year"
<NativeSelectOption v-for="(year) in yearRange" :key="year.toString()" :value="year.year" :selected="date.year === year.year"> :selected="date.year === year.year">
{{ formatter.custom(toDate(year), { year: 'numeric' }) }} {{ formatter.custom(toDate(year), { year: 'numeric' }) }}
</NativeSelectOption> </NativeSelectOption>
</NativeSelect> </NativeSelect>
@ -89,13 +83,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
</div> </div>
</DefineYearTemplate> </DefineYearTemplate>
<CalendarRoot <CalendarRoot v-slot="{ grid, weekDays, date }" v-bind="forwarded" v-model:placeholder="placeholder"
v-slot="{ grid, weekDays, date }" data-slot="calendar" :class="cn('p-3', props.class)">
v-bind="forwarded"
v-model:placeholder="placeholder"
data-slot="calendar"
:class="cn('p-3', props.class)"
>
<CalendarHeader class="pt-0"> <CalendarHeader class="pt-0">
<nav class="flex items-center gap-1 absolute top-0 inset-x-0 justify-between"> <nav class="flex items-center gap-1 absolute top-0 inset-x-0 justify-between">
<CalendarPrevButton> <CalendarPrevButton>
@ -135,24 +124,15 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<CalendarGrid v-for="month in grid" :key="month.value.toString()"> <CalendarGrid v-for="month in grid" :key="month.value.toString()">
<CalendarGridHead> <CalendarGridHead>
<CalendarGridRow> <CalendarGridRow>
<CalendarHeadCell <CalendarHeadCell v-for="day in weekDays" :key="day">
v-for="day in weekDays" :key="day"
>
{{ day }} {{ day }}
</CalendarHeadCell> </CalendarHeadCell>
</CalendarGridRow> </CalendarGridRow>
</CalendarGridHead> </CalendarGridHead>
<CalendarGridBody> <CalendarGridBody>
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full"> <CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
<CalendarCell <CalendarCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
v-for="weekDate in weekDates" <CalendarCellTrigger :day="weekDate" :month="month.value" />
:key="weekDate.toString()"
:date="weekDate"
>
<CalendarCellTrigger
:day="weekDate"
:month="month.value"
/>
</CalendarCell> </CalendarCell>
</CalendarGridRow> </CalendarGridRow>
</CalendarGridBody> </CalendarGridBody>

View File

@ -16,9 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps)
</script> </script>
<template> <template>
<CalendarCellTrigger <CalendarCellTrigger data-slot="calendar-cell-trigger" :class="cn(
data-slot="calendar-cell-trigger"
:class="cn(
buttonVariants({ variant: 'ghost' }), buttonVariants({ variant: 'ghost' }),
'size-8 p-0 font-normal aria-selected:opacity-100 cursor-default', 'size-8 p-0 font-normal aria-selected:opacity-100 cursor-default',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground', '[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
@ -31,9 +29,7 @@ const forwardedProps = useForwardProps(delegatedProps)
// Outside months // Outside months
'data-[outside-view]:text-muted-foreground', 'data-[outside-view]:text-muted-foreground',
props.class, props.class,
)" )" v-bind="forwardedProps">
v-bind="forwardedProps"
>
<slot /> <slot />
</CalendarCellTrigger> </CalendarCellTrigger>
</template> </template>