feat(flutter): 시안 기반 테마·폰트·아이콘과 공통 위젯 추가

Claude Design 시안(rup)을 옮기기 위한 공통 기반.
- 색·크기·글꼴 토큰을 시안 styles.css 값 그대로 상수화하고 테마에 연결
- SUITE·충북대체·어비꿍꿍체 번들 (웹폰트만 배포되는 서체는 TTF 로 변환, 출처는 fonts/README.md)
- 시안 아이콘 56종·전용 글리프를 Dart 로 옮긴 RupIcon
- 눌림 축소(Pressable)·pill 버튼·시안 좌표 배치(DesignArtBox)·그림자 이미지 위젯
- CSS 키프레임처럼 구간마다 곡선을 적용하는 보간 유틸과 테스트
- go_router · flutter_svg · shared_preferences 의존성 추가

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 18:54:03 +09:00
parent 9b37235d1e
commit 8e2fbe79d8
52 changed files with 1198 additions and 25 deletions
@@ -0,0 +1,36 @@
/// 에셋 경로 상수 — 문자열 경로를 위젯에 직접 적지 않는다.
abstract final class AppAssets {
// 스플래시
static const String splashBlob = 'assets/images/splash/blob.png';
static const String splashObjects = 'assets/images/splash/bg_objects.png';
static const String splashLetterR = 'assets/images/splash/letter_r.png';
static const String splashLetterU = 'assets/images/splash/letter_u.png';
static const String splashLetterP = 'assets/images/splash/letter_p.png';
static const String splashDog = 'assets/images/splash/dog.png';
// 인트로
static const String introDog = 'assets/svg/intro/intro_dog.svg';
static const String introBall = 'assets/svg/intro/intro_ball.svg';
// 로그인
static const String loginBack = 'assets/svg/login/login_back.svg';
static const String loginPaw = 'assets/svg/login/login_paw.svg';
static const String loginWand = 'assets/svg/login/login_wand.svg';
static const String loginCat = 'assets/images/login/login_cat.png';
static const String markNaver = 'assets/svg/login/mark_naver.svg';
static const String markKakao = 'assets/svg/login/mark_kakao.svg';
static const String markApple = 'assets/images/login/mark_apple.png';
// 온보딩 — 시안 SVG 를 움직이는 부분(ch/sp)과 배경으로 나눠 래스터화했다
static const String onb1Base = 'assets/images/onboarding/onb1_base.png';
static const String onb1Character = 'assets/images/onboarding/onb1_ch.png';
static const String onb2Base = 'assets/images/onboarding/onb2_base.png';
static const String onb2Character = 'assets/images/onboarding/onb2_ch.png';
static const String onb3Base = 'assets/images/onboarding/onb3_base.png';
static const String onb3Hero = 'assets/images/onboarding/onb3_hero.png';
static const String onb3Sparkle1 = 'assets/images/onboarding/onb3_sp1.png';
static const String onb3Sparkle2 = 'assets/images/onboarding/onb3_sp2.png';
static const String onb3Sparkle3 = 'assets/images/onboarding/onb3_sp3.png';
static const String onb4Base = 'assets/images/onboarding/onb4_base.png';
static const String onb4Heart = 'assets/images/onboarding/onb4_ch.png';
}
@@ -0,0 +1,51 @@
import 'package:flutter/painting.dart';
/// 시안(.design/styles.css `:root`)의 색상 토큰.
/// 표에 없는 색을 새로 쓰기 전에 여기에 먼저 추가한다.
abstract final class AppColors {
// 브랜드 — primary 파생색은 시안의 color-mix 결과를 계산해 고정했다
static const Color primary = Color(0xFFFF7500);
static const Color primary600 = Color(0xFFD96200); // mix(primary, #2b0d00 18%)
static const Color primary100 = Color(0xFFFFD8B8); // mix(primary, #fff 72%)
static const Color primary50 = Color(0xFFFFE9D6); // mix(primary, #fffaf6 87%)
static const Color primaryBorder = Color(0xFFE06700); // mix(primary, #000 12%)
// 건강·긍정(민트), 추억보관함(보라)
static const Color accent = Color(0xFF3FB58C);
static const Color accent100 = Color(0xFFDBF1E8);
static const Color memory = Color(0xFFC084FC);
static const Color memory50 = Color(0xFFF4ECFE);
// 면 · 글자 · 선
static const Color bg = Color(0xFFFFFDFB);
static const Color surface = Color(0xFFFFFFFF);
static const Color surface2 = Color(0xFFFFF7F1);
static const Color ink = Color(0xFF1F1F1F);
static const Color ink2 = Color(0xFF6E665C);
static const Color muted = Color(0xFFA89E92);
static const Color line = Color(0xFFEFE6DB);
static const Color line2 = Color(0xFFF6EFE7);
// 일정 분류색 (고정)
static const Color catHospital = Color(0xFFFF6B6B);
static const Color catVaccine = Color(0xFF4DA6E0);
static const Color catGrooming = Color(0xFFB07CF0);
static const Color catWalk = Color(0xFF3FB58C);
static const Color catFood = Color(0xFFFFAA4D);
static const Color catEtc = Color(0xFF9AA0A6);
// 하단 탭바 반투명 배경 — rgba(255,253,250,.86)
static const Color tabBarBg = Color(0xDBFFFDFA);
// 진입 화면 전용
static const Color splashYellow = Color(0xFFFFDE84);
static const Color splashInk = Color(0xFF2B2622);
static const Color offWhite = Color(0xFFFDFDFD); // 인트로·온보딩 흰 버튼
static const Color onboardingHint = Color(0xFFC8C8C8);
// 소셜 로그인 브랜드색 — 각 사 가이드 색상
static const Color naver = Color(0xFF00D03F);
static const Color kakao = Color(0xFFFAE100);
static const Color googleBorder = Color(0xFFE8EEF2);
static const Color apple = Color(0xFF000000);
}
+36
View File
@@ -0,0 +1,36 @@
import 'package:flutter/widgets.dart';
/// 시안 기기 크기 — iPhone 16 Pro(402×874) 기준으로 그려졌다.
/// 일러스트 배치처럼 시안 좌표를 그대로 옮기는 곳에서만 쓰고,
/// 실제 크기는 [DesignArtBox] 등이 화면 비율로 환산한다.
const Size kDesignScreen = Size(402, 874);
/// 여백·간격 토큰 (시안의 반복 값)
abstract final class AppSpacing {
static const double xs = 4;
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double xl = 20; // 화면 좌우 기본 여백
static const double xxl = 24;
}
/// 모서리 반경 토큰
abstract final class AppRadius {
static const double card = 10; // --radius
static const double pill = 999;
}
/// 공통 컨트롤 크기
abstract final class AppControlSize {
static const double buttonHeight = 46; // 인트로·로그인 pill 버튼
static const double navBarHeight = 44; // 상단 내비게이션 바
static const double tabIcon = 24;
}
/// 애니메이션 시간
abstract final class AppDurations {
static const Duration fast = Duration(milliseconds: 150);
static const Duration press = Duration(milliseconds: 100);
static const Duration fade = Duration(milliseconds: 480);
}
@@ -0,0 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// 앱 전역 SharedPreferences — main() 에서 미리 로드해 override 한다.
/// 토큰 같은 민감정보는 여기 저장하지 않는다 (flutter_secure_storage 사용).
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError('main() 에서 sharedPreferencesProvider 를 override 해야 한다');
});
+31
View File
@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:tma_app/core/constants/app_colors.dart';
import 'package:tma_app/core/theme/app_typography.dart';
/// 앱 테마 — 시안은 밝은 단일 테마다(다크 모드 없음).
abstract final class AppTheme {
static ThemeData get light {
final colorScheme = ColorScheme.fromSeed(
seedColor: AppColors.primary,
primary: AppColors.primary,
onPrimary: Colors.white,
secondary: AppColors.accent,
surface: AppColors.surface,
onSurface: AppColors.ink,
outline: AppColors.line,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
scaffoldBackgroundColor: AppColors.bg,
fontFamily: AppFonts.ui,
textTheme: AppTypography.textTheme,
iconTheme: const IconThemeData(color: AppColors.ink2),
// 시안은 물결(ripple) 대신 눌림 축소 효과를 쓴다 — Pressable 참고
splashFactory: NoSplash.splashFactory,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
);
}
}
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:tma_app/core/constants/app_colors.dart';
/// 서체 패밀리 이름 — pubspec.yaml 의 fonts 항목과 일치해야 한다.
abstract final class AppFonts {
/// 본문(UI) — 시안의 --font-ui
static const String ui = 'SUITE';
/// 제목 — 시안의 --font-display (Bold 컷을 400 으로 등록, 굵게 요청 시 합성)
static const String display = 'ChungbukBold';
/// 제목 서체의 일반 굵기 (펫 명함 이름 등)
static const String displayRegular = 'Chungbuk';
/// 인트로 손글씨
static const String handwriting = 'UhBeeKeongKeong';
}
/// 시안 전역 자간 -0.01em
double _tracking(double fontSize) => fontSize * -0.01;
/// 앱 TextTheme — 크기는 시안 px(=논리 픽셀) 값을 그대로 쓴다.
/// 화면에서는 `Theme.of(context).textTheme.xxx` 로 꺼내 쓰고, 굵기·색만 copyWith 로 조정한다.
abstract final class AppTypography {
static TextStyle _ui(double size, FontWeight weight, {double? height}) =>
TextStyle(
fontFamily: AppFonts.ui,
fontSize: size,
fontWeight: weight,
height: height,
letterSpacing: _tracking(size),
color: AppColors.ink,
);
static TextStyle _display(double size, FontWeight weight, {double? height}) =>
TextStyle(
fontFamily: AppFonts.display,
fontSize: size,
fontWeight: weight,
height: height,
letterSpacing: 0,
color: AppColors.ink,
);
static final TextTheme textTheme = TextTheme(
// 26 — 로그인 인사말 등 큰 제목
displaySmall: _display(26, FontWeight.w400),
// 24 — 화면 헤더 제목(.rup-head__title)
headlineSmall: _display(24, FontWeight.w800),
// 20 — 달력 월 표시
titleLarge: _display(20, FontWeight.w700),
// 17 — 섹션 제목(.rup-section-title h2)
titleMedium: _display(17, FontWeight.w700),
// 15 — 내비 제목·목록 제목
titleSmall: _ui(15, FontWeight.w700),
// 14.5 — 본문 강조
bodyLarge: _ui(14.5, FontWeight.w400, height: 1.62),
// 13.5 — 본문 보조·안내
bodyMedium: _ui(13.5, FontWeight.w400, height: 1.6),
// 12 — 메타 정보
bodySmall: _ui(12, FontWeight.w400),
// 15 — 버튼
labelLarge: _ui(15, FontWeight.w700),
// 13 — 작은 버튼·링크
labelMedium: _ui(13, FontWeight.w600),
// 12 — 탭 라벨
labelSmall: _ui(12, FontWeight.w700),
);
}
+30
View File
@@ -0,0 +1,30 @@
import 'package:flutter/animation.dart';
/// 전체 시간축 [elapsed] 에서 [start] 부터 [duration] 동안의 진행률(0~1).
/// 시작 전은 0, 끝난 뒤는 1 — CSS `animation-fill-mode: both` 와 같다.
double segmentProgress(double elapsed, double start, double duration) {
if (duration <= 0) return elapsed >= start ? 1 : 0;
return ((elapsed - start) / duration).clamp(0.0, 1.0);
}
/// CSS `@keyframes` 보간. [stops] 는 0~1 오름차순 위치, [values] 는 각 위치의 값이다.
/// CSS 와 마찬가지로 [curve] 는 전체가 아니라 **키프레임 구간마다** 적용된다.
double keyframeValue(
double t,
List<double> stops,
List<double> values, {
Curve curve = Curves.linear,
}) {
assert(stops.length == values.length && stops.length >= 2);
if (t <= stops.first) return values.first;
for (var i = 0; i < stops.length - 1; i++) {
final from = stops[i];
final to = stops[i + 1];
if (t <= to) {
final local = to == from ? 1.0 : (t - from) / (to - from);
final eased = curve.transform(local);
return values[i] + (values[i + 1] - values[i]) * eased;
}
}
return values.last;
}
@@ -0,0 +1,75 @@
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'package:tma_app/core/constants/app_sizes.dart';
/// 시안 좌표로 배치된 일러스트 묶음을 실제 화면 크기에 맞춰 그린다.
///
/// [designSize] 는 묶음 전체의 시안 크기이고, [children] 은 묶음 왼쪽 위를 원점으로 한
/// 시안 좌표의 [Positioned] 들이다. 화면 너비 비율(화면폭 / 402)로 확대·축소하되,
/// 부모가 준 높이가 모자라면 비율을 유지한 채 더 줄인다.
/// 좌표 숫자는 "시안 단위"이며 실제 픽셀이 아니다.
class DesignArtBox extends StatelessWidget {
const DesignArtBox({
super.key,
required this.designSize,
required this.children,
});
final Size designSize;
final List<Widget> children;
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.sizeOf(context).width;
return LayoutBuilder(
builder: (context, constraints) {
final widthScale = screenWidth / kDesignScreen.width;
final heightScale = constraints.hasBoundedHeight
? constraints.maxHeight / designSize.height
: double.infinity;
final scale = math.min(widthScale, heightScale);
return SizedBox(
width: designSize.width * scale,
height: designSize.height * scale,
child: FittedBox(
child: SizedBox.fromSize(
size: designSize,
child: Stack(clipBehavior: Clip.none, children: children),
),
),
);
},
);
}
}
/// 시안 전체 화면(402×874)을 기준으로 그린 장면을 화면을 덮도록(cover) 맞춘다.
/// 스플래시·온보딩처럼 화면 전체가 일러스트인 곳에서 쓴다.
class DesignCover extends StatelessWidget {
const DesignCover({
super.key,
required this.designSize,
required this.child,
this.fit = BoxFit.cover,
this.alignment = Alignment.center,
});
final Size designSize;
final Widget child;
final BoxFit fit;
final Alignment alignment;
@override
Widget build(BuildContext context) {
return ClipRect(
child: SizedBox.expand(
child: FittedBox(
fit: fit,
alignment: alignment,
child: SizedBox.fromSize(size: designSize, child: child),
),
),
);
}
}
@@ -0,0 +1,44 @@
import 'dart:ui' show ImageFilter;
import 'package:flutter/widgets.dart';
/// CSS `filter: drop-shadow(x y blur color)` — 투명 영역을 뺀 그림 모양대로 그림자를 드리운다.
/// [blur] 는 CSS 의 blur 반경(px)이며 내부에서 표준편차(반경/2)로 바꾼다.
class DropShadowImage extends StatelessWidget {
const DropShadowImage({
super.key,
required this.asset,
required this.shadowColor,
required this.offset,
required this.blur,
});
final String asset;
final Color shadowColor;
final Offset offset;
final double blur;
@override
Widget build(BuildContext context) {
final sigma = blur / 2;
return Stack(
fit: StackFit.expand,
clipBehavior: Clip.none,
children: [
Transform.translate(
offset: offset,
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
child: Image.asset(
asset,
fit: BoxFit.fill,
color: shadowColor,
colorBlendMode: BlendMode.srcIn,
),
),
),
Image.asset(asset, fit: BoxFit.fill),
],
);
}
}
@@ -0,0 +1,81 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tma_app/shared/widgets/icons/rup_icon_paths.dart';
export 'package:tma_app/shared/widgets/icons/rup_icon_paths.dart';
const double _kDefaultIconSize = 22;
const double _kDefaultStroke = 1.8;
/// 시안의 `<Icon name=... />` — 24 뷰박스 선 아이콘.
/// 색을 주지 않으면 주변 [IconTheme] 색(없으면 글자색)을 따른다.
class RupIcon extends StatelessWidget {
const RupIcon(
this.name, {
super.key,
this.size = _kDefaultIconSize,
this.stroke = _kDefaultStroke,
this.color,
this.semanticLabel,
});
final RupIconName name;
final double size;
final double stroke;
final Color? color;
final String? semanticLabel;
@override
Widget build(BuildContext context) {
return SvgPicture.string(
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" '
'stroke-width="$stroke" stroke-linecap="round" stroke-linejoin="round">'
'${name.paths}</svg>',
width: size,
height: size,
theme: SvgTheme(currentColor: _resolveColor(context, color)),
semanticsLabel: semanticLabel,
excludeFromSemantics: semanticLabel == null,
);
}
}
/// 시안의 전용 글리프(PetCardGlyph 등)
class RupGlyphIcon extends StatelessWidget {
const RupGlyphIcon(
this.glyph, {
super.key,
this.size = _kDefaultIconSize,
this.color,
this.semanticLabel,
});
final RupGlyph glyph;
final double size;
final Color? color;
final String? semanticLabel;
@override
Widget build(BuildContext context) {
final vb = glyph.viewBox;
final paint = glyph.fill
? 'fill="currentColor" stroke="none"'
: 'fill="none"${glyph.stroke != null ? ' stroke="currentColor" stroke-width="${glyph.stroke}"' : ''}';
return SvgPicture.string(
'<svg viewBox="0 0 $vb $vb" $paint stroke-linecap="round" '
'stroke-linejoin="round">${glyph.paths}</svg>',
width: size,
height: size,
theme: SvgTheme(currentColor: _resolveColor(context, color)),
semanticsLabel: semanticLabel,
excludeFromSemantics: semanticLabel == null,
);
}
}
Color _resolveColor(BuildContext context, Color? color) {
return color ??
IconTheme.of(context).color ??
DefaultTextStyle.of(context).style.color ??
const Color(0xFF000000);
}
@@ -0,0 +1,208 @@
// 시안(.design/app-data.jsx)의 ICON_PATHS · *_GLYPH 를 옮긴 아이콘 경로.
// 새 아이콘은 시안과 같은 규격(24 뷰박스, stroke=currentColor, round cap/join)으로 추가한다.
/// 선 아이콘 — 24×24 뷰박스, 채우지 않고 currentColor 로 선만 그린다.
enum RupIconName {
home(
'<path d="M6.5711 21.9345H17.4157C18.5642 21.9345 19.5414 21.0962 19.7145 19.963C19.9922 18.1596 20.0574 16.3322 19.9065 14.5168H22.0048C22.1583 14.5167 22.309 14.4755 22.4411 14.3973C22.5732 14.3191 22.6819 14.2069 22.7558 14.0724C22.8298 13.9379 22.8662 13.7859 22.8613 13.6325C22.8565 13.4791 22.8105 13.3298 22.7282 13.2002L22.3648 12.6293C20.1821 9.19151 17.4044 6.17002 14.162 3.70647L13.0305 2.84933C12.7323 2.6227 12.368 2.5 11.9934 2.5C11.6188 2.5 11.2545 2.6227 10.9562 2.84933L9.82481 3.70647C6.58218 6.17053 3.80449 9.19261 1.62196 12.631L1.25853 13.2019C1.17625 13.3315 1.13028 13.4808 1.12543 13.6342C1.12058 13.7877 1.15701 13.9396 1.23094 14.0741C1.30486 14.2086 1.41355 14.3208 1.54566 14.399C1.67777 14.4772 1.82845 14.5184 1.98196 14.5185H4.08024C3.93125 16.3346 3.99569 18.1619 4.27224 19.963C4.4471 21.0979 5.42424 21.9345 6.5711 21.9345Z"/><path d="M11.9905 13.8672C13.5694 13.8672 14.85 15.146 14.85 16.7249V21.9363H9.13281V16.7249C9.13281 15.1478 10.4117 13.8672 11.9905 13.8672Z"/>',
),
calendar(
'<rect x="3.5" y="5" width="17" height="16" rx="3"/><path d="M3.5 9.5h17M8 3v4M16 3v4"/>',
),
diary(
'<path d="M5 4.5A2 2 0 0 1 7 3h11a1 1 0 0 1 1 1v15a1 1 0 0 1-1 1H7a2 2 0 0 0-2 2z"/><path d="M5 18.5A2 2 0 0 1 7 17h12"/>',
),
paw(
'<path d="M10.9731 11.6619C6.70179 13.1828 6.12626 18.1092 6.37241 20.3822C6.51752 21.1413 6.94458 21.6692 7.13997 21.8383C9.02228 23.0862 10.9835 22.0722 11.7288 21.4093C12.7895 20.5045 14.0253 20.8089 14.5105 21.0741C15.9788 21.875 17.3915 21.2299 17.9143 20.8072C19.6762 18.4847 18.6484 15.3673 17.9143 14.0989C16.1523 10.6017 12.5527 11.0171 10.9731 11.6619Z" stroke-width="2.4"/><ellipse cx="8.63786" cy="5.54496" rx="2.27492" ry="2.6972" transform="rotate(-7.76126 8.63786 5.54496)" stroke-width="2.4"/><ellipse cx="2.27492" cy="2.6972" rx="2.27492" ry="2.6972" transform="matrix(-0.970693 -0.240323 -0.240323 0.970693 18.3203 2.86719)" stroke-width="2.4"/><ellipse cx="3.69577" cy="10.2428" rx="2.15174" ry="2.55115" transform="rotate(-25.9543 3.69577 10.2428)" stroke-width="2.4"/><ellipse cx="1.77315" cy="2.10228" rx="1.77315" ry="2.10228" transform="matrix(-0.713403 -0.700754 -0.700754 0.713403 23.3594 8.41406)" stroke-width="2.4"/>',
),
user(
'<circle cx="12" cy="8" r="4"/><path d="M4.5 20c0-3.6 3.4-6 7.5-6s7.5 2.4 7.5 6"/>',
),
bell(
'<path d="M18 8.5a6 6 0 1 0-12 0c0 5-2 6.5-2 6.5h16s-2-1.5-2-6.5Z"/><path d="M10.3 19a2 2 0 0 0 3.4 0"/>',
),
plus(
'<path d="M12 5v14M5 12h14"/>',
),
minus(
'<path d="M5 12h14"/>',
),
check(
'<path d="M5 12.5 10 17 19 7"/>',
),
chevR(
'<path d="M9 6l6 6-6 6"/>',
),
chevL(
'<path d="M15 6l-6 6 6 6"/>',
),
chevD(
'<path d="M6 9l6 6 6-6"/>',
),
chevU(
'<path d="M6 15l6-6 6 6"/>',
),
share(
'<path d="M12 15V3m0 0L8 7m4-4 4 4"/><path d="M5 12v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6"/>',
),
download(
'<path d="M12 3v12m0 0 4-4m-4 4-4-4"/><path d="M5 17v2a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-2"/>',
),
more(
'<circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/>',
),
moreV(
'<circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/>',
),
edit(
'<path d="M4 20h4L19 9l-4-4L4 16v4Z"/><path d="M14 6l4 4"/>',
),
pencilCute(
'<path d="M5.6 18.4l.9-3.1a2.4 2.4 0 0 1 .62-1.05l7.2-7.2a2.5 2.5 0 0 1 3.54 0l.53.53a2.5 2.5 0 0 1 0 3.54l-7.2 7.2a2.4 2.4 0 0 1-1.05.62l-3.1.9a.8.8 0 0 1-.99-.99Z"/><path d="M13.4 8.2l2.4 2.4"/>',
),
x(
'<path d="M6 6l12 12M18 6 6 18"/>',
),
heart(
'<path d="M12 20s-7-4.4-9.2-8.4C1.2 8.7 2.6 5 6 5c2 0 3.2 1.3 4 2.4C10.8 6.3 12 5 14 5c3.4 0 4.8 3.7 3.2 6.6C19 15.6 12 20 12 20Z"/>',
),
comment(
'<path d="M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H9l-4 4v-4H5.5A1.5 1.5 0 0 1 4 14.5Z"/>',
),
text(
'<path d="M5 6h14M5 10h14M5 14h14M5 18h9"/>',
),
vaccine(
'<path d="M16 3l5 5M18.5 5.5 9 15l-2 4-2-2 4-2 9.5-9.5"/><path d="M13 8l3 3"/>',
),
hospital(
'<rect x="3.5" y="6" width="17" height="14" rx="3"/><path d="M8 6V4.5A1.5 1.5 0 0 1 9.5 3h5A1.5 1.5 0 0 1 16 4.5V6"/><path d="M12 10v6M9 13h6"/>',
),
grooming(
'<circle cx="6" cy="6" r="2.4"/><circle cx="6" cy="18" r="2.4"/><path d="M8 7.5 20 17M8 16.5 20 7"/>',
),
walk(
'<ellipse cx="7" cy="6" rx="1.5" ry="2"/><ellipse cx="12" cy="5" rx="1.5" ry="2"/><ellipse cx="16.5" cy="7" rx="1.5" ry="2"/><path d="M8.5 11c-1.7 0-3 1.3-3 2.8 0 1.1.8 1.6 1.8 1.6.7 0 1-.3 1.4-.3s.7.3 1.4.3c1 0 1.8-.5 1.8-1.6 0-1.5-1.3-2.8-3.4-2.8Z" transform="translate(4 5)"/>',
),
food(
'<path d="M5 8h14l-1.2 10a2 2 0 0 1-2 1.8H8.2a2 2 0 0 1-2-1.8Z"/><path d="M9 8c0-2 1.3-3.5 3-3.5S15 6 15 8"/>',
),
tree(
'<path d="M12 13.6c2.9 0 5.2-2.1 5.2-4.8C17.2 6.1 14.9 4 12 4S6.8 6.1 6.8 8.8c0 2.7 2.3 4.8 5.2 4.8Z"/><path d="M12 13.6V21"/><path d="M12 17.4 8.9 15M12 16.4l3.1-2.4"/>',
),
repeat(
'<path d="M4 9.5V8a2.5 2.5 0 0 1 2.5-2.5H17"/><path d="M14 2.5 17.5 6 14 9.5"/><path d="M20 14.5V16a2.5 2.5 0 0 1-2.5 2.5H7"/><path d="M10 21.5 6.5 18 10 14.5"/>',
),
pill(
'<rect x="3" y="9" width="18" height="6" rx="3" transform="rotate(45 12 12)"/><path d="M12 7.8 16.2 12"/>',
),
cake(
'<path d="M4 21h16v-7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2Z"/><path d="M4 16c1.3 1 2.3 1 3.3 0s2-1 3.3 0 2 1 3.3 0 2-1 3.4 0"/><path d="M12 8.5V5M12 5l1-1.5M12 5l-1-1.5"/>',
),
weight(
'<rect x="4" y="7" width="16" height="14" rx="3"/><path d="M9 7a3 3 0 0 1 6 0"/><path d="M12 11v5m0-5-2 2m2-2 2 2"/>',
),
male(
'<circle cx="10" cy="14" r="5.5"/><path d="M14 10 20 4m0 0h-4m4 0v4"/>',
),
female(
'<circle cx="12" cy="9" r="5.5"/><path d="M12 14.5V21m-3-3h6"/>',
),
alert(
'<path d="M12 3 22 20H2Z"/><path d="M12 9v5M12 17h.01"/>',
),
shield(
'<path d="M12 3 5 6v5.5c0 4.3 3 7.5 7 9 4-1.5 7-4.7 7-9V6Z"/><path d="M9 12l2 2 4-4"/>',
),
camera(
'<path d="M4 8h3l1.5-2.5h7L17 8h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z"/><circle cx="12" cy="13" r="3.2"/>',
),
battery(
'<rect x="2.5" y="8" width="14" height="8" rx="2.2"/><path d="M19 10.5v3"/><path d="M9 9.9 7.4 12.4H9.6L8 14.9"/>',
),
qr(
'<rect x="3.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.5"/><path d="M14 14h2.5v2.5M20 14v.01M14 20v.01M17 17h3.5v3.5M20.5 17v.01"/>',
),
copy(
'<rect x="8.5" y="8.5" width="11.5" height="11.5" rx="2.5"/><path d="M15.5 8.5V6.2A2.2 2.2 0 0 0 13.3 4H6.2A2.2 2.2 0 0 0 4 6.2v7.1A2.2 2.2 0 0 0 6.2 15.5H8.5"/>',
),
gear(
'<circle cx="12" cy="12" r="3"/><path d="M12 2.5v3M12 18.5v3M21.5 12h-3M5.5 12h-3M18.7 5.3l-2.1 2.1M7.4 16.6l-2.1 2.1M18.7 18.7l-2.1-2.1M7.4 7.4 5.3 5.3"/>',
),
users(
'<circle cx="9" cy="8" r="3.3"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5"/><path d="M16 5.2A3.2 3.2 0 0 1 17 11.4M21 19c0-2.3-1.5-4-3.8-4.6"/>',
),
clock(
'<circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/>',
),
location(
'<path d="M12 21c4-4 7-7.2 7-11a7 7 0 1 0-14 0c0 3.8 3 7 7 11Z"/><circle cx="12" cy="10" r="2.5"/>',
),
search(
'<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
),
sparkle(
'<path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8Z"/><path d="M18.5 16l.7 2 .7-2 .7 2"/>',
),
qrcode(
'<rect x="4" y="4" width="6" height="6" rx="1"/><rect x="14" y="4" width="6" height="6" rx="1"/><rect x="4" y="14" width="6" height="6" rx="1"/><path d="M14 14h6v6h-6z"/>',
),
link(
'<path d="M9.5 14.5 14.5 9.5"/><path d="M11 7l1.2-1.2a4 4 0 0 1 5.7 5.7L16.7 12.7"/><path d="M13 17l-1.2 1.2a4 4 0 0 1-5.7-5.7L7.3 11.3"/>',
),
star(
'<path d="M12 3.5 14.6 9l5.9.6-4.4 4 1.3 5.8L12 16.6 6.6 19.4 7.9 13.6 3.5 9.6 9.4 9Z"/>',
),
logout(
'<path d="M14 4h4a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-4"/><path d="M10 8l-4 4 4 4M6 12h11"/>',
),
moon(
'<path d="M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5Z"/>',
),
memory(
'<path d="M5 7.5c1.6-.9 3.5-1.2 5-.7 1.4.5 2.5 1.6 3 3.3 1.7.1 3.3 1 4 2.2.5.8.6 1.7.2 2.5-.4.8-1.3 1.4-2.6 1.4H6.5c-1.4 0-2.7-.6-3.5-1.7-1.2-1.6-1.2-3.5-.2-5 .7-1 1.8-1.6 2.2-1.7Z"/>',
),
trash(
'<path d="M4 7h16M9.5 7V5.2A1.2 1.2 0 0 1 10.7 4h2.6a1.2 1.2 0 0 1 1.2 1.2V7M6.5 7l.8 12a2 2 0 0 0 2 1.9h5.4a2 2 0 0 0 2-1.9L17.5 7"/>',
),
shuffle(
'<path d="M16 4h4v4"/><path d="M4 20 20 4"/><path d="M16 20h4v-4"/><path d="M4 4l5.5 5.5M14.5 14.5 20 20"/>',
),
dice(
'<rect x="4" y="4" width="16" height="16" rx="4"/><circle cx="9" cy="9" r="1.15" fill="currentColor" stroke="none"/><circle cx="15" cy="15" r="1.15" fill="currentColor" stroke="none"/><circle cx="15" cy="9" r="1.15" fill="currentColor" stroke="none"/><circle cx="9" cy="15" r="1.15" fill="currentColor" stroke="none"/>',
);
const RupIconName(this.paths);
/// SVG 내부 요소 (svg 태그 제외)
final String paths;
}
/// 전용 글리프 — 뷰박스와 채움 방식이 제각각이라 SVG 본문을 통째로 보관한다.
enum RupGlyph {
/// 펫 명함 (하단 탭)
petCard(24, '<rect x="1.25" y="4.39062" width="21.4922" height="15.2266" rx="3" fill="none" stroke="currentColor" stroke-width="2.4"></rect><path d="M14.7344 8.25302C14.5355 8.25302 14.3447 8.33204 14.204 8.47269C14.0634 8.61334 13.9844 8.80411 13.9844 9.00302C13.9844 9.20193 14.0634 9.3927 14.204 9.53335C14.3447 9.674 14.5355 9.75302 14.7344 9.75302L18.8939 9.75C19.0929 9.75 19.2836 9.67098 19.4243 9.53033C19.5649 9.38968 19.6439 9.19891 19.6439 9C19.6439 8.80109 19.5649 8.61032 19.4243 8.46967C19.2836 8.32902 19.0929 8.25 18.8939 8.25L14.7344 8.25302ZM14.7344 11.253C14.5355 11.253 14.3447 11.332 14.204 11.4727C14.0634 11.6133 13.9844 11.8041 13.9844 12.003C13.9844 12.2019 14.0634 12.3927 14.204 12.5333C14.3447 12.674 14.5355 12.753 14.7344 12.753L18.8939 12.75C19.0929 12.75 19.2836 12.671 19.4243 12.5303C19.5649 12.3897 19.6439 12.1989 19.6439 12C19.6439 11.8011 19.5649 11.6103 19.4243 11.4697C19.2836 11.329 19.0929 11.25 18.8939 11.25L14.7344 11.253ZM13.9844 15.003C13.9844 14.8041 14.0634 14.6133 14.204 14.4727C14.3447 14.332 14.5355 14.253 14.7344 14.253L17.3939 14.25C17.5929 14.25 17.7836 14.329 17.9243 14.4697C18.0649 14.6103 18.1439 14.8011 18.1439 15C18.1439 15.1989 18.0649 15.3897 17.9243 15.5303C17.7836 15.671 17.5929 15.75 17.3939 15.75L14.7344 15.753C14.5355 15.753 14.3447 15.674 14.204 15.5334C14.0634 15.3927 13.9844 15.2019 13.9844 15.003Z" fill="currentColor"></path><path d="M11.308 8.65013C11.308 8.54307 11.1784 8.4896 11.1029 8.56549L9.75905 9.9162C9.71247 9.96301 9.71247 10.0387 9.75905 10.0855L10.8146 11.1464C10.8615 11.1936 10.9378 11.1936 10.9848 11.1464L11.2229 10.9071C11.2698 10.8599 11.3461 10.8599 11.393 10.9071L11.6312 11.1464C11.6781 11.1936 11.7544 11.1936 11.8013 11.1464L12.4486 10.4958C12.4952 10.449 12.4952 10.3734 12.4486 10.3266L11.3429 9.21521C11.3205 9.19273 11.308 9.1623 11.308 9.13058V8.65013ZM4.86054 10.0863C4.81363 10.0392 4.73731 10.0392 4.6904 10.0863L4.45139 10.3266C4.40482 10.3734 4.40482 10.449 4.45139 10.4958L5.5571 11.6072C5.57947 11.6297 5.59203 11.6601 5.59203 11.6918V12.8517C5.59203 12.866 5.58946 12.8803 5.58445 12.8937L5.19133 13.9465C5.18632 13.9599 5.18375 13.9741 5.18375 13.9884V15.6259C5.18375 15.6922 5.23748 15.7459 5.30375 15.7459H5.88031C5.94659 15.7459 6.00031 15.6922 6.00031 15.6259V14.5625C6.00031 14.5319 6.01199 14.5024 6.03296 14.4802L7.1244 13.3215C7.14707 13.2974 7.17868 13.2838 7.21175 13.2838L9.55484 13.2838C9.62112 13.2838 9.67484 13.3375 9.67484 13.4038V15.6259C9.67484 15.6922 9.72857 15.7459 9.79484 15.7459H10.3714C10.4377 15.7459 10.4914 15.6922 10.4914 15.6259V11.6918C10.4914 11.6601 10.4788 11.6297 10.4565 11.6072L9.35163 10.4967C9.30472 10.4495 9.22841 10.4495 9.18149 10.4967L8.89346 10.7862C8.87094 10.8088 8.84033 10.8216 8.8084 10.8216H5.64192C5.60998 10.8216 5.57937 10.8088 5.55685 10.7862L4.86054 10.0863Z" fill="currentColor"></path>'),
/// 다이어리
diary(16, '<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1944 8.02144e-08C13.5981 -0.000127663 13.9869 0.152325 14.2829 0.426798C14.5789 0.70127 14.7602 1.07748 14.7904 1.48L14.7944 1.6V14.4C14.7946 14.8037 14.6421 15.1925 14.3676 15.4884C14.0932 15.7844 13.717 15.9657 13.3144 15.996L13.1944 16H3.59443C3.19077 16.0001 2.80197 15.8477 2.50599 15.5732C2.21 15.2987 2.0287 14.9225 1.99843 14.52L1.99443 14.4V13.6C1.79052 13.5998 1.5944 13.5217 1.44613 13.3817C1.29786 13.2417 1.20864 13.0504 1.19669 12.8469C1.18474 12.6433 1.25097 12.4429 1.38184 12.2865C1.51271 12.1302 1.69835 12.0297 1.90083 12.0056L1.99443 12V10.4C1.79052 10.3998 1.5944 10.3217 1.44613 10.1817C1.29786 10.0417 1.20864 9.85044 1.19669 9.64688C1.18474 9.44333 1.25097 9.2429 1.38184 9.08653C1.51271 8.93017 1.69835 8.82968 1.90083 8.8056L1.99443 8.8V7.2C1.79052 7.19977 1.5944 7.1217 1.44613 6.98172C1.29786 6.84175 1.20864 6.65044 1.19669 6.44688C1.18474 6.24333 1.25097 6.0429 1.38184 5.88653C1.51271 5.73017 1.69835 5.62968 1.90083 5.6056L1.99443 5.6V4C1.79052 3.99977 1.5944 3.9217 1.44613 3.78172C1.29786 3.64175 1.20864 3.45044 1.19669 3.24688C1.18474 3.04333 1.25097 2.8429 1.38184 2.68653C1.51271 2.53017 1.69835 2.42968 1.90083 2.4056L1.99443 2.4V1.6C1.9943 1.19634 2.14675 0.807547 2.42122 0.511562C2.6957 0.215577 3.0719 0.0342747 3.47443 0.00400017L3.59443 8.02144e-08H13.1944ZM13.1944 1.6H3.59443V14.4H13.1944V1.6Z"></path><path d="M10.3225 3.27196C10.3225 3.20059 10.2361 3.16494 10.1858 3.21553L9.28989 4.11601C9.25884 4.14721 9.25884 4.19765 9.28989 4.22886L9.9936 4.93616C10.0249 4.96759 10.0758 4.96759 10.107 4.93616L10.2658 4.77659C10.2971 4.74515 10.3479 4.74515 10.3792 4.77659L10.538 4.93616C10.5692 4.96759 10.6201 4.96759 10.6514 4.93616L11.0829 4.50243C11.114 4.47122 11.114 4.42079 11.0829 4.38958L10.3458 3.64868C10.3309 3.63369 10.3225 3.61341 10.3225 3.59226V3.27196ZM6.02421 4.22943C5.99294 4.198 5.94206 4.198 5.91079 4.22943L5.75145 4.38958C5.7204 4.42079 5.7204 4.47122 5.75145 4.50243L6.48859 5.24333C6.5035 5.25832 6.51187 5.27861 6.51187 5.29975V6.07302C6.51187 6.08257 6.51016 6.09205 6.50682 6.101L6.24474 6.80285C6.2414 6.8118 6.23969 6.82128 6.23969 6.83083V7.9225C6.23969 7.96668 6.2755 8.0025 6.31969 8.0025H6.70406C6.74825 8.0025 6.78406 7.96668 6.78406 7.9225V7.21352C6.78406 7.19313 6.79185 7.17351 6.80583 7.15866L7.53345 6.38619C7.54857 6.37014 7.56964 6.36104 7.59169 6.36104L9.15375 6.36104C9.19793 6.36104 9.23375 6.39686 9.23375 6.44104V7.9225C9.23375 7.96668 9.26957 8.0025 9.31375 8.0025H9.69813C9.74231 8.0025 9.77813 7.96668 9.77813 7.9225V5.29975C9.77813 5.27861 9.76975 5.25832 9.75484 5.24333L9.01827 4.50301C8.987 4.47157 8.93612 4.47157 8.90485 4.50301L8.71283 4.69601C8.69782 4.7111 8.67741 4.71958 8.65612 4.71958H6.54513C6.52384 4.71958 6.50343 4.7111 6.48842 4.69601L6.02421 4.22943Z"></path><path d="M5.69531 9.52545C5.5627 9.52545 5.43553 9.57813 5.34176 9.6719C5.24799 9.76567 5.19531 9.89284 5.19531 10.0255C5.19531 10.1581 5.24799 10.2852 5.34176 10.379C5.43553 10.4728 5.5627 10.5255 5.69531 10.5255L11.135 10.5234C11.2676 10.5234 11.3948 10.4708 11.4886 10.377C11.5823 10.2832 11.635 10.156 11.635 10.0234C11.635 9.89083 11.5823 9.76365 11.4886 9.66988C11.3948 9.57612 11.2676 9.52344 11.135 9.52344L5.69531 9.52545ZM5.69531 11.5255C5.5627 11.5255 5.43553 11.5781 5.34176 11.6719C5.24799 11.7657 5.19531 11.8928 5.19531 12.0255C5.19531 12.1581 5.24799 12.2852 5.34176 12.379C5.43553 12.4728 5.5627 12.5255 5.69531 12.5255L11.135 12.5234C11.2676 12.5234 11.3948 12.4708 11.4886 12.377C11.5823 12.2832 11.635 12.156 11.635 12.0234C11.635 11.8908 11.5823 11.7637 11.4886 11.6699C11.3948 11.5761 11.2676 11.5234 11.135 11.5234L5.69531 11.5255Z"></path>', fill: true),
/// 추억보관함 (구름)
memory(15, '<path d="M3.53223 3.66992C4.76629 3.00286 6.20357 2.76179 7.44043 3.1416C8.59229 3.49522 9.59538 4.39488 10.1172 6.06152L10.2666 6.54199L10.7676 6.58496C12.0082 6.69208 13.1751 7.36852 13.792 8.24609V8.24707C14.128 8.72659 14.2952 9.25604 14.2393 9.78418C14.1904 10.2456 13.9694 10.7287 13.5029 11.1953L13.2881 11.3945C12.7069 11.8929 11.8306 12.0498 11.0273 12.0498H4.7832C4.25746 12.0499 3.73783 11.9339 3.26172 11.7109C2.78557 11.4879 2.36381 11.1628 2.02734 10.7588L2.02051 10.75L2.01367 10.7422L1.79004 10.4756C0.803639 9.23247 0.590492 7.99228 0.853516 6.89355L0.916992 6.66113C1.29871 5.41457 2.29777 4.33824 3.53223 3.66992Z" stroke="currentColor" stroke-width="1.5"></path>'),
/// 설정 톱니
gear(24, '<path d="M9.58295 3.8399C9.64015 3.23816 9.91964 2.67936 10.3668 2.27267C10.814 1.86598 11.3967 1.64062 12.0012 1.64062C12.6056 1.64062 13.1884 1.86598 13.6356 2.27267C14.0827 2.67936 14.3622 3.23816 14.4194 3.8399C14.4538 4.22862 14.5813 4.60334 14.7912 4.93233C15.0011 5.26133 15.2871 5.53491 15.6251 5.72994C15.9631 5.92496 16.3432 6.03568 16.733 6.05273C17.1229 6.06977 17.5111 5.99264 17.8649 5.82785C18.4141 5.57848 19.0365 5.5424 19.6109 5.72663C20.1853 5.91085 20.6707 6.30221 20.9724 6.82453C21.2742 7.34686 21.3708 7.96277 21.2435 8.55241C21.1162 9.14204 20.774 9.66321 20.2836 10.0145C19.9643 10.2386 19.7036 10.5362 19.5236 10.8824C19.3437 11.2285 19.2497 11.6129 19.2497 12.003C19.2497 12.3931 19.3437 12.7774 19.5236 13.1236C19.7036 13.4697 19.9643 13.7674 20.2836 13.9914C20.774 14.3427 21.1162 14.8639 21.2435 15.4535C21.3708 16.0431 21.2742 16.6591 20.9724 17.1814C20.6707 17.7037 20.1853 18.0951 19.6109 18.2793C19.0365 18.4635 18.4141 18.4274 17.8649 18.1781C17.5111 18.0133 17.1229 17.9361 16.733 17.9532C16.3432 17.9702 15.9631 18.081 15.6251 18.276C15.2871 18.471 15.0011 18.7446 14.7912 19.0736C14.5813 19.4026 14.4538 19.7773 14.4194 20.166C14.3622 20.7678 14.0827 21.3266 13.6356 21.7332C13.1884 22.1399 12.6056 22.3653 12.0012 22.3653C11.3967 22.3653 10.814 22.1399 10.3668 21.7332C9.91964 21.3266 9.64015 20.7678 9.58295 20.166C9.54863 19.7772 9.42111 19.4023 9.21117 19.0732C9.00124 18.7441 8.71507 18.4704 8.37692 18.2754C8.03878 18.0803 7.6586 17.9696 7.2686 17.9527C6.8786 17.9358 6.49026 18.0131 6.13648 18.1781C5.58721 18.4274 4.96481 18.4635 4.3904 18.2793C3.816 18.0951 3.33069 17.7037 3.02892 17.1814C2.72716 16.6591 2.63053 16.0431 2.75785 15.4535C2.88517 14.8639 3.22732 14.3427 3.71772 13.9914C4.03705 13.7674 4.29773 13.4697 4.47769 13.1236C4.65766 12.7774 4.75161 12.3931 4.75161 12.003C4.75161 11.6129 4.65766 11.2285 4.47769 10.8824C4.29773 10.5362 4.03705 10.2386 3.71772 10.0145C3.22801 9.66304 2.88646 9.14207 2.75945 8.55283C2.63244 7.9636 2.72904 7.34819 3.03048 6.8262C3.33192 6.30422 3.81666 5.91295 4.39049 5.72845C4.96433 5.54394 5.58627 5.57937 6.13544 5.82785C6.48918 5.99264 6.87741 6.06977 7.26727 6.05273C7.65714 6.03568 8.03716 5.92496 8.37516 5.72994C8.71317 5.53491 8.99922 5.26133 9.2091 4.93233C9.41897 4.60334 9.54649 4.22862 9.58087 3.8399"/><path d="M11.999 15.0058C13.6575 15.0058 15.0019 13.6614 15.0019 12.0029C15.0019 10.3444 13.6575 9 11.999 9C10.3405 9 8.99609 10.3444 8.99609 12.0029C8.99609 13.6614 10.3405 15.0058 11.999 15.0058Z"/>', stroke: 2.2),
/// QR 코드
qr(15, '<path fill-rule="evenodd" clip-rule="evenodd" d="M8.20312 2.10938C8.20312 1.92289 8.12905 1.74405 7.99718 1.61219C7.86532 1.48033 7.68648 1.40625 7.5 1.40625C7.31352 1.40625 7.13468 1.48033 7.00282 1.61219C6.87095 1.74405 6.79688 1.92289 6.79688 2.10938V6.79688H2.10938C1.92289 6.79688 1.74405 6.87095 1.61219 7.00282C1.48033 7.13468 1.40625 7.31352 1.40625 7.5C1.40625 7.68648 1.48033 7.86532 1.61219 7.99718C1.74405 8.12905 1.92289 8.20312 2.10938 8.20312H7.26562C7.51427 8.20312 7.75272 8.10435 7.92854 7.92854C8.10435 7.75272 8.20312 7.51427 8.20312 7.26562V2.10938ZM6.79688 10.3125C6.79688 10.0639 6.89565 9.8254 7.07146 9.64959C7.24728 9.47377 7.48573 9.375 7.73438 9.375H10.3125C10.499 9.375 10.6778 9.44908 10.8097 9.58094C10.9415 9.7128 11.0156 9.89164 11.0156 10.0781C11.0156 10.2646 10.9415 10.4434 10.8097 10.5753C10.6778 10.7072 10.499 10.7812 10.3125 10.7812H8.20312V12.8906C8.20312 13.0771 8.12905 13.2559 7.99718 13.3878C7.86532 13.5197 7.68648 13.5938 7.5 13.5938C7.31352 13.5938 7.13468 13.5197 7.00282 13.3878C6.87095 13.2559 6.79688 13.0771 6.79688 12.8906V10.3125ZM12.1875 12.1875H10.0781C9.89164 12.1875 9.7128 12.2616 9.58094 12.3934C9.44908 12.5253 9.375 12.7041 9.375 12.8906C9.375 13.0771 9.44908 13.2559 9.58094 13.3878C9.7128 13.5197 9.89164 13.5938 10.0781 13.5938H12.6562C12.9049 13.5938 13.1433 13.495 13.3192 13.3192C13.495 13.1433 13.5938 12.9049 13.5938 12.6562V7.73438C13.5938 7.48573 13.495 7.24728 13.3192 7.07146C13.1433 6.89565 12.9049 6.79688 12.6562 6.79688H10.0781C9.89164 6.79688 9.7128 6.87095 9.58094 7.00282C9.44908 7.13468 9.375 7.31352 9.375 7.5C9.375 7.68648 9.44908 7.86532 9.58094 7.99718C9.7128 8.12905 9.89164 8.20312 10.0781 8.20312H12.1875V12.1875ZM2.8125 4.21875V2.8125H4.21875V4.21875H2.8125ZM1.40625 2.34375C1.40625 2.09511 1.50502 1.85665 1.68084 1.68084C1.85665 1.50502 2.09511 1.40625 2.34375 1.40625H4.6875C4.93614 1.40625 5.1746 1.50502 5.35041 1.68084C5.52623 1.85665 5.625 2.09511 5.625 2.34375V4.6875C5.625 4.93614 5.52623 5.1746 5.35041 5.35041C5.1746 5.52623 4.93614 5.625 4.6875 5.625H2.34375C2.09511 5.625 1.85665 5.52623 1.68084 5.35041C1.50502 5.1746 1.40625 4.93614 1.40625 4.6875V2.34375ZM2.8125 10.7812V12.1875H4.21875V10.7812H2.8125ZM2.34375 9.375C2.09511 9.375 1.85665 9.47377 1.68084 9.64959C1.50502 9.8254 1.40625 10.0639 1.40625 10.3125V12.6562C1.40625 12.9049 1.50502 13.1433 1.68084 13.3192C1.85665 13.495 2.09511 13.5938 2.34375 13.5938H4.6875C4.93614 13.5938 5.1746 13.495 5.35041 13.3192C5.52623 13.1433 5.625 12.9049 5.625 12.6562V10.3125C5.625 10.0639 5.52623 9.8254 5.35041 9.64959C5.1746 9.47377 4.93614 9.375 4.6875 9.375H2.34375ZM10.7812 4.21875V2.8125H12.1875V4.21875H10.7812ZM9.375 2.34375C9.375 2.09511 9.47377 1.85665 9.64959 1.68084C9.8254 1.50502 10.0639 1.40625 10.3125 1.40625H12.6562C12.9049 1.40625 13.1433 1.50502 13.3192 1.68084C13.495 1.85665 13.5938 2.09511 13.5938 2.34375V4.6875C13.5938 4.93614 13.495 5.1746 13.3192 5.35041C13.1433 5.52623 12.9049 5.625 12.6562 5.625H10.3125C10.0639 5.625 9.8254 5.52623 9.64959 5.35041C9.47377 5.1746 9.375 4.93614 9.375 4.6875V2.34375Z" fill="currentColor"></path>', fill: true);
const RupGlyph(this.viewBox, this.paths, {this.fill = false, this.stroke});
final double viewBox;
final String paths;
/// true 면 currentColor 로 채우고, false 면 요소별 속성을 그대로 쓴다
final bool fill;
/// 선 굵기가 지정된 글리프 (null 이면 요소별 속성)
final double? stroke;
}
+62
View File
@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:tma_app/core/constants/app_sizes.dart';
/// 시안의 `:active { transform: scale(.95) }` 눌림 효과.
/// 물결 효과 없이 살짝 줄어들었다 돌아온다.
class Pressable extends StatefulWidget {
const Pressable({
super.key,
required this.child,
required this.onTap,
this.pressedScale = 0.95,
this.semanticLabel,
this.behavior = HitTestBehavior.opaque,
});
final Widget child;
final VoidCallback? onTap;
final double pressedScale;
/// 스크린리더용 이름. 없으면 자식의 텍스트를 그대로 읽는다.
final String? semanticLabel;
final HitTestBehavior behavior;
@override
State<Pressable> createState() => _PressableState();
}
class _PressableState extends State<Pressable> {
bool _pressed = false;
void _setPressed(bool value) {
if (_pressed == value) return;
setState(() => _pressed = value);
}
@override
Widget build(BuildContext context) {
final enabled = widget.onTap != null;
return Semantics(
button: true,
enabled: enabled,
label: widget.semanticLabel,
excludeSemantics: widget.semanticLabel != null,
child: MouseRegion(
cursor: enabled ? SystemMouseCursors.click : MouseCursor.defer,
child: GestureDetector(
behavior: widget.behavior,
onTap: widget.onTap,
onTapDown: enabled ? (_) => _setPressed(true) : null,
onTapUp: enabled ? (_) => _setPressed(false) : null,
onTapCancel: enabled ? () => _setPressed(false) : null,
child: AnimatedScale(
scale: _pressed ? widget.pressedScale : 1,
duration: AppDurations.press,
curve: Curves.easeOut,
child: widget.child,
),
),
),
);
}
}
+108
View File
@@ -0,0 +1,108 @@
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/shared/widgets/pressable.dart';
/// 시안 `.rup-btn` 변형
enum RupButtonVariant {
/// 주황 채움 (.rup-btn--primary)
primary,
/// 흰 바탕 + 선 (.rup-btn--ghost)
ghost,
/// 연주황 바탕 (.rup-btn--soft)
soft,
/// 주황 화면 위의 흰 버튼 (인트로·온보딩 CTA)
light,
}
/// 시안의 pill 버튼. 기본은 가로를 꽉 채운다(.rup-btn--block).
class RupButton extends StatelessWidget {
const RupButton({
super.key,
required this.label,
required this.onPressed,
this.variant = RupButtonVariant.primary,
this.large = false,
this.expand = true,
this.height,
this.leading,
});
final String label;
final VoidCallback? onPressed;
final RupButtonVariant variant;
/// .rup-btn--lg (글자 16, 세로 여백 16)
final bool large;
/// 가로 채움 여부
final bool expand;
/// 높이를 시안 값으로 고정해야 하는 자리(인트로 CTA 46 등)에서만 지정
final double? height;
final Widget? leading;
@override
Widget build(BuildContext context) {
final (bg, fg, border) = switch (variant) {
RupButtonVariant.primary => (
AppColors.primary,
Colors.white,
AppColors.primaryBorder,
),
RupButtonVariant.ghost => (AppColors.surface, AppColors.ink, AppColors.line),
RupButtonVariant.soft => (
AppColors.primary50,
AppColors.primary600,
AppColors.primary100,
),
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 content = Row(
mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (leading != null) ...[leading!, const SizedBox(width: 7)],
Flexible(
child: Text(
label,
style: textStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
return Opacity(
opacity: onPressed == null ? 0.5 : 1,
child: Pressable(
onTap: onPressed,
child: Container(
height: height,
width: expand ? double.infinity : null,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(
horizontal: 18,
vertical: height == null ? verticalPad : 0,
),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(AppRadius.pill),
border: border == null ? null : Border.all(color: border, width: 1.5),
),
child: content,
),
),
);
}
}