Skip to content

fix: improve sidebar animations#331

Merged
baginski11 merged 9 commits intonook-browser:devfrom
AllDaGearNoIdea:dev
Mar 27, 2026
Merged

fix: improve sidebar animations#331
baginski11 merged 9 commits intonook-browser:devfrom
AllDaGearNoIdea:dev

Conversation

@AllDaGearNoIdea
Copy link
Copy Markdown

@AllDaGearNoIdea AllDaGearNoIdea commented Mar 5, 2026

NOTE: I've unified sidebar into a single view instance. Previously everything was noticabley re-drawing everything.

When in "unpinned" sidebar mode, there's an animation between sides and it stays open for a couple of seconds to orient the user.

When in "pinned" mode, the toolbar content slides underneath the web-view column.

Switching between pinned/unpinned better matches Arc:
-there's a background colour on the web view area so the gap before content width catches up isn't noticeable
-if the user is hovering over the "unpinned" sidebar, we skip the animation when switching to pinned
-the animation is quicker and smoother: .smooth(duration: 0.1)

NOTE: I'm struggling to build with entitlements, so I can't test this with the opposing AI sidebar visible.

I used Claude Opus 4.6 for help with syntax and structure.

Summary by CodeRabbit

Release Notes

  • New Features

    • Floating sidebar overlay with hover activation when sidebar is unpinned.
    • Quit confirmation dialog with keyboard shortcuts (Return to confirm, Escape to cancel).
  • Improvements

    • Sidebar position changes now animate smoothly.
    • Enhanced keyboard handling in dialogs.
    • Better page background color support in split-view layouts.

AllDaGearNoIdea and others added 5 commits March 3, 2026 20:53
… flash

Replace the two separate SpacesSideBarView containers (HStack layout and
SidebarHoverOverlayView) with a single instance rendered as an overlay.
A Color.clear spacer animates width to push web content when pinned.
The floating sidebar slides in/out via offset instead of being
conditionally created/destroyed, preserving view identity and state.

- Delete SidebarHoverOverlayView.swift (logic moved to WindowView)
- Increase toggleSidebar animation from 0.1s to 0.2s for smoother transition
- Animate hover dismissal when sidebar becomes pinned

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace branching if/else HStack with fixed-order ZStack layout so
WebContent keeps stable view identity when swapping sidebar sides.
Sidebar and AI panels slide under web content (zIndex layering) during
the swap animation. When unpinned, sidebar floats above as before.

- Add peekOverlay to HoverSidebarManager so the floating sidebar
  briefly appears on its new side for 2s after a position swap
- Use layoutDirection environment to flip SidebarMenu tab placement
- Wrap all position setters (context menu, settings) in withAnimation

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Matches Arc-style instant snap when pinning the sidebar while preserving
the smooth ease-out animation when unpinning.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…sible

Animate sidebar toggle in both directions (pin and unpin). When the
floating sidebar is already showing via hover, pinning is instant since
the sidebar is already visually present.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…debar animation

Use each tab's pageBackgroundColor instead of windowBackgroundColor for
webview and split pane backgrounds. Switch sidebar pin/unpin animation
to .smooth(duration: 0.1) for a snappier feel.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 5, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc1c64d4-b673-49ee-a31f-4ba54311185a

📥 Commits

Reviewing files that changed from the base of the PR and between 777f29c and 9eb48a8.

📒 Files selected for processing (3)
  • App/Window/WindowView.swift
  • Navigation/Sidebar/SpacesSideBarView.swift
  • Nook/Managers/HoverSidebarManager/HoverSidebarManager.swift

📝 Walkthrough

Walkthrough

The PR refactors sidebar rendering and interaction patterns by consolidating the hover overlay into a unified sidebar component, enhances the hover sidebar manager with peek functionality, updates sidebar toggle logic to account for floating state, adds smooth animations to sidebar position changes, improves keyboard handling in dialogs with tab-key blocking and shortcut hints, and adjusts background color propagation in split-view layouts.

Changes

Cohort / File(s) Summary
Sidebar UI Refactoring
App/Window/WindowView.swift, Nook/Components/Sidebar/SidebarHoverOverlayView.swift
Removed separate SidebarHoverOverlayView and consolidated hover sidebar logic into new UnifiedSidebar component in WindowView. Replaced conditional HStack with fixed ZStack + spacer approach; sidebar now slides in/out via .offset(x:) with edge hover trigger zone and animation-driven visibility.
Sidebar Toggle and State Management
Navigation/Sidebar/SidebarHeader.swift, Nook/Components/Sidebar/NavButtonsView.swift, Nook/Managers/BrowserManager/BrowserManager.swift, Nook/Models/BrowserWindowState.swift
Updated toggleSidebar() API to accept new floatingVisible: Bool parameter and propagate hover sidebar state through toggle logic. Added hoverSidebarManager weak reference to BrowserWindowState and wired it into sidebar toggle calls via SidebarHeader and NavButtonsView.
Sidebar Animation Enhancement
Navigation/Sidebar/SpacesSideBarView.swift, Nook/Components/Settings/Tabs/Appearance.swift
Wrapped sidebar position binding updates in withAnimation(.smooth(duration: 0.3)) to animate position changes smoothly instead of occurring synchronously.
Sidebar Menu Layout
Nook/Components/Sidebar/Menu/SidebarMenu.swift
Refactored layout logic to replace inline content rendering with centralized @ViewBuilder content view; tabs now consistently render on right side while content renders based on border case.
Hover Sidebar Manager Enhancement
Nook/Managers/HoverSidebarManager/HoverSidebarManager.swift
Added isMouseInsideSidebar tracking, peekUntil deadline property, and new peekOverlay(for:) method to temporarily force sidebar visibility with expiration; updated mouse-handling to skip auto-hide/auto-show logic during active peek window.
Background Color Propagation
Nook/Components/WebsiteView/WebsiteView.swift
Updated background rendering to use current tab's pageBackgroundColor instead of always using .windowBackgroundColor; extended makePaneContainer to accept optional pageBackground parameter for split-view panes.
Quit Dialog and Keyboard Handling
App/AppDelegate.swift, App/NookCommands.swift, Nook/Components/Dialog/DialogView.swift, Nook/Managers/DialogManager/DialogManager.swift
Simplified quit flow by reading askBeforeQuit setting and returning .terminateNow when disabled. Added CommandGroup for Quit command with keyboard shortcuts. Enhanced dialog with tab-key blocking, first-responder resignation, and new KeycapLabel view for shortcut hints. Updated dialog buttons to support custom icon rendering and keyboard shortcuts (Escape for Cancel, Return for Quit).

Sequence Diagram

sequenceDiagram
    participant User as User<br/>(Mouse Movement)
    participant HSM as HoverSidebarManager
    participant US as UnifiedSidebar
    participant WC as WebContent
    participant WS as WindowState

    User->>HSM: Mouse moves to left edge (unpinned)
    activate HSM
    HSM->>HSM: Check if in sidebar zone<br/>(isMouseInsideSidebar)
    alt During peek window
        HSM->>HSM: Update cursor position only<br/>(return early)
    else Normal operation
        alt Mouse over sidebar
            HSM->>HSM: Compute visibility<br/>isOverlayVisible = true
            HSM->>US: Trigger animation
        else Mouse away from sidebar
            HSM->>HSM: Schedule auto-hide<br/>isOverlayVisible = false
            HSM->>US: Trigger animation
        end
    end
    deactivate HSM

    opt Programmatic Peek (e.g., sidebar position changed)
        HSM->>HSM: peekOverlay(for: 2.0)<br/>Set peekUntil deadline
        HSM->>HSM: Force isOverlayVisible = true
        HSM->>US: Show sidebar with animation
        par
            US->>US: Render with offset animation
            US->>WC: Keep centered
        and
            HSM->>HSM: Wait for peek duration
            HSM->>HSM: Clear peekUntil & resume<br/>normal mouse logic
        end
    end

    US->>WS: Update sidebar position
    WS->>WC: Adjust layout (leftWidth/rightWidth)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A sidebar unified at last, no more overlay cast!
Peek and hover, smooth and swift, animations give a gift.
Tab keys blocked, shortcuts bright, quit dialogs set just right.
Rabbit hops with joy today—better UX leads the way! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: improve sidebar animations' clearly and specifically summarizes the main objective of the pull request: improving sidebar animations through unified view architecture and smoother transitions.
Enforce Pr Base ✅ Passed The PR targets the dev branch as its base, which fully complies with all custom check requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
Nook/Components/Settings/Tabs/Appearance.swift (1)

32-39: Avoid stacking multiple animations for the same sidebarPosition write.

Line 35 adds an explicit animation, but App/Window/WindowView.swift (Line 222-227) already animates this value, and Navigation/Sidebar/SpacesSideBarView.swift (Line 263-278) also animates writes. Prefer a single animation owner to keep transitions predictable.

Suggested adjustment
             Picker(
                 "Sidebar Position",
                 selection: Binding(
                     get: { settings.sidebarPosition },
                     set: { newValue in
-                        withAnimation(.smooth(duration: 0.3)) {
-                            settings.sidebarPosition = newValue
-                        }
+                        guard settings.sidebarPosition != newValue else { return }
+                        settings.sidebarPosition = newValue
                     }
                 )
             ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Nook/Components/Settings/Tabs/Appearance.swift` around lines 32 - 39, The
selection Binding currently wraps writes to settings.sidebarPosition in
withAnimation(.smooth(duration: 0.3)), which stacks with other animations that
already animate sidebarPosition; remove the explicit animation in the setter so
the binding simply assigns settings.sidebarPosition = newValue (or perform the
assignment inside a non-animated Transaction) and let the single designated
owner (WindowView or SpacesSideBarView) control the transition to avoid
conflicting/stacked animations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Nook/Components/Settings/Tabs/Appearance.swift`:
- Around line 32-39: The SettingsAppearanceTab struct is mutating shared state
(settings.sidebarPosition) from the Binding setter and must be confined to the
main actor; add the `@MainActor` annotation to the SettingsAppearanceTab
declaration so all its property accesses and the Binding setter run on the main
thread, then rebuild to ensure no other concurrency diagnostics remain for
methods or properties within SettingsAppearanceTab (referencing
SettingsAppearanceTab, settings.sidebarPosition, and the Binding selection
setter).

In `@Nook/Components/Sidebar/Menu/SidebarMenu.swift`:
- Line 54: Remove the layoutDirection environment override and instead implement
explicit child ordering so icons and text are not mirrored: in SidebarMenu
(where .environment(\.layoutDirection, nookSettings.sidebarPosition == .left ?
.leftToRight : .rightToLeft) is applied) delete that environment modifier and
conditionally order the HStack children based on nookSettings.sidebarPosition —
e.g., place the tabs view (or ForEach over tabs / SidebarTabView) before the
content view when sidebarPosition == .left, and reverse the order when ==
.right; if you need to reverse the tabs array use tabs.reversed() for display
only (do not mutate the source), and keep all other layout/alignments unchanged
so SF Symbols and text remain unmirrored.

In `@Nook/Managers/HoverSidebarManager/HoverSidebarManager.swift`:
- Line 35: The `@Published` property isMouseInsideSidebar in HoverSidebarManager
is being set on every pointer move causing excessive SwiftUI updates; change
assignments to only update when the value actually differs (e.g., compare new
value to current before setting or use a didSet/guard in the setter) so that
isMouseInsideSidebar only publishes when its boolean changes; apply the same fix
to the other occurrence referenced (the second assignment around line with the
other mouse-event handler) so both places check for equality before assigning.

---

Nitpick comments:
In `@Nook/Components/Settings/Tabs/Appearance.swift`:
- Around line 32-39: The selection Binding currently wraps writes to
settings.sidebarPosition in withAnimation(.smooth(duration: 0.3)), which stacks
with other animations that already animate sidebarPosition; remove the explicit
animation in the setter so the binding simply assigns settings.sidebarPosition =
newValue (or perform the assignment inside a non-animated Transaction) and let
the single designated owner (WindowView or SpacesSideBarView) control the
transition to avoid conflicting/stacked animations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09368d2b-fab0-4f94-b2d0-f294c51db824

📥 Commits

Reviewing files that changed from the base of the PR and between d5fd6d2 and 2134f7c.

📒 Files selected for processing (11)
  • App/Window/WindowView.swift
  • Navigation/Sidebar/SidebarHeader.swift
  • Navigation/Sidebar/SpacesSideBarView.swift
  • Nook/Components/Settings/Tabs/Appearance.swift
  • Nook/Components/Sidebar/Menu/SidebarMenu.swift
  • Nook/Components/Sidebar/NavButtonsView.swift
  • Nook/Components/Sidebar/SidebarHoverOverlayView.swift
  • Nook/Components/WebsiteView/WebsiteView.swift
  • Nook/Managers/BrowserManager/BrowserManager.swift
  • Nook/Managers/HoverSidebarManager/HoverSidebarManager.swift
  • Nook/Models/BrowserWindowState.swift
💤 Files with no reviewable changes (1)
  • Nook/Components/Sidebar/SidebarHoverOverlayView.swift

Comment on lines +32 to +39
selection: Binding(
get: { settings.sidebarPosition },
set: { newValue in
withAnimation(.smooth(duration: 0.3)) {
settings.sidebarPosition = newValue
}
}
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "Appearance.swift" | grep -i settings

Repository: nook-browser/Nook

Length of output: 109


🏁 Script executed:

cat -n ./Nook/Components/Settings/Tabs/Appearance.swift | head -50

Repository: nook-browser/Nook

Length of output: 1972


🏁 Script executed:

wc -l ./Nook/Components/Settings/Tabs/Appearance.swift

Repository: nook-browser/Nook

Length of output: 112


Add @MainActor annotation to SettingsAppearanceTab struct to ensure thread-safe mutations of shared settings state.

The setter at lines 32–39 mutates settings.sidebarPosition, which is shared state. Per repository policy, all Swift code must be confined to the main actor for thread safety.

Suggested adjustment
+@MainActor
 struct SettingsAppearanceTab: View {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Nook/Components/Settings/Tabs/Appearance.swift` around lines 32 - 39, The
SettingsAppearanceTab struct is mutating shared state (settings.sidebarPosition)
from the Binding setter and must be confined to the main actor; add the
`@MainActor` annotation to the SettingsAppearanceTab declaration so all its
property accesses and the Binding setter run on the main thread, then rebuild to
ensure no other concurrency diagnostics remain for methods or properties within
SettingsAppearanceTab (referencing SettingsAppearanceTab,
settings.sidebarPosition, and the Binding selection setter).

…ssary hover publishes

Replace .environment(\.layoutDirection) override with explicit child ordering
to prevent SF Symbols and text from being mirrored. Guard isMouseInsideSidebar
updates to only publish when the value changes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Nook/Managers/HoverSidebarManager/HoverSidebarManager.swift (1)

14-14: ⚠️ Potential issue | 🟠 Major

Add @MainActor to HoverSidebarManager to enforce thread-safe state access.

Line 14 declares a non-isolated class that owns @Published UI state and receives mutations from AppKit event monitor closures (lines 75–82) running on arbitrary threads. The repository requires all Swift code to be confined to @MainActor. Wrapping the monitor callbacks with Task { @mainactor } ensures thread-safe access.

Proposed fix
+@MainActor
 final class HoverSidebarManager: ObservableObject {
@@
-    private func scheduleHandleMouseMovement() {
-        // Ensure main-actor work since we touch NSApp/window and main-actor BrowserManager
-        DispatchQueue.main.async { [weak self] in
-            self?.handleMouseMovementOnMain()
-        }
-    }
+    private func scheduleHandleMouseMovement() {
+        handleMouseMovementOnMain()
+    }
@@
-        localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged]) { [weak self] event in
-            self?.scheduleHandleMouseMovement()
+        localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged]) { [weak self] event in
+            Task { `@MainActor` in
+                self?.scheduleHandleMouseMovement()
+            }
             return event
         }
@@
-        globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged]) { [weak self] _ in
-            self?.scheduleHandleMouseMovement()
+        globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged]) { [weak self] _ in
+            Task { `@MainActor` in
+                self?.scheduleHandleMouseMovement()
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Nook/Managers/HoverSidebarManager/HoverSidebarManager.swift` at line 14, Add
`@MainActor` to the HoverSidebarManager class declaration to ensure all `@Published`
UI state is confined to the main actor, and modify the AppKit event monitor
callbacks (the closures that currently mutate HoverSidebarManager state) to
perform their state changes inside Task { `@MainActor` in ... } so the mutations
run on the main thread; update references to the class name HoverSidebarManager
and the monitor callback closures to apply these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Nook/Components/Sidebar/Menu/SidebarMenu.swift`:
- Around line 55-66: The SidebarMenu struct and its view content must be
confined to the main actor; add the `@MainActor` attribute to the SidebarMenu type
declaration (so the computed View property content and any child view usage like
SidebarMenuHistoryTab and SidebarMenuDownloadsTab run on the main thread).
Update the struct definition for SidebarMenu to be annotated with `@MainActor` and
ensure any related initializers or extensions for SidebarMenu are also
marked/moved under `@MainActor` as needed.

---

Outside diff comments:
In `@Nook/Managers/HoverSidebarManager/HoverSidebarManager.swift`:
- Line 14: Add `@MainActor` to the HoverSidebarManager class declaration to ensure
all `@Published` UI state is confined to the main actor, and modify the AppKit
event monitor callbacks (the closures that currently mutate HoverSidebarManager
state) to perform their state changes inside Task { `@MainActor` in ... } so the
mutations run on the main thread; update references to the class name
HoverSidebarManager and the monitor callback closures to apply these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c30f4094-0ebe-45b4-b903-2d779bb89a6b

📥 Commits

Reviewing files that changed from the base of the PR and between 2134f7c and 3df0eb6.

📒 Files selected for processing (2)
  • Nook/Components/Sidebar/Menu/SidebarMenu.swift
  • Nook/Managers/HoverSidebarManager/HoverSidebarManager.swift

Comment on lines +55 to +66
@ViewBuilder
private var content: some View {
VStack {
switch selectedTab {
case .history:
SidebarMenuHistoryTab()
case .downloads:
SidebarMenuDownloadsTab()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the SidebarMenu struct definition and surrounding context
echo "=== Checking SidebarMenu.swift for `@MainActor` annotation ==="
cat -n Nook/Components/Sidebar/Menu/SidebarMenu.swift | head -80

Repository: nook-browser/Nook

Length of output: 2544


Add @MainActor to confine this view to the main thread.

The SidebarMenu struct violates the codebase rule requiring all Swift code to be confined to @MainActor for thread safety. Annotate the struct definition at line 35.

Proposed fix
+@MainActor
 struct SidebarMenu: View {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Nook/Components/Sidebar/Menu/SidebarMenu.swift` around lines 55 - 66, The
SidebarMenu struct and its view content must be confined to the main actor; add
the `@MainActor` attribute to the SidebarMenu type declaration (so the computed
View property content and any child view usage like SidebarMenuHistoryTab and
SidebarMenuDownloadsTab run on the main thread). Update the struct definition
for SidebarMenu to be annotated with `@MainActor` and ensure any related
initializers or extensions for SidebarMenu are also marked/moved under
`@MainActor` as needed.

@jonathancaudill
Copy link
Copy Markdown
Contributor

@coderabbitai summarize your thoughts on the pr + any needed changes for me

@jonathancaudill
Copy link
Copy Markdown
Contributor

@AllDaGearNoIdea thanks so much for this PR. Good fix. Gonna review some before committing

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 5, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the PR diff to understand the full scope of changes
gh pr diff 331 --repo nook-browser/Nook 2>/dev/null | head -500

Length of output: 23780


🏁 Script executed:

#!/bin/bash
# Look at the key files changed
fd -e swift "HoverSidebarManager.swift" --exec cat {}
fd -e swift "BrowserWindowState.swift" --exec cat {}

Length of output: 12198


@jonathancaudill Here's my full analysis of PR #331:


Overall Assessment

This is a well-motivated refactor. The core idea — keeping a single SpacesSideBarView instance always in the view hierarchy and sliding it with offset instead of conditionally inserting/removing it — is the right fix for view-identity-driven redraws. The UX improvements (peek-after-swap, pinned slide-under, background color matching) are solid additions. A few things worth addressing before merge:


✅ What's Working Well

Area Notes
Single view identity The offset-based slide-in/out in UnifiedSidebar() eliminates the redraw root cause
Fixed-order layout [LeftSpacer][WebContent][RightSpacer] keeps WebContent stable regardless of sidebar side
Peek overlay peekOverlay(for: 2.0) after position swap is a great UX touch
isMouseInsideSidebar guard Correctly only publishes when value actually changes — reduces unnecessary SwiftUI updates
pageBackgroundColor for split panes Nice visual polish, avoids the grey flash
SidebarMenu refactor Extracting content into a @ViewBuilder var is cleaner
Backward-compatible API toggleSidebar(for:floatingVisible:) with = false default is a safe change

⚠️ Issues to Address

1. Missing .alwaysArrowCursor() on the floating panel

The old SidebarHoverOverlayView called .alwaysArrowCursor() on the sidebar overlay view. The new sidebarPanel(isPinned:) only sets NSCursor.arrow.set() on the edge trigger zone, not on the sidebar content itself. When floating, moving the cursor into the sidebar content area might not restore the arrow cursor if a web view underneath had set a different cursor.

// sidebarPanel(isPinned:) — add cursor reset for floating state
SpacesSideBarView()
    // ...existing modifiers...
    .alwaysArrowCursor() // ← add this back, or conditionally when !isPinned

2. Stacked .animation() modifiers on UnifiedSidebar

Two explicit animation modifiers are chained on the same view:

.animation(.easeInOut(duration: 0.15), value: isFloatingVisible)
.animation(.smooth(duration: 0.3), value: nookSettings.sidebarPosition)

In SwiftUI, stacking multiple .animation(_:value:) modifiers generally works, but the onChange(of: nookSettings.sidebarPosition) closure also calls peekOverlay which triggers its own withAnimation(.easeInOut(duration: 0.15)). This creates three concurrent animation drivers on the sidebar during a position swap — worth testing edge cases like rapid toggling between left/right.

3. HoverSidebarManager actor isolation — peekUntil race

HoverSidebarManager is not @MainActor isolated, yet peekUntil is written from both:

  • peekOverlay() (called from SwiftUI's onChange, which runs on main)
  • DispatchQueue.main.asyncAfter callback in peekOverlay
  • Read in handleMouseMovementOnMain() (dispatched to main via scheduleHandleMouseMovement)

In practice all reads/writes reach the main queue, but the class has no formal isolation guarantee. Consider marking HoverSidebarManager as @MainActor to make this explicit and compiler-enforced:

`@MainActor`
final class HoverSidebarManager: ObservableObject { ... }

4. AI sidebar is untested

The author notes they couldn't test the AI sidebar path due to entitlements. The new ZStack layout gives AISidebar a zIndex(0) equal to a pinned sidebar — potential overlap to verify once entitlements are available.

5. SpacesSideBarView always lives in the hierarchy

When the sidebar is unpinned and the overlay is hidden, sidebarPanel is still rendered (just offset offscreen). This is intentional for view identity, but means SpacesSideBarView is always computing/laying out. It's likely cheap enough, but worth keeping an eye on if CPU usage appears elevated in collapsed state.


Minor Nits

  • cornerRadius in makePaneContainer: The if #available(macOS 26.0, *) branch returns the same value (8) as the else branch — dead code that can be simplified to let cornerRadius: CGFloat = 8.
  • slideOffset uses windowState.sidebarWidth + 14: The 14 is inset × 2 (7 pts horizontal padding × 2). A named constant would make this clearer.
  • SpacesSideBarView.swift animation: The withAnimation(.smooth(duration: 0.3)) in the Toggle's set: closure is now duplicated with the same animation in Appearance.swift — both do the same thing from different entry points, which is fine but worth a comment noting the dual paths.

Summary

The PR is in good shape — the architecture is sound and addresses the root cause. The main actionable items before merging are the missing cursor reset on the floating panel (#1) and formally isolating HoverSidebarManager to @MainActor (#3). The AI sidebar interaction (#4) should be verified post-merge once entitlements allow.

AllDaGearNoIdea and others added 3 commits March 27, 2026 22:46
… keyboard input, closer visual design (nook-browser#329)

* fix: allow quitting with Cmd+Q when "warn before quitting" is disabled

Replace the "Force Quit App" menu item with a standard Cmd+Q that
routes through showQuitDialog(). When "warn before quitting" is off,
return .terminateNow immediately to avoid a deadlock in
applicationShouldTerminate.

* fix: prevent keyboard/mouse interaction with WebView while dialog is open and improve quit dialog design

Resign WebView as first responder when a dialog appears so Enter/Escape
reach dialog buttons. Disable hit testing on the WebView during dialog
display. Add an NSEvent monitor to block Tab key and play the system beep.
Update quit dialog copy and icon size. Add customIcon support to
DialogButton with a KeycapLabel view for text-based shortcut hints.
Simplify duplicate left-button branch in DialogFooter.

* chore: trim verbose comments to match project style

* fix: address review feedback on dialog focus and actor isolation

Remove overly narrow WKWebView type check when resigning first
responder. Drop unnecessary nonisolated(unsafe) from tabKeyMonitor.

---------

Co-authored-by: AllDaGearNoIdea <[email protected]>
…solation, minor nits

- Add .alwaysArrowCursor(when:) to floating sidebar panel so web views
  underneath don't override the cursor
- Mark HoverSidebarManager as @mainactor for compiler-enforced isolation
- Simplify dead cornerRadius availability check in WebContent
- Extract magic number 14 into named floatingInset constant
- Add comment noting dual animation paths for sidebar position

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@baginski11 baginski11 merged commit 8665266 into nook-browser:dev Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants