desktop: keep drag/resize working over embedded iframes + add embed loading spinner

This commit is contained in:
Martin Dvořák 2026-09-25 14:00:56 +00:00
commit bc738541cf
5 changed files with 65 additions and 3 deletions

View file

@ -1,6 +1,6 @@
"use client";
import { type CSSProperties, type PointerEvent as ReactPointerEvent } from "react";
import { useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from "react";
import type { AppDef } from "./apps";
export interface WinState {
@ -49,6 +49,10 @@ export default function WinWindow({
onMove,
onResize,
}: Props) {
// While a drag/resize is in flight we disarm the embedded iframe (see the
// `.interacting` rule in skeuo.css). Otherwise a service window swallows the
// pointer as soon as it passes over the site and the gesture freezes.
const [interacting, setInteracting] = useState(false);
const centered = win.centered && !win.maximized;
const style: CSSProperties = win.maximized
? { left: 0, top: 0, width: "100%", height: "100%", zIndex: win.z }
@ -62,6 +66,7 @@ export default function WinWindow({
if ((e.target as HTMLElement).closest(".win-controls")) return;
e.preventDefault();
setInteracting(true);
let ox = win.x;
let oy = win.y;
if (win.centered) {
@ -79,6 +84,7 @@ export default function WinWindow({
onMove(ox + (ev.clientX - sx), oy + (ev.clientY - sy));
};
const handleUp = () => {
setInteracting(false);
window.removeEventListener("pointermove", handleMove);
window.removeEventListener("pointerup", handleUp);
window.removeEventListener("pointercancel", handleUp);
@ -95,6 +101,7 @@ export default function WinWindow({
if (win.maximized) return;
e.preventDefault();
e.stopPropagation();
setInteracting(true);
const node = e.currentTarget.closest(".win-window") as HTMLElement | null;
const rect = node?.getBoundingClientRect();
@ -137,6 +144,7 @@ export default function WinWindow({
onResize(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
};
const handleUp = () => {
setInteracting(false);
window.removeEventListener("pointermove", handleMove);
window.removeEventListener("pointerup", handleUp);
window.removeEventListener("pointercancel", handleUp);
@ -156,7 +164,8 @@ export default function WinWindow({
(win.minimized ? " minimized" : "") +
(win.maximized ? " maximized" : "") +
(closing ? " closing" : "") +
(minimizing ? " minimizing" : "")
(minimizing ? " minimizing" : "") +
(interacting ? " interacting" : "")
}
style={style}
onPointerDown={onFocus}

View file

@ -818,12 +818,20 @@ function WebsiteApp({
/** Renders a site in an iframe. Only usable for sites that allow framing. */
function EmbeddedSite({ title, url }: { title: string; url: string }) {
const [loaded, setLoaded] = useState(false);
return (
<div className="website-embed">
{!loaded && (
<div className="website-embed-loading" role="status">
<span className="website-embed-spinner" aria-hidden="true" />
<span>Loading {title}…</span>
</div>
)}
<iframe
className="website-embed-frame"
src={url}
title={title}
onLoad={() => setLoaded(true)}
allow="autoplay; fullscreen; encrypted-media; clipboard-read; clipboard-write"
allowFullScreen
/>

View file

@ -686,6 +686,7 @@ a.win-icon {
/* Live service sites rendered inside the window. The site fills the window
body edge-to-edge, so the iframe owns all the scrolling. */
.website-embed {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
@ -698,6 +699,39 @@ a.win-icon {
border: 0;
background: #ffffff;
}
/* While the window is dragged/resized, disown the iframe's pointer so the
gesture isn't swallowed when the cursor passes over the embedded site. */
.win-window.interacting .website-embed-frame {
pointer-events: none;
}
/* Navidrome/Forgejo are full SPAs, so the frame is blank for a moment while
they boot — cover it with a spinner until the iframe reports load. */
.website-embed-loading {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background: #232629;
color: #9fd0f5;
font-size: 0.9rem;
}
.website-embed-spinner {
width: 26px;
height: 26px;
border-radius: 50%;
border: 3px solid rgba(159, 208, 245, 0.25);
border-top-color: #3daee9;
animation: website-embed-spin 0.8s linear infinite;
}
@keyframes website-embed-spin {
to {
transform: rotate(360deg);
}
}
/* ============================================================
MINECRAFT SMP JOIN MENU (opened from the SMP service shortcut)