diff --git a/Projects/DVDesign/SampleApp/Sources/ContentView.swift b/Projects/DVDesign/SampleApp/Sources/ContentView.swift index bcb9e0bf..27b9f535 100644 --- a/Projects/DVDesign/SampleApp/Sources/ContentView.swift +++ b/Projects/DVDesign/SampleApp/Sources/ContentView.swift @@ -34,6 +34,22 @@ struct ContentView: View { @ViewBuilder private func detailView(for component: Component) -> some View { switch component.name { + case "DVPageControl": + DVPageControlPreviewView() + case "DVCategory": + DVCategoryPreviewView() + case "DVProjectContainer": + DVProjectContainerPreviewView() + case "DVVaultContainer": + DVVaultContainerPreviewView() + case "DVButton": + DVButtonPreviewView() + case "DVCheckBox": + DVCheckBoxPreviewView() + case "DVTitleBar": + DVTitleBarPreviewView() + case "DVSecretType": + DVSecretTypePreviewView() case "DVRadioButton", "DVRadioButtonGroup": RadioButtonPreviewView() default: @@ -42,6 +58,32 @@ struct ContentView: View { } } +// MARK: - Routing + +@ViewBuilder +private func destination(for component: Component) -> some View { + switch component.name { + case "DVPageControl": + DVPageControlPreviewView() + case "DVCategory": + DVCategoryPreviewView() + case "DVProjectContainer": + DVProjectContainerPreviewView() + case "DVVaultContainer": + DVVaultContainerPreviewView() + case "DVButton": + DVButtonPreviewView() + case "DVCheckBox": + DVCheckBoxPreviewView() + case "DVTitleBar": + DVTitleBarPreviewView() + case "DVSecretType": + DVSecretTypePreviewView() + default: + ComponentPlaceholderView(name: component.name, owner: component.owner) + } +} + // MARK: - Placeholder Detail private struct ComponentPlaceholderView: View { @@ -93,6 +135,8 @@ private enum ComponentSection: CaseIterable { Component(name: "DVCategory", owner: "예성"), Component(name: "DVButton", owner: "예성"), Component(name: "DVCheckBox", owner: "예성"), + Component(name: "DVTitleBar", owner: "예성"), + Component(name: "DVSecretType", owner: "예성"), ] case .doyeon: return [ diff --git a/Projects/DVDesign/SampleApp/Sources/DVButtonPreviewView.swift b/Projects/DVDesign/SampleApp/Sources/DVButtonPreviewView.swift new file mode 100644 index 00000000..9bf121ef --- /dev/null +++ b/Projects/DVDesign/SampleApp/Sources/DVButtonPreviewView.swift @@ -0,0 +1,58 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI +import DVDesign + +struct DVButtonPreviewView: View { + + // MARK: - Body + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 32) { + previewSection("Primary") { + primaryStates + } + previewSection("Secondary") { + secondaryStates + } + previewSection("Button Pair") { + buttonPair + } + } + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("DVButton") + } +} + +// MARK: - Subviews + +extension DVButtonPreviewView { + + private var primaryStates: some View { + VStack(spacing: 8) { + DVButton(titleText: "Start") {} + DVButton(titleText: "Start") {} + .disabled(true) + } + .frame(width: 242) + } + + private var secondaryStates: some View { + VStack(spacing: 8) { + DVButton(titleText: "Cancel", style: .secondary) {} + DVButton(titleText: "Cancel", style: .secondary) {} + .disabled(true) + } + .frame(width: 74) + } + + private var buttonPair: some View { + VStack(spacing: 8) { + DVButton(titleText: "Cancel", style: .secondary) {} + DVButton(titleText: "Create") {} + } + } +} diff --git a/Projects/DVDesign/SampleApp/Sources/DVCategoryPreviewView.swift b/Projects/DVDesign/SampleApp/Sources/DVCategoryPreviewView.swift new file mode 100644 index 00000000..dbc84c6d --- /dev/null +++ b/Projects/DVDesign/SampleApp/Sources/DVCategoryPreviewView.swift @@ -0,0 +1,34 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI +import DVDesign + +struct DVCategoryPreviewView: View { + @State private var selected = 0 + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 32) { + previewSection("Interactive") { + HStack(spacing: 8) { + DVCategory(title: "All", count: 999, isSelected: selected == 0) { selected = 0 } + DVCategory(title: "Star", count: 12, isSelected: selected == 1) { selected = 1 } + DVCategory(title: "Expired", count: 5, isSelected: selected == 2) { selected = 2 } + DVCategory(title: "Deleted", count: 3, isSelected: selected == 3) { selected = 3 } + } + } + + previewSection("Selected") { + DVCategory(title: "All", count: 999, isSelected: true) {} + } + + previewSection("Unselected") { + DVCategory(title: "All", count: 999, isSelected: false) {} + } + } + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("DVCategory") + } +} diff --git a/Projects/DVDesign/SampleApp/Sources/DVCheckBoxPreviewView.swift b/Projects/DVDesign/SampleApp/Sources/DVCheckBoxPreviewView.swift new file mode 100644 index 00000000..234615a8 --- /dev/null +++ b/Projects/DVDesign/SampleApp/Sources/DVCheckBoxPreviewView.swift @@ -0,0 +1,45 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI +import DVDesign + +struct DVCheckBoxPreviewView: View { + + // MARK: - Properties + + @State private var isChecked = false + + // MARK: - Body + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 32) { + previewSection("Interactive") { + interactiveRow + } + previewSection("Checked") { + DVCheckBox(isChecked: true) {} + } + previewSection("Unchecked") { + DVCheckBox(isChecked: false) {} + } + } + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("DVCheckBox") + } +} + +// MARK: - Subviews + +extension DVCheckBoxPreviewView { + + private var interactiveRow: some View { + HStack(spacing: 12) { + DVCheckBox(isChecked: isChecked) { isChecked.toggle() } + Text(isChecked ? "Checked" : "Unchecked") + .foregroundStyle(.secondary) + } + } +} diff --git a/Projects/DVDesign/SampleApp/Sources/DVPageControlPreviewView.swift b/Projects/DVDesign/SampleApp/Sources/DVPageControlPreviewView.swift new file mode 100644 index 00000000..d9e46f98 --- /dev/null +++ b/Projects/DVDesign/SampleApp/Sources/DVPageControlPreviewView.swift @@ -0,0 +1,50 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI +import DVDesign + +struct DVPageControlPreviewView: View { + + // MARK: - Properties + + @State private var currentStep = 0 + + // MARK: - Body + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 32) { + previewSection("Interactive (탭해서 이동)") { + VStack(spacing: 16) { + DVPageControl(totalSteps: 3, currentStep: currentStep) + HStack(spacing: 8) { + DVButton(titleText: "이전", style: .secondary) { + if currentStep > 0 { currentStep -= 1 } + } + .frame(width: 80) + .disabled(currentStep == 0) + + DVButton(titleText: "다음", style: .secondary) { + if currentStep < 2 { currentStep += 1 } + } + .frame(width: 80) + .disabled(currentStep == 2) + } + } + } + previewSection("Step 1 / 3") { + DVPageControl(totalSteps: 3, currentStep: 0) + } + previewSection("Step 2 / 3") { + DVPageControl(totalSteps: 3, currentStep: 1) + } + previewSection("Step 3 / 3") { + DVPageControl(totalSteps: 3, currentStep: 2) + } + } + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("DVPageControl") + } +} diff --git a/Projects/DVDesign/SampleApp/Sources/DVProjectContainerPreviewView.swift b/Projects/DVDesign/SampleApp/Sources/DVProjectContainerPreviewView.swift new file mode 100644 index 00000000..afa24c6e --- /dev/null +++ b/Projects/DVDesign/SampleApp/Sources/DVProjectContainerPreviewView.swift @@ -0,0 +1,46 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI +import DVDesign + +struct DVProjectContainerPreviewView: View { + @State private var selected: String? = nil + + private let projects = [ + ("CheerLot", 8), + ("DrinkiG", 8), + ("LongLongNameExample", 8), + ] + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 32) { + previewSection("Interactive") { + VStack(spacing: 2) { + ForEach(projects, id: \.0) { name, count in + DVProjectContainer( + name: name, + count: count, + isSelected: selected == name + ) { selected = selected == name ? nil : name } + } + } + .frame(width: 220) + } + + previewSection("Selected") { + DVProjectContainer(name: "DrinkiG", count: 8, isSelected: true) {} + .frame(width: 220) + } + + previewSection("Unselected") { + DVProjectContainer(name: "DrinkiG", count: 8, isSelected: false) {} + .frame(width: 220) + } + } + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("DVProjectContainer") + } +} diff --git a/Projects/DVDesign/SampleApp/Sources/DVSecretTypePreviewView.swift b/Projects/DVDesign/SampleApp/Sources/DVSecretTypePreviewView.swift new file mode 100644 index 00000000..3b7fbb46 --- /dev/null +++ b/Projects/DVDesign/SampleApp/Sources/DVSecretTypePreviewView.swift @@ -0,0 +1,40 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI +import DVDesign + +struct DVSecretTypePreviewView: View { + + // MARK: - Body + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 32) { + previewSection("Icon 없음 (기본)") { + DVSecretType(labelText: "API Keys / Token") + } + previewSection("Icon 있음") { + DVSecretType( + labelText: "API Keys / Token", + icon: Image(systemName: "key.fill") + ) + } + previewSection("다른 타입") { + HStack(spacing: 24) { + DVSecretType( + labelText: "Password", + icon: Image(systemName: "lock.fill") + ) + DVSecretType( + labelText: "Note", + icon: Image(systemName: "note.text") + ) + } + } + } + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("DVSecretType") + } +} diff --git a/Projects/DVDesign/SampleApp/Sources/DVTitleBarPreviewView.swift b/Projects/DVDesign/SampleApp/Sources/DVTitleBarPreviewView.swift new file mode 100644 index 00000000..5ffb5f18 --- /dev/null +++ b/Projects/DVDesign/SampleApp/Sources/DVTitleBarPreviewView.swift @@ -0,0 +1,41 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI +import DVDesign + +struct DVTitleBarPreviewView: View { + + // MARK: - Properties + + @State private var searchText = "" + + // MARK: - Body + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 32) { + previewSection("Interactive") { + DVTitleBar( + titleText: "All", + searchText: $searchText, + onSortTapped: {} + ) + .frame(width: 280) + .background(Color.white) + } + previewSection("Expired") { + DVTitleBar( + titleText: "Expired", + searchText: .constant(""), + onSortTapped: {} + ) + .frame(width: 280) + .background(Color.white) + } + } + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("DVTitleBar") + } +} diff --git a/Projects/DVDesign/SampleApp/Sources/DVVaultContainerPreviewView.swift b/Projects/DVDesign/SampleApp/Sources/DVVaultContainerPreviewView.swift new file mode 100644 index 00000000..9a0547d0 --- /dev/null +++ b/Projects/DVDesign/SampleApp/Sources/DVVaultContainerPreviewView.swift @@ -0,0 +1,61 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI +import DVDesign + +struct DVVaultContainerPreviewView: View { + @State private var selectedIndex: Int? = nil + + private let vaults = [ + ("내가 설정한 이름", "2026.04.01"), + ("이름이름이름", "2026.04.01"), + ("Example", "2026.03.27"), + ("LongLongNameExam...", "2026.04.01"), + ] + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 32) { + previewSection("Interactive") { + VStack(spacing: 4) { + ForEach(vaults.indices, id: \.self) { index in + DVVaultContainer( + name: vaults[index].0, + date: vaults[index].1, + isSelected: selectedIndex == index + ) { selectedIndex = selectedIndex == index ? nil : index } + } + } + } + + previewSection("Selected") { + DVVaultContainer(name: "내가 설정한 이름", date: "2026.04.01", isSelected: true) {} + } + + previewSection("Unselected") { + DVVaultContainer(name: "내가 설정한 이름", date: "2026.04.01", isSelected: false) {} + } + + previewSection("Expiring Soon") { + DVVaultContainer( + name: "내가 설정한 이름", + date: "2026.04.01", + isSelected: false, + trailingIcon: .expiringSoon + ) {} + } + previewSection("Expired") { + DVVaultContainer( + name: "내가 설정한 이름", + date: "2026.04.01", + isSelected: false, + trailingIcon: .expired + ) {} + } + } + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + } + .navigationTitle("DVVaultContainer") + } +} diff --git a/Projects/DVDesign/SampleApp/Sources/PreviewHelpers.swift b/Projects/DVDesign/SampleApp/Sources/PreviewHelpers.swift new file mode 100644 index 00000000..fcafa8c0 --- /dev/null +++ b/Projects/DVDesign/SampleApp/Sources/PreviewHelpers.swift @@ -0,0 +1,13 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI + +@ViewBuilder +func previewSection(_ title: String, @ViewBuilder content: () -> Content) -> some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.headline) + .foregroundStyle(.secondary) + content() + } +} diff --git a/Projects/DVDesign/Sources/Components/DVButton.swift b/Projects/DVDesign/Sources/Components/DVButton.swift new file mode 100644 index 00000000..ed7472c6 --- /dev/null +++ b/Projects/DVDesign/Sources/Components/DVButton.swift @@ -0,0 +1,129 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI + +public struct DVButton: View { + + // MARK: - Types + + public enum Style { + case primary + case secondary + + var cornerRadius: CGFloat { + switch self { + case .primary: return 20 + case .secondary: return 6 + } + } + + var height: CGFloat { + switch self { + case .primary: return 40 + case .secondary: return 24 + } + } + + var horizontalPadding: CGFloat { + switch self { + case .primary: return 16 + case .secondary: return 16 + } + } + + var width: CGFloat { + switch self { + case .primary: return 242 + case .secondary: return 74 + } + } + + var font: DVFont { + switch self { + case .primary: return .bodyLG + case .secondary: return .bodyMD + } + } + } + + // MARK: - Properties + + public let titleText: String + public let style: Style + public let action: () -> Void + + @State private var isHovered = false + + // MARK: - Init + + public init( + titleText: String, + style: Style = .primary, + action: @escaping () -> Void + ) { + self.titleText = titleText + self.style = style + self.action = action + } + + // MARK: - Body + + public var body: some View { + Button(action: action) { + labelView + } + .buttonStyle(DVButtonStyle(isHovered: isHovered, style: style)) + .onHover { isHovered = $0 } + } +} + +// MARK: - Subviews + +extension DVButton { + + private var labelView: some View { + Text(titleText) + .dvFont(style.font) + .frame(width: style.width, height: style.height) + .padding(.horizontal, style.horizontalPadding) + } +} + +// MARK: - ButtonStyle + +private struct DVButtonStyle: ButtonStyle { + + let isHovered: Bool + let style: DVButton.Style + + @Environment(\.isEnabled) private var isEnabled + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .foregroundStyle(foregroundColor) + .background(backgroundColor(isPressed: configuration.isPressed)) + .clipShape(RoundedRectangle(cornerRadius: style.cornerRadius)) + } + + private var foregroundColor: Color { + switch style { + case .primary: + return Color.dv(.white) + case .secondary: + return isEnabled ? Color.dv(.gray800) : Color.dv(.gray400) + } + } + + private func backgroundColor(isPressed: Bool) -> Color { + switch style { + case .primary: + if !isEnabled { return Color.dv(.vaultGreenTint) } + if isPressed || isHovered { return Color.dv(.vaultGreenDark) } + return Color.dv(.vaultGreen) + case .secondary: + if !isEnabled { return Color.dv(.gray100) } + if isPressed || isHovered { return Color.dv(.gray200) } + return Color.dv(.gray100) + } + } +} diff --git a/Projects/DVDesign/Sources/Components/DVCategory.swift b/Projects/DVDesign/Sources/Components/DVCategory.swift new file mode 100644 index 00000000..2dfb2591 --- /dev/null +++ b/Projects/DVDesign/Sources/Components/DVCategory.swift @@ -0,0 +1,85 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI + +public struct DVCategory: View { + + // MARK: - Properties + + public let title: String + public let count: Int + public let isSelected: Bool + public let action: () -> Void + + @State private var isHovered = false + + // MARK: - Init + + public init( + title: String, + count: Int, + isSelected: Bool, + action: @escaping () -> Void + ) { + self.title = title + self.count = count + self.isSelected = isSelected + self.action = action + } + + // MARK: - Body + + public var body: some View { + Button(action: action) { + contentLayer + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .frame(width: 108, height: 72) + .background(isSelected ? Color.dv(.vaultGreen) : Color.dv(.gray100)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay(borderOverlay) + .onHover { isHovered = $0 } + } +} + +// MARK: - Subviews + +extension DVCategory { + + private var contentLayer: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .center) { + iconCircle + Spacer() + countLabel + } + Spacer() + titleLabel + } + .padding(12) + } + + private var iconCircle: some View { + Circle() + .fill(isSelected ? Color.dv(.white).opacity(0.25) : Color.dv(.gray300)) + .frame(width: 28, height: 28) + } + + private var titleLabel: some View { + Text(title) + .dvFont(.bodyLG) + .foregroundStyle(isSelected ? Color.dv(.white) : Color.dv(.gray800)) + } + + private var countLabel: some View { + Text(count > 999 ? "999+" : "\(count)") + .dvFont(.bodyMD) + .foregroundStyle(isSelected ? Color.dv(.white).opacity(0.8) : Color.dv(.gray600)) + } + + private var borderOverlay: some View { + RoundedRectangle(cornerRadius: 12) + .stroke(isHovered && !isSelected ? Color.dv(.gray300) : Color.clear, lineWidth: 1) + } +} diff --git a/Projects/DVDesign/Sources/Components/DVCheckBox.swift b/Projects/DVDesign/Sources/Components/DVCheckBox.swift new file mode 100644 index 00000000..ddd0e592 --- /dev/null +++ b/Projects/DVDesign/Sources/Components/DVCheckBox.swift @@ -0,0 +1,66 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI + +public struct DVCheckBox: View { + + // MARK: - Properties + + public let isChecked: Bool + public let action: () -> Void + + @State private var isHovered = false + + // MARK: - Init + + public init( + isChecked: Bool, + action: @escaping () -> Void + ) { + self.isChecked = isChecked + self.action = action + } + + // MARK: - Body + + public var body: some View { + Button(action: action) { + checkboxShape + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .onHover { isHovered = $0 } + } +} + +// MARK: - Subviews + +extension DVCheckBox { + + private var checkboxShape: some View { + ZStack { + RoundedRectangle(cornerRadius: 5.5) + .fill(isChecked ? Color.dv(.vaultGreen) : Color.dv(.gray200)) + .frame(width: 16, height: 16) + .overlay(borderOverlay) + + if isChecked { + checkmarkIcon + } + } + } + + private var checkmarkIcon: some View { + Image(systemName: "checkmark") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(Color.dv(.white)) + } + + private var borderOverlay: some View { + RoundedRectangle(cornerRadius: 5) + .stroke( + isHovered && !isChecked ? Color.dv(.gray400) : Color.clear, + lineWidth: 1 + ) + } +} diff --git a/Projects/DVDesign/Sources/Components/DVPageControl.swift b/Projects/DVDesign/Sources/Components/DVPageControl.swift new file mode 100644 index 00000000..7c4897ef --- /dev/null +++ b/Projects/DVDesign/Sources/Components/DVPageControl.swift @@ -0,0 +1,40 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI + +public struct DVPageControl: View { + + // MARK: - Properties + + public let totalSteps: Int + public let currentStep: Int + + // MARK: - Init + + public init(totalSteps: Int, currentStep: Int) { + self.totalSteps = totalSteps + self.currentStep = currentStep + } + + // MARK: - Body + + public var body: some View { + HStack(spacing: 6) { + ForEach(0 ..< totalSteps, id: \.self) { step in + dot(isActive: step == currentStep) + } + } + } +} + +// MARK: - Subviews + +extension DVPageControl { + + private func dot(isActive: Bool) -> some View { + RoundedRectangle(cornerRadius: 4) + .fill(isActive ? Color.dv(.vaultGreen) : Color.dv(.gray300)) + .frame(width: isActive ? 24 : 8, height: 8) + .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isActive) + } +} diff --git a/Projects/DVDesign/Sources/Components/DVProjectContainer.swift b/Projects/DVDesign/Sources/Components/DVProjectContainer.swift new file mode 100644 index 00000000..d192712f --- /dev/null +++ b/Projects/DVDesign/Sources/Components/DVProjectContainer.swift @@ -0,0 +1,84 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI + +public struct DVProjectContainer: View { + + // MARK: - Properties + + public let name: String + public let count: Int + public let isSelected: Bool + public let action: () -> Void + + @State private var isHovered = false + + // MARK: - Init + + public init( + name: String, + count: Int, + isSelected: Bool, + action: @escaping () -> Void + ) { + self.name = name + self.count = count + self.isSelected = isSelected + self.action = action + } + + // MARK: - Body + + public var body: some View { + Button(action: action) { + rowContent + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .frame(width: 228, height: 28) + .background(isSelected ? Color.dv(.vaultGreen) : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay(borderOverlay) + .onHover { isHovered = $0 } + } +} + +// MARK: - Subviews + +extension DVProjectContainer { + + private var rowContent: some View { + HStack(spacing: 8) { + projectIcon + nameLabel + Spacer() + countLabel + } + .padding(.horizontal, 10) + .padding(.vertical, 8) + } + + private var projectIcon: some View { + Image(systemName: "tray") + .dvFont(.captionLG) + .foregroundStyle(isSelected ? Color.dv(.white) : Color.dv(.gray700)) + } + + private var nameLabel: some View { + Text(name) + .dvFont(.bodyMD) + .foregroundStyle(isSelected ? Color.dv(.white) : Color.dv(.gray900)) + .lineLimit(1) + } + + private var countLabel: some View { + Text("\(count)") + .dvFont(.bodyMD) + .foregroundStyle(isSelected ? Color.dv(.white).opacity(0.8) : Color.dv(.gray500)) + } + + private var borderOverlay: some View { + RoundedRectangle(cornerRadius: 8) + .stroke(isHovered && !isSelected ? Color.dv(.gray300) : Color.clear, lineWidth: 1) + } +} diff --git a/Projects/DVDesign/Sources/Components/DVSecretType.swift b/Projects/DVDesign/Sources/Components/DVSecretType.swift new file mode 100644 index 00000000..5eaafeb4 --- /dev/null +++ b/Projects/DVDesign/Sources/Components/DVSecretType.swift @@ -0,0 +1,58 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI + +public struct DVSecretType: View { + + // MARK: - Properties + + public let labelText: String + public let icon: Image? + + // MARK: - Init + + public init( + labelText: String, + icon: Image? = nil + ) { + self.labelText = labelText + self.icon = icon + } + + // MARK: - Body + + public var body: some View { + VStack(spacing: 12) { + iconCircle + typeLabel + } + } +} + +// MARK: - Subviews + +extension DVSecretType { + + private var iconCircle: some View { + ZStack { + Circle() + .fill(Color.dv(.gray200)) + .frame(width: 120, height: 120) + + if let icon { + icon + .resizable() + .scaledToFit() + .frame(width: 56, height: 56) + .foregroundStyle(Color.dv(.gray600)) + } + } + } + + private var typeLabel: some View { + Text(labelText) + .dvFont(.bodyLG) + .foregroundStyle(Color.dv(.gray900)) + .multilineTextAlignment(.center) + } +} diff --git a/Projects/DVDesign/Sources/Components/DVTitleBar.swift b/Projects/DVDesign/Sources/Components/DVTitleBar.swift new file mode 100644 index 00000000..cdc455b1 --- /dev/null +++ b/Projects/DVDesign/Sources/Components/DVTitleBar.swift @@ -0,0 +1,79 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI + +public struct DVTitleBar: View { + + // MARK: - Properties + + public let titleText: String + public let searchText: Binding + public let searchPromptText: String + public let onSortTapped: () -> Void + + // MARK: - Init + + public init( + titleText: String, + searchText: Binding, + searchPromptText: String = "Search", + onSortTapped: @escaping () -> Void + ) { + self.titleText = titleText + self.searchText = searchText + self.searchPromptText = searchPromptText + self.onSortTapped = onSortTapped + } + + // MARK: - Body + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + titleRow + searchField + } + .frame(width: 280) + .padding(.horizontal, 12) + .padding(.vertical, 10) + } +} + +// MARK: - Subviews + +extension DVTitleBar { + + private var titleRow: some View { + HStack { + Text(titleText) + .dvFont(.headingXL) + .foregroundStyle(Color.dv(.gray900)) + Spacer() + sortButton + } + } + + private var sortButton: some View { + Button(action: onSortTapped) { + Image(systemName: "arrow.up.arrow.down") + .dvFont(.bodyXL) + .foregroundStyle(Color.dv(.gray600)) + } + .buttonStyle(.plain) + } + + private var searchField: some View { + HStack(spacing: 6) { + Image(systemName: "magnifyingglass") + .dvFont(.bodyMD) + .foregroundStyle(Color.dv(.gray500)) + TextField(searchPromptText, text: searchText) + .dvFont(.bodyMD) + .foregroundStyle(Color.dv(.gray900)) + .textFieldStyle(.plain) + } + .padding(.horizontal, 10) + .frame(height: 36) + .background(.regularMaterial) + .clipShape(RoundedRectangle(cornerRadius: 18)) + } +} diff --git a/Projects/DVDesign/Sources/Components/DVVaultContainer.swift b/Projects/DVDesign/Sources/Components/DVVaultContainer.swift new file mode 100644 index 00000000..12d063bc --- /dev/null +++ b/Projects/DVDesign/Sources/Components/DVVaultContainer.swift @@ -0,0 +1,120 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftUI + +public struct DVVaultContainer: View { + + // MARK: - Types + + public enum TrailingIcon { + case expiringSoon + case expired + } + + // MARK: - Properties + + public let name: String + public let date: String + public let isSelected: Bool + public let trailingIcon: TrailingIcon? + public let action: () -> Void + + @State private var isHovered = false + + // MARK: - Init + + public init( + name: String, + date: String, + isSelected: Bool, + trailingIcon: TrailingIcon? = nil, + action: @escaping () -> Void + ) { + self.name = name + self.date = date + self.isSelected = isSelected + self.trailingIcon = trailingIcon + self.action = action + } + + // MARK: - Body + + public var body: some View { + Button(action: action) { + rowContent + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .frame(width: 280) + .background(isSelected ? Color.dv(.vaultGreen) : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .overlay(borderOverlay) + .onHover { isHovered = $0 } + } +} + +// MARK: - Subviews + +extension DVVaultContainer { + + private var rowContent: some View { + HStack(spacing: 12) { + avatarCircle + textStack + Spacer() + trailingIconView + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + } + + private var avatarCircle: some View { + Circle() + .fill(isSelected ? Color.dv(.white).opacity(0.25) : Color.dv(.gray300)) + .frame(width: 44, height: 44) + } + + private var textStack: some View { + VStack(alignment: .leading, spacing: 2) { + Text(name) + .dvFont(.bodyLG) + .foregroundStyle(isSelected ? Color.dv(.white) : Color.dv(.gray900)) + .lineLimit(1) + Text(date) + .dvFont(.captionMDRegular) + .foregroundStyle(isSelected ? Color.dv(.white).opacity(0.7) : Color.dv(.gray500)) + } + } + + @ViewBuilder + private var trailingIconView: some View { + if let trailingIcon { + Image(systemName: trailingIcon.iconName) + .foregroundStyle(isSelected ? Color.dv(.white) : trailingIcon.iconColor) + } + } + + private var borderOverlay: some View { + RoundedRectangle(cornerRadius: 10) + .stroke(isHovered && !isSelected ? Color.dv(.gray300) : Color.clear, lineWidth: 1) + } +} + +// MARK: - TrailingIcon Appearance + +extension DVVaultContainer.TrailingIcon { + + fileprivate var iconName: String { + switch self { + case .expiringSoon: return "clock" + case .expired: return "exclamationmark.circle" + } + } + + fileprivate var iconColor: Color { + switch self { + case .expiringSoon: return Color.dv(.warning) + case .expired: return Color.dv(.danger) + } + } +}