1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Características & Bento

Feature Alternating Rows

Filas en zigzag que alternan texto e ilustraciones CSS (gráfico, código, tokens).

featureszig-zagalternatingsplit
Vista Previa en Vivo

How the kit helps your team

Insights

Dashboards your stakeholders actually read

Drop-in SVG charts with tooltips, legends and table fallbacks. No chart library to configure.

  • 45 chart types
  • Colorblind-safe palette
  • Keyboard-accessible marks

Developer experience

Typed props, sensible defaults

Every component ships a TypeScript Props interface and renders beautifully with zero configuration.

  • Strict TypeScript
  • JSDoc on every prop
  • Copy-paste friendly

Design system

One token file to re-theme everything

Swap colors, radii and spacing in global.css and every section updates consistently.

  • CSS custom properties
  • Dark-first
  • 8pt spacing grid

Uso

Copia y pega este código directamente en tu componente React:

tsx
import { FeatureAlternatingRows } from 'react-component-kit-ui/sections';

<FeatureAlternatingRows rows={[
    { eyebrow: 'Insights', title: 'Dashboards people read', text: '…', bullets: ['45 charts'], visual: 'chart' },
    { eyebrow: 'DX', title: 'Typed props', text: '…', bullets: ['Strict TS'], visual: 'code' },
  ]} />

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/sections/FeatureAlternatingRows.tsx + FeatureAlternatingRows.css:

FeatureAlternatingRows.tsx
'use client';

import type { CSSProperties } from 'react';
import './FeatureAlternatingRows.css';

/** "a: b; --c: d" → style object */
function st(css: string | undefined): CSSProperties {
  const o: Record<string, string> = {};
  for (const decl of String(css ?? '').split(';')) {
    const i = decl.indexOf(':');
    if (i < 0) continue;
    const k = decl.slice(0, i).trim();
    if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
  }
  return o as CSSProperties;
}

interface Row { eyebrow: string; title: string; text: string; bullets: string[]; visual: 'chart' | 'code' | 'stack' }

/**
 * FeatureAlternatingRows — Zig-zag feature section: text and a visual alternate
 * sides on each row. Visuals are pure-CSS illustrations (no images to load).
 *
 * @param {string} title - Section heading (visually hidden if empty-styled).
 * @param {Array<{eyebrow: string, title: string, text: string, bullets: string[], visual: 'chart'|'code'|'stack'}>} rows - Feature rows.
 * @param {boolean} startReversed - Put the visual on the left in the first row.
 * @param {string} variant - Visual frame (glass|outline|plain).
 * @param {string} size - Vertical rhythm (sm|md|lg).
 * @param {string} className - Extra CSS class.
 */
export interface FeatureAlternatingRowsProps {
  title?: string;
  rows?: Row[];
  startReversed?: boolean;
  variant?: 'glass' | 'outline' | 'plain';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function FeatureAlternatingRows({
  title = 'How the kit helps your team',
  rows = [
    {
      eyebrow: 'Insights',
      title: 'Dashboards your stakeholders actually read',
      text: 'Drop-in SVG charts with tooltips, legends and table fallbacks. No chart library to configure.',
      bullets: ['45 chart types', 'Colorblind-safe palette', 'Keyboard-accessible marks'],
      visual: 'chart',
    },
    {
      eyebrow: 'Developer experience',
      title: 'Typed props, sensible defaults',
      text: 'Every component ships a TypeScript Props interface and renders beautifully with zero configuration.',
      bullets: ['Strict TypeScript', 'JSDoc on every prop', 'Copy-paste friendly'],
      visual: 'code',
    },
    {
      eyebrow: 'Design system',
      title: 'One token file to re-theme everything',
      text: 'Swap colors, radii and spacing in global.css and every section updates consistently.',
      bullets: ['CSS custom properties', 'Dark-first', '8pt spacing grid'],
      visual: 'stack',
    },
  ],
  startReversed = false,
  variant = 'glass',
  size = 'md',
  className = '',
}: FeatureAlternatingRowsProps) {
  const id = `far-${Math.random().toString(36).slice(2, 8)}`;
  const bars = [38, 62, 48, 80, 66, 92, 74];
  return (
    <>
      <div className="cq-feature-alternating-rows-cq" data-rk-43b840="">
      <section className={`far far--${variant} far--${size} ${className}`} aria-labelledby={id} data-rk-43b840="">
        <h2 id={id} className="far__sr" data-rk-43b840="">{title}</h2>
        <div className="far__inner" data-rk-43b840="">
          {rows.map((r, i) => {
            const reversed = startReversed ? i % 2 === 0 : i % 2 === 1;
            return (
              <article className={`far__row ${reversed ? 'far__row--rev' : ''}`} data-rk-43b840="">
                <div className="far__copy" data-rk-43b840="">
                  <p className="far__eyebrow" data-rk-43b840="">{r.eyebrow}</p>
                  <h3 className="far__title" data-rk-43b840="">{r.title}</h3>
                  <p className="far__text" data-rk-43b840="">{r.text}</p>
                  <ul className="far__bullets" data-rk-43b840="">
                    {r.bullets.map((b, __i: number) => (
                      <li key={__i} data-rk-43b840="">
                        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-43b840=""><path d="M20 6 9 17l-5-5" data-rk-43b840="" /></svg>
                        {b}
                      </li>
                    ))}
                  </ul>
                </div>
                <div className="far__visual" aria-hidden="true" data-rk-43b840="">
                  {r.visual === 'chart' && (
                    <div className="far__chart" data-rk-43b840="">
                      {bars.map((h, bi) => <span key={bi} style={st(`--h:${h}%; --d:${bi * 80}ms`)} data-rk-43b840=""></span>)}
                    </div>
                  )}
                  {r.visual === 'code' && (
                    <pre className="far__code" data-rk-43b840=""><code data-rk-43b840=""><span className="k" data-rk-43b840="">interface</span> <span className="t" data-rk-43b840="">Props</span> {'{'}
        title?: <span className="t" data-rk-43b840="">string</span>;
        variant?: <span className="s" data-rk-43b840="">'glass'</span> | <span className="s" data-rk-43b840="">'solid'</span>;
        size?: <span className="s" data-rk-43b840="">'sm'</span> | <span className="s" data-rk-43b840="">'md'</span> | <span className="s" data-rk-43b840="">'lg'</span>;
      {'}'}
      
      <span className="c" data-rk-43b840="">// Sensible defaults, zero config</span>
      &lt;<span className="t" data-rk-43b840="">LineChart</span> /&gt;</code></pre>
                  )}
                  {r.visual === 'stack' && (
                    <div className="far__stack" data-rk-43b840="">
                      <span style={{ '--c': "var(--chart-1, #3987e5)" } as CSSProperties} data-rk-43b840="">--chart-1</span>
                      <span style={{ '--c': "var(--c-primary-light, #d4a017)" } as CSSProperties} data-rk-43b840="">--c-primary</span>
                      <span style={{ '--c': "var(--chart-3, #199e70)" } as CSSProperties} data-rk-43b840="">--chart-pos</span>
                      <span style={{ '--c': "var(--chart-7, #9085e9)" } as CSSProperties} data-rk-43b840="">--r-lg: 20px</span>
                    </div>
                  )}
                </div>
              </article>
            );
          })}
        </div>
      </section>
      </div>
    </>
  );
}

export default FeatureAlternatingRows;
FeatureAlternatingRows.css
/* FeatureAlternatingRows — ported from astro-component-kit (sections/FeatureAlternatingRows.astro) */

.cq-feature-alternating-rows-cq:where([data-rk-43b840]) { container-type: inline-size; }
.far:where([data-rk-43b840]) {
  --far-py: 5.5rem;
  padding: var(--far-py) var(--sp-6, 1.5rem);
  background: var(--c-bg, #050505);
}
.far--sm:where([data-rk-43b840]) { --far-py: 3.5rem; }
.far--lg:where([data-rk-43b840]) { --far-py: 7.5rem; }
.far__sr:where([data-rk-43b840]) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.far__inner:where([data-rk-43b840]) { max-width: 1120px; margin: 0 auto; display: grid; gap: clamp(4rem, 8vw, 7rem); }
.far__row:where([data-rk-43b840]) { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 5vw, 5rem); align-items: center; }
.far__row--rev:where([data-rk-43b840]) .far__copy:where([data-rk-43b840]) { order: 2; }
.far__eyebrow:where([data-rk-43b840]) { margin: 0 0 0.75rem; font-size: 0.8125rem; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-primary-light, #d4a017); }
.far__title:where([data-rk-43b840]) { margin: 0 0 1rem; font-size: clamp(1.6rem, 3vw, 2.25rem); font-weight: 750; letter-spacing: -0.025em; line-height: 1.15; color: var(--c-text-1, #fff); }
.far__text:where([data-rk-43b840]) { margin: 0 0 1.5rem; font-size: 1.0625rem; line-height: 1.65; color: var(--c-text-2, #a3a3a3); }
.far__bullets:where([data-rk-43b840]) { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.7rem; }
.far__bullets:where([data-rk-43b840]) li:where([data-rk-43b840]) { display: flex; align-items: center; gap: 0.6rem; color: var(--c-text-1, #fff); font-size: 0.9375rem; }
.far__bullets:where([data-rk-43b840]) svg:where([data-rk-43b840]) { color: var(--chart-pos, #34c38f); flex-shrink: 0; }

.far__visual:where([data-rk-43b840]) {
  aspect-ratio: 4 / 3;
  border-radius: var(--r-xl, 24px);
  display: grid;
  place-items: center;
  padding: 2rem;
  overflow: hidden;
}
.far--glass:where([data-rk-43b840]) .far__visual:where([data-rk-43b840]) {
  background: radial-gradient(circle at 30% 20%, rgb(var(--ink-rgb, 255 255 255) / 0.06), transparent 60%), rgb(var(--ink-rgb, 255 255 255) / 0.02);
  border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07));
}
.far--outline:where([data-rk-43b840]) .far__visual:where([data-rk-43b840]) { border: 1px dashed rgb(var(--ink-rgb, 255 255 255) / 0.14); }
.far__chart:where([data-rk-43b840]) { display: flex; align-items: flex-end; gap: 10px; width: 100%; height: 70%; }
.far__chart:where([data-rk-43b840]) span:where([data-rk-43b840]) {
  flex: 1;
  height: var(--h);
  border-radius: 6px 6px 0 0;
  background: linear-gradient(to top, rgba(57, 135, 229, 0.35), var(--chart-1, #3987e5));
  transform-origin: bottom;
  animation: feature-alternating-rows-far-grow 0.8s cubic-bezier(0.4, 0, 0.2, 1) both;
  animation-delay: var(--d);
}
@keyframes feature-alternating-rows-far-grow { from { transform: scaleY(0); } }
.far__code:where([data-rk-43b840]) {
  margin: 0;
  width: 100%;
  padding: 1.5rem;
  border-radius: var(--r-md, 12px);
  background: #0b0b0d;
  border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.06);
  font-family: var(--font-mono, monospace);
  font-size: 0.8125rem;
  line-height: 1.75;
  color: var(--c-text-2, #c4c4c4);
  overflow-x: auto;
}
.far__code:where([data-rk-43b840]) .k:where([data-rk-43b840]) { color: #c792ea; }
.far__code:where([data-rk-43b840]) .t:where([data-rk-43b840]) { color: #82aaff; }
.far__code:where([data-rk-43b840]) .s:where([data-rk-43b840]) { color: #c3e88d; }
.far__code:where([data-rk-43b840]) .c:where([data-rk-43b840]) { color: #6b7280; }
.far__stack:where([data-rk-43b840]) { display: grid; gap: 0.75rem; width: 100%; max-width: 320px; }
.far__stack:where([data-rk-43b840]) span:where([data-rk-43b840]) {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.85rem 1rem;
  border-radius: var(--r-md, 12px);
  background: rgb(var(--ink-rgb, 255 255 255) / 0.03);
  border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.07);
  font-family: var(--font-mono, monospace);
  font-size: 0.8125rem;
  color: var(--c-text-1, #fff);
}
.far__stack:where([data-rk-43b840]) span:where([data-rk-43b840])::before { content: ''; width: 20px; height: 20px; border-radius: 6px; background: var(--c); }
.far__stack:where([data-rk-43b840]) span:nth-child(2):where([data-rk-43b840]) { margin-left: 1.5rem; }
.far__stack:where([data-rk-43b840]) span:nth-child(3):where([data-rk-43b840]) { margin-left: 3rem; }
@container (max-width: 820px) {
  .far__row:where([data-rk-43b840]) { grid-template-columns: 1fr; }
  .far__row--rev:where([data-rk-43b840]) .far__copy:where([data-rk-43b840]) { order: 0; }
}
@media (prefers-reduced-motion: reduce) { .far__chart:where([data-rk-43b840]) span:where([data-rk-43b840]) { animation: none; } }