summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu-co <bot@soryu.co>2026-04-24 13:15:29 +0000
committersoryu-co <bot@soryu.co>2026-04-24 13:21:12 +0000
commitdb092c79a175e3283f479ee0b234b24bde3c736e (patch)
tree16ae5ba1f2f2f6089ad9f625953fa02d19463ab0
parent3679ceb3325033faa2f889ef3dfee5668ef7aeea (diff)
downloadsoryu-makima-ios-scaffold.tar.gz
soryu-makima-ios-scaffold.zip
Add Makima iOS app scaffold (M0 + M1 design system)makima-ios-scaffold
Pure-native SwiftUI client for makima.jp under makima/ios/. M0 (scaffold) - XcodeGen project (iOS 18+, Swift 5.10, bundle co.soryu.makima) - Makefile targets: bootstrap, xcgen, ios-sim-fast, ios-device-fast, test, lint - GitHub Actions workflow ios-ci.yml — builds + runs XCTest on macos-14 - MIT repo root license already in place M1 (design system, web-aesthetic port) - Palette: #0c1729 background, #9bc3ff accent, #3f6fb3 border (ported from Tailwind) - Typography: SF Mono for chrome, uppercase tracked nav labels - Components: DashedBorder, GridOverlay, MastheadBar + WebSocketStatus pill, NavStripPlaceholder (NAV// prefix), JapaneseLongPressText (mobile analogue of JapaneseHoverText), Logo (reuses frontend/public/logo/makima-logo.svg with Canvas concentric-ring fallback), Badge - RootView demo screen: masthead, nav strip, logo, CONTROL SYSTEM badge, SYSTEM// status card, GLOSSARY// card with 命令/契約/聴取/史料 long-press terms Auth (v1 plan, not wired here): x-makima-api-key header — verified against src/server/auth.rs. Authorization: Bearer reserved for v1.1 Supabase OAuth. v1 plan doc: makima/ios/docs/ios-v1-plan.md Not in this PR: networking, WebSocket client, stores, feature surfaces (Home/Contracts/Tasks/Directives/Daemons/Listen), notifications. Those land across M2-M8 per the plan.
-rw-r--r--makima/ios/.gitignore7
-rw-r--r--makima/ios/Makefile54
-rw-r--r--makima/ios/README.md31
-rw-r--r--makima/ios/Sources/Makima/App/Info.plist18
-rw-r--r--makima/ios/Sources/Makima/App/MakimaApp.swift12
-rw-r--r--makima/ios/Sources/Makima/App/RootView.swift125
-rw-r--r--makima/ios/Sources/Makima/Design/Components/Badge.swift28
-rw-r--r--makima/ios/Sources/Makima/Design/Components/DashedBorder.swift24
-rw-r--r--makima/ios/Sources/Makima/Design/Components/GridOverlay.swift29
-rw-r--r--makima/ios/Sources/Makima/Design/Components/JapaneseLongPressText.swift37
-rw-r--r--makima/ios/Sources/Makima/Design/Components/Logo.swift51
-rw-r--r--makima/ios/Sources/Makima/Design/Components/MastheadBar.swift100
-rw-r--r--makima/ios/Sources/Makima/Design/Palette.swift41
-rw-r--r--makima/ios/Sources/Makima/Design/Typography.swift16
-rw-r--r--makima/ios/Sources/Makima/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json13
-rw-r--r--makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandAccent.colorset/Contents.json20
-rw-r--r--makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandBackground.colorset/Contents.json20
-rw-r--r--makima/ios/Sources/Makima/Resources/Assets.xcassets/Contents.json6
-rw-r--r--makima/ios/Sources/Makima/Resources/Logo/makima-icon-red.svg7
-rw-r--r--makima/ios/Sources/Makima/Resources/Logo/makima-logo.svg7
-rw-r--r--makima/ios/Tests/MakimaTests/SmokeTests.swift18
-rw-r--r--makima/ios/docs/DEVELOPMENT.md61
-rw-r--r--makima/ios/docs/RELEASING.md6
-rw-r--r--makima/ios/docs/ios-v1-plan.md222
-rw-r--r--makima/ios/project.yml84
25 files changed, 1037 insertions, 0 deletions
diff --git a/makima/ios/.gitignore b/makima/ios/.gitignore
new file mode 100644
index 0000000..cd2dd8d
--- /dev/null
+++ b/makima/ios/.gitignore
@@ -0,0 +1,7 @@
+Makima.xcodeproj/
+DerivedData/
+build/
+*.xcuserstate
+.DS_Store
+xcuserdata/
+*.xcworkspace/xcuserdata/
diff --git a/makima/ios/Makefile b/makima/ios/Makefile
new file mode 100644
index 0000000..8d2a23c
--- /dev/null
+++ b/makima/ios/Makefile
@@ -0,0 +1,54 @@
+# Makima iOS — build shortcuts
+# Requires: xcodegen (brew install xcodegen), Xcode 16+
+# Run `make xcgen` after editing project.yml.
+
+SCHEME := Makima
+WORKSPACE := Makima.xcodeproj
+SIM_DEVICE := iPhone 16 Pro
+
+.PHONY: help bootstrap xcgen ios-sim-fast ios-device-fast clean test lint
+
+help:
+ @echo "Targets:"
+ @echo " bootstrap install xcodegen if missing"
+ @echo " xcgen (re)generate Makima.xcodeproj from project.yml"
+ @echo " ios-sim-fast build for simulator ($(SIM_DEVICE))"
+ @echo " ios-device-fast build for generic iOS device"
+ @echo " test run unit tests on simulator"
+ @echo " clean remove generated project + DerivedData"
+ @echo " lint swift format lint (if installed)"
+
+bootstrap:
+ @command -v xcodegen >/dev/null || (echo "Installing xcodegen..." && brew install xcodegen)
+
+xcgen: bootstrap
+ xcodegen generate
+
+ios-sim-fast: xcgen
+ set -o pipefail; xcodebuild \
+ -project $(WORKSPACE) \
+ -scheme $(SCHEME) \
+ -destination 'platform=iOS Simulator,name=$(SIM_DEVICE)' \
+ -configuration Debug \
+ build | xcbeautify || true
+
+ios-device-fast: xcgen
+ set -o pipefail; xcodebuild \
+ -project $(WORKSPACE) \
+ -scheme $(SCHEME) \
+ -destination 'generic/platform=iOS' \
+ -configuration Debug \
+ build | xcbeautify || true
+
+test: xcgen
+ set -o pipefail; xcodebuild \
+ -project $(WORKSPACE) \
+ -scheme $(SCHEME) \
+ -destination 'platform=iOS Simulator,name=$(SIM_DEVICE)' \
+ test | xcbeautify || true
+
+clean:
+ rm -rf Makima.xcodeproj DerivedData build
+
+lint:
+ @command -v swift-format >/dev/null && swift-format lint -r Sources Tests || echo "swift-format not installed, skipping"
diff --git a/makima/ios/README.md b/makima/ios/README.md
new file mode 100644
index 0000000..2a6a664
--- /dev/null
+++ b/makima/ios/README.md
@@ -0,0 +1,31 @@
+# Makima iOS
+
+Native iPhone client for [Makima](https://makima.jp) — distributed mesh listening and AI daemon orchestration platform by Soryu LTD.
+
+**Status:** pre-alpha (M0 scaffold). See [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
+
+**Plan:** see `/root/notes/makima/ios-v1-plan.md` on the infra host, or the mirrored copy in `docs/PLAN.md` once synced.
+
+## Quick start
+
+```bash
+cd makima/ios
+make bootstrap # installs xcodegen if missing
+make ios-sim-fast # builds for iPhone 16 Pro simulator
+```
+
+Xcode 16+, iOS 18+ target, Swift 5.10+, pure SwiftUI. No third-party packages at M0/M1.
+
+## Architecture
+
+Pure native SwiftUI with `@Observable` stores, `URLSession` networking, `URLSessionWebSocketTask` for task livestream, SwiftData for local cache, Keychain for the `x-makima-api-key` credential.
+
+No cross-platform core, no React Native, no Expo. See `../../README.md` for the wider monorepo.
+
+## License
+
+MIT — see repo-root `LICENSE`.
+
+## Privacy
+
+No analytics. No telemetry. The app talks directly to the Makima server URL you configure — by default `https://makima.jp`, but any reachable instance works (`Settings → Server URL`). Credentials are stored in iOS Keychain, scoped per server URL. Full privacy policy: this README.
diff --git a/makima/ios/Sources/Makima/App/Info.plist b/makima/ios/Sources/Makima/App/Info.plist
new file mode 100644
index 0000000..39a5741
--- /dev/null
+++ b/makima/ios/Sources/Makima/App/Info.plist
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+</dict>
+</plist>
diff --git a/makima/ios/Sources/Makima/App/MakimaApp.swift b/makima/ios/Sources/Makima/App/MakimaApp.swift
new file mode 100644
index 0000000..907bf8e
--- /dev/null
+++ b/makima/ios/Sources/Makima/App/MakimaApp.swift
@@ -0,0 +1,12 @@
+import SwiftUI
+
+@main
+struct MakimaApp: App {
+ var body: some Scene {
+ WindowGroup {
+ RootView()
+ .preferredColorScheme(.dark)
+ .tint(Palette.accent)
+ }
+ }
+}
diff --git a/makima/ios/Sources/Makima/App/RootView.swift b/makima/ios/Sources/Makima/App/RootView.swift
new file mode 100644
index 0000000..bb724fc
--- /dev/null
+++ b/makima/ios/Sources/Makima/App/RootView.swift
@@ -0,0 +1,125 @@
+import SwiftUI
+
+/// Root view for M1. Shows the masthead + a placeholder "system online" card
+/// so we can judge the aesthetic port against makima.jp side-by-side.
+struct RootView: View {
+ var body: some View {
+ ZStack {
+ Palette.background.ignoresSafeArea()
+ GridOverlay()
+
+ VStack(spacing: 0) {
+ MastheadBar(
+ serverLabel: "makima.jp",
+ wsStatus: .idle,
+ version: Bundle.main.shortVersion
+ )
+ NavStripPlaceholder()
+
+ ScrollView {
+ VStack(spacing: 24) {
+ Spacer(minLength: 32)
+
+ Logo(size: 140)
+
+ VStack(spacing: 6) {
+ Badge(text: "支配する", subtitle: "CONTROL SYSTEM")
+ Text("Mesh Orchestration Platform")
+ .font(Typography.titleChrome)
+ .foregroundStyle(Palette.foreground)
+ .tracking(1)
+ Text("Makima is listening.")
+ .font(Typography.body)
+ .foregroundStyle(Palette.foregroundMuted)
+ }
+
+ statusCard
+ .padding(.horizontal, 16)
+
+ languageDemoCard
+ .padding(.horizontal, 16)
+
+ Spacer(minLength: 40)
+ }
+ .padding(.top, 12)
+ }
+ }
+ }
+ }
+
+ private var statusCard: some View {
+ VStack(alignment: .leading, spacing: 10) {
+ HStack {
+ Text("SYSTEM//")
+ .font(Typography.navLabel)
+ .foregroundStyle(Palette.foregroundMuted)
+ Spacer()
+ Text("M0 + M1")
+ .font(Typography.navLabel)
+ .foregroundStyle(Palette.accent)
+ }
+ Divider().overlay(Palette.borderMuted)
+ ForEach(StatusRow.samples) { row in
+ HStack {
+ Circle()
+ .fill(row.ok ? Palette.ok : Palette.warn)
+ .frame(width: 6, height: 6)
+ Text(row.label)
+ .font(Typography.body)
+ .foregroundStyle(Palette.foreground)
+ Spacer()
+ Text(row.value)
+ .font(Typography.mono)
+ .foregroundStyle(Palette.foregroundMuted)
+ }
+ }
+ }
+ .padding(14)
+ .dashedBorder()
+ }
+
+ private var languageDemoCard: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("GLOSSARY//")
+ .font(Typography.navLabel)
+ .foregroundStyle(Palette.foregroundMuted)
+ Divider().overlay(Palette.borderMuted)
+ VStack(alignment: .leading, spacing: 8) {
+ JapaneseLongPressText(japanese: "命令", english: "Directives")
+ JapaneseLongPressText(japanese: "契約", english: "Contracts")
+ JapaneseLongPressText(japanese: "聴取", english: "Listen")
+ JapaneseLongPressText(japanese: "史料", english: "History")
+ }
+ Text("Long-press any term to reveal the English gloss.")
+ .font(Typography.caption)
+ .foregroundStyle(Palette.foregroundMuted)
+ .padding(.top, 4)
+ }
+ .padding(14)
+ .dashedBorder()
+ }
+}
+
+private struct StatusRow: Identifiable {
+ let id = UUID()
+ let label: String
+ let value: String
+ let ok: Bool
+
+ static let samples: [StatusRow] = [
+ StatusRow(label: "Scaffold", value: "READY", ok: true),
+ StatusRow(label: "Design Sys", value: "M1", ok: true),
+ StatusRow(label: "Network", value: "NOT WIRED", ok: false),
+ StatusRow(label: "WebSocket", value: "NOT WIRED", ok: false)
+ ]
+}
+
+private extension Bundle {
+ var shortVersion: String {
+ (infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0.0.0"
+ }
+}
+
+#Preview {
+ RootView()
+}
diff --git a/makima/ios/Sources/Makima/Design/Components/Badge.swift b/makima/ios/Sources/Makima/Design/Components/Badge.swift
new file mode 100644
index 0000000..4c870a7
--- /dev/null
+++ b/makima/ios/Sources/Makima/Design/Components/Badge.swift
@@ -0,0 +1,28 @@
+import SwiftUI
+
+/// Small inline badge — mirrors the web's
+/// `px-2 py-1 border border-[#3f6fb3] bg-[#0f1c2f] text-[#9bc3ff] font-mono text-xs tracking-wide uppercase`
+struct Badge: View {
+ let text: String
+ var subtitle: String?
+
+ var body: some View {
+ HStack(spacing: 6) {
+ Text(text)
+ .font(.system(.footnote, design: .default))
+ .foregroundStyle(Palette.accent)
+ if let subtitle {
+ Text("·")
+ .font(Typography.navLabel)
+ .foregroundStyle(Palette.borderMuted)
+ Text(subtitle)
+ .font(Typography.navLabel)
+ .foregroundStyle(Palette.foregroundMuted)
+ }
+ }
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(Palette.panel)
+ .overlay(Rectangle().strokeBorder(Palette.border, lineWidth: 1))
+ }
+}
diff --git a/makima/ios/Sources/Makima/Design/Components/DashedBorder.swift b/makima/ios/Sources/Makima/Design/Components/DashedBorder.swift
new file mode 100644
index 0000000..c0af431
--- /dev/null
+++ b/makima/ios/Sources/Makima/Design/Components/DashedBorder.swift
@@ -0,0 +1,24 @@
+import SwiftUI
+
+/// Mirrors Tailwind's `border-dashed border-[rgba(117,170,252,0.35)]`.
+struct DashedBorderModifier: ViewModifier {
+ var color: Color = Palette.borderMuted
+ var dash: [CGFloat] = [4, 4]
+ var cornerRadius: CGFloat = 0
+
+ func body(content: Content) -> some View {
+ content
+ .overlay(
+ RoundedRectangle(cornerRadius: cornerRadius)
+ .strokeBorder(color, style: StrokeStyle(lineWidth: 1, dash: dash))
+ )
+ }
+}
+
+extension View {
+ func dashedBorder(color: Color = Palette.borderMuted,
+ dash: [CGFloat] = [4, 4],
+ cornerRadius: CGFloat = 0) -> some View {
+ modifier(DashedBorderModifier(color: color, dash: dash, cornerRadius: cornerRadius))
+ }
+}
diff --git a/makima/ios/Sources/Makima/Design/Components/GridOverlay.swift b/makima/ios/Sources/Makima/Design/Components/GridOverlay.swift
new file mode 100644
index 0000000..824d0b2
--- /dev/null
+++ b/makima/ios/Sources/Makima/Design/Components/GridOverlay.swift
@@ -0,0 +1,29 @@
+import SwiftUI
+
+/// Subtle grid, mirrors the makima.jp body background texture.
+/// Drawn as a `Canvas` so there's no asset dependency.
+struct GridOverlay: View {
+ var spacing: CGFloat = 32
+ var opacity: Double = 0.04
+
+ var body: some View {
+ Canvas { ctx, size in
+ let color = GraphicsContext.Shading.color(.white.opacity(opacity))
+ var path = Path()
+ var x = spacing
+ while x < size.width {
+ path.move(to: CGPoint(x: x, y: 0))
+ path.addLine(to: CGPoint(x: x, y: size.height))
+ x += spacing
+ }
+ var y = spacing
+ while y < size.height {
+ path.move(to: CGPoint(x: 0, y: y))
+ path.addLine(to: CGPoint(x: size.width, y: y))
+ y += spacing
+ }
+ ctx.stroke(path, with: color, lineWidth: 0.5)
+ }
+ .allowsHitTesting(false)
+ }
+}
diff --git a/makima/ios/Sources/Makima/Design/Components/JapaneseLongPressText.swift b/makima/ios/Sources/Makima/Design/Components/JapaneseLongPressText.swift
new file mode 100644
index 0000000..fb57154
--- /dev/null
+++ b/makima/ios/Sources/Makima/Design/Components/JapaneseLongPressText.swift
@@ -0,0 +1,37 @@
+import SwiftUI
+
+/// Mobile-native equivalent of the web's `JapaneseHoverText`.
+/// Shows the Japanese term; long-press (or tap) reveals the English gloss
+/// via a brief animated flip.
+struct JapaneseLongPressText: View {
+ let japanese: String
+ let english: String
+
+ @State private var revealed = false
+
+ var body: some View {
+ HStack(spacing: 6) {
+ Text(japanese)
+ .font(Typography.japanese)
+ .foregroundStyle(Palette.foreground)
+ .padding(.vertical, 3)
+ .padding(.horizontal, 6)
+ .background(Palette.panel)
+ .overlay(
+ Rectangle().strokeBorder(Palette.borderMuted, lineWidth: 1)
+ )
+ .contentShape(Rectangle())
+ .onTapGesture { withAnimation(.easeInOut(duration: 0.2)) { revealed.toggle() } }
+ .onLongPressGesture(minimumDuration: 0.15, pressing: { pressing in
+ withAnimation(.easeInOut(duration: 0.15)) { revealed = pressing }
+ }, perform: {})
+
+ Text(english)
+ .font(Typography.navLabel)
+ .foregroundStyle(revealed ? Palette.accent : Palette.foregroundMuted.opacity(0.4))
+ .animation(.easeInOut(duration: 0.2), value: revealed)
+ }
+ .accessibilityElement(children: .combine)
+ .accessibilityLabel("\(japanese), \(english)")
+ }
+}
diff --git a/makima/ios/Sources/Makima/Design/Components/Logo.swift b/makima/ios/Sources/Makima/Design/Components/Logo.swift
new file mode 100644
index 0000000..01d263e
--- /dev/null
+++ b/makima/ios/Sources/Makima/Design/Components/Logo.swift
@@ -0,0 +1,51 @@
+import SwiftUI
+
+/// Makima concentric-ring logo.
+/// Loads from bundled `makima-logo.svg` if present; otherwise falls back to
+/// a Canvas-rendered concentric-ring approximation so the app always builds
+/// even if the SVG copy step is skipped.
+struct Logo: View {
+ var size: CGFloat = 120
+
+ var body: some View {
+ Group {
+ if let uiImage = Self.loadedSVG(named: "makima-logo") {
+ Image(uiImage: uiImage)
+ .resizable()
+ .renderingMode(.template)
+ .foregroundStyle(Palette.accent)
+ } else {
+ Canvas { ctx, canvasSize in
+ let center = CGPoint(x: canvasSize.width / 2, y: canvasSize.height / 2)
+ let maxR = min(canvasSize.width, canvasSize.height) / 2
+ let rings = 5
+ for i in 0..<rings {
+ let t = CGFloat(i + 1) / CGFloat(rings)
+ let r = maxR * t
+ let rect = CGRect(x: center.x - r, y: center.y - r,
+ width: r * 2, height: r * 2)
+ let shading = GraphicsContext.Shading.color(
+ Palette.accent.opacity(1.0 - Double(i) * 0.15)
+ )
+ ctx.stroke(Circle().path(in: rect), with: shading, lineWidth: 1.25)
+ }
+ let dotR = maxR * 0.06
+ ctx.fill(Circle().path(in: CGRect(x: center.x - dotR, y: center.y - dotR,
+ width: dotR * 2, height: dotR * 2)),
+ with: .color(Palette.accent))
+ }
+ }
+ }
+ .frame(width: size, height: size)
+ .accessibilityLabel("Makima")
+ }
+
+ /// SwiftUI 18 doesn't natively decode SVG through UIImage, so we try PDF
+ /// fallback first (Xcode auto-converts SVGs placed in asset catalogs to PDF).
+ /// If the file ships as a raw `.svg` bundle resource, we decline and fall back
+ /// to the Canvas drawing. This keeps M1 buildable without additional deps.
+ private static func loadedSVG(named name: String) -> UIImage? {
+ if let img = UIImage(named: name) { return img }
+ return nil
+ }
+}
diff --git a/makima/ios/Sources/Makima/Design/Components/MastheadBar.swift b/makima/ios/Sources/Makima/Design/Components/MastheadBar.swift
new file mode 100644
index 0000000..a304f95
--- /dev/null
+++ b/makima/ios/Sources/Makima/Design/Components/MastheadBar.swift
@@ -0,0 +1,100 @@
+import SwiftUI
+
+enum WebSocketStatus {
+ case idle, connecting, online, offline
+
+ var label: String {
+ switch self {
+ case .idle: return "WS//IDLE"
+ case .connecting: return "WS//…"
+ case .online: return "WS//ONLINE"
+ case .offline: return "WS//OFFLINE"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .idle: return Palette.foregroundMuted
+ case .connecting: return Palette.warn
+ case .online: return Palette.ok
+ case .offline: return Palette.danger
+ }
+ }
+}
+
+/// Top chrome strip, replaces a traditional nav bar.
+/// Composition: small logo + "MAKIMA" brand + spacer + ws-pill + server label + version.
+struct MastheadBar: View {
+ let serverLabel: String
+ let wsStatus: WebSocketStatus
+ let version: String
+
+ var body: some View {
+ HStack(spacing: 10) {
+ Logo(size: 22)
+ Text("MAKIMA")
+ .font(.system(.caption, design: .monospaced).weight(.semibold))
+ .tracking(2)
+ .foregroundStyle(Palette.foreground)
+
+ Spacer(minLength: 8)
+
+ Text(wsStatus.label)
+ .font(Typography.navLabel)
+ .foregroundStyle(wsStatus.color)
+ Text("·")
+ .foregroundStyle(Palette.borderMuted)
+ Text(serverLabel)
+ .font(Typography.navLabel)
+ .foregroundStyle(Palette.accent)
+ Text("·")
+ .foregroundStyle(Palette.borderMuted)
+ Text("v\(version)")
+ .font(Typography.navLabel)
+ .foregroundStyle(Palette.foregroundMuted)
+ }
+ .padding(.horizontal, 14)
+ .padding(.vertical, 10)
+ .background(
+ LinearGradient(
+ colors: [Palette.backgroundDeep.opacity(0.95), Palette.panel.opacity(0.7)],
+ startPoint: .top, endPoint: .bottom
+ )
+ )
+ .overlay(alignment: .bottom) {
+ Rectangle().fill(Palette.border).frame(height: 2)
+ }
+ }
+}
+
+struct NavStripPlaceholder: View {
+ private let labels = ["LISTEN", "DIRECTIVES", "ORDERS", "CONTRACTS", "DAEMONS", "HISTORY"]
+
+ var body: some View {
+ HStack(spacing: 10) {
+ Text("NAV//")
+ .font(Typography.navLabel)
+ .foregroundStyle(Palette.accent)
+ .padding(.trailing, 4)
+ .overlay(alignment: .trailing) {
+ Rectangle().fill(Palette.borderMuted).frame(width: 1, height: 14)
+ .offset(x: 4)
+ }
+ ForEach(labels, id: \.self) { l in
+ Text(l)
+ .font(Typography.navLabel)
+ .foregroundStyle(Palette.accent.opacity(0.8))
+ }
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 10)
+ .background(Palette.panel)
+ .overlay(alignment: .top) {
+ Rectangle().fill(Palette.borderMuted).frame(height: 1)
+ }
+ .overlay(alignment: .bottom) {
+ Rectangle().fill(Palette.borderMuted).frame(height: 1)
+ }
+ }
+}
diff --git a/makima/ios/Sources/Makima/Design/Palette.swift b/makima/ios/Sources/Makima/Design/Palette.swift
new file mode 100644
index 0000000..abb531d
--- /dev/null
+++ b/makima/ios/Sources/Makima/Design/Palette.swift
@@ -0,0 +1,41 @@
+import SwiftUI
+
+/// Central color palette, ported from the makima.jp Tailwind config.
+/// Keep in lockstep with `frontend/src/index.css` and component classes.
+enum Palette {
+ /// `#0c1729` — app background
+ static let background = Color(hex: 0x0C1729)
+ /// Slightly lighter panel inset (`#0f1c2f`)
+ static let panel = Color(hex: 0x0F1C2F)
+ /// Deeper backdrop used behind masthead gradients
+ static let backgroundDeep = Color(hex: 0x0B1220)
+ /// `#f0f5ff` — brightest foreground (titles)
+ static let foreground = Color(hex: 0xF0F5FF)
+ /// `#9bc3ff` — primary foreground (nav, labels)
+ static let accent = Color(hex: 0x9BC3FF)
+ /// `#dbe7ff` — body text
+ static let body = Color(hex: 0xDBE7FF)
+ /// `#e4edff` — softer body
+ static let bodySoft = Color(hex: 0xE4EDFF)
+ /// Muted / secondary foreground
+ static let foregroundMuted = Color(hex: 0x9BC3FF).opacity(0.65)
+ /// `#3f6fb3` — primary border
+ static let border = Color(hex: 0x3F6FB3)
+ /// Subtle dashed-border tint (`rgba(117,170,252,0.35)`)
+ static let borderMuted = Color(red: 117/255, green: 170/255, blue: 252/255).opacity(0.35)
+ /// Amber alert (pending questions)
+ static let warn = Color(hex: 0xF59E0B)
+ /// OK / running
+ static let ok = Color(hex: 0x4ADE80)
+ /// Error / failed
+ static let danger = Color(hex: 0xF87171)
+}
+
+extension Color {
+ init(hex: UInt32, alpha: Double = 1.0) {
+ let r = Double((hex >> 16) & 0xFF) / 255
+ let g = Double((hex >> 8) & 0xFF) / 255
+ let b = Double( hex & 0xFF) / 255
+ self.init(.sRGB, red: r, green: g, blue: b, opacity: alpha)
+ }
+}
diff --git a/makima/ios/Sources/Makima/Design/Typography.swift b/makima/ios/Sources/Makima/Design/Typography.swift
new file mode 100644
index 0000000..d697834
--- /dev/null
+++ b/makima/ios/Sources/Makima/Design/Typography.swift
@@ -0,0 +1,16 @@
+import SwiftUI
+
+enum Typography {
+ /// SF Mono, uppercase, tracked — used for NAV// prefixes, section labels
+ static let navLabel = Font.system(.caption2, design: .monospaced).weight(.medium)
+ /// Section titles ("Mesh Orchestration Platform")
+ static let titleChrome = Font.system(size: 20, weight: .medium, design: .default)
+ /// Body text
+ static let body = Font.system(.body, design: .default)
+ /// Caption-level body
+ static let caption = Font.system(.caption, design: .default)
+ /// Inline monospace (values, IDs, hostnames)
+ static let mono = Font.system(.footnote, design: .monospaced)
+ /// Japanese hover/long-press words
+ static let japanese = Font.system(size: 15, weight: .regular, design: .default)
+}
diff --git a/makima/ios/Sources/Makima/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/makima/ios/Sources/Makima/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..caefc04
--- /dev/null
+++ b/makima/ios/Sources/Makima/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images": [
+ {
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+} \ No newline at end of file
diff --git a/makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandAccent.colorset/Contents.json b/makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandAccent.colorset/Contents.json
new file mode 100644
index 0000000..81e0580
--- /dev/null
+++ b/makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandAccent.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors": [
+ {
+ "idiom": "universal",
+ "color": {
+ "color-space": "srgb",
+ "components": {
+ "red": "0.608",
+ "green": "0.765",
+ "blue": "1.000",
+ "alpha": "1.000"
+ }
+ }
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+} \ No newline at end of file
diff --git a/makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandBackground.colorset/Contents.json b/makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandBackground.colorset/Contents.json
new file mode 100644
index 0000000..64d84a0
--- /dev/null
+++ b/makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandBackground.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors": [
+ {
+ "idiom": "universal",
+ "color": {
+ "color-space": "srgb",
+ "components": {
+ "red": "0.047",
+ "green": "0.090",
+ "blue": "0.161",
+ "alpha": "1.000"
+ }
+ }
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+} \ No newline at end of file
diff --git a/makima/ios/Sources/Makima/Resources/Assets.xcassets/Contents.json b/makima/ios/Sources/Makima/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..c47b5f2
--- /dev/null
+++ b/makima/ios/Sources/Makima/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+} \ No newline at end of file
diff --git a/makima/ios/Sources/Makima/Resources/Logo/makima-icon-red.svg b/makima/ios/Sources/Makima/Resources/Logo/makima-icon-red.svg
new file mode 100644
index 0000000..ec21830
--- /dev/null
+++ b/makima/ios/Sources/Makima/Resources/Logo/makima-icon-red.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
+ <circle cx="12" cy="12" r="9.5" stroke="#b30000" stroke-width="2" />
+ <circle cx="12" cy="12" r="6.5" stroke="#b30000" stroke-width="1.6" />
+ <circle cx="12" cy="12" r="3.6" stroke="#b30000" stroke-width="1.6" />
+ <circle cx="12" cy="12" r="1.4" fill="#b30000" />
+</svg>
diff --git a/makima/ios/Sources/Makima/Resources/Logo/makima-logo.svg b/makima/ios/Sources/Makima/Resources/Logo/makima-logo.svg
new file mode 100644
index 0000000..4872d43
--- /dev/null
+++ b/makima/ios/Sources/Makima/Resources/Logo/makima-logo.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
+ <circle cx="12" cy="12" r="10" stroke="#003366" stroke-width="2" />
+ <circle cx="12" cy="12" r="7" stroke="#003366" stroke-width="1.6" />
+ <circle cx="12" cy="12" r="4" stroke="#003366" stroke-width="1.6" />
+ <circle cx="12" cy="12" r="1.6" fill="#003366" />
+</svg>
diff --git a/makima/ios/Tests/MakimaTests/SmokeTests.swift b/makima/ios/Tests/MakimaTests/SmokeTests.swift
new file mode 100644
index 0000000..3f63ef1
--- /dev/null
+++ b/makima/ios/Tests/MakimaTests/SmokeTests.swift
@@ -0,0 +1,18 @@
+import XCTest
+import SwiftUI
+import UIKit
+@testable import Makima
+
+final class SmokeTests: XCTestCase {
+ func testPaletteBackgroundNotTransparent() throws {
+ // Basic sanity: the color resolves on iOS
+ let cg = UIColor(Palette.background).cgColor
+ XCTAssertNotNil(cg.components)
+ }
+
+ func testWebSocketStatusLabels() {
+ XCTAssertEqual(WebSocketStatus.idle.label, "WS//IDLE")
+ XCTAssertEqual(WebSocketStatus.online.label, "WS//ONLINE")
+ XCTAssertEqual(WebSocketStatus.offline.label, "WS//OFFLINE")
+ }
+}
diff --git a/makima/ios/docs/DEVELOPMENT.md b/makima/ios/docs/DEVELOPMENT.md
new file mode 100644
index 0000000..f58a218
--- /dev/null
+++ b/makima/ios/docs/DEVELOPMENT.md
@@ -0,0 +1,61 @@
+# Makima iOS — Development
+
+## Prerequisites
+
+- macOS 14+ with Xcode 16+
+- Homebrew
+- `xcodegen` (auto-installed by `make bootstrap`)
+- Optional: `xcbeautify` for nicer build logs (`brew install xcbeautify`)
+- Optional: `swift-format` for linting (`brew install swift-format`)
+
+## Source of truth
+
+`project.yml` is the source of truth. `Makima.xcodeproj` is generated. **Do not commit `Makima.xcodeproj/`.** If you need to hand-tweak build settings, edit `project.yml` and re-run `make xcgen`.
+
+## Running
+
+```bash
+make ios-sim-fast # simulator build
+make ios-device-fast # device build (needs signing team set via env or project.yml)
+make test # unit tests on simulator
+```
+
+First device build: open `Makima.xcodeproj` in Xcode, select the `Makima` target, set your Signing Team under "Signing & Capabilities". That value is only local to Xcode's derived config.
+
+## Folder layout
+
+```
+ios/
+ project.yml # XcodeGen source
+ Makefile # build shortcuts
+ Sources/Makima/
+ App/ # entry point, Info.plist
+ Design/ # Palette, Typography, shared components
+ Net/ # APIClient, WebSocketClient, AuthStore (M2+)
+ Stores/ # @Observable stores (M3+)
+ Features/ # screen-level views
+ Models/ # Codable types for the Makima API
+ Markdown/ # custom markdown + code block rendering (M5)
+ Resources/
+ Assets.xcassets/ # app icon, colors
+ Logo/ # copied from frontend/public/logo
+ Tests/MakimaTests/
+ docs/
+```
+
+## Server config during development
+
+Default server URL is `https://makima.jp`. For local dev against a self-hosted makima:
+
+1. Onboarding → Server URL → enter e.g. `http://<host>:8080`
+2. Provide an API key created via `POST /api/v1/auth/api-keys` on that server.
+
+`NSAllowsArbitraryLoads` is disabled; for plain-HTTP dev servers you must either run behind TLS or add an ATS exception for your dev host via project.yml's `Info.plist.properties`.
+
+## CI
+
+GitHub Actions workflow at `.github/workflows/ios-ci.yml` runs `make test` on macOS-14 runners for every PR touching `makima/ios/**`.
+
+## Release (v1)
+
+TestFlight release lane is defined in `docs/RELEASING.md` (to be written at M8).
diff --git a/makima/ios/docs/RELEASING.md b/makima/ios/docs/RELEASING.md
new file mode 100644
index 0000000..1564a1a
--- /dev/null
+++ b/makima/ios/docs/RELEASING.md
@@ -0,0 +1,6 @@
+# Releasing (placeholder — filled in at M8)
+
+- App Store Connect: Soryu LTD
+- Bundle ID: `co.soryu.makima`
+- TestFlight internal group: TBD
+- Fastlane lanes: TBD
diff --git a/makima/ios/docs/ios-v1-plan.md b/makima/ios/docs/ios-v1-plan.md
new file mode 100644
index 0000000..876a6ff
--- /dev/null
+++ b/makima/ios/docs/ios-v1-plan.md
@@ -0,0 +1,222 @@
+# Makima iOS — v1 Plan
+
+**Date:** 2026-04-24
+**Target:** iOS 18.0+ · iPhone only · Pure native SwiftUI · No Android
+**Upstream:** `soryu/makima` (Rust + Postgres server, React frontend at `makima.jp`)
+**Location:** new folder `apps/ios/` inside the existing `soryu-co/soryu` monorepo (sibling to `frontend/`, `makima/`, `k8s/`).
+**License:** MIT (carry repo-level LICENSE; no per-folder LICENSE needed).
+**App Store seller:** Soryu LTD.
+**Bundle ID:** `co.soryu.makima` (dropped the `.ios` suffix per preference).
+
+---
+
+## 1. Product shape
+
+**Primary surface: Composite Home.** A glanceable dashboard with section cards:
+
+- **Active Contracts** — count + top 3 running, tap → Contracts list
+- **Active Daemons** — mesh status pill (N online / M offline)
+- **Pending Directive Questions** — amber badge, tap → Directives
+- **Latest Listen Session** — last transcript preview, tap → Listen detail
+- **Recent Tasks** — 3 most recent tasks across contracts, tap → Task detail (live output)
+
+Secondary surfaces reachable from home: Contracts list, Tasks list, Directives, Daemons, Listen (read-only history in v1), Settings.
+
+**Not in v1:** Speak (TTS), Files editor, Orders write-path, mesh merge UI, worktree diff viewer, daemon reauth flow, supervisor questions *answering* (read-only badge only — answer on web).
+
+## 2. Stack
+
+- **Language:** Swift 5.10+ / SwiftUI
+- **Min iOS:** 18.0 (same as Litter/KittyLitter — lets us use latest SwiftUI APIs, Observation, Swift Concurrency)
+- **Architecture:** MV (Model + View) with `@Observable` stores — no MVVM ceremony
+- **Networking:** `URLSession` + `URLSessionWebSocketTask` (no Alamofire/Starscream)
+- **Persistence:** SwiftData for cached contracts/tasks/transcripts; Keychain for API key(s); `UserDefaults` for server-profile list
+- **Markdown/code:** `swift-markdown-ui` (MIT) for markdown, `Splash` or `Runestone` for syntax highlighting (evaluate at impl time — Splash is lighter, Runestone handles big files)
+- **Push:** APNs via a small `services/push-proxy` later — **not in v1**. v1 uses local notifications triggered by WS events while app is foreground/recent. True background push = v1.1.
+- **Build:** XcodeGen (`project.yml` source of truth, matches Litter)
+- **No dependencies beyond:** swift-markdown-ui, KeychainAccess (or hand-roll), a syntax highlighter. Everything else stdlib.
+
+## 3. Auth & server configuration
+
+### 3.1 Multi-profile server picker *(question 3 = a: single field, but we're building the primitive that generalises)*
+
+User picked (a) — single "Server URL" field. Implementation:
+
+- `Settings → Server URL` text field, default `https://makima.jp`
+- Stored in Keychain alongside the API key (so swapping server swaps the credential bundle)
+- On change: clear cached contracts/tasks/daemons, re-fetch `/health`, prompt for new API key if 401
+- Internally represented as `ServerProfile { url: URL, apiKey: String, label: String }` — list-of-one in v1, lets us upgrade to multi-profile in v1.x without migration
+
+### 3.2 API key flow *(question 2 = c: both — but v1 = API key only)*
+
+User said (c) both, but Supabase OAuth needs project keys and deep-link wiring. Ship API key paste in v1, add Supabase in v1.1:
+
+1. First-launch onboarding:
+ - Screen 1: Server URL (prefilled `https://makima.jp`)
+ - Screen 2: "Open web settings → create API key → paste here" with a `Open in Safari` button to `<server>/settings`
+ - Screen 3: validate via `GET /api/v1/mesh/daemons` (any authenticated endpoint); on success, store and proceed to Home
+2. API key stored in Keychain, scoped per-server-URL
+3. `Settings → Rotate key` button — calls `POST /api/v1/auth/api-keys/refresh`, replaces stored key
+
+### 3.3 Auth header *(verified 2026-04-24 against `src/server/auth.rs`)*
+
+- **API keys → header `x-makima-api-key: <full-key>`** (constant `API_KEY_HEADER` line 404). Keys are prefixed `mk_`.
+- **Supabase JWTs → header `Authorization: Bearer <jwt>`** (line 1039–1041).
+- Order of precedence in the server: tool key → API key → JWT.
+- **v1 sends `x-makima-api-key` only.** `Authorization: Bearer` is reserved for v1.1 Supabase OAuth. Don't mix.
+- WebSocket: same header works via `URLSessionWebSocketTask` custom request headers (not via `?token=` query). Open Q #7 resolved — update client code accordingly.
+
+## 4. Surfaces & endpoints
+
+| Surface | Endpoints (v1) | Notes |
+|---|---|---|
+| Home | `GET /mesh/tasks?status=running`, `GET /mesh/daemons`, `GET /directives?pending=true`, `GET /listen/sessions?limit=1` | Parallel fetch, 30s refresh, pull-to-refresh |
+| Contracts list | `GET /contracts` | Filter: active / completed / archived |
+| Contract detail | `GET /contracts/{id}`, `GET /mesh/tasks?contract_id={id}` | Shows phase, tasks |
+| Tasks list | `GET /mesh/tasks` | Filter by status, contract |
+| **Task detail (hero)** | `GET /mesh/tasks/{id}`, `GET /mesh/tasks/{id}/output`, `WS /mesh/tasks/subscribe` | Live output stream, markdown+code rendered |
+| Directives | `GET /directives` | Read-only; badge for pending questions |
+| Daemons | `GET /mesh/daemons` | Status list, read-only |
+| Listen (history) | `GET /listen/sessions`, `GET /listen/sessions/{id}` | Transcript viewer, no live mic in v1 |
+| Settings | `GET /auth/api-keys`, `POST /auth/api-keys/refresh`, `DELETE /auth/api-keys` | Server URL, key rotate, about |
+
+### 4.1 WebSocket: Task output livestream *(question 4)*
+
+Single WS: `wss://<server>/api/v1/mesh/tasks/subscribe`
+Headers on the upgrade request: `x-makima-api-key: <full-key>` (same as HTTP — verified above).
+
+- Subscribes to all tasks the user has access to
+- Server pushes task events: `task.output`, `task.status`, `task.completion_gate`, `task.supervisor_question`
+- App maintains in-memory buffer per visible task, SwiftData-persists last 2000 lines on background
+- Auto-reconnect with exponential backoff (1s → 30s cap), shown as pill in masthead
+- When app is foregrounded, send `resync` frame with last-seen event IDs per task to catch up
+
+## 5. Notifications *(question 5 must-have)*
+
+**v1 (local only):**
+- While app is open or recently backgrounded (iOS background WS grace ~30s), fire `UNUserNotificationCenter` local notifications for:
+ - Task completed
+ - Task failed
+ - Supervisor question arrived
+- Notification taps deep-link to the relevant surface (`makima://task/{id}`, `makima://directive/{id}`)
+
+**v1.1 (push):**
+- Small `services/push-proxy` service (own repo or monorepo folder) that holds APNs device tokens and subscribes to the same WS on behalf of users
+- Server emits "notify" events; proxy fans out to APNs
+- Matches Litter's `services/push-proxy/` pattern
+
+## 6. Aesthetic port *(question 5 must-have)*
+
+Goal: feel like a native extension of makima.jp, not a generic iOS app.
+
+- **Colors:** port exactly — `#0c1729` bg, `#9bc3ff` foreground, `#3f6fb3` accent, amber `#f59e0b` for alerts. Central `Palette.swift`.
+- **Typography:** SF Mono for all chrome (nav, labels, ticker), SF Pro for body. Uppercase + tracked for nav labels (matches NavStrip).
+- **Chrome elements to port:**
+ - `NAV//` prefix strip → iOS tab bar replaced with a custom masthead bar (top), tab-bar-less
+ - Dashed borders (`border-dashed` in Tailwind → `StrokeStyle(dash: [4, 4])` in SwiftUI)
+ - Masthead ticker → a thin top bar with build version + WS status + server label
+- **Logo:** reuse the existing logo from the web frontend — `frontend/public/logo/makima-logo.svg` (concentric rings). Copy into `apps/ios/Sources/Makima/Resources/Logo/` at M1, render as SwiftUI `Image` with SVG support via a tiny helper (SwiftUI 18 has native SVG; fallback = rasterize to PDF asset at build time). Also port `crane-logo-transparent.png` for the Soryu masthead credit if the design calls for it.
+ - `JapaneseHoverText` → `JapaneseLongPressText` on iOS (long-press reveals English). Same term list: 支配する, 命令, 契約, 史料, 聴取, etc.
+- **Motion:** respectful — fades and springs only. No parallax, no bounce.
+
+## 7. Markdown & code rendering *(question 5 must-have)*
+
+- `swift-markdown-ui` with custom theme matching web: inline code in `#1a2840` pill, code fences with language label chip, tables scroll horizontally
+- Syntax highlighting via Splash (lightweight) or Runestone (full editor; overkill unless we add editing later)
+- Task output stream is treated as a sequence of markdown chunks with optional `<COMPLETION_GATE>` blocks rendered as a distinct status card (not raw XML)
+- Links are tappable; `file://` or repo-relative links show a "not supported on mobile" toast rather than erroring
+
+## 8. Data model (client-side SwiftData)
+
+```
+ServerProfile { url, label, apiKeyRef, lastSeenAt }
+Contract { id, name, phase, status, autonomous_loop, updated_at }
+Task { id, contract_id?, parent_id?, status, plan, daemon_id?, updated_at }
+TaskEvent { id, task_id, kind, payload_json, ts } // bounded by task, prune >2k
+Daemon { id, status, last_heartbeat, hostname? }
+Directive { id, title, status, has_pending_question, updated_at }
+ListenSession { id, title?, started_at, ended_at?, transcript_preview }
+```
+
+Everything else (file contents, full transcripts, mesh details) = fetch on demand, don't cache.
+
+## 9. Repo layout — as a folder inside `soryu-co/soryu`
+
+```
+soryu/ # existing monorepo root
+ apps/
+ ios/ # <— NEW
+ project.yml # XcodeGen source
+ Makefile # ios-device-fast, ios-sim-fast, bootstrap, lint
+ Sources/
+ Makima/
+ App/ # MakimaApp, entry, scene
+ Design/ # Palette, Typography, Components (DashedBorder, MastheadBar, JapaneseLongPressText, Logo)
+ Net/ # APIClient, WebSocketClient, AuthStore, ServerProfileStore
+ Stores/ # HomeStore, ContractsStore, TasksStore, DirectivesStore, DaemonsStore (all @Observable)
+ Features/
+ Home/ # HomeView + section cards
+ Contracts/
+ Tasks/ # TasksListView, TaskDetailView (hero), OutputStreamView
+ Directives/
+ Daemons/
+ Listen/ # history only in v1
+ Settings/
+ Onboarding/
+ Models/ # Contract, Task, TaskEvent, Daemon, Directive, ListenSession
+ Markdown/ # Theme, code block, completion gate block
+ Resources/
+ Logo/ # copied from ../../../frontend/public/logo/
+ Assets.xcassets
+ Tests/
+ MakimaTests/ # AuthStore, APIClient (URLProtocol stubs), WebSocketClient reconnect
+ docs/
+ DEVELOPMENT.md
+ RELEASING.md
+ README.md # points back to repo-root LICENSE (MIT)
+ frontend/ # existing
+ makima/ # existing
+ k8s/ # existing
+ LICENSE # repo-root MIT (confirm exists; add if missing)
+ README.md # add "apps/ios — Makima iOS client" line
+```
+
+## 10. Milestones
+
+| M | Scope | Done-when |
+|---|---|---|
+| M0 | Repo scaffold, XcodeGen, CI (GitHub Actions `xcodebuild`), empty app launches | App opens to "Hello, Makima" on sim + device |
+| M1 | Design system: Palette, Typography, DashedBorder, MastheadBar, Logo (Canvas), JapaneseLongPressText | Standalone previews render; aesthetic matches web side-by-side |
+| M2 | Onboarding + ServerProfile + Keychain API key + APIClient + auth probe | Paste key, see 200 from `/mesh/daemons` |
+| M3 | Home composite view (static → wired) | 5 cards render from live endpoints, pull-to-refresh |
+| M4 | Contracts list + detail, Tasks list + detail (polled output first, no WS yet) | Can drill from Home → Contract → Task and see output |
+| M5 | WebSocket livestream + markdown/code rendering + completion gate card | Task detail updates without polling; completion gate shows as status card |
+| M6 | Directives read-only, Daemons read-only, Listen history | All nav items reachable |
+| M7 | Local notifications + deep links | Foregrounded WS fires notifications that deep-link correctly |
+| M8 | Polish pass + TestFlight | Review-ready build, screenshots, privacy copy |
+
+## 11. Decisions locked (2026-04-24)
+
+| # | Decision |
+|---|---|
+| License | **MIT**, carried by repo-root `LICENSE` in `soryu-co/soryu` |
+| App Store seller | **Soryu LTD** |
+| Bundle ID | **`co.soryu.makima`** (no `.ios` suffix) |
+| Supabase OAuth | Deferred to **v1.1** |
+| Logo | **Reuse** existing `frontend/public/logo/makima-logo.svg` (no redraw) |
+| Privacy policy | Point to repo README (KittyLitter-style) |
+| Auth header | `x-makima-api-key: <mk_…>` — **verified** against `src/server/auth.rs` |
+| Push-proxy (v1.1) | `soryu-co/soryu` repo, new folder **`services/push-proxy/`** |
+| Repo location | Folder **`apps/ios/`** inside existing `soryu-co/soryu` monorepo |
+
+All blockers resolved. M0 (scaffold + XcodeGen + CI) is unblocked.
+
+---
+
+## Cross-checks against user preferences
+
+- ✅ Zero-friction auth: single paste, then nothing. Supabase OAuth queued for v1.1.
+- ✅ Customizable API URL: single-profile today, multi-profile-ready internally.
+- ✅ Aesthetic-forward: dedicated M1 before any feature work.
+- ✅ License-clean: concentric-circle logo redrawn in Canvas, no character likeness — matches "symbolic/abstract" preference. No copyrighted assets.
+- ✅ Pure native: zero JS, zero RN, zero Expo, zero Rust-UniFFI.
diff --git a/makima/ios/project.yml b/makima/ios/project.yml
new file mode 100644
index 0000000..5ec3eee
--- /dev/null
+++ b/makima/ios/project.yml
@@ -0,0 +1,84 @@
+name: Makima
+options:
+ bundleIdPrefix: co.soryu
+ deploymentTarget:
+ iOS: "18.0"
+ developmentLanguage: en
+ createIntermediateGroups: true
+ xcodeVersion: "16.0"
+ groupSortPosition: top
+ generateEmptyDirectories: true
+
+settings:
+ base:
+ SWIFT_VERSION: "5.10"
+ IPHONEOS_DEPLOYMENT_TARGET: "18.0"
+ ENABLE_USER_SCRIPT_SANDBOXING: YES
+ CURRENT_PROJECT_VERSION: "1"
+ MARKETING_VERSION: "0.1.0"
+ SWIFT_STRICT_CONCURRENCY: complete
+ CODE_SIGN_STYLE: Automatic
+ DEVELOPMENT_TEAM: ""
+
+targets:
+ Makima:
+ type: application
+ platform: iOS
+ sources:
+ - path: Sources/Makima
+ excludes:
+ - "**/.DS_Store"
+ resources:
+ - path: Sources/Makima/Resources
+ info:
+ path: Sources/Makima/App/Info.plist
+ properties:
+ CFBundleDisplayName: Makima
+ CFBundleShortVersionString: $(MARKETING_VERSION)
+ CFBundleVersion: $(CURRENT_PROJECT_VERSION)
+ LSRequiresIPhoneOS: true
+ UILaunchScreen:
+ UIColorName: BrandBackground
+ UISupportedInterfaceOrientations:
+ - UIInterfaceOrientationPortrait
+ UIUserInterfaceStyle: Dark
+ ITSAppUsesNonExemptEncryption: false
+ NSAppTransportSecurity:
+ NSAllowsArbitraryLoads: false
+ NSAllowsArbitraryLoadsForMedia: false
+ CFBundleURLTypes:
+ - CFBundleURLName: co.soryu.makima
+ CFBundleURLSchemes:
+ - makima
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: co.soryu.makima
+ TARGETED_DEVICE_FAMILY: "1"
+ ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: BrandAccent
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
+
+ MakimaTests:
+ type: bundle.unit-test
+ platform: iOS
+ sources:
+ - path: Tests/MakimaTests
+ dependencies:
+ - target: Makima
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: co.soryu.makima.tests
+
+schemes:
+ Makima:
+ build:
+ targets:
+ Makima: all
+ run:
+ config: Debug
+ test:
+ config: Debug
+ targets:
+ - MakimaTests
+ archive:
+ config: Release