Compare commits

..

No commits in common. "6f10efc88d489c7e79b24fa13cc866b9a34562ff" and "bee610ca2c4bfa8fdc7b6858b67d684fcd7c298d" have entirely different histories.

16 changed files with 200 additions and 910 deletions

View File

@ -2,9 +2,9 @@
## Current status ## Current status
- main baseline updated through: design-system-shared-widgets (Stage 4A complete) - main baseline updated through: list-efficiency-improvements (Stage 3 complete)
- main baseline commit: merge of `feat/design-system-shared-widgets` (2026-05-22) - main baseline commit: merge of `feat/list-efficiency-improvements` (2026-05-22)
- next branch: feat/shared-composition-pattern - next branch: feat/design-system-shared-widgets
- current stage: Stage 4 — Platform foundations and cross-platform readiness - current stage: Stage 4 — Platform foundations and cross-platform readiness
## Slice tracker ## Slice tracker
@ -89,25 +89,9 @@
### feat/design-system-shared-widgets ### feat/design-system-shared-widgets
- status: merged to main - status: queued (Stage 4A — next)
- date: 2026-05-22 - inspection: pending
- inspection: complete - implementation: pending
- implementation: complete - tests: pending
- files changed: - analyze: pending
- `design_system/lib/design_system.dart` — expanded barrel exports with typography, layout, and 5 new widgets - brief updated: no
- `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

View File

@ -92,7 +92,6 @@ Rules:
- ✅ Publishing workflow UX hardening landed (Stage 2B complete — merged `feat/publishing-ux-hardening``main` at `b81016d`, 2026-04-11). Stage 2 complete. - ✅ 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). - ✅ 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. - ✅ 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` ### Current narrow edit capabilities on `main`
@ -105,18 +104,16 @@ Rules:
### Latest known validation state on `main` ### Latest known validation state on `main`
- `dart analyze` clean - `dart analyze` clean
- `design_system` tests passing
- `feature_wordpress` tests passing - `feature_wordpress` tests passing
- `kell_web` tests passing - `kell_web` dashboard tests passing
- latest reported count for `design_system`: `41/41 passed`
- latest reported count for `feature_wordpress`: `294/294 passed` - latest reported count for `feature_wordpress`: `294/294 passed`
- latest reported count for `kell_web`: `24/24 passed` - latest reported count for `kell_web` dashboard tests: `5/5 passed`
- baseline commit: merge of `feat/design-system-shared-widgets` (2026-05-22) - baseline commit: merge of `feat/list-efficiency-improvements` (2026-05-22)
### Next recommended branch ### Next recommended branch
**`feat/flutter-cicd`** — Stage 4B: Cross-platform shell composition strategy. **`feat/design-system-shared-widgets`** — Stage 4A: Design system expansion and shared widget migration.
Branch from latest `main`. Stage 4A (design system expansion) is complete. Branch from latest `main`. Stage 3 (web application operator efficiency) is complete.
--- ---
@ -226,10 +223,27 @@ Invest in shared foundations now so that the Android expansion (Stage 5) and all
- `feat/flutter-cicd` - `feat/flutter-cicd`
- `feat/shared-composition-pattern` - `feat/shared-composition-pattern`
#### ~~Stage 4A — Design system expansion and shared widget migration~~ ✅ COMPLETE #### Stage 4A — Design system expansion and shared widget migration
> Merged `feat/design-system-shared-widgets``main` (2026-05-22). ##### Goal
> 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.
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
#### Stage 4B — Cross-platform shell composition strategy #### Stage 4B — Cross-platform shell composition strategy
@ -578,17 +592,17 @@ Working rules:
## Appendix: Feature maturity matrix ## Appendix: Feature maturity matrix
| Package | Domain Layer | Application Layer | Data (Fake) | Data (Real) | Presentation | Tests | Maturity | | Package | Domain Layer | Application Layer | Data (Fake) | Data (Real) | Presentation | Tests | Maturity |
| ------------------- | ------------ | ----------------- | ----------- | -------------- | -------------------------------------------------- | ------- | -------------------- | | ------------------- | ------------ | ----------------- | ----------- | -------------- | ------------------------------ | ------- | -------------------- |
| `feature_wordpress` | ✅ Complete | ✅ Complete | ✅ Complete | ✅ WooCommerce | ✅ Complete | 294 | **Production-ready** | | `feature_wordpress` | ✅ Complete | ✅ Complete | ✅ Complete | ✅ WooCommerce | ✅ Complete | 294 | **Production-ready** |
| `feature_inventory` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Minimal | **Fake-only MVP** | | `feature_inventory` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Minimal | **Fake-only MVP** |
| `feature_orders` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Some | **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_policy` | ✅ Complete | ✅ Complete | ✅ Complete | ❌ None | ✅ Complete | Minimal | **Fake-only MVP** |
| `feature_finance` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | ❌ Stub | None | **Scaffolded only** | | `feature_finance` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | ❌ Stub | None | **Scaffolded only** |
| `feature_mrp` | ❌ 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** | | `feature_social` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | ❌ Stub | None | **Scaffolded only** |
| `auth` | ❌ Stub | ❌ Stub | ❌ Stub | ❌ None | N/A | 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** | | `data` | ❌ Stub | N/A | N/A | ❌ None | N/A | None | **Scaffolded only** |
| `integrations` | ❌ 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** | | `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** | | `core` | ✅ Partial | N/A | N/A | N/A | N/A | Minimal | **Foundation only** |

View File

@ -5,6 +5,9 @@ import '../dashboard/application/dashboard_controller.dart';
import '../dashboard/domain/dashboard_summary.dart'; import '../dashboard/domain/dashboard_summary.dart';
import '../navigation/app_navigation.dart'; import '../navigation/app_navigation.dart';
import '../routing/app_routes.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. /// The main dashboard page showing aggregated summary data.
/// ///
@ -65,11 +68,11 @@ class _DashboardPageState extends State<DashboardPage> {
return ListView( return ListView(
children: [ children: [
const KcSectionHeader(title: 'Overview'), const SectionHeader(title: 'Overview'),
const SizedBox(height: KcSpacing.sm), const SizedBox(height: KcSpacing.sm),
_buildSummaryGrid(context, summary), _buildSummaryGrid(context, summary),
const SizedBox(height: KcSpacing.xl), const SizedBox(height: KcSpacing.xl),
KcSectionHeader( SectionHeader(
title: 'Quick Actions', title: 'Quick Actions',
action: TextButton( action: TextButton(
onPressed: () => Navigator.of(context).pushReplacementNamed(AppRoutes.inventory), onPressed: () => Navigator.of(context).pushReplacementNamed(AppRoutes.inventory),
@ -79,9 +82,9 @@ class _DashboardPageState extends State<DashboardPage> {
const SizedBox(height: KcSpacing.sm), const SizedBox(height: KcSpacing.sm),
_buildQuickActions(context), _buildQuickActions(context),
const SizedBox(height: KcSpacing.xl), const SizedBox(height: KcSpacing.xl),
const KcSectionHeader(title: 'Recent Activity'), const SectionHeader(title: 'Recent Activity'),
const SizedBox(height: KcSpacing.sm), const SizedBox(height: KcSpacing.sm),
const KcEmptyState( const EmptyStatePanel(
icon: Icons.history, icon: Icons.history,
message: message:
'No recent activity yet.\nActivity will appear here once orders and updates are tracked.', 'No recent activity yet.\nActivity will appear here once orders and updates are tracked.',
@ -105,56 +108,56 @@ class _DashboardPageState extends State<DashboardPage> {
} }
final cards = [ final cards = [
KcSummaryCard( SummaryCard(
icon: Icons.inventory_2, icon: Icons.inventory_2,
iconColor: KcColors.denimBlue, iconColor: KcColors.denimBlue,
label: 'Total Products', label: 'Total Products',
value: '${summary.totalProducts}', value: '${summary.totalProducts}',
onTap: () => AppNavigation.dashboardToInventory(context), onTap: () => AppNavigation.dashboardToInventory(context),
), ),
KcSummaryCard( SummaryCard(
icon: Icons.check_circle_outline, icon: Icons.check_circle_outline,
iconColor: KcColors.success, iconColor: KcColors.success,
label: 'In Stock', label: 'In Stock',
value: '${summary.inStock}', value: '${summary.inStock}',
onTap: () => AppNavigation.dashboardToInventory(context, filter: 'inStock'), onTap: () => AppNavigation.dashboardToInventory(context, filter: 'inStock'),
), ),
KcSummaryCard( SummaryCard(
icon: Icons.warning_amber_rounded, icon: Icons.warning_amber_rounded,
iconColor: KcColors.warning, iconColor: KcColors.warning,
label: 'Low Stock', label: 'Low Stock',
value: '${summary.lowStock}', value: '${summary.lowStock}',
onTap: () => AppNavigation.dashboardToInventory(context, filter: 'lowStock'), onTap: () => AppNavigation.dashboardToInventory(context, filter: 'lowStock'),
), ),
KcSummaryCard( SummaryCard(
icon: Icons.edit_note, icon: Icons.edit_note,
iconColor: KcColors.neutral, iconColor: KcColors.neutral,
label: 'Draft', label: 'Draft',
value: '${summary.draftProducts}', value: '${summary.draftProducts}',
onTap: () => AppNavigation.dashboardToProducts(context, filter: 'draft'), onTap: () => AppNavigation.dashboardToProducts(context, filter: 'draft'),
), ),
KcSummaryCard( SummaryCard(
icon: Icons.receipt_long, icon: Icons.receipt_long,
iconColor: KcColors.denimBlue, iconColor: KcColors.denimBlue,
label: 'Total Orders', label: 'Total Orders',
value: '${summary.totalOrders}', value: '${summary.totalOrders}',
onTap: () => AppNavigation.dashboardToOrders(context), onTap: () => AppNavigation.dashboardToOrders(context),
), ),
KcSummaryCard( SummaryCard(
icon: Icons.hourglass_empty, icon: Icons.hourglass_empty,
iconColor: KcColors.warning, iconColor: KcColors.warning,
label: 'Pending Orders', label: 'Pending Orders',
value: '${summary.pendingOrders}', value: '${summary.pendingOrders}',
onTap: () => AppNavigation.dashboardToOrders(context, filter: 'pending'), onTap: () => AppNavigation.dashboardToOrders(context, filter: 'pending'),
), ),
KcSummaryCard( SummaryCard(
icon: Icons.local_shipping_outlined, icon: Icons.local_shipping_outlined,
iconColor: KcColors.success, iconColor: KcColors.success,
label: 'Active Orders', label: 'Active Orders',
value: '${summary.activeOrders}', value: '${summary.activeOrders}',
onTap: () => AppNavigation.dashboardToOrders(context, filter: 'active'), onTap: () => AppNavigation.dashboardToOrders(context, filter: 'active'),
), ),
KcSummaryCard( SummaryCard(
icon: Icons.attach_money, icon: Icons.attach_money,
iconColor: KcColors.success, iconColor: KcColors.success,
label: 'Revenue', label: 'Revenue',

View File

@ -1,15 +1,38 @@
/// Re-exports [KcEmptyState] from the shared design system.
///
/// 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;
export 'package:design_system/design_system.dart' show KcEmptyState;
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// @Deprecated('Use KcEmptyState from design_system instead.') /// A reusable empty-state panel shown when a section has no data yet.
typedef EmptyStatePanel = KcEmptyState; ///
/// 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;
const EmptyStatePanel({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!],
],
),
),
),
);
}
}

View File

@ -1,15 +1,27 @@
/// Re-exports [KcSectionHeader] from the shared design system.
///
/// 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;
export 'package:design_system/design_system.dart' show KcSectionHeader;
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// @Deprecated('Use KcSectionHeader from design_system instead.') /// A reusable section header used across app pages.
typedef SectionHeader = KcSectionHeader; ///
/// 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;
const SectionHeader({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,
],
),
);
}
}

View File

@ -1,15 +1,52 @@
/// Re-exports [KcSummaryCard] from the shared design system.
///
/// 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;
export 'package:design_system/design_system.dart' show KcSummaryCard;
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// @Deprecated('Use KcSummaryCard from design_system instead.') /// A stat / summary card that displays a [value] with a [label] and an [icon].
typedef SummaryCard = KcSummaryCard; ///
/// 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;
/// Optional tap handler for cross-feature navigation from dashboard KPIs.
final VoidCallback? onTap;
const SummaryCard({
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),
);
}
}

View File

@ -1,19 +1,7 @@
library; library;
// Theme
export 'src/theme/kc_colors.dart'; export 'src/theme/kc_colors.dart';
export 'src/theme/kc_spacing.dart'; export 'src/theme/kc_spacing.dart';
export 'src/theme/kc_theme.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_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_status_chip.dart';
export 'src/widgets/kc_summary_card.dart';

View File

@ -1,61 +0,0 @@
/// 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;
}
}

View File

@ -1,7 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'kc_colors.dart'; import 'kc_colors.dart';
import 'kc_typography.dart';
ThemeData buildKcTheme() { ThemeData buildKcTheme() {
final base = ThemeData(useMaterial3: true); final base = ThemeData(useMaterial3: true);
@ -20,7 +18,30 @@ ThemeData buildKcTheme() {
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
), ),
cardTheme: const CardThemeData(color: KcColors.surface, elevation: 0, margin: EdgeInsets.zero), cardTheme: const CardThemeData(
textTheme: KcTypography.applyKcTypography(base.textTheme), 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,
),
),
); );
} }

View File

@ -1,134 +0,0 @@
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,
);
}
}

View File

@ -1,41 +0,0 @@
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!],
],
),
),
),
);
}
}

View File

@ -1,50 +0,0 @@
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'),
),
],
],
),
),
),
);
}
}

View File

@ -1,38 +0,0 @@
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,
),
],
],
),
),
);
}
}

View File

@ -1,28 +0,0 @@
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,
],
),
);
}
}

View File

@ -1,55 +0,0 @@
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),
);
}
}

View File

@ -4,8 +4,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
void main() { void main() {
// Existing widget tests
group('KcStatusChip', () { group('KcStatusChip', () {
testWidgets('renders label text', (WidgetTester tester) async { testWidgets('renders label text', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
@ -37,388 +35,5 @@ void main() {
final theme = buildKcTheme(); final theme = buildKcTheme();
expect(theme, isA<ThemeData>()); expect(theme, isA<ThemeData>());
}); });
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<Color>());
expect(KcColors.denimBlue, isA<Color>());
expect(KcColors.deepTeal, isA<Color>());
expect(KcColors.honeyGold, isA<Color>());
expect(KcColors.sunsetOrange, isA<Color>());
});
test('semantic colors are defined', () {
expect(KcColors.success, isA<Color>());
expect(KcColors.warning, isA<Color>());
expect(KcColors.danger, isA<Color>());
expect(KcColors.neutral, isA<Color>());
});
});
// 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);
});
}); });
} }