Menu

Change language

GitHub repo

Demo — Sandbox Panel

blockmarketing

Interactive sandbox panel where editing a definition field updates a rendered result and a generated query side by side, with a reset control.

Installation

$npx uipkge-ng@latest add demo-sandbox-panel
Manifest:https://uipkge.dev/r/angular/demo-sandbox-panel.jsonInstalls to:src/app/components/blocks/demo-sandbox-panel/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

SandboxForm
interface SandboxForm {
  metric: string
  window: string
  groupBy: string
}
SandboxResultRow
interface 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

Raw manifest:https://uipkge.dev/r/angular/demo-sandbox-panel.json