diff --git a/CHANGELOG.md b/CHANGELOG.md index f665b8f..90f1d40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fixed saving from the Figma plugin to a connected WordPress site, which waited 15 seconds and then reported "Failed to sync css with Figma" and "Failed to update project" even though the changes had reached the site. The plugin handed WordPress's reply back under the wrong message type, so the editor never recognised it. Builder synchronization now also waits for the stylesheet and project to finish saving, so Bricks global variables, color swatches and the Style Manager show the values you just saved, including on projects that define variables but no classes. An empty class list no longer creates a blank Oxygen selector. - Stopped disabled shades and tints from appearing as variables in the Oxygen and Bricks builders. The generated CSS already left out a color's shades or tints once you switched them off, but the builder variable dropdowns and Oxygen Classic's Alt-click variable panel still listed them, so picking one pointed at a custom property that did not exist. - Fixed the block editor and Site Editor marking a clean post, template or template part as modified ("Review changes") as soon as it opened. The Core Framework class panel wrote an empty class name to every block before loading its saved classes, which counted as an edit. The panel now starts from the block's saved classes and only writes when a class actually changes. +- Fixed View Only mode in the web app at coreframework.com/app. Opening someone else's public project with View Only still showed the Save buttons and saved on Cmd/Ctrl+S, because the buttons were removed before the editor had drawn them. View Only now hides both Save buttons and ignores the shortcut. +- Fixed saves in the web app at coreframework.com/app sometimes finishing without "Saved successfully" or an error. The editor listened for exactly one message after saving, so any other message arriving first, such as one from a browser extension, used it up and the site's answer was ignored. It now waits for the site's answer itself, and reports an error if none arrives within 30 seconds. - Restored the Core Framework dark and light preview toggle in the Bricks 2.4 toolbar. Bricks 2.4 replaced its single toolbar with several position-specific ones, so the toggle never appeared. It now sits last in the toolbar, comes back when Bricks rebuilds its toolbars, works from the keyboard, and follows an "auto" theme preference. ## [2.0.2] - 2026-08-28 diff --git a/packages/www/src/App.tsx b/packages/www/src/App.tsx index de6d53b..a88cf96 100644 --- a/packages/www/src/App.tsx +++ b/packages/www/src/App.tsx @@ -17,6 +17,7 @@ import { useEffectOnce } from "hooks/useEffectOnce"; import { isFullScreenModeAtom } from "./state/fullscreenAtoms"; import { searchAtom } from "./state/searchAtom"; import { currentPresetAtom, getPresetFromCurrentPresetAtom, setCurrentPresetAtom } from "state"; +import { embedViewOnlyAtom } from "state/embedAtom"; import { figmaAtom } from "state/figmaAtom"; import { onboardingAtom, onboardingNameAtom } from "state/onboardingAtom"; import { lastSavedStateAtom } from "state/saveAtom"; @@ -44,16 +45,14 @@ export function App() { const { handleLoadOfAutocomplete } = useAutoCompleteLoad(); const setFigma = useSetAtom(figmaAtom); + const setEmbedViewOnly = useSetAtom(embedViewOnlyAtom); function onEmbedLoadPreset(event: MessageEvent) { try { const currentFramework = event?.data?.preset; const isViewOnly = event?.data?.isViewOnly; - if (isViewOnly) { - document.getElementById("push1")?.remove(); - document.getElementById("push2")?.remove(); - } + setEmbedViewOnly(isViewOnly === true); if (currentFramework) { const parsed = JSON.parse(currentFramework); diff --git a/packages/www/src/__tests__/embedBridge.test.ts b/packages/www/src/__tests__/embedBridge.test.ts new file mode 100644 index 0000000..a62f55b --- /dev/null +++ b/packages/www/src/__tests__/embedBridge.test.ts @@ -0,0 +1,91 @@ +import { + EMBED_PUSH_RESPONSE, + EMBED_READ_CLIPBOARD, + skipWhenViewOnly, + waitForEmbedMessage, + waitForEmbedPushResponse, +} from "functions/embedBridge"; + +// The hosted editor at coreframework.com/app runs this app in an iframe. After +// a save the editor waits for the host's `cf-push-response`. It used to wait +// with a one-shot listener, so any other message reaching the window first +// used it up and the host's answer was ignored: no "Saved successfully", no +// error. View-only mode removed the save buttons from the DOM before they had +// rendered, so a read-only project could still be saved. + +function post(data: unknown) { + window.dispatchEvent(new MessageEvent("message", { data })); +} + +afterEach(() => { + jest.useRealTimers(); +}); + +describe("waitForEmbedPushResponse", () => { + it("still receives the host's reply when another message arrives first", async () => { + const response = waitForEmbedPushResponse(); + + post({ source: "react-devtools-content-script" }); + post({ type: "cf-embed-load-preset-default" }); + post({ type: EMBED_PUSH_RESPONSE, success: true }); + + await expect(response).resolves.toBe(true); + }); + + it("reports a save the host rejected", async () => { + const response = waitForEmbedPushResponse(); + + post({ type: EMBED_PUSH_RESPONSE, success: false }); + + await expect(response).resolves.toBe(false); + }); + + it("reports a failure when the host never answers", async () => { + jest.useFakeTimers(); + const response = waitForEmbedPushResponse(); + + jest.advanceTimersByTime(30_000); + + await expect(response).resolves.toBe(false); + }); + + it("stops listening once the reply arrives", async () => { + const removeSpy = jest.spyOn(window, "removeEventListener"); + const response = waitForEmbedPushResponse(); + + post({ type: EMBED_PUSH_RESPONSE, success: true }); + await response; + + expect(removeSpy).toHaveBeenCalledWith("message", expect.any(Function)); + removeSpy.mockRestore(); + }); +}); + +describe("waitForEmbedMessage", () => { + it("returns the clipboard text the host sends back", async () => { + const reply = waitForEmbedMessage<{ text: string }>(EMBED_READ_CLIPBOARD); + + post({ type: "something-else", text: "wrong" }); + post({ type: EMBED_READ_CLIPBOARD, text: "--primary: red;" }); + + await expect(reply).resolves.toEqual({ type: EMBED_READ_CLIPBOARD, text: "--primary: red;" }); + }); +}); + +describe("skipWhenViewOnly", () => { + it("does not save a project opened read-only", async () => { + const save = jest.fn(async () => {}); + + await skipWhenViewOnly(() => true, save)(); + + expect(save).not.toHaveBeenCalled(); + }); + + it("saves an editable project", async () => { + const save = jest.fn(async () => {}); + + await skipWhenViewOnly(() => false, save)(); + + expect(save).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/www/src/components/Nav.tsx b/packages/www/src/components/Nav.tsx index 52ea377..0e4f869 100644 --- a/packages/www/src/components/Nav.tsx +++ b/packages/www/src/components/Nav.tsx @@ -11,6 +11,7 @@ import { Save } from "assets/icons/Save.icon"; import { usePush } from "hooks/usePush"; import { NAV_ITEMS, NAV_ITEMS_BOTTOM } from "constants/navItems"; import { APP_VERSION } from "constants/version"; +import { embedViewOnlyAtom } from "../state/embedAtom"; import { figmaAtom } from "../state/figmaAtom"; import { currentPresetAtom, viewAtom } from "state"; import { Tooltip } from "./ui/Tooltip"; @@ -27,6 +28,7 @@ const scrollTop = () => { export const Nav = memo(() => { const [view, setView] = useAtom(viewAtom); const figma = useAtomValue(figmaAtom); + const isViewOnly = useAtomValue(embedViewOnlyAtom); const { handlePush, isLoading } = usePush(); @@ -50,11 +52,13 @@ export const Nav = memo(() => {