Noo design system rebuild #1
@@ -245,11 +245,23 @@ blocks are noted where they matter:
|
|||||||
near-black `bg` (a dark, diffuse shadow needs real density close to the
|
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,
|
edge to be visible against an already-dark backdrop), so a second,
|
||||||
tighter, more opaque contact shadow underneath it gives real elevation
|
tighter, more opaque contact shadow underneath it gives real elevation
|
||||||
in both themes. Also added `NooTopBar.androidTitleReplacement`: Android has no
|
in both themes. The card's border can go missing wherever an opaque row
|
||||||
large title to put a second search row under the way iOS's `search:`
|
sits against it too (every corner but the header's, which has no
|
||||||
slot does, so an inline search bar (`AppTopBar` passes a plain
|
full-bleed fill of its own) if a `Container` combines `border` with its
|
||||||
`ShellSearchLauncher()` when search isn't in the bottom bar) replaces the
|
own `clipBehavior` - that paints the border as part of the *outer*
|
||||||
title in its flexible slot instead of sitting beside it as a bare icon.
|
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) —
|
- [`SyncedHeaderScaffold`](../../lib/widgets/synced_header_scaffold.dart) —
|
||||||
the pull-to-sync `CustomScrollView` header with the persistent sync-status
|
the pull-to-sync `CustomScrollView` header with the persistent sync-status
|
||||||
chip and pull-to-refresh gesture/spinner. Every screen (including
|
chip and pull-to-refresh gesture/spinner. Every screen (including
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import 'shell/shell_common.dart';
|
|||||||
/// title, then an inline search field that opens [SearchView].
|
/// title, then an inline search field that opens [SearchView].
|
||||||
/// - Android: `menu`, title, avatar actions. Upload has no top-bar icon
|
/// - Android: `menu`, title, avatar actions. Upload has no top-bar icon
|
||||||
/// here - it's the [NooFab] `MainShellView` shows instead. When search
|
/// here - it's the [NooFab] `MainShellView` shows instead. When search
|
||||||
/// isn't in the bottom bar, the title itself is replaced by an inline
|
/// isn't in the bottom bar, an inline search-field-styled launcher
|
||||||
/// search-field-styled launcher (`androidTitleReplacement`) rather than
|
/// (`androidTitleTrailing`) sits to the title's own right, rather than a
|
||||||
/// a bare search icon sitting beside it.
|
/// bare search icon over in `actions`.
|
||||||
///
|
///
|
||||||
/// [style] is resolved by the caller (`NooLayout.navStyle`) rather than
|
/// [style] is resolved by the caller (`NooLayout.navStyle`) rather than
|
||||||
/// read from context here, since [preferredSize] is a plain getter with no
|
/// 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()
|
? () => Scaffold.of(context).openDrawer()
|
||||||
: null,
|
: null,
|
||||||
// Android has no large title to put a second search row under the
|
// Android has no large title to put a second search row under the
|
||||||
// way iOS does (`search:` below) - an inline bar replaces the title
|
// way iOS does (`search:` below) - an inline bar sits beside the
|
||||||
// itself instead of sitting beside it as a plain icon action.
|
// title instead, rather than a plain search icon over in `actions`.
|
||||||
androidTitleReplacement: !_ios && _showSearch
|
androidTitleTrailing: !_ios && _showSearch
|
||||||
? const ShellSearchLauncher()
|
? const ShellSearchLauncher()
|
||||||
: null,
|
: null,
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -75,9 +75,20 @@ class _AvatarMenuContent extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Container(
|
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(
|
decoration: BoxDecoration(
|
||||||
color: colors.surface,
|
|
||||||
border: Border.all(color: colors.line),
|
border: Border.all(color: colors.line),
|
||||||
borderRadius: BorderRadius.circular(NooRadii.card),
|
borderRadius: BorderRadius.circular(NooRadii.card),
|
||||||
// `nooDialogShadow` alone is a wide, soft, fairly faint
|
// `nooDialogShadow` alone is a wide, soft, fairly faint
|
||||||
@@ -99,81 +110,88 @@ class _AvatarMenuContent extends StatelessWidget {
|
|||||||
nooDialogShadow,
|
nooDialogShadow,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Material(
|
child: Padding(
|
||||||
color: Colors.transparent,
|
padding: const EdgeInsets.all(1),
|
||||||
child: Column(
|
child: ClipRRect(
|
||||||
mainAxisSize: MainAxisSize.min,
|
borderRadius: BorderRadius.circular(NooRadii.card - 1),
|
||||||
children: [
|
child: Material(
|
||||||
// Avatar on the right, name/host on the left - mirrors the
|
color: colors.surface,
|
||||||
// top bar's own right-aligned avatar exactly, so this
|
child: Column(
|
||||||
// header reads as a continuation of the button that
|
mainAxisSize: MainAxisSize.min,
|
||||||
// opened it rather than a disconnected card.
|
children: [
|
||||||
InkWell(
|
// Avatar on the right, name/host on the left - mirrors the
|
||||||
onTap: closeAndOpenSettings,
|
// top bar's own right-aligned avatar exactly, so this
|
||||||
child: Padding(
|
// header reads as a continuation of the button that
|
||||||
padding: const EdgeInsets.all(NooSpace.md),
|
// opened it rather than a disconnected card.
|
||||||
child: Row(
|
InkWell(
|
||||||
children: [
|
onTap: closeAndOpenSettings,
|
||||||
Expanded(
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(NooSpace.md),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
session.username,
|
child: Column(
|
||||||
maxLines: 1,
|
crossAxisAlignment:
|
||||||
overflow: TextOverflow.ellipsis,
|
CrossAxisAlignment.start,
|
||||||
style: NooText.bodyL.copyWith(
|
children: [
|
||||||
fontWeight: FontWeight.w600,
|
Text(
|
||||||
color: colors.fg1,
|
session.username,
|
||||||
),
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: NooText.bodyL.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.fg1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
serverHost(session.serverUrl),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: NooText.meta.copyWith(
|
||||||
|
color: colors.fg3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
),
|
||||||
Text(
|
const SizedBox(width: 12),
|
||||||
serverHost(session.serverUrl),
|
NooAvatar(
|
||||||
maxLines: 1,
|
initials: accountInitial(session.username),
|
||||||
overflow: TextOverflow.ellipsis,
|
current: true,
|
||||||
style: NooText.meta.copyWith(
|
size: 40,
|
||||||
color: colors.fg3,
|
),
|
||||||
),
|
],
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
),
|
||||||
NooAvatar(
|
|
||||||
initials: accountInitial(session.username),
|
|
||||||
current: true,
|
|
||||||
size: 40,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
if (hiddenTabs.isNotEmpty) ...[
|
||||||
),
|
Divider(height: 1, color: colors.line),
|
||||||
if (hiddenTabs.isNotEmpty) ...[
|
for (final tab in hiddenTabs)
|
||||||
Divider(height: 1, color: colors.line),
|
NooSettingsRow(
|
||||||
for (final tab in hiddenTabs)
|
icon: tab.icon,
|
||||||
|
label: Text(tab.label),
|
||||||
|
trailing: tab == AppTab.trash && trashCount > 0
|
||||||
|
? NooBadge(
|
||||||
|
tone: NooBadgeTone.accent,
|
||||||
|
child: Text('$trashCount'),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
settings.requestTab(tab);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Divider(height: 1, color: colors.line),
|
||||||
NooSettingsRow(
|
NooSettingsRow(
|
||||||
icon: tab.icon,
|
icon: LucideIcons.settings,
|
||||||
label: Text(tab.label),
|
label: const Text('Settings'),
|
||||||
trailing: tab == AppTab.trash && trashCount > 0
|
onTap: closeAndOpenSettings,
|
||||||
? NooBadge(
|
|
||||||
tone: NooBadgeTone.accent,
|
|
||||||
child: Text('$trashCount'),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
settings.requestTab(tab);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Divider(height: 1, color: colors.line),
|
|
||||||
NooSettingsRow(
|
|
||||||
icon: LucideIcons.settings,
|
|
||||||
label: const Text('Settings'),
|
|
||||||
onTap: closeAndOpenSettings,
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -56,14 +56,15 @@ class NooTopBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// iOS only: the search field shown below the large title.
|
/// iOS only: the search field shown below the large title.
|
||||||
final Widget? search;
|
final Widget? search;
|
||||||
|
|
||||||
/// Android only: replaces the plain title text with an arbitrary widget
|
/// Android only: an extra widget (e.g. a search-field-styled launcher) in
|
||||||
/// (e.g. a search-field-styled launcher) in that same flexible middle
|
/// the flexible middle slot, to the title's own right - [title] keeps
|
||||||
/// slot, between [leading]/the menu button and [actions]. iOS already
|
/// its natural (shrink-to-fit, ellipsized) width on the left, and this
|
||||||
/// has a dedicated [search] slot below its large title for this; Android
|
/// takes whatever space is left rather than replacing it. iOS already has
|
||||||
/// has no large title to put a second row under, so a caller that wants
|
/// a dedicated [search] slot below its large title for this; Android has
|
||||||
/// an inline search bar there replaces the title with one instead of
|
/// no large title to put a second row under, so a caller that wants an
|
||||||
/// adding a row.
|
/// inline search bar there puts it beside the title instead of adding a
|
||||||
final Widget? androidTitleReplacement;
|
/// row.
|
||||||
|
final Widget? androidTitleTrailing;
|
||||||
|
|
||||||
/// Defaults to `bg` - the top bar sits on the screen background.
|
/// Defaults to `bg` - the top bar sits on the screen background.
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
@@ -76,7 +77,7 @@ class NooTopBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.onMenu,
|
this.onMenu,
|
||||||
this.actions = const [],
|
this.actions = const [],
|
||||||
this.search,
|
this.search,
|
||||||
this.androidTitleReplacement,
|
this.androidTitleTrailing,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,16 +189,31 @@ class NooTopBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
?lead,
|
?lead,
|
||||||
Expanded(
|
// [androidTitleTrailing]'s own doc comment explains why this
|
||||||
child:
|
// isn't a straight replacement: the title keeps its natural
|
||||||
androidTitleReplacement ??
|
// width (`Flexible`, not `Expanded` - it still shrinks/
|
||||||
Text(
|
// ellipsizes if there's truly no room, but doesn't force the
|
||||||
title,
|
// row that wide otherwise) and sits to the trailing content's
|
||||||
maxLines: 1,
|
// left rather than being displaced by it.
|
||||||
overflow: TextOverflow.ellipsis,
|
if (androidTitleTrailing != null) ...[
|
||||||
style: NooText.title.copyWith(height: 1, color: colors.fg1),
|
Flexible(
|
||||||
),
|
child: Text(
|
||||||
),
|
title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: NooText.title.copyWith(height: 1, color: colors.fg1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: androidTitleTrailing!),
|
||||||
|
] else
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: NooText.title.copyWith(height: 1, color: colors.fg1),
|
||||||
|
),
|
||||||
|
),
|
||||||
...actions,
|
...actions,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user