Feature Drag Drop Organizer
blockfeatureInteractive dashboard block layout organizer with position reordering, visibility toggling, and JSON schema export.
Installation
$pnpm dlx uipkge-ng@latest add feature-drag-drop-organizer$npx uipkge-ng@latest add feature-drag-drop-organizer$yarn dlx uipkge-ng@latest add feature-drag-drop-organizer$bunx uipkge-ng@latest add feature-drag-drop-organizerManifest:
https://uipkge.dev/r/angular/feature-drag-drop-organizer.jsonInstalls to:src/app/components/blocks/feature-drag-drop-organizer/Variants
Loading interactive previews…
Loading interactive previews…
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)
src/app/components/blocks/feature-drag-drop-organizer/feature-drag-drop-organizer.component.ts12.4 kB
// Composable dashboard layout workbench: widget hierarchy with up/down reordering, visibility // toggles, a live canvas preview that follows the order, and a generated JSON schema with copy. // Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Activity, ArrowDown, ArrowRight, ArrowUp, Check, Copy, Eye, EyeOff, Layers, LucideAngularModule, RotateCcw, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' export interface LayoutWidget { id: string name: string category: string visible: boolean colSpan: number previewSnippet: string } export 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)', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-feature-drag-drop-organizer, [ui-feature-drag-drop-organizer]', standalone: true, // React renders the root <section> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent], template: ` <section data-slot="feature-drag-drop-organizer" [class]="rootClass"> <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" > <lucide-icon [img]="Layers" class="text-primary size-3.5" /> <span>Composable Layout Workbench</span> <lucide-icon [img]="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) --> <div ui-card class="border-border bg-card flex flex-col justify-between shadow-xs lg:col-span-6"> <div ui-card-content 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"> <lucide-icon [img]="Layers" class="text-primary size-4" /> <span class="text-foreground text-sm font-semibold">Widget Hierarchy</span> </div> <button ui-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()" > <lucide-icon [img]="RotateCcw" class="size-3" /> <span>Reset</span> </button> </div> <!-- Reorderable List of Widgets --> <div class="space-y-2"> @for (widget of widgets(); track widget.id; let idx = $index) { <div [class]="widgetRowClass(widget)"> <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" [attr.aria-label]="'Move ' + widget.name + ' up'" class="hover:bg-muted rounded p-1 transition-[colors,transform] active:scale-90 disabled:opacity-30" (click)="moveWidget(idx, 'up')" > <lucide-icon [img]="ArrowUp" class="size-3.5" /> </button> <button type="button" [disabled]="idx === widgets().length - 1" [attr.aria-label]="'Move ' + widget.name + ' down'" class="hover:bg-muted rounded p-1 transition-[colors,transform] active:scale-90 disabled:opacity-30" (click)="moveWidget(idx, 'down')" > <lucide-icon [img]="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> <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs"> {{ widget.category }} </span> </div> <div class="text-muted-foreground truncate text-xs">{{ widget.previewSnippet }}</div> </div> </div> <button ui-button variant="ghost" size="icon" class="text-muted-foreground hover:text-foreground size-8 shrink-0 active:scale-95" [attr.aria-label]="(widget.visible ? 'Hide ' : 'Show ') + widget.name" (click)="toggleVisibility(idx)" > @if (widget.visible) { <lucide-icon [img]="Eye" class="text-primary size-3.5" /> } @else { <lucide-icon [img]="EyeOff" 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 ui-button variant="ghost" size="sm" class="h-6 gap-1 px-2 font-mono text-xs active:scale-95" (click)="copyJson()" > @if (copied()) { <lucide-icon [img]="Check" class="text-primary size-3" /> } @else { <lucide-icon [img]="Copy" 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> </div> </div> <!-- Right: Live Canvas Preview (6 Cols) --> <div ui-card class="border-border bg-card flex flex-col justify-between shadow-xs lg:col-span-6"> <div ui-card-content 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"> <lucide-icon [img]="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 data-testid="canvas-preview" class="border-border/80 bg-background/80 space-y-3 rounded-lg border p-4" > @for (widget of widgets(); track widget.id) { @if (widget.visible) { <div 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> } } </div> </div> </div> </div> </div> </section> `, }) export class UiFeatureDragDropOrganizerComponent { protected readonly Activity = Activity protected readonly ArrowDown = ArrowDown protected readonly ArrowRight = ArrowRight protected readonly ArrowUp = ArrowUp protected readonly Check = Check protected readonly Copy = Copy protected readonly Eye = Eye protected readonly EyeOff = EyeOff protected readonly Layers = Layers protected readonly RotateCcw = RotateCcw @Input() title = 'Arrange dashboard layouts intuitively with zero boilerplate.' @Input() description = 'Customize widget order, toggle component visibility, and export clean declarative JSON layout schemas for your application.' @Input() set initialWidgets(v: LayoutWidget[] | undefined) { this.widgets.set((v ?? INITIAL_WIDGETS).map((w) => ({ ...w }))) } @Input('class') className?: string readonly widgets = signal<LayoutWidget[]>(INITIAL_WIDGETS.map((w) => ({ ...w }))) readonly copied = signal(false) private copyTimer: ReturnType<typeof setTimeout> | undefined readonly exportJson = computed(() => JSON.stringify( this.widgets().map((w, idx) => ({ order: idx, id: w.id, name: w.name, visible: w.visible, colSpan: w.colSpan })), null, 2, ), ) get rootClass(): string { return cn('bg-background relative overflow-hidden py-16 sm:py-24', this.className) } widgetRowClass(widget: LayoutWidget): string { return 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', ) } moveWidget(index: number, direction: 'up' | 'down'): void { const targetIndex = direction === 'up' ? index - 1 : index + 1 if (targetIndex < 0 || targetIndex >= this.widgets().length) return const updated = [...this.widgets()] const [removed] = updated.splice(index, 1) updated.splice(targetIndex, 0, removed!) this.widgets.set(updated) } toggleVisibility(index: number): void { this.widgets.update((prev) => { const next = [...prev] next[index] = { ...next[index]!, visible: !next[index]!.visible } return next }) } resetLayout(): void { this.widgets.set(INITIAL_WIDGETS.map((w) => ({ ...w }))) } async copyJson(): Promise<void> { try { await navigator.clipboard.writeText(this.exportJson()) this.copied.set(true) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => this.copied.set(false), 2000) } catch { // Clipboard unavailable (insecure context / jsdom): leave the label unchanged. } } }src/app/components/blocks/feature-drag-drop-organizer/index.ts0.1 kB
export { UiFeatureDragDropOrganizerComponent, INITIAL_WIDGETS, type LayoutWidget, } from './feature-drag-drop-organizer.component'
Raw manifest:https://uipkge.dev/r/angular/feature-drag-drop-organizer.json