Feature Deep-Dive — Alternating Rows
blockmarketingClassic 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
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/feature-alternating-rows.json$npx shadcn@latest add https://uipkge.dev/r/lit/feature-alternating-rows.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/feature-alternating-rows.json$bunx shadcn@latest add https://uipkge.dev/r/lit/feature-alternating-rows.jsonManifest:
https://uipkge.dev/r/lit/feature-alternating-rows.jsonInstalls to:src/uip/blocks/feature-alternating-rows/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.
FeatureRowinterface 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 }[]
}
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-feature-alternating-rows': UipFeatureAlternatingRows
}
}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