import "dart:math" as math; import "package:flutter/rendering.dart"; import "package:shadcn_flutter/shadcn_flutter.dart" as shad; bool _isCompactTouch(shad.BuildContext context) { return shad.MediaQuery.sizeOf(context).width < 900; } class PanelField { const PanelField({ required this.section, required this.label, required this.child, this.enabled = true, }); final String section; final shad.Widget label; final shad.Widget child; final bool enabled; } // auto-groups fields by section and renders them as PanelSection > PanelRow class PanelList extends shad.StatelessWidget { const PanelList({super.key, required this.fields}); final List fields; @override shad.Widget build(shad.BuildContext context) { final theme = shad.Theme.of(context); final Map> grouped = {}; for (final f in fields) { grouped.putIfAbsent(f.section, () => []).add(f); } final entries = grouped.entries.toList(); return shad.Column( crossAxisAlignment: shad.CrossAxisAlignment.start, children: [ for (int i = 0; i < entries.length; i++) ...[ PanelSection( title: entries[i].key, children: entries[i] .value .map((f) => PanelRow(label: f.label, child: f.child, enabled: f.enabled)) .toList(), ), if (i < entries.length - 1) shad.Divider(color: theme.colorScheme.border, height: 1), ], shad.Divider(color: theme.colorScheme.border, height: 1), const shad.SizedBox(height: 10), ], ); } } class PanelSection extends shad.StatefulWidget { const PanelSection({ super.key, required this.title, required this.children, }); final String title; final List children; @override shad.State createState() => _PanelSectionState(); } class _PanelSectionState extends shad.State { bool _expanded = true; @override shad.Widget build(shad.BuildContext context) { final theme = shad.Theme.of(context); return shad.Column( crossAxisAlignment: shad.CrossAxisAlignment.start, children: [ shad.SizedBox( child: shad.GestureDetector( onTap: () => setState(() => _expanded = !_expanded), behavior: shad.HitTestBehavior.opaque, child: shad.ColoredBox( color: theme.colorScheme.secondary, child: shad.Padding( padding: const shad.EdgeInsets.symmetric(horizontal: 10, vertical: 6), child: shad.Row( children: [ shad.AnimatedRotation( turns: _expanded ? 0.0 : -0.25, duration: const Duration(milliseconds: 120), child: shad.Icon( shad.LucideIcons.chevronDown, size: 12, color: theme.colorScheme.mutedForeground, ), ), const shad.SizedBox(width: 4), shad.Text( widget.title, style: shad.TextStyle( fontSize: 11, fontWeight: shad.FontWeight.w600, color: theme.colorScheme.foreground, letterSpacing: 0.4, ), ), ], ), ), ), ), ), shad.Divider(color: theme.colorScheme.border, height: 1), if (_expanded) shad.Column( crossAxisAlignment: shad.CrossAxisAlignment.start, children: [ for (int i = 0; i < widget.children.length; i++) ...[ widget.children[i], if (i < widget.children.length - 1) shad.Divider(color: theme.colorScheme.border, height: 1), ], ], ), ], ); } } class PanelRow extends shad.StatelessWidget { const PanelRow({super.key, required this.label, required this.child, this.enabled = true}); final shad.Widget label; final shad.Widget child; final bool enabled; @override shad.Widget build(shad.BuildContext context) { final isCompactTouch = _isCompactTouch(context); final theme = shad.Theme.of(context); final row = shad.ConstrainedBox( constraints: shad.BoxConstraints(minHeight: isCompactTouch ? 52 : 38), child: shad.IntrinsicHeight( child: shad.Padding( padding: const shad.EdgeInsets.only(left: 12), child: shad.Row( crossAxisAlignment: shad.CrossAxisAlignment.stretch, children: [ shad.SizedBox( width: 100, child: shad.Align( alignment: shad.Alignment.centerLeft, child: shad.Padding( padding: shad.EdgeInsets.symmetric(vertical: isCompactTouch ? 6 : 4), child: shad.DefaultTextStyle.merge( style: shad.TextStyle( fontSize: 11, color: theme.colorScheme.mutedForeground, decoration: !enabled ? shad.TextDecoration.lineThrough : null, decorationThickness: !enabled ? 3 : null, ), child: label, ), ), ), ), shad.VerticalDivider(color: theme.colorScheme.border, width: 1), shad.Expanded( child: shad.Padding( padding: shad.EdgeInsets.zero, child: shad.Stack( children: [ shad.Positioned.fill( child: shad.Padding( padding: shad.EdgeInsets.symmetric(vertical: isCompactTouch ? 6 : 4, horizontal: 8), child: child, ), ), if (!enabled) shad.Positioned.fill( child: shad.GestureDetector( onTap: () {}, behavior: shad.HitTestBehavior.opaque, child: shad.CustomPaint( painter: _DisabledStripePainter(theme.colorScheme.border), ), ), ), ], ), ), ), ], ), ), ), ); return row; } } class _DisabledStripePainter extends CustomPainter { const _DisabledStripePainter(this.color); final shad.Color color; @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color.withValues(alpha: 1) ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; const spacing = 4.0; final diag = math.sqrt(size.width * size.width + size.height * size.height); final count = (diag / spacing).ceil() + 2; canvas.save(); canvas.clipRect(Offset.zero & size); canvas.drawRect(Rect.fromLTWH(1, 1, size.width - 3, size.height - 2), paint); canvas.translate(0, size.height); canvas.rotate(-math.pi / 4); for (int i = -count; i <= count; i++) { final x = i * spacing; canvas.drawLine(Offset(x, -diag), Offset(x, diag), paint); } canvas.restore(); } @override bool shouldRepaint(_DisabledStripePainter old) => old.color != color; }