diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24de636..5b94e11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,11 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Voluntary support tip**: Settings (top of the page) includes an optional ¥28 Consumable in-app tip (StoreKit 2). All features stay free — no paywall or unlock. / **自愿打赏**:设置页顶部新增可选 ¥28 消耗型应用内打赏(StoreKit 2)。全功能仍免费,无付费墙或功能解锁。
+- **iOS appearance preference**: Settings → Preferences adds System / Light / Dark (iPhone + iPad), matching the Mac control. / **iOS 外观偏好**:设置 → 偏好设置新增跟随系统 / 浅色 / 深色(iPhone 与 iPad),与 Mac 一致。
+- **DEBUG demo seed URL**: `osgkeyboard://seed-demo` fills Home stats, History, and Dictionary with placeholder data and turns iCloud sync off (script: `scripts/seed_demo_data.py`). / **DEBUG 演示数据**:`osgkeyboard://seed-demo` 填充首页统计、历史与词库占位数据并关闭 iCloud 同步(脚本:`scripts/seed_demo_data.py`)。
### Changed
- **Unified welcome slogan**: iPad Home now reuses the iOS onboarding brand line, and macOS onboarding shows the same “Speak it. It’s typed.” welcome slogan. / **统一欢迎口号**:iPad 首页复用 iOS 引导页品牌句,macOS 引导页也显示同一句「开口即文字。」欢迎口号。
- **GitHub Pages landing**: redesign as a commercial product page with zh/en, light/dark, brand mark, scroll motion, and App Store screenshots; emphasizes free, cross-platform, open source, privacy, and BYOK. / **GitHub Pages 落地页**:改版为商业产品页,支持中英与日夜模式、品牌标、滚动动效与 App Store 截图;突出免费、跨端、开源、隐私与 BYOK。
- **Landing hero device family**: Mac + iPad + iPhone nested in one mockup cluster (no outer card stroke); screens swap with language/theme. / **落地页 Hero 设备组**:Mac、iPad、iPhone 叠放在同一组设备框内(无外卡片描边);截图随语言/主题切换。
+- **Landing hero polish**: replace CSS device frames with the marketing composite; full-bleed pale-green hero wash (no side gaps / no radial gradient); Mac story shots sit on transparent chrome. / **落地页 Hero 抛光**:设备框改为营销合成图;首屏淡绿单色通栏(无两侧留白 / 无径向渐变);Mac 故事截图去卡片底。
+
+### Fixed
+- **iPad sidebar brand mark**: use the template `OSGLogoWide` mark with accent tint so the logo stays visible in the split-view sidebar. / **iPad 侧栏品牌标**:改用可着色的 `OSGLogoWide`,保证分栏侧栏始终显示 logo。
## [0.5.3] - 2026-07-11
diff --git a/OSGKeyboard/OSGKeyboardApp.swift b/OSGKeyboard/OSGKeyboardApp.swift
index c8521bc..7b8be05 100644
--- a/OSGKeyboard/OSGKeyboardApp.swift
+++ b/OSGKeyboard/OSGKeyboardApp.swift
@@ -8,6 +8,14 @@ import OSGKeyboardShared
struct OSGKeyboardApp: App {
@UIApplicationDelegateAdaptor(AppURLHandler.self) private var appURLHandler
+ /// App-local light / dark preference (Settings ▸ Preferences ▸ Appearance).
+ @AppStorage(AppearancePreference.storageKey)
+ private var appearanceRaw = AppearancePreference.system.rawValue
+
+ private var appearance: AppearancePreference {
+ AppearancePreference.fromStored(appearanceRaw)
+ }
+
init() {
MaterialIconsFont.registerIfNeeded()
if AppGroup.isAvailable {
@@ -21,10 +29,12 @@ struct OSGKeyboardApp: App {
ThemedRoot {
MainAppRoot()
}
+ .preferredColorScheme(appearance.colorScheme)
} else {
ThemedRoot {
AppGroupErrorView()
}
+ .preferredColorScheme(appearance.colorScheme)
}
}
}
diff --git a/OSGKeyboard/Views/MainAppRoot.swift b/OSGKeyboard/Views/MainAppRoot.swift
index b165628..2ae60d2 100644
--- a/OSGKeyboard/Views/MainAppRoot.swift
+++ b/OSGKeyboard/Views/MainAppRoot.swift
@@ -80,6 +80,11 @@ struct MainAppRoot: View {
switch url.host {
case "startflow":
flowManager.startSession(coldStart: true)
+ #if DEBUG
+ case "seed-demo":
+ DemoDataSeeder.seedRichPlaceholderData()
+ config.reloadFromPersistedStorage()
+ #endif
default:
break
}
diff --git a/OSGKeyboard/Views/MainSplitView.swift b/OSGKeyboard/Views/MainSplitView.swift
index 0e5cf08..2df467e 100644
--- a/OSGKeyboard/Views/MainSplitView.swift
+++ b/OSGKeyboard/Views/MainSplitView.swift
@@ -54,13 +54,19 @@ struct MainSplitView: View {
private var brandHeader: some View {
HStack {
- Image("osglogo")
+ // Match macOS sidebar: template wide mark tinted with accent so
+ // light / dark both stay readable (PNG `osglogo` can fail to
+ // show under NavigationSplitView chrome on some iPad sizes).
+ Image("OSGLogoWide")
+ .renderingMode(.template)
.resizable()
.scaledToFit()
.frame(height: 28)
+ .foregroundStyle(palette.accent)
.accessibilityLabel("OSGKeyboard")
- Spacer()
+ Spacer(minLength: 0)
}
+ .frame(maxWidth: .infinity, minHeight: 28, alignment: .leading)
.padding(.leading, WideLayoutMetrics.sidebarContentInset)
.padding(.trailing, WideLayoutMetrics.sidebarInset)
.padding(.top, Spacing.lg)
diff --git a/OSGKeyboard/Views/SettingsView.swift b/OSGKeyboard/Views/SettingsView.swift
index 185fa3a..f40ffbe 100644
--- a/OSGKeyboard/Views/SettingsView.swift
+++ b/OSGKeyboard/Views/SettingsView.swift
@@ -156,6 +156,10 @@ struct SettingsView: View {
Divider().background(palette.divider)
+ AppearancePickerRow()
+
+ Divider().background(palette.divider)
+
LocalePickerRow(
locales: effectiveLocales,
selection: Binding(
@@ -495,6 +499,27 @@ private struct AppLanguagePickerRow: View {
}
}
+// MARK: - Appearance picker row
+
+private struct AppearancePickerRow: View {
+ @AppStorage(AppearancePreference.storageKey)
+ private var appearanceRaw = AppearancePreference.system.rawValue
+
+ private var options: [(id: String, label: String)] {
+ AppearancePreference.allCases.map { preference in
+ (preference.rawValue, AppL10n.string(preference.labelKey))
+ }
+ }
+
+ var body: some View {
+ PickerRow(
+ title: AppL10n.string("settings.appearance.title"),
+ options: options,
+ selection: $appearanceRaw
+ )
+ }
+}
+
// MARK: - Flow inactivity picker row
private struct FlowInactivityPickerRow: View {
diff --git a/OSGKeyboard/en.lproj/Localizable.strings b/OSGKeyboard/en.lproj/Localizable.strings
index 8a93cd6..5070a22 100644
--- a/OSGKeyboard/en.lproj/Localizable.strings
+++ b/OSGKeyboard/en.lproj/Localizable.strings
@@ -91,6 +91,10 @@
"settings.appLanguage.auto" = "Auto";
"settings.appLanguage.english" = "English";
"settings.appLanguage.chinese" = "Chinese";
+"settings.appearance.title" = "Appearance";
+"settings.appearance.system" = "System";
+"settings.appearance.light" = "Light";
+"settings.appearance.dark" = "Dark";
"settings.reset.title" = "Reset all settings?";
"settings.reset.message" = "API key, model, base URL, voice history, and cloud consent will be cleared.";
"settings.reset.confirm" = "Reset all settings";
diff --git a/OSGKeyboard/zh-Hans.lproj/Localizable.strings b/OSGKeyboard/zh-Hans.lproj/Localizable.strings
index c57d74f..35079cf 100644
--- a/OSGKeyboard/zh-Hans.lproj/Localizable.strings
+++ b/OSGKeyboard/zh-Hans.lproj/Localizable.strings
@@ -91,6 +91,10 @@
"settings.appLanguage.auto" = "自动";
"settings.appLanguage.english" = "英文";
"settings.appLanguage.chinese" = "中文";
+"settings.appearance.title" = "外观";
+"settings.appearance.system" = "跟随系统";
+"settings.appearance.light" = "浅色";
+"settings.appearance.dark" = "深色";
"settings.reset.title" = "重置所有设置?";
"settings.reset.message" = "将清空 API Key、模型、接口地址、语音历史及云端确认状态。";
"settings.reset.confirm" = "重置所有设置";
diff --git a/OSGKeyboardMac/Info.plist b/OSGKeyboardMac/Info.plist
index c8f8da3..863982a 100644
--- a/OSGKeyboardMac/Info.plist
+++ b/OSGKeyboardMac/Info.plist
@@ -23,6 +23,17 @@
APPL
CFBundleShortVersionString
$(MARKETING_VERSION)
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ com.osgkeyboard.mac.seed
+ CFBundleURLSchemes
+
+ osgkeyboard
+
+
+
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
ITSAppUsesNonExemptEncryption
diff --git a/OSGKeyboardMac/OSGKeyboardMacApp.swift b/OSGKeyboardMac/OSGKeyboardMacApp.swift
index 2850e99..e32882b 100644
--- a/OSGKeyboardMac/OSGKeyboardMacApp.swift
+++ b/OSGKeyboardMac/OSGKeyboardMacApp.swift
@@ -52,6 +52,20 @@ struct OSGKeyboardMacApp: App {
.onReceive(NotificationCenter.default.publisher(for: .speechHistoryDidSyncFromCloud)) { _ in
viewModel.speechHistory.reloadFromDisk()
}
+ .onOpenURL { url in
+ #if DEBUG
+ guard url.scheme == "osgkeyboard", url.host == "seed-demo" else { return }
+ DemoDataSeeder.seedRichPlaceholderData(
+ defaults: .standard,
+ historyDefaults: .standard
+ )
+ viewModel.reloadConfigFromCloud()
+ viewModel.refreshDictionaryFromCloud()
+ viewModel.usageStatistics.reloadFromDisk()
+ viewModel.speechHistory.reloadFromDisk()
+ hasCompletedMacOnboarding = true
+ #endif
+ }
}
// Borderless titlebar → content (sidebar + traffic lights) runs to the
// very top, matching macOS System Settings.
diff --git a/OSGKeyboardShared/Models/AppearancePreference.swift b/OSGKeyboardShared/Models/AppearancePreference.swift
new file mode 100644
index 0000000..7bd19a8
--- /dev/null
+++ b/OSGKeyboardShared/Models/AppearancePreference.swift
@@ -0,0 +1,41 @@
+// AppearancePreference.swift
+// OSGKeyboard · Shared
+//
+// In-app light / dark preference for the iOS main app (iPhone + iPad).
+// Mirrors macOS `MacAppearancePreference` but uses iOS Settings copy keys.
+// Stored in standard UserDefaults (app-local); not synced via iCloud.
+
+import SwiftUI
+
+/// How the iOS host app resolves its colour scheme.
+public enum AppearancePreference: String, CaseIterable, Identifiable, Sendable, Codable {
+ case system
+ case light
+ case dark
+
+ public var id: String { rawValue }
+
+ /// `nil` means follow the system — SwiftUI's `preferredColorScheme(nil)`.
+ public var colorScheme: ColorScheme? {
+ switch self {
+ case .system: return nil
+ case .light: return .light
+ case .dark: return .dark
+ }
+ }
+
+ public var labelKey: String {
+ switch self {
+ case .system: return "settings.appearance.system"
+ case .light: return "settings.appearance.light"
+ case .dark: return "settings.appearance.dark"
+ }
+ }
+
+ public static let storageKey = "config.appearancePreference"
+
+ public static func fromStored(_ raw: String?) -> AppearancePreference {
+ guard let raw, let value = AppearancePreference(rawValue: raw) else { return .system }
+ return value
+ }
+}
diff --git a/OSGKeyboardShared/Services/DemoDataSeeder.swift b/OSGKeyboardShared/Services/DemoDataSeeder.swift
new file mode 100644
index 0000000..f0bbda5
--- /dev/null
+++ b/OSGKeyboardShared/Services/DemoDataSeeder.swift
@@ -0,0 +1,143 @@
+// DemoDataSeeder.swift
+// OSGKeyboard · Shared
+//
+// DEBUG / screenshot helper: fills Home stats, History, and Dictionary with
+// rich placeholder content, and forces iCloud sync OFF so a remote pull cannot
+// wipe the seed. Trigger via `osgkeyboard://seed-demo`.
+
+import Foundation
+
+@MainActor
+public enum DemoDataSeeder {
+ /// Disable sync (local + KVS), write placeholder payloads, reload stores.
+ public static func seedRichPlaceholderData(
+ defaults: UserDefaults? = nil,
+ historyDefaults: UserDefaults = .standard
+ ) {
+ let store = defaults.map { AppGroupStore(defaults: $0) } ?? AppGroupStore()
+ let groupDefaults = store.defaults
+
+ // 1. Sync must be OFF before any pull can empty the dictionary.
+ store.setSettingsICloudSyncEnabled(false)
+ store.setPersonalDictionaryICloudSyncEnabled(false)
+ ICloudSyncPreferences.pushSettingsEnabled(false, kvs: NSUbiquitousKeyValueStore.default)
+ ICloudSyncPreferences.pushDictionaryEnabled(false, kvs: NSUbiquitousKeyValueStore.default)
+
+ groupDefaults.set(true, forKey: "usageStatistics.dirtyReset.v1")
+ groupDefaults.set(true, forKey: "home.keyboardHintDismissed")
+ groupDefaults.set(true, forKey: AppGroupConfiguration.Keys.hasCompletedOnboarding)
+ // Mac onboarding flag (harmless on iOS).
+ groupDefaults.set(true, forKey: "mac.hasCompletedOnboarding")
+ UserDefaults.standard.set(true, forKey: "mac.hasCompletedOnboarding")
+
+ let now = Date()
+ seedUsage(defaults: groupDefaults, now: now)
+ seedHistory(defaults: historyDefaults, now: now)
+ seedDictionary(store: store, now: now)
+
+ UsageStatisticsStore(defaults: groupDefaults).reloadFromDisk()
+ UsageStatisticsStore.shared.reloadFromDisk()
+ SpeechHistoryStore(defaults: historyDefaults).reloadFromDisk()
+ SpeechHistoryStore.shared.reloadFromDisk()
+
+ NotificationCenter.default.post(name: .personalDictionaryDidSyncFromCloud, object: nil)
+ NotificationCenter.default.post(name: .usageStatisticsDidSyncFromCloud, object: nil)
+ }
+
+ // MARK: - Payloads
+
+ private static func seedUsage(defaults: UserDefaults, now: Date) {
+ let calendar = Calendar.current
+ let today = calendar.startOfDay(for: now)
+ let dailyValues = [2100, 3400, 1800, 5200, 2900, 6100, 4300]
+ var daily: [String: Int] = [:]
+ for (offset, value) in dailyValues.enumerated() {
+ guard let day = calendar.date(byAdding: .day, value: offset - 6, to: today) else { continue }
+ daily[UsageStatisticsDayKey.key(for: day)] = value
+ }
+
+ let deviceID = SyncDeviceID.current(defaults: defaults)
+ let slice = UsageStatisticsDeviceSlice(
+ updatedAt: now,
+ dictationDurationSeconds: 6120,
+ dictationCharacterCount: 31_458,
+ translationCharacterCount: 3_200,
+ dailyDictationCharacters: daily
+ )
+ SyncedUsageStatisticsStorage.upsertCurrentDeviceSlice(
+ slice,
+ defaults: defaults,
+ deviceID: deviceID
+ )
+ }
+
+ private static func seedHistory(defaults: UserDefaults, now: Date) {
+ let samples: [(String, String)] = [
+ ("local", "请帮我把这段会议纪要整理成三条行动项,并标出负责人。"),
+ ("local", "下周产品评审改到周三下午两点,地点还是三楼会议室。"),
+ ("cloud", "帮我写一封礼貌的跟进邮件,询问合同进度。"),
+ ("local", "今天听写了三十分钟,词库命中率比昨天更好。"),
+ ("local", "把「开口即文字」加到品牌口号里,首页和引导页保持一致。"),
+ ("cloud", "Draft a short release note for the appearance preference on iOS and iPad."),
+ ("local", "提醒我晚上九点前提交 App Store 截图和落地页更新。"),
+ ("local", "语音输入在任意 App 可用,点按键盘麦克风即可开始听写。"),
+ ("cloud", "Summarize yesterday's dictation stats for the weekly report."),
+ ("local", "词库里加上 Cursor、DeepSeek、Qwen3-ASR,方便识别专有名词。"),
+ ("local", "跨设备同步先关掉,演示数据用本地占位,避免被 iCloud 覆盖。"),
+ ("local", "把首页近七天柱状图补齐,看起来更有真实使用痕迹。"),
+ ]
+
+ var entries: [SpeechHistoryEntry] = []
+ for (index, pair) in samples.enumerated() {
+ let created = now.addingTimeInterval(-Double(index * 5 * 3600 + index * 7 * 60))
+ entries.append(
+ SpeechHistoryEntry(
+ id: UUID(),
+ text: pair.1,
+ createdAt: created,
+ engineMode: pair.0
+ )
+ )
+ }
+
+ var history = SyncedSpeechHistory(updatedAt: now)
+ history.entries = entries
+ history.deletedEntryIDs = [:]
+ SpeechHistoryStorage.save(history, to: defaults)
+ }
+
+ private static func seedDictionary(store: AppGroupStore, now: Date) {
+ let terms: [(String, [String], PersonalDictionary.Entry.Category, Int)] = [
+ ("OSGKeyboard", ["OSG Keyboard", "开口即文字"], .productName, 48),
+ ("Cursor", ["cursor"], .productName, 36),
+ ("DeepSeek", ["deep seek", "深度求索"], .productName, 29),
+ ("Qwen3-ASR", ["千问 ASR", "Qwen ASR"], .technical, 22),
+ ("BYOK", ["自带密钥"], .acronym, 18),
+ ("Sherpa", ["sherpa onnx"], .technical, 15),
+ ("Typeless", [], .productName, 11),
+ ("iCloud", ["云同步"], .productName, 9),
+ ("SpeechAnalyzer", ["语音分析器"], .technical, 7),
+ ("Live Activity", ["灵动岛"], .custom, 5),
+ ("StoreKit", ["内购"], .technical, 4),
+ ("Rocky", ["rocky"], .properNoun, 3),
+ ]
+
+ var dictionary = PersonalDictionary()
+ for (index, item) in terms.enumerated() {
+ let created = now.addingTimeInterval(-Double((20 - index) * 86_400))
+ dictionary.entries.append(
+ PersonalDictionary.Entry(
+ id: UUID(),
+ term: item.0,
+ aliases: item.1,
+ category: item.2,
+ source: .manual,
+ createdAt: created,
+ updatedAt: created.addingTimeInterval(3600),
+ usageCount: item.3
+ )
+ )
+ }
+ store.setPersonalDictionary(dictionary)
+ }
+}
diff --git a/Scripts/seed_demo_data.py b/Scripts/seed_demo_data.py
new file mode 100644
index 0000000..98ba5ee
--- /dev/null
+++ b/Scripts/seed_demo_data.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+"""Seed rich demo data into OSGKeyboard via in-app DEBUG URL.
+
+Uses `osgkeyboard://seed-demo` so the app itself disables iCloud sync and
+writes Home / History / Dictionary placeholders with the real Swift models
+(avoids KVS wiping plist-only seeds).
+
+Usage:
+ python3 scripts/seed_demo_data.py --mac
+ python3 scripts/seed_demo_data.py --sim
+ python3 scripts/seed_demo_data.py --all
+"""
+
+from __future__ import annotations
+
+import argparse
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+BUNDLE_IOS = "com.osgkeyboard.ios"
+SEED_URL = "osgkeyboard://seed-demo"
+MAC_APP = Path(
+ "/Users/rocky/Library/Developer/Xcode/DerivedData/OSGKeyboard-dgaosbtwferhcpclfyzxmfuwaikn"
+ "/Build/Products/Debug/OSGKeyboard.app"
+)
+# Fallback if DerivedData folder hash changes.
+if not MAC_APP.exists():
+ alt = Path("/Users/rocky/Documents/OSGKeyboard/build/mac/Build/Products/Debug/OSGKeyboard.app")
+ if alt.exists():
+ MAC_APP = alt
+ else:
+ found = sorted(Path.home().glob(
+ "Library/Developer/Xcode/DerivedData/OSGKeyboard-*/Build/Products/Debug/OSGKeyboard.app"
+ ))
+ if found:
+ MAC_APP = found[-1]
+
+
+def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess[str]:
+ return subprocess.run(cmd, check=check, capture_output=True, text=True)
+
+
+def seed_mac() -> None:
+ print("=== macOS ===")
+ run(["osascript", "-e", 'tell application "OSGKeyboard" to quit'], check=False)
+ run(["pkill", "-x", "OSGKeyboard"], check=False)
+ time.sleep(0.5)
+ if MAC_APP.exists():
+ run(["open", str(MAC_APP)], check=False)
+ else:
+ run(["open", "-a", "OSGKeyboard"], check=False)
+ time.sleep(2.0)
+ # openURL while app is running
+ result = run(["open", SEED_URL], check=False)
+ if result.returncode != 0:
+ print("open URL failed:", result.stderr, file=sys.stderr)
+ else:
+ print(f"opened {SEED_URL}")
+ time.sleep(1.5)
+
+
+def seed_sim(udid: str) -> None:
+ print(f"=== simulator {udid} ===")
+ run(["xcrun", "simctl", "boot", udid], check=False)
+ run(["xcrun", "simctl", "bootstatus", udid, "-b"], check=False)
+ # Ensure app is frontmost
+ run(["xcrun", "simctl", "terminate", udid, BUNDLE_IOS], check=False)
+ time.sleep(0.3)
+ launch = run(["xcrun", "simctl", "launch", udid, BUNDLE_IOS], check=False)
+ if launch.returncode != 0:
+ raise RuntimeError(f"launch failed: {launch.stderr.strip()}")
+ time.sleep(2.0)
+ opened = run(["xcrun", "simctl", "openurl", udid, SEED_URL], check=False)
+ if opened.returncode != 0:
+ raise RuntimeError(f"openurl failed: {opened.stderr.strip()}")
+ print(f"opened {SEED_URL}")
+ time.sleep(1.5)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--mac", action="store_true")
+ parser.add_argument("--sim", action="append", default=[])
+ parser.add_argument("--all", action="store_true")
+ args = parser.parse_args()
+
+ sims = list(args.sim)
+ do_mac = args.mac
+ if args.all:
+ do_mac = True
+ for udid in [
+ "D6D8DA15-704E-4A4E-8A47-7AAB5A9DE4C4", # iPad A16
+ "B3EB5C4D-6802-42A7-92C1-56530523F8E3", # iPhone 17 Pro
+ ]:
+ if udid not in sims:
+ sims.append(udid)
+
+ if not do_mac and not sims:
+ parser.error("Pass --mac, --sim UDID, or --all")
+
+ if do_mac:
+ seed_mac()
+ for udid in sims:
+ try:
+ seed_sim(udid)
+ except Exception as exc: # noqa: BLE001
+ print(f"skip {udid}: {exc}", file=sys.stderr)
+ print("done — check Home / History / Dictionary in each running app")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/assets/hero-devices.png b/docs/assets/hero-devices.png
new file mode 100644
index 0000000..451fd37
Binary files /dev/null and b/docs/assets/hero-devices.png differ
diff --git a/docs/assets/screenshots/en/light/ipad-home.png b/docs/assets/screenshots/en/light/ipad-home.png
index 1f63b94..8c101c0 100644
Binary files a/docs/assets/screenshots/en/light/ipad-home.png and b/docs/assets/screenshots/en/light/ipad-home.png differ
diff --git a/docs/assets/screenshots/zh/dark/ipad-home.png b/docs/assets/screenshots/zh/dark/ipad-home.png
index 3be116b..9c4f7f3 100644
Binary files a/docs/assets/screenshots/zh/dark/ipad-home.png and b/docs/assets/screenshots/zh/dark/ipad-home.png differ
diff --git a/docs/assets/screenshots/zh/light/ipad-home.png b/docs/assets/screenshots/zh/light/ipad-home.png
index e59bb81..ad19235 100644
Binary files a/docs/assets/screenshots/zh/light/ipad-home.png and b/docs/assets/screenshots/zh/light/ipad-home.png differ
diff --git a/docs/index.html b/docs/index.html
index d3346a5..5e153f0 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -41,6 +41,7 @@
--bg: #f7f7f5;
--bg-elevated: #ffffff;
--bg-soft: #efefec;
+ --hero-wash: #e7f3eb;
--text: #121214;
--text-2: #5a5a62;
--text-3: #8e8e96;
@@ -60,6 +61,7 @@
--bg: #0a0a0b;
--bg-elevated: #131316;
--bg-soft: #18181b;
+ --hero-wash: #102018;
--text: #f4f4f2;
--text-2: #a8a8b0;
--text-3: #6f6f78;
@@ -77,6 +79,7 @@
--bg: #0a0a0b;
--bg-elevated: #131316;
--bg-soft: #18181b;
+ --hero-wash: #102018;
--text: #f4f4f2;
--text-2: #a8a8b0;
--text-3: #6f6f78;
@@ -219,24 +222,15 @@
/* —— Hero —— */
.hero {
position: relative;
- padding: 4.5rem 0 2rem;
+ /* Full-bleed solid wash (no side gaps from inset gradients). */
+ background: var(--hero-wash);
+ padding: 4.5rem 0 2.5rem;
overflow: hidden;
}
- .hero::before {
- content: "";
- position: absolute;
- inset: -20% 10% auto;
- height: 70%;
- background:
- radial-gradient(ellipse at 30% 40%, var(--accent-soft), transparent 55%),
- radial-gradient(ellipse at 75% 20%, rgba(47, 154, 82, 0.08), transparent 50%);
- pointer-events: none;
- z-index: 0;
- }
.hero .wrap { position: relative; z-index: 1; }
.hero-copy {
max-width: 42rem;
- margin: 0 auto 3rem;
+ margin: 0 auto 2.5rem;
text-align: center;
}
.eyebrow {
@@ -286,176 +280,19 @@
}
.hero-meta .material-symbols-outlined { font-size: 16px; color: var(--accent); }
- /* —— Hero device family (Mac + iPad + iPhone) —— */
+ /* Marketing device-family composite (replaces CSS mockup frames). */
.hero-stage {
position: relative;
- margin-top: 2.75rem;
+ margin: 2.25rem auto 0;
max-width: 1080px;
- margin-left: auto;
- margin-right: auto;
- }
- .device-cluster {
- position: relative;
- width: 100%;
- aspect-ratio: 16 / 10;
- min-height: 280px;
transform: translateY(16px);
opacity: 0;
animation: rise-in 1s cubic-bezier(0.22, 1, 0.36, 1) 0.12s forwards;
- --device-shell: #1c1c1e;
- --device-shell-2: #2c2c2e;
- --device-edge: rgba(255, 255, 255, 0.14);
- --device-shadow: 0 28px 60px rgba(0, 0, 0, 0.28);
}
- html[data-theme="light"] .device-cluster {
- --device-shell: #d8d8dc;
- --device-shell-2: #c4c4c8;
- --device-edge: rgba(0, 0, 0, 0.12);
- --device-shadow: 0 24px 50px rgba(18, 18, 20, 0.16);
- }
- @media (prefers-color-scheme: light) {
- html[data-theme="system"] .device-cluster {
- --device-shell: #d8d8dc;
- --device-shell-2: #c4c4c8;
- --device-edge: rgba(0, 0, 0, 0.12);
- --device-shadow: 0 24px 50px rgba(18, 18, 20, 0.16);
- }
- }
-
- .device {
- position: absolute;
- box-sizing: border-box;
- }
- .device img {
- display: block;
+ .hero-stage img {
width: 100%;
- height: 100%;
- object-fit: cover;
- object-position: top center;
- }
-
- /* MacBook — back right */
- .device-mac {
- right: 0;
- top: 4%;
- width: 72%;
- z-index: 1;
- filter: drop-shadow(var(--device-shadow));
- }
- .mac-lid {
- border-radius: 10px 10px 0 0;
- background: linear-gradient(180deg, var(--device-shell-2), var(--device-shell));
- padding: 1.4% 1.4% 0.9%;
- border: 1px solid var(--device-edge);
- border-bottom: none;
- }
- .mac-bezel {
- position: relative;
- border-radius: 6px 6px 2px 2px;
- overflow: hidden;
- background: #000;
- aspect-ratio: 16 / 10;
- }
- .mac-notch {
- position: absolute;
- top: 0;
- left: 50%;
- transform: translateX(-50%);
- width: 18%;
- height: 3.2%;
- max-height: 12px;
- background: #0a0a0b;
- border-radius: 0 0 7px 7px;
- z-index: 2;
- }
- .mac-screen {
- position: absolute;
- inset: 0;
- overflow: hidden;
- }
- .mac-base {
- height: 10px;
- margin: 0 -1.5% 0;
- border-radius: 0 0 10px 10px;
- background: linear-gradient(180deg, var(--device-shell), var(--device-shell-2));
- border: 1px solid var(--device-edge);
- border-top: none;
- position: relative;
- }
- .mac-base::after {
- content: "";
- position: absolute;
- left: 50%;
- top: 2px;
- transform: translateX(-50%);
- width: 16%;
- height: 4px;
- border-radius: 0 0 4px 4px;
- background: rgba(0, 0, 0, 0.22);
- }
-
- /* iPad — center */
- .device-ipad {
- left: 22%;
- top: 18%;
- width: 36%;
- z-index: 2;
- filter: drop-shadow(0 22px 40px rgba(0, 0, 0, 0.22));
- }
- .ipad-bezel {
- border-radius: 18px;
- background: linear-gradient(160deg, var(--device-shell-2), var(--device-shell));
- padding: 2.2%;
- border: 1px solid var(--device-edge);
- }
- .ipad-screen {
- border-radius: 10px;
- overflow: hidden;
- background: #000;
- aspect-ratio: 3 / 4;
- }
-
- /* iPhone — front left */
- .device-phone {
- left: 4%;
- bottom: 2%;
- width: 20%;
- z-index: 3;
- filter: drop-shadow(0 18px 32px rgba(0, 0, 0, 0.26));
- }
- .phone-bezel {
- position: relative;
- border-radius: 22px;
- background: linear-gradient(160deg, var(--device-shell-2), var(--device-shell));
- padding: 3.2%;
- border: 1px solid var(--device-edge);
- }
- .phone-island {
- position: absolute;
- top: 5.5%;
- left: 50%;
- transform: translateX(-50%);
- width: 32%;
- height: 3.8%;
- border-radius: 20px;
- background: #0a0a0b;
- z-index: 2;
- }
- .phone-screen {
- border-radius: 16px;
- overflow: hidden;
- background: #000;
- aspect-ratio: 9 / 19.5;
- }
-
- @media (max-width: 720px) {
- .device-cluster { aspect-ratio: 5 / 4; }
- .device-mac { width: 78%; right: -2%; top: 0; }
- .device-ipad { width: 42%; left: 14%; top: 22%; }
- .device-phone { width: 26%; left: 2%; bottom: 0; }
- .phone-bezel { border-radius: 18px; }
- .phone-screen { border-radius: 12px; }
- .ipad-bezel { border-radius: 14px; }
+ height: auto;
+ display: block;
}
@keyframes rise-in {
@@ -588,12 +425,13 @@
}
.shot {
margin: 0;
- border-radius: 16px;
- overflow: hidden;
- border: 1px solid var(--line);
- background: var(--bg-elevated);
- box-shadow: var(--shadow);
line-height: 0;
+ /* Desktop/Mac window PNGs already include chrome — no card fill behind them. */
+ border: none;
+ background: transparent;
+ box-shadow: none;
+ border-radius: 0;
+ overflow: visible;
}
.shot img {
width: 100%;
@@ -605,6 +443,10 @@
max-width: 300px;
width: 100%;
margin-inline: auto;
+ overflow: hidden;
+ border: 1px solid var(--line);
+ background: var(--bg-elevated);
+ box-shadow: var(--shadow);
}
/* —— Device strip —— */
@@ -617,12 +459,12 @@
}
.device-card {
margin: 0;
- border-radius: 16px;
- overflow: hidden;
- border: 1px solid var(--line);
- background: var(--bg-elevated);
- box-shadow: var(--shadow);
line-height: 0;
+ border: none;
+ background: transparent;
+ box-shadow: none;
+ border-radius: 0;
+ overflow: visible;
}
.device-card img {
width: 100%;
@@ -637,6 +479,10 @@
flex: 0 0 auto;
width: min(260px, 46vw);
border-radius: 26px;
+ overflow: hidden;
+ border: 1px solid var(--line);
+ background: var(--bg-elevated);
+ box-shadow: var(--shadow);
}
.device-caption {
margin-top: 0.65rem;
@@ -661,9 +507,7 @@
.compare {
border-radius: calc(var(--radius) + 4px);
border: 1px solid var(--line);
- background:
- linear-gradient(180deg, var(--accent-soft), transparent 42%),
- var(--bg-elevated);
+ background: var(--hero-wash);
padding: 2rem 1.5rem;
overflow: hidden;
}
@@ -793,7 +637,7 @@
transition: none !important;
}
.reveal { opacity: 1; transform: none; }
- .device-cluster { opacity: 1; transform: none; }
+ .hero-stage { opacity: 1; transform: none; }
}
@@ -857,34 +701,7 @@
-
-
-
-
-
-
-

-
-
-
-
-
-
-
-
-

-
-
-
-
-
-
-
-

-
-
-
-
+
@@ -1357,7 +1174,7 @@
}
// Soft parallax after hero entrance animation settles
- const stage = document.querySelector(".device-cluster");
+ const stage = document.querySelector(".hero-stage");
if (stage && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
let ready = false;
setTimeout(() => { ready = true; }, 1100);
diff --git a/project.yml b/project.yml
index c648775..89b06dd 100644
--- a/project.yml
+++ b/project.yml
@@ -469,6 +469,10 @@ targets:
NSSpeechRecognitionUsageDescription: "OSGKeyboard uses on-device speech recognition for local dictation mode."
NSHumanReadableCopyright: "OSGKeyboard"
ITSAppUsesNonExemptEncryption: false
+ CFBundleURLTypes:
+ - CFBundleURLName: com.osgkeyboard.mac.seed
+ CFBundleURLSchemes:
+ - osgkeyboard
settings:
base:
PRODUCT_NAME: OSGKeyboard