diff --git a/lib/src/shared/widgets/retro_button.dart b/lib/src/shared/widgets/retro_button.dart index 0bc6fcd..8f646e8 100644 --- a/lib/src/shared/widgets/retro_button.dart +++ b/lib/src/shared/widgets/retro_button.dart @@ -31,6 +31,7 @@ class RetroButton extends StatefulWidget { class _RetroButtonState extends State { bool _isPressed = false; + bool _isHovered = false; bool get _isEnabled => widget.onPressed != null; @@ -41,6 +42,12 @@ class _RetroButtonState extends State { ? RetroColors.buttonPrimaryPressed : RetroColors.buttonSecondaryPressed; } + // 호버 시 밝아지는 효과 + if (_isHovered) { + return widget.isPrimary + ? RetroColors.buttonPrimary.withValues(alpha: 0.9) + : RetroColors.buttonSecondary.withValues(alpha: 0.9); + } return widget.isPrimary ? RetroColors.buttonPrimary : RetroColors.buttonSecondary; @@ -48,43 +55,64 @@ class _RetroButtonState extends State { Color get _borderTopLeft { if (_isPressed) return RetroColors.panelBorderOuter; + // 호버 시 골드 테두리 + if (_isHovered && _isEnabled) return RetroColors.gold; return RetroColors.panelBorderInner; } Color get _borderBottomRight { if (_isPressed) return RetroColors.panelBorderInner; + // 호버 시 골드 테두리 + if (_isHovered && _isEnabled) return RetroColors.goldDark; return RetroColors.panelBorderOuter; } @override Widget build(BuildContext context) { - return GestureDetector( - onTapDown: _isEnabled ? (_) => setState(() => _isPressed = true) : null, - onTapUp: _isEnabled ? (_) => setState(() => _isPressed = false) : null, - onTapCancel: _isEnabled ? () => setState(() => _isPressed = false) : null, - onTap: widget.onPressed, - child: AnimatedContainer( - duration: const Duration(milliseconds: 50), - padding: widget.padding, - decoration: BoxDecoration( - color: _backgroundColor, - border: Border( - top: BorderSide(color: _borderTopLeft, width: 2), - left: BorderSide(color: _borderTopLeft, width: 2), - bottom: BorderSide(color: _borderBottomRight, width: 2), - right: BorderSide(color: _borderBottomRight, width: 2), + return MouseRegion( + onEnter: _isEnabled ? (_) => setState(() => _isHovered = true) : null, + onExit: _isEnabled ? (_) => setState(() => _isHovered = false) : null, + cursor: _isEnabled ? SystemMouseCursors.click : SystemMouseCursors.basic, + child: GestureDetector( + onTapDown: _isEnabled ? (_) => setState(() => _isPressed = true) : null, + onTapUp: _isEnabled ? (_) => setState(() => _isPressed = false) : null, + onTapCancel: _isEnabled ? () => setState(() => _isPressed = false) : null, + onTap: widget.onPressed, + child: AnimatedContainer( + duration: const Duration(milliseconds: 100), + padding: widget.padding, + decoration: BoxDecoration( + color: _backgroundColor, + border: Border( + top: BorderSide(color: _borderTopLeft, width: 2), + left: BorderSide(color: _borderTopLeft, width: 2), + bottom: BorderSide(color: _borderBottomRight, width: 2), + right: BorderSide(color: _borderBottomRight, width: 2), + ), + // 호버 시 글로우 효과 + boxShadow: _isHovered && _isEnabled + ? [ + BoxShadow( + color: RetroColors.gold.withValues(alpha: 0.3), + blurRadius: 8, + spreadRadius: 1, + ), + ] + : null, ), - ), - transform: _isPressed - ? Matrix4.translationValues(1, 1, 0) - : Matrix4.identity(), - child: DefaultTextStyle( - style: TextStyle( - fontFamily: 'PressStart2P', - fontSize: 10, - color: _isEnabled ? RetroColors.textLight : RetroColors.textDisabled, + transform: _isPressed + ? Matrix4.translationValues(1, 1, 0) + : Matrix4.identity(), + child: DefaultTextStyle( + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 10, + color: _isEnabled + ? (_isHovered ? RetroColors.gold : RetroColors.textLight) + : RetroColors.textDisabled, + ), + child: widget.child, ), - child: widget.child, ), ), ); @@ -148,3 +176,118 @@ class RetroIconButton extends StatelessWidget { ); } } + +/// 레트로 호버 효과 리스트 아이템 +/// 웹/데스크톱에서 마우스 호버 시 하이라이트 효과 +class RetroHoverItem extends StatefulWidget { + const RetroHoverItem({ + super.key, + required this.child, + this.onTap, + this.isSelected = false, + this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + }); + + final Widget child; + final VoidCallback? onTap; + final bool isSelected; + final EdgeInsets padding; + + @override + State createState() => _RetroHoverItemState(); +} + +class _RetroHoverItemState extends State { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + final isHighlighted = _isHovered || widget.isSelected; + + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + cursor: widget.onTap != null + ? SystemMouseCursors.click + : SystemMouseCursors.basic, + child: GestureDetector( + onTap: widget.onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 100), + padding: widget.padding, + decoration: BoxDecoration( + color: isHighlighted + ? RetroColors.panelBgLight.withValues(alpha: 0.5) + : Colors.transparent, + border: isHighlighted + ? Border( + left: BorderSide( + color: widget.isSelected + ? RetroColors.gold + : RetroColors.gold.withValues(alpha: 0.5), + width: 2, + ), + ) + : null, + ), + child: widget.child, + ), + ), + ); + } +} + +/// 레트로 호버 패널 +/// 웹/데스크톱에서 패널 호버 시 미세 확대 및 글로우 효과 +class RetroHoverPanel extends StatefulWidget { + const RetroHoverPanel({ + super.key, + required this.child, + this.onTap, + this.enableScale = true, + }); + + final Widget child; + final VoidCallback? onTap; + final bool enableScale; + + @override + State createState() => _RetroHoverPanelState(); +} + +class _RetroHoverPanelState extends State { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + cursor: widget.onTap != null + ? SystemMouseCursors.click + : SystemMouseCursors.basic, + child: GestureDetector( + onTap: widget.onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + transform: widget.enableScale && _isHovered + ? Matrix4.diagonal3Values(1.01, 1.01, 1.0) + : Matrix4.identity(), + transformAlignment: Alignment.center, + decoration: BoxDecoration( + boxShadow: _isHovered + ? [ + BoxShadow( + color: RetroColors.gold.withValues(alpha: 0.2), + blurRadius: 12, + spreadRadius: 2, + ), + ] + : null, + ), + child: widget.child, + ), + ), + ); + } +} diff --git a/lib/src/shared/widgets/retro_dialog.dart b/lib/src/shared/widgets/retro_dialog.dart new file mode 100644 index 0000000..45c7b00 --- /dev/null +++ b/lib/src/shared/widgets/retro_dialog.dart @@ -0,0 +1,303 @@ +import 'package:flutter/material.dart'; + +import 'package:askiineverdie/src/shared/retro_colors.dart'; + +/// 레트로 스타일 다이얼로그 베이스 위젯 +/// +/// 8-bit RPG 스타일의 다이얼로그 프레임 +class RetroDialog extends StatelessWidget { + const RetroDialog({ + super.key, + required this.title, + required this.child, + this.titleIcon, + this.maxWidth = 500, + this.maxHeight = 600, + this.accentColor = RetroColors.gold, + }); + + final String title; + final Widget child; + final String? titleIcon; + final double maxWidth; + final double maxHeight; + final Color accentColor; + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight), + decoration: BoxDecoration( + color: RetroColors.panelBg, + border: Border( + top: BorderSide(color: accentColor, width: 3), + left: BorderSide(color: accentColor, width: 3), + bottom: const BorderSide(color: RetroColors.panelBorderOuter, width: 3), + right: const BorderSide(color: RetroColors.panelBorderOuter, width: 3), + ), + boxShadow: [ + BoxShadow( + color: accentColor.withValues(alpha: 0.3), + blurRadius: 20, + spreadRadius: 2, + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 헤더 바 + _buildHeader(context), + // 본문 + Flexible(child: child), + ], + ), + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: accentColor.withValues(alpha: 0.2), + border: Border( + bottom: BorderSide(color: accentColor, width: 2), + ), + ), + child: Row( + children: [ + if (titleIcon != null) ...[ + Text( + titleIcon!, + style: TextStyle(fontSize: 14, color: accentColor), + ), + const SizedBox(width: 8), + ], + Expanded( + child: Text( + title.toUpperCase(), + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 10, + color: accentColor, + letterSpacing: 1, + ), + ), + ), + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + border: Border.all(color: RetroColors.textDisabled, width: 1), + ), + child: const Text( + 'X', + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 8, + color: RetroColors.textDisabled, + ), + ), + ), + ), + ], + ), + ); + } +} + +/// 레트로 스타일 탭 바 +class RetroTabBar extends StatelessWidget { + const RetroTabBar({ + super.key, + required this.controller, + required this.tabs, + this.accentColor = RetroColors.gold, + }); + + final TabController controller; + final List tabs; + final Color accentColor; + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: RetroColors.panelBorderOuter, width: 2), + ), + ), + child: TabBar( + controller: controller, + isScrollable: tabs.length > 3, + indicator: BoxDecoration( + color: accentColor.withValues(alpha: 0.3), + border: Border( + bottom: BorderSide(color: accentColor, width: 2), + ), + ), + labelColor: accentColor, + unselectedLabelColor: RetroColors.textDisabled, + labelStyle: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 7, + ), + unselectedLabelStyle: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 7, + ), + dividerColor: Colors.transparent, + tabs: tabs.map((t) => Tab(text: t.toUpperCase())).toList(), + ), + ); + } +} + +/// 레트로 스타일 섹션 헤더 +class RetroSectionHeader extends StatelessWidget { + const RetroSectionHeader({ + super.key, + required this.title, + this.icon, + this.accentColor = RetroColors.gold, + }); + + final String title; + final String? icon; + final Color accentColor; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + if (icon != null) ...[ + Text( + icon!, + style: TextStyle(fontSize: 12, color: accentColor), + ), + const SizedBox(width: 6), + ], + Text( + title.toUpperCase(), + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 8, + color: accentColor, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Container( + height: 2, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + accentColor, + accentColor.withValues(alpha: 0.3), + Colors.transparent, + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +/// 레트로 스타일 정보 박스 +class RetroInfoBox extends StatelessWidget { + const RetroInfoBox({ + super.key, + required this.content, + this.backgroundColor, + }); + + final String content; + final Color? backgroundColor; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: backgroundColor ?? RetroColors.deepBrown, + border: Border.all(color: RetroColors.panelBorderOuter, width: 1), + ), + child: Text( + content, + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 7, + color: RetroColors.textLight, + height: 1.8, + ), + ), + ); + } +} + +/// 레트로 스타일 통계 행 +class RetroStatRow extends StatelessWidget { + const RetroStatRow({ + super.key, + required this.label, + required this.value, + this.highlight = false, + this.highlightColor = RetroColors.gold, + }); + + final String label; + final String value; + final bool highlight; + final Color highlightColor; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 3), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 6, + color: RetroColors.textDisabled, + ), + ), + Container( + padding: highlight + ? const EdgeInsets.symmetric(horizontal: 6, vertical: 2) + : null, + decoration: highlight + ? BoxDecoration( + color: highlightColor.withValues(alpha: 0.2), + border: Border.all(color: highlightColor, width: 1), + ) + : null, + child: Text( + value, + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 9, + color: highlight ? highlightColor : RetroColors.textLight, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/shared/widgets/retro_widgets.dart b/lib/src/shared/widgets/retro_widgets.dart index 4778b42..73755ba 100644 --- a/lib/src/shared/widgets/retro_widgets.dart +++ b/lib/src/shared/widgets/retro_widgets.dart @@ -4,5 +4,6 @@ library; export 'pixel_border_painter.dart'; export 'retro_button.dart'; +export 'retro_dialog.dart'; export 'retro_panel.dart'; export 'retro_progress_bar.dart';