Update iPad marketing navigation screenshots

This commit is contained in:
Kaede Akino committed 2026-09-18 21:41:43 +08:00
1 parent b60e68e3d5
commit 2573df6f1b
3 files changed
+60 -40

No files matched your search

+33 -4
View File
@@ -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.
+5 -3
View File
@@ -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 \
+22 -33
View File
@@ -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<Content: View>: View {
private struct MarketingTabShell<Content: View>: 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)
}