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@latest add https://uipkge.dev/r/lit/feature-alternating-rows.json
Manifest:https://uipkge.dev/r/lit/feature-alternating-rows.jsonInstalls to:src/uip/blocks/feature-alternating-rows/

Variants

Loading interactive previews…

Schema

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

FeatureRow
interface FeatureRow {
  eyebrow: string
  icon: IconNode
  iconName: string
  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 }[]
  }
}
HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-feature-alternating-rows': UipFeatureAlternatingRows
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/feature-alternating-rows/feature-alternating-rows.ts9.3 kB
    import { LitElement, css, html, nothing } from 'lit'
    import { ArrowRight, Check, GitBranch, ShieldCheck, Zap, type IconNode } from 'lucide'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    import { icon } from '../../lib/icon'
    // Side-effect imports: register the composed elements for standalone use
    // (consumers who install only this block still get working primitives).
    import '../../components/badge/badge'
    import '../../components/button/button'
    
    // Explicit shape: the three preview panels are structurally different, so the
    // optional keys keep the union from collapsing when the template narrows.
    interface FeatureRow {
      eyebrow: string
      icon: IconNode
      iconName: string
      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 }[]
      }
    }
    
    const rows: FeatureRow[] = [
      {
        eyebrow: 'Versioning',
        icon: GitBranch,
        iconName: 'git-branch',
        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,
        iconName: 'shield-check',
        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,
        iconName: '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' },
          ],
        },
      },
    ]
    
    /**
     * <uip-feature-alternating-rows> — the registry FeatureAlternatingRows block
     * as a web component.
     *
     * Class strings are React's FeatureAlternatingRows verbatim. No props, no
     * state, no callbacks — a static zig-zag. Card and separator chrome stay
     * plain divs carrying the registry part classes (see the login-01 block).
     */
    export class UipFeatureAlternatingRows extends LitElement {
      static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
      // :host display can't be a utility class (the host has no template of its own).
      static styles = [tailwind, css`:host { display: block; }`]
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'feature-alternating-rows')
      }
    
      render() {
        return html`
          <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">
                <uip-badge variant="secondary">Platform</uip-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">
                ${rows.map(
                  (row) => html`<div class="grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
                    <div class=${row.reverse ? 'lg:order-2' : ''}>
                      <div class="text-muted-foreground flex items-center gap-2">
                        ${icon(row.icon, row.iconName, 'size-4')}
                        <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">
                        ${row.points.map(
                          (point) => html`<li class="flex items-start gap-2.5 text-sm">
                            ${icon(Check, 'check', 'text-primary mt-0.5 size-4 shrink-0')}
                            <span>${point}</span>
                          </li>`,
                        )}
                      </ul>
                      <uip-button
                        variant="link"
                        class="mt-5 h-auto p-0 [&::part(base)]:h-auto [&::part(base)]:p-0"
                      >
                        ${row.link} ${icon(ArrowRight, 'arrow-right', 'ml-1.5 size-3.5')}
                      </uip-button>
                    </div>
    
                    <div
                      data-uipkge=""
                      data-slot="card"
                      class=${row.reverse
                        ? 'bg-card text-card-foreground rounded-xl border border-border shadow-sm transition-colors duration-150 overflow-hidden lg:order-1'
                        : 'bg-card text-card-foreground rounded-xl border border-border shadow-sm transition-colors duration-150 overflow-hidden'}
                    >
                      <div data-uipkge="" data-slot="card-content" 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>
    
                        ${row.preview.lines
                          ? html`<div class="space-y-1 p-4 font-mono text-xs">
                              ${row.preview.lines.map(
                                (line) => html`<p
                                  class=${line.tone === 'add'
                                    ? 'text-success'
                                    : line.tone === 'remove'
                                      ? 'text-destructive'
                                      : 'text-muted-foreground'}
                                >
                                  ${line.text}
                                </p>`,
                              )}
                            </div>`
                          : nothing}
    
                        ${row.preview.rules
                          ? html`<div class="divide-border divide-y">
                              ${row.preview.rules.map(
                                (rule) => html`<div class="flex items-center justify-between gap-4 px-4 py-3">
                                  <span class="font-mono text-xs">${rule.scope}</span>
                                  <uip-badge variant=${rule.allowed ? 'secondary' : 'outline'}>${rule.value}</uip-badge>
                                </div>`,
                              )}
                            </div>`
                          : nothing}
    
                        ${row.preview.stats
                          ? html`<div class="p-4">
                              <ul class="space-y-3">
                                ${row.preview.stats.map(
                                  (stat) => html`<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>
                                    <div
                                      data-uipkge=""
                                      data-slot="separator"
                                      data-orientation="horizontal"
                                      role="none"
                                      class="bg-border shrink-0 h-px w-full mt-3"
                                    ></div>
                                  </li>`,
                                )}
                              </ul>
                            </div>`
                          : nothing}
                      </div>
                    </div>
                  </div>`,
                )}
              </div>
            </div>
          </section>
        `
      }
    }
    
    customElements.get('uip-feature-alternating-rows') ||
      customElements.define('uip-feature-alternating-rows', UipFeatureAlternatingRows)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-feature-alternating-rows': UipFeatureAlternatingRows
      }
    }
    

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