Trash and Shares built their mobile lists eagerly via NooGroupedList's own Column instead of a lazy sliver, so an account with hundreds of items made those tabs laggy - both now use a SliverList/ SliverChildBuilderDelegate instead, matching Files' own lazy list, while keeping the same one-continuous-card look via per-row corner rounding and a 1px divider. FilesControlsRow's sort/filter sheets also had a live-update bug: since showNooSheet's children are built once up front, tapping a row inside the open sheet updated the controller but the already-built checkmarks/toggles stayed stale until the sheet was closed and reopened. Wrapping each sheet's content in a ListenableBuilder fixes that. Also adds NooSelectionBar to styling.md's widget inventory table.
15 KiB
Styling
Migration in progress: the target look is
design-system/DESIGN_SYSTEM.md — warm
neutrals, one violet accent, pill controls, Schibsted Grotesk/Instrument
Sans, no gradients/shadows — built from the widgets/noo/ component kit
(see "Noo design-system components" below). Rebuilt so far: the app shell
(top/bottom bars, drawer, sidebar, toolbar), Files/Offline, Photos,
Favorites, Recent, Activity, Trash, Shares, Settings, the lock screen,
login, ShareUploadView, MoveCopyDestinationPicker,
MoveCopyConflictSheet, the shared Breadcrumbs widget, and the file
details/share bottom sheets/dialogs (DetailsSheet/ShareSheet). Still on
the pre-rework Material 3 theme documented in "Theme" and "Reusable
chrome" below: FileViewerScreen (the media viewer), SearchView, and
LoginWebviewView. Update this file to describe each area as it gets
reworked, rather than leaving stale Material 3 guidance next to a design
system that's already superseded it.
Theme
All theming goes through AppTheme
(AppTheme.light/AppTheme.dark) — don't set colors/fonts ad hoc in
widgets. Key points:
- Material 3, seed-color based.
AppTheme.seedColorsis the picker list users choose from;defaultNextcloudBlue(#0082C9) is the fallback. - Dynamic color (Android 12+ Material You / desktop accent color) is
supported via
package:dynamic_color'sDynamicColorBuilderwrapping the whole app inmain.dart. When available anduseDynamicColoris on, the OS-providedColorSchemewins over the seed color — always thread bothdynamicSchemeanduseDynamicColorthrough when adding a theme knob. - Font: Inter via
google_fonts, applied throughGoogleFonts.interTextTheme(...). In tests, setGoogleFonts.config.allowRuntimeFetching = falseinsetUpAll— without it, the font-fetch call to Google's CDN can stallpumpAndSettleindefinitely (seestandards.md). - Cards: flat (
elevation: 0), 20px rounded corners,surfaceContainerLow. - App bars: flat, not centered,
surfacebackground. - Two Flutter defaults are deliberately overridden app-wide rather than
per-widget, each with a comment explaining why in
app_theme.dart: Android predictive-back page transitions, and the non-2023SliderTheme. Follow that pattern (a themed default + a comment) instead of overriding per-instance if you need the same behavior elsewhere. - Dark theme supports an
amoledflag that flattens every surface tone to pure black — extendcolorScheme.copyWith(...)there if a new surface role needs the same treatment, don't hardcodeColors.blackat call sites. - Scrollbars are deliberately not shown — every scrollable list in the
app is a plain
ListView/CustomScrollViewwith noScrollbarwrapper and noscrollbarThemeoverride (tried once, explicitly reverted). Don't reintroduce one without being asked.
Noo design-system components
The rebuilt UI is assembled from lib/widgets/noo/, which implements
DESIGN_SYSTEM.md §2–3. Build new screens from these rather than from raw
Material widgets or the pre-rework chrome below.
- Tokens live in
design_tokens.dart: colors viacontext.nooColors(aNooColorsThemeExtension), plusNooText,NooSpace,NooRadii,NooSizes,NooMotionandnooDialogShadow.NooTextstyles set no color; callers add it withcopyWith(color: ...). Flutter'sTextStyle.heightis a multiple of font size, so the spec's CSS line-height maps to it directly (for example, 0.9 →height: 0.9). - Icons are Lucide (
lucide_icons_flutter,LucideIcons.*) insidenoo/, never MaterialIcons.*. - Components are data-agnostic. They take strings, icons, colors and
callbacks, not
NextcloudItem/AppTab, so the screen layer maps models onto them. - Platform differences are chosen with a flag rather than by reading the
platform inside the widget:
NooNavStyle(ios/android, withNooNavStyle.fromPlatform) for nav chrome, andiosStyleon rows and cards for the ellipsis vs. vertical-ellipsis overflow icon.
Catalog:
| Folder | Components |
|---|---|
core/ |
NooButton, NooFab, NooChip, NooSegmentedControl, NooToggle, NooSearchField, NooAvatar, NooBadge, NooProgressBar |
lists/ |
NooGroupedList, NooSettingsRow, NooTabOrderRow, NooBanner, NooSummaryCard, NooSelectionBar |
files/ |
NooFileKind (spec §1.2 tiles; NooFileKind.from(name:, mimeType:, isDirectory:)), NooFileTile, NooStatusIcon/NooSyncStatus, NooFileRow (mobile 64px), NooFileTableHeader/NooFileTableRow (desktop), NooSwipeAction |
media/ |
NooGridCard, NooPhotoTile (video badge, selection), NooPhotoGroupHeader/NooPhotoGrid (sliver, or .box), NooActivityItem, NooStatCard |
nav/ |
NooBottomBar, NooTopBar (a PreferredSizeWidget) with NooTopBarButton/NooTopBarBack, NooDrawer with its Account/Storage/Item/Link parts, NooSidebar with NooSidebarItem/Divider/Account/Storage, NooToolbar |
overlays/ |
showNooSheet, showNooDialog/NooDialog, NooOverlayHeader, NooTextField, and the share parts NooShareSection, NooPersonAccessRow, NooPermissionPill |
Gotchas:
NooDrawercan't set its own scrim. The hostScaffoldneedsdrawerScrimColor: context.nooColors.scrim.NooGroupedListdraws dividers by showinglinethrough 1px gaps, so each child must paint its own surface (NooSettingsRowandNooTabOrderRowdo).NooSwipeActiononly reveals its action. The user has to tap the block to trigger it; a full swipe never deletes.- Window chrome (macOS traffic lights, the Windows 40px title bar) isn't
built yet.
NooSidebar.windowControlsis the slot for it.
Reusable chrome
Rebuilt on the Noo design system (Files/Offline, Photos, Favorites,
Recent, Activity, Trash, Shares, Settings, the app shell, lock screen,
login, ShareUploadView, MoveCopyDestinationPicker, DetailsSheet and
ShareSheet): these no longer use the pieces below. Their own building
blocks are noted where they matter:
FilesControlsRow(lib/widgets/files_controls_row.dart) — now built fromNooChip/NooSegmentedControl; still the sort/hidden/scope/type-filter/ view-mode row shared by Files and Offline (showStorageScope: falsefor Offline), and reused as-is by Favorites.lib/widgets/files/file_breadcrumb_row.dart— the noo-styled breadcrumb trail Files uses in place of the sharedBreadcrumbswidget.Breadcrumbsis now also noo-styled (same tokens, private-widget-turned-shared) — it's used only byShareUploadView/MoveCopyDestinationPicker, which is why it was safe to restyle directly instead of forking anotherFileBreadcrumbRow-style copy; don't move Files back onto it.lib/widgets/tabs/(tab_state_slivers.dart,tab_day_groups.dart,tab_location.dart) — the loading/error/empty-state slivers and day/month grouping helpers shared by Recent/Activity/Trash/Shares.lib/widgets/settings/— Settings' 8 section widgets plussettings_section.dart'sSettingsSection/showSettingsPickerandsettings_dialogs.dart'sconfirmRemoveAccount.lib/widgets/shell/shell_common.dart— account/storage formatting,openSettings/openSearch,showAccountSwitcher,ShellAvatarButton,ShellSearchLauncher, shared by the mobile and desktop shell chrome.getItemIcon/getIconColor/ItemThumbnail—getItemIcon/getIconColorare superseded byNooFileKindin rebuilt screens;ItemThumbnailis still reused as-is, fed intoNooFileTile/NooFileRow/NooFileTableRow/NooGridCard'sthumbnailslot.ShareUploadView/MoveCopyDestinationPickerdon't use any of the three any more - both destination pickers now list folders only (see below), and a folder never gets a real thumbnail (only image/video do), so a plainNooFileTile(kind: NooFileKind.folder)covers every row.DetailsSheet/ShareSheet(lib/widgets/details/details_sheet.dart,lib/widgets/share_sheet.dart) are rebuilt:showNooSheet/showNooDialogperNooLayout.isDesktop, with the header built fromdetailsFileTile(aNooFileTilekeyed byNooFileKind.from) anddetailsMetaLine- shared top-level helpers indetails_sheet.dartso both sheets open on the same header, replacing the oldDetailsHeaderwidget. Neither sheet usesshowGradualBottomSheet's drag-to-resize any more:DetailsSheetswaps Info/Versions/Activity with aNooSegmentedControl(there's no tab-strip component in the noo kit) instead of aTabBar/TabBarView, and both sheets' content sits in oneColumnsoshowNooSheet's/showNooDialog's ownSingleChildScrollViewhandles overflow - a fixed page per tab no longer needs a resizable sheet to see the rest.showGradualBottomSheet(lib/widgets/gradual_bottom_sheet.dart) has no remaining callers as a result.ShareSheet's "Share with people"/"Share link"/"Send file directly" sections followDESIGN_SYSTEM.md§4 viaNooShareSection/NooPersonAccessRow/NooPermissionPill; the permission pill and the link's permission/expiry chips are read-only display (noItemOperationscall updates a share's permission/expiry/password/ hide-download yet) - promote those to real controls once that exists. Removing a person/group/email share is reachable by tapping their permission pill, which opens a small "Remove access" menu.MoveCopyConflictSheetis rebuilt:showNooDialog/showNooSheet(perNooLayout.isDesktop), aNooGroupedListof file-tile rows with an inlineNooSegmentedControl<ConflictChoice>once "Decide per item" is picked, andNooButtons for overwrite-all/keep-both/decide-per-item/confirm. Itsshow(BuildContext, List<MoveCopyConflict>)API is unchanged.FrostedGlassContainer— the blurred/translucent pill background for the media viewer's top/bottom bars and video transport controls (its other former user, the floating bottom nav bar, is gone - see below). Reuse this for any new floating overlay instead of building a new blur/shadow combo.SettingsController.bottomBarOpacity/bottomBarBlurandlib/widgets/floating_bottom_bar.dart/media_grid_tile.dart/swipeable_item.dart/sync_status_badge.dart/selectable_thumbnail.dartare gone: the bottom bar is now the flat, non-blurredNooBottomBar(no opacity/blur knob - flat surfaces per the design system), grid tiles areNooGridCard, swipe actions areNooSwipeAction, and per-item sync status isNooFileRow/NooFileTableRow's built-inNooStatusIconlist instead of a corner badge.SyncedHeaderScaffold— the pull-to-syncCustomScrollViewheader with the persistent sync-status chip and pull-to-refresh gesture/spinner. Every screen (includingShareUploadView/MoveCopyDestinationPicker, its last two users) has dropped it for a plainRefreshIndicator+CustomScrollView(device-sync status now shows per-row viaNooStatusIcon/the OfflineNooSummaryCard, not a shared header chip), so theSyncedHeaderScaffoldclass itself is now dead code - kept only because the same file's top-levelformatByteshelper is still widely used (files_view.dart/favorites_view.dart/shell_common.dart/widgets/details/*).SegmentedIconGroup/ToggleIconButtonandSortMenuButton— the Material sort/filter-chip piecesShareUploadView/MoveCopyDestinationPickerused to mirror Files' old controls row with. Both destination pickers dropped that whole row (they only ever browse folders, so sort/hidden/scope/type-filter/grid controls don't apply), so these two files are now dead code too -Breadcrumbsis the only shared widget promoted to noo styling instead of removed, since Files' ownFileBreadcrumbRowproved the same trail is still wanted elsewhere.SeekBarPainter/SeekBarPreview— the fourMediaProgressBarStylepresets (Default/Wavy/Slim/Squiggly) for the video player's seek bar, plus a perpetually-animatedSeekBarPreviewwrapper used by the Settings style picker so every preview always matches the real widget exactly (same painter, just fed demoprogress/phasevalues). Add new seek-bar presets here, not by forking the painter.- Chrome inside the media viewer (
file_viewer_screen.dart— the top bar's back button + filename, the bottom action bar, the video transport controls) all share one small hand-rolled icon-button pattern (_ActionIconButton:InkWell+Iconat a fixed 22px, colored fromcolorScheme.onSurfaceunless overridden) rather than plainIconButtons — match this instead of adding a bareIconButtonin that screen, since a default-styled one visibly stands out against the rest (this was a real bug: an unstyled back button read as "too large" next to everything else). - A title/label that might overflow a fixed-width chrome bar (e.g. the media
viewer's filename) should use
_MarqueeTitle-style logic — measure withTextPainterfirst and only switch to a scrollingMarqueewhen the text actually doesn't fit, rather than marqueeing unconditionally. - Icons: prefer
Icons.*_rounded(matches the rest of the app) ormaterial_symbols_iconswhere Material Symbols are already in use; avoid mixing in the sharp/outlined default set.
Conventions
- No hardcoded colors for anything themeable — pull from
Theme.of(context).colorScheme, notColors.blueetc. (per-file-type icon tinting infiles_view.dart's_getIconColoris the one deliberate exception, since those colors are content-identity cues, not theme). - Use
colorScheme.surfaceContainer*/onSurfaceVarianttokens for elevation/secondary text rather than manual opacity on black/white. - Anywhere the app shows its own icon in-app (splash, lock screen,
login screen) uses
assets/icon/app_icon_monochrome.png— a plain white silhouette on transparent, tinted viaColorFiltered(colorFilter: ColorFilter.mode(colorScheme.onSurface, BlendMode.srcIn), ...)so it reads correctly in both light and dark mode. Never the full-colorapp_icon.png/adaptive-icon assets for in-app UI — those are for the launcher icon only. The launcher icon is maintained by hand from an IconKitchen export (mipmap-*inandroid/app/src/main/res); there is no generator step.assets/icon/app_icon_monochrome.pngis deliberately a tightly-cropped glyph (unlike the launcher's safe-zone-padded monochrome layer), so it renders at a sensible size at 72-80px.