Skip to content

PRSM-68: meeting widget icon-stack UI + drag-anywhere#18

Merged
nchopra merged 14 commits into
mainfrom
prsm-68-widget-redesign
May 27, 2026
Merged

PRSM-68: meeting widget icon-stack UI + drag-anywhere#18
nchopra merged 14 commits into
mainfrom
prsm-68-widget-redesign

Conversation

@nchopra

@nchopra nchopra commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Originally PRSM-68 was just "make the expanded widget pill use a vertical button layout". It grew into a broader redesign of the recording widget:

  • New icon-stack UI. Idle: a thin sliver at the screen edge that morphs into Take Notes + Mic + drag handle on hover. Recording: always-expanded form with Open Note + a combined waveform/stop anchor + drag handle (the anchor itself swaps waveform → red Stop on its own hover, no separate stop button).
  • Take Notes: new action that creates a blank standalone note without auto-recording.
  • Drag anywhere with edge snap. Widget can be dragged across the screen and snaps to the right or bottom edge based on the post-drag window center. normalizedPosition is derived from the dropped window bounds (not the cursor), so the widget lands where you released it, not at the cursor position minus the grip offset.
  • Tooltips on every button (shadcn/radix Tooltip), per-button tooltipSide so labels don't clip at the screen edge.
  • backgroundThrottling: false on the widget BrowserWindow — without it, the widget renderer's RAF was throttled by Chromium whenever the main app was unfocused (i.e. whenever the widget was actually visible), and framer-motion height transitions froze. This made the waveform appear stuck.
  • Visibility on any wallpaper. Bar has a white-tinted ring; button hover keeps a fully-opaque black bg with brighter border instead of the original bg-white/10 (which made buttons disappear into light wallpapers).
  • Hit-zone fixes. The expanded bounding box (including gaps between buttons) is a single hit zone so the cursor doesn't fall off the widget while traveling between buttons. The drag handle has hover state + grab cursor + tooltip.

Architecture notes

  • Each pill owns its own 36×36 frame and absolutely-positioned siblings (drag handle, secondary button) around it. No flex-driven layout shifts during hover.
  • The sliver and Mic share a wrapper pinned to the screen-facing edge of the frame, so the bar's edge stays put as it morphs inward into the Mic.
  • Schema: meetingWidget.normalizedYmeetingWidget.{edge, normalizedPosition}. No Drizzle migration; pre-launch policy in CLAUDE.md allows aggressive schema changes.

Test plan

  • Idle hover: thin bar morphs into Take Notes + Mic + drag handle from the screen edge; reverse on unhover.
  • Take Notes button: creates a blank note without starting recording.
  • Mic button: starts a recording.
  • During recording: Open Note + waveform anchor + drag handle are always visible.
  • Hover the waveform anchor: morphs to a red Stop square; clicking it stops cleanly.
  • Waveform animates in time with speech in the widget (parity with the in-app dock).
  • Drag the widget by the handle around the screen → release: lands at the dropped position, snapped to the nearest of the right or bottom edge.
  • Drag position persists across app restart.
  • Tooltips appear on every button (Take Notes, Mic, Open Note, Stop, Drag).
  • Widget is visible on both dark and light wallpapers; hover state lifts the button without making it transparent on light backgrounds.
  • Cursor traveling between buttons (Mic ↔ drag handle ↔ Take Notes) keeps the pill expanded; no mid-traversal collapse.
  • Drag handle shows grab cursor on hover, grabbing while pressed.

Files touched

  • Main process: apps/desktop/src/main/core/window-manager.ts, apps/desktop/src/main/managers/meeting-recording-widget-manager.ts, apps/desktop/src/main/managers/meeting-start-notification-manager.ts, apps/desktop/src/main/preload.ts.
  • Schema/types: apps/desktop/src/db/schema.ts, apps/desktop/src/services/settings-service.ts, apps/desktop/src/types/meeting-widget.ts, apps/desktop/src/types/electron-api.ts.
  • tRPC: apps/desktop/src/trpc/routers/meeting-widget.ts, apps/desktop/src/trpc/routers/settings.ts.
  • Renderer: apps/desktop/src/renderer/recording-widget/{index,idle-pill,recording-pill,icon-button,drag-handle,waveform-stop-anchor}.tsx. icon-button-stack.tsx deleted.
  • Tests: apps/desktop/tests/main/window-manager-bounds.test.ts, apps/desktop/tests/services/meeting-recording-widget-manager.test.ts.

nchopra added 14 commits May 26, 2026 14:05
…M-68)

Swap meetingWidget.normalizedY for { edge, normalizedPosition }. Generalize
window-manager bounds to handle right and bottom edges. Replace
updateMeetingWidgetWindowPosition with a free-drag method that follows the
cursor unconstrained, plus snapMeetingWidgetToEdge that resolves the nearest
edge on drag end and returns the persisted position. IPC drag signatures
gain screenX/screenY/offsetX/offsetY.

Renderer + UI changes follow in subsequent commits.
Replace the expanded pill with a stack of round 36px icon buttons. Idle
state shows Take Notes (new createBlankNote mutation, creates a note without
auto-record), Start Recording (mic, matches the in-app dock icon), and a
drag handle. Recording state shows Stop, waveform-as-anchor, Open Note, drag
handle.

The stack orientation flips with the active edge: vertical column at the
right edge (today), horizontal row at the bottom edge. Drag-and-snap follows
the cursor freely and snaps to the nearest edge on release.

DetectionPill is intentionally unchanged.
Both components were used only by RecordingPill, which was rewritten in the
previous commit to use the new IconButton primitive. Removing the dead
exports keeps widget-buttons.tsx focused on the symbols DetectionPill still
needs.
Two related fixes after the icon-stack rewrite caused the widget to
collapse mid-interaction:

- Conditionally render the drag handle so it's not a phantom hit zone
  when invisible (collapsed state should be tight to the visible sliver,
  not span the would-be drag-handle slot).
- Mark the inner pill+drag-handle wrapper as a hit zone while expanded
  so cursor travel through the 6px gaps doesn't drop the hover state
  and unmount the buttons before clicks land.
Two widget regressions:

- Waveform froze when the main app lost focus (i.e. whenever the widget
  was actually visible). The widget BrowserWindow inherited Chromium's
  default backgroundThrottling, which clamps requestAnimationFrame to
  ~1Hz on backgrounded renderers — framer-motion's height transitions
  couldn't run. Level IPC kept arriving, but the bars couldn't animate.
  Disable backgroundThrottling on the widget window.

- Drag snap landed the widget "before" the cursor by the pointer offset.
  snapMeetingWidgetToEdge derived edge + normalizedPosition from raw
  cursor screenX/Y, so on release the window top-left jumped to the
  cursor — discarding the grip offset that updateMeetingWidgetWindowPositionFree
  was tracking during the drag. Snap now uses the window's post-drag
  bounds (center for edge selection, top-left for normalizedPosition)
  so the widget stays where the user dropped it.
…ded (PRSM-68)

- Swap IconButton's native `title` tooltip for the shadcn/radix Tooltip
  primitive (per-button `tooltipSide` so labels don't clip at screen
  edges). Wrap the widget renderer in `TooltipProvider`.

- New WaveformStopAnchor merges the recording stop button with the
  waveform: bars when idle, swap to a red Stop square on the anchor's
  own hover. Replaces the separate top-of-stack stop button.

- RecordingPill no longer has a collapsed/expanded distinction. While
  recording, the widget rests in expanded form: Open Note + waveform
  anchor + drag handle, all visible. Drag handle visibility is keyed
  off `isRecording || isHovered || dragState`.

- Reduce the anchor's bar count 6 → 4 so the bars fit within the 36px
  button with comfortable padding.
…RSM-68)

Rework the idle widget animation so the sliver bar morphs in place
instead of crossfading + reflowing.

- Each pill now owns a fixed 36×36 frame and renders its own
  absolutely-positioned siblings (drag handle, secondary button)
  around that frame. No more shared inner-flex shifting items
  during hover.

- The sliver and the Mic IconButton share a single anchor pinned
  to the screen-facing edge of the frame. The bar's right edge
  (or bottom edge) never moves — it just grows inward into the
  Mic's bounding box as it fades out, while the Mic fades in
  with its scale origin at that same edge.

- Lift the shared DragHandle into its own module; drop the now
  orphaned IconButtonStack.

- Pull MEETING_WIDGET_EDGE_MARGIN 12 → 6 so the bar sits closer
  to the screen edge at rest.
… (PRSM-68)

Cursor crossing the gap between Mic, drag handle, or Take Notes would
fall off the hit-zone and collapse the pill mid-traversal.

- Add an invisible hit-zone underlay sized to the full expanded
  bounding box (buttons + gaps). Underlay sits behind the buttons,
  so the actual buttons still own clicks; the underlay only catches
  the "still inside the widget" mousemove signal while the cursor is
  in a gap.

- For idle, the underlay's data-hit-zone is only active while hovered,
  so it doesn't extend the initial trigger area. For recording (always
  expanded), the underlay is always a hit-zone.

- Drive-by: move the bottom-edge drag handle to the left of the frame
  so it doesn't overlap with Open Note / Take Notes (which sit to the
  right).
The sliver's dark ring blended into dark wallpapers, making the bar
nearly invisible at rest. Override PILL_SHELL_CLASS's ring-black/60
with ring-white/40 just on the morphing sliver so it stays visible
without affecting the rest of the pill shells.
Resting `bg-black/80` looked faded on light wallpapers, and the prior
`hover:bg-white/10` made the button vanish into the background. Use
fully opaque black + a brighter `border-white/55` on hover so the
button visibly "lifts" on any wallpaper instead of disappearing.
- Remove dead `isTahoeOrLater` branch in `getTrafficLightPosition` —
  earlier change flattened the return to `{x:16, y:16}` but left the
  variable behind, tripping the unused-var warning.
- Show "Dismiss" instead of "Stop Recording" on the WaveformStopAnchor
  tooltip when the meeting is in error state.
- Add unit coverage for `updateMeetingWidgetWindowPositionFree`:
  grip-point alignment plus clamping against both corners of the work
  area, since the snap regression originated in this offset formula
  going untested.
The drag handle had no visual hover feedback, no cursor change, and
no tooltip — easy to miss as an interactive element.

- Match IconButton's hover language (opaque black + white/55 border +
  brighter dots) so the handle lifts on hover instead of staying dim.
- `cursor-grab` on hover, `cursor-grabbing` while pressed.
- Shadcn/radix tooltip "Drag to move", with `tooltipSide` matching
  the pill's other buttons so it doesn't clip at the screen edge.
@nchopra
nchopra requested a review from haritabh-z01 May 27, 2026 21:36
@nchopra
nchopra merged commit 914e141 into main May 27, 2026
6 checks passed
@nchopra
nchopra deleted the prsm-68-widget-redesign branch May 27, 2026 21:39
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