From e58ce962ff1e0aea9c9ba6a5e25d8899be47ed69 Mon Sep 17 00:00:00 2001 From: KrLite Date: Mon, 10 Aug 2026 20:41:12 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=90=9E=20Repair=20picker=20interactio?= =?UTF-8?q?ns=20and=20styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Composes/LuminarePickerMenu.swift | 26 +++++++----- .../Components/LuminareCompactPicker.swift | 42 ++++++------------- .../Components/LuminareCompactPicker.md | 6 ++- 3 files changed, 32 insertions(+), 42 deletions(-) diff --git a/Sources/Luminare/Components/Composes/LuminarePickerMenu.swift b/Sources/Luminare/Components/Composes/LuminarePickerMenu.swift index bc3be138..d4b3637b 100644 --- a/Sources/Luminare/Components/Composes/LuminarePickerMenu.swift +++ b/Sources/Luminare/Components/Composes/LuminarePickerMenu.swift @@ -10,18 +10,18 @@ import SwiftUI // MARK: - Picker Menu (Compose) public struct LuminarePickerMenu: View -where Label: View, Option: View, Item: Hashable { + where Label: View, Option: View, Item: Hashable { @Environment(\.luminareSectionHorizontalPadding) private var horizontalPadding - + // MARK: Fields - + @ViewBuilder private var label: () -> Label @Binding private var selection: Item let items: [Item] @ViewBuilder private var itemToView: (Item) -> Option - + // MARK: Initializers - + public init( selection: Binding, items: [Item], @@ -64,14 +64,14 @@ where Label: View, Option: View, Item: Hashable { Text(titleKey) } } - + // MARK: Body - + public var body: some View { LuminareCompose { HStack(spacing: 8) { itemToView(selection) - + Image(systemName: "chevron.up.chevron.down") .font(.caption2) .padding(4) @@ -85,9 +85,14 @@ where Label: View, Option: View, Item: Hashable { .tag(item) } } - .opacity(0.001) - .fixedSize() + .labelsHidden() + .pickerStyle(.menu) + .buttonStyle(.borderless) + .frame(maxWidth: .infinity, maxHeight: .infinity) .contentShape(.rect) + .mask { + Color.clear + } } } label: { label() @@ -116,4 +121,3 @@ where Label: View, Option: View, Item: Hashable { } .frame(width: 300) } - diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift index aa4caff2..d25309c8 100644 --- a/Sources/Luminare/Components/LuminareCompactPicker.swift +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -30,6 +30,7 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha // MARK: Environments @Environment(\.luminareCompactPickerStyle) private var style + @Environment(\.luminareMinHeight) private var minHeight // MARK: Fields @@ -64,6 +65,9 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha .pickerStyle(.menu) .buttonStyle(.borderless) .padding(.trailing, -2) + .frame(minHeight: minHeight) + .luminareSurface(isHovering: isHovering, style: .flat) + .luminareFilledStates(.all) case .segmented: UnaryVariadicView(content()) { children in SegmentedVariadic( @@ -73,9 +77,9 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha ) } .padding(.horizontal, 4) + .luminareSurface(style: .flat) } } - .luminareSurface(style: .flat) .onHover { isHovering = $0 } } @@ -105,7 +109,7 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha index: index, maxIndex: children.count - 1 ) - .foregroundStyle(isHovering && selection == value ? .primary : .secondary) + .foregroundStyle(selection == value ? .primary : .secondary) .zIndex(1) if hasDividers, @@ -117,6 +121,7 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha } } } + .frame(minHeight: minHeight, maxHeight: .infinity) } struct SegmentedKnob: View { @@ -143,10 +148,11 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha } } label: { child - .frame(maxWidth: .infinity, minHeight: minHeight - 8) + .frame(maxWidth: .infinity, minHeight: minHeight - 8, maxHeight: .infinity) .padding(.horizontal, 8) } .buttonStyle(.borderless) + .frame(minHeight: minHeight - 8, maxHeight: .infinity) .onHover { isHovering = $0 } .background { Group { @@ -162,7 +168,7 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha } } .padding(.vertical, 4) - .frame(minHeight: minHeight) + .frame(minHeight: minHeight, maxHeight: .infinity) } private var constrainedCornerRadii: RectangleCornerRadii { @@ -181,24 +187,8 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha } private func knob() -> some View { - Group { - if isParentHovering { - Rectangle() - .foregroundStyle(.background.opacity(0.8)) - } else { - // The `.blendMode()` prevents `.quinary` style to be clipped - Rectangle() - .foregroundStyle(.quinary.blendMode(.luminosity)) - } - } - .overlay { - if isHovering { - Rectangle() - .foregroundStyle(.background.opacity(0.2)) - .blendMode(.luminosity) - } - } - .clipShape(.rect(cornerRadii: constrainedCornerRadii)) + UnevenRoundedRectangle(cornerRadii: constrainedCornerRadii) + .foregroundStyle(isHovering ? .quaternary : .quinary) } } } @@ -213,7 +203,7 @@ private struct PickerPreview: View where V: Hashable & Equatable { var body: some View { LuminareCompactPicker(selection: $selection) { ForEach(elements, id: \.self) { element in - Text("\(element)") + Text(verbatim: "\(element)") } } } @@ -225,12 +215,6 @@ private struct PickerPreview: View where V: Hashable & Equatable { traits: .sizeThatFitsLayout ) { LuminareSection { - LuminareCompose("Button") { - Button {} label: { - Text("42") - } - } - LuminareCompose("Pick from a menu") { PickerPreview(elements: Array(0 ..< 200), selection: 42) } diff --git a/Sources/Luminare/Luminare.docc/Components/LuminareCompactPicker.md b/Sources/Luminare/Luminare.docc/Components/LuminareCompactPicker.md index c8304620..b138b3ee 100644 --- a/Sources/Luminare/Luminare.docc/Components/LuminareCompactPicker.md +++ b/Sources/Luminare/Luminare.docc/Components/LuminareCompactPicker.md @@ -15,11 +15,12 @@ @Row { @Column(size: 3) { ```swift - LuminareCompactPicker(selection: $selection, style: .menu) { + LuminareCompactPicker(selection: $selection) { ForEach(data, id: \.keyPath) { element in // Content } } + .luminareCompactPickerStyle(.menu) ``` } @@ -31,7 +32,7 @@ @Row { @Column(size: 2) { ```swift - LuminareCompactPicker(selection: $selection, style: .segmented) { + LuminareCompactPicker(selection: $selection) { ForEach(data, id: \.self) { element in Group { // Content @@ -39,6 +40,7 @@ .id(element) } } + .luminareCompactPickerStyle(.segmented) ``` } From b4b0402e9ea4005e2055eb84a29576816623c141 Mon Sep 17 00:00:00 2001 From: KrLite Date: Mon, 10 Aug 2026 20:59:14 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=90=9E=20Restore=20picker=20menu=20co?= =?UTF-8?q?ntents=20and=20styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Composes/LuminarePickerMenu.swift | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/Sources/Luminare/Components/Composes/LuminarePickerMenu.swift b/Sources/Luminare/Components/Composes/LuminarePickerMenu.swift index d4b3637b..31bb688c 100644 --- a/Sources/Luminare/Components/Composes/LuminarePickerMenu.swift +++ b/Sources/Luminare/Components/Composes/LuminarePickerMenu.swift @@ -69,31 +69,35 @@ public struct LuminarePickerMenu: View public var body: some View { LuminareCompose { - HStack(spacing: 8) { - itemToView(selection) - - Image(systemName: "chevron.up.chevron.down") - .font(.caption2) - .padding(4) - .luminareSurface() - .luminareCornerRadius(8) - } - .overlay { - Picker("", selection: $selection) { - ForEach(items, id: \.self) { item in + Menu { + ForEach(items, id: \.self) { item in + Toggle(isOn: Binding( + get: { selection == item }, + set: { isSelected in + if isSelected { + selection = item + } + } + )) { itemToView(item) - .tag(item) } } - .labelsHidden() - .pickerStyle(.menu) - .buttonStyle(.borderless) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .contentShape(.rect) - .mask { - Color.clear + } label: { + HStack(spacing: 8) { + itemToView(selection) + + Image(systemName: "chevron.up.chevron.down") + .font(.caption2) + .padding(4) + .luminareSurface() + .luminareCornerRadius(8) } + .contentShape(.rect) } + .menuStyle(.button) + .buttonStyle(.plain) + .menuIndicator(.hidden) + .fixedSize() } label: { label() } From 3ed5fa5622e0f26c78d3d38b2aeff3ee7af2b244 Mon Sep 17 00:00:00 2001 From: KrLite Date: Mon, 10 Aug 2026 21:12:01 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=92=84=20Refine=20compact=20picker=20?= =?UTF-8?q?spacing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Luminare/Components/LuminareCompactPicker.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift index d25309c8..aa76a944 100644 --- a/Sources/Luminare/Components/LuminareCompactPicker.swift +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -100,6 +100,11 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha HStack(spacing: 4) { ForEach(Array(children.enumerated()), id: \.offset) { index, child in if let value = child.id(as: V.self) { + let nextValue: V? = if index < children.count - 1 { + children[index + 1].id(as: V.self) + } else { + nil + } SegmentedKnob( child: child, namespace: namespace, @@ -116,6 +121,7 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha child.id != children.last?.id { Divider() .frame(height: minHeight / 2) + .opacity(selection == value || selection == nextValue ? 0 : 1) .zIndex(0) } } @@ -149,7 +155,7 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha } label: { child .frame(maxWidth: .infinity, minHeight: minHeight - 8, maxHeight: .infinity) - .padding(.horizontal, 8) + .padding(.horizontal, 12) } .buttonStyle(.borderless) .frame(minHeight: minHeight - 8, maxHeight: .infinity) From 306b9b188d29e7f0a7dd92d71c92aadba1aa8bf7 Mon Sep 17 00:00:00 2001 From: KrLite Date: Mon, 10 Aug 2026 21:20:46 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=90=9E=20Preserve=20compact=20picker?= =?UTF-8?q?=20intrinsic=20height?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Luminare/Components/LuminareCompactPicker.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift index aa76a944..03bc17c5 100644 --- a/Sources/Luminare/Components/LuminareCompactPicker.swift +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -127,7 +127,7 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha } } } - .frame(minHeight: minHeight, maxHeight: .infinity) + .frame(minHeight: minHeight) } struct SegmentedKnob: View { From 1f8075b51357be83c69b674db8cab38090fa2547 Mon Sep 17 00:00:00 2001 From: KrLite Date: Mon, 10 Aug 2026 21:30:47 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=90=9E=20Bound=20compact=20picker=20h?= =?UTF-8?q?eight=20proposals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/LuminareCompactPicker.swift | 94 +++++++++++++------ 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift index 03bc17c5..40480642 100644 --- a/Sources/Luminare/Components/LuminareCompactPicker.swift +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -97,37 +97,77 @@ public struct LuminareCompactPicker: View where Content: View, V: Ha @Namespace private var namespace var body: some View { - HStack(spacing: 4) { - ForEach(Array(children.enumerated()), id: \.offset) { index, child in - if let value = child.id(as: V.self) { - let nextValue: V? = if index < children.count - 1 { - children[index + 1].id(as: V.self) - } else { - nil - } - SegmentedKnob( - child: child, - namespace: namespace, - isParentHovering: isHovering, - selection: $selection, - value: value, - index: index, - maxIndex: children.count - 1 - ) - .foregroundStyle(selection == value ? .primary : .secondary) - .zIndex(1) - - if hasDividers, - child.id != children.last?.id { - Divider() - .frame(height: minHeight / 2) - .opacity(selection == value || selection == nextValue ? 0 : 1) - .zIndex(0) + ProposedHeightLayout(minimumHeight: minHeight) { + HStack(spacing: 4) { + ForEach(Array(children.enumerated()), id: \.offset) { index, child in + if let value = child.id(as: V.self) { + let nextValue: V? = if index < children.count - 1 { + children[index + 1].id(as: V.self) + } else { + nil + } + SegmentedKnob( + child: child, + namespace: namespace, + isParentHovering: isHovering, + selection: $selection, + value: value, + index: index, + maxIndex: children.count - 1 + ) + .foregroundStyle(selection == value ? .primary : .secondary) + .zIndex(1) + + if hasDividers, + child.id != children.last?.id { + Divider() + .frame(height: minHeight / 2) + .opacity(selection == value || selection == nextValue ? 0 : 1) + .zIndex(0) + } } } } } - .frame(minHeight: minHeight) + } + + struct ProposedHeightLayout: Layout { + let minimumHeight: CGFloat + + func sizeThatFits( + proposal: ProposedViewSize, + subviews: Subviews, + cache _: inout () + ) -> CGSize { + guard let subview = subviews.first else { + return .zero + } + + let proposedWidth = proposal.width.flatMap { $0.isFinite ? $0 : nil } + let proposedHeight = proposal.height.flatMap { $0.isFinite ? $0 : nil } + let size = subview.sizeThatFits(.init( + width: proposedWidth, + height: proposedHeight + )) + + return .init( + width: proposedWidth ?? size.width, + height: max(minimumHeight, proposedHeight ?? size.height) + ) + } + + func placeSubviews( + in bounds: CGRect, + proposal _: ProposedViewSize, + subviews: Subviews, + cache _: inout () + ) { + subviews.first?.place( + at: bounds.origin, + anchor: .topLeading, + proposal: .init(bounds.size) + ) + } } struct SegmentedKnob: View {