eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import SwiftUI
2
3// 🤖 CardView — the native renderer for the card schema (Card.swift; web twin
4// web/static/cards.js renderCard). Shared leaf view, per-platform container:
5// MessageBubble mounts it wherever a message carries a valid card, on iOS and
6// Mac alike. Anything that reaches here already normalized — render never
7// validates, so it can never disagree with the contract.
8
9public struct CardView: View {
10 public let card: Card
11
12 public init(card: Card) { self.card = card }
13
14 public var body: some View {
15 VStack(alignment: .leading, spacing: 8) {
16 ForEach(Array(card.blocks.enumerated()), id: \.offset) { _, block in
17 CardBlockView(block: block)
18 }
19 }
20 .padding(EdgeInsets(top: 12, leading: 14, bottom: 12, trailing: 14))
21 .frame(maxWidth: 480, alignment: .leading)
22 .background(
23 RoundedRectangle(cornerRadius: 12, style: .continuous)
24 .fill(Color.gray.opacity(0.08))
25 )
26 .overlay(
27 RoundedRectangle(cornerRadius: 12, style: .continuous)
28 .strokeBorder(Color.gray.opacity(0.25), lineWidth: 1)
29 )
30 }
31}
32
33struct CardBlockView: View {
34 let block: CardBlock
35
36 var body: some View {
37 switch block {
38 case .heading(let text, let sub):
39 VStack(alignment: .leading, spacing: 2) {
40 Text(text).font(.system(size: 16, weight: .bold))
41 if let sub {
42 Text(sub).font(.system(size: 12)).foregroundStyle(.secondary)
43 }
44 }
45 case .text(let text):
46 Text(text).font(.system(size: 14))
47 case .item(let text, let source, let url):
48 HStack(alignment: .firstTextBaseline, spacing: 8) {
49 if let url, let dest = URL(string: url) {
50 Link(text, destination: dest)
51 .font(.system(size: 14))
52 .frame(maxWidth: .infinity, alignment: .leading)
53 } else {
54 Text(text)
55 .font(.system(size: 14))
56 .frame(maxWidth: .infinity, alignment: .leading)
57 }
58 if let source {
59 Text(source.uppercased())
60 .font(.system(size: 10, weight: .medium))
61 .kerning(0.4)
62 .foregroundStyle(.secondary)
63 }
64 }
65 case .list(let items):
66 VStack(alignment: .leading, spacing: 4) {
67 ForEach(Array(items.enumerated()), id: \.offset) { _, item in
68 HStack(alignment: .firstTextBaseline, spacing: 6) {
69 Text("•").foregroundStyle(.secondary)
70 Text(item).font(.system(size: 14))
71 }
72 }
73 }
74 case .kv(let rows):
75 Grid(alignment: .leading, horizontalSpacing: 14, verticalSpacing: 2) {
76 ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
77 GridRow {
78 Text(row.key).font(.system(size: 13)).foregroundStyle(.secondary)
79 Text(row.value).font(.system(size: 13)).monospacedDigit()
80 }
81 }
82 }
83 case .divider:
84 Divider()
85 case .section(let title, let blocks):
86 VStack(alignment: .leading, spacing: 6) {
87 if let title {
88 Text(title.uppercased())
89 .font(.system(size: 11, weight: .semibold))
90 .kerning(0.5)
91 .foregroundStyle(.secondary)
92 }
93 ForEach(Array(blocks.enumerated()), id: \.offset) { _, inner in
94 CardBlockView(block: inner)
95 }
96 }
97 case .chips(let chips):
98 // Chips wrap like the web's flex row; a simple HStack suffices for
99 // the ≤4 chips the schema allows.
100 HStack(spacing: 8) {
101 ForEach(Array(chips.enumerated()), id: \.offset) { _, chip in
102 if let dest = URL(string: chip.url) {
103 Link(chip.label, destination: dest)
104 .font(.system(size: 13))
105 .padding(.horizontal, 12)
106 .padding(.vertical, 6)
107 .background(Capsule().strokeBorder(Color.gray.opacity(0.35), lineWidth: 1))
108 }
109 }
110 }
111 .padding(.top, 2)
112 }
113 }
114}
115