blob: 15b10654843e5cb09edb18aea5420eb1622e0403 [file]
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)),
}),
];
}