import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../domain/label_document.dart'; import '../domain/dynamic_field_definition.dart'; import '../domain/barcode_template.dart'; import '../providers/barcode_designer_provider.dart'; import '../providers/barcode_templates_provider.dart'; import 'widgets/canvas_ruler.dart'; import 'widgets/designer_toolbox.dart'; import 'widgets/product_variables_panel.dart'; import 'widgets/element_properties_panel.dart'; import 'widgets/printer_command_preview.dart'; import 'widgets/test_print_modal.dart'; import 'widgets/product_picker_dialog.dart'; import '../../inventory/domain/product.dart'; import '../../inventory/domain/inventory_item.dart'; import '../../inventory/providers/products_provider.dart'; import '../services/template_data_resolver.dart'; import 'package:barcode_widget/barcode_widget.dart' as bc; import 'saved_templates_screen.dart'; class BarcodeDesignerScreen extends ConsumerStatefulWidget { final BarcodeTemplate? initialTemplate; const BarcodeDesignerScreen({super.key, this.initialTemplate}); @override ConsumerState createState() => _BarcodeDesignerScreenState(); } class _BarcodeDesignerScreenState extends ConsumerState { int _leftTabIndex = 0; // 0: Toolbox, 1: Product Fields bool _isSaving = false; bool _isMobileBottomPanelExpanded = true; late final FocusNode _canvasFocusNode; @override void initState() { super.initState(); _canvasFocusNode = FocusNode(debugLabel: 'BarcodeDesignerCanvas'); WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.initialTemplate != null) { final doc = widget.initialTemplate!.toLabelDocument(); ref.read(barcodeDesignerProvider.notifier).loadDocument(doc, widget.initialTemplate!.id); } }); } @override void dispose() { _canvasFocusNode.dispose(); super.dispose(); } /// Returns true if the user currently has focus inside any text input field /// so that typing, deleting text, or backspacing is NOT intercepted by canvas shortcuts. bool _isUserTyping() { final primaryFocus = FocusManager.instance.primaryFocus; if (primaryFocus == null) return false; if (primaryFocus == _canvasFocusNode) return false; final context = primaryFocus.context; if (context != null && context.mounted) { if (context.widget is EditableText) return true; if (context.findAncestorWidgetOfExactType() != null) return true; if (context.findAncestorStateOfType() != null) return true; if (context.findAncestorWidgetOfExactType() != null) return true; if (context.findAncestorWidgetOfExactType() != null) return true; final renderObject = context.findRenderObject(); if (renderObject != null && renderObject.runtimeType.toString().contains('RenderEditable')) { return true; } } final debugLabel = primaryFocus.debugLabel?.toLowerCase() ?? ''; if (debugLabel.contains('editabletext') || debugLabel.contains('textfield')) { return true; } return false; } // Pixels per millimeter based on zoom level double _getScale(double zoom) => 3.2 * zoom; @override Widget build(BuildContext context) { final state = ref.watch(barcodeDesignerProvider); final notifier = ref.read(barcodeDesignerProvider.notifier); final isDark = Theme.of(context).brightness == Brightness.dark; // Keep sampleProduct in sync with the live products list in case it was updated final products = ref.watch(productsProvider).value ?? []; final currentSampleProduct = state.sampleProduct != null ? (products.where((p) => p.id == state.sampleProduct!.id).firstOrNull ?? state.sampleProduct) : null; ref.listen(productsProvider, (prev, next) { final list = next.value; if (list != null && state.sampleProduct != null) { final updatedProd = list.where((p) => p.id == state.sampleProduct!.id).firstOrNull; if (updatedProd != null && (updatedProd.mrp != state.sampleProduct!.mrp || updatedProd.sellingPrice != state.sampleProduct!.sellingPrice || updatedProd.name != state.sampleProduct!.name || updatedProd.barcode != state.sampleProduct!.barcode)) { notifier.setSampleProduct(updatedProd, state.sampleInventoryItem); } } }); final doc = state.document; final cfg = doc.config; final scale = _getScale(state.zoom); // Active elements to render (resolved in preview mode using live product data) final renderDoc = state.isPreviewMode ? (currentSampleProduct != null ? TemplateDataResolver.resolveDocument( document: state.document, product: currentSampleProduct, inventoryItem: state.sampleInventoryItem, ) : state.resolvedDocument) : state.document; return Scaffold( backgroundColor: isDark ? const Color(0xFF090D16) : const Color(0xFFF8FAFC), appBar: _buildTopAppBar(context, state, notifier, isDark), body: Focus( focusNode: _canvasFocusNode, autofocus: true, onKeyEvent: (node, event) { if (event is KeyDownEvent || event is KeyRepeatEvent) { // When user is typing inside any text input (e.g. properties panel), // ignore canvas shortcuts so Backspace/Delete/Arrows edit text normally. if (_isUserTyping()) { return KeyEventResult.ignored; } final isDeleteKey = event.logicalKey == LogicalKeyboardKey.delete || event.logicalKey == LogicalKeyboardKey.backspace || event.physicalKey == PhysicalKeyboardKey.delete || event.physicalKey == PhysicalKeyboardKey.backspace; if (isDeleteKey) { if (event is KeyDownEvent && state.selectedElementId != null) { if (state.selectedElement?.isLocked == true) { return KeyEventResult.handled; } notifier.deleteElement(state.selectedElementId!); return KeyEventResult.handled; } } if (state.selectedElement != null) { final elem = state.selectedElement!; final step = HardwareKeyboard.instance.isShiftPressed ? 2.0 : 0.5; if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { notifier.setElementPosition(elem.id, elem.xMm - step, elem.yMm); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { notifier.setElementPosition(elem.id, elem.xMm + step, elem.yMm); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { notifier.setElementPosition(elem.id, elem.xMm, elem.yMm - step); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { notifier.setElementPosition(elem.id, elem.xMm, elem.yMm + step); return KeyEventResult.handled; } } if (event.logicalKey == LogicalKeyboardKey.escape) { notifier.selectElement(null); return KeyEventResult.handled; } if ((HardwareKeyboard.instance.isControlPressed || HardwareKeyboard.instance.isMetaPressed) && event.logicalKey == LogicalKeyboardKey.keyD) { if (event is KeyDownEvent && state.selectedElementId != null) { notifier.duplicateElement(state.selectedElementId!); return KeyEventResult.handled; } } if ((HardwareKeyboard.instance.isControlPressed || HardwareKeyboard.instance.isMetaPressed) && event.logicalKey == LogicalKeyboardKey.keyZ) { if (event is KeyDownEvent) { if (HardwareKeyboard.instance.isShiftPressed) { notifier.redo(); } else { notifier.undo(); } return KeyEventResult.handled; } } // Bring to Front (]) and Send to Back ([) shortcuts if (event is KeyDownEvent && state.selectedElementId != null) { if (event.logicalKey == LogicalKeyboardKey.bracketRight) { notifier.bringToFront(state.selectedElementId!); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.bracketLeft) { notifier.sendToBack(state.selectedElementId!); return KeyEventResult.handled; } } } return KeyEventResult.ignored; }, child: LayoutBuilder( builder: (context, constraints) { final isDesktop = constraints.maxWidth >= 960; if (!isDesktop) { return _buildMobileLayout(context, state, notifier, renderDoc, scale, isDark, currentSampleProduct); } return Column( children: [ Expanded( child: Row( children: [ // Left Panel (Toolbox & Variables) SizedBox( width: 270, child: Container( decoration: BoxDecoration( color: isDark ? const Color(0xFF111827) : Colors.white, border: Border( right: BorderSide( color: isDark ? Colors.white10 : Colors.grey.shade200, ), ), ), child: Column( children: [ // Tab selector Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 4), child: SegmentedButton( segments: const [ ButtonSegment( value: 0, label: Text('Tools', style: TextStyle(fontSize: 12)), icon: Icon(LucideIcons.wrench, size: 14), ), ButtonSegment( value: 1, label: Text('Variables', style: TextStyle(fontSize: 12)), icon: Icon(LucideIcons.variable, size: 14), ), ], selected: {_leftTabIndex}, onSelectionChanged: (set) => setState(() => _leftTabIndex = set.first), ), ), const Divider(height: 12), Expanded( child: _leftTabIndex == 0 ? DesignerToolbox( onAddElement: (type) => _addNewElement(type, notifier, cfg), ) : ProductVariablesPanel( onAddField: (field) { notifier.addBoundFieldElement( fieldDef: field, dropXMm: 4.0, dropYMm: 6.0, ); }, ), ), ], ), ), ), // Center Interactive Canvas Expanded( child: _buildCenterCanvasArea( context, state, notifier, renderDoc, scale, isDark, currentSampleProduct, ), ), // Right Properties Panel SizedBox( width: 290, child: ElementPropertiesPanel( document: state.document, selectedElement: state.selectedElement, onUpdateElement: notifier.updateElement, onDeleteElement: notifier.deleteElement, onDuplicateElement: notifier.duplicateElement, onBringToFront: notifier.bringToFront, onSendToBack: notifier.sendToBack, onUpdateConfig: notifier.updateLabelConfig, ), ), ], ), ), // Bottom Live Printer Command Preview PrinterCommandPreview( commands: state.liveCommands, language: cfg.printerLanguage, ), ], ); }, ), ), ); } PreferredSizeWidget _buildTopAppBar( BuildContext context, BarcodeDesignerState state, BarcodeDesignerNotifier notifier, bool isDark, ) { final screenWidth = MediaQuery.of(context).size.width; final isMobile = screenWidth < 960; return AppBar( elevation: 1, backgroundColor: isDark ? const Color(0xFF111827) : Colors.white, leading: IconButton( icon: const Icon(LucideIcons.arrowLeft), onPressed: () => Navigator.pop(context), ), titleSpacing: 0, title: Row( mainAxisSize: isMobile ? MainAxisSize.min : MainAxisSize.max, children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: const Icon(LucideIcons.barcode, color: Colors.blue, size: 20), ), const SizedBox(width: 10), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ InkWell( onTap: () => _showRenameDialog(context, state.document.name, notifier), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( state.document.name, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 6), Icon(LucideIcons.pencil, size: 12, color: Colors.grey.shade400), ], ), ), Text( '${state.document.config.widthMm.round()} × ${state.document.config.heightMm.round()} mm • ${state.document.config.dpi} DPI', style: TextStyle(fontSize: 11, color: Colors.grey.shade500), overflow: TextOverflow.ellipsis, ), ], ), ), if (!isMobile) ...[ const SizedBox(width: 16), // Design vs Preview Mode Switch Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: isDark ? Colors.white10 : Colors.grey.shade200, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _buildModeButton( label: 'Design', icon: LucideIcons.draftingCompass, isSelected: !state.isPreviewMode, onTap: () => notifier.togglePreviewMode(false), isDark: isDark, ), _buildModeButton( label: 'Preview', icon: LucideIcons.eye, isSelected: state.isPreviewMode, onTap: () => notifier.togglePreviewMode(true), isDark: isDark, ), ], ), ), const SizedBox(width: 12), // Sample Product Selector if (state.sampleProduct != null) InputChip( avatar: const Icon(LucideIcons.package, size: 14, color: Colors.blue), label: Text( state.sampleProduct!.name, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), onDeleted: () => notifier.setSampleProduct(null), onPressed: () => _pickSampleProduct(context, notifier), ) else ActionChip( avatar: const Icon(LucideIcons.packagePlus, size: 14, color: Colors.blue), label: const Text('Sample Product', style: TextStyle(fontSize: 11)), onPressed: () => _pickSampleProduct(context, notifier), ), ], ], ), actions: isMobile ? [ IconButton( icon: const Icon(LucideIcons.printer, color: Colors.green, size: 20), tooltip: 'Test Print', onPressed: () { TestPrintModal.show( context, document: state.document, sampleProduct: state.sampleProduct, sampleInventoryItem: state.sampleInventoryItem, onSelectProduct: (p, item) => notifier.setSampleProduct(p, item), onUpdateConfig: (cfg) => notifier.updateLabelConfig(cfg), ); }, ), IconButton( icon: _isSaving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(LucideIcons.save, size: 20), tooltip: 'Save Template', onPressed: _isSaving ? null : () => _saveTemplateToBackend(context, state, notifier), ), PopupMenuButton( icon: const Icon(LucideIcons.ellipsisVertical, size: 18), onSelected: (val) { switch (val) { case 'undo': if (state.undoStack.isNotEmpty) notifier.undo(); break; case 'redo': if (state.redoStack.isNotEmpty) notifier.redo(); break; case 'zoom_in': notifier.setZoom(state.zoom + 0.3); break; case 'zoom_out': notifier.setZoom(state.zoom - 0.3); break; case 'zoom_reset': notifier.setZoom(1.0); break; case 'templates': Navigator.push( context, MaterialPageRoute(builder: (_) => const SavedTemplatesScreen()), ); break; } }, itemBuilder: (context) => [ PopupMenuItem( value: 'undo', enabled: state.undoStack.isNotEmpty, child: const Row( children: [ Icon(LucideIcons.undo, size: 16), SizedBox(width: 8), Text('Undo'), ], ), ), PopupMenuItem( value: 'redo', enabled: state.redoStack.isNotEmpty, child: const Row( children: [ Icon(LucideIcons.redo, size: 16), SizedBox(width: 8), Text('Redo'), ], ), ), const PopupMenuDivider(), PopupMenuItem( value: 'zoom_in', child: Row( children: [ const Icon(LucideIcons.zoomIn, size: 16), const SizedBox(width: 8), Text('Zoom In (${(state.zoom * 100).round()}%)'), ], ), ), const PopupMenuItem( value: 'zoom_out', child: Row( children: [ Icon(LucideIcons.zoomOut, size: 16), SizedBox(width: 8), Text('Zoom Out'), ], ), ), const PopupMenuItem( value: 'zoom_reset', child: Row( children: [ Icon(LucideIcons.maximize2, size: 16), SizedBox(width: 8), Text('Reset Zoom (100%)'), ], ), ), const PopupMenuDivider(), const PopupMenuItem( value: 'templates', child: Row( children: [ Icon(LucideIcons.folderOpen, size: 16), SizedBox(width: 8), Text('Saved Templates'), ], ), ), ], ), const SizedBox(width: 4), ] : [ // Desktop actions IconButton( icon: const Icon(LucideIcons.undo, size: 18), tooltip: 'Undo', onPressed: state.undoStack.isNotEmpty ? notifier.undo : null, ), IconButton( icon: const Icon(LucideIcons.redo, size: 18), tooltip: 'Redo', onPressed: state.redoStack.isNotEmpty ? notifier.redo : null, ), const VerticalDivider(width: 20, indent: 12, endIndent: 12), // Zoom Controls IconButton( icon: const Icon(LucideIcons.zoomOut, size: 18), tooltip: 'Zoom Out', onPressed: () => notifier.setZoom(state.zoom - 0.3), ), Text( '${(state.zoom * 100).round()}%', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600), ), IconButton( icon: const Icon(LucideIcons.zoomIn, size: 18), tooltip: 'Zoom In', onPressed: () => notifier.setZoom(state.zoom + 0.3), ), const VerticalDivider(width: 20, indent: 12, endIndent: 12), // Test Print Button OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: Colors.green, side: const BorderSide(color: Colors.green), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), icon: const Icon(LucideIcons.printer, size: 16), label: const Text('Test Print', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), onPressed: () { TestPrintModal.show( context, document: state.document, sampleProduct: state.sampleProduct, sampleInventoryItem: state.sampleInventoryItem, onSelectProduct: (p, item) => notifier.setSampleProduct(p, item), onUpdateConfig: (cfg) => notifier.updateLabelConfig(cfg), ); }, ), const SizedBox(width: 10), // Save Template Button ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), icon: _isSaving ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), ) : const Icon(LucideIcons.save, size: 16), label: const Text('Save Template', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), onPressed: _isSaving ? null : () => _saveTemplateToBackend(context, state, notifier), ), const SizedBox(width: 8), // Saved Templates Screen IconButton( icon: const Icon(LucideIcons.folderOpen, size: 18), tooltip: 'Saved Templates', onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const SavedTemplatesScreen()), ); }, ), const SizedBox(width: 8), ], ); } Widget _buildModeButton({ required String label, required IconData icon, required bool isSelected, required VoidCallback onTap, required bool isDark, }) { final isPreview = label == 'Preview'; Color? activeBg; Color? activeFg; if (isSelected) { if (isPreview) { activeBg = isDark ? const Color(0xFF065F46) : const Color(0xFF10B981); activeFg = Colors.white; } else { activeBg = isDark ? const Color(0xFF2563EB) : Colors.white; activeFg = isDark ? Colors.white : const Color(0xFF2563EB); } } return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: isSelected ? activeBg : Colors.transparent, borderRadius: BorderRadius.circular(16), boxShadow: isSelected && !isDark ? [const BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 1))] : null, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 13, color: isSelected ? activeFg : (isDark ? Colors.white60 : Colors.grey.shade600), ), const SizedBox(width: 5), Text( label, style: TextStyle( fontSize: 11, fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, color: isSelected ? activeFg : (isDark ? Colors.white60 : Colors.grey.shade600), ), ), ], ), ), ); } Widget _buildCenterCanvasArea( BuildContext context, BarcodeDesignerState state, BarcodeDesignerNotifier notifier, LabelDocument renderDoc, double scale, bool isDark, Product? currentSampleProduct, ) { final cfg = renderDoc.config; final canvasWidthPx = cfg.widthMm * scale; final canvasHeightPx = cfg.heightMm * scale; return Container( color: isDark ? const Color(0xFF090D16) : const Color(0xFFF1F5F9), child: Column( children: [ // Canvas Toolbar Controls Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: isDark ? const Color(0xFF111827) : Colors.white, border: Border( bottom: BorderSide( color: isDark ? Colors.white10 : Colors.grey.shade200, ), ), ), child: Row( children: [ FilterChip( label: const Text('Grid', style: TextStyle(fontSize: 11)), selected: state.showGrid, onSelected: (_) => notifier.toggleGrid(), avatar: const Icon(LucideIcons.grid, size: 14), ), const SizedBox(width: 8), FilterChip( label: const Text('Snap to Grid', style: TextStyle(fontSize: 11)), selected: state.snapToGrid, onSelected: (_) => notifier.toggleSnap(), avatar: const Icon(LucideIcons.magnet, size: 14), ), if (MediaQuery.of(context).size.width >= 700) ...[ const Spacer(), Text( 'Canvas: ${cfg.widthMm.round()} × ${cfg.heightMm.round()} mm' '${cfg.columnsAcross > 1 ? " • ${cfg.columnsAcross}-Across" : ""}', style: TextStyle(fontSize: 11, color: Colors.grey.shade500), ), ], ], ), ), // Scrollable Canvas Area with Physical Rulers Expanded( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => _canvasFocusNode.requestFocus(), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SingleChildScrollView( scrollDirection: Axis.vertical, child: Padding( padding: const EdgeInsets.all(40.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Top Horizontal Ruler (only in Design Mode) if (!state.isPreviewMode) Padding( padding: const EdgeInsets.only(left: 20.0), child: CanvasRuler( lengthMm: cfg.widthMm, scale: scale, isHorizontal: true, thickness: 18, ), ), Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Left Vertical Ruler (only in Design Mode) if (!state.isPreviewMode) CanvasRuler( lengthMm: cfg.heightMm, scale: scale, isHorizontal: false, thickness: 20, ), // Multi-Across Preview Mode or Interactive Single Canvas Box if (state.isPreviewMode && cfg.columnsAcross > 1) Row( mainAxisSize: MainAxisSize.min, children: List.generate(cfg.columnsAcross, (colIndex) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: canvasWidthPx, height: canvasHeightPx, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), boxShadow: const [ BoxShadow( color: Colors.black26, blurRadius: 10, offset: Offset(0, 3), ), ], ), child: Stack( clipBehavior: Clip.none, children: renderDoc.elements.map((elem) { return _CanvasElementWidget( key: ValueKey('${elem.id}_col_$colIndex'), elem: elem, scale: scale, isSelected: false, isPreviewMode: true, sampleProduct: currentSampleProduct ?? state.sampleProduct, sampleInventoryItem: state.sampleInventoryItem, notifier: notifier, onSelect: null, onBringToFront: null, onSendToBack: null, onBringForward: null, onSendBackward: null, onDuplicate: null, onDelete: null, renderContent: _renderElementContent, ); }).toList(), ), ), if (colIndex < cfg.columnsAcross - 1) Container( width: cfg.horizontalGapMm * scale, height: canvasHeightPx, alignment: Alignment.center, child: Container( width: 1.5, height: canvasHeightPx * 0.8, color: Colors.grey.shade400, ), ), ], ); }), ) else // The Physical Label Canvas Box DragTarget( onAcceptWithDetails: (details) { final box = context.findRenderObject() as RenderBox?; final localOffset = box != null ? box.globalToLocal(details.offset) : Offset.zero; final dropXMm = (localOffset.dx / scale).clamp(0.0, cfg.widthMm - 10.0); final dropYMm = (localOffset.dy / scale).clamp(0.0, cfg.heightMm - 5.0); final data = details.data; if (data is ElementType) { _addNewElementAt(data, dropXMm, dropYMm, notifier, cfg); } else if (data is DynamicFieldDefinition) { notifier.addBoundFieldElement( fieldDef: data, dropXMm: dropXMm, dropYMm: dropYMm, ); _canvasFocusNode.requestFocus(); } }, builder: (context, candidateData, rejectedData) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { notifier.selectElement(null); _canvasFocusNode.requestFocus(); }, child: Container( width: canvasWidthPx, height: canvasHeightPx, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), border: Border.all( color: candidateData.isNotEmpty ? Colors.blue : Colors.grey.shade400, width: candidateData.isNotEmpty ? 2.0 : 1.0, ), boxShadow: const [ BoxShadow( color: Colors.black26, blurRadius: 12, offset: Offset(0, 4), ), ], ), child: Stack( clipBehavior: Clip.none, children: [ // Grid background if (state.showGrid && !state.isPreviewMode) _buildGridOverlay(cfg.widthMm, cfg.heightMm, scale), // Render elements ...renderDoc.elements.map((elem) { final isSelected = elem.id == state.selectedElementId; return _CanvasElementWidget( key: ValueKey(elem.id), elem: elem, scale: scale, isSelected: isSelected, isPreviewMode: state.isPreviewMode, sampleProduct: currentSampleProduct ?? state.sampleProduct, sampleInventoryItem: state.sampleInventoryItem, notifier: notifier, onSelect: () => _canvasFocusNode.requestFocus(), onBringToFront: () => notifier.bringToFront(elem.id), onSendToBack: () => notifier.sendToBack(elem.id), onBringForward: () => notifier.bringForward(elem.id), onSendBackward: () => notifier.sendBackward(elem.id), onDuplicate: () => notifier.duplicateElement(elem.id), onDelete: () => notifier.deleteElement(elem.id), renderContent: _renderElementContent, ); }), ], ), ), ); }, ), ], ), ], ), ), ), ), ), ), ), ], ), ); } Widget _buildGridOverlay(double widthMm, double heightMm, double scale) { return CustomPaint( size: Size(widthMm * scale, heightMm * scale), painter: _GridPainter(scale: scale), ); } Alignment _toAlignment(TextAlign align) { switch (align) { case TextAlign.center: return Alignment.center; case TextAlign.right: case TextAlign.end: return Alignment.centerRight; case TextAlign.left: case TextAlign.start: case TextAlign.justify: return Alignment.centerLeft; } } bc.Barcode _getBarcodeSymbology(BarcodeType type) { switch (type) { case BarcodeType.ean13: return bc.Barcode.ean13(); case BarcodeType.code39: return bc.Barcode.code39(); case BarcodeType.upca: return bc.Barcode.upcA(); case BarcodeType.qrCode: return bc.Barcode.qrCode(); case BarcodeType.code128: return bc.Barcode.code128(); } } String _sanitizeBarcodeData(BarcodeType type, String rawData) { var data = rawData.trim(); if (data.isEmpty) data = '12345678'; switch (type) { case BarcodeType.ean13: final digits = data.replaceAll(RegExp(r'[^0-9]'), ''); if (digits.length >= 12) return digits.substring(0, 12); return digits.padLeft(12, '0'); case BarcodeType.upca: final digits = data.replaceAll(RegExp(r'[^0-9]'), ''); if (digits.length >= 11) return digits.substring(0, 11); return digits.padLeft(11, '0'); case BarcodeType.code39: final cleaned = data.toUpperCase().replaceAll(RegExp(r'[^0-9A-Z \-\.\$\/\+\%]'), ''); return cleaned.isEmpty ? 'CODE39' : cleaned; case BarcodeType.code128: case BarcodeType.qrCode: return data; } } Widget _renderElementContent( LabelElement elem, bool isPreviewMode, double scale, Product? sampleProduct, InventoryItem? sampleInventoryItem, ) { if (elem is TextElement) { return Container( alignment: _toAlignment(elem.alignment), padding: EdgeInsets.all(elem.paddingMm * scale * 0.3), child: Text( elem.text, textAlign: elem.alignment, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: (elem.fontSizePt * scale * 0.32).clamp(7.0, 48.0), fontWeight: elem.isBold ? FontWeight.bold : FontWeight.normal, fontStyle: elem.isItalic ? FontStyle.italic : FontStyle.normal, color: Colors.black, fontFamily: elem.fontFamily, height: 1.1, ), ), ); } else if (elem is DynamicTextElement) { final rawVal = TemplateDataResolver.resolveFieldValue( source: elem.source, fieldKey: elem.fieldKey, product: sampleProduct, inventoryItem: sampleInventoryItem, ); final isShowingRealData = rawVal != null; final displayText = isShowingRealData ? elem.formatValue(rawVal) : (elem.fieldKey.isNotEmpty ? '${elem.prefix ?? ''}{{${elem.source}.${elem.fieldKey}}}${elem.suffix ?? ''}' : '{{select_field}}'); return Container( alignment: _toAlignment(elem.alignment), padding: EdgeInsets.all(elem.paddingMm * scale * 0.3), child: Text( displayText, textAlign: elem.alignment, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: (elem.fontSizePt * scale * 0.32).clamp(7.0, 48.0), fontWeight: elem.isBold ? FontWeight.bold : FontWeight.normal, fontStyle: elem.isItalic ? FontStyle.italic : FontStyle.normal, color: isPreviewMode ? Colors.black : (isShowingRealData ? Colors.purple.shade900 : Colors.purple.shade700), fontFamily: isShowingRealData ? elem.fontFamily : 'monospace', height: 1.1, ), ), ); } else if (elem is BarcodeElement) { String barcodeValue = elem.staticValue; if (elem.valueType == 'dynamic') { final raw = TemplateDataResolver.resolveFieldValue( source: elem.source, fieldKey: elem.fieldKey, product: sampleProduct, inventoryItem: sampleInventoryItem, ); if (raw != null && raw.isNotEmpty) { barcodeValue = raw; } else { barcodeValue = elem.fieldKey.isNotEmpty ? '12345678' : elem.staticValue; } } final sanitized = _sanitizeBarcodeData(elem.barcodeType, barcodeValue); final barcodeSym = _getBarcodeSymbology(elem.barcodeType); return Padding( padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 1.0), child: bc.BarcodeWidget( barcode: barcodeSym, data: sanitized, drawText: elem.showText, style: TextStyle( fontSize: (scale * 1.8).clamp(6.0, 11.0), fontFamily: 'monospace', fontWeight: FontWeight.bold, color: Colors.black, ), color: Colors.black, errorBuilder: (context, error) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(LucideIcons.triangleAlert, size: 12, color: Colors.amber), Text( 'Invalid for ${elem.barcodeType.name.toUpperCase()}', style: const TextStyle(fontSize: 8, color: Colors.red), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ), ); } else if (elem is QrCodeElement) { String qrText = elem.staticValue; if (elem.valueType == 'dynamic') { final raw = TemplateDataResolver.resolveFieldValue( source: elem.source, fieldKey: elem.fieldKey, product: sampleProduct, inventoryItem: sampleInventoryItem, ); if (raw != null && raw.isNotEmpty) { qrText = raw; } else { qrText = elem.fieldKey.isNotEmpty ? 'https://kifi.app' : elem.staticValue; } } return Padding( padding: const EdgeInsets.all(2.0), child: bc.BarcodeWidget( barcode: bc.Barcode.qrCode(), data: qrText.isEmpty ? 'https://kifi.app' : qrText, drawText: false, color: Colors.black, errorBuilder: (context, error) => const Center( child: Icon(LucideIcons.qrCode, color: Colors.black), ), ), ); } else if (elem is RectangleElement) { return Container( decoration: BoxDecoration( color: elem.isFilled ? Colors.black : Colors.transparent, border: Border.all(color: Colors.black, width: 1.5), borderRadius: BorderRadius.circular(elem.cornerRadiusMm * scale), ), ); } else if (elem is LineElement) { return Center( child: Container( color: Colors.black, width: elem.isVertical ? 2.0 : double.infinity, height: elem.isVertical ? double.infinity : 2.0, ), ); } return const SizedBox(); } Widget _buildMobileLayout( BuildContext context, BarcodeDesignerState state, BarcodeDesignerNotifier notifier, LabelDocument renderDoc, double scale, bool isDark, Product? currentSampleProduct, ) { return Column( children: [ // Mobile Sub-bar: Mode Switch & Sample Product Picker Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isDark ? const Color(0xFF111827) : Colors.white, border: Border( bottom: BorderSide( color: isDark ? Colors.white10 : Colors.grey.shade200, ), ), ), child: Row( children: [ // Design vs Preview Mode Switch Container( padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2), decoration: BoxDecoration( color: isDark ? Colors.white10 : Colors.grey.shade200, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _buildModeButton( label: 'Design', icon: LucideIcons.draftingCompass, isSelected: !state.isPreviewMode, onTap: () => notifier.togglePreviewMode(false), isDark: isDark, ), _buildModeButton( label: 'Preview', icon: LucideIcons.eye, isSelected: state.isPreviewMode, onTap: () => notifier.togglePreviewMode(true), isDark: isDark, ), ], ), ), const SizedBox(width: 8), // Sample Product Selector Expanded( child: (currentSampleProduct ?? state.sampleProduct) != null ? InputChip( avatar: const Icon(LucideIcons.package, size: 14, color: Colors.blue), label: Text( (currentSampleProduct ?? state.sampleProduct)!.name, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), onDeleted: () => notifier.setSampleProduct(null), onPressed: () => _pickSampleProduct(context, notifier), ) : ActionChip( avatar: const Icon(LucideIcons.packagePlus, size: 14, color: Colors.blue), label: const Text('Sample Product', style: TextStyle(fontSize: 11), overflow: TextOverflow.ellipsis), onPressed: () => _pickSampleProduct(context, notifier), ), ), ], ), ), Expanded( child: _buildCenterCanvasArea(context, state, notifier, renderDoc, scale, isDark, currentSampleProduct), ), // Bottom Tool/Properties Panel (Collapsible on Mobile) AnimatedContainer( duration: const Duration(milliseconds: 200), height: _isMobileBottomPanelExpanded ? 260 : 44, decoration: BoxDecoration( color: isDark ? const Color(0xFF111827) : Colors.white, border: Border( top: BorderSide( color: isDark ? Colors.white10 : Colors.grey.shade200, ), ), ), child: DefaultTabController( length: 3, initialIndex: state.selectedElement != null ? 2 : 0, child: Column( children: [ Row( children: [ Expanded( child: TabBar( labelColor: Theme.of(context).colorScheme.primary, unselectedLabelColor: Colors.grey, indicatorColor: Theme.of(context).colorScheme.primary, labelPadding: const EdgeInsets.symmetric(horizontal: 4), tabs: [ const Tab( icon: Icon(LucideIcons.wrench, size: 15), text: 'Tools', ), const Tab( icon: Icon(LucideIcons.variable, size: 15), text: 'Variables', ), Tab( icon: Badge( isLabelVisible: state.selectedElement != null, smallSize: 8, child: const Icon(LucideIcons.slidersHorizontal, size: 15), ), text: state.selectedElement != null ? 'Properties ●' : 'Properties', ), ], ), ), IconButton( icon: Icon( _isMobileBottomPanelExpanded ? LucideIcons.chevronDown : LucideIcons.chevronUp, size: 18, color: Colors.grey.shade500, ), tooltip: _isMobileBottomPanelExpanded ? 'Collapse panel' : 'Expand panel', onPressed: () { setState(() => _isMobileBottomPanelExpanded = !_isMobileBottomPanelExpanded); }, ), ], ), if (_isMobileBottomPanelExpanded) Expanded( child: TabBarView( children: [ DesignerToolbox(onAddElement: (type) => _addNewElement(type, notifier, renderDoc.config)), ProductVariablesPanel(onAddField: (f) => notifier.addBoundFieldElement(fieldDef: f, dropXMm: 4, dropYMm: 6)), ElementPropertiesPanel( document: state.document, selectedElement: state.selectedElement, onUpdateElement: notifier.updateElement, onDeleteElement: notifier.deleteElement, onDuplicateElement: notifier.duplicateElement, onBringToFront: notifier.bringToFront, onSendToBack: notifier.sendToBack, onUpdateConfig: notifier.updateLabelConfig, ), ], ), ), ], ), ), ), ], ); } void _addNewElement(ElementType type, BarcodeDesignerNotifier notifier, LabelConfiguration cfg) { _addNewElementAt(type, 4.0, 6.0, notifier, cfg); } void _addNewElementAt( ElementType type, double xMm, double yMm, BarcodeDesignerNotifier notifier, LabelConfiguration cfg, ) { _canvasFocusNode.requestFocus(); final id = 'elem_${DateTime.now().millisecondsSinceEpoch}'; switch (type) { case ElementType.text: notifier.addElement( TextElement( id: id, xMm: xMm, yMm: yMm, widthMm: 24.0, heightMm: 4.5, text: 'kifi', fontSizePt: 9.0, isBold: true, ), ); break; case ElementType.dynamicText: notifier.addElement( DynamicTextElement( id: id, xMm: xMm, yMm: yMm, widthMm: 32.0, heightMm: 4.5, source: 'product', fieldKey: 'name', fontSizePt: 8.5, ), ); break; case ElementType.barcode: notifier.addElement( BarcodeElement( id: id, xMm: xMm, yMm: yMm, widthMm: 38.0, heightMm: 8.5, valueType: 'dynamic', source: 'product', fieldKey: 'barcode', showText: true, ), ); break; case ElementType.qrCode: notifier.addElement( QrCodeElement( id: id, xMm: xMm, yMm: yMm, widthMm: 12.0, heightMm: 12.0, valueType: 'dynamic', source: 'product', fieldKey: 'barcode', ), ); break; case ElementType.rectangle: notifier.addElement( RectangleElement( id: id, xMm: xMm, yMm: yMm, widthMm: 24.0, heightMm: 12.0, ), ); break; case ElementType.line: notifier.addElement( LineElement( id: id, xMm: xMm, yMm: yMm, widthMm: cfg.widthMm - 4.0, heightMm: 1.0, ), ); break; } } void _pickSampleProduct(BuildContext context, BarcodeDesignerNotifier notifier) async { ref.read(productsProvider.notifier).refresh(); final result = await ProductPickerDialog.show(context); if (result != null) { notifier.setSampleProduct(result.product, result.inventoryItem); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Selected sample: ${result.product.name}'), duration: const Duration(seconds: 2), ), ); } } void _showRenameDialog(BuildContext context, String currentName, BarcodeDesignerNotifier notifier) { final ctrl = TextEditingController(text: currentName); showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Template Name'), content: TextField( controller: ctrl, autofocus: true, decoration: const InputDecoration(labelText: 'Name'), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ElevatedButton( onPressed: () { if (ctrl.text.trim().isNotEmpty) { notifier.updateTemplateName(ctrl.text.trim()); } Navigator.pop(context); }, child: const Text('Save'), ), ], ), ); } Future _saveTemplateToBackend( BuildContext context, BarcodeDesignerState state, BarcodeDesignerNotifier notifier, ) async { setState(() => _isSaving = true); try { final template = BarcodeTemplate.fromLabelDocument( id: state.savedTemplateId, doc: state.document, ); final saved = await ref.read(barcodeTemplatesProvider.notifier).saveTemplate(template); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Template "${state.document.name}" saved successfully!'), backgroundColor: Colors.green, ), ); if (saved != null && saved.id != null) { notifier.loadDocument(state.document, saved.id); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error saving template: $e'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) setState(() => _isSaving = false); } } } class _GridPainter extends CustomPainter { final double scale; _GridPainter({required this.scale}); @override void paint(Canvas canvas, Size size) { final paint1mm = Paint() ..color = Colors.black.withValues(alpha: 0.04) ..strokeWidth = 0.5; final paint5mm = Paint() ..color = Colors.black.withValues(alpha: 0.1) ..strokeWidth = 1.0; for (double x = 0; x <= size.width; x += scale) { final is5 = (x / scale).round() % 5 == 0; canvas.drawLine(Offset(x, 0), Offset(x, size.height), is5 ? paint5mm : paint1mm); } for (double y = 0; y <= size.height; y += scale) { final is5 = (y / scale).round() % 5 == 0; canvas.drawLine(Offset(0, y), Offset(size.width, y), is5 ? paint5mm : paint1mm); } } @override bool shouldRepaint(covariant _GridPainter oldDelegate) => oldDelegate.scale != scale; } class _CanvasElementWidget extends StatefulWidget { final LabelElement elem; final double scale; final bool isSelected; final bool isPreviewMode; final Product? sampleProduct; final InventoryItem? sampleInventoryItem; final BarcodeDesignerNotifier notifier; final VoidCallback? onSelect; final VoidCallback? onBringToFront; final VoidCallback? onSendToBack; final VoidCallback? onBringForward; final VoidCallback? onSendBackward; final VoidCallback? onDuplicate; final VoidCallback? onDelete; final Widget Function( LabelElement elem, bool isPreviewMode, double scale, Product? sampleProduct, InventoryItem? sampleInventoryItem, ) renderContent; const _CanvasElementWidget({ super.key, required this.elem, required this.scale, required this.isSelected, required this.isPreviewMode, this.sampleProduct, this.sampleInventoryItem, required this.notifier, this.onSelect, this.onBringToFront, this.onSendToBack, this.onBringForward, this.onSendBackward, this.onDuplicate, this.onDelete, required this.renderContent, }); @override State<_CanvasElementWidget> createState() => _CanvasElementWidgetState(); } class _CanvasElementWidgetState extends State<_CanvasElementWidget> { double _dragStartX = 0.0; double _dragStartY = 0.0; double _accumDx = 0.0; double _accumDy = 0.0; double _resizeStartW = 0.0; double _resizeStartH = 0.0; double _accumResizeW = 0.0; double _accumResizeH = 0.0; void _showContextMenu(BuildContext context, Offset globalPos) { final isDark = Theme.of(context).brightness == Brightness.dark; showMenu( context: context, position: RelativeRect.fromLTRB( globalPos.dx, globalPos.dy, globalPos.dx + 1, globalPos.dy + 1, ), color: isDark ? const Color(0xFF1E293B) : Colors.white, elevation: 8, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide(color: isDark ? Colors.white12 : Colors.grey.shade200), ), items: [ PopupMenuItem( value: 'front', height: 34, child: Row( children: [ Icon(LucideIcons.arrowUpToLine, size: 15, color: isDark ? Colors.white70 : Colors.black87), const SizedBox(width: 8), const Expanded(child: Text('Bring to Front', style: TextStyle(fontSize: 12))), Text(']', style: TextStyle(fontSize: 11, color: isDark ? Colors.white38 : Colors.black38)), ], ), ), PopupMenuItem( value: 'forward', height: 34, child: Row( children: [ Icon(LucideIcons.arrowUp, size: 15, color: isDark ? Colors.white70 : Colors.black87), const SizedBox(width: 8), const Expanded(child: Text('Bring Forward', style: TextStyle(fontSize: 12))), ], ), ), PopupMenuItem( value: 'backward', height: 34, child: Row( children: [ Icon(LucideIcons.arrowDown, size: 15, color: isDark ? Colors.white70 : Colors.black87), const SizedBox(width: 8), const Expanded(child: Text('Send Backward', style: TextStyle(fontSize: 12))), ], ), ), PopupMenuItem( value: 'back', height: 34, child: Row( children: [ Icon(LucideIcons.arrowDownToLine, size: 15, color: isDark ? Colors.white70 : Colors.black87), const SizedBox(width: 8), const Expanded(child: Text('Send to Back', style: TextStyle(fontSize: 12))), Text('[', style: TextStyle(fontSize: 11, color: isDark ? Colors.white38 : Colors.black38)), ], ), ), const PopupMenuDivider(height: 8), PopupMenuItem( value: 'duplicate', height: 34, child: Row( children: [ Icon(LucideIcons.copy, size: 15, color: isDark ? Colors.white70 : Colors.black87), const SizedBox(width: 8), const Expanded(child: Text('Duplicate', style: TextStyle(fontSize: 12))), Text('Ctrl+D', style: TextStyle(fontSize: 11, color: isDark ? Colors.white38 : Colors.black38)), ], ), ), PopupMenuItem( value: 'delete', height: 34, child: const Row( children: [ Icon(LucideIcons.trash2, size: 15, color: Colors.redAccent), SizedBox(width: 8), Expanded(child: Text('Delete', style: TextStyle(fontSize: 12, color: Colors.redAccent))), Text('Del', style: TextStyle(fontSize: 11, color: Colors.redAccent)), ], ), ), ], ).then((value) { if (value == null) return; widget.onSelect?.call(); switch (value) { case 'front': widget.onBringToFront?.call(); break; case 'forward': widget.onBringForward?.call(); break; case 'backward': widget.onSendBackward?.call(); break; case 'back': widget.onSendToBack?.call(); break; case 'duplicate': widget.onDuplicate?.call(); break; case 'delete': widget.onDelete?.call(); break; } }); } Widget _buildQuickActionBtn({ required IconData icon, required String tooltip, required VoidCallback? onTap, required bool isDark, Color? color, }) { return Tooltip( message: tooltip, waitDuration: const Duration(milliseconds: 300), child: InkWell( borderRadius: BorderRadius.circular(4), onTap: () { widget.onSelect?.call(); onTap?.call(); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4), child: Icon( icon, size: 13, color: color ?? (isDark ? Colors.white70 : Colors.black87), ), ), ), ); } @override Widget build(BuildContext context) { final elem = widget.elem; final scale = widget.scale; final isSelected = widget.isSelected && !widget.isPreviewMode; final notifier = widget.notifier; final isDark = Theme.of(context).brightness == Brightness.dark; final left = elem.xMm * scale; final top = elem.yMm * scale; final width = elem.widthMm * scale; final height = elem.heightMm * scale; return Positioned( left: left, top: top, width: width, height: height, child: DragTarget( onAcceptWithDetails: widget.isPreviewMode ? null : (details) { notifier.bindFieldToElement(elem.id, details.data.source, details.data.fieldKey); }, builder: (context, candidateFields, rejectedData) { return MouseRegion( cursor: widget.isPreviewMode ? SystemMouseCursors.basic : SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: widget.isPreviewMode ? null : () { notifier.selectElement(elem.id); widget.onSelect?.call(); }, onSecondaryTapDown: widget.isPreviewMode ? null : (details) { notifier.selectElement(elem.id); widget.onSelect?.call(); _showContextMenu(context, details.globalPosition); }, onPanStart: widget.isPreviewMode ? null : (details) { notifier.selectElement(elem.id); widget.onSelect?.call(); notifier.recordUndo(); _dragStartX = elem.xMm; _dragStartY = elem.yMm; _accumDx = 0.0; _accumDy = 0.0; }, onPanUpdate: widget.isPreviewMode ? null : (details) { _accumDx += details.delta.dx / scale; _accumDy += details.delta.dy / scale; notifier.setElementPosition( elem.id, _dragStartX + _accumDx, _dragStartY + _accumDy, ); }, child: Stack( clipBehavior: Clip.none, children: [ // Visual Content Container Container( width: width, height: height, decoration: BoxDecoration( color: (candidateFields.isNotEmpty && !widget.isPreviewMode) ? Colors.purple.withValues(alpha: 0.15) : (isSelected ? Colors.blue.withValues(alpha: 0.04) : Colors.transparent), border: widget.isPreviewMode ? (elem.borderWidthMm > 0 ? Border.all( color: Colors.black, width: (elem.borderWidthMm * scale * 0.4).clamp(1.0, 4.0), ) : null) : Border.all( color: isSelected ? Colors.blue : (candidateFields.isNotEmpty ? Colors.purple : (elem.borderWidthMm > 0 ? Colors.black : Colors.blue.withValues(alpha: 0.25))), width: isSelected ? 1.5 : (elem.borderWidthMm > 0 ? (elem.borderWidthMm * scale * 0.4).clamp(1.0, 4.0) : 0.8), ), ), child: widget.renderContent( elem, widget.isPreviewMode, scale, widget.sampleProduct, widget.sampleInventoryItem, ), ), // Floating Quick Action Toolbar in Preview if (isSelected) Positioned( top: (top > 34) ? -32 : height + 4, left: 0, child: Material( elevation: 6, borderRadius: BorderRadius.circular(6), color: isDark ? const Color(0xFF1E293B) : Colors.white, shadowColor: Colors.black38, child: Container( height: 26, padding: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), border: Border.all( color: isDark ? Colors.white12 : Colors.grey.shade300, width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _buildQuickActionBtn( icon: LucideIcons.arrowUpToLine, tooltip: 'Bring to Front (])', onTap: widget.onBringToFront, isDark: isDark, ), _buildQuickActionBtn( icon: LucideIcons.arrowDownToLine, tooltip: 'Send to Back ([)', onTap: widget.onSendToBack, isDark: isDark, ), Container( width: 1, height: 12, margin: const EdgeInsets.symmetric(horizontal: 2), color: isDark ? Colors.white24 : Colors.grey.shade300, ), _buildQuickActionBtn( icon: LucideIcons.copy, tooltip: 'Duplicate (Ctrl+D)', onTap: widget.onDuplicate, isDark: isDark, ), _buildQuickActionBtn( icon: LucideIcons.trash2, tooltip: 'Delete (Del)', color: Colors.redAccent, onTap: widget.onDelete, isDark: isDark, ), ], ), ), ), ), // Selection Box and Corner Resize Handle if (isSelected) ...[ Positioned( right: -5, bottom: -5, child: MouseRegion( cursor: SystemMouseCursors.resizeDownRight, child: GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: (details) { widget.onSelect?.call(); notifier.recordUndo(); _resizeStartW = elem.widthMm; _resizeStartH = elem.heightMm; _accumResizeW = 0.0; _accumResizeH = 0.0; }, onPanUpdate: (details) { _accumResizeW += details.delta.dx / scale; _accumResizeH += details.delta.dy / scale; notifier.setElementSize( elem.id, _resizeStartW + _accumResizeW, _resizeStartH + _accumResizeH, ); }, child: Container( width: 12, height: 12, decoration: BoxDecoration( color: Colors.blue, border: Border.all(color: Colors.white, width: 2), borderRadius: BorderRadius.circular(3), boxShadow: const [ BoxShadow( color: Colors.black26, blurRadius: 3, offset: Offset(0, 1), ), ], ), ), ), ), ), ], ], ), ), ); }, ), ); } }