import 'package:flutter/material.dart'; import '../i18n/app_locale.dart'; import '../theme/tokens.dart'; /// 한국어 어절 단위 줄바꿈 — 글자 사이에 WORD JOINER(U+2060)를 넣어 공백에서만 줄을 바꾼다. /// Flutter 는 한글을 음절마다 끊어 「얻으세 / 요.」처럼 낱말 중간에서 줄을 바꾼다(10/6 실기기 D-4). /// 한 어절이 한 줄보다 길면 그 어절만 글자 단위로 끊긴다(엔진 기본). String keepWords(String text) => text.splitMapJoin( RegExp(r'\s+'), onNonMatch: (word) => word.characters.join('\u2060'), ); /// 앱 공통 확인 창 — Figma `2066:17799`(행성 생성 확인) · `554:24669`(설정 확인 팝업). /// /// 카드 312 폭 · #282828 r12 · 안쪽 24/16 · 문구 16/600 가운데 정렬, /// 그 아래 **취소(흰 테두리) · 실행(분홍 채움)을 가로로 반씩**(48 높이 · 간격 8 · r8). /// 단일 버튼 알림(Figma `511:86714` · `554:23103`)은 실행 버튼 하나가 폭을 다 쓴다. /// /// [content] 는 입력 칸이 있는 창(반려 사유 · 인증 제출 등)에서 문구 대신 넣는다. class AppDialog extends StatefulWidget { const AppDialog({ super.key, required this.title, this.body, this.content, this.cancelLabel, required this.confirmLabel, this.onConfirm, }); final String title; final String? body; final Widget? content; /// null 이면 실행 버튼 하나만 둔다. final String? cancelLabel; final String confirmLabel; /// 실행 버튼을 누르면 창을 닫기 **전에** 돈다. 도는 동안 실행 버튼은 진행 중 /// 표시로 바뀌고 두 버튼 모두 눌리지 않는다(로그아웃처럼 정리가 걸리는 실행). final Future Function()? onConfirm; @override State createState() => _AppDialogState(); } class _AppDialogState extends State { bool _busy = false; static const _shape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.sm)), ); static TextStyle _text(double size, FontWeight weight, Color color) => TextStyle( fontFamily: AppFonts.body, fontSize: size, fontWeight: weight, height: 1.5, letterSpacing: size * -0.025, color: color, ); @override Widget build(BuildContext context) { Widget button(String label, bool value, {required bool filled}) { final text = Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: _text( 16, FontWeight.w600, filled ? AppColors.onCosmicAccent : AppColors.onCosmic, ).copyWith(height: 1), ); Future pop() async { final run = widget.onConfirm; if (value && run != null) { setState(() => _busy = true); try { await run(); } finally { if (mounted) setState(() => _busy = false); } } if (context.mounted) Navigator.of(context).pop(value); } return Expanded( child: filled ? FilledButton( onPressed: _busy ? null : pop, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(48), padding: const EdgeInsets.symmetric(horizontal: 12), backgroundColor: AppColors.cosmicAccent, disabledBackgroundColor: AppColors.cosmicAccent, shape: _shape, ), child: _busy ? const SizedBox.square( dimension: 20, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.onCosmicAccent, ), ) : text, ) : OutlinedButton( onPressed: _busy ? null : pop, style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(48), padding: const EdgeInsets.symmetric(horizontal: 12), side: const BorderSide(color: AppColors.onCosmic), shape: _shape, ), child: text, ), ); } return Dialog( backgroundColor: const Color(0xFF282828), // Figma 2066:17799 fill insetPadding: const EdgeInsets.symmetric(horizontal: 31.5, vertical: 24), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(AppRadius.md)), ), child: Padding( padding: const EdgeInsets.fromLTRB(16, 24, 16, 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( widget.title, textAlign: TextAlign.center, style: _text(16, FontWeight.w600, AppColors.onCosmic), ), if (widget.body case final b?) ...[ const SizedBox(height: 16), Text( b, textAlign: TextAlign.center, style: _text(16, FontWeight.w500, AppColors.onCosmic) .copyWith(height: 1.4), ), ], if (widget.content case final c?) ...[ const SizedBox(height: 16), c, ], ], ), ), ), const SizedBox(height: 16), Row( children: [ if (widget.cancelLabel case final c?) ...[ button(c, false, filled: false), const SizedBox(width: 8), ], button(widget.confirmLabel, true, filled: true), ], ), ], ), ), ); } } const _barrier = Color(0xB3000000); /// 취소 · 실행 두 갈래. 실행이면 true. [confirmLabel] 기본은 「변경」. Future showConfirmDialog( BuildContext context, String title, { String? body, Widget? content, String? cancelLabel, String? confirmLabel, Future Function()? onConfirm, }) async { final r = await showDialog( context: context, barrierColor: _barrier, // 실행이 도는 동안 바깥을 눌러 닫히면 진행 중 표시가 사라진 채 실행만 남는다. barrierDismissible: onConfirm == null, builder: (context) => AppDialog( title: title, body: body, content: content, cancelLabel: cancelLabel ?? context.l10n.commonCancel, confirmLabel: confirmLabel ?? context.l10n.commonChange, onConfirm: onConfirm, ), ); return r ?? false; } /// 확인 하나뿐인 알림 — Figma `511:86714`(타이틀 가운데 · 확인 버튼 가운데). Future showAlertDialog( BuildContext context, String title, { String? body, String? confirmLabel, }) => showDialog( context: context, barrierColor: _barrier, builder: (context) => AppDialog( title: title, body: body, confirmLabel: confirmLabel ?? context.l10n.commonConfirm, ), );