feat(ui): 레트로 테마 상수 및 공통 위젯 추가
- RetroTheme: 패딩, 폰트, 애니메이션 상수 정의 - PanelHeader: 재사용 가능한 패널 헤더 위젯 - RarityColorMapper: 레어리티별 색상 매핑
This commit is contained in:
19
lib/src/core/animation/canvas/rarity_color_mapper.dart
Normal file
19
lib/src/core/animation/canvas/rarity_color_mapper.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||||
|
|
||||||
|
/// 아이템 희귀도와 애니메이션 색상 간의 매핑
|
||||||
|
///
|
||||||
|
/// Clean Architecture 준수를 위해 model(ItemRarity) → animation(AsciiCellColor)
|
||||||
|
/// 의존성을 animation 레이어에서 처리
|
||||||
|
extension ItemRarityColorMapper on ItemRarity {
|
||||||
|
/// 공격 이펙트 셀 색상 (Phase 9: 무기 등급별 이펙트)
|
||||||
|
///
|
||||||
|
/// common은 기본 positive(시안), 나머지는 등급별 고유 색상
|
||||||
|
AsciiCellColor get effectCellColor => switch (this) {
|
||||||
|
ItemRarity.common => AsciiCellColor.positive,
|
||||||
|
ItemRarity.uncommon => AsciiCellColor.rarityUncommon,
|
||||||
|
ItemRarity.rare => AsciiCellColor.rarityRare,
|
||||||
|
ItemRarity.epic => AsciiCellColor.rarityEpic,
|
||||||
|
ItemRarity.legendary => AsciiCellColor.rarityLegendary,
|
||||||
|
};
|
||||||
|
}
|
||||||
181
lib/src/shared/retro_theme_constants.dart
Normal file
181
lib/src/shared/retro_theme_constants.dart
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
|
/// 레트로 테마 상수 모음
|
||||||
|
///
|
||||||
|
/// 패딩, 폰트 크기, 애니메이션 시간 등 UI 전반에 사용되는 상수 정의.
|
||||||
|
/// 색상은 [RetroColors] 참조.
|
||||||
|
abstract class RetroTheme {
|
||||||
|
RetroTheme._();
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 패딩 (Padding/Spacing)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 4px - 아이콘 내부, 작은 요소 간격
|
||||||
|
static const double paddingXs = 4.0;
|
||||||
|
|
||||||
|
/// 8px - 텍스트 줄 간격, 버튼 내부 패딩
|
||||||
|
static const double paddingSm = 8.0;
|
||||||
|
|
||||||
|
/// 10px - 패널 내부 패딩 (소)
|
||||||
|
static const double paddingMd = 10.0;
|
||||||
|
|
||||||
|
/// 12px - 섹션 간격, 패널 내부 패딩 (중)
|
||||||
|
static const double paddingLg = 12.0;
|
||||||
|
|
||||||
|
/// 16px - 화면 여백, 패널 내부 패딩 (대)
|
||||||
|
static const double paddingXl = 16.0;
|
||||||
|
|
||||||
|
/// 24px - 화면 간 여백
|
||||||
|
static const double paddingXxl = 24.0;
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 폰트 크기 (Font Sizes)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 8px - 라벨, 작은 힌트
|
||||||
|
static const double fontSizeXs = 8.0;
|
||||||
|
|
||||||
|
/// 10px - 보조 텍스트, 상태 표시
|
||||||
|
static const double fontSizeSm = 10.0;
|
||||||
|
|
||||||
|
/// 11px - 툴팁, 버튼 라벨
|
||||||
|
static const double fontSizeMd = 11.0;
|
||||||
|
|
||||||
|
/// 12px - 본문 텍스트
|
||||||
|
static const double fontSizeLg = 12.0;
|
||||||
|
|
||||||
|
/// 14px - 부제목
|
||||||
|
static const double fontSizeXl = 14.0;
|
||||||
|
|
||||||
|
/// 18px - 제목
|
||||||
|
static const double fontSizeTitle = 18.0;
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 애니메이션 시간 (Animation Durations)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 100ms - 버튼 눌림 효과
|
||||||
|
static const Duration animFast = Duration(milliseconds: 100);
|
||||||
|
|
||||||
|
/// 150ms - 호버 효과
|
||||||
|
static const Duration animQuick = Duration(milliseconds: 150);
|
||||||
|
|
||||||
|
/// 200ms - 리스트 아이템 전환
|
||||||
|
static const Duration animNormal = Duration(milliseconds: 200);
|
||||||
|
|
||||||
|
/// 300ms - 패널 전환
|
||||||
|
static const Duration animMedium = Duration(milliseconds: 300);
|
||||||
|
|
||||||
|
/// 400ms - 페이드 인/아웃
|
||||||
|
static const Duration animSlow = Duration(milliseconds: 400);
|
||||||
|
|
||||||
|
/// 500ms - 결과 패널 표시
|
||||||
|
static const Duration animResult = Duration(milliseconds: 500);
|
||||||
|
|
||||||
|
/// 1500ms - ASCII 디스인테그레이트 효과
|
||||||
|
static const Duration animDisintegrate = Duration(milliseconds: 1500);
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 테두리 (Border)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 기본 테두리 두께
|
||||||
|
static const double borderWidth = 1.0;
|
||||||
|
|
||||||
|
/// 굵은 테두리 두께
|
||||||
|
static const double borderWidthBold = 2.0;
|
||||||
|
|
||||||
|
/// 기본 테두리 반경 (픽셀 스타일로 0 사용)
|
||||||
|
static const double borderRadius = 0.0;
|
||||||
|
|
||||||
|
/// 라운드 테두리 반경 (현대적 스타일)
|
||||||
|
static const double borderRadiusRound = 4.0;
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 패널 스타일 (Panel Styles)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 패널 기본 EdgeInsets
|
||||||
|
static const EdgeInsets panelPadding = EdgeInsets.all(paddingMd);
|
||||||
|
|
||||||
|
/// 패널 확장 EdgeInsets
|
||||||
|
static const EdgeInsets panelPaddingLarge = EdgeInsets.all(paddingLg);
|
||||||
|
|
||||||
|
/// 화면 여백 EdgeInsets
|
||||||
|
static const EdgeInsets screenPadding = EdgeInsets.all(paddingXl);
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 텍스트 스타일 팩토리 (Text Style Factories)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 패널 제목 스타일
|
||||||
|
static TextStyle panelTitle(BuildContext context) => TextStyle(
|
||||||
|
fontSize: fontSizeLg,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: RetroColors.goldOf(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 섹션 제목 스타일
|
||||||
|
static TextStyle sectionTitle(BuildContext context) => TextStyle(
|
||||||
|
fontSize: fontSizeMd,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: RetroColors.textPrimaryOf(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 본문 텍스트 스타일
|
||||||
|
static TextStyle bodyText(BuildContext context) => TextStyle(
|
||||||
|
fontSize: fontSizeLg,
|
||||||
|
color: RetroColors.textPrimaryOf(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 보조 텍스트 스타일
|
||||||
|
static TextStyle bodyTextSecondary(BuildContext context) => TextStyle(
|
||||||
|
fontSize: fontSizeSm,
|
||||||
|
color: RetroColors.textSecondaryOf(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 뮤트 텍스트 스타일
|
||||||
|
static TextStyle bodyTextMuted(BuildContext context) => TextStyle(
|
||||||
|
fontSize: fontSizeSm,
|
||||||
|
color: RetroColors.textMutedOf(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 라벨 텍스트 스타일
|
||||||
|
static TextStyle labelText(BuildContext context) => TextStyle(
|
||||||
|
fontSize: fontSizeXs,
|
||||||
|
color: RetroColors.textSecondaryOf(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 숫자/통계 스타일
|
||||||
|
static TextStyle statText(BuildContext context) => TextStyle(
|
||||||
|
fontSize: fontSizeSm,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: RetroColors.textPrimaryOf(context),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 박스 데코레이션 팩토리 (Box Decoration Factories)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 기본 패널 데코레이션
|
||||||
|
static BoxDecoration panelDecoration(BuildContext context) => BoxDecoration(
|
||||||
|
color: RetroColors.panelBgOf(context),
|
||||||
|
border: Border.all(color: RetroColors.borderOf(context)),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 강조 패널 데코레이션 (골드 테두리)
|
||||||
|
static BoxDecoration panelDecorationHighlight(BuildContext context) =>
|
||||||
|
BoxDecoration(
|
||||||
|
color: RetroColors.panelBgOf(context),
|
||||||
|
border: Border.all(color: RetroColors.goldOf(context)),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 표면 데코레이션 (밝은 배경)
|
||||||
|
static BoxDecoration surfaceDecoration(BuildContext context) => BoxDecoration(
|
||||||
|
color: RetroColors.surfaceOf(context),
|
||||||
|
border: Border.all(color: RetroColors.borderOf(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
171
lib/src/shared/widgets/panel_header.dart
Normal file
171
lib/src/shared/widgets/panel_header.dart
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/retro_theme_constants.dart';
|
||||||
|
|
||||||
|
/// 패널 헤더 변형
|
||||||
|
enum PanelHeaderVariant {
|
||||||
|
/// 기본 패널 헤더 (레트로 스타일, 골드 하단 테두리)
|
||||||
|
///
|
||||||
|
/// 사용처: 메인 패널 상단, 골드 강조 필요한 곳
|
||||||
|
primary,
|
||||||
|
|
||||||
|
/// 섹션 헤더 (테마 색상 사용)
|
||||||
|
///
|
||||||
|
/// 사용처: 리스트 섹션 구분, 탭 페이지 내부
|
||||||
|
section,
|
||||||
|
|
||||||
|
/// 장비 헤더 (3D 베벨 테두리)
|
||||||
|
///
|
||||||
|
/// 사용처: 장비 패널, 중요 강조 영역
|
||||||
|
equipment,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 레트로 스타일 패널 헤더 위젯
|
||||||
|
///
|
||||||
|
/// 여러 화면에서 반복되는 패널/섹션 헤더를 통합한 공통 위젯.
|
||||||
|
/// [variant]에 따라 다른 스타일이 적용됨.
|
||||||
|
///
|
||||||
|
/// 사용 예시:
|
||||||
|
/// ```dart
|
||||||
|
/// PanelHeader(title: 'INVENTORY')
|
||||||
|
/// PanelHeader(title: 'Stats', variant: PanelHeaderVariant.section)
|
||||||
|
/// ```
|
||||||
|
class PanelHeader extends StatelessWidget {
|
||||||
|
const PanelHeader({
|
||||||
|
required this.title,
|
||||||
|
this.variant = PanelHeaderVariant.primary,
|
||||||
|
this.icon,
|
||||||
|
this.uppercase = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 헤더 제목
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// 헤더 스타일 변형
|
||||||
|
final PanelHeaderVariant variant;
|
||||||
|
|
||||||
|
/// 선택적 아이콘 (equipment 변형에서 주로 사용)
|
||||||
|
final IconData? icon;
|
||||||
|
|
||||||
|
/// 대문자 변환 여부 (기본 true)
|
||||||
|
final bool uppercase;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final displayTitle = uppercase ? title.toUpperCase() : title;
|
||||||
|
|
||||||
|
return switch (variant) {
|
||||||
|
PanelHeaderVariant.primary => _buildPrimaryHeader(context, displayTitle),
|
||||||
|
PanelHeaderVariant.section => _buildSectionHeader(context, displayTitle),
|
||||||
|
PanelHeaderVariant.equipment =>
|
||||||
|
_buildEquipmentHeader(context, displayTitle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 기본 패널 헤더 (레트로 스타일)
|
||||||
|
Widget _buildPrimaryHeader(BuildContext context, String displayTitle) {
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
final panelBg = RetroColors.panelBgOf(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: panelBg,
|
||||||
|
border: Border(bottom: BorderSide(color: gold, width: 2)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
displayTitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: RetroTheme.fontSizeXs,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: gold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 섹션 헤더 (테마 색상)
|
||||||
|
Widget _buildSectionHeader(BuildContext context, String displayTitle) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
color: colorScheme.primaryContainer,
|
||||||
|
child: Text(
|
||||||
|
displayTitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
color: colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 헤더 (3D 베벨 테두리)
|
||||||
|
Widget _buildEquipmentHeader(BuildContext context, String displayTitle) {
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
final goldDark = RetroColors.goldDarkOf(context);
|
||||||
|
final panelBg = RetroColors.panelBgOf(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: panelBg,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: gold, width: 2),
|
||||||
|
left: BorderSide(color: gold, width: 2),
|
||||||
|
bottom: BorderSide(color: goldDark, width: 2),
|
||||||
|
right: BorderSide(color: goldDark, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (icon != null) ...[
|
||||||
|
Icon(icon, size: 16, color: gold),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
displayTitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
color: gold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 뮤트(회색) 섹션 헤더
|
||||||
|
///
|
||||||
|
/// game_play_screen.dart의 _buildSectionHeader와 동일한 스타일.
|
||||||
|
/// 메인 패널 내부의 작은 섹션 구분에 사용.
|
||||||
|
class MutedSectionHeader extends StatelessWidget {
|
||||||
|
const MutedSectionHeader({required this.title, super.key});
|
||||||
|
|
||||||
|
/// 헤더 제목
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textMuted = RetroColors.textMutedOf(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: Text(
|
||||||
|
title.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: RetroTheme.fontSizeXs,
|
||||||
|
color: textMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user