fix(flutter): 버튼·탭 라벨 글꼴을 시안 전역 규칙에 맞춤

시안 styles.css 뒷부분의 전역 규칙이 .rup-btn · 선택 버튼 · 하단 탭 라벨을
제목 서체(충북대체) 800 으로 덮어쓴다는 것을 놓쳤다.
- RupButton: 제목 서체 사용, thin(600)·danger 변형 추가 (인트로 CTA 는 본문 서체 유지)
- 탭 라벨: 제목 서체 800, 가운뎃점만 본문 서체로 그려 "일정·기록" 이 벌어져 보이지 않게 함
  (브라우저는 CJK 문장부호 간격을 자동으로 줄이지만 Flutter 는 글리프 폭을 그대로 쓴다)
- 이후 화면에 필요한 색 토큰(오류·시트 배경·빈 명함 등) 추가

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 19:35:23 +09:00
parent 2371dff023
commit e880f121b0
4 changed files with 77 additions and 13 deletions
@@ -37,6 +37,29 @@ abstract final class AppColors {
// 하단 탭바 반투명 배경 — rgba(255,253,250,.86)
static const Color tabBarBg = Color(0xDBFFFDFA);
// 시트·모달 뒤 어둡게 — rgba(40,28,18,.42)
static const Color scrim = Color(0x6B281C12);
// 입력 오류 · 파괴적 동작
static const Color error = Color(0xFFE0534F);
static const Color danger = Color(0xFFD9503F);
// 옅은 베이지 면 — 구분 띠·사진 추가 칸·텍스트 다이어리 (#F6F4EF)
static const Color sand = Color(0xFFF6F4EF);
static const Color sandInk = Color(0xFF8C8170); // 사진 추가 칸 아이콘·문구
// 빈 명함 슬롯
static const Color emptyCardBg = Color(0xFFFEFCFA);
static const Color emptyCardBorder = Color(0xFFCDC7C0); // mix(muted, #fff 42%)
static const Color emptyCardIcon = Color(0xFFB8AFA6); // mix(muted, #fff 18%)
static const Color emptyCardButtonBorder = Color(0xFFFFC594); // mix(primary, #fff 58%)
// 그림자 — 시안 --shadow-sm / --shadow-md 의 색
static const Color shadowWarm = Color(0x0D46321E); // rgba(70,50,30,.05)
static const Color shadowWarmSoft = Color(0x0A46321E); // rgba(70,50,30,.04)
static const Color shadowMd = Color(0x1450371E); // rgba(80,55,30,.08)
static const Color shadowMdSoft = Color(0x0D50371E); // rgba(80,55,30,.05)
// 진입 화면 전용
static const Color splashYellow = Color(0xFFFFDE84);
static const Color splashInk = Color(0xFF2B2622);
+10 -3
View File
@@ -59,11 +59,18 @@ abstract final class AppTypography {
bodyMedium: _ui(13.5, FontWeight.w400, height: 1.6),
// 12 — 메타 정보
bodySmall: _ui(12, FontWeight.w400),
// 15 — 버튼
// 15 — 본문 서체 버튼 (로그인·인트로 CTA 처럼 .rup-btn 이 아닌 버튼)
labelLarge: _ui(15, FontWeight.w700),
// 13 — 작은 버튼·링크
labelMedium: _ui(13, FontWeight.w600),
// 12 — 탭 라벨
labelSmall: _ui(12, FontWeight.w700),
// 12 — 하단 탭 라벨 (.rup-tab 은 제목 서체 800)
labelSmall: _display(12, FontWeight.w800),
);
/// `.rup-btn` · 선택 버튼 — 시안은 모든 버튼을 제목 서체 800 으로 통일한다
static TextStyle button(double size, {bool thin = false, Color? color}) {
// 제목 서체의 기본 줄 높이가 커서 버튼이 시안보다 높아지므로 줄 높이를 고정한다
return _display(size, thin ? FontWeight.w600 : FontWeight.w800, height: 1.2)
.copyWith(color: color);
}
}
@@ -3,6 +3,7 @@ import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'package:tma_app/core/constants/app_colors.dart';
import 'package:tma_app/core/constants/app_sizes.dart';
import 'package:tma_app/core/theme/app_typography.dart';
import 'package:tma_app/features/shell/domain/app_tab.dart';
import 'package:tma_app/shared/widgets/icons/rup_icon.dart';
@@ -63,6 +64,21 @@ class RupTabBar extends StatelessWidget {
}
}
/// 제목 서체의 가운뎃점(·)은 폭이 넓고 왼쪽에 붙어 있다.
/// 브라우저는 문장부호 간격을 자동으로 줄이지만 Flutter 는 그대로 그리므로
/// 가운뎃점만 본문 서체로 바꿔 "일정·기록" 처럼 붙여 보이게 한다.
TextSpan _narrowDots(String label) {
final parts = label.split('·');
return TextSpan(
children: [
for (final (i, part) in parts.indexed) ...[
if (i > 0) const TextSpan(text: '·', style: TextStyle(fontFamily: AppFonts.ui)),
TextSpan(text: part),
],
],
);
}
class _TabItem extends StatelessWidget {
const _TabItem({required this.tab, required this.active, required this.onTap});
@@ -108,7 +124,7 @@ class _TabItem extends StatelessWidget {
color: color,
letterSpacing: 0,
),
child: Text(tab.label, maxLines: 1, softWrap: false),
child: Text.rich(_narrowDots(tab.label), maxLines: 1, softWrap: false),
),
],
),
+27 -9
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:tma_app/core/constants/app_colors.dart';
import 'package:tma_app/core/constants/app_sizes.dart';
import 'package:tma_app/core/theme/app_typography.dart';
import 'package:tma_app/shared/widgets/pressable.dart';
/// 시안 `.rup-btn` 변형
@@ -14,10 +15,19 @@ enum RupButtonVariant {
/// 연주황 바탕 (.rup-btn--soft)
soft,
/// 주황 화면 위의 흰 버튼 (인트로·온보딩 CTA)
/// 파괴적 동작 (삭제 확인)
danger,
/// 주황 화면 위의 흰 버튼 (인트로·온보딩 CTA — .rup-btn 이 아니라 본문 서체)
light,
}
const double _kFontSize = 15;
const double _kFontSizeLarge = 16;
const double _kPadV = 13;
const double _kPadVLarge = 16;
const double _kPadH = 18;
/// 시안의 pill 버튼. 기본은 가로를 꽉 채운다(.rup-btn--block).
class RupButton extends StatelessWidget {
const RupButton({
@@ -26,7 +36,9 @@ class RupButton extends StatelessWidget {
required this.onPressed,
this.variant = RupButtonVariant.primary,
this.large = false,
this.thin = false,
this.expand = true,
this.dimmed = false,
this.height,
this.leading,
});
@@ -38,9 +50,15 @@ class RupButton extends StatelessWidget {
/// .rup-btn--lg (글자 16, 세로 여백 16)
final bool large;
/// .rup-btn--thin (제목 서체 굵기 600)
final bool thin;
/// 가로 채움 여부
final bool expand;
/// 누를 수는 있지만 아직 조건이 안 된 상태를 흐리게 보여 준다 (등록 버튼 등)
final bool dimmed;
/// 높이를 시안 값으로 고정해야 하는 자리(인트로 CTA 46 등)에서만 지정
final double? height;
final Widget? leading;
@@ -59,13 +77,13 @@ class RupButton extends StatelessWidget {
AppColors.primary600,
AppColors.primary100,
),
RupButtonVariant.danger => (AppColors.danger, Colors.white, null),
RupButtonVariant.light => (AppColors.offWhite, AppColors.ink, null),
};
final textStyle = Theme.of(context).textTheme.labelLarge!.copyWith(
color: fg,
fontSize: large ? 16 : null,
);
final verticalPad = large ? AppSpacing.lg : 13.0;
final size = large ? _kFontSizeLarge : _kFontSize;
final textStyle = variant == RupButtonVariant.light
? Theme.of(context).textTheme.labelLarge!.copyWith(color: fg, fontSize: size)
: AppTypography.button(size, thin: thin, color: fg);
final content = Row(
mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min,
@@ -84,7 +102,7 @@ class RupButton extends StatelessWidget {
);
return Opacity(
opacity: onPressed == null ? 0.5 : 1,
opacity: onPressed == null || dimmed ? 0.5 : 1,
child: Pressable(
onTap: onPressed,
child: Container(
@@ -92,8 +110,8 @@ class RupButton extends StatelessWidget {
width: expand ? double.infinity : null,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(
horizontal: 18,
vertical: height == null ? verticalPad : 0,
horizontal: _kPadH,
vertical: height == null ? (large ? _kPadVLarge : _kPadV) : 0,
),
decoration: BoxDecoration(
color: bg,