refactor(stylesheet): move Semantic CSS to the browser (#3329)

This commit is contained in:
Amruth Pillai
2026-08-16 16:50:27 +02:00
committed by GitHub
parent f848e57436
commit 9509b5bc2e
203 changed files with 11838 additions and 14842 deletions
@@ -17,11 +17,6 @@ import {
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
import { useResumeStore } from "@/features/resume/builder/draft";
import {
lockStylesheetStoreForRestore,
replaceStylesheetStoreAfterRestore,
unlockStylesheetStoreAfterRestore,
} from "@/features/resume/stylesheet/store";
import { useConfirm } from "@/hooks/use-confirm";
import { getResumeErrorMessage } from "@/libs/error-message";
import { formatRelativeTime } from "@/libs/locale";
@@ -53,26 +48,13 @@ export function BuilderVersionHistory({ resumeId }: BuilderVersionHistoryProps)
if (!confirmed) return;
const token = lockStylesheetStoreForRestore(resumeId);
if (!token) return;
try {
const restored = await restoreVersion({ resumeId, versionId });
const applied = replaceStylesheetStoreAfterRestore({
resumeId,
resumeData: restored.resume.data,
initial: restored.stylesheetState,
token,
});
if (!applied) {
unlockStylesheetStoreAfterRestore(token);
return;
}
replaceResumeFromServer(restored.resume as Resume);
queryClient.setQueryData(orpc.resume.getById.queryOptions({ input: { id: resumeId } }).queryKey, restored.resume);
replaceResumeFromServer(restored as Resume);
queryClient.setQueryData(orpc.resume.getById.queryOptions({ input: { id: resumeId } }).queryKey, restored);
void queryClient.invalidateQueries({ queryKey: orpc.resume.listVersions.queryKey({ input: { resumeId } }) });
toast.success(t`Your resume has been restored to the selected version.`);
} catch (error) {
unlockStylesheetStoreAfterRestore(token);
toast.error(getResumeErrorMessage(error));
}
};
@@ -1,5 +1,6 @@
// @vitest-environment happy-dom
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
@@ -17,16 +18,8 @@ const resumeMock = vi.hoisted(() => ({
id: string;
name: string;
slug: string;
data: typeof defaultResumeData;
data: ResumeData;
},
stylesheet: {
resumeId: "r1" as string | undefined,
mode: "semantic" as "legacy" | "semantic",
source: { languageVersion: 1, text: "@version 1;\nname {" },
applied: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
revision: 42,
renderDataVersion: 7,
},
}));
type SectionBaseProps = {
@@ -50,9 +43,6 @@ vi.mock("@/libs/resume/section-title-locale", () => ({
vi.mock("@/features/resume/builder/draft", () => ({
useResume: () => resumeMock.resume,
}));
vi.mock("@/features/resume/stylesheet/store", () => ({
useStylesheetStore: (selector: (state: typeof resumeMock.stylesheet) => unknown) => selector(resumeMock.stylesheet),
}));
const { ExportSectionBuilder } = await import("./export");
@@ -61,15 +51,12 @@ beforeAll(() => {
});
beforeEach(() => {
resumeMock.resume = { id: "r1", name: "My Resume", slug: "my-resume", data: defaultResumeData };
resumeMock.stylesheet = {
resumeId: "r1",
const data = structuredClone(defaultResumeData);
data.metadata.stylesheet = {
mode: "semantic",
source: { languageVersion: 1, text: "@version 1;\nname {" },
applied: { languageVersion: 1, text: "@version 1;\nname { color: #123456; }\n" },
revision: 42,
renderDataVersion: 7,
};
resumeMock.resume = { id: "r1", name: "My Resume", slug: "my-resume", data };
});
afterEach(() => {
@@ -115,7 +102,7 @@ describe("ExportSectionBuilder", () => {
expect(filename).toBe("My Resume.md");
});
it("downloads canonical stylesheet content in JSON without concurrency metadata", async () => {
it("downloads the current stylesheet source in JSON", async () => {
renderExport();
openDialog();
fireEvent.click(screen.getByRole("button", { name: "Download JSON" }));
@@ -127,13 +114,7 @@ describe("ExportSectionBuilder", () => {
expect((blob as Blob).type).toBe("application/json");
expect(filename).toBe("My Resume.json");
const exported = JSON.parse(await (blob as Blob).text());
expect(exported.metadata.stylesheet).toEqual({
mode: "semantic",
source: resumeMock.stylesheet.source,
applied: resumeMock.stylesheet.applied,
});
expect(JSON.stringify(exported)).not.toContain("revision");
expect(JSON.stringify(exported)).not.toContain("renderDataVersion");
expect(exported.metadata.stylesheet).toEqual(resumeMock.resume?.data.metadata.stylesheet);
});
it("calls buildDocx and downloads the resulting blob when DOCX is clicked", async () => {
@@ -155,12 +136,7 @@ describe("ExportSectionBuilder", () => {
await Promise.resolve();
expect(createResumePdfBlob).toHaveBeenCalledTimes(1);
expect(createResumePdfBlob).toHaveBeenCalledWith(defaultResumeData, undefined, undefined, {
stylesheet: {
mode: "semantic",
applied: resumeMock.stylesheet.applied,
},
});
expect(createResumePdfBlob).toHaveBeenCalledWith(resumeMock.resume?.data, undefined, undefined);
expect(downloadWithAnchor).toHaveBeenCalledTimes(1);
expect(downloadWithAnchor.mock.calls[0]?.[1]).toBe("My Resume.pdf");
});
@@ -1,10 +1,9 @@
import type { BuilderLayout } from "./-store/sidebar";
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { useEffect, useRef } from "react";
import { useEffect } from "react";
import { useMediaQuery } from "usehooks-ts";
import { useBuilderResumeUpdateSubscription, useResumeCleanup, useResumeStore } from "@/features/resume/builder/draft";
import { initializeStylesheetStore, useStylesheetStore } from "@/features/resume/stylesheet/store";
import { orpc } from "@/libs/orpc/client";
import { createNoindexFollowMeta } from "@/libs/seo";
import { DesktopBuilderShell } from "./-components/desktop-builder-shell";
@@ -21,9 +20,6 @@ export const Route = createFileRoute("/builder/$resumeId")({
const [layout, resume] = await Promise.all([
getBuilderLayout(),
context.queryClient.ensureQueryData(orpc.resume.getById.queryOptions({ input: { id: params.resumeId } })),
context.queryClient.ensureQueryData(
orpc.resume.stylesheet.getState.queryOptions({ input: { id: params.resumeId } }),
),
]);
return { layout, name: resume.name };
@@ -40,17 +36,11 @@ function RouteComponent() {
const { resumeId } = Route.useParams();
const { data: resume } = useSuspenseQuery(orpc.resume.getById.queryOptions({ input: { id: resumeId } }));
const { data: stylesheet } = useSuspenseQuery(
orpc.resume.stylesheet.getState.queryOptions({ input: { id: resumeId } }),
);
const initializeResumeStore = useResumeStore((state) => state.initialize);
const mergeResumeMetadata = useResumeStore((state) => state.mergeResumeMetadata);
const isReady = useResumeStore((state) => state.isReady);
const initializedResumeId = useResumeStore((state) => state.resumeId);
const isInitialized = isReady && initializedResumeId === resumeId;
const isStylesheetInitialized = useStylesheetStore((state) => state.resumeId === resumeId);
const stylesheetInitialization = useRef({ resume, stylesheet });
stylesheetInitialization.current = { resume, stylesheet };
useResumeCleanup();
useBuilderResumeUpdateSubscription();
@@ -60,16 +50,6 @@ function RouteComponent() {
initializeResumeStore(resume);
}, [initializeResumeStore, isInitialized, resume]);
useEffect(() => {
if (!isInitialized) return;
const initial = stylesheetInitialization.current;
return initializeStylesheetStore({
resumeId,
initial: initial.stylesheet,
resumeData: initial.resume.data,
});
}, [isInitialized, resumeId]);
useEffect(() => {
mergeResumeMetadata(resume);
}, [
@@ -85,7 +65,7 @@ function RouteComponent() {
resume,
]);
if (!isInitialized || !isStylesheetInitialized) return null;
if (!isInitialized) return null;
return <BuilderLayoutShell initialLayout={initialLayout} />;
}