generated from ttipo/project-base
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:
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user