Rebuild UI on the Noo design system

Replaces the Material 3 screens with the Noo design system: warm
neutrals, one violet accent, pill controls, Schibsted Grotesk/
Instrument Sans, Lucide icons, no gradients/shadows.

- New lib/widgets/noo/ component kit (core, lists, files, media, nav,
  overlays) plus lib/theme/design_tokens.dart for the color/type/space/
  radius/motion tokens.
- Rebuilt the app shell (top/bottom bars, drawer, desktop sidebar and
  toolbar), every tab (Files/Offline, Photos, Favorites, Recent,
  Activity, Trash, Shares), Settings, the lock and login screens, the
  media viewer, search, the details/share sheets, and the share-upload
  and move/copy destination pickers.
- Added a design canvas (linked from DESIGN_SYSTEM.md) covering the
  screens the spec didn't already describe, with matching Android and
  iOS chrome; wrote up the approved recipes into DESIGN_SYSTEM.md §4.
- Removed now-dead legacy widgets (media_grid_tile, swipeable_item,
  sync_status_badge, selectable_thumbnail) and updated
  architecture.md/styling.md/standards.md to describe the new
  structure and component/testing conventions.
- Added widget tests for the noo/ component kit.

This is a UI-only rework: no provider/model/service behavior changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 01:51:58 -04:00
co-authored by Claude Opus 5.5
parent 51682a901d
commit 56784ef212
125 changed files with 15263 additions and 6804 deletions
@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:provider/provider.dart';
import '../../models/app_tab.dart';
import '../../providers/session_controller.dart';
import '../../providers/sync_status_controller.dart';
import '../../services/sync_service.dart';
import '../../theme/design_tokens.dart';
import '../app_tab_view_builder.dart';
import '../noo/core/noo_button.dart';
import '../noo/core/noo_toggle.dart';
import '../noo/lists/noo_settings_row.dart';
import '../noo/nav/noo_top_bar.dart';
import '../noo/nav/noo_toolbar.dart';
import '../noo/noo_layout.dart';
import 'settings_section.dart';
/// Settings section 4: device sync (which folders/files mirror locally,
/// notifications, cellular use, and a manual "Sync now").
class SettingsFileSyncSection extends StatefulWidget {
const SettingsFileSyncSection({super.key});
@override
State<SettingsFileSyncSection> createState() => _SettingsFileSyncSectionState();
}
class _SettingsFileSyncSectionState extends State<SettingsFileSyncSection> {
bool _syncingNow = false;
Future<void> _syncNow(SessionController session, SyncStatusController sync) async {
setState(() => _syncingNow = true);
try {
await SyncService.syncNow(session, sync);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not start sync: $e'), behavior: SnackBarBehavior.floating),
);
}
} finally {
if (mounted) setState(() => _syncingNow = false);
}
}
/// The Offline tab has its own chrome-free content (see the shared
/// rebuild contract's "division of chrome" - only the shell draws top
/// bars for a tab), so opening it as a pushed screen from here needs a
/// minimal top bar of its own to stay navigable.
void _openOfflineFiles(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: NooLayout.isDesktop(context)
? const NooToolbar(title: 'Offline')
: NooTopBar(
style: NooLayout.navStyle(context),
title: 'Offline',
leading: const NooTopBarBack(),
),
body: buildAppTabView(AppTab.offline, ScrollController()),
),
),
);
}
@override
Widget build(BuildContext context) {
final session = context.watch<SessionController>();
final sync = context.watch<SyncStatusController>();
final colors = context.nooColors;
final folders = sync.syncedPaths;
final everything = sync.syncEverything;
final hasSyncScope = everything || folders.isNotEmpty;
return SettingsSection(
title: 'File sync',
children: [
NooSettingsRow(
icon: LucideIcons.cloud,
label: const Text('Sync everything'),
subtitle: const Text('Mirror the whole account instead of picking folders'),
trailing: NooToggle(checked: everything, onChanged: sync.setSyncEverything),
),
NooSettingsRow(
icon: LucideIcons.hardDriveDownload,
label: const Text('View offline files'),
subtitle: Text(
everything
? 'Every folder in this account is synced'
: folders.isEmpty
? 'Nothing synced yet'
: '${folders.length} synced folder${folders.length == 1 ? '' : 's'}/file${folders.length == 1 ? '' : 's'}',
),
onTap: () => _openOfflineFiles(context),
),
NooSettingsRow(
icon: LucideIcons.bellRing,
label: const Text('Background sync notifications'),
subtitle: const Text(
'Show a notification when a background sync updates files. '
'Conflicts and "Sync now" always notify.',
),
trailing: NooToggle(checked: sync.syncNotifications, onChanged: sync.setSyncNotifications),
),
NooSettingsRow(
icon: LucideIcons.signal,
label: const Text('Sync on cellular'),
subtitle: const Text('Off = background sync only runs on Wi-Fi'),
trailing: NooToggle(checked: sync.syncOnCellular, onChanged: sync.setSyncOnCellular),
),
NooSettingsRow(
icon: LucideIcons.refreshCw,
label: const Text('Sync now'),
trailing: _syncingNow
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2, color: colors.accentText),
)
: NooButton(
variant: NooButtonVariant.tonal,
size: NooButtonSize.compact,
disabled: !hasSyncScope,
onTap: () => _syncNow(session, sync),
child: const Text('Sync now'),
),
),
],
);
}
}