Watch app MVP: XcodeGen project, gateway client, pair, feed, thread, new thread, notifications
SwiftUI watch-only app in watch/, generated by XcodeGen. Codable models mirror gateway/src/contract.ts, with lenient string enums so new phases or decisions still decode. A DemoGateway serves fixtures from the docs/api.md examples for previews and demo mode. Swift Testing covers decoding the documented JSON, request encoding, notification action parsing and the small view helpers. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ba1624eb9e
commit
bdfb79b891
19 files changed
+2413
No files matched your search
@@ -0,0 +1,112 @@
|
||||
// Mock B colors and the shared card. Amber means it needs you, teal means working, grey means done.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum Theme {
|
||||
static let you = Color(red: 1.0, green: 0.690, blue: 0.180) // #ffb02e
|
||||
static let work = Color(red: 0.310, green: 0.820, blue: 0.773) // #4fd1c5
|
||||
static let done = Color(red: 0.420, green: 0.420, blue: 0.439) // #6b6b70
|
||||
static let card = Color(red: 0.090, green: 0.090, blue: 0.102) // #17171a
|
||||
static let control = Color(red: 0.173, green: 0.173, blue: 0.180) // #2c2c2e
|
||||
static let secondaryText = Color(red: 0.682, green: 0.682, blue: 0.698) // #aeaeb2
|
||||
static let bodyText = Color(red: 0.898, green: 0.898, blue: 0.918) // #e5e5ea
|
||||
}
|
||||
|
||||
extension Phase {
|
||||
var color: Color {
|
||||
if needsYou { return Theme.you }
|
||||
if isWorking { return Theme.work }
|
||||
return Theme.done
|
||||
}
|
||||
}
|
||||
|
||||
/// A dark rounded card with a 4 pt colored left edge.
|
||||
struct Card<Content: View>: View {
|
||||
var edge: Color
|
||||
@ViewBuilder var content: Content
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
content
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 9)
|
||||
.padding(.leading, 15)
|
||||
.padding(.trailing, 11)
|
||||
.background(alignment: .leading) {
|
||||
ZStack(alignment: .leading) {
|
||||
Theme.card
|
||||
edge.frame(width: 4)
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
/// A full-width pill button, like mock B's Reply and Say the task.
|
||||
struct PillButtonStyle: ButtonStyle {
|
||||
var fill: Color
|
||||
var foreground: Color = .black
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.system(size: 16, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(foreground)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(fill.opacity(configuration.isPressed ? 0.7 : 1), in: Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
/// A smaller button for choices on a card, such as Allow and Deny.
|
||||
struct ChoiceButtonStyle: ButtonStyle {
|
||||
var fill: Color = Theme.control
|
||||
var foreground: Color = .white
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.system(size: 13, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(foreground)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 4)
|
||||
.background(fill.opacity(configuration.isPressed ? 0.7 : 1), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
enum Format {
|
||||
/// A short age for a card, such as "now", "4 min", "2 h" or "3 d".
|
||||
static func age(of date: Date, now: Date = .now) -> String {
|
||||
let minutes = Int(now.timeIntervalSince(date) / 60)
|
||||
if minutes < 1 { return "now" }
|
||||
if minutes < 60 { return "\(minutes) min" }
|
||||
let hours = minutes / 60
|
||||
if hours < 24 { return "\(hours) h" }
|
||||
return "\(hours / 24) d"
|
||||
}
|
||||
|
||||
/// A model id made readable: `claude-opus-5-5` becomes "Claude Opus 5.5".
|
||||
static func modelName(_ id: String) -> String {
|
||||
var words: [String] = []
|
||||
var number: [String] = []
|
||||
func flush() {
|
||||
if !number.isEmpty { words.append(number.joined(separator: ".")) }
|
||||
number = []
|
||||
}
|
||||
for part in id.split(whereSeparator: { $0 == "-" || $0 == "_" }).map(String.init) {
|
||||
if part.allSatisfy(\.isNumber) {
|
||||
number.append(part)
|
||||
continue
|
||||
}
|
||||
flush()
|
||||
switch part.lowercased() {
|
||||
case "gpt": words.append("GPT")
|
||||
default: words.append(part.prefix(1).uppercased() + part.dropFirst())
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return words.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user