diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 0cf6d442d..8e85cce4e 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -43,7 +43,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b - sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 + sqlite3_flutter_libs: c7b32a4d17d2cbad19895873ccc815f276b1f125 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/app/lib/main.dart b/app/lib/main.dart index 40686ddfa..afba6ecfa 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:testpress/testpress.dart'; +import 'package:courses/providers/dashboard_providers.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -13,26 +14,22 @@ void main() async { /// 1. Change the primary color below /// 2. Save the file (hot reload) /// 3. See the UI update immediately -class CortexAppRoot extends StatefulWidget { +class CortexAppRoot extends ConsumerWidget { const CortexAppRoot({super.key}); @override - State createState() => _CortexAppRootState(); -} - -class _CortexAppRootState extends State { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { // 🎨 DARK MODE SUPPORT: Both configs are now passed to DesignProvider. final lightConfig = DesignConfig.light(); final darkConfig = DesignConfig.dark(); + final mode = ref.watch(designModeProvider); // 🌍 LOCALIZATION SUPPORT: Wrapping the root with LocalizationProvider. return LocalizationProvider( child: DesignProvider( config: lightConfig, darkConfig: darkConfig, - mode: DesignMode.system, + mode: mode, child: const CortexApp(), ), ); diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index a76a051af..c90dc6b8e 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import package_info_plus import sqlite3_flutter_libs func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) } diff --git a/app/pubspec.lock b/app/pubspec.lock index 53f7d1ff1..e65f74b27 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -185,6 +185,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" intl: dependency: transitive description: @@ -281,6 +297,22 @@ packages: url: "https://pub.dev" source: hosted version: "9.3.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -493,6 +525,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: diff --git a/openspec/changes/lms-home-drawer/.openspec.yaml b/openspec/changes/lms-home-drawer/.openspec.yaml new file mode 100644 index 000000000..e331c975d --- /dev/null +++ b/openspec/changes/lms-home-drawer/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-25 diff --git a/openspec/changes/lms-home-drawer/design.md b/openspec/changes/lms-home-drawer/design.md new file mode 100644 index 000000000..c45980b74 --- /dev/null +++ b/openspec/changes/lms-home-drawer/design.md @@ -0,0 +1,53 @@ +# Design: Home Screen Sidebar Menu (`lms-home-drawer`) + +## Overview +The `AppDrawer` is a custom-built side navigation component. Unlike the standard Material `Drawer`, it is designed to be platform-agnostic, using custom animations and the Cortex design system tokens for all visual attributes. + +## Visual Design + +### Layering & Layout +- **Backdrop**: A full-screen overlay with a semi-transparent black color (`Colors.black54` or equivalent from tokens). +- **Drawer Container**: + - **Width**: `design.layout.drawerWidth` (default: `280px`). + - **Height**: Full screen height (must overlay bottom navigation). + - **Background**: `design.colors.surface` or `design.colors.card` (consistent with dashboard headers). + - **Shadow**: `BoxShadow` with large blur radius and high spread to separate it from the content layer. + - **Border**: 1px solid border on the right edge in light mode for crisp definition. + +### Sections & Hierarchy +The drawer is divided into three distinct vertical sections separated by a thin divider (`design.colors.border`). + +1. **Section 1: Learning Tools** + - Items: Bookmark, Posts, Analytics, Forum, Doubts, Custom Exam, Your Report. +2. **Section 2: Account & Settings** + - Items: Profile, App Settings, Login Activity, Logout (Highlight with `design.colors.error`). +3. **Section 3: App Status & System** + - Items: Privacy Policy, Theme Toggle (Light/Dark), Version Info (Disabled/Label-only). + +### Typography & Iconography +- **Header**: Use `AppText.title("Menu")`. +- **Labels**: Use `AppText.body()` for item labels. +- **Icons**: Use `LucideIcons` (consistent with the rest of the app). Size set to `20px`. +- **Colors**: + - Labels: `design.colors.textPrimary`. + - Icons: `design.colors.textSecondary`. + - Colors (Logout): `design.colors.error`. + +## Design Layout Tokens +| Token | Value | Description | +|-------|-------|-------------| +| `drawerWidth` | `280.0` | Base width of the home navigation drawer. | +| `maxDrawerWidth` | `400.0` | Maximum width allowed when using responsive sizing. | + +## Interaction & State +- **Trigger**: Tapping the hamburger icon in `DashboardHeader`. +- **Animation**: + - Backdrop: Fade-in. + - Drawer: Slide-in from left. +- **Layering**: The drawer MUST be placed above the `AppTabBar` (bottom navigation) in the widget hierarchy. This is achieved by hosting the drawer in the `AppShell` level. + +## Component Integration +- **`AppDrawer` (core)**: Stateless widget that accepts a list of sections. +- **`AppShell` (core)**: Updated to accept an optional `drawer` widget and display it in a `Stack` over the main content and navigation. +- **`appRouter` (testpress)**: Configured to pass the `AppDrawer` into the `AppShell`, using Riverpod to sync state between the `DashboardHeader` and the `AppShell`. +- **`DashboardHeader` (courses)**: Continues to manage the drawer state via `isHomeDrawerOpenProvider`. diff --git a/openspec/changes/lms-home-drawer/proposal.md b/openspec/changes/lms-home-drawer/proposal.md new file mode 100644 index 000000000..345e5360b --- /dev/null +++ b/openspec/changes/lms-home-drawer/proposal.md @@ -0,0 +1,21 @@ +# Proposal: Home Screen Sidebar Menu (`lms-home-drawer`) + +## Problem +The current `DashboardHeader` contains a hamburger menu icon that is not functional. Users need a persistent navigation drawer to access secondary features such as Analytics, Bookmarks, Profile, and Settings, as well as system controls like Theme Switching and Logout. + +## Proposed Solution +Implement a high-fidelity `AppDrawer` component in `packages/core` and integrate it with the `DashboardHeader` in `packages/courses`. The drawer will follow the design and section organization defined in the design specifications, ensuring a custom, platform-agnostic look and feel that aligns with the Cortex design system. + +## Key Features +- **Sectioned Navigation**: + - Section 1: Learning tools (Bookmarks, Posts, Analytics, Forum, Doubts, Custom Exam, Reports). + - Section 2: Account management (Profile, Settings, Login Activity, Logout). + - Section 3: App info & System (Privacy, Theme Toggle, Version). +- **Custom Visuals**: Custom backdrop and drawer slide transitions to avoid default platform-specific behaviors. +- **Theme Support**: Full integration with the Design System for light/dark mode variants. +- **Semantic Typography**: Usage of `AppText` roles for labels and headers. +- **Design Governance**: Integration of layout tokens (drawer width) into the `DesignConfig` to avoid hardcoded dimensions and support future responsiveness. + +## Impact +- **Packages**: `core` (new widget), `courses` (integration). +- **User Experience**: Provides access to essential navigation items that were previously unreachable through a custom-built, brand-consistent interface. diff --git a/openspec/changes/lms-home-drawer/specs/spec.md b/openspec/changes/lms-home-drawer/specs/spec.md new file mode 100644 index 000000000..0f7dc5407 --- /dev/null +++ b/openspec/changes/lms-home-drawer/specs/spec.md @@ -0,0 +1,49 @@ +# Specs: Home Screen Sidebar Menu (`lms-home-drawer`) + +## Requirements + +### Requirement: Custom Drawer Shell +The system SHALL provide a custom-built navigation drawer that avoids default platform-specific (Material/Cupertino) UI behaviors. + +#### Scenario: Visual Parity +- **WHEN** the drawer is opened +- **THEN** it MUST use `design.layout.drawerWidth` for its width +- **AND** it MUST use `design.colors.card` for its background and `design.colors.border` for section separators. + +--- + +### Requirement: Functional Sectioning +The drawer MUST be organized into three logical sections with specific items as defined in the design reference. + +#### Scenario: Learning Tools Section +- **WHEN** the first section is rendered +- **THEN** it MUST contain: Bookmark, Posts, Analytics, Forum, Doubts, Custom Exam, and Your Report. +- **AND** each item MUST display its corresponding `LucideIcon`. + +#### Scenario: Account Management Section +- **WHEN** the second section is rendered +- **THEN** it MUST contain: Profile, App Settings, Login Activity, and Logout. +- **AND** the "Logout" item MUST use `design.colors.error` for its text and icon. + +#### Scenario: Theme Information Section +- **WHEN** the third section is rendered +- **THEN** it MUST contain a Theme Toggle (Light/Dark Mode) and Version Information. + +--- + +### Requirement: Dynamic Theme Switching +The drawer MUST allow the user to toggle between Light and Dark modes without closing the drawer immediately. + +#### Scenario: Theme Toggle Interaction +- **WHEN** the user taps the Theme Toggle item +- **THEN** the application theme MUST switch immediately +- **AND** the drawer MUST remain open to allow the user to see the transition. + +--- + +### Requirement: Integration with DashboardHeader +The existing `DashboardHeader` MUST be the designated trigger for opening the drawer. + +#### Scenario: Hamburger Trigger +- **WHEN** the hamburger menu icon in `DashboardHeader` is tapped +- **THEN** it MUST trigger the opening of the `AppDrawer`. diff --git a/openspec/changes/lms-home-drawer/tasks.md b/openspec/changes/lms-home-drawer/tasks.md new file mode 100644 index 000000000..9891c4b25 --- /dev/null +++ b/openspec/changes/lms-home-drawer/tasks.md @@ -0,0 +1,24 @@ +# Tasks: Home Screen Sidebar Menu (`lms-home-drawer`) + +## 1. Core Component Development (`packages/core`) + +- [x] 1.1 Implement `AppDrawer` base widget with custom slide-in animation and backdrop overlay. +- [x] 1.2 Implement `AppDrawerItem` for individual list items with hover/press states and custom coloring for errors. +- [x] 1.3 Implement `AppDrawerDivider` using `design.colors.border`. +- [x] 1.4 Add Theme Toggle logic within the drawer to interact with `DesignProvider`. +- [x] 1.5 Implement `DesignLayout` tokens in `DesignConfig`. +- [x] 1.6 Refactor `AppDrawer` to use `design.layout.drawerWidth`. + + +## 2. Integration & Dashboard Updates (`packages/courses`) + +- [x] 2.1 Update `DashboardHeader` to accept an `onMenuPressed` callback or integrate with a scaffold key. +- [x] 2.2 Define the menu data structure based on the specifications (Sections 1, 2, and 3). +- [x] 2.3 Integrate `AppDrawer` into `PaidActiveHomeScreen`. + +## 3. Verification & Polish + +- [x] 3.1 Verify item spacing and font sizes match the 15px/semantic role requirements. +- [x] 3.2 Ensure "Logout" and "Red" items use the correct error tokens in both themes. +- [x] 3.3 Verify backdrop tap-to-close behavior. +- [x] 3.4 Check accessibility labels for all navigation items. diff --git a/packages/core/lib/core.dart b/packages/core/lib/core.dart index d3f4a3c83..b60dcca61 100644 --- a/packages/core/lib/core.dart +++ b/packages/core/lib/core.dart @@ -12,6 +12,7 @@ export 'design/design_provider.dart'; // Icons export 'package:lucide_icons/lucide_icons.dart'; +export 'package:package_info_plus/package_info_plus.dart'; // Tokens (legacy tokens removed - use Design.of(context)) @@ -28,6 +29,7 @@ export 'widgets/app_subject_chip.dart'; export 'widgets/typography_gallery_screen.dart'; export 'widgets/app_carousel.dart'; export 'widgets/app_loading_indicator.dart'; +export 'widgets/app_drawer.dart'; // Shell export 'shell/app_shell.dart'; diff --git a/packages/core/lib/design/design_config.dart b/packages/core/lib/design/design_config.dart index 61a0d4c18..48a73f8eb 100644 --- a/packages/core/lib/design/design_config.dart +++ b/packages/core/lib/design/design_config.dart @@ -33,6 +33,7 @@ class DesignConfig { required this.typographyScale, required this.motion, required this.radius, + required this.layout, required this.subjectPalette, required this.statusColors, required this.shortcutPalette, @@ -45,6 +46,7 @@ class DesignConfig { final DesignTypographyScale typographyScale; final DesignMotion motion; final DesignRadius radius; + final DesignLayout layout; final DesignSubjectPalette subjectPalette; final DesignStatusColors statusColors; final DesignShortcutPalette shortcutPalette; @@ -69,6 +71,7 @@ class DesignConfig { typographyScale: scale, motion: DesignMotion.defaults(context: context), radius: DesignRadius.defaults(), + layout: DesignLayout.defaults(), subjectPalette: DesignSubjectPalette.light(), statusColors: DesignStatusColors.light(), shortcutPalette: DesignShortcutPalette.light(), @@ -87,6 +90,7 @@ class DesignConfig { typographyScale: scale, motion: DesignMotion.defaults(context: context), radius: DesignRadius.defaults(), + layout: DesignLayout.defaults(), subjectPalette: DesignSubjectPalette.dark(), statusColors: DesignStatusColors.dark(), shortcutPalette: DesignShortcutPalette.dark(), @@ -101,6 +105,7 @@ class DesignConfig { DesignTypographyScale? typographyScale, DesignMotion? motion, DesignRadius? radius, + DesignLayout? layout, DesignSubjectPalette? subjectPalette, DesignStatusColors? statusColors, DesignShortcutPalette? shortcutPalette, @@ -113,6 +118,7 @@ class DesignConfig { typographyScale: typographyScale ?? this.typographyScale, motion: motion ?? this.motion, radius: radius ?? this.radius, + layout: layout ?? this.layout, subjectPalette: subjectPalette ?? this.subjectPalette, statusColors: statusColors ?? this.statusColors, shortcutPalette: shortcutPalette ?? this.shortcutPalette, @@ -130,6 +136,7 @@ class DesignConfig { other.typographyScale == typographyScale && other.motion == motion && other.radius == radius && + other.layout == layout && other.subjectPalette == subjectPalette && other.statusColors == statusColors && other.shortcutPalette == shortcutPalette && @@ -145,6 +152,7 @@ class DesignConfig { typographyScale, motion, radius, + layout, subjectPalette, statusColors, shortcutPalette, @@ -195,6 +203,8 @@ class DesignColors { required this.rank2, required this.rank3, required this.rankDefault, + required this.overlay, + required this.shadow, }); // Primary brand colors @@ -250,6 +260,12 @@ class DesignColors { final Color rank3; final Color rankDefault; + // Overlay / Backdrop + final Color overlay; + + // Elevation / Shadows + final Color shadow; + factory DesignColors.light() { return const DesignColors( primary: Color(0xFF6366F1), @@ -288,6 +304,8 @@ class DesignColors { rank2: Color(0xFFCBD5E1), rank3: Color(0xFFFB923C), rankDefault: Color(0xFF94A3B8), + overlay: Color(0x8A000000), // black54 + shadow: Color(0x33000000), // 20% black ); } @@ -329,6 +347,8 @@ class DesignColors { rank2: Color(0xFF94A3B8), rank3: Color(0xFFFDBA74), rankDefault: Color(0xFF64748B), + overlay: Color(0x8A000000), // black54 + shadow: Color(0x66000000), // 40% black ); } @@ -374,6 +394,8 @@ class DesignColors { Color rank2 = const Color(0xFFCBD5E1), Color rank3 = const Color(0xFFFB923C), Color rankDefault = const Color(0xFF94A3B8), + Color overlay = const Color(0x8A000000), + Color shadow = const Color(0x33000000), }) { // Auto-calculate contrasting text colors final onPrimary = _contrastingColor(primary); @@ -427,6 +449,8 @@ class DesignColors { rank2: rank2, rank3: rank3, rankDefault: rankDefault, + overlay: overlay, + shadow: shadow, ); } @@ -501,7 +525,9 @@ class DesignColors { other.rank1 == rank1 && other.rank2 == rank2 && other.rank3 == rank3 && - other.rankDefault == rankDefault; + other.rankDefault == rankDefault && + other.overlay == overlay && + other.shadow == shadow; } @override @@ -543,6 +569,8 @@ class DesignColors { rank2, rank3, rankDefault, + overlay, + shadow, ]); } } @@ -1367,3 +1395,27 @@ class DesignRadius { ); } } + +/// Layout token group. +@immutable +class DesignLayout { + const DesignLayout({required this.drawerWidth, required this.maxDrawerWidth}); + + final double drawerWidth; + final double maxDrawerWidth; + + factory DesignLayout.defaults() { + return const DesignLayout(drawerWidth: 280.0, maxDrawerWidth: 400.0); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is DesignLayout && + other.drawerWidth == drawerWidth && + other.maxDrawerWidth == maxDrawerWidth; + } + + @override + int get hashCode => Object.hash(drawerWidth, maxDrawerWidth); +} diff --git a/packages/core/lib/generated/l10n/app_localizations.dart b/packages/core/lib/generated/l10n/app_localizations.dart index fcdf35555..14536cd27 100644 --- a/packages/core/lib/generated/l10n/app_localizations.dart +++ b/packages/core/lib/generated/l10n/app_localizations.dart @@ -459,6 +459,102 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'+{count} more'** String moreBadgesLabel(int count); + + /// No description provided for @drawerMenuTitle. + /// + /// In en, this message translates to: + /// **'Menu'** + String get drawerMenuTitle; + + /// No description provided for @drawerBookmark. + /// + /// In en, this message translates to: + /// **'Bookmark'** + String get drawerBookmark; + + /// No description provided for @drawerPosts. + /// + /// In en, this message translates to: + /// **'Posts'** + String get drawerPosts; + + /// No description provided for @drawerAnalytics. + /// + /// In en, this message translates to: + /// **'Analytics'** + String get drawerAnalytics; + + /// No description provided for @drawerForum. + /// + /// In en, this message translates to: + /// **'Forum'** + String get drawerForum; + + /// No description provided for @drawerDoubts. + /// + /// In en, this message translates to: + /// **'Doubts'** + String get drawerDoubts; + + /// No description provided for @drawerCustomExam. + /// + /// In en, this message translates to: + /// **'Custom Exam'** + String get drawerCustomExam; + + /// No description provided for @drawerReports. + /// + /// In en, this message translates to: + /// **'Reports'** + String get drawerReports; + + /// No description provided for @drawerProfile. + /// + /// In en, this message translates to: + /// **'Profile'** + String get drawerProfile; + + /// No description provided for @drawerSettings. + /// + /// In en, this message translates to: + /// **'App Settings'** + String get drawerSettings; + + /// No description provided for @drawerLoginActivity. + /// + /// In en, this message translates to: + /// **'Login Activity'** + String get drawerLoginActivity; + + /// No description provided for @drawerLogout. + /// + /// In en, this message translates to: + /// **'Logout'** + String get drawerLogout; + + /// No description provided for @drawerPrivacy. + /// + /// In en, this message translates to: + /// **'Privacy Policy'** + String get drawerPrivacy; + + /// No description provided for @drawerThemeLight. + /// + /// In en, this message translates to: + /// **'Light Mode'** + String get drawerThemeLight; + + /// No description provided for @drawerThemeDark. + /// + /// In en, this message translates to: + /// **'Dark Mode'** + String get drawerThemeDark; + + /// No description provided for @drawerVersion. + /// + /// In en, this message translates to: + /// **'Version - {version}'** + String drawerVersion(String version); } class _AppLocalizationsDelegate diff --git a/packages/core/lib/generated/l10n/app_localizations_ar.dart b/packages/core/lib/generated/l10n/app_localizations_ar.dart index 7aabf0a8a..2c3da267d 100644 --- a/packages/core/lib/generated/l10n/app_localizations_ar.dart +++ b/packages/core/lib/generated/l10n/app_localizations_ar.dart @@ -206,4 +206,54 @@ class AppLocalizationsAr extends AppLocalizations { String moreBadgesLabel(int count) { return '+$count more'; } + + @override + String get drawerMenuTitle => 'القائمة'; + + @override + String get drawerBookmark => 'الإشارات المرجعية'; + + @override + String get drawerPosts => 'المنشورات'; + + @override + String get drawerAnalytics => 'التحليلات'; + + @override + String get drawerForum => 'المنتدى'; + + @override + String get drawerDoubts => 'الأسئلة'; + + @override + String get drawerCustomExam => 'نموذج امتحان'; + + @override + String get drawerReports => 'التقارير الخاصة بك'; + + @override + String get drawerProfile => 'الملف الشخصي'; + + @override + String get drawerSettings => 'إعدادات التطبيق'; + + @override + String get drawerLoginActivity => 'نشاط تسجيل الدخول'; + + @override + String get drawerLogout => 'تسجيل الخروج'; + + @override + String get drawerPrivacy => 'سياسة الخصوصية'; + + @override + String get drawerThemeLight => 'الوضع الفاتح'; + + @override + String get drawerThemeDark => 'الوضع الداكن'; + + @override + String drawerVersion(String version) { + return 'الإصدار - $version'; + } } diff --git a/packages/core/lib/generated/l10n/app_localizations_en.dart b/packages/core/lib/generated/l10n/app_localizations_en.dart index f84802256..1af89b6e8 100644 --- a/packages/core/lib/generated/l10n/app_localizations_en.dart +++ b/packages/core/lib/generated/l10n/app_localizations_en.dart @@ -206,4 +206,54 @@ class AppLocalizationsEn extends AppLocalizations { String moreBadgesLabel(int count) { return '+$count more'; } + + @override + String get drawerMenuTitle => 'Menu'; + + @override + String get drawerBookmark => 'Bookmark'; + + @override + String get drawerPosts => 'Posts'; + + @override + String get drawerAnalytics => 'Analytics'; + + @override + String get drawerForum => 'Forum'; + + @override + String get drawerDoubts => 'Doubts'; + + @override + String get drawerCustomExam => 'Custom Exam'; + + @override + String get drawerReports => 'Reports'; + + @override + String get drawerProfile => 'Profile'; + + @override + String get drawerSettings => 'App Settings'; + + @override + String get drawerLoginActivity => 'Login Activity'; + + @override + String get drawerLogout => 'Logout'; + + @override + String get drawerPrivacy => 'Privacy Policy'; + + @override + String get drawerThemeLight => 'Light Mode'; + + @override + String get drawerThemeDark => 'Dark Mode'; + + @override + String drawerVersion(String version) { + return 'Version - $version'; + } } diff --git a/packages/core/lib/generated/l10n/app_localizations_ml.dart b/packages/core/lib/generated/l10n/app_localizations_ml.dart index b0e8ea9ec..5a737b517 100644 --- a/packages/core/lib/generated/l10n/app_localizations_ml.dart +++ b/packages/core/lib/generated/l10n/app_localizations_ml.dart @@ -206,4 +206,54 @@ class AppLocalizationsMl extends AppLocalizations { String moreBadgesLabel(int count) { return '+$count more'; } + + @override + String get drawerMenuTitle => 'മെനു'; + + @override + String get drawerBookmark => 'ബുക്ക്മാർക്ക്'; + + @override + String get drawerPosts => 'പോസ്റ്റുകൾ'; + + @override + String get drawerAnalytics => 'അനലിറ്റിക്സ്'; + + @override + String get drawerForum => 'ഫോറം'; + + @override + String get drawerDoubts => 'സംശയങ്ങൾ'; + + @override + String get drawerCustomExam => 'കസ്റ്റം എക്സാം'; + + @override + String get drawerReports => 'നിങ്ങളുടെ റിപ്പോർട്ട്'; + + @override + String get drawerProfile => 'പ്രൊഫൈൽ'; + + @override + String get drawerSettings => 'ആപ്പ് ക്രമീകരണങ്ങൾ'; + + @override + String get drawerLoginActivity => 'ലോഗിൻ ആക്റ്റിവിറ്റി'; + + @override + String get drawerLogout => 'ലോഗൗട്ട്'; + + @override + String get drawerPrivacy => 'സ്വകാര്യതാ നയം'; + + @override + String get drawerThemeLight => 'ലൈറ്റ് മോഡ്'; + + @override + String get drawerThemeDark => 'ഡാർക്ക് മോഡ്'; + + @override + String drawerVersion(String version) { + return 'വേർഷൻ - $version'; + } } diff --git a/packages/core/lib/l10n/app_ar.arb b/packages/core/lib/l10n/app_ar.arb index 5136175fc..01aca5c76 100644 --- a/packages/core/lib/l10n/app_ar.arb +++ b/packages/core/lib/l10n/app_ar.arb @@ -20,5 +20,21 @@ "labelStart": "ابدأ", "labelContinue": "استمر", "labelProgress": "تقدم", - "labelCourseProgress": "تقدم الدورة" + "labelCourseProgress": "تقدم الدورة", + "drawerMenuTitle": "القائمة", + "drawerBookmark": "الإشارات المرجعية", + "drawerPosts": "المنشورات", + "drawerAnalytics": "التحليلات", + "drawerForum": "المنتدى", + "drawerDoubts": "الأسئلة", + "drawerCustomExam": "نموذج امتحان", + "drawerReports": "التقارير الخاصة بك", + "drawerProfile": "الملف الشخصي", + "drawerSettings": "إعدادات التطبيق", + "drawerLoginActivity": "نشاط تسجيل الدخول", + "drawerLogout": "تسجيل الخروج", + "drawerPrivacy": "سياسة الخصوصية", + "drawerThemeLight": "الوضع الفاتح", + "drawerThemeDark": "الوضع الداكن", + "drawerVersion": "الإصدار - {version}" } diff --git a/packages/core/lib/l10n/app_en.arb b/packages/core/lib/l10n/app_en.arb index 471888ff0..71ed429dc 100644 --- a/packages/core/lib/l10n/app_en.arb +++ b/packages/core/lib/l10n/app_en.arb @@ -110,5 +110,28 @@ "type": "int" } } + }, + "drawerMenuTitle": "Menu", + "drawerBookmark": "Bookmark", + "drawerPosts": "Posts", + "drawerAnalytics": "Analytics", + "drawerForum": "Forum", + "drawerDoubts": "Doubts", + "drawerCustomExam": "Custom Exam", + "drawerReports": "Reports", + "drawerProfile": "Profile", + "drawerSettings": "App Settings", + "drawerLoginActivity": "Login Activity", + "drawerLogout": "Logout", + "drawerPrivacy": "Privacy Policy", + "drawerThemeLight": "Light Mode", + "drawerThemeDark": "Dark Mode", + "drawerVersion": "Version - {version}", + "@drawerVersion": { + "placeholders": { + "version": { + "type": "String" + } + } } } diff --git a/packages/core/lib/l10n/app_ml.arb b/packages/core/lib/l10n/app_ml.arb index f483d0a44..d5fa604bb 100644 --- a/packages/core/lib/l10n/app_ml.arb +++ b/packages/core/lib/l10n/app_ml.arb @@ -20,5 +20,21 @@ "labelStart": "തുടങ്ങുക", "labelContinue": "തുടരുക", "labelProgress": "പുരോഗതി", - "labelCourseProgress": "കോഴ്സിന്റെ പുരോഗതി" + "labelCourseProgress": "കോഴ്സിന്റെ പുരോഗതി", + "drawerMenuTitle": "മെനു", + "drawerBookmark": "ബുക്ക്മാർക്ക്", + "drawerPosts": "പോസ്റ്റുകൾ", + "drawerAnalytics": "അനലിറ്റിക്സ്", + "drawerForum": "ഫോറം", + "drawerDoubts": "സംശയങ്ങൾ", + "drawerCustomExam": "കസ്റ്റം എക്സാം", + "drawerReports": "നിങ്ങളുടെ റിപ്പോർട്ട്", + "drawerProfile": "പ്രൊഫൈൽ", + "drawerSettings": "ആപ്പ് ക്രമീകരണങ്ങൾ", + "drawerLoginActivity": "ലോഗിൻ ആക്റ്റിവിറ്റി", + "drawerLogout": "ലോഗൗട്ട്", + "drawerPrivacy": "സ്വകാര്യതാ നയം", + "drawerThemeLight": "ലൈറ്റ് മോഡ്", + "drawerThemeDark": "ഡാർക്ക് മോഡ്", + "drawerVersion": "വേർഷൻ - {version}" } diff --git a/packages/core/lib/shell/app_shell.dart b/packages/core/lib/shell/app_shell.dart index 567117168..6c1c073c6 100644 --- a/packages/core/lib/shell/app_shell.dart +++ b/packages/core/lib/shell/app_shell.dart @@ -11,25 +11,35 @@ class AppShell extends StatelessWidget { required this.child, this.backgroundColor, this.bottomNavigationBar, + this.drawer, }); final Widget child; final Color? backgroundColor; final Widget? bottomNavigationBar; + final Widget? drawer; @override Widget build(BuildContext context) { final design = Design.of(context); - return Container( - color: backgroundColor ?? design.colors.surface, - child: Column( - children: [ - Expanded( - child: SafeArea(bottom: bottomNavigationBar == null, child: child), + return Stack( + children: [ + Container( + color: backgroundColor ?? design.colors.surface, + child: Column( + children: [ + Expanded( + child: SafeArea( + bottom: bottomNavigationBar == null, + child: child, + ), + ), + if (bottomNavigationBar != null) bottomNavigationBar!, + ], ), - ?bottomNavigationBar, - ], - ), + ), + if (drawer != null) drawer!, + ], ); } } diff --git a/packages/core/lib/widgets/app_drawer.dart b/packages/core/lib/widgets/app_drawer.dart new file mode 100644 index 000000000..9aca28211 --- /dev/null +++ b/packages/core/lib/widgets/app_drawer.dart @@ -0,0 +1,240 @@ +import 'package:flutter/widgets.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import '../design/design_provider.dart'; +import 'app_text.dart'; + +class AppDrawerItem { + final IconData icon; + final String label; + final VoidCallback action; + final bool isRed; + final bool disabled; + final bool keepMenuOpen; + + const AppDrawerItem({ + required this.icon, + required this.label, + required this.action, + this.isRed = false, + this.disabled = false, + this.keepMenuOpen = false, + }); +} + +class AppDrawerSection { + final List items; + const AppDrawerSection({required this.items}); +} + +class AppDrawer extends StatefulWidget { + const AppDrawer({ + super.key, + required this.isOpen, + required this.onClose, + required this.sections, + this.title = 'Menu', + }); + + final bool isOpen; + final VoidCallback onClose; + final List sections; + final String title; + + @override + State createState() => _AppDrawerState(); +} + +class _AppDrawerState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _slideAnimation; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + _slideAnimation = Tween( + begin: const Offset(-1.0, 0.0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + _fadeAnimation = CurvedAnimation(parent: _controller, curve: Curves.linear); + + if (widget.isOpen) { + _controller.forward(); + } + } + + @override + void didUpdateWidget(AppDrawer oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isOpen != oldWidget.isOpen) { + if (widget.isOpen) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final design = Design.of(context); + + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + if (_controller.isDismissed && !widget.isOpen) { + return const SizedBox.shrink(); + } + + return Stack( + children: [ + // Backdrop + GestureDetector( + onTap: widget.onClose, + child: FadeTransition( + opacity: _fadeAnimation, + child: Container(color: design.colors.overlay), + ), + ), + // Drawer Container + SlideTransition( + position: _slideAnimation, + child: Container( + width: design.layout.drawerWidth, + decoration: BoxDecoration( + color: design.colors.card, + boxShadow: [ + BoxShadow( + color: design.colors.shadow, + blurRadius: 16, + offset: const Offset(8, 0), + ), + ], + border: Border( + right: BorderSide( + color: design.colors.border, + width: design.isDark ? 0 : 1, + ), + ), + ), + child: SafeArea( + child: Column( + children: [ + // Header + _buildHeader(context), + // Scrollable content + Expanded( + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: widget.sections.length, + separatorBuilder: (context, index) => Container( + height: 1, + margin: const EdgeInsets.symmetric(vertical: 8), + color: design.colors.border, + ), + itemBuilder: (context, index) { + return _buildSection( + context, + widget.sections[index], + ); + }, + ), + ), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } + + Widget _buildHeader(BuildContext context) { + final design = Design.of(context); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: design.colors.border, width: 1), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppText.title(widget.title, color: design.colors.textPrimary), + GestureDetector( + onTap: widget.onClose, + child: Icon( + LucideIcons.x, + size: 20, + color: design.colors.textSecondary, + ), + ), + ], + ), + ); + } + + Widget _buildSection(BuildContext context, AppDrawerSection section) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + children: section.items + .map((item) => _buildItem(context, item)) + .toList(), + ), + ); + } + + Widget _buildItem(BuildContext context, AppDrawerItem item) { + final design = Design.of(context); + final iconColor = item.disabled + ? design.colors.textTertiary + : item.isRed + ? design.colors.error + : design.colors.textSecondary; + final textColor = item.disabled + ? design.colors.textTertiary + : item.isRed + ? design.colors.error + : design.colors.textPrimary; + + return Semantics( + button: true, + label: item.label, + enabled: !item.disabled, + child: GestureDetector( + onTap: item.disabled + ? null + : () { + item.action(); + if (!item.keepMenuOpen) widget.onClose(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), + child: Row( + children: [ + Icon(item.icon, size: 20, color: iconColor), + const SizedBox(width: 12), + Expanded(child: AppText.body(item.label, color: textColor)), + ], + ), + ), + ), + ); + } +} diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index 28486edd4..e3ddb015f 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: go_router: ^17.1.0 intl: any lucide_icons: ^0.257.0 + package_info_plus: ^8.2.1 dev_dependencies: flutter_test: diff --git a/packages/core/test/design/design_motion_test.dart b/packages/core/test/design/design_motion_test.dart index 0b277b118..947a1a954 100644 --- a/packages/core/test/design/design_motion_test.dart +++ b/packages/core/test/design/design_motion_test.dart @@ -25,6 +25,7 @@ void main() { spring: Curves.easeOutCubic, ), radius: DesignRadius.defaults(), + layout: DesignLayout.defaults(), subjectPalette: DesignSubjectPalette.light(), statusColors: DesignStatusColors.light(), shortcutPalette: DesignShortcutPalette.light(), @@ -74,6 +75,7 @@ void main() { spring: Curves.easeOutCubic, ), radius: DesignRadius.defaults(), + layout: DesignLayout.defaults(), subjectPalette: DesignSubjectPalette.light(), statusColors: DesignStatusColors.light(), shortcutPalette: DesignShortcutPalette.light(), diff --git a/packages/core/test/design/design_provider_test.dart b/packages/core/test/design/design_provider_test.dart index 91db4bd4e..98c72adc9 100644 --- a/packages/core/test/design/design_provider_test.dart +++ b/packages/core/test/design/design_provider_test.dart @@ -43,6 +43,8 @@ void main() { rank2: Color(0xFFCBD5E1), rank3: Color(0xFFFB923C), rankDefault: Color(0xFF94A3B8), + overlay: Color(0x8A000000), + shadow: Color(0x33000000), ); final customConfig = DesignConfig( @@ -52,6 +54,7 @@ void main() { typographyScale: DesignTypographyScale.defaults(), motion: DesignMotion.defaults(), radius: DesignRadius.defaults(), + layout: DesignLayout.defaults(), subjectPalette: DesignSubjectPalette.light(), statusColors: DesignStatusColors.light(), shortcutPalette: DesignShortcutPalette.light(), @@ -129,6 +132,8 @@ void main() { rank2: Color(0xFFCBD5E1), rank3: Color(0xFFFB923C), rankDefault: Color(0xFF94A3B8), + overlay: Color(0x8A000000), + shadow: Color(0x33000000), ); final config2 = DesignConfig( @@ -138,6 +143,7 @@ void main() { typographyScale: DesignTypographyScale.defaults(), motion: DesignMotion.defaults(), radius: DesignRadius.defaults(), + layout: DesignLayout.defaults(), subjectPalette: DesignSubjectPalette.light(), statusColors: DesignStatusColors.light(), shortcutPalette: DesignShortcutPalette.light(), diff --git a/packages/core/test/widgets/app_primitive_design_test.dart b/packages/core/test/widgets/app_primitive_design_test.dart index a89811be1..c9dda5ea0 100644 --- a/packages/core/test/widgets/app_primitive_design_test.dart +++ b/packages/core/test/widgets/app_primitive_design_test.dart @@ -42,6 +42,8 @@ void main() { rank2: Color(0xFFCBD5E1), rank3: Color(0xFFFB923C), rankDefault: Color(0xFF94A3B8), + overlay: Color(0x8A000000), + shadow: Color(0x33000000), ); final customConfig = DesignConfig( @@ -51,6 +53,7 @@ void main() { typographyScale: DesignTypographyScale.defaults(), motion: DesignMotion.defaults(), radius: DesignRadius.defaults(), + layout: DesignLayout.defaults(), subjectPalette: DesignSubjectPalette.light(), statusColors: DesignStatusColors.light(), shortcutPalette: DesignShortcutPalette.light(), @@ -124,6 +127,8 @@ void main() { rank2: Color(0xFFCBD5E1), rank3: Color(0xFFFB923C), rankDefault: Color(0xFF94A3B8), + overlay: Color(0x8A000000), + shadow: Color(0x33000000), ); final customConfig = DesignConfig( @@ -133,6 +138,7 @@ void main() { typographyScale: DesignTypographyScale.defaults(), motion: DesignMotion.defaults(), radius: DesignRadius.defaults(), + layout: DesignLayout.defaults(), subjectPalette: DesignSubjectPalette.light(), statusColors: DesignStatusColors.light(), shortcutPalette: DesignShortcutPalette.light(), diff --git a/packages/courses/lib/courses.dart b/packages/courses/lib/courses.dart index 30447d609..b606a3e20 100644 --- a/packages/courses/lib/courses.dart +++ b/packages/courses/lib/courses.dart @@ -12,3 +12,4 @@ export 'widgets/today_snapshot.dart'; export 'widgets/top_learners_section.dart'; export 'widgets/promotional_banners.dart'; export 'widgets/contextual_hero_card.dart'; +export 'widgets/dashboard_drawer.dart'; diff --git a/packages/courses/lib/providers/dashboard_providers.dart b/packages/courses/lib/providers/dashboard_providers.dart index 4f1e342a0..1d5d41b26 100644 --- a/packages/courses/lib/providers/dashboard_providers.dart +++ b/packages/courses/lib/providers/dashboard_providers.dart @@ -1,10 +1,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:core/core.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:data/data.dart'; import '../data/mock_data.dart'; part 'dashboard_providers.g.dart'; +@riverpod +Future appVersion(Ref ref) async { + final packageInfo = await PackageInfo.fromPlatform(); + return packageInfo.version; +} + @riverpod Future> todayClasses(Ref ref) async { // Simulate API delay @@ -70,3 +77,8 @@ Future> quickShortcuts(Ref ref) async { await Future.delayed(const Duration(milliseconds: 450)); return mockQuickShortcuts; } + +final isHomeDrawerOpenProvider = StateProvider((ref) => false); +final designModeProvider = StateProvider( + (ref) => DesignMode.system, +); diff --git a/packages/courses/lib/providers/dashboard_providers.g.dart b/packages/courses/lib/providers/dashboard_providers.g.dart index 9a64ebca2..a7ddc68d8 100644 --- a/packages/courses/lib/providers/dashboard_providers.g.dart +++ b/packages/courses/lib/providers/dashboard_providers.g.dart @@ -6,6 +6,23 @@ part of 'dashboard_providers.dart'; // RiverpodGenerator // ************************************************************************** +String _$appVersionHash() => r'e8bdf0eb01e50b65eb7931eadc45c32b561fce64'; + +/// See also [appVersion]. +@ProviderFor(appVersion) +final appVersionProvider = AutoDisposeFutureProvider.internal( + appVersion, + name: r'appVersionProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$appVersionHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef AppVersionRef = AutoDisposeFutureProviderRef; String _$todayClassesHash() => r'2b7ed9170d498415e7295bc96e4937024efecb78'; /// See also [todayClasses]. diff --git a/packages/courses/lib/screens/paid_active_home_screen.dart b/packages/courses/lib/screens/paid_active_home_screen.dart index d01993f67..b7107fa67 100644 --- a/packages/courses/lib/screens/paid_active_home_screen.dart +++ b/packages/courses/lib/screens/paid_active_home_screen.dart @@ -36,7 +36,12 @@ class PaidActiveHomeScreen extends ConsumerWidget { backgroundColor: design.colors.canvas, body: Column( children: [ - DashboardHeader(title: L10n.of(context).homeHeaderTitle), + DashboardHeader( + title: L10n.of(context).homeHeaderTitle, + onMenuPressed: () { + ref.read(isHomeDrawerOpenProvider.notifier).state = true; + }, + ), Expanded( child: AppScroll( padding: EdgeInsets.symmetric(vertical: design.spacing.md), @@ -65,6 +70,7 @@ class PaidActiveHomeScreen extends ConsumerWidget { c.status == dto.LiveClassStatus.upcoming, orElse: () => classes.first, ); + return Padding( padding: EdgeInsets.symmetric( horizontal: design.spacing.md, diff --git a/packages/courses/lib/widgets/dashboard_drawer.dart b/packages/courses/lib/widgets/dashboard_drawer.dart new file mode 100644 index 000000000..bd530bb37 --- /dev/null +++ b/packages/courses/lib/widgets/dashboard_drawer.dart @@ -0,0 +1,122 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:core/core.dart'; +import '../providers/dashboard_providers.dart'; + +class DashboardDrawer extends ConsumerWidget { + const DashboardDrawer({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final design = Design.of(context); + final isOpen = ref.watch(isHomeDrawerOpenProvider); + + final l10n = L10n.of(context); + final version = ref.watch(appVersionProvider).value ?? '...'; + + return AppDrawer( + isOpen: isOpen, + title: l10n.drawerMenuTitle, + onClose: () { + ref.read(isHomeDrawerOpenProvider.notifier).state = false; + }, + sections: [ + AppDrawerSection( + items: [ + AppDrawerItem( + icon: LucideIcons.bookmark, + label: l10n.drawerBookmark, + action: () {}, + ), + AppDrawerItem( + icon: LucideIcons.fileText, + label: l10n.drawerPosts, + action: () {}, + ), + AppDrawerItem( + icon: LucideIcons.pieChart, + label: l10n.drawerAnalytics, + action: () {}, + ), + AppDrawerItem( + icon: LucideIcons.messageSquare, + label: l10n.drawerForum, + action: () {}, + ), + AppDrawerItem( + icon: LucideIcons.messageCircle, + label: l10n.drawerDoubts, + action: () {}, + ), + AppDrawerItem( + icon: LucideIcons.fileCheck, + label: l10n.drawerCustomExam, + action: () {}, + ), + AppDrawerItem( + icon: LucideIcons.barChart, + label: l10n.drawerReports, + action: () {}, + ), + ], + ), + AppDrawerSection( + items: [ + AppDrawerItem( + icon: LucideIcons.user, + label: l10n.drawerProfile, + action: () {}, + ), + AppDrawerItem( + icon: LucideIcons.settings, + label: l10n.drawerSettings, + action: () {}, + ), + AppDrawerItem( + icon: LucideIcons.lock, + label: l10n.drawerLoginActivity, + action: () {}, + ), + AppDrawerItem( + icon: LucideIcons.logOut, + label: l10n.drawerLogout, + action: () {}, + isRed: true, + ), + ], + ), + AppDrawerSection( + items: [ + AppDrawerItem( + icon: LucideIcons.shield, + label: l10n.drawerPrivacy, + action: () {}, + ), + AppDrawerItem( + icon: design.isDark ? LucideIcons.sun : LucideIcons.moon, + label: design.isDark + ? l10n.drawerThemeLight + : l10n.drawerThemeDark, + action: () { + final currentMode = ref.read(designModeProvider); + final newMode = currentMode == DesignMode.dark + ? DesignMode.light + : DesignMode.dark; + ref.read(designModeProvider.notifier).state = newMode; + }, + keepMenuOpen: true, + ), + AppDrawerItem( + icon: LucideIcons.info, + label: l10n.drawerVersion(version), + action: _dummyAction, + disabled: true, + ), + ], + ), + ], + ); + } + + static void _dummyAction() {} +} diff --git a/packages/courses/lib/widgets/dashboard_header.dart b/packages/courses/lib/widgets/dashboard_header.dart index 0135de534..47bdb7378 100644 --- a/packages/courses/lib/widgets/dashboard_header.dart +++ b/packages/courses/lib/widgets/dashboard_header.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart' show Icons; import 'package:core/core.dart'; class DashboardHeader extends StatelessWidget { - const DashboardHeader({super.key, required this.title}); + const DashboardHeader({super.key, required this.title, this.onMenuPressed}); final String title; + final VoidCallback? onMenuPressed; @override Widget build(BuildContext context) { @@ -26,7 +27,10 @@ class DashboardHeader extends StatelessWidget { ), child: Row( children: [ - const Icon(Icons.menu_rounded, size: 28), + GestureDetector( + onTap: onMenuPressed, + child: const Icon(Icons.menu_rounded, size: 28), + ), const SizedBox(width: 12), Expanded( child: AppText.subtitle(title, color: design.colors.textPrimary), diff --git a/packages/testpress/lib/navigation/app_router.dart b/packages/testpress/lib/navigation/app_router.dart index 274fadd4a..3ffd7725b 100644 --- a/packages/testpress/lib/navigation/app_router.dart +++ b/packages/testpress/lib/navigation/app_router.dart @@ -74,6 +74,7 @@ final GoRouter appRouter = GoRouter( activeItemId: _getCurrentTabId(navigationShell.currentIndex), onTabChange: (id) => _onTabItemTapped(navigationShell, id), ), + drawer: const DashboardDrawer(), child: navigationShell, ); },