Skip to content

Conversation

@dima6312
Copy link
Owner

@dima6312 dima6312 commented Dec 28, 2025

Summary

Adds "hybrid mode" layout support where compact leading/trailing indicators remain visible alongside expanded content. This enables use cases like audio players with playback indicators, dictation UIs with waveforms, or any interface where status icons should persist while showing detailed content.

New Features

Hybrid Mode (showCompactContentInExpandedMode)

  • New init parameter (defaults to false for backwards compatibility)
  • When enabled, compact leading/trailing content stays visible in expanded state
  • Symmetric layout positions indicators on either side of the notch

Floating Style Fallback

  • On Macs without a notch, calling compact() now enables hybrid mode and expands (instead of hiding)
  • Provides consistent UX across all Mac hardware - compact indicators are displayed when requested
  • New compactCenterContent property for optional center content in floating fallback

Public API Additions

  • showCompactContentInExpandedMode: Bool - enable hybrid layouts
  • compactCenterContent: AnyView - center content for floating mode
  • isHovering: Bool (read-only) - observe hover state
  • isHybridModeEnabled: Bool (computed) - check if hybrid mode is active
  • NSScreen extensions now public (hasNotch, notchSize, notchFrame, menubarHeight, screenWithMouse)

Bug Fixes

  • Race condition in hide(): Fixed continuation leak when closePanelTask is cancelled
  • Hover timeout: Added max retry limit (5s) to prevent infinite loops if hover state gets stuck
  • Smooth transitions: Direct expanded↔compact transitions without intermediate hide step
  • Animation coordination: Await animation completion before proceeding with state changes
  • Rapid state transitions: Fixed SwiftUI view identity corruption during rapid expand/compact/hide cycles
  • Accessibility: Added .accessibilityHidden() to opacity-hidden views for VoiceOver

Code Quality

  • Removed unused skipHide parameter from internal methods
  • Added comprehensive documentation for layout logic
  • Added animationDuration property to centralize timing

Test Coverage

  • 7 new test cases for hybrid mode behavior
  • Tests for floating fallback, state reset, computed property logic
  • Rapid state transition stress test
  • All 19 tests pass

Backwards Compatibility

Fully backwards compatible:

  • showCompactContentInExpandedMode defaults to false
  • No changes to existing public API signatures
  • All existing tests pass unchanged

⚠️ Intentional behavior change:

  • compact() on floating style now expands with hybrid mode instead of hiding
  • Apps wanting the old behavior can call hide() explicitly
  • This provides better UX parity across notch/non-notch Macs

Documentation

  • README updated with hybrid mode usage examples
  • DocC documentation added for new features
  • Inline documentation for layout logic

Adds support for "hybrid" layouts where compact leading/trailing content
remains visible while in expanded state. This enables use cases like
dictation UIs with waveform indicators alongside the notch while
transcript content shows below.

Changes:
- Add `showCompactContentInExpandedMode` property (defaults to false)
- Add init parameter for convenience
- Implement symmetric layout in NotchView for hybrid expanded mode
- Add comprehensive documentation for layout logic
- Add test cases for notch and floating styles

Also fixes:
- Prevent continuation leak in hide() when closePanelTask is cancelled

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Dec 28, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Hybrid layout: compact indicators (leading, center, trailing) can remain visible alongside expanded content; public option to show compact center content and public hover state.
  • Improvements

    • Smoother compact↔expanded transitions, unified animation timing, safer show/hide lifecycle with retry/cancellation safeguards, and floating-style hybrid behavior on notch-less Macs.
  • API Changes

    • Control methods accept an optional screen parameter; some initializers expose hybrid-mode options; screen-related utilities are now publicly accessible.
  • Tests

    • Expanded suite covering hybrid mode, floating fallback, resets and rapid transitions.
  • Documentation

    • Added hybrid-mode guidance and examples.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds a hybrid layout mode so compact indicators can remain visible with expanded content; exposes compact center content and toggle, reworks expand/compact/hide flow and hover/lifecycle handling, updates views/environment for hybrid layouts, makes NSScreen extension public, and adds hybrid-focused tests and docs.

Changes

Cohort / File(s) Summary
Core feature & API
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
Adds compactCenterContent: AnyView, showCompactContentInExpandedMode: Bool (initializer), makes isHovering public private(set), adds floatingHybridModeActive, derived isHybridModeEnabled, screenObserverTask, maxHideRetries; refactors expand/compact/hide, animation sequencing, hover-retry limits, and deinit cleanup.
Control protocol & callers
Sources/DynamicNotchKit/Utility/DynamicNotchControllable.swift, Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift
expand(on:) and compact(on:) signatures changed to accept NSScreen?; DynamicNotchInfo forwards internalDynamicNotch.objectWillChange, adds shouldSkipHideWhenConverting, accepts showCompactContentInExpandedMode in init, and simplifies internal expand/compact calls.
Views & layout
Sources/DynamicNotchKit/Views/NotchView.swift, Sources/DynamicNotchKit/Views/NotchlessView.swift
Reworks rendering for hybrid layout: overlays expanded + compact content, adds compactSideContent helper and compactIndicatorsRow(), supports symmetric/center compact content, adjusts sizing/offsets/transitions and safe-area inset behavior when hybrid is enabled.
Info helper views
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo+HelperViews.swift
Tightens visibility of view bodies to internal, introduces internalNotch and useMatchedGeometry to conditionally apply matchedGeometryEffect and avoid animation conflicts during hybrid conversions.
Environment
Sources/DynamicNotchKit/Utility/EnvironmentValues+Extensions.swift
Adds case compactCenter to DynamicNotchSection for injecting compact center content via environment.
NSScreen helpers exposure
Sources/DynamicNotchKit/Utility/NSScreen+Extensions.swift
Makes the NSScreen extension public (public extension NSScreen) to expose screen helper APIs.
Style / animation centralization
Sources/DynamicNotchKit/DynamicNotch/DynamicNotchStyle.swift
Introduces private standardAnimationDuration (0.4s) and internal animationDuration used across opening/closing/conversion animations.
Docs & README
README.md, Sources/DynamicNotchKit/Documentation.docc/Documentation.md
Adds "Hybrid Mode" docs and examples, and notes about floating-style fallback behavior when no notch is present.
Tests
Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift
Adds extensive hybrid-mode tests (notch & floating), floating fallback cases, reset/computed-property checks, rapid transitions, and helper _dynamicNotchHybridMode(with:).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as App / Caller
  participant Notch as DynamicNotch
  participant Screen as NSScreen
  participant View as NotchView/NotchlessView

  Note over App,Notch: User/Code requests state change
  App->>Notch: call compact(on: NSScreen?) / expand(on: NSScreen?)
  alt screen param nil
    Notch->>Screen: resolve main or first screen
  end
  Notch->>Notch: update hybrid flags (floatingHybridModeActive / showCompactContentInExpandedMode)
  Notch->>View: publish state changes (isHybridModeEnabled, isHovering, animations)
  Note right of View: renders compact indicators row or overlay\nuses compactCenterContent / compactSideContent
  View-->>App: visual state updated
  Note over Notch: hover retries / animation sequencing / screenObserverTask lifecycle
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature: adding hybrid mode support via showCompactContentInExpandedMode for improved notch layout capabilities.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering new features, bug fixes, backwards compatibility, and testing—all aspects present in the implementation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/hybrid-compact-expanded-mode

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3af16a7 and ce55ff5.

📒 Files selected for processing (6)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotchStyle.swift
  • Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift
  • Sources/DynamicNotchKit/Views/NotchView.swift
  • Sources/DynamicNotchKit/Views/NotchlessView.swift
  • Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift
🧰 Additional context used
🧬 Code graph analysis (4)
Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (2)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (3)
  • compact (243-245)
  • expand (198-200)
  • hide (311-317)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (3)
  • compact (126-130)
  • expand (120-124)
  • hide (132-134)
Sources/DynamicNotchKit/Views/NotchView.swift (1)
Sources/DynamicNotchKit/Views/NotchlessView.swift (1)
  • notchContent (51-74)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (2)
  • expand (198-200)
  • compact (243-245)
Sources/DynamicNotchKit/Views/NotchlessView.swift (2)
Sources/DynamicNotchKit/Views/NotchView.swift (2)
  • notchContent (85-111)
  • expandedContent (172-184)
Sources/DynamicNotchKit/Utility/BlurModifier.swift (1)
  • blur (31-36)
🔇 Additional comments (17)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotchStyle.swift (1)

66-89: Good refactor: Centralized animation duration.

Extracting the animation duration into a single constant improves maintainability and ensures consistency across all animation types. The internal animationDuration property enables proper coordination with async operations.

Sources/DynamicNotchKit/Views/NotchView.swift (3)

79-111: LGTM: Well-structured hybrid layout logic.

The notchContent() method correctly handles both normal and hybrid modes. The conditional fixedSize and frame(maxWidth:) modifiers ensure compact content expands properly in hybrid layouts, while the offset calculations maintain proper alignment during state transitions.


113-170: LGTM: Compact content visibility and layout.

The visibility logic correctly differentiates between normal mode (compact state only) and hybrid mode (both compact and expanded states). The symmetric layout implementation ensures balanced positioning of indicators around the notch.


185-233: LGTM: Well-designed helper for compact side content.

The compactSideContent helper reduces duplication between leading and trailing rendering while properly handling both symmetric (hybrid expanded) and non-symmetric (compact) layouts. The explicit frame constraint on content ensures custom views like gradients render with correct dimensions.

Sources/DynamicNotchKit/Views/NotchlessView.swift (2)

46-79: LGTM: Hybrid mode integration for floating style.

The indicatorsRowHeight and compactIconSize calculations are straightforward. The conditional rendering of compactIndicatorsRow() with explicit height/opacity/clipped modifiers maintains stable view identity during transitions, which is crucial for smooth animations.


81-114: LGTM: Compact indicators row with proper accessibility.

The implementation correctly addresses accessibility concerns by pairing .opacity(0) with .accessibilityHidden() for disabled content. The "always render but hide" pattern prevents SwiftUI animation glitches during rapid state transitions. Center content integration for floating mode is well-placed.

Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (3)

104-109: LGTM: Proper objectWillChange forwarding.

The Combine subscription correctly forwards state changes from internalDynamicNotch to observers of DynamicNotchInfo. The [weak self] capture prevents retain cycles, and storing the cancellable ensures the subscription stays alive.


56-68: LGTM: Compact leading state management.

The shouldSkipHideWhenConverting reset in compactLeading didSet ensures proper animation behavior when the user explicitly sets a different compact icon. The flag's internal visibility is appropriate.


80-130: LGTM: API alignment with DynamicNotch.

The initializer properly wires showCompactContentInExpandedMode to the internal notch, and the expand(on:)/compact(on:) signatures align with the updated DynamicNotch API using optional screen parameters.

Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (3)

277-342: LGTM: Comprehensive hybrid mode test coverage.

The _dynamicNotchHybridMode helper provides thorough coverage of hybrid mode behavior across both notch and floating styles. The differentiated expectations for floating (stays expanded) vs notch (goes to compact) correctly reflect the intended behavior.


344-404: LGTM: Floating fallback behavior verification.

This test correctly verifies the key invariants:

  • User's showCompactContentInExpandedMode setting is not mutated
  • Internal floatingHybridModeActive flag is set/reset appropriately
  • State remains expanded (not hidden) when compact() is called on floating style

458-506: LGTM: Edge case and stability tests.

The "floating fallback from hidden" test validates an important edge case where compact() is called directly from hidden state. The rapid state transitions test provides regression coverage for race conditions.

Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (5)

80-110: LGTM: Well-structured hybrid mode state management.

The separation between user-configurable showCompactContentInExpandedMode and internal floatingHybridModeActive ensures the API doesn't mutate user settings as a side effect. The isHybridModeEnabled computed property provides a clean unified view of hybrid state.


198-241: LGTM: Clean expand implementation with hybrid mode awareness.

The resetHybridMode parameter elegantly solves the issue where floating fallback's compact() needs to call _expand() without resetting the hybrid flag. Direct transitions from compact to expanded improve animation fluidity.


258-284: LGTM: Floating fallback with race condition prevention.

The implementation correctly awaits animation completion (line 275) before returning, addressing the previous race condition concern. The separation between "already expanded" and "needs expansion" paths is clear.


319-371: LGTM: Robust hide implementation with proper cleanup.

This implementation correctly addresses all previously raised concerns:

  • maxHideRetries prevents infinite hover loops
  • Explicit CancellationError handling keeps code clear
  • deinitializeWindow() is skipped when cancelled (preventing the race condition where a new window would be closed)
  • completion?() is always called (preventing continuation leaks)
  • floatingHybridModeActive reset after animation prevents visual glitches

160-178: LGTM: Proper lifecycle management.

The screen observer task correctly guards against reinitializing during active animations (line 167). The deinit properly cancels both observer and close panel tasks to prevent resource leaks.


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

@gemini-code-assist
Copy link

Summary of Changes

Hello @dima6312, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new feature to DynamicNotchKit, allowing for more flexible and dynamic user interfaces by enabling compact content to persist alongside expanded views. This 'hybrid' mode is particularly beneficial for applications requiring continuous indicators, such as audio waveforms in a dictation interface, while also displaying detailed information. Additionally, a critical bug related to task cancellation and continuation leaks has been resolved, improving the robustness of the notch's hide mechanism.

Highlights

  • New Feature: Hybrid Notch Layouts: Introduced showCompactContentInExpandedMode property and an init parameter to enable 'hybrid' layouts where compact leading/trailing content remains visible even when the notch is in an expanded state. This supports use cases like dictation UIs.
  • Layout Logic Enhancements: Implemented symmetric layout within NotchView for the new hybrid expanded mode, ensuring compact indicators are properly positioned. Comprehensive documentation has been added to explain the new layout logic.
  • Bug Fix: Continuation Leak: Addressed a potential continuation leak in the hide() method by ensuring the completion handler is always called, even if closePanelTask is cancelled.
  • Test Coverage: Added new test cases to validate the behavior of the hybrid mode for both notch and floating styles, ensuring the feature works as expected and degrades gracefully on non-notch devices.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Expose `hasNotch`, `notchSize`, `notchFrame`, `menubarHeight`, and
`screenWithMouse` for client apps that need to detect notch presence
and adjust their behavior accordingly (e.g., always using expand()
instead of compact() on non-notch Macs).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 3 files

Copy link

@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

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36e728b and ea98161.

📒 Files selected for processing (3)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
  • Sources/DynamicNotchKit/Views/NotchView.swift
  • Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift
🧰 Additional context used
🧬 Code graph analysis (2)
Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (2)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (3)
  • expand (169-171)
  • compact (208-210)
  • hide (257-263)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (3)
  • expand (104-111)
  • compact (113-120)
  • hide (122-124)
Sources/DynamicNotchKit/Views/NotchView.swift (4)
Sources/DynamicNotchKit/Views/NotchlessView.swift (1)
  • notchContent (46-56)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (2)
  • compact (208-210)
  • updateHoverState (153-163)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)
  • compact (113-120)
Sources/DynamicNotchKit/Utility/BlurModifier.swift (2)
  • blur (31-36)
  • scale (38-43)
🔇 Additional comments (7)
Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (2)

271-285: LGTM! Good test coverage for both styles.

The hybrid mode test entry points are well-structured, following the existing pattern in the test file. The comment on lines 281-283 appropriately documents the expected graceful degradation behavior for floating style.


287-320: LGTM! Comprehensive test flow for hybrid mode.

The helper function exercises the full lifecycle: expand (hybrid) → compact → expand again → hide. This validates that the showCompactContentInExpandedMode: true parameter works correctly and that transitions between states remain functional.

Sources/DynamicNotchKit/Views/NotchView.swift (3)

79-111: LGTM! Clean layout logic with good documentation.

The hybrid layout logic is well-structured:

  • showCompactInExpanded and useHybridLayout flags clearly express intent
  • The conditional width/height handling on lines 103-105 properly accommodates both normal and hybrid modes
  • Documentation comments explain the layout behavior clearly

113-170: LGTM! Well-refactored compact content handling.

The visibility and layout mode logic is clean:

  • showContent correctly determines visibility based on mode (hybrid vs normal)
  • useSymmetricLayout enables equal-width containers in hybrid-expanded mode
  • The refactoring to use compactSideContent helper reduces duplication

185-224: LGTM! Solid helper method implementation.

The compactSideContent helper:

  • Properly handles both symmetric and normal layout modes
  • Uses appropriate edge padding (15pt for symmetric, 8pt for normal)
  • Correctly applies environment, safe area insets, and width measurement
  • Documentation is thorough
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (2)

78-81: LGTM! Clean property addition with good documentation.

The new showCompactContentInExpandedMode property:

  • Is well-documented with clear use case explanation
  • Defaults to false preserving backwards compatibility
  • Uses @Published appropriately for SwiftUI reactivity

99-109: LGTM! Backwards-compatible initializer update.

The new parameter showCompactContentInExpandedMode: Bool = false is correctly positioned and defaults to false, ensuring existing code continues to work without modification.

When compact() is called in floating mode, instead of hiding the window,
expand with hybrid mode enabled. This ensures compact indicators (like
waveforms, status icons, cancel buttons) remain visible on non-notch Macs,
providing consistent UX across all Mac hardware.

Previously, non-notch Mac users would not see compact content at all since
floating mode has no physical notch to flank. Now they see the same
indicators as notch Mac users, displayed alongside the expanded content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift">

<violation number="1" location="Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift:218">
P2: Mutating the public `showCompactContentInExpandedMode` property as a side effect of `compact()` will permanently overwrite any user-configured value. Consider using a separate internal flag or saving/restoring the original value to avoid changing user configuration.</violation>
</file>

Reply to cubic to teach it or ask questions. Tag @cubic-dev-ai to re-run a review.

Copy link

@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

♻️ Duplicate comments (1)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (1)

291-294: Continuation leak fix is incomplete—cleanup still skipped on cancellation.

While calling completion?() on cancellation prevents the continuation from waiting indefinitely, the window is never deinitialized when the task is cancelled. The past review comment specifically requested that deinitializeWindow() be called before resuming the continuation, even on cancellation, so that await hide() guarantees full cleanup. The current implementation still violates the documented contract: "waits for the animation to complete before returning" (line 268).

🔎 Proposed fix
 closePanelTask = Task {
     try? await Task.sleep(for: .seconds(0.4)) // Wait for animation to complete
-    guard Task.isCancelled != true else {
-        completion?() // Always resume continuation to prevent leak
-        return
-    }
-    deinitializeWindow()
-    completion?()
+    if !Task.isCancelled {
+        // Animation completed normally
+    }
+    // Always cleanup, even on cancellation, before resuming
+    deinitializeWindow()
+    completion?()
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea98161 and 10a548d.

📒 Files selected for processing (2)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
  • Sources/DynamicNotchKit/Utility/NSScreen+Extensions.swift
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (2)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (1)

78-81: LGTM! Property is well-documented and backwards-compatible.

The new showCompactContentInExpandedMode property is clearly documented and defaults to false, ensuring existing clients are unaffected.

Sources/DynamicNotchKit/Utility/NSScreen+Extensions.swift (1)

10-10: LGTM! Public exposure of NSScreen utilities is appropriate.

Making the extension public allows clients to access notch detection and metric utilities, which aligns with the PR objectives. This is a purely additive change with no breaking impact.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new 'hybrid' layout mode, allowing compact content to remain visible in the expanded state, which is a great feature enhancement. The implementation is well-structured, especially the refactoring in NotchView.swift to support the new layout logic. The addition of documentation and tests is also appreciated. I've included one suggestion to improve the implementation of the continuation leak bug fix, making it more concise and maintainable. Overall, this is a solid contribution.

Comment on lines 288 to 293
closePanelTask?.cancel()
closePanelTask = Task {
try? await Task.sleep(for: .seconds(0.4)) // Wait for animation to complete
guard Task.isCancelled != true else { return }
guard Task.isCancelled != true else {
completion?() // Always resume continuation to prevent leak
return

Choose a reason for hiding this comment

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

medium

While this correctly fixes the continuation leak, the call to completion?() is now duplicated. You can make this code more concise and avoid redundancy by using a single completion?() call at the end of the task block.

Additionally, comparing a boolean with != true is less idiomatic in Swift. It's clearer to use the ! operator for negation (i.e., !Task.isCancelled).

Suggested change
closePanelTask?.cancel()
closePanelTask = Task {
try? await Task.sleep(for: .seconds(0.4)) // Wait for animation to complete
guard Task.isCancelled != true else { return }
guard Task.isCancelled != true else {
completion?() // Always resume continuation to prevent leak
return
if !Task.isCancelled {
deinitializeWindow()
}
completion?()

- Add internal floatingHybridModeActive flag to avoid mutating user property
- Add isHybridModeEnabled computed property for unified hybrid mode check
- Update compact() to auto-enable hybrid mode on floating style
- Add compact indicators overlay to NotchlessView for floating panels
- Fix animation when transitioning to hybrid mode from expanded state
- Add test cases for hybrid mode and floating fallback behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@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

🧹 Nitpick comments (2)
Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (1)

315-356: Consider adding an assertion for the floating fallback behavior.

The comment at lines 352-353 mentions verifying that showCompactContentInExpandedMode should NOT be mutated, but there's no actual assertion. While the file states tests are "examples of usage," adding one assertion here would validate the fix for the previously flagged issue about mutating user configuration.

🔎 Suggested assertion
         // Verify: user-facing property should NOT be mutated
         // (floatingHybridModeActive is internal, showCompactContentInExpandedMode stays false)
+        #expect(notch.showCompactContentInExpandedMode == false, "User-facing property should not be mutated by compact()")

         await notch.hide()
     }
Sources/DynamicNotchKit/Views/NotchlessView.swift (1)

62-83: Compact indicators overlay implemented correctly.

The overlay mirrors the structure in NotchView.compactContent() with appropriate environment injection and transitions. The scale anchors (.trailing for leading content, .leading for trailing content) create a natural expand-from-center effect.

One minor inconsistency: NotchView's compactSideContent uses vertical safe area insets of 4pt top and 8pt bottom, while this uses equal safeAreaInset (15pt) for top only. This may cause visual inconsistency between notch and floating hybrid modes.

🔎 Consider aligning vertical insets with NotchView
     .padding(.horizontal, safeAreaInset)
-    .padding(.top, safeAreaInset)
+    .padding(.top, 4)

Alternatively, keep the current padding if the floating style intentionally differs from notch style.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10a548d and f73fb2e.

📒 Files selected for processing (4)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
  • Sources/DynamicNotchKit/Views/NotchView.swift
  • Sources/DynamicNotchKit/Views/NotchlessView.swift
  • Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift
🧰 Additional context used
🧬 Code graph analysis (2)
Sources/DynamicNotchKit/Views/NotchView.swift (2)
Sources/DynamicNotchKit/Views/NotchlessView.swift (1)
  • notchContent (46-61)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (2)
  • compact (218-220)
  • updateHoverState (163-173)
Sources/DynamicNotchKit/Views/NotchlessView.swift (2)
Sources/DynamicNotchKit/Views/NotchView.swift (1)
  • expandedContent (172-184)
Sources/DynamicNotchKit/Utility/BlurModifier.swift (2)
  • blur (31-36)
  • scale (38-43)
🔇 Additional comments (9)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (4)

27-29: Documentation updated correctly.

The documentation now accurately reflects the new floating mode behavior where compact() expands with hybrid mode enabled instead of hiding. This addresses the previous review feedback about outdated documentation.


105-119: Init parameter correctly wired.

The new showCompactContentInExpandedMode parameter is properly documented, defaulted to false for backwards compatibility, and assigned in the initializer.


226-243: Floating mode hybrid behavior implemented correctly.

The logic properly handles both cases:

  1. Already expanded → animate hybrid mode activation
  2. Hidden → enable hybrid mode and expand

Using the internal floatingHybridModeActive flag instead of mutating the public property addresses the previous review feedback.


311-317: Continuation leak fix applied correctly.

The cleanup now always calls deinitializeWindow() and completion?() regardless of cancellation state, addressing the previous review concern about the continuation being resumed before cleanup completed.

Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (1)

271-313: Hybrid mode tests provide good usage examples.

The test helper _dynamicNotchHybridMode properly demonstrates the hybrid mode workflow with explicit showCompactContentInExpandedMode: true initialization. The tests cover both notch and floating styles.

Sources/DynamicNotchKit/Views/NotchlessView.swift (1)

46-61: ZStack enables proper overlay composition for hybrid mode.

The container change from VStack to ZStack(alignment: .top) correctly allows the compact indicators to overlay the expanded content. The conditional rendering based on isHybridModeEnabled is clean.

Sources/DynamicNotchKit/Views/NotchView.swift (3)

79-111: Well-documented layout logic for hybrid mode.

The documentation clearly explains the two layout behaviors (normal vs hybrid). The computed flags showCompactInExpanded and useHybridLayout make the intent clear. The conditional frame constraints correctly handle the hybrid layout where compact content fills the expanded width.


113-170: Compact content visibility and layout refactored cleanly.

The visibility logic correctly differentiates between hybrid mode (visible in non-hidden states) and normal mode (visible only in compact state). The useSymmetricLayout flag appropriately enables equal-width containers for hybrid expanded mode.

Good use of compactSideContent helper to reduce duplication.


185-224: Well-structured helper for per-side compact content.

The compactSideContent function consolidates the rendering logic with proper parameterization for:

  • Edge-specific padding (8pt normal, 15pt symmetric)
  • Scale animation anchors
  • Width tracking via binding
  • Environment injection

The symmetric layout branch correctly uses Spacer to push content to the outer edge while filling available width.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 4 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift">

<violation number="1" location="Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift:234">
P2: Missing state reset: `floatingHybridModeActive` is set to `true` but never reset to `false`. This will cause hybrid mode to persist unexpectedly across `hide()` → `expand()` transitions. Add `floatingHybridModeActive = false` at the beginning of `_hide()` to properly clean up state.</violation>

<violation number="2" location="Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift:314">
P1: Race condition: removing the `Task.isCancelled` check allows `deinitializeWindow()` to run after cancellation, potentially closing a window that was just opened by `expand()` or `compact()`. The guard should skip `deinitializeWindow()` when cancelled but still call `completion?()` to prevent continuation leaks.</violation>
</file>

<file name="Sources/DynamicNotchKit/Views/NotchlessView.swift">

<violation number="1" location="Sources/DynamicNotchKit/Views/NotchlessView.swift:66">
P2: The `HStack` in `compactIndicatorsOverlay()` won&#39;t expand to fill the ZStack&#39;s width because a `Spacer`&#39;s ideal width is 0. Without `.frame(maxWidth: .infinity)`, the compact indicators will cluster together instead of positioning at opposite corners. Compare with `NotchView.swift` which properly uses `.frame(maxWidth: .infinity)` in hybrid mode.</violation>
</file>

Reply to cubic to teach it or ask questions. Tag @cubic-dev-ai to re-run a review.

- Add compactCenterContent property for center UI in floating fallback
- Add compactCenter case to DynamicNotchSection enum
- Update NotchlessView to show indicators row with center content
- Remove top inset gap when in hybrid mode
- Reset floatingHybridModeActive on hide to prevent state persistence

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@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: 0

♻️ Duplicate comments (1)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (1)

318-324: Race condition: deinitializeWindow() can close a newly opened window after task cancellation.

When expand() or compact() cancels closePanelTask and immediately calls initializeWindow(), the cancelled task's sleep returns (error caught by try?), then execution continues to deinitializeWindow(), closing the just-opened window.

The Task.isCancelled guard should be restored to skip deinitializeWindow() when cancelled, while still calling completion?() to prevent continuation leaks:

Proposed fix
 closePanelTask = Task {
     try? await Task.sleep(for: .seconds(0.4)) // Wait for animation to complete
-    // Always cleanup and resume continuation, even on cancellation
-    deinitializeWindow()
+    if !Task.isCancelled {
+        deinitializeWindow()
+    }
     completion?()
 }
🧹 Nitpick comments (1)
Sources/DynamicNotchKit/Views/NotchlessView.swift (1)

79-82: Consider adding a conditional check for center content consistency.

Leading and trailing content are conditionally rendered based on disableCompactLeading/disableCompactTrailing, but center content is always rendered. While this works since the default is EmptyView, it's inconsistent. Consider adding a disableCompactCenter flag or conditionally rendering only when compactCenterContent is non-empty for consistency.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f73fb2e and d2f491f.

📒 Files selected for processing (3)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
  • Sources/DynamicNotchKit/Utility/EnvironmentValues+Extensions.swift
  • Sources/DynamicNotchKit/Views/NotchlessView.swift
🧰 Additional context used
🧬 Code graph analysis (1)
Sources/DynamicNotchKit/Views/NotchlessView.swift (2)
Sources/DynamicNotchKit/Views/NotchView.swift (1)
  • expandedContent (172-184)
Sources/DynamicNotchKit/Utility/BlurModifier.swift (2)
  • blur (31-36)
  • scale (38-43)
🔇 Additional comments (7)
Sources/DynamicNotchKit/Utility/EnvironmentValues+Extensions.swift (1)

19-19: LGTM!

The new compactCenter case properly extends the enum to support the hybrid layout feature, aligning with the compactCenterContent property in DynamicNotch and the environment bindings in NotchlessView.

Sources/DynamicNotchKit/Views/NotchlessView.swift (1)

46-64: LGTM!

The VStack layout correctly propagates the width from expandedContent to compactIndicatorsRow(), so the Spacers will expand properly. The conditional top safe area inset logic correctly avoids double-spacing when the indicators row is present.

Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (5)

27-29: LGTM!

Documentation correctly reflects the new floating mode behavior where compact(on:) expands with hybrid mode enabled instead of hiding.


79-95: LGTM!

Good separation of concerns: the internal floatingHybridModeActive flag avoids mutating the user-facing showCompactContentInExpandedMode property, addressing the previous review concern. The computed isHybridModeEnabled cleanly combines both sources.


113-130: LGTM!

The new showCompactContentInExpandedMode parameter defaults to false, maintaining backwards compatibility with existing callers.


229-247: LGTM!

The floating mode fallback correctly uses the internal floatingHybridModeActive flag to avoid mutating user configuration. The fire-and-forget Task for animation is appropriate here since it only triggers UI updates.


302-303: LGTM!

Correctly resets floatingHybridModeActive at the start of _hide() to prevent hybrid mode from persisting across hide/expand cycles, addressing the previous review concern.

dima6312 and others added 2 commits December 29, 2025 01:05
Remove intermediate hide step when transitioning from expanded to compact,
eliminating the visual flash. Uses conversionAnimation for seamless morphing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Only call deinitializeWindow() if task wasn't cancelled, since a new
window may have been opened by expand()/compact() after cancellation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@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

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d2f491f and 43f0a4b.

📒 Files selected for processing (1)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
🔇 Additional comments (7)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (7)

27-29: LGTM! Documentation accurately reflects the new floating mode behavior.

The updated documentation clearly explains that floating-style devices will expand with hybrid mode when compact() is called, showing compact indicators alongside expanded content.


79-86: LGTM! Well-designed public API additions.

Both properties are properly documented and use safe defaults that maintain backwards compatibility.


88-95: LGTM! Clean separation of user configuration and internal state.

The computed isHybridModeEnabled property properly combines the user-facing setting with the internal floating mode flag, avoiding mutation of user configuration.


109-123: LGTM! Initializer properly extended with new parameter.

The showCompactContentInExpandedMode parameter is correctly added with a default value, maintaining backwards compatibility.


262-265: LGTM! Direct transition improves UX.

The direct expanded → compact transition using conversionAnimation eliminates the visual flash mentioned in the PR objectives.


293-294: LGTM! Proper state cleanup on hide.

Resetting floatingHybridModeActive when hiding prevents the flag from persisting across show/hide cycles.


312-317: LGTM! Correct cancellation handling.

The implementation properly:

  • Guards deinitializeWindow() to prevent closing a window that was reopened after cancellation
  • Always calls completion?() to prevent continuation leaks
  • Uses idiomatic !Task.isCancelled check

dima6312 and others added 3 commits December 29, 2025 01:11
Changed fire-and-forget Task to awaited MainActor.run and added sleep
to wait for animation completion before compact() returns.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Allow clients to observe hover state changes via Combine to implement
hover-based activation patterns (e.g., activate panel when mouse enters).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Thread Safety & Race Conditions:
- Use structured concurrency with MainActor.run instead of unstructured Tasks
- Make state checks atomic in floating mode logic
- Add task cancellation on all early return paths

Memory Management:
- Track observeScreenParameters Task and cancel in deinit
- Use weak self capture to prevent retain cycles
- Only reinitialize window when state is hidden

Animation & State Transitions:
- Extract animationDuration constant from DynamicNotchStyle
- Make compact<->expanded transitions symmetric (both direct)
- Reset floatingHybridModeActive even when already expanded
- Move flag reset after animation completes to prevent visual glitch

API Improvements:
- Make floatingHybridModeActive private(set)
- Optimize redundant hybrid mode animations
- Add max retry count for hover-blocked hide operations

View Fixes:
- Constrain compact indicators row height in NotchlessView

Tests:
- Add comprehensive assertions for state verification
- Add tests for flag resets, computed property logic, rapid transitions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (1)

203-234: Critical bug: floatingHybridModeActive reset breaks compact() flow from hidden state.

Line 207 resets floatingHybridModeActive = false before the guard check at line 209, which breaks the floating fallback when compact() is called from .hidden state:

Scenario:

  1. Floating style, state is .hidden
  2. User calls compact()
  3. _compact() line 274 sets floatingHybridModeActive = true
  4. _compact() line 276 calls _expand()
  5. Line 207 resets floatingHybridModeActive = false
  6. Line 209 guard checks state != .expanded (state is .hidden, so continues)
  7. Result: Flag is false, but should be true for hybrid mode to work

The existing test (dynamicNotchFloatingFallback) doesn't catch this because it always calls expand() first (line 377), so the state is already .expanded and the guard returns early at line 209 before any damage is done.

🔎 Proposed fix

Option 1: Move the reset after the guard check so it only runs when actually expanding:

 func _expand(on screen: NSScreen = NSScreen.screens[0], skipHide: Bool) async {
-    // Reset floating hybrid mode when explicitly expanding
-    // (compact indicators should only show when compact() is called)
-    // This must happen even if already expanded (e.g., after floating fallback compact())
-    floatingHybridModeActive = false
-
     guard state != .expanded else {
         closePanelTask?.cancel()
+        // Reset hybrid mode when already expanded (user called expand() again)
+        floatingHybridModeActive = false
         return
     }
+    
+    // Reset hybrid mode when transitioning from compact to expanded
+    floatingHybridModeActive = false

     closePanelTask?.cancel()

Option 2: Add a parameter to skip the reset when called from _compact():

-func _expand(on screen: NSScreen = NSScreen.screens[0], skipHide: Bool) async {
+func _expand(on screen: NSScreen = NSScreen.screens[0], skipHide: Bool, resetHybridMode: Bool = true) async {
-    floatingHybridModeActive = false
+    if resetHybridMode {
+        floatingHybridModeActive = false
+    }

Then at line 276 in _compact():

-await _expand(on: screen, skipHide: skipHide)
+await _expand(on: screen, skipHide: skipHide, resetHybridMode: false)

Option 1 is simpler and doesn't add new parameters.

🧹 Nitpick comments (1)
Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (1)

338-398: Add test coverage for compact() from hidden state on floating style.

The test correctly verifies floating fallback when transitioning from expanded→compact (via line 377 expand() then line 386 compact()). However, there's an untested code path in Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift lines 272-277 where compact() is called from .hidden state on floating style.

🔎 Suggested test case

Add a test that calls compact() directly on a floating-style notch without calling expand() first:

@Test("DynamicNotch - Floating fallback from hidden state", .tags(.floatingStyle))
func dynamicNotchFloatingFallbackFromHidden() async throws {
    let notch = DynamicNotch(style: .floating) {
        Text("Test")
    } compactLeading: {
        Image(systemName: "circle")
    } compactTrailing: {
        Image(systemName: "square")
    }
    
    // Verify initial hidden state
    #expect(notch.state == .hidden)
    #expect(notch.floatingHybridModeActive == false)
    
    // Call compact() directly from hidden state
    await notch.compact()
    
    // Should auto-expand with hybrid mode enabled
    #expect(notch.state == .expanded, "Should expand from hidden state")
    #expect(notch.floatingHybridModeActive == true, "Hybrid mode should be active")
    #expect(notch.isHybridModeEnabled == true)
    
    await notch.hide()
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28f0f5d and e632b45.

📒 Files selected for processing (4)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotchStyle.swift
  • Sources/DynamicNotchKit/Views/NotchlessView.swift
  • Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotchStyle.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/DynamicNotchKit/Views/NotchlessView.swift
🧰 Additional context used
🧬 Code graph analysis (1)
Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (2)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (3)
  • expand (199-201)
  • compact (236-238)
  • hide (307-313)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (3)
  • expand (104-111)
  • compact (113-120)
  • hide (122-124)
🔇 Additional comments (8)
Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (3)

271-336: LGTM!

The hybrid mode tests are well-structured with clear assertions for each state transition. The tests correctly verify that:

  • Notch style transitions to compact state while keeping hybrid mode enabled
  • Floating style remains expanded and auto-enables hybrid mode
  • The compactCenterContent property is properly exercised

400-450: LGTM!

Excellent test coverage for the flag reset logic and computed property behavior. The tests correctly verify:

  • Line 419: expand() resets floatingHybridModeActive
  • Line 433: User setting takes precedence
  • Line 447: User property showCompactContentInExpandedMode is never mutated by internal logic

452-471: LGTM!

Good stress test for rapid state transitions. This helps ensure the task cancellation logic in _expand(), _compact(), and _hide() is robust and prevents crashes during quick state changes.

Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (5)

27-29: LGTM!

Documentation correctly reflects the new floating style behavior where compact() enables hybrid mode instead of hiding the window. This addresses the past review feedback about updating the documentation.


79-129: LGTM!

Property declarations and initialization are well-designed:

  • Appropriate access control (private(set) for floatingHybridModeActive prevents external mutation)
  • Clear separation between user-configurable (showCompactContentInExpandedMode) and internal state (floatingHybridModeActive)
  • Computed isHybridModeEnabled property correctly combines both flags with OR logic
  • Init parameter defaults to false for backwards compatibility

160-179: LGTM!

Excellent memory management and thread safety:

  • weak self capture prevents retain cycles in the long-lived notification observer
  • Task cancellation in deinit ensures clean cleanup
  • MainActor.run ensures UI state is accessed safely
  • Smart check to only reinitialize when hidden (line 166) prevents disrupting active animations

240-305: LGTM on thread safety and atomic state checks.

The floating style logic uses proper synchronization:

  • Lines 255-257: Atomically reads both state and floatingHybridModeActive to avoid race conditions
  • Line 261: Smart check to only animate if not already in hybrid mode (prevents redundant animations)
  • Lines 262-266: Properly wraps state mutation in MainActor.run with animation
  • Lines 290-301: Consistent use of MainActor.run for notch style state changes

Note: The critical bug with _expand() resetting the flag (line 276 calling into line 207) is flagged in a separate comment.


319-363: LGTM!

The _hide() implementation correctly addresses all past review feedback:

  • Lines 327-336: Retry logic with maxHideRetries (50 × 0.1s = 5s timeout) prevents infinite loops when hover state is stuck
  • Lines 346-350: do-catch block handles task cancellation gracefully
  • Line 354: Resetting floatingHybridModeActive after animation completes prevents visual glitch where indicators disappear before the window closes
  • Line 357-359: Only deinitializes if not cancelled, preventing race condition where a new window gets closed
  • Line 361: Always calls completion() to prevent continuation leaks

The comment on line 328-329 correctly explains the timeout safety mechanism.

…x code review issues (#2)

* fix: preserve floatingHybridModeActive when compact() calls _expand() from hidden state

The floatingHybridModeActive flag was being reset in _expand() before the
guard check, which broke the floating fallback when compact() was called
from .hidden state. The fix adds a resetHybridMode parameter to _expand()
that defaults to true but is set to false when called from _compact(),
preserving the hybrid mode flag set by the floating fallback logic.

Also adds a test case to verify compact() works correctly from hidden state.

* docs: add hybrid mode documentation to README and DocC

Document the new hybrid mode feature:
- README: Add Hybrid Mode section with code example and floating style behavior
- DocC: Add Hybrid Mode section explaining the feature and automatic fallback

* fix: improve floating hybrid mode reliability and documentation

- Clarify README wording: indicators are "displayed when requested" not "always visible"
- Fix icon sizing in floating hybrid mode by adding proper safeAreaInsets
- Fix rapid state transition rendering by using opacity instead of conditional rendering
- Add animation delay in test for state transition reliability
- Apply swiftformat

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: prevent compact indicator layout compression during rapid transitions

Add .fixedSize() and .layoutPriority(1) to compact leading/trailing
content in floating view so SwiftUI preserves their intrinsic sizes
even during rapid state changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 7 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="Sources/DynamicNotchKit/Views/NotchlessView.swift">

<violation number="1" location="Sources/DynamicNotchKit/Views/NotchlessView.swift:78">
P2: Views hidden with `.opacity(0)` remain accessible to VoiceOver. Consider adding `.accessibilityHidden(dynamicNotch.disableCompactLeading)` to prevent hidden content from being announced to screen reader users.</violation>

<violation number="2" location="Sources/DynamicNotchKit/Views/NotchlessView.swift:95">
P2: Views hidden with `.opacity(0)` remain accessible to VoiceOver. Consider adding `.accessibilityHidden(dynamicNotch.disableCompactTrailing)` for consistency with leading content.</violation>
</file>

Reply to cubic to teach it or ask questions. Tag @cubic-dev-ai to re-run a review.

Copy link

@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: 0

🧹 Nitpick comments (1)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (1)

203-203: Consider removing unused skipHide parameter.

The skipHide parameter is declared with an underscore prefix (skipHide _:), explicitly marking it as unused. While this prevents compiler warnings, the parameter is threaded through the call chain (DynamicNotchInfo.compact()_compact()_expand()) but never actually used. If this parameter is no longer needed after refactoring, consider removing it from the signature to reduce confusion. If it's kept for API compatibility or future use, a comment explaining the intent would be helpful.

🔎 Option 1: Remove if truly unused
-    func _expand(on screen: NSScreen = NSScreen.screens[0], skipHide _: Bool, resetHybridMode: Bool = true) async {
+    func _expand(on screen: NSScreen = NSScreen.screens[0], resetHybridMode: Bool = true) async {

Note: This would require updating callers in _compact() and potentially DynamicNotchInfo.

🔎 Option 2: Add clarifying comment if intentionally kept
+    // skipHide parameter kept for backwards compatibility with internal call sites
     func _expand(on screen: NSScreen = NSScreen.screens[0], skipHide _: Bool, resetHybridMode: Bool = true) async {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e632b45 and 2cb676d.

📒 Files selected for processing (7)
  • README.md
  • Sources/DynamicNotchKit/Documentation.docc/Documentation.md
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
  • Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo+HelperViews.swift
  • Sources/DynamicNotchKit/Views/NotchView.swift
  • Sources/DynamicNotchKit/Views/NotchlessView.swift
  • Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift
🧰 Additional context used
🧬 Code graph analysis (4)
Sources/DynamicNotchKit/Views/NotchlessView.swift (2)
Sources/DynamicNotchKit/Views/NotchView.swift (1)
  • expandedContent (172-184)
Sources/DynamicNotchKit/Utility/BlurModifier.swift (1)
  • blur (31-36)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (1)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)
  • compact (113-120)
Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (2)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (3)
  • expand (199-201)
  • compact (239-241)
  • hide (310-316)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (3)
  • expand (104-111)
  • compact (113-120)
  • hide (122-124)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo+HelperViews.swift (1)
Sources/DynamicNotchKit/Utility/BlurModifier.swift (2)
  • body (13-16)
  • body (24-27)
🔇 Additional comments (22)
README.md (1)

49-71: LGTM! Clear documentation for hybrid mode.

The documentation clearly explains the hybrid layout concept with a practical code example and covers the floating style fallback behavior. This helps users understand when and how to use the feature.

Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo+HelperViews.swift (3)

19-19: LGTM! Appropriate visibility reduction.

Reducing the body property visibility from public to internal is a good encapsulation practice for helper views that are implementation details.


37-37: LGTM! Consistent visibility change.

Consistent with CompactLeadingView, this helper view's body is correctly internalized.


52-52: LGTM! Helper view properly encapsulated.

The InfoView body is appropriately made internal, completing the visibility reduction for all helper views in this file.

Sources/DynamicNotchKit/Views/NotchlessView.swift (3)

48-51: LGTM! Hybrid mode integration is correct.

The conditional rendering of compactIndicatorsRow() based on isHybridModeEnabled is the right approach for showing compact indicators alongside expanded content in floating mode.


55-58: LGTM! Proper safe area adjustment for hybrid mode.

Suppressing the top inset when hybrid mode is active makes sense since the compactIndicatorsRow() provides its own spacing. This avoids double-padding the top edge.


66-101: LGTM! Well-implemented compact indicators row.

The implementation correctly:

  • Uses opacity instead of conditional rendering to avoid SwiftUI layout issues
  • Applies proper environment values for each section
  • Uses fixed sizing and layout priority for stable layouts
  • Includes appropriate transitions and padding

The approach of always rendering but hiding with opacity is a good pattern for avoiding SwiftUI's conditional rendering glitches.

Sources/DynamicNotchKit/Documentation.docc/Documentation.md (1)

17-22: LGTM! Consistent DocC documentation.

The Hybrid Mode documentation is well-written and consistent with the README, properly explaining both the explicit flag-based activation and the automatic floating fallback behavior.

Sources/DynamicNotchKit/Views/NotchView.swift (3)

79-111: LGTM! Well-structured hybrid layout logic.

The refactored notchContent() correctly computes hybrid mode state and applies appropriate sizing constraints. The logic for useHybridLayout (hybrid mode + expanded state) and the conditional .frame(maxWidth: .infinity) properly enables full-width compact content in hybrid mode while maintaining compact sizing in normal mode.


113-170: LGTM! Clean refactoring with proper visibility logic.

The compactContent() refactoring correctly:

  • Determines visibility based on hybrid vs normal mode
  • Uses symmetric layout for equal-width containers in hybrid expanded state
  • Delegates to the new compactSideContent helper for cleaner code
  • Maintains width tracking for offset calculations

The visibility logic (showContent) properly shows compact content in both states when hybrid mode is enabled, and only in compact state otherwise.


186-224: LGTM! Excellent helper abstraction.

The compactSideContent helper cleanly encapsulates single-side rendering with:

  • Proper environment value propagation
  • Correct safe area insets (adaptive edge padding based on layout mode)
  • Width measurement binding for offset calculations
  • Appropriate transitions for animations

The symmetric layout creates equal-width containers with content aligned to outer edges, which is exactly what's needed for balanced hybrid mode indicators.

Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (1)

271-500: LGTM! Comprehensive hybrid mode test coverage.

The test suite thoroughly covers:

  • Explicit hybrid mode activation for both notch and floating styles
  • Automatic floating fallback behavior when compact() is called
  • Proper flag reset on expand() and hide()
  • Computed property logic for isHybridModeEnabled
  • Edge case of compact() from hidden state
  • Rapid state transitions for race condition testing

All tests properly verify state transitions, flag values, and behavior differences between notch and floating styles. The use of expectations and appropriate delays for animations ensures reliable test execution.

Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (10)

27-29: Documentation accurately reflects new floating mode behavior.

The updated documentation correctly describes that compact(on:) on floating-style devices now expands with hybrid mode enabled, showing compact indicators alongside content rather than hiding the window.


79-96: Well-designed property separation for hybrid mode state.

The separation between showCompactContentInExpandedMode (user-facing) and floatingHybridModeActive (internal) is a good design choice that prevents automatic floating fallback behavior from overwriting user configuration. The isHybridModeEnabled computed property provides clean abstraction.


105-109: Retry limit prevents infinite loops in hover-blocking scenarios.

The maxHideRetries limit (50 retries × 0.1s = 5 seconds) is a sensible safety mechanism to prevent the hide operation from blocking indefinitely if hover state becomes stuck.


115-129: Init parameter addition maintains backwards compatibility.

The showCompactContentInExpandedMode parameter defaults to false, preserving existing behavior while enabling the new hybrid mode feature.


161-174: Screen parameter observation properly manages lifecycle and threading.

The observer task correctly uses weak self to prevent retain cycles, checks state before reinitializing to avoid disrupting active animations, and ensures window initialization happens on MainActor.


176-179: Proper task cleanup in deinit.

Both observer and close tasks are correctly cancelled during deinitialization, preventing resource leaks.


204-215: Hybrid mode reset logic correctly preserves state when needed.

The resetHybridMode parameter ensures that floatingHybridModeActive is preserved when _expand() is called from _compact()'s floating fallback path, while resetting it for explicit expand calls. The early return with task cancellation is appropriate.


243-280: Floating mode hybrid logic is well-structured and race-safe.

The implementation correctly handles the floating fallback by:

  1. Atomically checking state and hybrid mode status to avoid races
  2. Optimizing to skip redundant animations when already in hybrid mode
  3. Preserving the floatingHybridModeActive flag when calling _expand() via resetHybridMode: false

The atomic state read pattern (lines 258-260) followed by conditional state mutation (265-270) is appropriate since all state changes occur on MainActor.


293-307: Compact transition logic mirrors expand behavior.

The direct state transition from expanded to compact (without intermediate hide) provides consistent animation behavior in both directions and improves UX by reducing visual flashing.


318-366: Hide implementation correctly handles hover retry and cancellation.

The implementation properly addresses previous race conditions and continuation leaks:

  1. Retry logic with maxHideRetries limit prevents infinite loops while respecting hover behavior
  2. floatingHybridModeActive reset occurs after animation to avoid visual glitches
  3. deinitializeWindow() is skipped when cancelled to prevent closing a newly created window
  4. completion?() is always called to ensure continuation is resumed

The timeout mechanism (5 seconds) is a reasonable fallback if hover state becomes stuck.

- Remove unused skipHide parameter from _expand() and _compact()
- Add .accessibilityHidden() to opacity-hidden views for VoiceOver
- Fix rapid transition rendering by always rendering compact row
  (use frame height instead of conditional rendering to prevent
  SwiftUI view identity corruption)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)

62-62: Remove unused shouldSkipHideWhenConverting flag.

This flag is set on line 99 but is never read after the internal method calls were updated to use _expand and _compact (which no longer utilize this parameter). This is dead code that should be removed.

🔎 Proposed fix
-    @Published var shouldSkipHideWhenConverting: Bool = false

And remove the assignment on line 99:

         if let compactLeading {
             self.compactLeading = compactLeading
         } else {
             self.compactLeading = icon
-            self.shouldSkipHideWhenConverting = true
         }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cb676d and 96ebf91.

📒 Files selected for processing (3)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
  • Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift
  • Sources/DynamicNotchKit/Views/NotchlessView.swift
🧰 Additional context used
🧬 Code graph analysis (2)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (3)
  • _expand (203-237)
  • compact (239-241)
  • _compact (243-308)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (1)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)
  • compact (110-114)
🔇 Additional comments (10)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)

107-107: Behavior is correct and intentional.

The switch to internal _expand(on:) and _compact(on:) methods is properly designed. DynamicNotchInfo.expand() correctly defaults to resetHybridMode=true, which resets the floating hybrid mode flag when explicitly called. This is intentional behavior documented in the code comments ("Reset floating hybrid mode when explicitly expanding"). The public API should reset hybrid mode for consistent user-facing behavior, while internal calls (like from within _compact) preserve the mode by passing resetHybridMode=false.

Sources/DynamicNotchKit/Views/NotchlessView.swift (2)

48-59: LGTM: Clean approach to maintain view identity during transitions.

The use of .frame(height:) with .clipped() instead of conditional rendering avoids SwiftUI's identity issues during rapid state changes. The conditional top safe area inset correctly prevents double-spacing when the indicators row is visible.


70-103: LGTM: Indicators row implementation is complete and accessible.

The method properly handles:

  • Stable rendering with opacity-based visibility to avoid conditional rendering issues
  • Accessibility via .accessibilityHidden() for disabled content (addressing past feedback)
  • Proper environment values and spacing for all three content sections
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (7)

27-29: LGTM: Documentation updated to reflect new floating behavior.

The documentation now correctly states that compact() on floating devices expands with hybrid mode, addressing past feedback about the outdated "automatically hide" statement.


79-96: LGTM: Clean separation of user config and internal state.

The dual-flag design (showCompactContentInExpandedMode for user config, floatingHybridModeActive for floating fallback) correctly avoids mutating user settings as a side effect, addressing past feedback.


104-109: LGTM: Proper task lifecycle management and timeout safety.

The screenObserverTask tracking enables proper cleanup in deinit, and maxHideRetries provides a safety net against infinite retry loops (5-second timeout).


160-179: LGTM: Proper async lifecycle management.

The screen observer correctly:

  • Uses weak self to avoid retain cycles
  • Only reinitializes when hidden to avoid disrupting animations
  • Cancels tasks in deinit for proper cleanup

203-237: LGTM: Hybrid mode reset logic is well-designed.

The resetHybridMode parameter correctly handles two cases:

  • Explicit expand() calls reset hybrid mode (even if already expanded)
  • Calls from _compact() preserve the hybrid flag

The pre-guard reset is intentional per the inline comment and ensures consistent behavior.


243-308: LGTM: Floating fallback and direct transitions are well-implemented.

The logic correctly handles:

  • Floating mode expanding with hybrid indicators (addressing past "mutating user config" issue)
  • Atomic state reads to avoid race conditions
  • Conditional animation only when needed (prevents redundant animations)
  • Direct compact↔expanded transitions with consistent animation handling

310-366: LGTM: Robust hide logic with proper timeout and cancellation handling.

The implementation correctly addresses all past feedback:

  • Retry timeout prevents infinite loops (50 retries × 0.1s = 5s max)
  • Cancellation handling avoids race conditions (only deinitializes if not cancelled)
  • Continuation always resumed to prevent leaks
  • Hybrid flag reset after animation prevents visual glitches

- Apply animation at VStack level for smooth container resize when hiding top row
- Use explicit frame constraints for compact icons instead of fixedSize()
- Disable matchedGeometryEffect in hybrid mode to prevent icon animation conflicts
- Forward objectWillChange from internal DynamicNotch to DynamicNotchInfo
- Reset shouldSkipHideWhenConverting when compactLeading is explicitly changed
- Animate floatingHybridModeActive reset with conversionAnimation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="Sources/DynamicNotchKit/Views/NotchlessView.swift">

<violation number="1" location="Sources/DynamicNotchKit/Views/NotchlessView.swift:78">
P3: Comment references removed code. The `4pt` and `8pt` insets mentioned were removed in this same diff (the `safeAreaInset` modifiers are gone). Consider updating the comment to reflect the actual purpose, e.g., &quot;Leave visual margin within the row frame&quot; or similar.</violation>
</file>

Reply to cubic to teach it or ask questions. Tag @cubic-dev-ai to re-run a review.

Copy link

@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: 0

🧹 Nitpick comments (2)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotchStyle.swift (1)

66-91: Good centralization of animation duration constant.

The refactor to use Self.standardAnimationDuration improves maintainability. However, there's duplicated documentation between the private constant (lines 66-68) and the computed property (lines 87-88). Consider keeping the doc comment only on the public-facing animationDuration property.

🔎 Suggested simplification
-    /// The duration of animations in seconds.
-    /// Used internally to coordinate async operations with animation completion.
-    /// This constant is used by all animation properties to ensure consistency.
     private static let standardAnimationDuration: TimeInterval = 0.4
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)

117-127: Calling internal _expand/_compact bypasses public API.

These methods now call the internal _expand(on:) and _compact(on:) instead of the public wrappers. This is intentional to avoid double-wrapping, but it creates a coupling to internal implementation details. Consider whether exposing these internal methods is the right design, or if the forwarding should go through the public API.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96ebf91 and 3af16a7.

📒 Files selected for processing (7)
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift
  • Sources/DynamicNotchKit/DynamicNotch/DynamicNotchStyle.swift
  • Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo+HelperViews.swift
  • Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift
  • Sources/DynamicNotchKit/Utility/DynamicNotchControllable.swift
  • Sources/DynamicNotchKit/Views/NotchlessView.swift
  • Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift
🧰 Additional context used
🧬 Code graph analysis (4)
Sources/DynamicNotchKit/Views/NotchlessView.swift (2)
Sources/DynamicNotchKit/Views/NotchView.swift (2)
  • notchContent (85-111)
  • expandedContent (172-184)
Sources/DynamicNotchKit/Utility/BlurModifier.swift (1)
  • blur (31-36)
Sources/DynamicNotchKit/Utility/DynamicNotchControllable.swift (2)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (2)
  • expand (198-200)
  • compact (243-245)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (2)
  • expand (117-121)
  • compact (123-127)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (3)
  • _expand (202-241)
  • compact (243-245)
  • _compact (247-309)
Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (1)
Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (2)
  • expand (117-121)
  • compact (123-127)
🔇 Additional comments (12)
Sources/DynamicNotchKit/Utility/DynamicNotchControllable.swift (1)

21-29: Protocol signature updated to accept optional NSScreen.

The change from NSScreen to NSScreen? aligns with the implementations in DynamicNotch.swift and DynamicNotchInfo.swift, which provide default parameter values (= nil). The documentation correctly describes the fallback behavior.

Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo.swift (1)

101-106: Good use of Combine to forward objectWillChange events.

The sink pattern correctly propagates state changes from the internal DynamicNotch to observers of DynamicNotchInfo, ensuring views update when hybrid mode state changes. The cancellable is properly stored to prevent premature deallocation.

Sources/DynamicNotchKit/Views/NotchlessView.swift (2)

46-49: Verify padding calculation in indicatorsRowHeight.

The comment says "frame height + vertical padding" but the actual vertical padding in compactIndicatorsRow() at line 112 is .padding(.vertical, 10), which adds 20pt total (10pt top + 10pt bottom). This matches the + 20 calculation, so the implementation is correct.


81-113: Well-structured hybrid mode indicators row.

The implementation addresses previous review feedback:

  • accessibilityHidden is correctly applied to opacity-hidden content (lines 92, 108)
  • The row uses explicit frame constraints for consistent icon sizing
  • Center content is properly exposed via the environment

The animation at the container level (line 73) ensures smooth height transitions when toggling hybrid mode.

Tests/DynamicNotchKitTests/DynamicNotchKitTests.swift (3)

271-336: Comprehensive hybrid mode test coverage.

The new tests thoroughly validate:

  • User-configured hybrid mode (showCompactContentInExpandedMode = true)
  • Style-specific behavior differences between notch and floating modes
  • State assertions after each transition

Good practice including compactCenterContent setup to test the full hybrid mode layout.


340-398: Floating fallback test validates intentional behavior change.

This test documents the key behavioral change: compact() on floating style now expands with hybrid mode instead of hiding. The assertions clearly verify:

  • User property (showCompactContentInExpandedMode) is NOT mutated
  • Internal flag (floatingHybridModeActive) IS set
  • Computed property (isHybridModeEnabled) correctly reflects the combined state

481-500: Rapid state transition test provides regression coverage.

This test helps catch race conditions and ensures the async state machine handles rapid transitions gracefully. Consider adding assertions for intermediate states if flakiness is observed.

Sources/DynamicNotchKit/DynamicNotch/DynamicNotch.swift (4)

80-97: Clean separation of user setting vs internal hybrid mode state.

The design correctly separates:

  • showCompactContentInExpandedMode: User-configured public property
  • floatingHybridModeActive: Internal flag for floating fallback
  • isHybridModeEnabled: Computed property combining both

This addresses the past review feedback about not mutating user configuration.


175-178: Good resource cleanup in deinit.

Cancelling both screenObserverTask and closePanelTask prevents resource leaks and potential crashes from tasks running after deallocation.


319-369: Robust hide implementation with proper error handling.

The implementation addresses all past review feedback:

  1. Hover retry limit (lines 331-340): maxHideRetries = 50 prevents infinite loops
  2. Cancellation handling (lines 350-365): Explicitly catches CancellationError, resets floatingHybridModeActive after animation, and only deinitializes if not cancelled
  3. Continuation safety: completion?() is always called to prevent leaks

The do-catch structure at lines 350-356 is slightly verbose but correctly handles the limited exception types from Task.sleep.


265-278: Hybrid mode animation is now properly awaited.

This addresses the past review feedback about fire-and-forget Tasks. The animation completion is awaited with Task.sleep before returning, ensuring proper sequencing with subsequent operations.

Sources/DynamicNotchKit/DynamicNotchInfo/DynamicNotchInfo+HelperViews.swift (1)

19-32: Force unwrap is safe given the current initialization order.

The force unwrap of internalDynamicNotch at line 20 (and line 59 in InfoView) is safe because internalDynamicNotch is initialized on line 91 before compactLeading and compactTrailing are assigned (line 108+), which means the didSet observers execute only after initialization. View bodies are only rendered after init completes, ensuring the force unwrap never encounters nil.

The hybrid mode logic for conditionally disabling matchedGeometryEffect is well-designed with clear comments explaining the rationale.

- Fix race condition in closePanelTask that caused missing trailing icon on rapid transitions
- Add explicit frame constraints for compact icons in NotchView (fixes gradient sizing in notch mode)
- Update gradient test to skip compact() in floating mode (tested separately in hybrid mode tests)
- Expose showCompactContentInExpandedMode parameter on DynamicNotchInfo
- Remove duplicated doc comments in DynamicNotchStyle
- Update comment to reflect actual purpose (removed inset references)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@dima6312 dima6312 merged commit ce27a05 into main Dec 29, 2025
2 checks passed
@dima6312 dima6312 deleted the feature/hybrid-compact-expanded-mode branch December 29, 2025 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants