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
1 звезда на GitHub = 1 новый компонент. Уже звезды — помогите открыть следующий.
Поставить звезду →Зигзагообразные блоки, чередующие текст и CSS-иллюстрации (график, код, токены).
Insights
Drop-in SVG charts with tooltips, legends and table fallbacks. No chart library to configure.
Developer experience
Every component ships a TypeScript Props interface and renders beautifully with zero configuration.
interface Props {
title?: string;
variant?: 'glass' | 'solid';
size?: 'sm' | 'md' | 'lg';
}
// Sensible defaults, zero config
<LineChart /> Design system
Swap colors, radii and spacing in global.css and every section updates consistently.
Скопируйте и вставьте этот код на вашу страницу Astro:
---
import { FeatureAlternatingRows } from 'astro-component-kit/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' },
]}
/> ---
import { FeatureAlternatingRows } from 'astro-component-kit/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' },
]}
/> Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/lib/FeatureAlternatingRows.astro:
---
/**
* 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.
*/
interface Row { eyebrow: string; title: string; text: string; bullets: string[]; visual: 'chart' | 'code' | 'stack' }
interface Props {
title?: string;
rows?: Row[];
startReversed?: boolean;
variant?: 'glass' | 'outline' | 'plain';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
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 = '',
} = Astro.props;
const id = `far-${Math.random().toString(36).slice(2, 8)}`;
const bars = [38, 62, 48, 80, 66, 92, 74];
---
<div class="cq-feature-alternating-rows-cq">
<section class={`far far--${variant} far--${size} ${className}`} aria-labelledby={id}>
<h2 id={id} class="far__sr">{title}</h2>
<div class="far__inner">
{rows.map((r, i) => {
const reversed = startReversed ? i % 2 === 0 : i % 2 === 1;
return (
<article class={`far__row ${reversed ? 'far__row--rev' : ''}`}>
<div class="far__copy">
<p class="far__eyebrow">{r.eyebrow}</p>
<h3 class="far__title">{r.title}</h3>
<p class="far__text">{r.text}</p>
<ul class="far__bullets">
{r.bullets.map((b) => (
<li>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>
{b}
</li>
))}
</ul>
</div>
<div class="far__visual" aria-hidden="true">
{r.visual === 'chart' && (
<div class="far__chart">
{bars.map((h, bi) => <span style={`--h:${h}%; --d:${bi * 80}ms`}></span>)}
</div>
)}
{r.visual === 'code' && (
<pre class="far__code"><code><span class="k">interface</span> <span class="t">Props</span> {'{'}
title?: <span class="t">string</span>;
variant?: <span class="s">'glass'</span> | <span class="s">'solid'</span>;
size?: <span class="s">'sm'</span> | <span class="s">'md'</span> | <span class="s">'lg'</span>;
{'}'}
<span class="c">// Sensible defaults, zero config</span>
<<span class="t">LineChart</span> /></code></pre>
)}
{r.visual === 'stack' && (
<div class="far__stack">
<span style="--c:var(--chart-1, #3987e5)">--chart-1</span>
<span style="--c:var(--c-primary-light, #d4a017)">--c-primary</span>
<span style="--c:var(--chart-3, #199e70)">--chart-pos</span>
<span style="--c:var(--chart-7, #9085e9)">--r-lg: 20px</span>
</div>
)}
</div>
</article>
);
})}
</div>
</section>
</div>
<style>
.cq-feature-alternating-rows-cq { container-type: inline-size; }
.far {
--far-py: 5.5rem;
padding: var(--far-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.far--sm { --far-py: 3.5rem; }
.far--lg { --far-py: 7.5rem; }
.far__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.far__inner { max-width: 1120px; margin: 0 auto; display: grid; gap: clamp(4rem, 8vw, 7rem); }
.far__row { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 5vw, 5rem); align-items: center; }
.far__row--rev .far__copy { order: 2; }
.far__eyebrow { 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 { 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 { margin: 0 0 1.5rem; font-size: 1.0625rem; line-height: 1.65; color: var(--c-text-2, #a3a3a3); }
.far__bullets { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.7rem; }
.far__bullets li { display: flex; align-items: center; gap: 0.6rem; color: var(--c-text-1, #fff); font-size: 0.9375rem; }
.far__bullets svg { color: var(--chart-pos, #34c38f); flex-shrink: 0; }
.far__visual {
aspect-ratio: 4 / 3;
border-radius: var(--r-xl, 24px);
display: grid;
place-items: center;
padding: 2rem;
overflow: hidden;
}
.far--glass .far__visual {
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 .far__visual { border: 1px dashed rgb(var(--ink-rgb, 255 255 255) / 0.14); }
.far__chart { display: flex; align-items: flex-end; gap: 10px; width: 100%; height: 70%; }
.far__chart span {
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: far-grow 0.8s cubic-bezier(0.4, 0, 0.2, 1) both;
animation-delay: var(--d);
}
@keyframes far-grow { from { transform: scaleY(0); } }
.far__code {
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 .k { color: #c792ea; }
.far__code .t { color: #82aaff; }
.far__code .s { color: #c3e88d; }
.far__code .c { color: #6b7280; }
.far__stack { display: grid; gap: 0.75rem; width: 100%; max-width: 320px; }
.far__stack span {
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 span::before { content: ''; width: 20px; height: 20px; border-radius: 6px; background: var(--c); }
.far__stack span:nth-child(2) { margin-left: 1.5rem; }
.far__stack span:nth-child(3) { margin-left: 3rem; }
@container (max-width: 820px) {
.far__row { grid-template-columns: 1fr; }
.far__row--rev .far__copy { order: 0; }
}
@media (prefers-reduced-motion: reduce) { .far__chart span { animation: none; } }
</style> ---
/**
* 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.
*/
interface Row { eyebrow: string; title: string; text: string; bullets: string[]; visual: 'chart' | 'code' | 'stack' }
interface Props {
title?: string;
rows?: Row[];
startReversed?: boolean;
variant?: 'glass' | 'outline' | 'plain';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
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 = '',
} = Astro.props;
const id = `far-${Math.random().toString(36).slice(2, 8)}`;
const bars = [38, 62, 48, 80, 66, 92, 74];
---
<div class="cq-feature-alternating-rows-cq">
<section class={`far far--${variant} far--${size} ${className}`} aria-labelledby={id}>
<h2 id={id} class="far__sr">{title}</h2>
<div class="far__inner">
{rows.map((r, i) => {
const reversed = startReversed ? i % 2 === 0 : i % 2 === 1;
return (
<article class={`far__row ${reversed ? 'far__row--rev' : ''}`}>
<div class="far__copy">
<p class="far__eyebrow">{r.eyebrow}</p>
<h3 class="far__title">{r.title}</h3>
<p class="far__text">{r.text}</p>
<ul class="far__bullets">
{r.bullets.map((b) => (
<li>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>
{b}
</li>
))}
</ul>
</div>
<div class="far__visual" aria-hidden="true">
{r.visual === 'chart' && (
<div class="far__chart">
{bars.map((h, bi) => <span style={`--h:${h}%; --d:${bi * 80}ms`}></span>)}
</div>
)}
{r.visual === 'code' && (
<pre class="far__code"><code><span class="k">interface</span> <span class="t">Props</span> {'{'}
title?: <span class="t">string</span>;
variant?: <span class="s">'glass'</span> | <span class="s">'solid'</span>;
size?: <span class="s">'sm'</span> | <span class="s">'md'</span> | <span class="s">'lg'</span>;
{'}'}
<span class="c">// Sensible defaults, zero config</span>
<<span class="t">LineChart</span> /></code></pre>
)}
{r.visual === 'stack' && (
<div class="far__stack">
<span style="--c:var(--chart-1, #3987e5)">--chart-1</span>
<span style="--c:var(--c-primary-light, #d4a017)">--c-primary</span>
<span style="--c:var(--chart-3, #199e70)">--chart-pos</span>
<span style="--c:var(--chart-7, #9085e9)">--r-lg: 20px</span>
</div>
)}
</div>
</article>
);
})}
</div>
</section>
</div>
<style>
.cq-feature-alternating-rows-cq { container-type: inline-size; }
.far {
--far-py: 5.5rem;
padding: var(--far-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.far--sm { --far-py: 3.5rem; }
.far--lg { --far-py: 7.5rem; }
.far__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.far__inner { max-width: 1120px; margin: 0 auto; display: grid; gap: clamp(4rem, 8vw, 7rem); }
.far__row { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 5vw, 5rem); align-items: center; }
.far__row--rev .far__copy { order: 2; }
.far__eyebrow { 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 { 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 { margin: 0 0 1.5rem; font-size: 1.0625rem; line-height: 1.65; color: var(--c-text-2, #a3a3a3); }
.far__bullets { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.7rem; }
.far__bullets li { display: flex; align-items: center; gap: 0.6rem; color: var(--c-text-1, #fff); font-size: 0.9375rem; }
.far__bullets svg { color: var(--chart-pos, #34c38f); flex-shrink: 0; }
.far__visual {
aspect-ratio: 4 / 3;
border-radius: var(--r-xl, 24px);
display: grid;
place-items: center;
padding: 2rem;
overflow: hidden;
}
.far--glass .far__visual {
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 .far__visual { border: 1px dashed rgb(var(--ink-rgb, 255 255 255) / 0.14); }
.far__chart { display: flex; align-items: flex-end; gap: 10px; width: 100%; height: 70%; }
.far__chart span {
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: far-grow 0.8s cubic-bezier(0.4, 0, 0.2, 1) both;
animation-delay: var(--d);
}
@keyframes far-grow { from { transform: scaleY(0); } }
.far__code {
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 .k { color: #c792ea; }
.far__code .t { color: #82aaff; }
.far__code .s { color: #c3e88d; }
.far__code .c { color: #6b7280; }
.far__stack { display: grid; gap: 0.75rem; width: 100%; max-width: 320px; }
.far__stack span {
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 span::before { content: ''; width: 20px; height: 20px; border-radius: 6px; background: var(--c); }
.far__stack span:nth-child(2) { margin-left: 1.5rem; }
.far__stack span:nth-child(3) { margin-left: 3rem; }
@container (max-width: 820px) {
.far__row { grid-template-columns: 1fr; }
.far__row--rev .far__copy { order: 0; }
}
@media (prefers-reduced-motion: reduce) { .far__chart span { animation: none; } }
</style>
FeatureAlternatingRows.astro