FlowMetrics
Dashboard auth-walled · Equipo 4 devs · Desktop/corporate primary · LCP ≤ 2500ms
1
Análisis de Bundle
— scripts/bundle_analyzer.py ejemplo/input/
40
/100
F
Bundle Health Score Crítico
17 dependencias de producción analizadas. 5 paquetes pesados detectados.
Potencial de reducción estimado: ~400 KB reemplazando alternativas ligeras.
| Paquete | Tamaño | Problema | → | Alternativa | Ahorro |
|---|---|---|---|---|---|
| moment | 290 KB | Locale files bundleados por defecto | → | dayjs (2 KB) | −288 KB |
| @mui/material | ~300 KB | Runtime pesado, emotion SSR overhead | → | shadcn/ui + Radix | −270 KB |
| chart.js | 180 KB | Todos los tipos de gráfica incluidos | → | recharts (lazy) | ya instalado |
| lodash | 71 KB | Full library, sin tree-shaking | → | lodash-es + imports individuales | −60 KB |
| axios | 14 KB | fetch nativo cubre todos los casos | → | ky (3 KB) | −11 KB |
2
Profile Decision Engine
— scripts/frontend_decision_engine.py --primary-device corporate-network --auth-walled true
vite-spa
Fit 100%
Auth-walled dashboard. Login → app shell una vez → routing client-side. Sin SEO. Bundle más pesado aceptable porque los usuarios vuelven.
next-app-router
Fit 40%
SaaS customer-facing con SEO + dinámico. RSC-first. Overhead de infra sin beneficio para app auth-walled.
remix-or-sveltekit
Fit 25%
Mobile-4G primario, low-JS, progressive enhancement. No aplica aquí.
astro-or-static
Fit 5%
Marketing/blog/docs. Near-zero write. No aplica para dashboard interactivo.
Stack Seleccionado — vite-spa
Build
vite 5
Framework
react 18vuesolid
Lenguaje
typescript strict
Estilos
tailwindcss-modules
Componentes
shadcn/uiradix
Router
react-router v6tanstack-router
Estado
tanstack-queryzustand
Formularios
rhf + zod
Testing
vitestrtlplaywright
Code Split
route-level lazy
Anti-patrones a evitar
KILL
no-code-splitting
Bundle único en SPA multi-ruta = inutilizable en redes lentas
KILL
context-as-global-state
Re-renders en cascada; usar Zustand/Jotai para estado global UI
WARN
redux-without-justification
TanStack Query gestiona server state; Zustand el estado UI
WARN
next-or-remix-for-pure-spa
Overkill; Vite es más ligero para app auth-walled
Criterios de Éxito Verificables (Karpathy #4)
2500ms
LCP p75 desktop
<200ms
INP p75
<0.1
CLS p75
200 KB
Bundle init gzip
80 KB
Por ruta (lazy)
≥80
Lighthouse Perf
≥90
Lighthouse a11y
≥50%
Test coverage
3
Project Scaffolding
— scripts/frontend_scaffolder.py flowmetrics-dashboard --template react --features api,forms,testing
✓
Proyecto scaffoldeado: flowmetrics-dashboard
Template: React + Vite · 29 archivos generados · Features: api, forms, testing
$ cd flowmetrics-dashboard
$ npm install
$ npm run dev
$ npm install
$ npm run dev
Estructura generada
flowmetrics-dashboard/
src/
components/
ui/ Button, Input, Card
layout/ Header, Sidebar
pages/
Dashboard.tsx
Accounts.tsx
Settings.tsx
hooks/
useDebounce.ts
useLocalStorage.ts
lib/
utils.ts cn() helper
queryClient.ts
api/
client.ts ky wrapper
metrics.ts
types/
index.ts
main.tsx
App.tsx
vite.config.ts
tailwind.config.ts
tsconfig.json
vitest.config.ts
package.json
Features incluidas
API Client
TanStack Query + ky wrapper con interceptors de auth
Forms
React Hook Form + Zod validation schema
Testing
Vitest + React Testing Library + Playwright e2e
Design System
shadcn/ui + Tailwind CSS v3 + CSS variables
Code Splitting
Route-level lazy loading (React.lazy + Suspense)
4
Component Generation — MetricCard
— scripts/component_generator.py MetricCard --with-test --with-story
MetricCard.tsx
TSX
// MetricCard — client component 'use client'; import { useState } from 'react'; import { cn } from '@/lib/utils'; interface MetricCardProps { label: string; value: string | number; change?: string; trend?: 'up' | 'down' | 'flat'; className?: string; } export function MetricCard({ label, value, change, trend, className }: MetricCardProps) { const [hovered, setHovered] = useState(false); return ( <div className={cn( 'rounded-lg border p-4', hovered && 'shadow-md', className )} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} > <p className="text-xs text-muted"> {label} </p> <p className="text-2xl font-bold"> {value} </p> {change && ( <span className={cn( trend === 'up' && 'text-green-500', trend === 'down' && 'text-red-500' )}>{change}</span> )} </div> ); }
MetricCard.test.tsx
VITEST
import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MetricCard } from './MetricCard'; describe('MetricCard', () => { it('renders label and value', () => { render( <MetricCard label="MRR" value="€42,800" /> ); expect( screen.getByText('MRR') ).toBeInTheDocument(); expect( screen.getByText('€42,800') ).toBeInTheDocument(); }); it('shows change with trend', () => { render( <MetricCard label="MRR" value="€42,800" change="+12.4%" trend="up" /> ); const change = screen.getByText('+12.4%'); expect(change).toHaveClass( 'text-green-500' ); }); it('applies custom className', () => { render( <MetricCard label="LTV" value="€1,240" className="test-class" /> ); expect( screen.getByText('LTV') .closest('div') ).toHaveClass('test-class'); }); });
MetricCard.stories.tsx
STORYBOOK
import type { Meta, StoryObj } from '@storybook/react'; import { MetricCard } from './MetricCard'; const meta: Meta<typeof MetricCard> = { title: 'UI/MetricCard', component: MetricCard, tags: ['autodocs'], args: { label: 'MRR', value: '€42,800', }, }; export default meta; type Story = StoryObj<typeof MetricCard>; export const Default: Story = {}; export const Trending: Story = { args: { change: '+12.4%', trend: 'up', }, }; export const Dropping: Story = { args: { label: 'Churn', value: '3.2%', change: '+0.8%', trend: 'down', }, }; export const CAC: Story = { args: { label: 'CAC', value: '€184', change: '−€12', trend: 'up', }, };
Preview — MetricCard stories en FlowMetrics Dashboard
MRR
€42,800
▲ +12.4% vs mes anterior
CAC
€184
▼ −€12 mejorando
LTV
€1,240
▲ +8.1% vs mes anterior
Churn
3.2%
▲ +0.8pp atención
5
CI Gates Requeridos
— verifiable success criteria (Karpathy discipline)
bundlewatch-initial-and-per-route
Bloquea PR si init >200KB-gzip o ruta >80KB-gzip
a11y-axe-checks
axe-core en CI, Lighthouse a11y ≥90, WCAG AA
playwright-smoke-on-key-flows
Login → dashboard → tabla cuentas → logout
typecheck-strict
tsc --strict --noEmit sin warnings