Menu

Change language

GitHub repo

Feature Deep-Dive — Alternating Rows

blockmarketing

Classic zig-zag feature deep-dive: three full-width rows alternating copy and visual, each with an eyebrow, headline, checklist, inline link, and a bordered preview panel built from real primitives.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/feature-alternating-rows.json
Manifest:https://uipkge.dev/r/svelte/feature-alternating-rows.jsonInstalls to:src/lib/components/blocks/feature-alternating-rows/

Variants

Loading interactive previews…

Files installed (1)

  • src/lib/components/blocks/feature-alternating-rows/FeatureAlternatingRows.svelte6.8 kB
    <script lang="ts" module>
      import type { Component } from 'svelte'
    
      // Explicit shape: the three preview panels are structurally different, so the
      // optional keys keep the union from collapsing when the template narrows.
      export interface FeatureRow {
        eyebrow: string
        icon: Component
        title: string
        body: string
        points: string[]
        link: string
        reverse: boolean
        preview: {
          label: string
          lines?: { text: string; tone: 'add' | 'remove' | 'same' }[]
          rules?: { scope: string; value: string; allowed: boolean }[]
          stats?: { name: string; detail: string }[]
        }
      }
    </script>
    
    <script lang="ts">
      import { ArrowRight, Check, GitBranch, ShieldCheck, Zap } from '@lucide/svelte'
      import { Badge } from '$lib/components/ui/badge'
      import { Button } from '$lib/components/ui/button'
      import { Card, CardContent } from '$lib/components/ui/card'
      import { Separator } from '$lib/components/ui/separator'
    
      const rows: FeatureRow[] = [
        {
          eyebrow: 'Versioning',
          icon: GitBranch,
          title: 'Every change is a reviewable diff',
          body: 'Dashboards, metrics, and permissions are stored as plain files. Changes open a pull request instead of silently overwriting production.',
          points: ['Branch per change', 'Review before publish', 'One-click revert'],
          link: 'How version control works',
          reverse: false,
          preview: {
            label: 'metrics/revenue.yml',
            lines: [
              { text: '+ window: trailing_28d', tone: 'add' },
              { text: '- window: trailing_30d', tone: 'remove' },
              { text: '  owner: finance-analytics', tone: 'same' },
            ],
          },
        },
        {
          eyebrow: 'Access',
          icon: ShieldCheck,
          title: 'Permissions that follow the row, not the dashboard',
          body: 'Scope is evaluated at query time from your identity provider, so a shared link never leaks a region or account the viewer cannot see.',
          points: ['Row-level filters', 'SCIM group sync', 'Audited every query'],
          link: 'Read the access model',
          reverse: true,
          preview: {
            label: 'Effective access — EMEA analyst',
            rules: [
              { scope: 'region', value: 'EMEA only', allowed: true },
              { scope: 'revenue.net', value: 'Visible', allowed: true },
              { scope: 'payroll.*', value: 'Blocked', allowed: false },
            ],
          },
        },
        {
          eyebrow: 'Performance',
          icon: Zap,
          title: 'Cached where it matters, fresh where it counts',
          body: 'Hot aggregates are materialised on a schedule you control. Everything else falls through to the warehouse so the number is never stale by accident.',
          points: ['Per-metric freshness', 'Warehouse pass-through', 'Cost ceiling per team'],
          link: 'See the caching rules',
          reverse: false,
          preview: {
            label: 'Freshness',
            stats: [
              { name: 'Revenue rollup', detail: 'materialised · 5 min' },
              { name: 'Pipeline by stage', detail: 'materialised · 1 hr' },
              { name: 'Raw event search', detail: 'live query' },
            ],
          },
        },
      ]
    
      function toneClass(tone: 'add' | 'remove' | 'same') {
        return tone === 'add' ? 'text-success' : tone === 'remove' ? 'text-destructive' : 'text-muted-foreground'
      }
    </script>
    
    <section data-slot="feature-alternating-rows" class="bg-background">
      <div class="mx-auto max-w-6xl px-6 py-20 lg:py-28">
        <div class="max-w-2xl">
          <Badge variant="secondary">Platform</Badge>
          <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Built like infrastructure, not a widget</h2>
          <p class="text-muted-foreground mt-3 text-lg">
            Three decisions that stop a reporting layer from rotting after the first quarter.
          </p>
        </div>
    
        <div class="mt-16 space-y-16 lg:space-y-24">
          {#each rows as row (row.title)}
            {@const Icon = row.icon}
            <div class="grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
              <div class:lg:order-2={row.reverse}>
                <div class="text-muted-foreground flex items-center gap-2">
                  <Icon class="size-4" aria-hidden="true" />
                  <span class="font-mono text-xs tracking-[0.14em] uppercase">{row.eyebrow}</span>
                </div>
                <h3 class="mt-4 text-2xl font-semibold tracking-tight">{row.title}</h3>
                <p class="text-muted-foreground mt-3 leading-relaxed">{row.body}</p>
                <ul class="mt-6 space-y-2.5">
                  {#each row.points as point (point)}
                    <li class="flex items-start gap-2.5 text-sm">
                      <Check class="text-primary mt-0.5 size-4 shrink-0" aria-hidden="true" />
                      <span>{point}</span>
                    </li>
                  {/each}
                </ul>
                <Button variant="link" class="mt-5 h-auto p-0">
                  {row.link}
                  <ArrowRight class="ml-1.5 size-3.5" aria-hidden="true" />
                </Button>
              </div>
    
              <Card class={row.reverse ? 'lg:order-1' : ''}>
                <CardContent class="p-0">
                  <div class="border-border text-muted-foreground border-b px-4 py-2.5 font-mono text-xs">
                    {row.preview.label}
                  </div>
    
                  <!-- Diff preview -->
                  {#if row.preview.lines}
                    <div class="space-y-1 p-4 font-mono text-xs">
                      {#each row.preview.lines as line (line.text)}
                        <p class={toneClass(line.tone)}>
                          {line.text}
                        </p>
                      {/each}
                    </div>
                    <!-- Access preview -->
                  {:else if row.preview.rules}
                    <div class="divide-border divide-y">
                      {#each row.preview.rules as rule (rule.scope)}
                        <div class="flex items-center justify-between gap-4 px-4 py-3">
                          <span class="font-mono text-xs">{rule.scope}</span>
                          <Badge variant={rule.allowed ? 'secondary' : 'outline'}>{rule.value}</Badge>
                        </div>
                      {/each}
                    </div>
                    <!-- Freshness preview -->
                  {:else}
                    <div class="p-4">
                      <ul class="space-y-3">
                        {#each (row.preview.stats ?? []) as stat (stat.name)}
                          <li>
                            <div class="flex items-baseline justify-between gap-4">
                              <span class="text-sm font-medium">{stat.name}</span>
                              <span class="text-muted-foreground font-mono text-xs">{stat.detail}</span>
                            </div>
                            <Separator class="mt-3" />
                          </li>
                        {/each}
                      </ul>
                    </div>
                  {/if}
                </CardContent>
              </Card>
            </div>
          {/each}
        </div>
      </div>
    </section>
    

Raw manifest:https://uipkge.dev/r/svelte/feature-alternating-rows.json