feat(design-system): Stage 4A — design system expansion and shared widget migration
- Migrate EmptyStatePanel, SectionHeader, SummaryCard from kell_web into design_system as KcEmptyState, KcSectionHeader, KcSummaryCard - Add KcTypography shared typography scale with full Material 3 text style hierarchy - Add KcBreakpoints responsive layout breakpoint utilities (compact/medium/expanded/large) - Add KcLoadingState and KcErrorState shared state widgets - Update kc_theme.dart to use KcTypography.applyKcTypography() - Update kell_web dashboard_page.dart to use design_system widgets directly - Replace kell_web shell widget files with backward-compatible typedef re-exports - Expand design_system tests from 3 to 41 (all passing) - All existing tests passing: design_system 41/41, feature_wordpress 294/294, kell_web 24/24 - dart analyze clean across design_system and kell_web
This commit is contained in:
parent
bee610ca2c
commit
8facefdff1
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -593,7 +579,7 @@ 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** |
|
||||
|
|
@ -604,5 +590,5 @@ Working rules:
|
|||
| `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** |
|
||||
| `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** |
|
||||
|
|
|
|||
|
|
@ -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<DashboardPage> {
|
|||
|
||||
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<DashboardPage> {
|
|||
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<DashboardPage> {
|
|||
}
|
||||
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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!],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue