mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
feat(ios): refresh onboarding setup flow (#98868)
This commit is contained in:
parent
a1f4aa4a3b
commit
2db5bd3394
17 changed files with 2015 additions and 1002 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1089,138 +1089,138 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Connect to a Gateway?": {
|
||||
"Connect a nearby Gateway": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connect to a Gateway?"
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "连接到网关?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "要連線到 Gateway 嗎?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Conectar a um Gateway?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Mit einem Gateway verbinden?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "¿Conectar a un Gateway?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ゲートウェイに接続しますか?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "게이트웨이에 연결할까요?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Se connecter à une passerelle ?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "किसी गेटवे से कनेक्ट करें?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "الاتصال ببوابة؟"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connettersi a un Gateway?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Bir Gateway’e bağlanılsın mı?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Підключитися до шлюзу?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Hubungkan ke Gateway?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Połączyć z Gateway?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "เชื่อมต่อกับ Gateway หรือไม่?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Kết nối với Gateway?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbinden met een gateway?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "به یک Gateway متصل شوید؟"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Подключиться к шлюзу?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ansluta till en gateway?"
|
||||
"state": "new",
|
||||
"value": "Connect a nearby Gateway"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1769,12 +1769,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Don’t show this again": {
|
||||
"Don't show this again": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Don’t show this again"
|
||||
"value": "Don't show this again"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
|
|
@ -3265,138 +3265,138 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.": {
|
||||
"Looking for a Gateway": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled."
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "尚未找到网关。请确保你的网关正在运行,并且已启用 Bonjour 发现。"
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "尚未找到任何 gateway。請確認你的 gateway 正在執行,且已啟用 Bonjour 探索。"
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nenhum gateway encontrado ainda. Certifique-se de que seu gateway esteja em execução e que a descoberta Bonjour esteja ativada."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Noch keine Gateways gefunden. Stelle sicher, dass dein Gateway ausgeführt wird und die Bonjour-Erkennung aktiviert ist."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Aún no se encontraron gateways. Asegúrate de que tu gateway esté en ejecución y que el descubrimiento Bonjour esté activado."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ゲートウェイがまだ見つかりません。ゲートウェイが実行中で、Bonjour 検出が有効になっていることを確認してください。"
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "아직 게이트웨이를 찾을 수 없습니다. 게이트웨이가 실행 중이고 Bonjour 검색이 활성화되어 있는지 확인하세요."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Aucune passerelle trouvée pour le moment. Assurez-vous que votre passerelle est en cours d’exécution et que la découverte Bonjour est activée."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "अभी तक कोई गेटवे नहीं मिला। सुनिश्चित करें कि आपका गेटवे चल रहा है और Bonjour डिस्कवरी सक्षम है।"
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "لم يتم العثور على أي بوابات بعد. تأكد من أن بوابتك قيد التشغيل وأن اكتشاف Bonjour ممكّن."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nessun gateway trovato al momento. Assicurati che il gateway sia in esecuzione e che il rilevamento Bonjour sia abilitato."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Henüz gateway bulunamadı. Gateway’inizin çalıştığından ve Bonjour keşfinin etkin olduğundan emin olun."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Шлюзів поки не знайдено. Переконайтеся, що ваш шлюз запущено, а виявлення Bonjour увімкнено."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Belum ada gateway yang ditemukan. Pastikan gateway Anda berjalan dan penemuan Bonjour diaktifkan."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nie znaleziono jeszcze żadnych gatewayów. Upewnij się, że gateway działa i że wykrywanie Bonjour jest włączone."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ยังไม่พบ gateway ตรวจสอบให้แน่ใจว่า gateway ของคุณกำลังทำงานอยู่และเปิดใช้งานการค้นหา Bonjour แล้ว"
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Chưa tìm thấy gateway nào. Hãy đảm bảo gateway của bạn đang chạy và tính năng khám phá Bonjour được bật."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nog geen gateways gevonden. Zorg ervoor dat je gateway actief is en Bonjour-detectie is ingeschakeld."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "هنوز هیچ gatewayای پیدا نشده است. مطمئن شوید gateway شما در حال اجراست و Bonjour discovery فعال است."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Шлюзы пока не найдены. Убедитесь, что ваш шлюз запущен и обнаружение Bonjour включено."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Inga gateways hittades ännu. Kontrollera att din gateway körs och att Bonjour-upptäckt är aktiverad."
|
||||
"state": "new",
|
||||
"value": "Looking for a Gateway"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4353,138 +4353,138 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Save": {
|
||||
"Go to Chat": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Save"
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"zh-CN": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "保存"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"zh-TW": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "儲存"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Salvar"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Speichern"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Guardar"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "保存"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "저장"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Enregistrer"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"hi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "सहेजें"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "حفظ"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Salva"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Kaydet"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Зберегти"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Simpan"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Zapisz"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"th": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "บันทึก"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"vi": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Lưu"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Opslaan"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"fa": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ذخیره"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Сохранить"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Spara"
|
||||
"state": "new",
|
||||
"value": "Go to Chat"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Observation
|
||||
import OpenClawChatUI
|
||||
import SwiftUI
|
||||
|
||||
enum AppAppearancePreference: String, CaseIterable, Identifiable {
|
||||
|
|
@ -79,11 +80,9 @@ enum OpenClawBrand {
|
|||
static let uiAccentForeground = adaptiveUIColor(light: (183, 56, 51), dark: (255, 107, 102))
|
||||
static let uiAccentHot = adaptiveUIColor(light: (204, 75, 69), dark: (232, 92, 86))
|
||||
static let uiAccentHotForeground = adaptiveUIColor(light: (166, 55, 50), dark: (255, 123, 115))
|
||||
static let uiAccentPressed = adaptiveUIColor(light: (148, 40, 36), dark: (148, 40, 36))
|
||||
static let uiTeal = adaptiveUIColor(light: (0, 196, 176), dark: (0, 196, 176))
|
||||
static let uiVoid = adaptiveUIColor(light: (246, 247, 249), dark: (11, 12, 17))
|
||||
static let uiObsidian = adaptiveUIColor(light: (255, 255, 255), dark: (19, 21, 28))
|
||||
static let uiTextPrimary = adaptiveUIColor(light: (11, 12, 17), dark: (242, 239, 232))
|
||||
static let uiTextSecondary = adaptiveUIColor(light: (90, 94, 110), dark: (168, 170, 191))
|
||||
static let uiOK = adaptiveUIColor(light: (19, 122, 62), dark: (48, 209, 88))
|
||||
static let uiWarn = adaptiveUIColor(light: (154, 87, 0), dark: (255, 214, 10))
|
||||
|
|
@ -94,11 +93,9 @@ enum OpenClawBrand {
|
|||
static let accentForeground = Color(uiColor: Self.uiAccentForeground)
|
||||
static let accentHot = Color(uiColor: Self.uiAccentHot)
|
||||
static let accentHotForeground = Color(uiColor: Self.uiAccentHotForeground)
|
||||
static let accentPressed = Color(uiColor: Self.uiAccentPressed)
|
||||
static let teal = Color(uiColor: Self.uiTeal)
|
||||
static let void = Color(uiColor: Self.uiVoid)
|
||||
static let obsidian = Color(uiColor: Self.uiObsidian)
|
||||
static let textPrimary = Color(uiColor: Self.uiTextPrimary)
|
||||
static let textSecondary = Color(uiColor: Self.uiTextSecondary)
|
||||
static let danger = Color(uiColor: Self.uiDanger)
|
||||
static let ok = Color(uiColor: Self.uiOK)
|
||||
|
|
@ -106,6 +103,31 @@ enum OpenClawBrand {
|
|||
static let info = Color(uiColor: Self.uiInfo)
|
||||
static let graphite = void
|
||||
static let graphiteElevated = obsidian
|
||||
static let activationCanvas = Color(uiColor: adaptiveUIColor(light: (255, 255, 255), dark: (18, 14, 15)))
|
||||
static let activationSurface = Color(uiColor: adaptiveUIColor(light: (255, 253, 252), dark: (33, 29, 30)))
|
||||
static let activationSecondaryActionTop = Color(uiColor: adaptiveUIColor(
|
||||
light: (255, 255, 255),
|
||||
dark: (36, 32, 33)))
|
||||
static let activationSecondaryActionBottom = Color(uiColor: adaptiveUIColor(
|
||||
light: (248, 248, 250),
|
||||
dark: (30, 27, 28)))
|
||||
static let activationInsetSurface = Color(uiColor: adaptiveUIColor(light: (246, 241, 238), dark: (44, 37, 38)))
|
||||
static let activationNeutralSurface = Color(uiColor: adaptiveUIColor(light: (242, 242, 247), dark: (34, 34, 37)))
|
||||
static let activationNeutralInsetSurface = Color(uiColor: adaptiveUIColor(
|
||||
light: (247, 247, 249),
|
||||
dark: (42, 42, 45)))
|
||||
static let activationNeutralStroke = Color(uiColor: adaptiveUIColor(
|
||||
light: (0, 0, 0),
|
||||
dark: (255, 255, 255)).withAlphaComponent(0.08))
|
||||
static let activationNeutralDivider = Color(uiColor: adaptiveUIColor(
|
||||
light: (0, 0, 0),
|
||||
dark: (255, 255, 255)).withAlphaComponent(0.09))
|
||||
static let activationPrimaryAction = Color(uiColor: adaptiveUIColor(light: (209, 54, 51), dark: (238, 82, 76)))
|
||||
static let activationPrimaryActionText = Color.white
|
||||
static let activationHairline = Color(uiColor: adaptiveUIColor(
|
||||
light: (136, 44, 40),
|
||||
dark: (255, 210, 205)).withAlphaComponent(0.13))
|
||||
static let activationGlow = Color(uiColor: adaptiveUIColor(light: (228, 78, 67), dark: (255, 111, 96)))
|
||||
|
||||
static var sheetBackground: LinearGradient {
|
||||
LinearGradient(
|
||||
|
|
@ -118,6 +140,59 @@ enum OpenClawBrand {
|
|||
endPoint: .bottomTrailing)
|
||||
}
|
||||
|
||||
static var activationCanvasGradient: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
activationCanvas,
|
||||
activationCanvas,
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing)
|
||||
}
|
||||
|
||||
static var activationInsetGradient: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
activationInsetSurface.opacity(0.94),
|
||||
activationInsetSurface,
|
||||
activationSurface.opacity(0.36),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
}
|
||||
|
||||
static var activationNeutralGradient: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
activationNeutralInsetSurface,
|
||||
activationNeutralSurface,
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
}
|
||||
|
||||
static var activationPrimaryGradient: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 1.0, green: 0.42, blue: 0.34),
|
||||
activationGlow,
|
||||
activationPrimaryAction,
|
||||
Color(red: 0.66, green: 0.12, blue: 0.12),
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing)
|
||||
}
|
||||
|
||||
static var activationDisabledGradient: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
activationNeutralInsetSurface,
|
||||
activationNeutralSurface.opacity(0.92),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
}
|
||||
|
||||
private static func adaptiveUIColor(
|
||||
light: (red: CGFloat, green: CGFloat, blue: CGFloat),
|
||||
dark: (red: CGFloat, green: CGFloat, blue: CGFloat)) -> UIColor
|
||||
|
|
@ -133,6 +208,21 @@ enum OpenClawBrand {
|
|||
}
|
||||
}
|
||||
|
||||
struct OpenClawActivationGlyph: View {
|
||||
let size: CGFloat
|
||||
|
||||
var body: some View {
|
||||
OpenClawMascotView(floats: false)
|
||||
.frame(width: self.size, height: self.size)
|
||||
.shadow(
|
||||
color: OpenClawBrand.activationGlow.opacity(0.18),
|
||||
radius: self.size * 0.12,
|
||||
x: 0,
|
||||
y: self.size * 0.05)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func openClawSheetChrome() -> some View {
|
||||
self
|
||||
|
|
@ -142,4 +232,222 @@ extension View {
|
|||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
func openClawCraftSurface(cornerRadius: CGFloat = 24, shadow: Bool = true) -> some View {
|
||||
self.modifier(OpenClawCraftSurfaceModifier(cornerRadius: cornerRadius, shadow: shadow))
|
||||
}
|
||||
}
|
||||
|
||||
private struct OpenClawCraftSurfaceModifier: ViewModifier {
|
||||
let cornerRadius: CGFloat
|
||||
let shadow: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous)
|
||||
.fill(OpenClawBrand.activationSurface)
|
||||
.shadow(
|
||||
color: self.shadow ? Color.black.opacity(0.07) : .clear,
|
||||
radius: 16,
|
||||
x: 0,
|
||||
y: 8)
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous)
|
||||
.stroke(Color.white.opacity(0.36), lineWidth: 0.5)
|
||||
.blendMode(.plusLighter)
|
||||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous)
|
||||
.stroke(OpenClawBrand.activationHairline, lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenClawPrimaryActionButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
var height: CGFloat = 54
|
||||
var cornerRadius: CGFloat = 18
|
||||
|
||||
private var resolvedCornerRadius: CGFloat {
|
||||
self.height / 2
|
||||
}
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(self.isEnabled ? OpenClawBrand.activationPrimaryActionText : Color.secondary)
|
||||
.tint(self.isEnabled ? OpenClawBrand.activationPrimaryActionText : Color.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: self.height)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous)
|
||||
.fill(self.isEnabled ? Self.primaryFill : OpenClawBrand.activationDisabledGradient)
|
||||
.shadow(
|
||||
color: self.isEnabled ? OpenClawBrand.activationPrimaryAction.opacity(0.08) : .clear,
|
||||
radius: 1,
|
||||
x: 0,
|
||||
y: 1)
|
||||
.shadow(
|
||||
color: self.isEnabled ? OpenClawBrand.activationPrimaryAction.opacity(0.08) : .clear,
|
||||
radius: 2,
|
||||
x: 0,
|
||||
y: 2)
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.white.opacity(self.isEnabled ? 0.14 : 0.06),
|
||||
Color.white.opacity(0),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .center))
|
||||
.frame(height: self.height * 0.48)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.white.opacity(0),
|
||||
Color.white.opacity(self.isEnabled ? 0.08 : 0.03),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom))
|
||||
.frame(height: self.height * 0.34)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous)
|
||||
.stroke(
|
||||
self.isEnabled
|
||||
? OpenClawBrand.activationPrimaryAction.opacity(0.72)
|
||||
: OpenClawBrand.activationNeutralStroke,
|
||||
lineWidth: 0.75)
|
||||
}
|
||||
.scaleEffect(configuration.isPressed && self.isEnabled ? 0.98 : 1)
|
||||
.animation(.smooth(duration: 0.14), value: configuration.isPressed)
|
||||
}
|
||||
|
||||
private static var primaryFill: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color(red: 0.93, green: 0.27, blue: 0.25),
|
||||
Color(red: 0.91, green: 0.25, blue: 0.24),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenClawSecondaryActionButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
var height: CGFloat = 50
|
||||
var cornerRadius: CGFloat = 18
|
||||
var shadowOpacity: Double = 0.035
|
||||
|
||||
private var resolvedCornerRadius: CGFloat {
|
||||
self.height / 2
|
||||
}
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(self.isEnabled ? OpenClawBrand.activationPrimaryAction : .secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: self.height)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous)
|
||||
.fill(Self.secondaryFill)
|
||||
.shadow(
|
||||
color: self.isEnabled ? Color.black.opacity(self.shadowOpacity) : .clear,
|
||||
radius: 1,
|
||||
x: 0,
|
||||
y: 1)
|
||||
.shadow(
|
||||
color: self.isEnabled ? Color.black.opacity(self.shadowOpacity) : .clear,
|
||||
radius: 2,
|
||||
x: 0,
|
||||
y: 2)
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.white.opacity(0.42),
|
||||
Color.white.opacity(0),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .center))
|
||||
.frame(height: self.height * 0.48)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous)
|
||||
.stroke(OpenClawBrand.activationHairline, lineWidth: 0.75)
|
||||
}
|
||||
.scaleEffect(configuration.isPressed && self.isEnabled ? 0.98 : 1)
|
||||
.animation(.smooth(duration: 0.14), value: configuration.isPressed)
|
||||
}
|
||||
|
||||
private static var secondaryFill: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
OpenClawBrand.activationSecondaryActionTop,
|
||||
OpenClawBrand.activationSecondaryActionBottom,
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenClawCloseButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
var minWidth: CGFloat = 36
|
||||
var height: CGFloat = 36
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(
|
||||
self.isEnabled
|
||||
? OpenClawBrand.activationPrimaryAction
|
||||
: Color.secondary)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
.frame(minWidth: self.minWidth)
|
||||
.frame(height: self.height)
|
||||
.padding(.horizontal, 7)
|
||||
.background {
|
||||
Capsule(style: .continuous)
|
||||
.fill(OpenClawBrand.activationNeutralGradient)
|
||||
.shadow(
|
||||
color: self.isEnabled ? Color.black.opacity(0.045) : .clear,
|
||||
radius: 1,
|
||||
x: 0,
|
||||
y: 1)
|
||||
.shadow(
|
||||
color: self.isEnabled ? Color.black.opacity(0.03) : .clear,
|
||||
radius: 4,
|
||||
x: 0,
|
||||
y: 2)
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
Capsule(style: .continuous)
|
||||
.stroke(Color.white.opacity(0.55), lineWidth: 0.5)
|
||||
.blendMode(.plusLighter)
|
||||
}
|
||||
.overlay {
|
||||
Capsule(style: .continuous)
|
||||
.stroke(OpenClawBrand.activationNeutralStroke, lineWidth: 0.6)
|
||||
}
|
||||
.contentShape(Capsule(style: .continuous))
|
||||
.opacity(configuration.isPressed && self.isEnabled ? 0.66 : 1)
|
||||
.scaleEffect(configuration.isPressed && self.isEnabled ? 0.98 : 1)
|
||||
.animation(.smooth(duration: 0.14), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ enum OpenClawRadius {
|
|||
static let xs: CGFloat = 8
|
||||
static let sm: CGFloat = 10
|
||||
static let md: CGFloat = 12
|
||||
static let lg: CGFloat = 16
|
||||
}
|
||||
|
||||
struct OpenClawProBackground: View {
|
||||
|
|
@ -561,62 +560,6 @@ struct OpenClawStatusBadge: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct OpenClawPrimaryButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(OpenClawType.headline)
|
||||
.foregroundStyle(self.isEnabled ? Color.white : OpenClawBrand.textSecondary)
|
||||
.frame(maxWidth: .infinity, minHeight: 48)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous)
|
||||
.fill(
|
||||
!self.isEnabled
|
||||
? Color(uiColor: .tertiarySystemFill)
|
||||
: configuration.isPressed
|
||||
? OpenClawBrand.accentPressed
|
||||
: OpenClawBrand.accent)
|
||||
}
|
||||
.animation(.easeOut(duration: 0.15), value: configuration.isPressed)
|
||||
.animation(.easeOut(duration: 0.15), value: self.isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenClawSecondaryButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(OpenClawType.headline)
|
||||
.foregroundStyle(self.isEnabled ? OpenClawBrand.textPrimary : OpenClawBrand.textSecondary.opacity(0.68))
|
||||
.frame(maxWidth: .infinity, minHeight: 48)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous)
|
||||
.fill(Color(uiColor: self.isEnabled ? .secondarySystemBackground : .tertiarySystemFill))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous)
|
||||
.strokeBorder(
|
||||
Color(uiColor: .separator).opacity(self.isEnabled ? 0.35 : 0.20),
|
||||
lineWidth: 1)
|
||||
}
|
||||
}
|
||||
.opacity(!self.isEnabled ? 0.74 : configuration.isPressed ? 0.82 : 1)
|
||||
.animation(.easeOut(duration: 0.15), value: configuration.isPressed)
|
||||
.animation(.easeOut(duration: 0.15), value: self.isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func openClawPrimaryButton() -> some View {
|
||||
self.buttonStyle(OpenClawPrimaryButtonStyle())
|
||||
}
|
||||
|
||||
func openClawSecondaryButton() -> some View {
|
||||
self.buttonStyle(OpenClawSecondaryButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
struct ProStatusDot: View {
|
||||
var color: Color
|
||||
|
||||
|
|
|
|||
|
|
@ -129,10 +129,6 @@ enum OpenClawType {
|
|||
scaledMono(name: Mono.regular, size: 13, relativeTo: .footnote)
|
||||
}
|
||||
|
||||
static var monoHeadline: Font {
|
||||
scaledMono(name: Mono.medium, size: 17, relativeTo: .headline)
|
||||
}
|
||||
|
||||
/// PostScript names for bundled fonts. Keep aligned with `UIAppFonts` in `project.yml`.
|
||||
static let registeredPostScriptNames: [String] = [
|
||||
Display.postScriptName,
|
||||
|
|
|
|||
|
|
@ -85,9 +85,13 @@ struct TalkSiriWaveView: View {
|
|||
// across the line instead of scrolling off-screen.
|
||||
let lobes: [(A: Double, k: Double, t: Double)] = (0..<3).map { index in
|
||||
let f = Double(index)
|
||||
let amp = 0.30 + 0.70 * (0.5 + 0.5 * sin(time * (0.9 + 0.23 * f) + seed * 2.4 + f * 2.1))
|
||||
let ampFrequency = 0.9 + 0.23 * f
|
||||
let ampPhase = time * ampFrequency + seed * 2.4 + f * 2.1
|
||||
let amp = 0.30 + 0.70 * (0.5 + 0.5 * sin(ampPhase))
|
||||
let k = 0.62 + 0.11 * f
|
||||
let t = 2.8 * sin(time * (0.45 + 0.17 * f) + seed + f * 1.9)
|
||||
let driftFrequency = 0.45 + 0.17 * f
|
||||
let driftPhase = time * driftFrequency + seed + f * 1.9
|
||||
let t = 2.8 * sin(driftPhase)
|
||||
return (A: amp, k: k, t: t)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import OpenClawKit
|
||||
import SwiftUI
|
||||
|
||||
#if DEBUG
|
||||
import Network
|
||||
#endif
|
||||
|
||||
struct GatewayQuickSetupSheet: View {
|
||||
@Environment(NodeAppModel.self) private var appModel
|
||||
@Environment(GatewayConnectionController.self) private var gatewayController
|
||||
|
|
@ -13,95 +17,88 @@ struct GatewayQuickSetupSheet: View {
|
|||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Connect to a Gateway?")
|
||||
.font(OpenClawType.title2)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
GatewayQuickSetupHeader(hasCandidate: self.bestCandidate != nil)
|
||||
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
GatewayProblemBanner(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
Task { await self.handleGatewayProblemPrimaryAction(gatewayProblem) }
|
||||
},
|
||||
onShowDetails: {
|
||||
self.showGatewayProblemDetails = true
|
||||
})
|
||||
}
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
GatewayProblemBanner(
|
||||
problem: gatewayProblem,
|
||||
primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem),
|
||||
onPrimaryAction: {
|
||||
Task { await self.handleGatewayProblemPrimaryAction(gatewayProblem) }
|
||||
},
|
||||
onShowDetails: {
|
||||
self.showGatewayProblemDetails = true
|
||||
})
|
||||
}
|
||||
|
||||
if let candidate = self.bestCandidate {
|
||||
GatewayQuickSetupCandidatePanel(
|
||||
name: candidate.name,
|
||||
debugID: candidate.debugID,
|
||||
discoveryStatusText: self.gatewayController.discoveryStatusText,
|
||||
gatewayDisplayStatusText: self.appModel.gatewayDisplayStatusText,
|
||||
nodeStatusText: self.appModel.nodeStatusText,
|
||||
operatorStatusText: self.appModel.operatorStatusText)
|
||||
if let candidate = self.bestCandidate {
|
||||
GatewayQuickSetupCandidatePanel(
|
||||
name: candidate.name,
|
||||
debugID: candidate.debugID,
|
||||
tlsEnabled: candidate.tlsEnabled,
|
||||
gatewayPort: candidate.gatewayPort,
|
||||
discoveryStatusText: self.gatewayController.discoveryStatusText,
|
||||
gatewayDisplayStatusText: self.appModel.gatewayDisplayStatusText,
|
||||
nodeStatusText: self.appModel.nodeStatusText,
|
||||
operatorStatusText: self.appModel.operatorStatusText)
|
||||
|
||||
Button {
|
||||
self.connectError = nil
|
||||
self.connecting = true
|
||||
Task {
|
||||
let err = await self.gatewayController.connectWithDiagnostics(candidate)
|
||||
await MainActor.run {
|
||||
self.connecting = false
|
||||
self.connectError = err
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Group {
|
||||
if self.connecting {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView().progressViewStyle(.circular)
|
||||
Text("Connecting…")
|
||||
.font(OpenClawType.headline)
|
||||
Button {
|
||||
self.connectError = nil
|
||||
self.connecting = true
|
||||
Task {
|
||||
let err = await self.gatewayController.connectWithDiagnostics(candidate)
|
||||
await MainActor.run {
|
||||
self.connecting = false
|
||||
self.connectError = err
|
||||
}
|
||||
} else {
|
||||
Text("Connect")
|
||||
.font(OpenClawType.headline)
|
||||
}
|
||||
} label: {
|
||||
Group {
|
||||
if self.connecting {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView().progressViewStyle(.circular)
|
||||
Text("Connecting…")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
} else {
|
||||
Text("Connect to this Gateway")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.font(OpenClawType.headline)
|
||||
.openClawPrimaryButton()
|
||||
.disabled(self.connecting)
|
||||
.buttonStyle(OpenClawPrimaryActionButtonStyle())
|
||||
.disabled(self.connecting)
|
||||
|
||||
if let connectError {
|
||||
Text(connectError)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
if let connectError {
|
||||
GatewayQuickSetupErrorView(message: connectError)
|
||||
}
|
||||
|
||||
Button {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Text("Not now")
|
||||
.font(OpenClawType.headline)
|
||||
}
|
||||
.font(OpenClawType.headline)
|
||||
.openClawSecondaryButton()
|
||||
.disabled(self.connecting)
|
||||
Button {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Text("Not now")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(OpenClawSecondaryActionButtonStyle())
|
||||
.disabled(self.connecting)
|
||||
|
||||
self.fullRowToggle("Don’t show this again", isOn: self.$quickSetupDismissed)
|
||||
.padding(.top, 4)
|
||||
} else {
|
||||
Text("No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.")
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
self.fullRowToggle("Don't show this again", isOn: self.$quickSetupDismissed)
|
||||
.padding(.top, 2)
|
||||
} else {
|
||||
GatewayQuickSetupEmptyState(
|
||||
discoveryStatusText: self.gatewayController.discoveryStatusText)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
.padding(20)
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
.padding()
|
||||
.background(OpenClawBrand.activationCanvasGradient)
|
||||
.navigationTitle("Quick Setup")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text("Quick Setup")
|
||||
.font(OpenClawType.headline)
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
self.quickSetupDismissed = true
|
||||
|
|
@ -110,7 +107,7 @@ struct GatewayQuickSetupSheet: View {
|
|||
Text("Close")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.buttonStyle(OpenClawCloseButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,7 +124,6 @@ struct GatewayQuickSetupSheet: View {
|
|||
}
|
||||
|
||||
private var bestCandidate: GatewayDiscoveryModel.DiscoveredGateway? {
|
||||
// Prefer whatever discovery says is first; the list is already name-sorted.
|
||||
self.gatewayController.gateways.first
|
||||
}
|
||||
|
||||
|
|
@ -136,10 +132,8 @@ struct GatewayQuickSetupSheet: View {
|
|||
Text(title)
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.font(OpenClawType.subhead)
|
||||
.contentShape(Rectangle())
|
||||
.overlay {
|
||||
// Keep Toggle semantics for accessibility while making the full visual row tappable.
|
||||
Button {
|
||||
isOn.wrappedValue.toggle()
|
||||
} label: {
|
||||
|
|
@ -147,7 +141,6 @@ struct GatewayQuickSetupSheet: View {
|
|||
.fill(.clear)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.font(OpenClawType.subhead)
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
|
@ -175,46 +168,291 @@ struct GatewayQuickSetupSheet: View {
|
|||
}
|
||||
}
|
||||
|
||||
private struct GatewayQuickSetupCandidatePanel: View {
|
||||
private static let readableMonospaceWidth: CGFloat = 72 * 8
|
||||
private struct GatewayQuickSetupHeader: View {
|
||||
let hasCandidate: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
OpenClawActivationGlyph(size: 70)
|
||||
.shadow(color: OpenClawBrand.activationGlow.opacity(0.18), radius: 10, x: 0, y: 5)
|
||||
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(OpenClawBrand.activationPrimaryActionText)
|
||||
.frame(width: 28, height: 28)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(OpenClawBrand.activationPrimaryGradient)
|
||||
}
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(OpenClawBrand.activationCanvas, lineWidth: 3)
|
||||
}
|
||||
.offset(x: 4, y: 4)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Connect a nearby Gateway")
|
||||
.font(OpenClawType.title2SemiBold)
|
||||
Text(self.subtitle)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var subtitle: LocalizedStringKey {
|
||||
if self.hasCandidate {
|
||||
return "OpenClaw found a gateway on this network. Review it, then pair this iPhone as a secure node."
|
||||
}
|
||||
return "OpenClaw is searching the local network and tailnet for a Gateway you can trust."
|
||||
}
|
||||
}
|
||||
|
||||
private struct GatewayQuickSetupCandidatePanel: View {
|
||||
let name: String
|
||||
let debugID: String
|
||||
let tlsEnabled: Bool
|
||||
let gatewayPort: Int?
|
||||
let discoveryStatusText: String
|
||||
let gatewayDisplayStatusText: String
|
||||
let nodeStatusText: String
|
||||
let operatorStatusText: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(verbatim: self.name)
|
||||
.font(OpenClawType.monoHeadline)
|
||||
.foregroundStyle(.primary)
|
||||
Text(verbatim: self.debugID)
|
||||
.font(OpenClawType.monoFootnote)
|
||||
.foregroundStyle(.secondary)
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(systemName: "network")
|
||||
.font(OpenClawType.headline)
|
||||
.foregroundStyle(OpenClawBrand.activationPrimaryActionText)
|
||||
.frame(width: 36, height: 36)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(OpenClawBrand.activationPrimaryGradient)
|
||||
}
|
||||
.shadow(color: OpenClawBrand.activationGlow.opacity(0.18), radius: 6, x: 0, y: 3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
// Use verbatim strings so Bonjour-provided values can't be interpreted as
|
||||
// localized format strings (which can crash with Objective-C exceptions).
|
||||
Text(verbatim: "Discovery: \(self.discoveryStatusText)")
|
||||
.font(OpenClawType.monoFootnote)
|
||||
Text(verbatim: "Status: \(self.gatewayDisplayStatusText)")
|
||||
.font(OpenClawType.monoFootnote)
|
||||
Text(verbatim: "Node: \(self.nodeStatusText)")
|
||||
.font(OpenClawType.monoFootnote)
|
||||
Text(verbatim: "Operator: \(self.operatorStatusText)")
|
||||
.font(OpenClawType.monoFootnote)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(verbatim: self.name)
|
||||
.font(OpenClawType.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
|
||||
GatewayQuickSetupChip(text: self.tlsEnabled ? "Secure" : "Local")
|
||||
}
|
||||
|
||||
Text(verbatim: self.endpointSummary)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
|
||||
GatewayQuickSetupHairlineDivider()
|
||||
|
||||
VStack(spacing: 8) {
|
||||
GatewayQuickSetupStatusRow(title: "Discovery", value: self.discoveryStatusText)
|
||||
GatewayQuickSetupStatusRow(title: "Gateway", value: self.gatewayDisplayStatusText)
|
||||
GatewayQuickSetupStatusRow(title: "Node", value: self.nodeStatusText)
|
||||
GatewayQuickSetupStatusRow(title: "Operator", value: self.operatorStatusText)
|
||||
}
|
||||
.font(OpenClawType.monoFootnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: Self.readableMonospaceWidth, alignment: .leading)
|
||||
.padding(.vertical, 14)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
.background(OpenClawBrand.obsidian)
|
||||
.clipShape(RoundedRectangle(cornerRadius: OpenClawRadius.lg))
|
||||
.openClawCraftSurface(cornerRadius: 22)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
private var endpointSummary: String {
|
||||
let port = self.gatewayPort.map { ":\($0)" } ?? ""
|
||||
return "\(self.debugID)\(port)"
|
||||
}
|
||||
}
|
||||
|
||||
private struct GatewayQuickSetupHairlineDivider: View {
|
||||
var body: some View {
|
||||
Rectangle()
|
||||
.fill(OpenClawBrand.activationNeutralDivider)
|
||||
.frame(height: 0.5)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
private struct GatewayQuickSetupChip: View {
|
||||
let text: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
Text(self.text)
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(OpenClawBrand.activationPrimaryAction)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 8)
|
||||
.background {
|
||||
Capsule(style: .continuous)
|
||||
.fill(OpenClawBrand.activationGlow.opacity(0.10))
|
||||
}
|
||||
.overlay {
|
||||
Capsule(style: .continuous)
|
||||
.stroke(OpenClawBrand.activationHairline, lineWidth: 0.6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct GatewayQuickSetupStatusRow: View {
|
||||
let title: LocalizedStringKey
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
Text(self.title)
|
||||
.font(OpenClawType.captionMedium)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 70, alignment: .leading)
|
||||
Text(verbatim: self.value)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct GatewayQuickSetupErrorView: View {
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
.padding(.top, 1)
|
||||
Text(self.message)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(OpenClawBrand.activationInsetGradient)
|
||||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.stroke(OpenClawBrand.activationHairline, lineWidth: 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct GatewayQuickSetupEmptyState: View {
|
||||
let discoveryStatusText: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(OpenClawType.title2SemiBold)
|
||||
.foregroundStyle(OpenClawBrand.activationPrimaryAction)
|
||||
.frame(width: 42, height: 42)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(OpenClawBrand.activationInsetGradient)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Looking for a Gateway")
|
||||
.font(OpenClawType.headline)
|
||||
Text("Keep your iPhone on the same LAN or tailnet, then start the Gateway on your host machine.")
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
GatewayQuickSetupInstructionRow(text: "Run openclaw gateway --port 18789.")
|
||||
GatewayQuickSetupInstructionRow(text: "Check that Bonjour discovery is enabled.")
|
||||
GatewayQuickSetupInstructionRow(text: "Open Settings if you need a manual host.")
|
||||
}
|
||||
.padding(.top, 2)
|
||||
|
||||
Text(verbatim: "Discovery: \(self.discoveryStatusText)")
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.openClawCraftSurface(cornerRadius: 22)
|
||||
}
|
||||
}
|
||||
|
||||
private struct GatewayQuickSetupInstructionRow: View {
|
||||
let text: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Image(systemName: "arrow.right.circle.fill")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(self.text)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#Preview("Quick setup gateway", traits: .fixedLayout(width: 393, height: 520)) {
|
||||
GatewayQuickSetupPreviewHost(hasGateway: true)
|
||||
}
|
||||
|
||||
#Preview("Quick setup searching", traits: .fixedLayout(width: 393, height: 520)) {
|
||||
GatewayQuickSetupPreviewHost(hasGateway: false)
|
||||
}
|
||||
|
||||
private struct GatewayQuickSetupPreviewHost: View {
|
||||
@State private var appModel: NodeAppModel
|
||||
@State private var gatewayController: GatewayConnectionController
|
||||
|
||||
init(hasGateway: Bool) {
|
||||
let appModel = NodeAppModel()
|
||||
let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
||||
if hasGateway {
|
||||
controller._test_setGateways([.previewGateway])
|
||||
appModel.gatewayStatusText = "Ready to pair"
|
||||
}
|
||||
_appModel = State(initialValue: appModel)
|
||||
_gatewayController = State(initialValue: controller)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GatewayQuickSetupSheet()
|
||||
.environment(self.appModel)
|
||||
.environment(self.gatewayController)
|
||||
.openClawSheetChrome()
|
||||
}
|
||||
}
|
||||
|
||||
extension GatewayDiscoveryModel.DiscoveredGateway {
|
||||
fileprivate static let previewGateway = GatewayDiscoveryModel.DiscoveredGateway(
|
||||
name: "Studio Gateway",
|
||||
endpoint: .hostPort(
|
||||
host: .name("openclaw.local", nil),
|
||||
port: 18789),
|
||||
stableID: "preview-gateway",
|
||||
debugID: "openclaw.local",
|
||||
lanHost: "openclaw.local",
|
||||
tailnetDns: nil,
|
||||
gatewayPort: 18789,
|
||||
canvasPort: 18789,
|
||||
tlsEnabled: true,
|
||||
tlsFingerprintSha256: "preview",
|
||||
cliPath: "/opt/homebrew/bin/openclaw")
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,127 +1,279 @@
|
|||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
private enum OnboardingLayout {
|
||||
static let contentMaxWidth: CGFloat = 680
|
||||
private enum OnboardingVisual {
|
||||
static let maxWidth: CGFloat = 430
|
||||
}
|
||||
|
||||
private struct OnboardingActivationCanvas<Content: View>: View {
|
||||
@ViewBuilder let content: Content
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
ScrollView {
|
||||
self.content
|
||||
.frame(maxWidth: OnboardingVisual.maxWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: max(0, proxy.size.height - 94), alignment: .top)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 54)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.background(OpenClawBrand.activationCanvasGradient.ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingHeroGlyph: View {
|
||||
var body: some View {
|
||||
OpenClawActivationGlyph(size: 78)
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingHeroHeader: View {
|
||||
let title: LocalizedStringKey
|
||||
let subtitle: LocalizedStringKey?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 18) {
|
||||
OnboardingHeroGlyph()
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(self.title)
|
||||
.font(OpenClawType.title1)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(OpenClawType.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingWelcomePrompt: View {
|
||||
let text: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
Text(self.text)
|
||||
.font(OpenClawType.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
private typealias OnboardingPrimaryButtonStyle = OpenClawPrimaryActionButtonStyle
|
||||
|
||||
private enum OnboardingIntroPanelStyle {
|
||||
static let iconSize: CGFloat = 34
|
||||
static let contentSpacing: CGFloat = 12
|
||||
static let panelPadding: CGFloat = 16
|
||||
static let panelCornerRadius: CGFloat = 22
|
||||
|
||||
static let panelFill = OpenClawBrand.activationNeutralSurface
|
||||
static let iconFill = OpenClawBrand.activationNeutralInsetSurface
|
||||
static let stroke = OpenClawBrand.activationNeutralStroke
|
||||
}
|
||||
|
||||
private struct OnboardingIntroPanel<Content: View>: View {
|
||||
@ViewBuilder let content: Content
|
||||
|
||||
var body: some View {
|
||||
self.content
|
||||
.padding(Self.panelPadding)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: OnboardingIntroPanelStyle.panelCornerRadius, style: .continuous)
|
||||
.fill(OnboardingIntroPanelStyle.panelFill)
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
RoundedRectangle(cornerRadius: OnboardingIntroPanelStyle.panelCornerRadius, style: .continuous)
|
||||
.stroke(Color.white.opacity(0.42), lineWidth: 0.5)
|
||||
.blendMode(.plusLighter)
|
||||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: OnboardingIntroPanelStyle.panelCornerRadius, style: .continuous)
|
||||
.stroke(OnboardingIntroPanelStyle.stroke, lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
private static var panelPadding: CGFloat {
|
||||
OnboardingIntroPanelStyle.panelPadding
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingIntroIcon: View {
|
||||
let symbol: String
|
||||
let tint: Color
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: self.symbol)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(self.tint)
|
||||
.frame(
|
||||
width: OnboardingIntroPanelStyle.iconSize,
|
||||
height: OnboardingIntroPanelStyle.iconSize)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(OnboardingIntroPanelStyle.iconFill)
|
||||
}
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(OnboardingIntroPanelStyle.stroke, lineWidth: 0.6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingSafetyRow: View {
|
||||
let symbol: String
|
||||
let title: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: OnboardingIntroPanelStyle.contentSpacing) {
|
||||
OnboardingIntroIcon(
|
||||
symbol: self.symbol,
|
||||
tint: OpenClawBrand.activationPrimaryAction)
|
||||
|
||||
Text(self.title)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.primary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingSecurityNotice: View {
|
||||
var body: some View {
|
||||
OnboardingIntroPanel {
|
||||
HStack(alignment: .top, spacing: OnboardingIntroPanelStyle.contentSpacing) {
|
||||
OnboardingIntroIcon(
|
||||
symbol: "exclamationmark.triangle.fill",
|
||||
tint: OpenClawBrand.warn)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Security notice")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.primary)
|
||||
(
|
||||
Text("The connected OpenClaw agent can use device capabilities you enable.")
|
||||
+ Text(verbatim: " ")
|
||||
+ Text(
|
||||
"Camera, microphone, photos, contacts, calendar, and location may be available.")
|
||||
+ Text(verbatim: " ")
|
||||
+ Text(
|
||||
"Continue only if you trust the gateway and agent you connect to."))
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingCommandChip: View {
|
||||
@State private var didCopy = false
|
||||
private let command = "openclaw qr"
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
Text(self.command)
|
||||
.font(OpenClawType.mono)
|
||||
.textSelection(.enabled)
|
||||
Spacer(minLength: 0)
|
||||
Button {
|
||||
self.copyCommand()
|
||||
} label: {
|
||||
Image(systemName: self.didCopy ? "checkmark" : "doc.on.doc")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(
|
||||
self.didCopy ? OpenClawBrand.activationPrimaryAction : Color.secondary.opacity(0.56))
|
||||
.frame(width: 38, height: 38)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityLabel("Copy setup code command")
|
||||
.accessibilityValue(self.didCopy ? "Copied" : self.command)
|
||||
}
|
||||
.foregroundStyle(OpenClawBrand.activationPrimaryAction)
|
||||
.padding(.horizontal, 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.frame(height: 54)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(OpenClawBrand.activationNeutralSurface)
|
||||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.stroke(OpenClawBrand.activationNeutralStroke, lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
private func copyCommand() {
|
||||
UIPasteboard.general.string = self.command
|
||||
withAnimation(.smooth(duration: 0.14)) {
|
||||
self.didCopy = true
|
||||
}
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(nanoseconds: 1_100_000_000)
|
||||
withAnimation(.smooth(duration: 0.16)) {
|
||||
self.didCopy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingIntroStep: View {
|
||||
let onContinue: () -> Void
|
||||
|
||||
private static let features: [(icon: String, title: String, detail: String)] = [
|
||||
("link", "Connect your gateway", "Pair with a quick QR scan."),
|
||||
("hand.raised.fill", "You choose permissions", "Grant only the tools you want."),
|
||||
("bubble.left.and.bubble.right.fill", "Chat, voice, and camera", "All from your phone."),
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
OpenClawProMark(size: 76, shadowRadius: 12)
|
||||
.padding(.top, 32)
|
||||
.padding(.bottom, 20)
|
||||
OnboardingActivationCanvas {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
OnboardingHeroHeader(
|
||||
title: "OpenClaw",
|
||||
subtitle: "Securely connect this iPhone to your gateway.")
|
||||
.padding(.top, 18)
|
||||
|
||||
Text("Welcome to OpenClaw")
|
||||
.font(OpenClawType.title1)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 40)
|
||||
|
||||
VStack(alignment: .leading, spacing: 28) {
|
||||
ForEach(Array(Self.features.enumerated()), id: \.offset) { _, feature in
|
||||
OnboardingFeatureRow(icon: feature.icon, title: feature.title, detail: feature.detail)
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
Text(
|
||||
"The connected agent can use capabilities you enable, including camera, "
|
||||
+ "microphone, photos, contacts, calendar, and location. Continue only if "
|
||||
+ "you trust the gateway and agent.")
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} label: {
|
||||
Label("Security", systemImage: "lock.shield.fill")
|
||||
.font(OpenClawType.headline)
|
||||
}
|
||||
.tint(OpenClawBrand.warn)
|
||||
OnboardingIntroPanel {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
OnboardingSafetyRow(
|
||||
symbol: "link",
|
||||
title: "Connect to your gateway")
|
||||
OnboardingSafetyRow(
|
||||
symbol: "hand.raised",
|
||||
title: "Choose device permissions")
|
||||
OnboardingSafetyRow(
|
||||
symbol: "message.fill",
|
||||
title: "Use OpenClaw from your phone")
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.padding(.top, 44)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
Text("You can change permissions later in Settings.")
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
OnboardingSecurityNotice()
|
||||
.padding(.top, 18)
|
||||
|
||||
Button(action: self.onContinue) {
|
||||
Text("Continue")
|
||||
.font(OpenClawType.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.tint(OpenClawBrand.accent)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.frame(maxWidth: OnboardingLayout.contentMaxWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 40)
|
||||
|
||||
/// Inline command styled like a keyboard key so shell/chat commands stand out from prose.
|
||||
struct KeycapText: View {
|
||||
let text: String
|
||||
|
||||
init(_ text: String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text(self.text)
|
||||
.font(OpenClawType.monoFootnote)
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 4)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 7, style: .continuous)
|
||||
.fill(Color(uiColor: .secondarySystemFill))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 7, style: .continuous)
|
||||
.strokeBorder(Color(uiColor: .separator).opacity(0.6), lineWidth: 0.5)
|
||||
VStack(spacing: 14) {
|
||||
Button {
|
||||
self.onContinue()
|
||||
} label: {
|
||||
Text("Continue")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingFeatureRow: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let detail: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
Image(systemName: self.icon)
|
||||
.font(OpenClawType.title2)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.frame(width: 34, alignment: .center)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.title)
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.detail)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.buttonStyle(OnboardingPrimaryButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -129,118 +281,195 @@ private struct OnboardingFeatureRow: View {
|
|||
|
||||
struct OnboardingWelcomeStep: View {
|
||||
let statusLine: String
|
||||
let isConnecting: Bool
|
||||
let onScanQRCode: () -> Void
|
||||
let onManualSetup: () -> Void
|
||||
|
||||
@State private var pairCommandCopied = false
|
||||
var body: some View {
|
||||
let statusText = self.statusLine.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
OnboardingActivationCanvas {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
OnboardingHeroHeader(
|
||||
title: "Connect Gateway",
|
||||
subtitle: nil)
|
||||
.padding(.top, 18)
|
||||
|
||||
VStack(spacing: 36) {
|
||||
VStack(spacing: 14) {
|
||||
OnboardingWelcomePrompt(text: "Run this on your gateway host and scan the code")
|
||||
|
||||
OnboardingCommandChip()
|
||||
|
||||
Button(action: self.onScanQRCode) {
|
||||
if self.isConnecting {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.tint(OpenClawBrand.activationPrimaryActionText)
|
||||
Text("Connecting…")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
} else {
|
||||
Text("Scan QR")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
.buttonStyle(OnboardingPrimaryButtonStyle())
|
||||
.disabled(self.isConnecting)
|
||||
}
|
||||
|
||||
VStack(spacing: 14) {
|
||||
OnboardingWelcomePrompt(text: "or")
|
||||
|
||||
Button(action: self.onManualSetup) {
|
||||
Text("Connect Manually")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.buttonStyle(OpenClawSecondaryActionButtonStyle(height: 54, shadowOpacity: 0.018))
|
||||
.disabled(self.isConnecting)
|
||||
}
|
||||
}
|
||||
.padding(.top, 46)
|
||||
|
||||
if !statusText.isEmpty {
|
||||
Text(verbatim: statusText)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.top, 14)
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
Spacer(minLength: 40)
|
||||
}
|
||||
.animation(.smooth(duration: 0.18), value: statusText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingSuccessStep: View {
|
||||
let gatewayName: String
|
||||
let gatewayAddress: String?
|
||||
let onGetStarted: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
OnboardingActivationCanvas {
|
||||
VStack(spacing: 0) {
|
||||
Spacer(minLength: 54)
|
||||
|
||||
Image(systemName: "qrcode.viewfinder")
|
||||
.font(.system(size: 64))
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.padding(.bottom, 24)
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
OpenClawActivationGlyph(size: 86)
|
||||
.shadow(color: OpenClawBrand.activationGlow.opacity(0.18), radius: 12, x: 0, y: 6)
|
||||
|
||||
Text("Connect Gateway")
|
||||
.font(OpenClawType.title1)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
KeycapText("/pair qr")
|
||||
Button {
|
||||
var transaction = Transaction()
|
||||
transaction.disablesAnimations = true
|
||||
withTransaction(transaction) {
|
||||
UIPasteboard.general.string = "/pair qr"
|
||||
self.pairCommandCopied = true
|
||||
}
|
||||
} label: {
|
||||
Label(
|
||||
self.pairCommandCopied ? "Copied" : "Copy",
|
||||
systemImage: self.pairCommandCopied ? "checkmark" : "doc.on.doc")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
Image(systemName: "checkmark")
|
||||
.font(OpenClawType.headlineBold)
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 30, height: 30)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(OpenClawBrand.ok)
|
||||
}
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(OpenClawBrand.activationCanvas, lineWidth: 3)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
.tint(.primary)
|
||||
.animation(nil, value: self.pairCommandCopied)
|
||||
.accessibilityLabel(self.pairCommandCopied ? "Pair command copied" : "Copy pair command")
|
||||
.accessibilityIdentifier("onboarding-copy-pair-command")
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
.padding(.bottom, 22)
|
||||
|
||||
Text("Run this in your OpenClaw chat, then scan the code.")
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
Text("You're connected")
|
||||
.font(OpenClawType.title1)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Button {
|
||||
self.onScanQRCode()
|
||||
} label: {
|
||||
Label("Scan QR Code", systemImage: "qrcode")
|
||||
.font(OpenClawType.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.tint(OpenClawBrand.accent)
|
||||
|
||||
Button(action: self.onManualSetup) {
|
||||
Text("Set Up Manually")
|
||||
.font(OpenClawType.headline)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.controlSize(.large)
|
||||
.tint(OpenClawBrand.accent)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
.padding(.horizontal, OpenClawSpacing.space6)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
if !self.statusLine.isEmpty {
|
||||
Text(self.statusLine)
|
||||
.font(OpenClawType.footnote)
|
||||
Text(verbatim: self.gatewayName)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
|
||||
Color.clear.frame(height: 12)
|
||||
if let gatewayAddress, !gatewayAddress.isEmpty {
|
||||
Text(verbatim: gatewayAddress)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
|
||||
Spacer(minLength: 40)
|
||||
|
||||
Button {
|
||||
self.onGetStarted()
|
||||
} label: {
|
||||
Label("Go to Chat", systemImage: "bubble.left.and.bubble.right.fill")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.buttonStyle(OnboardingPrimaryButtonStyle())
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: OnboardingLayout.contentMaxWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingModeIcon: View {
|
||||
let symbol: String
|
||||
let selected: Bool
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: self.symbol)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(self.selected ? OpenClawBrand.activationPrimaryActionText : .secondary)
|
||||
.frame(width: 34, height: 34)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(self.selected ? OpenClawBrand.activationPrimaryGradient : OpenClawBrand
|
||||
.activationNeutralGradient)
|
||||
.shadow(
|
||||
color: self.selected ? OpenClawBrand.activationGlow.opacity(0.18) : .clear,
|
||||
radius: 5,
|
||||
x: 0,
|
||||
y: 2)
|
||||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.stroke(
|
||||
self.selected ? Color.white.opacity(0.30) : OpenClawBrand.activationNeutralStroke,
|
||||
lineWidth: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingModeRow: View {
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let title: LocalizedStringKey
|
||||
let subtitle: LocalizedStringKey
|
||||
let symbol: String
|
||||
let selected: Bool
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: self.action) {
|
||||
HStack {
|
||||
HStack(spacing: 12) {
|
||||
OnboardingModeIcon(symbol: self.symbol, selected: self.selected)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.title)
|
||||
.font(OpenClawType.headline)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(self.subtitle)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: self.selected ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundStyle(self.selected ? OpenClawBrand.accent : Color.secondary)
|
||||
.font(self.selected ? .title3.weight(.semibold) : .title3.weight(.regular))
|
||||
.foregroundStyle(
|
||||
self.selected
|
||||
? OpenClawBrand.activationPrimaryAction
|
||||
: Color(uiColor: .quaternaryLabel).opacity(0.55))
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
.frame(minHeight: 52)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.font(OpenClawType.subhead)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ private enum OnboardingStep: Int, CaseIterable {
|
|||
return "Step \(idx + 1) of \(manualSteps.count)"
|
||||
}
|
||||
|
||||
var title: String {
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .intro: "Welcome"
|
||||
case .welcome: "Connect Gateway"
|
||||
case .mode: "Connection Mode"
|
||||
case .connect: "Connect"
|
||||
case .auth: "Authentication"
|
||||
case .mode: "Gateway Setup"
|
||||
case .connect: "Gateway Details"
|
||||
case .auth: "Gateway Status"
|
||||
case .success: "Connected"
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,15 @@ struct GatewaySetupLinkStaging {
|
|||
}
|
||||
}
|
||||
|
||||
private enum OnboardingFocusedField: Hashable {
|
||||
case setupCode
|
||||
case manualHost
|
||||
case manualPort
|
||||
case discoveryDomain
|
||||
case gatewayToken
|
||||
case gatewayPassword
|
||||
}
|
||||
|
||||
struct OnboardingWizardView: View {
|
||||
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
|
||||
@Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController
|
||||
|
|
@ -96,6 +105,7 @@ struct OnboardingWizardView: View {
|
|||
@State private var setupCode: String = ""
|
||||
@State private var setupCodeStatus: String?
|
||||
@State private var setupAttemptID: UUID?
|
||||
@FocusState private var focusedField: OnboardingFocusedField?
|
||||
private static let pairingAutoResumeTicker = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect()
|
||||
|
||||
let allowSkip: Bool
|
||||
|
|
@ -160,11 +170,15 @@ struct OnboardingWizardView: View {
|
|||
EmptyView()
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color(uiColor: .systemGroupedBackground))
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
}
|
||||
}
|
||||
.navigationTitle(self.isFullScreenStep ? "" : self.step.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.tint(OpenClawBrand.activationPrimaryAction)
|
||||
.toolbar {
|
||||
if !self.isFullScreenStep {
|
||||
ToolbarItem(placement: .principal) {
|
||||
|
|
@ -177,42 +191,16 @@ struct OnboardingWizardView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
if self.step.canGoBack {
|
||||
Button {
|
||||
self.navigateBack()
|
||||
} label: {
|
||||
Label("Back", systemImage: "chevron.left")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
} else if self.allowSkip {
|
||||
Button {
|
||||
self.invalidateSetupAttempt()
|
||||
self.onClose()
|
||||
} label: {
|
||||
Text("Close")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Spacer()
|
||||
Button {
|
||||
UIApplication.shared.sendAction(
|
||||
#selector(UIResponder.resignFirstResponder),
|
||||
to: nil,
|
||||
from: nil,
|
||||
for: nil)
|
||||
} label: {
|
||||
Text("Done")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .trailing, spacing: 0) {
|
||||
self.keyboardDismissControl
|
||||
}
|
||||
.overlay(alignment: .topLeading) {
|
||||
self.leadingChromeButton
|
||||
.padding(.leading, 16)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
.gatewayTrustPromptAlert()
|
||||
.alert("QR Scanner Unavailable", isPresented: Binding(
|
||||
get: { self.scannerError != nil },
|
||||
|
|
@ -298,7 +286,7 @@ struct OnboardingWizardView: View {
|
|||
OnboardingStateStore.markCompleted(mode: selectedMode)
|
||||
self.didMarkCompleted = true
|
||||
}
|
||||
self.step = .success
|
||||
self.navigate(to: .success)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -320,11 +308,11 @@ struct OnboardingWizardView: View {
|
|||
self.showQRScanner = false
|
||||
})
|
||||
.ignoresSafeArea()
|
||||
.navigationTitle("Scan QR Code")
|
||||
.navigationTitle("Scan Setup Code")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text("Scan QR Code")
|
||||
Text("Scan Setup Code")
|
||||
.font(OpenClawType.headline)
|
||||
}
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
|
|
@ -372,75 +360,135 @@ struct OnboardingWizardView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var leadingChromeButton: some View {
|
||||
if self.step.canGoBack {
|
||||
Button {
|
||||
self.navigateBack()
|
||||
} label: {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.accessibilityLabel("Back")
|
||||
}
|
||||
.buttonStyle(OpenClawCloseButtonStyle())
|
||||
} else if self.allowSkip {
|
||||
Button {
|
||||
self.invalidateSetupAttempt()
|
||||
self.onClose()
|
||||
} label: {
|
||||
Text("Close")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.buttonStyle(OpenClawCloseButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var keyboardDismissControl: some View {
|
||||
if self.focusedField != nil {
|
||||
Button {
|
||||
self.dismissKeyboard()
|
||||
} label: {
|
||||
Image(systemName: "keyboard.chevron.compact.down")
|
||||
.font(OpenClawType.headline)
|
||||
.frame(width: 50, height: 44)
|
||||
.contentShape(Capsule(style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(OpenClawBrand.activationPrimaryAction)
|
||||
.background(.ultraThinMaterial, in: Capsule(style: .continuous))
|
||||
.overlay {
|
||||
Capsule(style: .continuous)
|
||||
.stroke(OpenClawBrand.activationNeutralStroke, lineWidth: 0.6)
|
||||
}
|
||||
.shadow(color: Color.black.opacity(0.08), radius: 14, x: 0, y: 4)
|
||||
.accessibilityLabel("Dismiss Keyboard")
|
||||
.padding(.trailing, 20)
|
||||
.padding(.bottom, 10)
|
||||
.transition(.opacity.combined(with: .scale(scale: 0.96)))
|
||||
.animation(.smooth(duration: 0.16), value: self.focusedField)
|
||||
}
|
||||
}
|
||||
|
||||
private var introStep: some View {
|
||||
OnboardingIntroStep(onContinue: advanceFromIntro)
|
||||
OnboardingIntroStep(onContinue: self.advanceFromIntro)
|
||||
}
|
||||
|
||||
private var welcomeStep: some View {
|
||||
OnboardingWelcomeStep(
|
||||
statusLine: self.statusLine,
|
||||
isConnecting: self.connectingGatewayID != nil,
|
||||
onScanQRCode: {
|
||||
self.openQRScannerFromOnboarding()
|
||||
},
|
||||
onManualSetup: {
|
||||
self.invalidateSetupAttempt()
|
||||
self.step = .mode
|
||||
self.statusLine = ""
|
||||
self.navigate(to: .mode)
|
||||
})
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var modeStep: some View {
|
||||
setupCodeSection
|
||||
self.setupCodeSection
|
||||
|
||||
Section {
|
||||
OnboardingModeRow(
|
||||
title: OnboardingConnectionMode.homeNetwork.title,
|
||||
title: "Home Network",
|
||||
subtitle: "LAN or Tailscale host",
|
||||
symbol: "house.and.flag",
|
||||
selected: self.selectedMode == .homeNetwork)
|
||||
{
|
||||
self.selectMode(.homeNetwork)
|
||||
}
|
||||
|
||||
OnboardingModeRow(
|
||||
title: OnboardingConnectionMode.remoteDomain.title,
|
||||
title: "Remote Domain",
|
||||
subtitle: "VPS with domain",
|
||||
symbol: "globe",
|
||||
selected: self.selectedMode == .remoteDomain)
|
||||
{
|
||||
self.selectMode(.remoteDomain)
|
||||
}
|
||||
|
||||
self.developerModeToggleRow
|
||||
|
||||
if self.developerModeEnabled {
|
||||
self.developerModeToggleRow
|
||||
|
||||
OnboardingModeRow(
|
||||
title: OnboardingConnectionMode.developerLocal.title,
|
||||
title: "Same Machine (Dev)",
|
||||
subtitle: "For local iOS app development",
|
||||
symbol: "wrench.and.screwdriver",
|
||||
selected: self.selectedMode == .developerLocal)
|
||||
{
|
||||
self.selectMode(.developerLocal)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Connection Mode")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
Text("Manual Connection")
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
.disabled(self.connectingGatewayID != nil)
|
||||
|
||||
Section {
|
||||
Button {
|
||||
self.step = .connect
|
||||
self.navigate(to: .connect)
|
||||
} label: {
|
||||
Text("Continue")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.disabled(self.selectedMode == nil || self.connectingGatewayID != nil)
|
||||
.buttonStyle(OpenClawPrimaryActionButtonStyle(height: 48, cornerRadius: 16))
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
private var developerModeToggleRow: some View {
|
||||
self.onboardingButtonToggle(
|
||||
"Developer mode",
|
||||
symbol: "wrench.and.screwdriver",
|
||||
isOn: Binding(
|
||||
get: { self.developerModeEnabled },
|
||||
set: { enabled in
|
||||
|
|
@ -451,23 +499,36 @@ struct OnboardingWizardView: View {
|
|||
}))
|
||||
}
|
||||
|
||||
private func onboardingButtonToggle(_ title: String, isOn: Binding<Bool>) -> some View {
|
||||
// Onboarding Form switch rows need full-width taps; native Toggle only hits the switch edge on iOS 26.
|
||||
Button {
|
||||
isOn.wrappedValue.toggle()
|
||||
} label: {
|
||||
HStack {
|
||||
private func onboardingButtonToggle(
|
||||
_ title: LocalizedStringKey,
|
||||
symbol: String? = nil,
|
||||
isOn: Binding<Bool>) -> some View
|
||||
{
|
||||
Toggle(isOn: isOn) {
|
||||
HStack(spacing: 12) {
|
||||
if let symbol {
|
||||
OnboardingModeIcon(symbol: symbol, selected: false)
|
||||
}
|
||||
|
||||
Text(title)
|
||||
.font(OpenClawType.subhead)
|
||||
Spacer(minLength: 8)
|
||||
OpenClawToggleIndicator(isOn: isOn.wrappedValue)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.frame(minHeight: 52)
|
||||
}
|
||||
.tint(OpenClawBrand.activationPrimaryAction)
|
||||
.contentShape(Rectangle())
|
||||
.overlay {
|
||||
Button {
|
||||
isOn.wrappedValue.toggle()
|
||||
} label: {
|
||||
Rectangle()
|
||||
.fill(.clear)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.font(OpenClawType.subhead)
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(title)
|
||||
.accessibilityValue(isOn.wrappedValue ? "On" : "Off")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -477,14 +538,14 @@ struct OnboardingWizardView: View {
|
|||
self.onboardingLabeledContent("Mode", value: selectedMode.title)
|
||||
self.onboardingLabeledContent("Discovery", value: self.gatewayController.discoveryStatusText)
|
||||
self.onboardingLabeledContent("Status", value: self.appModel.gatewayDisplayStatusText)
|
||||
self.onboardingLabeledContent("Progress", value: self.statusLine)
|
||||
self.onboardingLabeledContent("Progress", value: self.statusLine.isEmpty ? "Ready" : self.statusLine)
|
||||
} header: {
|
||||
Text("Status")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
} footer: {
|
||||
if let connectMessage {
|
||||
Text(connectMessage)
|
||||
.font(OpenClawType.caption)
|
||||
.font(OpenClawType.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -503,14 +564,13 @@ struct OnboardingWizardView: View {
|
|||
} else {
|
||||
Section {
|
||||
Text("Choose a mode first.")
|
||||
.font(OpenClawType.subhead)
|
||||
.font(OpenClawType.body)
|
||||
Button {
|
||||
self.step = .mode
|
||||
self.navigate(to: .mode)
|
||||
} label: {
|
||||
Text("Back to Mode Selection")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -549,13 +609,13 @@ struct OnboardingWizardView: View {
|
|||
.disabled(self.connectingGatewayID != nil)
|
||||
} header: {
|
||||
Text("Setup Link")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
} footer: {
|
||||
Text(link.tls
|
||||
? "Review this endpoint. Credentials are applied only after you tap Connect."
|
||||
:
|
||||
"Plaintext may expose credentials. Continue only if you trust this local network and host.")
|
||||
.font(OpenClawType.caption)
|
||||
.font(OpenClawType.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -564,7 +624,7 @@ struct OnboardingWizardView: View {
|
|||
Section {
|
||||
if self.gatewayController.gateways.isEmpty {
|
||||
Text("No gateways found yet.")
|
||||
.font(OpenClawType.subhead)
|
||||
.font(OpenClawType.body)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(self.gatewayController.gateways) { gateway in
|
||||
|
|
@ -573,7 +633,7 @@ struct OnboardingWizardView: View {
|
|||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(gateway.name)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.font(OpenClawType.body)
|
||||
if let host = gateway.lanHost ?? gateway.tailnetDns {
|
||||
Text(host)
|
||||
.font(OpenClawType.footnote)
|
||||
|
|
@ -611,7 +671,7 @@ struct OnboardingWizardView: View {
|
|||
.disabled(self.connectingGatewayID != nil)
|
||||
} header: {
|
||||
Text("Discovered Gateways")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
}
|
||||
|
||||
self.manualConnectionFieldsSection(title: "Manual Fallback")
|
||||
|
|
@ -619,36 +679,36 @@ struct OnboardingWizardView: View {
|
|||
}
|
||||
|
||||
private var remoteDomainConnectSection: some View {
|
||||
manualConnectionFieldsSection(title: "Domain Settings")
|
||||
self.manualConnectionFieldsSection(title: "Domain Settings")
|
||||
}
|
||||
|
||||
private var developerConnectSection: some View {
|
||||
Section {
|
||||
TextField("Host", text: self.manualHostBinding)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(OpenClawType.subhead)
|
||||
TextField("Port", text: self.manualPortTextBinding)
|
||||
self.onboardingTextField("Host", text: self.manualHostBinding, focusedField: .manualHost)
|
||||
self.onboardingTextField("Port", text: self.manualPortTextBinding, focusedField: .manualPort)
|
||||
.keyboardType(.numberPad)
|
||||
.font(OpenClawType.subhead)
|
||||
self.onboardingButtonToggle("Use TLS", isOn: self.$manualTLS)
|
||||
self.manualConnectButton
|
||||
} header: {
|
||||
Text("Developer Local")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
} footer: {
|
||||
Text("Default host is localhost. Use your Mac LAN IP if simulator networking requires it.")
|
||||
.font(OpenClawType.caption)
|
||||
.font(OpenClawType.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
private var authStep: some View {
|
||||
Group {
|
||||
Section {
|
||||
self.onboardingSecureField("Gateway Auth Token", text: self.gatewayTokenBinding)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
self.onboardingSecureField("Gateway Password", text: self.gatewayPasswordBinding)
|
||||
self.onboardingSecureField(
|
||||
"Gateway Auth Token",
|
||||
text: self.gatewayTokenBinding,
|
||||
focusedField: .gatewayToken)
|
||||
self.onboardingSecureField(
|
||||
"Gateway Password",
|
||||
text: self.gatewayPasswordBinding,
|
||||
focusedField: .gatewayPassword)
|
||||
|
||||
if let problem = self.currentProblem {
|
||||
GatewayProblemBanner(
|
||||
|
|
@ -661,17 +721,17 @@ struct OnboardingWizardView: View {
|
|||
self.showGatewayProblemDetails = true
|
||||
})
|
||||
} else if self.issue.needsAuthToken {
|
||||
Text("Gateway rejected credentials. Scan a fresh QR code or update token/password.")
|
||||
Text("Gateway rejected credentials. Scan a fresh setup code or update token/password.")
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Auth token looks valid.")
|
||||
Text("OpenClaw is checking gateway and node access.")
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Authentication")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
Text(self.gatewayStatusSectionTitle)
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
}
|
||||
|
||||
if self.issue.needsPairing {
|
||||
|
|
@ -686,7 +746,7 @@ struct OnboardingWizardView: View {
|
|||
.disabled(self.connectingGatewayID != nil)
|
||||
} header: {
|
||||
Text("Pairing Approval")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
} footer: {
|
||||
let requestLine: String = {
|
||||
if let id = self.currentProblem?.requestId ?? self.issue.requestId, !id.isEmpty {
|
||||
|
|
@ -709,7 +769,7 @@ struct OnboardingWizardView: View {
|
|||
Button {
|
||||
self.openQRScannerFromOnboarding()
|
||||
} label: {
|
||||
Label("Scan QR Code Again", systemImage: "qrcode.viewfinder")
|
||||
Label("Scan Setup Code Again", systemImage: "qrcode.viewfinder")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
|
|
@ -733,70 +793,54 @@ struct OnboardingWizardView: View {
|
|||
}
|
||||
|
||||
private var successStep: some View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 64))
|
||||
.foregroundStyle(OpenClawBrand.ok)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
Text("Connected")
|
||||
.font(OpenClawType.title1)
|
||||
.foregroundStyle(OpenClawBrand.textPrimary)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(self.successEndpoint)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
self.onComplete()
|
||||
} label: {
|
||||
Label("Go to Chat", systemImage: "bubble.left.and.bubble.right.fill")
|
||||
.font(OpenClawType.headline)
|
||||
}
|
||||
.font(OpenClawType.headline)
|
||||
.openClawPrimaryButton()
|
||||
.padding(.horizontal, OpenClawSpacing.space6)
|
||||
.padding(.bottom, 48)
|
||||
}
|
||||
OnboardingSuccessStep(
|
||||
gatewayName: self.appModel.gatewayServerName ?? "gateway",
|
||||
gatewayAddress: self.appModel.gatewayRemoteAddress,
|
||||
onGetStarted: self.onComplete)
|
||||
}
|
||||
}
|
||||
|
||||
extension OnboardingWizardView {
|
||||
private var gatewayStatusSectionTitle: String {
|
||||
if self.issue.needsPairing || self.currentProblem?.needsPairingApproval == true {
|
||||
return "Gateway Approval"
|
||||
}
|
||||
if self.issue.needsAuthToken || self.currentProblem != nil {
|
||||
return "Authentication"
|
||||
}
|
||||
return "Gateway Status"
|
||||
}
|
||||
|
||||
private var setupCodeSection: some View {
|
||||
Section {
|
||||
TextField("Paste setup code", text: self.$setupCode)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(OpenClawType.subhead)
|
||||
.disabled(self.connectingGatewayID != nil)
|
||||
.onSubmit {
|
||||
Task { await self.applySetupCodeAndConnect() }
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
self.onboardingTextField("Enter setup code", text: self.$setupCode, focusedField: .setupCode)
|
||||
.lineLimit(1)
|
||||
.submitLabel(.go)
|
||||
.onSubmit {
|
||||
guard self.canApplySetupCode else { return }
|
||||
Task { await self.applySetupCodeAndConnect() }
|
||||
}
|
||||
|
||||
Button {
|
||||
Task { await self.applySetupCodeAndConnect() }
|
||||
} label: {
|
||||
if self.connectingGatewayID == "setup-code" {
|
||||
HStack(spacing: 8) {
|
||||
Button {
|
||||
Task { await self.applySetupCodeAndConnect() }
|
||||
} label: {
|
||||
if self.connectingGatewayID == "setup-code" {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
Text("Applying...")
|
||||
.controlSize(.small)
|
||||
} else {
|
||||
Text("Apply")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
} else {
|
||||
Text("Apply Setup Code")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.regular)
|
||||
.tint(OpenClawBrand.activationPrimaryAction)
|
||||
.disabled(!self.canApplySetupCode)
|
||||
}
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.disabled(
|
||||
self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
|| self.connectingGatewayID != nil)
|
||||
.frame(minHeight: 50)
|
||||
|
||||
if let setupCodeStatus, !setupCodeStatus.isEmpty {
|
||||
Text(setupCodeStatus)
|
||||
|
|
@ -805,65 +849,92 @@ extension OnboardingWizardView {
|
|||
}
|
||||
} header: {
|
||||
Text("Setup Code")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
} footer: {
|
||||
Text("Use this if you received a setup code instead of a QR code.")
|
||||
.font(OpenClawType.caption)
|
||||
Text("Use this if you have a setup code instead of scanning.")
|
||||
.font(OpenClawType.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
private func manualConnectionFieldsSection(title: String) -> some View {
|
||||
private var canApplySetupCode: Bool {
|
||||
!self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
&& self.connectingGatewayID == nil
|
||||
}
|
||||
|
||||
private func manualConnectionFieldsSection(title: LocalizedStringKey) -> some View {
|
||||
Section {
|
||||
TextField("Host", text: self.manualHostBinding)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(OpenClawType.subhead)
|
||||
TextField("Port", text: self.manualPortTextBinding)
|
||||
self.onboardingTextField("Host", text: self.manualHostBinding, focusedField: .manualHost)
|
||||
self.onboardingTextField("Port", text: self.manualPortTextBinding, focusedField: .manualPort)
|
||||
.keyboardType(.numberPad)
|
||||
.font(OpenClawType.subhead)
|
||||
self.onboardingButtonToggle("Use TLS", isOn: self.$manualTLS)
|
||||
TextField("Discovery Domain (optional)", text: self.$discoveryDomain)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.font(OpenClawType.subhead)
|
||||
self.onboardingTextField(
|
||||
"Discovery Domain (optional)",
|
||||
text: self.$discoveryDomain,
|
||||
focusedField: .discoveryDomain)
|
||||
if self.selectedMode == .remoteDomain {
|
||||
self.onboardingSecureField("Gateway Auth Token", text: self.gatewayTokenBinding)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
self.onboardingSecureField("Gateway Password", text: self.gatewayPasswordBinding)
|
||||
self.onboardingSecureField(
|
||||
"Gateway Auth Token",
|
||||
text: self.gatewayTokenBinding,
|
||||
focusedField: .gatewayToken)
|
||||
self.onboardingSecureField(
|
||||
"Gateway Password",
|
||||
text: self.gatewayPasswordBinding,
|
||||
focusedField: .gatewayPassword)
|
||||
}
|
||||
self.manualConnectButton
|
||||
} header: {
|
||||
Text(title)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.font(OpenClawType.footnoteSemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
private func onboardingLabeledContent(_ title: String, value: String) -> some View {
|
||||
private func onboardingLabeledContent(_ title: LocalizedStringKey, value: String) -> some View {
|
||||
LabeledContent {
|
||||
Text(value)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(verbatim: value)
|
||||
.font(OpenClawType.body)
|
||||
} label: {
|
||||
Text(title)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
}
|
||||
|
||||
private func onboardingSecureField(_ placeholder: String, text: Binding<String>) -> some View {
|
||||
ZStack(alignment: .leading) {
|
||||
SecureField("", text: text)
|
||||
private func onboardingTextField(
|
||||
_ placeholder: LocalizedStringKey,
|
||||
text: Binding<String>,
|
||||
focusedField: OnboardingFocusedField) -> some View
|
||||
{
|
||||
TextField(
|
||||
"",
|
||||
text: text,
|
||||
prompt: Text(placeholder)
|
||||
.font(OpenClawType.subhead)
|
||||
.accessibilityLabel(placeholder)
|
||||
.foregroundStyle(.tertiary))
|
||||
.font(OpenClawType.subhead)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused(self.$focusedField, equals: focusedField)
|
||||
.accessibilityLabel(placeholder)
|
||||
}
|
||||
|
||||
private func onboardingSecureField(
|
||||
_ placeholder: LocalizedStringKey,
|
||||
text: Binding<String>,
|
||||
focusedField: OnboardingFocusedField) -> some View
|
||||
{
|
||||
ZStack(alignment: .leading) {
|
||||
if text.wrappedValue.isEmpty {
|
||||
Text(placeholder)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.tertiary)
|
||||
.allowsHitTesting(false)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
SecureField("", text: text)
|
||||
.font(OpenClawType.subhead)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused(self.$focusedField, equals: focusedField)
|
||||
}
|
||||
.font(OpenClawType.subhead)
|
||||
.accessibilityLabel(placeholder)
|
||||
}
|
||||
|
||||
private var manualConnectButton: some View {
|
||||
|
|
@ -914,10 +985,11 @@ extension OnboardingWizardView {
|
|||
|
||||
await self.applyGatewayLink(link)
|
||||
self.setupCode = ""
|
||||
self.setupCodeStatus = "Setup code applied. Connecting..."
|
||||
self.connectMessage = "Connecting via setup code..."
|
||||
self.statusLine = "Setup code loaded. Connecting to \(link.host):\(link.port)..."
|
||||
self.step = .connect
|
||||
self.setupCodeStatus = "Setup code applied. Connecting…"
|
||||
self.connectMessage = "Connecting via setup code…"
|
||||
self.statusLine = "Setup code loaded. Connecting to \(link.host):\(link.port)…"
|
||||
await self.prepareFocusedFieldForStepTransition()
|
||||
self.navigate(to: .connect)
|
||||
await self.connectManual(setupAttemptID: attemptID)
|
||||
}
|
||||
|
||||
|
|
@ -956,9 +1028,9 @@ extension OnboardingWizardView {
|
|||
let link = await self.gatewayController.selectReachableSetupLink(parsedLink)
|
||||
guard self.setupAttemptID == attemptID else { return }
|
||||
await self.applyGatewayLink(link)
|
||||
self.connectMessage = "Connecting via QR code..."
|
||||
self.statusLine = "QR loaded. Connecting to \(link.host):\(link.port)..."
|
||||
self.step = .connect
|
||||
self.connectMessage = "Connecting via setup code…"
|
||||
self.statusLine = "Setup code loaded. Connecting to \(link.host):\(link.port)…"
|
||||
self.navigate(to: .connect)
|
||||
await self.connectManual(setupAttemptID: attemptID)
|
||||
}
|
||||
|
||||
|
|
@ -976,7 +1048,7 @@ extension OnboardingWizardView {
|
|||
self.setupCodeStatus = "Setup link loaded for \(link.host):\(link.port). Tap Connect to apply."
|
||||
self.connectMessage = nil
|
||||
self.statusLine = self.setupCodeStatus ?? ""
|
||||
self.step = .connect
|
||||
self.navigate(to: .connect)
|
||||
}
|
||||
|
||||
private func connectStagedGatewaySetupLink() async {
|
||||
|
|
@ -997,7 +1069,7 @@ extension OnboardingWizardView {
|
|||
guard self.setupLinkStaging.link == link else { return }
|
||||
_ = self.setupLinkStaging.take()
|
||||
await self.applyGatewayLink(link, disconnectExistingGatewayForBootstrap: false)
|
||||
self.setupCodeStatus = "Setup link applied. Connecting..."
|
||||
self.setupCodeStatus = "Setup link applied. Connecting…"
|
||||
self.issue = .none
|
||||
self.connectMessage = "Connecting to \(link.host)…"
|
||||
self.statusLine = "Connecting to \(link.host):\(link.port)…"
|
||||
|
|
@ -1167,7 +1239,7 @@ extension OnboardingWizardView {
|
|||
OnboardingStateStore.markFirstRunIntroSeen()
|
||||
self.requestLocalNetworkAccess(reason: "onboarding_continue")
|
||||
self.statusLine = ""
|
||||
self.step = .welcome
|
||||
self.navigate(to: .welcome)
|
||||
}
|
||||
|
||||
private func requestLocalNetworkAccessIfPastIntro(reason: String) {
|
||||
|
|
@ -1183,6 +1255,27 @@ extension OnboardingWizardView {
|
|||
guard let target = step.previous else { return }
|
||||
self.invalidateSetupAttempt()
|
||||
self.connectMessage = nil
|
||||
self.navigate(to: target)
|
||||
}
|
||||
|
||||
private func prepareFocusedFieldForStepTransition() async {
|
||||
guard self.focusedField != nil else { return }
|
||||
self.dismissKeyboard()
|
||||
await Task.yield()
|
||||
try? await Task.sleep(nanoseconds: 120_000_000)
|
||||
}
|
||||
|
||||
private func dismissKeyboard() {
|
||||
self.focusedField = nil
|
||||
UIApplication.shared.sendAction(
|
||||
#selector(UIResponder.resignFirstResponder),
|
||||
to: nil,
|
||||
from: nil,
|
||||
for: nil)
|
||||
}
|
||||
|
||||
private func navigate(to target: OnboardingStep) {
|
||||
self.focusedField = nil
|
||||
self.step = target
|
||||
}
|
||||
|
||||
|
|
@ -1209,15 +1302,6 @@ extension OnboardingWizardView {
|
|||
return !host.isEmpty && self.resolvedManualPort(host: host) != nil
|
||||
}
|
||||
|
||||
private var successEndpoint: String {
|
||||
let serverName = self.appModel.gatewayServerName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !serverName.isEmpty {
|
||||
return serverName
|
||||
}
|
||||
let remoteAddress = self.appModel.gatewayRemoteAddress?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return remoteAddress.isEmpty ? "gateway" : remoteAddress
|
||||
}
|
||||
|
||||
private func initializeState() {
|
||||
if self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if let last = GatewaySettingsStore.loadLastGatewayConnection() {
|
||||
|
|
@ -1239,7 +1323,13 @@ extension OnboardingWizardView {
|
|||
}
|
||||
self.manualPortText = self.manualPort > 0 ? String(self.manualPort) : ""
|
||||
if self.selectedMode == nil {
|
||||
self.selectedMode = OnboardingStateStore.lastMode()
|
||||
let lastMode = OnboardingStateStore.lastMode()
|
||||
if lastMode == .developerLocal {
|
||||
self.developerModeEnabled = true
|
||||
}
|
||||
if self.developerModeEnabled || lastMode != .developerLocal {
|
||||
self.selectedMode = lastMode
|
||||
}
|
||||
}
|
||||
if self.selectedMode == .developerLocal, self.manualHost == "openclaw.local" {
|
||||
self.manualHost = "localhost"
|
||||
|
|
@ -1551,7 +1641,7 @@ extension OnboardingWizardView {
|
|||
self.connectMessage = nil
|
||||
self.issue = .none
|
||||
self.pairingRequestId = nil
|
||||
self.step = .connect
|
||||
self.navigate(to: .connect)
|
||||
self.openQRScannerFromOnboarding(status: "Scan a fresh setup QR code from this gateway.")
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -918,8 +918,8 @@ struct RootTabs: View {
|
|||
self.showOnboarding = false
|
||||
},
|
||||
onComplete: {
|
||||
self.selectSidebarDestination(.chat)
|
||||
self.showOnboarding = false
|
||||
self.selectSidebarDestination(.chat)
|
||||
})
|
||||
.environment(self.appModel)
|
||||
.environment(self.voiceWake)
|
||||
|
|
|
|||
|
|
@ -170,33 +170,44 @@ struct OpenClawTypographyTests {
|
|||
#expect(proComponents.contains(".font(OpenClawType.subheadSemiBold)"))
|
||||
#expect(proComponents.contains("Text(primaryActionTitle)"))
|
||||
#expect(proComponents.contains("Text(secondaryActionTitle)"))
|
||||
#expect(proComponents.contains("@Environment(\\.isEnabled)"))
|
||||
#expect(proComponents.contains(".animation(.easeOut(duration: 0.15), value: self.isEnabled)"))
|
||||
|
||||
#expect(chatTab.contains("Text(\"Export Transcript\")"))
|
||||
#expect(chatTab.contains(".font(OpenClawType.body)"))
|
||||
#expect(!chatTab.contains("Button(\"Export Transcript\")"))
|
||||
|
||||
#expect(!quickSetup.contains("Button(\"Close\")"))
|
||||
#expect(quickSetup.contains("Text(\"Quick Setup\")"))
|
||||
#expect(quickSetup.contains(".navigationTitle(\"Quick Setup\")"))
|
||||
#expect(quickSetup.contains("Text(\"Close\")"))
|
||||
#expect(quickSetup.contains(".font(OpenClawType.subheadSemiBold)"))
|
||||
#expect(quickSetup.contains("let text: LocalizedStringKey"))
|
||||
#expect(!quickSetup.contains("Text(verbatim: self.text)"))
|
||||
|
||||
#expect(gatewayProblem.contains("Text(\"Connection problem\")"))
|
||||
#expect(gatewayProblem.contains("Text(\"Copy request ID\")"))
|
||||
#expect(gatewayProblem.contains("Text(\"Copy command\")"))
|
||||
#expect(gatewayProblem.contains(".font(OpenClawType.subheadSemiBold)"))
|
||||
|
||||
#expect(onboardingSteps.contains("Text(\"Connect Gateway\")"))
|
||||
#expect(onboardingSteps.contains("Label(\"Scan QR Code\", systemImage: \"qrcode\")"))
|
||||
#expect(onboardingSteps.contains("Text(\"Set Up Manually\")"))
|
||||
#expect(onboardingSteps.contains(".font(OpenClawType.headline)"))
|
||||
#expect(onboardingSteps.contains("title: \"Connect Gateway\""))
|
||||
#expect(onboardingSteps.contains("Text(\"Scan QR\")"))
|
||||
#expect(onboardingSteps.contains("Text(\"Connect Manually\")"))
|
||||
#expect(onboardingSteps.contains("Label(\"Go to Chat\", systemImage: \"bubble.left.and.bubble.right.fill\")"))
|
||||
#expect(onboardingSteps.contains(".font(OpenClawType.subheadSemiBold)"))
|
||||
#expect(onboardingSteps.contains("let title: LocalizedStringKey"))
|
||||
#expect(onboardingSteps.contains("let subtitle: LocalizedStringKey?"))
|
||||
#expect(onboardingSteps.contains("let text: LocalizedStringKey"))
|
||||
|
||||
#expect(onboardingWizard.contains("Text(\"Scan QR Code\")"))
|
||||
#expect(onboardingWizard.contains("Text(\"Scan Setup Code\")")
|
||||
|| onboardingWizard.contains(".navigationTitle(\"Scan Setup Code\")"))
|
||||
#expect(onboardingWizard.contains("Label(\"Resume After Approval\", systemImage: \"arrow.clockwise\")"))
|
||||
#expect(onboardingWizard.contains("Label(\"Scan QR Code Again\", systemImage: \"qrcode.viewfinder\")"))
|
||||
#expect(onboardingWizard.contains("Text(\"Apply Setup Code\")"))
|
||||
#expect(onboardingWizard.contains("Label(\"Scan Setup Code Again\", systemImage: \"qrcode.viewfinder\")"))
|
||||
#expect(onboardingWizard.contains("Text(\"Apply\")"))
|
||||
#expect(onboardingWizard.contains(".font(OpenClawType.subheadSemiBold)"))
|
||||
#expect(onboardingWizard.contains("_ title: LocalizedStringKey"))
|
||||
#expect(onboardingWizard.contains("_ placeholder: LocalizedStringKey"))
|
||||
#expect(onboardingWizard.contains("if self.developerModeEnabled {"))
|
||||
#expect(onboardingWizard.contains("title: \"Same Machine (Dev)\""))
|
||||
#expect(onboardingWizard.contains("if lastMode == .developerLocal"))
|
||||
#expect(onboardingWizard.contains("self.developerModeEnabled = true"))
|
||||
|
||||
#expect(settingsSections.contains(".font(OpenClawType.body)"))
|
||||
#expect(settingsSections.contains("Toggle(isOn: self.$talkButtonEnabled)"))
|
||||
|
|
|
|||
|
|
@ -129,6 +129,73 @@ struct SwiftUIRenderSmokeTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test @MainActor func gatewayQuickSetupBuildsCandidateAndEmptyStates() {
|
||||
let gateways: [GatewayDiscoveryModel.DiscoveredGateway?] = [
|
||||
.previewGateway,
|
||||
nil,
|
||||
]
|
||||
|
||||
for gateway in gateways {
|
||||
let appModel = NodeAppModel()
|
||||
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
||||
if let gateway {
|
||||
gatewayController._test_setGateways([gateway])
|
||||
appModel.gatewayStatusText = "Ready to pair"
|
||||
}
|
||||
|
||||
let root = GatewayQuickSetupSheet()
|
||||
.environment(appModel)
|
||||
.environment(gatewayController)
|
||||
.openClawSheetChrome()
|
||||
|
||||
_ = Self.host(root, size: CGSize(width: 393, height: 520))
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor func onboardingActivationScreensBuildAcrossAppearanceAndTypeSize() {
|
||||
let screens: [AnyView] = [
|
||||
AnyView(OnboardingIntroStep(onContinue: {})),
|
||||
AnyView(OnboardingWelcomeStep(
|
||||
statusLine: "",
|
||||
isConnecting: false,
|
||||
onScanQRCode: {},
|
||||
onManualSetup: {})),
|
||||
AnyView(OnboardingSuccessStep(
|
||||
gatewayName: "OpenClaw Gateway",
|
||||
gatewayAddress: "openclaw.local",
|
||||
onGetStarted: {})),
|
||||
AnyView(NavigationStack {
|
||||
Form {
|
||||
Section("Connection Mode") {
|
||||
OnboardingModeRow(
|
||||
title: "Home Network",
|
||||
subtitle: "LAN or Tailscale host",
|
||||
symbol: "house.and.flag",
|
||||
selected: true,
|
||||
action: {})
|
||||
OnboardingModeRow(
|
||||
title: "Remote Domain",
|
||||
subtitle: "VPS with domain",
|
||||
symbol: "globe",
|
||||
selected: false,
|
||||
action: {})
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(OpenClawBrand.activationCanvas)
|
||||
}),
|
||||
]
|
||||
|
||||
for scheme in [ColorScheme.light, ColorScheme.dark] {
|
||||
for screen in screens {
|
||||
let root = screen
|
||||
.preferredColorScheme(scheme)
|
||||
.environment(\.dynamicTypeSize, .accessibility2)
|
||||
_ = Self.host(root, size: CGSize(width: 393, height: 852))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor func `root tabs build gateway state view hierarchies`() {
|
||||
for appModel in Self.rootTabsGatewayStateModels() {
|
||||
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
||||
|
|
@ -347,3 +414,20 @@ struct SwiftUIRenderSmokeTests {
|
|||
let verticalSizeClass: UserInterfaceSizeClass
|
||||
}
|
||||
}
|
||||
|
||||
extension GatewayDiscoveryModel.DiscoveredGateway {
|
||||
fileprivate static let previewGateway = GatewayDiscoveryModel.DiscoveredGateway(
|
||||
name: "Studio Gateway",
|
||||
endpoint: .hostPort(
|
||||
host: .name("openclaw.local", nil),
|
||||
port: 18789),
|
||||
stableID: "preview-gateway",
|
||||
debugID: "openclaw.local",
|
||||
lanHost: "openclaw.local",
|
||||
tailnetDns: nil,
|
||||
gatewayPort: 18789,
|
||||
canvasPort: 18789,
|
||||
tlsEnabled: true,
|
||||
tlsFingerprintSha256: "preview",
|
||||
cliPath: "/opt/homebrew/bin/openclaw")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ final class OpenClawSnapshotUITests: XCTestCase {
|
|||
self.app = app
|
||||
|
||||
XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8))
|
||||
XCTAssertTrue(app.staticTexts["Security"].exists)
|
||||
XCTAssertTrue(app.staticTexts["Security notice"].exists)
|
||||
let disclosure = app.staticTexts.matching(NSPredicate(
|
||||
format: "label CONTAINS[c] 'camera' AND label CONTAINS[c] 'trust the gateway and agent'")).firstMatch
|
||||
XCTAssertTrue(disclosure.exists)
|
||||
|
|
@ -337,7 +337,7 @@ final class OpenClawSnapshotUITests: XCTestCase {
|
|||
starter.tap()
|
||||
XCTAssertTrue(
|
||||
self.app?.staticTexts[
|
||||
"Fasse den aktuellen OpenClaw-Status zusammen und sage mir, was Aufmerksamkeit erfordert."
|
||||
"Fasse den aktuellen OpenClaw-Status zusammen und sage mir, was Aufmerksamkeit erfordert.",
|
||||
].waitForExistence(timeout: 5) == true)
|
||||
self.attachScreenshot(named: "chat-empty-starters-german")
|
||||
}
|
||||
|
|
@ -366,23 +366,23 @@ final class OpenClawSnapshotUITests: XCTestCase {
|
|||
app.buttons["Continue"].tap()
|
||||
app.tap()
|
||||
|
||||
let copyPairCommand = app.buttons["onboarding-copy-pair-command"]
|
||||
XCTAssertTrue(copyPairCommand.waitForExistence(timeout: 8))
|
||||
copyPairCommand.tap()
|
||||
XCTAssertEqual(copyPairCommand.label, "Pair command copied")
|
||||
self.attachScreenshot(named: "onboarding-copy-pair-command")
|
||||
let copySetupCommand = app.buttons["Copy setup code command"]
|
||||
XCTAssertTrue(copySetupCommand.waitForExistence(timeout: 8))
|
||||
copySetupCommand.tap()
|
||||
XCTAssertEqual(copySetupCommand.value as? String, "Copied")
|
||||
self.attachScreenshot(named: "onboarding-copy-setup-code-command")
|
||||
|
||||
app.buttons["Set Up Manually"].tap()
|
||||
let setupCode = app.textFields["Paste setup code"]
|
||||
app.buttons["Connect Manually"].tap()
|
||||
let setupCode = app.textFields["Enter setup code"]
|
||||
XCTAssertTrue(setupCode.waitForExistence(timeout: 5))
|
||||
setupCode.tap()
|
||||
setupCode.typeText("APPLE-REVIEW-DEMO")
|
||||
app.buttons["Done"].tap()
|
||||
app.buttons["Apply Setup Code"].tap()
|
||||
app.buttons["Dismiss Keyboard"].tap()
|
||||
app.buttons["Apply"].tap()
|
||||
|
||||
XCTAssertTrue(app.staticTexts["Connected"].waitForExistence(timeout: 8))
|
||||
XCTAssertTrue(app.staticTexts["You're connected"].waitForExistence(timeout: 8))
|
||||
XCTAssertTrue(app.staticTexts["Apple Review Demo Gateway"].exists)
|
||||
XCTAssertFalse(app.staticTexts["Local demo mode"].exists)
|
||||
XCTAssertTrue(app.staticTexts["Local demo mode"].exists)
|
||||
self.attachScreenshot(named: "onboarding-connected-go-to-chat")
|
||||
|
||||
app.buttons["Go to Chat"].tap()
|
||||
|
|
@ -622,29 +622,35 @@ final class OpenClawSnapshotUITests: XCTestCase {
|
|||
XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8))
|
||||
app.buttons["Continue"].tap()
|
||||
app.tap()
|
||||
XCTAssertTrue(app.buttons["Set Up Manually"].waitForExistence(timeout: 8))
|
||||
app.buttons["Set Up Manually"].tap()
|
||||
let developerMode = app.buttons["Developer mode"]
|
||||
if developerMode.value as? String != "On" {
|
||||
developerMode.tap()
|
||||
}
|
||||
app.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", "Same Machine (Dev)")).firstMatch.tap()
|
||||
XCTAssertTrue(app.buttons["Connect Manually"].waitForExistence(timeout: 8))
|
||||
app.buttons["Connect Manually"].tap()
|
||||
app.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", "Home Network")).firstMatch.tap()
|
||||
app.buttons["Continue"].tap()
|
||||
|
||||
let host = app.textFields["Host"]
|
||||
XCTAssertTrue(host.waitForExistence(timeout: 5))
|
||||
host.tap()
|
||||
host.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: 32) + "localhost")
|
||||
|
||||
let port = app.textFields["Port"]
|
||||
XCTAssertTrue(port.waitForExistence(timeout: 5))
|
||||
port.tap()
|
||||
port.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: 5) + "18920")
|
||||
let useTLS = app.switches["Use TLS"]
|
||||
XCTAssertTrue(useTLS.waitForExistence(timeout: 5))
|
||||
if useTLS.value as? String != "0" {
|
||||
useTLS.tap()
|
||||
}
|
||||
app.buttons["Connect"].tap()
|
||||
|
||||
let tokenField = app.secureTextFields["Gateway Auth Token"]
|
||||
XCTAssertTrue(tokenField.waitForExistence(timeout: 20))
|
||||
tokenField.tap()
|
||||
tokenField.typeText(token)
|
||||
app.buttons["Done"].tap()
|
||||
app.buttons["Dismiss Keyboard"].tap()
|
||||
app.buttons["Retry Connection"].tap()
|
||||
|
||||
XCTAssertTrue(app.staticTexts["Connected"].waitForExistence(timeout: 30))
|
||||
XCTAssertTrue(app.staticTexts["You're connected"].waitForExistence(timeout: 30))
|
||||
self.attachScreenshot(named: "manual-auth-retry-connected")
|
||||
}
|
||||
|
||||
|
|
@ -770,19 +776,18 @@ final class OpenClawSnapshotUITests: XCTestCase {
|
|||
XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8))
|
||||
app.buttons["Continue"].tap()
|
||||
app.tap()
|
||||
XCTAssertTrue(app.buttons["Set Up Manually"].waitForExistence(timeout: 8))
|
||||
app.buttons["Set Up Manually"].tap()
|
||||
XCTAssertTrue(app.buttons["Connect Manually"].waitForExistence(timeout: 8))
|
||||
app.buttons["Connect Manually"].tap()
|
||||
|
||||
let setupCodeField = app.textFields["Paste setup code"]
|
||||
let setupCodeField = app.textFields["Enter setup code"]
|
||||
XCTAssertTrue(setupCodeField.waitForExistence(timeout: 5))
|
||||
setupCodeField.tap()
|
||||
setupCodeField.press(forDuration: 1)
|
||||
XCTAssertTrue(app.menuItems["Paste"].waitForExistence(timeout: 3))
|
||||
app.menuItems["Paste"].tap()
|
||||
app.buttons["Done"].tap()
|
||||
app.buttons["Apply Setup Code"].tap()
|
||||
app.buttons["Apply"].tap()
|
||||
|
||||
XCTAssertTrue(app.staticTexts["Connected"].waitForExistence(timeout: 45))
|
||||
XCTAssertTrue(app.staticTexts["You're connected"].waitForExistence(timeout: 45))
|
||||
app.buttons["Go to Chat"].tap()
|
||||
return app
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ public struct OpenClawMascotView: View {
|
|||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
public init() {}
|
||||
private let floats: Bool
|
||||
|
||||
public init(floats: Bool = true) {
|
||||
self.floats = floats
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
let palette = OpenClawMascotPalette.forScheme(self.colorScheme)
|
||||
|
|
@ -23,7 +27,10 @@ public struct OpenClawMascotView: View {
|
|||
// antennae (art starts at y~5 of 120) at the -9.6 float peak.
|
||||
GeometryReader { proxy in
|
||||
OpenClawMascotCanvas(pose: pose, palette: palette)
|
||||
.offset(y: pose.floatOffset * min(proxy.size.width, proxy.size.height) / 120)
|
||||
.offset(
|
||||
y: self.floats
|
||||
? pose.floatOffset * min(proxy.size.width, proxy.size.height) / 120
|
||||
: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,10 +55,10 @@ const CATALOGS = [
|
|||
"apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift": [
|
||||
"Close",
|
||||
"Connect",
|
||||
"Connect to a Gateway?",
|
||||
"Connect a nearby Gateway",
|
||||
"Connecting…",
|
||||
"Don’t show this again",
|
||||
"No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.",
|
||||
"Don't show this again",
|
||||
"Looking for a Gateway",
|
||||
"Not now",
|
||||
"Quick Setup",
|
||||
],
|
||||
|
|
@ -68,7 +68,7 @@ const CATALOGS = [
|
|||
"Trust and connect",
|
||||
"Trust this gateway?",
|
||||
],
|
||||
"apps/ios/Sources/Onboarding/OnboardingWizardView.swift": ["Save"],
|
||||
"apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift": ["Go to Chat"],
|
||||
"apps/ios/Sources/RootTabs.swift": ["Agent", "Chat", "Control", "Settings", "Talk"],
|
||||
"apps/ios/WatchApp/Sources/WatchInboxView.swift": [
|
||||
"Approve",
|
||||
|
|
@ -168,7 +168,7 @@ export async function checkAppleAppI18n() {
|
|||
for (const locale of REQUIRED_LOCALES) {
|
||||
const unit = entry.localizations?.[locale]?.stringUnit;
|
||||
const value = unit?.value?.trim();
|
||||
if (!value || unit?.state !== "translated") {
|
||||
if (!value || (locale === "en" && unit?.state !== "translated")) {
|
||||
throw new Error(
|
||||
`Apple catalog ${spec.path} is missing ${locale} for ${JSON.stringify(key)}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,24 @@ describe("Apple app i18n catalogs", () => {
|
|||
await expect(checkAppleAppI18n()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not mark English setup fallbacks as completed translations", async () => {
|
||||
const catalog = JSON.parse(
|
||||
await readFile("apps/ios/Resources/Localizable.xcstrings", "utf8"),
|
||||
) as {
|
||||
strings: Record<
|
||||
string,
|
||||
{ localizations?: Record<string, { stringUnit?: { state?: string; value?: string } }> }
|
||||
>;
|
||||
};
|
||||
|
||||
expect(
|
||||
catalog.strings["Connect a nearby Gateway"]?.localizations?.de?.stringUnit,
|
||||
).toMatchObject({
|
||||
state: "new",
|
||||
value: "Connect a nearby Gateway",
|
||||
});
|
||||
});
|
||||
|
||||
it("compiles macOS catalogs into app-bundle localization directories", async () => {
|
||||
const outputDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-apple-i18n-"));
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ describe("native app i18n inventory", () => {
|
|||
),
|
||||
).toBe(true);
|
||||
expect(entries.some((entry) => entry.source === "No sessions yet")).toBe(true);
|
||||
expect(entries.some((entry) => entry.source === "Don’t show this again")).toBe(true);
|
||||
expect(entries.some((entry) => entry.source === "Don't show this again")).toBe(true);
|
||||
expect(entries.some((entry) => entry.source === "Use Manual Gateway")).toBe(true);
|
||||
expect(entries.some((entry) => entry.source === "Session target")).toBe(true);
|
||||
expect(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue