Fix frontend HMR so the page doesn't break upon saving TS files and restructure frontend architecture (#3871)

* Clean up component setup/tear-down side effects

* Clean up more component setup/tear-down side effects

* Remove nonfunctional debouncer

* Clean up even more component setup/tear-down side effects

* Reuse backend state

* Fix HMR for IO Managers and for State Providers

* Rename IO Managers -> Managers and State Providers -> Stores

* Restructure and partially flatten managers/stores

* Code review fixes

* Review fixes
This commit is contained in:
Keavon Chambers
2026-03-19 13:38:41 -07:00
committed by GitHub
parent eb562146ce
commit 124b17f609
54 changed files with 1385 additions and 931 deletions
@@ -17,7 +17,7 @@
</script>
<script lang="ts">
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";
import { onMount, onDestroy, afterUpdate, createEventDispatcher, tick } from "svelte";
import type { MenuDirection } from "@graphite/../wasm/pkg/graphite_wasm";
import { browserVersion } from "@graphite/utility-functions/platform";
@@ -54,9 +54,11 @@
// tell the floating menu content to use it as a min-width so the floating menu is at least the width of the parent element's floating menu spawner.
// This is the opposite concern of the natural width measurement system, which gets the natural width of the floating menu content in order for the
// spawner widget to optionally set its min-size to the floating menu's natural width.
let containerResizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
const containerResizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
resizeObserverCallback(entries);
});
let dialogResizeObserver: ResizeObserver | undefined;
let wasOpen = open;
let measuringOngoing = false;
let measuringOngoingGuard = false;
@@ -79,16 +81,7 @@
// Called only when `open` is changed from outside this component
async function watchOpenChange(isOpen: boolean) {
// Mitigate a Safari rendering bug which clips the floating menu extending beyond a scrollable container.
// The bug is possibly related to <https://bugs.webkit.org/show_bug.cgi?id=160953>, but in our case it happens when `overflow` of a parent is `auto` rather than `hidden`.
if (browserVersion().toLowerCase().includes("safari")) {
const scrollable = self?.closest("[data-scrollable-x], [data-scrollable-y]");
if (scrollable instanceof HTMLElement) {
// The issue exists when the container is set to `overflow: auto` but fine when `overflow: hidden`. So this workaround temporarily sets
// the scrollable container to `overflow: hidden`, thus removing the scrollbars and ability to scroll until the floating menu is closed.
scrollable.style.overflow = isOpen ? "hidden" : "";
}
}
setSafariScrollableOverflow(isOpen);
// Switching from closed to open
if (isOpen && !wasOpen) {
@@ -129,12 +122,22 @@
wasOpen = isOpen;
}
// Mitigate a Safari rendering bug which clips the floating menu extending beyond a scrollable container. The bug is possibly related to
// <https://bugs.webkit.org/show_bug.cgi?id=160953>, but in our case it happens when `overflow` of a parent is `auto` rather than `hidden`.
// The issue exists when the container is set to `overflow: auto` but fine when `overflow: hidden`. So this workaround temporarily sets
// the scrollable container to `overflow: hidden`, thus removing the scrollbars and ability to scroll until the floating menu is closed.
function setSafariScrollableOverflow(hidden: boolean) {
if (!browserVersion().toLowerCase().includes("safari")) return;
const scrollable = self?.closest("[data-scrollable-x], [data-scrollable-y]");
if (scrollable instanceof HTMLElement) scrollable.style.overflow = hidden ? "hidden" : "";
}
onMount(() => {
// Measure the content and round up its width and height to the nearest even integer.
// This solves antialiasing issues when the content isn't cleanly divisible by 2 and gets translated by (-50%, -50%) causing all its content to be blurry.
const floatingMenuContentDiv = floatingMenuContent?.div?.();
if (type === "Dialog" && floatingMenuContentDiv) {
const resizeObserver = new ResizeObserver((entries) => {
dialogResizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const existingWidth = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-x"));
const existingHeight = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-y"));
@@ -153,10 +156,23 @@
floatingMenuContentDiv.style.setProperty("--even-integer-subpixel-expansion-y", `${targetHeight - height}`);
});
});
resizeObserver.observe(floatingMenuContentDiv);
dialogResizeObserver.observe(floatingMenuContentDiv);
}
});
onDestroy(() => {
containerResizeObserver.disconnect();
dialogResizeObserver?.disconnect();
window.removeEventListener("pointermove", pointerMoveHandler);
window.removeEventListener("keydown", keyDownHandler);
window.removeEventListener("pointerdown", pointerDownHandler);
window.removeEventListener("pointerup", pointerUpHandler);
window.removeEventListener("click", clickHandlerCapture, true);
// Revert Safari overflow workaround if the menu was open when destroyed
if (open) setSafariScrollableOverflow(false);
});
afterUpdate(() => {
// Gets the client bounds of the elements and apply relevant styles to them.
// TODO: Use DOM attribute bindings more whilst not causing recursive updates. Turning measuring on and off both causes the component to change,