nvrsion: Add revert logic to Sources/NucleicApp/BuildBanner.swift, Sources/NucleicApp/ToolbarGlassKeeper.swift, and ios/NucleicRemote/NucleicRemote/Views/BuildBanner.swift to restore prior state, and fix sidebar color distortion in Sources/NucleicApp/RootView.swift.

Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-07 01:53:08 -07:00
co-authored by nucleic
parent 9d339459c9
commit d0f8de0509
4 changed files with 74 additions and 62 deletions
+16 -13
View File
@@ -66,11 +66,12 @@ struct BuildInfo {
}
}
/// Header warning that the window is a non-release build. A channel-colored stripe red for
/// locally-built / pre-prod copies, canary-yellow for canary, blue for beta, gold for a
/// release candidate spans the full width of the detail pane, with its content (channel
/// name, commit hash, and an all-caps "EXPERIMENTAL" pill) held at the leading edge. Renders
/// nothing for a shipping App Store release.
/// Header strip warning that the window is a non-release build: a red tint for
/// locally-built / pre-prod copies, canary-yellow for canary, blue for beta, gold for a release candidate. The
/// leading edge names the channel; the commit hash and an all-caps "EXPERIMENTAL"
/// badge sit on the trailing
/// edge. The tint is kept translucent so it reads as a quiet marker rather than
/// fighting the window background. Renders nothing for a shipping App Store release.
struct BuildBanner: View {
var info: BuildInfo = .current
@@ -94,12 +95,14 @@ struct BuildBanner: View {
}
.foregroundStyle(.white.opacity(0.92))
.padding(.horizontal, 12)
.padding(.vertical, 3)
.frame(maxWidth: .infinity, alignment: .leading)
// The channel-colored stripe fills the full width and height of the top area, with
// the content (channel name, commit, and the EXPERIMENTAL pill) laid across it
// the channel name at the leading edge, the commit and pill at the trailing edge.
.background(style.tint.opacity(0.9))
.padding(.vertical, 5)
.frame(maxWidth: .infinity)
// A plain translucent tint, not its own glass: this strip sits directly
// under the window's unified toolbar, whose Liquid Glass samples and blurs it
// so the *toolbar* is the glassy header and this is just its colored
// backdrop. Giving the strip its own `glassEffect` drew a bright glass edge on
// it; the toolbar's own glass is pinned visible in `RootView` instead.
.background(style.tint.opacity(0.5))
.help(style.tooltip(version: info.version, build: info.buildLabel))
}
}
@@ -111,7 +114,7 @@ struct BuildBanner: View {
var icon: String
// Most channels share the default glyph size; the canary bell-with-waves
// glyph is unusually wide, so it's shrunk to sit in line with its label.
var iconSize: CGFloat = 13
var iconSize: CGFloat = 17
init?(channel: BuildChannel) {
switch channel {
@@ -123,7 +126,7 @@ struct BuildBanner: View {
tint = Color(red: 1.00, green: 0.87, blue: 0.00) // bright canary yellow
label = "Canary Build"
icon = "bell.and.waves.left.and.right.fill"
iconSize = 10
iconSize = 13
case .beta:
tint = Color(red: 0.13, green: 0.40, blue: 0.86) // blue
label = "Public Beta"
+31 -3
View File
@@ -90,9 +90,6 @@ struct RootView: View {
// above it. It used to sit here in the VStack, outside the List's scroll.
sidebarList
}
// No opaque fill here: like the Settings sidebar, let NavigationSplitView's
// built-in sidebar glass material show through. `.scrollContentBackground(.hidden)`
// on the list keeps the List from painting over it.
.background {
GeometryReader { proxy in
Color.clear
@@ -100,6 +97,16 @@ struct RootView: View {
.onChange(of: proxy.size.width) { _, new in sidebarWidth = new }
}
}
// Back the column with Safari's sidebar material (see `SidebarGlassBackground`).
// NavigationSplitView's built-in Liquid Glass pane mirrors the *detail pane's*
// content behind the sidebar, so the BuildBanner's saturated stripe refracted a
// smeared tint across the sidebar's top. This explicit backdrop sits between that
// mirror layer and the sidebar content: it blurs only what's behind the window
// (the desktop the window is non-opaque, see `WindowTranslucencyConfigurator`),
// never in-window content, which is exactly how Safari's sidebar stays neutral
// beside vividly-colored page content. Extends through the safe area so the
// title-bar region above the list is shielded too.
.background { SidebarGlassBackground().ignoresSafeArea() }
// Sample one recent-row's height (hidden) so the swappable section can cap
// itself at eight cards regardless of the user's text-size setting.
.background(alignment: .top) { cardHeightSampler }
@@ -1258,6 +1265,27 @@ extension SessionStatus {
}
}
/// The sidebar's explicit backdrop the same glass Safari uses for its sidebar and top
/// bar: the system `.sidebar` material with *behind-window* blending, which samples only
/// what's behind the window (the desktop). The split view's own Liquid Glass sidebar pane
/// samples in-window content too it mirrors and blurs the detail pane's leading edge
/// under the column so the BuildBanner's bright channel stripe smeared a distorted tint
/// across the sidebar (canary yellow at the top of a canary build). Layered as the column's
/// background, this material draws over that mirror layer and replaces it with the neutral
/// desktop blur, while staying visually identical to the built-in sidebar glass everywhere
/// the detail content is calm.
private struct SidebarGlassBackground: NSViewRepresentable {
func makeNSView(context: Context) -> NSVisualEffectView {
let view = NSVisualEffectView()
view.material = .sidebar
view.blendingMode = .behindWindow
view.state = .followsWindowActiveState
return view
}
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {}
}
/// Clears the host window's opaque backing so the sidebar's behind-window glass material
/// blurs the desktop instead of the window's own content. The detail pane keeps its own
/// opaque `AppTheme.background` fill, so only the material-backed sidebar region turns
+18 -34
View File
@@ -9,13 +9,10 @@ import AppKit
/// dormant.
///
/// We can't observe the dropout directly, but it's reliably cured by a relayout (the
/// resize the user reaches for). So on every event that's been seen to leave the material
/// dormant the window being revealed, refocused, un-minimized, or moved to another
/// display, and the app reactivating or un-hiding we reproduce that relayout by nudging
/// the window frame a pixel and immediately back. The two `setFrame` calls coalesce within
/// one runloop pass, so there's no visible jump, but the toolbar re-composites its glass.
/// (A dropout that happens while the window stays key and frontmost isn't covered by any of
/// these; it heals on the next focus change.)
/// resize the user reaches for). So on the events that trigger it the window being
/// revealed, the app reactivating we reproduce that relayout by nudging the window
/// frame a pixel and immediately back. The two `setFrame` calls coalesce within one
/// runloop pass, so there's no visible jump, but the toolbar re-composites its glass.
struct ToolbarGlassKeeper: NSViewRepresentable {
func makeNSView(context: Context) -> NSView {
let view = NSView(frame: .zero)
@@ -38,38 +35,25 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
private weak var window: NSWindow?
private var observers: [NSObjectProtocol] = []
/// Idempotent: registers once for the window / app events that leave the toolbar
/// material dormant.
/// Idempotent: registers once for the window's reveal / app-reactivation events.
func attach(to window: NSWindow?) {
guard observers.isEmpty, let window else { return }
self.window = window
let center = NotificationCenter.default
// Window-scoped triggers: the material commonly drops while the window is
// occluded, unfocused, minimized, or on another display, and doesn't resume
// when that resolves.
let windowEvents: [NSNotification.Name] = [
NSWindow.didChangeOcclusionStateNotification,
NSWindow.didBecomeKeyNotification,
NSWindow.didDeminiaturizeNotification,
NSWindow.didChangeScreenNotification,
observers = [
// Fully-occluded visible again: the material commonly drops while
// hidden and doesn't resume on reveal.
center.addObserver(
forName: NSWindow.didChangeOcclusionStateNotification,
object: window, queue: .main
) { [weak self] _ in MainActor.assumeIsolated { self?.repairIfVisible() } },
// Reactivating the app after it sat in the background is the other
// common trigger.
center.addObserver(
forName: NSApplication.didBecomeActiveNotification,
object: nil, queue: .main
) { [weak self] _ in MainActor.assumeIsolated { self?.repairIfVisible() } },
]
// App-scoped triggers: reactivating or un-hiding the app after it sat in the
// background is the other common cause.
let appEvents: [NSNotification.Name] = [
NSApplication.didBecomeActiveNotification,
NSApplication.didUnhideNotification,
]
observers =
windowEvents.map { name in
center.addObserver(forName: name, object: window, queue: .main) {
[weak self] _ in MainActor.assumeIsolated { self?.repairIfVisible() }
}
}
+ appEvents.map { name in
center.addObserver(forName: name, object: nil, queue: .main) {
[weak self] _ in MainActor.assumeIsolated { self?.repairIfVisible() }
}
}
}
private func repairIfVisible() {
@@ -83,11 +83,11 @@ struct BuildInfo {
}
}
/// Header warning that this is a non-release build the same channel colors, icons, and copy
/// as the Mac's `BuildBanner`. A channel-colored stripe red for local builds, canary-yellow
/// for canary, blue for beta, gold for a release candidate spans the full width, with its
/// content (channel name, build label, and an all-caps "EXPERIMENTAL" pill) held at the
/// leading edge. Renders nothing for a shipping App Store release.
/// Header strip warning that this is a non-release build the same channel colors, icons,
/// and copy as the Mac's `BuildBanner`: red for local builds, canary-yellow for canary, blue
/// for beta, gold for a release candidate. The leading edge names the channel; the build
/// label and an all-caps "EXPERIMENTAL" badge sit on the trailing edge. Renders nothing for
/// a shipping App Store release.
struct BuildBanner: View {
var info: BuildInfo = .current
@@ -95,7 +95,7 @@ struct BuildBanner: View {
if let style = Style(channel: info.channel) {
HStack(spacing: 8) {
Image(systemName: style.icon)
.font(.system(size: 12, weight: .heavy))
.font(.system(size: 15, weight: .heavy))
Text(style.label)
.font(.footnote.weight(.semibold))
.lineLimit(1)
@@ -112,12 +112,9 @@ struct BuildBanner: View {
}
.foregroundStyle(.white.opacity(0.92))
.padding(.horizontal, 12)
.padding(.vertical, 3)
.frame(maxWidth: .infinity, alignment: .leading)
// The channel-colored stripe fills the full width and height of the top area, with
// the content (channel name, build label, and the EXPERIMENTAL pill) laid across it
// the channel name at the leading edge, the build label and pill at the trailing.
.background(style.tint.opacity(0.9))
.padding(.vertical, 5)
.frame(maxWidth: .infinity)
.background(style.tint.opacity(0.5))
.accessibilityLabel(style.tooltip(version: info.version, build: info.buildLabel))
}
}