From db092c79a175e3283f479ee0b234b24bde3c736e Mon Sep 17 00:00:00 2001 From: soryu-co Date: Fri, 24 Apr 2026 13:15:29 +0000 Subject: Add Makima iOS app scaffold (M0 + M1 design system) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- makima/ios/.gitignore | 7 + makima/ios/Makefile | 54 +++++ makima/ios/README.md | 31 +++ makima/ios/Sources/Makima/App/Info.plist | 18 ++ makima/ios/Sources/Makima/App/MakimaApp.swift | 12 ++ makima/ios/Sources/Makima/App/RootView.swift | 125 ++++++++++++ .../Sources/Makima/Design/Components/Badge.swift | 28 +++ .../Makima/Design/Components/DashedBorder.swift | 24 +++ .../Makima/Design/Components/GridOverlay.swift | 29 +++ .../Design/Components/JapaneseLongPressText.swift | 37 ++++ .../Sources/Makima/Design/Components/Logo.swift | 51 +++++ .../Makima/Design/Components/MastheadBar.swift | 100 ++++++++++ makima/ios/Sources/Makima/Design/Palette.swift | 41 ++++ makima/ios/Sources/Makima/Design/Typography.swift | 16 ++ .../AppIcon.appiconset/Contents.json | 13 ++ .../BrandAccent.colorset/Contents.json | 20 ++ .../BrandBackground.colorset/Contents.json | 20 ++ .../Makima/Resources/Assets.xcassets/Contents.json | 6 + .../Makima/Resources/Logo/makima-icon-red.svg | 7 + .../Sources/Makima/Resources/Logo/makima-logo.svg | 7 + makima/ios/Tests/MakimaTests/SmokeTests.swift | 18 ++ makima/ios/docs/DEVELOPMENT.md | 61 ++++++ makima/ios/docs/RELEASING.md | 6 + makima/ios/docs/ios-v1-plan.md | 222 +++++++++++++++++++++ makima/ios/project.yml | 84 ++++++++ 25 files changed, 1037 insertions(+) create mode 100644 makima/ios/.gitignore create mode 100644 makima/ios/Makefile create mode 100644 makima/ios/README.md create mode 100644 makima/ios/Sources/Makima/App/Info.plist create mode 100644 makima/ios/Sources/Makima/App/MakimaApp.swift create mode 100644 makima/ios/Sources/Makima/App/RootView.swift create mode 100644 makima/ios/Sources/Makima/Design/Components/Badge.swift create mode 100644 makima/ios/Sources/Makima/Design/Components/DashedBorder.swift create mode 100644 makima/ios/Sources/Makima/Design/Components/GridOverlay.swift create mode 100644 makima/ios/Sources/Makima/Design/Components/JapaneseLongPressText.swift create mode 100644 makima/ios/Sources/Makima/Design/Components/Logo.swift create mode 100644 makima/ios/Sources/Makima/Design/Components/MastheadBar.swift create mode 100644 makima/ios/Sources/Makima/Design/Palette.swift create mode 100644 makima/ios/Sources/Makima/Design/Typography.swift create mode 100644 makima/ios/Sources/Makima/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandAccent.colorset/Contents.json create mode 100644 makima/ios/Sources/Makima/Resources/Assets.xcassets/BrandBackground.colorset/Contents.json create mode 100644 makima/ios/Sources/Makima/Resources/Assets.xcassets/Contents.json create mode 100644 makima/ios/Sources/Makima/Resources/Logo/makima-icon-red.svg create mode 100644 makima/ios/Sources/Makima/Resources/Logo/makima-logo.svg create mode 100644 makima/ios/Tests/MakimaTests/SmokeTests.swift create mode 100644 makima/ios/docs/DEVELOPMENT.md create mode 100644 makima/ios/docs/RELEASING.md create mode 100644 makima/ios/docs/ios-v1-plan.md create mode 100644 makima/ios/project.yml 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 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + + 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.. 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 @@ + + 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 @@ + + + + + + + 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://: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 `/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: `** (constant `API_KEY_HEADER` line 404). Keys are prefixed `mk_`. +- **Supabase JWTs → header `Authorization: Bearer `** (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:///api/v1/mesh/tasks/subscribe` +Headers on the upgrade request: `x-makima-api-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 `` 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: ` — **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 -- cgit v1.2.3