From 2573df6f1bde2774fd8450e222e8fe0f3655dfaa Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Fri, 18 Sep 2026 21:41:07 +0800 Subject: [PATCH] Update iPad marketing navigation screenshots --- Memory/MARKETING.md | 37 +++++++++++-- Scripts/run-mobile-marketing-screenshots.sh | 8 ++- TFITests/MobileMarketingScreenshotTests.swift | 55 ++++++++----------- 3 files changed, 60 insertions(+), 40 deletions(-) diff --git a/Memory/MARKETING.md b/Memory/MARKETING.md index 07927639..e6b51f69 100644 --- a/Memory/MARKETING.md +++ b/Memory/MARKETING.md @@ -68,6 +68,11 @@ The required devices and outputs are: - Five scenes per locale: `vault`, `sftp`, `ssh`, `metrics`, and `assistant`. - Three locales, producing 15 screenshots per device and 30 total. +The runner defaults to the iOS/iPadOS 26.5 simulator runtime so installing a newer +runtime cannot silently change the marketing UI or select another same-name +device. Set `TERMIND_MARKETING_RUNTIME` to a full CoreSimulator runtime identifier +only when intentionally updating the runtime used for a capture set. + For a targeted recapture, set both filters. This example refreshes only the three localized iPad Assistant sources: @@ -77,7 +82,7 @@ TERMIND_MARKETING_DEVICES=ipad \ Scripts/run-mobile-marketing-screenshots.sh ``` -The iPhone Vault, Metrics and Assistant fixtures use the same native `Tab` API, +The iPhone and iPad Vault, Metrics and Assistant fixtures use the same native `Tab` API, sidebar-adaptable style and feature registration order as the production shell: Vault, Metrics, Workspaces, Assistant, Settings. Metrics is its own selected tab; Vault no longer contains the former Metrics navigation row. Refresh all nine @@ -93,10 +98,25 @@ Regenerate only their matching device frames, replace the corresponding Figma iPhone boards 04-06, and export those nine boards again. The three-device first boards use iPhone SSH and therefore do not change for this navigation refresh. +For the matching nine iPad sources, run: + +```sh +TERMIND_MARKETING_SCENES=vault,metrics,assistant \ +TERMIND_MARKETING_DEVICES=ipad \ +Scripts/run-mobile-marketing-screenshots.sh +``` + +The iPad fixtures use the native adaptive tab container: Metrics shows the top +tab bar, while Assistant and Vault show the expanded sidebar. Each capture fixes +that presentation independently of restored simulator state. They do not add +simulated sidebar or back buttons to the top-level Metrics and Assistant pages. +Regenerate only the matching nine iPad device frames and App Store boards 04-06; +first boards use iPad SFTP and remain unchanged. + The runner sets a deterministic `9:41` status bar, charged battery, Wi-Fi, and -cellular state. iPad framebuffer output is rotated after capture because `simctl` -keeps portrait pixel orientation for a landscape display. Confirm the final file -dimensions rather than trusting Simulator orientation alone. +cellular state. It sets the iPad orientation and captures its landscape display +with `devicectl`, verifying `2752x2064` before acknowledging each capture. The +iPhone uses `simctl` screenshots. No post-capture rotation is required. Whenever SFTP, Metrics, Assistant navigation, or system chrome changes, recapture the affected device and every locale. A marketing fixture may be updated to expose @@ -352,3 +372,12 @@ images changed; the other 108 marketing files, including SSH/SFTP, first boards, iPad and Mac assets, stayed unchanged. The nine Figma image fills match the local device-frame PNGs byte for byte, and the nine final JPGs are full-size `1284x2778` opaque sRGB exports. The App Store directory still contains 54 JPGs. + +The subsequent 2026-09-18 iPad refresh updated the same three scenes and locales: +Metrics uses native top tabs; Assistant and Vault use the expanded native sidebar. +Raw captures and final App Store exports are `2752x2064`, with `2960x2290` device +frames. Figwright readback confirmed matching original image bytes and unchanged +copy, fonts, layout, geometry and FIT mode. SHA-256 comparison against the completed +iPhone baseline found exactly 27 changed iPad images and 108 unchanged marketing +files, including iPhone, Mac, SSH/SFTP and the first boards. All 54 App Store JPGs +remain opaque three-channel sRGB. diff --git a/Scripts/run-mobile-marketing-screenshots.sh b/Scripts/run-mobile-marketing-screenshots.sh index 1169ac66..8cf0f14f 100755 --- a/Scripts/run-mobile-marketing-screenshots.sh +++ b/Scripts/run-mobile-marketing-screenshots.sh @@ -10,6 +10,7 @@ derived_data="$repository_directory/.build/mobile-marketing-screenshots" marketing_xctestrun="$derived_data/Build/Products/Marketing-TFI.xctestrun" marketing_scenes="${TERMIND_MARKETING_SCENES:-}" marketing_devices="${TERMIND_MARKETING_DEVICES:-iphone,ipad}" +marketing_runtime="${TERMIND_MARKETING_RUNTIME:-com.apple.CoreSimulator.SimRuntime.iOS-26-5}" expected_capture_count() { if [[ -z "$marketing_scenes" ]]; then @@ -25,7 +26,8 @@ expected_capture_count() { find_device() { local name="$1" xcrun simctl list devices available -j \ - | jq -r --arg name "$name" '.devices[][] | select(.name == $name) | .udid' \ + | jq -r --arg name "$name" --arg runtime "$marketing_runtime" \ + '.devices[$runtime][]? | select(.name == $name) | .udid' \ | head -n 1 } @@ -59,7 +61,7 @@ run_capture() { local device_id device_id="$(find_device "$name")" if [[ -z "$device_id" ]]; then - print -u2 "Required simulator is unavailable: $name" + print -u2 "Required simulator is unavailable: $name ($marketing_runtime)" exit 69 fi @@ -135,7 +137,7 @@ run_capture() { mkdir -p "$output_directory" build_device_id="$(find_device "iPhone 17 Pro Max")" if [[ -z "$build_device_id" ]]; then - print -u2 "Required simulator is unavailable: iPhone 17 Pro Max" + print -u2 "Required simulator is unavailable: iPhone 17 Pro Max ($marketing_runtime)" exit 69 fi xcodebuild \ diff --git a/TFITests/MobileMarketingScreenshotTests.swift b/TFITests/MobileMarketingScreenshotTests.swift index 3e9c8c1c..1da6d8d3 100644 --- a/TFITests/MobileMarketingScreenshotTests.swift +++ b/TFITests/MobileMarketingScreenshotTests.swift @@ -68,13 +68,9 @@ struct MobileMarketingScreenshotTests { if includes("metrics") { let metrics = makeMetrics(language: language) try await capture("metrics", language: language) { - if UIDevice.current.userInterfaceIdiom == .pad { - NavigationStack { MarketingMetricsScene(servers: metrics, language: language) } - } else { - MarketingPhoneTabShell(selected: .metrics) { - NavigationStack { - MarketingMetricsScene(servers: metrics, language: language, showsSidebarButton: false) - } + MarketingTabShell(selected: .metrics) { + NavigationStack { + MarketingMetricsScene(servers: metrics) } } } @@ -85,16 +81,9 @@ struct MobileMarketingScreenshotTests { let conversation = ConversationModel.demo(language: try #require(AppLanguage(rawValue: language))) assistant.library.insertAndSelect(conversation) try await capture("assistant", language: language) { - if UIDevice.current.userInterfaceIdiom == .pad { + MarketingTabShell(selected: .assistant) { NavigationStack { MobileAssistantView(app: app, model: assistant, libraryRoute: .constant(nil)) - .toolbar { marketingBackButton(language: language) } - } - } else { - MarketingPhoneTabShell(selected: .assistant) { - NavigationStack { - MobileAssistantView(app: app, model: assistant, libraryRoute: .constant(nil)) - } } } } @@ -207,6 +196,12 @@ struct MobileMarketingScreenshotTests { controller.view.layoutIfNeeded() try await Task.sleep(for: .milliseconds(900)) + if isPad, let tabs = tabController(in: controller) { + // Keep each scene's navigation presentation independent of restored simulator state. + tabs.sidebar.isHidden = name == "metrics" + try await Task.sleep(for: .milliseconds(900)) + } + if ProcessInfo.processInfo.environment["TERMIND_MARKETING_SYSTEM_CAPTURE"] == "1" { try await waitForSystemCapture(name, language: language) return @@ -233,6 +228,11 @@ struct MobileMarketingScreenshotTests { print("Marketing snapshot: \(output.path)") } + private func tabController(in controller: UIViewController) -> UITabBarController? { + if let tabs = controller as? UITabBarController { return tabs } + return controller.children.lazy.compactMap { tabController(in: $0) }.first + } + private func waitForSystemCapture(_ name: String, language: String) async throws { let documents = try #require(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first) let control = documents.appending(path: "marketing-capture-control", directoryHint: .isDirectory) @@ -281,22 +281,18 @@ private struct MarketingVaultScene: View { let app: MobileAppModel var body: some View { - if UIDevice.current.userInterfaceIdiom == .pad { - MobileRootView(app: app) - } else { - MarketingPhoneTabShell(selected: .vault) { - NavigationStack { - MobileVaultView( - app: app, editingHost: .constant(nil), activateHost: { _ in }, - open: { _, _ in } - ) - } + MarketingTabShell(selected: .vault) { + NavigationStack { + MobileVaultView( + app: app, editingHost: .constant(nil), activateHost: { _ in }, + open: { _, _ in } + ) } } } } -private struct MarketingPhoneTabShell: View { +private struct MarketingTabShell: View { let selected: MobileDestination let content: Content private let features = MobileFeatureRegistry.standard() @@ -355,8 +351,6 @@ private struct MarketingTerminalScene: View { private struct MarketingMetricsScene: View { let servers: [ServerMetric] - let language: String - var showsSidebarButton = true var body: some View { MobileCardPage { @@ -375,11 +369,6 @@ private struct MarketingMetricsScene: View { } .navigationTitle("Metrics") .toolbar { - if showsSidebarButton { - ToolbarItem(placement: .navigationBarLeading) { - Button("Sidebar", systemImage: "sidebar.left") {}.labelStyle(.iconOnly) - } - } ToolbarItem(placement: .primaryAction) { Button("Add Server", systemImage: "plus") {}.labelStyle(.iconOnly) }