// // ChipSelectionView.swift // Reflect // // Reusable chip selection grid for guided reflection quick-pick answers. // import SwiftUI struct ChipSelectionView: View { let chips: QuestionChips let accentColor: Color @Binding var selectedChips: [String] @Binding var textAnswer: String @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 { let isSelected = selectedChips.contains(label) return Button { toggleChip(label) } label: { Text(label) .font(.subheadline) .padding(.horizontal, 14) .padding(.vertical, 8) .background( Capsule() .fill(isSelected ? accentColor.opacity(0.2) : Color(.systemGray6)) ) .overlay( Capsule() .stroke(isSelected ? accentColor : Color(.systemGray4), lineWidth: 1) ) .foregroundColor(isSelected ? accentColor : .primary) } .accessibilityIdentifier(AccessibilityID.GuidedReflection.chip(label: label)) } // MARK: - Chip Toggle private func toggleChip(_ label: String) { if let index = selectedChips.firstIndex(of: label) { selectedChips.remove(at: index) // Remove from text answer let separator = ", " var parts = textAnswer .components(separatedBy: separator) .map { $0.trimmingCharacters(in: .whitespaces) } .filter { !$0.isEmpty } parts.removeAll { $0 == label } textAnswer = parts.joined(separator: separator) } else { selectedChips.append(label) // Append to text answer if textAnswer.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { textAnswer = label } else { textAnswer += ", \(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) } }