Release v0.1.18 2026-07-19
This commit is contained in:
@@ -1,39 +1,80 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { subscribeDisplaySurface, type DisplaySurface } from "../platform/displayRouting";
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { requestDisplaySurface, subscribeDisplaySurface, type DisplaySurface } from "../platform/displayRouting";
|
||||
import { resolveDisplayLayout } from "../platform/displayLayout";
|
||||
import { subscribeControllerToken } from "../input/controller";
|
||||
import { DEFAULT_CONTROLLER_GLYPHS } from "../input/controllerGlyphs";
|
||||
|
||||
export function DualDisplayFrame({ top, bottom }: { top: ReactNode; bottom: ReactNode }) {
|
||||
const dedicatedSurface = new URLSearchParams(window.location.search).get("display");
|
||||
export function DualDisplayFrame({ top, bottom, contextLabel = "Context" }: { top: ReactNode; bottom: ReactNode; contextLabel?: string }) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const dedicatedSurface = params.get("display");
|
||||
const layout = resolveDisplayLayout({ display: dedicatedSurface, layout: params.get("layout") });
|
||||
const [activeSurface, setActiveSurface] = useState<DisplaySurface>(() => dedicatedSurface === "bottom" ? "bottom" : "top");
|
||||
const activeSurfaceRef = useRef(activeSurface);
|
||||
activeSurfaceRef.current = activeSurface;
|
||||
const showSurface = useCallback((surface: DisplaySurface) => {
|
||||
activeSurfaceRef.current = surface;
|
||||
setActiveSurface(surface);
|
||||
}, []);
|
||||
const toggleSurface = useCallback(() => {
|
||||
requestDisplaySurface(activeSurfaceRef.current === "top" ? "bottom" : "top");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!document.documentElement.classList.contains("native-platform")) return;
|
||||
if (dedicatedSurface === "top" || dedicatedSurface === "bottom") return;
|
||||
const toggle = () => setActiveSurface((surface) => surface === "top" ? "bottom" : "top");
|
||||
const unsubscribeSurface = subscribeDisplaySurface(setActiveSurface);
|
||||
if (layout === "thor-preview") return;
|
||||
requestDisplaySurface("top");
|
||||
const unsubscribeSurface = subscribeDisplaySurface(showSurface);
|
||||
const unsubscribeController = subscribeControllerToken(({ token, repeat }) => {
|
||||
if (token === "Button8" && !repeat) toggle();
|
||||
if (token === "Button8" && !repeat) toggleSurface();
|
||||
});
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Tab" || event.repeat) return;
|
||||
if (event.repeat) return;
|
||||
if (event.key === "Escape" && activeSurfaceRef.current === "bottom") {
|
||||
event.preventDefault();
|
||||
requestDisplaySurface("top");
|
||||
return;
|
||||
}
|
||||
if (event.key !== "Tab") return;
|
||||
event.preventDefault();
|
||||
toggle();
|
||||
toggleSurface();
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
unsubscribeSurface();
|
||||
unsubscribeController();
|
||||
requestDisplaySurface("top");
|
||||
};
|
||||
}, [dedicatedSurface]);
|
||||
}, [dedicatedSurface, layout, showSurface, toggleSurface]);
|
||||
|
||||
if (dedicatedSurface === "top" || dedicatedSurface === "bottom") {
|
||||
return <div className={`dedicated-display-surface dedicated-${dedicatedSurface}`}>{dedicatedSurface === "top" ? top : bottom}</div>;
|
||||
}
|
||||
|
||||
if (layout === "single") {
|
||||
const contextOpen = activeSurface === "bottom";
|
||||
return (
|
||||
<div className={`single-display-frame ${contextOpen ? "context-open" : ""}`}>
|
||||
<div className="single-primary-surface">{top}</div>
|
||||
{contextOpen && (
|
||||
<div className="single-context-layer" role="dialog" aria-modal="true" aria-label={`${contextLabel} interface`}>
|
||||
<button className="single-context-backdrop" onClick={() => requestDisplaySurface("top")} aria-label={`Close ${contextLabel.toLowerCase()} interface`} />
|
||||
<div className="single-context-surface">{bottom}</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="single-context-toggle"
|
||||
onClick={toggleSurface}
|
||||
aria-expanded={contextOpen}
|
||||
aria-label={contextOpen ? "Return to main view" : `Open ${contextLabel.toLowerCase()} interface`}
|
||||
>
|
||||
<b>{contextOpen ? "Return" : contextLabel}</b>
|
||||
<small>{DEFAULT_CONTROLLER_GLYPHS.select} / TAB</small>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`device-frame active-${activeSurface}`}>
|
||||
<div className="screen-label"><span>Main viewport</span><small>960 × 540 CSS · 1920 × 1080 · 120Hz</small></div>
|
||||
@@ -43,7 +84,7 @@ export function DualDisplayFrame({ top, bottom }: { top: ReactNode; bottom: Reac
|
||||
<div className={`surface-slot bottom-slot ${activeSurface === "bottom" ? "is-active" : ""}`}>{bottom}</div>
|
||||
<button
|
||||
className="native-display-switch"
|
||||
onClick={() => setActiveSurface(activeSurfaceRef.current === "top" ? "bottom" : "top")}
|
||||
onClick={toggleSurface}
|
||||
aria-label={activeSurface === "top" ? "Open tactical display" : "Return to main display"}
|
||||
>
|
||||
<b>{activeSurface === "top" ? "Tactical" : "Main"}</b><small>{DEFAULT_CONTROLLER_GLYPHS.select} / TAB</small>
|
||||
|
||||
Reference in New Issue
Block a user