ayudacreativa-ui
v5.5.2
Published
Design system de AC — componentes React y tokens de diseño para productos de Ayuda Creativa
Maintainers
Readme
ayudacreativa-ui
Sistema de diseño oficial de Ayuda Creativa — componentes React, tokens de diseño y patrones de interfaz para productos AC.
Instalación
npm install ayudacreativa-uiRequisitos: React ≥ 18 · Node ≥ 18 · TypeScript ≥ 5.0
Inicio rápido
1. Importa los tokens CSS
En el punto de entrada de tu app (ej. main.tsx):
import "ayudacreativa-ui/tokens.css";2. Aplica el tema en el elemento raíz
<div data-theme="light">...</div> <!-- Claro -->
<div data-theme="dark">...</div> <!-- Oscuro -->
<div data-theme="system">...</div> <!-- Sigue el sistema operativo -->3. Importa y usa los componentes
import {
AcButton, AcBadge, AcInput, AcTextarea, AcCheckbox, AcSelect, AcDatePicker, AcToggle, AcTooltip,
AcAccordion, AcCard, AcDynamicForm, AcTitles,
useForm,
} from "ayudacreativa-ui";Componentes
Átomos
<AcButton>
<AcButton variant="primary">Guardar</AcButton>
<AcButton variant="secondary">Cancelar</AcButton>
<AcButton variant="ghost">Ver más</AcButton>
<AcButton variant="danger">Eliminar</AcButton>
<AcButton variant="primary" loading>Guardando...</AcButton>
<AcButton variant="primary" size="sm">Pequeño</AcButton>| Prop | Tipo | Default | Descripción |
| --------- | ---------------------------------------------------------------- | --------- | -------------------------------------------------- |
| variant | primary \| secondary \| ghost \| danger \| icon \| icon-ghost | primary | Estilo visual |
| size | md \| sm | md | Tamaño |
| loading | boolean | false | Muestra spinner y deshabilita |
| color | string | — | Color personalizado (sobreescribe el del variant) |
<AcBadge>
<AcBadge id="b1" variant="activa">Activa</AcBadge>
<AcBadge id="b2" variant="pendiente">Pendiente</AcBadge>
<AcBadge id="b3" variant="error">Error</AcBadge>
<AcBadge id="b4" variant="borrador">Borrador</AcBadge>
<AcBadge id="b5" variant="conectado">Conectado</AcBadge>
<AcBadge id="b6" variant="pro">Pro</AcBadge>Con ícono:
<AcBadge id="b7" variant="activa" icon="Check">Activa</AcBadge>
<AcBadge id="b8" variant="error" icon="AlertCircle">Error</AcBadge>Canales de distribución:
<AcBadge id="b9" variant="whatsapp" icon="MessageCircle">WhatsApp</AcBadge>
<AcBadge id="b10" variant="email" icon="Mail">Email</AcBadge>
<AcBadge id="b11" variant="widget" icon="Layout">Widget</AcBadge>
<AcBadge id="b12" variant="sms" icon="Smartphone">SMS</AcBadge>| Prop | Tipo | Default | Descripción |
| ----------- | ----------------- | ------- | ---------------------------------- |
| id | string | — | Identificador único (requerido) |
| variant | IBadgeVariant | — | Estilo visual |
| icon | IIconName | — | Ícono del registro antes del texto |
| className | string | "" | Clases adicionales |
FieldInputs
Grupo de componentes de entrada de datos: AcInput, AcSelect, AcDatePicker y AcToggle.
<AcInput>
<AcInput placeholder="Correo electrónico" />
<AcInput iconLeft="Search" placeholder="Buscar..." />
<AcInput iconRight="Eye" type="password" />
<AcInput error="Este campo es requerido" value={value} onChange={handleChange} />| Prop | Tipo | Descripción |
| ----------- | ----------- | ----------------------------------- |
| iconLeft | IIconName | Ícono izquierdo |
| iconRight | IIconName | Ícono derecho |
| error | string | Mensaje de error — pinta borde rojo |
<AcSelect>
Dropdown personalizado con búsqueda opcional.
<AcSelect
options={[
{ value: "co", label: "Colombia" },
{ value: "ar", label: "Argentina" },
]}
value={value}
onChange={setValue}
placeholder="Selecciona un país"
/>Con búsqueda interna:
<AcSelect options={options} value={value} onChange={setValue} searchable />Con error:
<AcSelect options={options} error="Campo requerido" />| Prop | Tipo | Default | Descripción |
| ------------- | ------------------------ | ------------------ | --------------------------------------- |
| options | ISelectOption[] | — | Array de { value: string, label: string } |
| value | string | — | Valor seleccionado |
| onChange | (value: string) => void| — | Callback al seleccionar |
| searchable | boolean | false | Activa input de búsqueda en el dropdown |
| placeholder | string | "Seleccionar..." | Texto cuando no hay valor |
| error | string | — | Mensaje de error |
| disabled | boolean | false | Deshabilita el componente |
<AcDatePicker>
Selector de fecha con cuatro modos y soporte para festivos colombianos.
Fecha individual:
<AcDatePicker value={value} onChange={setValue} />Rango de fechas:
<AcDatePicker
mode="range"
valueStart={start}
valueEnd={end}
onRangeChange={(s, e) => { setStart(s); setEnd(e); }}
/>Período mes/año — solo año:
<AcDatePicker mode="month" value={value} onChange={setValue} />
<AcDatePicker mode="year" value={value} onChange={setValue} />Con festivos de Colombia:
<AcDatePicker
mode="range"
valueStart={start}
valueEnd={end}
onRangeChange={handleRange}
showHolidays
/>| Prop | Tipo | Default | Descripción |
| ---------------- | ---------------------------------- | -------------------- | ---------------------------------------------------- |
| mode | single \| range \| month \| year | single | Modo de selección |
| value | string | — | ISO para single (YYYY-MM-DD), month (YYYY-MM), year (YYYY) |
| onChange | (value: string) => void | — | Callback para single, month, year |
| valueStart | string | — | ISO inicio para range |
| valueEnd | string | — | ISO fin para range |
| onRangeChange | (start, end) => void | — | Callback para range |
| showHolidays | boolean | false | Resalta festivos colombianos con punto rojo y tooltip |
| placeholder | string | "Seleccionar..." | Texto cuando no hay valor |
| error | string | — | Mensaje de error |
| disabled | boolean | false | Deshabilita el componente |
<AcTextarea>
<AcTextarea placeholder="Escribe tu mensaje..." />
<AcTextarea value={value} onChange={(e) => setValue(e.target.value)} rows={6} />
<AcTextarea error="Mínimo 20 caracteres" />
<AcTextarea disabled />| Prop | Tipo | Descripción |
|------------|-----------|-------------------------------------|
| error | string | Mensaje de error — pinta borde rojo |
| rows | number | Número de filas visibles (default 4)|
| disabled | boolean | Deshabilita la interacción |
<AcCheckbox>
<AcCheckbox checked={checked} onChange={setChecked} label="Acepto los términos" />
<AcCheckbox checked={false} onChange={() => {}} error="Campo requerido" />
<AcCheckbox checked={true} onChange={() => {}} disabled />| Prop | Tipo | Descripción |
|------------|------------------------------|-------------------------------------|
| checked | boolean | Estado del checkbox |
| onChange | (checked: boolean) => void | Callback al cambiar |
| label | string | Texto descriptivo |
| error | string | Mensaje de error |
| disabled | boolean | Deshabilita la interacción |
<AcTooltip>
Tooltip con tres variantes, cuatro posiciones y control externo de visibilidad. Se ajusta automáticamente si el trigger está cerca del borde del viewport.
Texto plano:
<AcTooltip content="Copiar al portapapeles" placement="top">
<AcButton variant="ghost" size="sm">Hover aquí</AcButton>
</AcTooltip>Título y descripción (variant="rich"):
<AcTooltip variant="rich" title="Ayuda" description="Más contexto aquí." placement="bottom">
<AcButton variant="secondary">?</AcButton>
</AcTooltip>Con acciones — permanece abierto al hacer hover sobre el tooltip (variant="action"):
<AcTooltip
variant="action"
placement="right"
title="¿Eliminar?"
description="Esta acción no se puede deshacer."
actions={[
{ label: "Cancelar", variant: "ghost", onClick: () => {} },
{ label: "Eliminar", variant: "primary", onClick: handleDelete },
]}
>
<AcButton variant="danger" size="sm">Eliminar</AcButton>
</AcTooltip>Control externo — útil para onboarding y tutoriales:
<AcTooltip variant="rich" title="Paso 1" open={open}>
<AcButton>Trigger</AcButton>
</AcTooltip>Deshabilitado por condición de negocio:
<AcTooltip content="Acción disponible" disabled={!hasPermission}>
<AcButton>Acción</AcButton>
</AcTooltip>| Prop | Tipo | Default | Descripción |
|-------------|-----------------------------------|----------|-----------------------------------------------------------|
| variant | text \| rich \| action | text | Estilo visual |
| placement | top \| bottom \| left \| right | top | Posición respecto al trigger (se invierte si no hay espacio) |
| content | string | — | Texto para variant="text" |
| title | string | — | Título para rich / action |
| description| string | — | Descripción para rich / action |
| actions | ITooltipAction[] | — | Botones para variant="action" |
| disabled | boolean | false | Bloquea el tooltip completamente |
| open | boolean | — | Control externo — si se define, el hover interno se ignora|
<AcToggle>
<AcToggle checked={activo} onChange={setActivo} label="Encuesta activa" />| Prop | Tipo | Descripción |
| ---------- | ---------------------------- | -------------------------- |
| checked | boolean | Estado del toggle |
| onChange | (checked: boolean) => void | Callback al cambiar |
| label | string | Texto descriptivo |
| disabled | boolean | Deshabilita la interacción |
<AcAvatar>
<AcAvatar name="Alexander Rodríguez" size="md" />
<AcAvatar src="/foto.jpg" name="Alexander Rodríguez" size="lg" />| Prop | Tipo | Default | Descripción |
| ------ | ---------------- | ------- | ---------------------------------------------- |
| name | string | — | Nombre para generar iniciales y accesibilidad |
| src | string | — | URL de imagen |
| size | sm \| md \| lg | md | Tamaño (28 / 36 / 44 px) |
<AcThemeSwitch>
Selector de tema con opciones Light / Dark / System.
<AcThemeSwitch />Moléculas
<AcAccordion>
Acordeón con soporte para ítems múltiples o exclusivo (solo uno abierto a la vez).
import { AcAccordion } from "ayudacreativa-ui";
import type { IAccordionItem } from "ayudacreativa-ui";
const items: IAccordionItem[] = [
{ id: "1", title: "¿Qué es Ayuda Creativa?", content: "Plataforma de diseño..." },
{ id: "2", title: "¿Cómo inicio?", content: "Instala el paquete..." },
{ id: "3", title: "¿Tiene soporte?", content: "Sí, escríbenos a...", disabled: true },
];Múltiple (varios abiertos a la vez) — exclusivo — con ítem abierto por defecto:
<AcAccordion items={items} />
<AcAccordion items={items} multiple={false} />
<AcAccordion items={items} defaultOpen={["1"]} />| Prop | Tipo | Default | Descripción |
|---------------|--------------------|---------|-------------------------------------------------|
| items | IAccordionItem[] | — | Lista de ítems |
| multiple | boolean | true | Permite varios ítems abiertos simultáneamente |
| defaultOpen | string[] | [] | IDs de ítems abiertos al montar |
IAccordionItem: { id, title, content: ReactNode, disabled? }
<AcTitles>
Encabezado de sección con ícono, título en negrita y descripción opcional.
<AcTitles
id="config"
icon="Settings"
iconColor="var(--ac-brand)"
title="Configuración"
description="Ajusta las preferencias de tu cuenta."
/>Sin ícono:
<AcTitles id="users" title="Usuarios" description="Gestiona el equipo." />| Prop | Tipo | Descripción |
|---------------|-------------|------------------------------------------------------|
| id | string | Identificador único (requerido) |
| icon | IIconName | Ícono del registro de íconos |
| iconColor | string | Color de fondo del ícono (acepta cualquier valor CSS)|
| title | string | Texto principal (negrita) |
| description | string | Texto secundario (opcional) |
<AcDynamicForm>
Formulario completamente dinámico que consume useForm internamente.
import { AcDynamicForm } from "ayudacreativa-ui";
import type { IFormItem } from "ayudacreativa-ui";
const fields: IFormItem[] = [
{
type: "group",
title: "Datos personales",
description: "Información básica del usuario.",
icon: "User",
iconColor: "var(--ac-brand)",
fields: [
{ name: "nombre", type: "text", label: "Nombre", rules: { required: true } },
{ name: "email", type: "email", label: "Correo", rules: { required: true } },
],
},
{ name: "mensaje", type: "textarea", label: "Mensaje", rules: { minLength: 10 } },
{ name: "acepto", type: "checkbox", label: "Acepto los términos", rules: { required: true } },
];
<AcDynamicForm
fields={fields}
onSubmit={(values) => console.log(values)}
submitLabel="Enviar"
/>IFormItem es un discriminated union entre IFormFieldConfig y IFormFieldGroup.
IFormFieldGroup: { type: "group"; title; description?; icon?; iconColor?; fields: IFormFieldConfig[] }
Tipos de campo soportados: text | email | password | number | textarea | select | toggle | checkbox | datepicker
| Prop | Tipo | Default | Descripción |
|---------------|---------------------------------|-------------|------------------------------------------|
| fields | IFormItem[] | — | Definición de campos y grupos |
| onSubmit | (values: IFormValues) => void | — | Callback al hacer submit válido |
| submitLabel | string | "Enviar" | Texto del botón de envío |
| loading | boolean | false | Deshabilita el botón durante peticiones |
<AcCard>
<AcCard>Contenido por defecto</AcCard>
<AcCard variant="metric">Widget KPI</AcCard>
<AcCard variant="accent-left">Destacado con borde brand</AcCard>
<AcCard hoverable>Hover con efecto en borde</AcCard>| Prop | Tipo | Default | Descripción |
| ----------- | ---------------------------------- | --------- | ------------------- |
| variant | default \| metric \| accent-left | default | Estilo visual |
| hoverable | boolean | false | Activa efecto hover |
<AcPagination>
<AcPagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={goToPage}
siblings={1}
showInfo
/>| Prop | Tipo | Default | Descripción |
| -------------- | ------------------------ | ------- | ----------------------------------------- |
| currentPage | number | — | Página activa (1-indexed) |
| totalPages | number | — | Total de páginas |
| onPageChange | (page: number) => void | — | Callback al cambiar página |
| siblings | number | 1 | Páginas visibles a cada lado de la activa |
| showInfo | boolean | false | Muestra "Página X de Y" |
Organismos
<AcTable>
Tabla con paginación, sort y skeleton integrados.
import { AcTable } from "ayudacreativa-ui";
import type { ITableColumn } from "ayudacreativa-ui";
type Survey = { id: number; nombre: string; estado: string };
const columns: ITableColumn<Survey>[] = [
{ key: "nombre", label: "Nombre", sortable: true },
{
key: "estado",
label: "Estado",
align: "center",
render: (row) => <AcBadge id={`estado-${row.id}`} variant={row.estado}>{row.estado}</AcBadge>,
},
];
<AcTable<Survey>
columns={columns}
data={surveys}
rowKey="id"
pageSize={10}
titleTable="Encuestas"
loading={isLoading}
onRowClick={(row) => navigate(`/encuestas/${row.id}`)}
/>| Prop | Tipo | Default | Descripción |
| ------------------ | ---------------------- | ------------------ | --------------------------- |
| columns | ITableColumn<T>[] | — | Definición de columnas |
| data | T[] | — | Dataset completo |
| rowKey | keyof T \| fn | "id" | Clave única por fila |
| pageSize | number | 10 | Filas por página |
| loading | boolean | false | Muestra skeleton |
| titleTable | string | — | Título sobre la tabla |
| descriptionTable | string | — | Descripción sobre la tabla |
| emptyTitle | string | "Sin resultados" | Título en estado vacío |
| onRowClick | (row, index) => void | — | Click en fila |
<AcToast> / AcToastProvider
Sistema de notificaciones flotantes con auto-dismiss.
import { AcToastProvider, useToast } from "ayudacreativa-ui";
// En la raíz de la app
<AcToastProvider position="bottom-right" maxToasts={5}>
<App />
</AcToastProvider>
// En cualquier componente
function MiComponente() {
const { toast } = useToast();
return (
<button onClick={() => toast({ variant: "success", title: "Guardado", duration: 4000 })}>
Guardar
</button>
);
}| Prop | Tipo | Default | Descripción |
| ----------- | -------------------------------------------------------------------------------------- | -------------- | ---------------------------------------- |
| variant | success \| danger \| warning \| info | info | Tipo de notificación |
| title | string | — | Texto principal |
| description| string | — | Texto secundario (opcional) |
| duration | number | 4000 | ms antes de auto-cerrar (0 = sin cierre) |
| position | bottom-right \| bottom-left \| top-right \| top-left \| bottom-center \| top-center | bottom-right | Posición en pantalla |
<AcModal>
import { AcModal } from "ayudacreativa-ui";
<AcModal
isOpen={open}
onClose={() => setOpen(false)}
title="¿Eliminar encuesta?"
subtitle="Esta acción no se puede deshacer."
variant="danger"
showCloseButton
closeOnOverlay
buttons={[
{ label: "Cancelar", variant: "ghost", onClick: () => setOpen(false) },
{ label: "Eliminar", variant: "danger", onClick: handleDelete },
]}
/>| Prop | Tipo | Default | Descripción |
| ----------------- | ---------------------------- | --------- | --------------------------- |
| isOpen | boolean | — | Controla visibilidad |
| title | string | — | Título principal |
| subtitle | string | — | Texto secundario |
| variant | default \| danger \| info | default | Estilo visual |
| size | default \| wide \| compact | default | Ancho (420 / 560 / 320 px) |
| showCloseButton | boolean | false | Muestra botón ✕ |
| closeOnOverlay | boolean | false | Cierra al hacer clic fuera |
| buttons | IModalButton[] | [] | Acciones del modal |
| onClose | () => void | — | Callback al cerrar |
Hooks
useForm
Hook para formularios con validación reactiva. Gestiona estado, errores y submit.
import { useForm } from "ayudacreativa-ui";
import type { IFormFieldConfig } from "ayudacreativa-ui";
const fields: IFormFieldConfig[] = [
{
name: "email",
type: "email",
label: "Correo",
rules: {
required: "El correo es obligatorio",
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Formato inválido" },
},
},
{
name: "mensaje",
type: "textarea",
label: "Mensaje",
rules: { required: true, minLength: { value: 10, message: "Mínimo 10 caracteres" } },
},
{ name: "acepto", type: "checkbox", label: "Acepto términos", rules: { required: true } },
];
const { values, errors, touched, isValid, setValue, setTouched, handleSubmit, reset } =
useForm({
fields,
onSubmit: async (vals) => { /* guardar */ },
});| Retorno | Tipo | Descripción |
|-----------------|------------------------------------------------|----------------------------------------------------------|
| values | IFormValues | Estado actual de todos los campos |
| errors | IFormErrors | Errores calculados reactivamente (via useMemo) |
| touched | IFormTouched | Qué campos han sido tocados (para mostrar errores) |
| isValid | boolean | true cuando no hay ningún error |
| isSubmitting | boolean | true mientras onSubmit está en curso |
| setValue | (name, value) => void | Actualiza el valor de un campo |
| setTouched | (name) => void | Marca un campo como tocado |
| handleSubmit | (e?) => void | Marca todos como tocados y llama onSubmit si es válido |
| reset | () => void | Restaura valores y touched a su estado inicial |
Reglas de validación (IFormFieldRules):
rules: {
required: true | "mensaje personalizado",
minLength: 3 | { value: 3, message: "Mínimo 3" },
maxLength: 100 | { value: 100, message: "Máximo 100" },
min: 0 | { value: 0, message: "Debe ser ≥ 0" },
max: 100 | { value: 100, message: "Debe ser ≤ 100" },
pattern: /regex/ | { value: /regex/, message: "Formato inválido" },
validate: (value) => string | undefined,
}useTheme
Gestiona el tema de la aplicación escribiendo en data-theme del elemento <html>.
import { useTheme } from "ayudacreativa-ui";
import type { IThemeMode } from "ayudacreativa-ui";
const { theme, changeTheme, readCurrentTheme } = useTheme();
// theme: "light" | "dark" | "system"
changeTheme("dark"); // actualiza estado + DOM
readCurrentTheme(); // lee data-theme del DOM directamente| Retorno | Tipo | Descripción |
| ------------------ | --------------------------- | -------------------------------------------------- |
| theme | IThemeMode | Estado React del tema activo |
| changeTheme(mode)| void | Cambia el tema y escribe data-theme en <html> |
| readCurrentTheme() | IThemeMode | Lee data-theme del DOM en tiempo real |
usePagination
Paginación cliente para usar junto con <AcTable> o cualquier lista.
import { usePagination } from "ayudacreativa-ui";
const {
pageData, // T[] — slice de datos de la página actual
currentPage, // number — página activa (1-indexed)
totalPages, // number
totalItems, // number
from, to, // índices visibles para "Mostrando X–Y de Z"
goToPage, // (page: number) => void — clampea automáticamente
nextPage, // () => void
prevPage, // () => void
reset, // () => void — vuelve a página 1 (útil al filtrar)
} = usePagination({ data, pageSize: 10, initialPage: 1 });useColombiaHolidays
Hook y utilidades para consultar los festivos colombianos (Ley 51/1983). Calcula festivos de fecha fija, "puentes" y los basados en Pascua (algoritmo Meeus/Jones/Butcher).
import {
useColombiaHolidays,
isColombiaHoliday,
getColombiaHolidayName,
getColombiaHolidaysByMonth,
getColombiaHolidaysByYearList,
} from "ayudacreativa-ui";
// Hook (con memoización)
const { holidays, isHoliday, getHolidayName } = useColombiaHolidays({ year: 2026 });
const { holidays } = useColombiaHolidays({ year: 2026, month: 5 }); // solo mayo
isHoliday("2026-05-01") // → true
getHolidayName("2026-05-01") // → "Día del Trabajo"
// Funciones puras (sin React — útiles en scripts, tests, SSR)
isColombiaHoliday("2026-07-20") // → true
getColombiaHolidayName("2026-12-25") // → "Navidad"
getColombiaHolidaysByMonth(12, 2026) // → [{ date, name }, ...]
getColombiaHolidaysByYearList(2026) // → ColombiaHoliday[] ordenado| Prop / Retorno | Tipo | Descripción |
| ------------------- | ------------------------------- | ------------------------------------------------------- |
| year | number | Año a consultar (default: año actual) |
| month | number (1–12) | Si se pasa, filtra solo ese mes |
| holidays | IColombiaHoliday[] | Lista ordenada de festivos del período |
| holidayMap | Map<string, string> | Map ISO→nombre para acceso O(1) |
| isHoliday(iso) | boolean | ¿Es festivo la fecha ISO indicada? |
| getHolidayName(iso) | string \| null | Nombre del festivo o null |
interface IColombiaHoliday {
date: string; // "YYYY-MM-DD"
name: string; // nombre oficial del festivo
}Tokens de diseño
Los tokens se cargan automáticamente al importar cualquier componente. También puedes importarlos directamente:
import "ayudacreativa-ui/tokens.css";Colores de marca
| Variable | Valor | Uso |
| ------------------ | --------- | ---------------------- |
| --ac-brand | #039be5 | Color principal |
| --ac-brand-dark | #0277b0 | Hover brand |
| --ac-brand-light | #e3f5fd | Fondos sutiles (light) |
| --ac-success | #0f7b33 | Estados positivos |
| --ac-danger | #ab1707 | Errores y destructivos |
| --ac-warning | #f5a623 | Advertencias |
Variables semánticas por tema
| Variable | Light | Dark |
| ------------ | --------- | --------------------------- |
| --ac-bg1 | #ffffff | #1a1f2e |
| --ac-bg2 | #f0f1f4 | #242938 |
| --ac-t1 | #1a1f2e | #f0f1f4 |
| --ac-t2 | #5c6380 | #9da5bc |
| --ac-t3 | #9da5bc | #5c6380 |
| --ac-bdc | #e2e4e9 | rgba(255, 255, 255, 0.08) |
Storybook
Explora todos los componentes y hooks de forma interactiva:
npm run dev # http://localhost:6006Contribuir
- Haz fork del repositorio
- Crea una rama:
git checkout -b feature/nombre-componente - Asegúrate de que
npm run lintynpm run buildpasen - Crea un PR — incluye screenshots de Storybook
Commits con formato automático
npm run commit # asistente interactivo (commitizen)
# O manualmente:
git commit -m "feat(select): agregar componente Select con búsqueda"
git commit -m "fix(datepicker): corregir cálculo primer día de la semana"Publicar nueva versión
npm run release # bump automático basado en commits
npm run release:patch # 4.0.1 → 4.0.2
npm run release:minor # 4.0.1 → 4.1.0
git push --follow-tags # dispara el GitHub Action de publicación en npmDesarrollado por Ayuda Creativa
