update theme

This commit is contained in:
Phuoc Nguyen
2025-12-02 15:20:54 +07:00
parent 12bd70479c
commit 49a41d24eb
78 changed files with 3263 additions and 2756 deletions

View File

@@ -0,0 +1,92 @@
/// Widget: Description Item
///
/// Displays a label-value pair for design request details.
library;
import 'package:flutter/material.dart';
/// Description Item Widget
///
/// Shows a label and value pair with:
/// - Inline layout (label: value) for single line
/// - Stacked layout for multi-line values
/// - Theme-aware colors
class DescriptionItem extends StatelessWidget {
const DescriptionItem({
super.key,
required this.label,
required this.value,
this.isMultiLine = false,
});
final String label;
final String value;
final bool isMultiLine;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
if (isMultiLine) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 15,
color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
height: 1.6,
),
),
],
);
}
return Container(
padding: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: colorScheme.surfaceContainerHighest),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
label,
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 15,
color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
height: 1.6,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,67 @@
/// Widget: File Item
///
/// Displays a file attachment item with icon and name.
library;
import 'package:flutter/material.dart';
/// File Item Widget
///
/// Shows a file attachment with:
/// - File type icon
/// - File name extracted from URL
/// - Theme-aware styling
class FileItem extends StatelessWidget {
const FileItem({
super.key,
required this.fileUrl,
required this.icon,
});
final String fileUrl;
final IconData icon;
String get fileName {
final uri = Uri.parse(fileUrl);
return uri.pathSegments.isNotEmpty ? uri.pathSegments.last : fileUrl;
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: colorScheme.primary,
borderRadius: BorderRadius.circular(6),
),
child: Icon(icon, color: colorScheme.onPrimary, size: 14),
),
const SizedBox(width: 12),
Expanded(
child: Text(
fileName,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,101 @@
/// Widget: File Preview Item
///
/// Displays a file preview with icon, name, size, and remove button.
library;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:worker/core/theme/colors.dart';
/// File Preview Item Widget
///
/// Shows uploaded file with:
/// - File type icon (PDF, image, etc.)
/// - File name
/// - File size
/// - Remove button
class FilePreviewItem extends StatelessWidget {
const FilePreviewItem({
super.key,
required this.file,
required this.onRemove,
});
final PlatformFile file;
final VoidCallback onRemove;
IconData _getFileIcon() {
final extension = file.extension?.toLowerCase();
if (extension == 'pdf') return Icons.picture_as_pdf;
if (extension == 'jpg' || extension == 'jpeg' || extension == 'png') {
return Icons.image;
}
return Icons.insert_drive_file;
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: colorScheme.primary,
borderRadius: BorderRadius.circular(6),
),
child: Icon(_getFileIcon(), color: colorScheme.surface, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
file.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
_formatFileSize(file.size),
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, size: 20),
color: AppColors.danger,
onPressed: onRemove,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
),
],
),
);
}
}

View File

@@ -0,0 +1,104 @@
/// Widget: Form Field
///
/// Reusable form field with label, validation, and theming support.
library;
import 'package:flutter/material.dart';
import 'package:worker/core/theme/colors.dart';
/// Form Field Widget
///
/// A styled text form field with:
/// - Label with optional required indicator
/// - Hint text
/// - Theme-aware borders
/// - Validation support
class FormFieldWidget extends StatelessWidget {
const FormFieldWidget({
super.key,
required this.label,
this.required = false,
required this.controller,
required this.hint,
this.keyboardType,
this.validator,
});
final String label;
final bool required;
final TextEditingController controller;
final String hint;
final TextInputType? keyboardType;
final String? Function(String?)? validator;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
children: required
? const [
TextSpan(
text: ' *',
style: TextStyle(color: AppColors.danger),
),
]
: null,
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 2),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
validator: validator,
),
],
);
}
}

View File

@@ -0,0 +1,132 @@
/// Widget: Image Viewer Dialog
///
/// Full-screen image viewer with swipe navigation.
library;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
/// Image Viewer Dialog
///
/// Full-screen dialog for viewing images with:
/// - PageView for swipe navigation
/// - Image counter (1/5)
/// - Close button
/// - Gradient overlay for visibility
class ImageViewerDialog extends StatefulWidget {
const ImageViewerDialog({
super.key,
required this.images,
required this.initialIndex,
});
final List<ProjectFile> images;
final int initialIndex;
@override
State<ImageViewerDialog> createState() => _ImageViewerDialogState();
}
class _ImageViewerDialogState extends State<ImageViewerDialog> {
late PageController _pageController;
late int _currentIndex;
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Container(
color: Colors.black,
child: Stack(
children: [
// Main PageView
Center(
child: PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemCount: widget.images.length,
itemBuilder: (context, index) {
return Center(
child: CachedNetworkImage(
imageUrl: widget.images[index].fileUrl,
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Colors.white),
),
errorWidget: (context, url, error) => const Icon(
Icons.error,
color: Colors.white,
size: 48,
),
),
);
},
),
),
// Top bar with counter and close button
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
],
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_currentIndex + 1} / ${widget.images.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
],
),
),
),
),
],
),
),
);
}
}