feat: detail screen UI polish — badge merge, clean typography, spec peek, no dividers

変更1: AI確信度 + MBTI相性バッジを横並び1行に統合(Wrap)
変更2: 銘柄名・酒蔵から常時表示の鉛筆アイコンを除去、区切りを / → · に変更
       キャッチコピーを銘柄名直下に移動、タグをpill形状(radius:20)に変更
変更3: スペック詳細アコーディオンのタイトルにアルコール度数・精米歩合をチラ見せ
変更4: Divider 4本 → SizedBox(height:32) に置換
       おすすめ見出しカラーを colorScheme.onSurface → appColors.textPrimary に統一

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ponshu Developer 2026-04-05 14:55:21 +09:00
parent 4758aa5c9c
commit 818f8862a1
3 changed files with 203 additions and 182 deletions

View File

@ -55,86 +55,80 @@ class SakeBasicInfoSection extends ConsumerWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Confidence Badge // 1: AI確信度 + MBTI相性 1
if (sake.metadata.aiConfidence != null && sake.itemType != ItemType.set) if ((sake.metadata.aiConfidence != null && sake.itemType != ItemType.set) || showMbti)
Center( Center(
child: Container( child: Padding(
margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
if (sake.metadata.aiConfidence != null && sake.itemType != ItemType.set)
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: confidenceColor.withValues(alpha: 0.1), color: confidenceColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(color: confidenceColor.withValues(alpha: 0.5)), border: Border.all(color: confidenceColor.withValues(alpha: 0.4)),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(LucideIcons.sparkles, size: 14, color: confidenceColor), Icon(LucideIcons.sparkles, size: 12, color: confidenceColor),
const SizedBox(width: 6), const SizedBox(width: 4),
Text( Text(
'AI確信度: $score%', 'AI $score%',
style: TextStyle( style: TextStyle(
color: confidenceColor, color: confidenceColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12, fontSize: 11,
), ),
), ),
], ],
), ),
), ),
),
// MBTI Compatibility Badge
if (showMbti) if (showMbti)
Center( GestureDetector(
child: GestureDetector( onTap: () => onTapMbtiCompatibility(context, mbtiResult!, appColors),
onTap: () => onTapMbtiCompatibility(context, mbtiResult, appColors),
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: badgeColor!.withValues(alpha: 0.1), color: badgeColor!.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(20),
border: Border.all(color: badgeColor.withValues(alpha: 0.3)), border: Border.all(color: badgeColor.withValues(alpha: 0.4)),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(LucideIcons.brainCircuit, size: 12, color: badgeColor),
const SizedBox(width: 4),
Text( Text(
'$mbtiType相性: ', '$mbtiType ${mbtiResult!.starDisplay}',
style: TextStyle(
color: appColors.textSecondary,
fontSize: 12,
),
),
Text(
mbtiResult.starDisplay,
style: TextStyle( style: TextStyle(
color: badgeColor, color: badgeColor,
fontSize: 14, fontWeight: FontWeight.bold,
letterSpacing: 1, fontSize: 11,
letterSpacing: 0.5,
), ),
), ),
const SizedBox(width: 4),
Icon(
LucideIcons.info,
size: 12,
color: appColors.iconSubtle,
),
], ],
), ),
), ),
), ),
],
),
),
), ),
// Brand Name // 2:
Center( Center(
child: InkWell( child: InkWell(
onTap: onTapName, onTap: onTapName,
child: Row( borderRadius: BorderRadius.circular(8),
mainAxisSize: MainAxisSize.min, child: Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
Flexible(
child: Text( child: Text(
sake.displayData.displayName, sake.displayData.displayName,
style: Theme.of(context).textTheme.headlineMedium?.copyWith( style: Theme.of(context).textTheme.headlineMedium?.copyWith(
@ -144,84 +138,85 @@ class SakeBasicInfoSection extends ConsumerWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
const SizedBox(width: 8),
Icon(LucideIcons.pencil, size: 18, color: appColors.iconSubtle),
],
), ),
), ),
), const SizedBox(height: 6),
const SizedBox(height: 8),
// Brand / Prefecture // 2: / ·
if (sake.itemType != ItemType.set) if (sake.itemType != ItemType.set)
Center( Center(
child: InkWell( child: InkWell(
onTap: onTapBrewery, onTap: onTapBrewery,
child: Row( borderRadius: BorderRadius.circular(8),
mainAxisSize: MainAxisSize.min, child: Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
Flexible(
child: Text( child: Text(
'${sake.displayData.displayBrewery} / ${sake.displayData.displayPrefecture}', '${sake.displayData.displayBrewery} · ${sake.displayData.displayPrefecture}',
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: appColors.textSecondary, color: appColors.textSecondary,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
), ),
), ),
), ),
const SizedBox(width: 8),
Icon(LucideIcons.pencil, size: 16, color: appColors.iconSubtle),
],
), ),
), const SizedBox(height: 20),
),
const SizedBox(height: 16),
// Tags Row // 調
if (sake.displayData.catchCopy != null && sake.itemType != ItemType.set)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
sake.displayData.catchCopy!,
style: GoogleFonts.zenOldMincho(
fontSize: 22,
height: 1.6,
fontWeight: FontWeight.w500,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white70
: appColors.brandPrimary.withValues(alpha: 0.8),
),
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 20),
// pill
if (sake.hiddenSpecs.flavorTags.isNotEmpty) if (sake.hiddenSpecs.flavorTags.isNotEmpty)
Padding( Center(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Material(
color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: onTapTags, onTap: onTapTags,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.symmetric(vertical: 4),
child: Wrap( child: Wrap(
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
spacing: 8, spacing: 6,
children: sake.hiddenSpecs.flavorTags runSpacing: 6,
.map((tag) => Chip( children: sake.hiddenSpecs.flavorTags.map((tag) => Container(
label: Text(tag, style: const TextStyle(fontSize: 10)), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
visualDensity: VisualDensity.compact, decoration: BoxDecoration(
backgroundColor: color: appColors.brandPrimary.withValues(alpha: 0.08),
Theme.of(context).primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20),
)) border: Border.all(
.toList(), color: appColors.brandPrimary.withValues(alpha: 0.2),
), ),
), ),
),
),
),
const SizedBox(height: 24),
// AI Catchcopy (Mincho)
if (sake.displayData.catchCopy != null && sake.itemType != ItemType.set)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text( child: Text(
sake.displayData.catchCopy!, tag,
style: GoogleFonts.zenOldMincho( style: TextStyle(
fontSize: 24, fontSize: 11,
height: 1.5, color: appColors.brandPrimary,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Theme.of(context).primaryColor,
), ),
textAlign: TextAlign.center, ),
)).toList(),
),
),
), ),
), ),
], ],

View File

@ -152,19 +152,15 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
onTapMbtiCompatibility: _showMbtiCompatibilityDialog, onTapMbtiCompatibility: _showMbtiCompatibilityDialog,
), ),
const SizedBox(height: 24), const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 24),
// Taste Radar Chart (Extracted) with Manual Edit // Taste Radar Chart
SakeDetailChart( SakeDetailChart(
sake: _sake, sake: _sake,
onTasteStatsEdited: (newStats) => _updateTasteStats(newStats), onTasteStatsEdited: (newStats) => _updateTasteStats(newStats),
), ),
const SizedBox(height: 24), const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 24),
// Description // Description
if (_sake.hiddenSpecs.description != null && _sake.itemType != ItemType.set) if (_sake.hiddenSpecs.description != null && _sake.itemType != ItemType.set)
@ -177,10 +173,8 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 16),
// AI Specs Accordion (Extracted) // AI Specs Accordion
SakeDetailSpecs( SakeDetailSpecs(
sake: _sake, sake: _sake,
onUpdate: (updatedSake) { onUpdate: (updatedSake) {
@ -188,22 +182,15 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 16),
// Memo Field (Extracted) // Memo Field
SakeDetailMemo( SakeDetailMemo(
initialMemo: _sake.userData.memo, initialMemo: _sake.userData.memo,
onUpdate: (value) async { onUpdate: (value) async {
// Auto-save
final box = Hive.box<SakeItem>('sake_items'); final box = Hive.box<SakeItem>('sake_items');
final updated = _sake.copyWith(memo: value, isUserEdited: true); final updated = _sake.copyWith(memo: value, isUserEdited: true);
await box.put(_sake.key, updated); await box.put(_sake.key, updated);
// Note: setState is needed to update the 'updated' variable locally
// But the text field manages its own state, so we don't strictly need to rebuild the text field
// However, other parts might depend on _sake.userData.memo? Unlikely.
// Actually, we should update _sake here to keep consistency.
setState(() => _sake = updated); setState(() => _sake = updated);
}, },
), ),
@ -211,21 +198,21 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
const SizedBox(height: 48), const SizedBox(height: 48),
// Related Items 3D Carousel // Related Items 3D Carousel
if (_sake.itemType != ItemType.set) ...[ // Hide for Set Items if (_sake.itemType != ItemType.set) ...[
Row( Row(
children: [ children: [
Icon(LucideIcons.sparkles, size: 16, color: Theme.of(context).colorScheme.onSurface), Icon(LucideIcons.sparkles, size: 16, color: appColors.iconDefault),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'おすすめの日本酒', 'おすすめの日本酒',
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface, color: appColors.textPrimary,
), ),
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 6),
Text( Text(
'五味チャート・タグ・酒蔵・産地から自動選出', '五味チャート・タグ・酒蔵・産地から自動選出',
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(

View File

@ -174,13 +174,33 @@ class _SakeDetailSpecsState extends State<SakeDetailSpecs> {
} }
}, },
leading: Icon(LucideIcons.sparkles, color: appColors.iconDefault), leading: Icon(LucideIcons.sparkles, color: appColors.iconDefault),
title: Text( title: Row(
'詳細', children: [
Text(
'詳細スペック',
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: textColor, color: textColor,
), ),
), ),
// :
if (!_isEditing) ...[
const SizedBox(width: 10),
if (widget.sake.hiddenSpecs.alcoholContent != null)
_buildPeekChip(
'${widget.sake.hiddenSpecs.alcoholContent}%',
appColors,
),
if (widget.sake.hiddenSpecs.polishingRatio != null) ...[
const SizedBox(width: 4),
_buildPeekChip(
'精米${widget.sake.hiddenSpecs.polishingRatio}%',
appColors,
),
],
],
],
),
trailing: _isEditing trailing: _isEditing
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -261,6 +281,25 @@ class _SakeDetailSpecsState extends State<SakeDetailSpecs> {
); );
} }
Widget _buildPeekChip(String label, AppColors appColors) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: appColors.surfaceSubtle,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: appColors.divider),
),
child: Text(
label,
style: TextStyle(
fontSize: 10,
color: appColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
);
}
void _showDatePicker(BuildContext context) { void _showDatePicker(BuildContext context) {
if (!_isEditing) return; if (!_isEditing) return;