| import 'package:jaspr/dom.dart'; |
| import 'package:jaspr/jaspr.dart'; |
| |
| import '../../../utils/styles.dart'; |
| import 'icons.dart'; |
| |
| /// A reusable image thumbnail component used for displaying image attachments. |
| /// |
| /// Supports an optional remove button and click-to-preview behavior. |
| /// Used in both the chat composer (for pending uploads) and message bubbles |
| /// (for sent image attachments). |
| class ImageThumbnail extends StatelessComponent { |
| const ImageThumbnail({ |
| required this.imageUrl, |
| required this.fileName, |
| this.onRemove, |
| this.onTap, |
| super.key, |
| }); |
| |
| /// The data URL or source URL of the image to display. |
| final String imageUrl; |
| |
| /// The alt text / file name for accessibility. |
| final String fileName; |
| |
| /// Optional callback when the remove button is clicked. |
| /// When null, the remove button is hidden. |
| final VoidCallback? onRemove; |
| |
| /// Optional callback when the thumbnail is clicked (excluding the remove button). |
| final VoidCallback? onTap; |
| |
| @override |
| Component build(BuildContext context) { |
| return div( |
| classes: 'image-thumbnail', |
| events: { |
| if (onTap != null) |
| 'click': (_) { |
| onTap!(); |
| }, |
| }, |
| [ |
| img( |
| classes: 'image-thumbnail-img', |
| src: imageUrl, |
| attributes: {'alt': fileName}, |
| ), |
| if (onRemove != null) |
| button( |
| classes: 'image-thumbnail-remove', |
| attributes: {'type': 'button', 'aria-label': 'Remove $fileName'}, |
| events: { |
| 'click': (e) { |
| e.stopPropagation(); |
| onRemove!(); |
| }, |
| }, |
| [ |
| closeIcon(size: 10, color: 'currentColor'), |
| ], |
| ), |
| ], |
| ); |
| } |
| |
| @css |
| static List<StyleRule> get styles => [ |
| // Thumbnail container |
| css('.image-thumbnail').styles( |
| display: .flex, |
| position: const Position.relative(), |
| width: 56.px, |
| height: 56.px, |
| border: Border.all(color: colorBorder, width: 1.px), |
| radius: .circular(6.px), |
| overflow: .hidden, |
| cursor: .pointer, |
| transition: Transition('border-color', duration: 150.ms), |
| raw: {'flex-shrink': '0'}, |
| ), |
| css('.image-thumbnail:hover').styles( |
| border: Border.all(color: colorPrimary.withOpacity(0.6), width: 1.px), |
| ), |
| // Image fill |
| css('.image-thumbnail-img').styles( |
| width: 100.percent, |
| height: 100.percent, |
| raw: {'object-fit': 'cover'}, |
| ), |
| // Remove button (overlay) |
| css('.image-thumbnail-remove').styles( |
| display: .flex, |
| position: .absolute(top: 2.px, right: 2.px), |
| 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: colorOnSurface, |
| backgroundColor: colorSurface.withOpacity(0.85), |
| raw: {'backdrop-filter': 'blur(4px)'}, |
| ), |
| css('.image-thumbnail-remove:hover').styles( |
| color: colorError, |
| backgroundColor: colorError.withOpacity(0.2), |
| ), |
| ]; |
| } |