| 1 | import SwiftUI |
| 2 | |
| 3 | struct SheetPreview: Decodable, Hashable, Sendable { |
| 4 | let Rows: Int |
| 5 | let Cols: Int |
| 6 | let Empty: Bool |
| 7 | let Cells: [SheetPreviewCell]? |
| 8 | } |
| 9 | struct SheetPreviewCell: Decodable, Hashable, Sendable { |
| 10 | let R: Int |
| 11 | let C: Int |
| 12 | let Fill: String |
| 13 | let Bold: Bool |
| 14 | } |
| 15 | |
| 16 | /// Renders shape metadata only: no cell text, web view, or external image fetch. |
| 17 | struct SheetThumbnail: View { |
| 18 | private let preview: SheetPreview? |
| 19 | var width: CGFloat = 52 |
| 20 | var height: CGFloat = 68 |
| 21 | init(_ preview: SheetPreview?, width: CGFloat = 52, height: CGFloat = 68) { self.preview = preview; self.width = width; self.height = height } |
| 22 | |
| 23 | var body: some View { |
| 24 | Canvas { context, size in |
| 25 | let border = Path(roundedRect: CGRect(origin: .zero, size: size).insetBy(dx: 0.5, dy: 0.5), cornerRadius: 3) |
| 26 | context.fill(border, with: .color(.primary.opacity(0.025))) |
| 27 | context.stroke(border, with: .color(.secondary.opacity(0.3)), lineWidth: 1) |
| 28 | guard let preview, !preview.Empty, (1...12).contains(preview.Rows), |
| 29 | (1...8).contains(preview.Cols) else { return } |
| 30 | let width = (size.width - 8) / Double(preview.Cols) |
| 31 | let height = (size.height - 8) / Double(preview.Rows) |
| 32 | for cell in (preview.Cells ?? []).prefix(96) { |
| 33 | guard (0..<preview.Rows).contains(cell.R), (0..<preview.Cols).contains(cell.C) else { continue } |
| 34 | let rect = CGRect(x: 4 + Double(cell.C) * width, y: 4 + Double(cell.R) * height, width: width, height: height) |
| 35 | if !cell.Fill.isEmpty { |
| 36 | let fill = cell.Fill |
| 37 | let isHex = fill.count == 7 && fill.hasPrefix("#") && UInt64(fill.dropFirst(), radix: 16) != nil |
| 38 | context.fill(Path(rect), with: .color(isHex ? Color(hex: fill) : .accentColor.opacity(0.15))) |
| 39 | } |
| 40 | let mark = CGRect(x: rect.minX + width * 0.15, y: rect.minY + height * 0.35, |
| 41 | width: width * 0.7, height: height * 0.3) |
| 42 | context.fill(Path(roundedRect: mark, cornerRadius: 0.5), |
| 43 | with: .color(.secondary.opacity(cell.Bold ? 0.8 : 0.4))) |
| 44 | } |
| 45 | } |
| 46 | .frame(width: width, height: height) |
| 47 | .accessibilityHidden(true) |
| 48 | } |
| 49 | } |
| 50 | |