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:
Trey t
2026-03-14 00:44:41 -05:00
parent 7689027bdd
commit cf2e6d8bcc
2 changed files with 54 additions and 68 deletions

View File

@@ -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

View File

@@ -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
}
}