import SwiftUI enum SybilAttachmentTone { case composer case user case assistant } struct SybilAttachmentListView: View { var attachments: [ChatAttachment] var tone: SybilAttachmentTone var onRemove: ((String) -> Void)? = nil var body: some View { VStack(alignment: .leading, spacing: 8) { ForEach(attachments) { attachment in Group { if attachment.kind == .image { imageCard(attachment) } else { textCard(attachment) } } } } } @ViewBuilder private func imageCard(_ attachment: ChatAttachment) -> some View { VStack(alignment: .leading, spacing: 0) { if let image = SybilChatAttachmentSupport.image(for: attachment) { Image(uiImage: image) .resizable() .scaledToFill() .frame(maxWidth: .infinity) .frame(height: 180) .clipped() } else { ZStack { RoundedRectangle(cornerRadius: 14) .fill(Color.black.opacity(0.18)) Image(systemName: "photo") .font(.system(size: 22, weight: .medium)) .foregroundStyle(SybilTheme.textMuted) } .frame(height: 140) } HStack(alignment: .top, spacing: 10) { Image(systemName: "photo") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(titleColor.opacity(0.92)) .frame(width: 26, height: 26) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.white.opacity(0.06)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.white.opacity(0.08), lineWidth: 1) ) ) VStack(alignment: .leading, spacing: 2) { Text(attachment.filename) .font(.sybil(.footnote, weight: .medium)) .foregroundStyle(titleColor) .lineLimit(1) Text(attachment.mimeType) .font(.sybil(.caption2)) .foregroundStyle(SybilTheme.textMuted) .lineLimit(1) } Spacer(minLength: 0) if let onRemove { removeButton(for: attachment.id, onRemove: onRemove) } } .padding(12) } .background(cardBackground) .clipShape(RoundedRectangle(cornerRadius: 14)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(cardBorder, lineWidth: 1) ) } @ViewBuilder private func textCard(_ attachment: ChatAttachment) -> some View { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .top, spacing: 10) { Image(systemName: "doc.text") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(titleColor.opacity(0.92)) .frame(width: 26, height: 26) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.white.opacity(0.06)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.white.opacity(0.08), lineWidth: 1) ) ) VStack(alignment: .leading, spacing: 2) { Text(attachment.filename) .font(.sybil(.footnote, weight: .medium)) .foregroundStyle(titleColor) .lineLimit(1) Text( attachment.truncated == true ? "\(attachment.mimeType) • truncated" : attachment.mimeType ) .font(.sybil(.caption2)) .foregroundStyle(SybilTheme.textMuted) .lineLimit(1) } Spacer(minLength: 0) if let onRemove { removeButton(for: attachment.id, onRemove: onRemove) } } Text(SybilChatAttachmentSupport.previewText(for: attachment)) .font(.system(.caption, design: .monospaced)) .foregroundStyle(bodyColor) .frame(maxWidth: .infinity, alignment: .leading) .padding(10) .background( RoundedRectangle(cornerRadius: 10) .fill(Color.black.opacity(0.16)) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.white.opacity(0.05), lineWidth: 1) ) ) } .padding(12) .background(cardBackground) .clipShape(RoundedRectangle(cornerRadius: 14)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(cardBorder, lineWidth: 1) ) } private func removeButton(for attachmentID: String, onRemove: @escaping (String) -> Void) -> some View { Button { onRemove(attachmentID) } label: { Image(systemName: "xmark") .font(.system(size: 11, weight: .bold)) .foregroundStyle(SybilTheme.textMuted) .frame(width: 24, height: 24) .background( Circle() .fill(Color.white.opacity(0.06)) ) } .buttonStyle(.plain) .accessibilityLabel("Remove attachment") } private var cardBackground: some ShapeStyle { switch tone { case .composer: return AnyShapeStyle( LinearGradient( colors: [SybilTheme.surface.opacity(0.86), SybilTheme.surfaceStrong.opacity(0.78)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) case .user: return AnyShapeStyle(Color.black.opacity(0.14)) case .assistant: return AnyShapeStyle( LinearGradient( colors: [SybilTheme.surface.opacity(0.58), SybilTheme.surfaceStrong.opacity(0.42)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } } private var cardBorder: Color { switch tone { case .composer: return SybilTheme.border.opacity(0.82) case .user: return Color.white.opacity(0.12) case .assistant: return SybilTheme.border.opacity(0.58) } } private var titleColor: Color { switch tone { case .composer, .assistant: return SybilTheme.text case .user: return SybilTheme.text } } private var bodyColor: Color { switch tone { case .composer, .assistant: return SybilTheme.text.opacity(0.94) case .user: return SybilTheme.text.opacity(0.96) } } }