generated from ttipo/project-base
fix(flutter): 쓰기 · 검색 입력칸에 테마의 둥근 테두리가 그려지던 것을 고치고 칸 줄을 보드에 맞춘다
InputDecoration.collapsed 는 enabledBorder · focusedBorder 를 비워 두어 TextField 가 앱 테마의 둥근 테두리로 채운다. 그래서 받는 사람 · 제목 · 본문 · 검색 칸에 글자를 감싼 테두리가 그려지고 초점이면 칸 줄의 아래 선과 청록 테두리가 겹쳤다 (11-12 배포 뒤 사용자 보고). 여섯 테두리를 모두 없앤 borderlessInputDecoration 으로 바꾸고, lib 에 InputDecoration.collapsed 가 다시 들어오면 시험이 실패하게 한다. 함께 칸 줄을 보드 App1Compose 에 맞췄다: 칸 이름 15 · 간격 6 · 오른쪽 여백 4, 칩이 여러 줄이면 칸 이름과 [참조 펼치기]를 첫 칩 줄에, 안내 글 색 · 칩 ✕ 색, 본문 칸 최소 높이(원본 메일이 있으면 150 · 없으면 서명이 있어도 240). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -489,9 +489,7 @@ class _MessageComposePageState extends ConsumerState<MessageComposePage> {
|
||||
focusNode: _bodyFocus,
|
||||
enabled: enabled,
|
||||
error: lengthError == ComposeLengthError.body,
|
||||
minHeight: quote != null || signature != null
|
||||
? AppSize.composeBodyMinHeightWithQuote
|
||||
: AppSize.composeBodyMinHeight,
|
||||
minHeight: composeBodyMinHeightOf(hasQuote: quote != null),
|
||||
),
|
||||
if (lengthError == ComposeLengthError.body)
|
||||
ComposeAlert(
|
||||
@@ -556,6 +554,12 @@ class _MessageComposePageState extends ConsumerState<MessageComposePage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 본문 칸의 최소 높이 — 원본 메일(인용)이 아래에 있으면(답장 · 전달) 낮게(150), 없으면(새 메일 등) 서명이 있어도 높게(240).
|
||||
/// 보드 `App1Compose` 의 「새 메일 · 받는 사람 추천」(서명 있음 — 240)과 「답장 · 참조 펼침 · 첨부 · 원본 접힘」(서명 · 원본 메일 — 150)
|
||||
double composeBodyMinHeightOf({required bool hasQuote}) => hasQuote
|
||||
? AppSize.composeBodyMinHeightWithQuote
|
||||
: AppSize.composeBodyMinHeight;
|
||||
|
||||
/// 올리지 못한 고른 파일의 수
|
||||
int failedUploadsOf(ComposeState state) => state.attachments
|
||||
.where(
|
||||
@@ -627,7 +631,6 @@ class _SubjectRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final AppExtraColors extra = theme.extension<AppExtraColors>()!;
|
||||
final TextStyle? style = theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: AppTypography.semiBold,
|
||||
);
|
||||
@@ -653,11 +656,13 @@ class _SubjectRow extends StatelessWidget {
|
||||
Utf16LengthLimitFormatter(kComposeMaxSubject),
|
||||
],
|
||||
style: style,
|
||||
decoration: InputDecoration.collapsed(
|
||||
// 테두리는 칸 줄(ComposeFieldRow)의 아래 선뿐이다 — 테마의 입력칸 테두리를 물려받지 않게
|
||||
// 안내 글은 보드의 placeholder 색(#8A94A3 — 테마의 outline, 검색 칸 안내 글과 같다)
|
||||
decoration: borderlessInputDecoration(
|
||||
hintText: MailComposeTexts.subjectHint,
|
||||
hintStyle: style?.copyWith(
|
||||
fontWeight: AppTypography.regular,
|
||||
color: extra.textMuted,
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
onSubmitted: (String _) => onSubmitted(),
|
||||
@@ -669,7 +674,7 @@ class _SubjectRow extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// 본문 칸 — 글만(서식 편집은 다음 판). 비어 있는 아래쪽을 눌러도 칸에 초점이 간다.
|
||||
/// 아래에 서명 · 원본 메일이 있으면 낮게(150), 없으면 높게(240) 둔다(보드)
|
||||
/// 아래에 원본 메일이 있으면 낮게(150), 없으면 서명이 있어도 높게(240) 둔다(보드 — [composeBodyMinHeightOf])
|
||||
class _BodyField extends StatelessWidget {
|
||||
const _BodyField({
|
||||
required this.controller,
|
||||
@@ -718,9 +723,9 @@ class _BodyField extends StatelessWidget {
|
||||
textInputAction: TextInputAction.newline,
|
||||
maxLines: null,
|
||||
style: style,
|
||||
decoration: InputDecoration.collapsed(
|
||||
decoration: borderlessInputDecoration(
|
||||
hintText: MailComposeTexts.bodyHint,
|
||||
hintStyle: style?.copyWith(color: extra.textMuted),
|
||||
hintStyle: style?.copyWith(color: theme.colorScheme.outline),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:arasystem_app/core/people/address_input.dart';
|
||||
import 'package:arasystem_app/core/people/people_texts.dart';
|
||||
import 'package:arasystem_app/core/people/recipient_match.dart';
|
||||
@@ -192,6 +194,10 @@ class _ComposeRecipientFieldState extends State<ComposeRecipientField> {
|
||||
error: invalid,
|
||||
onTap: widget.enabled ? widget.focusNode.requestFocus : null,
|
||||
trailing: widget.trailing,
|
||||
// 칩이 있으면 첫 줄은 칩 줄 — 여러 줄이어도 칸 이름 · [참조 펼치기]가 첫 칩 줄에 맞는다
|
||||
firstLineHeight: widget.tokens.isEmpty
|
||||
? null
|
||||
: AppSize.recipientChipHeight,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Wrap(
|
||||
@@ -263,7 +269,8 @@ class _ComposeRecipientFieldState extends State<ComposeRecipientField> {
|
||||
smartQuotesType: SmartQuotesType.disabled,
|
||||
inputFormatters: const <TextInputFormatter>[_LineBreaksToCommas()],
|
||||
style: theme.textTheme.bodyLarge,
|
||||
decoration: const InputDecoration.collapsed(hintText: ''),
|
||||
// 테두리는 칸 줄(ComposeFieldRow)의 아래 선뿐이다 — 테마의 입력칸 테두리를 물려받지 않게
|
||||
decoration: borderlessInputDecoration(hintText: ''),
|
||||
onChanged: widget.onChanged,
|
||||
onEditingComplete: _onNext,
|
||||
),
|
||||
@@ -342,7 +349,12 @@ class _LineBreaksToCommas extends TextInputFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
/// 쓰기 화면의 칸 줄 — 왼쪽 칸 이름(72) · 가운데 칸 · 오른쪽 단추, 아래 선(초점이 있으면 청록 2) (보드 `App1Compose`)
|
||||
/// 쓰기 화면의 칸 줄 — 왼쪽 칸 이름(72 · 글자 15) · 간격 6 · 가운데 칸 · 오른쪽 단추, 아래 선(초점이 있으면 청록 2)
|
||||
/// (보드 `App1Compose` — 최소 높이 56 · 여백 위 4 · 오른쪽 4 · 아래 4 · 왼쪽 16).
|
||||
///
|
||||
/// 칸 이름과 오른쪽 단추는 **첫 줄**([lineHeight] — 최소 높이에서 위아래 여백을 뺀 48)의 세로 가운데에 둔다 —
|
||||
/// 받는 사람 칩이 여러 줄이어도 첫 줄에 남는다(줄 전체의 가운데로 내려가지 않게 — 2026-11-12). 칸이 한 줄이면 줄의 가운데 그대로다.
|
||||
/// 아래 선은 자리를 차지하지 않게 그린다 — 초점 · 오류로 굵어져도 칸이 움직이지 않는다(보드의 inset 그림자와 같다)
|
||||
class ComposeFieldRow extends StatelessWidget {
|
||||
const ComposeFieldRow({
|
||||
required this.label,
|
||||
@@ -351,9 +363,14 @@ class ComposeFieldRow extends StatelessWidget {
|
||||
this.error = false,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.firstLineHeight,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// 첫 줄의 높이 — 줄의 최소 높이(56)에서 위아래 여백(4)을 뺀 자리
|
||||
static const double lineHeight =
|
||||
AppSize.composeRowMinHeight - AppSpacing.s4 * 2;
|
||||
|
||||
final String label;
|
||||
final Widget child;
|
||||
final bool focused;
|
||||
@@ -363,12 +380,20 @@ class ComposeFieldRow extends StatelessWidget {
|
||||
final Widget? trailing;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// 칸 안 첫 줄의 높이(받는 사람 칩 40) — 주면 칸을 위아래로 ([lineHeight] − 이 값) / 2 씩 띄워,
|
||||
/// 칩이 여러 줄이어도 첫 칩 줄이 칸 이름 · 오른쪽 단추와 같은 높이에 온다(한 줄이면 줄을 꼭 채워 지금처럼 가운데)
|
||||
final double? firstLineHeight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final ColorScheme scheme = theme.colorScheme;
|
||||
final AppExtraColors extra = theme.extension<AppExtraColors>()!;
|
||||
final Widget? action = trailing;
|
||||
final double? inner = firstLineHeight;
|
||||
final double inset = inner == null
|
||||
? 0
|
||||
: math.max(0, (lineHeight - inner) / 2);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onTap,
|
||||
@@ -376,13 +401,13 @@ class ComposeFieldRow extends StatelessWidget {
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: AppSize.composeRowMinHeight,
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.s16,
|
||||
AppSpacing.s4,
|
||||
action == null ? AppSpacing.s16 : AppSpacing.s4,
|
||||
AppSpacing.s4,
|
||||
AppSpacing.s4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
foregroundDecoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: error
|
||||
@@ -395,20 +420,32 @@ class ComposeFieldRow extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: AppSize.composeLabelWidth,
|
||||
child: ExcludeSemantics(
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: extra.textMuted,
|
||||
_FirstLine(
|
||||
child: SizedBox(
|
||||
width: AppSize.composeLabelWidth,
|
||||
child: ExcludeSemantics(
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: extra.textMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: child),
|
||||
?action,
|
||||
const SizedBox(width: AppSpacing.s6),
|
||||
Expanded(
|
||||
child: _FirstLine(
|
||||
stretch: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: inset),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (action != null) _FirstLine(child: action),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -416,6 +453,31 @@ class ComposeFieldRow extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 칸 줄의 첫 줄 자리 — [ComposeFieldRow.lineHeight] 안의 세로 가운데에 둔다. 더 높으면(칩이 여러 줄 · 큰 글자) 위에서부터
|
||||
class _FirstLine extends StatelessWidget {
|
||||
const _FirstLine({required this.child, this.stretch = false});
|
||||
|
||||
final Widget child;
|
||||
|
||||
/// 폭을 다 쓴다(가운데 칸)
|
||||
final bool stretch;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: ComposeFieldRow.lineHeight),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: stretch
|
||||
? CrossAxisAlignment.stretch
|
||||
: CrossAxisAlignment.start,
|
||||
children: <Widget>[child],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 받는 사람 칩 — 이름(없으면 주소) · ✕. 형식이 틀린 주소는 빨갛게
|
||||
class _RecipientChip extends StatelessWidget {
|
||||
const _RecipientChip({required this.token, required this.onRemove});
|
||||
@@ -427,7 +489,6 @@ class _RecipientChip extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final ColorScheme scheme = theme.colorScheme;
|
||||
final AppExtraColors extra = theme.extension<AppExtraColors>()!;
|
||||
final String? name = token.address.name?.trim();
|
||||
final String shown = !token.valid
|
||||
? token.label
|
||||
@@ -470,7 +531,8 @@ class _RecipientChip extends StatelessWidget {
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
iconSize: AppSize.iconTiny,
|
||||
color: token.valid ? extra.textStrong : scheme.error,
|
||||
// 보드의 ✕ #4A5363 — 보조 글자색(onSurfaceVariant)
|
||||
color: token.valid ? scheme.onSurfaceVariant : scheme.error,
|
||||
tooltip: MailComposeTexts.remove(shown),
|
||||
onPressed: onRemove,
|
||||
icon: const Icon(Icons.close),
|
||||
|
||||
@@ -94,8 +94,8 @@ class MailSearchBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
Utf16LengthLimitFormatter(kMailSearchMaxLength),
|
||||
],
|
||||
style: style,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: null,
|
||||
// 위 막대 안의 칸 — 테마의 입력칸 테두리를 물려받지 않게
|
||||
decoration: borderlessInputDecoration(
|
||||
hint: ExcludeSemantics(
|
||||
child: Text(
|
||||
MailSearchTexts.box,
|
||||
|
||||
Reference in New Issue
Block a user