import 'package:flutter/material.dart'; import '../theme/theme_x.dart'; /// A right-anchored detail panel over the still-visible list behind it — /// the "inspect a record without losing list context" pattern used by /// Services (and reused by Fleet's per-node cards). Place as the last child /// of a [Stack] alongside the screen's main content; it fills the stack /// itself via [Positioned.fill] and no-ops when [open] is false. class SlideOverPanel extends StatelessWidget { const SlideOverPanel({ super.key, required this.open, required this.onClose, required this.title, this.subtitle, required this.body, this.actions = const [], this.headerActions = const [], this.width = 420, }); final bool open; final VoidCallback onClose; final String title; final Widget? subtitle; final Widget body; final List actions; /// Icon buttons shown before the close button — for actions that belong /// with the record identity (e.g. Edit) rather than the primary action /// row at the bottom, which stays reserved for the few actions that need /// full label width. final List headerActions; final double width; @override Widget build(BuildContext context) { final reduceMotion = MediaQuery.disableAnimationsOf(context); final duration = reduceMotion ? Duration.zero : const Duration(milliseconds: 200); return Positioned.fill( child: IgnorePointer( ignoring: !open, child: Stack( children: [ AnimatedOpacity( opacity: open ? 1 : 0, duration: duration, child: GestureDetector( onTap: onClose, child: Container(color: Colors.black.withValues(alpha: 0.35)), ), ), Align( alignment: Alignment.centerRight, child: ClipRect( child: AnimatedSlide( offset: open ? Offset.zero : const Offset(1, 0), duration: duration, curve: Curves.easeOutCubic, child: Container( width: width, constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.92), height: double.infinity, decoration: BoxDecoration( color: context.colors.surface, border: Border(left: BorderSide(color: context.colors.outlineVariant)), boxShadow: [ BoxShadow(color: Colors.black.withValues(alpha: 0.18), blurRadius: 48, offset: const Offset(-8, 0)), ], ), child: SafeArea( child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 18, 12, 18), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(title, style: context.text.titleLarge), if (subtitle != null) ...[ const SizedBox(height: 6), subtitle!, ], ], ), ), ...headerActions, IconButton( onPressed: onClose, icon: const Icon(Icons.close, size: 19), tooltip: 'Close', ), ], ), ), const Divider(height: 1), Expanded(child: body), if (actions.isNotEmpty) ...[ const Divider(height: 1), Padding( padding: const EdgeInsets.all(16), child: Row( children: [ for (var i = 0; i < actions.length; i++) ...[ if (i > 0) const SizedBox(width: 8), Expanded(child: actions[i]), ], ], ), ), ], ], ), ), ), ), ), ), ], ), ), ); } }