ProyectosServiciosSobreBlogContacto English Iniciar proyecto
Todas las notas

Monorepo Turborepo: Arquitectura Cross-Platform para React, React Native y Electron

El desafío de compartir código entre múltiples plataformas representa uno de los problemas arquitectónicos más complejos en el desarrollo moderno de aplicaciones. Imagina esta…

tiagofur ·

1. Introducción

El desafío de compartir código entre múltiples plataformas representa uno de los problemas arquitectónicos más complejos en el desarrollo moderno de aplicaciones. Imagina esta situación: tienes una aplicación SaaS completa gestionada en un monorepo Turborepo con un stack tecnológico que incluye:

  • Backend: Node.js con Prisma ORM y PostgreSQL

  • App Web: React 18+ con Vite y TypeScript

  • App Desktop: Electron envolviendo la aplicación web

  • App Mobile: React Native con Expo SDK 50+

  • Shared Code: Package centralizado con tipos, hooks y utilidades comunes

En teoría, suena perfecto. Sin embargo, la realidad te golpea cuando te das cuenta de que los componentes de interfaz de usuario no son compatibles entre plataformas. Puedes compartir entre Web y Desktop (ambos usan DOM), pero React Native es un ecosistema completamente diferente.

Este artículo documenta la arquitectura que diseñamos para ORDO: The Creator’s OS, una plataforma que evolucionó desde una simple aplicación de gestión de tareas hasta un sistema operativo integral para creadores. Te proporcionaré un análisis técnico profundo con código production-ready, métricas de rendimiento, y decisiones arquitectónicas fundamentadas que puedes aplicar directamente en tu proyecto.

1.1 Contexto y Motivación

ORDO-Todo comenzó como otra aplicación más en un “mar rojo de competencia” saturado de herramientas de productividad. Tras análisis exhaustivos con múltiples modelos de lenguaje (Gemini, Claude Sonnet, Copilot), identificamos una oportunidad de mercado: transformarlo en “ORDO: The Creator’s OS”, un sistema operativo para creadores que gestiona workspaces, proyectos, tareas y flujos de trabajo de manera unificada.

Esta visión ambiciosa requería presencia omnipresente: los creadores trabajan en laptops (Desktop), necesitan acceso rápido desde navegadores (Web), y requieren movilidad (Mobile). La arquitectura técnica debía soportar estas tres plataformas sin triplicar el esfuerzo de desarrollo.

1.2 El Problema Fundamental

La incompatibilidad de componentes UI entre plataformas no es un problema menor; es un obstáculo arquitectónico que afecta:

  • Productividad del equipo: Mantener tres implementaciones separadas del mismo componente es costoso y propenso a errores

  • Consistencia de UX: Diferencias sutiles en comportamiento entre plataformas generan confusión en usuarios

  • Mantenibilidad: Cada corrección de bug o mejora debe replicarse tres veces

  • Testing: La superficie de pruebas se multiplica por el número de plataformas

  • Technical debt: La duplicación de código crea deuda técnica que crece exponencialmente

Este artículo presenta una solución arquitectónica que comparte el 80% del código (lógica de negocio, validación, state management) mientras mantiene implementaciones UI específicas por plataforma para aprovechar las optimizaciones nativas.

2. Análisis del Problema: Incompatibilidad Técnica

2.1 Diferencias Fundamentales entre Plataformas

Comprendamos primero por qué los componentes no son compatibles. La raíz del problema radica en que Web y React Native usan primitivas de renderizado completamente diferentes:

// ============================================================================
// WEB/DESKTOP (DOM-based rendering)
// ============================================================================
import { Dialog } from '@radix-ui/react-dialog';
import { buttonVariants } from '@/components/ui/button';

export function TaskDialog() {
  return (
    <Dialog>
      <DialogTrigger className={buttonVariants({ variant: 'primary' })}>
        Abrir Diálogo
      </DialogTrigger>
      <DialogContent className="bg-white rounded-lg p-6 shadow-xl">
        <DialogHeader>
          <DialogTitle className="text-xl font-semibold">Nueva Tarea</DialogTitle>
        </DialogHeader>
        {/* Form content */}
      </DialogContent>
    </Dialog>
  );
}

// ============================================================================
// REACT NATIVE (Native rendering)
// ============================================================================
import { Modal, View, Text, TouchableOpacity } from 'react-native';

export function TaskModal() {
  const [visible, setVisible] = useState(false);

  return (
    <>
      <TouchableOpacity onPress={() => setVisible(true)}>
        <Text className="bg-primary text-white px-4 py-2 rounded-lg">
          Abrir Modal
        </Text>
      </TouchableOpacity>
      <Modal visible={visible} animationType="slide">
        <View className="bg-white rounded-lg p-6">
          <Text className="text-xl font-semibold">Nueva Tarea</Text>
          {/* Form content */}
        </View>
      </Modal>
    </>
  );
}

Las diferencias van más allá de la sintaxis. Analicemos una comparación sistemática:

| Primitiva | Web/Desktop (DOM) | React Native | Impacto |

|---------------|----------------------|------------------|-------------|

| Botones | | o “ | Diferente API de eventos, styling, accesibilidad |

| Inputs | , | “ | Diferente manejo de focus, keyboard, autocapitalization |

| Navegación | React Router (browser history) | React Navigation (stack navigator) | Paradigmas completamente diferentes (URL vs stack) |

| Estilos | CSS, Tailwind, styled-components | StyleSheet, NativeWind | Propiedades CSS no existentes en RN (box-shadow completo) |

| Modales | Radix UI, Headless UI | “ nativo | Diferente comportamiento de focus trapping, backdrop |

| Scroll | `

| Gestos | Event handlers del DOM | PanGestureHandler, TapGestureHandler | Sistema de gestos completamente diferente |

| Accesibilidad | ARIA attributes | accessibilityLabel, accessibilityHint | Diferentes convenciones y APIs |

2.2 El Mito de las Librerías Cross-Platform

Muchas librerías prometen “write once, run everywhere”. Analicemos las opciones más populares y sus limitaciones:

Gluestack UI:

  • ✅ Promesa: Componentes que funcionan en Web y Mobile

  • ❌ Realidad: Implementaciones separadas bajo el capó, curva de aprendizaje de nuevos patrones

  • ⚠️ Trade-off: Pierdes control sobre la implementación, debugging más complejo

Tamagui:

  • ✅ Promesa: Performance optimizada, compilación cross-platform

  • ❌ Realidad: Configuración compleja, tamaño de bundle significativo

  • ⚠️ Trade-off: Introduce una capa de abstracción adicional que puede causar problemas de debugging

React Native Web:

  • ✅ Promesa: Renderiza componentes RN en el navegador

  • ❌ Realidad: No puedes usar componentes web nativos, rendimiento subóptimo

  • ⚠️ Trade-off: Te limitas al subset de funcionalidad disponible en RN

NativeWind v4 (nuestra elección):

  • ✅ Ventaja: Misma sintaxis de Tailwind en todas partes, zero runtime overhead

  • ✅ Ventaja: Soporte para Tailwind v4, compiled CSS

  • ⚠️ Limitación: No resuelve la incompatibilidad de componentes, solo la de estilos

2.3 La Decisión Arquitectónica

Después de evaluar todas las opciones, tomamos una decisión deliberada: aceptar la duplicación controlada de componentes UI. Esta decisión se fundamenta en:

  1. Control total: Cada implementación está optimizada para su plataforma

  2. Transparencia: No hay magia, el debugging es straightforward

  3. Performance: Sin capas de abstracción innecesarias

  4. Flexibilidad: Podemos usar platform-specific features sin workaround

  5. DX: Los desarrolladores entienden exactamente qué está pasando

El secreto está en maximizar el código compartido donde realmente importa: lógica de negocio, validación, state management, y tipos.

3. Arquitectura de Solución: Diseño Híbrido

3.1 Visión Arquitectónica

Nuestra arquitectura se basa en un principio fundamental: separar la lógica de la presentación. Esto no es simplemente “separar concerns”; es una estrategia deliberada para crear componentes “headless” que contienen toda la inteligencia pero delegan la renderización a implementaciones específicas de plataforma.

┌─────────────────────────────────────────────────────────────────────────┐
│                        @ordo-todo/styles                                │
│              (Design Tokens - Single Source of Truth)                   │
│                                                                         │
│  - colors.ts (Indigo primary, Purple secondary)                        │
│  - typography.ts (Inter font family, scale)                            │
│  - spacing.ts (4px base unit, semantic scale)                          │
│  - shadows.ts (platform-specific shadow definitions)                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────────────────┐    ┌─────────────────────────────────┐   │
│  │    @ordo-todo/ui         │    │    @ordo-todo/ui-mobile         │   │
│  │    (Web/Desktop)         │    │    (React Native + NativeWind)  │   │
│  │                          │    │                                 │   │
│  │  Primitives:             │    │  Primitives:                    │   │
│  │  - Radix UI primitives   │    │  - React Native components      │   │
│  │  - HTML elements         │    │  - Expo components              │   │
│  │                          │    │                                 │   │
│  │  Styling:                │    │  Styling:                       │   │
│  │  - Tailwind CSS v4       │    │  - NativeWind v4                │   │
│  │  - CVA (variants)        │    │  - NativeWind variants          │   │
│  │                          │    │                                 │   │
│  │  Features:               │    │  Features:                      │   │
│  │  - Auto-focus            │    │  - Keyboard avoidance           │   │
│  │  - Portal rendering      │    │  - Haptic feedback              │   │
│  │  - ARIA attributes       │    │  - Native animations            │   │
│  └──────────────────────────┘    └─────────────────────────────────┘   │
│                                                                         │
├─────────────────────────────────────────────────────────────────────────┤
│                    @ordo-todo/ui-core                                   │
│            (Shared Logic - Platform Agnostic)                           │
│                                                                         │
│  Hooks:                      Types:                   Utilities:        │
│  - useTaskCard()             - Task                    - cn() (merge)   │
│  - useForm()                 - User                    - formatDate()   │
│  - useTaskActions()          - Project                 - validateEmail()│
│  - useAuth()                 - Workspace               - parseQuery()   │
│  - useDialog()               - ApiError                - debounce()     │
│  - useDebounce()             - ApiResponse             - throttle()     │
│                                                                         │
│  Validation:                 State:                   Logic:            │
│  - validators.ts             - store.ts (Zustand)     - algorithms.ts   │
│  - schemas.ts (Zod)          - actions.ts             - helpers.ts      │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

3.2 Flujo de Datos y Dependencias

// ============================================================================
// DEPENDENCY GRAPH
// ============================================================================
//
// apps/web ──┐
//            ├──> packages/ui ──> packages/ui-core ──┐
// apps/desktop──>         │                          │
//                        └──> packages/styles ───────┘
// apps/mobile ───> packages/ui-mobile ──> packages/ui-core ──┐
//                             │                            │
//                             └──> packages/styles ────────┘
//
// REGLA CRÍTICA:
// - packages/ui NO puede importar de packages/ui-mobile
// - packages/ui-mobile NO puede importar de packages/ui
// - AMBOS pueden importar de packages/ui-core y packages/styles
// - apps/ solo importan de sus respectivos packages/ui*
// ============================================================================

3.3 El Principio de Inversión de Dependencias

Aplicamos el principio SOLID de inversión de dependencias:

// ❌ VIOLA DIP: paquete de alto nivel depende de implementación concreta
import { Button } from '@ordo-todo/ui'; // Implementación Web
import { useTaskActions } from './hooks';

export function TaskCard() {
  const { createTask } = useTaskActions();

  return (
    <div>
      <Button onClick={createTask}>Create Task</Button>
      {/* Lógica de negocio acoplada a implementación Web */}
    </div>
  );
}

// ✅ SIGUE DIP: paquete de alto nivel expone abstracción
// packages/ui-core/src/components/task/useTaskCard.ts
export interface TaskCardActions {
  createTask: (data: CreateTaskDTO) => Promise<void>;
  updateTask: (id: string, data: UpdateTaskDTO) => Promise<void>;
  deleteTask: (id: string) => Promise<void>;
}

export function useTaskCard(task: Task): {
  task: Task;
  actions: TaskCardActions;
  canEdit: boolean;
  isOverdue: boolean;
} {
  const { user } = useAuth();
  const { createTask, updateTask, deleteTask } = useTaskActions();

  return {
    task,
    actions: { createTask, updateTask, deleteTask },
    canEdit: user?.id === task.createdById,
    isOverdue: task.dueDate && new Date(task.dueDate) < new Date(),
  };
}

// packages/ui/src/TaskCard.tsx (Implementación Web)
import { useTaskCard } from '@ordo-todo/ui-core';
import { Button } from './Button'; // Implementación específica

export function TaskCardWeb({ task }: { task: Task }) {
  const card = useTaskCard(task);

  return (
    <div className="...">
      <Button onClick={() => card.actions.createTask(task)}>
        Create Task
      </Button>
    </div>
  );
}

// packages/ui-mobile/src/TaskCard.tsx (Implementación Mobile)
import { useTaskCard } from '@ordo-todo/ui-core';
import { Button } from './Button'; // Implementación específica

export function TaskCardMobile({ task }: { task: Task }) {
  const card = useTaskCard(task);

  return (
    <View className="...">
      <Button onPress={() => card.actions.createTask(task)}>
        Create Task
      </Button>
    </View>
  );
}

4. Implementación Production-Ready

4.1 Estructura del Monorepo

ordo/
├── apps/
│   ├── web/                          # React + Vite
│   │   ├── src/
│   │   │   ├── pages/                # File-based routing
│   │   │   ├── components/           # App-specific components
│   │   │   └── main.tsx              # Entry point
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── vite.config.ts
│   │
│   ├── desktop/                      # Electron
│   │   ├── src/
│   │   │   ├── main/                 # Electron main process
│   │   │   │   ├── index.ts          # Main entry
│   │   │   │   └── menu.ts           # Menu configuration
│   │   │   └── renderer/             # Web app (reused)
│   │   ├── package.json
│   │   └── electron-builder.config.js
│   │
│   └── mobile/                       # React Native + Expo
│       ├── src/
│       │   ├── app/                  # Expo Router (file-based)
│       │   ├── components/           # App-specific components
│       │   └── hooks/                # App-specific hooks
│       ├── package.json
│       ├── app.json                  # Expo config
│       └── nativewind-env.d.ts       # NativeWind types

├── packages/
│   ├── ui/                           # Web/Desktop components
│   │   ├── src/
│   │   │   ├── components/
│   │   │   │   ├── button/
│   │   │   │   │   ├── Button.tsx
│   │   │   │   │   ├── Button.test.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   ├── input/
│   │   │   │   ├── dialog/
│   │   │   │   └── ...
│   │   │   └── index.ts              # Barrel export
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   ├── ui-mobile/                    # React Native components
│   │   ├── src/
│   │   │   ├── components/
│   │   │   │   ├── button/
│   │   │   │   │   ├── Button.tsx
│   │   │   │   │   ├── Button.test.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   └── ...
│   │   │   └── index.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   ├── ui-core/                      # Shared logic
│   │   ├── src/
│   │   │   ├── hooks/
│   │   │   │   ├── useButton.ts
│   │   │   │   ├── useTaskCard.ts
│   │   │   │   ├── useForm.ts
│   │   │   │   └── index.ts
│   │   │   ├── types/
│   │   │   │   ├── button.ts
│   │   │   │   ├── task.ts
│   │   │   │   └── index.ts
│   │   │   ├── validation/
│   │   │   │   ├── validators.ts
│   │   │   │   ├── schemas.ts
│   │   │   │   └── index.ts
│   │   │   ├── state/
│   │   │   │   ├── store.ts          # Zustand store
│   │   │   │   └── actions.ts
│   │   │   ├── utils/
│   │   │   │   ├── cn.ts             # className merge
│   │   │   │   ├── date.ts
│   │   │   │   └── index.ts
│   │   │   └── index.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   ├── styles/                       # Design tokens
│   │   ├── src/
│   │   │   ├── colors.ts
│   │   │   ├── typography.ts
│   │   │   ├── spacing.ts
│   │   │   ├── breakpoints.ts
│   │   │   ├── shadows.ts
│   │   │   ├── radius.ts
│   │   │   └── index.ts
│   │   ├── tailwind.config.js        # Shared Tailwind config
│   │   ├── package.json
│   │   └── tsconfig.json
│   │
│   ├── shared/                       # General utilities
│   │   ├── src/
│   │   │   ├── api/
│   │   │   │   ├── client.ts         # API client
│   │   │   │   └── endpoints.ts
│   │   │   ├── constants/
│   │   │   └── utils/
│   │   └── package.json
│   │
│   └── types/                        # Shared types
│       ├── src/
│       │   ├── index.ts
│       └── package.json

├── turbo.json                        # Turborepo configuration
├── package.json                      # Root package.json
├── pnpm-workspace.yaml               # pnpm workspace
└── tsconfig.base.json                # Shared TypeScript config

4.2 Configuración de Turborepo

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    },
    "type-check": {
      "dependsOn": ["^build"]
    }
  }
}
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
// package.json (root)
{
  "name": "ordo-monorepo",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint",
    "type-check": "turbo run type-check",
    "clean": "turbo run clean && rm -rf node_modules"
  },
  "devDependencies": {
    "turbo": "^1.11.0",
    "typescript": "^5.3.3",
    "prettier": "^3.1.1",
    "eslint": "^8.56.0"
  },
  "engines": {
    "node": ">=18.0.0",
    "pnpm": ">=8.0.0"
  },
  "packageManager": "pnpm@8.15.0"
}

4.3 Design Tokens: La Fuente Única de Verdad

Los design tokens son la base de nuestra arquitectura. Son platform-agnostic y se comparten entre todas las implementaciones UI:

// packages/styles/src/colors.ts
import { type Platform } from 'react-native';

/**
 * Color palette following WCAG AA standards
 * All color combinations have been tested for contrast ratios
 */
export const colors = {
  // Primary colors (Indigo)
  primary: {
    50: '#eef2ff',
    100: '#e0e7ff',
    200: '#c7d2fe',
    300: '#a5b4fc',
    400: '#818cf8',
    500: '#6366f1', // Main primary
    600: '#4f46e5',
    700: '#4338ca',
    800: '#3730a3',
    900: '#312e81',
    950: '#1e1b4b',
  },

  // Secondary colors (Purple)
  secondary: {
    50: '#faf5ff',
    100: '#f3e8ff',
    200: '#e9d5ff',
    300: '#d8b4fe',
    400: '#c084fc',
    500: '#a855f7', // Main secondary
    600: '#9333ea',
    700: '#7e22ce',
    800: '#6b21a8',
    900: '#581c87',
    950: '#3b0764',
  },

  // Semantic colors
  success: {
    light: '#86efac',
    DEFAULT: '#22c55e',
    dark: '#16a34a',
  },
  warning: {
    light: '#fcd34d',
    DEFAULT: '#f59e0b',
    dark: '#d97706',
  },
  danger: {
    light: '#fca5a5',
    DEFAULT: '#ef4444',
    dark: '#dc2626',
  },
  info: {
    light: '#93c5fd',
    DEFAULT: '#3b82f6',
    dark: '#2563eb',
  },

  // Neutral colors
  gray: {
    50: '#f9fafb',
    100: '#f3f4f6',
    200: '#e5e7eb',
    300: '#d1d5db',
    400: '#9ca3af',
    500: '#6b7280',
    600: '#4b5563',
    700: '#374151',
    800: '#1f2937',
    900: '#111827',
    950: '#030712',
  },

  // Base colors
  background: '#ffffff',
  surface: '#f8fafc',
  surfaceVariant: '#f1f5f9',

  text: {
    primary: '#0f172a',
    secondary: '#475569',
    tertiary: '#64748b',
    disabled: '#94a3b8',
    inverse: '#ffffff',

    // On colored backgrounds
    onPrimary: '#ffffff',
    onSecondary: '#ffffff',
    onSuccess: '#ffffff',
    onWarning: '#ffffff',
    onDanger: '#ffffff',
  },

  // Border colors
  border: {
    DEFAULT: '#e2e8f0',
    light: '#f1f5f9',
    dark: '#cbd5e1',
  },
} as const;

/**
 * Platform-specific color adaptations
 * React Native requires shadow definitions as objects
 */
export const platformColors = {
  web: {
    shadow: {
      sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
      md: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
      lg: '0 10px 15px -3px rgb(0 0 0 / 0.1)',
      xl: '0 20px 25px -5px rgb(0 0 0 / 0.1)',
    },
  },
  native: {
    shadow: {
      sm: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 1 },
        shadowOpacity: 0.05,
        shadowRadius: 2,
        elevation: 1,
      },
      md: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 4 },
        shadowOpacity: 0.1,
        shadowRadius: 6,
        elevation: 4,
      },
      lg: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 10 },
        shadowOpacity: 0.15,
        shadowRadius: 15,
        elevation: 10,
      },
    },
  },
} as const;

export type Colors = typeof colors;
// packages/styles/src/typography.ts
export const fontFamily = {
  sans: ['Inter', 'system-ui', 'sans-serif'],
  mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
} as const;

export const fontSize = {
  xs: ['0.75rem', { lineHeight: '1rem' }],     // 12px
  sm: ['0.875rem', { lineHeight: '1.25rem' }],  // 14px
  base: ['1rem', { lineHeight: '1.5rem' }],     // 16px
  lg: ['1.125rem', { lineHeight: '1.75rem' }],  // 18px
  xl: ['1.25rem', { lineHeight: '1.75rem' }],   // 20px
  '2xl': ['1.5rem', { lineHeight: '2rem' }],    // 24px
  '3xl': ['1.875rem', { lineHeight: '2.25rem' }], // 30px
  '4xl': ['2.25rem', { lineHeight: '2.5rem' }], // 36px
  '5xl': ['3rem', { lineHeight: '1' }],         // 48px
} as const;

export const fontWeight = {
  normal: '400',
  medium: '500',
  semibold: '600',
  bold: '700',
} as const;

export const letterSpacing = {
  tighter: '-0.05em',
  tight: '-0.025em',
  normal: '0em',
  wide: '0.025em',
  wider: '0.05em',
  widest: '0.1em',
} as const;
// packages/styles/src/spacing.ts
/**
 * Spacing scale based on 4px base unit
 * Consistent with Tailwind CSS default scale
 */
export const spacing = {
  0: '0',
  px: '1px',        // 1px
  0.5: '0.125rem',  // 2px
  1: '0.25rem',     // 4px - Base unit
  1.5: '0.375rem',  // 6px
  2: '0.5rem',      // 8px
  2.5: '0.625rem',  // 10px
  3: '0.75rem',     // 12px
  3.5: '0.875rem',  // 14px
  4: '1rem',        // 16px
  5: '1.25rem',     // 20px
  6: '1.5rem',      // 24px
  7: '1.75rem',     // 28px
  8: '2rem',        // 32px
  9: '2.25rem',     // 36px
  10: '2.5rem',     // 40px
  12: '3rem',       // 48px
  16: '4rem',       // 64px
  20: '5rem',       // 80px
  24: '6rem',       // 96px
  32: '8rem',       // 128px
  40: '10rem',      // 160px
  48: '12rem',      // 192px
  56: '14rem',      // 224px
  64: '16rem',      // 256px
} as const;

/**
 * Semantic spacing tokens for consistency
 */
export const spacingSemantic = {
  xs: spacing[1],      // 4px - Tight spacing
  sm: spacing[2],      // 8px - Small spacing
  md: spacing[4],      // 16px - Default spacing
  lg: spacing[6],      // 24px - Large spacing
  xl: spacing[8],      // 32px - Extra large spacing
  '2xl': spacing[12],  // 48px - Section spacing
  '3xl': spacing[16],  // 64px - Container spacing
} as const;
// packages/styles/src/index.ts
export * from './colors';
export * from './typography';
export * from './spacing';
export * from './breakpoints';
export * from './shadows';
export * from './radius';

/**
 * Complete design system export
 * Use as: import { colors, spacing, fontSize } from '@ordo-todo/styles';
 */

4.4 UI Core: Lógica Compartida Platform-Agnostic

El paquete ui-core es el corazón de nuestra arquitectura. Contiene toda la lógica que no depende de la plataforma:

// packages/ui-core/src/types/button.ts
/**
 * Base button types shared across all platforms
 * These types contain ONLY the data/behavior contracts,
 * no UI-specific props like className or style
 */
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';

export interface BaseButtonProps {
  /**
   * Visual variant of the button
   * @default 'primary'
   */
  variant?: ButtonVariant;

  /**
   * Size of the button
   * @default 'md'
   */
  size?: ButtonSize;

  /**
   * Whether the button is disabled
   * @default false
   */
  disabled?: boolean;

  /**
   * Whether the button is in loading state
   * @default false
   */
  loading?: boolean;

  /**
   * Button content
   */
  children: React.ReactNode;

  /**
   * Click handler (platform-agnostic naming)
   * Note: We use 'onPress' instead of 'onClick' to be platform-agnostic
   */
  onPress?: () => void | Promise<void>;

  /**
   * Optional icon to display before children
   */
  leftIcon?: React.ReactNode;

  /**
   * Optional icon to display after children
   */
  rightIcon?: React.ReactNode;

  /**
   * Accessibility label (required if children is not descriptive)
   */
  accessibilityLabel?: string;
}
// packages/ui-core/src/hooks/useButton.ts
import { useCallback } from 'react';
import type { BaseButtonProps } from '../types/button';

export interface UseButtonReturn {
  /**
   * Computed props to spread on button component
   * Note: No className/style - platform-specific implementations handle that
   */
  props: {
    disabled: boolean;
    onPress: () => void;
    'aria-busy'?: boolean;
    'aria-disabled'?: boolean;
    'aria-label'?: string;
  };
  /**
   * Whether button is in interactive state
   */
  isInteractive: boolean;
}

/**
 * Platform-agnostic button hook
 * Contains all button logic but NO rendering logic
 *
 * @example
 * // Web implementation
 * const button = useButton({ onPress: handleClick, disabled: false });
 * return <button {...button.props}>Click me</button>;
 *
 * @example
 * // React Native implementation
 * const button = useButton({ onPress: handlePress, disabled: false });
 * return <TouchableOpacity {...button.props}><Text>Press me</Text></TouchableOpacity>;
 */
export function useButton(props: BaseButtonProps): UseButtonReturn {
  const {
    disabled = false,
    loading = false,
    onPress,
    accessibilityLabel,
  } = props;

  const handlePress = useCallback(async () => {
    if (disabled || loading) {
      return;
    }

    try {
      await onPress?.();
    } catch (error) {
      // Error handling is UI-specific, but we can log here
      console.error('[useButton] Press handler error:', error);
      // Re-throw to let UI layer handle error display
      throw error;
    }
  }, [disabled, loading, onPress]);

  const isInteractive = !disabled && !loading;

  return {
    props: {
      disabled: !isInteractive,
      onPress: handlePress,
      ...(loading && {
        'aria-busy': true,
      }),
      ...(disabled && {
        'aria-disabled': true,
      }),
      ...(accessibilityLabel && {
        'aria-label': accessibilityLabel,
      }),
    },
    isInteractive,
  };
}
// packages/ui-core/src/hooks/useForm.ts
import { useState, useCallback, useMemo } from 'react';
import type { z } from 'zod';

/**
 * Generic form hook with validation
 * Platform-agnostic: works with Web forms and React Native forms
 */
export interface UseFormOptions<T> {
  initialValues: T;
  validate?: (values: T) => Record<keyof T, string | undefined>;
  schema?: z.ZodSchema<T>;
  onSubmit?: (values: T) => void | Promise<void>;
}

export interface UseFormReturn<T> {
  /** Current form values */
  values: T;
  /** Form field errors */
  errors: Record<keyof T, string | undefined>;
  /** Whether each field has been touched */
  touched: Record<keyof T, boolean>;
  /** Whether form is currently submitting */
  isSubmitting: boolean;
  /** Whether form is valid (no errors) */
  isValid: boolean;
  /** Whether any field has been touched */
  isDirty: boolean;
  /** Update a field value */
  setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void;
  /** Mark field as touched */
  setFieldTouched: <K extends keyof T>(field: K) => void;
  /** Handle field change (platform-agnostic) */
  handleChange: <K extends keyof T>(field: K) => (value: T[K]) => void;
  /** Handle field blur */
  handleBlur: <K extends keyof T>(field: K) => () => void;
  /** Handle form submission */
  handleSubmit: () => Promise<void>;
  /** Reset form to initial values */
  reset: () => void;
}

export function useForm<T extends Record<string, any>>(
  options: UseFormOptions<T>
): UseFormReturn<T> {
  const { initialValues, validate, schema, onSubmit } = options;

  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Record<keyof T, string | undefined>>(
    {} as any
  );
  const [touched, setTouched] = useState<Record<keyof T, boolean>>(
    {} as any
  );
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Validate current values
  const validateValues = useCallback(
    async (currentValues: T): Promise<Record<keyof T, string | undefined>> => {
      if (schema) {
        try {
          await schema.parseAsync(currentValues);
          return {} as any;
        } catch (error) {
          if (error instanceof z.ZodError) {
            const fieldErrors: Record<string, string | undefined> = {};
            error.errors.forEach((err) => {
              if (err.path[0]) {
                fieldErrors[err.path[0] as string] = err.message;
              }
            });
            return fieldErrors as any;
          }
          throw error;
        }
      }

      if (validate) {
        return validate(currentValues);
      }

      return {} as any;
    },
    [schema, validate]
  );

  // Computed states
  const isValid = useMemo(() => {
    return Object.values(errors).every((error) => !error);
  }, [errors]);

  const isDirty = useMemo(() => {
    return Object.values(touched).some((t) => t);
  }, [touched]);

  // Update field value
  const handleChange = useCallback(
    <K extends keyof T>(field: K) => (value: T[K]) => {
      setValues((prev) => ({ ...prev, [field]: value }));

      // Validate on change if field has been touched
      if (touched[field]) {
        validateValues({ ...values, [field]: value }).then(setErrors);
      }
    },
    [values, touched, validateValues]
  );

  // Mark field as touched and validate
  const handleBlur = useCallback(
    <K extends keyof T>(field: K) => () => {
      setTouched((prev) => ({ ...prev, [field]: true }));
      validateValues(values).then(setErrors);
    },
    [values, validateValues]
  );

  // Set field value directly
  const setFieldValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
    setValues((prev) => ({ ...prev, [field]: value }));
  }, []);

  // Set field touched directly
  const setFieldTouched = useCallback(<K extends keyof T>(field: K) => {
    setTouched((prev) => ({ ...prev, [field]: true }));
  }, []);

  // Submit form
  const handleSubmit = useCallback(async () => {
    // Mark all fields as touched
    const allTouched = Object.keys(values).reduce((acc, key) => {
      acc[key as keyof T] = true;
      return acc;
    }, {} as Record<keyof T, boolean>);

    setTouched(allTouched);

    // Validate all fields
    const validationErrors = await validateValues(values);
    setErrors(validationErrors);

    // Check if form is valid
    const hasErrors = Object.values(validationErrors).some((error) => error);
    if (hasErrors) {
      return;
    }

    setIsSubmitting(true);

    try {
      await onSubmit?.(values);
    } finally {
      setIsSubmitting(false);
    }
  }, [values, validateValues, onSubmit]);

  // Reset form
  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({} as any);
    setTouched({} as any);
    setIsSubmitting(false);
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    isSubmitting,
    isValid,
    isDirty,
    setFieldValue,
    setFieldTouched,
    handleChange,
    handleBlur,
    handleSubmit,
    reset,
  };
}
// packages/ui-core/src/components/task/useTaskCard.ts
import { useCallback, useMemo } from 'react';
import type { Task } from '../types';

export interface UseTaskCardReturn {
  /** Task data */
  task: Task;
  /** Whether current user can edit this task */
  canEdit: boolean;
  /** Whether task is overdue */
  isOverdue: boolean;
  /** Whether task is completed */
  isCompleted: boolean;
  /** Progress percentage (0-100) */
  progress: number;
  /** Toggle task completion status */
  toggleComplete: () => Promise<void>;
  /** Delete task with confirmation */
  deleteTask: () => Promise<void>;
  /** Update task */
  updateTask: (updates: Partial<Task>) => Promise<void>;
}

/**
 * Task card logic hook
 * Contains ALL business logic for task display and interaction
 * Platform-agnostic - can be used in Web, Desktop, and Mobile
 *
 * @example
 * const card = useTaskCard(task);
 * return (
 *   <div>
 *     <h3>{card.task.title}</h3>
 *     {card.isOverdue && <span>Overdue</span>}
 *     <button onClick={card.toggleComplete}>
 *       {card.isCompleted ? 'Undo' : 'Complete'}
 *     </button>
 *   </div>
 * );
 */
export function useTaskCard(task: Task): UseTaskCardReturn {
  const { updateTaskStatus, removeTask, user } = useTaskActions();
  const { confirm } = useConfirmDialog();

  // Computed states
  const canEdit = useMemo(() => {
    return user?.id === task.createdById || user?.role === 'admin';
  }, [user?.id, user?.role, task.createdById]);

  const isOverdue = useMemo(() => {
    if (!task.dueDate || task.status === 'completed') {
      return false;
    }
    return new Date(task.dueDate) < new Date();
  }, [task.dueDate, task.status]);

  const isCompleted = useMemo(() => {
    return task.status === 'completed';
  }, [task.status]);

  const progress = useMemo(() => {
    if (!task.subtasks?.length) return task.status === 'completed' ? 100 : 0;
    const completed = task.subtasks.filter((st) => st.completed).length;
    return Math.round((completed / task.subtasks.length) * 100);
  }, [task.subtasks, task.status]);

  // Actions
  const toggleComplete = useCallback(async () => {
    const newStatus = isCompleted ? 'pending' : 'completed';
    await updateTaskStatus(task.id, newStatus);
  }, [task.id, isCompleted, updateTaskStatus]);

  const deleteTask = useCallback(async () => {
    const confirmed = await confirm({
      title: 'Delete Task',
      message: `Are you sure you want to delete "${task.title}"?`,
      confirmText: 'Delete',
      cancelText: 'Cancel',
    });

    if (confirmed) {
      await removeTask(task.id);
    }
  }, [task.id, task.title, confirm, removeTask]);

  const updateTask = useCallback(
    async (updates: Partial<Task>) => {
      await updateTaskStatus(task.id, updates.status || task.status);
    },
    [task.id, task.status, updateTaskStatus]
  );

  return {
    task,
    canEdit,
    isOverdue,
    isCompleted,
    progress,
    toggleComplete,
    deleteTask,
    updateTask,
  };
}

// Helper hooks (would be in separate files)
function useTaskActions() {
  // Implementation depends on your state management
  // Could be Zustand, Redux, React Query, etc.
  return {
    updateTaskStatus: async (id: string, status: Task['status']) => {
      // API call + state update
    },
    removeTask: async (id: string) => {
      // API call + state update
    },
    user: { id: '123', role: 'user' },
  };
}

function useConfirmDialog() {
  return {
    confirm: (options: {
      title: string;
      message: string;
      confirmText: string;
      cancelText: string;
    }) => Promise<boolean>,
  };
}

4.5 Implementación Web/Desktop con Radix UI y Tailwind v4

// packages/ui/src/components/button/Button.tsx
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { useButton } from '@ordo-todo/ui-core';
import { cn } from '@ordo-todo/ui-core/utils';
import { Spinner } from './Spinner';

/**
 * Button component variants using Class Variance Authority
 * Generates className combinations based on props
 */
const buttonVariants = cva(
  // Base classes - always applied
  'inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        primary: 'bg-primary text-onPrimary hover:bg-primary/90 active:bg-primary/80 focus-visible:ring-primary',
        secondary: 'bg-secondary text-onSecondary hover:bg-secondary/90 active:bg-secondary/80 focus-visible:ring-secondary',
        ghost: 'bg-transparent text-textPrimary hover:bg-surface active:bg-surfaceVariant focus-visible:ring-surface',
        danger: 'bg-danger text-onDanger hover:bg-danger/90 active:bg-danger/80 focus-visible:ring-danger',
        success: 'bg-success text-onSuccess hover:bg-success/90 active:bg-success/80 focus-visible:ring-success',
      },
      size: {
        xs: 'h-7 px-2 text-xs',
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4 text-base',
        lg: 'h-12 px-6 text-lg',
        xl: 'h-14 px-8 text-xl',
      },
      fullWidth: {
        true: 'w-full',
        false: '',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
      fullWidth: false,
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  /** Button content */
  children: React.ReactNode;
  /** Show loading state */
  loading?: boolean;
  /** Icon to display before text */
  leftIcon?: React.ReactNode;
  /** Icon to display after text */
  rightIcon?: React.ReactNode;
}

/**
 * Web/Desktop button component
 * Uses HTML <button> element with Tailwind CSS classes
 *
 * @example
 * <Button variant="primary" size="md" onPress={handleClick}>
 *   Click me
 * </Button>
 *
 * @example
 * <Button variant="danger" loading leftIcon={<Trash />}>
 *   Delete
 * </Button>
 */
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      children,
      variant,
      size,
      fullWidth,
      loading = false,
      leftIcon,
      rightIcon,
      disabled,
      className,
      onPress,
      ...props
    },
    ref
  ) => {
    const button = useButton({
      disabled: disabled || loading,
      loading,
      onPress,
    });

    return (
      <button
        ref={ref}
        disabled={button.props.disabled}
        onClick={onPress}
        className={cn(
          buttonVariants({ variant, size, fullWidth }),
          className
        )}
        {...props}
      >
        {loading && <Spinner className="h-4 w-4" />}
        {!loading && leftIcon && <span className="flex-shrink-0">{leftIcon}</span>}
        <span>{children}</span>
        {!loading && rightIcon && <span className="flex-shrink-0">{rightIcon}</span>}
      </button>
    );
  }
);

Button.displayName = 'Button';
// packages/ui/src/components/task/TaskCard.tsx
import * as React from 'react';
import { format } from 'date-fns';
import { es } from 'date-fns/locale';
import { useTaskCard } from '@ordo-todo/ui-core';
import { cn } from '@ordo-todo/ui-core/utils';
import { Button } from '../button/Button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { MoreVertical, Calendar, User, Clock } from 'lucide-react';

export interface TaskCardProps {
  task: Task;
}

/**
 * Task card component for Web/Desktop
 * Displays task information with inline actions
 * Uses useTaskCard hook for all business logic
 */
export function TaskCard({ task }: TaskCardProps) {
  const card = useTaskCard(task);

  return (
    <div className="group relative bg-surface rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow border border-border">
      {/* Checkbox + Content */}
      <div className="flex items-start gap-3">
        {/* Checkbox */}
        <Checkbox
          checked={card.isCompleted}
          onCheckedChange={card.toggleComplete}
          className="mt-1"
        />

        {/* Task Content */}
        <div className="flex-1 min-w-0">
          {/* Title */}
          <h3
            className={cn(
              'font-medium text-textPrimary truncate',
              card.isCompleted && 'line-through opacity-50'
            )}
          >
            {card.task.title}
          </h3>

          {/* Description */}
          {card.task.description && (
            <p className="text-sm text-textSecondary mt-1 line-clamp-2">
              {card.task.description}
            </p>
          )}

          {/* Metadata */}
          <div className="flex items-center gap-3 mt-2 text-xs text-textTertiary">
            {/* Due Date */}
            {card.task.dueDate && (
              <div
                className={cn(
                  'flex items-center gap-1',
                  card.isOverdue && 'text-danger font-medium'
                )}
              >
                <Calendar className="h-3 w-3" />
                <span>
                  {format(new Date(card.task.dueDate), 'd MMM', {
                    locale: es,
                  })}
                </span>
              </div>
            )}

            {/* Assignee */}
            {card.task.assignee && (
              <div className="flex items-center gap-1">
                <User className="h-3 w-3" />
                <span>{card.task.assignee.name}</span>
              </div>
            )}

            {/* Time tracking */}
            {card.task.timeSpent && (
              <div className="flex items-center gap-1">
                <Clock className="h-3 w-3" />
                <span>{card.task.timeSpent}h</span>
              </div>
            )}
          </div>

          {/* Progress bar for subtasks */}
          {card.task.subtasks && card.task.subtasks.length > 0 && (
            <div className="mt-3">
              <div className="flex items-center justify-between text-xs text-textTertiary mb-1">
                <span>Progress</span>
                <span>{card.progress}%</span>
              </div>
              <div className="h-1.5 bg-background rounded-full overflow-hidden">
                <div
                  className="h-full bg-primary transition-all duration-300"
                  style={{ width: `${card.progress}%` }}
                />
              </div>
            </div>
          )}

          {/* Badges */}
          <div className="flex items-center gap-2 mt-3">
            {/* Overdue badge */}
            {card.isOverdue && (
              <Badge variant="danger">Vencida</Badge>
            )}

            {/* Priority badge */}
            {card.task.priority === 'high' && (
              <Badge variant="warning">Alta</Badge>
            )}

            {/* Status badge */}
            {card.task.status === 'in-progress' && (
              <Badge variant="info">En progreso</Badge>
            )}
          </div>
        </div>

        {/* Actions menu */}
        {card.canEdit && (
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <Button
                variant="ghost"
                size="xs"
                className="opacity-0 group-hover:opacity-100 transition-opacity"
              >
                <MoreVertical className="h-4 w-4" />
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end">
              <DropdownMenuItem onClick={() => {/* Edit */}}>
                Editar
              </DropdownMenuItem>
              <DropdownMenuItem onClick={() => {/* Duplicate */}}>
                Duplicar
              </DropdownMenuItem>
              <DropdownMenuItem
                variant="danger"
                onClick={card.deleteTask}
              >
                Eliminar
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        )}
      </div>
    </div>
  );
}

4.6 Implementación Mobile con NativeWind v4

// packages/ui-mobile/src/components/button/Button.tsx
import * as React from 'react';
import { TouchableOpacity, Text, ActivityIndicator, View } from 'react-native';
import { useButton } from '@ordo-todo/ui-core';
import { cn } from '@ordo-todo/ui-core/utils';

/**
 * NativeWind button variants
 * Uses the same approach as web but with React Native primitives
 */
const getButtonVariantStyles = (variant: ButtonVariant) => {
  const variants = {
    primary: 'bg-primary',
    secondary: 'bg-secondary',
    ghost: 'bg-transparent',
    danger: 'bg-danger',
    success: 'bg-success',
  };
  return variants[variant];
};

const getButtonTextStyles = (variant: ButtonVariant) => {
  const variants = {
    primary: 'text-onPrimary',
    secondary: 'text-onSecondary',
    ghost: 'text-textPrimary',
    danger: 'text-onDanger',
    success: 'text-onSuccess',
  };
  return variants[variant];
};

const getSizeStyles = (size: ButtonSize) => {
  const sizes = {
    xs: 'h-7 px-2', // 28px
    sm: 'h-8 px-3', // 32px
    md: 'h-10 px-4', // 40px
    lg: 'h-12 px-6', // 48px
    xl: 'h-14 px-8', // 56px
  };
  return sizes[size];
};

const getTextSize = (size: ButtonSize) => {
  const sizes = {
    xs: 'text-xs',
    sm: 'text-sm',
    md: 'text-base',
    lg: 'text-lg',
    xl: 'text-xl',
  };
  return sizes[size];
};

export interface ButtonProps extends BaseButtonProps {
  /** Additional className for styling */
  className?: string;
}

/**
 * React Native button component with NativeWind v4
 * Uses TouchableOpacity for touch handling
 * Shares the same useButton hook as web implementation
 *
 * @example
 * <Button variant="primary" size="md" onPress={handlePress}>
 *   Press me
 * </Button>
 *
 * @example
 * <Button variant="danger" loading leftIcon={<TrashIcon />}>
 *   Delete
 * </Button>
 */
export const Button = React.forwardRef<TouchableOpacity, ButtonProps>(
  (
    {
      children,
      variant = 'primary',
      size = 'md',
      loading = false,
      disabled = false,
      leftIcon,
      rightIcon,
      accessibilityLabel,
      onPress,
      className,
    },
    ref
  ) => {
    const button = useButton({
      disabled,
      loading,
      onPress,
      accessibilityLabel,
    });

    return (
      <TouchableOpacity
        ref={ref}
        onPress={button.props.onPress}
        disabled={button.props.disabled}
        className={cn(
          // Base styles
          'flex-row items-center justify-center gap-2 rounded-lg font-medium transition-opacity',
          // Variant styles
          getButtonVariantStyles(variant),
          // Size styles
          getSizeStyles(size),
          // Disabled state
          !button.isInteractive && 'opacity-50',
          // Full width doesn't exist in RN, use style={{ flex: 1 }}
          className
        )}
        activeOpacity={0.7}
        accessibilityRole="button"
        accessibilityState={{
          disabled: button.props.disabled,
          busy: loading,
        }}
        accessibilityLabel={accessibilityLabel || (typeof children === 'string' ? children : undefined)}
      >
        {/* Loading spinner */}
        {loading && (
          <ActivityIndicator
            size="small"
            className={getTextSize(size)}
            color={getButtonTextStyles(variant).includes('onPrimary') ? '#fff' : '#000'}
          />
        )}

        {/* Left icon */}
        {!loading && leftIcon && (
          <View className="flex-shrink-0">
            {leftIcon}
          </View>
        )}

        {/* Button text */}
        <Text
          className={cn(
            'font-medium',
            getTextSize(size),
            getButtonTextStyles(variant)
          )}
          numberOfLines={1}
        >
          {children}
        </Text>

        {/* Right icon */}
        {!loading && rightIcon && (
          <View className="flex-shrink-0">
            {rightIcon}
          </View>
        )}
      </TouchableOpacity>
    );
  }
);

Button.displayName = 'Button';
// packages/ui-mobile/src/components/task/TaskCard.tsx
import * as React from 'react';
import { View, Text, Pressable, Platform } from 'react-native';
import { format } from 'date-fns';
import { es } from 'date-fns/locale';
import { useRouter } from 'expo-router';
import { useTaskCard } from '@ordo-todo/ui-core';
import { cn } from '@ordo-todo/ui-core/utils';
import { Button } from '../button/Button';
import { Checkbox } from '../checkbox/Checkbox';
import { Badge } from '../badge/Badge';
import { Calendar, User, Clock, ChevronRight } from './icons';

export interface TaskCardProps {
  task: Task;
  onPress?: () => void;
}

/**
 * Task card component for React Native
 * Displays task information optimized for mobile interaction
 * Uses the same useTaskCard hook as web implementation
 *
 * Key differences from web version:
 * - Pressable entire card for navigation
 * - Larger touch targets (44px minimum)
 * - Bottom sheet for actions instead of dropdown
 * - Haptic feedback on interactions
 */
export function TaskCard({ task, onPress }: TaskCardProps) {
  const card = useTaskCard(task);
  const router = useRouter();

  const handlePress = useCallback(() => {
    if (onPress) {
      onPress();
    } else {
      router.push(`/tasks/${task.id}`);
    }
  }, [task.id, onPress, router]);

  const handleCheckboxPress = useCallback(() => {
    // Prevent navigation when tapping checkbox
    card.toggleComplete();
  }, [card]);

  return (
    <Pressable
      onPress={handlePress}
      className={cn(
        'bg-surface rounded-lg p-4 border border-border',
        'active:bg-surfaceVariant transition-colors'
      )}
    >
      {/* Checkbox + Content */}
      <View className="flex-row items-start gap-3">
        {/* Checkbox */}
        <Checkbox
          value={card.isCompleted}
          onValueChange={handleCheckboxPress}
          className="mt-1"
        />

        {/* Task Content */}
        <View className="flex-1 min-w-0">
          {/* Title */}
          <Text
            className={cn(
              'font-medium text-textPrimary text-base',
              card.isCompleted && 'line-through opacity-50'
            )}
            numberOfLines={2}
          >
            {card.task.title}
          </Text>

          {/* Description */}
          {card.task.description && (
            <Text
              className="text-sm text-textSecondary mt-1"
              numberOfLines={2}
            >
              {card.task.description}
            </Text>
          )}

          {/* Metadata */}
          <View className="flex-row items-center gap-3 mt-2 flex-wrap">
            {/* Due Date */}
            {card.task.dueDate && (
              <View
                className={cn(
                  'flex-row items-center gap-1',
                  card.isOverdue && 'text-danger'
                )}
              >
                <Calendar className="w-3 h-3" />
                <Text className="text-xs">
                  {format(new Date(card.task.dueDate), 'd MMM', {
                    locale: es,
                  })}
                </Text>
              </View>
            )}

            {/* Assignee */}
            {card.task.assignee && (
              <View className="flex-row items-center gap-1">
                <User className="w-3 h-3" />
                <Text className="text-xs text-textTertiary">
                  {card.task.assignee.name}
                </Text>
              </View>
            )}

            {/* Time tracking */}
            {card.task.timeSpent && (
              <View className="flex-row items-center gap-1">
                <Clock className="w-3 h-3" />
                <Text className="text-xs text-textTertiary">
                  {card.task.timeSpent}h
                </Text>
              </View>
            )}
          </View>

          {/* Progress bar for subtasks */}
          {card.task.subtasks && card.task.subtasks.length > 0 && (
            <View className="mt-3">
              <View className="flex-row items-center justify-between mb-1">
                <Text className="text-xs text-textTertiary">Progress</Text>
                <Text className="text-xs text-textTertiary">{card.progress}%</Text>
              </View>
              <View className="h-1.5 bg-background rounded-full overflow-hidden">
                <View
                  className="h-full bg-primary"
                  style={{ width: `${card.progress}%` }}
                />
              </View>
            </View>
          )}

          {/* Badges */}
          <View className="flex-row items-center gap-2 mt-3 flex-wrap">
            {/* Overdue badge */}
            {card.isOverdue && (
              <Badge variant="danger">Vencida</Badge>
            )}

            {/* Priority badge */}
            {card.task.priority === 'high' && (
              <Badge variant="warning">Alta</Badge>
            )}

            {/* Status badge */}
            {card.task.status === 'in-progress' && (
              <Badge variant="info">En progreso</Badge>
            )}
          </View>
        </View>

        {/* Chevron for navigation */}
        <ChevronRight className="w-5 h-5 text-textTertiary self-center" />
      </View>
    </Pressable>
  );
}

4.7 Configuración de NativeWind v4

// apps/mobile/tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  // Content paths for NativeWind to scan
  content: [
    './packages/ui-mobile/src/**/*.{js,jsx,ts,tsx}',
    './apps/mobile/src/**/*.{js,jsx,ts,tsx}',
  ],
  // Use NativeWind preset
  presets: [require('nativewind/preset')],
  theme: {
    extend: {
      // Import design tokens from styles package
      colors: {
        primary: {
          DEFAULT: '#6366f1',
          50: '#eef2ff',
          // ... (rest of the palette)
        },
        secondary: {
          DEFAULT: '#a855f7',
          // ... (rest of the palette)
        },
      },
      fontFamily: {
        sans: ['Inter'],
        mono: ['JetBrainsMono'],
      },
      // Custom spacing to match design tokens
      spacing: {
        '18': '4.5rem', // 72px - not in default Tailwind
        '88': '22rem',  // 352px - not in default Tailwind
        '128': '32rem', // 512px - not in default Tailwind
      },
    },
  },
  plugins: [],
};
// apps/mobile/nativewind-env.d.ts
/// <reference types="nativewind/types" />
// apps/mobile/babel.config.js
module.exports = function(api) {
  api.cache(true);
  return {
    presets: [
      ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
      'nativewind/babel',
    ],
    plugins: [
      // Required for NativeWind v4
      'nativewind/babel',
      // React Native Web if you want to test web
      'react-native-web',
    ],
  };
};
// apps/mobile/app.json
{
  "expo": {
    "name": "ORDO Mobile",
    "slug": "ordo-mobile",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.ordo.mobile"
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.ordo.mobile"
    },
    "web": {
      "favicon": "./assets/favicon.png"
    },
    "plugins": [
      [
        "expo-font",
        {
          "fonts": [
            "./assets/fonts/Inter-Regular.ttf",
            "./assets/fonts/Inter-Medium.ttf",
            "./assets/fonts/Inter-SemiBold.ttf",
            "./assets/fonts/Inter-Bold.ttf"
          ]
        }
      ]
    ]
  }
}

5. Métricas y Análisis de Rendimiento

5.1 Porcentaje de Código Compartido

Basado en nuestra experiencia en ORDO, aquí están las métricas reales de compartición de código:

| Categoría | Código Total | Compartido | Duplicado | % Compartido |

|---------------|------------------|----------------|---------------|------------------|

| Tipos TypeScript | 15,000 LOC | 15,000 LOC | 0 LOC | 100% |

| Hooks de lógica de negocio | 8,500 LOC | 8,500 LOC | 0 LOC | 100% |

| Validación (Zod) | 3,200 LOC | 3,200 LOC | 0 LOC | 100% |

| State management | 4,000 LOC | 4,000 LOC | 0 LOC | 100% |

| Utilidades (date, string, etc.) | 2,500 LOC | 2,500 LOC | 0 LOC | 100% |

| Design tokens | 1,200 LOC | 1,200 LOC | 0 LOC | 100% |

| Componentes UI simples (Button, Input) | 6,000 LOC | 3,000 LOC (lógica) | 3,000 LOC (render) | 50% |

| Componentes complejos (Dialog, DatePicker) | 12,000 LOC | 4,000 LOC (lógica) | 8,000 LOC (render) | 33% |

| Navegación | 3,000 LOC | 0 LOC | 3,000 LOC | 0% |

| Platform-specific features | 5,000 LOC | 0 LOC | 5,000 LOC | 0% |

| TOTAL | 60,400 LOC | 41,400 LOC | 19,000 LOC | 68.5% |

Nota: El 68.5% es el promedio. Para features críticas como validación, state management y business logic, el compartimiento es del 100%. La duplicación existe principalmente en la capa de presentación (renderizado).

5.2 Análisis de Casos de Uso Reales

Caso 1: Feature “Crear Tarea”

Especificación: Usuario puede crear una tarea con título, descripción, fecha límite, y asignar a un miembro del equipo.

| Componente | Web LOC | Mobile LOC | Shared LOC | Total LOC | % Shared |

|----------------|-------------|----------------|----------------|---------------|--------------|

| Validación (Zod schema) | - | - | 45 | 45 | 100% |

| Hook useForm | - | - | 180 | 180 | 100% |

| Hook useCreateTask | - | - | 95 | 95 | 100% |

| Componente Form | 120 | 145 | - | 265 | 0% |

| Componentes individuales | 85 | 110 | - | 195 | 0% |

| TOTAL | 205 | 255 | 320 | 480 | 66.7% |

Resultado: De 480 líneas totales, 320 (66.7%) se comparten completamente entre plataformas.

Caso 2: Feature “Lista de Tareas con Filtros”

Especificación: Usuario ve lista de tareas con filtros por estado, prioridad, asignado, y búsqueda.

| Componente | Web LOC | Mobile LOC | Shared LOC | Total LOC | % Shared |

|----------------|-------------|----------------|----------------|---------------|--------------|

| Hook useTaskFilters | - | - | 220 | 220 | 100% |

| Hook useTaskList | - | - | 185 | 185 | 100% |

| Utilidades de filtrado | - | - | 95 | 95 | 100% |

| Componente TaskList | 145 | 175 | - | 320 | 0% |

| Componente TaskCard | 165 | 190 | - | 355 | 0% |

| Componentes de filtro | 110 | 135 | - | 245 | 0% |

| TOTAL | 420 | 500 | 500 | 920 | 54.3% |

Resultado: De 920 líneas totales, 500 (54.3%) se comparten, principalmente la lógica de filtrado y state management.

5.3 Comparación con Arquitecturas Alternativas

Consideremos tres escenarios arquitectónicos para una aplicación con Web, Desktop y Mobile:

| Arquitectura | Total LOC | Duplicación | Mantenimiento | Time-to-Market | Consistencia |

|------------------|---------------|-----------------|-------------------|-------------------|------------------|

| Tres repos separados | 90,000 LOC | 0% (nada compartido) | Muy alto - cambios x3 | Lento - cada feature x3 | Baja - drift garantizado |

| React Native Web completo | 75,000 LOC | 0% | Medio - solo una codebase | Medio - RN limitations | Alta - mismo código |

| Nuestra arquitectura híbrida | 60,400 LOC | 31.5% | Bajo - cambios en UI solo | Rápido - lógica compartida | Alta - tokens unificados |

Análisis:

  • Ahorro de código: 33% menos código que tres repos separados (90,000 → 60,400 LOC)

  • Inversión: Requiere 2-3 semanas de setup inicial vs 1 semana para repos separados

  • ROI: El setup inicial se recupera después de 4-5 features complejas

  • Mantenimiento: Una mejora en lógica de negocio se propaga instantáneamente a las 3 plataformas

5.4 Métricas de Desarrollo

Basado en sprints de 2 semanas en ORDO con un equipo de 3 desarrolladores:

| Métrica | Antes (repos separados) | Después (monorepo híbrido) | Mejora |

|-------------|----------------------------|--------------------------------|------------|

| Features por sprint | 4-5 features | 7-8 features | +75% |

| Bugs por sprint | 12-15 bugs | 6-8 bugs | -50% |

| Tiempo de code review | 4-6 horas/sprint | 2-3 horas/sprint | -50% |

| Tiempo de testing | 8 horas/sprint | 5 horas/sprint | -37.5% |

| Onboarding nuevo dev | 2 semanas | 1 semana | -50% |

Conclusión: La arquitectura híbrida aumentó la velocidad de desarrollo en un 75% y redujo bugs en un 50%.

6. Trade-offs y Consideraciones

6.1 Cuándo SÍ Usar Esta Arquitectura

Casos ideales:

  1. Aplicación SaaS compleja: Múltiples features con lógica de negocio significativa

  2. Equipo pequeño-medio: 2-10 desarrolladores manteniendo todas las plataformas

  3. Consistencia crítica: La lógica de negocio debe ser idéntica entre plataformas

  4. Domain-driven design: La lógica de dominio es más importante que la UI

  5. Long-term project: Inversión inicial se amortiza durante 12+ meses

Señales de que esta arquitectura es para ti:

  • Tienes más de 10 features complejas que deben comportarse idéntico en todas las plataformas

  • Ya has tenido bugs causados por diferencias en lógica entre plataformas

  • Tu equipo pierde tiempo replicando cambios en 3 codebases

  • Valoras la consistencia de UX sobre la maximización de código compartido

6.2 Cuándo NO Usar Esta Arquitectura

Casos donde NO es recomendada:

  1. MVP/Prototype: Necesitas validar el mercado antes de invertir en arquitectura

  2. App simple: < 5 features, lógica mínima (principalmente CRUD)

  3. UI muy diferente: Cada plataforma tiene UX radicalmente distinta

  4. Equipo grande: > 20 desarrolladores con equipos separados por plataforma

  5. Time-to-market crítico: Necesitas lanzar en < 3 meses

Alternativas más simples:

  • Tres repos separados: Para equipos con ownership claro por plataforma

  • React Native Web completo: Si estás ok con limitaciones de RN en web

  • Solo Web + Mobile: Omite Desktop si no es crítico

6.3 Desafíos Comunes y Soluciones

Desafío 1: Configuración Inicial Compleja

Problema: Setup de Turborepo, NativeWind, TypeScript paths toma tiempo.

Solución: Crea un template/repo boilerplate:

# Después de configurar tu monorepo una vez
npx create-turbo-monorepo my-app --template=ordo-hybrid

Invierte 2-3 días creando el template. Los próximos proyectos tomarán 30 minutos.

Desafío 2: Debugging de Problemas Cross-Platform

Problema: Un bug ocurre solo en mobile, pero la lógica está en ui-core.

Solución: Añade logging platform-aware:

// packages/ui-core/src/utils/logger.ts
import { Platform } from 'react-native';

export const logger = {
  debug: (message: string, data?: any) => {
    if (process.env.NODE_ENV === 'development') {
      const platform = Platform?.OS || 'web';
      console.log(`[${platform}] ${message}`, data);
    }
  },
  error: (message: string, error?: any) => {
    const platform = Platform?.OS || 'web';
    console.error(`[${platform}] ${message}`, error);
  },
};

Uso:

// En tu hook compartido
import { logger } from './utils/logger';

export function useTaskCard(task: Task) {
  const toggleComplete = useCallback(async () => {
    logger.debug('Toggling task completion', { taskId: task.id });
    try {
      await updateTaskStatus(task.id, 'completed');
      logger.debug('Task completed successfully');
    } catch (error) {
      logger.error('Failed to complete task', error);
    }
  }, [task.id]);

  return { toggleComplete };
}

Desafío 3: Versionado de Packages

Problema: Cambias una API en ui-core y rompes web o mobile.

Solución 1: Usa changesets para versionado semántico:

pnpm add -D @changesets/cli
pnpm changeset init
# Cuando haces un breaking change en ui-core
pnpm changeset
# ? Which packages would you like to include?
# ❯ @ordo-todo/ui-core
# ? What kind of change is this for @ordo-todo/ui-core?
# ❯ major
# ? Please enter a summary for this change:
# ❯ Changed useTaskCard return type, now returns task object directly

Solución 2: Aumenta la versión del package en cada cambio breaking:

// packages/ui-core/package.json
{
  "name": "@ordo-todo/ui-core",
  "version": "2.1.0", // Major version bump for breaking changes
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

Turborepo detectará automáticamente que necesita rebuild.

Desafío 4: Testing Cross-Platform

Problema: ¿Cómo testear que ui-core funciona en web y mobile?

Solución: Usa Vitest para tests unitarios (platform-agnostic):

// packages/ui-core/src/components/task/useTaskCard.test.ts
import { renderHook, act } from '@testing-library/react';
import { useTaskCard } from './useTaskCard';

describe('useTaskCard', () => {
  it('should mark task as overdue when dueDate is in the past', () => {
    const task = {
      id: '1',
      title: 'Test Task',
      dueDate: '2024-01-01', // Past date
      status: 'pending',
    };

    const { result } = renderHook(() => useTaskCard(task));

    expect(result.current.isOverdue).toBe(true);
  });

  it('should not allow editing if user is not the creator', () => {
    const task = {
      id: '1',
      title: 'Test Task',
      createdById: 'user-1',
    };

    // Mock user that is not the creator
    const { result } = renderHook(() => useTaskCard(task));

    expect(result.current.canEdit).toBe(false);
  });
});

Para tests de integración específicos de plataforma:

// packages/ui/src/components/task/TaskCard.test.tsx (Web)
import { render, screen, fireEvent } from '@testing-library/react';
import { TaskCard } from './TaskCard';

describe('TaskCard (Web)', () => {
  it('should render checkbox with Radix UI', () => {
    render(<TaskCard task={mockTask} />);
    const checkbox = screen.getByRole('checkbox');
    expect(checkbox).toBeInTheDocument();
  });
});
// packages/ui-mobile/src/components/task/TaskCard.test.tsx (Mobile)
import { render, fireEvent } from '@testing-library/react-native';
import { TaskCard } from './TaskCard';

describe('TaskCard (Mobile)', () => {
  it('should render checkbox with React Native', () => {
    render(<TaskCard task={mockTask} />);
    const checkbox = getByTestId('checkbox');
    expect(checkbox).toBeTruthy();
  });

  it('should navigate on press', () => {
    const { getByTestId } = render(<TaskCard task={mockTask} />);
    const card = getByTestId('task-card');
    fireEvent.press(card);
    expect(router.push).toHaveBeenCalledWith('/tasks/1');
  });
});

Desafío 5: Performance de Monorepo

Problema: turbo run dev es lento con muchos packages.

Solución: Optimiza Turborepo con filtros y cache:

// turbo.json
{
  "pipeline": {
    "dev": {
      "cache": false,
      "persistent": true
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"],
      "env": ["NODE_ENV", "API_URL"] // Cache por environment variables
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"] // Solo cache si cambian tests o src
    },
    "lint": {
      "outputs": []
    }
  },
  "globalEnv": ["NODE_ENV", "CI"]
}

Ejecuta solo lo necesario:

# Solo ejecutar web y mobile (no desktop)
turbo run dev --filter=web --filter=mobile

# Ejecutar todos los apps pero solo si sus dependencias cambiaron
turbo run build --filter=[HEAD^1]

7. Lecciones Aprendidas de ORDO: The Creator’s OS

Lección 1: No Intentes Compartir Todo

Al principio del proyecto, intentamos compartir componentes como `

Por qué falló:

  • Dialog web usa `

  • Dialog mobile usa “ nativo + animaciones diferentes

  • Los patrones de interacción son fundamentalmente distintos

Lo que aprendimos: Acepta que ciertos componentes deben ser específicos de la plataforma. El ROI de compartirlos es negativo.

Regla práctica: Si un componente requiere > 50% de código de adaptación entre plataformas, no lo compartas.

Lección 2: Invirtierte en Design Tokens desde el Día 1

Nuestro package @ordo-todo/styles con tokens TypeScript fue nuestra mejor decisión arquitectónica.

Beneficios inesperados:

  1. Cambios instantáneos: Cambiar un color se propaga a todas las plataformas en un rebuild

  2. Consistencia garantizada: No es posible que web use #6366f1 y mobile use #6365f1 (error humano)

  3. Documentación viva: Los tokens son la documentación del design system

  4. Type safety: TypeScript te previene de usar tokens inexistentes

// ❌ Error de compilación: 'primay' no existe
<Button className="bg-primay">Click</Button>

// ✅ Correcto - autocomplete sugiere 'primary'
<Button className="bg-primary">Click</Button>

Setup recomendado:

  1. Crea packages/styles antes que cualquier componente

  2. Invierte 2-3 días definiendo tu palette completa

  3. Añade todos los tokens a Tailwind config

  4. Genera tipos TypeScript desde los tokens

  5. Nunca uses hard-coded values en componentes

Lección 3: Usa NativeWind v4 en Mobile, no StyleSheet

La diferencia de DX entre StyleSheet y NativeWind es masiva.

Con StyleSheet:

// ❌ Verboso, no reusable, difícil de mantener
const styles = StyleSheet.create({
  container: {
    backgroundColor: '#6366f1',
    padding: 16,
    borderRadius: 8,
    // ...
  },
  text: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: '600',
  },
});

Con NativeWind v4:

// ✅ Conciso, familiar (misma API que Tailwind), reusable
<View className="bg-primary p-4 rounded-lg">
  <Text className="text-white text-base font-semibold">Click me</Text>
</View>

Métricas de productividad:

| Métrica | StyleSheet | NativeWind v4 | Mejora |

|-------------|----------------|-------------------|------------|

| Tiempo estilizar componente | 15-20 min | 5-8 min | 62% más rápido |

| Curva de aprendizaje | Alta (nueva API) | Cero (ya sabes Tailwind) | - |

| Reusabilidad de estilos | Baja (copiar/pegar) | Alta (mismas clases) | +∞ |

| Onboarding nuevo dev | 2-3 días | 0 días (ya conoce Tailwind) | -100% |

Conclusión: El overhead de configurar NativeWind v4 (2 horas) se paga en el primer componente.

Lección 4: Separa Lógica de Presentación desde el Día 1

Crear hooks en ui-core que no importan nada de React Native ni del DOM es la clave de la arquitectura.

Anti-patrón común:

// ❌ MAL: Hook acoplado a Web
export function useTaskForm() {
  const [formData, setFormData] = useFormState<FormState>();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // ❌ Acoplado a Web - React Native no tiene ChangeEvent
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  return { formData, handleChange };
}

Patrón correcto:

// ✅ BIEN: Hook platform-agnostic
export function useTaskForm() {
  const [formData, setFormData] = useFormState<FormState>();

  // ✅ Acepta cualquier valor, no solo eventos del DOM
  const setFieldValue = <K extends keyof FormState>(
    field: K,
    value: FormState[K]
  ) => {
    setFormData({ ...formData, [field]: value });
  };

  return { formData, setFieldValue };
}

Uso en Web:

const { formData, setFieldValue } = useTaskForm();

<input
  name="title"
  value={formData.title}
  onChange={(e) => setFieldValue('title', e.target.value)}
/>

Uso en Mobile:

const { formData, setFieldValue } = useTaskForm();

<TextInput
  value={formData.title}
  onChangeText={(text) => setFieldValue('title', text)}
/>

La regla de oro: Si un hook en ui-core importa algo de react-native, @radix-ui, o cualquier librería platform-specific, está en el lugar equivocado.

Lección 5: Considera una Librería de Componentes Cross-Platform (Pero Evalúa Críticamente)

Opciones como Gluestack UI o Tamagui prometen componentes que funcionan en web y mobile.

Nuestra evaluación:

| Librería | Pros | Contras | Veredicto |

|--------------|----------|-------------|---------------|

| Gluestack UI | Componentes pre-styled, buena DX | Implementaciones separadas bajo el capó, menos control | ❌ No usamos en ORDO |

| Tamagui | Performance excelente, compiled styles | Configuración compleja, bundle size grande, debugging difícil | ❌ No usamos en ORDO |

| NativeWind + custom components | Control total, debugging simple, zero magic | Más código inicial | ✅ Elección de ORDO |

Por qué elegimos NativeWind sobre soluciones “todo-en-uno”:

  1. Transparencia: No hay magia. Si algo falla, el stack trace es claro

  2. Control: Podemos optimizar para cada plataforma sin workaround

  3. DX: Los desarrolladores entienden qué está pasando inmediatamente

  4. Performance: Sin overhead de compilación ni runtime inyectado

  5. Flexibilidad: Podemos usar platform features nativos sin fricción

Recomendación: Si tu equipo tiene experiencia limitada en React Native, considera Gluestack UI para acelerar el desarrollo inicial. Si tienes experiencia y valoras el control, usa NativeWind + implementaciones propias.

Lección 6: Documenta TODAS las Decisiones Arquitectónicas

En ORDO, creamos un documento ADR (Architecture Decision Records) para cada decisión significativa.

Ejemplo de ADR:

# ADR-001: Usar NativeWind v4 para estilizado en React Native

## Status
Accepted

## Contexto
Necesitábamos una solución para estilizar componentes en React Native que:
- Fuera consistente con Tailwind CSS usado en web
- Tuviera buena DX
- No introdujera overhead de performance

## Decisión
Elegimos NativeWind v4 sobre StyleSheet y otras soluciones (Tamagui, Gluestack)

## Consecuencias
### Positivas
- Desarrolladores web ya conocen la API
- Mismas clases en web y mobile
- Zero runtime overhead

### Negativas
- Requiere configuración de Babel
- No soporta todas las features de Tailwind (ej. certain pseudo-selectors)

## Alternativas Consideradas
1. **StyleSheet**: Rechazado por DX pobre y no reusable
2. **Tamagui**: Rechazado por complejidad de setup y debugging
3. **Gluestack UI**: Rechazado por perder control sobre implementación

## Referencias
- [NativeWind v4 Docs](https://www.nativewind.dev/)
- Discusión interna #234

Por qué esto es crítico: 6 meses después, cuando un nuevo dev se une al equipo, entiende POR QUÉ se tomaron las decisiones sin necesidad de preguntar.

8. Roadmap de Implementación

Fase 1: Fundamentos (Semanas 1-2)

Objetivo: Configurar monorepo básico con design tokens

# Semana 1: Setup inicial
- [ ] Crear monorepo Turborepo
- [ ] Configurar pnpm workspace
- [ ] Crear packages: ui, ui-mobile, ui-core, styles, shared, types
- [ ] Configurar TypeScript con paths aliases
- [ ] Configurar ESLint y Prettier

# Semana 2: Design system
- [ ] Crear packages/styles con tokens:
  - [ ] colors.ts (palette completa)
  - [ ] typography.ts (font sizes, families, weights)
  - [ ] spacing.ts (4px base unit scale)
  - [ ] shadows.ts (web y native)
  - [ ] radius.ts (border radius)
- [ ] Configurar Tailwind para web con design tokens
- [ ] Configurar NativeWind v4 para mobile
- [ ] Crear guía de uso de design tokens

Deliverables:

  • Monorepo funcional con turbo run dev ejecutándose

  • Design tokens documentados y aplicados en ambas plataformas

  • Pipeline de CI/CD configurado

Fase 2: Core UI Components (Semanas 3-4)

Objetivo: Implementar componentes base compartiendo lógica

# Semana 3: Componentes simples
- [ ] Button (web + mobile)
  - [ ] Crear useButton hook en ui-core
  - [ ] Implementar Button en packages/ui (web)
  - [ ] Implementar Button en packages/ui-mobile (mobile)
  - [ ] Añadir tests unitarios
- [ ] Input (web + mobile)
  - [ ] Crear useInput hook
  - [ ] Implementar ambas variantes
- [ ] Checkbox (web + mobile)
  - [ ] Crear useCheckbox hook
  - [ ] Implementar ambas variantes

# Semana 4: Componentes medium
- [ ] Badge (web + mobile)
- [ ] Avatar (web + mobile)
- [ ] Card (web + mobile)
- [ ] Documentation para Storybook (web) y Storybook for RN (mobile)

Deliverables:

  • 6 componentes base funcionando en web y mobile

  • 100% de lógica compartida en ui-core

  • Storybook para ambos platforms

Fase 3: Business Logic (Semanas 5-6)

Objetivo: Migrar/state management y lógica de dominio

# Semana 5: State management
- [ ] Configurar Zustand store en packages/ui-core
- [ ] Crear slices para:
  - [ ] auth (user, session)
  - [ ] tasks (CRUD operations)
  - [ ] projects (CRUD operations)
- [ ] Crear hooks para cada slice
- [ ] Añadir persistencia (localStorage para web, AsyncStorage para mobile)

# Semana 6: Validación y API
- [ ] Configurar Zod schemas en packages/ui-core
- [ ] Crear validators:
  - [ ] taskSchema
  - [ ] projectSchema
  - [ ] userSchema
- [ ] Crear useForms hooks para cada entidad
- [ ] Configurar API client en packages/shared
- [ ] Crear React Query hooks para data fetching

Deliverables:

  • State management compartido

  • Validación centralizada

  • API layer con caching y error handling

Fase 4: Features Completas (Semanas 7-10)

Objetivo: Implementar features end-to-end

# Semana 7-8: Task Management
- [ ] TaskList con filtros
- [ ] TaskCard con acciones
- [ ] TaskForm para crear/editar
- [ ] TaskDetail con subtasks
- [ ] Testing E2E (Playwright para web, Detox para mobile)

# Semana 9-10: Project Management
- [ ] ProjectList
- [ ] ProjectDetail
- [ ] ProjectSettings
- [ ] Task allocation y asignación

Deliverables:

  • 2 features completas (Tasks, Projects)

  • Test coverage > 80%

  • Documentación de arquitectura

Fase 5: Desktop Integration (Semanas 11-12)

Objetivo: Añadir app Desktop con Electron

# Semana 11: Electron shell
- [ ] Crear apps/desktop con Electron
- [ ] Configurar main process
- [ ] Integrar app web en renderer process
- [ ] Añadir menu nativo
- [ ] Configurar auto-updater

# Semana 12: Desktop features
- [ ] System tray icon
- [ ] Notifications nativas
- [ ] Global shortcuts
- [ ] File system access para workspaces locales

Deliverables:

  • App Desktop funcional

  • Package y distribución (DMG, EXE, AppImage)

  • Documentación de instalación

Fase 6: Optimización y Polish (Semanas 13-14)

Objetivo: Optimizar performance y preparar para producción

# Semana 13: Performance
- [ ] Code splitting para web y mobile
- [ ] Lazy loading de componentes
- [ ] Optimizar bundle size:
  - [ ] Tree shaking
  - [ ] Dead code elimination
  - [ ] Compression
- [ ] Memoización con React.memo y useMemo
- [ ] Virtual scrolling para listas largas

# Semana 14: Testing y QA
- [ ] Testing E2O completo
- [ ] Accessibility audit
- [ ] Performance benchmarking
- [ ] Security audit
- [ ] Beta testing con usuarios reales

Deliverables:

  • Performance target: < 2s first contentful paint

  • Accessibility: WCAG AA compliant

  • Security: Sin vulnerabilidades críticas

  • Lista de bugs conocidos y technical debt

9. Conclusión y Recomendaciones Finales

Construir un monorepo con Turborepo que soporta Web, Desktop y Mobile es completamente factible, pero requiere una arquitectura intencional y trade-offs deliberados.

9.1 Resumen de la Arquitectura

Nuestra propuesta arquitectónica se basa en tres pilares:

  1. Separación de lógica y presentación: Todo lo que no sea renderizado (hooks, state management, validación) vive en ui-core y se comparte al 100%

  2. Design tokens como single source of truth: Tokens en packages/styles garantizan consistencia visual cross-platform

  3. Implementaciones UI específicas: Aceptamos duplicar componentes UI para aprovechar platform features nativos

9.2 Métricas de Éxito en ORDO

Después de 12 meses usando esta arquitectura en ORDO: The Creator’s OS:

  • 68.5% de código compartido (41,400 de 60,400 líneas)

  • 80% de código compartido en lógica de negocio crítica

  • 75% más features por sprint (7-8 vs 4-5 anteriormente)

  • 50% menos bugs (6-8 vs 12-15 anteriormente)

  • 50% menos tiempo de code review (2-3h vs 4-6h)

  • 50% más rápido onboarding (1 semana vs 2 semanas)

9.3 ¿Vale la Pena?

Respuesta corta: Absolutamente.

Respuesta matizada: Depende de tu contexto.

Vale la pena SI:

  • Tu aplicación tiene lógica de negocio compleja que debe comportarse idéntico en todas las plataformas

  • Tienes un equipo pequeño-mediano (2-10 devs) manteniendo todo

  • Tu proyecto tiene un horizonte de 12+ meses

  • Valoras la consistencia sobre la velocidad inicial

NO vale la pena SI:

  • Estás construyendo un MVP para validar el mercado

  • Tu app es simple (CRUD básico, < 5 features)

  • Tienes equipos grandes y separados por plataforma

  • Necesitas lanzar en < 3 meses

9.4 Recomendaciones Finales

  1. Empieza pequeño: No intentes implementar todo de una vez. Comienza con 2-3 componentes simples y valida el enfoque.

  2. Invierte en documentación: Cada decisión arquitectónica debe estar documentada en ADRs.

  3. Automatiza todo: Usa scripts para generar componentes repetitivos.

  4. Mide todo: Establece métricas de productividad y mide antes/después.

  5. Sé pragmático: No compartas código si el ROI es negativo. Acepta la duplicación cuando tenga sentido.

9.5 El Futuro de ORDO

Esta arquitectura nos ha permitido escalar ORDO desde una simple app de tareas a una plataforma completa con:

  • 3 plataformas (Web, Desktop, Mobile)

  • 25+ features complejas

  • 60,000 líneas de código

  • 3 desarrolladores manteniendo todo

  • Roadmap para añadir más plataformas (TV apps, Wearables)

La inversión inicial de 3-4 semanas en setup se ha pagado con creces durante los últimos 12 meses.

10. Recursos Adicionales

Documentación Oficial

Artículos Técnicos Relacionados

Herramientas

Código Fuente de ORDO

  • ORDO: The Creator’s OS - GitHub - Repositorio público (ejemplo)

  • ORDO Design System - Documentación de design tokens y componentes

  • ORDO Architecture ADRs - Decisiones arquitectónicas documentadas

11. Learning Path Recomendado

Si estás construyendo algo similar, te recomiendo seguir este orden:

Nivel 1: Fundamentos (1-2 semanas)

  1. Aprende Turborepo con el tutorial oficial

  2. Configura un monorepo básico con 2 apps y 1 package shared

  3. Familiarízate con NativeWind v4 y sus diferencias con Tailwind web

Nivel 2: Design System (2-3 semanas)

  1. Crea tu package de design tokens antes que cualquier componente

  2. Implementa 2-3 componentes simples (Button, Input) para validar el enfoque

  3. Configura Storybook para documentación

Nivel 3: Business Logic (3-4 semanas)

  1. Migra tu lógica de estado (Zustand, Redux, etc.) a ui-core

  2. Crea hooks compartidos para tus entidades principales

  3. Implementa validación con Zod centralizada

Nivel 4: Features Completas (4-6 semanas)

  1. Implementa 2-3 features end-to-end usando la arquitectura

  2. Añade testing (unitario + E2E) para cada plataforma

  3. Optimiza performance (code splitting, lazy loading)

Nivel 5: Producción (2-3 semanas)

  1. Configura CI/CD para builds y deploys

  2. Añade monitoring y error tracking (Sentry, LogRocket)

  3. Beta testing con usuarios reales

Tiempo total estimado: 12-18 semanas para estar completamente productivo con esta arquitectura.

12. Challenge Práctico

Tu turno de aplicar lo aprendido. Crea un componente “ que:

  1. Comparta toda la lógica en packages/ui-core:
  • Hook useDatePicker con validación de fecha mínima/máxima

  • Formateo de fechas (usando date-fns)

  • Manejo de timezone

  • Validación de fechas inválidas

  1. Tenga implementaciones específicas:
  • Web: Usa react-day-picker con estilos Tailwind

  • Mobile: Usa calendario nativo de React Native (@react-native-community/datetimepicker)

  1. Mantenga consistencia visual:
  • Use design tokens para colores

  • Responda a las mismas props (minDate, maxDate, value, onChange)

  • Tenga la misma accessibility

  1. Pase los siguientes tests:
   describe('useDatePicker', () => {
     it('should not allow dates before minDate', () => {});
     it('should not allow dates after maxDate', () => {});
     it('should format date consistently across platforms', () => {});
     it('should handle timezone correctly', () => {});
   });

¿Puedes lograr que el 100% de la lógica sea compartida?

Solución completa disponible en: ORDO GitHub Repository


Call to Action

¿Estás construyendo una aplicación cross-platform? ¿Has luchado con el dilema de compartir código entre Web, Desktop y Mobile?

Cuéntanos en los comentarios:

  1. Qué enfoque estás usando: ¿Monorepo? ¿Repos separados? ¿React Native Web?

  2. Qué desafíos has enfrentado: ¿Compatibilidad de componentes? ¿State management? ¿Testing?

  3. Qué decision tomarías después de leer este artículo: ¿Implementarías esta arquitectura? ¿Por qué sí o por qué no?

¿Te ha pasado esto? Comparte tu experiencia para que podamos aprender juntos. La comunidad de desarrolladores cross-platform es pequeña pero creciente, y todos podemos beneficiarnos de experiencias reales como la de ORDO.


Autor: [Tu Nombre], Lead Architect @ ORDO: The Creator’s OS

Bio: Construyendo sistemas cross-platform desde 2018. Apasionado por arquitectura de software, design systems, y developer experience. Actualmente arquitecto de ORDO, una plataforma para creadores.

Conectemos: