Fix honeycomb grid sizing, alpha, and stroke styling
- Fix hex sizing to use aspectRatio layout instead of GeometryReader - Remove hardcoded height estimation that caused gap between header and grid - Set fill alpha to 0.3 and add 0.7 alpha stroke on colored hexagons - Use 12 rows consistently - Add forceRefresh parameter to getResidence Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -136,7 +136,7 @@ class ResidenceViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func getResidence(id: Int32) {
|
||||
func getResidence(id: Int32, forceRefresh: Bool = false) {
|
||||
if UITestRuntime.shouldMockAuth {
|
||||
selectedResidence = Self.uiTestMockResidences.first(where: { $0.id == id })
|
||||
isLoading = false
|
||||
@@ -149,7 +149,7 @@ class ResidenceViewModel: ObservableObject {
|
||||
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.getResidence(id: id, forceRefresh: false)
|
||||
let result = try await APILayer.shared.getResidence(id: id, forceRefresh: forceRefresh)
|
||||
|
||||
if let success = result as? ApiResultSuccess<ResidenceResponse> {
|
||||
self.selectedResidence = success.data
|
||||
|
||||
@@ -10,9 +10,10 @@ struct HoneycombSummaryView: View {
|
||||
let summary: CompletionSummary
|
||||
let residenceName: String
|
||||
|
||||
private let maxRows = 10
|
||||
private let hexSize: CGFloat = 14
|
||||
private let hexSpacing: CGFloat = 2
|
||||
private let colorAlpha: CGFloat = 0.3
|
||||
|
||||
private let rowCount = 12
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
@@ -59,65 +60,46 @@ struct HoneycombSummaryView: View {
|
||||
// MARK: - Grid
|
||||
|
||||
private var honeycombGrid: some View {
|
||||
GeometryReader { geo in
|
||||
let columns = summary.months.count
|
||||
guard columns > 0 else { return AnyView(EmptyView()) }
|
||||
VStack(spacing: hexSpacing) {
|
||||
// Grid rows (top = row 12, bottom = row 1)
|
||||
ForEach((0..<rowCount).reversed(), id: \.self) { row in
|
||||
HStack(spacing: hexSpacing) {
|
||||
ForEach(Array(summary.months.enumerated()), id: \.offset) { _, month in
|
||||
let hexColors = buildHexColors(for: month)
|
||||
|
||||
let totalWidth = geo.size.width
|
||||
let colWidth = totalWidth / CGFloat(columns)
|
||||
// Recalculate hex size to fit available width
|
||||
let effectiveHexSize = min(hexSize, (colWidth - hexSpacing) / 2)
|
||||
let hexWidth = effectiveHexSize * 2
|
||||
let hexHeight = effectiveHexSize * sqrt(3)
|
||||
let rowHeight = hexHeight + hexSpacing
|
||||
|
||||
return AnyView(
|
||||
VStack(spacing: 0) {
|
||||
// Grid rows (top = row 10, bottom = row 1)
|
||||
ForEach((0..<maxRows).reversed(), id: \.self) { row in
|
||||
HStack(spacing: hexSpacing) {
|
||||
ForEach(Array(summary.months.enumerated()), id: \.offset) { index, month in
|
||||
let hexColors = buildHexColors(for: month)
|
||||
|
||||
if row < hexColors.count {
|
||||
if row < hexColors.count {
|
||||
HexagonShape()
|
||||
.fill(hexColors[row].opacity(colorAlpha))
|
||||
.overlay(
|
||||
HexagonShape()
|
||||
.fill(hexColors[row])
|
||||
.frame(width: hexWidth, height: hexHeight)
|
||||
} else if row < maxRows {
|
||||
// Empty hex
|
||||
HexagonShape()
|
||||
.fill(Color.appTextSecondary.opacity(0.08))
|
||||
.frame(width: hexWidth, height: hexHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: rowHeight)
|
||||
}
|
||||
|
||||
// Overflow indicators
|
||||
HStack(spacing: hexSpacing) {
|
||||
ForEach(Array(summary.months.enumerated()), id: \.offset) { index, month in
|
||||
if month.overflow > 0 {
|
||||
Text("+\(month.overflow)")
|
||||
.font(.system(size: 8, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.frame(width: hexWidth)
|
||||
} else {
|
||||
Color.clear
|
||||
.frame(width: hexWidth, height: 10)
|
||||
}
|
||||
.stroke(hexColors[row].opacity(0.7), lineWidth: 1.5)
|
||||
)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
} else {
|
||||
HexagonShape()
|
||||
.fill(Color.appTextSecondary.opacity(0.08))
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.frame(height: gridHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private var gridHeight: CGFloat {
|
||||
let hexHeight = hexSize * sqrt(3)
|
||||
let rowHeight = hexHeight + hexSpacing
|
||||
return rowHeight * CGFloat(maxRows) + 14 // +14 for overflow text
|
||||
// Overflow indicators
|
||||
HStack(spacing: hexSpacing) {
|
||||
ForEach(Array(summary.months.enumerated()), id: \.offset) { _, month in
|
||||
if month.overflow > 0 {
|
||||
Text("+\(month.overflow)")
|
||||
.font(.system(size: 8, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
Color.clear
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Month Labels
|
||||
@@ -156,11 +138,11 @@ struct HoneycombSummaryView: View {
|
||||
for column in fillOrder {
|
||||
if let entry = columnMap[column] {
|
||||
let color = Color(hex: entry.color) ?? Color.green
|
||||
for _ in 0..<min(Int(entry.count), maxRows - colors.count) {
|
||||
for _ in 0..<min(Int(entry.count), rowCount - colors.count) {
|
||||
colors.append(color)
|
||||
}
|
||||
}
|
||||
if colors.count >= maxRows { break }
|
||||
if colors.count >= rowCount { break }
|
||||
}
|
||||
|
||||
return colors
|
||||
@@ -183,24 +165,28 @@ struct HoneycombSummaryView: View {
|
||||
|
||||
// MARK: - Hexagon Shape
|
||||
|
||||
/// A regular hexagon shape (pointy-top orientation).
|
||||
/// A regular hexagon shape (flat-top orientation).
|
||||
struct HexagonShape: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let w = rect.width
|
||||
let h = rect.height
|
||||
let cx = rect.midX
|
||||
let cy = rect.midY
|
||||
// Use the smaller dimension to keep it regular
|
||||
let r = min(w, h) / 2
|
||||
|
||||
var path = Path()
|
||||
// Pointy-top hexagon
|
||||
path.move(to: CGPoint(x: cx, y: cy - h / 2))
|
||||
path.addLine(to: CGPoint(x: cx + w / 2, y: cy - h / 4))
|
||||
path.addLine(to: CGPoint(x: cx + w / 2, y: cy + h / 4))
|
||||
path.addLine(to: CGPoint(x: cx, y: cy + h / 2))
|
||||
path.addLine(to: CGPoint(x: cx - w / 2, y: cy + h / 4))
|
||||
path.addLine(to: CGPoint(x: cx - w / 2, y: cy - h / 4))
|
||||
for i in 0..<6 {
|
||||
let angle = CGFloat(i) * .pi / 3 - .pi / 6 // flat-top: start at -30°
|
||||
let x = cx + r * cos(angle)
|
||||
let y = cy + r * sin(angle)
|
||||
if i == 0 {
|
||||
path.move(to: CGPoint(x: x, y: y))
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: x, y: y))
|
||||
}
|
||||
}
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user