| 1 | /** |
| 2 | * Browser defaults - resolves the configured starting state for preview tabs. |
| 3 | * |
| 4 | * Settings → Integrations → Browser lets the user pick the viewport, zoom, and |
| 5 | * appearance a preview tab should open at. Those preferences apply to every |
| 6 | * entry point that opens a tab without stating its own: the user opening a |
| 7 | * browser panel, and agents calling `preview_open` with no size. |
| 8 | * |
| 9 | * The values live in client settings because the Chromium guest they configure |
| 10 | * is desktop-local, so this module reads them through the same external store |
| 11 | * the settings UI writes to, and exposes a non-hook accessor for the imperative |
| 12 | * open paths that run outside React. |
| 13 | * |
| 14 | * @module browserDefaults |
| 15 | */ |
| 16 | import { |
| 17 | DEFAULT_BROWSER_PROFILE_ID, |
| 18 | resolveBrowserProfiles, |
| 19 | type BrowserProfile, |
| 20 | type DesktopPreviewTabDefaults, |
| 21 | type PreviewAppearancePreference, |
| 22 | type PreviewViewportSetting, |
| 23 | } from "@t3tools/contracts"; |
| 24 | |
| 25 | import { |
| 26 | ensureClientSettingsHydrated, |
| 27 | getClientSettings, |
| 28 | useClientSettings, |
| 29 | } from "~/hooks/useSettings"; |
| 30 | |
| 31 | import { resolveResponsiveBrowserViewportSize } from "./browserViewportLayout"; |
| 32 | |
| 33 | export interface BrowserDefaults { |
| 34 | readonly viewport: PreviewViewportSetting; |
| 35 | readonly zoomFactor: number; |
| 36 | readonly appearance: PreviewAppearancePreference; |
| 37 | readonly autoShowFloatingPreview: boolean; |
| 38 | readonly profiles: ReadonlyArray<BrowserProfile>; |
| 39 | readonly profileId: string; |
| 40 | } |
| 41 | |
| 42 | const toBrowserDefaults = (settings: { |
| 43 | readonly browserDefaultViewport: PreviewViewportSetting; |
| 44 | readonly browserDefaultZoomFactor: number; |
| 45 | readonly browserDefaultAppearance: PreviewAppearancePreference; |
| 46 | readonly browserAutoShowFloatingPreview: boolean; |
| 47 | readonly browserProfiles: ReadonlyArray<BrowserProfile>; |
| 48 | readonly browserDefaultProfileId: string; |
| 49 | }): BrowserDefaults => { |
| 50 | const profiles = resolveBrowserProfiles(settings.browserProfiles); |
| 51 | return { |
| 52 | viewport: settings.browserDefaultViewport, |
| 53 | zoomFactor: settings.browserDefaultZoomFactor, |
| 54 | appearance: settings.browserDefaultAppearance, |
| 55 | autoShowFloatingPreview: settings.browserAutoShowFloatingPreview, |
| 56 | profiles, |
| 57 | // A default pointing at a deleted profile falls back rather than opening |
| 58 | // tabs into a partition with no profile behind it. |
| 59 | // Incognito is a per-tab choice, not a default: a profile that discards |
| 60 | // everything on close would leave every new tab signed out. Excluding it |
| 61 | // here keeps the resolved default equal to what the settings list offers, |
| 62 | // so the row badged "Default" is the one tabs actually open under. |
| 63 | profileId: |
| 64 | profiles.find( |
| 65 | (profile) => |
| 66 | profile.id === settings.browserDefaultProfileId && profile.kind !== "incognito", |
| 67 | )?.id ?? DEFAULT_BROWSER_PROFILE_ID, |
| 68 | }; |
| 69 | }; |
| 70 | |
| 71 | /** Non-hook accessor for imperative open paths (menu actions, automation hosts). */ |
| 72 | export function getBrowserDefaults(): BrowserDefaults { |
| 73 | return toBrowserDefaults(getClientSettings()); |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * The defaults, once client settings have actually loaded. |
| 78 | * |
| 79 | * Opening a preview is asynchronous anyway, and before hydration the snapshot |
| 80 | * is the schema defaults rather than the user's — a tab opened in that window |
| 81 | * would be born at the wrong viewport, zoom and appearance and never corrected. |
| 82 | * Read failures reject so a new tab cannot use the wrong profile or viewport. |
| 83 | */ |
| 84 | export async function resolveBrowserDefaults(): Promise<BrowserDefaults> { |
| 85 | await ensureClientSettingsHydrated(); |
| 86 | return getBrowserDefaults(); |
| 87 | } |
| 88 | |
| 89 | export function useBrowserDefaults(): BrowserDefaults { |
| 90 | return useClientSettings(toBrowserDefaults); |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * The zoom/appearance half of the defaults, in the shape `createTab` takes. |
| 95 | * Passing these at creation rather than after registration keeps the guest from |
| 96 | * painting a frame at 100%/system first. |
| 97 | */ |
| 98 | export function browserDefaultTabState( |
| 99 | defaults: BrowserDefaults = getBrowserDefaults(), |
| 100 | ): DesktopPreviewTabDefaults { |
| 101 | return { zoomFactor: defaults.zoomFactor, colorScheme: defaults.appearance }; |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * The viewport a *newly opened* tab should start at. Sent with `preview.open` |
| 106 | * so the session is born at the configured size instead of being resized a |
| 107 | * frame later, which the user would see as a visible reflow. |
| 108 | */ |
| 109 | export function browserDefaultOpenViewport( |
| 110 | defaults: BrowserDefaults = getBrowserDefaults(), |
| 111 | ): PreviewViewportSetting { |
| 112 | return defaults.viewport; |
| 113 | } |
| 114 | |
| 115 | /** Profile a tab opens under when the caller doesn't name one. */ |
| 116 | export function browserDefaultOpenProfileId( |
| 117 | defaults: BrowserDefaults = getBrowserDefaults(), |
| 118 | ): string { |
| 119 | return defaults.profileId; |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * The viewport to switch to when the user turns the device toolbar on for a tab |
| 124 | * currently in fill mode. |
| 125 | * |
| 126 | * A configured non-fill default is what the user said they want to look at, so |
| 127 | * it wins. When the default is fill there is no stated preference to honour, so |
| 128 | * fall back to fitting the panel — and only when the panel hasn't been measured |
| 129 | * yet does a fixed size apply. |
| 130 | */ |
| 131 | export const FALLBACK_RESPONSIVE_VIEWPORT_SIZE = { width: 1024, height: 768 } as const; |
| 132 | |
| 133 | export function browserResponsiveViewportForToggle(input: { |
| 134 | readonly defaults?: BrowserDefaults; |
| 135 | readonly panelRect: { readonly width: number; readonly height: number } | null; |
| 136 | readonly zoomFactor: number | undefined; |
| 137 | }): PreviewViewportSetting { |
| 138 | const defaults = input.defaults ?? getBrowserDefaults(); |
| 139 | if (defaults.viewport._tag !== "fill") return defaults.viewport; |
| 140 | const size = input.panelRect |
| 141 | ? resolveResponsiveBrowserViewportSize(input.panelRect, input.zoomFactor) |
| 142 | : FALLBACK_RESPONSIVE_VIEWPORT_SIZE; |
| 143 | return { _tag: "freeform", ...size }; |
| 144 | } |