From 70de58f4463fb7d90c96b96c8e6d8d7247c558bf Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 7 Aug 2026 14:50:01 +0200 Subject: [PATCH] fix(client/apple): the tray blur goes full-bleed by layout, and its frost sinks to black Round-3 field findings: the pop-in had retreated to the X axis alone - the one growth still left to safe-area resolution (the landscape side insets), which settles a beat after insertion, outside any geometry group and outside the view's own transaction. The scrim now reaches full-bleed purely by layout: a fixed 80 pt negative-padding overhang on the outer edge and both sides replaces ignoresSafeArea entirely, so every axis is deterministic from the first frame (and orientation no longer changes the gradient's proportions). The mask's strong region moves to 0.65 to account for the overhang leading the gradient. And the frost reads black now, not grey: an ink.shade(0.35) wash inside the mask sinks the material's luminance lift toward the palette's shade - black on a dark field, palette-honest on a pale one. --- .../PunktfunkClient/Home/GamepadChrome.swift | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift index 44334423..b35097c0 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift @@ -380,12 +380,18 @@ struct GamepadTrayScrim: View { // to keep the pinned title legible, so it has to frost dark under white ink and // light under dark ink. .environment(\.colorScheme, ink.isLight ? .light : .dark) - // Fade the whole blur out toward the content so it dissolves rather than ending on a line. + // Sink the material's grey luminance lift toward the palette's shade (black on a + // dark field — field ask: the frost read GREY over the aurora). Inside the mask, so + // the tint dissolves with the blur. + .overlay(ink.shade(0.35)) + // Fade the whole blur out toward the content so it dissolves rather than ending on a + // line. The strong region sits deep (0.65) because the first stretch of the gradient + // now runs over the fixed 80 pt outer overhang below. .mask { LinearGradient( stops: [ .init(color: .black, location: 0), - .init(color: .black.opacity(0.92), location: 0.55), + .init(color: .black.opacity(0.92), location: 0.65), .init(color: .clear, location: 1), ], startPoint: fromEdge, endPoint: toContent) @@ -395,12 +401,17 @@ struct GamepadTrayScrim: View { // gets the longer runway: its tray sits over SCROLLING rows plus the detail line, and // the field verdict on the short reach was rows colliding visibly with the legend. .padding(edge == .top ? .bottom : .top, edge == .top ? -44 : -72) - .ignoresSafeArea() - // This view's geometry must NEVER animate: mounted inside a pushed shell layer, the - // full-bleed growth above (the negative padding and the safe-area expansion) rode the - // push's transaction, so the blur visibly entered at content bounds and grew into - // place — and `.geometryGroup()` on the layer does not reach safe-area resolution. - // The layer's own fade/slide still carries the scrim; only its SHAPE is pinned. + // Full-bleed by LAYOUT, not by `.ignoresSafeArea()`: safe-area expansion resolves a + // beat after insertion (outside any geometry group and outside this view's own + // transaction), which is exactly the pop the field kept seeing — vertically first, + // then, once the vertical runway became padding, on the X axis alone (the landscape + // side insets). 80 pt clears every inset on every device; backgrounds never clip, + // so the overhang simply draws. + .padding(edge == .top ? .top : .bottom, -80) + .padding(.horizontal, -80) + // And the shape must NEVER animate: mounted inside a pushed shell layer, any late + // geometry would ride the push's transaction and visibly grow into place. The + // layer's own fade/slide still carries the scrim; only its SHAPE is pinned. .transaction { $0.animation = nil } } }