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 @@
-
-
-
-
- -
- OSGKeyboard on Mac -
-
-
- -
-
-
-
- OSGKeyboard on iPad -
-
-
-
-
- -
- OSGKeyboard on iPhone -
-
-
-
+ OSGKeyboard on iPad, Mac, and iPhone
@@ -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