commit 26851fa75084b3d444c4da1c2b3654e68ab32667 Author: Omar Date: Mon Aug 10 04:29:24 2026 -0600 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..719be06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +node_modules/ +dist/ +dist-playground/ +coverage/ +.vite/ +.env +.env.* +!.env.example +*.tgz +*.log +npm-debug.log* +*.local +.DS_Store + +# Material de referencia local, no parte del paquete +/prompt.md +/Captura de pantalla_*.png diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d8e9355 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@omaresquivel:registry=https://gitea.autobyte.dev/api/packages/omararch48/npm/ +//gitea.autobyte.dev/api/packages/omararch48/npm/:_authToken=${GITEA_NPM_TOKEN} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1fbdf06 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +dist +dist-playground +coverage +package-lock.json +*.png +prompt.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..2865b7b --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# @omaresquivel/design-system + +Sistema de diseño para estandarizar estilos y componentes en aplicaciones Vue 3. Su núcleo son tokens semánticos sobrescribibles y una hoja CSS compilada; los componentes `Ds*` ofrecen implementaciones accesibles y consistentes sobre esos fundamentos. + +## Requisitos + +- Node.js 22 o posterior. +- Vue 3.5 o posterior en la aplicación consumidora. +- Navegadores modernos compatibles con Tailwind CSS v4 y ``. + +## Desarrollo + +```bash +npm install +npm run dev +``` + +El playground queda disponible en la dirección indicada por Vite. Incluye catálogo, perfil, configuración, formulario largo y estados de panel. + +## Validación + +```bash +npm run typecheck +npm test +npm run build +``` + +`npm run build` genera la librería en `dist/` y el catálogo en `dist-playground/`. + +## Consumo + +Mientras el paquete sea privado puede instalarse desde una ruta local: + +```bash +npm install ../design_system +``` + +Importa componentes y la hoja compilada una sola vez: + +```ts +import { DsButton, DsTextInput } from "@omaresquivel/design-system"; +import "@omaresquivel/design-system/style.css"; +``` + +También puede registrarse el catálogo completo: + +```ts +import { createApp } from "vue"; +import { DesignSystem } from "@omaresquivel/design-system"; +import "@omaresquivel/design-system/style.css"; + +createApp(App).use(DesignSystem).mount("#app"); +``` + +Se recomienda usar importaciones nombradas para conservar tree-shaking. La personalización se realiza sobrescribiendo variables `--ds-*`; no es necesario instalar Tailwind en la aplicación consumidora. + +## Documentación + +- [Investigación y decisiones](docs/research.md) +- [Fundamentos y tokens](docs/foundations.md) +- [Catálogo de componentes](docs/components.md) +- [Accesibilidad](docs/accessibility.md) +- [Instalación y theming](docs/installation.md) +- [Roadmap](docs/roadmap.md) + +El paquete se publica en el registro npm de Gitea y no contiene lógica de negocio, router, almacenamiento ni acceso a APIs. diff --git a/docs/accessibility.md b/docs/accessibility.md new file mode 100644 index 0000000..52803a2 --- /dev/null +++ b/docs/accessibility.md @@ -0,0 +1,41 @@ +# Accesibilidad + +Objetivo: WCAG 2.2 nivel AA para los contratos entregados. + +## Formularios + +- Labels vinculados con `for`/`id` generado o explícito. +- Descripciones y errores unidos mediante `aria-describedby`. +- Los errores establecen `aria-invalid`; la aplicación decide cuándo anunciarlos dinámicamente. +- Checkbox, radio y select conservan semántica HTML nativa. +- `required` y `disabled` se transmiten al control real. +- El switch expone `role="switch"` y `aria-checked` porque representa encendido/apagado, no selección de formulario genérica. + +## Teclado y foco + +- El foco usa un contorno de 2 px con separación de 2 px. +- Tabs: Tab entra en el tab activo; flechas cambian a la pestaña anterior/siguiente; Home y End van a extremos; tabs deshabilitadas se omiten. +- Dialog: `showModal()` proporciona top layer, fondo inerte, contención y Escape nativos. Al cerrar se intenta restaurar el foco previo. +- Todos los botones tienen una altura mínima razonable; `DsIconButton` exige un `label` accesible. + +## Estados + +- Loading en botones conserva el texto, usa `aria-busy` y deshabilita interacciones duplicadas. +- `DsProgressBar` publica valor, mínimo y máximo. +- Alertas estáticas no usan live regions por defecto para evitar anuncios al cargar una página. `live` debe activarse solo cuando el mensaje aparece como resultado de una acción. +- Los iconos decorativos se ocultan con `aria-hidden`. +- Skeleton es decorativo; el contenedor que lo usa debe proporcionar el estado textual cuando sea necesario. + +## Movimiento y contraste + +`prefers-reduced-motion: reduce` reduce animaciones y transiciones. La paleta usa texto slate oscuro sobre fondos claros, blanco sobre acciones sky oscuras y variantes de estado con texto oscuro. Los indicadores de foco no dependen de cambios de color internos. + +## Pruebas incluidas + +- Asociación label/control, descripción, error y `v-model`. +- Disabled y loading. +- Navegación de Tabs con teclado. +- Apertura, nombre y cancelación de Dialog. +- Presencia de exportaciones públicas. + +Limitación: jsdom permite verificar contratos DOM, pero no sustituye pruebas manuales con lector de pantalla, zoom al 200 %, alto contraste ni navegación real en Safari/iOS. Estas comprobaciones forman parte de la revisión previa a una publicación estable. diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 0000000..9250202 --- /dev/null +++ b/docs/components.md @@ -0,0 +1,83 @@ +# Catálogo de componentes + +Todos los nombres comienzan con `Ds`. A menos que se indique lo contrario, los atributos HTML adicionales llegan al control raíz correspondiente. + +## Layout + +| Componente | Props principales | Slots | +| ----------------- | ----------------------------------------------- | -------------------------------------- | +| `DsAppShell` | `maxWidth` | default | +| `DsPageHeader` | `title`, `description`, `eyebrow`, `standalone` | default implícito en textos, `actions` | +| `DsPanel` | `as`, `padding: sm\|md\|lg`, `tone`, `flat` | default | +| `DsSectionHeader` | `title`, `description`, `eyebrow` | `actions` | +| `DsStack` | `as`, `gap`, `align` | default | +| `DsInline` | `as`, `gap`, `align`, `justify`, `wrap` | default | +| `DsDivider` | — | — | + +## Acciones + +| Componente | Props y eventos | Slots | +| --------------- | -------------------------------------------------------------------------------- | ------------- | +| `DsButton` | `variant`, `size`, `type`, `disabled`, `loading`, `block`, evento nativo `click` | default | +| `DsIconButton` | `label` obligatorio, más variantes y estados de `DsButton` | icono default | +| `DsButtonGroup` | `label` | default | +| `DsFormActions` | — | default | + +`loading` conserva el texto como nombre accesible, añade `aria-busy` y deshabilita el botón. + +## Formularios + +| Componente | Modelo | Props principales | +| ------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------- | +| `DsFormField` | — | `id`, `label`, `description`, `error`, `required`; slot scoped con `controlId`, `describedBy`, `invalid` | +| `DsTextInput` | `string` | `id`, `name`, `label`, `description`, `error`, `type`, `autocomplete`, `disabled`, `required`; slots `leading`, `trailing` | +| `DsTextarea` | `string` | contrato común más `rows` | +| `DsPasswordInput` | `string` | contrato común y botón mostrar/ocultar | +| `DsSearchInput` | `string` | contrato de texto con icono decorativo de búsqueda | +| `DsSelect` | `string` | contrato común y `options: DsSelectOption[]` | +| `DsCheckbox` | `boolean` | `label`, `description`, `error`, estados HTML | +| `DsRadioGroup` | `string` | `legend`, `name`, `options: DsRadioOption[]`, descripción y error | +| `DsSwitch` | `boolean` | `label`, `description`, `disabled`, `name` | +| `DsSettingsSection` | — | `title`, `description`; slots default y `actions` | + +Ejemplo: + +```vue + +``` + +## Retroalimentación + +| Componente | Props principales | Comportamiento | +| ----------------- | ------------------------------------ | -------------------------------- | +| `DsSpinner` | `size`, `label` | `role=status`, nombre oculto | +| `DsSkeleton` | `width`, `height`, `rounded` | decorativo y sin gradiente | +| `DsProgressBar` | `value`, `max`, `label`, `showValue` | semántica `progressbar` | +| `DsAlert` | `variant`, `title`, `live` | `live` activa `status` o `alert` | +| `DsInlineMessage` | `variant`, `live` | ayuda o error breve | +| `DsEmptyState` | `title`, `description` | slots `icon`, `actions` | +| `DsLoadingState` | `title`, `description` | región de estado anunciable | +| `DsErrorState` | `title`, `description` | alerta y slot `actions` | + +## Navegación y overlays + +`DsTabs` recibe `items: DsTabItem[]`, `label` y `v-model`. Cada `id` define un slot del mismo nombre. Admite flechas, Home y End. + +```vue + + + + +``` + +`DsDialog` recibe `v-model`, `title`, `description`, `closeLabel` y `closeOnBackdrop`. Emite `close` y `cancel`; ofrece slots default y `footer`. + +`DsConfirmDialog` añade `confirmLabel`, `cancelLabel`, `danger` y `loading`; emite `confirm` y `cancel`. diff --git a/docs/foundations.md b/docs/foundations.md new file mode 100644 index 0000000..60ca557 --- /dev/null +++ b/docs/foundations.md @@ -0,0 +1,50 @@ +# Fundamentos visuales + +## Principios + +1. Sobriedad: superficies claras, jerarquía tipográfica fuerte y sombras discretas. +2. Semántica antes que paleta: los componentes consumen intención (`primary`, `danger`, `surface`) y no tonos de producto directos. +3. Accesibilidad visible: foco, errores y estados no dependen únicamente del color. +4. Composición: `DsStack`, `DsInline`, `DsPanel` y encabezados resuelven layouts repetidos sin acoplarse a páginas. +5. Movimiento funcional: transiciones breves, anuladas con `prefers-reduced-motion`. + +## Tokens + +Los valores predeterminados viven en `src/tokens/`. + +| Familia | Variables principales | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Fondos | `--ds-color-background`, `--ds-color-surface`, `--ds-color-surface-muted`, `--ds-color-surface-inverse` | +| Texto | `--ds-color-text`, `--ds-color-text-muted`, `--ds-color-text-inverse`, `--ds-color-text-danger` | +| Bordes y foco | `--ds-color-border`, `--ds-color-border-hover`, `--ds-color-border-danger`, `--ds-color-focus-ring` | +| Acciones | `--ds-color-primary`, `--ds-color-primary-hover`, `--ds-color-primary-active`, `--ds-color-primary-disabled`, `--ds-color-danger` | +| Estado | variables `info-*`, `success-*`, `warning-*` y `error-*` | +| Espacio | `--ds-space-1` a `--ds-space-12` | +| Forma | `--ds-radius-sm` a `--ds-radius-xl`, `--ds-shadow-sm`, `--ds-shadow-md` | +| Controles | `--ds-control-height-sm`, `--ds-control-height-md`, `--ds-control-height-lg` | +| Movimiento | `--ds-duration-fast`, `--ds-duration-normal` | + +## Tema predeterminado + +- `slate-50` como fondo. +- Blanco para superficies. +- `slate-900` para texto y superficies inversas. +- `sky-600` para acciones principales y `sky-400` para foco. +- Radios de 10–18 px y sombras de baja opacidad. + +El sistema no usa gradientes, transparencias de tipo glass, sombras decorativas ni movimiento ornamental. + +## Personalización + +Sobrescribe variables después de importar la hoja del paquete: + +```css +:root { + --ds-color-primary: #2563eb; + --ds-color-primary-hover: #1d4ed8; + --ds-radius-md: 0.5rem; + --ds-page-width: 80rem; +} +``` + +No sobrescribas selectores internos salvo que estés corrigiendo una limitación documentada. La API estable de theming son las variables `--ds-*`. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..3ce95b4 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,87 @@ +# Instalación, consumo y theming + +## Instalar localmente + +Construye la librería: + +```bash +npm install +npm run build:lib +``` + +Desde otra aplicación Vue: + +```bash +npm install ../design_system +``` + +El paquete está configurado para publicarse en el registro npm de Gitea de `omararch48`. La publicación sigue siendo manual y requiere `GITEA_NPM_TOKEN`. + +## Publicar en Gitea + +Define un token personal con permiso de paquetes sin guardarlo en el repositorio: + +```bash +export GITEA_NPM_TOKEN="tu-token" +npm publish +``` + +El hook `prepublishOnly` ejecuta type-check, pruebas y build de la librería antes de subir el paquete. Gitea no permite reemplazar una combinación existente de nombre y versión; incrementa la versión antes de una publicación posterior: + +```bash +npm version patch +npm publish +``` + +El scope `@omaresquivel` apunta a: + +```text +https://gitea.autobyte.dev/api/packages/omararch48/npm/ +``` + +## Importar + +```ts +import { DsButton, DsPanel, DsTextInput } from "@omaresquivel/design-system"; +import "@omaresquivel/design-system/style.css"; +``` + +Vue `^3.5.0` es peer dependency. Tailwind, Vite y TypeScript no son dependencias runtime del consumidor. + +## Registro global opcional + +```ts +import { DesignSystem } from "@omaresquivel/design-system"; +import "@omaresquivel/design-system/style.css"; + +app.use(DesignSystem); +``` + +## Personalizar el tema + +Carga las personalizaciones después del CSS del paquete: + +```css +:root { + --ds-color-primary: #2563eb; + --ds-color-primary-hover: #1d4ed8; + --ds-color-focus-ring: #60a5fa; + --ds-radius-lg: 1rem; +} +``` + +No es necesario configurar `@source`, copiar `@apply` ni exponer Tailwind al consumidor. Si una aplicación ya usa Tailwind, ambos pueden coexistir porque los selectores del sistema llevan el prefijo `ds-` y los tokens llevan `--ds-`. + +## Enlace durante desarrollo + +Puede usarse una dependencia local en `package.json`: + +```json +{ + "dependencies": { + "@omaresquivel/design-system": "file:../design_system" + } +} +``` + +Después de modificar la librería ejecuta `npm run build:lib` y actualiza la instalación de la aplicación consumidora según el flujo de su gestor de paquetes. diff --git a/docs/research.md b/docs/research.md new file mode 100644 index 0000000..dbec476 --- /dev/null +++ b/docs/research.md @@ -0,0 +1,49 @@ +# Investigación y decisiones + +Investigación realizada el 10 de agosto de 2026. Se priorizaron fuentes oficiales y especificaciones. + +## Fuentes consultadas + +- [Vue: TypeScript con Composition API](https://vuejs.org/guide/typescript/composition-api): props y emits tipados con ` + + diff --git a/playground/src/App.vue b/playground/src/App.vue new file mode 100644 index 0000000..3c56521 --- /dev/null +++ b/playground/src/App.vue @@ -0,0 +1,490 @@ + + + diff --git a/playground/src/main.ts b/playground/src/main.ts new file mode 100644 index 0000000..3f7fd61 --- /dev/null +++ b/playground/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from "vue"; +import "@omaresquivel/design-system/style.css"; +import "./playground.css"; +import App from "./App.vue"; + +createApp(App).mount("#app"); diff --git a/playground/src/playground.css b/playground/src/playground.css new file mode 100644 index 0000000..594883b --- /dev/null +++ b/playground/src/playground.css @@ -0,0 +1,84 @@ +.playground-frame { + overflow: hidden; + border: 1px solid var(--ds-color-border); + border-radius: var(--ds-radius-xl); + background: var(--ds-color-surface); + box-shadow: var(--ds-shadow-md); +} + +.playground-body { + padding: var(--ds-space-5); +} +.playground-grid { + display: grid; + gap: var(--ds-space-5); +} +.playground-grid--2 { + grid-template-columns: minmax(0, 1fr); +} +.playground-nav { + margin-bottom: var(--ds-space-6); +} +.playground-swatch { + min-height: 5rem; + border: 1px solid var(--ds-color-border); + border-radius: var(--ds-radius-md); + padding: var(--ds-space-3); + font-size: var(--ds-font-size-xs); +} +.playground-swatch--primary { + color: white; + background: var(--ds-color-primary); +} +.playground-swatch--inverse { + color: white; + background: var(--ds-color-surface-inverse); +} +.playground-swatch--muted { + background: var(--ds-color-surface-muted); +} +.playground-icon { + width: 1.1rem; + height: 1.1rem; +} +.playground-profile { + display: grid; + grid-template-columns: 1fr; + gap: var(--ds-space-6); +} +.playground-avatar { + display: grid; + width: 5rem; + height: 5rem; + place-items: center; + border-radius: 50%; + color: white; + background: var(--ds-color-primary); + font-size: var(--ds-font-size-xl); + font-weight: 800; +} +.playground-meta { + margin: 0; + color: var(--ds-color-text-muted); + font-size: var(--ds-font-size-sm); +} +.playground-code { + overflow-x: auto; + border-radius: var(--ds-radius-md); + padding: var(--ds-space-4); + color: #e2e8f0; + background: var(--ds-color-surface-inverse); + font-size: var(--ds-font-size-sm); +} + +@media (min-width: 48rem) { + .playground-body { + padding: var(--ds-space-8); + } + .playground-grid--2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .playground-profile { + grid-template-columns: 15rem minmax(0, 1fr); + } +} diff --git a/src/components/actions/DsButton.vue b/src/components/actions/DsButton.vue new file mode 100644 index 0000000..0265dad --- /dev/null +++ b/src/components/actions/DsButton.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/components/actions/DsButtonGroup.vue b/src/components/actions/DsButtonGroup.vue new file mode 100644 index 0000000..23c08bc --- /dev/null +++ b/src/components/actions/DsButtonGroup.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/components/actions/DsFormActions.vue b/src/components/actions/DsFormActions.vue new file mode 100644 index 0000000..c4cad8f --- /dev/null +++ b/src/components/actions/DsFormActions.vue @@ -0,0 +1,3 @@ + diff --git a/src/components/actions/DsIconButton.vue b/src/components/actions/DsIconButton.vue new file mode 100644 index 0000000..cfb9bfd --- /dev/null +++ b/src/components/actions/DsIconButton.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/components/feedback/DsAlert.vue b/src/components/feedback/DsAlert.vue new file mode 100644 index 0000000..a953db0 --- /dev/null +++ b/src/components/feedback/DsAlert.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/components/feedback/DsEmptyState.vue b/src/components/feedback/DsEmptyState.vue new file mode 100644 index 0000000..ddf16a8 --- /dev/null +++ b/src/components/feedback/DsEmptyState.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/components/feedback/DsErrorState.vue b/src/components/feedback/DsErrorState.vue new file mode 100644 index 0000000..da0012e --- /dev/null +++ b/src/components/feedback/DsErrorState.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/feedback/DsInlineMessage.vue b/src/components/feedback/DsInlineMessage.vue new file mode 100644 index 0000000..51fcce8 --- /dev/null +++ b/src/components/feedback/DsInlineMessage.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/components/feedback/DsLoadingState.vue b/src/components/feedback/DsLoadingState.vue new file mode 100644 index 0000000..4e7c99f --- /dev/null +++ b/src/components/feedback/DsLoadingState.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/components/feedback/DsProgressBar.vue b/src/components/feedback/DsProgressBar.vue new file mode 100644 index 0000000..87788e9 --- /dev/null +++ b/src/components/feedback/DsProgressBar.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/feedback/DsSkeleton.vue b/src/components/feedback/DsSkeleton.vue new file mode 100644 index 0000000..29b7de6 --- /dev/null +++ b/src/components/feedback/DsSkeleton.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/feedback/DsSpinner.vue b/src/components/feedback/DsSpinner.vue new file mode 100644 index 0000000..3b62ea2 --- /dev/null +++ b/src/components/feedback/DsSpinner.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/forms/DsCheckbox.vue b/src/components/forms/DsCheckbox.vue new file mode 100644 index 0000000..ceb1f1c --- /dev/null +++ b/src/components/forms/DsCheckbox.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/components/forms/DsFormField.vue b/src/components/forms/DsFormField.vue new file mode 100644 index 0000000..a7a29b7 --- /dev/null +++ b/src/components/forms/DsFormField.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/components/forms/DsPasswordInput.vue b/src/components/forms/DsPasswordInput.vue new file mode 100644 index 0000000..b56bc88 --- /dev/null +++ b/src/components/forms/DsPasswordInput.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/components/forms/DsRadioGroup.vue b/src/components/forms/DsRadioGroup.vue new file mode 100644 index 0000000..ce51092 --- /dev/null +++ b/src/components/forms/DsRadioGroup.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/components/forms/DsSearchInput.vue b/src/components/forms/DsSearchInput.vue new file mode 100644 index 0000000..3558455 --- /dev/null +++ b/src/components/forms/DsSearchInput.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/components/forms/DsSelect.vue b/src/components/forms/DsSelect.vue new file mode 100644 index 0000000..6f105f7 --- /dev/null +++ b/src/components/forms/DsSelect.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/components/forms/DsSettingsSection.vue b/src/components/forms/DsSettingsSection.vue new file mode 100644 index 0000000..e29e0ed --- /dev/null +++ b/src/components/forms/DsSettingsSection.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/forms/DsSwitch.vue b/src/components/forms/DsSwitch.vue new file mode 100644 index 0000000..6d8ccc0 --- /dev/null +++ b/src/components/forms/DsSwitch.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/components/forms/DsTextInput.vue b/src/components/forms/DsTextInput.vue new file mode 100644 index 0000000..ccf6637 --- /dev/null +++ b/src/components/forms/DsTextInput.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/components/forms/DsTextarea.vue b/src/components/forms/DsTextarea.vue new file mode 100644 index 0000000..cd42734 --- /dev/null +++ b/src/components/forms/DsTextarea.vue @@ -0,0 +1,57 @@ + + +