Package Exports
- @jamx-framework/ui
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, type RenderContext } from "@jamx-framework/renderer";
import { Heading, Text, Button, Stack } from "@jamx-framework/ui";
export default {
render(_ctx: RenderContext) {
return jsx("div", {
class: "container",
children: [
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",
children: [
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,
children: [
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, 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" },
children: [
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)[]): string;Combina classNames, filtrando falsy values.
Ejemplo:
const className = cx("base-class", condition && "conditional", customClass);sp
function sp(token: SpacingToken): string;Helper 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],
children: [
jsx(Heading, { level: 1, children: "Dashboard" }),
// Grid de cards
jsx(Grid, {
columns: { sm: 1, md: 2, lg: 3 },
gap: spacing[4],
children: [
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],
children: [
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) {
return jsx(Box, {
as: "form",
onSubmit: handleSubmit,
children: [
jsx(Input, {
name: "email",
type: "email",
placeholder: "Correo electrónico",
value: email,
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
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)",
},
children: [
jsx(Heading, { level: 3, children: title }),
jsx(Text, {
variant: "body",
style: { marginTop: spacing[3] },
children: description,
}),
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],
children: [
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],
children: [
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",
children: [
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",
children: [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] },
children: [
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],
children: [
jsx(Box, {
as: "div",
padding: spacing[4],
style: { background: colors.gray[50] },
children: [
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] },
children: [
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" },
}),
]);
},
};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.