@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-domNecesita @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:
- La píldora «N mensajes nuevos» nunca se enciende. El estado, la píldora y el
onScrollque la apagaría existen enGtTicketThread, pero ninguna ruta del código la incrementa por encima de 0. Es un camino muerto, portado tal cual desdegt-ticket-thread.ts. - 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 degt-portal.tsdescriben nunca llegó a la plantilla degrowtickets-ng. Se documenta la limitación, no se inventa la pieza que falta.
Requisitos
- React
>=17.0.2(yreact-domde 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.
