fix(studio): a style set by its JavaScript name saves under its CSS name

This commit is contained in:
Miguel Angel Simon Sierra
2026-10-02 03:39:03 -07:00
parent 1a4e4dd04a
commit 3cc60107e1
3 changed files with 86 additions and 1 deletions
@@ -0,0 +1,73 @@
// @vitest-environment jsdom
import { expect, it } from "vitest";
import type { DomEditSelection } from "../components/editor/domEditing";
import type { PatchOperation } from "../utils/sourcePatcher";
import { commitDomStyles, type DomStyleCommitContext } from "./domStyleCommit";
const selection = {
id: "target",
selector: "#target",
label: "Target",
tagName: "div",
sourceFile: "index.html",
compositionPath: "index.html",
textFields: [],
inlineStyles: {},
computedStyles: {},
dataAttributes: {},
capabilities: { canSelect: true, canEditStyles: true },
} as unknown as DomEditSelection;
async function savedPatches(styles: Record<string, string>) {
const saves: Array<{ operations: PatchOperation[]; label?: string }> = [];
const context: DomStyleCommitContext = {
activeCompPath: "index.html",
previewIframeRef: { current: null },
showToast: () => undefined,
versions: new Map(),
persistDomEditOperations: (async (
_selection: DomEditSelection,
operations: PatchOperation[],
options?: { label?: string },
) => {
saves.push({ operations, label: options?.label });
}) as unknown as DomStyleCommitContext["persistDomEditOperations"],
};
await commitDomStyles(context, selection, styles);
return saves;
}
const written = (operations: PatchOperation[]) =>
operations.map((op) => [(op as { property: string }).property, (op as { value: string }).value]);
it("writes a style named the JavaScript way under its CSS name", async () => {
const saves = await savedPatches({ backgroundColor: "#ff0000" });
expect(saves.map((save) => written(save.operations))).toEqual([
[["background-color", "#ff0000"]],
]);
});
it("saves a map mixing both forms as one patch and one undo step, all under CSS names", async () => {
const saves = await savedPatches({
borderRadius: "4",
"border-color": "red",
WebkitTextStroke: "1px black",
msTransform: "none",
});
expect(saves).toHaveLength(1);
expect(saves[0]!.label).toBe("Edit layer style");
expect(written(saves[0]!.operations)).toEqual([
["border-radius", "4px"],
["border-color", "red"],
["-webkit-text-stroke", "1px black"],
["-ms-transform", "none"],
]);
});
it("keeps a custom property's name exactly as given", async () => {
const saves = await savedPatches({ "--brand": "#123456", "--brandAccent": "#654321" });
expect(written(saves[0]!.operations)).toEqual([
["--brand", "#123456"],
["--brandAccent", "#654321"],
]);
});
+8 -1
View File
@@ -1,5 +1,6 @@
import type { PatchOperation } from "../utils/sourcePatcher";
import {
cssPropertyName,
isImageBackgroundValue,
isManualGeometryStyleProperty,
normalizeDomEditStyleValue,
@@ -43,8 +44,14 @@ export interface DomStyleCommitContext {
export async function commitDomStyles(
context: DomStyleCommitContext,
selection: DomEditSelection,
styles: Record<string, string>,
stylesInEitherCase: Record<string, string>,
): Promise<DomEditCommitOutcome> {
const styles = Object.fromEntries(
Object.entries(stylesInEitherCase).map(([property, value]) => [
cssPropertyName(property),
value,
]),
);
const entries = Object.entries(styles);
if (entries.length === 0) return domEditCommitDeclined("no-selection");
if (entries.some(([property]) => isManualGeometryStyleProperty(property)))
@@ -92,6 +92,11 @@ export function isImageBackgroundValue(value: string): boolean {
return /^url\(/i.test(value.trim());
}
export function cssPropertyName(property: string): string {
if (property.startsWith("--")) return property;
return property.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).replace(/^ms-/, "-ms-");
}
export function isManualGeometryStyleProperty(property: string): boolean {
return property === "left" || property === "top" || property === "width" || property === "height";
}