import SwiftUI import SwiftData struct CategoryView: View { @Environment(\.modelContext) private var modelContext @State private var viewModel = CategoryViewModel() @State private var showAddSheet = false var body: some View { ZStack { Theme.background.ignoresSafeArea() Group { if viewModel.categories.isEmpty { emptyState } else { categoryList } } } .navigationTitle(String(localized: "tab.category")) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { showAddSheet = true } label: { Image(systemName: "plus") .foregroundStyle(Theme.green) } } } .sheet(isPresented: $showAddSheet) { AddCategorySheet { name, colorHex in viewModel.addCategory(name: name, colorHex: colorHex) } } .onAppear { viewModel.setup(context: modelContext) } } private var emptyState: some View { VStack(spacing: 16) { Image(systemName: "folder.badge.plus") .font(.system(size: 56)) .foregroundStyle(Theme.green.opacity(0.6)) Text(String(localized: "category.empty")) .foregroundStyle(Theme.primaryText.opacity(0.6)) } } private var categoryList: some View { List { ForEach(viewModel.categories) { category in CategoryRow(category: category, taskCount: viewModel.taskCount(for: category)) .listRowBackground(Theme.surface) .swipeActions(edge: .trailing, allowsFullSwipe: false) { Button(role: .destructive) { viewModel.deleteCategory(category) } label: { Label(String(localized: "category.delete"), systemImage: "trash") } } } } .listStyle(.plain) .scrollContentBackground(.hidden) } } // MARK: - Category Row private struct CategoryRow: View { let category: Category let taskCount: Int var body: some View { HStack(spacing: 12) { Circle() .fill(Color(hex: category.colorHex)) .frame(width: 14, height: 14) Text(category.name) .font(.body) .fontWeight(.medium) .foregroundStyle(Theme.primaryText) Spacer() Text("\(taskCount) \(String(localized: "category.task.count"))") .font(.caption) .foregroundStyle(Theme.primaryText.opacity(0.5)) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Theme.green.opacity(0.12), in: Capsule()) } .padding(.vertical, 6) } } // MARK: - Add Category Sheet private struct AddCategorySheet: View { @Environment(\.dismiss) private var dismiss @State private var name = "" @State private var selectedColor = CategoryViewModel.presetColors[0] let onAdd: (String, String) -> Void var body: some View { NavigationStack { Form { Section { TextField(String(localized: "category.name"), text: $name) } Section(String(localized: "category.color")) { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 16) { ForEach(CategoryViewModel.presetColors, id: \.self) { colorHex in Circle() .fill(Color(hex: colorHex)) .frame(width: 42, height: 42) .overlay(alignment: .center) { if selectedColor == colorHex { Image(systemName: "checkmark") .font(.caption) .fontWeight(.bold) .foregroundStyle(.white) } } .overlay( Circle() .strokeBorder( selectedColor == colorHex ? Theme.primaryText.opacity(0.4) : Color.clear, lineWidth: 2 ) ) .onTapGesture { selectedColor = colorHex } } } .padding(.vertical, 8) } } .navigationTitle(String(localized: "category.add")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(String(localized: "button.cancel")) { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(String(localized: "button.add")) { let trimmed = name.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty else { return } onAdd(trimmed, selectedColor) dismiss() } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) } } } } } // MARK: - Color Hex Extension extension Color { init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 Scanner(string: hex).scanHexInt64(&int) let r = Double((int >> 16) & 0xFF) / 255 let g = Double((int >> 8) & 0xFF) / 255 let b = Double(int & 0xFF) / 255 self.init(red: r, green: g, blue: b) } } #Preview { NavigationStack { CategoryView() } }