Files
fanuzu/app/lib/features/inside/inside_ui.dart
T

155 lines
4.5 KiB
Dart

import 'package:flutter/material.dart';
import '../../core/theme/tokens.dart';
import '../../core/ui/app_icons.dart';
import '../../core/ui/relative_time.dart';
import '../home/home_screen.dart' show formatPop;
import 'inside_data.dart';
/// INSIDE 화면군이 같이 쓰는 시안 조각 — 아이콘 · 글자 · 작성자 줄 · 현상금 칩 · 반응 줄.
///
/// 아이콘은 Figma Iconset(Lucide) 을 3배 PNG 로 둔 것이다. 공용 [AppIcons] 로 올릴 때까지
/// 여기 둔다(디자인 일치 2단계 「공유 변경 요청」).
/// 시안 INSIDE 의 회색 단계.
abstract final class InsideColors {
static const stat = Color(0xFF4F4F4F);
static const avatar = Color(0xFFD9D9D9);
static const rule = Color(0xFF1A1A1A);
static const hint = Color(0xFF6B6B6B);
static const chipLabel = Color(0xFFD5D7D9);
}
/// 42dot Sans · 자간 -2.5% · 행간 130%(시안 INSIDE 본문 기본값).
TextStyle insideText(
double size, {
FontWeight weight = FontWeight.w500,
Color color = AppColors.onCosmic,
double height = 1.3,
}) => TextStyle(
fontFamily: AppFonts.body,
fontSize: size,
fontWeight: weight,
height: height,
letterSpacing: -0.025 * size,
color: color,
);
/// 작성자 닉네임 — ABeeZee 12(한글은 42dot 대체).
const insideNameStyle = TextStyle(
fontFamily: AppFonts.display,
fontFamilyFallback: [AppFonts.body],
fontSize: 12,
height: 1.3,
letterSpacing: -0.3,
color: AppColors.onCosmic,
);
/// 첨부 이미지. `assets/` 로 시작하면 번들 표본이다.
ImageProvider insideImage(String url) =>
url.startsWith('assets/') ? AssetImage(url) : NetworkImage(url);
/// 아바타 26 · 이름 · (있으면) 시각 줄.
class InsideAuthor extends StatelessWidget {
const InsideAuthor({super.key, required this.name, this.meta});
final String name;
final String? meta;
@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 26,
height: 26,
decoration: const BoxDecoration(
color: InsideColors.avatar,
shape: BoxShape.circle,
),
),
const SizedBox(width: 9),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(name, style: insideNameStyle),
if (meta != null)
Text(meta!, style: insideText(10, color: AppColors.onCosmicMuted)),
],
),
],
);
}
/// 현상금 칩 — 시안 58×24 · 모서리 2 · 번개 + 「500 P」. 채택이 끝난 질문은 회색이다.
class InsideBountyChip extends StatelessWidget {
const InsideBountyChip({super.key, required this.question});
final InsideQuestion question;
@override
Widget build(BuildContext context) {
final c = question.isSolved ? InsideColors.stat : AppColors.cosmicBlue;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: c.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ImageIcon(AppIcons.bolt, size: 10, color: c),
const SizedBox(width: 4),
Text(
'${formatPop(question.bountyPop)} P',
style: insideText(12, color: c),
),
],
),
);
}
}
/// 좋아요 · 댓글 · 조회 줄. 0 이면 숫자를 빼고 아이콘만 둔다(시안 「아미보이」 카드).
class InsideStats extends StatelessWidget {
const InsideStats({
super.key,
required this.question,
this.size = 14,
this.textSize = 12,
this.color = InsideColors.stat,
});
final InsideQuestion question;
final double size;
final double textSize;
final Color color;
@override
Widget build(BuildContext context) {
Widget stat(ImageProvider icon, int n) => Row(
mainAxisSize: MainAxisSize.min,
children: [
ImageIcon(icon, size: size, color: color),
if (n > 0) ...[
const SizedBox(width: 4),
Text(
formatReaction(n),
style: insideText(textSize, color: color, height: 1),
),
],
],
);
return Row(
children: [
stat(AppIcons.heart, question.likeCount),
const SizedBox(width: 12),
stat(AppIcons.messageCircle, question.answerCount),
const SizedBox(width: 12),
stat(AppIcons.eye, question.viewCount),
],
);
}
}