diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b4211b9..a541074 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -25,6 +25,9 @@ }, "%lld min" : { + }, + "%lld minutes" : { + }, "%lld." : { @@ -44,11 +47,14 @@ }, "Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly." : { + }, + "Add" : { + }, "Add a new link to your collection" : { }, - "Add new label" : { + "Add new tag..." : { }, "all" : { @@ -61,6 +67,9 @@ } } } + }, + "All available tags" : { + }, "Archive" : { @@ -73,9 +82,18 @@ }, "Are you sure you want to log out? This will delete all your login credentials and return you to setup." : { + }, + "Automatic sync" : { + + }, + "Automatically mark articles as read" : { + }, "Cancel" : { + }, + "Clear cache" : { + }, "Clipboard" : { @@ -85,6 +103,9 @@ }, "Current labels" : { + }, + "Data Management" : { + }, "Delete" : { @@ -194,10 +215,10 @@ "No bookmarks found in %@." : { }, - "No labels available" : { + "OK" : { }, - "OK" : { + "Open external links in in-app Safari" : { }, "Optional: Custom title" : { @@ -239,18 +260,27 @@ } } } + }, + "Reading Settings" : { + }, "Remove" : { }, "Required" : { + }, + "Reset settings" : { + }, "Restore" : { }, "Resume listening" : { + }, + "Safari Reader Mode" : { + }, "Save bookmark" : { @@ -263,6 +293,9 @@ }, "Select a bookmark or tag" : { + }, + "Select labels" : { + }, "Server Endpoint" : { @@ -284,6 +317,12 @@ }, "Suche..." : { + }, + "Sync interval" : { + + }, + "Sync Settings" : { + }, "Theme" : { diff --git a/URLShare/ShareBookmarkView.swift b/URLShare/ShareBookmarkView.swift index c20b7e5..61d07f5 100644 --- a/URLShare/ShareBookmarkView.swift +++ b/URLShare/ShareBookmarkView.swift @@ -49,11 +49,53 @@ struct ShareBookmarkView: View { // Label Grid if !viewModel.labels.isEmpty { - LabelGridView(labels: viewModel.labels, selected: $viewModel.selectedLabels) + VStack(alignment: .leading, spacing: 8) { + Text("Select labels") + .font(.headline) + .padding(.horizontal) + + let pageSize = Constants.Labels.pageSize + let pages = stride(from: 0, to: viewModel.labels.count, by: pageSize).map { + Array(viewModel.labels[$0.. - private let columns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())] + @Binding var selectedLabels: Set + @State private var manualTag: String = "" + @State private var error: String? = nil + var body: some View { - LazyVGrid(columns: columns, spacing: 12) { - ForEach(labels.prefix(15), id: \ .name) { label in - Button(action: { - if selected.contains(label.name) { - selected.remove(label.name) - } else { - selected.insert(label.name) - } - }) { - Text(label.name) - .font(.system(size: 14, weight: .medium)) - .padding(.vertical, 10) - .frame(maxWidth: .infinity) - .background(selected.contains(label.name) ? Color.accentColor.opacity(0.2) : Color(.secondarySystemGroupedBackground)) - .foregroundColor(.primary) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(selected.contains(label.name) ? Color.accentColor : Color.clear, lineWidth: 1) - ) + VStack(alignment: .leading, spacing: 4) { + HStack { + TextField("Add new tag...", text: $manualTag) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .disableAutocorrection(true) + Button(action: addTag) { + Text("Add") + .font(.system(size: 15, weight: .semibold)) } + .disabled(manualTag.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + if let error = error { + Text(error) + .font(.caption) + .foregroundColor(.red) } } } + + private func addTag() { + let trimmed = manualTag.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + let lowercased = trimmed.lowercased() + let allExisting = Set(labels.map { $0.name.lowercased() }) + let allSelected = Set(selectedLabels.map { $0.lowercased() }) + if allExisting.contains(lowercased) || allSelected.contains(lowercased) { + error = "Tag already exists." + } else { + selectedLabels.insert(trimmed) + manualTag = "" + error = nil + } + } } diff --git a/URLShare/ShareBookmarkViewModel.swift b/URLShare/ShareBookmarkViewModel.swift index b01f36f..a03a01b 100644 --- a/URLShare/ShareBookmarkViewModel.swift +++ b/URLShare/ShareBookmarkViewModel.swift @@ -52,7 +52,7 @@ class ShareBookmarkViewModel: ObservableObject { let loaded = await SimpleAPI.getBookmarkLabels { [weak self] message, error in self?.statusMessage = (message, error, error ? "❌" : "✅") } ?? [] - let sorted = loaded.prefix(10).sorted { $0.count > $1.count } + let sorted = loaded.sorted { $0.count > $1.count } await MainActor.run { self.labels = Array(sorted) } diff --git a/readeck.xcodeproj/project.pbxproj b/readeck.xcodeproj/project.pbxproj index 9bb6f9c..60c7fac 100644 --- a/readeck.xcodeproj/project.pbxproj +++ b/readeck.xcodeproj/project.pbxproj @@ -86,6 +86,9 @@ Data/KeychainHelper.swift, Domain/Model/Bookmark.swift, readeck.xcdatamodeld, + Splash.storyboard, + UI/Components/Constants.swift, + UI/Components/UnifiedLabelChip.swift, ); target = 5D2B7FAE2DFA27A400EBDB2B /* URLShare */; }; @@ -447,6 +450,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -476,6 +483,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/readeck/Assets.xcassets/green2.colorset/Contents.json b/readeck/Assets.xcassets/green2.colorset/Contents.json index 8b258f9..7110ee9 100644 --- a/readeck/Assets.xcassets/green2.colorset/Contents.json +++ b/readeck/Assets.xcassets/green2.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x5A", - "green" : "0x4A", - "red" : "0x1F" + "blue" : "0x4B", + "green" : "0x41", + "red" : "0x20" } }, "idiom" : "universal" diff --git a/readeck/Assets.xcassets/logo.imageset/Contents.json b/readeck/Assets.xcassets/logo.imageset/Contents.json new file mode 100644 index 0000000..3927227 --- /dev/null +++ b/readeck/Assets.xcassets/logo.imageset/Contents.json @@ -0,0 +1,59 @@ +{ + "images" : [ + { + "filename" : "logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo 5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "logo 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo 4.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "logo 2.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo 3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/readeck/Assets.xcassets/logo.imageset/logo 1.png b/readeck/Assets.xcassets/logo.imageset/logo 1.png new file mode 100644 index 0000000..dcff68b Binary files /dev/null and b/readeck/Assets.xcassets/logo.imageset/logo 1.png differ diff --git a/readeck/Assets.xcassets/logo.imageset/logo 2.png b/readeck/Assets.xcassets/logo.imageset/logo 2.png new file mode 100644 index 0000000..dcff68b Binary files /dev/null and b/readeck/Assets.xcassets/logo.imageset/logo 2.png differ diff --git a/readeck/Assets.xcassets/logo.imageset/logo 3.png b/readeck/Assets.xcassets/logo.imageset/logo 3.png new file mode 100644 index 0000000..dcff68b Binary files /dev/null and b/readeck/Assets.xcassets/logo.imageset/logo 3.png differ diff --git a/readeck/Assets.xcassets/logo.imageset/logo 4.png b/readeck/Assets.xcassets/logo.imageset/logo 4.png new file mode 100644 index 0000000..dcff68b Binary files /dev/null and b/readeck/Assets.xcassets/logo.imageset/logo 4.png differ diff --git a/readeck/Assets.xcassets/logo.imageset/logo 5.png b/readeck/Assets.xcassets/logo.imageset/logo 5.png new file mode 100644 index 0000000..dcff68b Binary files /dev/null and b/readeck/Assets.xcassets/logo.imageset/logo 5.png differ diff --git a/readeck/Assets.xcassets/logo.imageset/logo.png b/readeck/Assets.xcassets/logo.imageset/logo.png new file mode 100644 index 0000000..dcff68b Binary files /dev/null and b/readeck/Assets.xcassets/logo.imageset/logo.png differ diff --git a/readeck/Assets.xcassets/splash.imageset/Contents.json b/readeck/Assets.xcassets/splash.imageset/Contents.json new file mode 100644 index 0000000..4adc1f1 --- /dev/null +++ b/readeck/Assets.xcassets/splash.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "readeck.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "readeck 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "readeck 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/readeck/Assets.xcassets/splash.imageset/readeck 1.png b/readeck/Assets.xcassets/splash.imageset/readeck 1.png new file mode 100644 index 0000000..a9ff749 Binary files /dev/null and b/readeck/Assets.xcassets/splash.imageset/readeck 1.png differ diff --git a/readeck/Assets.xcassets/splash.imageset/readeck 2.png b/readeck/Assets.xcassets/splash.imageset/readeck 2.png new file mode 100644 index 0000000..a9ff749 Binary files /dev/null and b/readeck/Assets.xcassets/splash.imageset/readeck 2.png differ diff --git a/readeck/Assets.xcassets/splash.imageset/readeck.png b/readeck/Assets.xcassets/splash.imageset/readeck.png new file mode 100644 index 0000000..a9ff749 Binary files /dev/null and b/readeck/Assets.xcassets/splash.imageset/readeck.png differ diff --git a/readeck/Assets.xcassets/splashBackground.colorset/Contents.json b/readeck/Assets.xcassets/splashBackground.colorset/Contents.json new file mode 100644 index 0000000..491e23f --- /dev/null +++ b/readeck/Assets.xcassets/splashBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x60", + "green" : "0x4E", + "red" : "0x01" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/readeck/Assets.xcassets/splashBg.colorset/Contents.json b/readeck/Assets.xcassets/splashBg.colorset/Contents.json index 8b258f9..5e290f2 100644 --- a/readeck/Assets.xcassets/splashBg.colorset/Contents.json +++ b/readeck/Assets.xcassets/splashBg.colorset/Contents.json @@ -5,8 +5,8 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x5A", - "green" : "0x4A", + "blue" : "0x5B", + "green" : "0x4B", "red" : "0x1F" } }, @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x5A", - "green" : "0x4A", - "red" : "0x1F" + "blue" : "0x4C", + "green" : "0x40", + "red" : "0x21" } }, "idiom" : "universal" diff --git a/readeck/Info.plist b/readeck/Info.plist index 47ba9df..a61a416 100644 --- a/readeck/Info.plist +++ b/readeck/Info.plist @@ -25,9 +25,9 @@ UILaunchScreen UIColorName - splashBg + splashBackground UIImageName - readeck + splash diff --git a/readeck/Splash.storyboard b/readeck/Splash.storyboard new file mode 100644 index 0000000..0dcded1 --- /dev/null +++ b/readeck/Splash.storyboard @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift index 3c9556e..06eda4e 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift @@ -5,32 +5,111 @@ struct BookmarkLabelsView: View { @State private var viewModel: BookmarkLabelsViewModel @Environment(\.dismiss) private var dismiss - init(bookmarkId: String, initialLabels: [String]) { + init(bookmarkId: String, initialLabels: [String], viewModel: BookmarkLabelsViewModel? = nil) { self.bookmarkId = bookmarkId - self._viewModel = State(initialValue: BookmarkLabelsViewModel(initialLabels: initialLabels)) + self._viewModel = State(initialValue: viewModel ?? BookmarkLabelsViewModel(initialLabels: initialLabels)) + + UIPageControl.appearance().currentPageIndicatorTintColor = UIColor(Color.primary) + UIPageControl.appearance().pageIndicatorTintColor = UIColor(Color.primary).withAlphaComponent(0.2) + } var body: some View { NavigationView { - VStack(spacing: 16) { - // Add new label section - addLabelSection + VStack(spacing: 12) { + // Add new label + HStack(spacing: 12) { + TextField("Enter label...", text: $viewModel.newLabelText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onSubmit { + Task { + await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText) + } + } + + Button(action: { + Task { + await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText) + } + }) { + Image(systemName: "plus.circle.fill") + .font(.title2) + .frame(width: 32, height: 32) + } + .disabled(viewModel.newLabelText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isLoading) + .foregroundColor(.accentColor) + } + .padding(.horizontal) - Divider() - .padding(.horizontal, -16) + // All available labels + if !viewModel.allLabels.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("All available tags") + .font(.headline) + .padding(.horizontal) + + TabView { + ForEach(Array(viewModel.labelPages.enumerated()), id: \ .offset) { pageIndex, labelsPage in + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .leading, spacing: 8) { + ForEach(labelsPage.filter { !viewModel.currentLabels.contains($0.name) }, id: \ .id) { label in + UnifiedLabelChip( + label: label.name, + isSelected: viewModel.currentLabels.contains(label.name), + isRemovable: false, + onTap: { + Task { + await viewModel.toggleLabel(for: bookmarkId, label: label.name) + } + } + ) + } + } + .frame(maxWidth: .infinity, alignment: .top) + .padding(.horizontal) + } + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic)) + .frame(height: 180) + .padding(.top, -20) + } + } - // Current labels section - currentLabelsSection + // Current labels + if !viewModel.currentLabels.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("Current labels") + .font(.headline) + .padding(.horizontal) + + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .leading, spacing: 8) { + ForEach(viewModel.currentLabels, id: \.self) { label in + UnifiedLabelChip( + label: label, + isSelected: true, + isRemovable: true, + onTap: { + // No action for current labels + }, + onRemove: { + Task { + await viewModel.removeLabel(from: bookmarkId, label: label) + } + } + ) + } + } + .padding(.horizontal) + } + } Spacer() } - .padding() + .padding(.vertical) .background(Color(.systemGroupedBackground)) .navigationTitle("Manage Labels") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { dismiss() } @@ -46,131 +125,17 @@ struct BookmarkLabelsView: View { } message: { Text(viewModel.errorMessage ?? "Unknown error") } - } - } - - private var addLabelSection: some View { - VStack(alignment: .leading, spacing: 12) { - Text("Add new label") - .font(.headline) - .foregroundColor(.primary) - - HStack(spacing: 12) { - TextField("Enter label...", text: $viewModel.newLabelText) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .onSubmit { - Task { - await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText) - } - } - - Button(action: { - Task { - await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText) - } - }) { - Image(systemName: "plus.circle.fill") - .font(.title2) - .foregroundColor(.white) - .frame(width: 32, height: 32) - .background( - Circle() - .fill(Color.accentColor) - ) - } - .disabled(viewModel.newLabelText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isLoading) + .task { + await viewModel.loadAllLabels() + } + .ignoresSafeArea(.keyboard) + .onTapGesture { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } - .padding() - .background( - RoundedRectangle(cornerRadius: 12) - .fill(Color(.secondarySystemGroupedBackground)) - ) - } - - private var currentLabelsSection: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Current labels") - .font(.headline) - .foregroundColor(.primary) - - Spacer() - - if viewModel.isLoading { - ProgressView() - .scaleEffect(0.8) - } - } - - if viewModel.currentLabels.isEmpty { - VStack(spacing: 8) { - Image(systemName: "tag") - .font(.title2) - .foregroundColor(.secondary) - Text("No labels available") - .font(.subheadline) - .foregroundColor(.secondary) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 32) - } else { - LazyVGrid(columns: [ - GridItem(.adaptive(minimum: 80, maximum: 150)) - ], spacing: 4) { - ForEach(viewModel.currentLabels, id: \.self) { label in - LabelChip( - label: label, - onRemove: { - Task { - await viewModel.removeLabel(from: bookmarkId, label: label) - } - } - ) - } - } - } - } - .padding() - .background( - RoundedRectangle(cornerRadius: 12) - .fill(Color(.secondarySystemGroupedBackground)) - ) - } -} - -struct LabelChip: View { - let label: String - let onRemove: () -> Void - - var body: some View { - HStack(spacing: 6) { - Text(label) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(.primary) - .lineLimit(1) - .truncationMode(.tail) - - Button(action: onRemove) { - Image(systemName: "xmark.circle.fill") - .font(.caption) - .foregroundColor(.secondary) - } - } - .padding(.horizontal, 10) - .padding(.vertical, 6) - .background( - RoundedRectangle(cornerRadius: 16) - .fill(Color.accentColor.opacity(0.15)) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(Color.accentColor.opacity(0.4), lineWidth: 1) - ) - ) } } #Preview { - BookmarkLabelsView(bookmarkId: "test-id", initialLabels: ["wichtig", "arbeit", "persönlich"]) -} + BookmarkLabelsView(bookmarkId: "test-id", initialLabels: ["wichtig", "arbeit", "persönlich"], viewModel: .init(MockUseCaseFactory(), initialLabels: ["test"])) +} diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift index 7b47811..4fac783 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift @@ -2,8 +2,9 @@ import Foundation @Observable class BookmarkLabelsViewModel { - private let addLabelsUseCase = DefaultUseCaseFactory.shared.makeAddLabelsToBookmarkUseCase() - private let removeLabelsUseCase = DefaultUseCaseFactory.shared.makeRemoveLabelsFromBookmarkUseCase() + private let addLabelsUseCase: PAddLabelsToBookmarkUseCase + private let removeLabelsUseCase: PRemoveLabelsFromBookmarkUseCase + private let getLabelsUseCase: PGetLabelsUseCase var isLoading = false var errorMessage: String? @@ -11,8 +12,38 @@ class BookmarkLabelsViewModel { var currentLabels: [String] = [] var newLabelText = "" - init(initialLabels: [String] = []) { + + var allLabels: [BookmarkLabel] = [] { + didSet { + let pageSize = Constants.Labels.pageSize + labelPages = stride(from: 0, to: allLabels.count, by: pageSize).map { + Array(allLabels[$0.. Void + let onRemove: (() -> Void)? + + init(label: String, isSelected: Bool = false, isRemovable: Bool = false, onTap: @escaping () -> Void, onRemove: (() -> Void)? = nil) { + self.label = label + self.isSelected = isSelected + self.isRemovable = isRemovable + self.onTap = onTap + self.onRemove = onRemove + } + + var body: some View { + Button(action: onTap) { + HStack(spacing: 6) { + Text(label) + .font(.caption) + .fontWeight(.medium) + .foregroundColor(isSelected ? .white : .primary) + .lineLimit(1) + .truncationMode(.tail) + + if isRemovable, let onRemove = onRemove { + Button(action: onRemove) { + Image(systemName: "xmark.circle.fill") + .font(.caption) + .foregroundColor(isSelected ? .white.opacity(0.8) : .secondary) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .frame(minHeight: 32) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(isSelected ? Color.accentColor : Color.accentColor.opacity(0.15)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.accentColor.opacity(0.4), lineWidth: 1) + ) + ) + } + .buttonStyle(PlainButtonStyle()) + } +} + +#Preview { + VStack(spacing: 16) { + UnifiedLabelChip( + label: "Sample Label", + isSelected: false, + isRemovable: false, + onTap: {} + ) + + UnifiedLabelChip( + label: "Selected Label", + isSelected: true, + isRemovable: false, + onTap: {} + ) + + UnifiedLabelChip( + label: "Removable Label", + isSelected: false, + isRemovable: true, + onTap: {}, + onRemove: {} + ) + + UnifiedLabelChip( + label: "Selected & Removable", + isSelected: true, + isRemovable: true, + onTap: {}, + onRemove: {} + ) + } + .padding() +} \ No newline at end of file