Skip to content

Commit 80a0674

Browse files
feat(ios): add Icon and List element rendering for v4.1.0
1 parent 3c88350 commit 80a0674

2 files changed

Lines changed: 120 additions & 0 deletions

File tree

apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/AdaptiveCardModels.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ enum CardElement: Codable {
2020
case codeBlock(CodeBlock)
2121
case imageSet(ImageSet)
2222
case actionSet(ActionSet)
23+
case icon(ACIcon)
24+
case list(ACList)
2325
case unknown
2426

2527
struct TextBlock: Codable {
@@ -107,6 +109,11 @@ enum CardElement: Codable {
107109
let actions: [CardAction]
108110
}
109111

112+
struct ACListItem: Codable {
113+
let text: String
114+
let icon: ACIcon?
115+
}
116+
110117
// Manual decoding keyed on "type"
111118
private enum CodingKeys: String, CodingKey {
112119
case type
@@ -137,6 +144,10 @@ enum CardElement: Codable {
137144
self = .imageSet(try ImageSet(from: decoder))
138145
case "ActionSet":
139146
self = .actionSet(try ActionSet(from: decoder))
147+
case "Icon":
148+
self = .icon(try ACIcon(from: decoder))
149+
case "List":
150+
self = .list(try ACList(from: decoder))
140151
default:
141152
self = .unknown
142153
}
@@ -175,12 +186,34 @@ enum CardElement: Codable {
175186
case .actionSet(let actSet):
176187
try container.encode("ActionSet", forKey: .type)
177188
try actSet.encode(to: encoder)
189+
case .icon(let ico):
190+
try container.encode("Icon", forKey: .type)
191+
try ico.encode(to: encoder)
192+
case .list(let lst):
193+
try container.encode("List", forKey: .type)
194+
try lst.encode(to: encoder)
178195
case .unknown:
179196
try container.encode("Unknown", forKey: .type)
180197
}
181198
}
182199
}
183200

201+
// MARK: - Icon element (v4.1.0)
202+
203+
struct ACIcon: Codable {
204+
let name: String
205+
let size: String?
206+
let color: String?
207+
let style: String? // "regular" or "filled"
208+
}
209+
210+
// MARK: - List element (v4.1.0)
211+
212+
struct ACList: Codable {
213+
let items: [CardElement.ACListItem]
214+
let style: String? // "ordered" or "unordered"
215+
}
216+
184217
enum CardAction: Codable {
185218
case submit(SubmitAction)
186219
case execute(ExecuteAction)

apps/shared/OpenClawKit/Sources/OpenClawChatUI/AdaptiveCard/AdaptiveCardView.swift

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ struct AdaptiveCardView: View {
4949
self.renderImageSet(imgSet)
5050
case .actionSet(let actSet):
5151
self.renderActions(actSet.actions)
52+
case .icon(let ico):
53+
self.renderIcon(ico)
54+
case .list(let lst):
55+
self.renderList(lst)
5256
case .unknown:
5357
EmptyView()
5458
}
@@ -239,6 +243,89 @@ struct AdaptiveCardView: View {
239243
}
240244
}
241245

246+
// MARK: - Icon rendering
247+
248+
@ViewBuilder
249+
private func renderIcon(_ icon: ACIcon) -> some View {
250+
self.iconView(icon)
251+
}
252+
253+
/// Builds a view for an ACIcon: SF Symbol when the name matches, otherwise a text label.
254+
@ViewBuilder
255+
private func iconView(_ icon: ACIcon) -> some View {
256+
let iconSize = self.iconFontSize(icon.size)
257+
let iconColor = self.iconColor(icon.color)
258+
259+
// Attempt SF Symbol lookup; fall back to a text label
260+
if self.sfSymbolExists(icon.name) {
261+
Image(systemName: icon.name)
262+
.font(.system(size: iconSize))
263+
.foregroundStyle(iconColor)
264+
} else {
265+
Text(icon.name)
266+
.font(.system(size: iconSize))
267+
.foregroundStyle(iconColor)
268+
}
269+
}
270+
271+
/// Map Adaptive Card icon size tokens to point sizes.
272+
private func iconFontSize(_ size: String?) -> CGFloat {
273+
switch size?.lowercased() {
274+
case "xxs": return 10
275+
case "xs": return 12
276+
case "sm", "small": return 14
277+
case "md", "medium": return 18
278+
case "lg", "large": return 24
279+
case "xl": return 30
280+
case "xxl": return 38
281+
default: return 16
282+
}
283+
}
284+
285+
/// Resolve Adaptive Card color tokens to SwiftUI colors.
286+
private func iconColor(_ color: String?) -> Color {
287+
switch color?.lowercased() {
288+
case "accent": return .accentColor
289+
case "good": return .green
290+
case "warning": return .orange
291+
case "attention": return .red
292+
case "light": return .secondary
293+
case "dark": return .primary
294+
default: return .primary
295+
}
296+
}
297+
298+
/// Check whether an SF Symbol name is valid at runtime.
299+
private func sfSymbolExists(_ name: String) -> Bool {
300+
#if canImport(UIKit)
301+
return UIImage(systemName: name) != nil
302+
#elseif canImport(AppKit)
303+
return NSImage(systemSymbolName: name, accessibilityDescription: nil) != nil
304+
#else
305+
return false
306+
#endif
307+
}
308+
309+
// MARK: - List rendering
310+
311+
@ViewBuilder
312+
private func renderList(_ list: ACList) -> some View {
313+
let ordered = list.style?.lowercased() == "ordered"
314+
VStack(alignment: .leading, spacing: 4) {
315+
ForEach(list.items.indices, id: \.self) { idx in
316+
HStack(alignment: .top, spacing: 4) {
317+
if let icon = list.items[idx].icon {
318+
self.iconView(icon)
319+
}
320+
let prefix = ordered ? "\(idx + 1)." : "\u{2022}"
321+
Text("\(prefix) \(list.items[idx].text)")
322+
.font(.subheadline)
323+
.fixedSize(horizontal: false, vertical: true)
324+
}
325+
}
326+
}
327+
}
328+
242329
private func imageMaxHeight(_ size: String?) -> CGFloat {
243330
switch size?.lowercased() {
244331
case "small": return 60

0 commit comments

Comments
 (0)