summaryrefslogtreecommitdiff
path: root/makima/ios/Sources/Makima/Design/Components
diff options
context:
space:
mode:
Diffstat (limited to 'makima/ios/Sources/Makima/Design/Components')
-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
6 files changed, 269 insertions, 0 deletions
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)
+ }
+ }
+}