타입 스케일·공통 크롬을 Figma 기준으로 정렬 — 상단 내비게이션 · 하단 탭 아이콘 · 버튼 · 시트
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 334 B |
|
After Width: | Height: | Size: 551 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 378 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 620 B |
|
After Width: | Height: | Size: 380 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 149 B |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../config/feature_flags.dart';
|
import '../config/feature_flags.dart';
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
import '../ui/app_icons.dart';
|
||||||
import '../../features/auth/auth_repository.dart';
|
import '../../features/auth/auth_repository.dart';
|
||||||
import '../../features/auth/login_screen.dart';
|
import '../../features/auth/login_screen.dart';
|
||||||
import '../../features/galaxy/galaxy_screen.dart';
|
import '../../features/galaxy/galaxy_screen.dart';
|
||||||
@@ -22,7 +24,9 @@ class AppTab {
|
|||||||
/// **계약 고정 문자열.** 한글화·치환·순서 변경 금지.
|
/// **계약 고정 문자열.** 한글화·치환·순서 변경 금지.
|
||||||
final String label;
|
final String label;
|
||||||
final String path;
|
final String path;
|
||||||
final IconData icon;
|
|
||||||
|
/// Figma Bottom Navigation 아이콘. 색은 NavigationBar 테마가 입힌다.
|
||||||
|
final Widget icon;
|
||||||
|
|
||||||
/// 이 탭을 여는 원격 플래그. null 이면 항상 보인다.
|
/// 이 탭을 여는 원격 플래그. null 이면 항상 보인다.
|
||||||
///
|
///
|
||||||
@@ -39,24 +43,29 @@ class AppTab {
|
|||||||
/// - 시안 `시안/screens/S-*.html` 의 `<nav class="tabbar">` (5개 루트 화면 전부 동일)
|
/// - 시안 `시안/screens/S-*.html` 의 `<nav class="tabbar">` (5개 루트 화면 전부 동일)
|
||||||
///
|
///
|
||||||
/// 라벨 문자열과 순서는 스모크 테스트가 정확 일치로 검증한다.
|
/// 라벨 문자열과 순서는 스모크 테스트가 정확 일치로 검증한다.
|
||||||
/// 아이콘은 아직 확정 자산이 없어 Material 기본 아이콘을 쓴다(README 「미투입 항목」).
|
/// 아이콘은 Figma Iconset(house · message-square · badge · circle-user-round)이다.
|
||||||
|
/// STATION 은 Figma 하단 탭에 없어(1차 비노출) Material 아이콘을 둔다.
|
||||||
///
|
///
|
||||||
/// **이 목록은 계약이지 화면이 아니다.** 지금 하단 탭에 실제로 그려지는 것은
|
/// **이 목록은 계약이지 화면이 아니다.** 지금 하단 탭에 실제로 그려지는 것은
|
||||||
/// [visibleTabs] 이고 STATION 은 원격 플래그가 켜질 때까지 빠진다 — 목록에서
|
/// [visibleTabs] 이고 STATION 은 원격 플래그가 켜질 때까지 빠진다 — 목록에서
|
||||||
/// 지우면 `design/craft-allowlist.yaml` 과 어긋나고, 켜려면 재배포가 필요해진다.
|
/// 지우면 `design/craft-allowlist.yaml` 과 어긋나고, 켜려면 재배포가 필요해진다.
|
||||||
const List<AppTab> appTabs = [
|
const List<AppTab> appTabs = [
|
||||||
AppTab(label: 'HOME', path: '/home', icon: Icons.home_outlined),
|
AppTab(label: 'HOME', path: '/home', icon: ImageIcon(AppIcons.house)),
|
||||||
AppTab(label: 'INSIDE', path: '/inside', icon: Icons.chat_bubble_outline),
|
AppTab(
|
||||||
AppTab(label: 'GALAXY', path: '/galaxy', icon: Icons.auto_awesome_outlined),
|
label: 'INSIDE',
|
||||||
|
path: '/inside',
|
||||||
|
icon: ImageIcon(AppIcons.messageSquare),
|
||||||
|
),
|
||||||
|
AppTab(label: 'GALAXY', path: '/galaxy', icon: ImageIcon(AppIcons.badge)),
|
||||||
// 별첨 B v1.4 — *1차에서는 사용자 탭과 Coming Soon 을 모두 비노출하고
|
// 별첨 B v1.4 — *1차에서는 사용자 탭과 Coming Soon 을 모두 비노출하고
|
||||||
// Route/데이터 구조만 유지해 향후 앱 재배포 없이 활성화합니다.*
|
// Route/데이터 구조만 유지해 향후 앱 재배포 없이 활성화합니다.*
|
||||||
AppTab(
|
AppTab(
|
||||||
label: 'STATION',
|
label: 'STATION',
|
||||||
path: '/station',
|
path: '/station',
|
||||||
icon: Icons.storefront_outlined,
|
icon: Icon(Icons.storefront_outlined),
|
||||||
gatedBy: _stationGate,
|
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;
|
bool _stationGate(FeatureFlags flags) => flags.stationTab;
|
||||||
@@ -159,18 +168,25 @@ class AppShell extends StatelessWidget {
|
|||||||
final selected = branches.indexOf(shell.currentIndex);
|
final selected = branches.indexOf(shell.currentIndex);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: shell,
|
body: shell,
|
||||||
bottomNavigationBar: NavigationBar(
|
// Figma 탭 줄은 좌우 16 을 비우고 칸을 나눈다(350:215853).
|
||||||
// 숨긴 탭에 머무는 동안(딥링크 등)에는 선택 표시를 첫 탭으로 둔다.
|
bottomNavigationBar: ColoredBox(
|
||||||
selectedIndex: selected < 0 ? 0 : selected,
|
color: AppColors.cosmicVoid,
|
||||||
onDestinationSelected: (i) => shell.goBranch(
|
child: Padding(
|
||||||
branches[i],
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
// initialLocation: true 를 주면 이미 열려 있는 탭을 다시 누를 때 루트로 되돌아간다.
|
child: NavigationBar(
|
||||||
initialLocation: branches[i] == shell.currentIndex,
|
// 숨긴 탭에 머무는 동안(딥링크 등)에는 선택 표시를 첫 탭으로 둔다.
|
||||||
|
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),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../ui/app_icons.dart';
|
||||||
import 'tokens.dart';
|
import 'tokens.dart';
|
||||||
|
|
||||||
/// design-tokens.md → ThemeData 조립.
|
/// design-tokens.md → ThemeData 조립.
|
||||||
@@ -43,21 +44,36 @@ ThemeData buildAppTheme() {
|
|||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: scheme,
|
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,
|
scaffoldBackgroundColor: AppColors.cosmicVoid,
|
||||||
textTheme: _textTheme(scheme),
|
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,
|
backgroundColor: AppColors.cosmicVoid,
|
||||||
|
foregroundColor: AppColors.onCosmic,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
centerTitle: true,
|
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,
|
// 시안 Bottom Navigation(350:215853) — 바탕 #141414, 알약 표시자 없음, 아이콘 24,
|
||||||
// 라벨 ABeeZee 10. 활성은 흰색, 비활성은 #777777.
|
// 라벨 ABeeZee 10/16. 활성은 흰색, 비활성은 #777777. 탭 칸 높이 64(홈 인디케이터 34 별도).
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: AppColors.cosmicVoid,
|
backgroundColor: AppColors.cosmicVoid,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
indicatorColor: Colors.transparent,
|
indicatorColor: Colors.transparent,
|
||||||
// 탭 항목 높이는 touch-min-app(48) 아래로 내리지 않는다.
|
height: 64,
|
||||||
height: AppSizing.touchMinApp + AppSpace.s5,
|
|
||||||
iconTheme: WidgetStateProperty.resolveWith(
|
iconTheme: WidgetStateProperty.resolveWith(
|
||||||
(states) => IconThemeData(
|
(states) => IconThemeData(
|
||||||
size: 24,
|
size: 24,
|
||||||
@@ -67,11 +83,9 @@ ThemeData buildAppTheme() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
labelTextStyle: WidgetStateProperty.resolveWith(
|
labelTextStyle: WidgetStateProperty.resolveWith(
|
||||||
(states) => TextStyle(
|
(states) => _style(
|
||||||
fontFamily: AppFonts.display,
|
AppType.navLabel,
|
||||||
fontSize: 10,
|
states.contains(WidgetState.selected)
|
||||||
letterSpacing: 0.2,
|
|
||||||
color: states.contains(WidgetState.selected)
|
|
||||||
? AppColors.onCosmic
|
? AppColors.onCosmic
|
||||||
: AppColors.onCosmicMuted,
|
: AppColors.onCosmicMuted,
|
||||||
),
|
),
|
||||||
@@ -81,30 +95,30 @@ ThemeData buildAppTheme() {
|
|||||||
|
|
||||||
// ── 아래는 테마를 주지 않으면 Material 3 기본값(보라 시드)으로 칠해지는 것들이다.
|
// ── 아래는 테마를 주지 않으면 Material 3 기본값(보라 시드)으로 칠해지는 것들이다.
|
||||||
// 코스믹 다크 위에서 눈에 띄게 어긋나므로 전부 토큰으로 고정한다.
|
// 코스믹 다크 위에서 눈에 띄게 어긋나므로 전부 토큰으로 고정한다.
|
||||||
|
// Figma 정렬 칩(Inside_최신순 196:2673) — 바탕 #1a1a1c · 테두리 #232324 · 모서리 8 ·
|
||||||
|
// 안쪽 6/10 · 글자 12/500. 선택은 흰 바탕에 검은 글자다.
|
||||||
chipTheme: ChipThemeData(
|
chipTheme: ChipThemeData(
|
||||||
backgroundColor: AppColors.cosmicSurfaceContainer,
|
backgroundColor: AppColors.cosmicSurfaceContainer,
|
||||||
selectedColor: AppColors.cosmicSurfaceRaised,
|
selectedColor: AppColors.onCosmic,
|
||||||
// 선택 상태는 색면이 아니라 테두리와 글자색으로 알린다 — 악센트는 행동에만 쓴다.
|
side: const BorderSide(color: AppColors.cosmicSurfaceRaised),
|
||||||
side: const BorderSide(color: AppColors.cosmicOutline),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||||
),
|
),
|
||||||
labelStyle: _style(AppType.labelL, AppColors.onCosmicVariant),
|
labelStyle: _style(AppType.caption, const Color(0xFFD5D7D9)),
|
||||||
secondaryLabelStyle: _style(AppType.labelL, AppColors.cosmicAccent),
|
secondaryLabelStyle: _style(AppType.caption, AppColors.onCosmicAccent),
|
||||||
checkmarkColor: AppColors.cosmicAccent,
|
checkmarkColor: AppColors.onCosmicAccent,
|
||||||
showCheckmark: false,
|
showCheckmark: false,
|
||||||
padding: const EdgeInsets.symmetric(
|
labelPadding: EdgeInsets.zero,
|
||||||
horizontal: AppSpace.s3,
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
vertical: AppSpace.s2,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
cardTheme: CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
color: AppColors.cosmicSurfaceContainer,
|
color: AppColors.cosmicSurfaceContainer,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
|
// Figma 카드(MY 373:632962 등 36곳) — 모서리 16.
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
@@ -137,18 +151,20 @@ ThemeData buildAppTheme() {
|
|||||||
linearTrackColor: AppColors.cosmicSurfaceRaised,
|
linearTrackColor: AppColors.cosmicSurfaceRaised,
|
||||||
circularTrackColor: Colors.transparent,
|
circularTrackColor: Colors.transparent,
|
||||||
),
|
),
|
||||||
|
// Figma 액션 시트(343:212939) #2C2C2E · 모서리 16.
|
||||||
bottomSheetTheme: BottomSheetThemeData(
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
backgroundColor: AppColors.cosmicSurfaceContainer,
|
backgroundColor: AppColors.cosmicSheet,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.lg)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.lg)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Figma 확인 팝업(차단하기 373:442417) #2D2D2D · 모서리 12.
|
||||||
dialogTheme: DialogThemeData(
|
dialogTheme: DialogThemeData(
|
||||||
backgroundColor: AppColors.cosmicSurfaceContainer,
|
backgroundColor: AppColors.cosmicDialog,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
snackBarTheme: SnackBarThemeData(
|
snackBarTheme: SnackBarThemeData(
|
||||||
@@ -174,34 +190,41 @@ ThemeData buildAppTheme() {
|
|||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: AppColors.onCosmic,
|
foregroundColor: AppColors.onCosmic,
|
||||||
side: const BorderSide(color: AppColors.cosmicOutline),
|
side: const BorderSide(color: AppColors.cosmicOutline),
|
||||||
minimumSize: const Size.fromHeight(AppSizing.touchMinApp),
|
minimumSize: const Size.fromHeight(_buttonHeight),
|
||||||
|
padding: _buttonPadding,
|
||||||
shape: RoundedRectangleBorder(
|
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),
|
iconTheme: const IconThemeData(color: AppColors.onCosmicVariant),
|
||||||
filledButtonTheme: FilledButtonThemeData(
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(AppSizing.touchMinApp),
|
minimumSize: const Size.fromHeight(_buttonHeight),
|
||||||
|
padding: _buttonPadding,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||||
),
|
),
|
||||||
// 글자색은 여기서 정하지 않는다 — foregroundColor 가 결정한다.
|
// 글자색은 여기서 정하지 않는다 — foregroundColor 가 결정한다.
|
||||||
// textStyle 에 색을 넣으면 소셜 버튼처럼 제공자별 색을 쓰는 곳과 지정이 두 곳으로 갈린다.
|
// 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(
|
TextTheme _textTheme(ColorScheme scheme) => TextTheme(
|
||||||
displayLarge: _style(AppType.displayL, scheme.onSurface),
|
displayLarge: _style(AppType.displayL, scheme.onSurface),
|
||||||
displayMedium: _style(AppType.displayM, scheme.onSurface),
|
displayMedium: _style(AppType.displayM, scheme.onSurface),
|
||||||
headlineSmall: _style(AppType.titleL, scheme.onSurface),
|
headlineSmall: _style(AppType.titleL, scheme.onSurface),
|
||||||
titleLarge: _style(AppType.titleL, scheme.onSurface),
|
titleLarge: _style(AppType.titleL, scheme.onSurface),
|
||||||
titleMedium: _style(AppType.titleM, scheme.onSurface),
|
titleMedium: _style(AppType.titleM, scheme.onSurface),
|
||||||
|
titleSmall: _style(AppType.labelL, scheme.onSurface),
|
||||||
bodyLarge: _style(AppType.bodyL, scheme.onSurface),
|
bodyLarge: _style(AppType.bodyL, scheme.onSurface),
|
||||||
bodyMedium: _style(AppType.bodyM, scheme.onSurface),
|
bodyMedium: _style(AppType.bodyM, scheme.onSurface),
|
||||||
bodySmall: _style(AppType.bodyS, scheme.onSurfaceVariant),
|
bodySmall: _style(AppType.bodyS, scheme.onSurfaceVariant),
|
||||||
@@ -219,6 +242,8 @@ TextStyle _style(TypeToken t, Color color) => _styleNoColor(t).copyWith(color: c
|
|||||||
/// 색 없는 타입 스타일. 색을 다른 속성(foregroundColor 등)이 정하는 자리에 쓴다.
|
/// 색 없는 타입 스타일. 색을 다른 속성(foregroundColor 등)이 정하는 자리에 쓴다.
|
||||||
TextStyle _styleNoColor(TypeToken t) => TextStyle(
|
TextStyle _styleNoColor(TypeToken t) => TextStyle(
|
||||||
fontFamily: t.family,
|
fontFamily: t.family,
|
||||||
|
// ABeeZee 는 라틴 전용이다. 한글은 본문 글꼴로 이어 그린다(Figma 도 같은 대체를 한다).
|
||||||
|
fontFamilyFallback: const [AppFonts.body],
|
||||||
fontSize: t.size,
|
fontSize: t.size,
|
||||||
fontWeight: _weight(t.weight),
|
fontWeight: _weight(t.weight),
|
||||||
height: t.height,
|
height: t.height,
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ abstract final class AppColors {
|
|||||||
static const onCosmic = Color(0xFFFFFFFF); // color-on-cosmic #FFFFFF
|
static const onCosmic = Color(0xFFFFFFFF); // color-on-cosmic #FFFFFF
|
||||||
static const onCosmicVariant = Color(0xFFA3A2A0); // color-on-cosmic-variant #A3A2A0
|
static const onCosmicVariant = Color(0xFFA3A2A0); // color-on-cosmic-variant #A3A2A0
|
||||||
static const onCosmicMuted = Color(0xFF767676); // color-on-cosmic-muted #767676
|
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 cosmicAccent = Color(0xFFFF7DDD); // color-cosmic-accent #FF7DDD
|
||||||
static const onCosmicAccent = Color(0xFF141414); // color-on-cosmic-accent #141414
|
static const onCosmicAccent = Color(0xFF141414); // color-on-cosmic-accent #141414
|
||||||
static const neonCyan = Color(0xFF64EFF9); // color-neon-cyan #64EFF9
|
static const neonCyan = Color(0xFF64EFF9); // color-neon-cyan #64EFF9
|
||||||
@@ -153,18 +155,22 @@ abstract final class AppFonts {
|
|||||||
|
|
||||||
/// 타입 스케일. 값은 design-tokens.md 의 type 블록 그대로다.
|
/// 타입 스케일. 값은 design-tokens.md 의 type 블록 그대로다.
|
||||||
abstract final class AppType {
|
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 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: 700, height: 1.2, letterSpacing: -0.224); // type-display-m
|
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.display, size: 24.0, weight: 700, height: 1.3, letterSpacing: 0.0); // type-title-l
|
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: 18.0, weight: 600, height: 1.3); // type-title-m
|
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: 18.0, weight: 400, height: 1.6); // type-body-l
|
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: 16.0, weight: 400, height: 1.6); // type-body-m
|
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: 14.0, weight: 400, height: 1.55); // type-body-s
|
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: 500, height: 1.2); // type-label-l
|
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 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 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 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.4); // type-caption
|
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 로 못 옮긴다)
|
// type-mono-num: {'family': 'font-mono', 'feature': 'tnum', 'note': '가격·지표·표는 tabular'} (크기 없는 지시자 — TextStyle 로 못 옮긴다)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
@@ -73,3 +73,7 @@ flutter:
|
|||||||
- family: ABeeZee
|
- family: ABeeZee
|
||||||
fonts:
|
fonts:
|
||||||
- asset: assets/fonts/ABeeZee-Regular.ttf
|
- asset: assets/fonts/ABeeZee-Regular.ttf
|
||||||
|
|
||||||
|
assets:
|
||||||
|
# Figma Iconset 아이콘(lib/core/ui/app_icons.dart).
|
||||||
|
- assets/icons/
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ def build(tok: dict) -> str:
|
|||||||
parts.append(f"height: {num(v['leading'])}")
|
parts.append(f"height: {num(v['leading'])}")
|
||||||
if "tracking" in v:
|
if "tracking" in v:
|
||||||
em = float(str(v["tracking"]).replace("em", ""))
|
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}")
|
lines.append(f"static const {camel(k, 'type-')} = TypeToken({', '.join(parts)}); // {k}")
|
||||||
out.append(emit_class("AppType", "타입 스케일. 값은 design-tokens.md 의 type 블록 그대로다.", lines))
|
out.append(emit_class("AppType", "타입 스케일. 값은 design-tokens.md 의 type 블록 그대로다.", lines))
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ colors:
|
|||||||
color-on-cosmic: "#FFFFFF"
|
color-on-cosmic: "#FFFFFF"
|
||||||
color-on-cosmic-variant: "#A3A2A0"
|
color-on-cosmic-variant: "#A3A2A0"
|
||||||
color-on-cosmic-muted: "#767676"
|
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·탭 활성, 블루가 정보 칩.
|
# 악센트 — 시안 실측. 핑크가 선택·CTA, 초록이 POP·성공, 시안이 FPI·탭 활성, 블루가 정보 칩.
|
||||||
color-cosmic-accent: "#FF7DDD"
|
color-cosmic-accent: "#FF7DDD"
|
||||||
color-on-cosmic-accent: "#141414"
|
color-on-cosmic-accent: "#141414"
|
||||||
@@ -170,18 +173,26 @@ font:
|
|||||||
font-direction: "geometric"
|
font-direction: "geometric"
|
||||||
font-source: "42dot Sans = Google Fonts OFL 가변 ttf · ABeeZee = Google Fonts OFL (app/assets/fonts/, 2026-09-02)"
|
font-source: "42dot Sans = Google Fonts OFL 가변 ttf · ABeeZee = Google Fonts OFL (app/assets/fonts/, 2026-09-02)"
|
||||||
type:
|
type:
|
||||||
type-display-l: {family: font-display, size: 44, weight: 700, tracking: -0.01em, leading: 1.06}
|
# --- 발주사 Figma 계약 시점 사본 실측 (2026-09-26, _작업/design-match/foundation.json · 앱 93화면 텍스트 1,868건 집계) ---
|
||||||
type-display-m: {family: font-display, size: 32, weight: 700, tracking: -0.007em, leading: 1.2}
|
# 42dot Sans 가 81%, ABeeZee 가 14%(Regular 한 벌뿐이라 굵기는 400). 본문 자간은 -2.5% 가 표준이다.
|
||||||
type-title-l: {family: font-display, size: 24, weight: 700, tracking: 0, leading: 1.3}
|
# 종전 값(Gmarket/포카 온 스테이지 재설계 — 44·32 Bold, 본문 400·행간 1.6)은 Figma 와 맞지 않아 교체한다.
|
||||||
type-title-m: {family: font-body, size: 18, weight: 600, leading: 1.3}
|
type-display-l: {family: font-display, size: 46, weight: 400, leading: 1.0}
|
||||||
type-body-l: {family: font-body, size: 18, weight: 400, leading: 1.6}
|
type-display-m: {family: font-display, size: 32, weight: 400, tracking: -0.025em, leading: 1.4}
|
||||||
type-body-m: {family: font-body, size: 16, weight: 400, leading: 1.6}
|
type-title-l: {family: font-body, size: 20, weight: 600, tracking: -0.025em, leading: 1.4}
|
||||||
type-body-s: {family: font-body, size: 14, weight: 400, leading: 1.55}
|
type-title-m: {family: font-body, size: 16, weight: 600, tracking: -0.025em, leading: 1.4}
|
||||||
type-label-l: {family: font-body, size: 14, weight: 500, leading: 1.2}
|
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-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-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-eyebrow: {family: font-body, size: 12, weight: 600, tracking: -0.025em, leading: 1.0}
|
||||||
type-caption: {family: font-body, size: 12, weight: 500, leading: 1.4}
|
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'}
|
type-mono-num: {family: font-mono, feature: 'tnum', note: '가격·지표·표는 tabular'}
|
||||||
sizing:
|
sizing:
|
||||||
touch-min-app: 48
|
touch-min-app: 48
|
||||||
|
|||||||