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,92 @@
import 'package:flutter/material.dart';
import '../../providers/session_controller.dart';
import '../../theme/design_tokens.dart';
import '../noo/core/noo_button.dart';
import '../noo/noo_layout.dart';
import '../noo/overlays/noo_dialog.dart';
import '../noo/overlays/noo_overlay_header.dart';
import '../noo/overlays/noo_sheet.dart';
/// Shared by the account card's own quick "Remove account" action and each
/// row in the saved-accounts list - same confirm/remove/pop-to-root gate
/// either way (mirrors the pre-rework `_confirmRemoveActive`/
/// `_AccountsCard._confirmRemove`), just restyled as [showNooDialog]
/// (desktop) / [showNooSheet] (mobile) instead of an [AlertDialog].
Future<void> confirmRemoveAccount(
BuildContext context,
SessionController session, {
required String accountId,
required String username,
required String host,
required bool isActive,
}) async {
final hasOtherAccounts = session.accounts.length > 1;
final message = isActive && hasOtherAccounts
? 'Remove $username ($host)? Another saved account will become active.'
: 'Remove $username ($host)? You can add it again later.';
var confirmed = false;
void cancel() => Navigator.pop(context);
void confirm() {
confirmed = true;
Navigator.pop(context);
}
Widget buttons(NooColors colors) => Row(
children: [
Expanded(
child: NooButton(
variant: NooButtonVariant.secondary,
size: NooButtonSize.card,
fullWidth: true,
onTap: cancel,
child: const Text('Cancel'),
),
),
const SizedBox(width: NooSpace.sm),
Expanded(
child: NooButton(
variant: NooButtonVariant.danger,
size: NooButtonSize.card,
fullWidth: true,
onTap: confirm,
child: const Text('Remove'),
),
),
],
);
final colors = context.nooColors;
if (NooLayout.isDesktop(context)) {
await showNooDialog(
context,
title: 'Remove account',
children: [
Text(message, style: NooText.body.copyWith(color: colors.fg2)),
buttons(colors),
],
);
} else {
await showNooSheet(
context,
children: [
NooOverlayHeader(title: 'Remove account', onClose: cancel),
Text(message, style: NooText.body.copyWith(color: colors.fg2)),
buttons(colors),
],
);
}
if (!confirmed) return;
if (!context.mounted) return;
final navigator = Navigator.of(context);
await session.removeAccount(accountId);
// Only when this was the last saved account does isLoggedIn drop to false
// and main.dart swap the root route to LoginView underneath - pop back to
// it then, rather than leaving Settings stranded on top. Removing a
// non-active account, or falling back to another one, both keep the user
// logged in, so Settings should just stay put.
if (!session.isLoggedIn) {
navigator.popUntil((route) => route.isFirst);
}
}