From c488e2430de9591fc2a278b1b66a0d574ab93bfa Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 16 Apr 2026 10:46:07 -0300 Subject: [PATCH 1/9] feat(web): bootstrap design system con paleta brand El Dia - Reemplazo de tokens HSL por OKLCH (Tailwind 4 native) - Brand color #008fbe escalado a brand-50..950 - Neutral cool slate (complementa brand) - Semantic: success/warning/destructive como tokens - Background tinted off-white (no pure white) para warmth - Dark mode usa neutral-950 (no pure black) - Brand utilities expuestos via @theme inline (bg-brand-500, etc) - Focus rings con ring brand color - Selection con brand-200/800 Skill registry actualizado con compact rules de design system para auto-inyeccion en sub-agents. Source of truth: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.14 Design System.md Engram: sig-cm2/design-system --- .atl/skill-registry.md | 12 ++ src/web/src/index.css | 251 +++++++++++++++++++++++++++++------------ 2 files changed, 189 insertions(+), 74 deletions(-) diff --git a/.atl/skill-registry.md b/.atl/skill-registry.md index 12adccb..5888a3f 100644 --- a/.atl/skill-registry.md +++ b/.atl/skill-registry.md @@ -59,3 +59,15 @@ 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) +- Source of truth: `Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.14 🎨 Design System.md`. Engram topic_key: `sig-cm2/design-system` +- Brand `#008fbe` (logo) β†’ escalado OKLCH `--brand-50..950`. Neutral cool slate (`--neutral-50..950`). NO usar `gray-*`/`slate-*`/`blue-*` genΓ©ricos +- Tokens semΓ‘nticos: `bg-background`, `text-foreground`, `bg-primary`, `bg-card`, `text-muted-foreground`, `border-border`, `ring-ring`. NUNCA hardcodear `bg-white`/`text-black`/hex inline +- Density compact: button 32-36px, input 36px, table row 40px. Spacing mΓΊltiplos de 4 +- Light + Dark con default = system preference (`useTheme()` hook). Smoke test en 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) +- Toasts via `sonner` (`` ya montado en `App.tsx`). `toast.success()` / `toast.error()` +- 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 diff --git a/src/web/src/index.css b/src/web/src/index.css index ba57b19..f7c4014 100644 --- a/src/web/src/index.css +++ b/src/web/src/index.css @@ -3,113 +3,216 @@ @import "@fontsource/inter/400.css"; @import "@fontsource/inter/500.css"; @import "@fontsource/inter/600.css"; +@import "@fontsource/inter/700.css"; @import "@fontsource/jetbrains-mono/400.css"; +/* ================================================================ + SIG-CM 2.0 Design System β€” Tokens + Source of truth: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.14 🎨 Design System.md + Brand color: #008fbe (logo El DΓ­a) β€” escalado OKLCH + ================================================================ */ + @layer base { :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 5.9% 10%; - --radius: 0.5rem; - --font-sans: "Inter", system-ui, sans-serif; - --font-mono: "JetBrains Mono", monospace; + /* ── Brand (cyan-blue around logo #008fbe) ─────────────────── */ + --brand-50: oklch(0.972 0.013 220); + --brand-100: oklch(0.935 0.035 220); + --brand-200: oklch(0.870 0.072 220); + --brand-300: oklch(0.780 0.108 220); + --brand-400: oklch(0.690 0.128 220); + --brand-500: oklch(0.625 0.130 220); /* logo #008fbe */ + --brand-600: oklch(0.550 0.128 222); + --brand-700: oklch(0.460 0.110 224); + --brand-800: oklch(0.360 0.082 226); + --brand-900: oklch(0.280 0.058 228); + --brand-950: oklch(0.200 0.040 230); + + /* ── Neutral (cool slate, complementa el blue) ─────────────── */ + --neutral-50: oklch(0.985 0.003 240); + --neutral-100: oklch(0.965 0.005 240); + --neutral-200: oklch(0.918 0.008 240); + --neutral-300: oklch(0.850 0.012 240); + --neutral-400: oklch(0.690 0.015 240); + --neutral-500: oklch(0.550 0.018 240); + --neutral-600: oklch(0.450 0.018 240); + --neutral-700: oklch(0.360 0.015 240); + --neutral-800: oklch(0.270 0.013 240); + --neutral-900: oklch(0.210 0.010 240); + --neutral-950: oklch(0.150 0.008 240); + + /* ── Semantic ──────────────────────────────────────────────── */ + --success: oklch(0.640 0.155 145); + --success-foreground: oklch(0.990 0.000 0); + --warning: oklch(0.760 0.150 75); + --warning-foreground: oklch(0.220 0.050 75); + + /* ── shadcn semantic mapping (LIGHT) ───────────────────────── */ + --background: oklch(0.992 0.003 220); /* off-white tinted brand */ + --foreground: var(--neutral-900); + + --card: oklch(1 0 0); /* pure white over tinted bg */ + --card-foreground: var(--neutral-900); + + --popover: oklch(1 0 0); + --popover-foreground: var(--neutral-900); + + --primary: var(--brand-600); /* WCAG AA on white */ + --primary-foreground: oklch(0.990 0.000 0); + + --secondary: var(--neutral-100); + --secondary-foreground: var(--neutral-800); + + --muted: var(--neutral-100); + --muted-foreground: var(--neutral-500); + + --accent: var(--brand-50); /* hover navigation, subtle brand tint */ + --accent-foreground: var(--brand-700); + + --destructive: oklch(0.600 0.220 25); + --destructive-foreground: oklch(0.990 0.000 0); + + --border: var(--neutral-200); + --input: var(--neutral-200); + --ring: var(--brand-500); + + /* ── Spacing & shape ──────────────────────────────────────── */ + --radius: 0.5rem; /* 8px */ + + /* ── Typography ───────────────────────────────────────────── */ + --font-sans: "Inter", system-ui, -apple-system, sans-serif; + --font-mono: "JetBrains Mono", "Cascadia Code", Consolas, monospace; } .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; + --background: var(--neutral-950); + --foreground: var(--neutral-50); + + --card: var(--neutral-900); + --card-foreground: var(--neutral-50); + + --popover: var(--neutral-900); + --popover-foreground: var(--neutral-50); + + --primary: var(--brand-400); /* lighter on dark */ + --primary-foreground: var(--neutral-950); + + --secondary: var(--neutral-800); + --secondary-foreground: var(--neutral-100); + + --muted: var(--neutral-800); + --muted-foreground: var(--neutral-400); + + --accent: var(--neutral-800); + --accent-foreground: var(--brand-300); + + --destructive: oklch(0.580 0.190 25); + --destructive-foreground: oklch(0.990 0.000 0); + + --border: var(--neutral-800); + --input: var(--neutral-800); + --ring: var(--brand-400); } } @layer base { * { - border-color: hsl(var(--border)); + border-color: var(--border); + } + + html { + /* Smooth color transitions when toggling theme */ + color-scheme: light dark; } body { margin: 0; - background-color: hsl(var(--background)); - color: hsl(var(--foreground)); + background-color: var(--background); + color: var(--foreground); font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } #root { min-height: 100svh; } - h1, - h2, - h3, - h4, - h5, - h6 { - font-family: var(--font-sans); + /* Selection color uses brand */ + ::selection { + background-color: var(--brand-200); + color: var(--brand-900); + } + .dark ::selection { + background-color: var(--brand-800); + color: var(--brand-50); } - code, - pre, - kbd, - samp { + /* Focus visible β€” accessible ring usando brand */ + *:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + } + + h1, h2, h3, h4, h5, h6 { + font-family: var(--font-sans); + font-weight: 600; + letter-spacing: -0.01em; + } + + code, pre, kbd, samp { font-family: var(--font-mono); } } +/* ================================================================ + Tailwind 4 inline theme β€” expone tokens como utility classes + Cada token aquΓ­ queda disponible como Tailwind utility + (bg-background, text-foreground, border-border, etc.) + ================================================================ */ @theme inline { - --color-background: hsl(var(--background)); - --color-foreground: hsl(var(--foreground)); - --color-card: hsl(var(--card)); - --color-card-foreground: hsl(var(--card-foreground)); - --color-popover: hsl(var(--popover)); - --color-popover-foreground: hsl(var(--popover-foreground)); - --color-primary: hsl(var(--primary)); - --color-primary-foreground: hsl(var(--primary-foreground)); - --color-secondary: hsl(var(--secondary)); - --color-secondary-foreground: hsl(var(--secondary-foreground)); - --color-muted: hsl(var(--muted)); - --color-muted-foreground: hsl(var(--muted-foreground)); - --color-accent: hsl(var(--accent)); - --color-accent-foreground: hsl(var(--accent-foreground)); - --color-destructive: hsl(var(--destructive)); - --color-destructive-foreground: hsl(var(--destructive-foreground)); - --color-border: hsl(var(--border)); - --color-input: hsl(var(--input)); - --color-ring: hsl(var(--ring)); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + /* Brand scale (utilities tipo bg-brand-500, text-brand-700, etc.) */ + --color-brand-50: var(--brand-50); + --color-brand-100: var(--brand-100); + --color-brand-200: var(--brand-200); + --color-brand-300: var(--brand-300); + --color-brand-400: var(--brand-400); + --color-brand-500: var(--brand-500); + --color-brand-600: var(--brand-600); + --color-brand-700: var(--brand-700); + --color-brand-800: var(--brand-800); + --color-brand-900: var(--brand-900); + --color-brand-950: var(--brand-950); + + /* Semantic */ + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); + --font-sans: var(--font-sans); --font-mono: var(--font-mono); } -- 2.49.1 From 6e6c729bacebf6e241385861bf37c36f77981030 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 16 Apr 2026 11:02:59 -0300 Subject: [PATCH 2/9] =?UTF-8?q?feat(web):=20design=20system=20v2=20?= =?UTF-8?q?=E2=80=94=20tech=20sophisticated=20con=20glass=20+=20gradient?= =?UTF-8?q?=20mesh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambios principales: - Agregado violet accent (oklch 0.62 0.20 280) para combo tech con brand cyan - Neutrals con shift sutil hacia hue 250 (slate-violet) - Dark mode con bg oklch(0.135 0.018 252) β€” no pure black, feel mas tech - Inputs con token --input propio (white en light, elevado en dark) y --input-border mas prominente. Fixea problema de input gris feo - Card soporta variant glass/elevated/default - Multi-layer shadows reales (shadow-sm/md/lg/xl/glow) - Gradient mesh utility (.gradient-mesh + token --gradient-mesh) - Clase .glass para glassmorphism (backdrop-blur 20px + saturate 180%) - Border radius default 10px (era 8px) β€” mas moderno - Headings con tracking-tight -0.015em LoginPage redesigned: - PublicLayout con gradient mesh + 2 glow blobs (brand+violet) + grid sutil - Card variant glass para el form - Logo mark con bg-gradient-to-br from-brand-500 to-violet-500 - Inputs con bg propio + ring brand glow al focus Tests: 136/136 verde. Doc Obsidian 2.14 actualizado v2.0. Engram sig-cm2/design-system actualizado. --- src/web/src/components/ui/card.tsx | 40 ++- src/web/src/components/ui/input.tsx | 6 +- src/web/src/features/auth/pages/LoginPage.tsx | 18 +- src/web/src/index.css | 227 ++++++++++++------ src/web/src/layouts/PublicLayout.tsx | 22 +- 5 files changed, 215 insertions(+), 98 deletions(-) diff --git a/src/web/src/components/ui/card.tsx b/src/web/src/components/ui/card.tsx index c5d18d4..3f2d85d 100644 --- a/src/web/src/components/ui/card.tsx +++ b/src/web/src/components/ui/card.tsx @@ -1,20 +1,34 @@ import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) +const cardVariants = cva('rounded-xl text-card-foreground transition-shadow', { + variants: { + variant: { + default: 'border border-border bg-card shadow-sm', + elevated: 'border border-border bg-card shadow-lg', + glass: 'glass shadow-xl', + }, + }, + defaultVariants: { + variant: 'default', + }, +}) + +export interface CardProps + extends React.HTMLAttributes, + VariantProps {} + +const Card = React.forwardRef( + ({ className, variant, ...props }, ref) => ( +
+ ), +) Card.displayName = 'Card' const CardHeader = React.forwardRef< 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( - - SIG-CM 2.0 - - Iniciar sesiΓ³n + + + {/* Brand mark */} +
+ +
+ SIG-CM 2.0 + + Sistema de gestiΓ³n comercial Β· El DΓ­a
- + {errorMessage && ( diff --git a/src/web/src/index.css b/src/web/src/index.css index f7c4014..db20bdb 100644 --- a/src/web/src/index.css +++ b/src/web/src/index.css @@ -7,110 +7,155 @@ @import "@fontsource/jetbrains-mono/400.css"; /* ================================================================ - SIG-CM 2.0 Design System β€” Tokens + SIG-CM 2.0 Design System v2.0 β€” Tokens Source of truth: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.14 🎨 Design System.md - Brand color: #008fbe (logo El DΓ­a) β€” escalado OKLCH + Brand: #008fbe (logo) + violet accent β†’ tech sophistication + Style: dark-first elegante, glassmorphism, multi-layer shadows ================================================================ */ @layer base { :root { - /* ── Brand (cyan-blue around logo #008fbe) ─────────────────── */ + /* ── Brand cyan (logo #008fbe) ──────────────────────────── */ --brand-50: oklch(0.972 0.013 220); --brand-100: oklch(0.935 0.035 220); --brand-200: oklch(0.870 0.072 220); --brand-300: oklch(0.780 0.108 220); - --brand-400: oklch(0.690 0.128 220); + --brand-400: oklch(0.700 0.135 220); --brand-500: oklch(0.625 0.130 220); /* logo #008fbe */ - --brand-600: oklch(0.550 0.128 222); - --brand-700: oklch(0.460 0.110 224); - --brand-800: oklch(0.360 0.082 226); - --brand-900: oklch(0.280 0.058 228); - --brand-950: oklch(0.200 0.040 230); + --brand-600: oklch(0.530 0.135 222); + --brand-700: oklch(0.440 0.115 224); + --brand-800: oklch(0.350 0.085 226); + --brand-900: oklch(0.270 0.060 228); + --brand-950: oklch(0.190 0.040 230); - /* ── Neutral (cool slate, complementa el blue) ─────────────── */ - --neutral-50: oklch(0.985 0.003 240); - --neutral-100: oklch(0.965 0.005 240); - --neutral-200: oklch(0.918 0.008 240); - --neutral-300: oklch(0.850 0.012 240); - --neutral-400: oklch(0.690 0.015 240); - --neutral-500: oklch(0.550 0.018 240); - --neutral-600: oklch(0.450 0.018 240); - --neutral-700: oklch(0.360 0.015 240); - --neutral-800: oklch(0.270 0.013 240); - --neutral-900: oklch(0.210 0.010 240); - --neutral-950: oklch(0.150 0.008 240); + /* ── Violet accent (combo tech con brand cyan) ──────────── */ + --accent-violet-400: oklch(0.700 0.180 280); + --accent-violet-500: oklch(0.620 0.200 280); + --accent-violet-600: oklch(0.530 0.205 280); - /* ── Semantic ──────────────────────────────────────────────── */ - --success: oklch(0.640 0.155 145); - --success-foreground: oklch(0.990 0.000 0); - --warning: oklch(0.760 0.150 75); - --warning-foreground: oklch(0.220 0.050 75); + /* ── Neutral (slate con shift sutil hacia azul/violeta) ── */ + --neutral-50: oklch(0.985 0.004 250); + --neutral-100: oklch(0.962 0.007 250); + --neutral-200: oklch(0.910 0.012 250); + --neutral-300: oklch(0.835 0.018 250); + --neutral-400: oklch(0.680 0.022 250); + --neutral-500: oklch(0.540 0.025 250); + --neutral-600: oklch(0.435 0.025 250); + --neutral-700: oklch(0.345 0.022 250); + --neutral-800: oklch(0.250 0.020 250); + --neutral-900: oklch(0.185 0.015 250); + --neutral-950: oklch(0.130 0.013 250); - /* ── shadcn semantic mapping (LIGHT) ───────────────────────── */ - --background: oklch(0.992 0.003 220); /* off-white tinted brand */ - --foreground: var(--neutral-900); + /* ── Semantic ──────────────────────────────────────────── */ + --success: oklch(0.640 0.155 145); + --success-foreground: oklch(0.990 0.000 0); + --warning: oklch(0.760 0.150 75); + --warning-foreground: oklch(0.220 0.050 75); - --card: oklch(1 0 0); /* pure white over tinted bg */ - --card-foreground: var(--neutral-900); + /* ── shadcn semantic mapping (LIGHT) ─────────────────── */ + --background: oklch(0.988 0.003 250); /* slate barely warm */ + --foreground: var(--neutral-900); - --popover: oklch(1 0 0); - --popover-foreground: var(--neutral-900); + --card: oklch(1 0 0); /* pure white */ + --card-foreground: var(--neutral-900); - --primary: var(--brand-600); /* WCAG AA on white */ - --primary-foreground: oklch(0.990 0.000 0); + --popover: oklch(1 0 0); + --popover-foreground: var(--neutral-900); - --secondary: var(--neutral-100); - --secondary-foreground: var(--neutral-800); + --primary: var(--brand-600); /* WCAG AA on white */ + --primary-foreground: oklch(0.990 0.000 0); - --muted: var(--neutral-100); - --muted-foreground: var(--neutral-500); + --secondary: var(--neutral-100); + --secondary-foreground: var(--neutral-800); - --accent: var(--brand-50); /* hover navigation, subtle brand tint */ - --accent-foreground: var(--brand-700); + --muted: var(--neutral-100); + --muted-foreground: var(--neutral-500); - --destructive: oklch(0.600 0.220 25); + --accent: var(--brand-50); + --accent-foreground: var(--brand-700); + + --destructive: oklch(0.600 0.220 25); --destructive-foreground: oklch(0.990 0.000 0); - --border: var(--neutral-200); - --input: var(--neutral-200); - --ring: var(--brand-500); + --border: var(--neutral-200); + --input: oklch(1 0 0); /* WHITE inputs en light, no gris */ + --input-border: var(--neutral-300); /* border mΓ‘s prominente */ + --ring: var(--brand-500); - /* ── Spacing & shape ──────────────────────────────────────── */ - --radius: 0.5rem; /* 8px */ + /* ── Glass surfaces (backdrop-blur) ───────────────────── */ + --glass-bg: oklch(1 0 0 / 0.7); + --glass-border: oklch(1 0 0 / 0.4); - /* ── Typography ───────────────────────────────────────────── */ + /* ── Gradient backgrounds (hero, login) ───────────────── */ + --gradient-mesh: + radial-gradient(at 100% 0%, oklch(0.625 0.130 220 / 0.12) 0px, transparent 50%), + radial-gradient(at 0% 100%, oklch(0.620 0.200 280 / 0.10) 0px, transparent 50%), + radial-gradient(at 50% 50%, oklch(0.700 0.135 220 / 0.05) 0px, transparent 60%); + + /* ── Shadows (multi-layer para depth real) ────────────── */ + --shadow-sm: 0 1px 2px 0 oklch(0.185 0.015 250 / 0.05); + --shadow: 0 1px 3px 0 oklch(0.185 0.015 250 / 0.08), 0 1px 2px -1px oklch(0.185 0.015 250 / 0.06); + --shadow-md: 0 4px 8px -2px oklch(0.185 0.015 250 / 0.08), 0 2px 4px -2px oklch(0.185 0.015 250 / 0.04); + --shadow-lg: 0 12px 24px -6px oklch(0.185 0.015 250 / 0.12), 0 4px 8px -3px oklch(0.185 0.015 250 / 0.06); + --shadow-xl: 0 24px 48px -12px oklch(0.185 0.015 250 / 0.18), 0 8px 16px -8px oklch(0.185 0.015 250 / 0.08); + --shadow-glow: 0 0 0 1px oklch(0.625 0.130 220 / 0.10), 0 0 24px -4px oklch(0.625 0.130 220 / 0.20); + + /* ── Spacing & shape ───────────────────────────────────── */ + --radius: 0.625rem; /* 10px */ + + /* ── Typography ────────────────────────────────────────── */ --font-sans: "Inter", system-ui, -apple-system, sans-serif; --font-mono: "JetBrains Mono", "Cascadia Code", Consolas, monospace; } .dark { - --background: var(--neutral-950); - --foreground: var(--neutral-50); + /* Background con shift sutil hacia violet β†’ "tech" feel */ + --background: oklch(0.135 0.018 252); /* deep slate-violet */ + --foreground: var(--neutral-50); - --card: var(--neutral-900); - --card-foreground: var(--neutral-50); + --card: oklch(0.180 0.020 252); /* elevaciΓ³n visual */ + --card-foreground: var(--neutral-50); - --popover: var(--neutral-900); - --popover-foreground: var(--neutral-50); + --popover: oklch(0.200 0.022 252); + --popover-foreground: var(--neutral-50); - --primary: var(--brand-400); /* lighter on dark */ - --primary-foreground: var(--neutral-950); + --primary: var(--brand-400); /* brighter en dark, "neon" feel */ + --primary-foreground: oklch(0.130 0.013 250); - --secondary: var(--neutral-800); - --secondary-foreground: var(--neutral-100); + --secondary: oklch(0.230 0.020 252); + --secondary-foreground: var(--neutral-100); - --muted: var(--neutral-800); - --muted-foreground: var(--neutral-400); + --muted: oklch(0.220 0.020 252); + --muted-foreground: var(--neutral-400); - --accent: var(--neutral-800); - --accent-foreground: var(--brand-300); + --accent: oklch(0.250 0.030 252); + --accent-foreground: var(--brand-300); - --destructive: oklch(0.580 0.190 25); + --destructive: oklch(0.580 0.190 25); --destructive-foreground: oklch(0.990 0.000 0); - --border: var(--neutral-800); - --input: var(--neutral-800); - --ring: var(--brand-400); + --border: oklch(1 0 0 / 0.10); /* sutil glass-style border */ + --input: oklch(0.215 0.020 252); /* elevado del bg, contrast con texto */ + --input-border: oklch(1 0 0 / 0.14); + --ring: var(--brand-400); + + /* Glass para dark */ + --glass-bg: oklch(0.180 0.020 252 / 0.6); + --glass-border: oklch(1 0 0 / 0.10); + + /* Gradient mesh dark mode β€” mΓ‘s vibrante */ + --gradient-mesh: + radial-gradient(at 100% 0%, oklch(0.625 0.130 220 / 0.20) 0px, transparent 50%), + radial-gradient(at 0% 100%, oklch(0.620 0.200 280 / 0.18) 0px, transparent 50%), + radial-gradient(at 50% 50%, oklch(0.700 0.135 220 / 0.08) 0px, transparent 60%); + + /* Shadows mΓ‘s intensas en dark */ + --shadow-sm: 0 1px 2px 0 oklch(0 0 0 / 0.30); + --shadow: 0 2px 4px 0 oklch(0 0 0 / 0.40), 0 1px 2px -1px oklch(0 0 0 / 0.30); + --shadow-md: 0 4px 8px -2px oklch(0 0 0 / 0.40), 0 2px 4px -2px oklch(0 0 0 / 0.30); + --shadow-lg: 0 12px 24px -6px oklch(0 0 0 / 0.50), 0 4px 8px -3px oklch(0 0 0 / 0.30); + --shadow-xl: 0 24px 48px -12px oklch(0 0 0 / 0.60), 0 8px 16px -8px oklch(0 0 0 / 0.30); + --shadow-glow: 0 0 0 1px oklch(0.700 0.135 220 / 0.30), 0 0 32px -4px oklch(0.700 0.135 220 / 0.40); } } @@ -120,7 +165,6 @@ } html { - /* Smooth color transitions when toggling theme */ color-scheme: light dark; } @@ -137,17 +181,15 @@ min-height: 100svh; } - /* Selection color uses brand */ ::selection { background-color: var(--brand-200); color: var(--brand-900); } .dark ::selection { - background-color: var(--brand-800); + background-color: var(--brand-700); color: var(--brand-50); } - /* Focus visible β€” accessible ring usando brand */ *:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; @@ -156,7 +198,7 @@ h1, h2, h3, h4, h5, h6 { font-family: var(--font-sans); font-weight: 600; - letter-spacing: -0.01em; + letter-spacing: -0.015em; } code, pre, kbd, samp { @@ -165,9 +207,30 @@ } /* ================================================================ - Tailwind 4 inline theme β€” expone tokens como utility classes - Cada token aquΓ­ queda disponible como Tailwind utility - (bg-background, text-foreground, border-border, etc.) + Glass surface utility + Uso:
…
+ ================================================================ */ +@layer components { + .glass { + background-color: var(--glass-bg); + backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); + border: 1px solid var(--glass-border); + } + + .gradient-mesh { + background-image: var(--gradient-mesh); + } + + /* Brand glow para focus en inputs crΓ­ticos */ + .focus-glow:focus-visible { + box-shadow: var(--shadow-glow); + outline: none; + } +} + +/* ================================================================ + Tailwind 4 inline theme β€” utilidades disponibles globalmente ================================================================ */ @theme inline { --color-background: var(--background); @@ -188,9 +251,10 @@ --color-destructive-foreground: var(--destructive-foreground); --color-border: var(--border); --color-input: var(--input); + --color-input-border: var(--input-border); --color-ring: var(--ring); - /* Brand scale (utilities tipo bg-brand-500, text-brand-700, etc.) */ + /* Brand */ --color-brand-50: var(--brand-50); --color-brand-100: var(--brand-100); --color-brand-200: var(--brand-200); @@ -203,6 +267,11 @@ --color-brand-900: var(--brand-900); --color-brand-950: var(--brand-950); + /* Violet accent */ + --color-violet-400: var(--accent-violet-400); + --color-violet-500: var(--accent-violet-500); + --color-violet-600: var(--accent-violet-600); + /* Semantic */ --color-success: var(--success); --color-success-foreground: var(--success-foreground); @@ -212,6 +281,14 @@ --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-glow: var(--shadow-glow); --font-sans: var(--font-sans); --font-mono: var(--font-mono); diff --git a/src/web/src/layouts/PublicLayout.tsx b/src/web/src/layouts/PublicLayout.tsx index 11731a8..fc4232a 100644 --- a/src/web/src/layouts/PublicLayout.tsx +++ b/src/web/src/layouts/PublicLayout.tsx @@ -6,8 +6,26 @@ interface PublicLayoutProps { export function PublicLayout({ children }: PublicLayoutProps) { return ( -
- {children} +
+ {/* Gradient mesh background β€” subtle radial blobs (brand cyan + violet) */} +
+ + {/* Subtle grid texture overlay (visible on dark, fades on light) */} +
+ + {/* Brand glow accents (positioned blobs) */} +
+
+ + {/* Content */} +
{children}
) } -- 2.49.1 From 3bc2625e21b3b0056bce080b8adfa429d2899cf7 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 16 Apr 2026 11:05:39 -0300 Subject: [PATCH 3/9] fix(web): agregar ThemeToggle en PublicLayout (login) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El ThemeToggle solo vivia en AppHeader (ProtectedLayout), por lo que desde /login era imposible cambiar el tema. Movido a esquina superior derecha con z-index 20 sobre el gradient mesh. useTheme defaultea a system preference, pero el usuario tiene que poder override desde cualquier pantalla β€” incluido el login. --- src/web/src/layouts/PublicLayout.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/web/src/layouts/PublicLayout.tsx b/src/web/src/layouts/PublicLayout.tsx index fc4232a..104641c 100644 --- a/src/web/src/layouts/PublicLayout.tsx +++ b/src/web/src/layouts/PublicLayout.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from 'react' +import { ThemeToggle } from '@/components/layout/ThemeToggle' interface PublicLayoutProps { children: ReactNode @@ -24,6 +25,11 @@ export function PublicLayout({ children }: PublicLayoutProps) {
+ {/* Theme toggle β€” top-right, glass over gradient */} +
+ +
+ {/* Content */}
{children}
-- 2.49.1 From 278e1cf3787e8790520d5ecd3d34361b64008b2f Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 16 Apr 2026 11:10:06 -0300 Subject: [PATCH 4/9] fix(web): light mode profundidad + grid global + autofill fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambios: - index.css: fix de browser autofill (Chrome/Safari forzaba bg amarillo + texto blanco que rompia contraste). Override -webkit-text-fill-color + box-shadow inset para mantener tokens del DS. Esta era la causa real de las 'letras blancas en gris' que se veian en login. - index.css: utility .grid-bg global (7% opacity light, 10% dark) β€” para usar como fondo cuadriculado en todos los layouts. - PublicLayout: agrego .grid-bg layer + bg-background explicito + glow blobs mas intensos (25%/20% en light vs 10% antes). Light ahora tiene la misma profundidad visual que dark. - ProtectedLayout: agrego .grid-bg + glow blobs sutiles en corners para dar profundidad al dashboard y todas las secciones internas. Resalta futuros componentes glass. Tests: 136/136 verde. --- src/web/src/index.css | 30 +++++++++++++++++++++++++ src/web/src/layouts/ProtectedLayout.tsx | 13 ++++++++--- src/web/src/layouts/PublicLayout.tsx | 23 +++++++------------ 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/web/src/index.css b/src/web/src/index.css index db20bdb..a3f7cee 100644 --- a/src/web/src/index.css +++ b/src/web/src/index.css @@ -204,6 +204,21 @@ code, pre, kbd, samp { font-family: var(--font-mono); } + + /* ── Browser autofill (Chrome/Safari): forzar tokens del DS ── + Sin esto, autofill aplica bg amarillo + texto blanco que se ve horrible + y rompe contraste con cualquier paleta. */ + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + input:-webkit-autofill:active, + textarea:-webkit-autofill, + select:-webkit-autofill { + -webkit-text-fill-color: var(--foreground) !important; + -webkit-box-shadow: 0 0 0 1000px var(--input) inset !important; + caret-color: var(--foreground) !important; + transition: background-color 5000s ease-in-out 0s; + } } /* ================================================================ @@ -222,6 +237,21 @@ background-image: var(--gradient-mesh); } + /* Grid texture overlay β€” usar como fondo decorativo en surfaces grandes + Light: 2.5% opacity (sutil pero visible). Dark: 5% (mΓ‘s presente) */ + .grid-bg { + background-image: + linear-gradient(to right, currentColor 1px, transparent 1px), + linear-gradient(to bottom, currentColor 1px, transparent 1px); + background-size: 56px 56px; + color: var(--neutral-500); + opacity: 0.07; + } + .dark .grid-bg { + color: var(--neutral-400); + opacity: 0.10; + } + /* Brand glow para focus en inputs crΓ­ticos */ .focus-glow:focus-visible { box-shadow: var(--shadow-glow); diff --git a/src/web/src/layouts/ProtectedLayout.tsx b/src/web/src/layouts/ProtectedLayout.tsx index 377047d..a5a5870 100644 --- a/src/web/src/layouts/ProtectedLayout.tsx +++ b/src/web/src/layouts/ProtectedLayout.tsx @@ -8,14 +8,21 @@ interface ProtectedLayoutProps { export function ProtectedLayout({ children }: ProtectedLayoutProps) { return ( -
+
+ {/* Grid texture β€” fondo cuadriculado para dar profundidad y resaltar glass */} +
+ + {/* Subtle brand/violet glow accents β€” corners */} +
+
+ {/* Desktop sidebar */} -
+
{/* Main column */} -
+
{children}
diff --git a/src/web/src/layouts/PublicLayout.tsx b/src/web/src/layouts/PublicLayout.tsx index 104641c..fb0931f 100644 --- a/src/web/src/layouts/PublicLayout.tsx +++ b/src/web/src/layouts/PublicLayout.tsx @@ -7,23 +7,16 @@ interface PublicLayoutProps { export function PublicLayout({ children }: PublicLayoutProps) { return ( -
- {/* Gradient mesh background β€” subtle radial blobs (brand cyan + violet) */} +
+ {/* Grid texture β€” sutil en light, mΓ‘s presente en dark */} +
+ + {/* Gradient mesh con radial blobs (brand cyan + violet) */}
- {/* Subtle grid texture overlay (visible on dark, fades on light) */} -
- - {/* Brand glow accents (positioned blobs) */} -
-
+ {/* Brand glow accents β€” mΓ‘s intensos para que la profundidad funcione en light tambiΓ©n */} +
+
{/* Theme toggle β€” top-right, glass over gradient */}
-- 2.49.1 From 41b6882b5cbdddd12b913b085f012a07619a8c22 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 16 Apr 2026 11:15:36 -0300 Subject: [PATCH 5/9] feat(web): mas contraste cards/tables sobre bg + utility .surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambios de tokens: - Light --background: 0.988 -> 0.962 (slate cool, hace pop el card white) - Dark --background: 0.135 -> 0.130 (mas oscuro) - Dark --card: 0.180 -> 0.220 (salto +0.090 vs bg, antes solo +0.045) - Dark --popover: 0.200 -> 0.245 (popovers/dropdowns aun mas elevados) - Dark --secondary/muted/accent/input: bumpeados al nivel correspondiente para que la jerarquia visual mantenga proporciones Card variants: - default: shadow-sm -> shadow-md (mas elevacion default) - nuevo variant 'flat' (sin shadow) para cuando se necesite Nueva utility .surface: - Mismo treatment visual que pero como clase CSS β€” para contenedores que no usan el componente Card (ej: tablas, listas custom). bg-card + border + radius + shadow-md. UsersTable refactorizado para usar .surface en lugar de border manual. Cualquier futura tabla/lista usa .surface por consistencia. Tests 136/136 verde. --- src/web/src/components/ui/card.tsx | 3 +- .../features/users/components/UsersTable.tsx | 4 +-- src/web/src/index.css | 28 +++++++++++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/web/src/components/ui/card.tsx b/src/web/src/components/ui/card.tsx index 3f2d85d..fd7f0de 100644 --- a/src/web/src/components/ui/card.tsx +++ b/src/web/src/components/ui/card.tsx @@ -6,9 +6,10 @@ import { cn } from '@/lib/utils' const cardVariants = cva('rounded-xl text-card-foreground transition-shadow', { variants: { variant: { - default: 'border border-border bg-card shadow-sm', + default: 'border border-border bg-card shadow-md', elevated: 'border border-border bg-card shadow-lg', glass: 'glass shadow-xl', + flat: 'border border-border bg-card', }, }, defaultVariants: { diff --git a/src/web/src/features/users/components/UsersTable.tsx b/src/web/src/features/users/components/UsersTable.tsx index 11f6151..f51e780 100644 --- a/src/web/src/features/users/components/UsersTable.tsx +++ b/src/web/src/features/users/components/UsersTable.tsx @@ -25,10 +25,10 @@ export function UsersTable({ rows, onRowClick }: UsersTableProps) { } return ( -
+
- + diff --git a/src/web/src/index.css b/src/web/src/index.css index a3f7cee..afca6c3 100644 --- a/src/web/src/index.css +++ b/src/web/src/index.css @@ -53,10 +53,10 @@ --warning-foreground: oklch(0.220 0.050 75); /* ── shadcn semantic mapping (LIGHT) ─────────────────── */ - --background: oklch(0.988 0.003 250); /* slate barely warm */ + --background: oklch(0.962 0.006 250); /* slate cool β€” pop con cards white */ --foreground: var(--neutral-900); - --card: oklch(1 0 0); /* pure white */ + --card: oklch(1 0 0); /* pure white β€” mΓ‘ximo contraste */ --card-foreground: var(--neutral-900); --popover: oklch(1 0 0); @@ -110,32 +110,32 @@ .dark { /* Background con shift sutil hacia violet β†’ "tech" feel */ - --background: oklch(0.135 0.018 252); /* deep slate-violet */ + --background: oklch(0.130 0.018 252); /* deep slate-violet β€” mΓ‘s oscuro para contraste */ --foreground: var(--neutral-50); - --card: oklch(0.180 0.020 252); /* elevaciΓ³n visual */ + --card: oklch(0.220 0.024 252); /* elevaciΓ³n visual marcada β€” pop sobre bg */ --card-foreground: var(--neutral-50); - --popover: oklch(0.200 0.022 252); + --popover: oklch(0.245 0.024 252); /* dropdowns/popovers mΓ‘s elevados aΓΊn */ --popover-foreground: var(--neutral-50); --primary: var(--brand-400); /* brighter en dark, "neon" feel */ --primary-foreground: oklch(0.130 0.013 250); - --secondary: oklch(0.230 0.020 252); + --secondary: oklch(0.255 0.022 252); --secondary-foreground: var(--neutral-100); - --muted: oklch(0.220 0.020 252); + --muted: oklch(0.245 0.022 252); --muted-foreground: var(--neutral-400); - --accent: oklch(0.250 0.030 252); + --accent: oklch(0.275 0.032 252); --accent-foreground: var(--brand-300); --destructive: oklch(0.580 0.190 25); --destructive-foreground: oklch(0.990 0.000 0); --border: oklch(1 0 0 / 0.10); /* sutil glass-style border */ - --input: oklch(0.215 0.020 252); /* elevado del bg, contrast con texto */ + --input: oklch(0.245 0.022 252); /* elevado, mismo nivel que muted */ --input-border: oklch(1 0 0 / 0.14); --ring: var(--brand-400); @@ -252,6 +252,16 @@ opacity: 0.10; } + /* Surface utility β€” para contenedores que no usan (ej: tablas, listas) + Mismo treatment visual: bg elevado + border + shadow */ + .surface { + background-color: var(--card); + color: var(--card-foreground); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-md); + } + /* Brand glow para focus en inputs crΓ­ticos */ .focus-glow:focus-visible { box-shadow: var(--shadow-glow); -- 2.49.1 From 7b7ef1c137249523f0b3d515963663b1e3338e92 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 16 Apr 2026 11:21:42 -0300 Subject: [PATCH 6/9] feat(web): sidebar colapsable con tooltips + fix scroll horizontal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambios: - Nuevo hook useSidebar() con persistencia en localStorage ('sidebar-collapsed' = '1'/'0'). - SidebarNav refactorizado: - Width controlled internamente (w-60 expanded, w-[68px] collapsed) - Toggle button al pie con PanelLeftClose/Open icon - Brand mark con gradient brand+violet (consistencia con login) - Active indicator: barra vertical sutil a la izquierda cuando expanded, bg accent cuando collapsed - SectionLabel se reemplaza por divider sutil cuando collapsed - Custom SidebarTooltip puro CSS (sin radix dep nuevo) que aparece a la derecha del item con animacion fade + slide al hover. Funciona con group-hover/item y group-hover/toggle (Tailwind named groups). - Items disabled muestran badge 'PrΓ³x.' chico (era 'PrΓ³ximamente' largo) y en tooltip cuando collapsed: 'Label Β· PrΓ³ximamente'. - Fix scroll horizontal: overflow-x-hidden en nav, truncate en spans, shrink-0 en iconos y badges. Layout robusto a labels largos. - ProtectedLayout deja de hardcodear lg:w-60 β€” sidebar controla su width. - AppHeader Sheet (mobile) usa para que en mobile siempre se vea expanded sin importar el state desktop. Tests 136/136 verde. --- src/web/src/components/layout/AppHeader.tsx | 2 +- src/web/src/components/layout/AppSidebar.tsx | 301 ++++++++++++------- src/web/src/hooks/useSidebar.ts | 24 ++ src/web/src/layouts/ProtectedLayout.tsx | 4 +- 4 files changed, 224 insertions(+), 107 deletions(-) create mode 100644 src/web/src/hooks/useSidebar.ts 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..a92e9d3 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, + Newspaper, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { useAuthStore } from '@/stores/authStore' +import { useSidebar } from '@/hooks/useSidebar' interface NavItem { label: string @@ -25,127 +29,216 @@ 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 ( -
Usuario Nombre Email
` 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 diff --git a/src/web/package-lock.json b/src/web/package-lock.json index 54939a6..95d4a76 100644 --- a/src/web/package-lock.json +++ b/src/web/package-lock.json @@ -11,19 +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", @@ -1531,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", @@ -2310,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", @@ -2597,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", @@ -3970,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", @@ -4964,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 7366720..3a7d6ee 100644 --- a/src/web/package.json +++ b/src/web/package.json @@ -16,19 +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/components/ui/alert-dialog.tsx b/src/web/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..8722561 --- /dev/null +++ b/src/web/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/web/src/components/ui/breadcrumb.tsx b/src/web/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/src/web/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>
+ + {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/pagination.tsx b/src/web/src/components/ui/pagination.tsx new file mode 100644 index 0000000..ea40d19 --- /dev/null +++ b/src/web/src/components/ui/pagination.tsx @@ -0,0 +1,117 @@ +import * as React from "react" +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" +import { ButtonProps, buttonVariants } from "@/components/ui/button" + +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( +