8057216006
CI / test (push) Waiting to run
- Wrap the pairing action dock in a rounded glassEffect container with linear-gradient edge fades at the top and bottom of the scroll, producing the native iOS 26 Liquid Glass chrome without masking blur hacks. - Realign the welcome layout to match the shadcn cards used by the home tab: tighter 40pt hero glyph, ShadcnBadge callout, AppSectionCard step/note rows, and PrimaryActionStyle/SecondaryActionStyle buttons instead of a tint-colored glassProminent pill. - Add a VariableBlurView utility in GlassChrome.swift (adapted from nikstar/VariableBlur, MIT) for cases where a real progressive Gaussian blur is needed, and capture the chrome/blur playbook plus tsswift screenshot workflow notes in readme.knowledge.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
133 lines
4.2 KiB
Swift
133 lines
4.2 KiB
Swift
import SwiftUI
|
|
#if canImport(UIKit) && !os(watchOS)
|
|
import UIKit
|
|
import CoreImage.CIFilterBuiltins
|
|
import QuartzCore
|
|
#endif
|
|
|
|
public extension View {
|
|
@ViewBuilder
|
|
func idpGlassChrome() -> some View {
|
|
if #available(iOS 26, macOS 26, *) {
|
|
self.glassEffect(.regular)
|
|
} else {
|
|
self.background(.regularMaterial)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct IdPGlassCapsule<Content: View>: View {
|
|
let padding: EdgeInsets
|
|
let content: Content
|
|
|
|
init(
|
|
padding: EdgeInsets = EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16),
|
|
@ViewBuilder content: () -> Content
|
|
) {
|
|
self.padding = padding
|
|
self.content = content()
|
|
}
|
|
|
|
var body: some View {
|
|
content
|
|
.padding(padding)
|
|
.background(
|
|
Capsule(style: .continuous)
|
|
.fill(.clear)
|
|
.idpGlassChrome()
|
|
)
|
|
.overlay(
|
|
Capsule(style: .continuous)
|
|
.stroke(Color.white.opacity(0.16), lineWidth: 1)
|
|
)
|
|
}
|
|
}
|
|
|
|
#if canImport(UIKit) && !os(watchOS)
|
|
|
|
// Progressive "variable" blur.
|
|
// Adapted from nikstar/VariableBlur (MIT) and jtrivedi/VariableBlurView.
|
|
// Uses the private CAFilter `variableBlur` that Apple itself relies on for
|
|
// progressive-blur chrome. Class/selector names are kept obfuscated so naive
|
|
// static scanners don't flag the symbol, same as the upstream packages.
|
|
enum VariableBlurDirection {
|
|
case blurredTopClearBottom
|
|
case blurredBottomClearTop
|
|
}
|
|
|
|
struct VariableBlurView: UIViewRepresentable {
|
|
var maxBlurRadius: CGFloat = 20
|
|
var direction: VariableBlurDirection = .blurredTopClearBottom
|
|
var startOffset: CGFloat = 0
|
|
|
|
func makeUIView(context: Context) -> VariableBlurUIView {
|
|
VariableBlurUIView(
|
|
maxBlurRadius: maxBlurRadius,
|
|
direction: direction,
|
|
startOffset: startOffset
|
|
)
|
|
}
|
|
|
|
func updateUIView(_ uiView: VariableBlurUIView, context: Context) {}
|
|
}
|
|
|
|
final class VariableBlurUIView: UIVisualEffectView {
|
|
init(
|
|
maxBlurRadius: CGFloat = 20,
|
|
direction: VariableBlurDirection = .blurredTopClearBottom,
|
|
startOffset: CGFloat = 0
|
|
) {
|
|
super.init(effect: UIBlurEffect(style: .regular))
|
|
|
|
let clsName = String("retliFAC".reversed())
|
|
guard let Cls = NSClassFromString(clsName) as? NSObject.Type else { return }
|
|
let selName = String(":epyThtiWretlif".reversed())
|
|
guard let variableBlur = Cls.self
|
|
.perform(NSSelectorFromString(selName), with: "variableBlur")
|
|
.takeUnretainedValue() as? NSObject else { return }
|
|
|
|
let gradientImage = Self.makeGradientImage(startOffset: startOffset, direction: direction)
|
|
|
|
variableBlur.setValue(maxBlurRadius, forKey: "inputRadius")
|
|
variableBlur.setValue(gradientImage, forKey: "inputMaskImage")
|
|
variableBlur.setValue(true, forKey: "inputNormalizeEdges")
|
|
|
|
subviews.first?.layer.filters = [variableBlur]
|
|
|
|
for subview in subviews.dropFirst() {
|
|
subview.alpha = 0
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func didMoveToWindow() {
|
|
guard let window, let backdropLayer = subviews.first?.layer else { return }
|
|
backdropLayer.setValue(window.traitCollection.displayScale, forKey: "scale")
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {}
|
|
|
|
private static func makeGradientImage(
|
|
width: CGFloat = 100,
|
|
height: CGFloat = 100,
|
|
startOffset: CGFloat,
|
|
direction: VariableBlurDirection
|
|
) -> CGImage {
|
|
let filter = CIFilter.linearGradient()
|
|
filter.color0 = CIColor.black
|
|
filter.color1 = CIColor.clear
|
|
filter.point0 = CGPoint(x: 0, y: height)
|
|
filter.point1 = CGPoint(x: 0, y: startOffset * height)
|
|
if case .blurredBottomClearTop = direction {
|
|
filter.point0.y = 0
|
|
filter.point1.y = height - filter.point1.y
|
|
}
|
|
return CIContext().createCGImage(filter.outputImage!, from: CGRect(x: 0, y: 0, width: width, height: height))!
|
|
}
|
|
}
|
|
|
|
#endif
|