pragma_design_system 1.3.0 copy "pragma_design_system: ^1.3.0" to clipboard
pragma_design_system: ^1.3.0 copied to clipboard

Flutter library that gathers Pragma's design tokens, themes, and base components for mobile apps.

example/lib/main.dart

// ignore_for_file: avoid_relative_lib_imports

import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:pragma_design_system/pragma_design_system.dart';

import 'calendar_demo_page.dart';
import 'theme_lab_page.dart';

// Consumimos la librería local directamente para iterar sin publicar a pub.dev.

void main() {
  GoogleFonts.config.allowRuntimeFetching = false;
  runApp(const PragmaShowcaseApp());
}

class PragmaShowcaseApp extends StatefulWidget {
  const PragmaShowcaseApp({super.key});

  @override
  State<PragmaShowcaseApp> createState() => _PragmaShowcaseAppState();
}

class _PragmaShowcaseAppState extends State<PragmaShowcaseApp> {
  ThemeMode _mode = ThemeMode.light;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pragma Design System',
      debugShowCheckedModeBanner: false,
      theme: PragmaTheme.light(),
      darkTheme: PragmaTheme.dark(),
      themeMode: _mode,
      home: ShowcaseScreen(
        mode: _mode,
        onModeChanged: (bool value) {
          setState(() => _mode = value ? ThemeMode.dark : ThemeMode.light);
        },
      ),
    );
  }
}

class ShowcaseScreen extends StatelessWidget {
  const ShowcaseScreen({
    required this.mode,
    required this.onModeChanged,
    super.key,
  });

  final ThemeMode mode;
  final ValueChanged<bool> onModeChanged;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final TextTheme textTheme = theme.textTheme;
    final ColorScheme colorScheme = theme.colorScheme;
    final Color onSurfaceVariant = colorScheme.onSurfaceVariant;
    final List<ModelPragmaComponent> documentedComponents = _componentCatalog
        .map<ModelPragmaComponent>(ModelPragmaComponent.fromJson)
        .toList();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Pragma Design System'),
        actions: <Widget>[
          Row(
            children: <Widget>[
              Icon(
                Icons.light_mode,
                size: 18,
                color: onSurfaceVariant,
              ),
              Switch(
                value: mode == ThemeMode.dark,
                onChanged: onModeChanged,
              ),
              Icon(
                Icons.dark_mode,
                size: 18,
                color: onSurfaceVariant,
              ),
              const SizedBox(width: PragmaSpacing.md),
            ],
          ),
          IconButton(
            tooltip: 'Grid debugger',
            icon: const Icon(Icons.grid_4x4_outlined),
            onPressed: () => _openGridDebugger(context),
          ),
          IconButton(
            tooltip: 'Calendar demo',
            icon: const Icon(Icons.event_note_outlined),
            onPressed: () => _openCalendarDemo(context),
          ),
          IconButton(
            tooltip: 'Theme lab',
            icon: const Icon(Icons.palette_outlined),
            onPressed: () => _openThemeLab(context),
          ),
        ],
      ),
      body: ListView(
        padding: PragmaSpacing.insetSymmetric(
          horizontal: PragmaSpacing.xl,
          vertical: PragmaSpacing.xl,
        ),
        children: <Widget>[
          const _LogoShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          Text('Paleta cromática', style: textTheme.headlineSmall),
          const SizedBox(height: PragmaSpacing.md),
          Text(
            'Visualiza los tokens de color incluidos en la librería y cómo se agrupan por intención.',
            style: textTheme.bodyMedium?.copyWith(color: onSurfaceVariant),
          ),
          const SizedBox(height: PragmaSpacing.lg),
          for (final _PaletteSection section in _paletteSections)
            _PaletteSectionView(section: section),
          const SizedBox(height: PragmaSpacing.lg),
          const _ColorTokenRowPlayground(),
          const SizedBox(height: PragmaSpacing.lg),
          Text('Componentes base', style: textTheme.headlineSmall),
          const SizedBox(height: PragmaSpacing.md),
          Wrap(
            spacing: PragmaSpacing.md,
            runSpacing: PragmaSpacing.md,
            children: <Widget>[
              const PragmaPrimaryButton(
                label: 'Primario',
                onPressed: _noop,
              ),
              const PragmaPrimaryButton(
                label: 'Primario inverso',
                tone: PragmaButtonTone.inverse,
                onPressed: _noop,
              ),
              const PragmaSecondaryButton(
                label: 'Secundario',
                onPressed: _noop,
              ),
              const PragmaTertiaryButton(
                label: 'Terciario',
                onPressed: _noop,
              ),
              const PragmaSecondaryButton(
                label: 'Secundario small',
                size: PragmaButtonSize.small,
                onPressed: _noop,
              ),
              PragmaButton.icon(
                label: 'Texto con ícono',
                icon: Icons.arrow_forward,
                hierarchy: PragmaButtonHierarchy.tertiary,
                onPressed: _noop,
              ),
              const PragmaIconButtonWidget(
                icon: Icons.favorite,
                onPressed: _noop,
                tooltip: 'Favorito',
                style: PragmaIconButtonStyle.filledLight,
              ),
              const PragmaIconButtonWidget(
                icon: Icons.palette,
                tooltip: 'Deshabilitado',
                style: PragmaIconButtonStyle.outlinedLight,
                onPressed: null,
              ),
              const PragmaAvatarWidget(
                radius: PragmaSpacing.md,
                initials: 'PD',
                tooltip: 'Avatar estático',
              ),
              const PragmaBreadcrumbWidget(
                items: <PragmaBreadcrumbItem>[
                  PragmaBreadcrumbItem(label: 'Inicio'),
                  PragmaBreadcrumbItem(label: 'Componentes'),
                  PragmaBreadcrumbItem(label: 'Breadcrumb', isCurrent: true),
                ],
              ),
            ],
          ),
          const SizedBox(height: PragmaSpacing.lg),
          PragmaCard.section(
            headline: 'Estado de diseño',
            action: PragmaButton.icon(
              label: 'Ver detalles',
              icon: Icons.open_in_new,
              hierarchy: PragmaButtonHierarchy.tertiary,
              onPressed: _noop,
            ),
            body: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  'Tokens sincronizados',
                  style: textTheme.titleMedium,
                ),
                const SizedBox(height: PragmaSpacing.sm),
                Text(
                  'Mantén consistencia visual reutilizando estos componentes en cada squad.',
                  style: textTheme.bodyMedium,
                ),
              ],
            ),
          ),
          const SizedBox(height: PragmaSpacing.lg),
          Text('PragmaCardWidget', style: textTheme.headlineSmall),
          const SizedBox(height: PragmaSpacing.md),
          Wrap(
            spacing: PragmaSpacing.lg,
            runSpacing: PragmaSpacing.lg,
            children: <Widget>[
              SizedBox(
                width: 360,
                child: PragmaCardWidget(
                  title: 'Actividad semanal',
                  subtitle: 'Actualizado hace 5 min',
                  metadata: Wrap(
                    spacing: PragmaSpacing.xs,
                    runSpacing: PragmaSpacing.xs,
                    children: <Widget>[
                      Chip(
                        label: const Text('Squad Atlas'),
                        backgroundColor: colorScheme.surfaceContainerHighest,
                      ),
                      Chip(
                        label: const Text('Mobile'),
                        backgroundColor: colorScheme.surfaceContainerHighest,
                      ),
                    ],
                  ),
                  body: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        'Sesiones totales',
                        style: textTheme.titleMedium,
                      ),
                      const SizedBox(height: PragmaSpacing.xs),
                      Text(
                        '18.4K · +4.1% vs semana anterior',
                        style: textTheme.bodyMedium?.copyWith(
                          color: onSurfaceVariant,
                        ),
                      ),
                    ],
                  ),
                  variant: PragmaCardVariant.tonal,
                  actions: <Widget>[
                    PragmaButton.icon(
                      label: 'Ver dashboard',
                      icon: Icons.open_in_new,
                      hierarchy: PragmaButtonHierarchy.tertiary,
                      onPressed: _noop,
                    ),
                  ],
                ),
              ),
              SizedBox(
                width: 320,
                child: PragmaCardWidget(
                  title: 'Prototipo desktop',
                  subtitle: 'Listo para pruebas internas',
                  metadata: Text(
                    'Actualizado por UX Research',
                    style: textTheme.bodySmall?.copyWith(
                      color: onSurfaceVariant,
                    ),
                  ),
                  media: AspectRatio(
                    aspectRatio: 4 / 3,
                    child: DecoratedBox(
                      decoration: BoxDecoration(
                        gradient: LinearGradient(
                          colors: <Color>[
                            colorScheme.primaryContainer,
                            colorScheme.secondaryContainer,
                          ],
                        ),
                      ),
                      child: const Center(
                        child: Icon(Icons.view_in_ar, size: 48),
                      ),
                    ),
                  ),
                  body: Text(
                    'Comparte enlaces a los archivos clave sin perder el contexto visual.',
                    style: textTheme.bodyMedium,
                  ),
                  size: PragmaCardSize.small,
                  variant: PragmaCardVariant.outlined,
                  actions: <Widget>[
                    TextButton.icon(
                      onPressed: _noop,
                      icon: const Icon(Icons.visibility_outlined),
                      label: const Text('Abrir preview'),
                    ),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: PragmaSpacing.lg),
          Text('PragmaAccordionWidget', style: textTheme.headlineSmall),
          const SizedBox(height: PragmaSpacing.md),
          PragmaAccordionWidget(
            text: 'Resumen del sprint',
            icon: Icons.calendar_today_outlined,
            child: Text(
              'Comparte acuerdos, links a tableros y métricas clave sin saturar la vista principal.',
              style: textTheme.bodyMedium,
            ),
          ),
          const SizedBox(height: PragmaSpacing.md),
          const PragmaAccordionWidget(
            text: 'Checklist bloqueado',
            icon: Icons.lock_outline,
            disable: true,
            size: PragmaAccordionSize.block,
            child:
                Text('Este panel permanece cerrado hasta habilitar el flujo.'),
          ),
          const SizedBox(height: PragmaSpacing.lg),
          const _InputPlayground(),
          const SizedBox(height: PragmaSpacing.lg),
          const _TextAreaShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _TagShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _BadgeShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _CheckboxShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _RadioButtonShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _ToastPlayground(),
          const SizedBox(height: PragmaSpacing.lg),
          const _LoadingShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _StepperShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _TableShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _PaginationShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _FilterShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _TooltipShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _SearchShowcase(),
          const SizedBox(height: PragmaSpacing.lg),
          const _DropdownPlayground(),
          const SizedBox(height: PragmaSpacing.lg),
          const _DropdownListPlayground(),
          const SizedBox(height: PragmaSpacing.lg),
          const _AvatarPlayground(),
          const SizedBox(height: PragmaSpacing.lg),
          const _BreadcrumbPlayground(),
          const SizedBox(height: PragmaSpacing.lg),
          const _IconButtonPlayground(),
          const SizedBox(height: PragmaSpacing.lg),
          Text('Componentes documentados', style: textTheme.headlineSmall),
          const SizedBox(height: PragmaSpacing.md),
          Column(
            children:
                documentedComponents.map((ModelPragmaComponent component) {
              return _ComponentDocCard(component: component);
            }).toList(),
          ),
        ],
      ),
    );
  }
}

void _noop() {}

void _openGridDebugger(BuildContext context) {
  Navigator.of(context).push(
    MaterialPageRoute<void>(
      builder: (_) => const GridDebuggerPage(),
    ),
  );
}

void _openCalendarDemo(BuildContext context) {
  Navigator.of(context).push(
    MaterialPageRoute<void>(
      builder: (_) => const CalendarDemoPage(),
    ),
  );
}

void _openThemeLab(BuildContext context) {
  Navigator.of(context).push(
    MaterialPageRoute<void>(
      builder: (_) => const ThemeLabPage(),
    ),
  );
}

class GridDebuggerPage extends StatelessWidget {
  const GridDebuggerPage({super.key});

  @override
  Widget build(BuildContext context) {
    final double width = MediaQuery.sizeOf(context).width;
    final PragmaViewportEnum viewport = getViewportFromWidth(width);

    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Grid debugger · ${viewport.name} · ${width.toStringAsFixed(0)}px',
        ),
      ),
      body: PragmaGridContainer(
        child: ListView(
          padding: PragmaSpacing.insetSymmetric(
            horizontal: PragmaSpacing.xl,
            vertical: PragmaSpacing.xl,
          ),
          children: <Widget>[
            Text(
              'Experimenta con el layout responsive',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: PragmaSpacing.md),
            Text(
              'Redimensiona la ventana o gira el dispositivo para ver cómo '
              'cambian los márgenes, gutters y columnas.',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: PragmaSpacing.md),
            Align(
              alignment: Alignment.centerLeft,
              child: PragmaButton.icon(
                label: 'Ver demo con ScaleBox',
                icon: Icons.open_in_new,
                hierarchy: PragmaButtonHierarchy.secondary,
                onPressed: () => Navigator.of(context).push(
                  MaterialPageRoute<void>(
                    builder: (_) => const ScaleBoxDemoPage(),
                  ),
                ),
              ),
            ),
            const SizedBox(height: PragmaSpacing.xl),
            Wrap(
              spacing: PragmaSpacing.md,
              runSpacing: PragmaSpacing.md,
              children: List<Widget>.generate(4, (int index) {
                return SizedBox(
                  width: 320,
                  child: PragmaCard.section(
                    headline: 'Módulo ${index + 1}',
                    action: PragmaButton.icon(
                      label: 'Más info',
                      icon: Icons.open_in_new,
                      hierarchy: PragmaButtonHierarchy.tertiary,
                      onPressed: _noop,
                    ),
                    body: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          'Contenido alineado a la grilla para validar '
                          'la maquetación.',
                          style: Theme.of(context).textTheme.bodyMedium,
                        ),
                        const SizedBox(height: PragmaSpacing.sm),
                        Text(
                          'Viewport actual: ${viewport.name}',
                          style: Theme.of(context)
                              .textTheme
                              .labelMedium
                              ?.copyWith(color: Colors.grey.shade600),
                        ),
                      ],
                    ),
                  ),
                );
              }),
            ),
          ],
        ),
      ),
    );
  }
}

class ScaleBoxDemoPage extends StatelessWidget {
  const ScaleBoxDemoPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('PragmaScaleBox demo'),
      ),
      body: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          final double width = constraints.maxWidth;
          final bool isCompact = width < 480;

          return ListView(
            padding: PragmaSpacing.insetSymmetric(
              horizontal: PragmaSpacing.xl,
              vertical: PragmaSpacing.xl,
            ),
            children: <Widget>[
              Text(
                'Escala composiciones completas',
                style: Theme.of(context).textTheme.headlineSmall,
              ),
              const SizedBox(height: PragmaSpacing.md),
              Text(
                'El PragmaScaleBox toma una composición con tamaño de diseño '
                'fijo (ej. 390x844) y la estira para ocupar el ancho '
                'disponible manteniendo la proporción. Úsalo para mostrar '
                'maquetas, capturas o layouts creados en Figma sin perder '
                'referencias visuales.',
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.xl),
              Container(
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.grey.shade300),
                  borderRadius: BorderRadius.circular(24),
                ),
                padding: const EdgeInsets.all(PragmaSpacing.lg),
                child: PragmaScaleBox(
                  designSize: const Size(390, 844),
                  minScale: 0.5,
                  maxScale: 1.5,
                  child: _MockupPhone(isCompact: isCompact),
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

class _MockupPhone extends StatelessWidget {
  const _MockupPhone({required this.isCompact});

  final bool isCompact;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Container(
      decoration: BoxDecoration(
        color: scheme.surface,
        borderRadius: BorderRadius.circular(48),
        boxShadow: <BoxShadow>[
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.9),
            blurRadius: 30,
            offset: const Offset(0, 30),
          ),
        ],
      ),
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Row(
            children: <Widget>[
              CircleAvatar(backgroundColor: scheme.primary),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text('Squad Andes',
                        style: Theme.of(context).textTheme.titleMedium),
                    Text(
                      'Dashboard mobile',
                      style: Theme.of(context)
                          .textTheme
                          .bodySmall
                          ?.copyWith(color: scheme.onSurfaceVariant),
                    ),
                  ],
                ),
              ),
              Icon(
                Icons.signal_cellular_alt,
                color: scheme.onSurfaceVariant,
              ),
            ],
          ),
          const SizedBox(height: PragmaSpacing.lg),
          Text(
            'Componentes sincronizados',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: PragmaSpacing.md),
          Text(
            'Este mockup respeta los márgenes y grilla de mobile, pero puede '
            'mostrarse en cualquier viewport gracias al escalado automático.',
            style: Theme.of(context)
                .textTheme
                .bodyMedium
                ?.copyWith(color: scheme.onSurfaceVariant),
          ),
          const SizedBox(height: 24),
          const Wrap(
            spacing: 12,
            runSpacing: 12,
            children: <Widget>[
              _MetricChip(icon: Icons.format_paint, label: 'Grid mobile 4px'),
              _MetricChip(icon: Icons.layers, label: '8 columnas'),
              _MetricChip(icon: Icons.lock, label: 'Ratio asegurado'),
              _MetricChip(icon: Icons.animation, label: 'Resizing automático'),
            ],
          ),
          const SizedBox(height: 24),
          AspectRatio(
            aspectRatio: isCompact ? 16 / 9 : 4 / 3,
            child: ClipRRect(
              borderRadius: BorderRadius.circular(32),
              child: Stack(
                children: <Widget>[
                  Positioned.fill(
                    child: DecoratedBox(
                      decoration: BoxDecoration(
                        gradient: LinearGradient(
                          begin: Alignment.topCenter,
                          end: Alignment.bottomCenter,
                          colors: <Color>[
                            scheme.primary
                                .withValues(alpha: PragmaOpacity.opacity8),
                            scheme.secondary
                                .withValues(alpha: PragmaOpacity.opacity8),
                          ],
                        ),
                      ),
                    ),
                  ),
                  Align(
                    alignment:
                        isCompact ? Alignment.topCenter : Alignment.centerLeft,
                    child: Padding(
                      padding: EdgeInsets.symmetric(
                        horizontal: isCompact ? 0 : 24,
                        vertical: isCompact ? 12 : 24,
                      ),
                      child: _CompactCard(isCompact: isCompact),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _CompactCard extends StatelessWidget {
  const _CompactCard({required this.isCompact});

  final bool isCompact;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Container(
      width: isCompact ? double.infinity : 280,
      decoration: BoxDecoration(
        color: scheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(24),
      ),
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text('Próximo release',
              style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 8),
          Text(
            'QA visual validado en 4 viewports usando el ScaleBox.',
            style: Theme.of(context)
                .textTheme
                .bodyMedium
                ?.copyWith(color: scheme.onSurfaceVariant),
          ),
          const SizedBox(height: 16),
          Row(
            children: <Widget>[
              const Icon(Icons.play_circle_outline),
              const SizedBox(width: 8),
              Text('Demo interactiva',
                  style: Theme.of(context).textTheme.bodyMedium),
            ],
          ),
        ],
      ),
    );
  }
}

class _MetricChip extends StatelessWidget {
  const _MetricChip({required this.icon, required this.label});

  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(99),
        color: scheme.surfaceContainer,
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Icon(icon, size: 16, color: scheme.onSurfaceVariant),
          const SizedBox(width: 6),
          Text(
            label,
            style: Theme.of(context)
                .textTheme
                .bodySmall
                ?.copyWith(color: scheme.onSurfaceVariant),
          ),
        ],
      ),
    );
  }
}

class _InputPlayground extends StatefulWidget {
  const _InputPlayground();

  @override
  State<_InputPlayground> createState() => _InputPlaygroundState();
}

class _InputPlaygroundState extends State<_InputPlayground> {
  late final PragmaInputController _controller;
  PragmaInputVariant _variant = PragmaInputVariant.filled;
  PragmaInputSize _size = PragmaInputSize.regular;
  bool _enabled = true;
  bool _passwordMode = false;
  final List<String> _suggestions = <String>[
    'Analytics Squad',
    'Aplicaciones iOS',
    'Aplicaciones Android',
    'Discovery Lab',
    'Growth',
    'Research',
  ];
  String? _lastSuggestion;

  @override
  void initState() {
    super.initState();
    _controller = PragmaInputController(
      ModelFieldState(suggestions: _suggestions),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _handleChanged(String value) {
    if (value.trim().isEmpty) {
      _controller
        ..setValidation(isDirty: true, isValid: false)
        ..setError('Dato requerido');
    } else {
      _controller
        ..setValidation(isDirty: true, isValid: true)
        ..setError(null);
    }
    setState(() {});
  }

  void _handleSuggestionSelected(String value) {
    setState(() => _lastSuggestion = value);
  }

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final ColorScheme scheme = Theme.of(context).colorScheme;
    final String statusLabel = _controller.value.errorText ??
        (_controller.value.value.isEmpty
            ? 'Esperando entrada'
            : 'Campo validado');
    final Color statusColor =
        _controller.value.errorText != null ? scheme.error : scheme.secondary;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaInputWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        Wrap(
          spacing: PragmaSpacing.lg,
          runSpacing: PragmaSpacing.lg,
          alignment: WrapAlignment.start,
          children: <Widget>[
            SizedBox(
              width: 360,
              child: PragmaInputWidget(
                label: 'Nombre del squad',
                placeholder: 'Introduce al menos 3 caracteres',
                helperText: 'Ofrecemos sugerencias de equipos activos',
                controller: _controller,
                variant: _variant,
                size: _size,
                enabled: _enabled,
                enablePasswordToggle: _passwordMode,
                obscureText: _passwordMode,
                onChanged: _handleChanged,
                onSuggestionSelected: _handleSuggestionSelected,
              ),
            ),
            SizedBox(
              width: 320,
              child: PragmaCard.section(
                headline: 'Configura el campo',
                body: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    DropdownButtonFormField<PragmaInputVariant>(
                      initialValue: _variant,
                      decoration: const InputDecoration(labelText: 'Variant'),
                      items: PragmaInputVariant.values
                          .map(
                            (PragmaInputVariant value) =>
                                DropdownMenuItem<PragmaInputVariant>(
                              value: value,
                              child: Text(value.name),
                            ),
                          )
                          .toList(),
                      onChanged: (PragmaInputVariant? value) {
                        if (value != null) {
                          setState(() => _variant = value);
                        }
                      },
                    ),
                    const SizedBox(height: PragmaSpacing.sm),
                    DropdownButtonFormField<PragmaInputSize>(
                      initialValue: _size,
                      decoration: const InputDecoration(labelText: 'Size'),
                      items: PragmaInputSize.values
                          .map(
                            (PragmaInputSize value) =>
                                DropdownMenuItem<PragmaInputSize>(
                              value: value,
                              child: Text(value.name),
                            ),
                          )
                          .toList(),
                      onChanged: (PragmaInputSize? value) {
                        if (value != null) {
                          setState(() => _size = value);
                        }
                      },
                    ),
                    const SizedBox(height: PragmaSpacing.xs),
                    SwitchListTile.adaptive(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Habilitado'),
                      value: _enabled,
                      onChanged: (bool value) =>
                          setState(() => _enabled = value),
                    ),
                    SwitchListTile.adaptive(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Modo contraseña'),
                      value: _passwordMode,
                      onChanged: (bool value) =>
                          setState(() => _passwordMode = value),
                    ),
                    if (_lastSuggestion != null) ...<Widget>[
                      const SizedBox(height: PragmaSpacing.xs),
                      Text(
                        'Sugerencia seleccionada: $_lastSuggestion',
                        style: textTheme.labelMedium,
                      ),
                    ],
                    const SizedBox(height: PragmaSpacing.xs),
                    Text(
                      'Estado: $statusLabel',
                      style: textTheme.labelLarge?.copyWith(color: statusColor),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
        const SizedBox(height: PragmaSpacing.md),
        Text(
          'Valor actual: ${_controller.value.value}',
          style: textTheme.bodyMedium,
        ),
      ],
    );
  }
}

class _TextAreaShowcase extends StatefulWidget {
  const _TextAreaShowcase();

  @override
  State<_TextAreaShowcase> createState() => _TextAreaShowcaseState();
}

class _TextAreaShowcaseState extends State<_TextAreaShowcase> {
  final TextEditingController _controller = TextEditingController(
    text:
        'Documenta los acuerdos del squad, riesgos conocidos y próximos entregables en un solo lugar.',
  );
  bool _disabled = false;
  bool _showError = false;
  bool _showSuccess = false;
  bool _showCounter = true;
  int _minLines = 4;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggleError(bool value) {
    setState(() {
      _showError = value;
      if (value) {
        _showSuccess = false;
      }
    });
  }

  void _toggleSuccess(bool value) {
    setState(() {
      _showSuccess = value;
      if (value) {
        _showError = false;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final String? errorText =
        _showError ? 'Describe el bloqueo o la tarea pendiente.' : null;
    final String? successText =
        _showSuccess ? 'Listo para compartir con el squad.' : null;
    final int maxLines = _minLines + 3;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaTextAreaWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Briefs extensos',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Combina estados default/focus/error/success y el contador para validar escenarios del spec.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.lg,
                runSpacing: PragmaSpacing.lg,
                children: <Widget>[
                  SizedBox(
                    width: 420,
                    child: PragmaTextAreaWidget(
                      label: 'Notas del requerimiento',
                      controller: _controller,
                      placeholder:
                          'Escribe alcance, supuestos, riesgos y decisiones clave...',
                      description: _showError || _showSuccess
                          ? null
                          : 'Usa este espacio para registrar acuerdos largos.',
                      errorText: errorText,
                      successText: successText,
                      enabled: !_disabled,
                      maxLength: _showCounter ? 320 : null,
                      showCounter: _showCounter,
                      minLines: _minLines,
                      maxLines: maxLines,
                      onChanged: (_) => setState(() {}),
                    ),
                  ),
                  SizedBox(
                    width: 320,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        DropdownButtonFormField<int>(
                          initialValue: _minLines,
                          decoration: const InputDecoration(
                              labelText: 'Líneas mínimas'),
                          items: const <int>[3, 4, 5, 6]
                              .map(
                                (int value) => DropdownMenuItem<int>(
                                  value: value,
                                  child: Text('$value líneas'),
                                ),
                              )
                              .toList(),
                          onChanged: (int? value) {
                            if (value != null) {
                              setState(() => _minLines = value);
                            }
                          },
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Deshabilitar campo'),
                          value: _disabled,
                          onChanged: (bool value) =>
                              setState(() => _disabled = value),
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Mostrar error'),
                          value: _showError,
                          onChanged: _toggleError,
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Mostrar success'),
                          value: _showSuccess,
                          onChanged: _toggleSuccess,
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Contador de caracteres'),
                          value: _showCounter,
                          onChanged: (bool value) =>
                              setState(() => _showCounter = value),
                        ),
                        const SizedBox(height: PragmaSpacing.xs),
                        Text(
                          'Caracteres actuales: ${_controller.text.length}',
                          style: textTheme.labelMedium,
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _TagShowcase extends StatefulWidget {
  const _TagShowcase();

  @override
  State<_TagShowcase> createState() => _TagShowcaseState();
}

class _TagShowcaseState extends State<_TagShowcase> {
  final List<String> _participants = <String>[
    'eugenia.sarmiento@pragma.com.co',
    'andres.burgos@pragma.com',
    'luisa.granados@pragma.com',
    'uxresearch@pragma.com',
  ];

  bool _withAvatar = true;
  bool _allowRemove = true;
  bool _disabled = false;
  String? _lastAction;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final TextTheme textTheme = theme.textTheme;
    final ColorScheme colorScheme = theme.colorScheme;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaTagWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Asignaciones rápidas',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Etiqueta squads, responsables o tópicos y permite eliminarlos con el ícono X.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.sm,
                runSpacing: PragmaSpacing.sm,
                children: _participants
                    .map((String email) => PragmaTagWidget(
                          label: email,
                          leading: _withAvatar
                              ? _buildAvatar(email, colorScheme)
                              : null,
                          enabled: !_disabled,
                          onPressed: _disabled
                              ? null
                              : () => setState(
                                    () => _lastAction = 'Tap: $email',
                                  ),
                          onRemove: _allowRemove && !_disabled
                              ? () => setState(
                                    () => _lastAction = 'Eliminar: $email',
                                  )
                              : null,
                        ))
                    .toList(),
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.lg,
                runSpacing: PragmaSpacing.md,
                children: <Widget>[
                  SizedBox(
                    width: 320,
                    child: Column(
                      children: <Widget>[
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Mostrar avatar'),
                          value: _withAvatar,
                          onChanged: (bool value) =>
                              setState(() => _withAvatar = value),
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Permitir eliminar'),
                          value: _allowRemove,
                          onChanged: (bool value) =>
                              setState(() => _allowRemove = value),
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Deshabilitar tags'),
                          value: _disabled,
                          onChanged: (bool value) =>
                              setState(() => _disabled = value),
                        ),
                      ],
                    ),
                  ),
                  SizedBox(
                    width: 280,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          'Última interacción',
                          style: textTheme.labelMedium,
                        ),
                        const SizedBox(height: PragmaSpacing.xs),
                        Text(
                          _lastAction ?? 'Ninguna todavía',
                          style: textTheme.bodyMedium?.copyWith(
                            color: colorScheme.onSurfaceVariant,
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildAvatar(String email, ColorScheme scheme) {
    final String alias = email.split('@').first;
    final List<String> segments = alias.split('.');
    final List<String> letters = <String>[];
    for (final String segment in segments) {
      if (segment.isEmpty) {
        continue;
      }
      letters.add(segment[0].toUpperCase());
      if (letters.length == 2) {
        break;
      }
    }
    if (letters.isEmpty && alias.isNotEmpty) {
      letters.add(alias[0].toUpperCase());
    }
    if (letters.length == 1 && alias.length > 1) {
      letters.add(alias[1].toUpperCase());
    }

    return SizedBox(
      width: 28,
      height: 28,
      child: CircleAvatar(
        backgroundColor: scheme.secondaryContainer,
        foregroundColor: scheme.onSecondaryContainer,
        child: Text(
          letters.join(),
          style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12),
        ),
      ),
    );
  }
}

class _BadgeShowcase extends StatefulWidget {
  const _BadgeShowcase();

  @override
  State<_BadgeShowcase> createState() => _BadgeShowcaseState();
}

class _BadgeShowcaseState extends State<_BadgeShowcase> {
  final TextEditingController _labelController =
      TextEditingController(text: 'Nuevo release');
  PragmaBadgeTone _tone = PragmaBadgeTone.brand;
  PragmaBadgeBrightness _brightness = PragmaBadgeBrightness.light;
  bool _dense = false;
  bool _withIcon = true;

  @override
  void dispose() {
    _labelController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final TextTheme textTheme = theme.textTheme;
    final ColorScheme scheme = theme.colorScheme;
    final IconData? icon = _withIcon ? Icons.auto_awesome : null;
    final Color previewBackground = _brightness == PragmaBadgeBrightness.dark
        ? Color.lerp(scheme.surface, Colors.black, 0.65)!
        : scheme.surfaceContainerHighest.withValues(alpha: 0.4);
    final Color borderColor = scheme.outlineVariant.withValues(alpha: 0.3);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaBadgeWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Capsulas informativas',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Etiqueta estados rápidos en dashboards o tarjetas usando los tonos brand/success/warning/info/neutral.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.lg,
                runSpacing: PragmaSpacing.lg,
                children: <Widget>[
                  SizedBox(
                    width: 360,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Container(
                          padding: const EdgeInsets.all(PragmaSpacing.md),
                          decoration: BoxDecoration(
                            color: previewBackground,
                            borderRadius:
                                BorderRadius.circular(PragmaBorderRadius.l),
                            border: Border.all(color: borderColor),
                          ),
                          child: Align(
                            alignment: Alignment.centerLeft,
                            child: PragmaBadgeWidget(
                              label: _labelController.text,
                              tone: _tone,
                              brightness: _brightness,
                              dense: _dense,
                              icon: icon,
                            ),
                          ),
                        ),
                        const SizedBox(height: PragmaSpacing.sm),
                        TextField(
                          controller: _labelController,
                          decoration: const InputDecoration(labelText: 'Label'),
                          onChanged: (_) => setState(() {}),
                        ),
                        const SizedBox(height: PragmaSpacing.sm),
                        DropdownButtonFormField<PragmaBadgeTone>(
                          initialValue: _tone,
                          decoration: const InputDecoration(labelText: 'Tone'),
                          items: PragmaBadgeTone.values
                              .map(
                                (PragmaBadgeTone tone) =>
                                    DropdownMenuItem<PragmaBadgeTone>(
                                  value: tone,
                                  child: Text(_toneLabel(tone)),
                                ),
                              )
                              .toList(),
                          onChanged: (PragmaBadgeTone? value) {
                            if (value != null) {
                              setState(() => _tone = value);
                            }
                          },
                        ),
                        const SizedBox(height: PragmaSpacing.xs),
                        SegmentedButton<PragmaBadgeBrightness>(
                          segments: const <ButtonSegment<
                              PragmaBadgeBrightness>>[
                            ButtonSegment<PragmaBadgeBrightness>(
                              value: PragmaBadgeBrightness.light,
                              label: Text('Light'),
                            ),
                            ButtonSegment<PragmaBadgeBrightness>(
                              value: PragmaBadgeBrightness.dark,
                              label: Text('Dark'),
                            ),
                          ],
                          selected: <PragmaBadgeBrightness>{_brightness},
                          showSelectedIcon: false,
                          onSelectionChanged:
                              (Set<PragmaBadgeBrightness> selection) {
                            setState(() => _brightness = selection.first);
                          },
                        ),
                        const SizedBox(height: PragmaSpacing.xs),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Modo denso'),
                          value: _dense,
                          onChanged: (bool value) =>
                              setState(() => _dense = value),
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Mostrar ícono'),
                          value: _withIcon,
                          onChanged: (bool value) =>
                              setState(() => _withIcon = value),
                        ),
                      ],
                    ),
                  ),
                  SizedBox(
                    width: 420,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text('Catálogo listo para copiar',
                            style: textTheme.titleSmall),
                        const SizedBox(height: PragmaSpacing.xs),
                        Text(
                          'Compara los tonos en light/dark y mezcla densidades para maquetar badges en wraps.',
                          style: textTheme.bodySmall
                              ?.copyWith(color: scheme.onSurfaceVariant),
                        ),
                        const SizedBox(height: PragmaSpacing.sm),
                        Wrap(
                          spacing: PragmaSpacing.sm,
                          runSpacing: PragmaSpacing.sm,
                          children: _buildCatalogBadges(),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }

  List<Widget> _buildCatalogBadges() {
    final List<Widget> badges = <Widget>[];
    for (final PragmaBadgeTone tone in PragmaBadgeTone.values) {
      badges
        ..add(
          PragmaBadgeWidget(
            label: _toneLabel(tone),
            tone: tone,
          ),
        )
        ..add(
          PragmaBadgeWidget(
            label: '${_toneLabel(tone)} dark',
            tone: tone,
            brightness: PragmaBadgeBrightness.dark,
            dense: true,
            icon: Icons.dark_mode,
          ),
        );
    }
    return badges;
  }

  String _toneLabel(PragmaBadgeTone tone) {
    switch (tone) {
      case PragmaBadgeTone.brand:
        return 'Brand';
      case PragmaBadgeTone.success:
        return 'Success';
      case PragmaBadgeTone.warning:
        return 'Warning';
      case PragmaBadgeTone.info:
        return 'Info';
      case PragmaBadgeTone.neutral:
        return 'Neutral';
    }
  }
}

class _CheckboxShowcase extends StatefulWidget {
  const _CheckboxShowcase();

  @override
  State<_CheckboxShowcase> createState() => _CheckboxShowcaseState();
}

class _CheckboxShowcaseState extends State<_CheckboxShowcase> {
  final List<_CheckboxItem> _items = <_CheckboxItem>[
    const _CheckboxItem(
      id: 'design',
      label: 'Sprint de diseño',
      description: 'Sketching, testeo y handoff semanal.',
    ),
    const _CheckboxItem(
      id: 'research',
      label: 'Investigación continua',
      description: 'Entrevistas y análisis de hallazgos.',
    ),
    const _CheckboxItem(
      id: 'qa',
      label: 'QA dedicado',
      description: 'Validaciones en staging antes de cada release.',
    ),
  ];

  final Set<String> _selected = <String>{'design', 'qa'};
  bool _disabled = false;
  bool _dense = false;

  bool? get _allValue {
    if (_selected.isEmpty) {
      return false;
    }
    if (_selected.length == _items.length) {
      return true;
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaCheckboxWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Tareas seleccionables',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Activa múltiples iniciativas del squad, incluyendo estado indeterminado para "Seleccionar todos".',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.lg,
                runSpacing: PragmaSpacing.lg,
                children: <Widget>[
                  SizedBox(
                    width: 420,
                    child: Column(
                      children: <Widget>[
                        PragmaCheckboxWidget(
                          value: _allValue,
                          tristate: true,
                          label: 'Seleccionar todos',
                          description:
                              'Activa cada frente del squad en una sola acción.',
                          dense: _dense,
                          enabled: !_disabled,
                          onChanged: _disabled
                              ? null
                              : (bool? value) => _toggleAll(value ?? false),
                        ),
                        Divider(
                            color:
                                scheme.outlineVariant.withValues(alpha: 0.3)),
                        ..._items.map(
                          (_CheckboxItem item) => PragmaCheckboxWidget(
                            value: _selected.contains(item.id),
                            label: item.label,
                            description: item.description,
                            dense: _dense,
                            enabled: !_disabled,
                            onChanged: _disabled
                                ? null
                                : (bool? value) =>
                                    _toggleItem(item, value ?? false),
                          ),
                        ),
                      ],
                    ),
                  ),
                  SizedBox(
                    width: 320,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Modo denso'),
                          value: _dense,
                          onChanged: (bool value) =>
                              setState(() => _dense = value),
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Deshabilitar grupo'),
                          value: _disabled,
                          onChanged: (bool value) =>
                              setState(() => _disabled = value),
                        ),
                        const SizedBox(height: PragmaSpacing.sm),
                        Text(
                          'Seleccionados (${_selected.length}/${_items.length})',
                          style: textTheme.labelMedium,
                        ),
                        const SizedBox(height: PragmaSpacing.xs),
                        Text(
                          _selectedLabels().isEmpty
                              ? 'Ninguna iniciativa activa'
                              : _selectedLabels().join(', '),
                          style: textTheme.bodyMedium?.copyWith(
                            color: scheme.onSurfaceVariant,
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }

  void _toggleAll(bool selectAll) {
    setState(() {
      if (selectAll) {
        _selected
          ..clear()
          ..addAll(_items.map((_CheckboxItem item) => item.id));
      } else {
        _selected.clear();
      }
    });
  }

  void _toggleItem(_CheckboxItem item, bool selected) {
    setState(() {
      if (selected) {
        _selected.add(item.id);
      } else {
        _selected.remove(item.id);
      }
    });
  }

  List<String> _selectedLabels() {
    return _items
        .where((_CheckboxItem item) => _selected.contains(item.id))
        .map((_CheckboxItem item) => item.label)
        .toList();
  }
}

class _CheckboxItem {
  const _CheckboxItem({
    required this.id,
    required this.label,
    required this.description,
  });

  final String id;
  final String label;
  final String description;
}

class _RadioButtonShowcase extends StatefulWidget {
  const _RadioButtonShowcase();

  @override
  State<_RadioButtonShowcase> createState() => _RadioButtonShowcaseState();
}

class _RadioButtonShowcaseState extends State<_RadioButtonShowcase> {
  String _selected = 'design';
  bool _disabled = false;
  bool _dense = false;
  bool _showDescription = true;

  List<_RadioOption> get _options => <_RadioOption>[
        const _RadioOption(
          value: 'design',
          label: 'Diseño lider',
          description: 'Aprueba specs y prioriza componentes.',
        ),
        const _RadioOption(
          value: 'product',
          label: 'Product management',
          description: 'Gestiona roadmap y stakeholders.',
        ),
        const _RadioOption(
          value: 'tech',
          label: 'Engineering',
          description: 'Activa despliegues y monitorea builds.',
        ),
        const _RadioOption(
          value: 'readonly',
          label: 'Solo lectura',
          description: 'Consulta métricas y descargas.',
        ),
      ];

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaRadioButtonWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Roles mutuamente excluyentes',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Elige un responsable por recurso y valida estados unselected/selected/hover/disabled.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.lg,
                runSpacing: PragmaSpacing.lg,
                children: <Widget>[
                  SizedBox(
                    width: 420,
                    child: Column(
                      children: _options
                          .map(
                            (_RadioOption option) =>
                                PragmaRadioButtonWidget<String>(
                              value: option.value,
                              groupValue: _selected,
                              label: option.label,
                              description:
                                  _showDescription ? option.description : null,
                              dense: _dense,
                              enabled: !_disabled,
                              onChanged: _disabled
                                  ? null
                                  : (String? value) {
                                      if (value != null) {
                                        setState(() => _selected = value);
                                      }
                                    },
                            ),
                          )
                          .toList(),
                    ),
                  ),
                  SizedBox(
                    width: 320,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Mostrar descripción'),
                          value: _showDescription,
                          onChanged: (bool value) =>
                              setState(() => _showDescription = value),
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Modo denso'),
                          value: _dense,
                          onChanged: (bool value) =>
                              setState(() => _dense = value),
                        ),
                        SwitchListTile.adaptive(
                          contentPadding: EdgeInsets.zero,
                          title: const Text('Deshabilitar radios'),
                          value: _disabled,
                          onChanged: (bool value) =>
                              setState(() => _disabled = value),
                        ),
                        const SizedBox(height: PragmaSpacing.sm),
                        Text(
                          'Seleccionado: $_selected',
                          style: textTheme.labelLarge?.copyWith(
                            color: scheme.primary,
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _RadioOption {
  const _RadioOption({
    required this.value,
    required this.label,
    required this.description,
  });

  final String value;
  final String label;
  final String description;
}

class _DropdownPlayground extends StatefulWidget {
  const _DropdownPlayground();

  @override
  State<_DropdownPlayground> createState() => _DropdownPlaygroundState();
}

class _ToastPlayground extends StatefulWidget {
  const _ToastPlayground();

  @override
  State<_ToastPlayground> createState() => _ToastPlaygroundState();
}

class _ToastPlaygroundState extends State<_ToastPlayground> {
  final TextEditingController _titleController =
      TextEditingController(text: 'Default Toast');
  final TextEditingController _messageController =
      TextEditingController(text: 'Este es un mensaje contextual.');
  PragmaToastVariant _variant = PragmaToastVariant.success;
  PragmaToastAlignment _alignment = PragmaToastAlignment.topCenter;
  double _durationSeconds = 6;
  bool _includeAction = false;
  int _counter = 0;

  @override
  void dispose() {
    _titleController.dispose();
    _messageController.dispose();
    super.dispose();
  }

  void _showToast(BuildContext context) {
    final Duration duration = Duration(
      milliseconds: (_durationSeconds * 1000).round(),
    );
    PragmaToastService.showToast(
      context: context,
      title: _titleController.text,
      message: _messageController.text,
      variant: _variant,
      duration: duration,
      alignment: _alignment,
      actionLabel: _includeAction ? 'Ver log' : null,
      onActionPressed: _includeAction
          ? () {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text('Acción ${++_counter} confirmada'),
                ),
              );
            }
          : null,
    );
  }

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;

    return PragmaCard.section(
      headline: 'PragmaToastWidget',
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            'Dispara toasts con variantes neon que caen desde la parte superior.',
            style: textTheme.bodyMedium,
          ),
          const SizedBox(height: PragmaSpacing.md),
          Wrap(
            spacing: PragmaSpacing.lg,
            runSpacing: PragmaSpacing.lg,
            children: <Widget>[
              SizedBox(
                width: 320,
                child: TextField(
                  controller: _titleController,
                  decoration: const InputDecoration(labelText: 'Título'),
                ),
              ),
              SizedBox(
                width: 420,
                child: TextField(
                  controller: _messageController,
                  decoration:
                      const InputDecoration(labelText: 'Mensaje opcional'),
                ),
              ),
            ],
          ),
          const SizedBox(height: PragmaSpacing.md),
          Wrap(
            spacing: PragmaSpacing.md,
            runSpacing: PragmaSpacing.sm,
            children: <Widget>[
              SizedBox(
                width: 220,
                child: DropdownButtonFormField<PragmaToastVariant>(
                  initialValue: _variant,
                  decoration: const InputDecoration(labelText: 'Variant'),
                  items: PragmaToastVariant.values
                      .map(
                        (PragmaToastVariant value) =>
                            DropdownMenuItem<PragmaToastVariant>(
                          value: value,
                          child: Text(value.name),
                        ),
                      )
                      .toList(),
                  onChanged: (PragmaToastVariant? value) {
                    if (value != null) {
                      setState(() => _variant = value);
                    }
                  },
                ),
              ),
              SizedBox(
                width: 220,
                child: DropdownButtonFormField<PragmaToastAlignment>(
                  initialValue: _alignment,
                  decoration: const InputDecoration(labelText: 'Alignment'),
                  items: PragmaToastAlignment.values
                      .map(
                        (PragmaToastAlignment value) =>
                            DropdownMenuItem<PragmaToastAlignment>(
                          value: value,
                          child: Text(value.name),
                        ),
                      )
                      .toList(),
                  onChanged: (PragmaToastAlignment? value) {
                    if (value != null) {
                      setState(() => _alignment = value);
                    }
                  },
                ),
              ),
              SizedBox(
                width: 220,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text('Duración ${_durationSeconds.toStringAsFixed(1)}s',
                        style: textTheme.labelSmall),
                    Slider(
                      value: _durationSeconds,
                      min: 2,
                      max: 10,
                      divisions: 16,
                      label: '${_durationSeconds.toStringAsFixed(1)}s',
                      onChanged: (double value) =>
                          setState(() => _durationSeconds = value),
                    ),
                  ],
                ),
              ),
              SizedBox(
                width: 220,
                child: SwitchListTile.adaptive(
                  contentPadding: EdgeInsets.zero,
                  title: const Text('Incluir acción secundaria'),
                  value: _includeAction,
                  onChanged: (bool value) =>
                      setState(() => _includeAction = value),
                ),
              ),
            ],
          ),
          const SizedBox(height: PragmaSpacing.md),
          PragmaButton.icon(
            label: 'Mostrar toast',
            icon: Icons.play_arrow,
            hierarchy: PragmaButtonHierarchy.primary,
            onPressed: () => _showToast(context),
          ),
        ],
      ),
    );
  }
}

class _LoadingShowcase extends StatefulWidget {
  const _LoadingShowcase();

  @override
  State<_LoadingShowcase> createState() => _LoadingShowcaseState();
}

class _LoadingShowcaseState extends State<_LoadingShowcase> {
  double _value = 0.72;
  bool _showLabels = true;

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;

    return PragmaCard.section(
      headline: 'PragmaLoadingWidget',
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            'Replica los indicadores morados con resplandor suave para estados '
            'determinísticos.',
            style: textTheme.bodyMedium,
          ),
          const SizedBox(height: PragmaSpacing.md),
          Wrap(
            spacing: PragmaSpacing.lg,
            runSpacing: PragmaSpacing.lg,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: <Widget>[
              PragmaLoadingWidget(
                value: _value,
                caption: 'Circular',
                size: 136,
                strokeWidth: 14,
                showPercentageLabel: _showLabels,
              ),
              const SizedBox(
                width: PragmaSpacing.xl,
              ),
              SizedBox(
                width: 280,
                child: PragmaLoadingWidget(
                  variant: PragmaLoadingVariant.linear,
                  value: _value,
                  caption: 'Progress bar',
                  showPercentageLabel: _showLabels,
                ),
              ),
            ],
          ),
          const SizedBox(height: PragmaSpacing.md),
          Text('Valor ${(_value * 100).round()}%', style: textTheme.labelLarge),
          Slider(
            value: _value,
            min: 0,
            max: 1,
            divisions: 20,
            label: '${(_value * 100).round()}%',
            onChanged: (double value) => setState(() => _value = value),
          ),
          SwitchListTile.adaptive(
            contentPadding: EdgeInsets.zero,
            title: const Text('Mostrar porcentaje integrado'),
            value: _showLabels,
            onChanged: (bool value) => setState(() => _showLabels = value),
          ),
        ],
      ),
    );
  }
}

class _StepperShowcase extends StatefulWidget {
  const _StepperShowcase();

  @override
  State<_StepperShowcase> createState() => _StepperShowcaseState();
}

class _StepperShowcaseState extends State<_StepperShowcase> {
  PragmaStepperSize _size = PragmaStepperSize.big;
  bool _showFailure = false;

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final List<PragmaStepperStep> steps = _buildSteps(compact: false);
    final List<PragmaStepperStep> compactSteps = _buildSteps(compact: true);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaStepperWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Configura el flujo',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Selecciona el tamaño y simula un escenario con error o con el flujo completo.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.sm),
              SegmentedButton<PragmaStepperSize>(
                segments: const <ButtonSegment<PragmaStepperSize>>[
                  ButtonSegment<PragmaStepperSize>(
                    value: PragmaStepperSize.big,
                    label: Text('Big'),
                  ),
                  ButtonSegment<PragmaStepperSize>(
                    value: PragmaStepperSize.small,
                    label: Text('Small'),
                  ),
                ],
                selected: <PragmaStepperSize>{_size},
                onSelectionChanged: (Set<PragmaStepperSize> values) {
                  setState(() => _size = values.first);
                },
              ),
              SwitchListTile.adaptive(
                contentPadding: EdgeInsets.zero,
                title: const Text('Simular error en el último paso'),
                value: _showFailure,
                onChanged: (bool value) => setState(() => _showFailure = value),
              ),
              const SizedBox(height: PragmaSpacing.md),
              PragmaStepperWidget(steps: steps, size: _size),
              const SizedBox(height: PragmaSpacing.md),
              Text('Preset compacto', style: textTheme.titleSmall),
              const SizedBox(height: PragmaSpacing.sm),
              PragmaStepperWidget(
                steps: compactSteps,
                size: PragmaStepperSize.small,
              ),
            ],
          ),
        ),
      ],
    );
  }

  List<PragmaStepperStep> _buildSteps({required bool compact}) {
    return <PragmaStepperStep>[
      PragmaStepperStep(
        title: compact ? 'Brief' : 'Discovery & Briefing',
        description: compact ? null : 'Historias aprobadas',
        status: PragmaStepperStatus.success,
      ),
      PragmaStepperStep(
        title: compact ? 'UX' : 'Investigación UX',
        description: compact ? 'Validando' : 'Puestos en pruebas con usuarios',
        status: PragmaStepperStatus.currentPurple,
      ),
      PragmaStepperStep(
        title: compact ? 'Dev' : 'Implementación',
        description: compact ? 'Pendiente' : 'Se define backlog de sprint',
        status: PragmaStepperStatus.currentWhite,
      ),
      PragmaStepperStep(
        title: compact ? 'QA' : 'QA funcional',
        description: compact
            ? (_showFailure ? 'Bloqueado' : 'Esperando dev')
            : (_showFailure ? 'Bloqueado por bug crítico' : 'Listo tras dev'),
        status: _showFailure
            ? PragmaStepperStatus.fail
            : PragmaStepperStatus.disabled,
      ),
    ];
  }
}

class _TableShowcase extends StatefulWidget {
  const _TableShowcase();

  @override
  State<_TableShowcase> createState() => _TableShowcaseState();
}

class _TableShowcaseState extends State<_TableShowcase> {
  PragmaTableRowTone _tone = PragmaTableRowTone.light;
  bool _compact = false;
  bool _hoverLastRow = true;
  int? _selectedRow;

  static const List<_TableMember> _members = <_TableMember>[
    _TableMember(
      name: 'Andreina Yajaira Francesca Serrano',
      role: 'Discovery Research · Squad Atlas',
      project: 'Discovery Lab',
      icon: Icons.auto_awesome_outlined,
    ),
    _TableMember(
      name: 'Samuel Valencia',
      role: 'Engineering Manager · Squad Pulsar',
      project: 'Onboarding Web',
      icon: Icons.settings_backup_restore_outlined,
    ),
    _TableMember(
      name: 'Gabriela Torres',
      role: 'UX Writer · Squad Cosmos',
      project: 'Comms Platform',
      icon: Icons.translate_outlined,
    ),
    _TableMember(
      name: 'Felipe Gaitán',
      role: 'iOS Developer · Squad Orbit',
      project: 'Discovery Lab',
      icon: Icons.phone_iphone_outlined,
    ),
  ];

  List<PragmaTableColumn> get _columns => const <PragmaTableColumn>[
        PragmaTableColumn(label: '', flex: 1, alignment: Alignment.centerLeft),
        PragmaTableColumn(
            label: 'Nombre', flex: 4, alignment: Alignment.centerLeft),
        PragmaTableColumn(
            label: 'Proyecto', flex: 3, alignment: Alignment.centerLeft),
        PragmaTableColumn(
            label: 'Estado', flex: 2, alignment: Alignment.center),
        PragmaTableColumn(
          label: 'Acción',
          flex: 2,
          alignment: Alignment.centerRight,
        ),
      ];

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaTableWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Tablas multi-paso',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Activa el modo oscuro/claro, reduce la densidad y simula el estado hover morado.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.sm),
              SegmentedButton<PragmaTableRowTone>(
                segments: const <ButtonSegment<PragmaTableRowTone>>[
                  ButtonSegment<PragmaTableRowTone>(
                    value: PragmaTableRowTone.light,
                    label: Text('Light'),
                  ),
                  ButtonSegment<PragmaTableRowTone>(
                    value: PragmaTableRowTone.dark,
                    label: Text('Dark'),
                  ),
                ],
                selected: <PragmaTableRowTone>{_tone},
                onSelectionChanged: (Set<PragmaTableRowTone> values) {
                  setState(() => _tone = values.first);
                },
              ),
              SwitchListTile.adaptive(
                contentPadding: EdgeInsets.zero,
                title: const Text('Modo compacto'),
                value: _compact,
                onChanged: (bool value) => setState(() => _compact = value),
              ),
              SwitchListTile.adaptive(
                contentPadding: EdgeInsets.zero,
                title: const Text('Hover en la última fila'),
                value: _hoverLastRow,
                onChanged: (bool value) =>
                    setState(() => _hoverLastRow = value),
              ),
              const SizedBox(height: PragmaSpacing.md),
              PragmaTableWidget(
                columns: _columns,
                rows: _buildRows(),
                compact: _compact,
              ),
              const SizedBox(height: PragmaSpacing.sm),
              Text(
                'Toca una fila para marcarla como seleccionada y replicar el highlight.',
                style: textTheme.bodySmall,
              ),
            ],
          ),
        ),
      ],
    );
  }

  List<PragmaTableRowData> _buildRows() {
    return List<PragmaTableRowData>.generate(_members.length, (int index) {
      final _TableMember member = _members[index];
      final bool isSelected = _selectedRow == index;
      final bool isHoverRow = _hoverLastRow && index == _members.length - 1;

      PragmaTableRowState state = PragmaTableRowState.idle;
      if (isHoverRow) {
        state = PragmaTableRowState.hover;
      }
      if (isSelected) {
        state = PragmaTableRowState.selected;
      }

      return PragmaTableRowData(
        tone: _tone,
        state: state,
        semanticLabel: '${member.name} asignada a ${member.project}',
        onTap: () => _toggleSelection(index),
        cells: <Widget>[
          Checkbox(
            value: isSelected,
            onChanged: (_) => _toggleSelection(index),
          ),
          _TableMemberCell(member: member),
          Text(member.project, overflow: TextOverflow.ellipsis),
          Icon(member.icon),
          const PragmaTertiaryButton(
            label: 'Ver ficha',
            size: PragmaButtonSize.small,
            onPressed: _noop,
          ),
        ],
      );
    });
  }

  void _toggleSelection(int index) {
    setState(() {
      _selectedRow = _selectedRow == index ? null : index;
    });
  }
}

class _TableMember {
  const _TableMember({
    required this.name,
    required this.role,
    required this.project,
    required this.icon,
  });

  final String name;
  final String role;
  final String project;
  final IconData icon;

  String get initials {
    final List<String> parts = name
        .trim()
        .split(RegExp(r'\s+'))
        .where((String part) => part.isNotEmpty)
        .toList();
    if (parts.isEmpty) {
      return 'P';
    }
    if (parts.length == 1) {
      return parts.first[0].toUpperCase();
    }
    final String first = parts.first[0].toUpperCase();
    final String second = parts[1][0].toUpperCase();
    return '$first$second';
  }
}

class _TableMemberCell extends StatelessWidget {
  const _TableMemberCell({required this.member});

  final _TableMember member;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Row(
      children: <Widget>[
        CircleAvatar(
          radius: 18,
          backgroundColor: scheme.secondaryContainer,
          child: Text(
            member.initials,
            style: const TextStyle(fontWeight: FontWeight.w600),
          ),
        ),
        const SizedBox(width: PragmaSpacing.xs),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              DefaultTextStyle.merge(
                style: const TextStyle(fontWeight: FontWeight.w600),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                child: Text(member.name),
              ),
              const SizedBox(height: 2),
              DefaultTextStyle.merge(
                style:
                    const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                child: Text(member.role),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _PaginationShowcase extends StatefulWidget {
  const _PaginationShowcase();

  @override
  State<_PaginationShowcase> createState() => _PaginationShowcaseState();
}

class _PaginationShowcaseState extends State<_PaginationShowcase> {
  static const List<int> _perPageOptions = <int>[10, 25, 50, 100];
  static const List<String> _owners = <String>[
    'Andreina Serrano',
    'Luisa Granados',
    'Samuel Valencia',
    'Gabriela Torres',
    'Felipe Gaitán',
    'Diego Salazar',
    'Juliana Pardo',
  ];
  static const List<_PaginationStatus> _statuses = <_PaginationStatus>[
    _PaginationStatus(label: 'QA activo', tone: PragmaBadgeTone.info),
    _PaginationStatus(label: 'En curso', tone: PragmaBadgeTone.warning),
    _PaginationStatus(label: 'Deploy listo', tone: PragmaBadgeTone.success),
  ];

  PragmaPaginationTone _tone = PragmaPaginationTone.dark;
  bool _showSummary = true;
  int _currentPage = 2;
  int _itemsPerPage = 25;
  double _totalItemsSlider = 180;

  int get _totalRecords => _totalItemsSlider.round();
  int get _totalPages => math.max(1, (_totalRecords / _itemsPerPage).ceil());

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final List<_PaginationRecord> pageRecords = _buildRecordsForPage();
    final List<_PaginationRecord> preview = pageRecords.take(6).toList();

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaPaginationWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Paginación viva',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Alterna superficie, summary y registros por página para replicar catálogos extensos.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.md),
              SegmentedButton<PragmaPaginationTone>(
                segments: const <ButtonSegment<PragmaPaginationTone>>[
                  ButtonSegment<PragmaPaginationTone>(
                    value: PragmaPaginationTone.dark,
                    label: Text('Dark'),
                  ),
                  ButtonSegment<PragmaPaginationTone>(
                    value: PragmaPaginationTone.light,
                    label: Text('Light'),
                  ),
                ],
                selected: <PragmaPaginationTone>{_tone},
                onSelectionChanged: (Set<PragmaPaginationTone> value) {
                  setState(() => _tone = value.first);
                },
              ),
              SwitchListTile.adaptive(
                contentPadding: EdgeInsets.zero,
                title: const Text('Mostrar summary'),
                value: _showSummary,
                onChanged: (bool value) => setState(() => _showSummary = value),
              ),
              ListTile(
                contentPadding: EdgeInsets.zero,
                title: Text('Total de registros: $_totalRecords'),
                subtitle:
                    const Text('Usa el slider para llegar hasta 400 items.'),
                trailing: Text('$_totalPages páginas'),
              ),
              Slider(
                value: _totalItemsSlider,
                min: 60,
                max: 400,
                divisions: 17,
                label: '$_totalRecords registros',
                onChanged: (double value) {
                  setState(() {
                    _totalItemsSlider = value;
                    _currentPage = math.min(_currentPage, _totalPages);
                  });
                },
              ),
              const SizedBox(height: PragmaSpacing.sm),
              PragmaPaginationWidget(
                currentPage: _currentPage,
                totalPages: _totalPages,
                tone: _tone,
                itemsPerPage: _itemsPerPage,
                itemsPerPageOptions: _perPageOptions,
                totalItems: _totalRecords,
                showSummary: _showSummary,
                onPageChanged: (int page) {
                  setState(() => _currentPage = page);
                },
                onItemsPerPageChanged: (int value) {
                  setState(() {
                    _itemsPerPage = value;
                    _currentPage = math.min(_currentPage, _totalPages);
                  });
                },
              ),
              const SizedBox(height: PragmaSpacing.md),
              _PaginationRecordsPreview(
                records: preview,
                pageSize: pageRecords.length,
              ),
            ],
          ),
        ),
      ],
    );
  }

  List<_PaginationRecord> _buildRecordsForPage() {
    final int startIndex = (_currentPage - 1) * _itemsPerPage;
    final int endIndexExclusive =
        math.min(startIndex + _itemsPerPage, _totalRecords);
    final List<_PaginationRecord> records = <_PaginationRecord>[];
    for (int index = startIndex; index < endIndexExclusive; index += 1) {
      final _PaginationStatus status = _statuses[index % _statuses.length];
      records.add(
        _PaginationRecord(
          ticket: 'Ticket ${(index + 1).toString().padLeft(3, '0')}',
          squad: 'Squad ${(index % 8) + 1}',
          owner: _owners[index % _owners.length],
          status: status,
        ),
      );
    }
    return records;
  }
}

class _PaginationRecord {
  const _PaginationRecord({
    required this.ticket,
    required this.squad,
    required this.owner,
    required this.status,
  });

  final String ticket;
  final String squad;
  final String owner;
  final _PaginationStatus status;
}

class _PaginationStatus {
  const _PaginationStatus({required this.label, required this.tone});

  final String label;
  final PragmaBadgeTone tone;
}

class _PaginationRecordsPreview extends StatelessWidget {
  const _PaginationRecordsPreview({
    required this.records,
    required this.pageSize,
  });

  final List<_PaginationRecord> records;
  final int pageSize;

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final Color captionColor = Theme.of(context).colorScheme.onSurfaceVariant;
    if (records.isEmpty) {
      return Text(
        'Esta página no tiene registros cargados.',
        style: textTheme.bodyMedium,
      );
    }

    final bool isFullPreview = records.length == pageSize;
    final String subtitle = isFullPreview
        ? 'Mostrando $pageSize registros en esta página.'
        : 'Vista previa de ${records.length} de $pageSize registros.';

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('Vista previa interactiva', style: textTheme.titleMedium),
        const SizedBox(height: PragmaSpacing.xs),
        Text(subtitle,
            style: textTheme.bodySmall?.copyWith(color: captionColor)),
        const SizedBox(height: PragmaSpacing.sm),
        Card(
          margin: EdgeInsets.zero,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(PragmaSpacing.md),
          ),
          child: Column(
            children: List<Widget>.generate(records.length, (int index) {
              final _PaginationRecord record = records[index];
              return Column(
                children: <Widget>[
                  ListTile(
                    dense: true,
                    title: Text(record.ticket),
                    subtitle: Text('${record.squad} · ${record.owner}'),
                    trailing: PragmaBadgeWidget(
                      label: record.status.label,
                      tone: record.status.tone,
                    ),
                  ),
                  if (index != records.length - 1) const Divider(height: 1),
                ],
              );
            }),
          ),
        ),
      ],
    );
  }
}

class _FilterShowcase extends StatefulWidget {
  const _FilterShowcase();

  @override
  State<_FilterShowcase> createState() => _FilterShowcaseState();
}

class _FilterShowcaseState extends State<_FilterShowcase> {
  Set<String> _selectedSquads = <String>{'atlas'};
  Set<String> _selectedStatuses = <String>{'qa'};
  PragmaFilterTone _tone = PragmaFilterTone.dark;
  bool _showTags = true;
  bool _showHelper = true;
  bool _enabled = true;

  static const List<PragmaFilterOption> _squadOptions = <PragmaFilterOption>[
    PragmaFilterOption(value: 'atlas', label: 'Squad Atlas', meta: 'Discovery'),
    PragmaFilterOption(value: 'cosmos', label: 'Squad Cosmos', meta: 'Comms'),
    PragmaFilterOption(value: 'orbit', label: 'Squad Orbit', meta: 'Mobile'),
    PragmaFilterOption(value: 'pulsar', label: 'Squad Pulsar', meta: 'Growth'),
  ];

  static const List<PragmaFilterOption> _statusOptions = <PragmaFilterOption>[
    PragmaFilterOption(value: 'draft', label: 'Briefing', meta: 'Ideando'),
    PragmaFilterOption(value: 'qa', label: 'QA activo', meta: 'Validando'),
    PragmaFilterOption(value: 'ready', label: 'Listo para deploy', meta: '✅'),
  ];

  static const List<_FilterRecord> _records = <_FilterRecord>[
    _FilterRecord(
      name: 'Andreina Serrano',
      role: 'Product Designer',
      squad: 'atlas',
      status: _FilterStatus.qa,
    ),
    _FilterRecord(
      name: 'Gabriela Torres',
      role: 'UX Writer',
      squad: 'cosmos',
      status: _FilterStatus.draft,
    ),
    _FilterRecord(
      name: 'Samuel Valencia',
      role: 'Engineering Manager',
      squad: 'pulsar',
      status: _FilterStatus.ready,
    ),
    _FilterRecord(
      name: 'Luisa Granados',
      role: 'iOS Developer',
      squad: 'orbit',
      status: _FilterStatus.qa,
    ),
  ];

  static const List<PragmaTableColumn> _columns = <PragmaTableColumn>[
    PragmaTableColumn(label: 'Nombre', flex: 4),
    PragmaTableColumn(label: 'Squad', flex: 2),
    PragmaTableColumn(label: 'Estado', flex: 2, alignment: Alignment.center),
    PragmaTableColumn(
      label: 'Acción',
      flex: 2,
      alignment: Alignment.centerRight,
    ),
  ];

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final List<_FilterRecord> data = _filteredRecords;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaFilterWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Filtros flotantes',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Combina cápsulas con contador, helper text, tags y una tabla reactiva.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.lg,
                runSpacing: PragmaSpacing.lg,
                children: <Widget>[
                  SizedBox(
                    width: 320,
                    child: PragmaFilterWidget(
                      label: 'Squad',
                      options: _squadOptions,
                      selectedValues: _selectedSquads,
                      tone: _tone,
                      summaryLabel: 'Squads activos',
                      helperText: _showHelper
                          ? 'Selecciona squads para acotar la tabla.'
                          : null,
                      showSummaryTags: _showTags,
                      enabled: _enabled,
                      onChanged: (Set<String> values) {
                        setState(() => _selectedSquads = values);
                      },
                    ),
                  ),
                  SizedBox(
                    width: 320,
                    child: PragmaFilterWidget(
                      label: 'Estado',
                      options: _statusOptions,
                      selectedValues: _selectedStatuses,
                      tone: _tone,
                      helperText: _showHelper
                          ? 'Puedes marcar varios estados a la vez.'
                          : null,
                      showSummaryTags: _showTags,
                      enabled: _enabled,
                      onChanged: (Set<String> values) {
                        setState(() => _selectedStatuses = values);
                      },
                    ),
                  ),
                ],
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.md,
                runSpacing: PragmaSpacing.sm,
                children: <Widget>[
                  SegmentedButton<PragmaFilterTone>(
                    segments: const <ButtonSegment<PragmaFilterTone>>[
                      ButtonSegment<PragmaFilterTone>(
                        value: PragmaFilterTone.dark,
                        label: Text('Dark'),
                      ),
                      ButtonSegment<PragmaFilterTone>(
                        value: PragmaFilterTone.light,
                        label: Text('Light'),
                      ),
                    ],
                    selected: <PragmaFilterTone>{_tone},
                    onSelectionChanged: (Set<PragmaFilterTone> values) {
                      setState(() => _tone = values.first);
                    },
                  ),
                  SizedBox(
                    width: 240,
                    child: SwitchListTile.adaptive(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Mostrar tags'),
                      value: _showTags,
                      onChanged: (bool value) =>
                          setState(() => _showTags = value),
                    ),
                  ),
                  SizedBox(
                    width: 240,
                    child: SwitchListTile.adaptive(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Helper text'),
                      value: _showHelper,
                      onChanged: (bool value) =>
                          setState(() => _showHelper = value),
                    ),
                  ),
                  SizedBox(
                    width: 240,
                    child: SwitchListTile.adaptive(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Habilitar filtros'),
                      value: _enabled,
                      onChanged: (bool value) =>
                          setState(() => _enabled = value),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: PragmaSpacing.md),
              PragmaTableWidget(
                columns: _columns,
                rows: _buildRows(data),
                showHeader: true,
                emptyPlaceholder: const Text(
                  'Ninguna persona coincide con los filtros aplicados.',
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  List<_FilterRecord> get _filteredRecords {
    return _records.where((_FilterRecord record) {
      final bool squadMatch =
          _selectedSquads.isEmpty || _selectedSquads.contains(record.squad);
      final bool statusMatch = _selectedStatuses.isEmpty ||
          _selectedStatuses.contains(record.status.name);
      return squadMatch && statusMatch;
    }).toList(growable: false);
  }

  List<PragmaTableRowData> _buildRows(List<_FilterRecord> records) {
    return records.map(((_FilterRecord record) {
      return PragmaTableRowData(
        tone: PragmaTableRowTone.light,
        cells: <Widget>[
          _FilterRecordCell(record: record),
          Text(_squadLabel(record.squad)),
          Align(
            alignment: Alignment.center,
            child: PragmaBadgeWidget(
              label: record.status.label,
              tone: record.status.badgeTone,
              brightness: PragmaBadgeBrightness.light,
              dense: true,
            ),
          ),
          const PragmaSecondaryButton(
            label: 'Asignar',
            size: PragmaButtonSize.small,
            onPressed: _noop,
          ),
        ],
      );
    })).toList(growable: false);
  }

  String _squadLabel(String squad) {
    for (final PragmaFilterOption option in _squadOptions) {
      if (option.value == squad) {
        return option.label;
      }
    }
    return squad;
  }
}

class _FilterRecordCell extends StatelessWidget {
  const _FilterRecordCell({required this.record});

  final _FilterRecord record;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Row(
      children: <Widget>[
        CircleAvatar(
          radius: 18,
          backgroundColor: scheme.primaryContainer,
          child: Text(
            record.initials,
            style: const TextStyle(fontWeight: FontWeight.w600),
          ),
        ),
        const SizedBox(width: PragmaSpacing.xs),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              DefaultTextStyle.merge(
                style: const TextStyle(fontWeight: FontWeight.w600),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                child: Text(record.name),
              ),
              const SizedBox(height: 2),
              DefaultTextStyle.merge(
                style:
                    const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                child: Text(record.role),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _TooltipShowcase extends StatefulWidget {
  const _TooltipShowcase();

  @override
  State<_TooltipShowcase> createState() => _TooltipShowcaseState();
}

class _TooltipShowcaseState extends State<_TooltipShowcase> {
  PragmaTooltipTone _tone = PragmaTooltipTone.dark;
  bool _showTitle = true;
  bool _showIcon = true;
  bool _showAction = true;
  bool _longCopy = false;

  String get _message => _longCopy
      ? 'Comparte instrucciones precisas sin saturar la UI. El tooltip desaparece después de unos segundos o al salir del hover.'
      : 'Copy breve y descriptivo para guiar acciones inmediatas.';

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final IconData? icon = _showIcon ? Icons.lightbulb_outline : null;
    final PragmaTooltipAction? action = _showAction
        ? PragmaTooltipAction(
            label: 'Button',
            onPressed: () => debugPrint('Tooltip action fired'),
          )
        : null;
    final String? title = _showTitle ? 'Title (optional)' : null;

    final List<_TooltipTargetConfig> targets = <_TooltipTargetConfig>[
      const _TooltipTargetConfig(
        label: 'Bottom',
        placement: PragmaTooltipPlacement.bottom,
        icon: Icons.touch_app,
      ),
      const _TooltipTargetConfig(
        label: 'Top',
        placement: PragmaTooltipPlacement.top,
        icon: Icons.keyboard_arrow_up,
      ),
      const _TooltipTargetConfig(
        label: 'Left',
        placement: PragmaTooltipPlacement.left,
        icon: Icons.arrow_back,
      ),
      const _TooltipTargetConfig(
        label: 'Right',
        placement: PragmaTooltipPlacement.right,
        icon: Icons.arrow_forward,
      ),
    ];

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaTooltipWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Tooltips multivariantes',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Combina título, ícono, botón interno y arrow para simular top/bottom/left/right en light o dark.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.md,
                runSpacing: PragmaSpacing.sm,
                children: <Widget>[
                  SegmentedButton<PragmaTooltipTone>(
                    segments: const <ButtonSegment<PragmaTooltipTone>>[
                      ButtonSegment<PragmaTooltipTone>(
                        value: PragmaTooltipTone.dark,
                        label: Text('Dark'),
                      ),
                      ButtonSegment<PragmaTooltipTone>(
                        value: PragmaTooltipTone.light,
                        label: Text('Light'),
                      ),
                    ],
                    selected: <PragmaTooltipTone>{_tone},
                    onSelectionChanged: (Set<PragmaTooltipTone> values) {
                      setState(() => _tone = values.first);
                    },
                  ),
                  SizedBox(
                    width: 220,
                    child: SwitchListTile.adaptive(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Mostrar título'),
                      value: _showTitle,
                      onChanged: (bool value) =>
                          setState(() => _showTitle = value),
                    ),
                  ),
                  SizedBox(
                    width: 220,
                    child: SwitchListTile.adaptive(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Ícono inicial'),
                      value: _showIcon,
                      onChanged: (bool value) =>
                          setState(() => _showIcon = value),
                    ),
                  ),
                  SizedBox(
                    width: 220,
                    child: SwitchListTile.adaptive(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Botón interno'),
                      value: _showAction,
                      onChanged: (bool value) =>
                          setState(() => _showAction = value),
                    ),
                  ),
                  SizedBox(
                    width: 220,
                    child: SwitchListTile.adaptive(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Texto largo'),
                      value: _longCopy,
                      onChanged: (bool value) =>
                          setState(() => _longCopy = value),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: PragmaSpacing.lg),
              Container(
                width: double.infinity,
                padding: PragmaSpacing.insetAll(PragmaSpacing.lg),
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.surfaceContainerHighest,
                  borderRadius:
                      BorderRadius.circular(PragmaBorderRadiusTokens.l.value),
                ),
                child: Wrap(
                  spacing: PragmaSpacing.lg,
                  runSpacing: PragmaSpacing.lg,
                  alignment: WrapAlignment.center,
                  children: targets.map((_) {
                    return PragmaTooltipWidget(
                      tone: _tone,
                      placement: _.placement,
                      title: title,
                      message: _message,
                      icon: icon,
                      action: action,
                      child: PragmaButton.icon(
                        label: '${_.label} tooltip',
                        icon: _.icon,
                        hierarchy: PragmaButtonHierarchy.secondary,
                        onPressed: _noop,
                        size: PragmaButtonSize.small,
                      ),
                    );
                  }).toList(),
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _TooltipTargetConfig {
  const _TooltipTargetConfig({
    required this.label,
    required this.placement,
    required this.icon,
  });

  final String label;
  final PragmaTooltipPlacement placement;
  final IconData icon;
}

class _FilterRecord {
  const _FilterRecord({
    required this.name,
    required this.role,
    required this.squad,
    required this.status,
  });

  final String name;
  final String role;
  final String squad;
  final _FilterStatus status;

  String get initials {
    final List<String> parts = name
        .trim()
        .split(RegExp(r'\s+'))
        .where((String part) => part.isNotEmpty)
        .toList();
    if (parts.isEmpty) {
      return 'P';
    }
    if (parts.length == 1) {
      return parts.first[0].toUpperCase();
    }
    return parts.first[0].toUpperCase() + parts[1][0].toUpperCase();
  }
}

enum _FilterStatus { draft, qa, ready }

extension _FilterStatusX on _FilterStatus {
  String get label {
    switch (this) {
      case _FilterStatus.draft:
        return 'Briefing';
      case _FilterStatus.qa:
        return 'QA activo';
      case _FilterStatus.ready:
        return 'Listo para deploy';
    }
  }

  PragmaBadgeTone get badgeTone {
    switch (this) {
      case _FilterStatus.draft:
        return PragmaBadgeTone.info;
      case _FilterStatus.qa:
        return PragmaBadgeTone.warning;
      case _FilterStatus.ready:
        return PragmaBadgeTone.success;
    }
  }
}

class _SearchShowcase extends StatefulWidget {
  const _SearchShowcase();

  @override
  State<_SearchShowcase> createState() => _SearchShowcaseState();
}

class _SearchShowcaseState extends State<_SearchShowcase> {
  final TextEditingController _darkController = TextEditingController();
  final TextEditingController _lightController = TextEditingController();
  PragmaSearchSize _size = PragmaSearchSize.large;
  bool _disabled = false;
  bool _showInfo = true;

  @override
  void dispose() {
    _darkController.dispose();
    _lightController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final List<String> darkSuggestions = _filtered(_darkController.text);
    final List<String> lightSuggestions = _filtered(_lightController.text);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaSearchWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        PragmaCard.section(
          headline: 'Búsqueda con glow',
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Explora los estados default/hover/filled usando los tonos light y dark. '
                'Al escribir mostramos sugerencias tipo “dropdown list” para simular el flujo del spec.',
                style: textTheme.bodyMedium,
              ),
              const SizedBox(height: PragmaSpacing.sm),
              SegmentedButton<PragmaSearchSize>(
                segments: const <ButtonSegment<PragmaSearchSize>>[
                  ButtonSegment<PragmaSearchSize>(
                    value: PragmaSearchSize.small,
                    label: Text('Small'),
                  ),
                  ButtonSegment<PragmaSearchSize>(
                    value: PragmaSearchSize.large,
                    label: Text('Large'),
                  ),
                ],
                selected: <PragmaSearchSize>{_size},
                onSelectionChanged: (Set<PragmaSearchSize> values) {
                  setState(() => _size = values.first);
                },
              ),
              SwitchListTile.adaptive(
                contentPadding: EdgeInsets.zero,
                title: const Text('Deshabilitar campo'),
                value: _disabled,
                onChanged: (bool value) => setState(() => _disabled = value),
              ),
              SwitchListTile.adaptive(
                contentPadding: EdgeInsets.zero,
                title: const Text('Mostrar texto informativo'),
                value: _showInfo,
                onChanged: (bool value) => setState(() => _showInfo = value),
              ),
              const SizedBox(height: PragmaSpacing.md),
              Wrap(
                spacing: PragmaSpacing.lg,
                runSpacing: PragmaSpacing.lg,
                children: <Widget>[
                  _buildSearchColumn(
                    context: context,
                    title: 'Dark preset',
                    controller: _darkController,
                    tone: PragmaSearchTone.dark,
                    suggestions: darkSuggestions,
                  ),
                  _buildSearchColumn(
                    context: context,
                    title: 'Light preset',
                    controller: _lightController,
                    tone: PragmaSearchTone.light,
                    suggestions: lightSuggestions,
                  ),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildSearchColumn({
    required BuildContext context,
    required String title,
    required TextEditingController controller,
    required PragmaSearchTone tone,
    required List<String> suggestions,
  }) {
    final TextTheme textTheme = Theme.of(context).textTheme;

    return SizedBox(
      width: 360,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(title, style: textTheme.titleSmall),
          const SizedBox(height: PragmaSpacing.xs),
          PragmaSearchWidget(
            controller: controller,
            placeholder: 'Placeholder text here...',
            tone: tone,
            size: _size,
            enabled: !_disabled,
            infoText: _showInfo
                ? 'Usa palabras clave o despliega opciones con Dropdown list.'
                : null,
            onChanged: (_) => setState(() {}),
            onSubmitted: (String value) => _announceSearch(context, value),
            onClear: () => _announceSearch(context, ''),
          ),
          if (suggestions.isNotEmpty) ...<Widget>[
            const SizedBox(height: PragmaSpacing.xs),
            _SearchSuggestionPanel(
              items: suggestions,
              onSelected: (String value) {
                controller
                  ..text = value
                  ..selection = TextSelection.collapsed(offset: value.length);
                setState(() {});
                _announceSearch(context, value);
              },
            ),
          ],
        ],
      ),
    );
  }

  List<String> _filtered(String query) {
    if (query.trim().isEmpty) {
      return const <String>[];
    }
    final String normalized = query.toLowerCase();
    return _searchTopics
        .where((String topic) => topic.toLowerCase().contains(normalized))
        .take(4)
        .toList(growable: false);
  }

  void _announceSearch(BuildContext context, String query) {
    if (!mounted) {
      return;
    }
    final String message =
        query.isEmpty ? 'Búsqueda reiniciada' : 'Buscar "$query"';
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(content: Text(message), duration: const Duration(seconds: 1)),
      );
  }
}

class _SearchSuggestionPanel extends StatelessWidget {
  const _SearchSuggestionPanel({
    required this.items,
    required this.onSelected,
  });

  final List<String> items;
  final ValueChanged<String> onSelected;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final ColorScheme scheme = theme.colorScheme;
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(PragmaSpacing.sm),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(PragmaBorderRadius.l),
        color: scheme.surfaceContainerLowest,
        border: Border.all(color: scheme.primary.withValues(alpha: 0.35)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: items
            .map(
              (String item) => ListTile(
                contentPadding: EdgeInsets.zero,
                dense: true,
                leading: const Icon(Icons.search, size: 16),
                title: Text(item),
                onTap: () => onSelected(item),
              ),
            )
            .toList(growable: false),
      ),
    );
  }
}

class _DropdownPlaygroundState extends State<_DropdownPlayground> {
  String? _selectedRole;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final TextTheme textTheme = theme.textTheme;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaDropdownWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        Wrap(
          spacing: PragmaSpacing.lg,
          runSpacing: PragmaSpacing.lg,
          children: <Widget>[
            SizedBox(
              width: 320,
              child: PragmaDropdownWidget<String>(
                label: 'Rol asignado',
                placeholder: 'Selecciona un rol',
                helperText: 'Define permisos en tableros y reportes',
                options: _dropdownOptions,
                value: _selectedRole,
                onChanged: (String? value) {
                  setState(() => _selectedRole = value);
                },
              ),
            ),
            SizedBox(
              width: 320,
              child: PragmaDropdownWidget<String>(
                label: 'Estado de revisión',
                placeholder: 'Selecciona un estado',
                errorText: 'Campo obligatorio',
                options: _dropdownOptions,
                value: null,
                onChanged: (String? value) {
                  setState(() => _selectedRole = value);
                },
              ),
            ),
            SizedBox(
              width: 320,
              child: PragmaDropdownWidget<String>(
                label: 'Revisor asignado',
                placeholder: 'No disponible',
                enabled: false,
                options: _dropdownOptions,
                value: null,
                onChanged: (_) {},
              ),
            ),
          ],
        ),
      ],
    );
  }
}

class _DropdownListPlayground extends StatefulWidget {
  const _DropdownListPlayground();

  @override
  State<_DropdownListPlayground> createState() =>
      _DropdownListPlaygroundState();
}

class _DropdownListPlaygroundState extends State<_DropdownListPlayground> {
  Set<String> _selectedValues = <String>{'ux', 'ios'};
  bool _showCheckbox = true;
  bool _showIcons = true;
  bool _showRemoveAction = true;

  void _handleSelectionChanged(List<String> values) {
    setState(() => _selectedValues = values.toSet());
  }

  void _handleRemove(String value) {
    setState(() => _selectedValues.remove(value));
  }

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('PragmaDropdownListWidget', style: textTheme.headlineSmall),
        const SizedBox(height: PragmaSpacing.md),
        Wrap(
          spacing: PragmaSpacing.lg,
          runSpacing: PragmaSpacing.lg,
          children: <Widget>[
            SizedBox(
              width: 360,
              child: PragmaDropdownListWidget<String>(
                label: 'Equipo colaborador',
                placeholder: 'Selecciona perfiles',
                options: _dropdownListOptions,
                initialSelectedValues: _selectedValues.toList(),
                showCheckbox: _showCheckbox,
                showOptionIcons: _showIcons,
                showRemoveAction: _showRemoveAction,
                onSelectionChanged: _handleSelectionChanged,
                onItemRemoved: _handleRemove,
              ),
            ),
            SizedBox(
              width: 360,
              child: PragmaDropdownListWidget<String>(
                label: 'Resumen de equipo',
                placeholder: 'Equipo sin asignar',
                options: _dropdownListOptions,
                initialSelectedValues: _selectedValues.toList(),
                showCheckbox: false,
                showOptionIcons: false,
                showRemoveAction: true,
                summaryBuilder: (
                  BuildContext context,
                  List<PragmaDropdownOption<String>> selected,
                ) {
                  return Text(
                    '${selected.length} perfiles seleccionados',
                    style: textTheme.bodyLarge,
                  );
                },
                optionBuilder: (
                  BuildContext context,
                  PragmaDropdownOption<String> option,
                  bool isSelected,
                ) {
                  final Color iconColor =
                      isSelected ? scheme.surface : scheme.primary;
                  return Row(
                    children: <Widget>[
                      Icon(
                        isSelected ? Icons.check_circle : Icons.circle_outlined,
                        size: 16,
                        color: iconColor,
                      ),
                      const SizedBox(width: PragmaSpacing.xs),
                      Text(option.label),
                    ],
                  );
                },
                onSelectionChanged: _handleSelectionChanged,
                onItemRemoved: _handleRemove,
              ),
            ),
          ],
        ),
        const SizedBox(height: PragmaSpacing.md),
        Wrap(
          spacing: PragmaSpacing.md,
          runSpacing: PragmaSpacing.md,
          children: <Widget>[
            SizedBox(
              width: 280,
              child: SwitchListTile.adaptive(
                contentPadding: EdgeInsets.zero,
                title: const Text('Mostrar checkbox'),
                value: _showCheckbox,
                onChanged: (bool value) =>
                    setState(() => _showCheckbox = value),
              ),
            ),
            SizedBox(
              width: 280,
              child: SwitchListTile.adaptive(
                contentPadding: EdgeInsets.zero,
                title: const Text('Mostrar iconos'),
                value: _showIcons,
                onChanged: (bool value) => setState(() => _showIcons = value),
              ),
            ),
            SizedBox(
              width: 280,
              child: SwitchListTile.adaptive(
                contentPadding: EdgeInsets.zero,
                title: const Text('Acción de eliminar'),
                value: _showRemoveAction,
                onChanged: (bool value) =>
                    setState(() => _showRemoveAction = value),
              ),
            ),
          ],
        ),
      ],
    );
  }
}

class _AvatarPlayground extends StatefulWidget {
  const _AvatarPlayground();

  @override
  State<_AvatarPlayground> createState() => _AvatarPlaygroundState();
}

class _AvatarPlaygroundState extends State<_AvatarPlayground> {
  static const double _minRadius = PragmaSpacing.sm;
  static const double _maxRadius = PragmaSpacing.xxl3;

  late final TextEditingController _initialsController;
  late final TextEditingController _imageUrlController;
  double _radius = PragmaSpacing.md;
  PragmaAvatarStyle _style = PragmaAvatarStyle.primary;
  bool _useImage = false;

  @override
  void initState() {
    super.initState();
    _initialsController = TextEditingController(text: 'PD');
    _imageUrlController = TextEditingController(
      text: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1',
    );
  }

  @override
  void dispose() {
    _initialsController.dispose();
    _imageUrlController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final Widget avatar = PragmaAvatarWidget(
      radius: _radius,
      initials: _useImage ? null : _initialsController.text,
      imageUrl: _useImage ? _imageUrlController.text : null,
      style: _style,
      tooltip: 'Avatar preview',
    );

    return PragmaCard.section(
      headline: 'PragmaAvatarWidget',
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Center(child: avatar),
          const SizedBox(height: PragmaSpacing.md),
          Text(
            'Radius ${_radius.toStringAsFixed(0)} px',
            style: theme.textTheme.labelLarge,
          ),
          Slider(
            value: _radius,
            min: _minRadius,
            max: _maxRadius,
            divisions: (_maxRadius - _minRadius).toInt(),
            onChanged: (double value) => setState(() => _radius = value),
          ),
          const SizedBox(height: PragmaSpacing.sm),
          TextField(
            controller: _initialsController,
            decoration: const InputDecoration(labelText: 'Initials'),
            onChanged: (_) {
              if (!_useImage) {
                setState(() {});
              }
            },
          ),
          const SizedBox(height: PragmaSpacing.sm),
          DropdownButtonFormField<PragmaAvatarStyle>(
            initialValue: _style,
            decoration: const InputDecoration(labelText: 'Style'),
            items: PragmaAvatarStyle.values.map((PragmaAvatarStyle style) {
              return DropdownMenuItem<PragmaAvatarStyle>(
                value: style,
                child: Text(style.name),
              );
            }).toList(),
            onChanged: (PragmaAvatarStyle? value) {
              if (value != null) {
                setState(() => _style = value);
              }
            },
          ),
          const SizedBox(height: PragmaSpacing.sm),
          SwitchListTile.adaptive(
            value: _useImage,
            contentPadding: EdgeInsets.zero,
            title: const Text('Use image URL'),
            onChanged: (bool value) => setState(() => _useImage = value),
          ),
          if (_useImage) ...<Widget>[
            const SizedBox(height: PragmaSpacing.xs),
            TextField(
              controller: _imageUrlController,
              decoration: const InputDecoration(labelText: 'Image URL'),
              onChanged: (_) => setState(() {}),
            ),
          ],
        ],
      ),
    );
  }
}

class _BreadcrumbPlayground extends StatefulWidget {
  const _BreadcrumbPlayground();

  @override
  State<_BreadcrumbPlayground> createState() => _BreadcrumbPlaygroundState();
}

class _BreadcrumbPlaygroundState extends State<_BreadcrumbPlayground> {
  PragmaBreadcrumbType _type = PragmaBreadcrumbType.standard;
  bool _disabled = false;
  int _activeIndex = 2;

  final List<String> _labels = <String>['Inicio', 'Componentes', 'Breadcrumb'];

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final List<PragmaBreadcrumbItem> items =
        List<PragmaBreadcrumbItem>.generate(
      _labels.length,
      (int index) {
        return PragmaBreadcrumbItem(
          label: _labels[index],
          isCurrent: index == _activeIndex,
          onTap: index == _activeIndex
              ? null
              : () {
                  final ScaffoldMessengerState messenger =
                      ScaffoldMessenger.of(context);
                  messenger.clearSnackBars();
                  messenger.showSnackBar(
                    SnackBar(content: Text('Navigate to ${_labels[index]}')),
                  );
                },
        );
      },
    );

    return PragmaCard.section(
      headline: 'PragmaBreadcrumbWidget',
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          PragmaBreadcrumbWidget(
            items: items,
            type: _type,
            disabled: _disabled,
          ),
          const SizedBox(height: PragmaSpacing.md),
          DropdownButtonFormField<PragmaBreadcrumbType>(
            initialValue: _type,
            decoration: const InputDecoration(labelText: 'Type'),
            items:
                PragmaBreadcrumbType.values.map((PragmaBreadcrumbType value) {
              return DropdownMenuItem<PragmaBreadcrumbType>(
                value: value,
                child: Text(value.name),
              );
            }).toList(),
            onChanged: (PragmaBreadcrumbType? value) {
              if (value != null) {
                setState(() => _type = value);
              }
            },
          ),
          const SizedBox(height: PragmaSpacing.sm),
          DropdownButtonFormField<int>(
            initialValue: _activeIndex,
            decoration: const InputDecoration(labelText: 'Active item'),
            items: List<DropdownMenuItem<int>>.generate(
              _labels.length,
              (int index) => DropdownMenuItem<int>(
                value: index,
                child: Text(_labels[index]),
              ),
            ),
            onChanged: (int? value) {
              if (value != null) {
                setState(() => _activeIndex = value);
              }
            },
          ),
          const SizedBox(height: PragmaSpacing.sm),
          SwitchListTile.adaptive(
            value: _disabled,
            contentPadding: EdgeInsets.zero,
            title: const Text('Disabled'),
            onChanged: (bool value) => setState(() => _disabled = value),
          ),
          const SizedBox(height: PragmaSpacing.sm),
          Text(
            'Active page: ${_labels[_activeIndex]}',
            style: theme.textTheme.labelLarge,
          ),
        ],
      ),
    );
  }
}

class _IconButtonPlayground extends StatefulWidget {
  const _IconButtonPlayground();

  @override
  State<_IconButtonPlayground> createState() => _IconButtonPlaygroundState();
}

class _IconButtonPlaygroundState extends State<_IconButtonPlayground> {
  PragmaIconButtonStyle _style = PragmaIconButtonStyle.filledLight;
  PragmaIconButtonSize _size = PragmaIconButtonSize.regular;
  bool _disabled = false;

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;

    return PragmaCard.section(
      headline: 'PragmaIconButtonWidget',
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            'Explora variantes y tamaños para superficies claras u oscuras.',
            style: textTheme.bodyMedium,
          ),
          const SizedBox(height: PragmaSpacing.sm),
          Wrap(
            spacing: PragmaSpacing.md,
            runSpacing: PragmaSpacing.sm,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: <Widget>[
              SizedBox(
                width: 220,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text('Style', style: textTheme.labelSmall),
                    DropdownButton<PragmaIconButtonStyle>(
                      isExpanded: true,
                      value: _style,
                      items: PragmaIconButtonStyle.values
                          .map((PragmaIconButtonStyle value) {
                        return DropdownMenuItem<PragmaIconButtonStyle>(
                          value: value,
                          child: Text(value.name),
                        );
                      }).toList(),
                      onChanged: (PragmaIconButtonStyle? value) {
                        if (value != null) {
                          setState(() => _style = value);
                        }
                      },
                    ),
                  ],
                ),
              ),
              SizedBox(
                width: 160,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text('Size', style: textTheme.labelSmall),
                    DropdownButton<PragmaIconButtonSize>(
                      isExpanded: true,
                      value: _size,
                      items: PragmaIconButtonSize.values
                          .map((PragmaIconButtonSize value) {
                        return DropdownMenuItem<PragmaIconButtonSize>(
                          value: value,
                          child: Text(value.name),
                        );
                      }).toList(),
                      onChanged: (PragmaIconButtonSize? value) {
                        if (value != null) {
                          setState(() => _size = value);
                        }
                      },
                    ),
                  ],
                ),
              ),
              Row(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  const Text('Disabled'),
                  Switch(
                    value: _disabled,
                    onChanged: (bool value) =>
                        setState(() => _disabled = value),
                  ),
                ],
              ),
            ],
          ),
          const SizedBox(height: PragmaSpacing.md),
          Wrap(
            spacing: PragmaSpacing.md,
            runSpacing: PragmaSpacing.md,
            children: <Widget>[
              PragmaIconButtonWidget(
                icon: Icons.add,
                tooltip: 'Añadir',
                style: _style,
                size: _size,
                onPressed: _disabled ? null : _noop,
              ),
              PragmaIconButtonWidget(
                icon: Icons.remove,
                tooltip: 'Quitar',
                style: PragmaIconButtonStyle.outlinedDark,
                size: _size,
                onPressed: _disabled ? null : _noop,
              ),
              PragmaIconButtonWidget(
                icon: Icons.lightbulb_outline,
                tooltip: 'Idea',
                style: PragmaIconButtonStyle.filledDark,
                size: PragmaIconButtonSize.regular,
                onPressed: _disabled ? null : _noop,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _ComponentDocCard extends StatelessWidget {
  const _ComponentDocCard({required this.component});

  final ModelPragmaComponent component;

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Card(
      margin: const EdgeInsets.only(bottom: PragmaSpacing.md),
      child: Padding(
        padding: const EdgeInsets.all(PragmaSpacing.lg),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              component.titleComponent,
              style: textTheme.titleLarge,
            ),
            const SizedBox(height: PragmaSpacing.xs),
            Text(component.description, style: textTheme.bodyMedium),
            if (component.useCases.isNotEmpty) ...<Widget>[
              const SizedBox(height: PragmaSpacing.md),
              Text('Casos de uso', style: textTheme.labelLarge),
              const SizedBox(height: PragmaSpacing.xs),
              Wrap(
                spacing: PragmaSpacing.sm,
                runSpacing: PragmaSpacing.xs,
                children: component.useCases.map((String useCase) {
                  return Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: PragmaSpacing.sm,
                      vertical: PragmaSpacing.xs,
                    ),
                    decoration: BoxDecoration(
                      color: scheme.surfaceContainerHighest,
                      borderRadius: BorderRadius.circular(99),
                    ),
                    child: Text(
                      useCase,
                      style: textTheme.bodySmall,
                    ),
                  );
                }).toList(),
              ),
            ],
            if (component.anatomy.isNotEmpty) ...<Widget>[
              const SizedBox(height: PragmaSpacing.md),
              Text('Anatomía', style: textTheme.labelLarge),
              const SizedBox(height: PragmaSpacing.xs),
              Column(
                children: component.anatomy.map((ModelAnatomyAttribute attr) {
                  final String percentage =
                      '${(attr.value * 100).toStringAsFixed(0)}%';
                  return ListTile(
                    contentPadding: EdgeInsets.zero,
                    dense: true,
                    leading: CircleAvatar(
                      radius: 18,
                      backgroundColor: scheme.primary.withValues(
                        alpha: PragmaOpacity.opacity30,
                      ),
                      child: Text(
                        percentage,
                        style: textTheme.labelSmall,
                      ),
                    ),
                    title: Text(attr.title, style: textTheme.bodyMedium),
                    subtitle: attr.description.isEmpty
                        ? null
                        : Text(attr.description, style: textTheme.bodySmall),
                  );
                }).toList(),
              ),
            ],
            if (component.urlImages.isNotEmpty) ...<Widget>[
              const SizedBox(height: PragmaSpacing.md),
              Text('Referencias visuales', style: textTheme.labelLarge),
              const SizedBox(height: PragmaSpacing.xs),
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: component.urlImages.map((String url) {
                  return Padding(
                    padding: const EdgeInsets.only(bottom: PragmaSpacing.xs),
                    child: SelectableText(
                      url,
                      style:
                          textTheme.bodySmall?.copyWith(color: scheme.primary),
                    ),
                  );
                }).toList(),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

class _ColorTokenRowPlayground extends StatefulWidget {
  const _ColorTokenRowPlayground();

  @override
  State<_ColorTokenRowPlayground> createState() =>
      _ColorTokenRowPlaygroundState();
}

class _LogoShowcase extends StatefulWidget {
  const _LogoShowcase();

  @override
  State<_LogoShowcase> createState() => _LogoShowcaseState();
}

class _LogoShowcaseState extends State<_LogoShowcase> {
  PragmaLogoVariant _variant = PragmaLogoVariant.wordmark;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

    return PragmaCard.section(
      headline: 'PragmaLogoWidget',
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            'Visualiza las variantes oficiales y cómo responden al tema. El widget detecta automáticamente si debe usar el asset claro u oscuro.',
            style: theme.textTheme.bodyMedium,
          ),
          const SizedBox(height: PragmaSpacing.md),
          PragmaLogoWidget(
            width: 220,
            variant: _variant,
            alignment: Alignment.centerLeft,
          ),
          const SizedBox(height: PragmaSpacing.md),
          SegmentedButton<PragmaLogoVariant>(
            segments: PragmaLogoVariant.values
                .map(
                  (PragmaLogoVariant variant) =>
                      ButtonSegment<PragmaLogoVariant>(
                    value: variant,
                    label: Text(_labelForVariant(variant)),
                  ),
                )
                .toList(),
            selected: <PragmaLogoVariant>{_variant},
            showSelectedIcon: false,
            onSelectionChanged: (Set<PragmaLogoVariant> selection) {
              setState(() => _variant = selection.first);
            },
          ),
        ],
      ),
    );
  }

  String _labelForVariant(PragmaLogoVariant variant) {
    switch (variant) {
      case PragmaLogoVariant.wordmark:
        return 'Wordmark';
      case PragmaLogoVariant.app:
        return 'App logo';
      case PragmaLogoVariant.isotypeCircle:
        return 'Isotipo circular';
      case PragmaLogoVariant.isotypeCircles:
        return 'Isotipo círculos';
    }
  }
}

class _ColorTokenRowPlaygroundState extends State<_ColorTokenRowPlayground> {
  late final List<ModelColorToken> _tokens;

  @override
  void initState() {
    super.initState();
    _tokens = <ModelColorToken>[
      ModelColorToken(label: 'Primary', color: '#6750A4'),
      ModelColorToken(label: 'Secondary', color: '#625B71'),
      ModelColorToken(label: 'Tertiary', color: '#7D5260'),
      ModelColorToken(label: 'Neutral', color: '#1C1B1F'),
    ];
  }

  void _updateToken(int index, ModelColorToken updated) {
    setState(() => _tokens[index] = updated);
  }

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Card(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(PragmaSpacing.md),
        side: BorderSide(color: scheme.outlineVariant),
      ),
      child: Padding(
        padding: const EdgeInsets.all(PragmaSpacing.lg),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Editor rápido de tokens', style: textTheme.titleLarge),
            const SizedBox(height: PragmaSpacing.xs),
            Text(
              'Modifica el valor HEX y observa cómo los previews reflejan el color.',
              style: textTheme.bodyMedium
                  ?.copyWith(color: scheme.onSurfaceVariant),
            ),
            const SizedBox(height: PragmaSpacing.md),
            ...List<Widget>.generate(_tokens.length, (int index) {
              return PragmaColorTokenRowWidget(
                key: ValueKey('color-token-row-$index'),
                token: _tokens[index],
                onChanged: (ModelColorToken updated) =>
                    _updateToken(index, updated),
              );
            }),
          ],
        ),
      ),
    );
  }
}

class _PaletteSectionView extends StatelessWidget {
  const _PaletteSectionView({required this.section});

  final _PaletteSection section;

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final Color onSurfaceVariant =
        Theme.of(context).colorScheme.onSurfaceVariant;

    return Padding(
      padding: const EdgeInsets.only(bottom: PragmaSpacing.xl),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(section.title, style: textTheme.titleLarge),
          if (section.caption != null) ...<Widget>[
            const SizedBox(height: PragmaSpacing.xs),
            Text(
              section.caption!,
              style: textTheme.bodyMedium?.copyWith(color: onSurfaceVariant),
            ),
          ],
          const SizedBox(height: PragmaSpacing.md),
          for (final _PaletteGroup group in section.groups)
            _PaletteGroupTable(group: group),
        ],
      ),
    );
  }
}

class _PaletteGroupTable extends StatelessWidget {
  const _PaletteGroupTable({required this.group});

  final _PaletteGroup group;

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;
    final TextTheme textTheme = Theme.of(context).textTheme;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text(group.title, style: textTheme.titleMedium),
        const SizedBox(height: PragmaSpacing.sm),
        Card(
          margin: const EdgeInsets.only(bottom: PragmaSpacing.lg),
          clipBehavior: Clip.antiAlias,
          child: SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            padding: PragmaSpacing.insetSymmetric(
              horizontal: PragmaSpacing.lg,
              vertical: PragmaSpacing.md,
            ),
            child: DataTable(
              columnSpacing: PragmaSpacing.xl,
              dataRowMinHeight: PragmaSpacing.xl,
              dataRowMaxHeight: PragmaSpacing.xl + (PragmaSpacing.xxxs * 2),
              headingRowColor: WidgetStatePropertyAll<Color>(
                scheme.surfaceContainerHighest,
              ),
              headingTextStyle: textTheme.labelMedium?.copyWith(
                color: scheme.onSurfaceVariant,
              ),
              columns: const <DataColumn>[
                DataColumn(label: Text('Preview')),
                DataColumn(label: Text('Color name')),
                DataColumn(label: Text('Hex')),
                DataColumn(label: Text('Design token')),
              ],
              rows: group.colors
                  .map(
                    (_PaletteColor color) => DataRow(
                      cells: <DataCell>[
                        DataCell(
                          _PaletteCell(
                            child: _ColorPreview(color: color.color),
                          ),
                        ),
                        DataCell(
                          _PaletteCell(child: Text(color.name)),
                        ),
                        DataCell(
                          _PaletteCell(child: Text(color.hex)),
                        ),
                        DataCell(
                          _PaletteCell(child: Text(color.token)),
                        ),
                      ],
                    ),
                  )
                  .toList(),
            ),
          ),
        ),
      ],
    );
  }
}

class _PaletteCell extends StatelessWidget {
  const _PaletteCell({required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: PragmaSpacing.xxxs),
      child: child,
    );
  }
}

class _ColorPreview extends StatelessWidget {
  const _ColorPreview({required this.color});

  final Color color;

  @override
  Widget build(BuildContext context) {
    return SizedBox.square(
      dimension: PragmaSpacing.xl,
      child: DecoratedBox(
        decoration: BoxDecoration(
          color: color,
          borderRadius: BorderRadius.circular(PragmaSpacing.sm),
        ),
      ),
    );
  }
}

class _PaletteSection {
  const _PaletteSection({
    required this.title,
    required this.groups,
    this.caption,
  });

  final String title;
  final String? caption;
  final List<_PaletteGroup> groups;
}

class _PaletteGroup {
  const _PaletteGroup({
    required this.title,
    required this.colors,
  });

  final String title;
  final List<_PaletteColor> colors;
}

class _PaletteColor {
  const _PaletteColor({
    required this.name,
    required this.color,
    required this.token,
  });

  final String name;
  final Color color;
  final String token;

  String get hex => _hexFromColor(color);
}

String _hexFromColor(Color color) {
  final String value =
      color.toARGB32().toRadixString(16).padLeft(8, '0').toUpperCase();
  return '#${value.substring(2)}';
}

const List<Map<String, dynamic>> _componentCatalog = <Map<String, dynamic>>[
  <String, dynamic>{
    'titleComponent': 'PragmaAccordionWidget',
    'description':
        'Panel expandible que gestiona su estado interno y expone callbacks.',
    'anatomy': <Map<String, dynamic>>[
      <String, dynamic>{
        'title': 'Header',
        'description': 'Dispara el toggle y aloja íconos auxiliares.',
        'value': 0.35,
      },
      <String, dynamic>{
        'title': 'Body',
        'description': 'Contenedor con contenido colapsable.',
        'value': 0.65,
      },
    ],
    'useCases': <String>['FAQs', 'Centros de ayuda', 'Documentación interna'],
    'urlImages': <String>[
      'https://cdn.pragma.co/components/accordion/cover.png',
    ],
  },
  <String, dynamic>{
    'titleComponent': 'PragmaScaleBox',
    'description':
        'Escala mockups de Figma al width disponible manteniendo el aspecto.',
    'anatomy': <Map<String, dynamic>>[
      <String, dynamic>{
        'title': 'Viewport wrapper',
        'description': 'Delimita el ancho máximo disponible.',
        'value': 0.4,
      },
      <String, dynamic>{
        'title': 'Mockup',
        'description': 'Contenido diseñado a un tamaño fijo.',
        'value': 0.6,
      },
    ],
    'useCases': <String>['Demos responsivas', 'QA visual'],
    'urlImages': <String>[
      'https://cdn.pragma.co/components/scalebox/cover.png',
    ],
  },
  <String, dynamic>{
    'titleComponent': 'PragmaBadgeWidget',
    'description':
        'Capsulas informativas con tonos brand/success/warning/info/neutral listos para superfícies claras u oscuras.',
    'anatomy': <Map<String, dynamic>>[
      <String, dynamic>{
        'title': 'Contenedor',
        'description': 'Capsula full radius con borde de 1dp.',
        'value': 0.4,
      },
      <String, dynamic>{
        'title': 'Ícono opcional',
        'description': 'Elemento de 16px que refuerza el estado.',
        'value': 0.15,
      },
      <String, dynamic>{
        'title': 'Label',
        'description': 'Texto bold 12px truncado a una línea.',
        'value': 0.45,
      },
    ],
    'useCases': <String>[
      'Etiquetas de estado',
      'Dashboards',
      'Metadatos en tarjetas',
    ],
    'urlImages': <String>[
      'https://cdn.pragma.co/components/badge/cover.png',
    ],
  },
  <String, dynamic>{
    'titleComponent': 'PragmaFilterWidget',
    'description':
        'Filtro mejorado para tablas que despliega un panel flotante multi-select, contador y tags persistentes.',
    'anatomy': <Map<String, dynamic>>[
      <String, dynamic>{
        'title': 'Cápsula',
        'description': 'Surface con contador e ícono para abrir el panel.',
        'value': 0.3,
      },
      <String, dynamic>{
        'title': 'Panel',
        'description':
            'Lista de checkboxes, helper text y acciones Filtrar/Limpiar.',
        'value': 0.45,
      },
      <String, dynamic>{
        'title': 'Tags activos',
        'description': 'Resumen de opciones aplicadas fuera de la tabla.',
        'value': 0.25,
      },
    ],
    'useCases': <String>[
      'Tablas extensas',
      'Dashboards de datos',
      'Listas jerarquizadas'
    ],
    'urlImages': <String>[
      'https://cdn.pragma.co/components/filter/cover.png',
    ],
  },
  <String, dynamic>{
    'titleComponent': 'PragmaPaginationWidget',
    'description':
        'Paginador con cápsula glow, flechas, páginas numeradas y dropdown "por página" sincronizado con el summary.',
    'anatomy': <Map<String, dynamic>>[
      <String, dynamic>{
        'title': 'Cápsula principal',
        'description': 'Surface light/dark con degradado, flechas y números.',
        'value': 0.4,
      },
      <String, dynamic>{
        'title': 'Dropdown por página',
        'description': 'Control comprimido para elegir 10/25/50/100 registros.',
        'value': 0.25,
      },
      <String, dynamic>{
        'title': 'Summary',
        'description': 'Etiqueta accesible con rango (1-25 de 240).',
        'value': 0.35,
      },
    ],
    'useCases': <String>['Tablas extensas', 'Listas de catálogo', 'Reportes'],
    'urlImages': <String>[
      'https://cdn.pragma.co/components/pagination/cover.png',
    ],
  },
  <String, dynamic>{
    'titleComponent': 'PragmaTooltipWidget',
    'description':
        'Tooltip con gradiente morado o surface clara, título opcional, ícono y botón interno más arrow en las cuatro direcciones.',
    'anatomy': <Map<String, dynamic>>[
      <String, dynamic>{
        'title': 'Cápsula',
        'description':
            'Surface 16dp con glow y borde para alojar el contenido.',
        'value': 0.4,
      },
      <String, dynamic>{
        'title': 'Contenido',
        'description': 'Ícono, título y copy principal.',
        'value': 0.35,
      },
      <String, dynamic>{
        'title': 'Botón interno',
        'description': 'Acción terciaria opcional.',
        'value': 0.15,
      },
      <String, dynamic>{
        'title': 'Arrow',
        'description': 'Triángulo que apunta al elemento asociado.',
        'value': 0.1,
      },
    ],
    'useCases': <String>[
      'Formularios densos',
      'Icon buttons sin label',
      'Atajos rápidos',
    ],
    'urlImages': <String>[
      'https://cdn.pragma.co/components/tooltip/cover.png',
    ],
  },
];

const List<PragmaDropdownOption<String>> _dropdownListOptions =
    <PragmaDropdownOption<String>>[
  PragmaDropdownOption<String>(
    label: 'UX Research',
    value: 'ux',
    icon: Icons.psychology_alt_outlined,
  ),
  PragmaDropdownOption<String>(
    label: 'Product Design',
    value: 'design',
    icon: Icons.design_services_outlined,
  ),
  PragmaDropdownOption<String>(
    label: 'Product Manager',
    value: 'pm',
    icon: Icons.view_kanban,
  ),
  PragmaDropdownOption<String>(
    label: 'Mobile iOS',
    value: 'ios',
    icon: Icons.phone_iphone,
  ),
  PragmaDropdownOption<String>(
    label: 'Mobile Android',
    value: 'android',
    icon: Icons.android,
  ),
];

const List<PragmaDropdownOption<String>> _dropdownOptions =
    <PragmaDropdownOption<String>>[
  PragmaDropdownOption<String>(
    label: 'Product Designer',
    value: 'ux',
  ),
  PragmaDropdownOption<String>(
    label: 'Product Manager',
    value: 'pm',
  ),
  PragmaDropdownOption<String>(
    label: 'iOS Engineer',
    value: 'ios',
  ),
  PragmaDropdownOption<String>(
    label: 'Android Engineer',
    value: 'android',
  ),
];

const List<String> _searchTopics = <String>[
  'Discovery Lab',
  'Growth Squad',
  'Research Guild',
  'Design System backlog',
  'Mobile Core initiatives',
  'Onboarding Web',
  'Analytics Squad',
  'QA Automation',
  'Payments Platform',
  'Content Studio',
];

const List<_PaletteSection> _paletteSections = <_PaletteSection>[
  _PaletteSection(
    title: 'Paleta primaria',
    caption:
        'Es la paleta que identifica nuestra marca. En web se utiliza aproximadamente el 60% del color en los diseños.',
    groups: <_PaletteGroup>[
      _PaletteGroup(
        title: 'Primary purple',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Primary-purple-50',
            color: PragmaColorTokens.primaryPurple50,
            token: r'$pds-color-primary-purple-50',
          ),
          _PaletteColor(
            name: 'Primary-purple-300',
            color: PragmaColorTokens.primaryPurple300,
            token: r'$pds-color-primary-purple-300',
          ),
          _PaletteColor(
            name: 'Primary-purple-500',
            color: PragmaColorTokens.primaryPurple500,
            token: r'$pds-color-primary-purple-500',
          ),
          _PaletteColor(
            name: 'Primary-purple-700',
            color: PragmaColorTokens.primaryPurple700,
            token: r'$pds-color-primary-purple-700',
          ),
          _PaletteColor(
            name: 'Primary-purple-900',
            color: PragmaColorTokens.primaryPurple900,
            token: r'$pds-color-primary-purple-900',
          ),
        ],
      ),
      _PaletteGroup(
        title: 'Primary gray',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Primary-gray-50',
            color: PragmaColorTokens.primaryGray50,
            token: r'$pds-color-primary-gray-50',
          ),
          _PaletteColor(
            name: 'Primary-gray-300',
            color: PragmaColorTokens.primaryGray300,
            token: r'$pds-color-primary-gray-300',
          ),
          _PaletteColor(
            name: 'Primary-gray-500',
            color: PragmaColorTokens.primaryGray500,
            token: r'$pds-color-primary-gray-500',
          ),
          _PaletteColor(
            name: 'Primary-gray-700',
            color: PragmaColorTokens.primaryGray700,
            token: r'$pds-color-primary-gray-700',
          ),
          _PaletteColor(
            name: 'Primary-gray-900',
            color: PragmaColorTokens.primaryGray900,
            token: r'$pds-color-primary-gray-900',
          ),
        ],
      ),
      _PaletteGroup(
        title: 'Primary indigo',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Primary-indigo-50',
            color: PragmaColorTokens.primaryIndigo50,
            token: r'$pds-color-primary-indigo-50',
          ),
          _PaletteColor(
            name: 'Primary-indigo-300',
            color: PragmaColorTokens.primaryIndigo300,
            token: r'$pds-color-primary-indigo-300',
          ),
          _PaletteColor(
            name: 'Primary-indigo-500',
            color: PragmaColorTokens.primaryIndigo500,
            token: r'$pds-color-primary-indigo-500',
          ),
          _PaletteColor(
            name: 'Primary-indigo-700',
            color: PragmaColorTokens.primaryIndigo700,
            token: r'$pds-color-primary-indigo-700',
          ),
          _PaletteColor(
            name: 'Primary-indigo-900',
            color: PragmaColorTokens.primaryIndigo900,
            token: r'$pds-color-primary-indigo-900',
          ),
        ],
      ),
    ],
  ),
  _PaletteSection(
    title: 'Paleta secundaria',
    caption:
        'Estos tonos aportan contraste y presencia limpia/legible. En web se recomienda usar alrededor del 30% del canvas.',
    groups: <_PaletteGroup>[
      _PaletteGroup(
        title: 'Secondary fuchsia',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Secondary-fuchsia-50',
            color: PragmaColorTokens.secondaryFuchsia50,
            token: r'$pds-color-secondary-fuchsia-50',
          ),
          _PaletteColor(
            name: 'Secondary-fuchsia-300',
            color: PragmaColorTokens.secondaryFuchsia300,
            token: r'$pds-color-secondary-fuchsia-300',
          ),
          _PaletteColor(
            name: 'Secondary-fuchsia-500',
            color: PragmaColorTokens.secondaryFuchsia500,
            token: r'$pds-color-secondary-fuchsia-500',
          ),
          _PaletteColor(
            name: 'Secondary-fuchsia-700',
            color: PragmaColorTokens.secondaryFuchsia700,
            token: r'$pds-color-secondary-fuchsia-700',
          ),
          _PaletteColor(
            name: 'Secondary-fuchsia-900',
            color: PragmaColorTokens.secondaryFuchsia900,
            token: r'$pds-color-secondary-fuchsia-900',
          ),
        ],
      ),
      _PaletteGroup(
        title: 'Secondary purple',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Secondary-purple-50',
            color: PragmaColorTokens.secondaryPurple50,
            token: r'$pds-color-secondary-purple-50',
          ),
          _PaletteColor(
            name: 'Secondary-purple-300',
            color: PragmaColorTokens.secondaryPurple300,
            token: r'$pds-color-secondary-purple-300',
          ),
          _PaletteColor(
            name: 'Secondary-purple-500',
            color: PragmaColorTokens.secondaryPurple500,
            token: r'$pds-color-secondary-purple-500',
          ),
          _PaletteColor(
            name: 'Secondary-purple-700',
            color: PragmaColorTokens.secondaryPurple700,
            token: r'$pds-color-secondary-purple-700',
          ),
          _PaletteColor(
            name: 'Secondary-purple-900',
            color: PragmaColorTokens.secondaryPurple900,
            token: r'$pds-color-secondary-purple-900',
          ),
        ],
      ),
    ],
  ),
  _PaletteSection(
    title: 'Paleta terciaria',
    caption:
        'Paleta de acento para resaltar información importante sin exceder el 10% del canvas.',
    groups: <_PaletteGroup>[
      _PaletteGroup(
        title: 'Tertiary yellow',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Tertiary-yellow-50',
            color: PragmaColorTokens.tertiaryYellow50,
            token: r'$pds-color-tertiary-yellow-50',
          ),
          _PaletteColor(
            name: 'Tertiary-yellow-300',
            color: PragmaColorTokens.tertiaryYellow300,
            token: r'$pds-color-tertiary-yellow-300',
          ),
          _PaletteColor(
            name: 'Tertiary-yellow-500',
            color: PragmaColorTokens.tertiaryYellow500,
            token: r'$pds-color-tertiary-yellow-500',
          ),
          _PaletteColor(
            name: 'Tertiary-yellow-700',
            color: PragmaColorTokens.tertiaryYellow700,
            token: r'$pds-color-tertiary-yellow-700',
          ),
          _PaletteColor(
            name: 'Tertiary-yellow-900',
            color: PragmaColorTokens.tertiaryYellow900,
            token: r'$pds-color-tertiary-yellow-900',
          ),
        ],
      ),
    ],
  ),
  _PaletteSection(
    title: 'Escala de grises',
    caption: 'Soporte secundario para fondos, textos y componentes modales.',
    groups: <_PaletteGroup>[
      _PaletteGroup(
        title: 'Gray scale',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Gray-50',
            color: PragmaColorTokens.neutralGray50,
            token: r'$pds-color-gray-50',
          ),
          _PaletteColor(
            name: 'Gray-100',
            color: PragmaColorTokens.neutralGray100,
            token: r'$pds-color-gray-100',
          ),
          _PaletteColor(
            name: 'Gray-200',
            color: PragmaColorTokens.neutralGray200,
            token: r'$pds-color-gray-200',
          ),
          _PaletteColor(
            name: 'Gray-300',
            color: PragmaColorTokens.neutralGray300,
            token: r'$pds-color-gray-300',
          ),
          _PaletteColor(
            name: 'Gray-400',
            color: PragmaColorTokens.neutralGray400,
            token: r'$pds-color-gray-400',
          ),
          _PaletteColor(
            name: 'Gray-500',
            color: PragmaColorTokens.neutralGray500,
            token: r'$pds-color-gray-500',
          ),
          _PaletteColor(
            name: 'Gray-600',
            color: PragmaColorTokens.neutralGray600,
            token: r'$pds-color-gray-600',
          ),
          _PaletteColor(
            name: 'Gray-700',
            color: PragmaColorTokens.neutralGray700,
            token: r'$pds-color-gray-700',
          ),
          _PaletteColor(
            name: 'Gray-800',
            color: PragmaColorTokens.neutralGray800,
            token: r'$pds-color-gray-800',
          ),
          _PaletteColor(
            name: 'Gray-900',
            color: PragmaColorTokens.neutralGray900,
            token: r'$pds-color-gray-900',
          ),
        ],
      ),
    ],
  ),
  _PaletteSection(
    title: 'Colores de acciones exitosas',
    caption:
        'Se utilizan como feedback visual para confirmar acciones satisfactorias.',
    groups: <_PaletteGroup>[
      _PaletteGroup(
        title: 'Success',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Success-50',
            color: PragmaColorTokens.success50,
            token: r'$pds-color-success-50',
          ),
          _PaletteColor(
            name: 'Success-300',
            color: PragmaColorTokens.success300,
            token: r'$pds-color-success-300',
          ),
          _PaletteColor(
            name: 'Success-500',
            color: PragmaColorTokens.success500,
            token: r'$pds-color-success-500',
          ),
          _PaletteColor(
            name: 'Success-700',
            color: PragmaColorTokens.success700,
            token: r'$pds-color-success-700',
          ),
          _PaletteColor(
            name: 'Success-900',
            color: PragmaColorTokens.success900,
            token: r'$pds-color-success-900',
          ),
        ],
      ),
    ],
  ),
  _PaletteSection(
    title: 'Colores de advertencia',
    caption:
        'Usados para alertar y preparar al usuario ante acciones sensibles.',
    groups: <_PaletteGroup>[
      _PaletteGroup(
        title: 'Warning',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Warning-50',
            color: PragmaColorTokens.warning50,
            token: r'$pds-color-warning-50',
          ),
          _PaletteColor(
            name: 'Warning-300',
            color: PragmaColorTokens.warning300,
            token: r'$pds-color-warning-300',
          ),
          _PaletteColor(
            name: 'Warning-500',
            color: PragmaColorTokens.warning500,
            token: r'$pds-color-warning-500',
          ),
          _PaletteColor(
            name: 'Warning-700',
            color: PragmaColorTokens.warning700,
            token: r'$pds-color-warning-700',
          ),
          _PaletteColor(
            name: 'Warning-900',
            color: PragmaColorTokens.warning900,
            token: r'$pds-color-warning-900',
          ),
        ],
      ),
    ],
  ),
  _PaletteSection(
    title: 'Colores de error',
    caption:
        'Acompañan mensajes críticos y ayudan a comunicar estados de fallo.',
    groups: <_PaletteGroup>[
      _PaletteGroup(
        title: 'Error',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'Error-50',
            color: PragmaColorTokens.error50,
            token: r'$pds-color-error-50',
          ),
          _PaletteColor(
            name: 'Error-300',
            color: PragmaColorTokens.error300,
            token: r'$pds-color-error-300',
          ),
          _PaletteColor(
            name: 'Error-500',
            color: PragmaColorTokens.error500,
            token: r'$pds-color-error-500',
          ),
          _PaletteColor(
            name: 'Error-700',
            color: PragmaColorTokens.error700,
            token: r'$pds-color-error-700',
          ),
          _PaletteColor(
            name: 'Error-900',
            color: PragmaColorTokens.error900,
            token: r'$pds-color-error-900',
          ),
        ],
      ),
    ],
  ),
  _PaletteSection(
    title: 'Basic shades',
    caption: 'Tonos esenciales para mantener contraste extremo.',
    groups: <_PaletteGroup>[
      _PaletteGroup(
        title: 'Neutros absolutos',
        colors: <_PaletteColor>[
          _PaletteColor(
            name: 'White-50',
            color: PragmaColorTokens.basicWhite50,
            token: r'$pds-color-white-50',
          ),
          _PaletteColor(
            name: 'Black-300',
            color: PragmaColorTokens.basicBlack300,
            token: r'$pds-color-black-300',
          ),
        ],
      ),
    ],
  ),
];
1
likes
160
points
419
downloads

Publisher

verified publisherpragma.co

Weekly Downloads

Flutter library that gathers Pragma's design tokens, themes, and base components for mobile apps.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, google_fonts, intl, meta

More

Packages that depend on pragma_design_system