# Styling **Migration in progress:** the target look is [`design-system/DESIGN_SYSTEM.md`](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`](../../lib/theme/app_theme.dart) (`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`](../../lib/theme/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` | | `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). - `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`](../../lib/widgets/breadcrumbs.dart) 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`](../../lib/widgets/item_icon.dart) — `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`](../../lib/widgets/move_copy_conflict_sheet.dart) is rebuilt: `showNooDialog`/`showNooSheet` (per `NooLayout.isDesktop`), a `NooGroupedList` of file-tile rows with an inline `NooSegmentedControl` once "Decide per item" is picked, and `NooButton`s for overwrite-all/keep-both/decide-per-item/confirm. Its `show(BuildContext, List)` API is unchanged. - [`FrostedGlassContainer`](../../lib/widgets/frosted_glass_container.dart) — 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`](../../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 `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`](../../lib/widgets/segmented_icon_toggle.dart) and [`SortMenuButton`](../../lib/widgets/sort_menu_button.dart) — 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`](../../lib/widgets/seek_bar_painter.dart) — 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 `IconButton`s — 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.