Skip to content
Back to skills

Config Themes

ASecurity

Skill para implementar sistema de temas y personalización visual

  • 207 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 4, 2026
developmentgosqlapifrontendbackend

Works with

  • api

Security analysis

A100/100

Pro scans all 13 files and shows the line behind each finding

Scanned September 4, 2026

npx -y skills add NeverSight/skills_feed --skill config-themes --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Config Themes?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Config Themes
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/neversight-config-themes/badge)](https://www.skillsdirectory.com/skills/neversight-config-themes)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: config-themes
description: Skill para implementar sistema de temas y personalización visual
---

# Skill: config-themes

## Propósito

Este skill genera un módulo de **Temas y Personalización Visual** que permite:

- Temas claros/oscuros dinámicos
- Colores personalizables por tenant
- Persistencia de preferencias
- Sistema de diseño unificado

---

## Variantes Disponibles

| Variante  | Descripción                                                 |
| --------- | ----------------------------------------------------------- |
| **basic** | Tema claro/oscuro con colores de marca                      |
| **full**  | Personalización completa por tenant + presets + exportación |

---

## Estructura de Archivos a Crear

```text
proyecto/
├── backend/
│   └── internal/
│       └── handlers/
│           └── settings.go (endpoints de tema)
└── frontend/
    └── lib/
        ├── core/
        │   └── theme/
        │       ├── app_theme.dart
        │       └── theme_provider.dart
        └── features/
            └── configuration/
                ├── data/
                │   └── models/
                │       └── theme_model.dart
                └── presentation/
                    └── widgets/
                        └── theme_settings_tab.dart
```

---

## Paso 1: Modelo de Tema

### Backend (Almacenado en tenant.settings o tabla separada)

```sql
-- En la tabla de tenants, el campo settings JSONB contiene:
{
  "theme": {
    "mode": "light",            -- "light", "dark", "system"
    "primary_color": "#1976D2",
    "secondary_color": "#FF9800",
    "tertiary_color": "#4CAF50",
    "font_family": "Inter",
    "border_radius": 8,
    "compact_mode": false
  }
}

-- O crear tabla dedicada:
CREATE TABLE public.theme_settings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID REFERENCES public.tenants(id) ON DELETE CASCADE UNIQUE,
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
    
    mode VARCHAR(20) DEFAULT 'system',  -- 'light', 'dark', 'system'
    primary_color VARCHAR(20) DEFAULT '#1976D2',
    secondary_color VARCHAR(20) DEFAULT '#FF9800',
    tertiary_color VARCHAR(20) DEFAULT '#4CAF50',
    
    font_family VARCHAR(50) DEFAULT 'Inter',
    border_radius INT DEFAULT 8,
    compact_mode BOOLEAN DEFAULT false,
    
    -- Presets personalizados
    custom_presets JSONB DEFAULT '[]',
    
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    
    -- Un registro por tenant O por usuario
    CONSTRAINT unique_tenant_theme UNIQUE (tenant_id),
    CONSTRAINT unique_user_theme UNIQUE (user_id)
);
```

---

## Paso 2: API Backend

### Endpoints

| Método | Ruta                        | Descripción                  |
| ------ | --------------------------- | ---------------------------- |
| GET    | /settings/theme             | Obtener tema actual          |
| PUT    | /settings/theme             | Actualizar tema              |
| GET    | /settings/theme/presets     | Listar presets disponibles   |
| POST   | /settings/theme/presets     | Guardar preset personalizado |
| DELETE | /settings/theme/presets/:id | Eliminar preset              |

### DTOs

```go
type ThemeSettings struct {
    Mode           string `json:"mode"`            // light, dark, system
    PrimaryColor   string `json:"primary_color"`
    SecondaryColor string `json:"secondary_color"`
    TertiaryColor  string `json:"tertiary_color,omitempty"`
    FontFamily     string `json:"font_family,omitempty"`
    BorderRadius   int    `json:"border_radius,omitempty"`
    CompactMode    bool   `json:"compact_mode,omitempty"`
}

type UpdateThemeRequest struct {
    PrimaryColor   string `json:"primary_color,omitempty"`
    SecondaryColor string `json:"secondary_color,omitempty"`
    TertiaryColor  string `json:"tertiary_color,omitempty"`
    LogoURL        string `json:"logo_url,omitempty"`
}

type ThemePreset struct {
    ID             string `json:"id"`
    Name           string `json:"name"`
    PrimaryColor   string `json:"primary_color"`
    SecondaryColor string `json:"secondary_color"`
    TertiaryColor  string `json:"tertiary_color"`
    IsDefault      bool   `json:"is_default,omitempty"`
}
```

---

## Paso 3: Modelo Flutter

```dart
/// Configuración de tema
class ThemeSettings {
  final ThemeMode mode;
  final Color primaryColor;
  final Color secondaryColor;
  final Color tertiaryColor;
  final String fontFamily;
  final double borderRadius;
  final bool compactMode;

  const ThemeSettings({
    this.mode = ThemeMode.system,
    this.primaryColor = const Color(0xFF1976D2),
    this.secondaryColor = const Color(0xFFFF9800),
    this.tertiaryColor = const Color(0xFF4CAF50),
    this.fontFamily = 'Inter',
    this.borderRadius = 8.0,
    this.compactMode = false,
  });

  factory ThemeSettings.fromJson(Map<String, dynamic> json) {
    return ThemeSettings(
      mode: _parsethemeMode(json['mode']),
      primaryColor: _parseColor(json['primary_color']) ?? const Color(0xFF1976D2),
      secondaryColor: _parseColor(json['secondary_color']) ?? const Color(0xFFFF9800),
      tertiaryColor: _parseColor(json['tertiary_color']) ?? const Color(0xFF4CAF50),
      fontFamily: json['font_family'] ?? 'Inter',
      borderRadius: (json['border_radius'] ?? 8).toDouble(),
      compactMode: json['compact_mode'] ?? false,
    );
  }

  Map<String, dynamic> toJson() => {
    'mode': mode.name,
    'primary_color': '#${primaryColor.value.toRadixString(16).padLeft(8, '0').substring(2)}',
    'secondary_color': '#${secondaryColor.value.toRadixString(16).padLeft(8, '0').substring(2)}',
    'tertiary_color': '#${tertiaryColor.value.toRadixString(16).padLeft(8, '0').substring(2)}',
    'font_family': fontFamily,
    'border_radius': borderRadius.toInt(),
    'compact_mode': compactMode,
  };

  ThemeSettings copyWith({
    ThemeMode? mode,
    Color? primaryColor,
    Color? secondaryColor,
    Color? tertiaryColor,
    String? fontFamily,
    double? borderRadius,
    bool? compactMode,
  }) {
    return ThemeSettings(
      mode: mode ?? this.mode,
      primaryColor: primaryColor ?? this.primaryColor,
      secondaryColor: secondaryColor ?? this.secondaryColor,
      tertiaryColor: tertiaryColor ?? this.tertiaryColor,
      fontFamily: fontFamily ?? this.fontFamily,
      borderRadius: borderRadius ?? this.borderRadius,
      compactMode: compactMode ?? this.compactMode,
    );
  }

  static ThemeMode _parsethemeMode(String? mode) {
    switch (mode) {
      case 'light': return ThemeMode.light;
      case 'dark': return ThemeMode.dark;
      default: return ThemeMode.system;
    }
  }

  static Color? _parseColor(String? hex) {
    if (hex == null || hex.isEmpty) return null;
    hex = hex.replaceFirst('#', '');
    if (hex.length == 6) hex = 'FF$hex';
    return Color(int.parse(hex, radix: 16));
  }
}

/// Preset de tema
class ThemePreset {
  final String id;
  final String name;
  final Color primaryColor;
  final Color secondaryColor;
  final Color tertiaryColor;
  final bool isDefault;

  const ThemePreset({
    required this.id,
    required this.name,
    required this.primaryColor,
    required this.secondaryColor,
    this.tertiaryColor = Colors.green,
    this.isDefault = false,
  });

  /// Presets predefinidos
  static List<ThemePreset> get defaultPresets => [
    ThemePreset(
      id: 'ocean',
      name: 'Océano',
      primaryColor: const Color(0xFF1976D2),
      secondaryColor: const Color(0xFF26C6DA),
      tertiaryColor: const Color(0xFF00897B),
    ),
    ThemePreset(
      id: 'forest',
      name: 'Bosque',
      primaryColor: const Color(0xFF2E7D32),
      secondaryColor: const Color(0xFF8BC34A),
      tertiaryColor: const Color(0xFF33691E),
    ),
    ThemePreset(
      id: 'sunset',
      name: 'Atardecer',
      primaryColor: const Color(0xFFE65100),
      secondaryColor: const Color(0xFFFFB300),
      tertiaryColor: const Color(0xFFD84315),
    ),
    ThemePreset(
      id: 'lavender',
      name: 'Lavanda',
      primaryColor: const Color(0xFF7B1FA2),
      secondaryColor: const Color(0xFFBA68C8),
      tertiaryColor: const Color(0xFF4A148C),
    ),
    ThemePreset(
      id: 'midnight',
      name: 'Medianoche',
      primaryColor: const Color(0xFF283593),
      secondaryColor: const Color(0xFF5C6BC0),
      tertiaryColor: const Color(0xFF1A237E),
    ),
    ThemePreset(
      id: 'rose',
      name: 'Rosa',
      primaryColor: const Color(0xFFC2185B),
      secondaryColor: const Color(0xFFF48FB1),
      tertiaryColor: const Color(0xFF880E4F),
    ),
  ];
}
```

---

## Paso 4: Theme Provider

```dart
class ThemeProvider extends ChangeNotifier {
  ThemeSettings _settings = const ThemeSettings();
  bool _isLoading = false;

  ThemeSettings get settings => _settings;
  bool get isLoading => _isLoading;

  ThemeMode get themeMode => _settings.mode;
  Color get primaryColor => _settings.primaryColor;
  Color get secondaryColor => _settings.secondaryColor;

  ThemeProvider() {
    _loadFromStorage();
  }

  /// Cargar configuración guardada
  Future<void> _loadFromStorage() async {
    final prefs = await SharedPreferences.getInstance();
    final json = prefs.getString('theme_settings');
    if (json != null) {
      _settings = ThemeSettings.fromJson(jsonDecode(json));
      notifyListeners();
    }
  }

  /// Sincronizar desde datos del tenant
  void syncFromTenant({
    String? primaryColor,
    String? secondaryColor,
    String? tertiaryColor,
  }) {
    _settings = _settings.copyWith(
      primaryColor: ThemeSettings._parseColor(primaryColor) ?? _settings.primaryColor,
      secondaryColor: ThemeSettings._parseColor(secondaryColor) ?? _settings.secondaryColor,
      tertiaryColor: ThemeSettings._parseColor(tertiaryColor) ?? _settings.tertiaryColor,
    );
    notifyListeners();
    _saveToStorage();
  }

  /// Cambiar modo de tema
  void setThemeMode(ThemeMode mode) {
    _settings = _settings.copyWith(mode: mode);
    notifyListeners();
    _saveToStorage();
  }

  /// Actualizar colores
  void updateColors({
    Color? primary,
    Color? secondary,
    Color? tertiary,
  }) {
    _settings = _settings.copyWith(
      primaryColor: primary,
      secondaryColor: secondary,
      tertiaryColor: tertiary,
    );
    notifyListeners();
    _saveToStorage();
  }

  /// Aplicar preset
  void applyPreset(ThemePreset preset) {
    _settings = _settings.copyWith(
      primaryColor: preset.primaryColor,
      secondaryColor: preset.secondaryColor,
      tertiaryColor: preset.tertiaryColor,
    );
    notifyListeners();
    _saveToStorage();
  }

  /// Guardar en storage local
  Future<void> _saveToStorage() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('theme_settings', jsonEncode(_settings.toJson()));
  }

  /// Construir ThemeData de Flutter
  ThemeData buildTheme({required Brightness brightness}) {
    final colorScheme = ColorScheme.fromSeed(
      seedColor: _settings.primaryColor,
      secondary: _settings.secondaryColor,
      tertiary: _settings.tertiaryColor,
      brightness: brightness,
    );

    return ThemeData(
      useMaterial3: true,
      colorScheme: colorScheme,
      fontFamily: _settings.fontFamily,
      
      // Bordes redondeados consistentes
      cardTheme: CardTheme(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(_settings.borderRadius),
        ),
      ),
      inputDecorationTheme: InputDecorationTheme(
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(_settings.borderRadius),
        ),
      ),
      filledButtonTheme: FilledButtonThemeData(
        style: FilledButton.styleFrom(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(_settings.borderRadius),
          ),
        ),
      ),
      
      // Modo compacto
      visualDensity: _settings.compactMode 
          ? VisualDensity.compact 
          : VisualDensity.standard,
    );
  }

  ThemeData get lightTheme => buildTheme(brightness: Brightness.light);
  ThemeData get darkTheme => buildTheme(brightness: Brightness.dark);
}
```

---

## Paso 5: App Theme Base

```dart
// lib/core/theme/app_theme.dart

class AppTheme {
  // Singleton para acceso global
  static ThemeProvider? _provider;
  
  static void initialize(ThemeProvider provider) {
    _provider = provider;
  }

  static ThemeData get light => _provider?.lightTheme ?? _defaultLight;
  static ThemeData get dark => _provider?.darkTheme ?? _defaultDark;
  static ThemeMode get mode => _provider?.themeMode ?? ThemeMode.system;

  // Temas por defecto
  static final _defaultLight = ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF1976D2),
      brightness: Brightness.light,
    ),
  );

  static final _defaultDark = ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF1976D2),
      brightness: Brightness.dark,
    ),
  );

  // Constantes de diseño
  static const double spacing = 8.0;
  static const double spacingSmall = 4.0;
  static const double spacingMedium = 16.0;
  static const double spacingLarge = 24.0;
  static const double spacingXLarge = 32.0;

  static const double radiusSmall = 4.0;
  static const double radiusMedium = 8.0;
  static const double radiusLarge = 16.0;
  static const double radiusXLarge = 24.0;

  // Sombras consistentes
  static List<BoxShadow> get shadowSmall => [
    BoxShadow(
      color: Colors.black.withValues(alpha: 0.05),
      blurRadius: 4,
      offset: const Offset(0, 2),
    ),
  ];

  static List<BoxShadow> get shadowMedium => [
    BoxShadow(
      color: Colors.black.withValues(alpha: 0.1),
      blurRadius: 8,
      offset: const Offset(0, 4),
    ),
  ];

  static List<BoxShadow> get shadowLarge => [
    BoxShadow(
      color: Colors.black.withValues(alpha: 0.15),
      blurRadius: 16,
      offset: const Offset(0, 8),
    ),
  ];
}
```

---

## Paso 6: Widget de Configuración

```dart
class ThemeSettingsTab extends StatelessWidget {
  const ThemeSettingsTab({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Sección: Modo de tema
              _SectionCard(
                title: 'Modo de Tema',
                child: _ThemeModeSelector(
                  currentMode: themeProvider.themeMode,
                  onChanged: themeProvider.setThemeMode,
                ),
              ),

              const SizedBox(height: 16),

              // Sección: Presets
              _SectionCard(
                title: 'Paletas de Colores',
                child: _PresetGrid(
                  presets: ThemePreset.defaultPresets,
                  currentPrimary: themeProvider.primaryColor,
                  onPresetSelected: themeProvider.applyPreset,
                ),
              ),

              const SizedBox(height: 16),

              // Sección: Colores personalizados
              _SectionCard(
                title: 'Personalizar Colores',
                child: Column(
                  children: [
                    _ColorPickerTile(
                      label: 'Color Primario',
                      color: themeProvider.primaryColor,
                      onColorChanged: (color) => themeProvider.updateColors(primary: color),
                    ),
                    const Divider(),
                    _ColorPickerTile(
                      label: 'Color Secundario',
                      color: themeProvider.secondaryColor,
                      onColorChanged: (color) => themeProvider.updateColors(secondary: color),
                    ),
                  ],
                ),
              ),

              const SizedBox(height: 16),

              // Preview
              _SectionCard(
                title: 'Vista Previa',
                child: _ThemePreview(),
              ),
            ],
          ),
        );
      },
    );
  }
}

class _ThemeModeSelector extends StatelessWidget {
  final ThemeMode currentMode;
  final ValueChanged<ThemeMode> onChanged;

  const _ThemeModeSelector({
    required this.currentMode,
    required this.onChanged,
  });

  @override
  Widget build(BuildContext context) {
    return SegmentedButton<ThemeMode>(
      segments: const [
        ButtonSegment(
          value: ThemeMode.light,
          icon: Icon(Icons.light_mode),
          label: Text('Claro'),
        ),
        ButtonSegment(
          value: ThemeMode.system,
          icon: Icon(Icons.settings_suggest),
          label: Text('Sistema'),
        ),
        ButtonSegment(
          value: ThemeMode.dark,
          icon: Icon(Icons.dark_mode),
          label: Text('Oscuro'),
        ),
      ],
      selected: {currentMode},
      onSelectionChanged: (selection) => onChanged(selection.first),
    );
  }
}

class _PresetGrid extends StatelessWidget {
  final List<ThemePreset> presets;
  final Color currentPrimary;
  final ValueChanged<ThemePreset> onPresetSelected;

  const _PresetGrid({
    required this.presets,
    required this.currentPrimary,
    required this.onPresetSelected,
  });

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 12,
      runSpacing: 12,
      children: presets.map((preset) {
        final isSelected = preset.primaryColor == currentPrimary;
        return GestureDetector(
          onTap: () => onPresetSelected(preset),
          child: Container(
            width: 80,
            padding: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              border: Border.all(
                color: isSelected 
                    ? Theme.of(context).colorScheme.primary 
                    : Colors.grey.shade300,
                width: isSelected ? 2 : 1,
              ),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Column(
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    _ColorDot(color: preset.primaryColor),
                    _ColorDot(color: preset.secondaryColor),
                    _ColorDot(color: preset.tertiaryColor),
                  ],
                ),
                const SizedBox(height: 8),
                Text(
                  preset.name,
                  style: const TextStyle(fontSize: 12),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        );
      }).toList(),
    );
  }
}

class _ColorDot extends StatelessWidget {
  final Color color;

  const _ColorDot({required this.color});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 16,
      height: 16,
      margin: const EdgeInsets.symmetric(horizontal: 2),
      decoration: BoxDecoration(
        color: color,
        shape: BoxShape.circle,
        border: Border.all(color: Colors.white, width: 1),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.2),
            blurRadius: 2,
          ),
        ],
      ),
    );
  }
}
```

---

## Uso en MaterialApp

```dart
// main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return MaterialApp(
          title: 'Mi App',
          theme: themeProvider.lightTheme,
          darkTheme: themeProvider.darkTheme,
          themeMode: themeProvider.themeMode,
          home: const HomePage(),
        );
      },
    );
  }
}
```

---

## Dependencias Recomendadas

```yaml
# pubspec.yaml
dependencies:
  shared_preferences: ^2.2.0
  flex_color_picker: ^3.3.0  # Para selector de color visual
  provider: ^6.0.0
```

Files in this skill

  • SKILL.md19.8 KB
  • description_ar.txt78 B
  • description_cn.txt46 B
  • description_de.txt83 B
  • description_en.txt66 B
  • description_es.txt71 B
  • description_fr.txt89 B
  • description_it.txt69 B
  • description_ja.txt67 B
  • description_ko.txt71 B
  • description_ru.txt107 B
  • description_tw.txt46 B
  • stats.json68 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…