mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-21 21:31:12 +02:00
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
This commit is contained in:
parent
f2d6bbbff3
commit
9ee332e21b
3 changed files with 65 additions and 12 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<div
|
||||
className={`group ${isPreview ? 'relative' : 'h-screen w-screen relative bg-transparent'}`}
|
||||
style={isPreview ? { width: 400, height: 84 } : dragRegion}
|
||||
style={isPreview ? { width: 400, height: 76 } : undefined}
|
||||
>
|
||||
{/* Close — floats over the card's top-left corner, revealed on hover */}
|
||||
<button
|
||||
onClick={() => act('dismiss')}
|
||||
className="absolute left-0.5 top-0.5 z-10 flex size-6 items-center justify-center rounded-full bg-neutral-800 border border-neutral-600 text-neutral-200 shadow-md hover:bg-neutral-700 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
style={noDragRegion}
|
||||
aria-label="Dismiss"
|
||||
className="absolute left-0.5 top-0.5 z-10 flex size-6 items-center justify-center rounded-full bg-neutral-800 border border-neutral-600 text-neutral-200 shadow-md hover:bg-neutral-700 opacity-0 group-hover:opacity-100 transition-opacity" aria-label="Dismiss"
|
||||
>
|
||||
<X className="size-3.5" strokeWidth={2.5} />
|
||||
</button>
|
||||
|
||||
{/* Card */}
|
||||
<div className="absolute left-3 right-3 top-3 h-14 rounded-2xl bg-[#1d1d1d] shadow-[0_8px_28px_rgba(0,0,0,0.55)] flex items-center gap-3 pl-4 pr-2.5">
|
||||
<div className="absolute left-3 right-3 top-3 h-12 rounded-2xl bg-[#1d1d1d] shadow-[0_8px_28px_rgba(0,0,0,0.55)] flex items-center gap-3 pl-4 pr-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-white leading-tight truncate">
|
||||
{payload?.title ?? 'Meeting detected'}
|
||||
|
|
@ -90,8 +87,7 @@ export function MeetingDetectedPopup() {
|
|||
</div>
|
||||
<button
|
||||
onClick={() => act('take-notes')}
|
||||
className="flex h-9.5 shrink-0 items-center gap-2 rounded-xl bg-neutral-800/90 border border-neutral-700 pl-2 pr-3 hover:bg-neutral-700 transition-colors"
|
||||
style={noDragRegion}
|
||||
className="flex h-8.5 shrink-0 items-center gap-1.5 rounded-lg bg-neutral-800/90 border border-neutral-700 pl-1.5 pr-2.5 hover:bg-neutral-700 transition-colors"
|
||||
>
|
||||
<span className="flex size-6 items-center justify-center">
|
||||
<img src={SAIL_ICON} alt="" className="size-4.5 invert" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue