Feature Drag Drop Organizer
blockfeatureInteractive dashboard block layout organizer with position reordering, visibility toggling, and JSON schema export.
Installation
$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/feature-drag-drop-organizer.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/feature-drag-drop-organizer.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/feature-drag-drop-organizer.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/feature-drag-drop-organizer.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/feature-drag-drop-organizerInstalls to:app/components/blocks/feature-drag-drop-organizer/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | 'Arrange dashboard layouts intuitively with zero boilerpl… | optional |
description | string | 'Customize widget order | optional |
class | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
LayoutWidgetinterface LayoutWidget {
id: string
name: string
category: string
visible: boolean
colSpan: number
previewSnippet: string
}npm dependencies
Files installed (2)
app/components/blocks/feature-drag-drop-organizer/FeatureDragDropOrganizer.vue9.7 kB
<script setup lang="ts"> import { computed, ref } from 'vue' import { Activity, ArrowDown, ArrowRight, ArrowUp, Check, Copy, Eye, EyeOff, Layers, RotateCcw } from 'lucide-vue-next' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { cn } from '@/lib/utils' export interface LayoutWidget { id: string name: string category: string visible: boolean colSpan: number previewSnippet: string } export interface FeatureDragDropOrganizerProps { title?: string description?: string class?: string } const INITIAL_WIDGETS: LayoutWidget[] = [ { id: 'widget-kpi', name: 'Real-time KPI Metrics', category: 'Analytics', visible: true, colSpan: 12, previewSnippet: '3 Cards: $124.5k MRR • 18ms Latency • 99.99% Uptime', }, { id: 'widget-chart', name: 'Traffic Sparkline Chart', category: 'Telemetry', visible: true, colSpan: 8, previewSnippet: 'SVG Real-time stream (35,000 req/s live)', }, { id: 'widget-nodes', name: 'Edge PoP Status', category: 'Infrastructure', visible: true, colSpan: 4, previewSnippet: '8 global edge regions active (optimal)', }, { id: 'widget-feed', name: 'Deployment Activity Feed', category: 'Workflows', visible: true, colSpan: 12, previewSnippet: 'sha-a81f9c deployed to edge via CLI (2 mins ago)', }, ] const props = withDefaults(defineProps<FeatureDragDropOrganizerProps>(), { title: 'Arrange dashboard layouts intuitively with zero boilerplate.', description: 'Customize widget order, toggle component visibility, and export clean declarative JSON layout schemas for your application.', }) const widgets = ref<LayoutWidget[]>([...INITIAL_WIDGETS]) const copied = ref(false) function moveWidget(index: number, direction: 'up' | 'down') { const targetIndex = direction === 'up' ? index - 1 : index + 1 if (targetIndex < 0 || targetIndex >= widgets.value.length) return const updated = [...widgets.value] const [removed] = updated.splice(index, 1) updated.splice(targetIndex, 0, removed) widgets.value = updated } function toggleVisibility(index: number) { widgets.value[index].visible = !widgets.value[index].visible } function resetLayout() { widgets.value = JSON.parse(JSON.stringify(INITIAL_WIDGETS)) } const exportJson = computed(() => { return JSON.stringify( widgets.value.map((w, idx) => ({ order: idx, id: w.id, name: w.name, visible: w.visible, colSpan: w.colSpan, })), null, 2, ) }) async function copyJson() { try { await navigator.clipboard.writeText(exportJson.value) copied.value = true setTimeout(() => { copied.value = false }, 2000) } catch { // fallback } } </script> <template> <section data-slot="feature-drag-drop-organizer" :class="cn('bg-background relative overflow-hidden py-16 sm:py-24', props.class)" > <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <!-- Section Header --> <div class="mx-auto max-w-3xl space-y-4 text-center"> <a href="#layout-organizer" class="group border-border/80 bg-secondary/60 hover:bg-secondary text-foreground inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs font-medium shadow-2xs transition-colors" > <Layers class="text-primary size-3.5" /> <span>Composable Layout Workbench</span> <ArrowRight class="text-muted-foreground size-3 transition-transform group-hover:translate-x-0.5" /> </a> <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl"> {{ title }} </h2> <p class="text-muted-foreground text-base sm:text-lg"> {{ description }} </p> </div> <!-- Workbench Grid (2 Columns: Controls & Live Canvas) --> <div class="mt-12 grid grid-cols-1 gap-6 lg:grid-cols-12"> <!-- Left: Widget Re-ordering Controls (6 Cols) --> <Card class="border-border bg-card flex flex-col justify-between shadow-xs lg:col-span-6"> <CardContent class="space-y-5 p-6"> <div class="border-border flex items-center justify-between border-b pb-3"> <div class="flex items-center gap-2"> <Layers class="text-primary size-4" /> <span class="text-foreground text-sm font-semibold">Widget Hierarchy</span> </div> <Button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-7 gap-1 px-2 font-mono text-xs active:scale-95" @click="resetLayout" > <RotateCcw class="size-3" /> <span>Reset</span> </Button> </div> <!-- Reorderable List of Widgets --> <div class="space-y-2"> <div v-for="(widget, idx) in widgets" :key="widget.id" :class=" cn( 'flex items-center justify-between rounded-lg border p-3 transition-colors', widget.visible ? 'border-border bg-background' : 'border-border/60 bg-muted/30 opacity-60', ) " > <div class="flex min-w-0 items-center gap-3"> <div class="text-muted-foreground flex items-center gap-1"> <button type="button" :disabled="idx === 0" class="hover:bg-muted rounded p-1 transition-[colors,transform] active:scale-90 disabled:opacity-30" @click="moveWidget(idx, 'up')" > <ArrowUp class="size-3.5" /> </button> <button type="button" :disabled="idx === widgets.length - 1" class="hover:bg-muted rounded p-1 transition-[colors,transform] active:scale-90 disabled:opacity-30" @click="moveWidget(idx, 'down')" > <ArrowDown class="size-3.5" /> </button> </div> <div class="min-w-0 space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground text-xs font-bold">{{ widget.name }}</span> <Badge variant="outline" class="border-border text-muted-foreground text-xs"> {{ widget.category }} </Badge> </div> <div class="text-muted-foreground truncate text-xs">{{ widget.previewSnippet }}</div> </div> </div> <Button variant="ghost" size="icon" class="text-muted-foreground hover:text-foreground size-8 shrink-0 active:scale-95" @click="toggleVisibility(idx)" > <Eye v-if="widget.visible" class="text-primary size-3.5" /> <EyeOff v-else class="size-3.5" /> </Button> </div> </div> <!-- Export JSON Box --> <div class="border-border space-y-2 border-t pt-2"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-medium">Generated Layout JSON</span> <Button variant="ghost" size="sm" class="h-6 gap-1 px-2 font-mono text-xs active:scale-95" @click="copyJson" > <Check v-if="copied" class="text-primary size-3" /> <Copy v-else class="size-3" /> <span>{{ copied ? 'Copied' : 'Copy JSON' }}</span> </Button> </div> <pre class="border-border bg-muted/40 text-muted-foreground max-h-32 overflow-y-auto rounded-lg border p-2.5 font-mono text-xs" ><code>{{ exportJson }}</code></pre> </div> </CardContent> </Card> <!-- Right: Live Canvas Preview (6 Cols) --> <Card class="border-border bg-card flex flex-col justify-between shadow-xs lg:col-span-6"> <CardContent class="space-y-4 p-6"> <div class="border-border flex items-center justify-between border-b pb-3"> <div class="flex items-center gap-2"> <Activity class="text-success size-4" /> <span class="text-foreground text-sm font-semibold">Rendered Canvas Preview</span> </div> <span class="text-success font-mono text-xs">Live Synchronized</span> </div> <!-- Simulated Dashboard Canvas with Reordered Components --> <div class="border-border/80 bg-background/80 space-y-3 rounded-lg border p-4"> <template v-for="widget in widgets" :key="widget.id"> <div v-if="widget.visible" class="border-border bg-muted/30 space-y-1 rounded-lg border p-3 transition-colors" > <div class="text-foreground flex items-center justify-between text-xs font-semibold"> <span>{{ widget.name }}</span> <span class="text-muted-foreground font-mono text-xs">col-span-{{ widget.colSpan }}</span> </div> <div class="text-muted-foreground text-xs">{{ widget.previewSnippet }}</div> </div> </template> </div> </CardContent> </Card> </div> </div> </section> </template>app/components/blocks/feature-drag-drop-organizer/index.ts0.2 kB
export { default as FeatureDragDropOrganizer } from './FeatureDragDropOrganizer.vue' export type { FeatureDragDropOrganizerProps, LayoutWidget } from './FeatureDragDropOrganizer.vue'
Raw manifest:https://uipkge.dev/r/vue/feature-drag-drop-organizer.json