flutter_json_render 0.4.1 copy "flutter_json_render: ^0.4.1" to clipboard
flutter_json_render: ^0.4.1 copied to clipboard

Guardrailed JSON-to-Widget renderer for Flutter and Dart.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_json_render/flutter_json_render.dart';

const String _initialStylePresetId = String.fromEnvironment(
  'STYLE_PRESET',
  defaultValue: 'clean',
);
const String _initialScenarioId = String.fromEnvironment(
  'SCENARIO',
  defaultValue: 'counter',
);
const bool _autoRunStream = bool.fromEnvironment(
  'AUTO_RUN_STREAM',
  defaultValue: false,
);
const bool _captureMode = bool.fromEnvironment(
  'CAPTURE_MODE',
  defaultValue: false,
);
const int _streamStepDelayMs = int.fromEnvironment(
  'STREAM_STEP_DELAY_MS',
  defaultValue: 500,
);
const int _autoRunDelayMs = int.fromEnvironment(
  'AUTO_RUN_DELAY_MS',
  defaultValue: 700,
);
const String _customStyleJson = String.fromEnvironment(
  'CUSTOM_STYLE_JSON',
  defaultValue: '',
);

void main() {
  runApp(const ShowcaseApp());
}

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

  @override
  State<ShowcaseApp> createState() => _ShowcaseAppState();
}

class _ShowcaseAppState extends State<ShowcaseApp> {
  final List<String> _startupMessages = <String>[];

  late final List<ShowcaseVisualStyle> _styles = _buildStyles();
  late final JsonRegistry _registry = _buildRegistry();
  late final List<ShowcaseCase> _cases = _buildCases();

  late ShowcaseVisualStyle _selectedStyle = _findStyle(_initialStylePresetId);
  late ShowcaseCase _selectedCase = _findCase(_initialScenarioId);
  late JsonRenderSpec _activeSpec = _selectedCase.spec;

  final JsonSpecStreamCompiler _streamCompiler = JsonSpecStreamCompiler();

  Map<String, dynamic> _latestState = <String, dynamic>{};
  final List<String> _eventLog = <String>[];
  bool _isStreaming = false;

  JsonCatalog get _catalog => _buildCatalog();

  @override
  void initState() {
    super.initState();
    _latestState = _activeSpec.state;
    _eventLog
      ..add('[system] Scenario: ${_selectedCase.id}')
      ..add('[system] Style: ${_selectedStyle.id}')
      ..addAll(_startupMessages);

    if (_autoRunStream && _selectedCase.streamLines.isNotEmpty) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        Future<void>.delayed(
          Duration(milliseconds: _autoRunDelayMs < 0 ? 0 : _autoRunDelayMs),
          () {
            if (!mounted || _isStreaming) {
              return;
            }
            _runStreamSimulation();
          },
        );
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_captureMode) {
      return MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'flutter_json_render Showcase',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(
            seedColor: _selectedStyle.seedColor,
          ),
          useMaterial3: true,
        ),
        home: Scaffold(body: _buildPreviewPanel()),
      );
    }

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'flutter_json_render Showcase',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: _selectedStyle.seedColor),
        useMaterial3: true,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('flutter_json_render Showcase'),
          actions: <Widget>[
            TextButton.icon(
              onPressed: _resetCase,
              icon: const Icon(Icons.refresh),
              label: const Text('Reset'),
            ),
            const SizedBox(width: 8),
          ],
        ),
        body: LayoutBuilder(
          builder: (context, constraints) {
            final isWide = constraints.maxWidth >= 1100;
            if (isWide) {
              return Row(
                children: <Widget>[
                  SizedBox(width: 420, child: _buildControlPanel()),
                  const VerticalDivider(width: 1),
                  Expanded(child: _buildPreviewPanel()),
                ],
              );
            }

            return Column(
              children: <Widget>[
                SizedBox(height: 420, child: _buildControlPanel()),
                const Divider(height: 1),
                Expanded(child: _buildPreviewPanel()),
              ],
            );
          },
        ),
      ),
    );
  }

  Widget _buildControlPanel() {
    final stateText = const JsonEncoder.withIndent('  ').convert(_latestState);
    final specJson = <String, dynamic>{
      ..._activeSpec.toJson(),
      'style': _selectedStyle.id,
    };
    final specText = const JsonEncoder.withIndent('  ').convert(specJson);
    final promptText = _catalog.prompt(
      options: JsonPromptOptions(
        includeProps: true,
        includeExamples: false,
        includeActions: true,
        includeStyles: true,
        selectedStyleId: _selectedStyle.id,
      ),
    );

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
          child: DropdownButtonFormField<ShowcaseCase>(
            value: _selectedCase,
            decoration: const InputDecoration(
              labelText: 'Scenario',
              border: OutlineInputBorder(),
            ),
            items: _cases
                .map(
                  (entry) => DropdownMenuItem<ShowcaseCase>(
                    value: entry,
                    child: Text(entry.title),
                  ),
                )
                .toList(growable: false),
            onChanged: (next) {
              if (next == null) return;
              setState(() {
                _selectedCase = next;
                _activeSpec = _selectedCase.spec;
                _latestState = _activeSpec.state;
                _eventLog.clear();
                _isStreaming = false;
              });
            },
          ),
        ),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Text(_selectedCase.description),
        ),
        Padding(
          padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
          child: DropdownButtonFormField<ShowcaseVisualStyle>(
            value: _selectedStyle,
            decoration: const InputDecoration(
              labelText: 'Style Preset',
              border: OutlineInputBorder(),
            ),
            items: _styles
                .map(
                  (entry) => DropdownMenuItem<ShowcaseVisualStyle>(
                    value: entry,
                    child: Text('${entry.name} (${entry.id})'),
                  ),
                )
                .toList(growable: false),
            onChanged: (next) {
              if (next == null) return;
              setState(() {
                _selectedStyle = next;
                _eventLog.add('[style] ${_selectedStyle.id}');
              });
            },
          ),
        ),
        const SizedBox(height: 8),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Wrap(
            spacing: 8,
            runSpacing: 8,
            children: <Widget>[
              FilledButton.tonalIcon(
                onPressed: _resetCase,
                icon: const Icon(Icons.restart_alt),
                label: const Text('Reset Scenario'),
              ),
              FilledButton.icon(
                onPressed: _selectedCase.streamLines.isEmpty || _isStreaming
                    ? null
                    : _runStreamSimulation,
                icon: const Icon(Icons.play_arrow),
                label: const Text('Run Stream'),
              ),
              OutlinedButton.icon(
                onPressed: _showAddCustomStyleDialog,
                icon: const Icon(Icons.palette_outlined),
                label: const Text('Add Custom Style'),
              ),
            ],
          ),
        ),
        const SizedBox(height: 8),
        Expanded(
          child: DefaultTabController(
            length: 4,
            child: Column(
              children: <Widget>[
                const TabBar(
                  tabs: <Tab>[
                    Tab(text: 'State'),
                    Tab(text: 'Spec'),
                    Tab(text: 'Prompt'),
                    Tab(text: 'Log'),
                  ],
                ),
                Expanded(
                  child: TabBarView(
                    children: <Widget>[
                      _textPane(stateText),
                      _textPane(specText),
                      _textPane(promptText),
                      _textPane(
                        _eventLog.isEmpty
                            ? 'No events yet.'
                            : _eventLog.join('\n'),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildPreviewPanel() {
    final style = _selectedStyle;
    return ColoredBox(
      color: style.previewBackground,
      child: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 900),
          child: Card(
            elevation: 0,
            margin: const EdgeInsets.all(16),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(20),
              side: BorderSide(color: style.panelBorder),
            ),
            color: style.panelBackground,
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: SingleChildScrollView(
                child: JsonRenderer(
                  spec: _activeSpec,
                  registry: _registry,
                  styleId: _selectedStyle.id,
                  onStateChanged: (state) {
                    setState(() {
                      _latestState = state;
                    });
                  },
                  onError: (error, stackTrace, context) {
                    setState(() {
                      _eventLog.add('[error][$context] $error');
                      _eventLog.add('[error][stack] $stackTrace');
                    });
                  },
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _textPane(String content) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(12),
      child: SelectableText(
        content,
        style: const TextStyle(fontFamily: 'monospace', height: 1.4),
      ),
    );
  }

  void _resetCase() {
    setState(() {
      _isStreaming = false;
      _activeSpec = _selectedCase.spec;
      _latestState = _activeSpec.state;
      _eventLog
        ..clear()
        ..add('[system] Scenario reset: ${_selectedCase.title}')
        ..add('[system] Style: ${_selectedStyle.id}');
    });
  }

  Future<void> _runStreamSimulation() async {
    if (_selectedCase.streamLines.isEmpty) {
      return;
    }

    setState(() {
      _isStreaming = true;
      _eventLog.add('[stream] Starting JSONL patch stream...');
    });

    _streamCompiler.reset(initialSpec: _selectedCase.spec);

    final delayMs = _streamStepDelayMs < 16 ? 16 : _streamStepDelayMs;
    for (final line in _selectedCase.streamLines) {
      if (!mounted) return;
      await Future<void>.delayed(Duration(milliseconds: delayMs));
      try {
        final pushed = _streamCompiler.push('$line\n');
        if (!mounted) return;
        setState(() {
          if (pushed.result != null) {
            _activeSpec = pushed.result!;
            _latestState = _activeSpec.state;
          }
          _eventLog.add('[stream] $line');
        });
      } catch (error) {
        setState(() {
          _eventLog.add('[stream][error] $error');
        });
      }
    }

    setState(() {
      _isStreaming = false;
      _eventLog.add('[stream] Completed.');
    });
  }

  Future<void> _showAddCustomStyleDialog() async {
    final controller = TextEditingController(text: _customStyleTemplate());
    final theme = Theme.of(context);

    final raw = await showDialog<String>(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Add Custom Style'),
          content: SizedBox(
            width: 520,
            child: TextField(
              controller: controller,
              minLines: 12,
              maxLines: 24,
              style: const TextStyle(fontFamily: 'monospace'),
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                hintText: 'Paste style JSON',
              ),
            ),
          ),
          actions: <Widget>[
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Cancel'),
            ),
            FilledButton(
              onPressed: () => Navigator.of(context).pop(controller.text),
              child: const Text('Apply'),
            ),
          ],
        );
      },
    );

    controller.dispose();
    if (raw == null) {
      return;
    }

    try {
      final created = _parseCustomStyle(raw);
      setState(() {
        final existing = _styles.indexWhere((entry) => entry.id == created.id);
        if (existing >= 0) {
          _styles[existing] = created;
        } else {
          _styles.add(created);
        }
        _selectedStyle = created;
        _eventLog.add('[style] Applied custom style "${created.id}".');
      });
    } on FormatException catch (error) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Invalid style JSON: ${error.message}'),
          backgroundColor: theme.colorScheme.errorContainer,
        ),
      );
    }
  }

  ShowcaseCase _findCase(String caseId) {
    for (final value in _cases) {
      if (value.id == caseId) {
        return value;
      }
    }
    return _cases.first;
  }

  ShowcaseVisualStyle _findStyle(String styleId) {
    for (final style in _styles) {
      if (style.id == styleId) {
        return style;
      }
    }
    return _styles.first;
  }

  List<ShowcaseVisualStyle> _buildStyles() {
    final styles = List<ShowcaseVisualStyle>.of(kShowcaseStyles);
    if (_customStyleJson.trim().isEmpty) {
      return styles;
    }

    try {
      final custom = _parseCustomStyle(_customStyleJson, knownStyles: styles);
      final existing = styles.indexWhere((entry) => entry.id == custom.id);
      if (existing >= 0) {
        styles[existing] = custom;
      } else {
        styles.add(custom);
      }
      _startupMessages.add(
        '[style] Loaded custom style from CUSTOM_STYLE_JSON: ${custom.id}',
      );
    } on FormatException catch (error) {
      _startupMessages.add('[style][error] ${error.message}');
    } catch (error) {
      _startupMessages.add(
        '[style][error] Failed to parse CUSTOM_STYLE_JSON: $error',
      );
    }
    return styles;
  }

  ShowcaseVisualStyle _parseCustomStyle(
    String raw, {
    List<ShowcaseVisualStyle>? knownStyles,
  }) {
    dynamic decoded;
    try {
      decoded = jsonDecode(raw);
    } catch (_) {
      throw const FormatException('Value must be valid JSON.');
    }

    if (decoded is! Map) {
      throw const FormatException('Custom style JSON must be an object.');
    }

    final map = Map<String, dynamic>.from(decoded);
    final id = map['id']?.toString().trim() ?? '';
    if (id.isEmpty) {
      throw const FormatException('Field "id" is required.');
    }

    final baseId = map['base']?.toString().trim();
    final baseStyle = _resolveBaseStyle(baseId, knownStyles: knownStyles);

    final styleDefinition = JsonStyleDefinition.fromJson(map);
    return ShowcaseVisualStyle.fromDefinition(
      id: id,
      base: baseStyle,
      style: styleDefinition,
    );
  }

  ShowcaseVisualStyle _resolveBaseStyle(
    String? baseId, {
    List<ShowcaseVisualStyle>? knownStyles,
  }) {
    final basePool = knownStyles ?? _styles;
    if (baseId == null || baseId.isEmpty) {
      return kShowcaseStyles.first;
    }
    for (final style in basePool) {
      if (style.id == baseId) {
        return style;
      }
    }
    for (final style in kShowcaseStyles) {
      if (style.id == baseId) {
        return style;
      }
    }
    throw FormatException('Unknown base style "$baseId".');
  }

  String _customStyleTemplate() {
    return const JsonEncoder.withIndent('  ').convert(<String, dynamic>{
      'id': 'aurora',
      'base': 'midnight',
      'displayName': 'Aurora',
      'description': 'Deep blue surface with bright cyan accents.',
      'guidance': 'Use high contrast and cool accent colors.',
      'tokens': <String, dynamic>{
        'seedColor': '#0EA5E9',
        'previewBackground': '#020B1A',
        'panelBackground': '#0B1220',
        'panelBorder': '#123047',
        'textPrimary': '#E0F2FE',
        'accent': '#22D3EE',
        'trackBackground': '#17314A',
        'neutralChip': <String, dynamic>{
          'background': '#102338',
          'border': '#1E3A5F',
          'foreground': '#D6EFFF',
        },
        'successChip': <String, dynamic>{
          'background': '#063B2E',
          'border': '#0F766E',
          'foreground': '#99F6E4',
        },
        'warningChip': <String, dynamic>{
          'background': '#4A2E05',
          'border': '#A16207',
          'foreground': '#FDE68A',
        },
        'dangerChip': <String, dynamic>{
          'background': '#4A0D1B',
          'border': '#BE123C',
          'foreground': '#FECDD3',
        },
      },
    });
  }

  JsonCatalog _buildCatalog() {
    return JsonCatalog(
      components: <String, JsonComponentDefinition>{
        ...standardComponentDefinitions,
        'Panel': const JsonComponentDefinition(
          description: 'Card-like visual container.',
          props: <String, JsonPropDefinition>{
            'title': JsonPropDefinition(type: 'string', required: true),
          },
        ),
        'StatusChip': const JsonComponentDefinition(
          description: 'Compact status pill with color variant.',
          props: <String, JsonPropDefinition>{
            'label': JsonPropDefinition(type: 'string', required: true),
            'variant': JsonPropDefinition(
              type: 'string',
              enumValues: <String>['neutral', 'success', 'warning', 'danger'],
            ),
          },
        ),
        'ProgressBar': const JsonComponentDefinition(
          description: 'Simple horizontal progress bar from 0 to 1.',
          props: <String, JsonPropDefinition>{
            'value': JsonPropDefinition(type: 'number', required: true),
            'color': JsonPropDefinition(type: 'string'),
          },
        ),
      },
      styles: {
        for (final style in _styles) style.id: style.toStyleDefinition(),
      },
      actions: <String, JsonActionDefinition>{
        ...standardActionDefinitions,
        'increment': const JsonActionDefinition(
          description: 'Increase count by 1.',
        ),
        'decrement': const JsonActionDefinition(
          description: 'Decrease count by 1.',
        ),
        'toggle_hint': const JsonActionDefinition(
          description: 'Toggle hint visibility.',
        ),
        'add_item': const JsonActionDefinition(
          description: 'Add random todo item.',
        ),
        'toggle_item': const JsonActionDefinition(
          description: 'Toggle done state for item index.',
          params: <String, JsonPropDefinition>{
            'index': JsonPropDefinition(type: 'number', required: true),
          },
        ),
        'remove_item': const JsonActionDefinition(
          description: 'Remove item by index.',
          params: <String, JsonPropDefinition>{
            'index': JsonPropDefinition(type: 'number', required: true),
          },
        ),
        'cycle_growth': const JsonActionDefinition(
          description: 'Rotate growth values between positive and negative.',
        ),
        'refresh_metrics': const JsonActionDefinition(
          description: 'Simulate async server refresh of dashboard metrics.',
        ),
      },
    );
  }

  JsonRegistry _buildRegistry() {
    final random = Random();

    return defineRegistry(
      components: <String, JsonComponentBuilder>{
        ...standardComponentBuilders(),
        'Panel': (ctx) {
          final style = _selectedStyle;
          return Container(
            padding: const EdgeInsets.all(16),
            margin: const EdgeInsets.only(bottom: 12),
            decoration: BoxDecoration(
              color: style.panelBackground,
              borderRadius: BorderRadius.circular(14),
              border: Border.all(color: style.panelBorder),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                Text(
                  ctx.props['title']?.toString() ?? 'Panel',
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.w700,
                    color: style.textPrimary,
                  ),
                ),
                if (ctx.children.isNotEmpty) const SizedBox(height: 12),
                ...ctx.children,
              ],
            ),
          );
        },
        'StatusChip': (ctx) {
          final style = _selectedStyle;
          final variant = ctx.props['variant']?.toString() ?? 'neutral';
          final pair = _chipStyle(style, variant);
          return Container(
            padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
            decoration: BoxDecoration(
              color: pair.background,
              borderRadius: BorderRadius.circular(999),
              border: Border.all(color: pair.border),
            ),
            child: Text(
              ctx.props['label']?.toString() ?? '',
              style: TextStyle(
                color: pair.foreground,
                fontSize: 12,
                fontWeight: FontWeight.w600,
              ),
            ),
          );
        },
        'ProgressBar': (ctx) {
          final style = _selectedStyle;
          final valueRaw = ctx.props['value'];
          final value = (valueRaw is num ? valueRaw.toDouble() : 0.0).clamp(
            0.0,
            1.0,
          );
          final colorHex = ctx.props['color']?.toString();
          final color = _safeColor(colorHex) ?? style.accent;

          return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              ClipRRect(
                borderRadius: BorderRadius.circular(999),
                child: LinearProgressIndicator(
                  value: value,
                  minHeight: 12,
                  backgroundColor: style.trackBackground,
                  valueColor: AlwaysStoppedAnimation<Color>(color),
                ),
              ),
              const SizedBox(height: 6),
              Text('${(value * 100).toStringAsFixed(0)}%'),
            ],
          );
        },
      },
      actions: <String, JsonActionHandler>{
        'noop': (_) {},
        'increment': (ctx) {
          ctx.setStateModel((prev) {
            final next = <String, dynamic>{...prev};
            next['count'] = ((prev['count'] as num?) ?? 0) + 1;
            return next;
          });
          setState(() {
            _eventLog.add('[action] increment');
          });
        },
        'decrement': (ctx) {
          ctx.setStateModel((prev) {
            final next = <String, dynamic>{...prev};
            next['count'] = ((prev['count'] as num?) ?? 0) - 1;
            return next;
          });
          setState(() {
            _eventLog.add('[action] decrement');
          });
        },
        'toggle_hint': (ctx) {
          ctx.setStateModel((prev) {
            final next = <String, dynamic>{...prev};
            next['showHint'] = !((prev['showHint'] as bool?) ?? false);
            return next;
          });
          setState(() {
            _eventLog.add('[action] toggle_hint');
          });
        },
        'add_item': (ctx) {
          ctx.setStateModel((prev) {
            final next = <String, dynamic>{...prev};
            final items = ((prev['items'] as List?) ?? <dynamic>[])
                .map((entry) => Map<String, dynamic>.from(entry as Map))
                .toList(growable: true);
            items.add(<String, dynamic>{
              'name': 'Task #${100 + random.nextInt(900)}',
              'done': false,
            });
            next['items'] = items;
            return next;
          });
          setState(() {
            _eventLog.add('[action] add_item');
          });
        },
        'toggle_item': (ctx) {
          final index = (ctx.params?['index'] as num?)?.toInt();
          if (index == null) return;

          ctx.setStateModel((prev) {
            final next = <String, dynamic>{...prev};
            final items = ((prev['items'] as List?) ?? <dynamic>[])
                .map((entry) => Map<String, dynamic>.from(entry as Map))
                .toList(growable: true);
            if (index < 0 || index >= items.length) {
              return next;
            }
            final item = items[index];
            item['done'] = !((item['done'] as bool?) ?? false);
            next['items'] = items;
            return next;
          });
          setState(() {
            _eventLog.add('[action] toggle_item(index: $index)');
          });
        },
        'remove_item': (ctx) {
          final index = (ctx.params?['index'] as num?)?.toInt();
          if (index == null) return;

          ctx.setStateModel((prev) {
            final next = <String, dynamic>{...prev};
            final items = ((prev['items'] as List?) ?? <dynamic>[])
                .map((entry) => Map<String, dynamic>.from(entry as Map))
                .toList(growable: true);
            if (index < 0 || index >= items.length) {
              return next;
            }
            items.removeAt(index);
            next['items'] = items;
            return next;
          });
          setState(() {
            _eventLog.add('[action] remove_item(index: $index)');
          });
        },
        'cycle_growth': (ctx) {
          ctx.setStateModel((prev) {
            final next = <String, dynamic>{...prev};
            final current = (prev['growth'] as num?)?.toDouble() ?? 0;
            if (current < 0) {
              next['growth'] = 6.4;
              next['progress'] = 0.74;
            } else if (current < 5) {
              next['growth'] = 14.1;
              next['progress'] = 0.93;
            } else {
              next['growth'] = -4.2;
              next['progress'] = 0.43;
            }
            return next;
          });
          setState(() {
            _eventLog.add('[action] cycle_growth');
          });
        },
        'refresh_metrics': (ctx) async {
          ctx.setStateModel((prev) {
            final next = <String, dynamic>{...prev};
            next['status'] = 'loading';
            return next;
          });

          await Future<void>.delayed(const Duration(milliseconds: 900));

          ctx.setStateModel((prev) {
            final next = <String, dynamic>{...prev};
            next['status'] = 'ready';
            next['lastUpdated'] = DateTime.now().toIso8601String();
            next['count'] = ((prev['count'] as num?) ?? 0) + 3;
            return next;
          });
          setState(() {
            _eventLog.add('[action] refresh_metrics complete');
          });
        },
      },
    );
  }

  List<ShowcaseCase> _buildCases() {
    return <ShowcaseCase>[
      ShowcaseCase(
        id: 'counter',
        title: '1) Counter + Visibility',
        description:
            r'Basic action bindings, state reads ($state), and visible condition toggle.',
        spec: JsonRenderSpec.fromJson(<String, dynamic>{
          'root': 'panel',
          'state': <String, dynamic>{'count': 2, 'showHint': false},
          'elements': <String, dynamic>{
            'panel': <String, dynamic>{
              'type': 'Panel',
              'props': <String, dynamic>{'title': 'Interactive Counter'},
              'children': <String>['countText', 'buttons', 'hint'],
            },
            'countText': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{
                'text': <String, dynamic>{r'$state': '/count'},
                'fontSize': 42,
                'fontWeight': '700',
              },
            },
            'buttons': <String, dynamic>{
              'type': 'Row',
              'props': <String, dynamic>{
                'spacing': 8,
                'runSpacing': 8,
                'overflow': 'wrap',
              },
              'children': <String>['dec', 'inc', 'toggle'],
            },
            'dec': <String, dynamic>{
              'type': 'Button',
              'props': <String, dynamic>{'label': '-1'},
              'on': <String, dynamic>{
                'press': <String, dynamic>{'action': 'decrement'},
              },
            },
            'inc': <String, dynamic>{
              'type': 'Button',
              'props': <String, dynamic>{'label': '+1'},
              'on': <String, dynamic>{
                'press': <String, dynamic>{'action': 'increment'},
              },
            },
            'toggle': <String, dynamic>{
              'type': 'Button',
              'props': <String, dynamic>{'label': 'Toggle Hint'},
              'on': <String, dynamic>{
                'press': <String, dynamic>{'action': 'toggle_hint'},
              },
            },
            'hint': <String, dynamic>{
              'type': 'Text',
              'visible': <String, dynamic>{r'$state': '/showHint', 'eq': true},
              'props': <String, dynamic>{
                'text': 'Hint: This node is controlled by `visible`.',
                'color': '#0F766E',
              },
            },
          },
        }),
      ),
      ShowcaseCase(
        id: 'repeat',
        title: '2) Repeat + Item Scope',
        description:
            r'repeat.statePath with $item/$index expressions and per-row action params.',
        spec: JsonRenderSpec.fromJson(<String, dynamic>{
          'root': 'todoPanel',
          'state': <String, dynamic>{
            'items': <Map<String, dynamic>>[
              <String, dynamic>{'name': 'Write docs', 'done': true},
              <String, dynamic>{'name': 'Add tests', 'done': false},
              <String, dynamic>{'name': 'Ship package', 'done': false},
            ],
          },
          'elements': <String, dynamic>{
            'todoPanel': <String, dynamic>{
              'type': 'Panel',
              'props': <String, dynamic>{'title': 'Todo List'},
              'children': <String>['repeater', 'controls'],
            },
            'repeater': <String, dynamic>{
              'type': 'Column',
              'props': <String, dynamic>{'spacing': 8},
              'repeat': <String, dynamic>{'statePath': '/items'},
              'children': <String>['row'],
            },
            'row': <String, dynamic>{
              'type': 'Container',
              'props': <String, dynamic>{
                'padding': 10,
                'radius': 10,
                'borderColor': '#CBD5E1',
                'borderWidth': 1,
              },
              'children': <String>['line'],
            },
            'line': <String, dynamic>{
              'type': 'Row',
              'props': <String, dynamic>{
                'spacing': 8,
                'runSpacing': 8,
                'overflow': 'wrap',
              },
              'children': <String>[
                'name',
                'doneChip',
                'toggleBtn',
                'removeBtn',
              ],
            },
            'name': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{
                'text': <String, dynamic>{r'$item': 'name'},
              },
            },
            'doneChip': <String, dynamic>{
              'type': 'StatusChip',
              'visible': <String, dynamic>{r'$item': 'done', 'eq': true},
              'props': <String, dynamic>{'label': 'DONE', 'variant': 'success'},
            },
            'toggleBtn': <String, dynamic>{
              'type': 'Button',
              'props': <String, dynamic>{
                'label': <String, dynamic>{
                  r'$cond': <String, dynamic>{r'$item': 'done', 'eq': true},
                  r'$then': 'Undo',
                  r'$else': 'Done',
                },
              },
              'on': <String, dynamic>{
                'press': <String, dynamic>{
                  'action': 'toggle_item',
                  'params': <String, dynamic>{
                    'index': <String, dynamic>{r'$index': true},
                  },
                },
              },
            },
            'removeBtn': <String, dynamic>{
              'type': 'Button',
              'props': <String, dynamic>{'label': 'Remove'},
              'on': <String, dynamic>{
                'press': <String, dynamic>{
                  'action': 'remove_item',
                  'params': <String, dynamic>{
                    'index': <String, dynamic>{r'$index': true},
                  },
                },
              },
            },
            'controls': <String, dynamic>{
              'type': 'Row',
              'props': <String, dynamic>{'spacing': 8},
              'children': <String>['addButton'],
            },
            'addButton': <String, dynamic>{
              'type': 'Button',
              'props': <String, dynamic>{'label': 'Add Item'},
              'on': <String, dynamic>{
                'press': <String, dynamic>{'action': 'add_item'},
              },
            },
          },
        }),
      ),
      ShowcaseCase(
        id: 'cond',
        title: '3) Dynamic Props + Progress',
        description:
            r'Uses $cond for text/color and demonstrates custom ProgressBar component.',
        spec: JsonRenderSpec.fromJson(<String, dynamic>{
          'root': 'growthPanel',
          'state': <String, dynamic>{'growth': -4.2, 'progress': 0.43},
          'elements': <String, dynamic>{
            'growthPanel': <String, dynamic>{
              'type': 'Panel',
              'props': <String, dynamic>{'title': 'Growth Monitor'},
              'children': <String>['trend', 'progress', 'cycle'],
            },
            'trend': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{
                'text': <String, dynamic>{
                  r'$cond': <String, dynamic>{r'$state': '/growth', 'gte': 0},
                  r'$then': 'Trend: Positive',
                  r'$else': 'Trend: Negative',
                },
                'fontSize': 22,
                'fontWeight': '700',
                'color': <String, dynamic>{
                  r'$cond': <String, dynamic>{r'$state': '/growth', 'gte': 0},
                  r'$then': '#15803D',
                  r'$else': '#B91C1C',
                },
              },
            },
            'progress': <String, dynamic>{
              'type': 'ProgressBar',
              'props': <String, dynamic>{
                'value': <String, dynamic>{r'$state': '/progress'},
                'color': <String, dynamic>{
                  r'$cond': <String, dynamic>{r'$state': '/growth', 'gte': 0},
                  r'$then': '#0F766E',
                  r'$else': '#B91C1C',
                },
              },
            },
            'cycle': <String, dynamic>{
              'type': 'Button',
              'props': <String, dynamic>{'label': 'Cycle Growth State'},
              'on': <String, dynamic>{
                'press': <String, dynamic>{'action': 'cycle_growth'},
              },
            },
          },
        }),
      ),
      ShowcaseCase(
        id: 'async',
        title: '4) Async Action',
        description:
            'Asynchronous action updates loading state, then commits final values.',
        spec: JsonRenderSpec.fromJson(<String, dynamic>{
          'root': 'asyncPanel',
          'state': <String, dynamic>{
            'status': 'idle',
            'lastUpdated': '-',
            'count': 0,
          },
          'elements': <String, dynamic>{
            'asyncPanel': <String, dynamic>{
              'type': 'Panel',
              'props': <String, dynamic>{'title': 'Async Refresh'},
              'children': <String>[
                'statusRow',
                'lastUpdated',
                'count',
                'refresh',
              ],
            },
            'statusRow': <String, dynamic>{
              'type': 'Row',
              'props': <String, dynamic>{'spacing': 8},
              'children': <String>['statusLabel', 'statusChip'],
            },
            'statusLabel': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{'text': 'Status:'},
            },
            'statusChip': <String, dynamic>{
              'type': 'StatusChip',
              'props': <String, dynamic>{
                'label': <String, dynamic>{r'$state': '/status'},
                'variant': <String, dynamic>{
                  r'$cond': <String, dynamic>{
                    r'$state': '/status',
                    'eq': 'ready',
                  },
                  r'$then': 'success',
                  r'$else': <String, dynamic>{
                    r'$cond': <String, dynamic>{
                      r'$state': '/status',
                      'eq': 'loading',
                    },
                    r'$then': 'warning',
                    r'$else': 'neutral',
                  },
                },
              },
            },
            'lastUpdated': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{
                'text': <String, dynamic>{
                  r'$cond': <String, dynamic>{
                    r'$state': '/lastUpdated',
                    'eq': '-',
                  },
                  r'$then': 'Last updated: never',
                  r'$else': <String, dynamic>{r'$state': '/lastUpdated'},
                },
              },
            },
            'count': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{
                'text': <String, dynamic>{r'$state': '/count'},
              },
            },
            'refresh': <String, dynamic>{
              'type': 'Button',
              'props': <String, dynamic>{'label': 'Refresh Metrics'},
              'on': <String, dynamic>{
                'press': <String, dynamic>{'action': 'refresh_metrics'},
              },
            },
          },
        }),
      ),
      ShowcaseCase(
        id: 'stream',
        title: '5) Streamed JSONL Patch',
        description:
            'Simulates server-sent JSONL patches using JsonSpecStreamCompiler.',
        spec: JsonRenderSpec.fromJson(<String, dynamic>{
          'root': 'streamPanel',
          'state': <String, dynamic>{'status': 'starting'},
          'elements': <String, dynamic>{
            'streamPanel': <String, dynamic>{
              'type': 'Panel',
              'props': <String, dynamic>{'title': 'Streaming Spec'},
              'children': <String>['statusText'],
            },
            'statusText': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{
                'text': <String, dynamic>{r'$state': '/status'},
              },
            },
          },
        }),
        streamLines: <String>[
          '{"op":"replace","path":"/state/status","value":"creating widgets"}',
          '{"op":"add","path":"/elements/subtitle","value":{"type":"Text","props":{"text":"Patches are applied incrementally.","color":"#0F766E"}}}',
          '{"op":"add","path":"/elements/streamPanel/children/1","value":"subtitle"}',
          '{"op":"replace","path":"/state/status","value":"done"}',
          '{"op":"add","path":"/elements/chip","value":{"type":"StatusChip","props":{"label":"STREAM COMPLETE","variant":"success"}}}',
          '{"op":"add","path":"/elements/streamPanel/children/2","value":"chip"}',
        ],
      ),
      ShowcaseCase(
        id: 'chat_stream',
        title: '6) Chat Stream Build-Up',
        description:
            'Mimics LLM output chunks that progressively build a chat-like UI.',
        spec: JsonRenderSpec.fromJson(<String, dynamic>{
          'root': 'chatPanel',
          'state': <String, dynamic>{'phase': 'starting'},
          'elements': <String, dynamic>{
            'chatPanel': <String, dynamic>{
              'type': 'Panel',
              'props': <String, dynamic>{'title': 'LLM Chat Stream'},
              'children': <String>['statusChip', 'messages'],
            },
            'statusChip': <String, dynamic>{
              'type': 'StatusChip',
              'props': <String, dynamic>{
                'label': <String, dynamic>{r'$state': '/phase'},
                'variant': 'warning',
              },
            },
            'messages': <String, dynamic>{
              'type': 'Column',
              'props': <String, dynamic>{'spacing': 8},
              'children': <String>['assistantIntro'],
            },
            'assistantIntro': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{
                'text': 'assistant: waiting for first token...',
                'fontSize': 16,
                'fontWeight': '600',
              },
            },
          },
        }),
        streamLines: <String>[
          '{"op":"replace","path":"/state/phase","value":"receiving intent"}',
          '{"op":"add","path":"/elements/userMsg","value":{"type":"Text","props":{"text":"user: Build a compact analytics card with trend and status.","fontSize":16}}}',
          '{"op":"add","path":"/elements/messages/children/1","value":"userMsg"}',
          '{"op":"replace","path":"/state/phase","value":"generating layout"}',
          '{"op":"add","path":"/elements/assistantMsg","value":{"type":"Text","props":{"text":"assistant: Added title, KPI value, and trend indicator.","fontSize":16,"fontWeight":"600"}}}',
          '{"op":"add","path":"/elements/messages/children/2","value":"assistantMsg"}',
          '{"op":"replace","path":"/state/phase","value":"applying patch"}',
          '{"op":"add","path":"/elements/assistantDone","value":{"type":"StatusChip","props":{"label":"render-ready","variant":"success"}}}',
          '{"op":"add","path":"/elements/messages/children/3","value":"assistantDone"}',
          '{"op":"replace","path":"/elements/statusChip/props/variant","value":"success"}',
          '{"op":"replace","path":"/state/phase","value":"complete"}',
        ],
      ),
      ShowcaseCase(
        id: 'component_stream',
        title: '7) Streamed Component Mix',
        description:
            'Streams patches that progressively render many component types.',
        spec: JsonRenderSpec.fromJson(<String, dynamic>{
          'root': 'mixPanel',
          'state': <String, dynamic>{
            'status': 'booting',
            'progress': 0.12,
            'showCta': false,
          },
          'elements': <String, dynamic>{
            'mixPanel': <String, dynamic>{
              'type': 'Panel',
              'props': <String, dynamic>{'title': 'Component Stream'},
              'children': <String>['statusRow', 'stack'],
            },
            'statusRow': <String, dynamic>{
              'type': 'Row',
              'props': <String, dynamic>{'spacing': 8},
              'children': <String>['statusLabel', 'statusChip'],
            },
            'statusLabel': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{'text': 'Status'},
            },
            'statusChip': <String, dynamic>{
              'type': 'StatusChip',
              'props': <String, dynamic>{
                'label': <String, dynamic>{r'$state': '/status'},
                'variant': 'warning',
              },
            },
            'stack': <String, dynamic>{
              'type': 'Column',
              'props': <String, dynamic>{'spacing': 10},
              'children': <String>['progressLabel', 'progressBar'],
            },
            'progressLabel': <String, dynamic>{
              'type': 'Text',
              'props': <String, dynamic>{
                'text': 'Progress',
                'fontSize': 15,
                'fontWeight': '600',
              },
            },
            'progressBar': <String, dynamic>{
              'type': 'ProgressBar',
              'props': <String, dynamic>{
                'value': <String, dynamic>{r'$state': '/progress'},
              },
            },
          },
        }),
        streamLines: <String>[
          '{"op":"replace","path":"/state/status","value":"receiving schema"}',
          '{"op":"add","path":"/elements/heroCard","value":{"type":"Container","props":{"padding":12,"radius":12,"borderColor":"#FDBA74","borderWidth":1,"color":"#FFF7ED"},"children":["heroTitle","heroBody"]}}',
          '{"op":"add","path":"/elements/heroTitle","value":{"type":"Text","props":{"text":"Quarterly Snapshot","fontSize":17,"fontWeight":"700"}}}',
          '{"op":"add","path":"/elements/heroBody","value":{"type":"Text","props":{"text":"Revenue +12.4%, churn down 1.3pt.","fontSize":14}}}',
          '{"op":"add","path":"/elements/stack/children/2","value":"heroCard"}',
          '{"op":"replace","path":"/state/progress","value":0.38}',
          '{"op":"add","path":"/elements/gapA","value":{"type":"SizedBox","props":{"height":6}}}',
          '{"op":"add","path":"/elements/stack/children/3","value":"gapA"}',
          '{"op":"add","path":"/elements/metricsRow","value":{"type":"Row","props":{"spacing":8,"runSpacing":8,"overflow":"wrap"},"children":["metricA","metricB","metricC"]}}',
          '{"op":"add","path":"/elements/metricA","value":{"type":"Container","props":{"padding":10,"radius":10,"borderColor":"#FDBA74","borderWidth":1},"children":["metricAText"]}}',
          '{"op":"add","path":"/elements/metricAText","value":{"type":"Text","props":{"text":"Sessions 18.2k"}}}',
          '{"op":"add","path":"/elements/metricB","value":{"type":"Container","props":{"padding":10,"radius":10,"borderColor":"#FDBA74","borderWidth":1},"children":["metricBText"]}}',
          '{"op":"add","path":"/elements/metricBText","value":{"type":"Text","props":{"text":"Conversion 6.8%"}}}',
          '{"op":"add","path":"/elements/metricC","value":{"type":"Container","props":{"padding":10,"radius":10,"borderColor":"#FDBA74","borderWidth":1},"children":["metricCText"]}}',
          '{"op":"add","path":"/elements/metricCText","value":{"type":"Text","props":{"text":"AOV \$84"}}}',
          '{"op":"add","path":"/elements/stack/children/4","value":"metricsRow"}',
          '{"op":"replace","path":"/state/progress","value":0.74}',
          '{"op":"add","path":"/elements/buttonCenter","value":{"type":"Center","visible":{"\$state":"/showCta","eq":true},"children":["publishBtn"]}}',
          '{"op":"add","path":"/elements/publishBtn","value":{"type":"Button","props":{"label":"Publish Snapshot"}}}',
          '{"op":"add","path":"/elements/stack/children/5","value":"buttonCenter"}',
          '{"op":"replace","path":"/state/showCta","value":true}',
          '{"op":"replace","path":"/state/progress","value":1}',
          '{"op":"replace","path":"/elements/statusChip/props/variant","value":"success"}',
          '{"op":"replace","path":"/state/status","value":"complete"}',
        ],
      ),
    ];
  }
}

class ShowcaseCase {
  const ShowcaseCase({
    required this.id,
    required this.title,
    required this.description,
    required this.spec,
    this.streamLines = const <String>[],
  });

  final String id;
  final String title;
  final String description;
  final JsonRenderSpec spec;
  final List<String> streamLines;
}

class ShowcaseChipStyle {
  const ShowcaseChipStyle({
    required this.background,
    required this.border,
    required this.foreground,
  });

  final Color background;
  final Color border;
  final Color foreground;

  ShowcaseChipStyle copyWith({
    Color? background,
    Color? border,
    Color? foreground,
  }) {
    return ShowcaseChipStyle(
      background: background ?? this.background,
      border: border ?? this.border,
      foreground: foreground ?? this.foreground,
    );
  }

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'background': _hexOf(background),
      'border': _hexOf(border),
      'foreground': _hexOf(foreground),
    };
  }
}

class ShowcaseVisualStyle {
  const ShowcaseVisualStyle({
    required this.id,
    required this.name,
    required this.description,
    required this.promptGuidance,
    required this.seedColor,
    required this.previewBackground,
    required this.panelBackground,
    required this.panelBorder,
    required this.textPrimary,
    required this.accent,
    required this.trackBackground,
    required this.neutralChip,
    required this.successChip,
    required this.warningChip,
    required this.dangerChip,
  });

  factory ShowcaseVisualStyle.fromDefinition({
    required String id,
    required ShowcaseVisualStyle base,
    required JsonStyleDefinition style,
  }) {
    final tokens = style.tokens;
    return base.copyWith(
      id: id,
      name: style.displayName.trim().isEmpty ? id : style.displayName.trim(),
      description: style.description.trim().isEmpty
          ? base.description
          : style.description.trim(),
      promptGuidance: style.guidance.trim().isEmpty
          ? base.promptGuidance
          : style.guidance.trim(),
      seedColor: _safeColor(tokens['seedColor']?.toString()) ?? base.seedColor,
      previewBackground:
          _safeColor(tokens['previewBackground']?.toString()) ??
          base.previewBackground,
      panelBackground:
          _safeColor(tokens['panelBackground']?.toString()) ??
          base.panelBackground,
      panelBorder:
          _safeColor(tokens['panelBorder']?.toString()) ?? base.panelBorder,
      textPrimary:
          _safeColor(tokens['textPrimary']?.toString()) ?? base.textPrimary,
      accent: _safeColor(tokens['accent']?.toString()) ?? base.accent,
      trackBackground:
          _safeColor(tokens['trackBackground']?.toString()) ??
          base.trackBackground,
      neutralChip: _mergeChip(base.neutralChip, tokens['neutralChip']),
      successChip: _mergeChip(base.successChip, tokens['successChip']),
      warningChip: _mergeChip(base.warningChip, tokens['warningChip']),
      dangerChip: _mergeChip(base.dangerChip, tokens['dangerChip']),
    );
  }

  final String id;
  final String name;
  final String description;
  final String promptGuidance;
  final Color seedColor;
  final Color previewBackground;
  final Color panelBackground;
  final Color panelBorder;
  final Color textPrimary;
  final Color accent;
  final Color trackBackground;
  final ShowcaseChipStyle neutralChip;
  final ShowcaseChipStyle successChip;
  final ShowcaseChipStyle warningChip;
  final ShowcaseChipStyle dangerChip;

  ShowcaseVisualStyle copyWith({
    String? id,
    String? name,
    String? description,
    String? promptGuidance,
    Color? seedColor,
    Color? previewBackground,
    Color? panelBackground,
    Color? panelBorder,
    Color? textPrimary,
    Color? accent,
    Color? trackBackground,
    ShowcaseChipStyle? neutralChip,
    ShowcaseChipStyle? successChip,
    ShowcaseChipStyle? warningChip,
    ShowcaseChipStyle? dangerChip,
  }) {
    return ShowcaseVisualStyle(
      id: id ?? this.id,
      name: name ?? this.name,
      description: description ?? this.description,
      promptGuidance: promptGuidance ?? this.promptGuidance,
      seedColor: seedColor ?? this.seedColor,
      previewBackground: previewBackground ?? this.previewBackground,
      panelBackground: panelBackground ?? this.panelBackground,
      panelBorder: panelBorder ?? this.panelBorder,
      textPrimary: textPrimary ?? this.textPrimary,
      accent: accent ?? this.accent,
      trackBackground: trackBackground ?? this.trackBackground,
      neutralChip: neutralChip ?? this.neutralChip,
      successChip: successChip ?? this.successChip,
      warningChip: warningChip ?? this.warningChip,
      dangerChip: dangerChip ?? this.dangerChip,
    );
  }

  JsonStyleDefinition toStyleDefinition() {
    return JsonStyleDefinition(
      displayName: name,
      description: description,
      guidance: promptGuidance,
      tokens: <String, dynamic>{
        'seedColor': _hexOf(seedColor),
        'previewBackground': _hexOf(previewBackground),
        'panelBackground': _hexOf(panelBackground),
        'panelBorder': _hexOf(panelBorder),
        'textPrimary': _hexOf(textPrimary),
        'accent': _hexOf(accent),
        'trackBackground': _hexOf(trackBackground),
        'neutralChip': neutralChip.toJson(),
        'successChip': successChip.toJson(),
        'warningChip': warningChip.toJson(),
        'dangerChip': dangerChip.toJson(),
      },
    );
  }
}

const List<ShowcaseVisualStyle> kShowcaseStyles = <ShowcaseVisualStyle>[
  ShowcaseVisualStyle(
    id: 'clean',
    name: 'Clean',
    description: 'Neutral colors and subtle borders for productivity UIs.',
    promptGuidance:
        'Use restrained color. Keep spacing balanced and typography straightforward.',
    seedColor: Color(0xFF0A5B9E),
    previewBackground: Color(0xFFF1F5F9),
    panelBackground: Color(0xFFFFFFFF),
    panelBorder: Color(0xFFE2E8F0),
    textPrimary: Color(0xFF0F172A),
    accent: Color(0xFF0F766E),
    trackBackground: Color(0xFFE2E8F0),
    neutralChip: ShowcaseChipStyle(
      background: Color(0xFFE2E8F0),
      border: Color(0xFFCBD5E1),
      foreground: Color(0xFF334155),
    ),
    successChip: ShowcaseChipStyle(
      background: Color(0xFFDCFCE7),
      border: Color(0xFF86EFAC),
      foreground: Color(0xFF166534),
    ),
    warningChip: ShowcaseChipStyle(
      background: Color(0xFFFEF3C7),
      border: Color(0xFFFCD34D),
      foreground: Color(0xFF92400E),
    ),
    dangerChip: ShowcaseChipStyle(
      background: Color(0xFFFEE2E2),
      border: Color(0xFFFCA5A5),
      foreground: Color(0xFF991B1B),
    ),
  ),
  ShowcaseVisualStyle(
    id: 'midnight',
    name: 'Midnight',
    description: 'Dark surfaces with cyan accents for data-heavy screens.',
    promptGuidance:
        'Prefer strong contrast. Use dark panels, bright accents, and compact spacing.',
    seedColor: Color(0xFF155E75),
    previewBackground: Color(0xFF020617),
    panelBackground: Color(0xFF0F172A),
    panelBorder: Color(0xFF1E293B),
    textPrimary: Color(0xFFE2E8F0),
    accent: Color(0xFF06B6D4),
    trackBackground: Color(0xFF1E293B),
    neutralChip: ShowcaseChipStyle(
      background: Color(0xFF1E293B),
      border: Color(0xFF334155),
      foreground: Color(0xFFE2E8F0),
    ),
    successChip: ShowcaseChipStyle(
      background: Color(0xFF052E2B),
      border: Color(0xFF0F766E),
      foreground: Color(0xFF99F6E4),
    ),
    warningChip: ShowcaseChipStyle(
      background: Color(0xFF422006),
      border: Color(0xFFA16207),
      foreground: Color(0xFFFDE68A),
    ),
    dangerChip: ShowcaseChipStyle(
      background: Color(0xFF450A0A),
      border: Color(0xFFB91C1C),
      foreground: Color(0xFFFCA5A5),
    ),
  ),
  ShowcaseVisualStyle(
    id: 'sunset',
    name: 'Sunset',
    description: 'Warm cards with orange and rose accents for marketing feel.',
    promptGuidance:
        'Use energetic warm colors, rounded containers, and expressive labels.',
    seedColor: Color(0xFFEA580C),
    previewBackground: Color(0xFFFFF7ED),
    panelBackground: Color(0xFFFFFBF5),
    panelBorder: Color(0xFFFED7AA),
    textPrimary: Color(0xFF7C2D12),
    accent: Color(0xFFEA580C),
    trackBackground: Color(0xFFFED7AA),
    neutralChip: ShowcaseChipStyle(
      background: Color(0xFFFFEDD5),
      border: Color(0xFFFDBA74),
      foreground: Color(0xFF9A3412),
    ),
    successChip: ShowcaseChipStyle(
      background: Color(0xFFDCFCE7),
      border: Color(0xFF86EFAC),
      foreground: Color(0xFF166534),
    ),
    warningChip: ShowcaseChipStyle(
      background: Color(0xFFFEF3C7),
      border: Color(0xFFFCD34D),
      foreground: Color(0xFF92400E),
    ),
    dangerChip: ShowcaseChipStyle(
      background: Color(0xFFFFE4E6),
      border: Color(0xFFFDA4AF),
      foreground: Color(0xFF9F1239),
    ),
  ),
];

ShowcaseChipStyle _chipStyle(ShowcaseVisualStyle style, String variant) {
  switch (variant) {
    case 'success':
      return style.successChip;
    case 'warning':
      return style.warningChip;
    case 'danger':
      return style.dangerChip;
    default:
      return style.neutralChip;
  }
}

Color? _safeColor(String? raw) {
  if (raw == null || raw.isEmpty) return null;
  final text = raw.startsWith('#') ? raw.substring(1) : raw;
  if (text.length == 6) {
    return Color(int.parse('FF$text', radix: 16));
  }
  if (text.length == 8) {
    return Color(int.parse(text, radix: 16));
  }
  return null;
}

ShowcaseChipStyle _mergeChip(ShowcaseChipStyle base, dynamic raw) {
  if (raw is! Map) {
    return base;
  }
  final map = Map<String, dynamic>.from(raw);
  return base.copyWith(
    background: _safeColor(map['background']?.toString()) ?? base.background,
    border: _safeColor(map['border']?.toString()) ?? base.border,
    foreground: _safeColor(map['foreground']?.toString()) ?? base.foreground,
  );
}

String _hexOf(Color color) {
  // ignore: deprecated_member_use
  final hex = color.value.toRadixString(16).toUpperCase().padLeft(8, '0');
  return '#${hex.substring(2)}';
}
1
likes
150
points
114
downloads

Publisher

unverified uploader

Weekly Downloads

Guardrailed JSON-to-Widget renderer for Flutter and Dart.

Repository (GitHub)
View/report issues

Topics

#flutter #json-ui #dynamic-ui #widget-rendering #llm-ui

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_json_render