On mobile, AccountView used to render all 9 Settings sections inline in one long, individually-collapsible column (_MobileList). Replace that with a native-style menu: the account card stays pinned at the top, and every other section becomes a NooSettingsRow in a NooGroupedList that pushes a dedicated screen (NooTopBar + NooTopBarBack) holding just that section's content. This removes the scroll-depth problem outright instead of working around it with per-section collapsing, so SettingsSection no longer needs NooGroupedList's collapsible mode on mobile. Desktop's 2-column grid is unchanged - it already shows every section at once. Audited every lib/widgets/settings/*.dart file for the reported "description and controls are flipped" row-layout bug: every row goes through NooSettingsRow directly, or - for the few hand-rolled rows (_SavedAccountRow, _ThemeRow/_BottomBarStyleRow, _CacheIntervalRow) - preserves its icon/description-then-control order (the stacked control-below-label shape used by _ThemeRow and _CacheIntervalRow is an intentional, spec'd variant, not a flip). Found no instance of the bug; no fix was needed. Updates DESIGN_SYSTEM.md's Settings recipe and styling.md's NooGroupedList notes to describe the new menu-then-pushed-screen pattern, and adds a widget test covering the category menu and the push/pop navigation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
121 lines
4.2 KiB
Dart
121 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../theme/design_tokens.dart';
|
|
import '../noo/lists/noo_grouped_list.dart';
|
|
import '../noo/noo_layout.dart';
|
|
import '../noo/overlays/noo_dialog.dart';
|
|
import '../noo/overlays/noo_sheet.dart';
|
|
|
|
/// One block of Settings (DESIGN_SYSTEM.md 4's 9-part order): a
|
|
/// [NooGroupedList] on mobile (label above a radius-20 card), or a titled,
|
|
/// bordered radius-20 card holding a flat row group on desktop
|
|
/// ("Mobile uses one column of grouped lists. Desktop uses a 2-column grid
|
|
/// of cards with a 1px line and radius 20."). Shared by every
|
|
/// `lib/widgets/settings/*.dart` section so both layouts stay in sync from
|
|
/// one place.
|
|
class SettingsSection extends StatelessWidget {
|
|
final String title;
|
|
final String? subtitle;
|
|
final List<Widget> children;
|
|
|
|
const SettingsSection({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
required this.children,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = context.nooColors;
|
|
if (!NooLayout.isDesktop(context)) {
|
|
return NooGroupedList(
|
|
label: title,
|
|
footer: subtitle != null ? Text(subtitle!) : null,
|
|
// Not collapsible: each mobile Settings section now renders on its
|
|
// own pushed screen (see `account_view.dart`'s doc comment for the
|
|
// menu-then-pushed-screen pattern), so there's no long single-scroll
|
|
// page left to collapse sections *within* - an earlier design had
|
|
// every section inline in one column and made them individually
|
|
// collapsible for exactly that reason; that's gone now that each
|
|
// one is already isolated on its own screen.
|
|
children: children,
|
|
);
|
|
}
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: colors.surface,
|
|
border: Border.all(color: colors.line),
|
|
borderRadius: BorderRadius.circular(NooRadii.card),
|
|
),
|
|
padding: const EdgeInsets.all(NooSpace.lg),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: NooText.cardTitle.copyWith(color: colors.fg1)),
|
|
if (subtitle != null) ...[
|
|
const SizedBox(height: 4),
|
|
Text(subtitle!, style: NooText.meta.copyWith(color: colors.fg3)),
|
|
],
|
|
const SizedBox(height: NooSpace.md),
|
|
FlatRowGroup(children: children),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The line-gapped row stack [NooGroupedList] draws internally, without its
|
|
/// own outer card/label - used for [SettingsSection]'s desktop card body,
|
|
/// which already supplies the surrounding card chrome, so nesting a second
|
|
/// radius-20 card there would double up the framing. Also reused directly
|
|
/// by `settings_tabs.dart`, whose desktop card mixes plain rows with a
|
|
/// reorderable list under one shared title/border instead of going through
|
|
/// [SettingsSection] itself.
|
|
class FlatRowGroup extends StatelessWidget {
|
|
final List<Widget> children;
|
|
const FlatRowGroup({super.key, required this.children});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = context.nooColors;
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(NooRadii.input),
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(color: colors.line),
|
|
child: Column(
|
|
children: [
|
|
for (var i = 0; i < children.length; i++) ...[
|
|
if (i > 0) const SizedBox(height: 1),
|
|
children[i],
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The picker pattern for every "value + chevron" settings row (cache
|
|
/// policy, swipe actions, default tab...): a titled option list, a
|
|
/// [NooGroupedList] sheet on mobile or a [NooDialog] on desktop (mirrors
|
|
/// `files_controls_row.dart`'s sort/filter sheets). Each option row should
|
|
/// call `Navigator.pop(context)` itself after applying its choice.
|
|
void showSettingsPicker(
|
|
BuildContext context, {
|
|
required String title,
|
|
required List<Widget> options,
|
|
}) {
|
|
if (NooLayout.isDesktop(context)) {
|
|
showNooDialog(
|
|
context,
|
|
title: title,
|
|
children: [Column(children: options)],
|
|
);
|
|
} else {
|
|
showNooSheet(
|
|
context,
|
|
children: [NooGroupedList(label: title, children: options)],
|
|
);
|
|
}
|
|
}
|