Frontends
Dos maneras de poner una UI delante de un backend JFast. Elige según si quieres un paso de build o no.
--kind web |
--kind spa |
|
|---|---|---|
| Renderiza | Jinja2 + HTMX del lado del servidor | Vue 3 o React, del lado del cliente |
| Paso de build | ninguno | Vite |
| Vive | dentro del servicio backend | en su propio servicio |
| Bueno para | paneles de administración, herramientas internas, CRUD | estado de cliente rico, offline, UX tipo móvil |
--kind web está cubierto en modules.md. Esta página trata de
--kind spa.
Crear uno
jfast new service admin --kind spa --frontend vue
cd admin
npm install
npm run dev
React en su lugar:
jfast new service portal --kind spa --frontend react
Angular no se genera. Ver "Angular" al final.
Elige un look: --template
Un frontend se genera en uno de dos looks. La elección solo cambia los
archivos que dibujan: la hoja de estilos, los componentes base, el layout, las
dos pantallas incluidas y la página que escribe jfast new view. Router,
stores, la instancia de axios, el manejo del 401 y los marcadores del
generador son un solo conjunto compartido, así que todo lo demás de esta
página vale para los dos.
--template |
Cómo se ve |
|---|---|
nexora (por defecto) |
Vidrio líquido, del sistema de diseño Nexora: paneles esmerilados sobre un estudio negro (y uno claro), barra lateral de vidrio, isla superior flotante, Plus Jakarta Sans con JetBrains Mono para las cifras, y un listón líquido en WebGL detrás de todo. La página de inicio es un pequeño dashboard — tarjetas KPI, una tabla de vistas registradas, una columna de siguientes pasos — hecho solo con valores reales: /health, su tiempo de ida y vuelta, y la barra lateral |
classic |
Paneles de Tailwind v4 sobre una superficie neutra con un solo acento carmesí. Sin fuentes web, sin WebGL. Exactamente como se veía todo frontend antes de que existieran los looks |
jfast new service admin --kind spa --frontend vue # nexora
jfast new service admin --kind spa --frontend react --template classic
jfast start shop --template classic # el stack por defecto
jfast init # pregunta "Which look?"
Un valor desconocido se rechaza antes de escribir nada, nombrando los dos que
existen. --template en un servicio que no es --kind spa también se
rechaza: solo un frontend tiene look.
Las vistas siguen al look
La elección se registra una vez, en el sello .jfast-template del proyecto
(frontend_template). jfast new view lo lee, así que una página generada un
año después se dibuja como las pantallas que la rodean. --template en
jfast new view se impone al sello; un proyecto sin sello — hecho antes de
los sellos, o no por jfast — recibe la página classic y una nota que lo dice,
porque una página nexora nombraría clases que ese proyecto no tiene.
Lo que agrega Nexora
| Ruta | Contiene |
|---|---|
src/nexora/nexora.css |
El sistema de diseño: la paleta, los tokens claro y oscuro, y las clases card-panel, btn-modern, badge-status, kpi-card, erp-table, nx-*. Se importa en la capa components de Tailwind, así que una utilidad junto a una de sus clases sigue ganando |
src/nexora/accent.js |
El acento: seis presets, la paleta entera derivada de un color, y recordar lo que eligió quien mira |
src/nexora/brand.js |
El nombre del producto en el logotipo |
src/nexora/background.js |
Si el listón corre o no |
src/nexora/backdrop.js |
El fondo que eligió quien mira: 3D, 2D o nada |
src/nexora/sidebar.js |
Si el sidebar está plegado en pantalla ancha |
src/nexora/liquid.js |
El listón, sobre three.js |
src/components/AccentPicker.*, LiquidBackground.* |
Los dos componentes que el look classic no tiene |
El listón no cuesta nada cuando no puede aportar. three es una
dependencia de npm, nunca un script de CDN, y va en un chunk propio (unos
130 kB con gzip) que se descarga después del primer pintado. Con
prefers-reduced-motion nunca se carga, y activar esa preferencia lo detiene.
Sin WebGL 2 queda en pantalla el cuadro fijo de CSS que siempre está debajo.
Una pestaña oculta no dibuja nada. Se monta una sola vez en la raíz de la app,
no en el layout, así que navegar no reconstruye la escena.
El nombre es VITE_APP_NAME, tal cual está escrito, o jfastframework
cuando está vacío. Se cambia en .env para npm run dev y en
.env.production para npm run build. jfast workspace env reescribe
.env, así que hay que volver a ponerlo ahí después de correrlo.
El acento es un solo color, y de él se derivan el brillo, el tono profundo, el texto de los botones y dos tonos seguros para texto — movidos justo lo necesario para leerse a 4.5:1 en cada tema, así que un acento ámbar o pizarra no produce etiquetas ilegibles. Tres lugares lo fijan, gana el primero:
- quien mira, con la muestra junto al botón de tema: Ruby, Blue, Emerald,
Violet, Amber, Slate o cualquier color propio, guardado en ese navegador
bajo
<service>:accenty reaplicado antes del primer pintado porindex.html; VITE_ACCENT="#3B82F6"en.env/.env.production— entre comillas, porque un#sin comillas empieza un comentario en esos archivos;- los tripletes
--c-*al inicio desrc/nexora/nexora.css.
Botones, brillos, badges, toda utilidad brand-* y el listón lo siguen.
El fondo se elige en el mismo popover que el acento: 3D es el listón,
2D el cuadro fijo que tiene debajo -- y three.js nunca se descarga -- y
Off el color liso de la página. Cambiar de 3D a otro destruye la escena que
corre; regresar la carga. VITE_BACKGROUND=liquid|still|none en .env /
.env.production es el default del proyecto, liquid si está vacío; la
elección de quien mira, guardada bajo <service>:backdrop, gana sobre él. Los
dos los aplica index.html antes del primer pintado, así que "Off" no muestra
un instante el cuadro fijo al recargar.
El botón de menú abre el drawer debajo de 960px y, arriba, pliega el
sidebar para que la página use todo el ancho. Plegado se queda plegado entre
páginas y recargas (<service>:sidebar), y un sidebar plegado queda fuera del
foco del teclado, no solo fuera de la pantalla.
JFast Suite, como referencia
Con --agent-docs, un frontend nexora también recibe la skill
nexora-reference: una copia de JFast Suite -- las páginas estáticas de las que
salió el look: una landing, una consola CRM, una pasarela de pagos, un feed
social, tablas de operación, una galería de widgets -- en
.jfast/skills/nexora-reference/suite/. Sirve esa carpeta y ábrela antes de
construir una pantalla que el proyecto todavía no tiene. La skill dice cómo
traer un patrón: reusar un componente incluido si alguno sirve, mover el CSS a
src/nexora/nexora.css con los tokens del proyecto, y hacerlo componente. La
suite nunca se enlaza desde la app, y sus imágenes (unos 400 kB en WebP) son
arte de demostración.
Pedir otro look
El look incluido es un punto de partida. Para el otro, genera con
--template; para un look que jfast no incluye, reestiliza los tokens y los
componentes de src/components/, conservando sus props, porque las páginas
generadas los usan. Un agente que trabaje en el proyecto recibe la misma
instrucción: la skill de diseño que escribe --agent-docs describe el look
con el que se generó el proyecto, y abre diciendo que el look que pide el
usuario gana sobre él.
La URL de la API ya está bien
.env se escribe a partir de jfast.workspace.toml:
VITE_API_URL=http://localhost:8030
Eso apunta al gateway cuando el workspace tiene uno, y al único backend cuando
no. La home page generada llama a /health a través de él en la primera carga
y muestra el resultado, así que un valor equivocado aparece de inmediato en vez
de en tu primera feature real.
Después de agregar un backend (o el día que aparezca un gateway):
jfast workspace env
Agregar un módulo
Esta es la parte modelada sobre el generador que ya tenías:
jfast new view Facturas
src/ModuloFacturas/
├── Components/
│ ├── Modals/
│ └── Tables/
├── Pages/FacturasView.vue
├── Routes/router.js
└── Services/facturas.service.js
y después registra el módulo en dos lugares:
// src/router/index.js
import { ModuloFacturas } from '@/ModuloFacturas/Routes/router.js'
...
...ModuloFacturas,
/*nuevaRuta*/
// src/menuAside.js
import { mdiHomeOutline, mdiViewDashboardOutline } from '@mdi/js'
...
{
to: '/facturas',
icon: mdiViewDashboardOutline,
label: 'Facturas',
},
/*nuevoModulo*/
El framework se detecta desde el proyecto, así que no repites
--frontend react dentro de un proyecto React. El look también: la página se
dibuja en nexora o classic según el .jfast-template del proyecto (ver
Las vistas siguen al look).
Los marcadores
Conserva /*nuevaRuta*/ y /*nuevoModulo*/. Tres propiedades están
garantizadas, y cada una es un modo de falla que de otro modo es silencioso:
| Propiedad | Sin ella |
|---|---|
| Idempotente — primero se revisa una cadena de guarda | Dos rutas y dos entradas de sidebar por cada re-ejecución |
| Ruidoso — un archivo o marcador faltante lanza error con la ruta | Una página en blanco y ninguna explicación |
| Preserva el marcador — el marcador se vuelve a escribir después del bloque | El segundo módulo no tiene a dónde ir |
Un marcador reformateado (/* nuevaRuta */) igual coincide: hacer el match
estricto convertiría una corrida de prettier en un no-op silencioso.
Nombres
Facturas → ModuloFacturas, /facturas, facturas.service.js.
BillingAccount → ModuloBillingAccount, /billing-account, y la etiqueta
del sidebar dice Billing Account.
Componentes base
Ambos frontends traen el mismo set, así que los dos son el mismo producto en vez de dos proyectos que casualmente comparten un backend.
| Componente | Qué resuelve por ti |
|---|---|
BaseButton |
primary / outline / ghost / danger, dos tamaños, y un estado de carga que además lo deshabilita |
BaseInput |
Label, texto de error, hint, disabled — está pensado para que sea imposible mandar a producción un input pelado sin label |
BaseModal |
Blur del fondo, Escape y clic en el fondo, foco atrapado mientras está abierto y devuelto al disparador al cerrar |
BaseBadge |
Colores de estado, con la palabra siempre visible |
SkeletonLoader |
La forma del contenido que viene en camino |
EmptyState |
Un encabezado, una frase, y la acción que crea el primero |
ToastHost |
Renderiza la cola de toasts; se monta una vez en la raíz de la app |
Un botón destructivo va outlined, nunca sólido. El rojo sólido ya está tomado por "continuar", y una pantalla donde el acento significa dos cosas opuestas no tiene acento.
El drawer móvil bloquea el scroll del body, igual que BaseModal y por la
misma razón: cubre la página, así que un swipe destinado al menú si no scrollea
el artículo de abajo y se lleva el menú con él. Restaura el valor previo en vez
de limpiarlo, así que un modal que ya tenía el lock lo conserva, y se cierra
solo pasado md — donde el sidebar es estático, no hay nada que cerrar y el
lock sería nada más una página que no scrollea.
Los dos estados que la gente olvida
Cargando no es la palabra "Cargando". SkeletonLoader recibe props para
líneas y forma, así que se le puede dar la forma de lo que está llegando — que
es lo que evita que el layout salte cuando llega.
Vacío no es una página vacía. EmptyState dice qué estaría ahí y ofrece la
acción que lo crea. Una tabla en blanco se lee como un bug.
Toasts
// Vue
import { useToast } from '@/composables/useToast.js'
const toast = useToast()
toast.success('Invoice created')
// React
import useToast from '@/hooks/useToast.js'
const toasts = useToast((state) => state.toasts)
// Anywhere that cannot call a hook — an interceptor, a loader, a guard:
import { toast } from '@/stores/notification.store.js'
toast.error('Could not reach the server')
Un store, dos puertas. Un toast lanzado desde el fondo de un service y uno lanzado en un componente caen en la misma lista, en orden, bajo los mismos timers. Dos stores significarían que un toast lanzado desde un service va a una lista que el host no renderiza — nada en pantalla, y tampoco un error.
Los timers viven fuera del store y se limpian al descartar manualmente. Un toast descartado igual tiene un timeout apuntándole; si se lo deja vivo, cada descarte manual filtra uno que después escribe estado para un toast que ya no existe.
Estado
| Vue | React | |
|---|---|---|
| Librería | Pinia | Zustand |
| Auth | src/stores/auth.store.js |
misma ruta |
| Notificaciones | src/stores/notification.store.js |
misma ruta |
Ambos vienen cableados: Pinia está instalado en la app, Zustand está en
package.json, y el store de auth persiste su token de la misma forma en que
services/api.js ya lo lee — una sola fuente de verdad, nombrada en un
comentario en el archivo.
Cuándo no usar un store. El estado que ningún otro componente lee pertenece al componente. Un store es para algo que necesitan dos lugares no relacionados: el layout lee el usuario, axios necesita el token. Eso es dos, así que es un store.
Sesiones, y qué pasa en un 401
Los access tokens duran poco. Ambos frontends traen las dos mitades que hacen falta:
| Dónde | Cubre | |
|---|---|---|
| Guard de rutas | src/router/index.js / index.jsx |
navegar a una página sin sesión |
| Interceptor de 401 | src/services/api.js |
el token venciendo con la página ya abierta |
Un guard solo no alcanza. Corre en la navegación, y nada navega mientras cuatro paneles de la pantalla actual fallan en silencio.
Las rutas son privadas salvo que digan lo contrario. PUBLIC_BY_DEFAULT,
arriba del router, es la única línea que cambiar, y viene en false: el
backend inicia sesión con el plugin accounts (accounts.md),
así que cada página queda detrás del sign-in, incluidas las que jfast new view
agregue después. Una página para todo el mundo se sale con
meta: { public: true } (Vue) o handle: { public: true } (React). El guard de
React envuelve la lista entera de rutas en vez de cada ruta, así que un módulo
agregado en /*nuevaRuta*/ queda cubierto sin que el generador sepa nada de
autenticación.
Un frontend generado para un workspace sin backend con accounts
(frontend_accounts = false, abajo) conserva el default de antes: público, con
rutas que se protegen con requiresAuth, y sin las páginas de cuenta -- un
formulario de sign-in delante de todo que nada puede satisfacer sería peor.
LOGIN_ROUTE se exporta desde auth.store.js, porque api.js también lo
necesita y una segunda copia de '/login' es un segundo lugar que olvidar.
El interceptor, y el bug que tiene la versión obvia
Ante un 401 refresca una vez y reenvía el request. Tres cosas ahí no son opcionales:
Un refresh para toda la ráfaga. Cuatro paneles cargando juntos producen
cuatro 401. Cuatro refreshes presentan el mismo refresh token cuatro veces —
rotan, así que tres de esos están gastados, y un backend que lee el replay como
robo revoca la sesión. refreshOnce() le entrega a cada llamador la misma
promise.
Un refresh que a su vez es rechazado termina la sesión. Limpia el store y
hace una navegación de página completa a LOGIN_ROUTE con ?next=, una sola
vez, sin importar cuántos requests fallaron juntos. Reintentar un refresh
rechazado es el loop.
Al request reenviado hay que ponerle el token nuevo, y esta es la parte que
parece que ya funciona. Refrescar actualiza el lugar de donde el token sale
normalmente — api.defaults.headers en Vue. Los defaults no llegan a un config
que ya carga Authorization, y el config reenviado carga uno: axios mezcló el
token viejo cuando el request se armó la primera vez. Entonces el refresh
devuelve 200, los reintentos vuelven a salir con el token que acaba de vencer, y
la app se renderiza como logueada con todos los paneles vacíos:
/invoices 401 Bearer A1
/clients 401 Bearer A1
/projects 401 Bearer A1
/notifications 401 Bearer A1
refresh 200 -> A2
/invoices 401 Bearer A1 <-- refreshed, retried, same dead token
/clients 401 Bearer A1
/projects 401 Bearer A1
/notifications 401 Bearer A1
Vue lo arregla poniendo original.headers.Authorization antes del reenvío.
React no necesita esa línea, y por una razón que vale conocer en vez de por
suerte: su interceptor de request pone el header por request desde
localStorage, y el reenvío vuelve a pasar por ahí. Eso se sostiene solo
mientras la asignación siga siendo incondicional — envuélvela en
if (!config.headers.Authorization) y React tiene el bug idéntico.
Las páginas de cuenta
Lo que ofrece el plugin accounts tiene su página en el frontend. Cada una está
en src/views/, se carga bajo demanda y se dibuja dentro de un solo marco,
AuthShell -- la tarjeta en classic, el hero y el panel de vidrio en
nexora --, así que las páginas se comparten entre los dos looks y solo cambia
el marco.
| Ruta | Página | Llama a |
|---|---|---|
/login |
LoginView |
POST /auth/login, y /auth/login/mfa cuando falta el código |
/register |
RegisterView |
POST /auth/register |
/verify-email?token= |
VerifyEmailView |
POST /auth/verify |
/forgot-password |
ForgotPasswordView |
POST /auth/password/forgot |
/reset-password?token= |
ResetPasswordView |
POST /auth/password/reset |
/mfa/enrol |
MfaEnrolView |
/auth/mfa/setup y /confirm con el token MFA del sign-in |
/account/security |
SecurityView |
activar y quitar MFA, códigos de recuperación nuevos, cerrar todas las sesiones |
Iniciar sesión trae al usuario. accounts contesta /auth/login solo con
tokens, así que el store de auth (Vue) o services/auth.service.js (React) los
guarda y luego llama a GET /auth/account; user tiene lo que eso devuelve
(email, display_name, roles, permissions, email_verified,
mfa_enabled). Un sign-in también puede quedarse antes de la sesión: login()
resuelve a { status: 'signed-in' }, { status: 'mfa', mfaToken } -- la página
pide el código -- o { status: 'enrol', mfaToken }, cuando un rol de la cuenta
exige MFA y no lo tiene, y la página manda al usuario a configurarlo.
Los links que ofrece una página siguen al backend. "¿Olvidaste tu
contraseña?", "Crea una" y la tarjeta de MFA solo aparecen cuando
GET /auth/features dice que la función está activa, así que un link nunca
lleva a un 404.
Los tokens de los correos salen de la barra de direcciones. Las páginas de
verificación y de reset leen ?token=, reemplazan la URL sin él y lo postean una
vez -- no queda en el historial ni se manda como Referer. El token MFA de un
sign-in viaja en el state del router, nunca en la URL. Las dos rutas de los
correos tienen que coincidir con [plugin.accounts] verify_email_path y
reset_password_path.
Una contraseña equivocada no es una sesión vencida. Las llamadas de cuenta
llevan skipAuthRefresh, y el interceptor de 401 las deja en paz: sin eso, una
contraseña mal escrita dispararía un refresh y sacaría a la persona del
formulario que está llenando. Los errores traen el code del backend
(email_not_verified, mfa_code_invalid, mfa_token_invalid, token_invalid)
junto al mensaje, y las páginas deciden por el código, nunca por el texto.
No se dibuja un QR para MFA: sería una dependencia por una imagen. El link
otpauth:// abre el autenticador en un teléfono, y la clave se muestra para
teclearla en cualquier otro lado. Agrega una librería de QR a MfaSetupPanel si
tus usuarios lo esperan.
?next solo se sigue cuando es un path, nunca una URL absoluta -- viene de la
barra de direcciones.
Para el instalador: frontend_accounts. La variable de template que decide
todo lo anterior. Si falta, vale true. Pásala en false cuando ningún
backend del workspace active accounts.
Claro y oscuro
Tres estados, no dos:
<html> |
Resultado |
|---|---|
| sin atributo | sigue al sistema operativo |
data-theme="dark" |
oscuro, diga lo que diga el sistema |
data-theme="light" |
claro, diga lo que diga el sistema |
El dark: de fábrica de Tailwind solo lee el sistema, lo que deja a quien
quiere el otro sin forma de decirlo. src/style.css redefine la variante para
revisar primero una elección explícita:
@custom-variant dark {
&:where([data-theme="dark"], [data-theme="dark"] *) { @slot; }
@media (prefers-color-scheme: dark) {
&:where(:root:not([data-theme="light"]), :root:not([data-theme="light"]) *) { @slot; }
}
}
El :not([data-theme="light"]) de la segunda mitad es la parte que vale
entender: sin eso, una elección explícita de claro pierde contra un sistema en
oscuro y el switch parece funcionar en una sola dirección.
El toggle es ThemeToggle, en el header de LayoutAuthenticated. Invierte
lo que está en pantalla y lo fija. Al lado, y solo una vez que la elección está
fijada, hay un segundo botón — followSystem(), con la etiqueta "Follow the
system theme". Ese no es decoración: el toggle solo puede fijar light o
dark, así que sin él el primer clic se lleva el tercer estado para siempre y
la app vuelve a ser el switch de dos estados que este diseño existe para
evitar. La elección se guarda bajo <service>:theme.
resolved es compartido, no por llamador. Las dos implementaciones de
useTheme() mantienen theme y resolved en scope de módulo — un computed
en Vue, un valor que React recalcula en cada render detrás de un set de
suscriptores. Armar resolved dentro del composable es la versión que parece
correcta y no lo es: un clic actualiza al componente que lo manejó y a nadie
más, así que un toggle en el header y un switch en ajustes terminan mostrando
íconos opuestos mientras <html> lleva uno solo de los dos. Un test para esto
necesita dos componentes, no uno — un único toggle verificando que data-theme
cambia está verificando la mitad que nunca estuvo rota.
El flash está resuelto. index.html lleva doce líneas inline que leen la
elección guardada y ponen el atributo antes del primer pintado. Aplicar el tema
después del mount significa un frame del color equivocado en cada recarga, que
es el defecto más notorio que un switch de tema puede tener.
Las superficies son tokens, no colores
@theme inline {
--color-surface: var(--ui-surface); /* the page */
--color-panel: var(--ui-panel); /* cards, sidebar, header */
--color-elevated: var(--ui-elevated); /* hover, skeletons, code */
--color-line: var(--ui-line); /* every border */
--color-ink: var(--ui-ink); /* primary text */
--color-ink-soft: var(--ui-ink-soft); /* secondary */
--color-ink-faint: var(--ui-ink-faint);
}
inline es lo que lo hace funcionar: las utilidades generadas emiten
var(--ui-surface) en vez de resolver en build time, así que redefinir siete
variables bajo [data-theme="dark"] da vuelta toda la interfaz.
Lo que eso compra se ve en los componentes: ninguno lleva una clase dark:.
bg-panel es correcto en los dos temas. Un componente que necesita un valor
claro y uno oscuro en cada línea no tiene un design system, tiene dos temas
hardcodeados que se desincronizan la primera vez que alguien edita uno.
La única excepción es el color de estado: success, warning, danger, info
en BaseBadge y ToastHost. Ahí el tono es el significado, así que no
puede salir de un token de superficie compartido. Esos se arman como un tinte al
10% de un solo tono más un dark: en el texto nada más, porque un tinte se lee
bien sobre cualquiera de las dos superficies y un bg-emerald-50 sólido no.
color-scheme se define junto a los tokens, así que las barras de scroll, los
date pickers y el cursor de texto — que dibuja el navegador y nunca ve una
clase — combinan con el resto.
Convenciones que los templates imponen
Una sola instancia de axios. src/services/api.js tiene la base URL, el
timeout (VITE_API_TIMEOUT, 60 000 ms por defecto -- un endpoint que llama a un
modelo de lenguaje tarda veinte segundos sin problema, y un timeout más corto
convierte una respuesta lenta en un error que el usuario reintenta, pagando la
llamada dos veces), el manejo de 401 de arriba, y un interceptor que convierte el detail
RFC 7807 del backend en error.message. Sin eso, cada componente muestra
"Request failed with status code 409" en vez de "Invoice INV-1 already exists".
La única excepción es plain, exportado desde el mismo archivo: misma
configuración, sin interceptores, y /auth/refresh es todo para lo que sirve —
un refresh que pasara por api tendría su propio 401 respondido con otro
refresh.
HTTP en Services/, nunca en componentes. La página generada llama a
listFacturas(); ella maneja el estado de carga y de error, el service maneja
el request.
Tailwind v4 con tokens. Un @import "tailwindcss" y los bloques @theme
de src/style.css. Usa bg-panel, border-line y text-ink en vez de una
rampa neutra: un componente que nombra zinc-200 directo es un componente al
que el tema no llega, y dos componentes que eligen rampas distintas son la
razón por la que una UI termina viéndose sutilmente mal sin que nada esté
identificablemente roto. Ver
.jfast/skills/design-system/SKILL.md.
Nexora conserva las mismas utilidades — src/style.css apunta bg-panel,
text-ink y la rampa brand-* a sus propios tokens — y agrega encima sus
clases de componentes, así que una página escrita para cualquiera de los dos
looks toma sus colores de tokens.
Íconos sin runtime. @mdi/js trae solo strings de path; BaseIcon
renderiza uno en un SVG. Sin icon font, tree-shaken a lo que importas.
Específico de Vue: <script setup> y la Composition API, nunca Options API;
componentes PascalCase.vue; las páginas a nivel de ruta terminan en
View.vue.
Específico de React: componentes función y hooks; componentes PascalCase.jsx;
las páginas a nivel de ruta terminan en View.jsx.
Verificado cómo
Sé preciso con esto, porque importa para cuánto deberías confiar en ello.
Probado en CI: cada archivo renderiza; la estructura del módulo cae donde
debe; el router y el sidebar se parchan correctamente y de forma idempotente;
las {{ }} de Vue y las llaves de JSX sobreviven al scaffolding; el framework
se detecta desde el proyecto. Y, desde que aterrizaron los componentes base,
npm install seguido de npm run build en ambos frontends generados —
scripts/smoke_components.sh, que además verifica que ToastHost esté montado
en algún lado, que haya exactamente un store de toasts, que el bundle contenga
una vuelta a system y un refresh-on-401, y que el drawer bloquee el scroll del
body. Renderizar un template prueba que las llaves estaban bien; no prueba que
un componente importe algo que existe.
Ejercitado contra un backend simulado, en Node, a mano: tres llamadores de
useTheme() y un clic, verificando que después los tres coinciden; y cuatro 401
concurrentes contra el api.js y el auth.store.js reales con un adapter de
axios como servidor, verificando un refresh y cuatro 200, y después lo mismo con
un refresh que a su vez es rechazado. Los dos se escribieron primero y los dos
fallaron con los templates anteriores — el primero con
["Switch to dark theme", "Switch to light theme", "Switch to light theme"], el
segundo con cuatro reintentos llevando el token que acababa de vencer. Son
harnesses descartables, no una suite: no están en CI.
Ejercitado en un navegador real, una vez, a mano: el switch de tema en los dos frontends generados — claro, oscuro, una elección explícita de claro contra un sistema en oscuro, la preferencia sobreviviendo una recarga, y el drawer móvil abriendo. Los estilos computados se leyeron de vuelta, no se miraron a ojo. Es una corrida en un navegador, no una suite: no está en CI y no va a atrapar una regresión.
Los dos looks, los dos frameworks, en CI: scripts/smoke_frontend.sh
genera Vue y React en nexora y en classic, corre jfast new view en cada
uno y los construye; también comprueba que three.js quede fuera del chunk de
entrada y que el build respete VITE_ACCENT. Las pantallas nexora, el listón,
el selector de acento y los dos temas se revisaron a mano en un navegador
cuando se escribieron — una vez, no en CI.
Las páginas de cuenta: CI renderiza los dos frameworks en los dos looks y
revisa las rutas, el default y cada endpoint que llama el store. npm install y
npm run build se corrieron a mano en los cuatro cuando se escribieron. Los
flujos del backend que llaman están probados por HTTP
(tests/test_accounts_flows.py); las páginas en sí no se han recorrido en
un navegador contra un backend corriendo.
No probado: npm run dev como sesión interactiva, las páginas de cuenta en
un navegador, y nada más sobre cómo se ve. Que el build pase significa que compila, no que un modal
atrape el foco correctamente con un lector de pantalla real.
Angular
No se genera, a propósito. Un angular.json hecho a mano y una config de
builder que nunca corrió con ng serve es peor que no tener scaffold: parece
terminado y falla de una forma difícil de atribuir.
Si quieres Angular hoy: haz ng new del proyecto tú mismo, y después mantén la
misma estructura Modulo<Name> a mano. El soporte del generador es la fase 3
de PLAN.md, y debería aterrizar solo junto a un job de CI que realmente buildee
la salida.