diff --git a/.gitignore b/.gitignore index 719be06..31a152f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ npm-debug.log* # Material de referencia local, no parte del paquete /prompt.md /Captura de pantalla_*.png + +# Gitea personal +INSTALAR_DESDE_GITEA.md \ No newline at end of file diff --git a/README.md b/README.md index 2865b7b..5e2bee6 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,11 @@ El playground queda disponible en la dirección indicada por Vite. Incluye catá ## Validación ```bash -npm run typecheck -npm test -npm run build +npm run check ``` `npm run build` genera la librería en `dist/` y el catálogo en `dist-playground/`. +Usa `npm run test:coverage` para revisar la cobertura antes de ampliar el catálogo. ## Consumo @@ -61,6 +60,7 @@ Se recomienda usar importaciones nombradas para conservar tree-shaking. La perso - [Catálogo de componentes](docs/components.md) - [Accesibilidad](docs/accessibility.md) - [Instalación y theming](docs/installation.md) +- [Herramientas interactivas](docs/interactive-tools.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 index 52803a2..9b5e45c 100644 --- a/docs/accessibility.md +++ b/docs/accessibility.md @@ -17,6 +17,8 @@ Objetivo: WCAG 2.2 nivel AA para los contratos entregados. - 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. +- La lista ordenable ofrece botones subir/bajar y anuncia la nueva posición; arrastrar nunca es la única forma de completar la operación. +- El árbol lógico usa controles nativos con nombres accesibles y conserva una jerarquía de listas y grupos comprensible sin presentación visual. ## Estados diff --git a/docs/components.md b/docs/components.md index 9250202..b9b9f0f 100644 --- a/docs/components.md +++ b/docs/components.md @@ -81,3 +81,28 @@ Ejemplo: `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`. + +## Datos y reglas + +`DsSortableList` ordena un `v-model` mediante drag-and-drop. Cada elemento requiere `id` y `label`; puede incluir `description` y `disabled`. El evento `reorder` entrega el elemento y sus posiciones `from` y `to`. Los botones subir/bajar ofrecen la misma operación sin depender del puntero. + +```vue + +``` + +`DsLogicTree` edita un `v-model` serializable. Un grupo combina nodos con `operator: "and" | "or"`; cada condición guarda `field`, `comparison` y `value`. `fields` y `comparisons` reciben opciones configurables, y `readonly` permite mostrar el árbol sin editarlo. + +```vue + +``` + +Los textos del constructor pueden adaptarse mediante la prop `labels`. El componente emite `change` con el árbol completo después de cada edición. + +Las decisiones internas, limitaciones conocidas y propuestas de evolución están registradas en [Herramientas interactivas](interactive-tools.md). diff --git a/docs/interactive-tools.md b/docs/interactive-tools.md new file mode 100644 index 0000000..44ba60e --- /dev/null +++ b/docs/interactive-tools.md @@ -0,0 +1,138 @@ +# Herramientas interactivas + +Estado: primera versión funcional. Este documento conserva el contexto necesario para extender `DsSortableList` y `DsLogicTree` sin cambiar accidentalmente sus contratos públicos. + +## Principios compartidos + +- Los modelos son serializables y no contienen referencias DOM ni estado interno de Vue. +- Cada elemento o nodo necesita un `id` estable y único. +- Las actualizaciones reemplazan arreglos y nodos en lugar de mutar los objetos recibidos. +- La interacción con puntero siempre debe tener una alternativa de teclado. +- La lógica de negocio, persistencia y traducción a consultas pertenece a la aplicación consumidora o a adaptadores independientes. +- Cualquier ampliación debe actualizar tipos, API pública, pruebas, playground y documentación. + +## `DsSortableList` + +### Contrato actual + +Recibe `v-model`. Cada elemento contiene: + +```ts +interface DsSortableItem { + id: string; + label: string; + description?: string; + disabled?: boolean; +} +``` + +El usuario puede arrastrar un elemento o utilizar los botones subir/bajar. Cada cambio reemplaza el arreglo y emite: + +```ts +interface DsSortableChange { + item: DsSortableItem; + from: number; + to: number; +} +``` + +El slot predeterminado puede personalizar la representación, pero `id` y `label` continúan formando parte del contrato porque se utilizan para identidad y nombres accesibles. + +### Decisiones + +- Se usa drag-and-drop nativo del navegador para mantener pequeña la primera versión y evitar una dependencia runtime. +- Los botones subir/bajar son la alternativa accesible y también permiten operar la lista en pantallas táctiles. +- La nueva posición se anuncia mediante una región `status`. +- Un elemento `disabled` no puede iniciar un movimiento. + +### Limitaciones conocidas + +- Solo reordena elementos dentro de una misma lista. +- No tiene gesto táctil de arrastre; en móvil se usan los botones. +- No ofrece clonación, selección múltiple, grupos ni zonas de descarte. +- El destino corresponde al elemento sobre el que se suelta; todavía no existe un indicador entre filas. +- No administra persistencia ni historial para deshacer/rehacer. + +### Próximas extensiones + +1. Mejorar el indicador de inserción antes/después de una fila. +2. Evaluar Pointer Events para arrastre táctil sin eliminar la alternativa por botones. +3. Diseñar drag-and-drop entre listas. Antes de implementarlo hay que definir `sourceListId`, `targetListId`, copia frente a movimiento y el evento público resultante. +4. Añadir soporte opcional para deshacer/rehacer como composable independiente. +5. Incorporar pruebas visuales y de interacción en navegadores reales. + +## `DsLogicTree` + +### Contrato actual + +Recibe `v-model`. El árbol utiliza una unión discriminada: + +```ts +type DsLogicNode = DsLogicCondition | DsLogicGroup; + +interface DsLogicGroup { + id: string; + type: "group"; + operator: "and" | "or"; + children: DsLogicNode[]; +} + +interface DsLogicCondition { + id: string; + type: "condition"; + field: string; + comparison: string; + value: string; +} +``` + +`fields` y `comparisons` usan opciones `{ label, value, disabled? }`. La prop `labels` permite adaptar todos los textos del editor y `readonly` conserva la estructura sin permitir cambios. Cada edición actualiza el `v-model` y emite `change` con el árbol completo. + +El render recursivo vive en `DsLogicTreeNode.vue`, que es una implementación interna y no forma parte de la API pública. + +### Decisiones + +- `AND` y `OR` se almacenan como `and` y `or`; los textos visibles son configurables. +- Las condiciones usan cadenas para que la primera versión sea neutral respecto al dominio. +- Los nodos nuevos reciben UUID y los nodos proporcionados por el consumidor deben conservar IDs estables. +- El árbol solamente describe reglas. No evalúa condiciones ni genera SQL, JSON Logic u otra sintaxis. +- Los controles nativos mantienen nombres accesibles y el anidamiento se representa con listas y grupos. + +### Limitaciones conocidas + +- Todos los valores se editan como texto, sin tipos `number`, `boolean`, fecha o selección múltiple. +- No existe validación de campos vacíos, operadores incompatibles o grupos sin condiciones. +- Los nodos no pueden reordenarse ni moverse entre grupos. +- No hay operadores unarios como `is-empty` ni condiciones que omitan `value`. +- No se muestran resúmenes en lenguaje natural. +- No se incluye un evaluador ni adaptadores para backends. + +### Próximas extensiones + +1. Añadir validación sin cambiar el modelo serializable; los errores deben asociarse al control correspondiente. +2. Diseñar definiciones de campo tipadas con un `valueType` y opciones de valor. Hay que conservar compatibilidad con el esquema actual o introducir el cambio en una versión mayor. +3. Permitir comparadores que no requieran valor y editores personalizados mediante slots. +4. Reordenar condiciones y mover nodos entre grupos reutilizando un contrato de drag-and-drop estable. +5. Crear adaptadores separados para evaluar el árbol o convertirlo a formatos como JSON Logic. Estos adaptadores no deben acoplar el componente visual a un backend. +6. Añadir colapsado de grupos, resumen legible y deshacer/rehacer cuando el árbol crezca. + +## Checklist para futuras sesiones + +Antes de modificar estas herramientas: + +1. Revisa este documento, `src/types.ts` y las pruebas de `tests/data-tools.test.ts`. +2. Decide si el cambio es compatible con los modelos actuales y documenta cualquier migración. +3. Conserva una alternativa completa al arrastre para teclado y tecnologías asistivas. +4. Añade el caso nuevo al playground en `?section=tools`. +5. Actualiza `docs/components.md` si cambia la API pública. +6. Ejecuta `npm run check` y `npm run test:coverage`. + +## Fuera de alcance actual + +- Ejecución de reglas contra datos reales. +- Generación directa de SQL o llamadas a APIs. +- Colaboración en tiempo real. +- Persistencia automática. +- Un editor visual de flujos con nodos y conexiones libres. + +Estas capacidades requieren contratos y análisis de seguridad propios; no deben incorporarse como comportamiento implícito de los componentes visuales. diff --git a/docs/roadmap.md b/docs/roadmap.md index 5f4f84d..78cb0e3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -9,6 +9,17 @@ - Mejoras de formularios: grupos de campos, contador de caracteres y estados asíncronos. - Versionado semántico, changelog y política de deprecación antes de compartir el paquete ampliamente. +## Herramientas interactivas + +- Mejorar el indicador de inserción de `DsSortableList` y evaluar arrastre táctil con Pointer Events. +- Diseñar un contrato explícito para mover o copiar elementos entre listas. +- Añadir validación y valores tipados al árbol lógico sin romper su esquema serializable. +- Permitir reordenar condiciones y mover nodos entre grupos. +- Crear adaptadores independientes para evaluar o convertir reglas a formatos externos. +- Añadir pruebas de navegador real para drag-and-drop, teclado y árboles profundamente anidados. + +El contrato actual, sus restricciones y el orden recomendado de evolución se detallan en [Herramientas interactivas](interactive-tools.md). + ## Deliberadamente pospuesto - Tabla de datos interactiva. diff --git a/package-lock.json b/package-lock.json index 825f468..d690f7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@omaresquivel/design-system", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@omaresquivel/design-system", - "version": "0.1.0", + "version": "0.2.0", "devDependencies": { "@tailwindcss/vite": "^4.3.3", "@testing-library/vue": "^8.1.0", diff --git a/package.json b/package.json index 3c790ff..6e43213 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@omaresquivel/design-system", - "version": "0.1.0", + "version": "0.2.0", "private": false, "type": "module", "files": [ @@ -24,11 +24,13 @@ "typecheck": "vue-tsc --noEmit -p tsconfig.app.json", "test": "vitest run", "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "build:lib": "vite build", "build:playground": "vite build --config vite.playground.config.ts", "build": "npm run build:lib && npm run build:playground", "format": "prettier --write .", "format:check": "prettier --check .", + "check": "npm run typecheck && npm test && npm run format:check && npm run build", "prepublishOnly": "npm run typecheck && npm test && npm run build:lib" }, "peerDependencies": { diff --git a/playground/src/App.vue b/playground/src/App.vue index 3c56521..73ef04e 100644 --- a/playground/src/App.vue +++ b/playground/src/App.vue @@ -15,6 +15,7 @@ import { DsInline, DsInlineMessage, DsLoadingState, + DsLogicTree, DsPageHeader, DsPanel, DsPasswordInput, @@ -25,23 +26,34 @@ import { DsSelect, DsSettingsSection, DsSkeleton, + DsSortableList, DsSpinner, DsStack, DsSwitch, DsTabs, DsTextarea, DsTextInput, + type DsLogicGroup, + type DsSortableItem, type DsTabItem, } from "@omaresquivel/design-system"; const sections: DsTabItem[] = [ { id: "catalog", label: "Catálogo" }, + { id: "tools", label: "Herramientas" }, { id: "profile", label: "Perfil" }, { id: "settings", label: "Configuración" }, { id: "form", label: "Formulario largo" }, { id: "states", label: "Estados de panel" }, ]; -const currentSection = ref("catalog"); +const requestedSection = new URLSearchParams(window.location.search).get( + "section", +); +const currentSection = ref( + sections.some((section) => section.id === requestedSection) + ? (requestedSection ?? "catalog") + : "catalog", +); const name = ref("Omar Esquivel"); const email = ref("omar@example.com"); const password = ref("correct horse battery staple"); @@ -55,6 +67,63 @@ const terms = ref(false); const dialogOpen = ref(false); const confirmOpen = ref(false); const panelState = ref<"loading" | "error" | "empty" | "success">("success"); +const sortableItems = ref([ + { + id: "discovery", + label: "Descubrimiento", + description: "Entender el problema y las personas usuarias.", + }, + { + id: "design", + label: "Diseño", + description: "Proponer y validar una solución.", + }, + { + id: "delivery", + label: "Entrega", + description: "Construir, medir y aprender.", + }, +]); +const logicFields = [ + { label: "Estado", value: "status" }, + { label: "País", value: "country" }, + { label: "Plan", value: "plan" }, +]; +const logicTree = ref({ + id: "audience", + type: "group", + operator: "and", + children: [ + { + id: "active-users", + type: "condition", + field: "status", + comparison: "equals", + value: "active", + }, + { + id: "market", + type: "group", + operator: "or", + children: [ + { + id: "mexico", + type: "condition", + field: "country", + comparison: "equals", + value: "MX", + }, + { + id: "pro-plan", + type: "condition", + field: "plan", + comparison: "equals", + value: "pro", + }, + ], + }, + ], +}); const nameError = computed(() => name.value.length > 0 && name.value.length < 3 ? "Escribe al menos tres caracteres." @@ -261,6 +330,52 @@ const planOptions = [ + +