← Volver al blog

Theme Designer: Construyendo un Editor Visual de Temas MUI con Vista Previa en Tiempo Real

El Desafío y Contexto

En el desarrollo web moderno, los sistemas de diseño se han convertido en la piedra angular de las aplicaciones escalables. Sin embargo, los enfoques tradicionales para la gestión de temas a menudo se quedan cortos cuando se trata de requisitos dinámicos, especialmente en aplicaciones white-label donde múltiples marcas necesitan identidades visuales distintas.

Limitaciones de los Paquetes de Temas Estáticos

La mayoría de las bibliotecas de temas disponibles en npm proporcionan temas estáticos predefinidos que requieren configuración manual y carecen de la flexibilidad necesaria para entornos empresariales. Cuando necesitas soportar múltiples marcas o permitir que usuarios no técnicos personalicen temas, estas soluciones se convierten en cuellos de botella en lugar de habilitadores.

Requisitos de Aplicaciones White-Label

El desafío central era construir un sistema que pudiera:

Por Qué las Soluciones Existentes No Eran Suficientes

Las soluciones de gestión de temas existentes típicamente:

Inmersión Profunda en la Arquitectura Técnica

Estrategia de Gestión de Estado: Implementación de Zustand

El proyecto aprovecha Zustand para la gestión de estado, elegido por su simplicidad, soporte de TypeScript y mínimo boilerplate comparado con Redux. El store de temas implementa un patrón completo de gestión de estado con persistencia.

// src/domains/themes/store/themes.store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { themesInitialState } from "./constants";
import type { ThemesStore, ThemeResponse } from "./types";
import { THEMES_SERVICE } from "../services/themes";
import { AxiosError } from "axios";

export const useThemesStore = create<ThemesStore>()(
  persist(
    (set, get) => ({
      ...themesInitialState,

      fetchThemes: async () => {
        set({ isLoading: true, error: null });

        try {
          const response = await THEMES_SERVICE.getThemes();
          const { data, ...pagination } = response;
          
          set({
            themes: data,
            pagination: pagination as Omit<ThemeResponse, 'data'>,
            isLoading: false,
            error: null,
          });
        } catch (error) {
          set({
            isLoading: false,
            error: error instanceof AxiosError 
              ? error.response?.data.message || "Error al obtener temas"
              : "Error al obtener temas",
          });
        }
      },

      createTheme: async (themeData) => {
        set({ isLoading: true, error: null });

        try {
          const newTheme = await THEMES_SERVICE.createTheme(themeData);
          const currentThemes = get().themes;
          
          set({
            themes: [...(currentThemes || []), newTheme],
            isLoading: false,
            error: null,
          });
        } catch (error) {
          set({
            isLoading: false,
            error: error instanceof AxiosError 
              ? error.response?.data.message || "Error al crear tema"
              : "Error al crear tema",
          });
        }
      },

      // Operaciones CRUD adicionales...
    }),
    {
      name: "themes-store",
      partialize: (state) => ({
        themes: state.themes,
        selectedTheme: state.selectedTheme,
        pagination: state.pagination,
      }),
    }
  )
);

El store implementa una separación limpia de responsabilidades con:

Capa de Validación: Esquemas de Zod

Si bien la implementación actual se enfoca en la gestión de temas, el proyecto incluye patrones de validación de Zod para formularios de autenticación, demostrando la estrategia de validación:

// src/domains/auth/schemas/login.schema.ts
import { z } from 'zod';

export const loginSchema = z.object({
  email: z.email(),
  password: z.string()
    .min(6, { message: 'La contraseña debe tener al menos 6 caracteres' })
    .regex(/[A-Z]/, { message: 'La contraseña debe contener al menos una letra mayúscula' })
    .regex(/[0-9]/, { message: 'La contraseña debe contener al menos un número' }),
});

// src/domains/auth/schemas/register.schema.ts
export const registerSchema = z.object({
  firstName: z.string().min(1, { message: 'El nombre es requerido' }),
  lastName: z.string().min(1, { message: 'El apellido es requerido' }),
  email: z.email({ message: 'Dirección de email inválida' }),
  password: z.string()
    .min(6, { message: 'La contraseña debe tener al menos 6 caracteres' })
    .regex(/[A-Z]/, { message: 'La contraseña debe contener al menos una letra mayúscula' })
    .regex(/[0-9]/, { message: 'La contraseña debe contener al menos un número' }),
  confirmPassword: z.string().min(6, { message: 'La confirmación de contraseña debe tener al menos 6 caracteres' }),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Las contraseñas no coinciden",
  path: ["confirmPassword"],
});

Gestión de Formularios: Integración de React Hook Forms

El proyecto usa React Hook Forms con resolvers de Zod para validación de formularios, proporcionando una solución limpia y performante para la gestión de formularios:

// src/domains/auth/providers/LoginProvider.tsx
import { zodResolver } from "@hookform/resolvers/zod";
import { FormProvider, useForm } from "react-hook-form";
import { Login } from "../pages";
import { loginSchema, type LoginSchema } from "../schemas/login.schema";

const LoginProvider = () => {
  const methods = useForm<LoginSchema>({
    mode: "all",
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: "",
      password: "",
    },
  });

  return (
    <FormProvider {...methods}>
      <Login />
    </FormProvider>
  );
};

Vista Previa en Tiempo Real: Integración de MUI ThemeProvider

La innovación central radica en el sistema de vista previa de temas en tiempo real, que crea dinámicamente temas de MUI y los aplica instantáneamente:

// src/shared/components/ThemeProvider.tsx
import type { Theme } from '@domains/themes/store/types';
import { CssBaseline } from '@mui/material';
import { createTheme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import { useMemo } from 'react';
import type { ReactNode } from 'react';
import { DemoComponents } from './DemoComponents';

interface ThemeProviderProps {
  selectedTheme?: Theme | null;
  children?: ReactNode;
}

export const ThemeProvider = ({ selectedTheme, children }: ThemeProviderProps) => {
  const muiTheme = useMemo(() => {
    if (!selectedTheme?.themeConfig) {
      return createTheme();
    }

    return createTheme({
      palette: {
        mode: selectedTheme.themeConfig.palette.mode as 'light' | 'dark',
        primary: {
          main: selectedTheme.themeConfig.palette.primary.main,
        },
        secondary: {
          main: selectedTheme.themeConfig.palette.secondary.main,
        },
      },
    });
  }, [selectedTheme]);

  return (
    <MuiThemeProvider theme={muiTheme}>
      <CssBaseline />
      {children || <DemoComponents />}
    </MuiThemeProvider>
  );
};

Los DemoComponents proporcionan una vista previa integral de todos los elementos del tema:

// src/shared/components/DemoComponents.tsx
export const DemoComponents = () => {
  return (
    <Box sx={{ p: 3, display: 'flex', flexDirection: 'column', gap: 3 }}>
      <Typography variant="h4" gutterBottom>
        Vista Previa del Tema
      </Typography>

      <Paper sx={{ p: 3 }}>
        <Typography variant="h6" gutterBottom>
          Colores y Tipografía
        </Typography>
        <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
          <Typography variant="h1">Título 1</Typography>
          <Typography variant="h2">Título 2</Typography>
          <Typography variant="h3">Título 3</Typography>
          <Typography variant="body1">
            Texto del cuerpo con colores primarios y secundarios aplicados desde el tema seleccionado.
          </Typography>
          <Typography variant="body2" color="text.secondary">
            Texto secundario mostrando la jerarquía de texto del tema.
          </Typography>
        </Box>
      </Paper>

      {/* Vista previa adicional de componentes para botones, formularios, tarjetas, etc. */}
    </Box>
  );
};

Integración de API: Configuración de Axios con Manejo Adecuado de Errores

El proyecto implementa un cliente HTTP robusto con interceptores para autenticación y manejo de errores:

// src/shared/services/http/client.ts
import type { AxiosInstance } from "axios";
import axios from "axios";
import { requestInterceptor, responseInterceptor } from "./interceptors";
import type { HttpClient, HttpClientConfig } from "./types";

class AxiosHttpClient implements HttpClient {
  private instance: AxiosInstance;

  constructor(baseURL?: string) {
    this.instance = axios.create({
      baseURL:
        baseURL ||
        import.meta.env.VITE_API_BASE_URL ||
        "http://localhost:3000/api",
      timeout: 10000,
      headers: {
        "Content-Type": "application/json",
      },
    });

    this.setupInterceptors();
  }

  private setupInterceptors(): void {
    // Interceptor de solicitud
    if (requestInterceptor.onFulfilled || requestInterceptor.onRejected) {
      this.instance.interceptors.request.use(
        requestInterceptor.onFulfilled,
        requestInterceptor.onRejected
      );
    }

    // Interceptor de respuesta
    if (responseInterceptor.onFulfilled || responseInterceptor.onRejected) {
      this.instance.interceptors.response.use(
        responseInterceptor.onFulfilled,
        responseInterceptor.onRejected
      );
    }
  }

  // Métodos CRUD con tipado apropiado
  async get<T = unknown>(url: string, config?: HttpClientConfig): Promise<T> {
    const response = await this.instance.get<T>(url, config);
    return response.data;
  }

  async post<T = unknown>(url: string, data?: unknown, config?: HttpClientConfig): Promise<T> {
    const response = await this.instance.post<T>(url, data, config);
    return response.data;
  }

  // Métodos de utilidad adicionales
  setAuthToken(token: string): void {
    this.instance.defaults.headers.common["Authorization"] = `Bearer ${token}`;
    localStorage.setItem("auth_token", token);
  }

  clearAuthToken(): void {
    delete this.instance.defaults.headers.common["Authorization"];
    localStorage.removeItem("auth_token");
  }
}

export const httpClient = new AxiosHttpClient();
export default httpClient;

Destacados de Implementación

Estructura de Temas y Definiciones de Tipos

El sistema de temas usa una estructura de tipos bien definida que asegura consistencia en toda la aplicación:

// src/domains/themes/store/types/themes.type.ts
export interface Theme {
  id: string;
  name: string;
  description: string | null;
  themeConfig: ThemeConfig;
  previewImage: string | null;
  tags: string[] | null;
  isActive: boolean;
  createdById: string;
  updatedById: string;
  createdBy: CreatedBy;
  updatedBy: CreatedBy;
  createdAt: string;
  updatedAt: string;
}

interface ThemeConfig {
  palette: Palette;
}

interface Palette {
  mode: 'light' | 'dark';
  primary: {
    main: string;
  };
  secondary: {
    main: string;
  };
}

export interface ThemesState {
  pagination?: Omit<ThemeResponse, 'data'>;
  themes?: Theme[];
  selectedTheme?: Theme | null;
  isLoading: boolean;
  error: string | null;
}

export interface ThemesActions {
  fetchThemes: () => Promise<void>;
  createTheme: (theme: Omit<Theme, 'id' | 'createdAt' | 'updatedAt' | 'userId'>) => Promise<void>;
  updateTheme: (id: string, theme: Partial<Theme>) => Promise<void>;
  deleteTheme: (id: string) => Promise<void>;
  setSelectedTheme: (theme: Theme | null) => void;
  setPagination: (pagination: Omit<ThemeResponse, 'data'>) => void;
  setLoading: (loading: boolean) => void;
  setError: (error: string | null) => void;
  clearError: () => void;
}

export type ThemesStore = ThemesState & ThemesActions;

Asistente de Creación de Temas

El proceso de creación de temas usa un enfoque paso a paso tipo asistente para una mejor experiencia de usuario:

// src/domains/themes/pages/ThemeCreate.tsx
const steps = [
  { label: 'Información Básica', icon: InfoIcon },
  { label: 'Colores', icon: ColorLensIcon },
  { label: 'Tipografía', icon: TextFieldsIcon },
  { label: 'Vista Previa', icon: CheckCircleIcon },
];

const ThemeCreate = () => {
  const { createTheme } = useThemesStore();
  const navigate = useNavigate();
  const [activeStep, setActiveStep] = useState(0);
  const [previewDevice, setPreviewDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop');
  const [isCreating, setIsCreating] = useState(false);
  const [newTheme, setNewTheme] = useState({
    name: '',
    description: '',
    themeConfig: {
      palette: {
        mode: 'light' as 'light' | 'dark',
        primary: {
          main: '#1976d2',
        },
        secondary: {
          main: '#dc004e',
        },
      },
    },
  });

  const handleColorChange = (colorType: 'primary' | 'secondary', value: string) => {
    setNewTheme(prev => ({
      ...prev,
      themeConfig: {
        ...prev.themeConfig,
        palette: {
          ...prev.themeConfig.palette,
          [colorType]: {
            ...prev.themeConfig.palette[colorType],
            main: value,
          },
        },
      },
    }));
  };

  const handleModeChange = (mode: 'light' | 'dark') => {
    setNewTheme(prev => ({
      ...prev,
      themeConfig: {
        ...prev.themeConfig,
        palette: {
          ...prev.themeConfig.palette,
          mode,
        },
      },
    }));
  };

  // Lógica de validación y navegación de pasos
  const isStepValid = (step: number) => {
    switch (step) {
      case 0:
        return newTheme.name.trim().length > 0;
      case 1:
        return true; // Los colores siempre son válidos
      case 2:
        return true; // La tipografía siempre es válida
      case 3:
        return true; // La vista previa siempre es válida
      default:
        return false;
    }
  };

  const handleCreateTheme = async () => {
    if (!isStepValid(activeStep)) return;
    
    setIsCreating(true);
    try {
      const themeToCreate = {
        ...newTheme,
        id: '',
        previewImage: null,
        tags: null,
        isActive: false,
        createdById: '',
        updatedById: '',
        createdBy: {} as CreatedBy,
        updatedBy: {} as CreatedBy,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };
      await createTheme(themeToCreate);
      
      navigate(ROUTES.THEMES);
    } catch (error) {
      console.error('Error al crear tema:', error);
    } finally {
      setIsCreating(false);
    }
  };

  // Renderizar contenido del paso basado en el paso activo
  const getStepContent = (step: number) => {
    switch (step) {
      case 0:
        return (
          <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
            <Box sx={{ textAlign: 'center', mb: 3 }}>
              <InfoIcon sx={{ fontSize: 48, color: '#1E90FF', mb: 2 }} />
              <Typography variant="h5" gutterBottom sx={{ fontWeight: 600 }}>
                Información Básica
              </Typography>
              <Typography variant="body2" color="text.secondary">
                Comencemos con lo básico. Dale a tu tema un nombre y descripción.
              </Typography>
            </Box>
            
            <TextField
              label="Nombre del Tema"
              value={newTheme.name}
              onChange={(e) => setNewTheme(prev => ({ ...prev, name: e.target.value }))}
              fullWidth
              variant="outlined"
              required
              error={!newTheme.name.trim() && activeStep > 0}
              helperText={!newTheme.name.trim() && activeStep > 0 ? 'El nombre del tema es requerido' : ''}
              sx={{
                '& .MuiOutlinedInput-root': {
                  backgroundColor: '#F8F9FA',
                  borderRadius: '8px',
                },
              }}
            />
            
            <TextField
              label="Descripción"
              value={newTheme.description}
              onChange={(e) => setNewTheme(prev => ({ ...prev, description: e.target.value }))}
              multiline
              rows={4}
              fullWidth
              variant="outlined"
              helperText="Opcional: Describe el propósito y estilo de tu tema"
              sx={{
                '& .MuiOutlinedInput-root': {
                  backgroundColor: '#F8F9FA',
                  borderRadius: '8px',
                },
              }}
            />
          </Box>
        );
      // Implementaciones adicionales de pasos...
    }
  };

  return (
    <Container maxWidth="lg" sx={{ py: 4 }}>
      {/* Renderizado del asistente y contenido */}
    </Container>
  );
};

Interfaz de Edición de Temas

La interfaz de edición de temas proporciona una interfaz con pestañas integral para modificar temas existentes:

// src/domains/themes/pages/ThemeEdit.tsx
const ThemeEdit = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const { themes, updateTheme, isLoading } = useThemesStore();
  const [editingTheme, setEditingTheme] = useState<Theme | null>(null);
  const [tabValue, setTabValue] = useState(0);
  const [previewDevice, setPreviewDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop');
  const [isSaving, setIsSaving] = useState(false);
  const [showSaveSuccess, setShowSaveSuccess] = useState(false);
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

  const selectedTheme = useMemo(() => {
    return themes?.find(theme => theme.id === id);
  }, [themes, id]);

  useEffect(() => {
    if (selectedTheme) {
      setEditingTheme(selectedTheme);
    }
  }, [selectedTheme]);

  useEffect(() => {
    if (selectedTheme && editingTheme) {
      const hasChanges = JSON.stringify(selectedTheme) !== JSON.stringify(editingTheme);
      setHasUnsavedChanges(hasChanges);
    }
  }, [selectedTheme, editingTheme]);

  const handleColorChange = (colorType: "primary" | "secondary", value: string) => {
    setEditingTheme((prev) => {
      if (!prev) return null;
      return {
        ...prev,
        themeConfig: {
          ...prev.themeConfig,
          palette: {
            ...prev.themeConfig.palette,
            [colorType]: {
              ...prev.themeConfig.palette[colorType],
              main: value,
            },
          },
        },
      };
    });
  };

  const handleModeChange = (mode: "light" | "dark") => {
    setEditingTheme((prev) => {
      if (!prev) return null;
      return {
        ...prev,
        themeConfig: {
          ...prev.themeConfig,
          palette: {
            ...prev.themeConfig.palette,
            mode,
          },
        },
      };
    });
  };

  const handleSave = async () => {
    setIsSaving(true);
    try {
      await updateTheme(editingTheme.id, editingTheme);
      setShowSaveSuccess(true);
      setHasUnsavedChanges(false);
    } catch (error) {
      console.error('Error al guardar tema:', error);
    } finally {
      setIsSaving(false);
    }
  };

  return (
    <Container maxWidth="lg" sx={{ py: 4 }}>
      {/* Interfaz con pestañas y vista previa en tiempo real */}
      <Grid container spacing={3}>
        <Grid item xs={12} md={8}>
          {/* Controles de edición de temas */}
        </Grid>
        <Grid item xs={12} md={4}>
          {/* Vista previa en tiempo real */}
          <ThemeProvider selectedTheme={editingTheme}>
            <DemoComponents />
          </ThemeProvider>
        </Grid>
      </Grid>
    </Container>
  );
};

Desafíos Técnicos y Soluciones

Gestión de Objetos de Temas Anidados Complejos

Desafío: Los objetos de temas contienen estructuras profundamente anidadas que necesitan ser actualizadas atómicamente mientras mantienen la inmutabilidad.

Solución: Implementamos un enfoque estructurado usando operadores spread y gestión de estado apropiada:

const handleColorChange = (colorType: 'primary' | 'secondary', value: string) => {
  setNewTheme(prev => ({
    ...prev,
    themeConfig: {
      ...prev.themeConfig,
      palette: {
        ...prev.themeConfig.palette,
        [colorType]: {
          ...prev.themeConfig.palette[colorType],
          main: value,
        },
      },
    },
  }));
};

Optimización de Rendimiento para Actualizaciones en Tiempo Real

Desafío: Las actualizaciones de temas en tiempo real podrían causar problemas de rendimiento con re-renderizados frecuentes.

Solución: Implementamos useMemo para la creación de temas y optimizamos la estructura de componentes:

const muiTheme = useMemo(() => {
  if (!selectedTheme?.themeConfig) {
    return createTheme();
  }

  return createTheme({
    palette: {
      mode: selectedTheme.themeConfig.palette.mode as 'light' | 'dark',
      primary: {
        main: selectedTheme.themeConfig.palette.primary.main,
      },
      secondary: {
        main: selectedTheme.themeConfig.palette.secondary.main,
      },
    },
  });
}, [selectedTheme]);

Seguridad de Tipos en Todo el Sistema de Temas

Desafío: Asegurar la seguridad de tipos en los sistemas de creación, edición y vista previa de temas.

Solución: Interfaces TypeScript integrales y tipado estricto:

export interface Theme {
  id: string;
  name: string;
  description: string | null;
  themeConfig: ThemeConfig;
  previewImage: string | null;
  tags: string[] | null;
  isActive: boolean;
  createdById: string;
  updatedById: string;
  createdBy: CreatedBy;
  updatedBy: CreatedBy;
  createdAt: string;
  updatedAt: string;
}

export type ThemesStore = ThemesState & ThemesActions;

Integración con Persistencia Backend

Desafío: Integración perfecta con API backend mientras se mantienen capacidades offline.

Solución: Middleware de persistencia de Zustand con manejo apropiado de errores:

export const useThemesStore = create<ThemesStore>()(
  persist(
    (set, get) => ({
      // Implementación del store
    }),
    {
      name: "themes-store",
      partialize: (state) => ({
        themes: state.themes,
        selectedTheme: state.selectedTheme,
        pagination: state.pagination,
      }),
    }
  )
);

Proceso de Desarrollo y Decisiones

Por Qué Vite

Decisión: Elegimos Vite por su experiencia de desarrollo superior y rendimiento de build.

Beneficios:

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@app': path.resolve(__dirname, 'src/app'),
      '@domains': path.resolve(__dirname, 'src/domains'),
      '@shared': path.resolve(__dirname, 'src/shared'),
    },
  },
})

Decisión de Gestión de Estado: Zustand vs Redux/Context

Decisión: Elegimos Zustand por su simplicidad e integración con TypeScript.

Razón:

Razonamiento de Selección de Biblioteca de Formularios

Decisión: React Hook Forms con validación de Zod.

Beneficios:

Elecciones de Estrategia de Validación

Decisión: Zod para validación en tiempo de ejecución con integración de TypeScript.

Ventajas:

Resultados e Impacto

Métricas de Rendimiento

Mejoras en la Experiencia del Desarrollador

Logros de Escalabilidad

Posibilidades de Mejoras Futuras

Lecciones Aprendidas

Lo Que Funcionó Bien

  1. Arquitectura Dirigida por Dominios: Separación clara de responsabilidades hizo el código mantenible y escalable
  2. Integración de TypeScript: Tipado integral previno errores en tiempo de ejecución y mejoró la experiencia del desarrollador
  3. Gestión de Estado Zustand: Gestión de estado simple pero potente con excelente persistencia
  4. Vista Previa en Tiempo Real: Retroalimentación visual instantánea mejoró significativamente la experiencia del usuario
  5. Composición de Componentes: Componentes reutilizables con interfaces de props apropiadas

Lo Que Se Haría Diferente

  1. Validación de Esquemas de Temas: Implementar esquemas de Zod para validación de temas desde el inicio
  2. Monitoreo de Rendimiento: Agregar métricas de rendimiento y monitoreo antes
  3. Estrategia de Testing: Implementar tests unitarios e integrales completos
  4. Documentación: Documentación más detallada de API y ejemplos de uso
  5. Accesibilidad: Características de accesibilidad mejoradas y soporte ARIA

Conclusiones Clave para Proyectos Similares

  1. Comenzar con TypeScript: La seguridad de tipos desde el principio previene muchos problemas
  2. Elegir las Herramientas Correctas: Vite, Zustand y React Hook Forms proporcionaron excelente experiencia de desarrollador
  3. Planificar para Escalabilidad: La arquitectura dirigida por dominios soporta el crecimiento futuro
  4. Enfocarse en la Experiencia del Usuario: Vista previa en tiempo real e interfaces intuitivas son cruciales
  5. Manejo de Errores: Manejo integral de errores mejora la confiabilidad
  6. Rendimiento Primero: Optimizar para rendimiento desde el inicio, no como una ocurrencia tardía

Conclusión

El proyecto Theme Designer demuestra cómo las tecnologías frontend modernas pueden combinarse para crear aplicaciones potentes y amigables que resuelven problemas del mundo real. Al aprovechar React, MUI, Zustand y TypeScript, construimos un sistema integral de gestión de temas que va más allá de los paquetes npm estáticos para proporcionar capacidades dinámicas de creación y vista previa de temas en tiempo real.

El proyecto muestra varios principios arquitectónicos clave:

Este estudio de caso ilustra cómo las decisiones arquitectónicas reflexivas, combinadas con herramientas modernas, pueden resultar en aplicaciones que no solo son funcionales sino también mantenibles, escalables y agradables de usar. El Theme Designer sirve como base para construir sistemas de diseño dinámicos que pueden adaptarse a las necesidades evolutivas de las aplicaciones web modernas.


Este proyecto demuestra el poder de combinar tecnologías frontend modernas para resolver desafíos complejos de sistemas de diseño. El código está disponible en GitHub para mayor exploración y contribución.