Integrations Grid
Directorio de integraciones con filtros por categoría, monogramas y estado de disponibilidad.
integrationsfilterdirectoryapps
Vista Previa en Vivo
Plays well with your stack
Connect the tools your team already uses. New integrations every month.
8 integrations
- Sl
Slack
AvailablePost alerts and weekly digests to channels.
Messaging - Gi
GitHub
AvailableAnnotate charts with deploys and releases.
Developer - St
Stripe
AvailableSync MRR, churn and invoices automatically.
Payments - Li
Linear
BetaTurn insights into issues in one click.
Developer - No
Notion
AvailableEmbed live charts in your team wiki.
Docs - Hu
HubSpot
BetaAttribute revenue to campaigns and deals.
CRM - Po
Postgres
AvailableQuery your warehouse with zero ETL.
Data - Di
Discord
Coming soonCommunity metrics and bot notifications.
Messaging
Uso
Copia y pega este código directamente en tu componente React:
tsx
import { IntegrationsGrid } from 'react-component-kit-ui/sections';
<IntegrationsGrid items={[
{ name: 'Slack', category: 'Messaging', description: 'Alerts in channels.', status: 'available' },
{ name: 'Linear', category: 'Developer', description: 'Insights to issues.', status: 'beta' },
]} />Instalación Manual
Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/sections/IntegrationsGrid.tsx + IntegrationsGrid.css:
IntegrationsGrid.tsx
'use client';
import { useEffect, useRef } from 'react';
import type { CSSProperties } from 'react';
import './IntegrationsGrid.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 Item { name: string; category: string; description: string; status?: 'available' | 'beta' | 'soon' }
/**
* IntegrationsGrid — Integrations directory with category filter chips
* (progressively enhanced), monogram tiles and connection status badges.
*
* @param {string} title - Section heading.
* @param {string} subtitle - Section description.
* @param {Array<{name: string, category: string, description: string, status?: 'available'|'beta'|'soon'}>} items - Integrations.
* @param {string} variant - Tile style (card|compact|outline).
* @param {string} size - Vertical rhythm (sm|md|lg).
* @param {string} className - Extra CSS class.
*/
export interface IntegrationsGridProps {
title?: string;
subtitle?: string;
items?: Item[];
variant?: 'card' | 'compact' | 'outline';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function IntegrationsGrid({
title = 'Plays well with your stack',
subtitle = 'Connect the tools your team already uses. New integrations every month.',
items = [
{ name: 'Slack', category: 'Messaging', description: 'Post alerts and weekly digests to channels.', status: 'available' },
{ name: 'GitHub', category: 'Developer', description: 'Annotate charts with deploys and releases.', status: 'available' },
{ name: 'Stripe', category: 'Payments', description: 'Sync MRR, churn and invoices automatically.', status: 'available' },
{ name: 'Linear', category: 'Developer', description: 'Turn insights into issues in one click.', status: 'beta' },
{ name: 'Notion', category: 'Docs', description: 'Embed live charts in your team wiki.', status: 'available' },
{ name: 'HubSpot', category: 'CRM', description: 'Attribute revenue to campaigns and deals.', status: 'beta' },
{ name: 'Postgres', category: 'Data', description: 'Query your warehouse with zero ETL.', status: 'available' },
{ name: 'Discord', category: 'Messaging', description: 'Community metrics and bot notifications.', status: 'soon' },
],
variant = 'card',
size = 'md',
className = '',
}: IntegrationsGridProps) {
const id = `ig-${Math.random().toString(36).slice(2, 8)}`;
const categories = ['All', ...new Set(items.map((i) => i.category))];
const hues = ['var(--chart-1, #3987e5)', 'var(--chart-2, #d95926)', 'var(--chart-3, #199e70)', 'var(--chart-4, #c98500)', 'var(--chart-5, #d55181)', 'var(--chart-7, #9085e9)'];
const statusLabel = { available: 'Available', beta: 'Beta', soon: 'Coming soon' };
// Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
document.querySelectorAll<HTMLElement>('[data-ig]').forEach((root) => {
const chips = root.querySelectorAll<HTMLButtonElement>('.ig__chip');
const tiles = root.querySelectorAll<HTMLElement>('.ig__tile');
const count = root.querySelector('.ig__count');
chips.forEach((chip) => {
chip.addEventListener('click', () => {
const f = chip.dataset.filter;
chips.forEach((c) => c.setAttribute('aria-pressed', String(c === chip)));
let shown = 0;
tiles.forEach((t) => {
const on = f === 'All' || t.dataset.category === f;
t.hidden = !on;
if (on) shown += 1;
});
if (count) count.textContent = `${shown} integration${shown === 1 ? '' : 's'}${f === 'All' ? '' : ` in ${f}`}`;
});
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<section className={`ig ig--${variant} ig--${size} ${className}`} aria-labelledby={id} data-ig data-rk-32bfaf="">
<div className="ig__inner" data-rk-32bfaf="">
<header className="ig__head" data-rk-32bfaf="">
<h2 id={id} className="ig__title" data-rk-32bfaf="">{title}</h2>
<p className="ig__subtitle" data-rk-32bfaf="">{subtitle}</p>
</header>
<div className="ig__filters" role="group" aria-label="Filter integrations by category" data-rk-32bfaf="">
{categories.map((c, i) => (
<button key={i} type="button" className="ig__chip" data-filter={c} aria-pressed={i === 0 ? 'true' : 'false'} data-rk-32bfaf="">{c}</button>
))}
</div>
<p className="ig__count" role="status" aria-live="polite" data-rk-32bfaf="">{items.length} integrations</p>
<ul className="ig__grid" data-rk-32bfaf="">
{items.map((it, i) => (
<li key={i} className="ig__tile" data-category={it.category} data-rk-32bfaf="">
<span className="ig__logo" style={st(`--c:${hues[i % hues.length]}`)} aria-hidden="true" data-rk-32bfaf="">{it.name.slice(0, 2)}</span>
<div className="ig__body" data-rk-32bfaf="">
<div className="ig__row" data-rk-32bfaf="">
<h3 className="ig__name" data-rk-32bfaf="">{it.name}</h3>
{it.status && <span className={`ig__status ig__status--${it.status}`} data-rk-32bfaf="">{statusLabel[it.status]}</span>}
</div>
<p className="ig__desc" data-rk-32bfaf="">{it.description}</p>
<span className="ig__cat" data-rk-32bfaf="">{it.category}</span>
</div>
</li>
))}
</ul>
</div>
</section>
</>
);
}
export default IntegrationsGrid;IntegrationsGrid.css
/* IntegrationsGrid — ported from astro-component-kit (sections/IntegrationsGrid.astro) */
.ig:where([data-rk-32bfaf]) {
--ig-py: 5.5rem;
padding: var(--ig-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.ig--sm:where([data-rk-32bfaf]) { --ig-py: 3.5rem; }
.ig--lg:where([data-rk-32bfaf]) { --ig-py: 7.5rem; }
.ig__inner:where([data-rk-32bfaf]) { max-width: 1180px; margin: 0 auto; }
.ig__head:where([data-rk-32bfaf]) { text-align: center; max-width: 620px; margin: 0 auto 2rem; }
.ig__title:where([data-rk-32bfaf]) { margin: 0 0 0.75rem; font-size: clamp(1.9rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
.ig__subtitle:where([data-rk-32bfaf]) { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
.ig__filters:where([data-rk-32bfaf]) { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; }
.ig__chip:where([data-rk-32bfaf]) {
min-height: 40px;
padding: 0 1rem;
border-radius: var(--r-full, 999px);
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
background: transparent;
color: var(--c-text-2, #a3a3a3);
font: inherit;
font-size: 0.875rem;
font-weight: 550;
cursor: pointer;
transition: all 0.2s ease;
}
.ig__chip:hover:where([data-rk-32bfaf]) { color: var(--c-text-1, #fff); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.25); }
.ig__chip[aria-pressed='true']:where([data-rk-32bfaf]) { background: var(--c-text-1, #fff); color: var(--c-on-primary, #0a0a0a); border-color: transparent; }
.ig__chip:focus-visible:where([data-rk-32bfaf]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.ig__count:where([data-rk-32bfaf]) { margin: 1rem 0 1.5rem; text-align: center; font-size: 0.8125rem; color: var(--c-text-3, #8a8a8a); }
.ig__grid:where([data-rk-32bfaf]) { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; }
.ig__tile:where([data-rk-32bfaf]) {
display: flex;
gap: 1rem;
padding: 1.25rem;
border-radius: var(--r-lg, 18px);
transition: border-color 0.2s ease, transform 0.2s ease;
}
.ig__tile[hidden]:where([data-rk-32bfaf]) { display: none; }
.ig--card:where([data-rk-32bfaf]) .ig__tile:where([data-rk-32bfaf]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.025); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07)); }
.ig--outline:where([data-rk-32bfaf]) .ig__tile:where([data-rk-32bfaf]) { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.12); }
.ig--compact:where([data-rk-32bfaf]) .ig__tile:where([data-rk-32bfaf]) { padding: 0.75rem; }
.ig--compact:where([data-rk-32bfaf]) .ig__desc:where([data-rk-32bfaf]) { display: none; }
.ig__tile:hover:where([data-rk-32bfaf]) { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.2); transform: translateY(-2px); }
.ig__logo:where([data-rk-32bfaf]) {
display: grid;
place-items: center;
flex-shrink: 0;
width: 44px;
height: 44px;
border-radius: 12px;
background: color-mix(in srgb, var(--c) 22%, transparent);
border: 1px solid color-mix(in srgb, var(--c) 45%, transparent);
color: var(--c-text-1, #fff);
font-weight: 750;
font-size: 0.9rem;
}
.ig__body:where([data-rk-32bfaf]) { min-width: 0; flex: 1; }
.ig__row:where([data-rk-32bfaf]) { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.ig__name:where([data-rk-32bfaf]) { margin: 0; font-size: 1rem; font-weight: 650; color: var(--c-text-1, #fff); }
.ig__status:where([data-rk-32bfaf]) { font-size: 0.6875rem; font-weight: 650; padding: 0.15rem 0.5rem; border-radius: var(--r-full, 999px); white-space: nowrap; }
.ig__status--available:where([data-rk-32bfaf]) { background: rgba(52, 195, 143, 0.14); color: #7ee2b8; }
.ig__status--beta:where([data-rk-32bfaf]) { background: rgba(201, 133, 0, 0.16); color: #f2c66a; }
.ig__status--soon:where([data-rk-32bfaf]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.06); color: var(--c-text-2, #a3a3a3); }
.ig__desc:where([data-rk-32bfaf]) { margin: 0.35rem 0 0.6rem; font-size: 0.875rem; line-height: 1.5; color: var(--c-text-2, #a3a3a3); }
.ig__cat:where([data-rk-32bfaf]) { font-size: 0.75rem; color: var(--c-text-3, #8a8a8a); }
@media (prefers-reduced-motion: reduce) { .ig__tile:where([data-rk-32bfaf]), .ig__chip:where([data-rk-32bfaf]) { transition: none; } }