Files
AnchorOS/docs/APERTURE_SQUARE_UI.md
Marco Gallegos bb25d6bde6 feat: Implement FASE 5 (Clients & Loyalty) and FASE 6 (Payments & Financial)
FASE 5 - Clientes y Fidelización:
- Client Management (CRM) con búsqueda fonética
- Galería de fotos restringida por tier (VIP/Black/Gold)
- Sistema de Lealtad con puntos y expiración (6 meses)
- Membresías (Gold, Black, VIP) con beneficios configurables
- Notas técnicas con timestamp

APIs Implementadas:
- GET/POST /api/aperture/clients - CRUD completo de clientes
- GET /api/aperture/clients/[id] - Detalles con historial de reservas
- POST /api/aperture/clients/[id]/notes - Notas técnicas
- GET/POST /api/aperture/clients/[id]/photos - Galería de fotos
- GET /api/aperture/loyalty - Resumen de lealtad
- GET/POST /api/aperture/loyalty/[customerId] - Historial y puntos

FASE 6 - Pagos y Protección:
- Stripe Webhooks (payment_intent.succeeded, payment_failed, charge.refunded)
- No-Show Logic con detección automática (ventana 12h)
- Check-in de clientes para prevenir no-shows
- Override Admin para waivar penalizaciones
- Finanzas y Reportes (expenses, daily closing, staff performance)

APIs Implementadas:
- POST /api/webhooks/stripe - Handler de webhooks Stripe
- GET /api/cron/detect-no-shows - Detectar no-shows (cron job)
- POST /api/aperture/bookings/no-show - Aplicar penalización
- POST /api/aperture/bookings/check-in - Registrar check-in
- GET /api/aperture/finance - Resumen financiero
- POST/GET /api/aperture/finance/daily-closing - Reportes diarios
- GET/POST /api/aperture/finance/expenses - Gestión de gastos
- GET /api/aperture/finance/staff-performance - Performance de staff

Documentación:
- docs/APERATURE_SPECS.md - Especificaciones técnicas completas
- docs/APERTURE_SQUARE_UI.md - Ejemplos de Radix UI con Square UI
- docs/API.md - Actualizado con nuevas rutas

Migraciones SQL:
- 20260118050000_clients_loyalty_system.sql - Clientes, fotos, lealtad, membresías
- 20260118060000_stripe_webhooks_noshow_logic.sql - Webhooks, no-shows, check-ins
- 20260118070000_financial_reporting_expenses.sql - Gastos, reportes financieros
2026-01-18 23:05:09 -06:00

28 KiB

Aperture Design System - Square UI Style

Documento de estilo de diseño para Aperture (HQ Dashboard) Última actualización: Enero 2026


1. Stack Técnico

Frontend Framework

  • Next.js 14 (App Router) - Ya implementado
  • UI Library: Radix UI (componentes accesibles preconstruidos)
  • Estilizado: Tailwind CSS + Square UI custom styling
  • Icons: Lucide React (24px, stroke 2px)

Backend

  • Database: Supabase (PostgreSQL + RLS)
  • Auth: Supabase Auth

Notas Importantes

  • Radix UI es la librería principal para componentes accesibles
  • Solo si Radix NO tiene un componente, usar Headless UI
  • Estilizado personalizado con tokens Square UI
  • Accesibilidad priorizada (ARIA attributes, keyboard navigation)

2. Objetivo

Aperture (aperture.anchor23.mx) es el dashboard administrativo y CRM interno de AnchorOS. El estilo de diseño debe seguir principios similares a SquareUi pero construido con Radix UI:

  • Minimalista y limpio
  • Cards bien definidas con sombras sutiles
  • Espaciado generoso
  • Foco en usabilidad y claridad
  • Animaciones sutiles
  • Accesibilidad completa (prioridad Radix)

3. Componentes Radix UI Utilizados

Componentes Instalados

npm install @radix-ui/react-button @radix-ui/react-select @radix-ui/react-tabs \
  @radix-ui/react-dropdown-menu @radix-ui/react-dialog \
  @radix-ui/react-tooltip @radix-ui/react-label @radix-ui/react-switch \
  @radix-ui/react-checkbox

Componentes Radix con Estilizado Square UI

  1. @radix-ui/react-button

    • Estilos: primary, secondary, ghost, danger, success, warning
    • Squared corners (border-radius: 0 o 4px)
    • Full width con variant default (ancho 100%)
    • Transiciones suaves (150ms ease-out)
  2. @radix-ui/react-select

    • Dropdown con Square UI styling
    • Background: --ui-bg-card
    • Border: --ui-border
    • Hover: background --ui-bg-hover
    • Selected: background --ui-bg-hover, font-weight 500
  3. @radix-ui/react-tabs

    • Tabs con Square UI styling
    • Active indicator: borde inferior 2px solid --ui-primary
    • Colors: --ui-text-primary para activo, --ui-text-secondary para inactivo
  4. @radix-ui/react-dropdown-menu

    • Menús desplegables Square UI
    • Background: --ui-bg-card
    • Border: --ui-border
    • Shadow: --ui-shadow-md
    • Hover: background: var(--ui-bg-hover)
  5. @radix-ui/react-dialog

    • Modals con Square UI styling
    • Background: --ui-bg-card
    • Border: --ui-border
    • Radius: --ui-radius-xl
    • Shadow: --ui-shadow-xl
  6. @radix-ui/react-tooltip

    • Tooltips con Square UI styling
    • Background: --ui-text-primary
  • Font size: --text-sm
  • Padding: --space-2 / --space-3
  • Shadow: --ui-shadow-md
  1. @radix-ui/react-label
    • Labels con Square UI styling
    • Color: --ui-text-primary
  • Font-weight: 500 o 600
  • Required indicator con asterisco rojo
  1. @radix-ui/react-switch
    • Switches con Square UI styling
    • Track: --ui-border
  • Thumb: --ui-primary background
  • Thumb radius: 0 (squared)
  1. @radix-ui/react-checkbox
    • Checkboxes con Square UI styling
    • Border: --ui-border
  • Checked: Background --ui-primary
  • Checkmark color: --ui-text-inverse

Componentes Custom (No Radix UI)

  1. Card - Custom

    • Background: --ui-bg-card
    • Border: --ui-border
    • Radius: --ui-radius-lg (8px)
    • Shadow: --ui-shadow-md o --ui-shadow-lg
    • Variants: default, elevated, bordered
  2. Avatar - Custom

    • Iniciales para usuarios sin foto
    • Status indicators: online (green), offline (gray), busy (red)
    • Radius: --ui-radius-full
  3. Table - Custom

    • Headers con font-weight: 600
    • Row hover: background: var(--ui-bg-hover)
    • Sticky header
    • Sort indicators
  4. Badge - Custom

    • Variants: default, success, warning, error, info
    • Small: text-xs, Medium: text-sm
  • Radius: --ui-radius-full

4. Estilos Square UI Componentes

Aperture (aperture.anchor23.mx) es el dashboard administrativo y CRM interno de AnchorOS. El estilo de diseño debe seguir principios similares a SquareUi:

  • Minimalista y limpio
  • Cards bien definidas con sombras sutiles
  • Espaciado generoso
  • Foco en usabilidad y claridad
  • Animaciones sutiles

2. Paleta de Colores

Primarios

--ui-primary: #006AFF;
--ui-primary-hover: #005ED6;
--ui-primary-light: #E6F0FF;

Neutros

--ui-bg: #F6F8FA;
--ui-bg-card: #FFFFFF;
--ui-bg-hover: #F3F4F6;

--ui-border: #E1E4E8;
--ui-border-light: #F3F4F6;

Texto

--ui-text-primary: #24292E;
--ui-text-secondary: #586069;
--ui-text-tertiary: #8B949E;
--ui-text-inverse: #FFFFFF;

Estados

--ui-success: #28A745;
--ui-success-light: #D4EDDA;

--ui-warning: #DBAB09;
--ui-warning-light: #FFF3CD;

--ui-error: #D73A49;
--ui-error-light: #F8D7DA;

--ui-info: #0366D6;
--ui-info-light: #CCE5FF;

Grises Semánticos

--ui-gray-50: #F6F8FA;
--ui-gray-100: #EAECEF;
--ui-gray-200: #D1D5DA;
--ui-gray-300: #B4B9C2;
--ui-gray-400: #8A8A8A;
--ui-gray-500: #586069;
--ui-gray-600: #444C56;
--ui-gray-700: #24292F;
--ui-gray-800: #1F2428;
--ui-gray-900: #0D1117;

3. Bordes y Radii

--ui-radius-sm: 4px;
--ui-radius-md: 6px;
--ui-radius-lg: 8px;
--ui-radius-xl: 12px;
--ui-radius-2xl: 16px;
--ui-radius-full: 9999px;

Uso recomendado:

  • md para inputs y small cards
  • lg para buttons y medium cards
  • xl para modals y large cards
  • full para avatares y badges

4. Sombras (Elevations)

--ui-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 4px rgba(0, 0, 0, 0.04);
--ui-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1);
--ui-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
--ui-shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04);

Uso recomendado:

  • sm para tooltips y dropdowns
  • md para cards y modals
  • lg para sidebars y panels
  • xl para overlays y fullscreen modals

5. Tipografía

Font Family

--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

Font Sizes

--text-xs: 0.75rem;      /* 12px */
--text-sm: 0.875rem;     /* 14px */
--text-base: 1rem;         /* 16px */
--text-lg: 1.125rem;      /* 18px */
--text-xl: 1.25rem;       /* 20px */
--text-2xl: 1.5rem;      /* 24px */
--text-3xl: 1.875rem;    /* 30px */
--text-4xl: 2.25rem;     /* 36px */
--text-5xl: 3rem;        /* 48px */

Font Weights

--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;

Line Heights

--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;

Uso recomendado:

  • text-xs + font-medium para labels
  • text-sm + font-normal para body text
  • text-base + font-semibold para headings
  • text-xl + font-bold para page titles
  • text-3xl + font-bold for hero titles

6. Espaciado (Spacing)

--space-0: 0;
--space-1: 0.25rem;  /* 4px */
--space-2: 0.5rem;   /* 8px */
--space-3: 0.75rem;  /* 12px */
--space-4: 1rem;      /* 16px */
--space-5: 1.25rem;   /* 20px */
--space-6: 1.5rem;    /* 24px */
--space-8: 2rem;      /* 32px */
--space-10: 2.5rem;   /* 40px */
--space-12: 3rem;     /* 48px */
--space-16: 4rem;     /* 64px */

Uso recomendado:

  • space-2 para padding de inputs
  • space-4 para padding de cards
  • space-6 para gaps en grid
  • space-8 para section padding
  • space-12 para margin entre secciones grandes

7. Z-Index Layers

--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
--z-modal-backdrop: 400;
--z-modal: 500;
--z-popover: 600;
--z-tooltip: 700;

8. Transiciones y Animaciones

--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);

Principios:

  • Todas las transiciones deben usar cubic-bezier(0.4, 0, 0.2, 1) (ease-out)
  • Animaciones de entrada: fadeIn, slideUp, scaleIn
  • Animaciones de salida: fadeOut, slideDown, scaleOut
  • No usar animaciones llamativas o distractivas

9. Grid System

Breakpoints

--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;

Columnas

  • Mobile: 4 columnas
  • Tablet: 8 columnas
  • Desktop: 12 columnas

Gutter

  • Todos los niveles: 16px (1rem)

10. Estados de Componentes

Button States

.btn-primary {
  background: var(--ui-primary);
  color: var(--ui-text-inverse);
  border-radius: var(--ui-radius-lg);
  padding: var(--space-2) var(--space-4);
  transition: all var(--transition-base);
}

.btn-primary:hover {
  background: var(--ui-primary-hover);
  transform: translateY(-1px);
}

.btn-primary:active {
  transform: translateY(0);
}

.btn-primary:disabled {
  background: var(--ui-gray-300);
  cursor: not-allowed;
  opacity: 0.6;
}

Input States

.input {
  background: var(--ui-bg-card);
  border: 1px solid var(--ui-border);
  border-radius: var(--ui-radius-md);
  padding: var(--space-2) var(--space-3);
  transition: border-color var(--transition-fast);
}

.input:focus {
  outline: none;
  border-color: var(--ui-primary);
  box-shadow: 0 0 0 3px var(--ui-primary-light);
}

.input:disabled {
  background: var(--ui-gray-50);
  cursor: not-allowed;
}

Card States

.card {
  background: var(--ui-bg-card);
  border: 1px solid var(--ui-border);
  border-radius: var(--ui-radius-xl);
  box-shadow: var(--ui-shadow-md);
  transition: all var(--transition-base);
}

.card:hover {
  box-shadow: var(--ui-shadow-lg);
  transform: translateY(-2px);
}

11. Layout Pattern

Sidebar Layout

<Sidebar>
  width: 256px;
  height: 100vh;
  background: var(--ui-gray-50);
  border-right: 1px solid var(--ui-border);
  position: fixed;
  left: 0;
  top: 0;
</Sidebar>

<MainContent>
  margin-left: 256px;
  background: var(--ui-bg);
  min-height: 100vh;
</MainContent>

Card Grid

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  {items.map(item => (
    <Card key={item.id}>
      {/* Card content */}
    </Card>
  ))}
</div>

12. Accesibilidad

Contrast Ratios

  • Background --ui-bg-card + Text --ui-text-primary: 12.4:1 (AAA)
  • Background --ui-primary + Text --ui-text-inverse: 4.6:1 (AA)
  • Background --ui-success + Text --ui-text-inverse: 4.5:1 (AA)

Focus States

  • Todos los elementos interactivos deben tener focus visible
  • Usar outline: 2px solid var(--ui-primary) con offset

Keyboard Navigation

  • Todas las acciones deben ser accesibles por teclado
  • Tab order lógico y predecible

13. Dark Mode (Opcional)

No implementado actualmente, pero preparado con:

@media (prefers-color-scheme: dark) {
  :root {
    --ui-bg: #0D1117;
    --ui-bg-card: #161B22;
    --ui-text-primary: #F0F6FC;
    --ui-text-secondary: #8B949E;
    --ui-border: #30363D;
  }
}

14. Iconografía

  • Tamaño estándar: 24px
  • Stroke width: 2px
  • Color: hereda del texto o usa variables de color

Icon Sizes

--icon-xs: 16px;
--icon-sm: 20px;
--icon-md: 24px; /* estándar */
--icon-lg: 32px;
--icon-xl: 40px;

15. Componentes Específicos de Aperture

Stats Card

<StatsCard>
  icon: IconComponent;
  title: string;
  value: number | string;
  trend?: {
    value: number;
    isPositive: boolean;
  };
</StatsCard>

Booking Card

<BookingCard>
  customerName: string;
  serviceName: string;
  startTime: string;
  endTime: string;
  status: 'confirmed' | 'pending' | 'completed' | 'no_show';
  staff: StaffInfo;
</BookingCard>

Calendar Time Slot

<TimeSlot>
  time: string;
  isAvailable: boolean;
  booking?: BookingInfo;
  onClick: () => void;
</TimeSlot>

16. Responsive Adaptations

Mobile (< 640px)

  • Sidebar: hidden behind hamburger menu
  • Table: horizontal scroll
  • Grid: 1 columna
  • Modal: fullscreen

Tablet (640px - 1024px)

  • Sidebar: collapsable (64px when collapsed)
  • Table: horizontal scroll if needed
  • Grid: 2 columnas
  • Modal: centered with max-width

Desktop (> 1024px)

  • Sidebar: fixed 256px
  • Table: sticky header
  • Grid: 3-4 columnas
  • Modal: centered with max-width

17. Convenciones de Código

Naming Convention

// Componentes: PascalCase
const StatsCard = () => { }

// Props: camelCase
interface StatsCardProps {
  icon: React.ReactNode;
  title: string;
  value: number;
}

// CSS classes: kebab-case
.stats-card { }

// CSS variables: kebab-case con prefijo 'ui-'
--ui-primary: #006AFF;

Estructura de Archivos

components/hq/
├── StatsCard.tsx
├── BookingCard.tsx
├── MultiColumnCalendar.tsx
├── StaffTable.tsx
├── ResourceGrid.tsx
└── index.ts

18. Checklist de Implementación

Antes de considerar un componente como "completado":

  • Implementa todos los estados (default, hover, focus, active, disabled)
  • Usa variables CSS del sistema
  • Tiene accesibilidad (contrast, keyboard, focus)
  • Es responsive (mobile, tablet, desktop)
  • Tiene animaciones sutiles (150-300ms)
  • Tiene TypeScript types completos
  • Está documentado con JSDoc
  • Tiene ejemplos de uso

19. Recursos


20. Notas Importantes

Principios de Diseño

  1. Claridad sobre creatividad: La información debe ser fácil de entender, no decorativa
  2. Consistencia: Todos los componentes similares deben comportarse igual
  3. Minimalismo: Menos es más. Elimina elementos innecesarios
  4. Feedback: Las acciones deben dar feedback inmediato (loading, success, error)
  5. Accesibilidad: Todo debe ser accesible por teclado y screen readers

Lo que NO hacer

  • No usar gradients
  • No usar sombras duras
  • No usar colores saturados
  • No usar animaciones llamativas
  • No usar UI densa
  • No usar efectos innecesarios

Lo que SÍ hacer

  • Usar espacio negativo dominante
  • Usar tipografía legible
  • Usar animaciones sutiles
  • Usar contrastes apropiados
  • Usar focus states visibles
  • Usar feedback inmediato
  • Usar grid systems consistentes
  • Usar espaciado generoso

21. Ejemplos de Uso de Radix UI con Square UI Styling

21.1 Button Component (Radix UI)

// components/ui/button.tsx
'use client'
import * as React from 'react'
import * as ButtonPrimitive from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-lg text-sm font-medium transition-all duration-200 ease-out focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-[#006AFF] text-white hover:bg-[#005ED6] active:translate-y-0',
        secondary: 'bg-white text-[#24292E] border border-[#E1E4E8] hover:bg-[#F3F4F6]',
        ghost: 'text-[#24292E] hover:bg-[#F3F4F6]',
        danger: 'bg-[#D73A49] text-white hover:bg-[#B91C3C]',
        success: 'bg-[#28A745] text-white hover:bg-[#218838]',
      },
      size: {
        sm: 'h-8 px-3 text-xs',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-base',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, ...props }, ref) => {
    return (
      <button
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = 'Button'

export { Button, buttonVariants }

Uso:

<Button variant="default" size="md">
  Save Changes
</Button>

<Button variant="secondary" size="sm">
  Cancel
</Button>

<Button variant="danger" size="lg">
  Delete
</Button>

21.2 Dialog Component (Radix UI)

// components/ui/dialog.tsx
'use client'
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'

const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close

const DialogOverlay = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Overlay
    ref={ref}
    className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
    {...props}
  />
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName

const DialogContent = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPortal>
    <DialogOverlay />
    <DialogPrimitive.Content
      ref={ref}
      className="fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-[#E1E4E8] bg-white p-6 shadow-xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-xl"
      {...props}
    >
      {children}
      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-[#006AFF] focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100">
        <X className="h-4 w-4" />
        <span className="sr-only">Close</span>
      </DialogPrimitive.Close>
    </DialogPrimitive.Content>
  </DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName

const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
  <div className="flex flex-col space-y-1.5 text-center sm:text-left" {...props} />
)
DialogHeader.displayName = 'DialogHeader'

const DialogTitle = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Title
    ref={ref}
    className="text-lg font-semibold leading-none tracking-tight"
    {...props}
  />
))
DialogTitle.displayName = DialogPrimitive.Title.displayName

export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogClose }

Uso:

<Dialog>
  <DialogTrigger asChild>
    <Button>Open Dialog</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Confirm Action</DialogTitle>
    </DialogHeader>
    <p>Are you sure you want to proceed?</p>
    <div className="flex gap-2 justify-end">
      <DialogClose asChild>
        <Button variant="secondary">Cancel</Button>
      </DialogClose>
      <Button variant="danger">Confirm</Button>
    </div>
  </DialogContent>
</Dialog>

21.3 Select Component (Radix UI)

// components/ui/select.tsx
'use client'
import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { Check, ChevronDown } from 'lucide-react'

const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value

const SelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Trigger
    ref={ref}
    className="flex h-10 w-full items-center justify-between rounded-lg border border-[#E1E4E8] bg-white px-3 py-2 text-sm placeholder:text-[#8B949E] focus:outline-none focus:ring-2 focus:ring-[#006AFF] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1"
    {...props}
  >
    {children}
    <SelectPrimitive.Icon asChild>
      <ChevronDown className="h-4 w-4 opacity-50" />
    </SelectPrimitive.Icon>
  </SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className="relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-[#E1E4E8] bg-white text-[#24292E] shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
      position={position}
      {...props}
    >
      <SelectPrimitive.Viewport className="p-1">
        {children}
      </SelectPrimitive.Viewport>
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Item
    ref={ref}
    className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-[#F3F4F6] focus:text-[#24292E] data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
    {...props}
  >
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
      <SelectPrimitive.ItemIndicator>
        <Check className="h-4 w-4" />
      </SelectPrimitive.ItemIndicator>
    </span>
    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
  </SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName

export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectItem }

Uso:

<Select>
  <SelectTrigger className="w-[180px]">
    <SelectValue placeholder="Select a fruit" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectItem value="banana">Banana</SelectItem>
    <SelectItem value="orange">Orange</SelectItem>
  </SelectContent>
</Select>

21.4 Tabs Component (Radix UI)

// components/ui/tabs.tsx
'use client'
import * as React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'

const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.List
    ref={ref}
    className="inline-flex h-10 items-center justify-center rounded-lg bg-[#F6F8FA] p-1 text-[#586069]"
    {...props}
  />
))
TabsList.displayName = TabsPrimitive.List.displayName

const TabsTrigger = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Trigger
    ref={ref}
    className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#006AFF] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-[#24292E] data-[state=active]:shadow-sm"
    {...props}
  />
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName

const TabsContent = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Content
    ref={ref}
    className="mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#006AFF] focus-visible:ring-offset-2"
    {...props}
  />
))
TabsContent.displayName = TabsPrimitive.Content.displayName

export { Tabs, TabsList, TabsTrigger, TabsContent }

Uso:

<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">
    <div>Account settings...</div>
  </TabsContent>
  <TabsContent value="password">
    <div>Password settings...</div>
  </TabsContent>
</Tabs>

21.5 Accesibilidad con Radix UI

ARIA Attributes Automáticos:

// Radix UI agrega automáticamente:
// - role="button" para botones
// - aria-expanded para dropdowns
// - aria-selected para tabs
// - aria-checked para checkboxes
// - aria-invalid para inputs con error
// - aria-describedby para errores de formulario

// Ejemplo con manejo de errores:
<Select>
  <SelectTrigger aria-invalid={hasError} aria-describedby={errorMessage ? 'error-message' : undefined}>
    <SelectValue />
  </SelectTrigger>
  {errorMessage && (
    <p id="error-message" className="text-sm text-[#D73A49]">
      {errorMessage}
    </p>
  )}
</Select>

Keyboard Navigation:

// Radix UI soporta automáticamente:
// - Tab: Navigate focusable elements
// - Enter/Space: Activate buttons, select options
// - Escape: Close modals, dropdowns
// - Arrow keys: Navigate within components (lists, menus)
// - Home/End: Jump to start/end of list

// Para keyboard shortcuts personalizados:
useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
      e.preventDefault()
      // Open search modal
    }
  }
  window.addEventListener('keydown', handleKeyDown)
  return () => window.removeEventListener('keydown', handleKeyDown)
}, [])

22. Guía de Migración a Radix UI

22.1 Componentes que Migrar

De Headless UI a Radix UI:

  • <Dialog />@radix-ui/react-dialog
  • <Menu />@radix-ui/react-dropdown-menu
  • <Tabs />@radix-ui/react-tabs
  • <Switch />@radix-ui/react-switch

Componentes Custom a Mantener:

  • <Card /> - No existe en Radix
  • <Table /> - No existe en Radix
  • <Avatar /> - No existe en Radix
  • <Badge /> - No existe en Radix

22.2 Patrones de Migración

// ANTES (Headless UI)
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
  <DialogPanel>
    <DialogTitle>Title</DialogTitle>
    <DialogContent>...</DialogContent>
  </DialogPanel>
</Dialog>

// DESPUÉS (Radix UI)
<Dialog open={isOpen} onOpenChange={setIsOpen}>
  <DialogContent>
    <DialogTitle>Title</DialogTitle>
    <DialogContent>...</DialogContent>
  </DialogContent>
</Dialog>

23. Changelog

2026-01-18

  • Agregada sección 21: Ejemplos de uso de Radix UI con Square UI styling
  • Agregados ejemplos completos de Button, Dialog, Select, Tabs
  • Agregada guía de accesibilidad con Radix UI
  • Agregada guía de migración de Headless UI a Radix UI

2026-01-17

  • Documento inicial creado
  • Definición de paleta de colores
  • Definición de sistema de tipografía
  • Definición de principios de diseño