From a1f8234bc237903865571bfca884eef64f589eb7 Mon Sep 17 00:00:00 2001 From: Kaloyan Stoyanov Date: Wed, 28 Oct 2026 16:36:00 +0200 Subject: [PATCH] feat(web): remember the last 3D or flat device view --- .../src/settings/DesktopClientSettings.test.ts | 1 + apps/web/src/components/device/DeviceStreamView.tsx | 12 +++++++----- docs/user/devices.md | 10 ++++++---- packages/contracts/src/settings.ts | 7 +++++++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index ac4c6e61378e3..c5add1b6268b9 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -27,6 +27,7 @@ const clientSettings: ClientSettings = { browserRecordingFrameRate: 60, browserLinkTarget: "app", browserAutoShowFloatingPreview: false, + deviceView: "flat", browserProfiles: [{ id: "work", name: "Work", kind: "persistent" }], browserDefaultProfileId: "work", confirmQuit: "double-click", diff --git a/apps/web/src/components/device/DeviceStreamView.tsx b/apps/web/src/components/device/DeviceStreamView.tsx index 22ae466acb0ed..429aad49b8afd 100644 --- a/apps/web/src/components/device/DeviceStreamView.tsx +++ b/apps/web/src/components/device/DeviceStreamView.tsx @@ -1,8 +1,9 @@ -import type { DevicePlatform, EnvironmentId } from "@t3tools/contracts"; +import type { DevicePlatform, DeviceView, EnvironmentId } from "@t3tools/contracts"; import { useAtomValue } from "@effect/atom-react"; import { AsyncResult, Atom } from "effect/reactivity"; import { useId, useRef, useState, type ReactNode } from "react"; +import { updateClientSettings, useClientSettings } from "~/hooks/useSettings"; import { cn } from "~/lib/utils"; import { Button } from "~/components/ui/button"; import { renewDeviceHubAccess, useDeviceHubAccess } from "~/state/device"; @@ -96,7 +97,8 @@ export function DeviceStreamView(props: { }) { const model = deviceModel(props.platform, props.deviceName ?? ""); const isDuo = model?.id === "iphone-duo"; - const [presentation, setPresentation] = useState<"phone" | "flat">("phone"); + const presentation = useClientSettings((settings) => settings.deviceView); + const setPresentation = (view: DeviceView) => void updateClientSettings({ deviceView: view }); const [keyboardAttached, setKeyboardAttached] = useState(false); const cancelPhoneInputRef = useRef<(() => void) | null>(null); const cancelPhoneInput = () => cancelPhoneInputRef.current?.(); @@ -150,7 +152,7 @@ export function DeviceStreamView(props: { props.allowPhoneView && (status === "streaming" || retainingFrame) && props.visible && - presentation === "phone" && + presentation === "3d" && !phoneUnavailable && !mjpegUrl && !props.axOverlay && @@ -281,7 +283,7 @@ export function DeviceStreamView(props: { }, } : null, - showPhone: () => setPresentation("phone"), + showPhone: () => setPresentation("3d"), showFlat: () => setPresentation("flat"), handle: client ? { @@ -433,7 +435,7 @@ export function DeviceStreamView(props: { aria-pressed={!!showPhone} disabled={!!phoneUnavailableReason} title={phoneUnavailableReason ?? "Show 3D phone"} - onClick={() => setPresentation("phone")} + onClick={() => setPresentation("3d")} > 3D diff --git a/docs/user/devices.md b/docs/user/devices.md index 42c0db4b27afa..1c54b0b6196cd 100644 --- a/docs/user/devices.md +++ b/docs/user/devices.md @@ -48,10 +48,12 @@ unless you power it off. Closed tabs stay closed after a reload. To watch the device again, choose it from **+ → Device**. Choose **3D view** to inspect supported devices while the live screen stays -interactive. On iPhone Duo, use the fold and stance controls to change its -physical pose, or pinch over the device to adjust the hinge. Turning the model -to the other screen switches the live display and touch input to that screen. -**Restore 3D view** returns the device to a screen-facing position. +interactive. Choose **Flat view** to go back to the plain screen. T3 Code +remembers the last one you picked and uses it for every device. On iPhone Duo, +use the fold and stance controls to change its physical pose, or pinch over the +device to adjust the hinge. Turning the model to the other screen switches the +live display and touch input to that screen. **Restore 3D view** returns the +device to a screen-facing position. On supported Android foldables, use **Fold device** and **Unfold device** beside the screen to change its posture in either view. diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 8fc9a46116e78..555a808aadda3 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -293,6 +293,9 @@ export { BrowserRecordingFrameRate, DEFAULT_BROWSER_RECORDING_FRAME_RATE, } from "./preview.ts"; +export const DeviceView = Schema.Literals(["3d", "flat"]); +export type DeviceView = typeof DeviceView.Type; +const DEFAULT_DEVICE_VIEW: DeviceView = "3d"; export const BrowserLinkTarget = Schema.Literals(["system", "app"]); export type BrowserLinkTarget = typeof BrowserLinkTarget.Type; export const DEFAULT_BROWSER_LINK_TARGET: BrowserLinkTarget = "system"; @@ -397,6 +400,9 @@ export const ClientSettingsSchema = Schema.Struct({ Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW)), ), + // Remembers the last 3D or flat choice made in the device toolbar. + deviceView: DeviceView.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_DEVICE_VIEW))), + browserProfiles: Schema.Array(BrowserProfile).pipe( Schema.withDecodingDefault(Effect.succeed([])), ), @@ -1666,6 +1672,7 @@ export const ClientSettingsPatch = Schema.Struct({ browserSearchEngine: Schema.optionalKey(BrowserSearchEngine), browserCustomSearchUrl: Schema.optionalKey(BrowserCustomSearchUrl), browserAutoShowFloatingPreview: Schema.optionalKey(Schema.Boolean), + deviceView: Schema.optionalKey(DeviceView), browserProfiles: Schema.optionalKey(Schema.Array(BrowserProfile)), browserDefaultProfileId: Schema.optionalKey(BrowserProfileId), confirmQuit: Schema.optionalKey(QuitConfirmationMode),