@@ -351,17 +352,17 @@ function Editor({ repo, templateId }: EditorProps) {
/>
-
+
@@ -376,6 +377,78 @@ function Editor({ repo, templateId }: EditorProps) {
);
}
+/**
+ * Renders a resizable side panel, or nothing while the sheets carry it instead.
+ *
+ * @param props - Visibility, edge, resizer state, and the panel to host.
+ */
+function SidePanel({
+ show,
+ side,
+ width,
+ label,
+ onResize,
+ children,
+}: {
+ show: boolean;
+ side: 'left' | 'right';
+ width: number;
+ label: string;
+ onResize(width: number): void;
+ children: ReactNode;
+}) {
+ if (!show) {
+ return null;
+ }
+
+ const resizer = (
+
+ );
+
+ return (
+
+ );
+}
+
+/**
+ * Tracks whether the viewport is wide enough for the side panels.
+ *
+ * @returns Whether the panels render beside the canvas instead of in sheets.
+ */
+function useSidePanels() {
+ const [sidePanels, setSidePanels] = useState(
+ () => window.matchMedia(panelQuery).matches,
+ );
+
+ useEffect(() => {
+ const media = window.matchMedia(panelQuery);
+ const update = () => setSidePanels(media.matches);
+
+ media.addEventListener('change', update);
+ update();
+ return () => media.removeEventListener('change', update);
+ }, []);
+
+ return sidePanels;
+}
+
/** @returns Saved panel widths clamped to the resizer bounds, or the defaults. */
function restorePanelWidths(): { left: number; right: number } {
const saved = readPanelWidths();
diff --git a/src/components/editor/preview.tsx b/src/components/editor/preview.tsx
index 2973b96..a52b09a 100644
--- a/src/components/editor/preview.tsx
+++ b/src/components/editor/preview.tsx
@@ -31,12 +31,22 @@ function Preview({ scene, label, compact = false }: PreviewProps) {
let stage: Awaited
> | undefined;
let observer: ResizeObserver | undefined;
+ // A hidden host measures zero, so the scale it reports is zero too and the
+ // stage it carries holds no canvas until the host is shown.
+ function scaleFor(hostElement: HTMLDivElement) {
+ return Math.min(
+ hostElement.clientWidth / scene.width,
+ hostElement.clientHeight / scene.height,
+ 1,
+ );
+ }
+
async function mountStage(
hostElement: HTMLDivElement,
stageContainer: HTMLDivElement,
) {
const renderContainer = document.createElement('div');
- stage = await renderScene(scene, renderContainer);
+ stage = await renderScene(scene, renderContainer, scaleFor(hostElement));
if (disposed) {
stage.destroy();
stage = undefined;
@@ -50,17 +60,11 @@ function Preview({ scene, label, compact = false }: PreviewProps) {
return;
}
- // A host that is hidden or on its way out of the tree measures zero,
- // and sizing the stage to nothing fails the draw that follows.
- if (hostElement.clientWidth === 0 || hostElement.clientHeight === 0) {
+ const scale = scaleFor(hostElement);
+ if (scale === 0) {
return;
}
- const scale = Math.min(
- hostElement.clientWidth / scene.width,
- hostElement.clientHeight / scene.height,
- 1,
- );
stage.scale({ x: scale, y: scale });
stage.width(scene.width * scale);
stage.height(scene.height * scale);
diff --git a/src/lib/renderer/stage.ts b/src/lib/renderer/stage.ts
index 32eb2ad..3f82251 100644
--- a/src/lib/renderer/stage.ts
+++ b/src/lib/renderer/stage.ts
@@ -18,19 +18,23 @@ Konva.autoDrawEnabled = false;
*
* @param scene - Renderer-independent scene to draw.
* @param container - Element that hosts the Konva stage.
+ * @param scale - Fraction of the scene size the stage is drawn at.
* @returns The rendered stage, which the caller must destroy when finished.
*/
-async function renderScene(scene: Scene, container: HTMLDivElement) {
+async function renderScene(scene: Scene, container: HTMLDivElement, scale = 1) {
await fontsReady;
const stage = new Konva.Stage({
container,
- width: scene.width,
- height: scene.height,
+ width: scene.width * scale,
+ height: scene.height * scale,
+ scaleX: scale,
+ scaleY: scale,
});
const layer = new Konva.Layer();
- // A preview is drawn below the scene size, so the layer keeps at least a
- // doubled backing store to stop images softening on low-density screens.
+ // The layer is allocated at the size it is displayed at, so its backing
+ // store follows the screen density and keeps at least a doubled store to
+ // stop images softening on low-density screens.
layer.getCanvas().setPixelRatio(Math.max(2, window.devicePixelRatio || 1));
layer.add(
new Konva.Rect({