sortable
Sortable list of items with grid layout
Sortable list of items with grid layout
Загрузка превью…
src/sortable/c-sortable-2.vue
<!--
Расхождение с апстримом: `IconPlaceholder` заменена инлайновым `<svg>`
(lucide-react v0.545.0 "grip-vertical") — см. docs/PORTING.md §5.
-->
<script setup lang="ts">
import { ref } from "vue"
import { Badge } from "@/components/reui/badge"
import { Sortable, SortableItem, SortableItemHandle } from "@/components/reui/sortable"
import { cn } from "@/lib/utils"
interface GridItem {
id: string
title: string
description: string
type: "image" | "document" | "audio" | "video" | "featured"
size: string
priority: "high" | "medium" | "low"
}
const items = ref<GridItem[]>([
{ id: "1", title: "Hero Image", description: "Main banner image", type: "image", size: "2.4 MB", priority: "high" },
{ id: "2", title: "Product Specs", description: "Technical documentation", type: "document", size: "1.2 MB", priority: "medium" },
{ id: "3", title: "Demo Video", description: "Product demonstration", type: "video", size: "15.7 MB", priority: "high" },
{ id: "4", title: "Audio Guide", description: "Voice instructions", type: "audio", size: "8.3 MB", priority: "low" },
{ id: "5", title: "Gallery Photo 1", description: "Product view 1", type: "image", size: "3.1 MB", priority: "medium" },
{ id: "6", title: "Gallery Photo 2", description: "Product view 2", type: "image", size: "2.8 MB", priority: "medium" },
{ id: "7", title: "User Manual", description: "Installation guide", type: "document", size: "4.2 MB", priority: "high" },
{ id: "8", title: "Background Music", description: "Ambient soundtrack", type: "audio", size: "12.1 MB", priority: "low" },
{ id: "9", title: "Feature Highlight", description: "Key product features", type: "featured", size: "N/A", priority: "high" },
])
function getTypeColor(type: GridItem["type"]) {
switch (type) {
case "image":
return "primary-light"
case "document":
return "success-light"
case "audio":
return "destructive-light"
case "video":
return "info-light"
case "featured":
return "warning-light"
}
}
function getItemSize(type: GridItem["type"]) {
switch (type) {
case "featured":
return "col-span-2 row-span-2"
default:
return "col-span-1 row-span-1"
}
}
function getItemValue(item: GridItem) {
return item.id
}
</script>
<template>
<div class="mx-auto w-full max-w-2xl space-y-6 p-4">
<Sortable
v-model:value="items"
:get-item-value="getItemValue"
strategy="grid"
class="grid auto-rows-fr grid-cols-3 gap-3"
>
<SortableItem v-for="item in items" :key="item.id" :value="item.id">
<div
:class="
cn(
'group bg-background border-border hover:bg-accent/50 rounded-md relative cursor-pointer border p-3 transition-colors',
getItemSize(item.type),
'flex min-h-[100px] flex-col'
)
"
>
<SortableItemHandle class="text-muted-foreground hover:text-foreground absolute end-1.5 top-2.5 z-10 opacity-0 transition-opacity group-hover:opacity-100">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-grip-vertical h-3.5 w-3.5">
<circle cx="9" cy="12" r="1" />
<circle cx="9" cy="5" r="1" />
<circle cx="9" cy="19" r="1" />
<circle cx="15" cy="12" r="1" />
<circle cx="15" cy="5" r="1" />
<circle cx="15" cy="19" r="1" />
</svg>
</SortableItemHandle>
<div class="min-w-0 flex-1">
<h4 class="truncate text-sm font-medium">{{ item.title }}</h4>
<p class="text-muted-foreground mt-0.5 truncate text-xs">{{ item.description }}</p>
</div>
<div class="mt-2 flex items-center justify-between">
<Badge :variant="getTypeColor(item.type)" size="sm">{{ item.type }}</Badge>
<span v-if="item.type !== 'featured'" class="text-muted-foreground text-xs">{{ item.size }}</span>
</div>
</div>
</SortableItem>
</Sortable>
</div>
</template>