diff --git a/docs/development/build_execution_tracker.md b/docs/development/build_execution_tracker.md index c3e40d4..4f54a8b 100644 --- a/docs/development/build_execution_tracker.md +++ b/docs/development/build_execution_tracker.md @@ -2,9 +2,9 @@ ## Current status -- main baseline updated through: list-efficiency-improvements (Stage 3 complete) -- main baseline commit: merge of `feat/list-efficiency-improvements` (2026-05-22) -- next branch: feat/design-system-shared-widgets +- main baseline updated through: design-system-shared-widgets (Stage 4A complete) +- main baseline commit: merge of `feat/design-system-shared-widgets` (2026-05-22) +- next branch: feat/shared-composition-pattern - current stage: Stage 4 — Platform foundations and cross-platform readiness ## Slice tracker @@ -89,9 +89,25 @@ ### feat/design-system-shared-widgets -- status: queued (Stage 4A — next) -- inspection: pending -- implementation: pending -- tests: pending -- analyze: pending -- brief updated: no +- status: merged to main +- date: 2026-05-22 +- inspection: complete +- implementation: complete +- files changed: + - `design_system/lib/design_system.dart` — expanded barrel exports with typography, layout, and 5 new widgets + - `design_system/lib/src/theme/kc_typography.dart` — new shared typography scale (KcTypography) with full Material 3 text style hierarchy and `applyKcTypography()` helper + - `design_system/lib/src/theme/kc_theme.dart` — updated to use `KcTypography.applyKcTypography()` instead of inline text styles + - `design_system/lib/src/layout/kc_breakpoints.dart` — new responsive breakpoint utilities (KcBreakpoints) with compact/medium/expanded/large queries and grid column helper + - `design_system/lib/src/widgets/kc_empty_state.dart` — migrated from kell_web EmptyStatePanel as KcEmptyState + - `design_system/lib/src/widgets/kc_section_header.dart` — migrated from kell_web SectionHeader as KcSectionHeader + - `design_system/lib/src/widgets/kc_summary_card.dart` — migrated from kell_web SummaryCard as KcSummaryCard + - `design_system/lib/src/widgets/kc_loading_state.dart` — new shared loading state widget + - `design_system/lib/src/widgets/kc_error_state.dart` — new shared error state widget with optional retry + - `design_system/test/design_system_test.dart` — expanded from 3 to 41 tests covering all new widgets, typography, breakpoints, colors, spacing + - `kell_web/lib/pages/dashboard_page.dart` — updated imports to use design_system widgets directly (KcSectionHeader, KcSummaryCard, KcEmptyState) + - `kell_web/lib/shell/widgets/empty_state_panel.dart` — replaced with backward-compatible typedef re-export + - `kell_web/lib/shell/widgets/section_header.dart` — replaced with backward-compatible typedef re-export + - `kell_web/lib/shell/widgets/summary_card.dart` — replaced with backward-compatible typedef re-export +- tests: passed (41/41 design_system, 294/294 feature_wordpress, 24/24 kell_web) +- analyze: passed (dart analyze — no issues found in design_system and kell_web) +- brief updated: yes diff --git a/docs/development/master_development_brief.md b/docs/development/master_development_brief.md index 1f37a09..21bb4e6 100644 --- a/docs/development/master_development_brief.md +++ b/docs/development/master_development_brief.md @@ -92,6 +92,7 @@ Rules: - ✅ Publishing workflow UX hardening landed (Stage 2B complete — merged `feat/publishing-ux-hardening` → `main` at `b81016d`, 2026-04-11). Stage 2 complete. - ✅ Multi-select groundwork landed (Stage 3A complete — merged `feat/multi-select-groundwork` → `main`, 2026-05-22). - ✅ List efficiency improvements landed (Stage 3B complete — merged `feat/list-efficiency-improvements` → `main`, 2026-05-22). Stage 3 complete. +- ✅ Design system expansion and shared widget migration landed (Stage 4A complete — merged `feat/design-system-shared-widgets` → `main`, 2026-05-22). ### Current narrow edit capabilities on `main` @@ -104,16 +105,18 @@ Rules: ### Latest known validation state on `main` - `dart analyze` clean +- `design_system` tests passing - `feature_wordpress` tests passing -- `kell_web` dashboard tests passing +- `kell_web` tests passing +- latest reported count for `design_system`: `41/41 passed` - latest reported count for `feature_wordpress`: `294/294 passed` -- latest reported count for `kell_web` dashboard tests: `5/5 passed` -- baseline commit: merge of `feat/list-efficiency-improvements` (2026-05-22) +- latest reported count for `kell_web`: `24/24 passed` +- baseline commit: merge of `feat/design-system-shared-widgets` (2026-05-22) ### Next recommended branch -**`feat/design-system-shared-widgets`** — Stage 4A: Design system expansion and shared widget migration. -Branch from latest `main`. Stage 3 (web application operator efficiency) is complete. +**`feat/flutter-cicd`** — Stage 4B: Cross-platform shell composition strategy. +Branch from latest `main`. Stage 4A (design system expansion) is complete. --- @@ -223,27 +226,10 @@ Invest in shared foundations now so that the Android expansion (Stage 5) and all - `feat/flutter-cicd` - `feat/shared-composition-pattern` -#### Stage 4A — Design system expansion and shared widget migration +#### ~~Stage 4A — Design system expansion and shared widget migration~~ ✅ COMPLETE -##### Goal - -Expand the `design_system` package with reusable widgets and responsive layout primitives so both `kell_web` and `kell_mobile` share a common component library. - -##### Requirements - -- migrate `EmptyStatePanel`, `SectionHeader`, and `SummaryCard` from `kell_web/lib/shell/widgets/` into `design_system` -- add shared typography scale to `design_system` -- add responsive layout breakpoint utilities for web vs mobile -- add shared loading state and error state widget patterns -- update `kell_web` imports to reference `design_system` instead of local shell widgets -- preserve existing visual behavior — no regressions - -##### Definition of done - -- shared widgets live in `design_system` package -- `kell_web` references `design_system` for all migrated widgets -- responsive layout utilities exist for future mobile use -- analyze clean, existing tests passing +> Merged `feat/design-system-shared-widgets` → `main` (2026-05-22). +> Migrated `EmptyStatePanel`, `SectionHeader`, and `SummaryCard` from `kell_web/lib/shell/widgets/` into `design_system` as `KcEmptyState`, `KcSectionHeader`, and `KcSummaryCard`. Added `KcTypography` shared typography scale, `KcBreakpoints` responsive layout breakpoint utilities, `KcLoadingState` and `KcErrorState` shared state widgets. Updated `kell_web` dashboard to use design_system widgets directly; shell widget files now contain backward-compatible typedefs. Theme updated to use `KcTypography.applyKcTypography()`. 41 new design_system tests added (41 total `design_system` tests, 294 `feature_wordpress` tests, 24 `kell_web` tests — all passing). Analyze clean. #### Stage 4B — Cross-platform shell composition strategy @@ -592,17 +578,17 @@ Working rules: ## Appendix: Feature maturity matrix -| Package | Domain Layer | Application Layer | Data (Fake) | Data (Real) | Presentation | Tests | Maturity | -| ------------------- | ------------ | ----------------- | ----------- | -------------- | ------------------------------ | ------- | -------------------- | -| `feature_wordpress` | ✅ Complete | ✅ Complete | ✅ Complete | ✅ WooCommerce | ✅ Complete | 294 | **Production-ready** | -| `feature_inventory` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Minimal | **Fake-only MVP** | -| `feature_orders` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Some | **Fake-only MVP** | -| `feature_policy` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Minimal | **Fake-only MVP** | -| `feature_finance` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | ❌ Stub | None | **Scaffolded only** | -| `feature_mrp` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | ❌ Stub | None | **Scaffolded only** | -| `feature_social` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | ❌ Stub | None | **Scaffolded only** | -| `auth` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | N/A | None | **Scaffolded only** | -| `data` | ❌ Stub | N/A | N/A | ❌ None | N/A | None | **Scaffolded only** | -| `integrations` | ❌ Stub | N/A | N/A | ❌ None | N/A | None | **Scaffolded only** | -| `design_system` | N/A | N/A | N/A | N/A | ✅ Partial (theme + 2 widgets) | Minimal | **Foundation only** | -| `core` | ✅ Partial | N/A | N/A | N/A | N/A | Minimal | **Foundation only** | +| Package | Domain Layer | Application Layer | Data (Fake) | Data (Real) | Presentation | Tests | Maturity | +| ------------------- | ------------ | ----------------- | ----------- | -------------- | -------------------------------------------------- | ------- | -------------------- | +| `feature_wordpress` | ✅ Complete | ✅ Complete | ✅ Complete | ✅ WooCommerce | ✅ Complete | 294 | **Production-ready** | +| `feature_inventory` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Minimal | **Fake-only MVP** | +| `feature_orders` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Some | **Fake-only MVP** | +| `feature_policy` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Minimal | **Fake-only MVP** | +| `feature_finance` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | ❌ Stub | None | **Scaffolded only** | +| `feature_mrp` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | ❌ Stub | None | **Scaffolded only** | +| `feature_social` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | ❌ Stub | None | **Scaffolded only** | +| `auth` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | N/A | None | **Scaffolded only** | +| `data` | ❌ Stub | N/A | N/A | ❌ None | N/A | None | **Scaffolded only** | +| `integrations` | ❌ Stub | N/A | N/A | ❌ None | N/A | None | **Scaffolded only** | +| `design_system` | N/A | N/A | N/A | N/A | ✅ Expanded (theme, typography, layout, 7 widgets) | 41 | **Foundation ready** | +| `core` | ✅ Partial | N/A | N/A | N/A | N/A | Minimal | **Foundation only** | diff --git a/kell_creations_apps/apps/kell_web/lib/pages/dashboard_page.dart b/kell_creations_apps/apps/kell_web/lib/pages/dashboard_page.dart index 31c6962..84b2640 100644 --- a/kell_creations_apps/apps/kell_web/lib/pages/dashboard_page.dart +++ b/kell_creations_apps/apps/kell_web/lib/pages/dashboard_page.dart @@ -5,9 +5,6 @@ import '../dashboard/application/dashboard_controller.dart'; import '../dashboard/domain/dashboard_summary.dart'; import '../navigation/app_navigation.dart'; import '../routing/app_routes.dart'; -import '../shell/widgets/empty_state_panel.dart'; -import '../shell/widgets/section_header.dart'; -import '../shell/widgets/summary_card.dart'; /// The main dashboard page showing aggregated summary data. /// @@ -68,11 +65,11 @@ class _DashboardPageState extends State { return ListView( children: [ - const SectionHeader(title: 'Overview'), + const KcSectionHeader(title: 'Overview'), const SizedBox(height: KcSpacing.sm), _buildSummaryGrid(context, summary), const SizedBox(height: KcSpacing.xl), - SectionHeader( + KcSectionHeader( title: 'Quick Actions', action: TextButton( onPressed: () => Navigator.of(context).pushReplacementNamed(AppRoutes.inventory), @@ -82,9 +79,9 @@ class _DashboardPageState extends State { const SizedBox(height: KcSpacing.sm), _buildQuickActions(context), const SizedBox(height: KcSpacing.xl), - const SectionHeader(title: 'Recent Activity'), + const KcSectionHeader(title: 'Recent Activity'), const SizedBox(height: KcSpacing.sm), - const EmptyStatePanel( + const KcEmptyState( icon: Icons.history, message: 'No recent activity yet.\nActivity will appear here once orders and updates are tracked.', @@ -108,56 +105,56 @@ class _DashboardPageState extends State { } final cards = [ - SummaryCard( + KcSummaryCard( icon: Icons.inventory_2, iconColor: KcColors.denimBlue, label: 'Total Products', value: '${summary.totalProducts}', onTap: () => AppNavigation.dashboardToInventory(context), ), - SummaryCard( + KcSummaryCard( icon: Icons.check_circle_outline, iconColor: KcColors.success, label: 'In Stock', value: '${summary.inStock}', onTap: () => AppNavigation.dashboardToInventory(context, filter: 'inStock'), ), - SummaryCard( + KcSummaryCard( icon: Icons.warning_amber_rounded, iconColor: KcColors.warning, label: 'Low Stock', value: '${summary.lowStock}', onTap: () => AppNavigation.dashboardToInventory(context, filter: 'lowStock'), ), - SummaryCard( + KcSummaryCard( icon: Icons.edit_note, iconColor: KcColors.neutral, label: 'Draft', value: '${summary.draftProducts}', onTap: () => AppNavigation.dashboardToProducts(context, filter: 'draft'), ), - SummaryCard( + KcSummaryCard( icon: Icons.receipt_long, iconColor: KcColors.denimBlue, label: 'Total Orders', value: '${summary.totalOrders}', onTap: () => AppNavigation.dashboardToOrders(context), ), - SummaryCard( + KcSummaryCard( icon: Icons.hourglass_empty, iconColor: KcColors.warning, label: 'Pending Orders', value: '${summary.pendingOrders}', onTap: () => AppNavigation.dashboardToOrders(context, filter: 'pending'), ), - SummaryCard( + KcSummaryCard( icon: Icons.local_shipping_outlined, iconColor: KcColors.success, label: 'Active Orders', value: '${summary.activeOrders}', onTap: () => AppNavigation.dashboardToOrders(context, filter: 'active'), ), - SummaryCard( + KcSummaryCard( icon: Icons.attach_money, iconColor: KcColors.success, label: 'Revenue', diff --git a/kell_creations_apps/apps/kell_web/lib/shell/widgets/empty_state_panel.dart b/kell_creations_apps/apps/kell_web/lib/shell/widgets/empty_state_panel.dart index fd7056c..25fb6e1 100644 --- a/kell_creations_apps/apps/kell_web/lib/shell/widgets/empty_state_panel.dart +++ b/kell_creations_apps/apps/kell_web/lib/shell/widgets/empty_state_panel.dart @@ -1,38 +1,15 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A reusable empty-state panel shown when a section has no data yet. +/// Re-exports [KcEmptyState] from the shared design system. /// -/// Displays an [icon], a [message], and an optional [action] widget -/// (e.g. a button to create the first item). -class EmptyStatePanel extends StatelessWidget { - final IconData icon; - final String message; - final Widget? action; +/// This file preserves backward compatibility for existing imports. +/// New code should import directly from `package:design_system/design_system.dart`. +/// +/// The original `EmptyStatePanel` class name is preserved as a typedef +/// so that existing references continue to compile without changes. +library; - const EmptyStatePanel({super.key, required this.icon, required this.message, this.action}); +export 'package:design_system/design_system.dart' show KcEmptyState; - @override - Widget build(BuildContext context) { - return KcCard( - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: KcSpacing.xl), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 48, color: KcColors.neutral), - const SizedBox(height: KcSpacing.md), - Text( - message, - style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: KcColors.neutral), - textAlign: TextAlign.center, - ), - if (action != null) ...[const SizedBox(height: KcSpacing.md), action!], - ], - ), - ), - ), - ); - } -} +import 'package:design_system/design_system.dart'; + +/// @Deprecated('Use KcEmptyState from design_system instead.') +typedef EmptyStatePanel = KcEmptyState; diff --git a/kell_creations_apps/apps/kell_web/lib/shell/widgets/section_header.dart b/kell_creations_apps/apps/kell_web/lib/shell/widgets/section_header.dart index f49e0e0..51eb3ab 100644 --- a/kell_creations_apps/apps/kell_web/lib/shell/widgets/section_header.dart +++ b/kell_creations_apps/apps/kell_web/lib/shell/widgets/section_header.dart @@ -1,27 +1,15 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A reusable section header used across app pages. +/// Re-exports [KcSectionHeader] from the shared design system. /// -/// Displays a [title] with an optional trailing [action] widget -/// (e.g. a "View all" button). -class SectionHeader extends StatelessWidget { - final String title; - final Widget? action; +/// This file preserves backward compatibility for existing imports. +/// New code should import directly from `package:design_system/design_system.dart`. +/// +/// The original `SectionHeader` class name is preserved as a typedef +/// so that existing references continue to compile without changes. +library; - const SectionHeader({super.key, required this.title, this.action}); +export 'package:design_system/design_system.dart' show KcSectionHeader; - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: KcSpacing.sm), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: Theme.of(context).textTheme.titleLarge), - ?action, - ], - ), - ); - } -} +import 'package:design_system/design_system.dart'; + +/// @Deprecated('Use KcSectionHeader from design_system instead.') +typedef SectionHeader = KcSectionHeader; diff --git a/kell_creations_apps/apps/kell_web/lib/shell/widgets/summary_card.dart b/kell_creations_apps/apps/kell_web/lib/shell/widgets/summary_card.dart index 8b51dba..ab63275 100644 --- a/kell_creations_apps/apps/kell_web/lib/shell/widgets/summary_card.dart +++ b/kell_creations_apps/apps/kell_web/lib/shell/widgets/summary_card.dart @@ -1,52 +1,15 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A stat / summary card that displays a [value] with a [label] and an [icon]. +/// Re-exports [KcSummaryCard] from the shared design system. /// -/// Used on the dashboard to show high-level KPIs such as total products, -/// in-stock count, etc. Optionally tappable via [onTap] to navigate to a -/// related page. -class SummaryCard extends StatelessWidget { - final IconData icon; - final Color iconColor; - final String label; - final String value; +/// This file preserves backward compatibility for existing imports. +/// New code should import directly from `package:design_system/design_system.dart`. +/// +/// The original `SummaryCard` class name is preserved as a typedef +/// so that existing references continue to compile without changes. +library; - /// Optional tap handler for cross-feature navigation from dashboard KPIs. - final VoidCallback? onTap; +export 'package:design_system/design_system.dart' show KcSummaryCard; - const SummaryCard({ - super.key, - required this.icon, - required this.iconColor, - required this.label, - required this.value, - this.onTap, - }); +import 'package:design_system/design_system.dart'; - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final card = KcCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: iconColor, size: 28), - const SizedBox(height: KcSpacing.sm), - Text(value, style: theme.textTheme.headlineMedium?.copyWith(fontSize: 32)), - const SizedBox(height: KcSpacing.xs), - Text(label, style: theme.textTheme.bodyMedium?.copyWith(color: KcColors.neutral)), - ], - ), - ); - - if (onTap == null) return card; - - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector(onTap: onTap, child: card), - ); - } -} +/// @Deprecated('Use KcSummaryCard from design_system instead.') +typedef SummaryCard = KcSummaryCard; diff --git a/kell_creations_apps/packages/design_system/lib/design_system.dart b/kell_creations_apps/packages/design_system/lib/design_system.dart index 269b6f5..8ec21c7 100644 --- a/kell_creations_apps/packages/design_system/lib/design_system.dart +++ b/kell_creations_apps/packages/design_system/lib/design_system.dart @@ -1,7 +1,19 @@ library; +// Theme export 'src/theme/kc_colors.dart'; export 'src/theme/kc_spacing.dart'; export 'src/theme/kc_theme.dart'; +export 'src/theme/kc_typography.dart'; + +// Layout +export 'src/layout/kc_breakpoints.dart'; + +// Widgets export 'src/widgets/kc_card.dart'; +export 'src/widgets/kc_empty_state.dart'; +export 'src/widgets/kc_error_state.dart'; +export 'src/widgets/kc_loading_state.dart'; +export 'src/widgets/kc_section_header.dart'; export 'src/widgets/kc_status_chip.dart'; +export 'src/widgets/kc_summary_card.dart'; diff --git a/kell_creations_apps/packages/design_system/lib/src/layout/kc_breakpoints.dart b/kell_creations_apps/packages/design_system/lib/src/layout/kc_breakpoints.dart new file mode 100644 index 0000000..85346c8 --- /dev/null +++ b/kell_creations_apps/packages/design_system/lib/src/layout/kc_breakpoints.dart @@ -0,0 +1,61 @@ +/// Responsive layout breakpoint utilities for Kell Creations. +/// +/// Provides named breakpoints and helper methods for building responsive +/// layouts that adapt between mobile, tablet, and desktop form factors. +/// +/// Usage: +/// ```dart +/// if (KcBreakpoints.isDesktop(MediaQuery.of(context).size.width)) { +/// // desktop layout +/// } +/// ``` +abstract final class KcBreakpoints { + /// Maximum width considered "compact" (phone portrait). + static const double compact = 600; + + /// Maximum width considered "medium" (tablet / phone landscape). + static const double medium = 900; + + /// Maximum width considered "expanded" (small desktop / large tablet). + static const double expanded = 1200; + + /// Anything wider than [expanded] is "large" (full desktop). + + // ── Named queries ──────────────────────────────────────────────────── + + /// True when the available width is below the compact breakpoint. + static bool isCompact(double width) => width < compact; + + /// True when the available width is at least compact but below medium. + static bool isMedium(double width) => width >= compact && width < medium; + + /// True when the available width is at least medium but below expanded. + static bool isExpanded(double width) => width >= medium && width < expanded; + + /// True when the available width is at or above the expanded breakpoint. + static bool isLarge(double width) => width >= expanded; + + /// True when the width is at least [medium] (tablet-and-up). + static bool isTabletOrLarger(double width) => width >= medium; + + /// True when the width is at least [compact] but below [medium]. + static bool isTablet(double width) => isMedium(width); + + /// True when the width is below [compact] (phone). + static bool isMobile(double width) => isCompact(width); + + /// True when the width is at least [medium] (desktop-class). + static bool isDesktop(double width) => width >= medium; + + // ── Grid helpers ───────────────────────────────────────────────────── + + /// Returns a suggested grid cross-axis count based on the available [width]. + /// + /// Useful for GridView layouts that adapt column count to screen size. + static int gridColumns(double width) { + if (width >= expanded) return 4; + if (width >= medium) return 3; + if (width >= compact) return 2; + return 1; + } +} diff --git a/kell_creations_apps/packages/design_system/lib/src/theme/kc_theme.dart b/kell_creations_apps/packages/design_system/lib/src/theme/kc_theme.dart index 82b5cab..0328031 100644 --- a/kell_creations_apps/packages/design_system/lib/src/theme/kc_theme.dart +++ b/kell_creations_apps/packages/design_system/lib/src/theme/kc_theme.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; + import 'kc_colors.dart'; +import 'kc_typography.dart'; ThemeData buildKcTheme() { final base = ThemeData(useMaterial3: true); @@ -18,30 +20,7 @@ ThemeData buildKcTheme() { elevation: 0, centerTitle: false, ), - cardTheme: const CardThemeData( - color: KcColors.surface, - elevation: 0, - margin: EdgeInsets.zero, - ), - textTheme: base.textTheme.copyWith( - headlineMedium: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w700, - color: KcColors.deepTeal, - ), - titleLarge: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: KcColors.deepTeal, - ), - bodyLarge: const TextStyle( - fontSize: 16, - color: KcColors.deepTeal, - ), - bodyMedium: const TextStyle( - fontSize: 14, - color: KcColors.deepTeal, - ), - ), + cardTheme: const CardThemeData(color: KcColors.surface, elevation: 0, margin: EdgeInsets.zero), + textTheme: KcTypography.applyKcTypography(base.textTheme), ); -} \ No newline at end of file +} diff --git a/kell_creations_apps/packages/design_system/lib/src/theme/kc_typography.dart b/kell_creations_apps/packages/design_system/lib/src/theme/kc_typography.dart new file mode 100644 index 0000000..28f93f5 --- /dev/null +++ b/kell_creations_apps/packages/design_system/lib/src/theme/kc_typography.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; + +import 'kc_colors.dart'; + +/// Shared typography scale for Kell Creations design system. +/// +/// Provides a consistent text style hierarchy across web and mobile +/// applications. Use [applyKcTypography] to merge these styles into +/// a [TextTheme], or reference individual styles directly. +abstract final class KcTypography { + // ── Display ────────────────────────────────────────────────────────── + + static const displayLarge = TextStyle( + fontSize: 57, + fontWeight: FontWeight.w400, + letterSpacing: -0.25, + color: KcColors.deepTeal, + ); + + static const displayMedium = TextStyle( + fontSize: 45, + fontWeight: FontWeight.w400, + color: KcColors.deepTeal, + ); + + static const displaySmall = TextStyle( + fontSize: 36, + fontWeight: FontWeight.w400, + color: KcColors.deepTeal, + ); + + // ── Headline ───────────────────────────────────────────────────────── + + static const headlineLarge = TextStyle( + fontSize: 32, + fontWeight: FontWeight.w700, + color: KcColors.deepTeal, + ); + + static const headlineMedium = TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + color: KcColors.deepTeal, + ); + + static const headlineSmall = TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: KcColors.deepTeal, + ); + + // ── Title ──────────────────────────────────────────────────────────── + + static const titleLarge = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: KcColors.deepTeal, + ); + + static const titleMedium = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: KcColors.deepTeal, + ); + + static const titleSmall = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: KcColors.deepTeal, + ); + + // ── Body ───────────────────────────────────────────────────────────── + + static const bodyLarge = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: KcColors.deepTeal, + ); + + static const bodyMedium = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: KcColors.deepTeal, + ); + + static const bodySmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: KcColors.deepTeal, + ); + + // ── Label ──────────────────────────────────────────────────────────── + + static const labelLarge = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: KcColors.deepTeal, + ); + + static const labelMedium = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: KcColors.deepTeal, + ); + + static const labelSmall = TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + color: KcColors.deepTeal, + ); + + /// Applies the Kell Creations typography scale to the given [base] + /// text theme (or a default one if omitted). + static TextTheme applyKcTypography([TextTheme? base]) { + return (base ?? const TextTheme()).copyWith( + displayLarge: KcTypography.displayLarge, + displayMedium: KcTypography.displayMedium, + displaySmall: KcTypography.displaySmall, + headlineLarge: KcTypography.headlineLarge, + headlineMedium: KcTypography.headlineMedium, + headlineSmall: KcTypography.headlineSmall, + titleLarge: KcTypography.titleLarge, + titleMedium: KcTypography.titleMedium, + titleSmall: KcTypography.titleSmall, + bodyLarge: KcTypography.bodyLarge, + bodyMedium: KcTypography.bodyMedium, + bodySmall: KcTypography.bodySmall, + labelLarge: KcTypography.labelLarge, + labelMedium: KcTypography.labelMedium, + labelSmall: KcTypography.labelSmall, + ); + } +} diff --git a/kell_creations_apps/packages/design_system/lib/src/widgets/kc_empty_state.dart b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_empty_state.dart new file mode 100644 index 0000000..c708323 --- /dev/null +++ b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_empty_state.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import '../theme/kc_colors.dart'; +import '../theme/kc_spacing.dart'; +import 'kc_card.dart'; + +/// A reusable empty-state panel shown when a section has no data yet. +/// +/// Displays an [icon], a [message], and an optional [action] widget +/// (e.g. a button to create the first item). Wraps content in a [KcCard]. +class KcEmptyState extends StatelessWidget { + final IconData icon; + final String message; + final Widget? action; + + const KcEmptyState({super.key, required this.icon, required this.message, this.action}); + + @override + Widget build(BuildContext context) { + return KcCard( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: KcSpacing.xl), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 48, color: KcColors.neutral), + const SizedBox(height: KcSpacing.md), + Text( + message, + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: KcColors.neutral), + textAlign: TextAlign.center, + ), + if (action != null) ...[const SizedBox(height: KcSpacing.md), action!], + ], + ), + ), + ), + ); + } +} diff --git a/kell_creations_apps/packages/design_system/lib/src/widgets/kc_error_state.dart b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_error_state.dart new file mode 100644 index 0000000..dd17ce9 --- /dev/null +++ b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_error_state.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import '../theme/kc_colors.dart'; +import '../theme/kc_spacing.dart'; +import 'kc_card.dart'; + +/// A shared error-state widget for use across Kell Creations apps. +/// +/// Displays a warning icon, an error [message], and an optional [onRetry] +/// action button. Wraps content in a [KcCard] for visual consistency. +class KcErrorState extends StatelessWidget { + /// The error message to display. + final String message; + + /// Optional retry callback. When provided, a "Retry" button is shown. + final VoidCallback? onRetry; + + const KcErrorState({super.key, required this.message, this.onRetry}); + + @override + Widget build(BuildContext context) { + return KcCard( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: KcSpacing.xl), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, size: 48, color: KcColors.danger), + const SizedBox(height: KcSpacing.md), + Text( + message, + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: KcColors.danger), + textAlign: TextAlign.center, + ), + if (onRetry != null) ...[ + const SizedBox(height: KcSpacing.md), + FilledButton.icon( + onPressed: onRetry, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/kell_creations_apps/packages/design_system/lib/src/widgets/kc_loading_state.dart b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_loading_state.dart new file mode 100644 index 0000000..45a7112 --- /dev/null +++ b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_loading_state.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../theme/kc_colors.dart'; +import '../theme/kc_spacing.dart'; + +/// A shared loading-state widget for use across Kell Creations apps. +/// +/// Displays a centered [CircularProgressIndicator] with an optional +/// [message] below it. Useful as a placeholder while data is being fetched. +class KcLoadingState extends StatelessWidget { + /// Optional message displayed below the spinner. + final String? message; + + const KcLoadingState({super.key, this.message}); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: KcSpacing.xl), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + if (message != null) ...[ + const SizedBox(height: KcSpacing.md), + Text( + message!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: KcColors.neutral), + textAlign: TextAlign.center, + ), + ], + ], + ), + ), + ); + } +} diff --git a/kell_creations_apps/packages/design_system/lib/src/widgets/kc_section_header.dart b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_section_header.dart new file mode 100644 index 0000000..3ed9deb --- /dev/null +++ b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_section_header.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import '../theme/kc_spacing.dart'; + +/// A reusable section header used across app pages. +/// +/// Displays a [title] with an optional trailing [action] widget +/// (e.g. a "View all" button). +class KcSectionHeader extends StatelessWidget { + final String title; + final Widget? action; + + const KcSectionHeader({super.key, required this.title, this.action}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: KcSpacing.sm), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: Theme.of(context).textTheme.titleLarge), + ?action, + ], + ), + ); + } +} diff --git a/kell_creations_apps/packages/design_system/lib/src/widgets/kc_summary_card.dart b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_summary_card.dart new file mode 100644 index 0000000..764311f --- /dev/null +++ b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_summary_card.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import '../theme/kc_colors.dart'; +import '../theme/kc_spacing.dart'; +import 'kc_card.dart'; + +/// A stat / summary card that displays a [value] with a [label] and an [icon]. +/// +/// Used on dashboards to show high-level KPIs such as total products, +/// in-stock count, etc. Optionally tappable via [onTap] to navigate to a +/// related page. +class KcSummaryCard extends StatelessWidget { + final IconData icon; + final Color iconColor; + final String label; + final String value; + + /// Optional tap handler for cross-feature navigation from dashboard KPIs. + final VoidCallback? onTap; + + const KcSummaryCard({ + super.key, + required this.icon, + required this.iconColor, + required this.label, + required this.value, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final card = KcCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: iconColor, size: 28), + const SizedBox(height: KcSpacing.sm), + Text(value, style: theme.textTheme.headlineMedium?.copyWith(fontSize: 32)), + const SizedBox(height: KcSpacing.xs), + Text(label, style: theme.textTheme.bodyMedium?.copyWith(color: KcColors.neutral)), + ], + ), + ); + + if (onTap == null) return card; + + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector(onTap: onTap, child: card), + ); + } +} diff --git a/kell_creations_apps/packages/design_system/test/design_system_test.dart b/kell_creations_apps/packages/design_system/test/design_system_test.dart index e1e608e..4a4b87e 100644 --- a/kell_creations_apps/packages/design_system/test/design_system_test.dart +++ b/kell_creations_apps/packages/design_system/test/design_system_test.dart @@ -4,6 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:design_system/design_system.dart'; void main() { + // ── Existing widget tests ──────────────────────────────────────────────── + group('KcStatusChip', () { testWidgets('renders label text', (WidgetTester tester) async { await tester.pumpWidget( @@ -35,5 +37,388 @@ void main() { final theme = buildKcTheme(); expect(theme, isA()); }); + + test('theme uses KcTypography scale', () { + final theme = buildKcTheme(); + expect(theme.textTheme.headlineMedium?.fontSize, 28); + expect(theme.textTheme.headlineMedium?.fontWeight, FontWeight.w700); + expect(theme.textTheme.titleLarge?.fontSize, 20); + expect(theme.textTheme.bodyLarge?.fontSize, 16); + expect(theme.textTheme.bodyMedium?.fontSize, 14); + }); + }); + + // ── KcTypography tests ─────────────────────────────────────────────────── + + group('KcTypography', () { + test('displayLarge has expected properties', () { + expect(KcTypography.displayLarge.fontSize, 57); + expect(KcTypography.displayLarge.fontWeight, FontWeight.w400); + expect(KcTypography.displayLarge.color, KcColors.deepTeal); + }); + + test('headlineMedium has expected properties', () { + expect(KcTypography.headlineMedium.fontSize, 28); + expect(KcTypography.headlineMedium.fontWeight, FontWeight.w700); + }); + + test('titleLarge has expected properties', () { + expect(KcTypography.titleLarge.fontSize, 20); + expect(KcTypography.titleLarge.fontWeight, FontWeight.w600); + }); + + test('bodyLarge has expected properties', () { + expect(KcTypography.bodyLarge.fontSize, 16); + expect(KcTypography.bodyLarge.fontWeight, FontWeight.w400); + }); + + test('bodySmall has expected properties', () { + expect(KcTypography.bodySmall.fontSize, 12); + }); + + test('labelSmall has expected properties', () { + expect(KcTypography.labelSmall.fontSize, 11); + expect(KcTypography.labelSmall.letterSpacing, 0.5); + }); + + test('applyKcTypography produces a complete TextTheme', () { + final textTheme = KcTypography.applyKcTypography(); + expect(textTheme.displayLarge, KcTypography.displayLarge); + expect(textTheme.displayMedium, KcTypography.displayMedium); + expect(textTheme.displaySmall, KcTypography.displaySmall); + expect(textTheme.headlineLarge, KcTypography.headlineLarge); + expect(textTheme.headlineMedium, KcTypography.headlineMedium); + expect(textTheme.headlineSmall, KcTypography.headlineSmall); + expect(textTheme.titleLarge, KcTypography.titleLarge); + expect(textTheme.titleMedium, KcTypography.titleMedium); + expect(textTheme.titleSmall, KcTypography.titleSmall); + expect(textTheme.bodyLarge, KcTypography.bodyLarge); + expect(textTheme.bodyMedium, KcTypography.bodyMedium); + expect(textTheme.bodySmall, KcTypography.bodySmall); + expect(textTheme.labelLarge, KcTypography.labelLarge); + expect(textTheme.labelMedium, KcTypography.labelMedium); + expect(textTheme.labelSmall, KcTypography.labelSmall); + }); + + test('applyKcTypography can merge with an existing TextTheme', () { + const base = TextTheme(displayLarge: TextStyle(fontSize: 100)); + final textTheme = KcTypography.applyKcTypography(base); + // Should override the base + expect(textTheme.displayLarge?.fontSize, 57); + }); + }); + + // ── KcBreakpoints tests ────────────────────────────────────────────────── + + group('KcBreakpoints', () { + test('isCompact returns true below 600', () { + expect(KcBreakpoints.isCompact(599), true); + expect(KcBreakpoints.isCompact(600), false); + }); + + test('isMedium returns true between 600 and 900', () { + expect(KcBreakpoints.isMedium(600), true); + expect(KcBreakpoints.isMedium(899), true); + expect(KcBreakpoints.isMedium(900), false); + expect(KcBreakpoints.isMedium(599), false); + }); + + test('isExpanded returns true between 900 and 1200', () { + expect(KcBreakpoints.isExpanded(900), true); + expect(KcBreakpoints.isExpanded(1199), true); + expect(KcBreakpoints.isExpanded(1200), false); + }); + + test('isLarge returns true at 1200 and above', () { + expect(KcBreakpoints.isLarge(1200), true); + expect(KcBreakpoints.isLarge(1199), false); + }); + + test('isMobile returns true below 600', () { + expect(KcBreakpoints.isMobile(400), true); + expect(KcBreakpoints.isMobile(600), false); + }); + + test('isDesktop returns true at 900 and above', () { + expect(KcBreakpoints.isDesktop(900), true); + expect(KcBreakpoints.isDesktop(899), false); + }); + + test('isTabletOrLarger returns true at 900 and above', () { + expect(KcBreakpoints.isTabletOrLarger(900), true); + expect(KcBreakpoints.isTabletOrLarger(899), false); + }); + + test('isTablet matches isMedium range', () { + expect(KcBreakpoints.isTablet(700), true); + expect(KcBreakpoints.isTablet(500), false); + expect(KcBreakpoints.isTablet(900), false); + }); + + test('gridColumns returns correct count for each range', () { + expect(KcBreakpoints.gridColumns(400), 1); + expect(KcBreakpoints.gridColumns(700), 2); + expect(KcBreakpoints.gridColumns(1000), 3); + expect(KcBreakpoints.gridColumns(1300), 4); + }); + + test('gridColumns boundary values', () { + expect(KcBreakpoints.gridColumns(599), 1); + expect(KcBreakpoints.gridColumns(600), 2); + expect(KcBreakpoints.gridColumns(899), 2); + expect(KcBreakpoints.gridColumns(900), 3); + expect(KcBreakpoints.gridColumns(1199), 3); + expect(KcBreakpoints.gridColumns(1200), 4); + }); + }); + + // ── KcEmptyState tests ─────────────────────────────────────────────────── + + group('KcEmptyState', () { + testWidgets('renders icon and message', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: KcEmptyState(icon: Icons.inbox, message: 'No items yet'), + ), + ), + ); + + expect(find.byIcon(Icons.inbox), findsOneWidget); + expect(find.text('No items yet'), findsOneWidget); + }); + + testWidgets('renders optional action widget', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: KcEmptyState( + icon: Icons.inbox, + message: 'No items yet', + action: ElevatedButton(onPressed: () {}, child: const Text('Add Item')), + ), + ), + ), + ); + + expect(find.text('Add Item'), findsOneWidget); + }); + + testWidgets('does not render action when null', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: KcEmptyState(icon: Icons.inbox, message: 'No items yet'), + ), + ), + ); + + expect(find.byType(ElevatedButton), findsNothing); + }); + }); + + // ── KcErrorState tests ─────────────────────────────────────────────────── + + group('KcErrorState', () { + testWidgets('renders error icon and message', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: KcErrorState(message: 'Something went wrong')), + ), + ); + + expect(find.byIcon(Icons.error_outline), findsOneWidget); + expect(find.text('Something went wrong'), findsOneWidget); + }); + + testWidgets('renders retry button when onRetry is provided', (WidgetTester tester) async { + var retried = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: KcErrorState(message: 'Failed to load', onRetry: () => retried = true), + ), + ), + ); + + expect(find.text('Retry'), findsOneWidget); + await tester.tap(find.text('Retry')); + expect(retried, true); + }); + + testWidgets('does not render retry button when onRetry is null', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: KcErrorState(message: 'Error occurred')), + ), + ); + + expect(find.text('Retry'), findsNothing); + }); + }); + + // ── KcLoadingState tests ───────────────────────────────────────────────── + + group('KcLoadingState', () { + testWidgets('renders a CircularProgressIndicator', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: Scaffold(body: KcLoadingState()))); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets('renders optional message', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: KcLoadingState(message: 'Loading data...')), + ), + ); + + expect(find.text('Loading data...'), findsOneWidget); + }); + + testWidgets('does not render message when null', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: Scaffold(body: KcLoadingState()))); + + // Only the progress indicator, no text widget for message. + expect(find.byType(Text), findsNothing); + }); + }); + + // ── KcSectionHeader tests ──────────────────────────────────────────────── + + group('KcSectionHeader', () { + testWidgets('renders title text', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: KcSectionHeader(title: 'My Section')), + ), + ); + + expect(find.text('My Section'), findsOneWidget); + }); + + testWidgets('renders optional action widget', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: KcSectionHeader( + title: 'Section', + action: TextButton(onPressed: () {}, child: const Text('View All')), + ), + ), + ), + ); + + expect(find.text('View All'), findsOneWidget); + }); + + testWidgets('does not render action when null', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: KcSectionHeader(title: 'Section')), + ), + ); + + expect(find.byType(TextButton), findsNothing); + }); + }); + + // ── KcSummaryCard tests ────────────────────────────────────────────────── + + group('KcSummaryCard', () { + testWidgets('renders icon, label, and value', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: KcSummaryCard( + icon: Icons.inventory_2, + iconColor: Colors.blue, + label: 'Total Products', + value: '42', + ), + ), + ), + ); + + expect(find.byIcon(Icons.inventory_2), findsOneWidget); + expect(find.text('Total Products'), findsOneWidget); + expect(find.text('42'), findsOneWidget); + }); + + testWidgets('is tappable when onTap is provided', (WidgetTester tester) async { + var tapped = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: KcSummaryCard( + icon: Icons.inventory_2, + iconColor: Colors.blue, + label: 'Total', + value: '10', + onTap: () => tapped = true, + ), + ), + ), + ); + + await tester.tap(find.text('10')); + expect(tapped, true); + }); + + testWidgets('is not tappable when onTap is null', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: KcSummaryCard( + icon: Icons.inventory_2, + iconColor: Colors.blue, + label: 'Total', + value: '10', + ), + ), + ), + ); + + // Should not have a GestureDetector wrapping + expect(find.byType(GestureDetector), findsNothing); + }); + }); + + // ── KcColors tests ─────────────────────────────────────────────────────── + + group('KcColors', () { + test('brand colors are defined', () { + expect(KcColors.skyBlue, isA()); + expect(KcColors.denimBlue, isA()); + expect(KcColors.deepTeal, isA()); + expect(KcColors.honeyGold, isA()); + expect(KcColors.sunsetOrange, isA()); + }); + + test('semantic colors are defined', () { + expect(KcColors.success, isA()); + expect(KcColors.warning, isA()); + expect(KcColors.danger, isA()); + expect(KcColors.neutral, isA()); + }); + }); + + // ── KcSpacing tests ────────────────────────────────────────────────────── + + group('KcSpacing', () { + test('spacing scale is ordered correctly', () { + expect(KcSpacing.xs, lessThan(KcSpacing.sm)); + expect(KcSpacing.sm, lessThan(KcSpacing.md)); + expect(KcSpacing.md, lessThan(KcSpacing.lg)); + expect(KcSpacing.lg, lessThan(KcSpacing.xl)); + }); + + test('spacing values match expected constants', () { + expect(KcSpacing.xs, 4.0); + expect(KcSpacing.sm, 8.0); + expect(KcSpacing.md, 16.0); + expect(KcSpacing.lg, 24.0); + expect(KcSpacing.xl, 32.0); + }); }); }