PRSM-68: meeting widget icon-stack UI + drag-anywhere#18
Merged
Conversation
…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.
haritabh-z01
approved these changes
May 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
normalizedPositionis 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.tooltipSideso labels don't clip at the screen edge.backgroundThrottling: falseon 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.bg-white/10(which made buttons disappear into light wallpapers).Architecture notes
meetingWidget.normalizedY→meetingWidget.{edge, normalizedPosition}. No Drizzle migration; pre-launch policy inCLAUDE.mdallows aggressive schema changes.Test plan
grabcursor on hover,grabbingwhile pressed.Files touched
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.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.apps/desktop/src/trpc/routers/meeting-widget.ts,apps/desktop/src/trpc/routers/settings.ts.apps/desktop/src/renderer/recording-widget/{index,idle-pill,recording-pill,icon-button,drag-handle,waveform-stop-anchor}.tsx.icon-button-stack.tsxdeleted.apps/desktop/tests/main/window-manager-bounds.test.ts,apps/desktop/tests/services/meeting-recording-widget-manager.test.ts.