Skip to content

[C] Task 1: Notch Bar — adaptive shape, synthetic notch, hover affordance#1

Open
sheepxux wants to merge 17 commits into
mainfrom
c/notch-bar-poc
Open

[C] Task 1: Notch Bar — adaptive shape, synthetic notch, hover affordance#1
sheepxux wants to merge 17 commits into
mainfrom
c/notch-bar-poc

Conversation

@sheepxux

@sheepxux sheepxux commented Apr 25, 2026

Copy link
Copy Markdown
Owner

Summary

Task 1 complete: collapsed Notch Bar with full design polish.

Visual

  • Adaptive layout: notched displays use the real notch dimensions; no-notch displays render a synthetic black notch flush with the screen top.
  • Continuous-curvature ("squircle") rounded bottom corners.
  • Notched bar protrudes 7pt below the hardware notch (Dynamic Island feel).
  • Synthetic notch fills the OS menu-bar height exactly (uses frame.maxY - visibleFrame.maxY for accurate measurement).

Hover affordance

  • Width grows by 50pt (capsule) / 30pt (notched) with spring animation.
  • Capsule drops 8pt below the menu bar on hover.
  • Status dot + count fade in on hover (idle = clean silhouette).
  • Pointer cursor changes to .pointingHand.
  • System NSWindow shadow toggles on for hover (Apple-native, no compositor seams).

Adaptivity

  • Re-measures on didChangeScreenParametersNotification — display swap, "Larger Text", scale changes are all auto-handled.
  • Notched ⇄ synthetic-notch flips automatically when window moves to a different display.

Architecture

  • Split into IslandApp (exec, @main + AppDelegate) + IslandAppLib (library, all view code) so SwiftUI Previews work under Xcode 26's ENABLE_DEBUG_DYLIB requirement.
  • All UI consumes TaskStore from IslandCore per the contract.

Test plan

  • Open in Xcode 26+ on macOS 14+
  • Switch scheme to IslandApp → ⌘R
  • Confirm: bar appears at top of screen, vertically centered in menu bar
  • Hover: bar widens, content fades in, shadow appears, cursor changes
  • Leave hover: bar shrinks back, shadow + content disappear
  • Switch scheme to IslandAppLib → open NotchBarView.swift → Canvas Resume
  • Confirm: 5-state previews render correctly for both notched and capsule modes

Axu and others added 17 commits April 25, 2026 00:32
- Theme tokens: Palette, Motion, Typo, NotchMetrics
- NotchBarShape: dual-rect path framing the hardware notch
- StatusDot: static color per BarState (animations come in Task 3)
- NotchBarView: bar composition with status dot + task count
- NotchBarWindow: borderless .statusBar level, multi-Space + full-screen aux
- AppDelegate: spawns the bar after launch, repositions on screen change

Build gating: #Preview blocks wrapped in #if PREVIEWS so swift build still
passes on Command Line Tools-only machines. Xcode auto-defines PREVIEWS via
Package.swift detection of /Applications/Xcode.app.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…Previews

Xcode 26 requires ENABLE_DEBUG_DYLIB=YES for previewing in executable
targets, but SwiftPM does not expose that build setting. The recommended
workaround (per the Preview error message) is to move preview-able code
into a library target.

- IslandApp executable target now contains only the @main entry + AppDelegate
- All Theme/Views/Windows code moves to new IslandAppLib library target
- NotchBarWindow + init + reposition() promoted to public so the exec can
  reach them from AppDelegate
- Files relocated with git mv to preserve history

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: status-bar/notch height varies per Mac, and the bar should
have a rounded "tab" silhouette instead of sharp rectangles.

- NotchMetrics.Layout: per-screen snapshot driving Window framing + Shape
  drawing. Reads NSScreen.safeAreaInsets.top on notched displays and
  NSStatusBar.system.thickness on plain displays.
- NotchBarShape: rounded outer-bottom corners on each side extension
  (cornerRadius 14pt). Inner edges stay sharp; the S-curve transition will
  arrive with the panel in Task 2.
- NotchBarView: dual-mode rendering — NotchBarShape on notched screens,
  Capsule on no-notch (Mac mini, external displays); status dot + count
  laid out appropriately for each.
- NotchBarWindow: re-measures on every reposition() so screen-parameter
  changes (display swap, scale change) flip notched ⇄ capsule + adjust
  height without a relaunch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace hand-drawn quarter-circle arcs with UnevenRoundedRectangle in
.continuous style (Apple's squircle math, same as macOS app icons). The
curvature transition is no longer abrupt at the corner endpoints.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: side vertical edges should read taller, with shorter
curve transitions (Dynamic Island feel).

- Bar now protrudes bottomOverhang (7pt) below the hardware notch on
  notched displays, giving the side a visible straight stretch before
  the curve starts.
- NotchBarShape rewritten as one connected outline with a top-center
  cutout at notchHeight × notchWidth. The bottom corners use the same
  continuous-curvature squircle.
- cornerRadius dropped 14 → 11 so the curve occupies less of the bottom.
- NotchMetrics.Layout adds notchHeight separate from barHeight to drive
  the cutout sizing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hover the bar / capsule → it grows wider + slightly taller with a spring
animation, and the cursor switches to pointingHand. Signals "clickable"
without firing the panel expansion (that's still Task 2).

- NotchMetrics adds hover-boost constants (notched: +30w +4h, capsule:
  +50w +6h) and Layout.hovered() returning the expanded snapshot.
- NotchBarWindow sizes itself to the hover-expanded dimensions so the
  bar can animate inside the window without any NSWindow frame churn.
- NotchBarRootView holds @State isHovering, animates between idle and
  hover layouts via withAnimation(.spring(response: 0.3, damping: 0.75)).
- Hover hit-testing uses contentShape(barHitShape) so the cursor only
  reacts over the actual bar pixels, not transparent window padding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: hover-expanded capsule overflowed the menu-bar zone.

- Idle capsule height = thickness - capsuleVerticalInset (6pt). Sits
  inside the OS status bar with a small visible gap top + bottom.
- Hover capsule height = thickness (full status bar). No overflow.
- Capsule alignment in window switched to .center so growth is symmetric
  (top + bottom expand toward the status-bar edges). Notched bar still
  anchors .top so it grows downward into the menu-bar zone (Dynamic
  Island feel preserved).
- Removed fallbackTopMargin constant; topMargin is now always 0 because
  the OS status bar zone is centered around the capsule via SwiftUI
  alignment instead of pixel offsets.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: capsule sat too high in the menu bar zone, and felt small.

Root cause: NSStatusBar.system.thickness (~22pt) under-reports the actual
visible menu-bar height on macOS 26 (~28pt). Sizing the window to thickness
made the window land at the top of the screen but cover only the upper
slice of the menu bar — the centered capsule then drifted upward.

- NotchMetrics.Layout gains menuBarHeight, measured from the screen as
  `frame.maxY - visibleFrame.maxY` (authoritative regardless of OS
  changes). Falls back to thickness when visibleFrame is unavailable.
- Window container height (no-notch) = menuBarHeight, so the window
  covers the status-bar zone exactly and the centered capsule sits at
  its true vertical center.
- Hover capsule height = menuBarHeight (still fills the bar exactly).
- Idle capsule width 140 → 168, height inset trimmed 6 → 4 — both
  cooperate to give the pill more presence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: prefer a MacBook-style notch silhouette over a centered
capsule, with the bar flush against the screen edge (no top gap).

- No-notch backdrop switched from Capsule (#0A0A0A, all-rounded) to
  UnevenRoundedRectangle in .continuous style (#000000, flat top +
  rounded bottom corners) — visually a "fake notch" that hangs from
  the screen edge identical to a real one.
- Window container anchors the bar to TOP for both modes; the no-notch
  container height equals the OS menu-bar height so the bar hangs from
  the screen edge and grows downward into the menu bar zone on hover.
- Hover hit-test shape mirrors the new fake-notch silhouette.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: synthetic notch should fill the status bar height at idle;
hover should only widen it, not change height.

- Removed capsuleVerticalInset; barHeight now == menuBarHeight at idle.
- Layout.hovered() already sets barHeight = menuBarHeight on no-notch,
  so the width-only growth pattern falls out for free.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback (no-notch displays only):
- Idle should show a clean fake-notch silhouette without status dot or count.
- Hover should drop slightly past the menu bar bottom (Dynamic Island feel).

- NotchBarView gains showsContent flag; opacity-fades the dot + count.
  Notched mode keeps showsContent always true (the bar is always visible
  around the hardware notch); synthetic-notch mode toggles on hover.
- NotchMetrics adds hoverHeightBoostCapsule (8pt). Layout.hovered() for
  no-notch now returns barHeight = menuBarHeight + boost.
- Window container sized to hover dimensions so the bar can expand into
  the extra height without changing the NSWindow frame.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Soft black shadow appears under the bar on hover, animating in with the
size growth and fading out on exit. Window container gains shadowPadding
(28pt sides + bottom) so the shadow renders without NSWindow clipping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previous shadow (opacity 0.45, radius 16, y 8) produced a visible hard
edge between the bar bottom and its shadow. Replaced with two stacked
soft shadows — a close tight one (0.18 / radius 6) for definition and
a wide ambient one (0.12 / radius 22) for diffusion. Lower offsets so
the shadow sits around the bar instead of below.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the two-layer shadow with one very soft ambient one (opacity 0.10,
radius 20, y=0). Add .compositingGroup() before the shadow so the bar's
flat edges don't produce a hard shadow band.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The .shadow primitive in SwiftUI was producing a faint horizontal seam
where the bar's flat bottom met its shadow, even with .compositingGroup
and zero offset. The seam was a compositor artifact tied to the way
shadow gets sampled along sharp shape edges.

Switch to a manual halo: render the bar's silhouette behind the bar,
filled black, scaled 1.06×1.20 and blurred radius 18. The halo's
center is fully covered by the actual bar; only the soft blurred edges
peek out as a glow. Because it's a real blurred shape (not a shadow
filter), the falloff is smooth gaussian, no artifacts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the manual halo and SwiftUI .shadow attempts entirely. NSWindow
with hasShadow=true and isOpaque=false renders the system's standard
window shadow against the bar's alpha silhouette — same model used by
Finder, Dock, and Notification Center. Subtle, clean, no compositor
seams.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire SwiftUI hover state back to NSWindow.hasShadow via an onHoverChange
closure. Idle state: hasShadow=false (no shadow, clean silhouette).
Hover: hasShadow=true + invalidateShadow() so the system recomputes
against the bar's alpha mask. Same Apple-native shadow as Finder, only
revealed on hover.

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Package.swift">

<violation number="1" location="Package.swift:9">
P2: Hard-coding `/Applications/Xcode.app` for Xcode detection is brittle and can incorrectly disable `PREVIEWS` on valid Xcode setups.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Package.swift
// Xcode app. When only Command Line Tools are present, gate previews behind
// the PREVIEWS flag so `swift build` still succeeds. Xcode builds get them
// automatically.
let hasXcode = FileManager.default.fileExists(atPath: "/Applications/Xcode.app")

@cubic-dev-ai cubic-dev-ai Bot Apr 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Hard-coding /Applications/Xcode.app for Xcode detection is brittle and can incorrectly disable PREVIEWS on valid Xcode setups.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Package.swift, line 9:

<comment>Hard-coding `/Applications/Xcode.app` for Xcode detection is brittle and can incorrectly disable `PREVIEWS` on valid Xcode setups.</comment>

<file context>
@@ -1,22 +1,42 @@
+// Xcode app. When only Command Line Tools are present, gate previews behind
+// the PREVIEWS flag so `swift build` still succeeds. Xcode builds get them
+// automatically.
+let hasXcode = FileManager.default.fileExists(atPath: "/Applications/Xcode.app")
+let appSettings: [SwiftSetting] = hasXcode ? [.define("PREVIEWS")] : []
+
</file context>
Suggested change
let hasXcode = FileManager.default.fileExists(atPath: "/Applications/Xcode.app")
let hasXcode = ProcessInfo.processInfo.environment["XCODE_VERSION_ACTUAL"] != nil
Fix with Cubic

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