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,84 @@
import 'package:flutter/material.dart';
/// Central colour palette for Nearle POS.
///
/// Everything is derived from the brand primary `#662582` so that a single
/// change here re-skins the entire application.
class AppColors {
const AppColors._();
// ---------------------------------------------------------------- Brand
static const Color primary = Color(0xFF662582);
static const Color primaryDark = Color(0xFF4B1A60);
static const Color primaryLight = Color(0xFF8B4BA6);
static const Color primarySurface = Color(0xFFF4EDF7);
static const Color primaryBorder = Color(0xFFE3D3EC);
static const MaterialColor primarySwatch = MaterialColor(0xFF662582, {
50: Color(0xFFF4EDF7),
100: Color(0xFFE3D3EC),
200: Color(0xFFCDB1DC),
300: Color(0xFFB68FCC),
400: Color(0xFFA475C0),
500: Color(0xFF662582),
600: Color(0xFF5C2175),
700: Color(0xFF4B1A60),
800: Color(0xFF3B144B),
900: Color(0xFF2A0E36),
});
// -------------------------------------------------------------- Neutrals
static const Color background = Color(0xFFF7F7F9);
static const Color surface = Color(0xFFFFFFFF);
static const Color surfaceAlt = Color(0xFFFAFAFC);
static const Color border = Color(0xFFE8E8EE);
static const Color divider = Color(0xFFEFEFF4);
static const Color textPrimary = Color(0xFF17131C);
static const Color textSecondary = Color(0xFF6B6577);
static const Color textTertiary = Color(0xFF9A94A6);
static const Color textOnPrimary = Color(0xFFFFFFFF);
// ------------------------------------------------------------- Semantic
static const Color success = Color(0xFF16A34A);
static const Color successSurface = Color(0xFFEAF7EF);
static const Color warning = Color(0xFFF59E0B);
static const Color warningSurface = Color(0xFFFEF6E7);
static const Color danger = Color(0xFFDC2626);
static const Color dangerSurface = Color(0xFFFDECEC);
static const Color info = Color(0xFF2563EB);
static const Color infoSurface = Color(0xFFEAF0FE);
// ------------------------------------------------------- Membership tiers
static const Color tierBronze = Color(0xFFB08D57);
static const Color tierSilver = Color(0xFF8E96A3);
static const Color tierGold = Color(0xFFD4A017);
static const Color tierPlatinum = Color(0xFF4C5A6B);
// ---------------------------------------------------------------- Effects
static const Color glassTint = Color(0x14662582);
static const List<BoxShadow> shadowSm = [
BoxShadow(color: Color(0x0D17131C), blurRadius: 6, offset: Offset(0, 2)),
];
static const List<BoxShadow> shadowMd = [
BoxShadow(color: Color(0x1417131C), blurRadius: 16, offset: Offset(0, 6)),
];
static const List<BoxShadow> shadowLg = [
BoxShadow(color: Color(0x1F17131C), blurRadius: 32, offset: Offset(0, 12)),
];
static const LinearGradient primaryGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF7A2E9A), Color(0xFF4B1A60)],
);
static const LinearGradient glassGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0x40FFFFFF), Color(0x0DFFFFFF)],
);
}

View File

@@ -0,0 +1,83 @@
import 'package:flutter/widgets.dart';
/// Spacing scale — 4pt grid.
class AppSpacing {
const AppSpacing._();
static const double xxs = 2;
static const double xs = 4;
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double xl = 20;
static const double xxl = 24;
static const double xxxl = 32;
static const double huge = 40;
static const double giant = 56;
static const EdgeInsets pageDesktop = EdgeInsets.all(xxl);
static const EdgeInsets pageTablet = EdgeInsets.all(lg);
static const EdgeInsets card = EdgeInsets.all(lg);
}
/// Corner radii — brand standard is 16.
class AppRadius {
const AppRadius._();
static const double xs = 6;
static const double sm = 10;
static const double md = 12;
static const double lg = 16;
static const double xl = 20;
static const double xxl = 28;
static const double pill = 999;
static const BorderRadius brXs = BorderRadius.all(Radius.circular(xs));
static const BorderRadius brSm = BorderRadius.all(Radius.circular(sm));
static const BorderRadius brMd = BorderRadius.all(Radius.circular(md));
static const BorderRadius brLg = BorderRadius.all(Radius.circular(lg));
static const BorderRadius brXl = BorderRadius.all(Radius.circular(xl));
static const BorderRadius brPill = BorderRadius.all(Radius.circular(pill));
}
/// Minimum sizes tuned for finger targets on tablets.
class AppSizes {
const AppSizes._();
static const double touchTarget = 48;
static const double buttonHeight = 52;
static const double buttonHeightLarge = 64;
static const double inputHeight = 56;
static const double headerHeight = 76;
static const double customerBarHeight = 72;
static const double navItemHeight = 46;
static const double productCardAspect = 0.86;
static const double keypadKeySize = 76;
}
/// Motion durations & curves.
class AppMotion {
const AppMotion._();
static const Duration instant = Duration(milliseconds: 90);
static const Duration fast = Duration(milliseconds: 160);
static const Duration normal = Duration(milliseconds: 240);
static const Duration slow = Duration(milliseconds: 400);
static const Curve emphasized = Curves.easeOutCubic;
static const Curve standard = Curves.easeInOut;
static const Curve bouncy = Curves.easeOutBack;
}
/// Responsive breakpoints.
class AppBreakpoints {
const AppBreakpoints._();
static const double tabletPortrait = 920;
static const double tabletLandscape = 1300;
static const double desktop = 1650;
static bool isCompact(double w) => w < tabletPortrait;
static bool isMedium(double w) => w >= tabletPortrait && w < desktop;
static bool isExpanded(double w) => w >= desktop;
}

View File

@@ -0,0 +1,98 @@
import 'package:flutter/widgets.dart';
/// How the left navigation should render at the current width.
enum SidebarMode {
/// Off-canvas; reachable through the page-header menu button.
drawer,
/// Icons only, 84px.
rail,
/// Icons plus labels and section headers, 252px.
expanded,
}
/// How the bill should render at the current width.
enum BillingMode {
/// Bottom sheet, opened from the floating cart button.
sheet,
/// Docked column on the right.
docked,
}
/// Resolves every layout decision for the POS dashboard from a single width.
///
/// Keeping this in one place means the sidebar, product grid and billing panel
/// can never disagree about which breakpoint they are in.
class PosLayout {
const PosLayout({
required this.sidebar,
required this.billing,
required this.billingWidth,
required this.gridTileExtent,
required this.contentPadding,
});
final SidebarMode sidebar;
final BillingMode billing;
final double billingWidth;
/// Maximum width of a product card; the grid fits as many as will fit.
final double gridTileExtent;
final double contentPadding;
static const double railWidth = 84;
static const double expandedWidth = 252;
/// Below this the sidebar goes off-canvas.
static const double drawerBelow = 920;
/// Below this the sidebar is icons-only.
static const double railBelow = 1300;
/// Below this the bill becomes a bottom sheet.
static const double sheetBelow = 1120;
/// Above this we have room for a wider bill and larger cards.
static const double wideAbove = 1650;
factory PosLayout.of(BuildContext context) =>
PosLayout.forWidth(MediaQuery.sizeOf(context).width);
factory PosLayout.forWidth(double width) {
final sidebar = width < drawerBelow
? SidebarMode.drawer
: (width < railBelow ? SidebarMode.rail : SidebarMode.expanded);
final billing =
width < sheetBelow ? BillingMode.sheet : BillingMode.docked;
final billingWidth = width >= wideAbove ? 440.0 : 380.0;
// Cards stay finger-sized on tablets and grow a little on large desktops.
final tile = width < drawerBelow
? 168.0
: (width >= wideAbove ? 208.0 : 186.0);
final padding = width < drawerBelow ? 16.0 : 24.0;
return PosLayout(
sidebar: sidebar,
billing: billing,
billingWidth: billingWidth,
gridTileExtent: tile,
contentPadding: padding,
);
}
double get sidebarWidth => switch (sidebar) {
SidebarMode.drawer => 0,
SidebarMode.rail => railWidth,
SidebarMode.expanded => expandedWidth,
};
bool get sidebarIsDrawer => sidebar == SidebarMode.drawer;
bool get billingIsSheet => billing == BillingMode.sheet;
}

View File

@@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart';
import 'app_dimens.dart';
import 'app_typography.dart';
/// Builds the single source of truth [ThemeData] for Nearle POS.
class AppTheme {
const AppTheme._();
static ThemeData get light {
final colorScheme = ColorScheme.fromSeed(
seedColor: AppColors.primary,
primary: AppColors.primary,
onPrimary: AppColors.textOnPrimary,
secondary: AppColors.primaryLight,
surface: AppColors.surface,
onSurface: AppColors.textPrimary,
error: AppColors.danger,
brightness: Brightness.light,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
scaffoldBackgroundColor: AppColors.background,
textTheme: AppTypography.textTheme,
splashFactory: InkSparkle.splashFactory,
visualDensity: VisualDensity.standard,
appBarTheme: AppBarTheme(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.textOnPrimary,
elevation: 0,
centerTitle: false,
toolbarHeight: AppSizes.headerHeight,
systemOverlayStyle: SystemUiOverlayStyle.light,
titleTextStyle: AppTypography.textTheme.titleLarge
?.copyWith(color: AppColors.textOnPrimary),
),
cardTheme: CardThemeData(
color: AppColors.surface,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: AppRadius.brLg,
side: const BorderSide(color: AppColors.border),
),
),
dividerTheme: const DividerThemeData(
color: AppColors.divider,
thickness: 1,
space: 1,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.textOnPrimary,
disabledBackgroundColor: AppColors.border,
disabledForegroundColor: AppColors.textTertiary,
minimumSize: const Size(0, AppSizes.buttonHeight),
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xxl),
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: AppRadius.brMd),
textStyle: AppTypography.textTheme.labelLarge,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
minimumSize: const Size(0, AppSizes.buttonHeight),
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xxl),
side: const BorderSide(color: AppColors.primaryBorder, width: 1.5),
shape: const RoundedRectangleBorder(borderRadius: AppRadius.brMd),
textStyle: AppTypography.textTheme.labelLarge,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.primary,
minimumSize: const Size(0, AppSizes.touchTarget),
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
shape: const RoundedRectangleBorder(borderRadius: AppRadius.brSm),
textStyle: AppTypography.textTheme.labelLarge,
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surface,
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.lg,
),
hintStyle: AppTypography.textTheme.bodyMedium
?.copyWith(color: AppColors.textTertiary),
labelStyle: AppTypography.textTheme.bodyMedium
?.copyWith(color: AppColors.textSecondary),
border: _inputBorder(AppColors.border),
enabledBorder: _inputBorder(AppColors.border),
focusedBorder: _inputBorder(AppColors.primary, width: 1.8),
errorBorder: _inputBorder(AppColors.danger),
focusedErrorBorder: _inputBorder(AppColors.danger, width: 1.8),
),
chipTheme: ChipThemeData(
backgroundColor: AppColors.surface,
selectedColor: AppColors.primary,
side: const BorderSide(color: AppColors.border),
labelStyle: AppTypography.textTheme.labelMedium,
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.md,
),
shape: const RoundedRectangleBorder(borderRadius: AppRadius.brPill),
),
dialogTheme: DialogThemeData(
backgroundColor: AppColors.surface,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.xl),
),
titleTextStyle: AppTypography.textTheme.headlineSmall,
),
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.textPrimary,
contentTextStyle: AppTypography.textTheme.bodyMedium
?.copyWith(color: Colors.white),
behavior: SnackBarBehavior.floating,
shape: const RoundedRectangleBorder(borderRadius: AppRadius.brMd),
),
scrollbarTheme: ScrollbarThemeData(
thickness: WidgetStateProperty.all(6),
radius: const Radius.circular(AppRadius.pill),
thumbColor: WidgetStateProperty.all(AppColors.primaryBorder),
),
pageTransitionsTheme: const PageTransitionsTheme(builders: {
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.macOS: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
}),
);
}
static OutlineInputBorder _inputBorder(Color color, {double width = 1}) {
return OutlineInputBorder(
borderRadius: AppRadius.brMd,
borderSide: BorderSide(color: color, width: width),
);
}
}

View File

@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart';
/// Poppins-based type scale.
///
/// Poppins is geometric and runs slightly wide, so headings get negative
/// tracking to keep them compact in the sidebar and page header. POS screens
/// are read at arm's length, so body text starts at 15 rather than the
/// Material default of 14.
class AppTypography {
const AppTypography._();
static TextTheme get textTheme {
final base = GoogleFonts.poppinsTextTheme();
return base.copyWith(
displayLarge: _s(base.displayLarge, 44, FontWeight.w700, -1.2),
displayMedium: _s(base.displayMedium, 36, FontWeight.w700, -1.0),
displaySmall: _s(base.displaySmall, 30, FontWeight.w700, -0.8),
headlineLarge: _s(base.headlineLarge, 28, FontWeight.w600, -0.7),
headlineMedium: _s(base.headlineMedium, 24, FontWeight.w600, -0.6),
headlineSmall: _s(base.headlineSmall, 20, FontWeight.w600, -0.4),
titleLarge: _s(base.titleLarge, 18, FontWeight.w600, -0.3),
titleMedium: _s(base.titleMedium, 15.5, FontWeight.w600, -0.2),
titleSmall: _s(base.titleSmall, 13.5, FontWeight.w600, -0.1),
bodyLarge: _s(base.bodyLarge, 15.5, FontWeight.w400, 0),
bodyMedium: _s(base.bodyMedium, 14.5, FontWeight.w400, 0),
bodySmall: _s(base.bodySmall, 12.5, FontWeight.w400, 0,
color: AppColors.textSecondary),
labelLarge: _s(base.labelLarge, 14.5, FontWeight.w600, 0),
labelMedium: _s(base.labelMedium, 12.5, FontWeight.w600, 0.1),
labelSmall: _s(base.labelSmall, 10.5, FontWeight.w600, 0.6,
color: AppColors.textTertiary),
);
}
static TextStyle _s(
TextStyle? base,
double size,
FontWeight weight,
double tracking, {
Color color = AppColors.textPrimary,
}) {
return (base ?? const TextStyle()).copyWith(
fontSize: size,
fontWeight: weight,
letterSpacing: tracking,
color: color,
height: 1.35,
);
}
/// Tabular figures — essential so totals don't jitter as quantities change.
static TextStyle money(double size,
{FontWeight weight = FontWeight.w700, Color? color}) =>
GoogleFonts.poppins(
fontSize: size,
fontWeight: weight,
color: color ?? AppColors.textPrimary,
letterSpacing: -0.5,
height: 1.15,
fontFeatures: const [FontFeature.tabularFigures()],
);
/// Fixed-pitch, used only for the receipt preview.
static TextStyle mono(double size, {Color? color}) => GoogleFonts.robotoMono(
fontSize: size,
color: color ?? AppColors.textTertiary,
letterSpacing: 0.2,
);
/// Small uppercase caption for sidebar section dividers.
static TextStyle sectionLabel() => GoogleFonts.poppins(
fontSize: 10.5,
fontWeight: FontWeight.w600,
letterSpacing: 1.0,
color: AppColors.textTertiary,
);
}