Demo — Sandbox Panel
blockmarketingInteractive sandbox panel where editing a definition field updates a rendered result and a generated query side by side, with a reset control.
Installation
$pnpm dlx uipkge-ng@latest add demo-sandbox-panel$npx uipkge-ng@latest add demo-sandbox-panel$yarn dlx uipkge-ng@latest add demo-sandbox-panel$bunx uipkge-ng@latest add demo-sandbox-panelManifest:
https://uipkge.dev/r/angular/demo-sandbox-panel.jsonInstalls to:src/app/components/blocks/demo-sandbox-panel/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.
SandboxForminterface SandboxForm {
metric: string
window: string
groupBy: string
}SandboxResultRowinterface SandboxResultRow {
key: string
value: number
}Files installed (2)
src/app/components/blocks/demo-sandbox-panel/demo-sandbox-panel.component.ts7.7 kB
// Interactive sandbox panel where editing a definition field updates a rendered result // and a generated query side by side, with a reset control. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { RotateCcw, LucideAngularModule } 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' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, } from '@/ui/tabs/tabs.component' export interface SandboxForm { metric: string window: string groupBy: string } export interface SandboxResultRow { key: string value: number } export const SANDBOX_DEFAULTS: SandboxForm = { metric: 'revenue_net', window: 'trailing_28d', groupBy: 'region' } export const SANDBOX_BASE: Record<string, { key: string; value: number }[]> = { region: [ { key: 'EMEA', value: 1_842_100 }, { key: 'AMER', value: 1_536_400 }, { key: 'APAC', value: 762_400 }, ], channel: [ { key: 'Direct', value: 2_104_900 }, { key: 'Marketplace', value: 1_218_600 }, { key: 'Wholesale', value: 817_400 }, ], } export const SANDBOX_MULTIPLIER: Record<string, number> = { trailing_7d: 0.25, trailing_28d: 1, trailing_90d: 3.1, } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-demo-sandbox-panel, [ui-demo-sandbox-panel]', standalone: true, host: { '[attr.data-slot]': '"demo-sandbox-panel"', '[class]': 'hostClass', }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSeparatorComponent, UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, ], template: ` <div class="mx-auto max-w-5xl px-6 py-20 lg:py-28"> <div class="flex flex-wrap items-end justify-between gap-6"> <div class="max-w-xl"> <span ui-badge variant="secondary">Sandbox</span> <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl"> Change the definition, watch both sides </h2> </div> <button ui-button variant="ghost" size="sm" (click)="reset()"> <lucide-icon [img]="RotateCcw" class="mr-1.5 size-3.5" /> Reset </button> </div> <div ui-card class="mt-8"> <div ui-card-content class="grid gap-6 p-6 lg:grid-cols-[18rem_1fr] lg:gap-8"> <div class="space-y-4"> <div> <label for="sandbox-metric" class="text-sm font-medium">Metric</label> <ui-input id="sandbox-metric" [value]="metric()" (valueChange)="metric.set($event)" class="mt-1.5 font-mono text-sm" /> </div> <div> <label for="sandbox-window" class="text-sm font-medium">Window</label> <ui-select [value]="window()" (valueChange)="setWindow($event)"> <button ui-select-trigger id="sandbox-window" class="mt-1.5"> <ui-select-value /> </button> <ui-select-content> <ui-select-item value="trailing_7d">trailing_7d</ui-select-item> <ui-select-item value="trailing_28d">trailing_28d</ui-select-item> <ui-select-item value="trailing_90d">trailing_90d</ui-select-item> </ui-select-content> </ui-select> </div> <div> <label for="sandbox-group" class="text-sm font-medium">Group by</label> <ui-select [value]="groupBy()" (valueChange)="setGroupBy($event)"> <button ui-select-trigger id="sandbox-group" class="mt-1.5"> <ui-select-value /> </button> <ui-select-content> <ui-select-item value="region">region</ui-select-item> <ui-select-item value="channel">channel</ui-select-item> </ui-select-content> </ui-select> </div> <ui-separator /> <p class="text-muted-foreground text-xs leading-relaxed"> Both panels recompute from these three fields. Nothing is cached between them. </p> </div> <ui-tabs defaultValue="result"> <ui-tabs-list variant="segmented"> <button ui-tabs-trigger value="result" variant="segmented">Result</button> <button ui-tabs-trigger value="query" variant="segmented">Generated query</button> </ui-tabs-list> <ui-tabs-content value="result" class="mt-4"> <div class="border-border min-h-[12rem] overflow-hidden rounded-lg border"> <div class="border-border text-muted-foreground border-b px-4 py-2 font-mono text-xs"> {{ metric() }} · {{ window() }} </div> <ul class="divide-border divide-y"> @for (row of results(); track row.key) { <li class="flex items-center justify-between gap-4 px-4 py-2.5"> <span class="text-sm">{{ row.key }}</span> <span class="font-mono text-sm tabular-nums">£{{ formatValue(row.value) }}</span> </li> } </ul> </div> </ui-tabs-content> <ui-tabs-content value="query" class="mt-4"> <pre class="border-border min-h-[12rem] overflow-x-auto rounded-lg border p-4 font-mono text-xs leading-relaxed" ><code>{{ query() }}</code></pre> </ui-tabs-content> </ui-tabs> </div> </div> </div> `, }) export class UiDemoSandboxPanelComponent { protected readonly RotateCcw = RotateCcw @Input('class') className?: string readonly metric = signal(SANDBOX_DEFAULTS.metric) readonly window = signal(SANDBOX_DEFAULTS.window) readonly groupBy = signal(SANDBOX_DEFAULTS.groupBy) // Both outputs derive from the same form, so the query and the result can never // describe different things — the failure mode of most fake sandboxes. readonly query = computed( () => `SELECT ${this.groupBy()}, SUM(amount) AS ${this.metric()} FROM metrics.${this.metric()} WHERE window = '${this.window()}' GROUP BY ${this.groupBy()} ORDER BY ${this.metric()} DESC`, ) readonly results = computed<SandboxResultRow[]>(() => (SANDBOX_BASE[this.groupBy()] ?? SANDBOX_BASE['region']!).map((row) => ({ key: row.key, value: Math.round(row.value * (SANDBOX_MULTIPLIER[this.window()] ?? 1)), })), ) get hostClass(): string { return cn('bg-background block w-full', this.className) } setWindow(value: string): void { this.window.set(value) } setGroupBy(value: string): void { this.groupBy.set(value) } formatValue(value: number): string { return value.toLocaleString('en-GB') } reset(): void { this.metric.set(SANDBOX_DEFAULTS.metric) this.window.set(SANDBOX_DEFAULTS.window) this.groupBy.set(SANDBOX_DEFAULTS.groupBy) } }src/app/components/blocks/demo-sandbox-panel/index.ts0.2 kB
export { UiDemoSandboxPanelComponent, SANDBOX_DEFAULTS, SANDBOX_BASE, SANDBOX_MULTIPLIER, type SandboxForm, type SandboxResultRow, } from './demo-sandbox-panel.component'
Raw manifest:https://uipkge.dev/r/angular/demo-sandbox-panel.json