From 388ea4227cc2fb01878da248c0881e0a63f277fe Mon Sep 17 00:00:00 2001 From: Sn-Kinos Date: Sun, 27 Sep 2026 19:59:33 +0900 Subject: [PATCH] =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=9D=BC=C2=B7=EA=B3=B5=ED=86=B5=20=ED=81=AC=EB=A1=AC=EC=9D=84?= =?UTF-8?q?=20Figma=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=E2=80=94=20=EC=83=81=EB=8B=A8=20=EB=82=B4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=C2=B7=20=ED=95=98=EB=8B=A8=20?= =?UTF-8?q?=ED=83=AD=20=EC=95=84=EC=9D=B4=EC=BD=98=20=C2=B7=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=C2=B7=20=EC=8B=9C=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/icons/badge.png | Bin 0 -> 1807 bytes app/assets/icons/chevron_left.png | Bin 0 -> 334 bytes app/assets/icons/chevron_right.png | Bin 0 -> 551 bytes app/assets/icons/circle_user_round.png | Bin 0 -> 2312 bytes app/assets/icons/ellipsis_vertical.png | Bin 0 -> 378 bytes app/assets/icons/house.png | Bin 0 -> 1142 bytes app/assets/icons/message_square.png | Bin 0 -> 620 bytes app/assets/icons/plus.png | Bin 0 -> 380 bytes app/assets/icons/search.png | Bin 0 -> 1157 bytes app/assets/icons/settings.png | Bin 0 -> 1398 bytes app/assets/icons/share_2.png | Bin 0 -> 149 bytes app/assets/icons/x.png | Bin 0 -> 1720 bytes app/lib/core/router/app_router.dart | 52 +++++++++------ app/lib/core/theme/app_theme.dart | 85 ++++++++++++++++--------- app/lib/core/theme/tokens.dart | 26 +++++--- app/lib/core/ui/app_icons.dart | 22 +++++++ app/pubspec.yaml | 4 ++ app/tool/gen_tokens.py | 2 +- design/design-tokens.md | 31 ++++++--- 19 files changed, 153 insertions(+), 69 deletions(-) create mode 100644 app/assets/icons/badge.png create mode 100644 app/assets/icons/chevron_left.png create mode 100644 app/assets/icons/chevron_right.png create mode 100644 app/assets/icons/circle_user_round.png create mode 100644 app/assets/icons/ellipsis_vertical.png create mode 100644 app/assets/icons/house.png create mode 100644 app/assets/icons/message_square.png create mode 100644 app/assets/icons/plus.png create mode 100644 app/assets/icons/search.png create mode 100644 app/assets/icons/settings.png create mode 100644 app/assets/icons/share_2.png create mode 100644 app/assets/icons/x.png create mode 100644 app/lib/core/ui/app_icons.dart diff --git a/app/assets/icons/badge.png b/app/assets/icons/badge.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a4318559c9b262c348cc1bc776764d1a9cbd97 GIT binary patch literal 1807 zcmV+q2k`ibP)VSr4@kSG?53l9$u+1JFIh)^i{H>x1R;5q$EYR-mZU6(ifz!Nr*cQB06@MN=jCllV}yD`l@j=>cznSYz(*@4w)*AbzmKKif)M6~N!}UL)gDo|~K7M}F(#ftNsi z$aD>vUUAKIp-@omHpg0mG`i@?*!_lhnL5rF@Rz+?eSUtv z;%f_Odbu9C@QHhUGISCoK7)b#a;e=C`8ERS@^gof*Z zSa3DcsfG@#c+!hNfusa_^SbM39liBpY9=seNDd?=aHwNQ%BpuX-)l-xLyNpAontFW z36{L()w^oZ3IyLLFVPcW9Ec8Wttu{0PfrzjpPJQC;EIbbp-Uh&?lKN7Kn*4GYZ`o@ zCiI0k%_8RBjH7}JooC%8kP;^_Jm-otfv2aZY3kzNV1dG`rbwo|)3fesHM%C26Frt-7yhSh>` zusxkbn2ezH!w)f7?S)Q(v^7Po6pR&IV>2YRY0xQ9sJjCOZBWq}Lqi2ebb)tmb0j;v z`ER{ZojGQhFgYatG=hhGKA~{0W+94===CVvO z>hK*@WQvaP=;)|zmZD7{<%XNqk+N8uzeux4D!#xNwxe?%ySjPQA}$1){GK%iFr=|p zW3Q!xFH9Xyh@;YZu}dJYs1;Muvm^9TS7N`oF2~7WE3fI#*ifTF8)YJ%r!Sj+H z@)8kK6cu}I^rTCrQUNu^G@zrkPrbn=v^x-|XiAS9YM>_N#V`Zc=}?V0q2YQB;^ZX) zm$8_s+fUN~Hq>22AzRE>&;o&A5v??h< z1$XNBE*7i;MMQDr(%=gzr*_SdTonsefz$-I)(*ilJ(Ce9Vi7cS+4SN%qiFJn@_T!G zQQ@{j1#9ljO*UPqO*B@V1X3H}w7^DH7;-Bk2J*y$iwZDxim!|lNX{>dnmDWUWBXCB zs1bW)WC}RvNI@KM7AVGMk}K|1ZZYmb%RN0WK&nl_c@A~h!#1}&@Hc424y?-=(K>+8kfo*OyJb(%61PcRrB# zaP#doje65IHCG+f(o9a`LlbaG_he|BdUdF!As!4q4|okPDdP8%F0i?CikQZiHs%|J z9;t2NF2HG{kJML>Tg?RKWxyoz%L})+w^vI`OF!ZCqx1(ldj0$*2P297b_YcIb9s50 z(+f1V-d#1C(;lXRi;@tD2}EvgZfX$eT1~)}{b2&Nw x)(2`vYt3RW6yo@bw5v=eQya%1t_c$+$p6qz?3Y0pi^~82002ovPDHLkV1gdJOB(2PuIT49r^SM^Q0S4$5zmgv0tkWA zPxT+E-1_|gNB-kEk0Ny4w(psgnSLbCaF?Bz(p!O*4ojFOa=B<>V=NHJ?7H~YWz*st usuNkNZg-fx>w9Ew>k%z}ayiId55oCd9=fhGJZItx;(EIJxvX)lT literal 0 HcmV?d00001 diff --git a/app/assets/icons/chevron_right.png b/app/assets/icons/chevron_right.png new file mode 100644 index 0000000000000000000000000000000000000000..7ed4687da138d20fbf19002ff2c7dc3c2f19b9fc GIT binary patch literal 551 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 N;OXk;vd$@?2>_5nlLY_( literal 0 HcmV?d00001 diff --git a/app/assets/icons/house.png b/app/assets/icons/house.png new file mode 100644 index 0000000000000000000000000000000000000000..d6af378ec709dc161ac0f166ecb31a6d9e1a9794 GIT binary patch literal 1142 zcmV-+1d02JP)t?@8>A;FtPxT;LFukL%C0-Y z0?4jrgZQ%`oS@+Zg&2WdcE%76kZ7|AcXo)@rrQlarH_ zDxgjkaBy(21#zB87KdRWpZFN6fCfe8@bGXKEuxP%Zu~eUBp@jYeP2&(EKW92?r=lE{ODl0^q%i8K>$9 z*)C5{Po+HX%kAN3*OdpYDZ$M9h&X@SZ(Rp@j25UJE4UDm0J64T)d3*^hGiW1A=3U#qi zPF0kU4wp-y7*17`P!2b-Krx-FC?OnfB7v%Ks-pOII8h*TLUyXAj}ykgw06l|vqqz_ zd3=2AyrFgJM3OimovJ94I^65)>#g+4WWkka?t-i3T;gJ)I8|QE4OTZ6~&kmw~TU zcuTo$OK^=rW9^(zRF#dF0h7cYxB78us8ic^TyB9@R#y5nqYQ_`o+YVeJ0|zDSd5i~ zQ1p7e+}B>o%IYkk0tu)<0xFPz3M8Nc38+8TIM}8-G(?({ZVXZg?;z-faXv77~qevOoO$uKYZQgu(v_|evk7f#v?dCd7 z@>M{tk6Fz3Vf=qLK9T#Ijj02Ir1-hRc7;}RM~b2-iZb{70hryx{&z&W5&!@I07*qo IM6N<$g0Tn%A^-pY literal 0 HcmV?d00001 diff --git a/app/assets/icons/message_square.png b/app/assets/icons/message_square.png new file mode 100644 index 0000000000000000000000000000000000000000..7f9c97a03808486a04eaf5e9fa58b7378dcd0b0c GIT binary patch literal 620 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9FJ79h;%I?XTvD9BhG z;yg!_LmG+9I`EbK2{u}4(t~k?=Y4uSd(P=*kaPU z+)v3b=SlWD@TYy-Fs*)zQj37oi4e^TOvz`0c270CeO{IK+`6~ji+*(I@^A$i7Qgjc zd31@-(zlVWq5C}7D@@&d;oX9_ai1P&ZCz4a{-|GJx~2L_cfaPTU#^^8GBZ8VZ|3D= z$70IZ>p3sIshn8C_sCG^uJ_4)ziDb`!frn?K7DLKu5jn(o?JUtvs@L!JM!-yZ{EA^ zMUGM9{pt&cUu$tboY&T!@0a}d<0bEaYG591e~QTucE14NVLpEDIDE zC=!~mcEP^zS;glTgeLrvxg)qMQkV0g-lY9A=ZT+u|HLh3)jqFSv((is%j%@-@Bb^h zY?~RIcQi+J?xX2m&b2xGJ?9jrcV&CFzn``7-Gdd*R-u;rRvkXrBs8UX%B~6Let(nt z(xIxdd{Wez-CdU7?uk~Pon4_j^JzlT(F{!i&V4LJWs{iGWUNoJ8qb;gq=l_!;T(=- zb{U%LEo}Ri$TV<|jf^I!k~ literal 0 HcmV?d00001 diff --git a/app/assets/icons/plus.png b/app/assets/icons/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..c94bf6c0baa03338b9105a436e46c3206fb91ed7 GIT binary patch literal 380 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9FJ79h;%I?XTvD9BhG zx(~ZH)hZV&TuT>oSk}S~Y)* zJsokN*zkcI3l9OfO=!ix0^QwfS){BvP8cWL?>3QP(UIHWJqhTAy2H#*GYWtP-> z)&KO`XS0iCHlC)}f$P`u%B`%iu7j#hHR*v{fRE59M^Oh$@&D?`|2# z#=GvjE;4|v+;Yuc3so?_3jsO+G&>}-5`@)<-$gO9>e)Mn9rc0K=r09gdHCHI%c9KiSbpz zvazKt#gfu04Kh&5%9xEf-?XGI#rElCZH=xiCVZHJ(N`8qac^H<_9PRd*67|O-grD- zDt%_1BQ@*8N+$9y<9t3JQ3T#d3l(aW)yfOUB3a9&b}!buj90LDQLFT2XCSxejaXv` zA}N_z8B23oMGG}~37J#$mRNPEnwm_k6kL%P?(?2g({Z-Uw|9nEc0yFY-6m%DkoSd` zo=0l$)^D9Es5&Q-ynz?BKMaG0FvyqfFFBPfr*cXPuQycan|Gy%PB{T^A)C0# z^s$`r7|BVY!nhwSIw|A9m1DyHMEaD~BsUV|tYqu7RGuj$E+|4OY~-94UO2_iOKJ!? zpf+;7R?)J8Jl*t3yeSMlOKj?!b0Q0$(RD@VK5NN47(JO!{||D~G+E$1KGH+%S<5=2 zvk2Y=siKcIO)Cdq%z?~09dOinX`g6oZa?}&yd~O7-F}Fjhb_@BB4t|pN-ZrdEyvD3 XGIWd$Rl8kS00000NkvXXu0mjfwU7@Y literal 0 HcmV?d00001 diff --git a/app/assets/icons/settings.png b/app/assets/icons/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..9215b4c2f816f018848f5acf0e9ed9ebe0ee7ab1 GIT binary patch literal 1398 zcmV-+1&R8JP)NklL1 z$^>)+hY1=cu$ds);E#l4nwR=cpLW~CzxVW#=SVu~B%Mwl4$;ia%qat)RAH?`tHREP zd;9DYohw!`S}87coQlo~RpD!db4}+!q;k$fNqVRtozt~T@UuBCS`{v6B3{u%z!=(s z`MFc!QiV4*T-s+t6e^rvFH#EnDfEb9AbtV9$KBDX0pr^YQ3+{EPN{TCXq&-Q6tU?r zksX@K25~lHO9G3>x52uVSeEyxhxRfemh_yrl? zL@Vp!K)5cXo{l$^5i74!;oXru?G|j0<=LLXxf{CSzENNSxV6)oUMS#a;_y0wUptlb zLIFP;-g9ZBr3Po+JIVyiTI<@jYo+FQ6HHPepLV>XlQcK?i=68jB1ob5F zJ*TIj7~u9?y@nf%&50RS96R4isdzVzyu$pSqs(L(@O;`5@cT_q!ncc;UR1#q?L?);VGLNp0$up>OJ?!>tFpJ zZk%t_2%< z=a)*5f`sn57m{^>c@4!~M#$Z#%GG!)$Qm}zuu8jx9rL^-f37H8F8A9FavN!wsg|3; zG?kE+n}NK%VaR?|tb5U7LLW#J2Kjv5mDVNOC3hDq+Y9(DH7bvM8b=;$9M~E7osv5J zD9F92S5nfkPnLxqo_4W%>&UC77$A>*&23CEc)J>5B{f-N1ga zs4o-TrQYf|#v$+a_&y@cDNMy=#~Nwrc84DOdTdHZ(0#_bkR{1%RJcsNND1R{-+0RF z8>J$-9v`C0g>fz{MGsNrJzMLkDFw)@_*jCKgmG@WrQg`ce2LRrNgP(2-Ak}T^d4ZoMs z5i|S1%yfk<AZ&?7236_D^+Q*63iOmZ&fc&&*&VHJAG{@D(fSTMhm zDWfX&i61TI-xaJ<3j4hk;#YKvAk%B%G$B);&3~bEzS1d!v;}ZY=Rl;AYC0$2HpD$T zPDxKh)KViSFn?B}gMO{4pog8ojS4?oo1K-Qc)b_ z?!>U}oXkrghb7(7*O7r?V?XzwL{=c5uf#Q?Bsf2qzo2`#Eupp74d?%f08-dwbs6zeWr~jIs2S} zvOhTFGR<`6o3++nd+h@di9{liNF)-8L?V$$BywLNpd@48Z7-@UTM$Bk0@3cCIe!s` z$oOYJEDEI-AWJ}GNxL(9vefG-C8*KczLKeI0fhfF3MEa>zz}F&zq5*BBxC}F!q=@CzeJM^inLICAQUVHV4$JEE|DAR&mXkvSsX^T{6PW)Qhq9|jJ__XMUME<(HCJ@x zkQ`y^4u8*AI5|WRK}7fL6iZSj#TUK4IDDesyF?EN362 z024XpdFgxe+*puLN(fAFK}7fLgj9W%vKS-Et}HqoP}0t=LrZF|ey^8uPAU6*kY6Mx zaO7Aj5kz#KMM)W^8>N8~`Q^G?OtU$^lM zx47%I`z4m3`$a3@LXC$pex^RQn^npt$^ZTL6As&PwB4_}q5`XLfJ`TkF}OR7~+^G1(P9|pG%E{c>&sS$J& zLa!CjBUK?Mmwv8QQf-1*PG8Fr-tOK-4ow2*PD$={aOL6?3GWC;*wqcUC6n*`Qk$e& z1hE_%u)B?F0=7|flfb#W3G7@xA+vpw1TMS@xa&8Ci zI&ud8uJh(o^f#$H@8WhKZy@P8r(gfQa!Hj6YOeM4>4VaPQt;lS+m+u#j%EApCvnqC zlE7Qt{Mv<)bm`AZB~>Pfxm9soVLti((d76{ zz42QcpQq(cPu*tLtcC*GUwrcL3X_J5Ib=56Zc4d^tW-ul>P8xBLSrK90xV7)cKWiKHMAt{jbSk^xt!6T(C z73M|>d%uLfH9*nNLDH?1pz#~GuL#L5rGk4qfpR=|R|qf>gBAHRO*Di-4w z=(Jkyyn1>Gh6r}lSMft?@mo{b^7^$$mLL*|L?V$$Boc{4B9TZWV)+*`E89dYJZh`} O0000` (5개 루트 화면 전부 동일) /// /// 라벨 문자열과 순서는 스모크 테스트가 정확 일치로 검증한다. -/// 아이콘은 아직 확정 자산이 없어 Material 기본 아이콘을 쓴다(README 「미투입 항목」). +/// 아이콘은 Figma Iconset(house · message-square · badge · circle-user-round)이다. +/// STATION 은 Figma 하단 탭에 없어(1차 비노출) Material 아이콘을 둔다. /// /// **이 목록은 계약이지 화면이 아니다.** 지금 하단 탭에 실제로 그려지는 것은 /// [visibleTabs] 이고 STATION 은 원격 플래그가 켜질 때까지 빠진다 — 목록에서 /// 지우면 `design/craft-allowlist.yaml` 과 어긋나고, 켜려면 재배포가 필요해진다. const List appTabs = [ - AppTab(label: 'HOME', path: '/home', icon: Icons.home_outlined), - AppTab(label: 'INSIDE', path: '/inside', icon: Icons.chat_bubble_outline), - AppTab(label: 'GALAXY', path: '/galaxy', icon: Icons.auto_awesome_outlined), + AppTab(label: 'HOME', path: '/home', icon: ImageIcon(AppIcons.house)), + AppTab( + label: 'INSIDE', + path: '/inside', + icon: ImageIcon(AppIcons.messageSquare), + ), + AppTab(label: 'GALAXY', path: '/galaxy', icon: ImageIcon(AppIcons.badge)), // 별첨 B v1.4 — *1차에서는 사용자 탭과 Coming Soon 을 모두 비노출하고 // Route/데이터 구조만 유지해 향후 앱 재배포 없이 활성화합니다.* AppTab( label: 'STATION', path: '/station', - icon: Icons.storefront_outlined, + icon: Icon(Icons.storefront_outlined), gatedBy: _stationGate, ), - AppTab(label: 'MY', path: '/my', icon: Icons.account_circle_outlined), + AppTab(label: 'MY', path: '/my', icon: ImageIcon(AppIcons.circleUserRound)), ]; bool _stationGate(FeatureFlags flags) => flags.stationTab; @@ -159,18 +168,25 @@ class AppShell extends StatelessWidget { final selected = branches.indexOf(shell.currentIndex); return Scaffold( body: shell, - bottomNavigationBar: NavigationBar( - // 숨긴 탭에 머무는 동안(딥링크 등)에는 선택 표시를 첫 탭으로 둔다. - selectedIndex: selected < 0 ? 0 : selected, - onDestinationSelected: (i) => shell.goBranch( - branches[i], - // initialLocation: true 를 주면 이미 열려 있는 탭을 다시 누를 때 루트로 되돌아간다. - initialLocation: branches[i] == shell.currentIndex, + // Figma 탭 줄은 좌우 16 을 비우고 칸을 나눈다(350:215853). + bottomNavigationBar: ColoredBox( + color: AppColors.cosmicVoid, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: NavigationBar( + // 숨긴 탭에 머무는 동안(딥링크 등)에는 선택 표시를 첫 탭으로 둔다. + selectedIndex: selected < 0 ? 0 : selected, + onDestinationSelected: (i) => shell.goBranch( + branches[i], + // initialLocation: true 를 주면 이미 열려 있는 탭을 다시 누를 때 루트로 되돌아간다. + initialLocation: branches[i] == shell.currentIndex, + ), + destinations: [ + for (final tab in tabs) + NavigationDestination(icon: tab.icon, label: tab.label), + ], + ), ), - destinations: [ - for (final tab in tabs) - NavigationDestination(icon: Icon(tab.icon), label: tab.label), - ], ), ); }, diff --git a/app/lib/core/theme/app_theme.dart b/app/lib/core/theme/app_theme.dart index cc3aa37..8f0c67d 100644 --- a/app/lib/core/theme/app_theme.dart +++ b/app/lib/core/theme/app_theme.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../ui/app_icons.dart'; import 'tokens.dart'; /// design-tokens.md → ThemeData 조립. @@ -43,21 +44,36 @@ ThemeData buildAppTheme() { return ThemeData( useMaterial3: true, colorScheme: scheme, + // 글꼴을 지정하지 않은 글자도 Figma 본문 글꼴(42dot Sans)로 그린다. + fontFamily: AppFonts.body, + // Figma Iconset(Lucide) — 뒤로가기 chevron-left · 닫기 x. + actionIconTheme: ActionIconThemeData( + backButtonIconBuilder: (_) => const ImageIcon(AppIcons.chevronLeft, size: 24), + closeButtonIconBuilder: (_) => const ImageIcon(AppIcons.x, size: 24), + ), scaffoldBackgroundColor: AppColors.cosmicVoid, textTheme: _textTheme(scheme), - appBarTheme: const AppBarTheme( + // Figma Top Navigation(알림 229:11265 · 설정 354:222008 등) — 높이 56, 바탕 #141414, + // 제목 42dot Sans 16/600 가운데, 아이콘 24 를 56×56 칸에. + appBarTheme: AppBarTheme( backgroundColor: AppColors.cosmicVoid, + foregroundColor: AppColors.onCosmic, surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, centerTitle: true, + toolbarHeight: 56, + titleTextStyle: _style(AppType.navTitle, AppColors.onCosmic), + iconTheme: const IconThemeData(color: AppColors.onCosmic, size: 24), + actionsIconTheme: const IconThemeData(color: AppColors.onCosmic, size: 24), ), // 시안 Bottom Navigation(350:215853) — 바탕 #141414, 알약 표시자 없음, 아이콘 24, - // 라벨 ABeeZee 10. 활성은 흰색, 비활성은 #777777. + // 라벨 ABeeZee 10/16. 활성은 흰색, 비활성은 #777777. 탭 칸 높이 64(홈 인디케이터 34 별도). navigationBarTheme: NavigationBarThemeData( backgroundColor: AppColors.cosmicVoid, surfaceTintColor: Colors.transparent, indicatorColor: Colors.transparent, - // 탭 항목 높이는 touch-min-app(48) 아래로 내리지 않는다. - height: AppSizing.touchMinApp + AppSpace.s5, + height: 64, iconTheme: WidgetStateProperty.resolveWith( (states) => IconThemeData( size: 24, @@ -67,11 +83,9 @@ ThemeData buildAppTheme() { ), ), labelTextStyle: WidgetStateProperty.resolveWith( - (states) => TextStyle( - fontFamily: AppFonts.display, - fontSize: 10, - letterSpacing: 0.2, - color: states.contains(WidgetState.selected) + (states) => _style( + AppType.navLabel, + states.contains(WidgetState.selected) ? AppColors.onCosmic : AppColors.onCosmicMuted, ), @@ -81,30 +95,30 @@ ThemeData buildAppTheme() { // ── 아래는 테마를 주지 않으면 Material 3 기본값(보라 시드)으로 칠해지는 것들이다. // 코스믹 다크 위에서 눈에 띄게 어긋나므로 전부 토큰으로 고정한다. + // Figma 정렬 칩(Inside_최신순 196:2673) — 바탕 #1a1a1c · 테두리 #232324 · 모서리 8 · + // 안쪽 6/10 · 글자 12/500. 선택은 흰 바탕에 검은 글자다. chipTheme: ChipThemeData( backgroundColor: AppColors.cosmicSurfaceContainer, - selectedColor: AppColors.cosmicSurfaceRaised, - // 선택 상태는 색면이 아니라 테두리와 글자색으로 알린다 — 악센트는 행동에만 쓴다. - side: const BorderSide(color: AppColors.cosmicOutline), + selectedColor: AppColors.onCosmic, + side: const BorderSide(color: AppColors.cosmicSurfaceRaised), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.full), + borderRadius: BorderRadius.circular(AppRadius.sm), ), - labelStyle: _style(AppType.labelL, AppColors.onCosmicVariant), - secondaryLabelStyle: _style(AppType.labelL, AppColors.cosmicAccent), - checkmarkColor: AppColors.cosmicAccent, + labelStyle: _style(AppType.caption, const Color(0xFFD5D7D9)), + secondaryLabelStyle: _style(AppType.caption, AppColors.onCosmicAccent), + checkmarkColor: AppColors.onCosmicAccent, showCheckmark: false, - padding: const EdgeInsets.symmetric( - horizontal: AppSpace.s3, - vertical: AppSpace.s2, - ), + labelPadding: EdgeInsets.zero, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), ), cardTheme: CardThemeData( color: AppColors.cosmicSurfaceContainer, surfaceTintColor: Colors.transparent, elevation: 0, margin: EdgeInsets.zero, + // Figma 카드(MY 373:632962 등 36곳) — 모서리 16. shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.md), + borderRadius: BorderRadius.circular(AppRadius.lg), ), ), floatingActionButtonTheme: FloatingActionButtonThemeData( @@ -137,18 +151,20 @@ ThemeData buildAppTheme() { linearTrackColor: AppColors.cosmicSurfaceRaised, circularTrackColor: Colors.transparent, ), + // Figma 액션 시트(343:212939) #2C2C2E · 모서리 16. bottomSheetTheme: BottomSheetThemeData( - backgroundColor: AppColors.cosmicSurfaceContainer, + backgroundColor: AppColors.cosmicSheet, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.lg)), ), ), + // Figma 확인 팝업(차단하기 373:442417) #2D2D2D · 모서리 12. dialogTheme: DialogThemeData( - backgroundColor: AppColors.cosmicSurfaceContainer, + backgroundColor: AppColors.cosmicDialog, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.lg), + borderRadius: BorderRadius.circular(AppRadius.md), ), ), snackBarTheme: SnackBarThemeData( @@ -174,34 +190,41 @@ ThemeData buildAppTheme() { style: OutlinedButton.styleFrom( foregroundColor: AppColors.onCosmic, side: const BorderSide(color: AppColors.cosmicOutline), - minimumSize: const Size.fromHeight(AppSizing.touchMinApp), + minimumSize: const Size.fromHeight(_buttonHeight), + padding: _buttonPadding, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.md), + borderRadius: BorderRadius.circular(AppRadius.sm), ), - textStyle: _styleNoColor(AppType.titleM), + textStyle: _styleNoColor(AppType.navTitle), ), ), iconTheme: const IconThemeData(color: AppColors.onCosmicVariant), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight(AppSizing.touchMinApp), + minimumSize: const Size.fromHeight(_buttonHeight), + padding: _buttonPadding, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.md), + borderRadius: BorderRadius.circular(AppRadius.sm), ), // 글자색은 여기서 정하지 않는다 — foregroundColor 가 결정한다. // textStyle 에 색을 넣으면 소셜 버튼처럼 제공자별 색을 쓰는 곳과 지정이 두 곳으로 갈린다. - textStyle: _styleNoColor(AppType.titleM), + textStyle: _styleNoColor(AppType.navTitle), ), ), ); } +/// Figma 주 버튼(퀘스트 상세 264:48835 등) — 343×56, 모서리 8, 안쪽 16/24, 글자 16/600. +const _buttonHeight = 56.0; +const _buttonPadding = EdgeInsets.symmetric(horizontal: 24, vertical: 16); + TextTheme _textTheme(ColorScheme scheme) => TextTheme( displayLarge: _style(AppType.displayL, scheme.onSurface), displayMedium: _style(AppType.displayM, scheme.onSurface), headlineSmall: _style(AppType.titleL, scheme.onSurface), titleLarge: _style(AppType.titleL, scheme.onSurface), titleMedium: _style(AppType.titleM, scheme.onSurface), + titleSmall: _style(AppType.labelL, scheme.onSurface), bodyLarge: _style(AppType.bodyL, scheme.onSurface), bodyMedium: _style(AppType.bodyM, scheme.onSurface), bodySmall: _style(AppType.bodyS, scheme.onSurfaceVariant), @@ -219,6 +242,8 @@ TextStyle _style(TypeToken t, Color color) => _styleNoColor(t).copyWith(color: c /// 색 없는 타입 스타일. 색을 다른 속성(foregroundColor 등)이 정하는 자리에 쓴다. TextStyle _styleNoColor(TypeToken t) => TextStyle( fontFamily: t.family, + // ABeeZee 는 라틴 전용이다. 한글은 본문 글꼴로 이어 그린다(Figma 도 같은 대체를 한다). + fontFamilyFallback: const [AppFonts.body], fontSize: t.size, fontWeight: _weight(t.weight), height: t.height, diff --git a/app/lib/core/theme/tokens.dart b/app/lib/core/theme/tokens.dart index be9dd89..4b04b21 100644 --- a/app/lib/core/theme/tokens.dart +++ b/app/lib/core/theme/tokens.dart @@ -31,6 +31,8 @@ abstract final class AppColors { static const onCosmic = Color(0xFFFFFFFF); // color-on-cosmic #FFFFFF static const onCosmicVariant = Color(0xFFA3A2A0); // color-on-cosmic-variant #A3A2A0 static const onCosmicMuted = Color(0xFF767676); // color-on-cosmic-muted #767676 + static const cosmicSheet = Color(0xFF2C2C2E); // color-cosmic-sheet #2C2C2E + static const cosmicDialog = Color(0xFF2D2D2D); // color-cosmic-dialog #2D2D2D static const cosmicAccent = Color(0xFFFF7DDD); // color-cosmic-accent #FF7DDD static const onCosmicAccent = Color(0xFF141414); // color-on-cosmic-accent #141414 static const neonCyan = Color(0xFF64EFF9); // color-neon-cyan #64EFF9 @@ -153,18 +155,22 @@ abstract final class AppFonts { /// 타입 스케일. 값은 design-tokens.md 의 type 블록 그대로다. abstract final class AppType { - static const displayL = TypeToken(family: AppFonts.display, size: 44.0, weight: 700, height: 1.06, letterSpacing: -0.44); // type-display-l - static const displayM = TypeToken(family: AppFonts.display, size: 32.0, weight: 700, height: 1.2, letterSpacing: -0.224); // type-display-m - static const titleL = TypeToken(family: AppFonts.display, size: 24.0, weight: 700, height: 1.3, letterSpacing: 0.0); // type-title-l - static const titleM = TypeToken(family: AppFonts.body, size: 18.0, weight: 600, height: 1.3); // type-title-m - static const bodyL = TypeToken(family: AppFonts.body, size: 18.0, weight: 400, height: 1.6); // type-body-l - static const bodyM = TypeToken(family: AppFonts.body, size: 16.0, weight: 400, height: 1.6); // type-body-m - static const bodyS = TypeToken(family: AppFonts.body, size: 14.0, weight: 400, height: 1.55); // type-body-s - static const labelL = TypeToken(family: AppFonts.body, size: 14.0, weight: 500, height: 1.2); // type-label-l + static const displayL = TypeToken(family: AppFonts.display, size: 46.0, weight: 400, height: 1.0); // type-display-l + static const displayM = TypeToken(family: AppFonts.display, size: 32.0, weight: 400, height: 1.4, letterSpacing: -0.8); // type-display-m + static const titleL = TypeToken(family: AppFonts.body, size: 20.0, weight: 600, height: 1.4, letterSpacing: -0.5); // type-title-l + static const titleM = TypeToken(family: AppFonts.body, size: 16.0, weight: 600, height: 1.4, letterSpacing: -0.4); // type-title-m + static const bodyL = TypeToken(family: AppFonts.body, size: 16.0, weight: 500, height: 1.4, letterSpacing: -0.4); // type-body-l + static const bodyM = TypeToken(family: AppFonts.body, size: 14.0, weight: 500, height: 1.3, letterSpacing: -0.35); // type-body-m + static const bodyS = TypeToken(family: AppFonts.body, size: 12.0, weight: 500, height: 1.3, letterSpacing: -0.3); // type-body-s + static const labelL = TypeToken(family: AppFonts.body, size: 14.0, weight: 600, height: 1.3, letterSpacing: -0.35); // type-label-l static const labelMobile = TypeToken(family: AppFonts.body, size: 16.0, weight: 700, height: 1.4); // type-label-mobile static const metaMobile = TypeToken(family: AppFonts.body, size: 13.0, weight: 600, height: 1.4); // type-meta-mobile - static const eyebrow = TypeToken(family: AppFonts.body, size: 12.0, weight: 600, letterSpacing: 0.48); // type-eyebrow - static const caption = TypeToken(family: AppFonts.body, size: 12.0, weight: 500, height: 1.4); // type-caption + static const eyebrow = TypeToken(family: AppFonts.body, size: 12.0, weight: 600, height: 1.0, letterSpacing: -0.3); // type-eyebrow + static const caption = TypeToken(family: AppFonts.body, size: 12.0, weight: 500, height: 1.3, letterSpacing: -0.3); // type-caption + static const meta = TypeToken(family: AppFonts.body, size: 10.0, weight: 500, height: 1.3, letterSpacing: -0.25); // type-meta + static const navTitle = TypeToken(family: AppFonts.body, size: 16.0, weight: 600, height: 1.5); // type-nav-title + static const tabTitle = TypeToken(family: AppFonts.display, size: 20.0, weight: 400, height: 1.4); // type-tab-title + static const navLabel = TypeToken(family: AppFonts.display, size: 10.0, weight: 400, height: 1.6); // type-nav-label // type-mono-num: {'family': 'font-mono', 'feature': 'tnum', 'note': '가격·지표·표는 tabular'} (크기 없는 지시자 — TextStyle 로 못 옮긴다) } diff --git a/app/lib/core/ui/app_icons.dart b/app/lib/core/ui/app_icons.dart new file mode 100644 index 0000000..0c969a3 --- /dev/null +++ b/app/lib/core/ui/app_icons.dart @@ -0,0 +1,22 @@ +import 'package:flutter/widgets.dart'; + +/// Figma Iconset(Lucide 계열) 아이콘. 03_Wireframe 인스턴스를 figma-cli 로 3배 PNG 로 뽑았다 +/// (2026-09-26, `_작업/design-match/figma/assets.json`). 색은 [ImageIcon] 이 IconTheme 으로 입힌다. +abstract final class AppIcons { + static const house = AssetImage('assets/icons/house.png'); + static const messageSquare = AssetImage('assets/icons/message_square.png'); + static const badge = AssetImage('assets/icons/badge.png'); + static const circleUserRound = AssetImage( + 'assets/icons/circle_user_round.png', + ); + static const chevronLeft = AssetImage('assets/icons/chevron_left.png'); + static const chevronRight = AssetImage('assets/icons/chevron_right.png'); + static const ellipsisVertical = AssetImage( + 'assets/icons/ellipsis_vertical.png', + ); + static const settings = AssetImage('assets/icons/settings.png'); + static const plus = AssetImage('assets/icons/plus.png'); + static const x = AssetImage('assets/icons/x.png'); + static const search = AssetImage('assets/icons/search.png'); + static const share = AssetImage('assets/icons/share_2.png'); +} diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 57511ed..eb62449 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -73,3 +73,7 @@ flutter: - family: ABeeZee fonts: - asset: assets/fonts/ABeeZee-Regular.ttf + + assets: + # Figma Iconset 아이콘(lib/core/ui/app_icons.dart). + - assets/icons/ diff --git a/app/tool/gen_tokens.py b/app/tool/gen_tokens.py index 529747b..771e8ed 100644 --- a/app/tool/gen_tokens.py +++ b/app/tool/gen_tokens.py @@ -179,7 +179,7 @@ def build(tok: dict) -> str: parts.append(f"height: {num(v['leading'])}") if "tracking" in v: em = float(str(v["tracking"]).replace("em", "")) - parts.append(f"letterSpacing: {em * float(v['size'])}") # em → 논리픽셀 + parts.append(f"letterSpacing: {round(em * float(v['size']), 3):g}") # em → 논리픽셀 lines.append(f"static const {camel(k, 'type-')} = TypeToken({', '.join(parts)}); // {k}") out.append(emit_class("AppType", "타입 스케일. 값은 design-tokens.md 의 type 블록 그대로다.", lines)) diff --git a/design/design-tokens.md b/design/design-tokens.md index 7301813..2afe0ee 100644 --- a/design/design-tokens.md +++ b/design/design-tokens.md @@ -103,6 +103,9 @@ colors: color-on-cosmic: "#FFFFFF" color-on-cosmic-variant: "#A3A2A0" color-on-cosmic-muted: "#767676" + # 오버레이 — Figma 실측(2026-09-26). 액션 시트 #2C2C2E(홈_초기사용자 343:212939) · 확인 팝업 #2D2D2D(차단하기 373:442417). + color-cosmic-sheet: "#2C2C2E" + color-cosmic-dialog: "#2D2D2D" # 악센트 — 시안 실측. 핑크가 선택·CTA, 초록이 POP·성공, 시안이 FPI·탭 활성, 블루가 정보 칩. color-cosmic-accent: "#FF7DDD" color-on-cosmic-accent: "#141414" @@ -170,18 +173,26 @@ font: font-direction: "geometric" font-source: "42dot Sans = Google Fonts OFL 가변 ttf · ABeeZee = Google Fonts OFL (app/assets/fonts/, 2026-09-02)" type: - type-display-l: {family: font-display, size: 44, weight: 700, tracking: -0.01em, leading: 1.06} - type-display-m: {family: font-display, size: 32, weight: 700, tracking: -0.007em, leading: 1.2} - type-title-l: {family: font-display, size: 24, weight: 700, tracking: 0, leading: 1.3} - type-title-m: {family: font-body, size: 18, weight: 600, leading: 1.3} - type-body-l: {family: font-body, size: 18, weight: 400, leading: 1.6} - type-body-m: {family: font-body, size: 16, weight: 400, leading: 1.6} - type-body-s: {family: font-body, size: 14, weight: 400, leading: 1.55} - type-label-l: {family: font-body, size: 14, weight: 500, leading: 1.2} + # --- 발주사 Figma 계약 시점 사본 실측 (2026-09-26, _작업/design-match/foundation.json · 앱 93화면 텍스트 1,868건 집계) --- + # 42dot Sans 가 81%, ABeeZee 가 14%(Regular 한 벌뿐이라 굵기는 400). 본문 자간은 -2.5% 가 표준이다. + # 종전 값(Gmarket/포카 온 스테이지 재설계 — 44·32 Bold, 본문 400·행간 1.6)은 Figma 와 맞지 않아 교체한다. + type-display-l: {family: font-display, size: 46, weight: 400, leading: 1.0} + type-display-m: {family: font-display, size: 32, weight: 400, tracking: -0.025em, leading: 1.4} + type-title-l: {family: font-body, size: 20, weight: 600, tracking: -0.025em, leading: 1.4} + type-title-m: {family: font-body, size: 16, weight: 600, tracking: -0.025em, leading: 1.4} + type-body-l: {family: font-body, size: 16, weight: 500, tracking: -0.025em, leading: 1.4} + type-body-m: {family: font-body, size: 14, weight: 500, tracking: -0.025em, leading: 1.3} + type-body-s: {family: font-body, size: 12, weight: 500, tracking: -0.025em, leading: 1.3} + type-label-l: {family: font-body, size: 14, weight: 600, tracking: -0.025em, leading: 1.3} type-label-mobile: {family: font-body, size: 16, weight: 700, leading: 1.4} type-meta-mobile: {family: font-body, size: 13, weight: 600, leading: 1.4} - type-eyebrow: {family: font-body, size: 12, weight: 600, tracking: 0.04em} - type-caption: {family: font-body, size: 12, weight: 500, leading: 1.4} + type-eyebrow: {family: font-body, size: 12, weight: 600, tracking: -0.025em, leading: 1.0} + type-caption: {family: font-body, size: 12, weight: 500, tracking: -0.025em, leading: 1.3} + type-meta: {family: font-body, size: 10, weight: 500, tracking: -0.025em, leading: 1.3} + # 공통 크롬 — Top Navigation 제목(229:12006 등 39화면) · 탭 루트 제목(MY/GALAXY/INSIDE) · Bottom Navigation 라벨(350:215853) + type-nav-title: {family: font-body, size: 16, weight: 600, leading: 1.5} + type-tab-title: {family: font-display, size: 20, weight: 400, leading: 1.4} + type-nav-label: {family: font-display, size: 10, weight: 400, leading: 1.6} type-mono-num: {family: font-mono, feature: 'tnum', note: '가격·지표·표는 tabular'} sizing: touch-min-app: 48