feat(ui): 레트로 테마 상수 및 공통 위젯 추가

- RetroTheme: 패딩, 폰트, 애니메이션 상수 정의
- PanelHeader: 재사용 가능한 패널 헤더 위젯
- RarityColorMapper: 레어리티별 색상 매핑
This commit is contained in:
JiWoong Sul
2026-01-15 01:53:36 +09:00
parent 92e5fbbf1a
commit a4bbc6c7cb
3 changed files with 371 additions and 0 deletions

View 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,
};
}

View 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)),
);
}

View 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,
),
),
);
}
}