@jamx-framework/ui
v1.0.1
Published
JAMX Framework — UI Component Library
Maintainers
Readme
@jamx-framework/ui
Descripción
Biblioteca de componentes UI para JAMX Framework. Proporciona un conjunto de componentes básicos (layout, tipografía, interactivos, feedback) y tokens de diseño (colores, espaciado, tipografía) para construir interfaces de usuario de forma consistente y type-safe. Todos los componentes están diseñados para funcionar con el renderer SSR de JAMX y son completamente personalizables.
Cómo funciona
La biblioteca implementa componentes funcionales que retornan elementos JSX usando el runtime de @jamx-framework/renderer. Cada componente acepta props tipadas y aplica estilos mediante tokens o estilos en línea. Los tokens proporcionan valores consistentes para colores, espaciado y tipografía que pueden ser personalizados.
Componentes principales
Layout
- Box: Contenedor genérico con padding, margin, display, etc.
- Stack: Contenedor con layout vertical/horizontal y gap
- Grid: Grid system con columnas y gap
Typography
- Text: Texto con variantes (body, caption, etc.)
- Heading: Encabezados h1-h6 con tokens de tamaño
Interactive
- Button: Botón con variantes (primary, secondary, etc.)
- Link: Enlace con estilos consistentes
- Input: Campo de texto con validación visual
Feedback
- Alert: Mensajes de alerta (info, success, warning, error)
- Badge: Etiquetas y contadores
Tokens
- colors: Paleta de colores (primario, neutros, semánticos)
- spacing: Espaciado (margins, paddings)
- typography: Tamaños, pesos, familias de fuente
Uso básico
Instalación
pnpm add @jamx-framework/uiImportar componentes
import { Box, Stack, Heading, Text, Button, Alert } from '@jamx-framework/ui';
import { jsx } from '@jamx-framework/renderer';Ejemplo: Página simple
// src/pages/index.page.tsx
import { jsx } from '@jamx-framework/renderer';
import { Box, Stack, Heading, Text, Button } from '@jamx-framework/ui';
export default {
render(ctx) {
return jsx('div', { class: 'container' }, [
jsx(Heading, { level: 1, children: 'Bienvenido a JAMX' }),
jsx(Text, { variant: 'body', children: 'Esta es una aplicación de ejemplo' }),
jsx(Stack, { direction: 'row', gap: '16px' }, [
jsx(Button, { variant: 'primary', children: 'Empezar' }),
jsx(Button, { variant: 'secondary', children: 'Aprender más' }),
]),
]);
},
};Ejemplo: Formulario
import { jsx } from '@jamx-framework/renderer';
import { Box, Stack, Input, Button, Alert } from '@jamx-framework/ui';
export default {
render(ctx) {
return jsx('form', { onSubmit: handleSubmit }, [
jsx(Input, {
name: 'email',
type: 'email',
placeholder: 'Correo electrónico',
required: true,
}),
jsx(Input, {
name: 'password',
type: 'password',
placeholder: 'Contraseña',
required: true,
}),
jsx(Button, { type: 'submit', variant: 'primary', children: 'Iniciar sesión' }),
jsx(Alert, { variant: 'error', children: 'Credenciales inválidas' }),
]);
},
};Ejemplo: Card component
import { jsx } from '@jamx-framework/renderer';
import { Box, Stack, Heading, Text, Button } from '@jamx-framework/ui';
function Card(props: { title: string; children: string }) {
return jsx(Box, {
as: 'article',
padding: '24px',
style: { border: '1px solid #e5e7eb', borderRadius: '8px' },
}, [
jsx(Heading, { level: 2, children: props.title }),
jsx(Text, { variant: 'body', children: props.children }),
jsx(Button, { variant: 'primary', children: 'Acción' }),
]);
}Usar tokens
import { colors, spacing, typography } from '@jamx-framework/ui';
// Colores
const primaryColor = colors.primary[500]; // #3b82f6
const successColor = colors.success[500]; // #22c55e
// Espaciado
const padding = spacing[4]; // 16px
const margin = spacing[8]; // 32px
// Tipografía
const fontSize = typography.fontSizes.lg; // 18px
const fontWeight = typography.fontWeights.semibold; // 600API Reference
Componentes
Box
interface BoxProps extends BaseProps {
as?: string; // elemento HTML (default: 'div')
padding?: string; // padding (ej: '16px', 'spacing[4]')
margin?: string; // margin
display?: "block" | "flex" | "grid" | "inline" | "inline-block" | "none";
width?: string; // ancho (ej: '100%', '300px')
height?: string; // alto
}Contenedor genérico. Renderiza cualquier elemento HTML con estilos aplicados.
Ejemplo:
jsx(Box, {
as: 'section',
padding: spacing[6],
display: 'flex',
children: 'Contenido',
});Stack
interface StackProps extends BaseProps {
direction?: 'row' | 'column'; // dirección del layout
gap?: string; // espacio entre hijos
align?: 'start' | 'center' | 'end' | 'stretch';
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
wrap?: boolean; // permitir wrap (solo row)
}Contenedor con layout flex y gap automático.
Ejemplo:
jsx(Stack, {
direction: 'row',
gap: spacing[4],
align: 'center',
children: [item1, item2, item3],
});Grid
interface GridProps extends BaseProps {
columns?: number | string; // número de columnas o 'auto-fit'
gap?: string; // espacio entre celdas
}Grid system basado en CSS Grid.
Ejemplo:
jsx(Grid, {
columns: 3,
gap: spacing[4],
children: [item1, item2, item3, item4, item5, item6],
});Text
interface TextProps extends BaseProps {
variant?: 'body' | 'caption' | 'label' | 'helper';
size?: 'sm' | 'md' | 'lg'; // override de tamaño
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
color?: string; // color personalizado
}Componente de texto con variantes predefinidas.
Ejemplo:
jsx(Text, { variant: 'body', children: 'Texto normal' });
jsx(Text, { variant: 'caption', children: 'Texto pequeño' });Heading
interface HeadingProps extends BaseProps {
level: 1 | 2 | 3 | 4 | 5 | 6; // nivel del encabezado (h1-h6)
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'; // override
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
}Encabezados con jerarquía semántica.
Ejemplo:
jsx(Heading, { level: 1, children: 'Título principal' });
jsx(Heading, { level: 2, size: 'lg', children: 'Subtítulo' });Button
interface ButtonProps extends BaseProps {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
loading?: boolean; // mostrar spinner
fullWidth?: boolean; // ancho 100%
}Botón con estilos consistentes.
Ejemplo:
jsx(Button, {
variant: 'primary',
size: 'md',
onClick: handleClick,
children: 'Guardar',
});Link
interface LinkProps extends BaseProps {
href: string;
external?: boolean; // abrir en nueva pestaña
underline?: boolean; // subrayado
}Enlace con estilos de link.
Ejemplo:
jsx(Link, {
href: '/about',
children: 'Acerca de',
});
jsx(Link, {
href: 'https://example.com',
external: true,
children: 'Ejemplo externo',
});Input
interface InputProps extends BaseProps {
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
name?: string;
placeholder?: string;
value?: string;
defaultValue?: string;
disabled?: boolean;
required?: boolean;
error?: boolean; // estado de error
helperText?: string; // texto de ayuda
}Campo de texto con validación visual.
Ejemplo:
jsx(Input, {
name: 'email',
type: 'email',
placeholder: 'Correo electrónico',
required: true,
error: !isValid,
helperText: isValid ? '' : 'Correo inválido',
});Alert
interface AlertProps extends BaseProps {
variant?: 'info' | 'success' | 'warning' | 'error';
title?: string; // título opcional
dismissible?: boolean; // mostrar botón cerrar
onDismiss?: () => void;
}Mensaje de alerta con variantes semánticas.
Ejemplo:
jsx(Alert, {
variant: 'success',
title: 'Éxito',
children: 'Operación completada',
});Badge
interface BadgeProps extends BaseProps {
variant?: 'default' | 'primary' | 'success' | 'warning' | 'error';
size?: 'sm' | 'md' | 'lg';
count?: number; // para contadores
dot?: boolean; // punto de notificación
}Etiqueta o contador.
Ejemplo:
jsx(Badge, { variant: 'primary', children: 'Nuevo' });
jsx(Badge, { count: 5, children: 'Notificaciones' });Tokens
colors
export const colors = {
// Paleta primaria (configurable)
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6', // primario por defecto
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
// Colores semánticos
success: { /* ... */ },
warning: { /* ... */ },
error: { /* ... */ },
info: { /* ... */ },
// Escala de grises
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
},
};spacing
export const spacing = {
0: '0px',
1: '4px',
2: '8px',
3: '12px',
4: '16px',
5: '20px',
6: '24px',
8: '32px',
10: '40px',
12: '48px',
16: '64px',
24: '96px',
32: '128px',
};typography
export const typography = {
fontSizes: {
xs: '12px',
sm: '14px',
md: '16px',
lg: '18px',
xl: '20px',
'2xl': '24px',
'3xl': '30px',
'4xl': '36px',
'5xl': '48px',
'6xl': '60px',
},
fontWeights: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
lineHeights: {
tight: 1.25,
normal: 1.5,
relaxed: 1.75,
},
fontFamilies: {
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
mono: 'ui-monospace, SFMono-Regular, "SF Mono", monospace',
},
};Utilidades
cx
function cx(...classNames: (string | undefined | null | false)[]): stringCombina classNames, filtrando falsy values.
Ejemplo:
const className = cx('base-class', condition && 'conditional', customClass);sp
function sp(token: SpacingToken): stringHelper para obtener valores de spacing.
Ejemplo:
import { sp } from '@jamx-framework/ui';
jsx(Box, { padding: sp(4) }); // '16px'Tipos
BaseProps
interface BaseProps {
class?: string; // clase CSS adicional
className?: string; // alias de class
style?: Record<string, string>; // estilos en línea
testId?: string; // para testing (data-testid)
id?: string; // ID HTML
[key: string]: unknown; // otras props pasan al elemento
}Props base que todos los componentes aceptan.
ColorVariant
type ColorVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';Size
type Size = 'sm' | 'md' | 'lg' | 'xl';Personalización
Override de tokens
Puedes crear tus propios tokens:
// theme.ts
import { createTheme } from '@jamx-framework/ui';
export const myTheme = createTheme({
colors: {
primary: {
500: '#ff5722', // naranja personalizado
},
},
spacing: {
4: '20px', // spacing personalizado
},
typography: {
fontSizes: {
lg: '20px',
},
},
});Estilos personalizados
Todos los componentes aceptan style y className:
jsx(Button, {
variant: 'primary',
style: { borderRadius: '9999px' },
className: 'my-custom-button',
children: 'Botón',
});CSS Modules
Puedes usar CSS modules con los componentes:
import styles from './MyComponent.module.css';
jsx(Box, {
class: styles.container,
children: jsx(Button, { class: styles.button, children: 'Click' }),
});Ejemplos completos
Layout con Stack y Grid
import { jsx } from '@jamx-framework/renderer';
import { Box, Stack, Grid, Heading, Text, Card } from '@jamx-framework/ui';
export default {
render(ctx) {
return jsx(Box, { as: 'main', padding: spacing[6] }, [
jsx(Heading, { level: 1, children: 'Dashboard' }),
// Grid de cards
jsx(Grid, {
columns: { sm: 1, md: 2, lg: 3 },
gap: spacing[4],
}, [
jsx(Card, { title: 'Ventas', children: '$12,345' }),
jsx(Card, { title: 'Usuarios', children: '1,234' }),
jsx(Card, { title: 'Pedidos', children: '567' }),
]),
// Stack vertical
jsx(Stack, { direction: 'column', gap: spacing[4] }, [
jsx(Heading, { level: 2, children: 'Actividad reciente' }),
jsx(Text, { variant: 'body', children: 'No hay actividad' }),
]),
]);
},
};Formulario con validación
import { jsx, useState } from '@jamx-framework/renderer';
import { Box, Stack, Input, Button, Alert } from '@jamx-framework/ui';
export default {
async render(ctx) {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleSubmit = async () => {
if (!email.includes('@')) {
setError('Email inválido');
return;
}
await submitForm({ email });
};
return jsx(Box, { as: 'form', onSubmit: handleSubmit }, [
jsx(Input, {
name: 'email',
type: 'email',
placeholder: 'Correo electrónico',
value: email,
onChange: (e) => setEmail(e.target.value),
error: !!error,
helperText: error,
}),
jsx(Button, {
type: 'submit',
variant: 'primary',
disabled: !email,
children: 'Enviar',
}),
]);
},
};Componente Card reutilizable
import { jsx } from '@jamx-framework/renderer';
import { Box, Stack, Heading, Text, Button } from '@jamx-framework/ui';
interface CardProps {
title: string;
description: string;
actionLabel?: string;
onAction?: () => void;
}
function Card({ title, description, actionLabel, onAction }: CardProps) {
return jsx(Box, {
as: 'article',
padding: spacing[6],
style: {
border: `1px solid ${colors.gray[200]}`,
borderRadius: '8px',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
},
}, [
jsx(Heading, { level: 3, children: title }),
jsx(Text, { variant: 'body', children: description, style: { marginTop: spacing[3] } }),
actionLabel && jsx(Button, {
variant: 'primary',
onClick: onAction,
style: { marginTop: spacing[4] },
children: actionLabel,
}),
]);
}Alertas con dismiss
import { jsx, useState } from '@jamx-framework/renderer';
import { Alert, Button, Stack } from '@jamx-framework/ui';
export default {
render(ctx) {
const [showSuccess, setShowSuccess] = useState(true);
return jsx(Stack, { direction: 'column', gap: spacing[3] }, [
showSuccess && jsx(Alert, {
variant: 'success',
title: 'Éxito',
dismissible: true,
onDismiss: () => setShowSuccess(false),
children: 'Cambios guardados correctamente',
}),
jsx(Button, {
variant: 'primary',
onClick: () => setShowSuccess(true),
children: 'Mostrar alerta',
}),
]);
},
};Badge con contador
import { jsx } from '@jamx-framework/renderer';
import { Badge, Button, Stack } from '@jamx-framework/ui';
export default {
render(ctx) {
return jsx(Stack, { direction: 'row', gap: spacing[4] }, [
jsx(Button, { variant: 'primary', children: 'Inbox' }),
jsx(Badge, { count: 5, children: 'Notificaciones' }),
jsx(Badge, { variant: 'success', children: 'Activo' }),
jsx(Badge, { variant: 'error', children: 'Error' }),
]);
},
};Diseño y tokens
Sistema de diseño
La biblioteca sigue un sistema de diseño basado en tokens:
- Colores: Escala de 50-900 + colores semánticos (success, warning, error, info)
- Espaciado: Escala de 0-32 (0px a 128px)
- Tipografía: Tamaños xs-6xl, pesos normal/semibold/bold, line-heights
Customización global
Para cambiar el tema global, puedes:
- Modificar los archivos de tokens (si tienes acceso al código fuente)
- Usar CSS variables y sobreescribir en tu hoja de estilos:
:root {
--color-primary-500: #ff5722;
--spacing-4: 20px;
--font-size-lg: 20px;
}- Crear un wrapper de componentes con tus estilos por defecto:
function MyButton(props: ButtonProps) {
return jsx(Button, {
...props,
style: { borderRadius: '9999px', ...props.style },
});
}Testing
Tests unitarios
import { describe, it, expect } from 'vitest';
import { render } from '@jamx-framework/testing';
import { Button, Alert } from '@jamx-framework/ui';
describe('Button', () => {
it('should render with primary variant', () => {
const html = render(jsx(Button, { variant: 'primary', children: 'Click' }));
expect(html).toContain('class="btn btn-primary"');
});
it('should be disabled when disabled prop', () => {
const html = render(jsx(Button, { disabled: true, children: 'Click' }));
expect(html).toContain('disabled');
});
});Tests de integración
import { createTestServer } from '@jamx-framework/testing';
import { Button } from '@jamx-framework/ui';
// Testear que los componentes se renderizan correctamente en el servidorAccesibilidad
Los componentes están diseñados con accesibilidad en mente:
- Box: Usa
aspara cambiar elemento semántico (section, article, etc.) - Button: Usa
<button>nativo con type correcto - Link: Usa
<a>con href - Input: Usa
<input>con label implícito (placeholder) o explícito - Heading: Jerarquía h1-h6 semántica
- Alert: Usa
role="alert"automáticamente
Ejemplo accesible
jsx(Box, { as: 'main' }, [
jsx(Heading, { level: 1 }, 'Título principal'),
jsx(Input, {
name: 'email',
type: 'email',
placeholder: 'Correo electrónico',
'aria-label': 'Correo electrónico',
required: true,
}),
jsx(Button, { type: 'submit' }, 'Enviar'),
]);Limitaciones
Sin CSS framework integrado
- No incluye Tailwind, CSS Modules, o styled-components
- Los estilos son en línea o className manual
- Se recomienda usar con un sistema de estilos externo
Componentes básicos
- Solo incluye componentes fundamentales
- No tiene componentes complejos (tables, modals, dropdowns, etc.)
- Para componentes avanzados, extender o usar otra biblioteca
Sin JavaScript interactivo
- Los componentes son estáticos (no tienen estado interno)
- Para interactividad, usar hooks de JAMX o estado externo
Renderer dependency
- Depende de
@jamx-framework/renderer - No funciona con React, Vue, etc.
Buenas prácticas
1. Usar tokens en lugar de valores hardcodeados
// ✅ Bien
jsx(Box, { padding: spacing[4] });
// ❌ No
jsx(Box, { padding: '16px' });2. Componer componentes
// ✅ Bien: crear componentes compuestos
function UserCard({ user }) {
return jsx(Box, { as: 'article' }, [
jsx(Heading, { level: 3, children: user.name }),
jsx(Text, { variant: 'body', children: user.email }),
jsx(Button, { variant: 'primary', children: 'Ver perfil' }),
]);
}
// ❌ No: repetir estructura3. Usar variantes semánticas
// ✅ Bien: usar variantes apropiadas
jsx(Alert, { variant: 'error', children: 'Error crítico' });
jsx(Button, { variant: 'primary', children: 'Guardar' });
// ❌ No: inventar variantes
jsx(Alert, { variant: 'red', children: 'Error' });4. Proporcionar fallbacks
// ✅ Bien: manejar datos undefined
jsx(Text, { variant: 'body', children: user?.name ?? 'Anónimo' });
// ❌ No: asumir datos
jsx(Text, { variant: 'body', children: user.name });5. Testear con testId
jsx(Button, {
testId: 'login-submit',
children: 'Iniciar sesión',
});
// En tests
const button = screen.getByTestId('login-submit');
expect(button).toBeInTheDocument();Integración con otros paquetes
Con @jamx-framework/renderer
import { jsx } from '@jamx-framework/renderer';
import { Box, Text } from '@jamx-framework/ui';
const page = {
render(ctx) {
return jsx(Box, { padding: '16px' }, [
jsx(Text, { children: 'Hello' }),
]);
},
};Con @jamx-framework/server
import { JamxServer } from '@jamx-framework/server';
import { Box, Heading } from '@jamx-framework/ui';
const server = await JamxServer.create();
server.use(async (req, res) => {
const html = render(jsx(Box, {}, jsx(Heading, { level: 1, children: 'Hello' })));
res.send(html);
});Con @jamx-framework/testing
import { render } from '@jamx-framework/testing';
import { Button } from '@jamx-framework/ui';
test('renders button', () => {
const html = render(jsx(Button, { children: 'Click' }));
expect(html).toContain('Click');
});Roadmap futuro
- [ ] Componentes adicionales: Select, Checkbox, Radio, Modal, Dropdown, Table
- [ ] Soporte para dark mode con tokens de color
- [ ] Animaciones y transiciones
- [ ] Accesibilidad mejorada (ARIA, focus management)
- [ ] Internacionalización (i18n) integrada
- [ ] Soporte para CSS-in-JS (emotion, styled-components)
- [ ] Componentes de formulario completos (Form, Field, FieldGroup)
- [ ] Data display: List, Card, Avatar, AvatarGroup
- [ ] Navigation: Tabs, Breadcrumb, Pagination
- [ ] Layout: Divider, Spacer, Container
Contribución
Para añadir un nuevo componente:
- Crear archivo en
src/components/<category>/<ComponentName>.ts - Definir interface de props que extienda
BaseProps - Implementar componente como función que retorna
JamxElement - Exportar desde
src/index.ts - Añadir tests en
tests/unit/components/ - Documentar en este README
Archivos importantes
src/index.ts- Punto de entradasrc/components/- Componentes organizados por categoríasrc/tokens/- Tokens de diseñosrc/types.ts- Tipos compartidostests/unit/components/- Tests de componentes
Dependencias
@jamx-framework/renderer- Para jsx y tipos@types/node- Tipos de Node.jsvitest- Testing
Scripts del paquete
pnpm build- Compila TypeScriptpnpm dev- Watch modepnpm test- Tests unitariospnpm test:watch- Tests en watchpnpm type-check- Verificar tipospnpm clean- Limpiar build
Ejemplo completo de aplicación
// src/pages/dashboard.page.tsx
import { jsx } from '@jamx-framework/renderer';
import {
Box,
Stack,
Grid,
Heading,
Text,
Button,
Alert,
Badge,
Input,
} from '@jamx-framework/ui';
import { colors, spacing, typography } from '@jamx-framework/ui';
export default {
render(ctx) {
return jsx(Box, { as: 'div', padding: spacing[6] }, [
// Header
jsx(Stack, {
direction: 'row',
justify: 'between',
align: 'center',
style: { marginBottom: spacing[8] },
}, [
jsx(Heading, { level: 1, children: 'Dashboard' }),
jsx(Button, { variant: 'primary', children: 'Nuevo' }),
]),
// Stats grid
jsx(Grid, {
columns: { sm: 1, md: 2, lg: 4 },
gap: spacing[4],
}, [
jsx(Box, { as: 'div', padding: spacing[4], style: { background: colors.gray[50] } }, [
jsx(Text, { variant: 'caption', children: 'Usuarios' }),
jsx(Heading, { level: 2, children: '1,234' }),
]),
jsx(Box, { as: 'div', padding: spacing[4], style: { background: colors.gray[50] } }, [
jsx(Text, { variant: 'caption', children: 'Ingresos' }),
jsx(Heading, { level: 2, children: '$12,345' }),
]),
]),
// Alert
jsx(Alert, {
variant: 'info',
title: 'Bienvenido',
children: 'Esta es tu dashboard personal',
}),
// Search
jsx(Input, {
placeholder: 'Buscar...',
style: { maxWidth: '300px' },
}),
]);
},
};Comparación con otras bibliotecas
| Característica | JAMX UI | Material-UI | Chakra UI | Tailwind CSS | |----------------|---------|-------------|-----------|-------------| | Framework | JAMX | React | React | Agnostico | | Componentes | Básicos | Completo | Completo | Ninguno | | Tokens | Sí | Sí | Sí | No | | SSR | Sí | Sí | Sí | Sí | | TypeScript | Nativo | Nativo | Nativo | No | | Tamaño | Pequeño | Grande | Medio | Variable |
Conclusión
@jamx-framework/ui proporciona un conjunto minimalista pero poderoso de componentes y tokens para construir interfaces de usuario en JAMX. Su diseño modular y type-safe lo hace ideal para aplicaciones que necesitan consistencia visual sin el overhead de bibliotecas grandes.
