import "dart:math" as math; import "package:shadcn_flutter/shadcn_flutter.dart"; class PanelField { const PanelField({ required this.section, required this.label, required this.child, this.enabled = true, }); final String section; final Widget label; final Widget child; final bool enabled; } // auto groups fields by section string class PanelList extends StatelessWidget { const PanelList({super.key, required this.fields}); final List fields; @override Widget build(BuildContext context) { final theme = Theme.of(context); final Map> grouped = {}; for (final f in fields) { grouped.putIfAbsent(f.section, () => []).add(f); } final entries = grouped.entries.toList(); return Column( crossAxisAlignment: 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) Divider(color: theme.colorScheme.border, height: 1), ], Divider(color: theme.colorScheme.border, height: 1), const SizedBox(height: 10), ], ); } } class PanelSection extends StatefulWidget { const PanelSection({ super.key, required this.title, required this.children, }); final String title; final List children; @override State createState() => _PanelSectionState(); } class _PanelSectionState extends State { bool _expanded = true; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onTap: () => setState(() => _expanded = !_expanded), behavior: HitTestBehavior.opaque, child: ColoredBox( color: theme.colorScheme.secondary, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), child: Row( children: [ AnimatedRotation( turns: _expanded ? 0.0 : -0.25, duration: const Duration(milliseconds: 120), child: Icon(LucideIcons.chevronDown, size: 12, color: theme.colorScheme.mutedForeground), ), const SizedBox(width: 4), Text( widget.title, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: theme.colorScheme.foreground, letterSpacing: 0.4, ), ), ], ), ), ), ), Divider(color: theme.colorScheme.border, height: 1), if (_expanded) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (int i = 0; i < widget.children.length; i++) ...[ widget.children[i], if (i < widget.children.length - 1) Divider(color: theme.colorScheme.border, height: 1), ], ], ), ], ); } } class PanelRow extends StatelessWidget { const PanelRow({super.key, required this.label, required this.child, this.enabled = true}); final Widget label; final Widget child; final bool enabled; @override Widget build(BuildContext context) { final theme = Theme.of(context); return ConstrainedBox( constraints: const BoxConstraints(minHeight: 38), child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.only(left: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( width: 120, child: Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: DefaultTextStyle.merge( style: TextStyle( fontSize: 11, color: theme.colorScheme.mutedForeground, decoration: !enabled ? TextDecoration.lineThrough : null, decorationThickness: !enabled ? 3 : null, ), child: label, ), ), ), ), VerticalDivider(color: theme.colorScheme.border, width: 1), Expanded( child: Stack( children: [ Positioned.fill( child: Padding( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), child: child, ), ), if (!enabled) Positioned.fill( child: GestureDetector( onTap: () {}, behavior: HitTestBehavior.opaque, child: CustomPaint( painter: _DisabledStripePainter(theme.colorScheme.border), ), ), ), ], ), ), ], ), ), ), ); } } class _DisabledStripePainter extends CustomPainter { const _DisabledStripePainter(this.color); final 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; }