Ask AI

@joeblew999/remy-ui

La interfaz que comparten todas las apps de Remy: shadcn de fábrica, fuentes, catálogos de Paraglide, las páginas del sitio y de la app, el pegamento de TanStack Start y sus comprobaciones.

La interfaz que comparten todas las apps de Remy: componentes y tema de shadcn de fábrica, fuentes, catálogos de Paraglide y ayudantes de locale, las páginas del sitio y de la app, el pegamento de TanStack Start y las comprobaciones de Playwright que lo demuestran. Los consumidores son remy-auth (renderizado en servidor) y remy-auth-app (prerrenderizado); ambos ejecutan las mismas comprobaciones.

shadcn, de fábrica

Todo lo que hay en src/components, src/hooks y src/styles/globals.css es lo que escribe la CLI de shadcn fijada en una versión concreta (diseño de monorepo: el components.json de la app encamina shadcn add hacia aquí), incluido cn, del propio paquete cn de shadcn; nada de eso se edita a mano. npm run ui:components y npm run ui:theme los regeneran, y npm run ui:verify falla ante cualquier desviación (ver docs/gui.md). Los bloques son copias propias en src/blocks.

Uso

Importa la hoja de estilo del paquete en el CSS de la app (aquí src/styles.css), y luego indica a Tailwind dónde viven las clases propias de la app. tailwind.css importa globals.css, fonts.css y text.css en ese orden y declara el @source propio del paquete:

@import "@joeblew999/remy-ui/tailwind.css";
@source "../src";

fonts.css nombra las fuentes de reserva (fallback) de fontaine; añade FontaineTransform.vite({ fallbacks: { 'Geist Variable': ['Arial'] } }) antes de Tailwind en la configuración de Vite (el vite.config.ts de remy-auth es el ejemplo).

Cada página es de uno de dos tipos, y nunca se mezclan (isAppPath de paths es la regla; cada app lista las suyas):

  • Páginas del sitio (las sitePaths de la app), enmarcadas por SiteShell: completas sin JavaScript, indexadas, en el sitemap, evaluadas por Lighthouse y Core Web Vitals.
  • Páginas de la app (sus appPaths, bajo /app), enmarcadas por AppShell (el bloque sidebar-16 de shadcn): necesitan JavaScript y pageHead las marca noindex. AppShell tiene su propia exportación para que una página del sitio nunca lo descargue.
  • Navegación de la app: la lista propia de la app, en su configuración defineRemyApp (app-config).

Las páginas propias de remy-auth (las páginas de inicio y de formatos, los formatos, el reloj, la demo, la ubicación, los ajustes y la cuenta de la app, sus listas de navegación, la reserva de demostración y sus comprobaciones) no son de la plataforma: son @joeblew999/remy-showcase (packages/showcase, su README), para las apps que las muestran. Las tabletas y los ordenadores obtienen la barra lateral (que se contrae a iconos); los teléfonos obtienen la barra inferior (blocks/bottom-nav, piezas de shadcn de fábrica, ya que shadcn no tiene navegación inferior) con las páginas principales y Más, que abre la barra lateral. Por qué: el plan.

Lo que tiene toda app basada en el paquete, para que las tareas y comprobaciones compartidas funcionen en ella (remy-auth-app es el ejemplo prerrenderizado):

  • Sus ajustes de marco, una sola vez, en defineRemyApp (app-config): su nombre (el del producto, escrito una sola vez), el código fuente, los enlaces de la cabecera del sitio y de la barra lateral de la app (cada uno escrito con linkOptions de TanStack, de modo que se comprueba contra las propias rutas de la app donde está escrito). El marco compartido no nombra ninguna ruta salvo /, de modo que una app con otras páginas distintas a las de remy-auth tipa correctamente tal cual.
  • Su raíz renderiza las páginas dentro de AppProviders (providers), con esa configuración y el idioma preferred del loader raíz: dirección de lectura, el tema (el interruptor de la cabecera y la página Ajustes lo necesitan), los ajustes de marco y el idioma a ofrecer, que lee cada marco, de modo que ninguna página lo pasa. Las themeChecks compartidas fallan sin él.
  • Una ruta para cada path de appPaths y sitePaths, cada una de pocas líneas sobre la página compartida (el Reloj de la demostración (showcase) propaga clockRouteOptions desde @joeblew999/remy-showcase/clock-route).
  • tests/smoke.spec.ts, una sola llamada a smokeChecks(...): npm run test -- smoke y la prueba de humo en vivo tras cada despliegue lo ejecutan, y fallan con «No tests found» si no existe.
  • .plans/now.md, la única lista ordenada del trabajo pendiente (remy plans check, en la comprobación).
  • Instalaciones y actualizaciones mediante los comandos del paquete, nunca a mano: npm run setup (instalar, skills, MCP, las comprobaciones de la instalación) y npm run upgrade -- <version> (la versión exacta del paquete y el workflow de CI en el mismo tag, y después las mismas comprobaciones). Ambos toman el token de GitHub Packages de gh auth token.

Exportaciones

Todas bajo @joeblew999/remy-ui/, como TSX y CSS para consumidores de Vite y Tailwind.

ExportQué contiene
tailwind.cssLas tres hojas de estilo de abajo en orden, más @source para las clases propias del paquete: una sola importación para una app
globals.css, fonts.css, text.cssLa hoja de estilo de shadcn tal como la escribe la CLI; las fuentes para cada idioma; cómo se corta el texto en cada idioma (guionización por lang, cortes de frase en japonés)
components/*, hooks/*, buttonComponentes y hooks de shadcn (button es también una ruta corta)
pathsisAppPath: si una ruta sin localizar es una página de app
shellEl marco del sitio: SiteShell (alias Shell), Intro, ZoneBadge, SkipLink
app-shellSolo el marco de la app: AppShell (la barra lateral, la barra inferior del teléfono), para las páginas de app propias de una app. Su pie de página tiene el sello de build (versions)
app-configdefineRemyApp y sus tipos (RemyApp, NavItem): el nombre, el código fuente y la navegación de la app para cada marco; useRemyApp, usePreferredLocale. brand es el nombre del producto, el único lugar donde se escribe: todo mensaje que lo menciona lo recibe como {product}; pageTitle(title, brand) es el título de una página tal como lo muestra la pestaña del navegador
rootremyRoot(app, { devtools }): las opciones de la ruta raíz (el documento en el idioma y dirección de la página, AppProviders, el idioma a ofrecer, las páginas de problema); RemyRouterContext, preferredLocale. El __root.tsx de una app es createRootRouteWithContext<RemyRouterContext>()(remyRoot(remyApp, { devtools: <TanStackDevtools … /> })): las devtools permanecen en el archivo de la app, donde el plugin de Vite de TanStack las elimina de los builds de producción
routerremyRouter(routeTree): el router que crea cada app (la reescritura de locale, un QueryClient por solicitud con la integración SSR de Query, precarga por intención, el nonce de CSP)
app/viteremyApp({ plugins, start, cloudflare, port, build }): el vite.config.ts completo de una app (TanStack Devtools, las partes, el sello de build, Cloudflare, Fontaine, Tailwind, Start, React; el catálogo propio de la app cuando tiene project.inlang)
providersAppProviders: dentro de lo que la raíz de una app renderiza sus páginas (dirección, tema, la configuración defineRemyApp de la app, el idioma preferred)
languageLanguageSwitcher (enlaces simples, páginas del sitio), LanguageMenu (DropdownMenu de shadcn, páginas de la app), LanguageHint
messages, runtimeMensajes y runtime compilados de Paraglide
localegetLocale, setLocale, localizeHref, localizeUrl, deLocalizeHref, cookieName de Paraglide y más, además de direction, localeName y followLocale(runtime) (un segundo catálogo, el de la app o el de un paquete, en el idioma de la plataforma)
locale-infoCalendarios, dígitos, reloj y convenciones de semana de Intl Locale Info; formatLocale (la etiqueta que usa cada formateador, que indica el calendario y los dígitos propios del idioma), weekOrder, words (Intl.Segmenter)
matchingLa estrategia custom-chinese de Paraglide (las etiquetas de chino tradicional llegan a zh-TW), matchChinese, preferredFromHeader, preferredFromNavigator
seoCanonical y alternativas hreflang; sitemapXml({ origin, paths, extra }) (las páginas del sitio de la app en cada locale, y luego las entradas propias de la app), robotsTxt(origin), sitemapType, robotsType para las dos rutas de servidor de la app
prerenderprerenderPages({ notFoundPath, paths }): el prerender.pages de TanStack Start para una app prerrenderizada (cada página sin locale y por locale, robots.txt, sitemap.xml, el 404.html de cada locale)
tanstacklocalizedWorker (punto de entrada del Worker: observabilidad, el middleware de Paraglide y redirecciones de entrada alrededor de TanStack Start), localeRewrite, pageHead, suggestedLocale, suggestedLocaleInBrowser
clientuseSuggestedLocale, DeviceTime para apps prerrenderizadas
workerwithObservability y los ayudantes de ID de solicitud; su /healthz indica qué despliegue respondió (el Deployment de build)
build, build/viteQué versión está desplegada. build/vite: remyBuild({ packages }), el plugin de Vite detrás de virtual:remy-build (ya presente en remyApp() y remyDocs()), y buildStamp(). build: las formas de Zod stamp y deployment con sus tipos Build y Deployment, askDeployment(origin), deploymentQuery(origin) (opciones de TanStack Query: consultado en el navegador, de nuevo cada cinco minutos), sameBuild, shortCommit
versionsBuildStamp (una línea discreta: el entorno cuando no es producción, el nombre del producto, el commit, y un control de recarga cuando el despliegue ha avanzado bajo la página) y Versions (esta app, su Worker de docs y los deployments que la app lista en defineRemyApp, cada uno respondiendo por sí mismo, y los paquetes de este build). Piezas de shadcn de fábrica; coloca cualquiera de las dos en cualquier lugar dentro de AppProviders
cloudflareplaceFromCloudflare
problemLas páginas de problema localizadas: Problem, NotFound, ErrorPage, problemPages (una línea de propagación por cada ruta de página)
parts, parts/vite, parts/checksPartes (plan): una app las lista en src/parts.json, una por línea: las de la plataforma por nombre, las de otro paquete como <package>/<name>. remyParts() en vite.config.ts (su plugin entre los plugins, sus routes como tanstackStart({ router: { virtualRouteConfig } })) monta las rutas de cada parte listada junto a src/routes y genera virtual:remy-parts (parts, hasPart); partChecks() en el archivo de pruebas ejecuta las comprobaciones de cada parte listada. La parte de la plataforma es seo-routes (robots.txt y el sitemap); la demostración (showcase) ofrece time-zones, deferred-place y status-card (@joeblew999/remy-showcase/<name>); ver Escribir una parte
rows, zod-cspGroup, Row, NameList: filas de etiqueta/valor; el interruptor jitless de Zod (lo importa AppProviders)
invalidateinvalidateEverything(router, queryClient): todos los loaders y consultas marcados como obsoletos y recargados (tras cerrar sesión o un cambio de rol; la actualización de la tarjeta de estado)
samplesLos valores fijos que renderizan las páginas
checks, problem.checks, build-boundaries.checks, code-splitting.checksComprobaciones de Playwright compartidas: páginas públicas, URLs de entrada (con la estrategia china), texto (textChecks: 320 px, guionización, capitalización por idioma), fuentes (fontChecks: la fuente que dibuja cada idioma es la que fonts.css nombra para su escritura), zonas, observabilidad, Lighthouse y Core Web Vitals, las páginas de problema, lo que descarga el navegador (buildBoundaryChecks) y la división de código (codeSplittingChecks)
app-checksUna llamada por tipo de app para el conjunto de comprobaciones compartidas sobre las páginas propias de la app: serverAppChecks({ service, sitePaths, appPaths, home, oneLanguage, cspEnforced }) (renderizada en servidor: URLs de entrada con redirección, CSP, fuentes) y prerenderedAppChecks({ service, sitePaths, appPaths, home }) (páginas de entrada estáticas), ambas con el sello de build y el nombre del producto (versions.checks); la app solo añade comprobaciones para lo que ella añade. Una app que muestra las páginas de demostración (showcase) también llama a showcaseChecks({ product }) (@joeblew999/remy-showcase/showcase.checks; product es su brand); problemChecks es problem.checks
playwrightplaywrightConfig(), la configuración compartida de Playwright
api/serverapiHandlers (el OpenAPIHandler de oRPC como los handlers de una ruta de servidor de Start, con la página de referencia en /api/doc y el documento generado en /api/openapi.json; errorStatuses, el estado HTTP de cada uno de los códigos de error propios de la API, ya que oRPC mantiene los estados fuera de los errores; origins, los orígenes exactos de las apps registradas que permite el CORSHandlerPlugin de oRPC, ninguno por defecto; una respuesta a una solicitud con credenciales, y todo 401, es no-store), generateSpec(router, info, errorStatuses) (el documento OpenAPI 3.1), ApiInfo, ErrorStatuses
api/guardEl guard para la versión de oRPC instalada, que importa el router de una app: guard() (el middleware raíz del router: implement(contract).$context<...>().use(guard<User>())), GuardContext (el getSession() del contexto: la sesión de quien llama o null, que se pide solo cuando una política lo necesita, tal como lo comparte la guía de Better Auth de oRPC; y relations, el motor de relaciones de la app, para los procedimientos cuya política es una acción), once (esa búsqueda, ejecutada como máximo una vez por llamada), signedIn(context) (la persona para la que se ejecuta un procedimiento session o de acción), noSession (el contexto de una app que no da sesión a nadie); y todo lo de api/guard-core y api/policy. JavaScript plano, porque las comprobaciones de una app cargan su router en Node
api/guard-coreEl propio guard, sin ninguna importación de oRPC, de modo que funciona tanto en oRPC 1 como en 2: Policy (public: cualquiera, y la respuesta no nombra a ninguna persona; session: una persona con sesión iniciada, y la respuesta es la suya propia; { action }: una persona con sesión iniciada que posee una relación a la que el vocabulario de la app concede esa acción, sobre el objeto que nombra el id de la entrada), guardMiddleware({ ORPCError }) (impone la meta.policy de cada procedimiento; para una acción pregunta al motor de relaciones, responde 404 para un objeto inexistente antes que 403, y un procedimiento que no declara ninguna política nunca se ejecuta), guardProblems(router, { personFields, vocabulary }) (qué está mal, una frase por cada problema: sin política, sin guard por delante, una respuesta pública que nombra a una persona, una respuesta personal sin meta.personal que diga quién la recibe, una acción que el vocabulario no define o cuya entrada no puede nombrar su objeto), policyOf, actionOf, isGuarded, walk, personFields
api/policyLo que un procedimiento del contrato declara para el guard, como metadatos de oRPC: policy('public' | 'session'), personal('<quién la recibe>') y actions(vocabulary), que da may('EDIT_NOTE') exactamente para las acciones que define el vocabulario de la app, como en oc.meta(may('EDIT_NOTE'))
api/relationsEl motor de relaciones: quién puede hacer qué se responde mediante relaciones, a partir de las tablas propias de la app en su propia D1. defineVocabulary({ objectTypes, relations, actions, grants }) (el vocabulario de la app como datos, con las formas de fila de remy-sport: una relación se deriva de una fila de tabla, a través de un padre, de un rol de la plataforma, o la tiene todo el mundo), relationEngine(vocabulary, db) (can, canAll, canFor para los mapas de permisos de una lista entera, holds, heldAmong, objectsHeldBy, usersHolding, audienceFor, objectExists), vocabularyProblems y schemaProblems (el vocabulario contrastado consigo mismo y con el esquema que construyen sus migraciones), y los tipos Can, ActionCode, ActionOn, que llevan los nombres de acción de una app a su contrato y a sus páginas
api/clientcontractClient(contract, { origin }) (un cliente tipado para cualquier contrato: OpenAPILink con ResponseValidationLinkPlugin, el idioma de la página como Accept-Language; sin origin, el propio de la página), isomorphicClient (el cliente propio de una app: el router en el servidor, contractClient en el navegador, mediante createIsomorphicFn)
api/coveragecoverageProblems(router, { errorStatuses }) (cada procedimiento tiene una ruta bajo /api/, una política, una salida y errores documentados, cada uno con un mensaje y un estado HTTP), procedures, routeOf (los metadatos openapi() de un procedimiento), statusesOf
api/checksapiChecks({ router, title, origins, errorStatuses, personFields, vocabulary }) (cobertura, el documento servido y la página de referencia, CORS exactamente para los origins registrados, y el guard: guardProblems está vacío, y todo procedimiento que necesita una sesión responde 401 a un desconocido, sin caché)
versions.checks, build.checksbuildChecks({ service }) (/healthz indica el entorno y el build; el sello de una ejecución local es el commit de este checkout), buildStampChecks({ path }) (el sello del marco, y el control de recarga solo cuando el despliegue responde con otro build), productNameChecks({ paths }) (todo título termina con el nombre de la app; una app con otro nombre nunca muestra «Remy»). Los conjuntos de comprobación por app ejecutan los tres
allowed, allowed.checks<Allowed can={row.can} action="EDIT_NOTE">: muestra sus hijos solo cuando el servidor permitió esa acción para este visitante sobre este objeto (can es el mapa que el servidor envió junto con la fila; solo tipa una acción que la fila lleva), de modo que una página nunca calcula los permisos por sí misma. offeredActions(locator) y allowedActions(can) permiten que la comprobación de una app compare lo que una página ofrece con lo que permite el servidor
environmentenvironments({ production: {...}, local: {...} }): la tabla de una app de lo que permite cada entorno, una fila por capacidad; permits(env, 'capability'), policyFor, environmentOf. El entorno se declara (ENVIRONMENT), y todo lo ausente o desconocido es producción
mailmailerFor({ capture, binding, from }): correo mediante Cloudflare Email Service (el binding send_email del Worker), o guardado en el buzón de salida del Worker donde el entorno lo captura (readOutbox, clearOutbox); un rechazo dice para quién era el correo y por qué; un mensaje a una dirección a la que no puede llegar ningún correo (unreachable: los dominios reservados de prueba y ejemplo) nunca se envía

Las dependencias del paquete son el único conjunto de dependencias de la plataforma, fijado con precisión: el framework (React, TanStack Start, Router y Query), la cadena de herramientas (Vite, Wrangler, Tailwind, TypeScript) y las herramientas que ejecutan las tareas y comprobaciones compartidas (Playwright, Lighthouse, el servidor MCP de DevTools, npm-check-updates). Una app solo nombra el paquete; remy.singleCopy en su package.json lista las que se rompen si se instalan dos veces, lo que mantiene la parte de copias únicas de la comprobación (la línea de comandos).

Idioma

El comportamiento de idioma es el de Paraglide: estrategias url, cookie, custom-chinese (el propio hook de estrategia personalizada de Paraglide, en matching.js), preferredLanguage, baseLocale, con cada locale prefijado en la URL, configurado una sola vez en paraglide.mjs, que compila messages/*.json durante la generación de tipos y el build de Vite. Pasa { locale } explícitamente en cada llamada de mensaje; no existe un locale global para todo el proceso. Una app renderizada en el servidor ejecuta el middleware y propaga la preferencia del visitante; una app prerrenderizada la resuelve en el navegador tras la hidratación; ambas renderizan los mismos componentes. Los idiomas son los locales de project.inlang/settings.json; todos los catálogos salvo inglés y español (árabe, persa, hebreo, tailandés, japonés, chino tradicional, hindi, amárico, polaco, turco y alemán) están escritos por un agente y sin revisar.

Publicación

El paquete es @joeblew999/remy-ui en GitHub Packages (ahí el scope debe coincidir con el propietario de GitHub). npm run dev:release (alias release) hace toda la publicación desde esta máquina y se detiene en el primer fallo: traducciones completas y al día (remy i18n check, estricto), todas nuestras comprobaciones (remy verify), los archivos de shadcn exactamente como los escribe su CLI (npm run ui:verify), las auditorías Lighthouse de Google y Core Web Vitals medidos en frío sobre un Worker de Cloudflare desechable (npm run test -- cwv: las primeras visitas tras un despliegue, que es donde una página lenta sale más cara). Después etiqueta vX.Y.Z a partir de packages/ui/package.json, hace push, publica con tu token de gh y crea el release de GitHub a partir de la sección correspondiente del CHANGELOG. CI (.github/workflows/google.yml) repite el nivel 2 sobre el tag.

Antes de publicar, un solo commit: la versión en packages/ui/package.json (y en packages/contract/package.json cuando el contrato cambió) y la sección del CHANGELOG. Ningún otro archivo nombra una versión: los workspaces dependen unos de otros con "*" y el rango de peer del contrato es abierto. La tarea rechaza un árbol sucio, una rama distinta de main, o un tag ya existente.

Los consumidores añaden esto a su .npmrc:

@joeblew999:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

e instalan mediante npm run setup, que toma un token con read:packages de gh auth token (o GITHUB_TOKEN en el shell). Las versiones publicadas están en el registro de cambios.

Escribir una parte

Una parte es una funcionalidad que una app activa o desactiva con una línea en su src/parts.json. Las partes de la plataforma se listan por nombre ("seo-routes"); cualquier paquete puede ofrecer partes también, listadas como "<package>/<name>" ("@joeblew999/remy-showcase/time-zones"), y funcionan de la misma manera.

  1. Carpeta: src/parts/<name>/ en el paquete, con cualquiera de estos:
    • routes/: archivos de ruta de TanStack, montados junto a las src/routes propias de la app cuando la parte está listada;
    • módulos de entrada (p. ej. ui.tsx, place.ts) que la app importa como virtual:remy-parts/<name>/<entry>: las exportaciones reales cuando está listada, undefined cuando no, de modo que la app escribe {Card && <Card />} o getPlace?.() y no se envía nada cuando la parte está desactivada. Un módulo por entrada mantiene la división de código;
    • checks.js, cuya exportación por defecto (options, listed) => void ejecuta sus comprobaciones de Playwright: partChecks() la ejecuta solo cuando la parte está listada, con las options[<name>] propias de la app.
  2. Catálogo: las de la plataforma son catalog en src/parts/list.js; las de otro paquete son su src/parts/catalog.json, exportado como <package>/parts/catalog.json, con la misma forma: routes, requires (partes que también deben estar listadas), entries (entrada → nombres de exportación), app (opciones que la app aporta desde su propio src/parts/<name>.ts, leído como virtual:remy-parts/<name>/app) y sitePaths (páginas del sitio que añade, para el sitemap y sus comprobaciones).
  3. Tipos: declara sus módulos virtuales en el src/parts/virtual.d.ts propio del paquete (el de otro paquete referencia el de la plataforma con /// <reference types="@joeblew999/remy-ui/parts/virtual" />). Una parte solo enlaza con sus propias rutas, o con las páginas de la app mediante sus opciones app (la miga de pan de la parte time-zones toma parent), nunca con una página que otra app pueda no tener: el fixture consumidor lista una parte de la demostración (showcase) y la tipa sin las páginas de la demostración (showcase).
  4. Demuéstralo: npm run dev:change pasa con la parte listada, sin ella y con la lista vacía (ejecuta un build una vez tras editar src/parts.json, lo que regenera el árbol de rutas).

Lo que en cambio sigue siendo un módulo normal del paquete: el código que toda app necesita (observabilidad), un hook que una app debe llamar siempre (leave-guard) o los controles propios de una página compartida (search-params).

On this page