- 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>
This commit is contained in:
@@ -54,33 +54,82 @@ private struct PairingWelcomeView: View {
|
||||
let onNFCRequested: () -> Void
|
||||
|
||||
var body: some View {
|
||||
AppScrollScreen(compactLayout: true, bottomPadding: 170) {
|
||||
PairingHero()
|
||||
GeometryReader { proxy in
|
||||
let topInset = proxy.safeAreaInsets.top
|
||||
|
||||
PairingStepsCard()
|
||||
ZStack(alignment: .bottom) {
|
||||
AppScrollScreen(compactLayout: true, bottomPadding: 220) {
|
||||
Color.clear.frame(height: topInset + 8)
|
||||
.padding(.top, -AppLayout.compactVerticalPadding)
|
||||
|
||||
PairingChecklistCard()
|
||||
PairingHero()
|
||||
|
||||
PairingStepsCard()
|
||||
|
||||
PairingChecklistCard()
|
||||
}
|
||||
.ignoresSafeArea(.container, edges: .vertical)
|
||||
|
||||
EdgeFade(edge: .top, height: topInset)
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.ignoresSafeArea()
|
||||
.allowsHitTesting(false)
|
||||
|
||||
EdgeFade(edge: .bottom, height: 200)
|
||||
.frame(maxHeight: .infinity, alignment: .bottom)
|
||||
.ignoresSafeArea()
|
||||
.allowsHitTesting(false)
|
||||
|
||||
PairingActionDock(
|
||||
isAuthenticating: model.isAuthenticating,
|
||||
onScanRequested: onScanRequested,
|
||||
onNFCRequested: onNFCRequested
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle("")
|
||||
.toolbar(.hidden, for: .navigationBar)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
PairingActionDock(
|
||||
isAuthenticating: model.isAuthenticating,
|
||||
onScanRequested: onScanRequested,
|
||||
onNFCRequested: onNFCRequested
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct EdgeFade: View {
|
||||
enum Edge { case top, bottom }
|
||||
let edge: Edge
|
||||
let height: CGFloat
|
||||
|
||||
var body: some View {
|
||||
LinearGradient(
|
||||
stops: edge == .top ? [
|
||||
.init(color: Color.idpGroupedBackground, location: 0.0),
|
||||
.init(color: Color.idpGroupedBackground.opacity(0.95), location: 0.4),
|
||||
.init(color: Color.idpGroupedBackground.opacity(0.0), location: 1.0)
|
||||
] : [
|
||||
.init(color: Color.idpGroupedBackground.opacity(0.0), location: 0.0),
|
||||
.init(color: Color.idpGroupedBackground.opacity(0.95), location: 0.6),
|
||||
.init(color: Color.idpGroupedBackground, location: 1.0)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.frame(height: height)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PairingHero: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
HeroGlyph()
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(spacing: 10) {
|
||||
HeroGlyph()
|
||||
ShadcnBadge(
|
||||
title: "Passport setup",
|
||||
tone: .accent,
|
||||
leading: Image(systemName: "lock.shield")
|
||||
)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Your passport, in your pocket")
|
||||
.font(.system(size: 30, weight: .bold, design: .default))
|
||||
.font(.title2.weight(.bold))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text("Link this iPhone to your idp.global web session to approve sign-ins, receive security alerts, and prove your identity with NFC.")
|
||||
@@ -90,66 +139,44 @@ private struct PairingHero: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 8)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
|
||||
private struct HeroGlyph: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 26, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
IdP.tint,
|
||||
IdP.tint.opacity(0.78)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 76, height: 76)
|
||||
.shadow(color: IdP.tint.opacity(0.35), radius: 18, x: 0, y: 10)
|
||||
|
||||
RoundedRectangle(cornerRadius: 26, style: .continuous)
|
||||
.stroke(Color.white.opacity(0.22), lineWidth: 1)
|
||||
.frame(width: 76, height: 76)
|
||||
RoundedRectangle(cornerRadius: IdP.controlRadius, style: .continuous)
|
||||
.fill(IdP.tint)
|
||||
|
||||
Image(systemName: "shield.lefthalf.filled")
|
||||
.font(.system(size: 34, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.foregroundStyle(Color.idpPrimaryForeground)
|
||||
}
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PairingStepsCard: View {
|
||||
var body: some View {
|
||||
AppPanel(compactLayout: true, radius: 22) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("How pairing works")
|
||||
.font(.headline)
|
||||
Text("Three quick steps to finish setup.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color.idpMutedForeground)
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
AppSectionCard(
|
||||
title: "How pairing works",
|
||||
subtitle: "Three quick steps to finish setup.",
|
||||
compactLayout: true
|
||||
) {
|
||||
VStack(spacing: 14) {
|
||||
PairingStepRow(
|
||||
number: 1,
|
||||
title: "Start from the web",
|
||||
message: "Open idp.global in your browser and begin a new device pairing."
|
||||
)
|
||||
|
||||
StepDivider()
|
||||
|
||||
PairingStepRow(
|
||||
number: 2,
|
||||
title: "Scan the pairing QR",
|
||||
message: "Point this iPhone at the QR shown by that browser session."
|
||||
)
|
||||
|
||||
StepDivider()
|
||||
|
||||
PairingStepRow(
|
||||
number: 3,
|
||||
title: "Approve future sign-ins",
|
||||
@@ -162,15 +189,11 @@ private struct PairingStepsCard: View {
|
||||
|
||||
private struct PairingChecklistCard: View {
|
||||
var body: some View {
|
||||
AppPanel(compactLayout: true, radius: 22) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Before you scan")
|
||||
.font(.headline)
|
||||
Text("A few quick checks help the link complete cleanly.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color.idpMutedForeground)
|
||||
}
|
||||
|
||||
AppSectionCard(
|
||||
title: "Before you scan",
|
||||
subtitle: "A few quick checks help the link complete cleanly.",
|
||||
compactLayout: true
|
||||
) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
PairingNoteRow(
|
||||
icon: "safari",
|
||||
@@ -222,39 +245,31 @@ private struct PairingActionDock: View {
|
||||
.buttonStyle(SecondaryActionStyle())
|
||||
.disabled(isAuthenticating)
|
||||
}
|
||||
.padding(16)
|
||||
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 14)
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(.regularMaterial)
|
||||
.ignoresSafeArea()
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(Color.idpSeparator.opacity(0.5))
|
||||
.frame(height: 0.5)
|
||||
}
|
||||
)
|
||||
.padding(.bottom, 12)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct PairingStepRow: View {
|
||||
let number: Int
|
||||
let title: String
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 14) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(IdP.tint.opacity(0.14))
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
.fill(IdP.tint.opacity(0.12))
|
||||
Text("\(number)")
|
||||
.font(.subheadline.weight(.bold))
|
||||
.font(.caption.weight(.bold))
|
||||
.foregroundStyle(IdP.tint)
|
||||
}
|
||||
.frame(width: 30, height: 30)
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(message)
|
||||
@@ -265,19 +280,6 @@ private struct PairingStepRow: View {
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
}
|
||||
|
||||
private struct StepDivider: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 14) {
|
||||
Rectangle()
|
||||
.fill(Color.idpSeparator.opacity(0.6))
|
||||
.frame(width: 1, height: 10)
|
||||
.padding(.leading, 14)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,14 +290,14 @@ private struct PairingNoteRow: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(IdP.tint)
|
||||
.frame(width: 26, height: 26)
|
||||
.background(IdP.tint.opacity(0.12), in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
.frame(width: 24, height: 24)
|
||||
.background(IdP.tint.opacity(0.12), in: RoundedRectangle(cornerRadius: 6, style: .continuous))
|
||||
|
||||
Text(text)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color.idpForeground.opacity(0.82))
|
||||
.foregroundStyle(Color.idpForeground)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Reference in New Issue
Block a user