Files
noo/.claude/context/styling.md
T
ayushyaandClaude Sonnet 5 f9653bc0f2 Fix share copy-link, destination-picker filters, and type-filter UX
- SharesController's fetchShares() built NextcloudShare objects through
  its own thin inline parser instead of _shareFromJson, so url/token/
  permissions/expireDate were always null - a public link share could
  never show "Copy link" in the Shares tab. Reuses _shareFromJson.
- Upload/Move/Copy's destination picker hid External storage in its
  filter sheet (showStorageScope: false) unlike Files' own controls
  row, despite the design doc already calling for full parity.
- Files' and Photos' type filter (All/Files/Folders, All/Photos/Videos)
  now render as one consistent style in both filter sheets: every icon
  always shown, label only on the selected segment
  (NooSegmentedControl's new labelOnlySelected), replacing Files' old
  checkmark list and Photos' own always-labelled track.
- Fixes Photos' type filter only visually updating after closing and
  reopening the filter sheet - it lived on PhotosView's own State, so
  Listenable.merge([photos, files]) never rebuilt the sheet when it
  changed; a StatefulBuilder now gives it that trigger.
- Replaces Settings' trailing section jump rail with per-section
  collapsible cards (NooGroupedList's new collapsible param, expanded
  by default) - one less parallel way to navigate a long screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:49:24 -04:00

15 KiB
Raw Blame History

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.seedColors is 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's DynamicColorBuilder wrapping the whole app in main.dart. When available and useDynamicColor is on, the OS-provided ColorScheme wins over the seed color — always thread both dynamicScheme and useDynamicColor through when adding a theme knob.
  • Font: Inter via google_fonts, applied through GoogleFonts.interTextTheme(...). In tests, set GoogleFonts.config.allowRuntimeFetching = false in setUpAll — without it, the font-fetch call to Google's CDN can stall pumpAndSettle indefinitely (see standards.md).
  • Cards: flat (elevation: 0), 20px rounded corners, surfaceContainerLow.
  • App bars: flat, not centered, surface background.
  • 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-2023 SliderTheme. Follow that pattern (a themed default + a comment) instead of overriding per-instance if you need the same behavior elsewhere.
  • Dark theme supports an amoled flag that flattens every surface tone to pure black — extend colorScheme.copyWith(...) there if a new surface role needs the same treatment, don't hardcode Colors.black at call sites.
  • Scrollbars are deliberately not shown — every scrollable list in the app is a plain ListView/CustomScrollView with no Scrollbar wrapper and no scrollbarTheme override (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 via context.nooColors (a NooColors ThemeExtension), plus NooText, NooSpace, NooRadii, NooSizes, NooMotion and nooDialogShadow. NooText styles set no color; callers add it with copyWith(color: ...). Flutter's TextStyle.height is 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.*) inside noo/, never Material Icons.*.
  • 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, with NooNavStyle.fromPlatform) for nav chrome, and iosStyle on 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:

  • NooDrawer can't set its own scrim. The host Scaffold needs drawerScrimColor: context.nooColors.scrim.
  • NooGroupedList draws dividers by showing line through 1px gaps, so each child must paint its own surface (NooSettingsRow and NooTabOrderRow do). Its collapsible/initiallyExpanded params (off by default) make label a tap target that shows/hides the card - SettingsSection is the only caller that opts in, for Settings' mobile sections.
  • NooSwipeAction only 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.windowControls is 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 from NooChip/NooSegmentedControl; still the sort/hidden/scope/type-filter/ view-mode row shared by Files and Offline (showStorageScope: false for 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 shared Breadcrumbs widget. Breadcrumbs is now also noo-styled (same tokens, private-widget-turned-shared) — it's used only by ShareUploadView/MoveCopyDestinationPicker, which is why it was safe to restyle directly instead of forking another FileBreadcrumbRow-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 plus settings_section.dart's SettingsSection/showSettingsPicker and settings_dialogs.dart's confirmRemoveAccount. 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/getIconColor are superseded by NooFileKind in rebuilt screens; ItemThumbnail is still reused as-is, fed into NooFileTile/NooFileRow/NooFileTableRow/NooGridCard's thumbnail slot. ShareUploadView/MoveCopyDestinationPicker don'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 plain NooFileTile(kind: NooFileKind.folder) covers every row.
  • DetailsSheet/ShareSheet (lib/widgets/details/details_sheet.dart, lib/widgets/share_sheet.dart) are rebuilt: showNooSheet/showNooDialog per NooLayout.isDesktop, with the header built from detailsFileTile (a NooFileTile keyed by NooFileKind.from) and detailsMetaLine - shared top-level helpers in details_sheet.dart so both sheets open on the same header, replacing the old DetailsHeader widget. Neither sheet uses showGradualBottomSheet's drag-to-resize any more: DetailsSheet swaps Info/Versions/Activity with a NooSegmentedControl (there's no tab-strip component in the noo kit) instead of a TabBar/TabBarView, and both sheets' content sits in one Column so showNooSheet's/ showNooDialog's own SingleChildScrollView handles 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 follow DESIGN_SYSTEM.md §4 via NooShareSection/ NooPersonAccessRow/NooPermissionPill; the permission pill and the link's permission/expiry chips are read-only display (no ItemOperations call 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. MoveCopyConflictSheet is rebuilt: showNooDialog/showNooSheet (per NooLayout.isDesktop), a NooGroupedList of file-tile rows with an inline NooSegmentedControl<ConflictChoice> once "Decide per item" is picked, and NooButtons for overwrite-all/keep-both/decide-per-item/confirm. Its show(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/bottomBarBlur and lib/widgets/floating_bottom_bar.dart/media_grid_tile.dart/ swipeable_item.dart/sync_status_badge.dart/selectable_thumbnail.dart are gone: the bottom bar is now the flat, non-blurred NooBottomBar (no opacity/blur knob - flat surfaces per the design system), grid tiles are NooGridCard, swipe actions are NooSwipeAction, and per-item sync status is NooFileRow/NooFileTableRow's built-in NooStatusIcon list instead of a corner badge.
  • SyncedHeaderScaffold — the pull-to-sync CustomScrollView header with the persistent sync-status chip and pull-to-refresh gesture/spinner. Every screen (including ShareUploadView/MoveCopyDestinationPicker, its last two users) has dropped it for a plain RefreshIndicator + CustomScrollView (device-sync status now shows per-row via NooStatusIcon/the Offline NooSummaryCard, not a shared header chip), so the SyncedHeaderScaffold class itself is now dead code - kept only because the same file's top-level formatBytes helper is still widely used (files_view.dart/favorites_view.dart/shell_common.dart/ widgets/details/*).
  • SegmentedIconGroup/ToggleIconButton and SortMenuButton — the Material sort/filter-chip pieces ShareUploadView/ MoveCopyDestinationPicker used 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 - Breadcrumbs is the only shared widget promoted to noo styling instead of removed, since Files' own FileBreadcrumbRow proved the same trail is still wanted elsewhere.
  • SeekBarPainter/SeekBarPreview — the four MediaProgressBarStyle presets (Default/Wavy/Slim/Squiggly) for the video player's seek bar, plus a perpetually-animated SeekBarPreview wrapper used by the Settings style picker so every preview always matches the real widget exactly (same painter, just fed demo progress/phase values). 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 + Icon at a fixed 22px, colored from colorScheme.onSurface unless overridden) rather than plain IconButtons — match this instead of adding a bare IconButton in 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 with TextPainter first and only switch to a scrolling Marquee when the text actually doesn't fit, rather than marqueeing unconditionally.
  • Icons: prefer Icons.*_rounded (matches the rest of the app) or material_symbols_icons where 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, not Colors.blue etc. (per-file-type icon tinting in files_view.dart's _getIconColor is the one deliberate exception, since those colors are content-identity cues, not theme).
  • Use colorScheme.surfaceContainer*/onSurfaceVariant tokens 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 via ColorFiltered(colorFilter: ColorFilter.mode(colorScheme.onSurface, BlendMode.srcIn), ...) so it reads correctly in both light and dark mode. Never the full-color app_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-* in android/app/src/main/res); there is no generator step. assets/icon/app_icon_monochrome.png is deliberately a tightly-cropped glyph (unlike the launcher's safe-zone-padded monochrome layer), so it renders at a sensible size at 72-80px.