diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..2843eed9 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +.DEFAULT_GOAL := help + +SHELL := /bin/bash + +FLUTTER ?= flutter +PNPM ?= pnpm +DART ?= dart +DEVICE ?= macos + +.PHONY: help deps analyze test check format run build-macos build-ios-sim package-mac install-mac clean + +help: ## Show available targets + @grep -E '^[a-zA-Z0-9_.-]+:.*?## ' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-18s %s\n", $$1, $$2}' + +deps: ## Install Flutter dependencies + $(FLUTTER) pub get + +analyze: ## Run static analysis + $(FLUTTER) analyze + +test: ## Run Flutter tests + $(FLUTTER) test + +check: analyze test ## Run the standard validation suite + +format: ## Format Dart sources + $(DART) format lib test + +run: ## Run the app on a device or desktop target (DEVICE=macos by default) + $(FLUTTER) run -d $(DEVICE) + +build-macos: ## Build the macOS app in release mode + $(FLUTTER) build macos --release + +build-ios-sim: ## Build the iOS app for the simulator + $(FLUTTER) build ios --simulator + +package-mac: ## Create the macOS .app and DMG + bash scripts/package-flutter-mac-app.sh + +install-mac: ## Package and install the macOS app into /Applications + bash scripts/package-flutter-mac-app.sh + bash scripts/install-flutter-mac-dmg.sh + +clean: ## Remove generated artifacts + $(FLUTTER) clean + rm -rf build dist diff --git a/lib/theme/app_palette.dart b/lib/theme/app_palette.dart index eb9f6c4b..4fc5772b 100644 --- a/lib/theme/app_palette.dart +++ b/lib/theme/app_palette.dart @@ -12,7 +12,9 @@ class AppPalette extends ThemeExtension { required this.stroke, required this.strokeSoft, required this.accent, + required this.accentHover, required this.accentMuted, + required this.idle, required this.success, required this.warning, required this.danger, @@ -32,7 +34,9 @@ class AppPalette extends ThemeExtension { final Color stroke; final Color strokeSoft; final Color accent; + final Color accentHover; final Color accentMuted; + final Color idle; final Color success; final Color warning; final Color danger; @@ -43,45 +47,49 @@ class AppPalette extends ThemeExtension { final Color hover; static const AppPalette light = AppPalette( - canvas: Color(0xFFF5F6F7), - sidebar: Color(0xFFF3F4F6), - sidebarBorder: Color(0xFFE6E8EB), + canvas: Color(0xFFF8FAFC), + sidebar: Color(0xFFF8FAFC), + sidebarBorder: Color(0xFFE5E7EB), surfacePrimary: Color(0xFFFFFFFF), - surfaceSecondary: Color(0xFFFAFAFB), - surfaceTertiary: Color(0xFFF2F4F6), - stroke: Color(0xFFE7E9EC), - strokeSoft: Color(0xFFF1F3F5), - accent: Color(0xFF247A66), - accentMuted: Color(0xFFE6F3EF), - success: Color(0xFF228163), - warning: Color(0xFFC88A34), - danger: Color(0xFFD15A5A), - textPrimary: Color(0xFF13161A), - textSecondary: Color(0xFF4F5A68), - textMuted: Color(0xFF78808B), - shadow: Color(0x14111822), - hover: Color(0xFFF0F2F4), + surfaceSecondary: Color(0xFFF8FAFC), + surfaceTertiary: Color(0xFFF1F5F9), + stroke: Color(0xFFE5E7EB), + strokeSoft: Color(0xFFF1F5F9), + accent: Color(0xFF3B82F6), + accentHover: Color(0xFF2563EB), + accentMuted: Color(0xFFDBEAFE), + idle: Color(0xFF94A3B8), + success: Color(0xFF22C55E), + warning: Color(0xFFF59E0B), + danger: Color(0xFFEF4444), + textPrimary: Color(0xFF111827), + textSecondary: Color(0xFF6B7280), + textMuted: Color(0xFF64748B), + shadow: Color(0x0F0F172A), + hover: Color(0xFFEFF6FF), ); static const AppPalette dark = AppPalette( - canvas: Color(0xFF121416), - sidebar: Color(0xFF15181B), - sidebarBorder: Color(0xFF23272C), - surfacePrimary: Color(0xFF1A1D21), - surfaceSecondary: Color(0xFF20242A), - surfaceTertiary: Color(0xFF262B33), - stroke: Color(0xFF2D333A), - strokeSoft: Color(0xFF21262C), - accent: Color(0xFF3AB08F), - accentMuted: Color(0xFF16372E), - success: Color(0xFF51C397), - warning: Color(0xFFE2A14A), - danger: Color(0xFFFF8585), - textPrimary: Color(0xFFF4F6F8), - textSecondary: Color(0xFFBCC3CC), - textMuted: Color(0xFF9098A4), + canvas: Color(0xFF0B1220), + sidebar: Color(0xFF0F172A), + sidebarBorder: Color(0xFF1E293B), + surfacePrimary: Color(0xFF111827), + surfaceSecondary: Color(0xFF0F172A), + surfaceTertiary: Color(0xFF172033), + stroke: Color(0xFF223046), + strokeSoft: Color(0xFF162033), + accent: Color(0xFF3B82F6), + accentHover: Color(0xFF2563EB), + accentMuted: Color(0xFF142B52), + idle: Color(0xFF94A3B8), + success: Color(0xFF22C55E), + warning: Color(0xFFF59E0B), + danger: Color(0xFFEF4444), + textPrimary: Color(0xFFF8FAFC), + textSecondary: Color(0xFF94A3B8), + textMuted: Color(0xFF64748B), shadow: Color(0x52000000), - hover: Color(0xFF232830), + hover: Color(0xFF11213A), ); @override @@ -95,7 +103,9 @@ class AppPalette extends ThemeExtension { Color? stroke, Color? strokeSoft, Color? accent, + Color? accentHover, Color? accentMuted, + Color? idle, Color? success, Color? warning, Color? danger, @@ -115,7 +125,9 @@ class AppPalette extends ThemeExtension { stroke: stroke ?? this.stroke, strokeSoft: strokeSoft ?? this.strokeSoft, accent: accent ?? this.accent, + accentHover: accentHover ?? this.accentHover, accentMuted: accentMuted ?? this.accentMuted, + idle: idle ?? this.idle, success: success ?? this.success, warning: warning ?? this.warning, danger: danger ?? this.danger, @@ -152,7 +164,9 @@ class AppPalette extends ThemeExtension { stroke: Color.lerp(stroke, other.stroke, t) ?? stroke, strokeSoft: Color.lerp(strokeSoft, other.strokeSoft, t) ?? strokeSoft, accent: Color.lerp(accent, other.accent, t) ?? accent, + accentHover: Color.lerp(accentHover, other.accentHover, t) ?? accentHover, accentMuted: Color.lerp(accentMuted, other.accentMuted, t) ?? accentMuted, + idle: Color.lerp(idle, other.idle, t) ?? idle, success: Color.lerp(success, other.success, t) ?? success, warning: Color.lerp(warning, other.warning, t) ?? warning, danger: Color.lerp(danger, other.danger, t) ?? danger, diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 6dcb01dd..793054f7 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'app_palette.dart'; @@ -13,6 +14,11 @@ class AppTheme { required Brightness brightness, required AppPalette palette, }) { + final platform = defaultTargetPlatform; + final isDesktop = + platform == TargetPlatform.macOS || + platform == TargetPlatform.windows || + platform == TargetPlatform.linux; final colorScheme = ColorScheme.fromSeed( seedColor: palette.accent, @@ -43,47 +49,22 @@ class AppTheme { final base = ThemeData( useMaterial3: true, brightness: brightness, + typography: Typography.material2021(platform: platform), colorScheme: colorScheme, scaffoldBackgroundColor: palette.canvas, extensions: [palette], ); + final tunedTextTheme = _textTheme( + base.textTheme, + palette: palette, + isDesktop: isDesktop, + ); return base.copyWith( splashFactory: NoSplash.splashFactory, dividerColor: palette.strokeSoft, hoverColor: palette.hover, - textTheme: base.textTheme.copyWith( - displaySmall: base.textTheme.displaySmall?.copyWith( - fontWeight: FontWeight.w600, - letterSpacing: -0.9, - ), - headlineSmall: base.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - letterSpacing: -0.5, - ), - titleLarge: base.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - letterSpacing: -0.2, - ), - titleMedium: base.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - bodyLarge: base.textTheme.bodyLarge?.copyWith( - height: 1.45, - color: palette.textPrimary, - ), - bodyMedium: base.textTheme.bodyMedium?.copyWith( - height: 1.4, - color: palette.textSecondary, - ), - bodySmall: base.textTheme.bodySmall?.copyWith( - height: 1.35, - color: palette.textMuted, - ), - labelLarge: base.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, - ), - ), + textTheme: tunedTextTheme, appBarTheme: const AppBarTheme( backgroundColor: Colors.transparent, elevation: 0, @@ -121,10 +102,12 @@ class AppTheme { inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: palette.surfaceSecondary, - hintStyle: TextStyle(color: palette.textMuted), + hintStyle: tunedTextTheme.bodyMedium?.copyWith( + color: palette.textMuted, + ), contentPadding: const EdgeInsets.symmetric( - horizontal: 18, - vertical: 18, + horizontal: 16, + vertical: 16, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), @@ -170,4 +153,60 @@ class AppTheme { ), ); } + + static TextTheme _textTheme( + TextTheme base, { + required AppPalette palette, + required bool isDesktop, + }) { + return base.copyWith( + displaySmall: base.displaySmall?.copyWith( + fontSize: isDesktop ? 32 : 34, + fontWeight: FontWeight.w600, + letterSpacing: -0.9, + ), + headlineSmall: base.headlineSmall?.copyWith( + fontSize: isDesktop ? 22 : 24, + fontWeight: FontWeight.w600, + letterSpacing: -0.45, + ), + titleLarge: base.titleLarge?.copyWith( + fontSize: isDesktop ? 18 : 20, + fontWeight: FontWeight.w600, + letterSpacing: -0.2, + ), + titleMedium: base.titleMedium?.copyWith( + fontSize: isDesktop ? 15 : 16, + fontWeight: FontWeight.w600, + ), + titleSmall: base.titleSmall?.copyWith( + fontSize: isDesktop ? 13 : 14, + fontWeight: FontWeight.w600, + ), + bodyLarge: base.bodyLarge?.copyWith( + fontSize: isDesktop ? 14 : 15, + height: 1.45, + color: palette.textPrimary, + ), + bodyMedium: base.bodyMedium?.copyWith( + fontSize: isDesktop ? 13 : 14, + height: 1.4, + color: palette.textSecondary, + ), + bodySmall: base.bodySmall?.copyWith( + fontSize: isDesktop ? 12 : 12, + height: 1.35, + color: palette.textMuted, + ), + labelLarge: base.labelLarge?.copyWith( + fontSize: isDesktop ? 13 : 14, + fontWeight: FontWeight.w600, + ), + labelMedium: base.labelMedium?.copyWith( + fontSize: isDesktop ? 12 : 12, + fontWeight: FontWeight.w600, + ), + labelSmall: base.labelSmall?.copyWith(fontSize: isDesktop ? 11 : 11), + ); + } } diff --git a/lib/widgets/surface_card.dart b/lib/widgets/surface_card.dart index 720af10c..ba9ce6ba 100644 --- a/lib/widgets/surface_card.dart +++ b/lib/widgets/surface_card.dart @@ -6,9 +6,9 @@ class SurfaceCard extends StatefulWidget { const SurfaceCard({ super.key, required this.child, - this.padding = const EdgeInsets.all(20), + this.padding = const EdgeInsets.all(16), this.onTap, - this.borderRadius = 20, + this.borderRadius = 16, this.color, }); @@ -43,8 +43,8 @@ class _SurfaceCardState extends State { boxShadow: [ BoxShadow( color: palette.shadow.withValues(alpha: _hovered ? 0.12 : 0.07), - blurRadius: _hovered ? 22 : 16, - offset: const Offset(0, 10), + blurRadius: _hovered ? 12 : 8, + offset: const Offset(0, 4), ), ], ), diff --git a/lib/widgets/top_bar.dart b/lib/widgets/top_bar.dart index 2600b487..80554ac1 100644 --- a/lib/widgets/top_bar.dart +++ b/lib/widgets/top_bar.dart @@ -23,12 +23,9 @@ class TopBar extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.headlineSmall), - const SizedBox(height: 8), + const SizedBox(height: 6), Text(subtitle, style: Theme.of(context).textTheme.bodyLarge), - if (trailing != null) ...[ - const SizedBox(height: 16), - trailing!, - ], + if (trailing != null) ...[const SizedBox(height: 12), trailing!], ], ); } @@ -41,13 +38,13 @@ class TopBar extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.headlineSmall), - const SizedBox(height: 8), + const SizedBox(height: 6), Text(subtitle, style: Theme.of(context).textTheme.bodyLarge), ], ), ), if (trailing != null) ...[ - const SizedBox(width: 24), + const SizedBox(width: 16), Flexible(child: trailing!), ], ],