// // ChipSelectionView.swift // Reflect // // Reusable chip grid for guided reflection — tapping a chip appends its text. // import SwiftUI struct ChipSelectionView: View { let chips: QuestionChips let accentColor: Color @Binding var textAnswer: String var onInsert: ((String) -> Void)? = nil @State private var showMore = false var body: some View { VStack(alignment: .leading, spacing: 10) { chipFlow(chips: chips.topRow) if chips.hasExpanded { if showMore { chipFlow(chips: chips.expanded) .transition(.opacity.combined(with: .move(edge: .top))) } Button { withAnimation(.easeInOut(duration: 0.25)) { showMore.toggle() } } label: { HStack(spacing: 4) { Text(showMore ? String(localized: "guided_chip_show_less") : String(localized: "guided_chip_show_more")) .font(.caption) .fontWeight(.medium) Image(systemName: showMore ? "chevron.up" : "chevron.down") .font(.caption2) } .foregroundStyle(accentColor) } .accessibilityIdentifier(AccessibilityID.GuidedReflection.chipMoreButton) } } .accessibilityIdentifier(AccessibilityID.GuidedReflection.chipGrid) } // MARK: - Chip Flow Layout private func chipFlow(chips: [String]) -> some View { FlowLayout(spacing: 8) { ForEach(chips, id: \.self) { chip in chipButton(chip) } } } private func chipButton(_ label: String) -> some View { Button { appendChip(label) } label: { Text(label) .font(.subheadline) .padding(.horizontal, 14) .padding(.vertical, 8) .background( Capsule() .fill(Color(.systemGray6)) ) .overlay( Capsule() .stroke(Color(.systemGray4), lineWidth: 1) ) .foregroundColor(.primary) } .accessibilityIdentifier(AccessibilityID.GuidedReflection.chip(label: label)) } // MARK: - Append Chip Text private func appendChip(_ label: String) { let trimmed = textAnswer.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { textAnswer = label } else { textAnswer = trimmed + "\n" + label } onInsert?(label) } } // MARK: - Flow Layout /// A simple wrapping horizontal layout for chips. struct FlowLayout: Layout { var spacing: CGFloat = 8 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let result = arrange(proposal: proposal, subviews: subviews) return result.size } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let result = arrange(proposal: proposal, subviews: subviews) for (index, subview) in subviews.enumerated() { let point = result.origins[index] subview.place(at: CGPoint(x: bounds.minX + point.x, y: bounds.minY + point.y), proposal: .unspecified) } } private struct ArrangeResult { var size: CGSize var origins: [CGPoint] } private func arrange(proposal: ProposedViewSize, subviews: Subviews) -> ArrangeResult { let maxWidth = proposal.width ?? .infinity var origins: [CGPoint] = [] var x: CGFloat = 0 var y: CGFloat = 0 var rowHeight: CGFloat = 0 for subview in subviews { let size = subview.sizeThatFits(.unspecified) if x + size.width > maxWidth && x > 0 { x = 0 y += rowHeight + spacing rowHeight = 0 } origins.append(CGPoint(x: x, y: y)) rowHeight = max(rowHeight, size.height) x += size.width + spacing } let totalHeight = y + rowHeight return ArrangeResult(size: CGSize(width: maxWidth, height: totalHeight), origins: origins) } }