Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
e2ae4ef
[#76] fix: 수정 경로에 만료일·이름 정규화가 빠져 있던 문제 수정
doyeonk429 Aug 15, 2026
45c882d
[#76] fix: 수정 저장 시 UI 미노출 metadata 필드가 사라지던 문제
doyeonk429 Aug 16, 2026
9b27090
[#76] feat: 복호화된 payload를 수정 폼 필드로 되돌리는 역매핑 추가
doyeonk429 Aug 16, 2026
108e4af
[#76] refactor: 폼 섹션 디스패처를 화면 밖으로 추출
doyeonk429 Aug 16, 2026
50110af
[#76] fix: 만료일을 지워도 예약된 알림이 남던 문제
doyeonk429 Aug 16, 2026
b213ac1
[#76] feat: 수정 저장을 위한 SecretClient.updateSecret 추가
doyeonk429 Aug 16, 2026
18cdee0
[#76] feat: 편집 중 헤더 액션 정리
doyeonk429 Aug 16, 2026
0d366b8
[#76] refactor: 복호화 후속 동작을 RevealContinuation 타입으로 표현
doyeonk429 Aug 16, 2026
a3384a2
[#76] feat: 수정 모드 진입과 취소
doyeonk429 Aug 16, 2026
a251c80
[#76] feat: 수정 저장 — dirty 판정과 다시 쓸 대상 결정
doyeonk429 Aug 16, 2026
fe79abe
[#76] feat: 수정 화면을 실제로 연결
doyeonk429 Aug 16, 2026
7d61583
[#76] chore: 화면 캡처 차단을 Debug 빌드에서 해제
doyeonk429 Aug 16, 2026
1e5fb5c
[#76] fix: 여러 줄 보안 필드에 없는 생략 표시가 붙던 문제
doyeonk429 Aug 16, 2026
b547cd1
[#76] feat: 폼 컴포넌트가 컨테이너 폭을 따라 연속으로 줄어들도록 수정
doyeonk429 Aug 16, 2026
cbc797b
[#76] feat: 진행 오버레이를 창 전체로 통일
doyeonk429 Aug 16, 2026
d3cc4e6
[#76] feat: 조회 화면에 선택된 프로젝트 없음 안내 추가
doyeonk429 Aug 16, 2026
721e268
[#76] feat: 수정 화면 전환 마감과 수정 진입 인증 문구 분리
doyeonk429 Aug 16, 2026
fdd2224
[#76] feat: 폼에서 만든 프로젝트를 사이드바에 즉시 반영
doyeonk429 Aug 16, 2026
28bb118
[#76] fix: 팝오버에서 연속 선택 시 이전 항목의 체크가 갱신되지 않던 문제
doyeonk429 Aug 16, 2026
7db3538
[#76] test: 다중 선택 드롭다운 연속 선택 회귀 화면 추가
doyeonk429 Aug 16, 2026
8c64979
[#76] fix: 조회 화면 여러 줄 값이 글자 단위로 접히도록 변경
doyeonk429 Aug 16, 2026
23f035e
[#76] fix: 수정 저장 경로의 데이터 손실·잠금 구멍 차단
doyeonk429 Aug 16, 2026
3c2586c
[#76] fix: 저장하며 인증해도 인증 창이 닫힌 채로 남던 문제
doyeonk429 Aug 16, 2026
bbb7cf6
[#76] fix: 수정 진입 복호화 중 두 번째 복호화가 시작되던 문제
doyeonk429 Aug 16, 2026
2601c20
[#76] docs: 폼 폭 정책 주석이 현재 동작과 반대로 남아 있던 것 정리
doyeonk429 Aug 16, 2026
343bb1d
[#76] feat: 메모를 여러 줄 입력필드로 바꿈
doyeonk429 Aug 16, 2026
45cd4b8
[#76] fix: assertionFailure에 평문 시크릿이 실리지 않도록 case 이름만 남김
doyeonk429 Aug 16, 2026
e87c628
[#76] feat: 진행 오버레이에 접근성 라벨 추가
doyeonk429 Aug 16, 2026
9769ea4
[#76] docs: 테스트 헬퍼 주석을 설명 대상 함수로 옮김
doyeonk429 Aug 16, 2026
daafa4c
[#96] chore: 조회 화면 접근성 보완과 죽은 코드 정리
doyeonk429 Aug 16, 2026
df20d6f
[#76] feat: 설정의 defaultEnvironment을 생성 화면에 반영
doyeonk429 Aug 16, 2026
83e0efb
[#76] fix: reason 인자가 빠져 DVDomain 테스트가 컴파일되지 않던 문제
doyeonk429 Aug 16, 2026
3ef690a
[#76] fix: 생성 화면을 드나들 때 사이드바가 깜빡이던 문제
doyeonk429 Aug 16, 2026
5cfcbcc
[#96] refactor: 뷰 분기를 screen으로 모으고 split view를 하나로 고정
doyeonk429 Aug 16, 2026
9f020b4
[#96] refactor: 사이드바 선택 상태를 mode enum으로 모으고 생성 중 재선택이 삼켜지던 문제 수정
doyeonk429 Aug 16, 2026
8373c44
[#96] feat: 생성 중 사이드바를 누르면 확인을 거쳐 목록으로 돌아가도록 기능 추가
doyeonk429 Aug 16, 2026
80be1ec
[#96] fix: 삭제 alert를 확인 없이 닫아도 대상 프로젝트가 남던 문제
doyeonk429 Aug 16, 2026
4163233
[#96] fix: 프로젝트 이름 변경이 실패하면 입력한 이름이 사라지던 문제 수정
doyeonk429 Aug 16, 2026
6734210
[#96] feat: 프로젝트가 하나도 없을 때 사이드바에 안내 표시 추가
doyeonk429 Aug 16, 2026
f8c9a3f
[#96] fix: 같은 대상으로 돌아올 때 불러온 목록이 비워지던 문제 수정
doyeonk429 Aug 16, 2026
53046ab
[#96] fix: 시크릿을 만든 뒤 목록이 갱신되지 않던 문제
doyeonk429 Aug 16, 2026
c5ee422
[#103] refactor: 창·컬럼 너비를 WindowLayoutMetrics로 모으고 사이드바 폭을 메인·설정 동일하게 고정
doyeonk429 Aug 16, 2026
52d9ea1
[#96] feat: 화면·컬럼 전환과 목록 재배치에 애니메이션 추가
doyeonk429 Aug 16, 2026
baca552
[#96] refactor: 애니메이션 상수를 MotionMetrics로 모으고 길이를 통일
doyeonk429 Aug 16, 2026
166fd8a
[#104] fix: 검색창에 커서가 들어간 뒤 다른 영역을 눌러도 포커스가 풀리지 않던 문제
doyeonk429 Aug 16, 2026
eab2c9e
[#104] chore: 목록 셀 우클릭 메뉴에서 Add to Project 제거
doyeonk429 Aug 16, 2026
a2c47b9
[#103] chore: 프로젝트 셀 우클릭 메뉴에서 New Secret 제거
doyeonk429 Aug 16, 2026
9bc671f
[#103] feat: 사이드바 하단 생성·설정 버튼을 Liquid Glass로 (macOS 26 이상)
doyeonk429 Aug 16, 2026
c080814
[#96] feat: 아이콘 버튼 hover·press 추가
doyeonk429 Aug 16, 2026
6bc225e
[#96] chore: 주석 간소화 누락분 반영과 테스트 doc 주석 위치 정정
doyeonk429 Aug 16, 2026
5764c80
[#96] fix: 생성 중 고른 항목이 확정 전에 사이드바를 옮기던 문제
doyeonk429 Aug 17, 2026
bfab8e2
[#96] fix: 사이드바 + 버튼이 확인 없이 생성 폼을 버리던 문제
doyeonk429 Aug 17, 2026
3bb4203
[#96] fix: 프로젝트를 만들면 조회 중이던 상세가 남던 문제
doyeonk429 Aug 17, 2026
ce0da0a
[#96] fix: 프로젝트 이름 변경을 목록이 보고 있는 것 기준으로 판정
doyeonk429 Aug 17, 2026
2bf1015
[#96] fix: 저장소 전환·초기화 뒤 기억해 둔 목적지가 남던 문제
doyeonk429 Aug 17, 2026
ddb9c33
[#96] fix: Return 키가 강조되지 않은 프로젝트의 이름 변경을 열던 문제
doyeonk429 Aug 17, 2026
77782d5
[#76] fix: 설정 화면을 다녀오면 생성 폼의 환경 선택이 초기화되던 문제
doyeonk429 Aug 17, 2026
d48184c
[#96] fix: 뷰가 다시 만들어질 때 이미 받아둔 목록을 비우던 문제
doyeonk429 Aug 17, 2026
87e8bc2
[#104] fix: 목록 탭 제스처가 행 클릭과 경합하지 않도록 변경
doyeonk429 Aug 17, 2026
a2947a4
[#104] chore: 어디서도 쓰이지 않는 AddToProject 화면 제거
doyeonk429 Aug 17, 2026
38e2894
[#96] chore: 사이드바 로딩 분기 정리와 첫 로드 fade-in 명시
doyeonk429 Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Projects/DVDesign/SampleApp/Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ struct ContentView: View {
DVLabeledFieldPreviewView()
case "DVChipsField":
DVChipsFieldPreviewView()
case "DVMultiSelectDropdown":
DVMultiSelectDropdownPreviewView()
default:
ComponentPlaceholderView(name: component.name)
}
Expand Down Expand Up @@ -107,6 +109,7 @@ private struct Component: Hashable {
Component(name: "DVCategory"),
Component(name: "DVButton"),
Component(name: "DVCheckBox"),
Component(name: "DVMultiSelectDropdown"),
Component(name: "DVTitleBar"),
Component(name: "DVSecretType"),
Component(name: "DVRadioButton"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright © 2026 Devault. All rights reserved

import SwiftUI

import DVDesign

/// `DVMultiSelectDropdown`의 **연속 선택** 회귀 화면.
///
/// 여러 항목을 잇따라 누를 때 팝오버의 체크 상태가 매번 따라오는지 본다. 한때 방금 누른 행만
/// 갱신되고 이전에 누른 행들은 옛 상태로 남았고, 몇 번은 멀쩡해서 눈으로는 판정이 어려웠다.
///
/// TCA도 Array↔Set 어댑터도 없이 순수 `@State`만 쓰므로, 여기서 어긋나면 원인은 컴포넌트 안에 있다.
/// 나머지 설정은 `ProjectFieldView`와 맞춘다 — `groupsSelectedAtTop: true`, 검색 없음, add-new 있음.
struct DVMultiSelectDropdownPreviewView: View {

private struct Item: Identifiable, Hashable {
let id = UUID()
let name: String
}

private let items: [Item] = [
.init(name: "DeVault"),
.init(name: "DrinkiG"),
.init(name: "CheerLot"),
.init(name: "SipStream"),
.init(name: "Example"),
]

@State private var selection: Set<UUID> = []
@State private var log: [String] = []

var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 24) {
header

DVMultiSelectDropdown(
"Select Project",
items: items,
selection: $selection,
label: \.name,
size: .md,
onCreate: { log.insert("+ Add new 눌림", at: 0) },
createLabel: "Add new project",
emptyMessage: "No projects yet",
groupsSelectedAtTop: true
)

stateReadout
Spacer(minLength: 0)
}
.padding(40)
.frame(maxWidth: .infinity, alignment: .leading)
}
.onAppear {
// 열기 전에 두 개를 선택해 둬야 Selected / All 두 섹션이 모두 생긴다.
if selection.isEmpty {
selection = Set(items.prefix(2).map(\.id))
}
}
}

private var header: some View {
VStack(alignment: .leading, spacing: 6) {
Text("연속 선택 재현")
.font(.title2).fontWeight(.semibold)
Text("팝오버를 열고 여러 항목을 **연속으로** 눌러보세요. 아래 상태가 누를 때마다 즉시 "
+ "따라오면 컴포넌트는 정상입니다.")
.font(.callout)
.foregroundStyle(.secondary)
}
}

/// 팝오버 밖에서 본 실제 상태. 팝오버 안 체크와 여기가 어긋나면 **렌더 문제**,
/// 둘 다 틀리면 **쓰기 유실**이다.
private var stateReadout: some View {
VStack(alignment: .leading, spacing: 8) {
Text("selection (\(selection.count)개)")
.font(.headline)
ForEach(items) { item in
HStack(spacing: 8) {
Image(systemName: selection.contains(item.id) ? "checkmark.square.fill" : "square")
.foregroundStyle(selection.contains(item.id) ? .green : .secondary)
Text(item.name)
}
.font(.system(.body, design: .monospaced))
}
if !log.isEmpty {
Divider()
ForEach(log, id: \.self) { Text($0).font(.caption).foregroundStyle(.secondary) }
}
}
.padding(16)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.secondary.opacity(0.08)))
}
}
4 changes: 2 additions & 2 deletions Projects/DVDesign/Sources/Components/DVCategory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ public struct DVCategory: View {
.contentShape(Rectangle())
}
.buttonStyle(DVCategoryButtonStyle(isHovered: isHovered, isSelected: isSelected))
.animation(.easeOut(duration: 0.12), value: isHovered)
.animation(.easeOut(duration: 0.12), value: isSelected)
.animation(MotionMetrics.hover, value: isHovered)
.animation(MotionMetrics.hover, value: isSelected)
.onHover { isHovered = $0 }
}
}
Expand Down
49 changes: 34 additions & 15 deletions Projects/DVDesign/Sources/Components/DVChipsContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import SwiftUI
/// }
/// ```
///
/// 값이 비면 chip 없이 박스만 남는다 — ``DVTextContainer``의 Empty 상태와 같은 취급이며,
/// 이때도 높이는 다른 읽기 전용 필드와 맞도록 32pt를 유지한다.
/// 값이 비면 `placeholder`를 표시한다. 박스만 남겨두면 "아직 안 불러온 것"과 "고른 것이 없는 것"이
/// 구분되지 않는다. `placeholder`가 `nil`이면 빈 박스로 둔다.
/// 어느 쪽이든 높이는 다른 읽기 전용 필드와 맞도록 32pt를 유지한다.
///
/// > Note: chip은 ``DVChip``(내부적으로 `Button`)이라 그대로 두면 포커스를 받는다.
/// > 컨테이너 전체에 `allowsHitTesting(false)`를 걸어 인터랙션을 차단한다.
Expand All @@ -40,37 +41,55 @@ public struct DVChipsContainer: View {
// MARK: - Properties

private let labels: [String]
private let placeholder: String?
private let size: DVComponentSize

// MARK: - Init

/// - Parameters:
/// - labels: chip으로 표시할 텍스트 목록. 빈 배열이면 박스만 표시된다.
/// - labels: chip으로 표시할 텍스트 목록.
/// - placeholder: `labels`가 비었을 때 대신 표시할 문구. `nil`이면 빈 박스.
/// - size: 너비 변형. 기본값 ``DVComponentSize/md``.
public init(
_ labels: [String],
placeholder: String? = nil,
size: DVComponentSize = .md
) {
self.labels = labels
self.placeholder = placeholder
self.size = size
}

// MARK: - Body

public var body: some View {
DVFlowLayout(hSpacing: Metrics.chipsSpacing, vSpacing: Metrics.chipsSpacing) {
// 라벨이 중복될 수 있으므로 값이 아니라 위치를 id로 쓴다.
ForEach(Array(labels.enumerated()), id: \.offset) { _, label in
DVChip(label)
content
.allowsHitTesting(false)
.padding(Metrics.chipsPadding)
.frame(minHeight: Metrics.minHeight)
.dvComponentWidth(size, alignment: .leading)
.background {
RoundedRectangle(cornerRadius: Metrics.cornerRadius)
.fill(Color.dv(.gray300))
}
}

@ViewBuilder
private var content: some View {
if labels.isEmpty, let placeholder {
// 값 텍스트(gray900)가 아니라 안내 문구로 읽혀야 하므로 placeholder 색을 쓴다 —
// `DVTextField`·`DVMultilineTextField`의 placeholder와 같은 취급이다.
Text(placeholder)
.dvFont(.bodyLG)
.foregroundStyle(Color.dv(.gray400))
.frame(maxWidth: .infinity, alignment: .leading)
} else {
DVFlowLayout(hSpacing: Metrics.chipsSpacing, vSpacing: Metrics.chipsSpacing) {
// 라벨이 중복될 수 있으므로 값이 아니라 위치를 id로 쓴다.
ForEach(Array(labels.enumerated()), id: \.offset) { _, label in
DVChip(label)
}
}
}
.allowsHitTesting(false)
.padding(Metrics.chipsPadding)
.frame(minHeight: Metrics.minHeight)
.dvComponentWidth(size, alignment: .leading)
.background {
RoundedRectangle(cornerRadius: Metrics.cornerRadius)
.fill(Color.dv(.gray300))
}
}
}
Expand Down
96 changes: 96 additions & 0 deletions Projects/DVDesign/Sources/Components/DVIconButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright © 2026 Devault. All rights reserved

import SwiftUI

// MARK: - DVIconButton

/// 라벨 없이 아이콘만 있는 작은 버튼. hover·press 되먹임을 준다.
///
/// **색은 호출부가 정한다.** 회색 아이콘은 회색 계열에서, 초록 아이콘은 초록 계열에서
/// 움직여야 버튼의 성격이 유지된다.
public struct DVIconButton: View {

// MARK: - Properties

private let systemName: String
private let font: DVFont
private let idle: DVColor
private let hovered: DVColor
private let pressed: DVColor
/// 색 토큰만으로 press와 hover가 구분되지 않을 때 쓰는 보조 수단.
private let pressedOpacity: Double
private let hitSize: CGFloat
private let action: () -> Void

@State private var isHovered = false

// MARK: - Init

public init(
systemName: String,
font: DVFont = .captionLG,
idle: DVColor,
hovered: DVColor,
pressed: DVColor,
pressedOpacity: Double = 1,
hitSize: CGFloat = 22,
action: @escaping () -> Void
) {
self.systemName = systemName
self.font = font
self.idle = idle
self.hovered = hovered
self.pressed = pressed
self.pressedOpacity = pressedOpacity
self.hitSize = hitSize
self.action = action
}

// MARK: - Body

public var body: some View {
Button(action: action) {
Image(systemName: systemName)
.dvFont(font)
}
.buttonStyle(
DVIconButtonStyle(
isHovered: isHovered,
idle: idle,
hovered: hovered,
pressed: pressed,
pressedOpacity: pressedOpacity,
hitSize: hitSize
)
)
.onHover { isHovered = $0 }
.animation(MotionMetrics.hover, value: isHovered)
}
}

// MARK: - ButtonStyle

private struct DVIconButtonStyle: ButtonStyle {

let isHovered: Bool
let idle: DVColor
let hovered: DVColor
let pressed: DVColor
let pressedOpacity: Double
let hitSize: CGFloat

func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(Color.dv(tint(isPressed: configuration.isPressed)))
.opacity(configuration.isPressed ? pressedOpacity : 1)
// 배경이 없으면 넓힌 영역이 클릭을 받지 못한다.
.frame(width: hitSize, height: hitSize)
.contentShape(Rectangle())
.animation(MotionMetrics.hover, value: configuration.isPressed)
}

private func tint(isPressed: Bool) -> DVColor {
if isPressed { return pressed }
return isHovered ? hovered : idle
}
}
Loading