| import 'package:jaspr/dom.dart'; |
| import 'package:jaspr/jaspr.dart'; |
| import '../../../utils/styles.dart'; |
| import 'icons.dart'; |
| |
| /// A modal dialog overlay for previewing an image in large format. |
| class ImagePreviewDialog extends StatelessComponent { |
| const ImagePreviewDialog({ |
| required this.imageUrl, |
| required this.fileName, |
| required this.onClose, |
| super.key, |
| }); |
| |
| /// The data URL or source URL of the image. |
| final String imageUrl; |
| |
| /// The name of the file to display in the header. |
| final String fileName; |
| |
| /// Callback when the dialog is closed. |
| final VoidCallback onClose; |
| |
| @override |
| Component build(BuildContext context) { |
| return div( |
| classes: 'image-preview-overlay', |
| events: { |
| 'click': (e) { |
| onClose(); |
| }, |
| }, |
| [ |
| // Top floating pill with filename and close action |
| div( |
| classes: 'image-preview-pill', |
| events: { |
| 'click': (e) { |
| e.stopPropagation(); |
| }, |
| }, |
| [ |
| span(classes: 'image-preview-filename', [Component.text(fileName)]), |
| button( |
| classes: 'image-preview-close-btn', |
| attributes: const {'aria-label': 'Close preview'}, |
| onClick: onClose, |
| [ |
| closeIcon(size: 12, color: 'currentColor'), |
| ], |
| ), |
| ], |
| ), |
| // Large Centered Image |
| img( |
| classes: 'image-preview-img', |
| src: imageUrl, |
| attributes: {'alt': fileName}, |
| events: { |
| 'click': (e) { |
| e.stopPropagation(); |
| }, |
| }, |
| ), |
| ], |
| ); |
| } |
| |
| @css |
| static List<StyleRule> get styles => [ |
| css('.image-preview-overlay').styles( |
| display: .flex, |
| position: .fixed(top: 0.px, left: 0.px, right: 0.px, bottom: 0.px), |
| zIndex: const ZIndex(3000), |
| backdropFilter: .blur(16.px), |
| flexDirection: .column, |
| justifyContent: .center, |
| alignItems: .center, |
| backgroundColor: Colors.black.withOpacity(0.8), |
| raw: { |
| 'animation': 'preview-fade-in 180ms ease-out', |
| }, |
| ), |
| css.keyframes('preview-fade-in', { |
| '0%': const Styles(opacity: 0.0), |
| '100%': const Styles(opacity: 1.0), |
| }), |
| css('.image-preview-pill').styles( |
| display: .flex, |
| position: .absolute(top: 24.px), |
| padding: Padding.symmetric(horizontal: 14.px, vertical: 8.px), |
| border: Border.all(color: colorBorder, width: 1.px), |
| radius: .circular(20.px), |
| shadow: BoxShadow( |
| offsetX: 0.px, |
| offsetY: 4.px, |
| blur: 12.px, |
| color: Colors.black.withOpacity(0.3), |
| ), |
| alignItems: .center, |
| gap: Gap.all(10.px), |
| backgroundColor: colorContainer.withOpacity(0.85), |
| raw: { |
| 'backdrop-filter': 'blur(8px)', |
| 'max-width': '90vw', |
| }, |
| ), |
| css('.image-preview-filename').styles( |
| color: colorOnSurface, |
| fontSize: 12.px, |
| fontWeight: .w500, |
| raw: { |
| 'white-space': 'nowrap', |
| 'text-overflow': 'ellipsis', |
| 'overflow': 'hidden', |
| 'max-width': '200px', |
| }, |
| ), |
| css('.image-preview-close-btn').styles( |
| display: .flex, |
| width: 18.px, |
| height: 18.px, |
| padding: Padding.zero, |
| border: Border.none, |
| radius: .circular(9.px), |
| cursor: .pointer, |
| transition: Transition('all', duration: 150.ms), |
| justifyContent: .center, |
| alignItems: .center, |
| color: colorOnSurfaceVariant, |
| backgroundColor: Colors.transparent, |
| ), |
| css('.image-preview-close-btn:hover').styles( |
| color: colorOnSurface, |
| backgroundColor: colorBorder.withOpacity(0.5), |
| ), |
| css('.image-preview-img').styles( |
| maxWidth: 90.percent, |
| maxHeight: 80.percent, |
| radius: .circular(8.px), |
| shadow: BoxShadow( |
| offsetX: 0.px, |
| offsetY: 16.px, |
| blur: 40.px, |
| color: Colors.black.withOpacity(0.6), |
| ), |
| raw: { |
| 'object-fit': 'contain', |
| 'user-select': 'none', |
| 'animation': 'preview-scale-up 220ms cubic-bezier(0.34, 1.56, 0.64, 1)', |
| }, |
| ), |
| css.keyframes('preview-scale-up', { |
| '0%': const Styles(opacity: 0.0, transform: Transform.scale(0.95)), |
| '100%': const Styles(opacity: 1.0, transform: Transform.scale(1.0)), |
| }), |
| ]; |
| } |