Compact-only notch with expandable panels#1

open10
main
feat/compact-only
opened8d agobyKKainoa Newton
View on GitHubTwo-way sync enabledPR last imported 8d ago

Summary

Compact is now the only notch layout (VISION.md items 1 and 2). Calendar, mirror and shelf open as panels: the notch shape springs wider and taller to each panel's own size, then collapses back to the music player. The design is in docs/PANELS.md.

What changed

  • NotchPanel (components/Notch/NotchPanel.swift) describes a panel: id, title, icon, availability, preferred size and content, plus start/stop hooks. The built-in panels are listed in NotchPanels.builtIn, so a Raycast panel later is one more entry there.
  • Header row (NotchPanelHeader):
    • Left side: the settings gear first, then the calendar, mirror and shelf buttons.
    • Right side: the battery, or the volume/brightness indicator while one is showing.
    • On a notched display the two sides sit either side of the cutout. Without a notch it's a plain bar.
    • Clicking the active button again goes back to the music player.
  • Morph. The panel area is framed at the panel's size inside a spring (StandardAnimations.panelMorph), so a single notch shape animates between sizes in one window. Only the content fades.
  • Hover and close. After each panel switch, the real pointer position is checked against the notch's measured frame and fed into the existing hover handling. A pointer left outside after a collapse closes the notch through the usual 350 ms grace period. Popovers and sharing still keep it open.
  • Per-display state. BoringViewModel.activePanel replaces the shared coordinator.currentView. open(panel:) picks the starting panel. Dragging a file in opens the shelf, or switches to it if the notch is already open.
  • Mirror. The panel takes the camera's aspect ratio, or a square for the circle shape. The camera only runs while the panel is open, and CameraPreviewView fills the frame it's given.
  • Sizing. The window size comes from maxOpenNotchSize (now 640Γ—280, plus shadow room), and panels are clamped to it. Panel widths are widened where needed so the header fits beside the real cutout.
  • Removed:
    • the tab bar, BoringHeader, and the old full layout NotchHomeView (the music controls still in use moved to components/Music/MusicPlayerControls.swift)
    • the compactMode, "Always show tabs" and "Remember last tab" settings and their strings
    • the stale values of those settings, which are cleared from user defaults at launch
  • Defaults. showCalendar and showMirror now default to on, so the buttons are there out of the box. Anyone who turned either off keeps it off.

Unverified at runtime

Agents don't launch the app, so none of this has been seen running. scripts/dev.sh build passes, and the two compiler warnings it shows are the same as on main.

  • How the spring feels compared with Vorsaint, and whether the music player's measured height is right.
  • Whether the header fits beside wider cutouts. The music player is now about 50 pt wider on notched Macs so the four left-side buttons fit.
  • Behaviour on non-notch displays (MacBook Air M1) and external monitors.
  • The notch window is now 300 pt tall instead of 210 pt. Clear areas should let clicks through as before, but the lock-screen bug where the window swallows nearby clicks may now cover a larger area.
  • Turning off a panel in Settings while it's open may not take effect until the notch redraws.
  • The mirror opens at 16:9 the first time and reshapes once a camera with a different shape starts.

How to test

  1. Quit the installed Boring Notch first (right-click the notch and choose Quit). The dev build uses the same bundle ID and won't start while it's running.
  2. Build and run:
    sh
    1cd /Users/knewton/.t3/worktrees/not.boring.notch/feat-compact-only && scripts/dev.sh
    Ctrl-C quits. scripts/dev.sh kill also stops it.
  3. Permissions:
    • Camera for the mirror. You're asked the first time you open the mirror panel.
    • Calendar and Reminders for the calendar panel.
    • Accessibility, only if you use the volume/brightness overlay or notification features.
    • Ad-hoc rebuilds may ask for these again.

Checklist (start on the notch-less MacBook Air M1):

  • Hover over or click the notch. Expected: the music player, under a top bar with the gear, calendar, camera and tray icons on the left and the battery on the right. There's no gap in the middle on a display without a notch.
  • Click the calendar icon. Expected: the notch springs wider and taller and shows the date strip and events. Click it again and it springs back to the music player.
  • Click the camera icon. Expected: the notch reshapes to the camera's shape and the camera's green light comes on. Switching panels or closing the notch turns the camera off.
  • Set the mirror shape to circle in Settings. Expected: a square panel with a round preview.
  • Click the tray icon. Expected: a wide, short shelf panel.
  • Drag a file to the top of the screen, once with the notch closed and once with it open on another panel. Expected: it opens or switches to the shelf.
  • Open the calendar, collapse it, then move the pointer away. Expected: the notch closes about 0.35 s later. While the pointer stays inside it never closes, even right after a collapse.
  • Close the notch and reopen it. Expected: it opens on the music player, or on the shelf if the shelf has files and "open shelf by default" is on.
  • Try with no music playing. Expected: the last track's info is still shown and the panels still work.
  • Open Settings. Expected: "Compact mode", "Always show tabs" and "Remember last tab" are gone. Turning off "Show calendar" or "Enable boring mirror" hides that button.
  • On a notched Mac or an external monitor later: the icons sit beside the cutout without overlapping it.

πŸ€– Generated with Claude Code


Summary by cubic

Makes compact the only notch layout. Calendar, mirror and shelf now open as panels that spring the notch shape to their own size, then collapse back to the music player.

  • The open notch is a header row above one panel: settings and feature buttons on one side of the cutout, battery or OSD on the other (a plain bar without a notch).
  • Each panel is a NotchPanel (id, title, icon, availability, size, content, start/stop hooks); the built-ins are registered in NotchPanels.builtIn, so a future Raycast panel is one entry.
  • The notch shape morphs between panel sizes in a single window via StandardAnimations.panelMorph; only the content fades. Clicking the active button again returns to home.
  • activePanel is per display and replaces the shared coordinator.currentView; dragging a file opens the shelf, or switches to it if the notch is already open. Hover is reconciled against the measured frame on every activePanel change, covering switches, drags, and panels turned off in Settings.
  • The mirror sizes to the camera's aspect ratio (square for the circle shape) and defaults to 4:3 until a camera has run; the camera only runs while its panel is open and stops when the panel is turned off in Settings. A camera disconnecting only hides the mirror button: the panel stays active instead of silently falling back to home, and reconnecting never restarts capture behind home.
  • The mirror button is hidden on Macs without a camera.
  • Removed the tab bar, the old full layout, the compactMode setting, "Always show tabs" and "Remember last tab"; stale values of the removed settings are cleared from user defaults at launch.
  • showCalendar and showMirror now default to on; users who turned either off keep it off.

Known risks

  • None of this has been run; it builds with the same compiler warnings as main.
  • The notch window is now 300pt tall instead of 210pt; the lock-screen click-swallowing bug may cover a larger area.
  • The mirror opens at 4:3 the first time and reshapes once a camera with a different shape starts.

Written for commit 3a75b1066eb300c269d4af535381c6057ff19349. Summary will update on new commits.

View guided diff Turn on auto-fix

Checks

7 successful Β· 1 neutral Β· 1 skipped
All checks passed
3a75b10

Added 5 commits

  • Merge branch 'main' into feat/compact-only764b605
  • Document the notch panel designc4b000e
  • Move the settings gear to the start of the header's leading sided2f4e96
  • Remove the compactMode setting and show calendar and mirror by defaulteb8d826
  • Replace the tabbed notch with expandable panelsf4da9df
Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 690142ca-0a73-490b-949a-86b6ccc7d38b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review
  • Autofix Β· Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

❀️ Share

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

Synced from GitHub

⚠️ This PR targets main.

Fork PRs should normally target dev. Targeting main is only allowed for repository metadata changes such as docs, licensing, and .github/** updates. This PR changes one or more files outside that allowlist, so please retarget it to dev.

boringNotch/models/CameraModel.swiftline 38Outdated Β· d2f4e96Open thread
……@@ -31,6 +31,11 @@ final class CameraModel {
35+ /// the session stops so the mirror panel opens at the right shape next
36+ /// time instead of reshaping once capture starts. 16:9 until a camera
37+ /// has run, which is what built-in FaceTime cameras deliver.
38+ private(set) var videoAspectRatio: CGFloat = 16.0 / 9.0
Ccubic-dev-aiBoton line 388d ago

P3: The 16:9 default and its comment don't match the capture path. The engine uses the .medium preset, which likely yields 4:3 frames, so the value read after start will usually differ from the default. Derive the default from the same preset, or correct the comment, and note that the first open after launch still reshapes.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/models/CameraModel.swift, line 38:
2
3<comment>The 16:9 default and its comment don't match the capture path. The engine uses the `.medium` preset, which likely yields 4:3 frames, so the value read after start will usually differ from the default. Derive the default from the same preset, or correct the comment, and note that the first open after launch still reshapes.</comment>
4
5<file context>
6@@ -31,6 +31,11 @@ final class CameraModel {
7+ /// the session stops so the mirror panel opens at the right shape next
8+ /// time instead of reshaping once capture starts. 16:9 until a camera
9+ /// has run, which is what built-in FaceTime cameras deliver.
10+ private(set) var videoAspectRatio: CGFloat = 16.0 / 9.0
11
12 @ObservationIgnored private let engine: CameraSessionEngine
13</file context>

βœ… Addressed in 5cc78e3

boringNotch/ContentView.swiftline 409Outdated Β· d2f4e96Open thread
……@@ -387,9 +399,15 @@ struct ContentView: View {
406+ doOpen(panel: .shelf)
407+ } else {
408+ withAnimation(StandardAnimations.panelMorph) {
409+ vm.showPanel(.shelf)
Ccubic-dev-aiBoton line 4098d ago

P2: Switching to Shelf from an already-open notch does not reconcile hover state after the resize. If the pointer is in the area removed by the smaller Shelf panel, AppKit may not send an exit, and the drop cleanup clears dropEvent without closing; call reconcileHoverAfterResize() after showPanel(.shelf) as the header path already does.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/ContentView.swift, line 409:
2
3<comment>Switching to Shelf from an already-open notch does not reconcile hover state after the resize. If the pointer is in the area removed by the smaller Shelf panel, AppKit may not send an exit, and the drop cleanup clears `dropEvent` without closing; call `reconcileHoverAfterResize()` after `showPanel(.shelf)` as the header path already does.</comment>
4
5<file context>
6@@ -387,9 +399,15 @@ struct ContentView: View {
7+ doOpen(panel: .shelf)
8+ } else {
9+ withAnimation(StandardAnimations.panelMorph) {
10+ vm.showPanel(.shelf)
11+ }
12 }
13</file context>

βœ… Addressed in 5cc78e3

βœ… Addressed in ae302cf

boringNotch/models/Constants.swiftline 442Outdated Β· d2f4e96Open thread
……@@ -433,13 +433,13 @@ extension Defaults.Keys {
442- static let showMirror = Key<Bool>("showMirror", default: false)
440+ /// Shows the mirror panel's button. On by default: the camera only runs
441+ /// while that panel is open, so it costs nothing until it's used.
442+ static let showMirror = Key<Bool>("showMirror", default: true)
Ccubic-dev-aiBoton line 4428d ago

P2: Mirror now defaults to on, but its button ignores camera presence. On Macs without a camera, the header gets a mirror button that opens an empty panel. Gate isAvailable on a camera being present, or keep the header button hidden when CameraModel.cameraAvailable is false.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/models/Constants.swift, line 442:
2
3<comment>Mirror now defaults to on, but its button ignores camera presence. On Macs without a camera, the header gets a mirror button that opens an empty panel. Gate `isAvailable` on a camera being present, or keep the header button hidden when `CameraModel.cameraAvailable` is false.</comment>
4
5<file context>
6@@ -433,13 +433,13 @@ extension Defaults.Keys {
7- static let showMirror = Key<Bool>("showMirror", default: false)
8+ /// Shows the mirror panel's button. On by default: the camera only runs
9+ /// while that panel is open, so it costs nothing until it's used.
10+ static let showMirror = Key<Bool>("showMirror", default: true)
11 static let isMirrored = Key<Bool>("isMirrored", default: true)
12 static let mirrorShape = Key<MirrorShapeEnum>("mirrorShape", default: MirrorShapeEnum.rectangle)
13</file context>

βœ… Addressed in 5cc78e3

boringNotch/models/BoringViewModel.swiftline 204Outdated Β· d2f4e96Open thread
……@@ -160,23 +186,40 @@ final class BoringViewModel: NSObject, ObservableObject {
202+ }
168203
169- return position.y >= baseY && position.x >= baseX && position.x <= baseX + notchSize.width
204+ var currentPanel: NotchPanel {
Ccubic-dev-aiBoton line 2048d ago

P2: Disabling the active mirror panel does not run its deactivation hook, so the camera can keep running while the notch remains open. Reconcile the active panel when availability changes and call the previous panel’s onDeactivate before falling back to home.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/models/BoringViewModel.swift, line 204:
2
3<comment>Disabling the active mirror panel does not run its deactivation hook, so the camera can keep running while the notch remains open. Reconcile the active panel when availability changes and call the previous panel’s `onDeactivate` before falling back to home.</comment>
4
5<file context>
6@@ -160,23 +186,40 @@ final class BoringViewModel: NSObject, ObservableObject {
7+ }
8
9- return position.y >= baseY && position.x >= baseX && position.x <= baseX + notchSize.width
10+ var currentPanel: NotchPanel {
11+ if let panel = NotchPanels.panel(for: activePanel), panel.isAvailable() {
12+ return panel
13</file context>

βœ… Addressed in 5cc78e3

Fixed in 5cc78e3. BoringViewModel now watches the showCalendar/showMirror/boringShelf settings. If the panel that's showing gets turned off, it runs that panel's onDeactivate (which stops the camera for the mirror) and falls back to home.

docs/PANELS.mdline 29Outdated Β· d2f4e96Open thread
……@@ -0,0 +1,43 @@
26+|---|---|
27+| Home | 336 wide, measured height |
28+| Calendar | 360 Γ— 220 |
29+| Mirror | 200 tall, width from the camera's aspect ratio (square when the mirror shape is a circle) |
Ccubic-dev-aiBoton line 298d ago

P3: The panel table documents Mirror as 200 points tall, but the registry uses a 190-point height. Update the table to match the implementation.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/PANELS.md, line 29:
2
3<comment>The panel table documents Mirror as 200 points tall, but the registry uses a 190-point height. Update the table to match the implementation.</comment>
4
5<file context>
6@@ -0,0 +1,43 @@
7+|---|---|
8+| Home | 336 wide, measured height |
9+| Calendar | 360 Γ— 220 |
10+| Mirror | 200 tall, width from the camera's aspect ratio (square when the mirror shape is a circle) |
11+| Shelf | 480 Γ— 130 |
12+
13</file context>
suggestion
1| Mirror | 190 tall, width from the camera's aspect ratio (square when the mirror shape is a circle) |

βœ… Addressed in 5cc78e3

boringNotch/components/Notch/NotchPanelHeader.swiftline 108Outdated Β· d2f4e96Open thread
……@@ -0,0 +1,147 @@
105+ .contentShape(Circle())
106+ }
107+ .buttonStyle(.plain)
108+ .help(Text("Settings"))
Ccubic-dev-aiBoton line 1088d ago

P3: The settings gear has no accessibility label, so VoiceOver falls back to the SF Symbol name. The panel buttons on the same row set .accessibilityLabel, so add one here too.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/components/Notch/NotchPanelHeader.swift, line 108:
2
3<comment>The settings gear has no accessibility label, so VoiceOver falls back to the SF Symbol name. The panel buttons on the same row set `.accessibilityLabel`, so add one here too.</comment>
4
5<file context>
6@@ -0,0 +1,147 @@
7+ .contentShape(Circle())
8+ }
9+ .buttonStyle(.plain)
10+ .help(Text("Settings"))
11+ }
12+
13</file context>

βœ… Addressed in 5cc78e3

docs/PANELS.mdline 42Outdated Β· d2f4e96Open thread
……@@ -0,0 +1,43 @@
39+
40+## Hit-testing and closing
41+
42+- `.onHover` is attached to the notch's laid-out frame. That frame jumps to the target size at the start of the spring, so the hover region always matches where the notch is going.
Ccubic-dev-aiBoton line 428d ago

P2: The function is named reconcileHoverAfterResize() in ContentView.swift, not reconcileHover(). Rename the reference so readers can find the code.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/PANELS.md, line 42:
2
3<comment>The function is named `reconcileHoverAfterResize()` in `ContentView.swift`, not `reconcileHover()`. Rename the reference so readers can find the code.</comment>
4
5<file context>
6@@ -0,0 +1,43 @@
7+
8+## Hit-testing and closing
9+
10+- `.onHover` is attached to the notch's laid-out frame. That frame jumps to the target size at the start of the spring, so the hover region always matches where the notch is going.
11+- AppKit doesn't always send a hover exit when a view shrinks out from under a pointer that isn't moving. So after each panel change, `reconcileHover()` compares the pointer with the measured notch frame (`vm.notchFrameSize`, centred at the top of the screen, which is how the window is placed on both notched and non-notch displays) and passes the result to `handleHover`. A pointer left outside after a collapse goes through the same 350 ms hover-exit grace and close path as any other exit, and popovers and sharing still block the close.
12</file context>

βœ… Addressed in 5cc78e3

boringNotch/components/Notch/NotchPanel.swiftline 179Outdated Β· d2f4e96Open thread
……@@ -0,0 +1,185 @@
176+ let maximum = maximumContentSize(headerHeight: headerHeight)
177+ let cutout = vm.hasNotch ? vm.closedNotchSize.width : 0
178+ let headerMinimum = cutout + 2 * NotchPanelHeader.sideWidth - 2 * NotchPanelHeader.edgeOverhang
179+ size.width = min(max(size.width, headerMinimum), maximum.width)
Ccubic-dev-aiBoton line 1798d ago

P2: On notched displays this widening breaks the mirror's shape. headerMinimum forces the width to about 385pt, so circle mode becomes a 385x190 pill instead of a square circle, and the rectangle preview is cropped to a width that isn't the camera's aspect ratio. The mirror needs its own width, or the header minimum must not apply to it.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/components/Notch/NotchPanel.swift, line 179:
2
3<comment>On notched displays this widening breaks the mirror's shape. `headerMinimum` forces the width to about 385pt, so circle mode becomes a 385x190 pill instead of a square circle, and the rectangle preview is cropped to a width that isn't the camera's aspect ratio. The mirror needs its own width, or the header minimum must not apply to it.</comment>
4
5<file context>
6@@ -0,0 +1,185 @@
7+ let maximum = maximumContentSize(headerHeight: headerHeight)
8+ let cutout = vm.hasNotch ? vm.closedNotchSize.width : 0
9+ let headerMinimum = cutout + 2 * NotchPanelHeader.sideWidth - 2 * NotchPanelHeader.edgeOverhang
10+ size.width = min(max(size.width, headerMinimum), maximum.width)
11+ if let height = size.height {
12+ size.height = min(height, maximum.height)
13</file context>

βœ… Addressed in 5cc78e3

boringNotch/components/Notch/NotchPanelHeader.swiftline 35Outdated Β· d2f4e96Open thread
……@@ -0,0 +1,147 @@
32+ /// Room each side needs beside the cutout: the gear plus three panel
33+ /// buttons on the leading side (the battery on the trailing side needs
34+ /// less).
35+ static let sideWidth: CGFloat = 4 * buttonSize + 3 * buttonSpacing
Ccubic-dev-aiBoton line 358d ago

P2: sideWidth hardcodes four header buttons (gear plus three panels), but the docs say a new panel only needs a NotchPanels.builtIn entry. A fifth feature button would overflow under the cutout, because the layout only reserves sideWidth per side. Derive the width from the actual button count, or document this constant as a manual step.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/components/Notch/NotchPanelHeader.swift, line 35:
2
3<comment>`sideWidth` hardcodes four header buttons (gear plus three panels), but the docs say a new panel only needs a `NotchPanels.builtIn` entry. A fifth feature button would overflow under the cutout, because the layout only reserves `sideWidth` per side. Derive the width from the actual button count, or document this constant as a manual step.</comment>
4
5<file context>
6@@ -0,0 +1,147 @@
7+ /// Room each side needs beside the cutout: the gear plus three panel
8+ /// buttons on the leading side (the battery on the trailing side needs
9+ /// less).
10+ static let sideWidth: CGFloat = 4 * buttonSize + 3 * buttonSpacing
11+ /// How far the header may reach into the shape's side padding. That
12+ /// padding is sized for the flared top corners, which at header height
13</file context>

βœ… Addressed in 5cc78e3

docs/PANELS.mdline 28Outdated Β· d2f4e96Open thread
……@@ -0,0 +1,43 @@
25+| Panel | Size |
26+|---|---|
27+| Home | 336 wide, measured height |
28+| Calendar | 360 Γ— 220 |
Ccubic-dev-aiBoton line 288d ago

P3: The panel table documents Calendar as 360 Γ— 220, but NotchPanels.calendar uses 360 Γ— 210. Update the table to match the registry.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/PANELS.md, line 28:
2
3<comment>The panel table documents Calendar as 360 Γ— 220, but `NotchPanels.calendar` uses 360 Γ— 210. Update the table to match the registry.</comment>
4
5<file context>
6@@ -0,0 +1,43 @@
7+| Panel | Size |
8+|---|---|
9+| Home | 336 wide, measured height |
10+| Calendar | 360 Γ— 220 |
11+| Mirror | 200 tall, width from the camera's aspect ratio (square when the mirror shape is a circle) |
12+| Shelf | 480 Γ— 130 |
13</file context>
suggestion
1| Calendar | 360 Γ— 210 |

βœ… Addressed in 5cc78e3

Added 2 commits

  • Fix panel review findings5cc78e3
  • Merge origin/main into feat/compact-onlyae302cf
boringNotch/components/Notch/NotchPanel.swiftline 96Outdated Β· 5cc78e3Open thread
……@@ -0,0 +1,201 @@
93+ title: "Mirror",
94+ systemImage: "web.camera",
95+ // No button on a Mac without a camera: the panel would be empty.
96+ isAvailable: { vm in Defaults[.showMirror] && vm.camera.cameraAvailable },
Ccubic-dev-aiBoton line 968d ago

P1: isAvailable now depends on the mutable device list, but currentPanel uses it to silently fall back to Home. A disconnect therefore leaves activePanel == .mirror without invoking onDeactivate; its running intent can restart capture when the camera reconnects, while Home is displayed. It also hides Mirror while permission is pending, so startCameraPreview cannot reach its permission-request path. Separate hardware discovery from panel selection and explicitly deactivate/reset Mirror on removal.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At boringNotch/components/Notch/NotchPanel.swift, line 96:
2
3<comment>`isAvailable` now depends on the mutable device list, but `currentPanel` uses it to silently fall back to Home. A disconnect therefore leaves `activePanel == .mirror` without invoking `onDeactivate`; its running intent can restart capture when the camera reconnects, while Home is displayed. It also hides Mirror while permission is pending, so `startCameraPreview` cannot reach its permission-request path. Separate hardware discovery from panel selection and explicitly deactivate/reset Mirror on removal.</comment>
4
5<file context>
6@@ -92,28 +92,43 @@ enum NotchPanels {
7 systemImage: "web.camera",
8- isAvailable: { Defaults[.showMirror] },
9+ // No button on a Mac without a camera: the panel would be empty.
10+ isAvailable: { vm in Defaults[.showMirror] && vm.camera.cameraAvailable },
11 preferredSize: { vm in
12- let height: CGFloat = 190
13</file context>

Valid, fixed in 91b9278. A missing camera now only hides the mirror's header button (new hasHardware check). It no longer makes the panel unavailable, so a disconnect while the mirror is open keeps activePanel == .mirror and shows the camera's unavailable state. Reconnecting resumes capture in the mirror, not behind home, and the active panel keeps its button so there's a way back. On permission: the engine discovers devices regardless of authorization, so the button isn't hidden while permission is pending.

docs/PANELS.mdline 43Outdated Β· 5cc78e3Open thread
……@@ -0,0 +1,43 @@
40+## Hit-testing and closing
41+
42+- `.onHover` is attached to the notch's laid-out frame. That frame jumps to the target size at the start of the spring, so the hover region always matches where the notch is going.
43+- AppKit doesn't always send a hover exit when a view shrinks out from under a pointer that isn't moving. So after each panel change, `reconcileHoverAfterResize()` compares the pointer with the measured notch frame (`vm.notchFrameSize`, centred at the top of the screen, which is how the window is placed on both notched and non-notch displays) and passes the result to `handleHover`. A pointer left outside after a collapse goes through the same 350 ms hover-exit grace and close path as any other exit, and popovers and sharing still block the close.
Ccubic-dev-aiBoton line 438d ago

P3: reconcileHoverAfterResize() does not run after every panel change: disabling the active panel in Settings switches to home through leaveUnavailablePanel() without calling this helper. Invoke reconciliation for that path, or narrow this documentation to the panel-change paths that actually call it.

Prompt for AI agents
text
1Check if this issue is valid β€” if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/PANELS.md, line 43:
2
3<comment>`reconcileHoverAfterResize()` does not run after every panel change: disabling the active panel in Settings switches to home through `leaveUnavailablePanel()` without calling this helper. Invoke reconciliation for that path, or narrow this documentation to the panel-change paths that actually call it.</comment>
4
5<file context>
6@@ -40,4 +40,4 @@ The panel host is a single top-aligned `ZStack` whose frame is the target panel'
7
8 - `.onHover` is attached to the notch's laid-out frame. That frame jumps to the target size at the start of the spring, so the hover region always matches where the notch is going.
9-- AppKit doesn't always send a hover exit when a view shrinks out from under a pointer that isn't moving. So after each panel change, `reconcileHover()` compares the pointer with the measured notch frame (`vm.notchFrameSize`, centred at the top of the screen, which is how the window is placed on both notched and non-notch displays) and passes the result to `handleHover`. A pointer left outside after a collapse goes through the same 350 ms hover-exit grace and close path as any other exit, and popovers and sharing still block the close.
10+- AppKit doesn't always send a hover exit when a view shrinks out from under a pointer that isn't moving. So after each panel change, `reconcileHoverAfterResize()` compares the pointer with the measured notch frame (`vm.notchFrameSize`, centred at the top of the screen, which is how the window is placed on both notched and non-notch displays) and passes the result to `handleHover`. A pointer left outside after a collapse goes through the same 350 ms hover-exit grace and close path as any other exit, and popovers and sharing still block the close.
11</file context>

βœ… Addressed in 91b9278

Fixed in 91b9278. ContentView now runs reconcileHoverAfterResize() from onChange(of: vm.activePanel), so it covers every panel change, including a panel being turned off in Settings.

Added 1 commit

  • Keep the mirror active when its camera disconnects91b9278

Added 2 commits

  • Merge origin/main into feat/compact-onlybc81956
  • Move the panel host out of ContentView3a75b10