UI 전체 리디자인 및 개선사항 적용
## 주요 변경사항: ### UI/UX 개선 - shadcn/ui 스타일 기반의 새로운 디자인 시스템 도입 - 모든 주요 화면에 대한 리디자인 구현 완료 - 로그인 화면: 모던한 카드 스타일 적용 - 대시보드: 통계 카드와 차트를 활용한 개요 화면 - 리스트 화면들: 일관된 테이블 디자인과 검색/필터 기능 - 다크모드 지원을 위한 테마 시스템 구축 ### 기능 개선 - Equipment List: 고급 필터링 (상태, 담당자별) - Company List: 검색 및 정렬 기능 강화 - User List: 역할별 필터링 추가 - License List: 만료일 기반 상태 표시 - Warehouse Location: 재고 수준 시각화 ### 기술적 개선 - 재사용 가능한 컴포넌트 라이브러리 구축 - 일관된 코드 패턴 가이드라인 작성 - 프로젝트 구조 분석 및 문서화 ### 문서화 - 프로젝트 분석 문서 추가 - UI 리디자인 진행 상황 문서 - 코드 패턴 가이드 작성 - Equipment 기능 격차 분석 및 구현 계획 ### 삭제/리팩토링 - goods_list.dart 제거 (equipment_list로 통합) - 불필요한 import 및 코드 정리 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,6 @@ import 'package:superport/screens/company/company_list.dart';
|
||||
import 'package:superport/screens/user/user_list.dart';
|
||||
import 'package:superport/screens/license/license_list.dart';
|
||||
import 'package:superport/screens/warehouse_location/warehouse_location_list.dart';
|
||||
import 'package:superport/screens/goods/goods_list.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
|
||||
/// SPA 스타일의 앱 레이아웃 클래스
|
||||
@@ -41,8 +40,6 @@ class _AppLayoutState extends State<AppLayout> {
|
||||
case Routes.equipmentRentList:
|
||||
// 장비 목록 화면에 현재 라우트 정보를 전달
|
||||
return EquipmentListScreen(currentRoute: route);
|
||||
case Routes.goods:
|
||||
return const GoodsListScreen();
|
||||
case Routes.company:
|
||||
return const CompanyListScreen();
|
||||
case Routes.license:
|
||||
|
||||
574
lib/screens/common/app_layout_redesign.dart
Normal file
574
lib/screens/common/app_layout_redesign.dart
Normal file
@@ -0,0 +1,574 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/overview/overview_screen_redesign.dart';
|
||||
import 'package:superport/screens/equipment/equipment_list_redesign.dart';
|
||||
import 'package:superport/screens/company/company_list_redesign.dart';
|
||||
import 'package:superport/screens/user/user_list_redesign.dart';
|
||||
import 'package:superport/screens/license/license_list_redesign.dart';
|
||||
import 'package:superport/screens/warehouse_location/warehouse_location_list_redesign.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
|
||||
/// Microsoft Dynamics 365 스타일의 메인 레이아웃
|
||||
/// 상단 헤더 + 좌측 사이드바 + 메인 콘텐츠 구조
|
||||
class AppLayoutRedesign extends StatefulWidget {
|
||||
final String initialRoute;
|
||||
|
||||
const AppLayoutRedesign({Key? key, this.initialRoute = Routes.home})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<AppLayoutRedesign> createState() => _AppLayoutRedesignState();
|
||||
}
|
||||
|
||||
class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
||||
with TickerProviderStateMixin {
|
||||
late String _currentRoute;
|
||||
bool _sidebarCollapsed = false;
|
||||
late AnimationController _sidebarAnimationController;
|
||||
late Animation<double> _sidebarAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentRoute = widget.initialRoute;
|
||||
_setupAnimations();
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_sidebarAnimationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
_sidebarAnimation = Tween<double>(begin: 280.0, end: 72.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _sidebarAnimationController,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sidebarAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 현재 경로에 따라 적절한 컨텐츠 섹션을 반환
|
||||
Widget _getContentForRoute(String route) {
|
||||
switch (route) {
|
||||
case Routes.home:
|
||||
return const OverviewScreenRedesign();
|
||||
case Routes.equipment:
|
||||
case Routes.equipmentInList:
|
||||
case Routes.equipmentOutList:
|
||||
case Routes.equipmentRentList:
|
||||
return EquipmentListRedesign(currentRoute: route);
|
||||
case Routes.company:
|
||||
return const CompanyListRedesign();
|
||||
case Routes.user:
|
||||
return const UserListRedesign();
|
||||
case Routes.license:
|
||||
return const LicenseListRedesign();
|
||||
case Routes.warehouseLocation:
|
||||
return const WarehouseLocationListRedesign();
|
||||
default:
|
||||
return const OverviewScreenRedesign();
|
||||
}
|
||||
}
|
||||
|
||||
/// 경로 변경 메서드
|
||||
void _navigateTo(String route) {
|
||||
setState(() {
|
||||
_currentRoute = route;
|
||||
});
|
||||
}
|
||||
|
||||
/// 사이드바 토글
|
||||
void _toggleSidebar() {
|
||||
setState(() {
|
||||
_sidebarCollapsed = !_sidebarCollapsed;
|
||||
});
|
||||
|
||||
if (_sidebarCollapsed) {
|
||||
_sidebarAnimationController.forward();
|
||||
} else {
|
||||
_sidebarAnimationController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
/// 현재 페이지 제목 가져오기
|
||||
String _getPageTitle() {
|
||||
switch (_currentRoute) {
|
||||
case Routes.home:
|
||||
return '대시보드';
|
||||
case Routes.equipment:
|
||||
case Routes.equipmentInList:
|
||||
case Routes.equipmentOutList:
|
||||
case Routes.equipmentRentList:
|
||||
return '장비 관리';
|
||||
case Routes.company:
|
||||
return '회사 관리';
|
||||
case Routes.license:
|
||||
return '유지보수 관리';
|
||||
case Routes.warehouseLocation:
|
||||
return '입고지 관리';
|
||||
default:
|
||||
return '대시보드';
|
||||
}
|
||||
}
|
||||
|
||||
/// 브레드크럼 경로 가져오기
|
||||
List<String> _getBreadcrumbs() {
|
||||
switch (_currentRoute) {
|
||||
case Routes.home:
|
||||
return ['홈', '대시보드'];
|
||||
case Routes.equipment:
|
||||
return ['홈', '장비 관리', '전체'];
|
||||
case Routes.equipmentInList:
|
||||
return ['홈', '장비 관리', '입고'];
|
||||
case Routes.equipmentOutList:
|
||||
return ['홈', '장비 관리', '출고'];
|
||||
case Routes.equipmentRentList:
|
||||
return ['홈', '장비 관리', '대여'];
|
||||
case Routes.company:
|
||||
return ['홈', '회사 관리'];
|
||||
case Routes.license:
|
||||
return ['홈', '유지보수 관리'];
|
||||
case Routes.warehouseLocation:
|
||||
return ['홈', '입고지 관리'];
|
||||
default:
|
||||
return ['홈', '대시보드'];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ShadcnTheme.muted,
|
||||
body: Column(
|
||||
children: [
|
||||
// 상단 헤더
|
||||
_buildTopHeader(),
|
||||
|
||||
// 메인 콘텐츠 영역
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// 좌측 사이드바
|
||||
AnimatedBuilder(
|
||||
animation: _sidebarAnimation,
|
||||
builder: (context, child) {
|
||||
return SizedBox(
|
||||
width: _sidebarAnimation.value,
|
||||
child: _buildSidebar(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 메인 콘텐츠
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
boxShadow: ShadcnTheme.cardShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 페이지 헤더
|
||||
_buildPageHeader(),
|
||||
|
||||
// 메인 콘텐츠
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(ShadcnTheme.radiusLg),
|
||||
bottomRight: Radius.circular(
|
||||
ShadcnTheme.radiusLg,
|
||||
),
|
||||
),
|
||||
child: _getContentForRoute(_currentRoute),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 상단 헤더 빌드
|
||||
Widget _buildTopHeader() {
|
||||
return Container(
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
border: Border(bottom: BorderSide(color: ShadcnTheme.border)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing4),
|
||||
child: Row(
|
||||
children: [
|
||||
// 사이드바 토글 버튼
|
||||
IconButton(
|
||||
onPressed: _toggleSidebar,
|
||||
icon: Icon(
|
||||
_sidebarCollapsed ? Icons.menu : Icons.menu_open,
|
||||
color: ShadcnTheme.foreground,
|
||||
),
|
||||
tooltip: _sidebarCollapsed ? '사이드바 펼치기' : '사이드바 접기',
|
||||
),
|
||||
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
|
||||
// 앱 로고 및 제목
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing2),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [ShadcnTheme.gradient1, ShadcnTheme.gradient2],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.directions_boat,
|
||||
size: 24,
|
||||
color: ShadcnTheme.primaryForeground,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: ShadcnTheme.spacing3),
|
||||
|
||||
Text('supERPort', style: ShadcnTheme.headingH4),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// 상단 액션 버튼들
|
||||
_buildTopActions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 상단 액션 버튼들
|
||||
Widget _buildTopActions() {
|
||||
return Row(
|
||||
children: [
|
||||
// 알림 버튼
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
// 알림 기능
|
||||
},
|
||||
icon: Stack(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.notifications_outlined,
|
||||
color: ShadcnTheme.foreground,
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.destructive,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
tooltip: '알림',
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
|
||||
// 설정 버튼
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
// 설정 기능
|
||||
},
|
||||
icon: Icon(Icons.settings_outlined, color: ShadcnTheme.foreground),
|
||||
tooltip: '설정',
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
|
||||
// 프로필 아바타
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_showProfileMenu(context);
|
||||
},
|
||||
child: ShadcnAvatar(initials: 'A', size: 36),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 사이드바 빌드
|
||||
Widget _buildSidebar() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.background,
|
||||
border: Border(right: BorderSide(color: ShadcnTheme.border)),
|
||||
),
|
||||
child: SidebarMenuRedesign(
|
||||
currentRoute: _currentRoute,
|
||||
onRouteChanged: _navigateTo,
|
||||
collapsed: _sidebarCollapsed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 페이지 헤더 빌드
|
||||
Widget _buildPageHeader() {
|
||||
final breadcrumbs = _getBreadcrumbs();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: ShadcnTheme.border)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 브레드크럼
|
||||
Row(
|
||||
children: [
|
||||
for (int i = 0; i < breadcrumbs.length; i++) ...[
|
||||
if (i > 0) ...[
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 16,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
],
|
||||
Text(
|
||||
breadcrumbs[i],
|
||||
style:
|
||||
i == breadcrumbs.length - 1
|
||||
? ShadcnTheme.bodyMedium
|
||||
: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 프로필 메뉴 표시
|
||||
void _showProfileMenu(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: ShadcnTheme.background,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(ShadcnTheme.radiusXl),
|
||||
),
|
||||
),
|
||||
builder:
|
||||
(context) => Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 프로필 정보
|
||||
Row(
|
||||
children: [
|
||||
ShadcnAvatar(initials: 'A', size: 48),
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('관리자', style: ShadcnTheme.headingH4),
|
||||
Text(
|
||||
'admin@superport.com',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing6),
|
||||
const ShadcnSeparator(),
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// 로그아웃 버튼
|
||||
ShadcnButton(
|
||||
text: '로그아웃',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
},
|
||||
variant: ShadcnButtonVariant.destructive,
|
||||
fullWidth: true,
|
||||
icon: Icon(Icons.logout),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 재설계된 사이드바 메뉴 (접기/펼치기 지원)
|
||||
class SidebarMenuRedesign extends StatelessWidget {
|
||||
final String currentRoute;
|
||||
final Function(String) onRouteChanged;
|
||||
final bool collapsed;
|
||||
|
||||
const SidebarMenuRedesign({
|
||||
Key? key,
|
||||
required this.currentRoute,
|
||||
required this.onRouteChanged,
|
||||
required this.collapsed,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildMenuItem(
|
||||
icon: Icons.dashboard,
|
||||
title: '대시보드',
|
||||
route: Routes.home,
|
||||
isActive: currentRoute == Routes.home,
|
||||
),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing2),
|
||||
|
||||
_buildMenuItem(
|
||||
icon: Icons.inventory,
|
||||
title: '장비 관리',
|
||||
route: Routes.equipment,
|
||||
isActive: [
|
||||
Routes.equipment,
|
||||
Routes.equipmentInList,
|
||||
Routes.equipmentOutList,
|
||||
Routes.equipmentRentList,
|
||||
].contains(currentRoute),
|
||||
),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing2),
|
||||
|
||||
_buildMenuItem(
|
||||
icon: Icons.location_on,
|
||||
title: '입고지 관리',
|
||||
route: Routes.warehouseLocation,
|
||||
isActive: currentRoute == Routes.warehouseLocation,
|
||||
),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing2),
|
||||
|
||||
_buildMenuItem(
|
||||
icon: Icons.business,
|
||||
title: '회사 관리',
|
||||
route: Routes.company,
|
||||
isActive: currentRoute == Routes.company,
|
||||
),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing2),
|
||||
|
||||
_buildMenuItem(
|
||||
icon: Icons.vpn_key,
|
||||
title: '유지보수 관리',
|
||||
route: Routes.license,
|
||||
isActive: currentRoute == Routes.license,
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuItem({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String route,
|
||||
required bool isActive,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: () => onRouteChanged(route),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: collapsed ? ShadcnTheme.spacing2 : ShadcnTheme.spacing4,
|
||||
vertical: ShadcnTheme.spacing3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? ShadcnTheme.primary : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color:
|
||||
isActive
|
||||
? ShadcnTheme.primaryForeground
|
||||
: ShadcnTheme.foreground,
|
||||
),
|
||||
if (!collapsed) ...[
|
||||
const SizedBox(width: ShadcnTheme.spacing3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color:
|
||||
isActive
|
||||
? ShadcnTheme.primaryForeground
|
||||
: ShadcnTheme.foreground,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
509
lib/screens/common/components/shadcn_components.dart
Normal file
509
lib/screens/common/components/shadcn_components.dart
Normal file
@@ -0,0 +1,509 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
|
||||
/// shadcn/ui 스타일 기본 컴포넌트들
|
||||
|
||||
// 카드 컴포넌트
|
||||
class ShadcnCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ShadcnCard({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cardContent = Container(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: padding ?? const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
boxShadow: ShadcnTheme.cardShadow,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (onTap != null) {
|
||||
return GestureDetector(onTap: onTap, child: cardContent);
|
||||
}
|
||||
|
||||
return cardContent;
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼 컴포넌트
|
||||
class ShadcnButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback? onPressed;
|
||||
final ShadcnButtonVariant variant;
|
||||
final ShadcnButtonSize size;
|
||||
final Widget? icon;
|
||||
final bool fullWidth;
|
||||
final bool loading;
|
||||
final Color? backgroundColor;
|
||||
final Color? textColor;
|
||||
|
||||
const ShadcnButton({
|
||||
Key? key,
|
||||
required this.text,
|
||||
this.onPressed,
|
||||
this.variant = ShadcnButtonVariant.primary,
|
||||
this.size = ShadcnButtonSize.medium,
|
||||
this.icon,
|
||||
this.fullWidth = false,
|
||||
this.loading = false,
|
||||
this.backgroundColor,
|
||||
this.textColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ButtonStyle style = _getButtonStyle();
|
||||
final EdgeInsetsGeometry padding = _getPadding();
|
||||
|
||||
Widget buttonChild = Row(
|
||||
mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (loading)
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
textColor ?? _getDefaultTextColor(),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (icon != null)
|
||||
icon!,
|
||||
if ((loading || icon != null) && text.isNotEmpty)
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
if (text.isNotEmpty) Text(text, style: _getTextStyle()),
|
||||
],
|
||||
);
|
||||
|
||||
if (variant == ShadcnButtonVariant.primary) {
|
||||
return SizedBox(
|
||||
width: fullWidth ? double.infinity : null,
|
||||
child: ElevatedButton(
|
||||
onPressed: loading ? null : onPressed,
|
||||
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
||||
child: buttonChild,
|
||||
),
|
||||
);
|
||||
} else if (variant == ShadcnButtonVariant.secondary) {
|
||||
return SizedBox(
|
||||
width: fullWidth ? double.infinity : null,
|
||||
child: OutlinedButton(
|
||||
onPressed: loading ? null : onPressed,
|
||||
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
||||
child: buttonChild,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: fullWidth ? double.infinity : null,
|
||||
child: TextButton(
|
||||
onPressed: loading ? null : onPressed,
|
||||
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
||||
child: buttonChild,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ButtonStyle _getButtonStyle() {
|
||||
switch (variant) {
|
||||
case ShadcnButtonVariant.primary:
|
||||
return ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor ?? ShadcnTheme.primary,
|
||||
foregroundColor: textColor ?? ShadcnTheme.primaryForeground,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
);
|
||||
case ShadcnButtonVariant.secondary:
|
||||
return OutlinedButton.styleFrom(
|
||||
backgroundColor: backgroundColor ?? ShadcnTheme.secondary,
|
||||
foregroundColor: textColor ?? ShadcnTheme.secondaryForeground,
|
||||
side: const BorderSide(color: ShadcnTheme.border),
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
);
|
||||
case ShadcnButtonVariant.destructive:
|
||||
return ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor ?? ShadcnTheme.destructive,
|
||||
foregroundColor: textColor ?? ShadcnTheme.destructiveForeground,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
);
|
||||
case ShadcnButtonVariant.ghost:
|
||||
return TextButton.styleFrom(
|
||||
backgroundColor: backgroundColor ?? Colors.transparent,
|
||||
foregroundColor: textColor ?? ShadcnTheme.foreground,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry _getPadding() {
|
||||
switch (size) {
|
||||
case ShadcnButtonSize.small:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing3,
|
||||
vertical: ShadcnTheme.spacing1,
|
||||
);
|
||||
case ShadcnButtonSize.medium:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing4,
|
||||
vertical: ShadcnTheme.spacing2,
|
||||
);
|
||||
case ShadcnButtonSize.large:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing8,
|
||||
vertical: ShadcnTheme.spacing3,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextStyle _getTextStyle() {
|
||||
TextStyle baseStyle;
|
||||
switch (size) {
|
||||
case ShadcnButtonSize.small:
|
||||
baseStyle = ShadcnTheme.labelSmall;
|
||||
break;
|
||||
case ShadcnButtonSize.medium:
|
||||
baseStyle = ShadcnTheme.labelMedium;
|
||||
break;
|
||||
case ShadcnButtonSize.large:
|
||||
baseStyle = ShadcnTheme.labelLarge;
|
||||
break;
|
||||
}
|
||||
return textColor != null ? baseStyle.copyWith(color: textColor) : baseStyle;
|
||||
}
|
||||
|
||||
Color _getDefaultTextColor() {
|
||||
switch (variant) {
|
||||
case ShadcnButtonVariant.primary:
|
||||
return ShadcnTheme.primaryForeground;
|
||||
case ShadcnButtonVariant.secondary:
|
||||
return ShadcnTheme.secondaryForeground;
|
||||
case ShadcnButtonVariant.destructive:
|
||||
return ShadcnTheme.destructiveForeground;
|
||||
case ShadcnButtonVariant.ghost:
|
||||
return ShadcnTheme.foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼 variants
|
||||
enum ShadcnButtonVariant { primary, secondary, destructive, ghost }
|
||||
|
||||
// 버튼 사이즈
|
||||
enum ShadcnButtonSize { small, medium, large }
|
||||
|
||||
// 입력 필드 컴포넌트
|
||||
class ShadcnInput extends StatelessWidget {
|
||||
final String? label;
|
||||
final String? placeholder;
|
||||
final String? errorText;
|
||||
final TextEditingController? controller;
|
||||
final bool obscureText;
|
||||
final TextInputType? keyboardType;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final VoidCallback? onTap;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final bool readOnly;
|
||||
final bool enabled;
|
||||
final int? maxLines;
|
||||
|
||||
const ShadcnInput({
|
||||
Key? key,
|
||||
this.label,
|
||||
this.placeholder,
|
||||
this.errorText,
|
||||
this.controller,
|
||||
this.obscureText = false,
|
||||
this.keyboardType,
|
||||
this.onChanged,
|
||||
this.onTap,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.readOnly = false,
|
||||
this.enabled = true,
|
||||
this.maxLines = 1,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (label != null) ...[
|
||||
Text(label!, style: ShadcnTheme.labelMedium),
|
||||
const SizedBox(height: ShadcnTheme.spacing1),
|
||||
],
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
onChanged: onChanged,
|
||||
onTap: onTap,
|
||||
readOnly: readOnly,
|
||||
enabled: enabled,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
hintText: placeholder,
|
||||
prefixIcon: prefixIcon,
|
||||
suffixIcon: suffixIcon,
|
||||
errorText: errorText,
|
||||
filled: true,
|
||||
fillColor: ShadcnTheme.background,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing3,
|
||||
vertical: ShadcnTheme.spacing2,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(color: ShadcnTheme.input),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(color: ShadcnTheme.input),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(color: ShadcnTheme.ring, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(color: ShadcnTheme.destructive),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(
|
||||
color: ShadcnTheme.destructive,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
hintStyle: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 배지 컴포넌트
|
||||
class ShadcnBadge extends StatelessWidget {
|
||||
final String text;
|
||||
final ShadcnBadgeVariant variant;
|
||||
final ShadcnBadgeSize size;
|
||||
|
||||
const ShadcnBadge({
|
||||
Key? key,
|
||||
required this.text,
|
||||
this.variant = ShadcnBadgeVariant.primary,
|
||||
this.size = ShadcnBadgeSize.medium,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: _getPadding(),
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusXl),
|
||||
border: Border.all(color: _getBorderColor()),
|
||||
),
|
||||
child: Text(text, style: _getTextStyle()),
|
||||
);
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry _getPadding() {
|
||||
switch (size) {
|
||||
case ShadcnBadgeSize.small:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing1,
|
||||
vertical: ShadcnTheme.spacing1 / 2,
|
||||
);
|
||||
case ShadcnBadgeSize.medium:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing2,
|
||||
vertical: ShadcnTheme.spacing1,
|
||||
);
|
||||
case ShadcnBadgeSize.large:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing3,
|
||||
vertical: ShadcnTheme.spacing1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Color _getBackgroundColor() {
|
||||
switch (variant) {
|
||||
case ShadcnBadgeVariant.primary:
|
||||
return ShadcnTheme.primary;
|
||||
case ShadcnBadgeVariant.secondary:
|
||||
return ShadcnTheme.secondary;
|
||||
case ShadcnBadgeVariant.destructive:
|
||||
return ShadcnTheme.destructive;
|
||||
case ShadcnBadgeVariant.success:
|
||||
return ShadcnTheme.success;
|
||||
case ShadcnBadgeVariant.warning:
|
||||
return ShadcnTheme.warning;
|
||||
case ShadcnBadgeVariant.outline:
|
||||
return Colors.transparent;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getBorderColor() {
|
||||
switch (variant) {
|
||||
case ShadcnBadgeVariant.outline:
|
||||
return ShadcnTheme.border;
|
||||
default:
|
||||
return Colors.transparent;
|
||||
}
|
||||
}
|
||||
|
||||
TextStyle _getTextStyle() {
|
||||
final Color textColor =
|
||||
variant == ShadcnBadgeVariant.outline
|
||||
? ShadcnTheme.foreground
|
||||
: variant == ShadcnBadgeVariant.secondary
|
||||
? ShadcnTheme.secondaryForeground
|
||||
: ShadcnTheme.primaryForeground;
|
||||
|
||||
switch (size) {
|
||||
case ShadcnBadgeSize.small:
|
||||
return ShadcnTheme.labelSmall.copyWith(color: textColor);
|
||||
case ShadcnBadgeSize.medium:
|
||||
return ShadcnTheme.labelMedium.copyWith(color: textColor);
|
||||
case ShadcnBadgeSize.large:
|
||||
return ShadcnTheme.labelLarge.copyWith(color: textColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 배지 variants
|
||||
enum ShadcnBadgeVariant {
|
||||
primary,
|
||||
secondary,
|
||||
destructive,
|
||||
success,
|
||||
warning,
|
||||
outline,
|
||||
}
|
||||
|
||||
// 배지 사이즈
|
||||
enum ShadcnBadgeSize { small, medium, large }
|
||||
|
||||
// 구분선 컴포넌트
|
||||
class ShadcnSeparator extends StatelessWidget {
|
||||
final Axis direction;
|
||||
final double thickness;
|
||||
final Color? color;
|
||||
|
||||
const ShadcnSeparator({
|
||||
Key? key,
|
||||
this.direction = Axis.horizontal,
|
||||
this.thickness = 1.0,
|
||||
this.color,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: direction == Axis.horizontal ? double.infinity : thickness,
|
||||
height: direction == Axis.vertical ? double.infinity : thickness,
|
||||
color: color ?? ShadcnTheme.border,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 아바타 컴포넌트
|
||||
class ShadcnAvatar extends StatelessWidget {
|
||||
final String? imageUrl;
|
||||
final String? initials;
|
||||
final double size;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const ShadcnAvatar({
|
||||
Key? key,
|
||||
this.imageUrl,
|
||||
this.initials,
|
||||
this.size = 40,
|
||||
this.backgroundColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? ShadcnTheme.muted,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
),
|
||||
child: ClipOval(
|
||||
child:
|
||||
imageUrl != null
|
||||
? Image.network(
|
||||
imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => _buildFallback(),
|
||||
)
|
||||
: _buildFallback(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFallback() {
|
||||
return Container(
|
||||
color: backgroundColor ?? ShadcnTheme.muted,
|
||||
child: Center(
|
||||
child: Text(
|
||||
initials ?? '?',
|
||||
style: ShadcnTheme.labelMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
fontSize: size * 0.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
291
lib/screens/common/theme_shadcn.dart
Normal file
291
lib/screens/common/theme_shadcn.dart
Normal file
@@ -0,0 +1,291 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
/// shadcn/ui 스타일 테마 시스템
|
||||
class ShadcnTheme {
|
||||
// shadcn/ui 색상 시스템
|
||||
static const Color background = Color(0xFFFFFFFF);
|
||||
static const Color foreground = Color(0xFF020817);
|
||||
static const Color card = Color(0xFFFFFFFF);
|
||||
static const Color cardForeground = Color(0xFF020817);
|
||||
static const Color popover = Color(0xFFFFFFFF);
|
||||
static const Color popoverForeground = Color(0xFF020817);
|
||||
static const Color primary = Color(0xFF0F172A);
|
||||
static const Color primaryForeground = Color(0xFFF8FAFC);
|
||||
static const Color secondary = Color(0xFFF1F5F9);
|
||||
static const Color secondaryForeground = Color(0xFF0F172A);
|
||||
static const Color muted = Color(0xFFF1F5F9);
|
||||
static const Color mutedForeground = Color(0xFF64748B);
|
||||
static const Color accent = Color(0xFFF1F5F9);
|
||||
static const Color accentForeground = Color(0xFF0F172A);
|
||||
static const Color destructive = Color(0xFFEF4444);
|
||||
static const Color destructiveForeground = Color(0xFFF8FAFC);
|
||||
static const Color border = Color(0xFFE2E8F0);
|
||||
static const Color input = Color(0xFFE2E8F0);
|
||||
static const Color ring = Color(0xFF020817);
|
||||
static const Color radius = Color(0xFF000000); // 사용하지 않음
|
||||
|
||||
// 그라데이션 색상
|
||||
static const Color gradient1 = Color(0xFF6366F1);
|
||||
static const Color gradient2 = Color(0xFF8B5CF6);
|
||||
static const Color gradient3 = Color(0xFFEC4899);
|
||||
|
||||
// 상태 색상
|
||||
static const Color success = Color(0xFF10B981);
|
||||
static const Color warning = Color(0xFFF59E0B);
|
||||
static const Color error = Color(0xFFEF4444);
|
||||
static const Color info = Color(0xFF3B82F6);
|
||||
|
||||
// 그림자 설정
|
||||
static List<BoxShadow> get cardShadow => [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
];
|
||||
|
||||
static List<BoxShadow> get buttonShadow => [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
];
|
||||
|
||||
// 간격 시스템
|
||||
static const double spacing1 = 4.0;
|
||||
static const double spacing2 = 8.0;
|
||||
static const double spacing3 = 12.0;
|
||||
static const double spacing4 = 16.0;
|
||||
static const double spacing5 = 20.0;
|
||||
static const double spacing6 = 24.0;
|
||||
static const double spacing8 = 32.0;
|
||||
static const double spacing10 = 40.0;
|
||||
static const double spacing12 = 48.0;
|
||||
static const double spacing16 = 64.0;
|
||||
static const double spacing20 = 80.0;
|
||||
|
||||
// 라운드 설정
|
||||
static const double radiusNone = 0.0;
|
||||
static const double radiusSm = 2.0;
|
||||
static const double radiusMd = 6.0;
|
||||
static const double radiusLg = 8.0;
|
||||
static const double radiusXl = 12.0;
|
||||
static const double radius2xl = 16.0;
|
||||
static const double radius3xl = 24.0;
|
||||
static const double radiusFull = 9999.0;
|
||||
|
||||
// 타이포그래피 시스템
|
||||
static TextStyle get headingH1 => GoogleFonts.inter(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: foreground,
|
||||
letterSpacing: -0.02,
|
||||
);
|
||||
|
||||
static TextStyle get headingH2 => GoogleFonts.inter(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: foreground,
|
||||
letterSpacing: -0.02,
|
||||
);
|
||||
|
||||
static TextStyle get headingH3 => GoogleFonts.inter(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: foreground,
|
||||
letterSpacing: -0.01,
|
||||
);
|
||||
|
||||
static TextStyle get headingH4 => GoogleFonts.inter(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: foreground,
|
||||
letterSpacing: -0.01,
|
||||
);
|
||||
|
||||
static TextStyle get bodyLarge => GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: foreground,
|
||||
letterSpacing: 0,
|
||||
);
|
||||
|
||||
static TextStyle get bodyMedium => GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: foreground,
|
||||
letterSpacing: 0,
|
||||
);
|
||||
|
||||
static TextStyle get bodySmall => GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: mutedForeground,
|
||||
letterSpacing: 0,
|
||||
);
|
||||
|
||||
static TextStyle get bodyMuted => GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: mutedForeground,
|
||||
letterSpacing: 0,
|
||||
);
|
||||
|
||||
static TextStyle get labelLarge => GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: foreground,
|
||||
letterSpacing: 0,
|
||||
);
|
||||
|
||||
static TextStyle get labelMedium => GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: foreground,
|
||||
letterSpacing: 0,
|
||||
);
|
||||
|
||||
static TextStyle get labelSmall => GoogleFonts.inter(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: mutedForeground,
|
||||
letterSpacing: 0,
|
||||
);
|
||||
|
||||
// Flutter 테마 데이터
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: primary,
|
||||
secondary: secondary,
|
||||
surface: background,
|
||||
surfaceContainerHighest: card,
|
||||
onSurface: foreground,
|
||||
onPrimary: primaryForeground,
|
||||
onSecondary: secondaryForeground,
|
||||
error: destructive,
|
||||
onError: destructiveForeground,
|
||||
outline: border,
|
||||
outlineVariant: input,
|
||||
),
|
||||
scaffoldBackgroundColor: background,
|
||||
textTheme: TextTheme(
|
||||
headlineLarge: headingH1,
|
||||
headlineMedium: headingH2,
|
||||
headlineSmall: headingH3,
|
||||
titleLarge: headingH4,
|
||||
bodyLarge: bodyLarge,
|
||||
bodyMedium: bodyMedium,
|
||||
bodySmall: bodySmall,
|
||||
labelLarge: labelLarge,
|
||||
labelMedium: labelMedium,
|
||||
labelSmall: labelSmall,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: background,
|
||||
foregroundColor: foreground,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
titleTextStyle: headingH4,
|
||||
iconTheme: const IconThemeData(color: foreground),
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
color: card,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusLg),
|
||||
side: const BorderSide(color: border, width: 1),
|
||||
),
|
||||
shadowColor: Colors.black.withOpacity(0.05),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primary,
|
||||
foregroundColor: primaryForeground,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing4,
|
||||
vertical: spacing2,
|
||||
),
|
||||
textStyle: labelMedium,
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: foreground,
|
||||
side: const BorderSide(color: border),
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing4,
|
||||
vertical: spacing2,
|
||||
),
|
||||
textStyle: labelMedium,
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: foreground,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing4,
|
||||
vertical: spacing2,
|
||||
),
|
||||
textStyle: labelMedium,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: background,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: spacing3,
|
||||
vertical: spacing2,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: input),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: input),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: ring, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: destructive),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(radiusMd),
|
||||
borderSide: const BorderSide(color: destructive, width: 2),
|
||||
),
|
||||
hintStyle: bodyMedium.copyWith(color: mutedForeground),
|
||||
labelStyle: labelMedium,
|
||||
),
|
||||
dividerTheme: const DividerThemeData(color: border, thickness: 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user