webapp pagescene: fix top blank space; de-topbar shared lib layout

- styles.css: move .glass into @layer components so Tailwind's
  .absolute utility wins the cascade. OnboardingHelper was rendering
  position: relative and pushing the editor down on first paint until
  clicking dismissed it.
- SceneContainer + Stage3DBody: drop hardcoded calc(100vh - 68px) and
  height - 56 topbar offsets; the lib now fills its parent container.
- Webapp adapter: drop the +56 getViewportSize compensation now that
  the lib doesn't subtract a topbar from the supplied height.
- Tauri PageScene: wrap Stage3D in a calc(100vh - 56px) container
  (matching .topbar-spacer) so its shape is preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tanish MankuandClaude Opus 4.7 committed 2026-05-12 14:20:27 -07:00
1 parent 3bb188eaf7
commit fcb0ddec56
5 files changed
+48 -53

No files matched your search

@@ -5,32 +5,23 @@ import { useSession } from "../../lib/session";
import { useSidebar } from "../../components/ui/sidebar";
import { useWebAppPageSceneAdapter } from "./web-adapter";
// Stage3D was designed for the Tauri host: its outermost div is
// `w-screen` × `calc(100vh - 68px)` and SceneContainer subtracts a
// hardcoded 56px from the adapter-supplied viewport height to leave
// room for the host's TopBar. The webapp lives inside a sidebar +
// topbar shell that already takes its slice out of the viewport, so
// this wrapper translates the lib's assumptions into webapp space:
// Stage3D fills its parent box; this wrapper is that box. It clamps
// the editor to the SidebarInset area and feeds the lib its rect:
//
// - `overflow-hidden h-full w-full` clamps the lib's viewport-sized
// chrome to the area SidebarInset gives us, so the inset never
// grows a scrollbar around the editor.
// - `getViewportSize` reads the wrapper's current rect on every call
// and adds the lib's hardcoded 56px topbar compensation, so the
// SceneContainer's `height - 56` math lands on the wrapper rect.
// - `overflow-hidden h-full w-full` sizes Stage3D to the inset and
// prevents the inset from growing a scrollbar around the editor.
// - `getViewportSize` reports the wrapper's current rect. Consumers
// in the lib (getScale, absolute-positioned chrome) read it via
// `useViewportSize`.
// - The lib's `useViewportSize` only re-reads on `window` resize, so
// a ResizeObserver here dispatches a synthetic resize whenever our
// rect changes (sidebar collapse, peer mounts, browser resize) and
// once on mount (the lib's `useState` initializer ran before any
// refs were attached and saw the window-size fallback).
// rect changes (sidebar collapse, browser resize).
// - `transform: translateZ(0)` makes this div a containing block
// for the lib's `position: fixed` overlays so they scope here.
//
// On entry the sidebar auto-collapses on desktop to give the lib's
// 100vw-leaning layout the room it expects.
const LIB_TOP_BAR_COMPENSATION_PX = 56;
export default function PageScene() {
const { sceneToken } = useParams<{ sceneToken?: string }>();
const { user } = useSession();
@@ -78,7 +69,7 @@ export default function PageScene() {
}
return {
width: node.clientWidth,
height: node.clientHeight + LIB_TOP_BAR_COMPENSATION_PX,
height: node.clientHeight,
};
}, []);
+5 -3
View File
@@ -176,9 +176,11 @@
}
}
.glass {
@apply relative border border-white/10 shadow-lg backdrop-blur-xl transition-colors duration-300;
@apply bg-ui-controls/85 !important;
@layer components {
.glass {
@apply relative border border-white/10 shadow-lg backdrop-blur-xl transition-colors duration-300;
@apply bg-ui-controls/85 !important;
}
}
.liquid-glass {
@@ -60,12 +60,17 @@ export const PageScene = ({ sceneToken }: { sceneToken?: string }) => {
}
});
// Sized container for Stage3D. The lib fills its parent; the
// Tauri host's TopBar lives outside this box, so we reserve space
// here. 56px matches the `.topbar-spacer` height in base.css.
return (
<Stage3D
adapter={adapter}
sceneToken={sceneToken}
cacheJsonString={cacheJsonString}
onSceneSerialized={onSceneSerialized}
/>
<div className="w-screen" style={{ height: "calc(100vh - 56px)" }}>
<Stage3D
adapter={adapter}
sceneToken={sceneToken}
cacheJsonString={cacheJsonString}
onSceneSerialized={onSceneSerialized}
/>
</div>
);
};
@@ -108,7 +108,6 @@ export const Stage3DBody = () => {
// window.innerWidth/innerHeight when the host adapter doesn't
// supply getViewportSize, so the layout always has sane values.
const viewport = useViewportSize();
const height = viewport.height - 56;
const getScale = () => {
const h = viewport.height - 56;
@@ -320,17 +319,13 @@ export const Stage3DBody = () => {
}, [editor]);
return (
<div>
<div className="h-full w-full">
<OnboardingHelper />
<div
className="relative flex w-screen"
style={{ height: "calc(100vh - 68px)" }}
>
<div className="relative flex h-full w-full">
<div
id="engine-n-panels-wrapper"
className="flex"
style={{ height }}
className="flex h-full w-full"
>
<div className="relative w-full overflow-hidden bg-transparent">
<SceneContainer>
@@ -1,43 +1,45 @@
import { useCallback } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { usePageSceneStore } from "../../PageSceneStore";
import { useViewportSize } from "../../hooks/useViewportSize";
import { Letterbox } from "./Letterbox";
// 56px is the artcraft host's TopBar; subtracted from the viewport
// height so the scene container fills the area below the bar. Hosts
// without a TopBar can pass a getViewportSize that already accounts
// for their chrome.
const TOP_BAR_PX = 56;
export const SceneContainer = ({ children }: { children: React.ReactNode }) => {
const editorLetterBox = usePageSceneStore((s) => s.editorLetterBox);
const { width, height } = useViewportSize();
const containerWidth = width;
const containerHeight = height - TOP_BAR_PX;
const [size, setSize] = useState({ width: 0, height: 0 });
const nodeRef = useRef<HTMLDivElement | null>(null);
// Sets the DOM node both on mount (truthy) and unmount (null) so the
// engine lifecycle effect can react to the canvas unmounting when the
// tab switches away from 3D. The previous `if (node)` filter swallowed
// the unmount case and leaked the Editor instance.
const callbackRef = useCallback((node: HTMLDivElement | null) => {
nodeRef.current = node;
usePageSceneStore.getState().setSceneContainerEl(node);
if (node) {
setSize({ width: node.clientWidth, height: node.clientHeight });
}
}, []);
useEffect(() => {
const node = nodeRef.current;
if (!node) return undefined;
const observer = new ResizeObserver(() => {
setSize({ width: node.clientWidth, height: node.clientHeight });
});
observer.observe(node);
return () => observer.disconnect();
}, []);
return (
<div
ref={callbackRef}
id="video-scene-container"
className="relative"
style={{
width: containerWidth,
height: containerHeight,
}}
className="relative h-full w-full"
>
{children}
<Letterbox
isShowing={editorLetterBox}
width={containerWidth}
height={containerHeight}
width={size.width}
height={size.height}
/>
</div>
);