diff --git a/.atl/skill-registry.md b/.atl/skill-registry.md index 12adccb..22dc4d3 100644 --- a/.atl/skill-registry.md +++ b/.atl/skill-registry.md @@ -59,3 +59,25 @@ Generated: 2026-04-13 - Frontend: Vitest + React Testing Library β€” comando: `vitest` - Coverage backend: `dotnet test --collect:"XPlat Code Coverage"` - Coverage frontend: `vitest --coverage` + +### Design System (frontend) β€” v2.3 +- Source of truth: `Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.14 🎨 Design System.md`. Engram topic_key: `sig-cm2/design-system` +- Personality: tech sophisticated (Vercel/Linear/Railway). Glass + gradient mesh + multi-layer shadows + glow blobs corners +- Brand `#008fbe` (logo) β†’ escalado OKLCH `--brand-50..950`. **Violet accent** `oklch(0.62 0.20 280)` (`--accent-violet-*`) para combos tech. Neutral cool slate con shift hue 250-252 (`--neutral-50..950`) +- NO usar `gray-*`/`slate-*`/`blue-*` genΓ©ricos de Tailwind. Solo brand/neutral/violet/semantic +- Tokens semΓ‘nticos: `bg-background`, `text-foreground`, `bg-primary`, `bg-card`, `text-muted-foreground`, `border-border`, `ring-ring`, `bg-input` (con `border-input-border`). NUNCA hardcodear `bg-white`/`text-black`/hex inline +- Density compact: button 32-40px, input 40px (`h-10`), table row 40px. `--radius` base 10px (sm/md/lg/xl = 6/8/10/14) +- Light + Dark con default = system preference (`useTheme()` hook). Dark NO es pure black (slate-violet). Smoke test ambos antes de mergear +- Forms: ≀4 campos single col, β‰₯5 campos `grid grid-cols-1 md:grid-cols-2 gap-4` +- Tablas mobile: priority columns + tap-to-expand (NO cards-on-mobile, NO pure horizontal scroll) +- **Utilities CSS** (`@layer components` en index.css): `.glass`, `.gradient-mesh`, `.grid-bg` (usar en root layouts), `.surface` (tablas), `.focus-glow` +- **Card variants**: `default` (shadow-md) / `elevated` / `glass` / `flat` +- **Tooltips**: usar SIEMPRE `` de `@/components/ui/tooltip` (Radix Portal). NO CSS absolute en sidebars/modals β€” clipping issue +- **Sidebar**: colapsable con `useSidebar()` hook (persiste en localStorage). Toggle en top header al lado del brand +- **DataTable**: usar SIEMPRE `` de `@/components/ui/data-table` para tablas. NUNCA HTML `` crudo. Soporta `meta: { priority: 'high'|'medium'|'low' }` para responsive + tap-to-expand row mobile automΓ‘tico +- **shadcn MCP**: registrado globalmente (user scope). Pedirle a Claude que instale componentes shadcn β€” lo hace via MCP sin que el dev toque CLI. 22 componentes ya instalados +- Toasts via `sonner` (`` ya montado en `App.tsx`). `toast.success()` / `toast.error()` +- TooltipProvider ya envuelve App con `delayDuration={150}` +- Componentes shadcn: instalar via shadcn MCP server o `npx shadcn@latest add`. NUNCA copy-paste manual del website +- WCAG AA obligatorio: focus rings visibles (ya forzado en CSS base), contrast β‰₯ 4.5:1 texto normal, aria-label en botones icon-only +- Browser autofill fix ya aplicado en `@layer base` β€” respeta tokens del DS diff --git a/src/web/package-lock.json b/src/web/package-lock.json index db4dd82..95d4a76 100644 --- a/src/web/package-lock.json +++ b/src/web/package-lock.json @@ -11,18 +11,24 @@ "@fontsource/inter": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8", "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.99.0", + "@tanstack/react-table": "^8.21.3", "axios": "1.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "lucide-react": "^1.8.0", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -1530,12 +1536,102 @@ "node": ">=14" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -2309,6 +2405,99 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -2596,6 +2785,105 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", @@ -2723,6 +3011,96 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -3879,6 +4257,39 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -4873,6 +5284,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/src/web/package.json b/src/web/package.json index 2f0bde3..3a7d6ee 100644 --- a/src/web/package.json +++ b/src/web/package.json @@ -16,18 +16,24 @@ "@fontsource/inter": "^5.2.8", "@fontsource/jetbrains-mono": "^5.2.8", "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.99.0", + "@tanstack/react-table": "^8.21.3", "axios": "1.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "lucide-react": "^1.8.0", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/src/web/src/App.tsx b/src/web/src/App.tsx index 8286e98..d669adf 100644 --- a/src/web/src/App.tsx +++ b/src/web/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Toaster } from 'sonner' +import { TooltipProvider } from '@/components/ui/tooltip' import { AppRoutes } from './router' const queryClient = new QueryClient({ @@ -13,10 +14,12 @@ const queryClient = new QueryClient({ function App() { return ( - - - - + + + + + + ) } diff --git a/src/web/src/components/layout/AppHeader.tsx b/src/web/src/components/layout/AppHeader.tsx index 77b27d3..895b285 100644 --- a/src/web/src/components/layout/AppHeader.tsx +++ b/src/web/src/components/layout/AppHeader.tsx @@ -57,7 +57,7 @@ export function AppHeader() { NavegaciΓ³n - + diff --git a/src/web/src/components/layout/AppSidebar.tsx b/src/web/src/components/layout/AppSidebar.tsx index a46ac66..0ca5055 100644 --- a/src/web/src/components/layout/AppSidebar.tsx +++ b/src/web/src/components/layout/AppSidebar.tsx @@ -9,10 +9,14 @@ import { Users, ShieldCheck, KeyRound, + PanelLeftClose, + PanelLeftOpen, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useAuthStore } from '@/stores/authStore' +import { useSidebar } from '@/hooks/useSidebar' interface NavItem { label: string @@ -25,127 +29,209 @@ const navItems: NavItem[] = [ { label: 'Dashboard', href: '/', icon: LayoutDashboard }, { label: 'Ventas', href: '/ventas', icon: ShoppingCart, disabled: true }, { label: 'TasaciΓ³n', href: '/tasacion', icon: Calculator, disabled: true }, - { - label: 'Integraciones', - href: '/integraciones', - icon: Zap, - disabled: true, - }, - { - label: 'AdministraciΓ³n', - href: '/administracion', - icon: Settings, - disabled: true, - }, + { label: 'Integraciones', href: '/integraciones', icon: Zap, disabled: true }, + { label: 'AdministraciΓ³n', href: '/administracion', icon: Settings, disabled: true }, ] -export function SidebarNav() { +const adminItems: NavItem[] = [ + { label: 'Usuarios', href: '/usuarios', icon: Users }, + { label: 'Crear Usuario', href: '/usuarios/nuevo', icon: UserPlus }, + { label: 'Roles', href: '/admin/roles', icon: ShieldCheck }, + { label: 'Permisos', href: '/admin/permisos', icon: KeyRound }, +] + +interface SidebarNavProps { + /** When true forces expanded layout regardless of persisted state β€” used by mobile Sheet. */ + forceExpanded?: boolean +} + +export function SidebarNav({ forceExpanded = false }: SidebarNavProps) { const { pathname } = useLocation() const user = useAuthStore((s) => s.user) const isAdmin = user?.rol === 'admin' + const { collapsed: persisted, toggle } = useSidebar() + const collapsed = forceExpanded ? false : persisted + + function isItemActive(item: NavItem): boolean { + if (item.disabled) return false + if (item.href === '/') return pathname === '/' + if (item.href === '/usuarios') { + return pathname.startsWith('/usuarios') && pathname !== '/usuarios/nuevo' + } + if (item.href === '/usuarios/nuevo') return pathname === '/usuarios/nuevo' + return pathname.startsWith(item.href) + } return ( -
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {hasHiddenCols && ( + + ))} + + + {table.getRowModel().rows.map((row) => ( + + setExpandedId((prev) => (prev === row.id ? null : row.id)) + } + onRowClick={onRowClick} + hasHiddenCols={hasHiddenCols} + /> + ))} + +
+ + ) +} + +/* ──────────────────────────────────────────────────────────────── + Row con expand para mobile + ──────────────────────────────────────────────────────────────── */ + +interface DataTableRowProps { + row: Row + expanded: boolean + onToggleExpand: () => void + onRowClick?: (row: TData) => void + hasHiddenCols: boolean +} + +function DataTableRow({ + row, + expanded, + onToggleExpand, + onRowClick, + hasHiddenCols, +}: DataTableRowProps) { + const cells = row.getVisibleCells() + const visibleCellsCount = cells.length + (hasHiddenCols ? 1 : 0) + + // Cells that DO get hidden (medium/low priority) β€” needed for expanded panel + const hiddenCells = cells.filter((cell) => { + const p = cell.column.columnDef.meta?.priority + return p === 'medium' || p === 'low' + }) + + return ( + + onRowClick(row.original) : undefined} + > + {hasHiddenCols && ( + { + e.stopPropagation() + onToggleExpand() + }} + > + + + )} + {cells.map((cell) => { + const meta = cell.column.columnDef.meta + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) + })} + + + {/* Expanded panel β€” visible solo en mobile cuando hay cols ocultas */} + {expanded && hiddenCells.length > 0 && ( + + +
+ {hiddenCells.map((cell) => { + const header = cell.column.columnDef.header + return ( + +
+ {typeof header === 'string' ? header : cell.column.id} +
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ) + })} +
+
+
+ )} +
+ ) +} + +/* ──────────────────────────────────────────────────────────────── + Skeleton + ──────────────────────────────────────────────────────────────── */ + +function DataTableSkeleton({ columns, rows }: { columns: number; rows: number }) { + return ( +
+
+
+ {Array.from({ length: columns }).map((_, i) => ( + + ))} +
+ {Array.from({ length: rows }).map((_, i) => ( +
+ {Array.from({ length: columns }).map((_, j) => ( + + ))} +
+ ))} +
+
+ ) +} diff --git a/src/web/src/components/ui/dialog.tsx b/src/web/src/components/ui/dialog.tsx new file mode 100644 index 0000000..c680b9d --- /dev/null +++ b/src/web/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/web/src/components/ui/input.tsx b/src/web/src/components/ui/input.tsx index 54ed77b..46fa970 100644 --- a/src/web/src/components/ui/input.tsx +++ b/src/web/src/components/ui/input.tsx @@ -10,7 +10,11 @@ const Input = React.forwardRef( ) => ( +