Menu

Change language

GitHub repo

Feature Code Preview Split

blockmarketing

Split-pane developer workbench featuring live code generation across Vue 3.5, React 19, and Tailwind v4 tokens synchronized with an interactive component canvas.

Installation

$npx uipkge-ng@latest add feature-code-preview-split
Manifest:https://uipkge.dev/r/angular/feature-code-preview-split.jsonInstalls to:src/app/components/blocks/feature-code-preview-split/

Variants

Loading interactive previews…

npm dependencies

Files installed (2)

  • src/app/components/blocks/feature-code-preview-split/feature-code-preview-split.component.ts11.7 kB
    // Split-pane developer workbench featuring live code generation across Vue 3.5,
    // React 19, and Tailwind v4 tokens synchronized with an interactive component canvas.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'
    import { Check, Code2, Copy, Eye, LucideAngularModule, Zap } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCardComponent } from '@/ui/card/card.component'
    
    export type FrameworkTab = 'vue' | 'react' | 'tailwind'
    export type PreviewButtonVariant = 'default' | 'secondary' | 'outline' | 'destructive'
    export type PreviewButtonSize = 'sm' | 'default' | 'lg'
    
    const frameworkTabs: { id: FrameworkTab; label: string }[] = [
      { id: 'vue', label: 'Vue 3.5 SFC' },
      { id: 'react', label: 'React 19 JSX' },
      { id: 'tailwind', label: 'Tailwind Tokens' },
    ]
    
    const variantOptions: PreviewButtonVariant[] = ['default', 'secondary', 'outline', 'destructive']
    const sizeOptions: PreviewButtonSize[] = ['sm', 'default', 'lg']
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-feature-code-preview-split, [ui-feature-code-preview-split]',
      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],
      template: `
        <section data-slot="feature-code-preview-split" [class]="rootClass">
          <div class="mx-auto max-w-7xl space-y-12">
            <!-- Section Header -->
            <div class="mx-auto max-w-3xl space-y-4 text-center">
              <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs">
                <lucide-icon [img]="Code2" class="text-primary size-3.5" />
                Interactive DX Playground
              </span>
              <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
                Real-time code generation with instant live preview.
              </h2>
              <p class="text-muted-foreground text-base">
                Modify the interactive controls on the right to see the AST code synchronize live across Vue 3.5, React 19,
                and Tailwind v4.
              </p>
            </div>
    
            <!-- Split-Pane DX Workbench -->
            <div class="grid grid-cols-1 items-stretch gap-6 lg:grid-cols-12">
              <!-- Left Pane: Interactive Code Sandbox Editor (7 Cols) -->
              <div
                ui-card
                class="border-border bg-card/95 flex flex-col justify-between space-y-4 rounded-2xl p-6 text-left shadow-sm lg:col-span-7"
              >
                <!-- Editor Top Bar -->
                <div class="border-border flex items-center justify-between border-b pb-3">
                  <!-- Framework Selector Tabs -->
                  <div class="bg-muted/60 border-border flex items-center gap-1 rounded-lg border p-1">
                    @for (tab of frameworkTabs; track tab.id) {
                      <button type="button" [class]="frameworkTabClass(tab.id)" (click)="activeFramework.set(tab.id)">
                        {{ tab.label }}
                      </button>
                    }
                  </div>
    
                  <!-- Copy Code Button -->
                  <button
                    ui-button
                    size="sm"
                    variant="ghost"
                    class="h-8 gap-1.5 px-2 font-mono text-xs"
                    (click)="copyCode()"
                  >
                    @if (isCopied()) {
                      <lucide-icon [img]="Check" class="text-success size-3.5" />
                    } @else {
                      <lucide-icon [img]="Copy" class="text-muted-foreground size-3.5" />
                    }
                    <span>{{ isCopied() ? 'Copied' : 'Copy' }}</span>
                  </button>
                </div>
    
                <!-- Code Viewport -->
                <pre
                  class="border-border bg-muted/40 text-foreground my-2 flex-1 overflow-x-auto rounded-xl border p-4 font-mono text-xs leading-relaxed"
                ><code>{{ dynamicCode() }}</code></pre>
    
                <!-- Footer Metadata -->
                <div
                  class="text-muted-foreground border-border flex items-center justify-between border-t pt-3 font-mono text-xs"
                >
                  <span>Syntax: {{ syntaxLabel() }}</span>
                  <span class="text-success font-semibold">&#10003; 100% Tree-Shakeable</span>
                </div>
              </div>
    
              <!-- Right Pane: Live Component Render & Controls (5 Cols) -->
              <div
                ui-card
                class="border-border bg-card/95 flex flex-col justify-between space-y-6 rounded-2xl p-6 text-left shadow-sm lg:col-span-5"
              >
                <div class="space-y-4">
                  <div class="border-border flex items-center justify-between border-b pb-3">
                    <div class="flex items-center gap-2">
                      <lucide-icon [img]="Eye" class="text-primary size-4" />
                      <h3 class="text-foreground font-mono text-xs font-bold">Interactive Component Canvas</h3>
                    </div>
                    <span ui-badge variant="outline" class="border-success/20 bg-success/10 text-success font-mono text-xs">
                      Live Render
                    </span>
                  </div>
    
                  <!-- Live Rendered Component Center Stage -->
                  <div
                    class="border-border bg-background/50 flex h-44 items-center justify-center rounded-xl border border-dashed p-6 transition-colors"
                  >
                    <button ui-button [variant]="activeVariant()" [size]="activeSize()" class="gap-2 font-mono shadow-sm">
                      @if (showIcon()) {
                        <lucide-icon [img]="Zap" class="size-4" />
                      }
                      <span>Deploy Application</span>
                    </button>
                  </div>
                </div>
    
                <!-- Component Controls Box -->
                <div class="border-border space-y-4 border-t pt-2">
                  <p class="text-muted-foreground font-mono text-xs tracking-wider uppercase">Interactive Prop Controls</p>
    
                  <!-- Variant Selector -->
                  <div class="space-y-1.5">
                    <span class="text-muted-foreground font-mono text-xs">Variant:</span>
                    <div class="grid grid-cols-2 gap-1.5">
                      @for (v of variantOptions; track v) {
                        <button type="button" [class]="variantButtonClass(v)" (click)="activeVariant.set(v)">
                          {{ v }}
                        </button>
                      }
                    </div>
                  </div>
    
                  <!-- Size Selector & Icon Toggle -->
                  <div class="grid grid-cols-2 gap-4">
                    <div class="space-y-1.5">
                      <span class="text-muted-foreground font-mono text-xs">Size:</span>
                      <div class="flex items-center gap-1">
                        @for (s of sizeOptions; track s) {
                          <button type="button" [class]="sizeButtonClass(s)" (click)="activeSize.set(s)">
                            {{ s === 'default' ? 'md' : s }}
                          </button>
                        }
                      </div>
                    </div>
    
                    <div class="space-y-1.5">
                      <span class="text-muted-foreground font-mono text-xs">Leading Icon:</span>
                      <button
                        ui-button
                        size="sm"
                        variant="outline"
                        [class]="iconToggleClass"
                        (click)="showIcon.set(!showIcon())"
                      >
                        {{ showIcon() ? 'Enabled' : 'Disabled' }}
                      </button>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiFeatureCodePreviewSplitComponent {
      protected readonly Check = Check
      protected readonly Code2 = Code2
      protected readonly Copy = Copy
      protected readonly Eye = Eye
      protected readonly Zap = Zap
    
      protected readonly frameworkTabs = frameworkTabs
      protected readonly variantOptions = variantOptions
      protected readonly sizeOptions = sizeOptions
    
      @Input('class') className?: string
    
      readonly activeFramework = signal<FrameworkTab>('vue')
      readonly activeVariant = signal<PreviewButtonVariant>('default')
      readonly activeSize = signal<PreviewButtonSize>('default')
      readonly showIcon = signal(true)
      readonly isCopied = signal(false)
    
      private copyTimer: ReturnType<typeof setTimeout> | undefined
    
      constructor() {
        inject(DestroyRef).onDestroy(() => clearTimeout(this.copyTimer))
      }
    
      readonly dynamicCode = computed(() => {
        const variant = this.activeVariant()
        const size = this.activeSize()
        const showIcon = this.showIcon()
        if (this.activeFramework() === 'vue') {
          const iconImport = showIcon ? "\nimport { Zap } from 'lucide-vue-next'" : ''
          const iconElement = showIcon ? '\n    <Zap class="size-4" />' : ''
          return `<script setup lang="ts">\nimport { Button } from '@/components/ui/button'${iconImport}\n</scr${'ipt'}>\n\n<template>\n  <Button\n    variant="${variant}"\n    size="${size}"\n    class="font-mono"\n  >${iconElement}\n    <span>Deploy Application</span>\n  </Button>\n</template>`
        }
    
        if (this.activeFramework() === 'react') {
          const iconImport = showIcon ? "\nimport { Zap } from 'lucide-react'" : ''
          const iconElement = showIcon ? '\n      <Zap className="size-4" />' : ''
          return `'use client'\n\nimport { Button } from '@/components/ui/button'${iconImport}\n\nexport function DeployTrigger() {\n  return (\n    <Button\n      variant="${variant}"\n      size="${size}"\n      className="font-mono"\n    >${iconElement}\n      <span>Deploy Application</span>\n    </Button>\n  )\n}`
        }
    
        return `@theme inline {\n  --color-primary: oklch(0.205 0 0);\n  --color-primary-foreground: oklch(0.985 0 0);\n  --color-destructive: oklch(0.577 0.245 27.325);\n  --radius-lg: 0.625rem;\n  --ease-spring: cubic-bezier(0.16, 1, 0.3, 1);\n}`
      })
    
      readonly syntaxLabel = computed(() =>
        this.activeFramework() === 'vue'
          ? 'Vue SFC + TS'
          : this.activeFramework() === 'react'
            ? 'TypeScript TSX'
            : 'CSS @theme',
      )
    
      get rootClass(): string {
        return cn('bg-background relative overflow-hidden px-4 py-16 sm:px-6 sm:py-24 lg:px-8', this.className)
      }
    
      get iconToggleClass(): string {
        return cn('h-7 w-full font-mono text-xs', this.showIcon() ? 'border-primary text-primary' : 'text-muted-foreground')
      }
    
      frameworkTabClass(id: FrameworkTab): string {
        return cn(
          'rounded-md px-2.5 py-1 font-mono text-xs transition-colors',
          this.activeFramework() === id
            ? 'bg-primary text-primary-foreground font-semibold shadow-xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      variantButtonClass(v: PreviewButtonVariant): string {
        return cn(
          'rounded-lg border px-2.5 py-1 text-center font-mono text-xs capitalize transition-colors',
          this.activeVariant() === v
            ? 'border-primary bg-primary text-primary-foreground font-semibold shadow-xs'
            : 'border-border bg-card text-muted-foreground hover:text-foreground',
        )
      }
    
      sizeButtonClass(s: PreviewButtonSize): string {
        return cn(
          'flex-1 rounded-lg border py-1 text-center font-mono text-xs uppercase transition-colors',
          this.activeSize() === s
            ? 'border-primary bg-primary text-primary-foreground font-semibold shadow-xs'
            : 'border-border bg-card text-muted-foreground',
        )
      }
    
      copyCode(): void {
        try {
          void navigator.clipboard?.writeText(this.dynamicCode())
        } catch {
          // fallback
        }
        this.isCopied.set(true)
        clearTimeout(this.copyTimer)
        this.copyTimer = setTimeout(() => this.isCopied.set(false), 2000)
      }
    }
    
  • src/app/components/blocks/feature-code-preview-split/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/feature-code-preview-split.json