1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Características & Bento

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

  • Slack

    Available

    Post alerts and weekly digests to channels.

    Messaging
  • GitHub

    Available

    Annotate charts with deploys and releases.

    Developer
  • Stripe

    Available

    Sync MRR, churn and invoices automatically.

    Payments
  • Linear

    Beta

    Turn insights into issues in one click.

    Developer
  • Notion

    Available

    Embed live charts in your team wiki.

    Docs
  • HubSpot

    Beta

    Attribute revenue to campaigns and deals.

    CRM
  • Postgres

    Available

    Query your warehouse with zero ETL.

    Data
  • Discord

    Coming soon

    Community 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; } }