Code Snippet Playground
blockdevopsMulti-file interactive code runner, editor tabs, and terminal console output for components with live render sandbox, syntax highlighting, and test execution.
Installation
$pnpm dlx uipkge-ng@latest add code-snippet-playground$npx uipkge-ng@latest add code-snippet-playground$yarn dlx uipkge-ng@latest add code-snippet-playground$bunx uipkge-ng@latest add code-snippet-playgroundManifest:
https://uipkge.dev/r/angular/code-snippet-playground.jsonInstalls to:src/app/components/blocks/code-snippet-playground/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.
CodeSnippetPlaygroundFileinterface CodeSnippetPlaygroundFile {
id: string
name: string
path: string
language: string
size: string
content: string
}CodeSnippetPlaygroundTestLoginterface CodeSnippetPlaygroundTestLog {
id: string
name: string
duration: string
status: 'pass' | 'fail'
}Files installed (2)
src/app/components/blocks/code-snippet-playground/code-snippet-playground.component.ts34.9 kB
// Edit this file to change the files shown in the editor, the test log or the copy. // Everything is mocked: "Run Code" (or Cmd/Ctrl+Enter) replays the Vitest run after 480ms. import { Component, DestroyRef, Input, computed, inject, signal, ChangeDetectionStrategy } from '@angular/core' import { Braces, Check, CheckCircle2, Code2, Copy, Eye, FileCode, Layers, Loader2, LucideAngularModule, Play, RotateCcw, Terminal, Zap, type LucideIconData, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, } from '@/ui/tabs/tabs.component' export interface CodeSnippetPlaygroundFile { id: string name: string path: string language: string size: string content: string } export interface CodeSnippetPlaygroundTestLog { id: string name: string duration: string status: 'pass' | 'fail' } const files: CodeSnippetPlaygroundFile[] = [ { id: 'button-tsx', name: 'Button.tsx', path: 'packages/registry-react/components/button/Button.tsx', language: 'typescript', size: '1.1 KB', content: `import * as React from 'react' import { Slot } from '@radix-ui/react-slot' import { cn } from '@/lib/utils' import { buttonVariants, type ButtonVariants } from './button.variants' export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, ButtonVariants { asChild?: boolean } const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, asChild = false, type = 'button', ...props }, ref) => { const Comp = asChild ? Slot : 'button' return ( <Comp data-slot="button" data-variant={variant ?? undefined} data-size={size ?? undefined} className={cn(buttonVariants({ variant, size }), className)} {...(asChild ? {} : { type })} ref={ref} {...props} /> ) }, ) Button.displayName = 'Button' export { Button }`, }, { id: 'button-variants', name: 'button.variants.ts', path: 'packages/registry-react/components/button/button.variants.ts', language: 'typescript', size: '1.4 KB', content: `import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' export const buttonVariants = cva( 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0', { variants: { variant: { default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', destructive: 'bg-destructive text-white shadow-xs hover:bg-destructive/90', outline: 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-9 px-4 py-2', sm: 'h-8 rounded-md px-3 text-xs', lg: 'h-10 rounded-md px-6', icon: 'size-9', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ) export type ButtonVariants = VariantProps<typeof buttonVariants>`, }, { id: 'index-ts', name: 'index.ts', path: 'packages/registry-react/components/button/index.ts', language: 'typescript', size: '220 B', content: `export { Button, type ButtonProps } from './Button' export { buttonVariants, type ButtonVariants } from './button.variants'`, }, { id: 'button-test', name: 'button.test.tsx', path: 'packages/registry-react/components/button/__tests__/button.spec.tsx', language: 'typescript', size: '1.7 KB', content: `import { describe, it, expect, vi } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import { Button } from '../Button' describe('Button Primitive', () => { it('renders default button with children text', () => { render(<Button>Submit</Button>) const btn = screen.getByRole('button', { name: /submit/i }) expect(btn).toBeInTheDocument() expect(btn).toHaveAttribute('data-slot', 'button') }) it('applies variant classes correctly', () => { render(<Button variant="destructive">Delete</Button>) const btn = screen.getByRole('button', { name: /delete/i }) expect(btn).toHaveAttribute('data-variant', 'destructive') expect(btn.className).toContain('bg-destructive') }) it('applies size styles correctly', () => { render(<Button size="sm">Small</Button>) const btn = screen.getByRole('button', { name: /small/i }) expect(btn).toHaveAttribute('data-size', 'sm') expect(btn.className).toContain('h-8') }) it('handles click events when active', () => { const handleClick = vi.fn() render(<Button onClick={handleClick}>Trigger</Button>) fireEvent.click(screen.getByRole('button', { name: /trigger/i })) expect(handleClick).toHaveBeenCalledTimes(1) }) it('respects disabled state attribute', () => { render(<Button disabled>Disabled</Button>) expect(screen.getByRole('button', { name: /disabled/i })).toBeDisabled() }) it('forwards custom class names with cn()', () => { render(<Button className="custom-btn">Custom</Button>) const btn = screen.getByRole('button', { name: /custom/i }) expect(btn.className).toContain('custom-btn') }) })`, }, ] const testLogs: CodeSnippetPlaygroundTestLog[] = [ { id: '1', name: 'renders default button with children text', duration: '4ms', status: 'pass' }, { id: '2', name: 'applies variant classes correctly', duration: '7ms', status: 'pass' }, { id: '3', name: 'applies size styles correctly', duration: '5ms', status: 'pass' }, { id: '4', name: 'handles click events when active', duration: '8ms', status: 'pass' }, { id: '5', name: 'respects disabled state attribute', duration: '6ms', status: 'pass' }, { id: '6', name: 'forwards custom class names with cn()', duration: '12ms', status: 'pass' }, ] export type CodeSnippetPlaygroundButtonVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' export type CodeSnippetPlaygroundButtonSize = 'default' | 'sm' | 'lg' | 'icon' function highlightSyntaxLine(line: string): string { if (!line.trim()) return ' ' const escaped = line.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') // Single line comments if (/^\s*\/\//.test(escaped) || /^\s*\/\*/.test(escaped) || /^\s*\*/.test(escaped) || /^\s*<!--/.test(escaped)) { return `<span class="text-zinc-500 italic">${escaped}</span>` } // Every emitted <span> is parked behind a letter-only placeholder so later // passes cannot match inside the markup they already produced. const parked: string[] = [] const park = (html: string) => { const key = String(parked.length) .split('') .map((d) => String.fromCharCode(97 + Number(d))) .join('') parked.push(html) return `\u0000${key}\u0000` } let out = escaped // Strings out = out.replace(/(["'`])(?:(?=(\\?))\2.)*?\1/g, (m) => park(`<span class="text-success font-normal">${m}</span>`)) // Trailing comments out = out.replace(/(\/\/.*$)/, (m) => park(`<span class="text-zinc-500 italic">${m}</span>`)) // Keywords out = out.replace( /\b(import|export|from|const|let|var|function|return|interface|type|default|as|typeof|withDefaults|defineProps|defineEmits|describe|it|expect|test|async|await|extends|new|true|false|null|undefined)\b/g, (m) => park(`<span class="text-chart-1 font-semibold">${m}</span>`), ) // Types out = out.replace( /\b(string|boolean|number|void|HTMLAttributes|VariantProps|ButtonVariants|ButtonProps|Props|Variant|Size|HTMLButtonElement|Record)\b/g, (m) => park(`<span class="text-warning font-medium">${m}</span>`), ) // Functions / methods out = out.replace( /\b(cn|buttonVariants|cva|mount|render|screen|getByRole|fireEvent|vi|trigger|classes|attributes|emitted|toBe|toContain|toHaveProperty|toBeDefined|toBeInTheDocument|toHaveAttribute|toHaveBeenCalledTimes|toHaveBeenCalled|forwardRef|displayName|ref|computed|onMounted|onUnmounted|fn)\b/g, (m) => park(`<span class="text-info">${m}</span>`), ) // Vue/TSX tags out = out.replace( /(<\/?(?:template|script|Primitive|Button|Slot|Comp|slot|div|span|button)\b(?:\s|\/|>)?)/g, (m) => park(`<span class="text-info font-medium">${m}</span>`), ) // Attributes / directives out = out.replace( /(\b(?:data-slot|data-variant|data-size|className|variant|size|asChild|as|type|class|lang|setup|ref|onClick|disabled)\b|:[a-zA-Z0-9_-]+)/g, (m) => park(`<span class="text-chart-2">${m}</span>`), ) // Numbers out = out.replace(/\b(\d+)\b/g, (m) => park(`<span class="text-warning">${m}</span>`)) // Restore every parked span return out.replace(/\u0000([a-j]+)\u0000/g, (_, key: string) => { const idx = Number( key .split('') .map((c: string) => String(c.charCodeAt(0) - 97)) .join(''), ) return parked[idx] ?? '' }) } const variants: CodeSnippetPlaygroundButtonVariant[] = [ 'default', 'secondary', 'destructive', 'outline', 'ghost', 'link', ] const sizes: CodeSnippetPlaygroundButtonSize[] = ['sm', 'default', 'lg', 'icon'] /** * Angular port of the React CodeSnippetPlayground block: a two-column studio with file * tabs and a syntax-coloured editor on the left, and a Live Preview / Terminal tab pair on * the right. Signals for state; Cmd/Ctrl+Enter runs the (mock) suite from anywhere. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-code-snippet-playground, [ui-code-snippet-playground]', standalone: true, // React renders the Card itself: keep this wrapper out of the layout. host: { class: 'contents', '(window:keydown)': 'onWindowKeydown($event)' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiSeparatorComponent, UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, ], template: ` <div ui-card data-slot="code-snippet-playground" [class]="rootClass"> <!-- Top Toolbar --> <div ui-card-header class="border-border bg-muted/20 flex flex-col gap-3 border-b p-4 sm:flex-row sm:items-center sm:justify-between" > <div class="flex items-center gap-3"> <div class="bg-primary/10 text-primary flex size-9 items-center justify-center rounded-lg"> <lucide-icon [img]="Code2" class="size-4.5" /> </div> <div> <div class="flex items-center gap-2"> <h3 ui-card-title class="text-base font-semibold tracking-tight">Button.tsx · Radix UI Primitive</h3> <span ui-badge variant="outline" class="text-muted-foreground font-mono text-xs">React 19</span> <span ui-badge variant="secondary" class="font-mono text-xs">TypeScript 5.6</span> </div> <p ui-card-description class="text-muted-foreground text-xs"> Multi-file component studio with live preview, syntax engine, and Vitest suite </p> </div> </div> <!-- Action Buttons --> <div class="flex flex-wrap items-center gap-2"> <div class="border-border/80 bg-background/80 flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs" > <span [class]=" cn('size-2 rounded-full transition-colors', isRunning() ? 'bg-warning animate-pulse' : 'bg-success') " ></span> <span class="text-muted-foreground font-mono">{{ isRunning() ? 'Running suite...' : '6 tests passing' }}</span> </div> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs shadow-none" title="Reset Playground" (click)="handleResetPlayground()" > <lucide-icon [img]="RotateCcw" class="size-3.5" /> <span class="hidden sm:inline">Reset</span> </button> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs shadow-none" title="Copy active file contents" (click)="handleCopyFile()" > @if (isCopied()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } <span>{{ isCopied() ? 'Copied!' : 'Copy File' }}</span> </button> <button ui-button size="sm" class="h-8 gap-1.5 text-xs font-medium" [disabled]="isRunning()" (click)="handleRunCode()" > @if (isRunning()) { <lucide-icon [img]="Loader2" class="size-3.5 animate-spin" /> } @else { <lucide-icon [img]="Play" class="size-3.5 fill-current" /> } <span>Run Code</span> <kbd class="border-primary-foreground/30 bg-primary-foreground/10 hidden rounded border px-1 font-mono text-xs sm:inline" > ⌘↵ </kbd> </button> </div> </div> <!-- Main 2-Column Studio Grid --> <div class="divide-border grid grid-cols-1 divide-y lg:grid-cols-12 lg:divide-x lg:divide-y-0"> <!-- Left Column: File Tabs & Dark Code Editor (7/12 cols) --> <section class="flex min-h-[560px] flex-col bg-zinc-950 text-zinc-100 lg:col-span-7"> <!-- File Navigation Tabs --> <div class="flex items-center justify-between border-b border-zinc-800/80 bg-zinc-900/70 px-2"> <div class="flex scrollbar-none items-center gap-1 overflow-x-auto py-1.5"> @for (file of files; track file.id) { <button type="button" [class]=" cn( 'group flex cursor-pointer items-center gap-2 rounded-md px-3 py-1.5 font-mono text-xs transition-colors', activeFileId() === file.id ? 'border border-zinc-800 bg-zinc-950 text-zinc-100 shadow-xs' : 'text-zinc-400 hover:bg-zinc-800/50 hover:text-zinc-200' ) " (click)="activeFileId.set(file.id)" > <lucide-icon [img]="fileIcon(file)" [class]=" cn('size-3.5 transition-colors', activeFileId() === file.id ? fileTone(file) : 'text-zinc-500') " /> <span>{{ file.name }}</span> </button> } </div> <span class="hidden font-mono text-xs text-zinc-500 xl:inline">{{ activeFile().size }}</span> </div> <!-- Breadcrumb / Path Info --> <div class="flex items-center justify-between border-b border-zinc-800/60 bg-zinc-950/80 px-4 py-1.5 font-mono text-xs text-zinc-400" > <div class="flex items-center gap-1.5 overflow-hidden text-ellipsis whitespace-nowrap"> <span class="text-zinc-600">src /</span> <span>{{ activeFile().path }}</span> </div> <span class="text-zinc-500">{{ activeFileLines().length }} lines</span> </div> <!-- Code Editor Body --> <div class="relative flex flex-1 overflow-x-auto bg-zinc-950 py-3 font-mono text-xs leading-relaxed select-text" > <!-- Gutter Line Numbers --> <div class="flex flex-col border-r border-zinc-800/60 px-3 text-right text-zinc-600 select-none"> @for (line of activeFileLines(); track $index) { <span [class]=" cn( 'h-5 leading-5 transition-colors', hoveredLine() === $index + 1 ? 'font-semibold text-zinc-300' : '' ) " >{{ $index + 1 }}</span > } </div> <!-- Code Lines with Syntax Coloring --> <div class="flex-1 px-4 whitespace-pre"> @for (html of highlightedLines(); track $index) { <div [class]=" cn( 'group flex h-5 items-center rounded-xs px-1 leading-5 transition-colors', hoveredLine() === $index + 1 ? 'bg-zinc-800/40' : '' ) " (mouseenter)="hoveredLine.set($index + 1)" (mouseleave)="hoveredLine.set(null)" > <span [innerHTML]="html"></span> </div> } </div> </div> <!-- Editor Status Bar --> <div class="flex items-center justify-between border-t border-zinc-800/60 bg-zinc-900/90 px-3 py-1 font-mono text-xs text-zinc-400" > <div class="flex items-center gap-3"> <span>UTF-8</span> <span>2 Spaces</span> <span class="text-zinc-500">React TSX</span> </div> <div class="flex items-center gap-3"> <span>Ln {{ hoveredLine() ?? 1 }}, Col 1</span> <span class="text-success">Prettier ✓</span> </div> </div> </section> <!-- Right Column: Live Sandbox & Terminal Console (5/12 cols) --> <section class="bg-background flex min-h-[560px] flex-col lg:col-span-5"> <ui-tabs [value]="activeRightTab()" (valueChange)="activeRightTab.set($event)" class="flex h-full flex-col"> <!-- Right Tab Header --> <div class="border-border bg-muted/30 border-b px-3 py-2"> <ui-tabs-list class="grid w-full grid-cols-2"> <button ui-tabs-trigger value="preview" class="gap-1.5 text-xs"> <lucide-icon [img]="Eye" class="size-3.5" /> <span>Live Preview</span> </button> <button ui-tabs-trigger value="console" class="gap-1.5 text-xs"> <lucide-icon [img]="Terminal" class="size-3.5" /> <span>Terminal</span> <span ui-badge variant="secondary" class="text-success ml-1 px-1 py-0 font-mono text-xs">PASS</span> </button> </ui-tabs-list> </div> <!-- TAB 1: Live Component Sandbox --> <ui-tabs-content value="preview" class="m-0 flex flex-1 flex-col gap-4 p-4"> <!-- Variant & Style Controls --> <div class="border-border bg-card space-y-3 rounded-lg border p-3 shadow-xs"> <div class="flex items-center justify-between"> <span class="text-foreground text-xs font-semibold">Variant</span> <span class="text-muted-foreground font-mono text-xs">{{ selectedVariant() }}</span> </div> <div class="flex flex-wrap gap-1.5"> @for (v of variants; track v) { <button type="button" [class]=" cn( 'min-h-6 cursor-pointer rounded-md px-2.5 py-1 font-mono text-xs capitalize transition-colors', selectedVariant() === v ? 'bg-primary text-primary-foreground font-medium shadow-xs' : 'bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground' ) " (click)="selectedVariant.set(v)" > {{ v }} </button> } </div> <ui-separator class="my-2" /> <div class="flex items-center justify-between"> <span class="text-foreground text-xs font-semibold">Size & Options</span> </div> <div class="flex flex-wrap items-center gap-1.5"> @for (s of sizes; track s) { <button type="button" [class]=" cn( 'min-h-6 cursor-pointer rounded-md px-2 py-0.5 font-mono text-xs uppercase transition-colors', selectedSize() === s ? 'bg-secondary text-secondary-foreground font-semibold' : 'bg-muted/40 text-muted-foreground hover:bg-muted' ) " (click)="selectedSize.set(s)" > {{ s }} </button> } <div class="ml-auto flex items-center gap-2"> <label class="text-muted-foreground flex cursor-pointer items-center gap-1.5 text-xs select-none"> <input type="checkbox" [checked]="isDisabled()" (change)="isDisabled.set(checked($event))" class="accent-primary" /> <span>Disabled</span> </label> <label class="text-muted-foreground flex cursor-pointer items-center gap-1.5 text-xs select-none"> <input type="checkbox" [checked]="isLoading()" (change)="isLoading.set(checked($event))" class="accent-primary" /> <span>Loading</span> </label> <label class="text-muted-foreground flex cursor-pointer items-center gap-1.5 text-xs select-none"> <input type="checkbox" [checked]="withIcon()" (change)="withIcon.set(checked($event))" class="accent-primary" /> <span>Icon</span> </label> </div> </div> </div> <!-- Render Stage Canvas --> <div class="border-border bg-muted/20 relative flex min-h-[160px] flex-1 flex-col items-center justify-center rounded-lg border border-dashed p-6" > <div class="flex flex-col items-center gap-3"> <button ui-button [variant]="selectedVariant()" [size]="selectedSize()" [disabled]="isDisabled() || isLoading()" class="transition-[color,background-color,border-color,box-shadow,opacity,transform,scale,translate,rotate] active:scale-95" (click)="handleButtonClick()" > @if (isLoading()) { <lucide-icon [img]="Loader2" class="size-4 animate-spin" /> } @else if (withIcon() || selectedSize() === 'icon') { <lucide-icon [img]="Zap" class="size-4" /> } @if (selectedSize() !== 'icon') { <span>Interactive Button</span> } </button> <p class="text-muted-foreground font-mono text-xs"> Clicks: <span class="text-foreground font-semibold">{{ clickCount() }}</span> </p> </div> <!-- Last Interaction Status --> <div class="bg-background/80 text-muted-foreground absolute right-2 bottom-2 left-2 flex items-center justify-between rounded-md px-2 py-1 text-xs backdrop-blur-xs" > <span class="truncate font-mono">{{ lastInteraction() }}</span> <span class="text-success shrink-0 font-medium">Rendered OK</span> </div> </div> <!-- Dynamic Snippet Output --> <div class="space-y-1.5"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-medium">Usage Code</span> <button type="button" class="text-muted-foreground hover:text-foreground flex min-h-6 cursor-pointer items-center gap-1" (click)="handleCopySnippet()" > @if (isSnippetCopied()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } <span>{{ isSnippetCopied() ? 'Copied' : 'Copy' }}</span> </button> </div> <pre class="border-border bg-muted/40 text-foreground overflow-x-auto rounded-md border p-2.5 font-mono text-xs leading-relaxed" ><code>{{ generatedSnippet() }}</code></pre> </div> </ui-tabs-content> <!-- TAB 2: Terminal Console Execution Output --> <ui-tabs-content value="console" class="m-0 flex flex-1 flex-col bg-zinc-950 p-4 font-mono text-xs text-zinc-300" > <!-- Console Toolbar --> <div class="mb-3 flex items-center justify-between border-b border-zinc-800 pb-2 text-zinc-400"> <div class="flex items-center gap-2"> <span class="font-semibold text-zinc-200">Vitest Test Runner</span> <span ui-badge variant="outline" class="border-zinc-700 bg-zinc-900 font-mono text-xs text-zinc-300" >v2.1.8</span > </div> <div class="flex items-center gap-3"> <span class="text-zinc-500">Run #{{ executionCount() }} ({{ lastRunTime() }})</span> <button type="button" class="cursor-pointer text-zinc-400 hover:text-zinc-100" title="Re-run tests" (click)="handleRunCode()" > <lucide-icon [img]="RotateCcw" class="size-3.5" /> </button> </div> </div> <!-- Terminal Execution Logs --> <div class="flex-1 space-y-2 overflow-y-auto pr-1"> <div class="text-zinc-500"> $ vitest run packages/registry-react/components/button/__tests__/button.spec.tsx </div> <div class="text-success flex items-center gap-2 font-semibold"> <lucide-icon [img]="CheckCircle2" class="size-3.5 shrink-0" /> <span>PASS packages/registry-react/components/button/__tests__/button.spec.tsx (6 tests)</span> <span class="font-normal text-zinc-500">42ms</span> </div> <div class="ml-4 space-y-1.5 border-l border-zinc-800 pl-3"> @for (log of testLogs; track log.id) { <div class="flex items-center justify-between text-zinc-300"> <div class="flex items-center gap-2"> <lucide-icon [img]="Check" class="text-success size-3" /> <span>{{ log.name }}</span> </div> <span class="text-zinc-500">{{ log.duration }}</span> </div> } </div> <!-- Summary Card --> <div class="mt-4 space-y-1 rounded-md border border-zinc-800/80 bg-zinc-900/60 p-2.5 text-zinc-400"> <div class="flex justify-between"> <span>Test Files</span> <span class="text-success font-medium">1 passed (1)</span> </div> <div class="flex justify-between"> <span>Tests</span> <span class="text-success font-medium">6 passed (6)</span> </div> <div class="flex justify-between"> <span>Start at</span> <span class="text-zinc-300">{{ lastRunTime() }}</span> </div> <div class="flex justify-between"> <span>Duration</span> <span class="text-zinc-300">42ms (transform 12ms, setup 0ms, collect 8ms, tests 42ms)</span> </div> </div> </div> <!-- Bottom Console Status Banner --> <div class="border-success/30 bg-success/10 text-success mt-3 flex items-center justify-between rounded-md border px-3 py-2" > <div class="flex items-center gap-2 font-semibold"> <lucide-icon [img]="CheckCircle2" class="size-4" /> <span>PASS · 6 passed, 0 failed in 42ms</span> </div> <span class="text-success/80 text-xs">Exit code: 0</span> </div> </ui-tabs-content> </ui-tabs> </section> </div> </div> `, }) export class UiCodeSnippetPlaygroundComponent { /** Extra classes for the root Card (React `className`). */ @Input('class') className?: string protected readonly cn = cn protected readonly files = files protected readonly testLogs = testLogs protected readonly variants = variants protected readonly sizes = sizes protected readonly Code2 = Code2 protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Copy = Copy protected readonly Eye = Eye protected readonly Loader2 = Loader2 protected readonly Play = Play protected readonly RotateCcw = RotateCcw protected readonly Terminal = Terminal protected readonly Zap = Zap readonly activeFileId = signal('button-tsx') readonly activeRightTab = signal('preview') readonly hoveredLine = signal<number | null>(null) readonly isCopied = signal(false) readonly isSnippetCopied = signal(false) readonly isRunning = signal(false) readonly lastRunTime = signal('14:32:08') readonly executionCount = signal(1) // Interactive Workbench State readonly selectedVariant = signal<CodeSnippetPlaygroundButtonVariant>('default') readonly selectedSize = signal<CodeSnippetPlaygroundButtonSize>('default') readonly isDisabled = signal(false) readonly isLoading = signal(false) readonly withIcon = signal(true) readonly clickCount = signal(0) readonly lastInteraction = signal('Ready for test interactions') readonly activeFile = computed(() => files.find((f) => f.id === this.activeFileId()) ?? files[0]!) readonly activeFileLines = computed(() => this.activeFile().content.split('\n')) readonly highlightedLines = computed(() => this.activeFileLines().map(highlightSyntaxLine)) readonly generatedSnippet = computed(() => { const variant = this.selectedVariant() const size = this.selectedSize() const v = variant !== 'default' ? ` variant="${variant}"` : '' const s = size !== 'default' ? ` size="${size}"` : '' const d = this.isDisabled() ? ' disabled' : '' if (size === 'icon') { return `<Button${v}${s}${d}>\n <Zap className="size-4" />\n</Button>` } if (this.isLoading()) { return `<Button${v}${s}${d}>\n <Loader2 className="size-4 animate-spin" />\n Please wait\n</Button>` } if (this.withIcon()) { return `<Button${v}${s}${d}>\n <Zap className="size-4" />\n Interactive Button\n</Button>` } return `<Button${v}${s}${d}>Interactive Button</Button>` }) private readonly timers = new Set<ReturnType<typeof setTimeout>>() constructor() { inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t))) } get rootClass(): string { return cn('border-border bg-card w-full overflow-hidden shadow-xs', this.className) } fileIcon(file: CodeSnippetPlaygroundFile): LucideIconData { if (file.name.endsWith('.tsx')) return FileCode if (file.name.endsWith('.variants.ts')) return Braces if (file.name === 'index.ts') return Layers return CheckCircle2 } fileTone(file: CodeSnippetPlaygroundFile): string { if (file.name.endsWith('.tsx')) return 'text-info' if (file.name.endsWith('.variants.ts')) return 'text-chart-1' if (file.name === 'index.ts') return 'text-warning' return 'text-success' } checked(event: Event): boolean { return (event.target as HTMLInputElement).checked } handleRunCode(): void { if (this.isRunning()) return this.isRunning.set(true) this.activeRightTab.set('console') this.later(() => { this.isRunning.set(false) this.executionCount.update((prev) => prev + 1) this.lastRunTime.set(new Date().toTimeString().split(' ')[0]!) }, 480) } handleCopyFile(): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(this.activeFile().content) this.isCopied.set(true) this.later(() => this.isCopied.set(false), 2000) } } handleCopySnippet(): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(this.generatedSnippet()) this.isSnippetCopied.set(true) this.later(() => this.isSnippetCopied.set(false), 2000) } } handleResetPlayground(): void { this.activeFileId.set('button-tsx') this.selectedVariant.set('default') this.selectedSize.set('default') this.isDisabled.set(false) this.isLoading.set(false) this.withIcon.set(true) this.clickCount.set(0) this.lastInteraction.set('Playground reset to initial state') } handleButtonClick(): void { if (this.isDisabled() || this.isLoading()) return const next = this.clickCount() + 1 this.clickCount.set(next) this.lastInteraction.set(`Dispatched onClick event #${next} at ${new Date().toLocaleTimeString()}`) } onWindowKeydown(e: KeyboardEvent): void { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault() this.handleRunCode() } } private later(fn: () => void, ms: number): void { const t = setTimeout(() => { this.timers.delete(t) fn() }, ms) this.timers.add(t) } }src/app/components/blocks/code-snippet-playground/index.ts0.2 kB
export { UiCodeSnippetPlaygroundComponent, type CodeSnippetPlaygroundFile, type CodeSnippetPlaygroundTestLog, type CodeSnippetPlaygroundButtonVariant, type CodeSnippetPlaygroundButtonSize, } from './code-snippet-playground.component'
Raw manifest:https://uipkge.dev/r/angular/code-snippet-playground.json