Skip to content

Conversation

@dima6312
Copy link

@dima6312 dima6312 commented Dec 29, 2025

Summary

This PR introduces hybrid mode - a layout option that displays compact leading/trailing content alongside the expanded notch panel. This enables use cases like dictation UIs with persistent waveform indicators, or media players with always-visible controls.

Key additions:

  • showCompactContentInExpandedMode property for hybrid layouts
  • Automatic hybrid mode fallback for floating style on non-notch Macs
  • Public NSScreen extensions for notch detection (hasNotch, notchSize, notchFrame)
  • Public isHovering property for hover-based activation patterns

Motivation

When building interfaces like voice dictation UIs, developers need status indicators (waveforms, cancel buttons) that remain visible while expanded content displays below. Previously, compact content was only visible in .compact state on notch Macs and invisible entirely on non-notch Macs.

This PR solves both:

  1. Notch Macs: Enable hybrid mode to show compact indicators alongside expanded content
  2. Non-notch Macs: Automatic fallback shows the same indicators in floating panels

Usage

let notch = DynamicNotch(
    compactLeading: { WaveformView() },
    compactTrailing: { CancelButton() },
    expanded: { TranscriptView() },
    style: .notch,
    showCompactContentInExpandedMode: true
)

On non-notch Macs, compact() automatically enables hybrid mode in floating style.

  // Dynamically hide compact views
  dynamicNotch.disableCompactLeading = true  // Hides leading, no gap
  dynamicNotch.disableCompactTrailing = true // Hides trailing, no gap

Changes

Features

  • showCompactContentInExpandedMode - compact content visible in expanded state
  • compactCenterContent - optional center content for floating hybrid mode
  • Public NSScreen extensions for notch detection
  • Public isHovering for hover observation
  • Public disableCompactLeading and disableCompactTrailing properties for dynamic compact view control

Bug Fixes

  • Prevent continuation leak in hide() when closePanelTask is cancelled
  • Direct compact↔expanded transitions without intermediate hide step
  • Proper task cancellation and weak self captures
  • Reset namespace on window deinitialization to prevent stale references
  • Fix compact content rendering on window recreation during rapid transitions
  • Allow compact content to expand horizontally for text labels
  • Fix floating mode layout: disabled compact views no longer reserve space (use conditional rendering instead of opacity)

Code Quality

  • Structured concurrency with MainActor.run
  • animationDuration constant extracted from DynamicNotchStyle
  • Accessibility improvements (.accessibilityHidden() for VoiceOver)
  • Comprehensive test coverage (19 tests)
  • DocC and README documentation

Test Plan

  • All 19 tests pass
  • Project builds with zero warnings
  • Hybrid mode works on notch Macs
  • Floating fallback works on non-notch Macs
  • Rapid state transitions render correctly
  • Text labels in compact content expand properly

Breaking Changes

None. All APIs are additive with sensible defaults.

Demo

Hybrid mode demo
┌─────────────────────────────────────────┐
│  [indicator]    ████████    [cancel]    │  ← Compact content visible
├─────────────────────────────────────────┤
│                                         │
│           Expanded Content              │  ← Expanded content below
│                                         │
└─────────────────────────────────────────┘

dima6312 and others added 4 commits December 29, 2025 14:22
* feat: add showCompactContentInExpandedMode for hybrid notch layouts

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 <[email protected]>

* feat: Make NSScreen extensions public

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 <[email protected]>

* feat: Auto-enable hybrid mode in floating style for UX consistency

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 <[email protected]>

* fix: Add hybrid mode support for floating style fallback

- 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 <[email protected]>

* feat: Add compactCenterContent for floating fallback mode

- 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 <[email protected]>

* fix: smooth direct transition from expanded to compact state

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 <[email protected]>

* fix: Prevent race condition when cancelling hide task

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 <[email protected]>

* fix: Await hybrid mode animation to prevent race with hide()

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 <[email protected]>

* feat: make isHovering property public for client observation

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 <[email protected]>

* fix: comprehensive improvements for hybrid mode reliability

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 <[email protected]>

* Fix and docs: add hybrid mode documentation to README and DocC and fix 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Claude <[email protected]>

* fix: improve code quality and accessibility

- 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 <[email protected]>

* fix: improve floating mode icon layout and animation smoothness

- 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 <[email protected]>

* fix: improve code quality, fix race conditions, and refine test behavior

- 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 <[email protected]>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
Change frame constraints from fixed width to minWidth for compact
leading/trailing content in both NotchView and NotchlessView. This
allows text content like "Pasted" to display fully instead of being
clipped to icon size.

- NotchView: content expands away from notch (leading→left, trailing→right)
- NotchlessView: add fixedSize() and layoutPriority(1) for layout stability
- Guard compactIconSize with max(..., 0) to prevent negative values

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Reset namespace in deinitializeWindow() to prevent stale references
- Switch to conditional rendering with transition for compact indicators
  row instead of height 0 + clipped approach which caused layout issues
- Remove unnecessary fixedSize() and layoutPriority() modifiers

The previous approach of always rendering the row with height 0 and
clipped caused the trailing content to not render on subsequent window
recreations during rapid state transitions.

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Make disableCompactLeading and disableCompactTrailing properties public
  so developers can dynamically hide compact views on DynamicNotch instances
- Fix floating mode (NotchlessView) to use conditional rendering instead of
  opacity, ensuring disabled views don't reserve space
- Use HStack spacing for cleaner layout when views are conditionally removed

No breaking changes - existing DynamicNotchInfo API continues to work unchanged.

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

Co-authored-by: Claude Opus 4.5 <[email protected]>
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.

1 participant