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:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user