second commit

This commit is contained in:
2026-07-29 11:41:53 +05:30
parent fcccf22bac
commit d72522e737
211 changed files with 19260 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
class EmptyState extends StatelessWidget {
const EmptyState({
super.key,
required this.title,
this.message,
this.emoji = '🛒',
this.action,
this.compact = false,
});
final String title;
final String? message;
final String emoji;
final Widget? action;
final bool compact;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.xxl),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: compact ? 64 : 96,
height: compact ? 64 : 96,
decoration: const BoxDecoration(
color: AppColors.primarySurface,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(emoji,
style: TextStyle(fontSize: compact ? 28 : 40)),
),
SizedBox(height: compact ? AppSpacing.lg : AppSpacing.xxl),
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: compact ? 15 : 18,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
if (message != null) ...[
const SizedBox(height: AppSpacing.sm),
Text(
message!,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
height: 1.45,
),
),
],
if (action != null) ...[
const SizedBox(height: AppSpacing.xxl),
action!,
],
],
),
),
);
}
}

View File

@@ -0,0 +1,85 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
/// Glassmorphism-inspired surface used for elevated panels and product cards.
class GlassCard extends StatelessWidget {
const GlassCard({
super.key,
required this.child,
this.padding = AppSpacing.card,
this.radius = AppRadius.lg,
this.blur = 0,
this.tinted = false,
this.borderColor,
this.shadows,
this.onTap,
this.width,
this.height,
});
final Widget child;
final EdgeInsetsGeometry padding;
final double radius;
/// Backdrop blur strength. Zero renders an opaque card, which is cheaper and
/// is the right default for the dense product grid.
final double blur;
final bool tinted;
final Color? borderColor;
final List<BoxShadow>? shadows;
final VoidCallback? onTap;
final double? width;
final double? height;
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(radius);
Widget surface = AnimatedContainer(
duration: AppMotion.fast,
width: width,
height: height,
padding: padding,
decoration: BoxDecoration(
color: tinted
? AppColors.primarySurface.withValues(alpha: blur > 0 ? 0.75 : 1)
: AppColors.surface.withValues(alpha: blur > 0 ? 0.78 : 1),
borderRadius: borderRadius,
border: Border.all(
color: borderColor ??
(tinted ? AppColors.primaryBorder : AppColors.border),
),
boxShadow: shadows ?? AppColors.shadowSm,
),
child: child,
);
if (blur > 0) {
surface = ClipRRect(
borderRadius: borderRadius,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
child: surface,
),
);
}
if (onTap == null) return surface;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: borderRadius,
splashColor: AppColors.primary.withValues(alpha: 0.08),
highlightColor: AppColors.primary.withValues(alpha: 0.04),
child: surface,
),
);
}
}

View File

@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
/// Large on-screen keypad for mobile numbers and cash amounts.
class NumericKeypad extends StatelessWidget {
const NumericKeypad({
super.key,
required this.onKey,
required this.onBackspace,
this.onClear,
this.onSubmit,
this.submitLabel,
this.allowDecimal = false,
this.maxWidth = 360,
});
final ValueChanged<String> onKey;
final VoidCallback onBackspace;
final VoidCallback? onClear;
final VoidCallback? onSubmit;
final String? submitLabel;
final bool allowDecimal;
final double maxWidth;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final row in const [
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
])
_row(row.map((d) => _digit(d)).toList()),
_row([
allowDecimal
? _digit('.')
: _action(
icon: Icons.clear_all_rounded,
onTap: onClear,
tone: AppColors.textSecondary,
),
_digit('0'),
_action(
icon: Icons.backspace_outlined,
onTap: onBackspace,
tone: AppColors.danger,
),
]),
if (onSubmit != null) ...[
const SizedBox(height: AppSpacing.md),
SizedBox(
width: double.infinity,
height: AppSizes.buttonHeight,
child: FilledButton.icon(
onPressed: onSubmit,
icon: const Icon(Icons.check_rounded),
label: Text(submitLabel ?? 'Done'),
),
),
],
],
),
);
}
Widget _row(List<Widget> children) => Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.md),
child: Row(
children: [
for (var i = 0; i < children.length; i++) ...[
Expanded(child: children[i]),
if (i != children.length - 1)
const SizedBox(width: AppSpacing.md),
],
],
),
);
Widget _digit(String value) => _Key(
onTap: () {
HapticFeedback.selectionClick();
onKey(value);
},
child: Text(
value,
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
);
Widget _action({
required IconData icon,
required VoidCallback? onTap,
required Color tone,
}) =>
_Key(
onTap: onTap == null
? null
: () {
HapticFeedback.lightImpact();
onTap();
},
child: Icon(icon, size: 24, color: tone),
);
}
class _Key extends StatelessWidget {
const _Key({required this.child, this.onTap});
final Widget child;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
height: AppSizes.keypadKeySize,
child: Material(
color: AppColors.surface,
borderRadius: AppRadius.brMd,
child: InkWell(
onTap: onTap,
borderRadius: AppRadius.brMd,
splashColor: AppColors.primary.withValues(alpha: 0.1),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: AppRadius.brMd,
border: Border.all(color: AppColors.border),
),
child: child,
),
),
),
);
}
}

View File

@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
enum ButtonTone { primary, neutral, success, danger, ghost }
/// Large, touch-first action button with built-in busy state.
class PrimaryButton extends StatelessWidget {
const PrimaryButton({
super.key,
required this.label,
this.onPressed,
this.icon,
this.tone = ButtonTone.primary,
this.expanded = true,
this.large = false,
this.busy = false,
this.trailing,
});
final String label;
final VoidCallback? onPressed;
final IconData? icon;
final ButtonTone tone;
final bool expanded;
final bool large;
final bool busy;
/// Optional right-aligned widget, typically the bill total.
final Widget? trailing;
@override
Widget build(BuildContext context) {
final enabled = onPressed != null && !busy;
final (bg, fg, border) = _palette;
final height =
large ? AppSizes.buttonHeightLarge : AppSizes.buttonHeight;
final content = busy
? SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(strokeWidth: 2.4, color: fg),
)
: Row(
mainAxisSize: expanded ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: trailing != null
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: large ? 24 : 20, color: fg),
const SizedBox(width: AppSpacing.sm),
],
Flexible(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: fg,
fontSize: large ? 19 : 16,
fontWeight: FontWeight.w700,
letterSpacing: 0.1,
),
),
),
],
),
if (trailing != null) trailing!,
],
);
return SizedBox(
width: expanded ? double.infinity : null,
height: height,
child: Material(
color: enabled ? bg : AppColors.border,
borderRadius: AppRadius.brMd,
child: InkWell(
onTap: enabled ? onPressed : null,
borderRadius: AppRadius.brMd,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: large ? AppSpacing.xxl : AppSpacing.xl,
),
decoration: BoxDecoration(
borderRadius: AppRadius.brMd,
border: border == null
? null
: Border.all(color: border, width: 1.5),
),
alignment: Alignment.center,
child: DefaultTextStyle.merge(
style: TextStyle(color: enabled ? fg : AppColors.textTertiary),
child: content,
),
),
),
),
);
}
(Color, Color, Color?) get _palette => switch (tone) {
ButtonTone.primary => (
AppColors.primary,
AppColors.textOnPrimary,
null
),
ButtonTone.success => (AppColors.success, Colors.white, null),
ButtonTone.danger => (AppColors.danger, Colors.white, null),
ButtonTone.neutral => (
AppColors.surface,
AppColors.textPrimary,
AppColors.border
),
ButtonTone.ghost => (
AppColors.primarySurface,
AppColors.primary,
AppColors.primaryBorder
),
};
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
import '../../domain/entities/customer.dart';
/// Compact labelled badge — stock states, tiers, live indicator.
class StatusPill extends StatelessWidget {
const StatusPill({
super.key,
required this.label,
this.color = AppColors.primary,
this.background,
this.icon,
this.dense = false,
});
final String label;
final Color color;
final Color? background;
final IconData? icon;
final bool dense;
factory StatusPill.tier(MembershipTier tier, {bool dense = false}) {
final color = switch (tier) {
MembershipTier.bronze => AppColors.tierBronze,
MembershipTier.silver => AppColors.tierSilver,
MembershipTier.gold => AppColors.tierGold,
MembershipTier.platinum => AppColors.tierPlatinum,
};
return StatusPill(
label: tier.label.toUpperCase(),
color: color,
background: color.withValues(alpha: 0.12),
dense: dense,
);
}
factory StatusPill.stock(double stock, {required int lowThreshold}) {
if (stock <= 0) {
return const StatusPill(
label: 'Out of stock',
color: AppColors.danger,
background: AppColors.dangerSurface,
dense: true,
);
}
if (stock <= lowThreshold) {
return StatusPill(
label: '${stock.toStringAsFixed(0)} left',
color: AppColors.warning,
background: AppColors.warningSurface,
dense: true,
);
}
return StatusPill(
label: '${stock.toStringAsFixed(0)} in stock',
color: AppColors.textTertiary,
background: Colors.transparent,
dense: true,
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: dense ? AppSpacing.sm : AppSpacing.md,
vertical: dense ? 3 : AppSpacing.xs + 2,
),
decoration: BoxDecoration(
color: background ?? color.withValues(alpha: 0.12),
borderRadius: AppRadius.brPill,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: dense ? 11 : 13, color: color),
const SizedBox(width: AppSpacing.xs),
],
Text(
label,
style: TextStyle(
color: color,
fontSize: dense ? 10.5 : 12,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
],
),
);
}
}