npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@growconnect/growtickets-next-react

v0.3.0

Published

El portal de GrowTickets para React/Next.js: provider, cliente al proxy del propio producto, hooks, Markdown y la UI completa del portal del reportante (lista, hilo, alta, tiempo real, avisos).

Readme

@growconnect/growtickets-next-react

El portal de GrowTickets para React/Next.js: la interfaz COMPLETA que ve el reportante —lista de conversaciones, hilo de un ticket, alta por pasos, compositor con Markdown, tiempo real y avisos de escritorio—, montada con un solo componente sobre un <GrowTicketsProvider> que habla con el PROXY del propio producto (nunca con GrowTickets directamente). Puerto 1:1 de @growconnect/growtickets-ng a React: mismas reglas de producto, mismo protocolo de tiempo real, mismo intérprete de Markdown — sólo cambia el mecanismo del framework.

Instalación

npm install @growconnect/growtickets-next-react react react-dom

Necesita @growconnect/growtickets-next-server ya montado detrás de una ruta (/api/soporte, por ejemplo) — ver el README de -server. react-dom es obligatorio desde esta fase: el portal usa createPortal para pintar dentro de sus propios bordes de Shadow DOM.

Montaje mínimo

import { GrowTicketsProvider, GtPortal } from '@growconnect/growtickets-next-react';

export function SoportePage() {
  return (
    <GrowTicketsProvider baseUrl="/api/soporte" realtime>
      <GtPortal />
    </GrowTicketsProvider>
  );
}

baseUrl es la ruta del proxy en ESTE producto — nunca la URL de GrowTickets, que sólo -server conoce. realtime enciende el canal en vivo (WebSocket) para que la lista y el hilo se actualicen solos; sin él, el portal sigue funcionando a base de recargar tras cada acción. headers? es la sesión DEL PRODUCTO (un token en memoria, por ejemplo) y jamás la clave de API de GrowTickets: ésa no sale del servidor.

Con qué versión escribes (0.3.0)

Desde la 0.3.0 el portal manda, en cada petición a tu proxy, la cabecera X-GrowTickets-Client: qué librería es y su versión, que es web, el navegador (user_agent) y —si la pasas con app— la versión de tu app: <GrowTicketsProvider baseUrl="/api/soporte" app={{ version: APP_VERSION }}>. En web no hay forma de saberla sola. Tu proxy (@growconnect/growtickets-next-server o el paquete de Composer, 0.4.0 o superior) la reenvía a la mesa de ayuda, y allí quien atiende la ve en la ficha del ticket y en el catálogo de proyectos.

Es información para orientar, nunca autorización, y no hace falta configurar nada: con un proxy anterior la cabecera simplemente no se reenvía y el portal funciona igual.

GtPortal es un punto de montaje independiente: le basta el <GrowTicketsProvider> alrededor, mide su propio ancho y decide sola si se pinta en un panel o en dos.

Los componentes

Ninguno necesita PrimeNG, Angular Material ni ningún tema ajeno — esta librería viaja a productos de terceros y no puede arrastrarles una dependencia de interfaz. Todos aceptan className/style, que es lo único que el producto anfitrión puede tocar (ver «Aislamiento», abajo); el resto de props usa el vocabulario del producto, en castellano.

| Componente | Para | |---|---| | GtPortal | El orquestador: la bandeja del reportante entera —lista, hilo y alta— en un solo punto de montaje. Corta a un panel o a dos según SU PROPIO ancho, nunca el de la ventana. | | GtTicketThread | La conversación de un ticket: cabecera fija, hilo que se desplaza, compositor pegado abajo. Se puede montar suelto, sin GtPortal alrededor. | | GtNewTicket | El formulario de alta, por pasos: primero qué falla (categoría), después el resto. La prioridad se pregunta en el idioma de quien reporta, no en el de la bandeja del agente. | | GtConversationItem | Una fila de la lista de conversaciones, con su estado, su fecha resumida y el icono del proyecto. | | GtMessage | Una burbuja del hilo: adjuntos, cita, tandas del mismo autor, Markdown ya interpretado. | | GtRichEditor | El compositor: se escribe con formato Y se ve con formato —las marcas (**, *) se quedan a la vista, apagadas—. | | GtRichText | Interpreta el mismo subconjunto de Markdown para pintar un mensaje ya enviado. | | GtPicker | Un combobox de un nivel: se escribe para filtrar, sin abrir nada antes. | | GtIcon | Un icono incrustado (trazados de lucide, sin arrastrar lucide-react como dependencia). | | GtMark | El símbolo de la marca, dibujándose una vez al montar. |

Cada uno abre su PROPIO borde de Shadow DOM si nadie de la librería lo envuelve ya —así que un GtMessage o un GtIcon sueltos siguen funcionando aislados—, y se pinta como DOM normal dentro del borde de un ancestro cuando lo hay. GtPortal es, casi siempre, ese único ancestro.

Aislamiento: Shadow DOM real, no clases con ámbito

El CSS del producto anfitrión no puede alcanzar nada de dentro, ni con !important, porque no es una convención de nombres ni una hoja con especificidad alta: es un ShadowRoot de verdad (attachShadow({ mode: 'open' })), la misma barrera que usa cualquier <video> o <input type="range"> del navegador. Lo único que un anfitrión puede tocar es className/style en el punto de montaje, y aterrizan en el elemento que ENVUELVE el borde —nunca dentro de él—: un <div class="mi-clase"> alrededor, jamás una grieta hacia la burbuja de un mensaje o el botón de un icono.

Por punto de montaje independiente se abre un solo borde, no uno por componente: un hilo de 40 mensajes con sus iconos y su texto enriquecido comparten la única raíz que abrió GtTicketThread (o GtPortal), en vez de sumar cerca de 200 raíces de sombra por pantalla. Todas las raíces de una misma página adoptan además la MISMA hoja de estilos ya parseada (adoptedStyleSheets), así que montar varias piezas sueltas no repite el parseo del CSS por cada una.

La tipografía se carga sola

--gt-font pide Google Sans. Un componente en Shadow DOM no puede confiar en que el anfitrión ya la tenga cargada —la mayoría de productos externos no la traen—, así que la librería inserta su propio <link> al montarse: no hace falta ninguna acción del lado del producto, y se inserta una sola vez sin importar cuántos componentes de la librería convivan en la página.

Tiempo real y avisos

import { useRealtime, useAvisos } from '@growconnect/growtickets-next-react';

function EstadoDeConexion() {
  const { estado } = useRealtime(); // 'apagado' | 'conectando' | 'conectado' | 'caido'
  const { soportado, escritorio, sonido, alternarEscritorio, alternarSonido } = useAvisos();

  return (
    <button onClick={() => alternarEscritorio(!escritorio)} disabled={!soportado}>
      {escritorio ? 'Avisos de escritorio activados' : 'Activar avisos de escritorio'}
    </button>
  );
}

useRealtime() sólo tiene sentido con realtime encendido en el provider; con él apagado, estado se queda en 'apagado' y nada intenta conectar. Los avisos son de OPT-IN y sólo se pueden pedir desde un gesto del usuario —un onClick, nunca un useEffect al montar—: es una regla del propio navegador, que ignora en silencio cualquier Notification.requestPermission() que no venga de un clic o una tecla. El sonido es sintetizado (Web Audio), así que no hay ningún archivo de audio que empaquetar ni que cargar por red.

Subir un archivo

import { useGrowTickets, subirArchivo } from '@growconnect/growtickets-next-react';

async function subir(archivo: File) {
  const { client } = useGrowTickets();
  const { uploadUrl, path } = await client.firmarSubida(archivo.name.split('.').pop() ?? '', archivo.type);

  await subirArchivo(uploadUrl, archivo, archivo.type, (avance) => console.log(avance.porcentaje));

  return path; // Lo que se manda al abrir el ticket o al responder.
}

subirArchivo sube DIRECTO al bucket con la URL firmada y nunca aplica las headers? del provider: una URL firmada ya lleva su propia autenticación en la query, y una cabecera adicional hace que S3/Spaces rechacen la subida con InvalidArgument: Only one auth mechanism allowed.

Hooks, para quien construye su propia interfaz

GtPortal cubre el caso de uso normal, pero los hooks de abajo siguen públicos para quien prefiera componer su propia pantalla con GtMessage, GtConversationItem, etc. sueltos.

| Hook | Para | |---|---| | useConfiguracion() | Colores, estados, categorías y el canal de tiempo real del proyecto. | | useTickets(filtros?) | El listado paginado. | | useTicket(radicado) | Un ticket con su hilo completo. | | useAbrirTicket() | { state, ejecutar } — abre un ticket nuevo. | | useResponder(radicado) | { state, ejecutar } — responde (o deja una nota interna) sobre un radicado fijo. | | useUrlFirmada(path, descargar?) | La URL firmada de un adjunto, cacheada 12 minutos. | | useRealtime() | { estado, ultimo } — el canal en vivo del proyecto. | | useAvisos() | Permiso, preferencias y disparo de avisos de escritorio + sonido. |

Las consultas (useConfiguracion, useTickets, useTicket) devuelven { status, data, error, recargar } con status en 'loading' | 'success' | 'error'. Las mutaciones (useAbrirTicket, useResponder) devuelven { state, ejecutar }, con state.status en 'idle' | 'loading' | 'success' | 'error'.

Marca y Markdown

import { useBrandStyle, GtRichText } from '@growconnect/growtickets-next-react';

function Ticket({ tema, mensaje }: { tema: BrandTheme | null; mensaje: string }) {
  const style = useBrandStyle(tema);

  return (
    <div style={style}>
      <GtRichText text={mensaje} className="prose" />
    </div>
  );
}

GtRichText interpreta el mismo subconjunto de Markdown que el resto del ecosistema (**negrita**, *cursiva*, ~~tachado~~, código, enlaces y listas) — nunca HTML crudo, tablas ni imágenes: lo que entra lo escribe un tercero. GtRichEditor usa el mismo intérprete para pintar, mientras se escribe, exactamente lo que se está tecleando.

Dos límites heredados de growtickets-ng, tal cual

Portados por fidelidad al origen Angular, no arreglados aquí — arreglarlos es una decisión de producto aparte de este port:

  1. La píldora «N mensajes nuevos» nunca se enciende. El estado, la píldora y el onScroll que la apagaría existen en GtTicketThread, pero ninguna ruta del código la incrementa por encima de 0. Es un camino muerto, portado tal cual desde gt-ticket-thread.ts.
  2. El buscador del historial que describen algunos docblocks no existe. La constante que gobierna el «apretado» de las tarjetas a partir de la sexta conversación (HISTORIAL_LARGO) es real; el cuadro de búsqueda que los comentarios de gt-portal.ts describen nunca llegó a la plantilla de growtickets-ng. Se documenta la limitación, no se inventa la pieza que falta.

Requisitos

  • React >=17.0.2 (y react-dom de la misma versión, ver «Instalación»).
  • Un navegador de verdad. Los componentes de esta librería no pintan nada útil en el servidor: el Shadow DOM sólo se abre en el cliente, así que el SSR de Next entrega el elemento anfitrión vacío y el contenido aparece en la hidratación. No hay contenido que indexar en el HTML servido por esta librería.

Qué falta de este README y por qué

La tabla de fugas de secretos (§6 del plan) y los ejemplos de resolveReporter para NextAuth v4 y Auth.js v5 viven en el README de -server, que es donde de verdad se decide la sesión: este paquete nunca ve la clave de API ni valida quién reporta.