# Cross-Platform Pages Pages that need to exist in both the Modrinth Website (`apps/frontend`) and the Modrinth App (`apps/app-frontend`) live in `packages/ui/src/layouts/`. There are two categories based on whether the page logic differs between platforms. ## Shared Layouts (`layouts/shared/`) For pages where the **logic differs** between the website and app (e.g. the app fetches data via Tauri `invoke` while the website uses `api-client`). Each shared layout is a self-contained module: ``` shared/content-tab/ ├── layout.vue # Main layout component ├── types.ts # TypeScript types ├── components/ # Internal UI components ├── composables/ # Stateful logic (search, filtering, selection) └── providers/ # DI context definitions ``` ### How it works 1. A **DI contract** in `providers/` defines all platform-specific operations as an interface. 2. The **layout component** injects that context and handles all UI logic (search, filtering, selection, bulk operations, modals) without knowing the platform. 3. Each **platform provides its own implementation** of the contract. ### DI contract example ```ts // shared/content-tab/providers/content-manager.ts export interface ContentManagerContext { items: Ref | ComputedRef loading: Ref | ComputedRef // Platform-abstracted operations toggleEnabled: (item: ContentItem) => Promise deleteItem: (item: ContentItem) => Promise refresh: () => Promise // Optional capabilities — not every platform supports everything hasUpdateSupport: boolean updateItem?: (id: string) => void bulkDeleteItems?: (items: ContentItem[]) => Promise mapToTableItem: (item: ContentItem) => ContentCardTableItem } export const [injectContentManager, provideContentManager] = createContext('ContentPageLayout', 'contentManagerContext') ``` ### Platform implementations **Website** — uses `api-client` and TanStack Query: ```vue ``` **App** — uses Tauri `invoke`: ```vue ``` ### Optional capabilities The DI contract uses optional fields for features that not every platform supports. The layout checks for them before rendering the corresponding UI: ```ts // Contract bulkUpdateItems?: (items: ContentItem[]) => Promise shareItems?: (items: ContentItem[], format: string) => void // Layout checks before showing UI v-if="ctx.bulkUpdateItems && hasOutdatedProjects" ``` ### Props vs DI | Use | When | | --------- | ------------------------------------------------------------------------------------------ | | **DI** | Data depends on _how_ it's fetched — API calls, file operations, navigation (per-platform) | | **Props** | Data is the same regardless of platform — configuration flags, display options | ## Wrapped Pages (`layouts/wrapped/`) For pages where the **logic is identical** on both platforms — same API source, same data fetching, same state management. These are full page-level Vue components that directly implement routes: ``` wrapped/hosting/manage/ ├── index.vue ├── content.vue ├── backups.vue ├── files.vue └── [id]/onboarding.vue ``` Wrapped pages handle their own data fetching (typically via TanStack Query and `api-client`) and are consumed as simple component imports in both frontends: ```vue ``` ### Platform route shells: prefetch with `ensureQueryData` #### Wrapped layout: `ReadyTransition` and `useReadyState` Many wrapped pages wrap the main UI in [`ReadyTransition`](../../packages/ui/src/components/base/ReadyTransition.vue) with `:pending` driven by [`useReadyState`](../../packages/ui/src/composables/use-ready-state.ts) on the **primary** TanStack query (true only on the first load while that query has no cached data yet—background refetches stay “ready”). That avoids flashing empty content before data exists. ```vue ``` ```ts const primaryQuery = useQuery({ /* ... */ }) const readyPending = useReadyState(primaryQuery) // or useReadyState({ isLoading, data }) when not using the full query object ``` Shell prefetch (below) warms the cache so that on navigation the query often **already has data** when the layout mounts; `pending` stays false and `ReadyTransition` can skip the enter animation on that fast path (see `ReadyTransition` docs and stories). #### Rule: `ensureQueryData` in each platform route shell When a wrapped layout uses that pattern, the **thin platform page** that imports the layout must **prefetch the same primary query** in `