# Home Tab Architecture

> **Note:** Do not add a file structure section to this document. File layout is navigable directly in the project and goes stale quickly.

## Pattern: MVVM + Coordinator + Domain Controllers

Each screen in Home Tab follows MVVM with a single shared Coordinator that handles navigation and page view lifecycle. Domain state lives in `HomeTabStore` and its sub-controllers, which are independent of the UI layer. `UIFactory` centralises all view and ViewModel construction.

```mermaid
graph TD
    Feature["HomeTabFeature\n(long-lived owner)"]
    Controller["HomeTabStore"]
    Documents["DocumentsStore"]
    Thumbnails["ThumbnailStore"]
    Tools["ToolsStore"]
    Factory["UIFactory"]
    Coord["HomeTabCoordinator"]
    NavState["HomeTabNavigationStateContainer"]
    BannerCtrl["HomeTabBannerController"]
    VC["HomeTabViewController\n(lifecycle root)"]
    VM["HomeTabViewModel"]
    Cache["pages cache\n[HomeTabPage → NSView]"]
    Hosting["NSHostingView"]
    PageView["PageView (SwiftUI)"]
    PageVM["PageViewModel"]

    Feature -->|strong| Controller
    Controller -->|strong| Documents
    Controller -->|strong| Tools
    Documents -->|strong| Thumbnails
    Thumbnails -->|weak delegate| Documents

    Feature -->|creates per call| Factory
    Feature -->|creates per call| BannerCtrl
    Factory -->|strong| Controller
    Factory -->|strong| BannerCtrl
    Feature -->|creates per call| Coord
    Coord -->|strong| Factory
    Coord -->|strong| NavState
    Coord -->|strong| Cache

    Factory -->|creates| VC
    Factory -->|creates| VM

    VC -->|strong| Coord
    VC -->|strong| VM

    VM -->|weak| Coord
    VM -->|strong| BannerCtrl
    VM -->|subscribes| NavState

    Cache -->|strong| Hosting
    Hosting -->|strong| PageView
    PageView -->|strong| PageVM

    PageVM -->|weak| Coord
    PageVM -->|strong| Controller
    PageVM -.->|subscribes (home page only)| BannerCtrl
```

## Ownership Rules

**`HomeTabViewController` is the lifecycle root** for a single tab instance. It holds strong references to both the coordinator and the view model. Domain state lives longer — it is owned by `HomeTabFeature`.

| Reference | Strength | Reason |
|---|---|---|
| `HomeTabFeature` → `HomeTabStore` | **strong** | Domain state outlives any individual VC instance. |
| `HomeTabFeature` → `UIFactory` | transient (per call) | Feature creates a factory per `makeHomeTabViewController()` call and hands it to the coordinator. |
| `HomeTabFeature` → `HomeTabBannerController` | transient (per call) | Same lifecycle as `UIFactory` — one banner controller per `makeHomeTabViewController()` call. |
| `HomeTabFeature` → `HomeTabFeatureGatewayProtocol` | **strong** | Feature holds the host-provided gateway for the lifetime of the feature. |
| `HomeTabCoordinator` → `HomeTabFeatureGatewayProtocol` | **weak** | Coordinator forwards out-of-framework actions; the gateway is owned by the host, not the coordinator. |
| `HomeTabCoordinator` → `UIFactory` | **strong** | Coordinator delegates all creation to the factory for the duration of its lifetime. |
| `UIFactory` → `HomeTabStore` | **strong** | Factory injects the controller into every ViewModel it creates. |
| `UIFactory` → `HomeTabBannerController` | **strong** | Factory injects the banner controller into ViewModels that need it. |
| `HomeTabCoordinator` → `HomeTabNavigationStateContainer` | **strong** | Coordinator owns the navigation state and drives page transitions. |
| `HomeTabViewController` → `HomeTabCoordinator` | **strong** | Retention anchor. Coordinator lives as long as the VC. |
| `HomeTabViewController` → `HomeTabViewModel` | **strong** | VC owns its model. |
| `HomeTabViewModel` → `HomeTabCoordinator` | **weak** | VM calls `setCurrentPage` on the coordinator, but must not extend its lifetime. |
| `HomeTabViewModel` → `HomeTabBannerController` | **strong** | VM proxies `setBannerView(_:)` from the public VC API to the controller. |
| `HomeTabViewModel` → `HomeTabNavigationStateContainer` | subscription (`AnyCancellable`) | VM observes page changes; subscription is cancelled when VM is deallocated. |
| `HomeTabCoordinator` → page `NSView` cache | **strong** | Coordinator is responsible for page view lifecycle. |
| `PageViewModel` → `HomeTabCoordinator` | **weak** | Coordinator owns the page view (via cache) which owns the ViewModel — strong would be a retain cycle. |
| `PageViewModel` → `HomeTabStore` | **strong** | ViewModels read and mutate domain state. `HomeTabStore` is not owned by the VC, so no cycle. |
| `HomePageViewModel` → `HomeTabBannerController` | **strong** + subscription (`AnyCancellable`) | Home page reads `presentedBannerView` and subscribes for change notifications to refresh its SwiftUI body. |

## Creation Flow

`HomeTabFeature` creates `HomeTabStore` once and keeps it alive. Each call to `makeHomeTabViewController()` creates a fresh `UIFactory` and coordinator pair backed by the shared controller:

```swift
// HomeTabFeature.makeHomeTabViewController()
let bannerController = HomeTabBannerController()
let factory = UIFactory(store: self.store, bannerController: bannerController)
let coordinator = HomeTabCoordinator(factory: factory, gateway: self.gateway)
return coordinator.start()

// HomeTabCoordinator.start()
factory.makeHomeTabViewController(coordinator: self, navigationState: self.navigationState)

// UIFactory.makeHomeTabViewController(coordinator:navigationState:)
let model = HomeTabViewModel(coordinator: coordinator, store: self.store, bannerController: self.bannerController, navigationState: navigationState)
return HomeTabViewController(coordinator: coordinator, model: model)
```

Navigation state is decoupled via `HomeTabNavigationStateContainer`: the coordinator drives it, and the view model subscribes to it — no direct back-reference from VM to coordinator is needed for state observation.

Page views are created lazily by the coordinator and cached:

```swift
// HomeTabCoordinator.reusePageView(for:)
factory.makePageView(for: page, coordinator: self)  // UIFactory injects controller into PageViewModel
```

## Domain Controllers

`HomeTabStore` is a lightweight container for the feature's sub-controllers:

| Controller | Responsibility |
|---|---|
| `DocumentsStore` | Source of truth for all documents. Owns `allDocuments` (each `DocumentItem` carries `thumbnail: NSImage`, initialised to a placeholder), `recentDocumentIDs` (`OrderedSet`), `favoriteDocumentIDs` (`OrderedSet`). Internally owns `ThumbnailStore` (acting as its delegate) and exposes `requestThumbnailIfNeeded(id:)` for on-demand thumbnail generation. Exposes `clearAllRecents()` and `clearAllFavorites()` which delegate to the data source. Notifies observers on mutation and on thumbnail updates. |
| `ToolsStore` | Exposes the tool categories shown on the Tools page and forwards tool activation. Backed by a host-provided `HomeTabToolsDataSourceProtocol` (injected via the gateway). Tools are static — there is no observer pattern. (Will later also own customizable section-order persistence.) |

`DocumentsStore` uses a manual observer pattern (not Combine `@Published`) so that `@MainActor`-isolated mutations and notifications are explicit. Observers register via `addObserver(_:) -> AnyCancellable`.

### HomeTabBannerController

`HomeTabBannerController` is a lightweight UI-layer controller (not part of `HomeTabStore`) that manages the banner `NSView` currently displayed on the Home page. It lives outside `HomeTabStore` because banners are UI-owned — they are injected by the host via `HomeTabViewController.setBannerView(_:)` and have no domain-level state.

- The host calls `HomeTabViewController.setBannerView(_:)`, which forwards to `HomeTabViewModel.setBannerView(_:)`, which calls `HomeTabBannerController.setBannerView(_:)`.
- `HomePageViewModel` observes the controller via `HomeTabBannerControllerObserver` and calls `objectWillChange.send()` on every change so SwiftUI picks up the new `presentedBannerView`.
- One `HomeTabBannerController` is created per `makeHomeTabViewController()` call, so each tab instance manages its banner independently.

### Thumbnail loading

`DocumentItem.thumbnail` is always a valid `NSImage`. Until real generation completes, it is the loading placeholder (`ImageResource.Thumbnails.loading`). Clients (e.g. `DocumentItemView.onAppear`) call `store.documents.requestThumbnailIfNeeded(id:)` to kick off generation; the call is a no-op if generation is already in progress or complete. `ThumbnailStore` (owned privately by `DocumentsStore`) handles caching and deduplication, and notifies `DocumentsStore` via `ThumbnailStoreDelegate` when a thumbnail is ready. `DocumentsStore` then updates `allDocuments` and calls `notifyObservers()`, delivering the result through the standard `documentsDidChange` callback.

`ThumbnailGenerator` is the injection point for thumbnail generation. It returns `Result<NSImage, ThumbnailGeneratorError>` where `ThumbnailGeneratorError` has `.broken` and `.locked` cases. `ThumbnailStore` maps the result to a concrete `NSImage` before caching:

| Result | Stored / delivered image |
|---|---|
| `.success(image)` | the generated thumbnail |
| `.failure(.locked)` | `ImageResource.Thumbnails.locked` |
| `.failure(.broken)` | `ImageResource.Thumbnails.broken` |

The resolved image (including the placeholder paths) is cached — so a broken or locked file does not retry on every UI request.

## Roles

**`HomeTabFeature`**
- Long-lived owner of `HomeTabStore`.
- Factory for `HomeTabViewController` instances; each call returns a new independent VC backed by the shared controller.
- Holds the `HomeTabFeatureGatewayProtocol` and forwards it to each coordinator it creates.

**`HomeTabFeatureGatewayProtocol`**
- Bridge out of the framework for actions that must escalate to the host app (e.g. open Preferences, Help Center, open file picker, open dropped files).
- `openDroppedDocuments(at: [URL])` passes all dropped URLs in one call, matching the existing new-tab drop-zone pattern.
- Provides the host-implemented data sources consumed by `HomeTabStore`: `documentsDataSource` (`HomeTabDocumentsDataSourceProtocol`) and `toolsDataSource` (`HomeTabToolsDataSourceProtocol`).
- Implemented by the host (`HomeTabFeatureGateway` in `PDFExpertMac`); held weakly by `HomeTabCoordinator`.

**`HomeTabToolsDataSourceProtocol`**
- Host-provided source of the tools surface inside Home Tab:
  - `toolCategories` — full list shown on the Tools page, grouped into `HomeTabToolCategory` (`HomeTabTool` carries `id`, `name`, `isNew`, `icon`, and its own `action` closure).
  - `personalizedTools` — a curated single-row list shown in the "Tools for You" section on the Home page (currently a static set; expected to be derived from user activity in a future iteration).
- Each `HomeTabTool` owns its action — there is no separate `activateTool` call; views invoke `tool.action()` directly on tap.
- Implemented by `HomeTabToolsDataSource` in `PDFExpertMac`, which maps each tool id to its `QuickAction` (reusing the Tools Tab actions) and runs it via `QuickActionsRunner`. Availability gating (DMG-only, macOS-version, feature flags) is applied per tool in both lists.

**`UIFactory`**
- Centralises all view and ViewModel construction.
- Holds `HomeTabStore` and `HomeTabBannerController`, injecting each into the ViewModels that need them.
- Created fresh per `makeHomeTabViewController()` call; lifetime is tied to the coordinator.

**`HomeTabCoordinator`**
- Caches page `NSView`s (one per `HomeTabPage`, created lazily via `UIFactory`).
- Owns `HomeTabNavigationStateContainer` and drives page transitions via `setCurrentPage(_:)`.
- Delegates all construction to `UIFactory`; contains no `HomeTabStore` reference itself.
- Exposes `openMenuBarSettings()` — opens System Settings to the Menu Bar & Dock panel via a URL scheme; called by `RecentsPageViewModel` from the recents-limit tip banner.

**`HomeTabNavigationStateContainer`**
- Source of truth for the currently active page.
- Decouples the coordinator (writer) from the view model (reader) without a direct reference between them.

**`HomeTabViewModel`**
- Observes `HomeTabNavigationStateContainer` to keep `currentPage` in sync with SwiftUI.
- Forwards `setSelectedPage` calls to the coordinator (weak ref).
- Proxies `setBannerView(_:)` calls from `HomeTabViewController` to `HomeTabBannerController`.

**`HomeTabViewController`**
- Hosts the SwiftUI root view (`HomeTabRootView`) inside `HomeTabRootNSView`.
- `HomeTabRootNSView` is a thin `NSView` wrapper that registers for `fileURL` drag types and implements `NSDraggingDestination`. On drag enter/exit it updates `HomeTabViewModel.isDraggingOver`; on drop it calls `HomeTabCoordinator.openDroppedDocuments(at:)`.
- `HomeTabRootView` composes the page container and the sidebar in a `ZStack` so the sidebar floats over the page content rather than sitting in an `HStack` next to it. This lets page content extend full-width under the sidebar while the page-level layout reserves space for it (see *Page Layout*).
- Shows a `DropOverlayView` overlay (dashed border + label) when `isDraggingOver` is true, covering the entire tab content area.
- Single strong owner of both coordinator and view model for the tab's lifetime.
- Exposes `setBannerView(_:)` as the public host API for injecting a banner `NSView` into the Home page. Forwards to `HomeTabViewModel`, which forwards to `HomeTabBannerController`.

**`PageViewModel` (e.g. `RecentsPageViewModel`)**
- Owns the UI state for a single page.
- Holds `HomeTabStore` strongly to read/mutate domain state.
- Calls coordinator weakly to trigger navigation to another page.
- `HomePageViewModel` additionally holds `HomeTabBannerController` strongly and observes it via `HomeTabBannerControllerObserver` to surface `presentedBannerView` to SwiftUI.
- `RecentsPageViewModel` persists the "recents limit tip dismissed" flag via `@UserDefault("homeTab.recentsLimitTipDismissed")`; it calls `coordinator?.openMenuBarSettings()` when the user taps "Open Settings" in the tip.

## Page Layout

All pages share a single layout container — `HomeTabPageView` (`Pages/Common/HomeTabPageView.swift`) — so they line up consistently against the floating sidebar. Pages do **not** declare their own `ScrollView` or width constraints; they wrap their body content in `HomeTabPageView { … }` and let it handle scrolling, sizing, and gradient fades.

```swift
var body: some View {
    HomeTabPageView {
        VStack(alignment: .leading, spacing: …) {
            // page content
        }
    }
}
```

`HomeTabPageView` is a small struct (not itself a `View`) that exposes `callAsFunction(_:)` so the trailing-closure call site reads like a regular SwiftUI container. It provides:

- A vertical `ScrollView` around the content.
- A fixed **880pt content width** centered horizontally with `padding(.vertical, 40)`.
- Left/right gradient spacers (`.rdElevatedL0Color → .clear`) that fade content under the sidebar on the left and against the trailing edge on the right. Horizontal section scrollers (e.g. Home Page Recents/Favorites) use `.scrollClipDisabled()` so their items glide *under* these gradients rather than getting hard-clipped.
- A reserved leading inset equal to `sidebarWidth + sidebarLeadingPadding` (the two `static` constants on `HomeTabPageView`, also consumed by `HomeTabSidebarView`) so the centered content never collides with the floating sidebar.
- An `if #available(macOS 14.0, *)` path that uses a custom `Layout` (`HomeTabPageLayout`) for the proportional allocation; a simpler `HStack` fallback runs on macOS 13.

Pages should generally apply small per-element horizontal padding (e.g. `.padding(.horizontal, 10)` on titles and section headers) rather than padding the whole content stack, since the 880pt width already determines the outer bounds.

## Adding a New Page

1. Add a case to `HomeTabPage`.
2. Add the display name to `HomeTabSidebarView.pageTitle(for:)`.
3. Create `Pages/<Name>/<Name>PageView.swift` with a SwiftUI `View` named `<Name>PageView` and `Pages/<Name>/<Name>PageViewModel.swift` with an `ObservableObject` named `<Name>PageViewModel`. The ViewModel takes `store: HomeTabStore` (stored `private let`) and `coordinator: HomeTabCoordinator?` (stored `private weak`).
4. Wrap the page's body in `HomeTabPageView { … }` (see *Page Layout*). Do not declare a `ScrollView` or width frame on the page itself.
5. Add a `case` to `UIFactory.makePageView(for:coordinator:)` that instantiates the ViewModel and wraps the view in `NSHostingView`.
