fix: overflow, add window in fullscreen mode to avoid jittering when opening up select...

add: launchTheme.css for consistent styling accross the whole components spectrum related to LaunchWindow
add: correct styles accross the popovers! with unstyled parameters in Popover.tsx because it was needed to make sure its the same
This commit is contained in:
Alan Trebugeais committed 2026-05-06 23:12:03 +02:00
1 parent d4d5b47661
commit 6df02e847d
18 files changed
+226 -323

No files matched your search

+20 -115
View File
@@ -26,23 +26,11 @@ let countdownWindow: BrowserWindow | null = null;
let updateToastWindow: BrowserWindow | null = null;
const HUD_OVERLAY_SETTINGS_FILE = path.join(USER_DATA_PATH, "hud-overlay-settings.json");
const HUD_BOTTOM_CLEARANCE_CM = 3.5;
const DIP_PER_INCH = 96;
const CM_PER_INCH = 2.54;
const HUD_EDGE_MARGIN_DIP = 16;
const HUD_SHADOW_BLEED_DIP = 36;
const HUD_MIN_WINDOW_WIDTH = 560;
const HUD_COMPACT_HEIGHT = 96;
const HUD_MIN_EXPANDED_HEIGHT = 520 + HUD_SHADOW_BLEED_DIP;
const UPDATE_TOAST_WIDTH = 456;
const UPDATE_TOAST_HEIGHT = 252;
const UPDATE_TOAST_GAP_DIP = 18;
let hudOverlayExpanded = false;
let hudOverlayCompactWidth = HUD_MIN_WINDOW_WIDTH;
let hudOverlayCompactHeight = HUD_COMPACT_HEIGHT;
let hudOverlayExpandedHeight = HUD_MIN_EXPANDED_HEIGHT;
function getEditorWindowQuery(): Record<string, string> {
const query: Record<string, string> = {
windowType: "editor",
@@ -182,61 +170,21 @@ function getHudOverlayDisplay() {
return getScreen().getPrimaryDisplay();
}
function getHudOverlayBounds(expanded: boolean) {
const { bounds, workArea } = getHudOverlayDisplay();
const maxWindowWidth = Math.max(HUD_MIN_WINDOW_WIDTH, workArea.width - HUD_EDGE_MARGIN_DIP * 2);
const windowWidth = Math.min(
maxWindowWidth,
Math.max(HUD_MIN_WINDOW_WIDTH, Math.round(hudOverlayCompactWidth)),
);
const maxWindowHeight = Math.max(HUD_COMPACT_HEIGHT, workArea.height - HUD_EDGE_MARGIN_DIP * 2);
const desiredHeight = expanded
? Math.max(HUD_MIN_EXPANDED_HEIGHT, Math.round(hudOverlayExpandedHeight))
: Math.max(HUD_COMPACT_HEIGHT, Math.round(hudOverlayCompactHeight));
const windowHeight = Math.min(maxWindowHeight, desiredHeight);
const bottomClearanceDip = Math.round((HUD_BOTTOM_CLEARANCE_CM / CM_PER_INCH) * DIP_PER_INCH);
const screenBottom = bounds.y + bounds.height;
const workAreaBottom = workArea.y + workArea.height;
const preferredBottom = screenBottom - bottomClearanceDip;
const maximumSafeBottom = workAreaBottom - HUD_EDGE_MARGIN_DIP;
const windowBottom = Math.min(preferredBottom, maximumSafeBottom);
const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2);
const y = Math.max(workArea.y + HUD_EDGE_MARGIN_DIP, Math.floor(windowBottom - windowHeight));
function getHudOverlayBounds() {
const { workArea } = getHudOverlayDisplay();
return {
x,
y,
width: windowWidth,
height: windowHeight,
x: workArea.x,
y: workArea.y,
width: workArea.width,
height: workArea.height,
};
}
function applyHudOverlayBounds(expanded: boolean) {
function applyHudOverlayBounds() {
if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) {
return;
}
hudOverlayExpanded = expanded;
const computed = getHudOverlayBounds(expanded);
if (hudUserPosition) {
// Resize in-place at the user's dragged position, clamped so the
// window stays fully within the current display's work area.
const { workArea } = getHudOverlayDisplay();
const x = Math.max(
workArea.x,
Math.min(hudUserPosition.x, workArea.x + workArea.width - computed.width),
);
const y = Math.max(
workArea.y,
Math.min(hudUserPosition.y, workArea.y + workArea.height - computed.height),
);
hudOverlayWindow.setBounds({ x, y, width: computed.width, height: computed.height }, false);
} else {
hudOverlayWindow.setBounds(computed, false);
}
hudOverlayWindow.setBounds(getHudOverlayBounds(), false);
positionUpdateToastWindow();
if (!hudOverlayWindow.isVisible()) {
@@ -299,9 +247,7 @@ ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => {
}
});
// When the user drags the HUD, remember their chosen position so that
// subsequent size changes (e.g. idle → recording UI swap) resize in-place
// instead of snapping back to the default centered location.
// Keep compatibility with existing drag IPC/state.
let hudUserPosition: { x: number; y: number } | null = null;
let hudDragOffset: { x: number; y: number } | null = null;
let hudDragLastCursor: { x: number; y: number } | null = null;
@@ -315,7 +261,7 @@ ipcMain.on("hud-overlay-drag", (_event, phase: string, screenX: number, screenY:
// the HUD appears "stuck". The renderer marks the drag handle as
// -webkit-app-region: drag on Linux, letting the OS move the window for us.
// The resulting position is captured by the win.on("moved", ...) listener
// below so `hudUserPosition` stays in sync for in-place resize.
// below so `hudUserPosition` stays in sync.
if (process.platform === "linux") {
return;
}
@@ -365,52 +311,19 @@ ipcMain.on("hud-overlay-hide", () => {
});
ipcMain.on("set-hud-overlay-expanded", (_event, expanded: boolean) => {
applyHudOverlayBounds(Boolean(expanded));
// Compatibility no-op for launcher HUD: window bounds are now fixed to workArea.
void expanded;
});
ipcMain.on("set-hud-overlay-compact-width", (_event, width: number) => {
if (!Number.isFinite(width)) {
return;
}
const maxWindowWidth = Math.max(
HUD_MIN_WINDOW_WIDTH,
getHudOverlayDisplay().workArea.width - HUD_EDGE_MARGIN_DIP * 2,
);
const nextWidth = Math.min(maxWindowWidth, Math.max(HUD_MIN_WINDOW_WIDTH, Math.round(width)));
if (nextWidth === hudOverlayCompactWidth) {
return;
}
hudOverlayCompactWidth = nextWidth;
applyHudOverlayBounds(hudOverlayExpanded);
// Compatibility no-op for launcher HUD: window bounds are now fixed to workArea.
void width;
});
ipcMain.on("set-hud-overlay-measured-height", (_event, height: number, expanded: boolean) => {
if (!Number.isFinite(height)) {
return;
}
const maxWindowHeight = Math.max(
HUD_COMPACT_HEIGHT,
getHudOverlayDisplay().workArea.height - HUD_EDGE_MARGIN_DIP * 2,
);
const nextHeight = Math.min(maxWindowHeight, Math.max(HUD_COMPACT_HEIGHT, Math.round(height)));
if (expanded) {
if (nextHeight === hudOverlayExpandedHeight) {
return;
}
hudOverlayExpandedHeight = Math.max(HUD_MIN_EXPANDED_HEIGHT, nextHeight);
} else {
if (nextHeight === hudOverlayCompactHeight) {
return;
}
hudOverlayCompactHeight = nextHeight;
}
applyHudOverlayBounds(hudOverlayExpanded);
// Compatibility no-op for launcher HUD: window bounds are now fixed to workArea.
void height;
void expanded;
});
ipcMain.handle("get-hud-overlay-capture-protection", () => {
@@ -450,17 +363,11 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean)
export function createHudOverlayWindow(): BrowserWindow {
loadHudOverlayCaptureProtectionSetting();
const initialBounds = getHudOverlayBounds(false);
const initialBounds = getHudOverlayBounds();
const win = new BrowserWindow({
width: initialBounds.width,
height: initialBounds.height,
minWidth: HUD_MIN_WINDOW_WIDTH,
minHeight: HUD_COMPACT_HEIGHT,
maxHeight: Math.max(
HUD_COMPACT_HEIGHT,
getHudOverlayDisplay().workArea.height - HUD_EDGE_MARGIN_DIP * 2,
),
x: initialBounds.x,
y: initialBounds.y,
frame: false,
@@ -538,9 +445,7 @@ export function createHudOverlayWindow(): BrowserWindow {
hudOverlayWindow = win;
// On Linux the HUD is dragged by the OS via -webkit-app-region (Wayland
// forbids client-side positioning). Mirror the resulting bounds into
// hudUserPosition so subsequent expand/collapse resizes stay in place
// instead of snapping back to the default centered spot.
// forbids client-side positioning). Mirror moved bounds into drag state.
if (process.platform === "linux") {
win.on("moved", () => {
if (win.isDestroyed()) return;
@@ -569,7 +474,7 @@ export function createHudOverlayWindow(): BrowserWindow {
hudUserPosition = null;
}
}
applyHudOverlayBounds(hudOverlayExpanded);
applyHudOverlayBounds();
};
screen.on("display-removed", handleDisplayRemoved);
screen.on("display-metrics-changed", handleDisplayMetricsChanged);
+6 -1
View File
@@ -19,6 +19,7 @@ export default function App() {
const type = params.get("windowType") || "";
const isMacOS = /mac/i.test(navigator.platform);
setWindowType(type);
document.documentElement.dataset.windowType = type;
if (
type === "hud-overlay" ||
@@ -31,7 +32,11 @@ export default function App() {
document.getElementById("root")?.style.setProperty("background", "transparent");
}
if (type === "hud-overlay" || type === "update-toast") {
if (type === "hud-overlay") {
document.documentElement.classList.add("hud-overlay-window");
document.body.classList.add("hud-overlay-window");
document.getElementById("root")?.classList.add("hud-overlay-window");
} else if (type === "update-toast") {
document.documentElement.style.overflow = "visible";
document.body.style.overflow = "visible";
document.getElementById("root")?.style.setProperty("overflow", "visible");
+3 -1
View File
@@ -21,6 +21,7 @@ import { SUPPORTED_LOCALES } from "@/i18n/config";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useScopedT } from "../../contexts/I18nContext";
import { AudioLevelMeter } from "../ui/audio-level-meter";
import "./launchTheme.css";
import { SourceSelector } from "./SourceSelector";
import styles from "./LaunchWindow.module.css";
@@ -123,7 +124,8 @@ function HudPopover({
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent
className={`${styles.menuCard} ${styles.electronNoDrag}`}
className={`launch-theme ${styles.menuCard} ${styles.electronNoDrag}`}
unstyled
side="bottom"
align={align}
sideOffset={8}
+14 -10
View File
@@ -104,15 +104,18 @@
width: 300px;
max-height: 400px;
overflow-y: auto;
background: rgba(22, 22, 30, 0.96);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 14px;
background: var(--launch-panel, rgba(34, 40, 56, 0.62));
border: 1px solid var(--launch-border, rgba(255, 255, 255, 0.07));
border-radius: 16px;
backdrop-filter: blur(18px) saturate(140%);
-webkit-backdrop-filter: blur(18px) saturate(140%);
padding: 8px;
margin-top: auto;
margin-bottom: 8px;
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.22),
0 2px 10px rgba(0, 0, 0, 0.1);
0 16px 40px rgba(0, 0, 0, 0.28),
0 2px 12px rgba(0, 0, 0, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
pointer-events: auto;
animation: menuCardIn 0.18s ease;
}
@@ -144,7 +147,7 @@
.ddLabel {
font-size: 9px;
font-weight: 600;
color: #6b6b78;
color: var(--launch-label, #8f99b1);
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 6px 10px 4px;
@@ -160,19 +163,20 @@
border: none;
background: transparent;
font-size: 12px;
color: #6b6b78;
color: var(--launch-text-muted, #aab3c7);
cursor: pointer;
transition: all 0.12s ease;
text-align: left;
}
.ddItem:hover {
background: rgba(255, 255, 255, 0.06);
color: #eeeef2;
background: var(--launch-hover, rgba(255, 255, 255, 0.1));
color: var(--launch-text, #f3f6ff);
}
.ddItemSelected {
color: #3d8bff;
background: var(--launch-selected, rgba(92, 153, 255, 0.22));
color: var(--launch-accent-soft, #dbeafe);
}
.finalizingState {
+62 -173
View File
@@ -12,8 +12,6 @@ import {
ArrowClockwise as RefreshCw,
} from "@phosphor-icons/react";
import { AnimatePresence, motion } from "motion/react";
import type { ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { RxDragHandleDots2 } from "react-icons/rx";
import { useScopedT } from "../../contexts/I18nContext";
import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices";
@@ -42,6 +40,7 @@ import {
import { Separator } from "@/components/ui/separator";
import { Button } from "../ui/button";
import { RecordingControls } from "./RecordingControls";
import { useCallback, useEffect, useRef, useState } from "react";
interface DesktopSource {
id: string;
@@ -106,6 +105,7 @@ export function LaunchWindow() {
const [showFloatingWebcamPreview, setShowFloatingWebcamPreview] = useState(true);
const [webcamPreviewOffset, setWebcamPreviewOffset] = useState(DEFAULT_WEBCAM_PREVIEW_OFFSET);
const [recordingHudOffset, setRecordingHudOffset] = useState(DEFAULT_RECORDING_HUD_OFFSET);
const [isHudDragging, setIsHudDragging] = useState(false);
const [hudOverlayMousePassthroughSupported, setHudOverlayMousePassthroughSupported] = useState<
boolean | null
>(null);
@@ -113,6 +113,8 @@ export function LaunchWindow() {
const [appVersion, setAppVersion] = useState<string | null>(null);
const hudContentRef = useRef<HTMLDivElement>(null);
const hudBarRef = useRef<HTMLDivElement>(null);
const hudBarTransformRef = useRef<HTMLDivElement | null>(null);
const recordingHudOffsetRef = useRef(DEFAULT_RECORDING_HUD_OFFSET);
const webcamPreviewRef = useRef<HTMLVideoElement | null>(null);
const recordingWebcamPreviewRef = useRef<HTMLVideoElement | null>(null);
const recordingWebcamPreviewContainerRef = useRef<HTMLDivElement | null>(null);
@@ -132,7 +134,6 @@ export function LaunchWindow() {
const hudDragStartRef = useRef<
| {
pointerId: number;
mode: "webcam-preview";
startX: number;
startY: number;
originX: number;
@@ -142,18 +143,13 @@ export function LaunchWindow() {
hudWidth: number;
hudHeight: number;
}
| {
pointerId: number;
mode: "overlay";
}
| null
>(null);
const isHudDraggingRef = useRef(false);
const isWebcamPreviewDraggingRef = useRef(false);
const hudDragMoveRafRef = useRef<number | null>(null);
const hudDragPendingScreenPositionRef = useRef<{ x: number; y: number } | null>(null);
const hudDragPendingPointerRef = useRef<{ clientX: number; clientY: number } | null>(null);
const [isAtTop, setIsAtTop] = useState(false);
const anyPopoverOpen =
sourcePopoverOpen ||
micPopoverOpen ||
@@ -169,18 +165,6 @@ export function LaunchWindow() {
setMorePopoverOpen(false);
}, []);
useEffect(() => {
const checkPosition = () => {
const screenHeight = window.screen.availHeight;
const windowY = window.screenY;
setIsAtTop(windowY < screenHeight / 2);
};
checkPosition();
if (anyPopoverOpen || projectBrowserOpen) {
checkPosition();
}
}, [anyPopoverOpen, projectBrowserOpen]);
const showWebcamControls = webcamEnabled && !recording;
const showRecordingWebcamPreview =
webcamEnabled &&
@@ -219,7 +203,6 @@ export function LaunchWindow() {
useEffect(() => {
if (!webcamEnabled) {
setWebcamPreviewOffset(DEFAULT_WEBCAM_PREVIEW_OFFSET);
setRecordingHudOffset(DEFAULT_RECORDING_HUD_OFFSET);
webcamPreviewDragStartRef.current = null;
isWebcamPreviewDraggingRef.current = false;
setShowFloatingWebcamPreview(true);
@@ -227,10 +210,11 @@ export function LaunchWindow() {
}, [webcamEnabled]);
useEffect(() => {
if (!showRecordingWebcamPreview) {
setRecordingHudOffset(DEFAULT_RECORDING_HUD_OFFSET);
recordingHudOffsetRef.current = recordingHudOffset;
if (!isHudDraggingRef.current && hudBarTransformRef.current) {
hudBarTransformRef.current.style.transform = `translate3d(${recordingHudOffset.x}px, ${recordingHudOffset.y}px, 0)`;
}
}, [showRecordingWebcamPreview]);
}, [recordingHudOffset]);
const handleWebcamPreviewPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) {
@@ -318,31 +302,23 @@ export function LaunchWindow() {
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
isHudDraggingRef.current = true;
setIsHudDragging(true);
window.electronAPI?.hudOverlaySetIgnoreMouse?.(false);
if (showRecordingWebcamPreview && hudBarRef.current) {
const hudRect = hudBarRef.current.getBoundingClientRect();
hudDragStartRef.current = {
pointerId: event.pointerId,
mode: "webcam-preview",
startX: event.clientX,
startY: event.clientY,
originX: recordingHudOffset.x,
originY: recordingHudOffset.y,
initialLeft: hudRect.left,
initialTop: hudRect.top,
hudWidth: hudRect.width,
hudHeight: hudRect.height,
};
if (!hudBarRef.current) {
return;
}
const hudRect = hudBarRef.current.getBoundingClientRect();
hudDragStartRef.current = {
pointerId: event.pointerId,
mode: "overlay",
startX: event.clientX,
startY: event.clientY,
originX: recordingHudOffsetRef.current.x,
originY: recordingHudOffsetRef.current.y,
initialLeft: hudRect.left,
initialTop: hudRect.top,
hudWidth: hudRect.width,
hudHeight: hudRect.height,
};
hudDragPendingScreenPositionRef.current = null;
window.electronAPI?.hudOverlayDrag?.("start", event.screenX, event.screenY);
};
const handleHudBarPointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
@@ -351,42 +327,42 @@ export function LaunchWindow() {
return;
}
if (dragState.mode === "webcam-preview") {
const deltaX = event.clientX - dragState.startX;
const deltaY = event.clientY - dragState.startY;
const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0);
const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0);
const unclampedLeft = dragState.initialLeft + deltaX;
const unclampedTop = dragState.initialTop + deltaY;
const clampedLeft = Math.min(
Math.max(0, unclampedLeft),
Math.max(0, viewportWidth - dragState.hudWidth),
);
const clampedTop = Math.min(
Math.max(0, unclampedTop),
Math.max(0, viewportHeight - dragState.hudHeight),
);
setRecordingHudOffset({
x: dragState.originX + (clampedLeft - dragState.initialLeft),
y: dragState.originY + (clampedTop - dragState.initialTop),
});
return;
}
hudDragPendingScreenPositionRef.current = { x: event.screenX, y: event.screenY };
hudDragPendingPointerRef.current = { clientX: event.clientX, clientY: event.clientY };
if (hudDragMoveRafRef.current !== null) {
return;
}
hudDragMoveRafRef.current = requestAnimationFrame(() => {
hudDragMoveRafRef.current = null;
const position = hudDragPendingScreenPositionRef.current;
if (!position) {
const latestDragState = hudDragStartRef.current;
const pointer = hudDragPendingPointerRef.current;
if (!latestDragState || !pointer) {
return;
}
window.electronAPI?.hudOverlayDrag?.("move", position.x, position.y);
const deltaX = pointer.clientX - latestDragState.startX;
const deltaY = pointer.clientY - latestDragState.startY;
const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0);
const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0);
const unclampedLeft = latestDragState.initialLeft + deltaX;
const unclampedTop = latestDragState.initialTop + deltaY;
const clampedLeft = Math.min(
Math.max(0, unclampedLeft),
Math.max(0, viewportWidth - latestDragState.hudWidth),
);
const clampedTop = Math.min(
Math.max(0, unclampedTop),
Math.max(0, viewportHeight - latestDragState.hudHeight),
);
const nextOffset = {
x: latestDragState.originX + (clampedLeft - latestDragState.initialLeft),
y: latestDragState.originY + (clampedTop - latestDragState.initialTop),
};
recordingHudOffsetRef.current = nextOffset;
if (hudBarTransformRef.current) {
hudBarTransformRef.current.style.transform = `translate3d(${nextOffset.x}px, ${nextOffset.y}px, 0)`;
}
});
};
@@ -396,22 +372,17 @@ export function LaunchWindow() {
return;
}
if (dragState.mode === "overlay") {
if (hudDragMoveRafRef.current !== null) {
cancelAnimationFrame(hudDragMoveRafRef.current);
hudDragMoveRafRef.current = null;
}
const position = hudDragPendingScreenPositionRef.current;
if (position) {
window.electronAPI?.hudOverlayDrag?.("move", position.x, position.y);
}
hudDragPendingScreenPositionRef.current = null;
window.electronAPI?.hudOverlayDrag?.("end", 0, 0);
if (hudDragMoveRafRef.current !== null) {
cancelAnimationFrame(hudDragMoveRafRef.current);
hudDragMoveRafRef.current = null;
}
hudDragPendingPointerRef.current = null;
hudDragStartRef.current = null;
const wasDragging = isHudDraggingRef.current;
isHudDraggingRef.current = false;
setIsHudDragging(false);
setRecordingHudOffset({ ...recordingHudOffsetRef.current });
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
@@ -460,7 +431,8 @@ export function LaunchWindow() {
cancelAnimationFrame(hudDragMoveRafRef.current);
}
hudDragMoveRafRef.current = null;
hudDragPendingScreenPositionRef.current = null;
hudDragPendingPointerRef.current = null;
hudDragStartRef.current = null;
};
}, []);
@@ -692,86 +664,6 @@ export function LaunchWindow() {
};
}, []);
useEffect(() => {
const expanded =
anyPopoverOpen || projectBrowserOpen || showRecordingWebcamPreview;
window.electronAPI.setHudOverlayExpanded(expanded);
return () => {
window.electronAPI.setHudOverlayExpanded(false);
};
}, [anyPopoverOpen, projectBrowserOpen, showRecordingWebcamPreview]);
const reportHudSize = useCallback(() => {
const hudContent = hudContentRef.current;
const hudBar = hudBarRef.current;
if (!hudContent || !hudBar) {
return;
}
if (showRecordingWebcamPreview) {
const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0);
const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0);
window.electronAPI.setHudOverlayCompactWidth(Math.ceil(viewportWidth));
window.electronAPI.setHudOverlayMeasuredHeight(Math.ceil(viewportHeight), true);
return;
}
const hudContentRect = hudContent.getBoundingClientRect();
const hudBarRect = hudBar.getBoundingClientRect();
const standardWidth = Math.max(
hudBarRect.width,
hudBar.scrollWidth,
hudContentRect.width,
hudContent.scrollWidth,
);
const standardHeight = Math.max(hudContentRect.height, hudContent.scrollHeight);
window.electronAPI.setHudOverlayCompactWidth(Math.ceil(standardWidth + 24));
window.electronAPI.setHudOverlayMeasuredHeight(
Math.ceil(standardHeight + 24),
anyPopoverOpen || projectBrowserOpen,
);
}, [anyPopoverOpen, projectBrowserOpen, showRecordingWebcamPreview]);
useEffect(() => {
const hudContent = hudContentRef.current;
const hudBar = hudBarRef.current;
const previewContainer = recordingWebcamPreviewContainerRef.current;
if (!hudContent || !hudBar || typeof ResizeObserver === "undefined") {
return;
}
let frameId = 0;
const scheduleHudSizeReport = () => {
if (frameId !== 0) {
cancelAnimationFrame(frameId);
}
frameId = requestAnimationFrame(() => {
frameId = 0;
reportHudSize();
});
};
scheduleHudSizeReport();
const resizeObserver = new ResizeObserver(() => {
scheduleHudSizeReport();
});
resizeObserver.observe(hudContent);
resizeObserver.observe(hudBar);
if (previewContainer) {
resizeObserver.observe(previewContainer);
}
return () => {
resizeObserver.disconnect();
if (frameId !== 0) {
cancelAnimationFrame(frameId);
}
};
}, [reportHudSize]);
const fetchSources = useCallback(async () => {
if (!window.electronAPI) return;
setSourcesLoading(true);
@@ -1174,16 +1066,12 @@ export function LaunchWindow() {
return (
<div
className={`w-full flex justify-center bg-transparent overflow-visible ${
isAtTop ? "items-start pt-5" : "items-end pb-5"
}`}
className="w-full flex justify-center bg-transparent overflow-visible items-end pb-5"
style={{ height: "100vh" }}
>
<div
ref={hudContentRef}
className={`flex items-center overflow-visible ${
isAtTop ? "flex-col" : "flex-col-reverse"
}`}
className="flex items-center overflow-visible flex-col-reverse"
onMouseEnter={() => window.electronAPI?.hudOverlaySetIgnoreMouse?.(false)}
onMouseLeave={() => {
if (
@@ -1199,15 +1087,16 @@ export function LaunchWindow() {
>
<div className="flex flex-col items-center pointer-events-auto">
<div
ref={hudBarTransformRef}
style={{
transform: `translate(${recordingHudOffset.x}px, ${recordingHudOffset.y}px)`,
transform: `translate3d(${recordingHudOffset.x}px, ${recordingHudOffset.y}px, 0)`,
}}
>
<motion.div
ref={hudBarRef}
layout={!showRecordingWebcamPreview}
layout={!showRecordingWebcamPreview && !isHudDragging}
transition={hudStateTransition}
className={`${styles.bar} ${isAtTop ? "mt-2" : "mb-2"}`}
className={`${styles.bar} mb-2`}
>
<div
// On Linux (especially Wayland) the compositor owns window
@@ -1233,7 +1122,7 @@ export function LaunchWindow() {
<AnimatePresence initial={false} mode="wait">
<motion.div
key={hudMode}
layout={!showRecordingWebcamPreview}
layout={!showRecordingWebcamPreview && !isHudDragging}
className={styles.barState}
initial={{
opacity: 0,
+58
View File
@@ -1,3 +1,61 @@
.source-selector-popover {
background: var(--launch-panel, rgba(34, 40, 56, 0.62));
border: 1px solid var(--launch-border, rgba(255, 255, 255, 0.07));
border-radius: 16px;
backdrop-filter: blur(18px) saturate(140%);
-webkit-backdrop-filter: blur(18px) saturate(140%);
box-shadow:
0 16px 40px rgba(0, 0, 0, 0.28),
0 2px 12px rgba(0, 0, 0, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.source-selector-item {
color: var(--launch-text-muted, #aab3c7);
}
.source-selector-item:hover {
background: var(--launch-hover, rgba(255, 255, 255, 0.1));
color: var(--launch-text, #f3f6ff);
}
.source-selector-item-selected {
background: var(--launch-selected, rgba(92, 153, 255, 0.22));
color: var(--launch-accent-soft, #dbeafe);
}
.source-selector-thumb-fallback {
background: #23232d;
}
.source-selector-text {
color: var(--launch-text, #f3f6ff);
}
.source-selector-muted {
color: var(--launch-text-muted, #aab3c7);
}
.source-selector-subtle {
color: #747483;
}
.source-selector-label {
color: var(--launch-label, #8f99b1);
}
.source-selector-dot {
background: var(--launch-accent, #3d8bff);
}
.source-selector-dot-inner {
background: #f8fbff;
}
.source-selector-accent-border {
border-color: var(--launch-accent, #3d8bff);
}
/* Custom Scrollbar for Source Selector */
.source-selector-scroll::-webkit-scrollbar {
width: 5px;
+22 -20
View File
@@ -5,6 +5,7 @@ import { useScopedT } from "@/contexts/I18nContext";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import "./launchTheme.css";
import "./SourceSelector.css";
interface DesktopSource {
@@ -131,12 +132,12 @@ export const SourceSelectorContent = ({
// Memoized source item to prevent unnecessary re-renders
const SourceItem = useCallback(
({ source, isSelected }: { source: DesktopSource; isSelected: boolean }) => (
<button
type="button"
<Button
variant="ghost"
size="sm"
className={cn(
"group flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors w-full text-left",
"hover:bg-accent hover:text-accent-foreground",
isSelected && "bg-accent text-accent-foreground",
"source-selector-item group !h-auto w-full justify-start gap-3 rounded-[11px] px-3 py-2.5 text-left font-medium",
isSelected && "source-selector-item-selected",
)}
onClick={() => onSourceSelect(source)}
>
@@ -146,23 +147,23 @@ export const SourceSelectorContent = ({
<img
src={source.thumbnail}
alt=""
className="w-12 h-8 rounded-md object-cover bg-black/50"
className="w-12 h-8 rounded-[8px] object-cover bg-black/50"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
) : (
<div className="w-12 h-8 rounded-md bg-muted flex items-center justify-center">
<div className="source-selector-thumb-fallback w-12 h-8 rounded-[8px] flex items-center justify-center">
{source.sourceType === "window" ? (
<AppWindow className="w-5 h-5 text-muted-foreground" />
<AppWindow className="w-5 h-5 source-selector-muted" />
) : (
<Monitor className="w-5 h-5 text-muted-foreground" />
<Monitor className="w-5 h-5 source-selector-muted" />
)}
</div>
)}
{isSelected && (
<div className="absolute -top-0.5 -right-0.5 w-4 h-4 bg-primary rounded-full flex items-center justify-center">
<div className="w-1.5 h-1.5 bg-primary-foreground rounded-full" />
<div className="source-selector-dot absolute -top-0.5 -right-0.5 w-4 h-4 rounded-full flex items-center justify-center">
<div className="w-1.5 h-1.5 source-selector-dot-inner rounded-full" />
</div>
)}
</div>
@@ -172,20 +173,20 @@ export const SourceSelectorContent = ({
<div className="flex items-center gap-2 w-full overflow-hidden">
<MarqueeText
text={source.windowTitle || source.name}
className="text-sm font-medium"
className="text-sm font-medium source-selector-text"
speed={40}
/>
{source.appName && source.appName !== source.name && (
<span className="text-xs text-muted-foreground truncate shrink-0">
<span className="text-xs source-selector-muted truncate shrink-0">
{source.appName}
</span>
)}
</div>
<div className="text-xs text-muted-foreground truncate w-full text-left">
<div className="text-xs source-selector-subtle truncate w-full text-left">
{source.sourceType === "screen" ? t("recording.screen") : t("recording.window")}
</div>
</div>
</button>
</Button>
),
[onSourceSelect, t],
);
@@ -196,7 +197,7 @@ export const SourceSelectorContent = ({
return (
<div className="space-y-1">
<div className="px-3 py-2 text-xs font-medium text-muted-foreground">
<div className="px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.08em] source-selector-label">
{t("recording.screens")}
</div>
<div className="space-y-0.5">
@@ -218,7 +219,7 @@ export const SourceSelectorContent = ({
return (
<div className="space-y-1">
<div className="px-3 py-2 text-xs font-medium text-muted-foreground">
<div className="px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.08em] source-selector-label">
{t("recording.windows")}
</div>
<div className="space-y-0.5">
@@ -237,7 +238,7 @@ export const SourceSelectorContent = ({
if (loading) {
return (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-primary" />
<div className="animate-spin rounded-full h-5 w-5 border-b-2 source-selector-accent-border" />
</div>
);
}
@@ -250,7 +251,7 @@ export const SourceSelectorContent = ({
{windowSection}
</>
) : (
<div className="text-center py-8 text-sm text-muted-foreground">
<div className="text-center py-8 text-sm source-selector-muted">
{t("recording.noSourcesFound")}
</div>
)}
@@ -312,7 +313,8 @@ export const SourceSelector = React.memo(function SourceSelector({
)}
</PopoverTrigger>
<PopoverContent
className="w-80 p-0"
className="launch-theme w-80 p-0 source-selector-popover"
unstyled
align="start"
sideOffset={8}
side="bottom"
+13
View File
@@ -0,0 +1,13 @@
.launch-theme {
--launch-surface: rgba(26, 28, 36, 0.92);
--launch-panel: rgba(18, 18, 24, 0.82);
--launch-border: rgba(255, 255, 255, 0.07);
--launch-border-strong: rgba(255, 255, 255, 0.12);
--launch-text: #f3f6ff;
--launch-text-muted: #aab3c7;
--launch-label: #8f99b1;
--launch-hover: rgba(255, 255, 255, 0.1);
--launch-selected: rgba(92, 153, 255, 0.22);
--launch-accent: #3d8bff;
--launch-accent-soft: #dbeafe;
}
+5 -1
View File
@@ -19,10 +19,12 @@ function PopoverContent({
sideOffset = 4,
animated = true,
usePortal = true,
unstyled = false,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content> & {
animated?: boolean;
usePortal?: boolean;
unstyled?: boolean;
}) {
const content = (
<PopoverPrimitive.Content
@@ -30,7 +32,9 @@ function PopoverContent({
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground z-50 w-72 rounded-md border p-4 shadow-md outline-hidden",
!unstyled &&
"bg-popover text-popover-foreground z-50 w-72 rounded-md border p-4 shadow-md outline-hidden",
unstyled && "z-50 outline-hidden",
animated &&
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-popover-content-transform-origin)",
className,
+2
View File
@@ -20,6 +20,8 @@
"closeApp": "Close App",
"screens": "Screens",
"windows": "Windows",
"screen": "Screen",
"window": "Window",
"noSourcesFound": "No sources found",
"microphone": "Microphone",
"turnOffMicrophone": "Turn Off Microphone",
+2
View File
@@ -20,6 +20,8 @@
"closeApp": "Cerrar aplicación",
"screens": "Pantallas",
"windows": "Ventanas",
"screen": "Pantalla",
"window": "Ventana",
"noSourcesFound": "No se encontraron fuentes",
"microphone": "Micrófono",
"turnOffMicrophone": "Desactivar micrófono",
+2
View File
@@ -20,6 +20,8 @@
"closeApp": "Fermer l’application",
"screens": "Écrans",
"windows": "Fenêtres",
"screen": "Écran",
"window": "Fenêtre",
"noSourcesFound": "Aucune source trouvée",
"microphone": "Microphone",
"turnOffMicrophone": "Désactiver le microphone",
+2
View File
@@ -20,6 +20,8 @@
"closeApp": "앱 닫기",
"screens": "화면",
"windows": "창",
"screen": "화면",
"window": "창",
"noSourcesFound": "사용 가능한 소스를 찾을 수 없습니다",
"microphone": "마이크",
"turnOffMicrophone": "마이크 끄기",
+2
View File
@@ -20,6 +20,8 @@
"closeApp": "App sluiten",
"screens": "Schermen",
"windows": "Vensters",
"screen": "Scherm",
"window": "Venster",
"noSourcesFound": "Geen bronnen gevonden",
"microphone": "Microfoon",
"turnOffMicrophone": "Microfoon uitschakelen",
+2
View File
@@ -20,6 +20,8 @@
"closeApp": "Fechar app",
"screens": "Telas",
"windows": "Janelas",
"screen": "Tela",
"window": "Janela",
"noSourcesFound": "Nenhuma fonte encontrada",
"microphone": "Microfone",
"turnOffMicrophone": "Desligar microfone",
+2
View File
@@ -20,6 +20,8 @@
"closeApp": "关闭应用",
"screens": "屏幕",
"windows": "窗口",
"screen": "屏幕",
"window": "窗口",
"noSourcesFound": "未找到源",
"microphone": "麦克风",
"turnOffMicrophone": "关闭麦克风",
+3 -1
View File
@@ -20,6 +20,8 @@
"closeApp": "關閉應用程式",
"screens": "螢幕",
"windows": "視窗",
"screen": "螢幕",
"window": "視窗",
"noSourcesFound": "找不到可錄製的來源",
"microphone": "麥克風",
"turnOffMicrophone": "關閉麥克風",
@@ -74,4 +76,4 @@
"failedToStart": "無法開始錄製:{{error}}",
"failedToStartGeneric": "無法開始錄製"
}
}
}
+6 -1
View File
@@ -22,6 +22,12 @@
margin: 0;
}
html.hud-overlay-window,
body.hud-overlay-window,
#root.hud-overlay-window {
overflow: hidden;
}
:root {
--app-font-sans: "SF Pro Display", "SF Pro Text", Helvetica, sans-serif;
--brand-accent: #2563eb;
@@ -265,4 +271,3 @@