From 9ee332e21bda88211e7812dd868627b3b49c3c36 Mon Sep 17 00:00:00 2001 From: Gagan Date: Tue, 14 Jul 2026 01:27:32 +0530 Subject: [PATCH] style(x): tray recording waveform + popup polish - animated sine-wave indicator (braille dot bars, two per cell, 300ms frames) beside the tray icon while a meeting records - popup: hover-anywhere reveals the close button (the drag region was swallowing mouse events), leaner card (48px, 76px window), sits 44px below the menu bar --- apps/x/apps/main/src/meeting-popup.ts | 5 +- apps/x/apps/main/src/tray.ts | 56 +++++++++++++++++++ .../src/components/meeting-detected-popup.tsx | 16 ++---- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/apps/x/apps/main/src/meeting-popup.ts b/apps/x/apps/main/src/meeting-popup.ts index 0b36fe8a..972873fa 100644 --- a/apps/x/apps/main/src/meeting-popup.ts +++ b/apps/x/apps/main/src/meeting-popup.ts @@ -15,7 +15,7 @@ import type { DetectedMeeting } from "@x/core/dist/meetings/detector.js"; // Lean bar + margins for the overhanging × and the CSS drop shadow. const POPUP_WIDTH = 400; -const POPUP_HEIGHT = 84; +const POPUP_HEIGHT = 76; const AUTO_DISMISS_MS = 45_000; // Display names, Granola-style ("Chrome", not "Google Chrome"). @@ -98,7 +98,8 @@ export function showMeetingPopup(meeting: DetectedMeeting): void { width: POPUP_WIDTH, height: POPUP_HEIGHT, x: workArea.x + 24, - y: workArea.y + 24, + // Sit a bit clear of the menu bar rather than hugging it. + y: workArea.y + 44, // NSPanel (macOS): non-activating, and — unlike a regular window with // visibleOnFullScreen — can float over fullscreen Spaces without // turning Rowboat into an "agent" app that loses its Dock icon. diff --git a/apps/x/apps/main/src/tray.ts b/apps/x/apps/main/src/tray.ts index 44a65791..2d55081d 100644 --- a/apps/x/apps/main/src/tray.ts +++ b/apps/x/apps/main/src/tray.ts @@ -88,6 +88,62 @@ export function setTrayRecordingState(isRecording: boolean): void { if (recording === isRecording) return; recording = isRecording; rebuildMenu(); + if (isRecording) startWaveAnimation(); + else stopWaveAnimation(); +} + +// --- Recording indicator: animated mini-waveform beside the tray icon --- +// macOS renders tray titles to the right of the icon. Braille cells give +// 1-dot-wide bars (two bars per character, four height steps each) — a slim +// waveform, an unmissable "Rowboat is capturing this meeting" signal. + +const WAVE_FRAME_MS = 300; +const WAVE_BAR_COUNT = 5; +// Dot bits for a bar of height 1–4 (index 0–3), built bottom-up. Two bars +// per braille cell (left column: dots 7,3,2,1 — right column: dots 8,6,5,4) +// keeps the columns tightly packed; the sine wave keeps every bar ≥1 dot so +// no column ever reads as missing. +const WAVE_LEFT_BITS = [0x40, 0x44, 0x46, 0x47]; +const WAVE_RIGHT_BITS = [0x80, 0xa0, 0xb0, 0xb8]; +// Radians per bar / per frame: together they make the crest travel smoothly +// leftward across the five bars. +const WAVE_SPATIAL_STEP = 1.1; +const WAVE_PHASE_STEP = 0.9; + +let waveTimer: NodeJS.Timeout | null = null; +let wavePhase = 0; + +function waveString(phase: number): string { + const levels: number[] = []; + for (let i = 0; i < WAVE_BAR_COUNT; i++) { + const level = Math.round(1.5 + 1.5 * Math.sin(phase + i * WAVE_SPATIAL_STEP)); + levels.push(Math.min(3, Math.max(0, level))); + } + let out = ""; + for (let i = 0; i < levels.length; i += 2) { + const left = WAVE_LEFT_BITS[levels[i]]; + const right = levels[i + 1] !== undefined ? WAVE_RIGHT_BITS[levels[i + 1]] : 0; + out += String.fromCharCode(0x2800 + left + right); + } + return out; +} + +function startWaveAnimation(): void { + if (!tray || process.platform !== "darwin") return; + stopWaveAnimation(); + waveTimer = setInterval(() => { + if (!tray) return; + wavePhase += WAVE_PHASE_STEP; + tray.setTitle(` ${waveString(wavePhase)}`, { fontType: "monospaced" }); + }, WAVE_FRAME_MS); +} + +function stopWaveAnimation(): void { + if (waveTimer) { + clearInterval(waveTimer); + waveTimer = null; + } + if (tray && process.platform === "darwin") tray.setTitle(""); } function rebuildMenu(): void { diff --git a/apps/x/apps/renderer/src/components/meeting-detected-popup.tsx b/apps/x/apps/renderer/src/components/meeting-detected-popup.tsx index 75f555de..c0798823 100644 --- a/apps/x/apps/renderer/src/components/meeting-detected-popup.tsx +++ b/apps/x/apps/renderer/src/components/meeting-detected-popup.tsx @@ -7,9 +7,6 @@ type PopupPayload = { hasCalendarEvent: boolean } -const dragRegion = { WebkitAppRegion: 'drag' } as React.CSSProperties -const noDragRegion = { WebkitAppRegion: 'no-drag' } as React.CSSProperties - // Rowboat sail glyph (black on transparent, same asset as the tray icon) — // rendered on a white chip inside the "Take notes" pill. const SAIL_ICON = @@ -63,23 +60,23 @@ export function MeetingDetectedPopup() { // In the browser preview, reproduce the real popup window's exact size // (448×96) on a desktop-ish backdrop; in Electron the window IS that size. + // No drag region: draggable areas swallow mouse events, which would keep + // the hover-revealed × from ever showing while over the card body. const popup = (
{/* Close — floats over the card's top-left corner, revealed on hover */} {/* Card */} -
+
{payload?.title ?? 'Meeting detected'} @@ -90,8 +87,7 @@ export function MeetingDetectedPopup() {