Noo design system rebuild #1

Merged
ayushya merged 31 commits from noo-design-system-rebuild into main 2026-10-01 09:23:58 -04:00
4 changed files with 147 additions and 101 deletions
Showing only changes of commit fb4829d035 - Show all commits
+17 -5
View File
@@ -245,11 +245,23 @@ blocks are noted where they matter:
near-black `bg` (a dark, diffuse shadow needs real density close to the
edge to be visible against an already-dark backdrop), so a second,
tighter, more opaque contact shadow underneath it gives real elevation
in both themes. Also added `NooTopBar.androidTitleReplacement`: Android has no
large title to put a second search row under the way iOS's `search:`
slot does, so an inline search bar (`AppTopBar` passes a plain
`ShellSearchLauncher()` when search isn't in the bottom bar) replaces the
title in its flexible slot instead of sitting beside it as a bare icon.
in both themes. The card's border can go missing wherever an opaque row
sits against it too (every corner but the header's, which has no
full-bleed fill of its own) if a `Container` combines `border` with its
own `clipBehavior` - that paints the border as part of the *outer*
decoration, then the clipped child on top right up to the same boundary,
with no gap for the border's own stroke to show through. Fixed the same
way any bordered-and-clipped `Container` should be: no `clipBehavior` on
the bordered `Container` itself, and a 1px-inset `ClipRRect` (radius
reduced by that same 1px) around the filled, clipped content instead, so
it never paints over the border. Also added
`NooTopBar.androidTitleTrailing`: Android has no large title to put a
second search row under the way iOS's `search:` slot does, so an inline
search bar (`AppTopBar` passes a plain `ShellSearchLauncher()` when
search isn't in the bottom bar) sits to the title's own right in that
flexible slot - `Flexible`, not `Expanded`, so the title still shrinks/
ellipsizes if there's truly no room but doesn't claim more than it needs
otherwise - instead of the title being replaced by it.
- [`SyncedHeaderScaffold`](../../lib/widgets/synced_header_scaffold.dart) —
the pull-to-sync `CustomScrollView` header with the persistent sync-status
chip and pull-to-refresh gesture/spinner. Every screen (including
+6 -6
View File
@@ -16,9 +16,9 @@ import 'shell/shell_common.dart';
/// title, then an inline search field that opens [SearchView].
/// - Android: `menu`, title, avatar actions. Upload has no top-bar icon
/// here - it's the [NooFab] `MainShellView` shows instead. When search
/// isn't in the bottom bar, the title itself is replaced by an inline
/// search-field-styled launcher (`androidTitleReplacement`) rather than
/// a bare search icon sitting beside it.
/// isn't in the bottom bar, an inline search-field-styled launcher
/// (`androidTitleTrailing`) sits to the title's own right, rather than a
/// bare search icon over in `actions`.
///
/// [style] is resolved by the caller (`NooLayout.navStyle`) rather than
/// read from context here, since [preferredSize] is a plain getter with no
@@ -68,9 +68,9 @@ class AppTopBar extends StatelessWidget implements PreferredSizeWidget {
? () => Scaffold.of(context).openDrawer()
: null,
// Android has no large title to put a second search row under the
// way iOS does (`search:` below) - an inline bar replaces the title
// itself instead of sitting beside it as a plain icon action.
androidTitleReplacement: !_ios && _showSearch
// way iOS does (`search:` below) - an inline bar sits beside the
// title instead, rather than a plain search icon over in `actions`.
androidTitleTrailing: !_ios && _showSearch
? const ShellSearchLauncher()
: null,
actions: [
+22 -4
View File
@@ -75,9 +75,20 @@ class _AvatarMenuContent extends StatelessWidget {
child: SizedBox(
width: double.infinity,
child: Container(
clipBehavior: Clip.antiAlias,
// No `color`/`clipBehavior` here - a `Container` with both a
// `border` and `clipBehavior` set paints the border as part
// of its *outer* decoration, then paints its (clipped) child
// on top right up to that same boundary with no gap for the
// border's own stroke width to show through. An opaque child
// touching that edge - every row below has its own full-bleed
// `Material` fill - then paints straight over the inner half
// of the border, which read as the border going missing
// specifically wherever an opaque row sits (every corner but
// the two by the header, which has no opaque fill of its
// own). The 1px `Padding` + inset `ClipRRect` below keeps the
// clipped, filled content entirely inside the border's own
// stroke instead of racing it for the same pixels.
decoration: BoxDecoration(
color: colors.surface,
border: Border.all(color: colors.line),
borderRadius: BorderRadius.circular(NooRadii.card),
// `nooDialogShadow` alone is a wide, soft, fairly faint
@@ -99,8 +110,12 @@ class _AvatarMenuContent extends StatelessWidget {
nooDialogShadow,
],
),
child: Padding(
padding: const EdgeInsets.all(1),
child: ClipRRect(
borderRadius: BorderRadius.circular(NooRadii.card - 1),
child: Material(
color: Colors.transparent,
color: colors.surface,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -116,7 +131,8 @@ class _AvatarMenuContent extends StatelessWidget {
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
session.username,
@@ -180,6 +196,8 @@ class _AvatarMenuContent extends StatelessWidget {
),
),
),
),
),
);
}
}
+28 -12
View File
@@ -56,14 +56,15 @@ class NooTopBar extends StatelessWidget implements PreferredSizeWidget {
/// iOS only: the search field shown below the large title.
final Widget? search;
/// Android only: replaces the plain title text with an arbitrary widget
/// (e.g. a search-field-styled launcher) in that same flexible middle
/// slot, between [leading]/the menu button and [actions]. iOS already
/// has a dedicated [search] slot below its large title for this; Android
/// has no large title to put a second row under, so a caller that wants
/// an inline search bar there replaces the title with one instead of
/// adding a row.
final Widget? androidTitleReplacement;
/// Android only: an extra widget (e.g. a search-field-styled launcher) in
/// the flexible middle slot, to the title's own right - [title] keeps
/// its natural (shrink-to-fit, ellipsized) width on the left, and this
/// takes whatever space is left rather than replacing it. iOS already has
/// a dedicated [search] slot below its large title for this; Android has
/// no large title to put a second row under, so a caller that wants an
/// inline search bar there puts it beside the title instead of adding a
/// row.
final Widget? androidTitleTrailing;
/// Defaults to `bg` - the top bar sits on the screen background.
final Color? backgroundColor;
@@ -76,7 +77,7 @@ class NooTopBar extends StatelessWidget implements PreferredSizeWidget {
this.onMenu,
this.actions = const [],
this.search,
this.androidTitleReplacement,
this.androidTitleTrailing,
this.backgroundColor,
});
@@ -188,10 +189,25 @@ class NooTopBar extends StatelessWidget implements PreferredSizeWidget {
spacing: 4,
children: [
?lead,
// [androidTitleTrailing]'s own doc comment explains why this
// isn't a straight replacement: the title keeps its natural
// width (`Flexible`, not `Expanded` - it still shrinks/
// ellipsizes if there's truly no room, but doesn't force the
// row that wide otherwise) and sits to the trailing content's
// left rather than being displaced by it.
if (androidTitleTrailing != null) ...[
Flexible(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: NooText.title.copyWith(height: 1, color: colors.fg1),
),
),
Expanded(child: androidTitleTrailing!),
] else
Expanded(
child:
androidTitleReplacement ??
Text(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,