diff --git a/apps/web/package.json b/apps/web/package.json index 6bd361a9c..8f475c862 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,7 +26,6 @@ "@codemirror/commands": "^6.11.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/language": "^6.12.4", - "@codemirror/lint": "^6.9.7", "@codemirror/search": "^6.7.2", "@codemirror/state": "^6.7.6", "@codemirror/view": "^6.43.13", diff --git a/apps/web/src/features/resume/stylesheet/editor-extensions.test.ts b/apps/web/src/features/resume/stylesheet/editor-extensions.test.ts index 7f30bc9bc..60db1008b 100644 --- a/apps/web/src/features/resume/stylesheet/editor-extensions.test.ts +++ b/apps/web/src/features/resume/stylesheet/editor-extensions.test.ts @@ -1,6 +1,6 @@ // @vitest-environment happy-dom -import type { SemanticCssDiagnostic, SemanticNode } from "@reactive-resume/resume/stylesheet"; +import type { SemanticNode } from "@reactive-resume/resume/stylesheet"; import { afterEach, describe, expect, it, vi } from "vitest"; import { Transaction } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; @@ -12,7 +12,6 @@ import { copySourceToClipboard, createSemanticCssEditorExtensions, getSemanticCssHoverDocumentation, - mapCompilerDiagnostics, } from "./editor-extensions"; const semanticTree: SemanticNode = { @@ -89,7 +88,7 @@ describe("Semantic CSS editor extensions", () => { expect(variableLabels).toEqual(expect.arrayContaining(["--brand-accent", "--resume-primary-color"])); expect(systemLabels).toEqual(expect.arrayContaining(["--resume-primary-color", "--resume-sidebar-width"])); expect(systemLabels).not.toContain("--resume-font-family"); - expect(directiveLabels).toEqual(expect.arrayContaining(["@media", "@version 1;"])); + expect(directiveLabels).toEqual(["@media"]); }); it("offers only the current property's registered compiler vocabulary", () => { @@ -164,7 +163,7 @@ describe("Semantic CSS editor extensions", () => { ); }); - it("maps compiler offsets and only decorates compiler-confirmed color values", () => { + it("only decorates compiler-confirmed color values", () => { const source = "@version 1;\nsection { color: #ff0000; background-color: rgb(0 0 0); }\n"; const compiled = compileStylesheet({ languageVersion: 1, text: source }); expect(compiled.program).not.toBeNull(); @@ -178,25 +177,11 @@ describe("Semantic CSS editor extensions", () => { }, ]); - const diagnostic: SemanticCssDiagnostic = { - code: "INVALID_VALUE", - severity: "error", - message: "Bad value", - range: { - start: { line: 1, column: 1, offset: 2 }, - end: { line: 1, column: 30, offset: 99 }, - }, - }; - expect(mapCompilerDiagnostics(10, [diagnostic])).toEqual([ - expect.objectContaining({ from: 2, to: 10, severity: "error", message: "Bad value" }), - ]); - const selected = vi.fn(); const view = new EditorView({ doc: source, extensions: createSemanticCssEditorExtensions({ metadata, - diagnostics: [], colorTokens: tokens, onColorSelect: selected, }), @@ -208,19 +193,6 @@ describe("Semantic CSS editor extensions", () => { expect(selected).toHaveBeenCalledWith(tokens[0], expect.any(DOMRect)); }); - it("keeps unsupported-gradient diagnostics visible in the editor", () => { - const source = "@version 1;\nheader { background-image: linear-gradient(red, blue); }\n"; - const compiled = compileStylesheet({ languageVersion: 1, text: source }); - - expect(mapCompilerDiagnostics(source.length, compiled.diagnostics)).toContainEqual( - expect.objectContaining({ - severity: "error", - message: "Gradients are not supported by Semantic CSS. Use background-color or another supported property.", - source: "UNSUPPORTED_PROPERTY", - }), - ); - }); - it("preserves exact clipboard text and emits one change for an IME composition", async () => { const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } }); @@ -256,7 +228,6 @@ describe("Semantic CSS editor extensions", () => { doc: "section { color: red; }", extensions: createSemanticCssEditorExtensions({ metadata, - diagnostics: [], colorTokens: [], onColorSelect: vi.fn(), }), diff --git a/apps/web/src/features/resume/stylesheet/editor-extensions.ts b/apps/web/src/features/resume/stylesheet/editor-extensions.ts index ce3a1839f..acd671be4 100644 --- a/apps/web/src/features/resume/stylesheet/editor-extensions.ts +++ b/apps/web/src/features/resume/stylesheet/editor-extensions.ts @@ -1,12 +1,10 @@ import type { Completion, CompletionContext, CompletionResult, CompletionSource } from "@codemirror/autocomplete"; -import type { Diagnostic } from "@codemirror/lint"; import type { EditorState, Extension } from "@codemirror/state"; import type { DecorationSet, EditorView as EditorViewType, ViewUpdate } from "@codemirror/view"; -import type { SemanticCssDiagnostic, SemanticNode } from "@reactive-resume/resume/stylesheet/registry"; +import type { SemanticNode } from "@reactive-resume/resume/stylesheet/registry"; import type { SemanticCssColorToken } from "./color-tokens"; import type { SemanticCssEditorMetadata } from "./protocol"; import { autocompletion } from "@codemirror/autocomplete"; -import { linter, lintGutter } from "@codemirror/lint"; import { search, searchKeymap } from "@codemirror/search"; import { Decoration, EditorView, hoverTooltip, keymap, ViewPlugin, WidgetType } from "@codemirror/view"; import { @@ -20,7 +18,7 @@ import { export type SemanticCssColorSelection = (token: SemanticCssColorToken, rect: DOMRect) => void; -const directives = ["@media", "@version 1;"] as const; +const directives = ["@media"] as const; function walk(root: SemanticNode): SemanticNode[] { const nodes: SemanticNode[] = []; @@ -140,19 +138,6 @@ export function getSemanticCssHoverDocumentation( return; } -export function mapCompilerDiagnostics( - docLength: number, - diagnostics: readonly SemanticCssDiagnostic[], -): readonly Diagnostic[] { - return diagnostics.map(({ message, severity, range, code }) => ({ - from: Math.max(0, Math.min(docLength, range.start.offset)), - to: Math.max(0, Math.min(docLength, Math.max(range.start.offset, range.end.offset))), - severity, - message, - source: code, - })); -} - export function compositionAwareDocumentListener( onChange: (source: string) => void, ignore?: (update: ViewUpdate) => boolean, @@ -301,7 +286,6 @@ function colorExtension(tokens: readonly SemanticCssColorToken[], onSelect: Sema export function createSemanticCssEditorExtensions(input: { metadata: SemanticCssEditorMetadata; - diagnostics: readonly SemanticCssDiagnostic[]; colorTokens: readonly SemanticCssColorToken[]; onColorSelect: SemanticCssColorSelection; }): Extension { @@ -310,8 +294,6 @@ export function createSemanticCssEditorExtensions(input: { hoverExtension(input.metadata), search({ top: true }), keymap.of(searchKeymap), - lintGutter(), - linter((view) => mapCompilerDiagnostics(view.state.doc.length, input.diagnostics), { delay: 0 }), colorExtension(input.colorTokens, input.onColorSelect), ]; } diff --git a/apps/web/src/features/resume/stylesheet/editor.test.tsx b/apps/web/src/features/resume/stylesheet/editor.test.tsx index 95c07caf3..1a9875ddc 100644 --- a/apps/web/src/features/resume/stylesheet/editor.test.tsx +++ b/apps/web/src/features/resume/stylesheet/editor.test.tsx @@ -11,8 +11,6 @@ import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { TooltipProvider } from "@reactive-resume/ui/components/tooltip"; import { collectCompiledColorTokens } from "./color-tokens"; import StylesheetEditorShell, { StylesheetCodeEditor } from "./editor"; -import { LegacyStylesheetBanner } from "./legacy-banner"; -import { StylesheetStatus } from "./status"; const media = vi.hoisted(() => ({ mobile: false })); const compileWorker = vi.hoisted(() => ({ @@ -60,28 +58,16 @@ vi.mock("./worker-client", () => ({ }), })); -const recoverableError: SemanticCssDiagnostic = { - code: "INVALID_VALUE", +const fatalError: SemanticCssDiagnostic = { + code: "VERSION_MISMATCH", severity: "error", - message: "Invalid value", + message: "Version mismatch", range: { start: { line: 2, column: 3, offset: 17 }, end: { line: 2, column: 9, offset: 23 }, }, }; -const fatalError: SemanticCssDiagnostic = { - ...recoverableError, - code: "VERSION_MISMATCH", - message: "Version mismatch", -}; - -const resolutionError: SemanticCssDiagnostic = { - ...recoverableError, - code: "UNRESOLVED_VARIABLE", - message: "Undefined variable --missing", -}; - const guideName = /read the applying custom styles guide.*opens in new tab/i; const expectGuideLink = (root: HTMLElement) => { @@ -116,7 +102,6 @@ function renderColorEditor(source: string) { result.rerender(editor(value)) }; } -describe("stylesheet editor status", () => { - it("explains that fatal source falls back to base styles", () => { - renderWithI18n(); - - expect(screen.getByText(/preview and export fall back to base styles/i)).toBeInTheDocument(); - expect(screen.getByText("Version mismatch")).toBeInTheDocument(); - }); - - it("explains that recoverable errors preserve valid styles", () => { - renderWithI18n(); - - expect(screen.getByText("Valid with errors")).toBeInTheDocument(); - expect(screen.getByText(/preview and export keep valid styles/i)).toBeInTheDocument(); - expect(screen.queryByText(/fall back to base styles/i)).not.toBeInTheDocument(); - }); - - it("labels a valid legacy draft as ready to activate", () => { - renderWithI18n(); - - expect(screen.getByText("Ready to activate")).toBeInTheDocument(); - }); - - it("labels a legacy draft with warnings as ready to activate", () => { - renderWithI18n( - , - ); - - expect(screen.getByText("Ready to activate with warnings")).toBeInTheDocument(); - }); - - it("disables activation while the converted draft has errors", () => { - renderWithI18n(); - - expect(screen.getByRole("button", { name: /activate semantic css/i })).toBeDisabled(); - }); -}); - describe("StylesheetCodeEditor", () => { it("preserves contextual currentcolor without exposing an editable literal swatch", () => { const source = "@version 1;\nsection { color: red; border-color: currentcolor; }"; @@ -249,7 +197,6 @@ describe("StylesheetCodeEditor", () => { const onChange = vi.fn(); const destroy = vi.spyOn(EditorView.prototype, "destroy"); const props = { - diagnostics: [] as const, theme: "light" as const, onChange, onUndo: vi.fn(), @@ -276,13 +223,7 @@ describe("StylesheetCodeEditor", () => { rerender(
- +
, ); @@ -290,7 +231,6 @@ describe("StylesheetCodeEditor", () => { "contenteditable", "false", ); - expect(container.querySelector(".cm-gutter-lint")).toBeInTheDocument(); unmount(); expect(destroy).toHaveBeenCalledOnce(); @@ -304,7 +244,6 @@ describe("StylesheetCodeEditor", () => { const { container } = renderWithI18n( { expectGuideLink(container); }); - it("has no apply or save action for an already-semantic stylesheet", async () => { + it("has no apply or save action for an already-semantic stylesheet", () => { if (!builder.data) throw new Error("Missing resume fixture"); builder.data.metadata.stylesheet = { mode: "semantic", @@ -355,7 +294,6 @@ describe("StylesheetEditorShell", () => {
, ); - await screen.findByText("Valid"); expect(screen.queryByRole("button", { name: /activate semantic css/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /save|apply/i })).not.toBeInTheDocument(); }); @@ -380,7 +318,7 @@ describe("StylesheetEditorShell", () => { act(() => view.dispatch({ changes: { from: view.state.doc.length, insert: "name { color: blue; }\n" } })); - expect(builder.data.metadata.stylesheet.source.text).toBe("@version 1;\nname { color: blue; }\n"); + expect(builder.data.metadata.stylesheet.source.text).toBe("name { color: blue; }\n"); }); it("switches a converted legacy draft through the ordinary resume update", async () => { @@ -398,29 +336,11 @@ describe("StylesheetEditorShell", () => { expect(builder.data?.metadata.stylesheet).toEqual({ mode: "semantic", - source: { languageVersion: 1, text: "@version 1;\n" }, + source: { languageVersion: 1, text: "" }, }); }); - it("allows a converted legacy draft with recoverable errors to activate", async () => { - compileWorker.diagnostics = [resolutionError]; - - render( - - - - - , - ); - - const activate = await screen.findByRole("button", { name: "Activate Semantic CSS" }); - await waitFor(() => expect(activate).toBeEnabled()); - fireEvent.click(activate); - - expect(builder.data?.metadata.stylesheet?.mode).toBe("semantic"); - }); - - it("keeps legacy activation disabled for fatal diagnostics", async () => { + it("does not block legacy activation on compiler errors", async () => { compileWorker.diagnostics = [fatalError]; render( @@ -431,8 +351,11 @@ describe("StylesheetEditorShell", () => { , ); - await screen.findByText("Fatal error"); - expect(screen.getByRole("button", { name: "Activate Semantic CSS" })).toBeDisabled(); + const activate = await screen.findByRole("button", { name: "Activate Semantic CSS" }); + expect(activate).toBeEnabled(); + fireEvent.click(activate); + + expect(builder.data?.metadata.stylesheet?.mode).toBe("semantic"); }); it("makes editor mutation controls read-only while the resume is locked", () => { @@ -476,7 +399,6 @@ describe("StylesheetEditorShell", () => { expect(within(sheet).getByRole("heading", { name: "Semantic CSS stylesheet" })).toBeInTheDocument(); expect(within(sheet).getByRole("button", { name: "Activate Semantic CSS" })).toBeInTheDocument(); expect(within(sheet).getByRole("toolbar", { name: "Stylesheet editor" })).toBeInTheDocument(); - await within(sheet).findByText("Ready to activate"); expectGuideLink(sheet); expect(document.querySelectorAll(".cm-editor")).toHaveLength(1); media.mobile = false; diff --git a/apps/web/src/features/resume/stylesheet/editor.tsx b/apps/web/src/features/resume/stylesheet/editor.tsx index 13e4b8281..917dbac97 100644 --- a/apps/web/src/features/resume/stylesheet/editor.tsx +++ b/apps/web/src/features/resume/stylesheet/editor.tsx @@ -1,5 +1,5 @@ import type { Extension } from "@codemirror/state"; -import type { SemanticCssDiagnostic, SemanticNode } from "@reactive-resume/resume/stylesheet"; +import type { SemanticNode } from "@reactive-resume/resume/stylesheet"; import type { ResumeData } from "@reactive-resume/schema/resume/data"; import type { StylesheetSource } from "@reactive-resume/schema/resume/stylesheet"; import type { SemanticCssColorToken } from "./color-tokens"; @@ -27,7 +27,6 @@ import { semanticNodeKeys, shouldShowResumeHeader, } from "@reactive-resume/pdf/semantic-tree"; -import { isFatalStylesheetDiagnostic } from "@reactive-resume/resume/stylesheet"; import { Icon } from "@reactive-resume/ui/components/icon"; import { PopoverTrigger } from "@reactive-resume/ui/components/popover"; import { Sheet, SheetContent, SheetTitle } from "@reactive-resume/ui/components/sheet"; @@ -39,7 +38,6 @@ import { serializeStylesheetColor, toStylesheetPickerColor } from "./color-forma import { compositionAwareDocumentListener, createSemanticCssEditorExtensions } from "./editor-extensions"; import { formatEditorDocument } from "./formatter"; import { LegacyStylesheetBanner } from "./legacy-banner"; -import { StylesheetStatus } from "./status"; import { StylesheetToolbar } from "./toolbar"; import { createCompileWorkerClient } from "./worker-client"; @@ -103,7 +101,6 @@ const readOnlyExtensions = (readOnly: boolean): Extension => [ export type StylesheetCodeEditorProps = { value: string; - diagnostics: readonly SemanticCssDiagnostic[]; colorTokens?: readonly SemanticCssColorToken[]; metadata?: SemanticCssEditorMetadata; theme: "light" | "dark"; @@ -118,7 +115,6 @@ export type StylesheetCodeEditorProps = { export function StylesheetCodeEditor({ value, - diagnostics, colorTokens = [], metadata = emptyMetadata, theme, @@ -133,7 +129,7 @@ export function StylesheetCodeEditor({ const hostRef = useRef(null); const viewRef = useRef(null); const compartmentsRef = useRef(null); - const initialPropsRef = useRef({ value, diagnostics, colorTokens, metadata, theme, readOnly, label }); + const initialPropsRef = useRef({ value, colorTokens, metadata, theme, readOnly, label }); const onChangeRef = useRef(onChange); const onFocusChangeRef = useRef(onFocusChange); const onReadyRef = useRef(onReady); @@ -233,7 +229,6 @@ export function StylesheetCodeEditor({ compartments.intelligence.of( createSemanticCssEditorExtensions({ metadata: initial.metadata, - diagnostics: initial.diagnostics, colorTokens: initial.colorTokens, onColorSelect: selectColor, }), @@ -273,13 +268,12 @@ export function StylesheetCodeEditor({ effects: compartments.intelligence.reconfigure( createSemanticCssEditorExtensions({ metadata, - diagnostics, colorTokens, onColorSelect: selectColor, }), ), }); - }, [colorTokens, diagnostics, metadata, selectColor]); + }, [colorTokens, metadata, selectColor]); useEffect(() => { const view = viewRef.current; @@ -398,9 +392,7 @@ const createEditorMetadata = (data: ResumeData): SemanticCssEditorMetadata => { function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps) { const { resolvedTheme: theme } = useTheme(); const [focusOpen, setFocusOpen] = useState(false); - const [diagnostics, setDiagnostics] = useState([]); const [colorTokens, setColorTokens] = useState([]); - const [status, setStatus] = useState<"idle" | "compiling" | "error">("compiling"); const [compiler, setCompiler] = useState>(); const data = useResumeData(); const updateResumeData = useUpdateResumeData(); @@ -425,21 +417,16 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps) const stylesheet = data?.metadata.stylesheet; const mode = stylesheet?.mode ?? "legacy"; const source = useMemo( - () => - stylesheet?.source ?? - (data ? convertLegacyStyleRules(data).source : { languageVersion: 1, text: "@version 1;\n" }), + () => stylesheet?.source ?? (data ? convertLegacyStyleRules(data).source : { languageVersion: 1, text: "" }), [data, stylesheet], ); const metadata = useMemo(() => (data ? createEditorMetadata(data) : emptyMetadata), [data]); - const hasFatalErrors = status === "error" || diagnostics.some(isFatalStylesheetDiagnostic); - const isChecking = status === "compiling"; const disabled = readOnly || isLocked; useEffect(() => { if (!compiler || !data) return; let cancelled = false; const editGeneration = ++compileGenerationRef.current; - setStatus("compiling"); setColorTokens([]); const timer = window.setTimeout(() => { void compiler @@ -459,13 +446,10 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps) }) .then((result) => { if (cancelled || result.editGeneration !== compileGenerationRef.current) return; - setDiagnostics(result.diagnostics); setColorTokens(result.colorTokens ?? []); - setStatus(result.program && !result.diagnostics.some(isFatalStylesheetDiagnostic) ? "idle" : "error"); }) - .catch(() => { - if (!cancelled && editGeneration === compileGenerationRef.current) setStatus("error"); - }); + // Swatches are a nicety: a stylesheet that doesn't compile just shows none. + .catch(() => undefined); }, 180); return () => { @@ -476,15 +460,19 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps) if (!data) return null; - const setSourceText = (text: string) => { - if (disabled || text === source.text) return; + // Stylesheets saved before the version moved out of the text start with `@version 1;`: it isn't shown, and the + // first edit drops it (the compiler ignores it either way). + const text = source.text.replace(/^\s*@version\s+\d+\s*;[ \t]*\r?\n?/, ""); + + const setSourceText = (next: string) => { + if (disabled || next === text) return; updateResumeData((draft) => { - draft.metadata.stylesheet = { mode, source: { ...source, text } }; + draft.metadata.stylesheet = { mode, source: { ...source, text: next } }; }); }; const activate = () => { - if (disabled || mode === "semantic" || hasFatalErrors || isChecking) return; + if (disabled || mode === "semantic") return; updateResumeData((draft) => { draft.metadata.stylesheet = { mode: "semantic", source }; }); @@ -495,8 +483,7 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps) const editor = ( - {mode === "legacy" && ( - - )} + {mode === "legacy" && }
{editor}
- - ); diff --git a/apps/web/src/features/resume/stylesheet/status.tsx b/apps/web/src/features/resume/stylesheet/status.tsx deleted file mode 100644 index 11e93d3e0..000000000 --- a/apps/web/src/features/resume/stylesheet/status.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { SemanticCssDiagnostic } from "@reactive-resume/resume/stylesheet"; -import { Trans } from "@lingui/react/macro"; -import { isFatalStylesheetDiagnostic } from "@reactive-resume/resume/stylesheet"; -import { Alert, AlertDescription, AlertTitle } from "@reactive-resume/ui/components/alert"; -import { Badge } from "@reactive-resume/ui/components/badge"; -import { Icon } from "@reactive-resume/ui/components/icon"; -import { ScrollArea } from "@reactive-resume/ui/components/scroll-area"; - -export type StylesheetStatusProps = { - mode: "legacy" | "semantic"; - status: "idle" | "compiling" | "error"; - diagnostics: readonly SemanticCssDiagnostic[]; -}; - -export function StylesheetStatus({ mode, status, diagnostics }: StylesheetStatusProps) { - const errors = diagnostics.filter(({ severity }) => severity === "error"); - const warnings = diagnostics.filter(({ severity }) => severity === "warning"); - const hasFatalErrors = status === "error" || diagnostics.some(isFatalStylesheetDiagnostic); - const hasRecoverableErrors = !hasFatalErrors && errors.length > 0; - const isPending = status === "compiling"; - - return ( -
- {hasFatalErrors ? ( - - - Fatal error - - ) : isPending ? ( - {mode === "legacy" ? Checking draft : Checking} - ) : hasRecoverableErrors ? ( - - - {mode === "legacy" ? Ready to activate with errors : Valid with errors} - - ) : warnings.length > 0 ? ( - - - {mode === "legacy" ? Ready to activate with warnings : Valid with warnings} - - ) : ( - {mode === "legacy" ? Ready to activate : Valid} - )} - - {hasFatalErrors && ( - - - - Stylesheet has fatal errors - - - Preview and export fall back to base styles. - - - )} - - {hasRecoverableErrors && ( - - - - Some styles were ignored - - - Preview and export keep valid styles and ignore invalid styles. - - - )} - - {diagnostics.length > 0 && ( - -
    - {diagnostics.map((diagnostic) => ( -
  • -

    {diagnostic.message}

    -

    - - Line {diagnostic.range.start.line}, column {diagnostic.range.start.column} - -

    -
  • - ))} -
-
- )} -
- ); -} diff --git a/docs/applying-custom-styles.mdx b/docs/applying-custom-styles.mdx index 66ea12a3e..1db5ce4d5 100644 --- a/docs/applying-custom-styles.mdx +++ b/docs/applying-custom-styles.mdx @@ -22,7 +22,7 @@ current rules remain active while you review it. Open the resume in the builder, select **Design**, then select **Custom Styles**. - Check the preview and warnings below the editor. The draft starts with `@version 1;`. + Check the preview against the legacy result. Select **Activate Semantic CSS** only after the preview matches the legacy result. Reactive Resume never applies both @@ -35,24 +35,18 @@ current rules remain active while you review it. Open the resume you want to style, select **Design**, then select **Custom Styles**. Start with a complete stylesheet: ```css -@version 1; - section[type="experience"] > section-heading { color: #0f766e; text-transform: uppercase; } ``` -The first line tells Reactive Resume which language version the stylesheet uses. Keep `@version 1;` at the start of -every stylesheet. - Add the stylesheet to the editor. Start with one visual change so it is easy to review in the preview. - - Reactive Resume checks the source in the browser. When the status changes to **Valid**, compare the preview and - export if you are ready to share the resume. + + The preview updates as you type. If nothing changes, the rule didn't apply; check the selector and property. Add one related change at a time. Your changes use the normal resume autosave and undo history. @@ -123,8 +117,6 @@ combinators. It also supports `:root`, `:first-child`, `:last-child`, `:only-chi `:nth-child()`, and `:nth-of-type()`. ```css -@version 1; - section[type="experience"] > section-heading { border-bottom: 1pt solid #0f766e; } @@ -148,8 +140,6 @@ Semantic CSS exposes the resolved builder settings as read-only `--resume-*` var reuse the builder values instead of duplicating colors or dimensions. ```css -@version 1; - :root { --accent: var(--resume-primary-color); --rule: #cbd5e1; @@ -195,16 +185,13 @@ data. Semantic CSS keeps background styling PDF-safe. Use a flat color for headers and regions: ```css -@version 1; - header { background-color: #1e293b; } ``` -Gradient declarations such as `background-image: linear-gradient(...)` remain unsupported. The editor reports that limitation -and suggests `background-color` or another supported property; the unsupported declaration is omitted while neighboring valid -declarations remain available for preview and export. +Gradient declarations such as `background-image: linear-gradient(...)` remain unsupported; use `background-color` instead. +An unsupported declaration is left out while the declarations around it still apply. ### Style rich-text lists @@ -212,8 +199,6 @@ declarations remain available for preview and export. for the bullet or number, and `list-item-content` for the text flow. ```css -@version 1; - rich-text list-item { gap: 4pt; } @@ -232,8 +217,6 @@ list-item-content { Target `level` to adjust the space between a skill's circles, icons, or other level decorations: ```css -@version 1; - section[type="skills"] level { column-gap: 4pt; } @@ -248,8 +231,6 @@ Named fields let you make a focused change without styling every item value. Use exists in the selected resume and template. ```css -@version 1; - section[type="experience"] field[name="position"] { font-weight: 600; } @@ -265,8 +246,6 @@ Template parts expose optional visual details that are not shared by every templ with `resume[template="..."]`; otherwise the selector may match nothing after a template change. ```css -@version 1; - resume[template="azurill"] template-part[name="timeline-line"] { background-color: #94a3b8; } @@ -298,8 +277,6 @@ Use structural declarations sparingly and review the exported PDF after each cha leave space before a section, or set a custom page size. ```css -@version 1; - page { size: 210mm 297mm; } @@ -317,8 +294,6 @@ item { browser viewport. ```css -@version 1; - @media (max-width: 600pt) { region[placement="sidebar"] { padding: 12pt; @@ -331,17 +306,16 @@ Supported media features are `width`, `min-width`, `max-width`, `height`, `min-h ## Diagnose and recover safely -The editor saves the current source even when it has an error. A recoverable error ignores only the invalid declaration, -value, selector, or rule; valid parts still appear in preview and PDF export. A fatal version or resource-limit error -ignores the whole stylesheet and renders the resume with its base styles until you fix the source. +The editor saves whatever you write. Anything it can't apply (an unknown property, a value it doesn't accept, a selector +that doesn't parse) is left out, and everything else still appears in the preview and PDF export. If you see no change, +the rule didn't apply. If a rule does not work: -1. Read the status below the editor. Errors include a line and column number when available. -2. Check the selector's spelling, attribute value, placement, and template guard. A **selector matches nothing** - warning usually means the resume does not contain that semantic node. -3. Simplify the rule to one selector and one declaration, then wait for **Valid** before adding more. -4. Use the stylesheet undo and redo controls to restore an earlier source. +1. Check the selector's spelling, attribute value, placement, and template guard. A selector that matches nothing usually + means the resume does not contain that semantic node. +2. Simplify the rule to one selector and one declaration, then add more once it shows in the preview. +3. Use the stylesheet undo and redo controls to restore an earlier source. Select **Open focus mode** when you need a taller editor. On mobile, it opens a full-width sheet; switch to **Preview** to inspect the result. @@ -358,9 +332,9 @@ starting point. Exact IDs and template parts are intentionally specific to a res 1. Select **Copy stylesheet** in the source resume. 2. Open **Design → Custom Styles** in the destination resume. -3. Paste the stylesheet and review any warnings. +3. Paste the stylesheet. 4. Replace or remove exact IDs and template-part rules that do not apply. -5. Wait for **Valid**, then compare the preview and exported PDF. +5. Compare the preview and exported PDF. Semantic CSS does not support classes, pseudo-elements, CSS Grid, arbitrary at-rules, `@import`, `@font-face`, `url()`, browser APIs, animations, filters, gradients, general box shadows, or external assets. Use the normal builder settings diff --git a/packages/pdf/src/semantic/__fixtures__/legacy/merge-specificity.expected.css b/packages/pdf/src/semantic/__fixtures__/legacy/merge-specificity.expected.css index 2f688c44b..38da50a19 100644 --- a/packages/pdf/src/semantic/__fixtures__/legacy/merge-specificity.expected.css +++ b/packages/pdf/src/semantic/__fixtures__/legacy/merge-specificity.expected.css @@ -1,5 +1,3 @@ -@version 1; - /* Global text */ section field:not([name="content"]):not([name="description"]):not([name="recipient"]):not([name="keywords"]) { color: #111111; diff --git a/packages/pdf/src/semantic/legacy-converter.ts b/packages/pdf/src/semantic/legacy-converter.ts index 385d4296c..014f66b32 100644 --- a/packages/pdf/src/semantic/legacy-converter.ts +++ b/packages/pdf/src/semantic/legacy-converter.ts @@ -116,9 +116,7 @@ const serializeBlock = ( serializeGeneratedStylesheet({ languageVersion: 1, blocks: [{ selector, declarations, ...(comment === undefined ? {} : { comment }) }], - }) - .replace(/^@version 1;\n\n/, "") - .trimEnd(); + }).trimEnd(); const appliesToAwards = (data: ResumeData, rule: StyleRule): boolean => { if (rule.target.scope === "global") return true; @@ -257,7 +255,7 @@ export function convertLegacyStyleRules(data: ResumeData): LegacyStyleConversion if (!rule.enabled) return [disabledRuleChunk(sanitizedData, rule)]; return styleSlots.flatMap((slot) => (rule.slots[slot] ? activeSlotChunks(sanitizedData, rule, slot) : [])); }); - const text = `@version 1;\n${chunks.length > 0 ? `\n${chunks.join("\n\n")}\n` : ""}`; + const text = chunks.length > 0 ? `${chunks.join("\n\n")}\n` : ""; return { source: { languageVersion: 1, text }, diff --git a/packages/resume/src/stylesheet/compile.ts b/packages/resume/src/stylesheet/compile.ts index a52bd2757..601793960 100644 --- a/packages/resume/src/stylesheet/compile.ts +++ b/packages/resume/src/stylesheet/compile.ts @@ -9,10 +9,6 @@ import { SEMANTIC_NODE_KINDS } from "./registry/semantic"; import { SYSTEM_VARIABLE_REGISTRY_V1 } from "./registry/system-variables"; import { compileProgram, cssFunctionDepth } from "./values"; -function isPositiveInteger(value: string): boolean { - return /^[1-9]\d*$/.test(value); -} - const compileVersionOne = (rules: readonly CompiledStyleRule[]): StyleProgram => Object.freeze({ languageVersion: 1, rules: Object.freeze([...rules]) }); @@ -42,53 +38,6 @@ export function compileStylesheet(source: StylesheetSource): CompileStylesheetRe const stylesheet = parseStylesheet(source.text); const diagnostics = [...stylesheet.diagnostics]; - const versionDirectives = stylesheet.atRules.filter((atRule) => atRule.name === "version"); - - if (versionDirectives.length === 0 && source.languageVersion === 1) { - diagnostics.push( - createDiagnostic("MISSING_VERSION_DIRECTIVE", "warning", "Version-one stylesheets should start with @version 1;"), - ); - } - - if (versionDirectives.length > 1) { - for (const directive of versionDirectives.slice(1)) { - diagnostics.push( - createDiagnostic( - "DUPLICATE_VERSION_DIRECTIVE", - "error", - "A stylesheet can contain only one @version directive.", - directive.range, - ), - ); - } - } - - for (const directive of versionDirectives) { - if (directive.hasBlock || !isPositiveInteger(directive.prelude)) { - diagnostics.push( - createDiagnostic( - "INVALID_VERSION", - "error", - "@version must contain one positive integer and no block.", - directive.range, - ), - ); - continue; - } - - const version = Number(directive.prelude); - if (version !== source.languageVersion) { - diagnostics.push( - createDiagnostic( - "VERSION_MISMATCH", - "error", - "@version must match the stylesheet language version.", - directive.range, - ), - ); - } - } - const compiler = source.languageVersion === 1 ? compileVersionOne : undefined; if (!compiler) { diagnostics.push( diff --git a/packages/resume/src/stylesheet/diagnostics.ts b/packages/resume/src/stylesheet/diagnostics.ts index 78969d777..bf5a0df55 100644 --- a/packages/resume/src/stylesheet/diagnostics.ts +++ b/packages/resume/src/stylesheet/diagnostics.ts @@ -7,26 +7,6 @@ type DiagnosticReference = { }; export const SEMANTIC_CSS_DIAGNOSTIC_CATALOG_V1 = { - MISSING_VERSION_DIRECTIVE: { - severity: "warning", - meaning: "The stylesheet omitted @version.", - action: "Add @version 1; as the first statement.", - }, - DUPLICATE_VERSION_DIRECTIVE: { - severity: "error", - meaning: "More than one @version directive was found.", - action: "Keep exactly one version directive.", - }, - INVALID_VERSION: { - severity: "error", - meaning: "The version directive is not one positive integer without a block.", - action: "Use @version 1;.", - }, - VERSION_MISMATCH: { - severity: "error", - meaning: "The directive and stored language version disagree.", - action: "Set both to version 1.", - }, UNSUPPORTED_VERSION: { severity: "error", meaning: "The requested Semantic CSS version is not implemented.", @@ -50,7 +30,7 @@ export const SEMANTIC_CSS_DIAGNOSTIC_CATALOG_V1 = { UNSUPPORTED_AT_RULE: { severity: "error", meaning: "The at-rule is not part of Semantic CSS version 1.", - action: "Use only @version and documented @media queries.", + action: "Use only documented @media queries.", }, INVALID_MEDIA_QUERY: { severity: "error", @@ -127,13 +107,7 @@ export const SEMANTIC_CSS_DIAGNOSTIC_CATALOG_V1 = { export type SemanticCssCompilerDiagnosticCode = keyof typeof SEMANTIC_CSS_DIAGNOSTIC_CATALOG_V1; -const FATAL_DIAGNOSTIC_CODES = new Set([ - "DUPLICATE_VERSION_DIRECTIVE", - "INVALID_VERSION", - "RESOURCE_LIMIT", - "UNSUPPORTED_VERSION", - "VERSION_MISMATCH", -]); +const FATAL_DIAGNOSTIC_CODES = new Set(["RESOURCE_LIMIT", "UNSUPPORTED_VERSION"]); export function isFatalStylesheetDiagnostic({ code }: Pick): boolean { return FATAL_DIAGNOSTIC_CODES.has(code); diff --git a/packages/resume/src/stylesheet/serialize.test.ts b/packages/resume/src/stylesheet/serialize.test.ts index a3679ec58..5620d593b 100644 --- a/packages/resume/src/stylesheet/serialize.test.ts +++ b/packages/resume/src/stylesheet/serialize.test.ts @@ -15,7 +15,7 @@ describe("generated Semantic CSS serialization", () => { }); expect(output).toBe( - '@version 1;\n\n/* Bad *\\/ label */\nsection[id="projects"] > section-heading {\n\tcolor: #123456;\n\tfont-size: 12pt;\n}\n', + '/* Bad *\\/ label */\nsection[id="projects"] > section-heading {\n\tcolor: #123456;\n\tfont-size: 12pt;\n}\n', ); }); diff --git a/packages/resume/src/stylesheet/serialize.ts b/packages/resume/src/stylesheet/serialize.ts index 6eea49787..7df7cde83 100644 --- a/packages/resume/src/stylesheet/serialize.ts +++ b/packages/resume/src/stylesheet/serialize.ts @@ -43,5 +43,5 @@ export function serializeGeneratedStylesheet(stylesheet: GeneratedStylesheet): s return `${comment}${block.selector} {\n${declarations}\n}`; }); - return `@version ${stylesheet.languageVersion};\n${blocks.length > 0 ? `\n${blocks.join("\n\n")}\n` : ""}`; + return blocks.length > 0 ? `${blocks.join("\n\n")}\n` : ""; } diff --git a/packages/resume/src/stylesheet/values.ts b/packages/resume/src/stylesheet/values.ts index e04c34408..357bdafee 100644 --- a/packages/resume/src/stylesheet/values.ts +++ b/packages/resume/src/stylesheet/values.ts @@ -648,6 +648,7 @@ export function compileProgram(stylesheet: ParsedStylesheet, languageVersion: nu } if (node.type !== "Atrule" || !node.name) continue; const name = identifier(node.name).toLowerCase(); + // Stylesheets saved before the version moved out of the text start with `@version 1;`. if (name === "version") continue; if (name !== "media") { diagnostic( diff --git a/packages/resume/src/stylesheet/version.test.ts b/packages/resume/src/stylesheet/version.test.ts index d4e2fd969..4df46e08d 100644 --- a/packages/resume/src/stylesheet/version.test.ts +++ b/packages/resume/src/stylesheet/version.test.ts @@ -2,68 +2,22 @@ import { describe, expect, it } from "vitest"; import { compileStylesheet } from "./compile"; describe("compileStylesheet", () => { - it("compiles canonical version-one source", () => { - const result = compileStylesheet({ - languageVersion: 1, - text: "@version 1;\nsection { color: #123456; }\n", - }); + it("compiles plain CSS without a version line", () => { + const result = compileStylesheet({ languageVersion: 1, text: "section { color: #123456; }\n" }); expect(result.program?.languageVersion).toBe(1); expect(result.diagnostics).toEqual([]); }); - it("warns when version-one source omits the directive", () => { - const result = compileStylesheet({ languageVersion: 1, text: "section { color: red; }" }); + it("ignores the @version line older stylesheets start with", () => { + const result = compileStylesheet({ languageVersion: 1, text: "@version 1;\nsection { color: #123456; }\n" }); - expect(result.diagnostics).toContainEqual( - expect.objectContaining({ code: "MISSING_VERSION_DIRECTIVE", severity: "warning" }), - ); - }); - - it("rejects a directive that disagrees with persisted metadata", () => { - const result = compileStylesheet({ languageVersion: 1, text: "@version 2;" }); - - expect(result.program).toBeNull(); - expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "VERSION_MISMATCH", severity: "error" })); - }); - - it("rejects duplicate directives", () => { - const result = compileStylesheet({ languageVersion: 1, text: "@version 1;\n@version 1;" }); - - expect(result.program).toBeNull(); - expect(result.diagnostics).toContainEqual( - expect.objectContaining({ code: "DUPLICATE_VERSION_DIRECTIVE", severity: "error" }), - ); - }); - - it("rejects a duplicate version directive nested in an at-rule block", () => { - const result = compileStylesheet({ - languageVersion: 1, - text: "@version 1; @media (width: 600pt) { @version 1; name { color: red; } }", - }); - - expect(result.program).toBeNull(); - expect(result.diagnostics).toContainEqual( - expect.objectContaining({ code: "DUPLICATE_VERSION_DIRECTIVE", severity: "error" }), - ); - }); - - it("rejects malformed directives", () => { - const result = compileStylesheet({ languageVersion: 1, text: "@version one;" }); - - expect(result.program).toBeNull(); - expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "INVALID_VERSION", severity: "error" })); - }); - - it("rejects non-positive directives", () => { - const result = compileStylesheet({ languageVersion: 1, text: "@version 0;" }); - - expect(result.program).toBeNull(); - expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "INVALID_VERSION", severity: "error" })); + expect(result.program?.rules).toHaveLength(1); + expect(result.diagnostics).toEqual([]); }); it("rejects unsupported persisted language versions", () => { - const result = compileStylesheet({ languageVersion: 2, text: "@version 2;" }); + const result = compileStylesheet({ languageVersion: 2, text: "section { color: red; }" }); expect(result.program).toBeNull(); expect(result.diagnostics).toContainEqual( @@ -72,7 +26,7 @@ describe("compileStylesheet", () => { }); it("compiles around a recovered CSS error", () => { - const result = compileStylesheet({ languageVersion: 1, text: "@version 1;\nsection { color red; }" }); + const result = compileStylesheet({ languageVersion: 1, text: "section { color red; }" }); expect(result.program).not.toBeNull(); expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "CSS_PARSE_ERROR", severity: "error" })); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64becd1ef..60db7f126 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -522,9 +522,6 @@ importers: '@codemirror/language': specifier: ^6.12.4 version: 6.12.4 - '@codemirror/lint': - specifier: ^6.9.7 - version: 6.9.7 '@codemirror/search': specifier: ^6.7.2 version: 6.7.2 @@ -2174,9 +2171,6 @@ packages: '@codemirror/language@6.12.4': resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} - '@codemirror/lint@6.9.7': - resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} - '@codemirror/search@6.7.2': resolution: {integrity: sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==} @@ -9747,12 +9741,6 @@ snapshots: '@lezer/lr': 1.4.10 style-mod: 4.1.4 - '@codemirror/lint@6.9.7': - dependencies: - '@codemirror/state': 6.7.6 - '@codemirror/view': 6.43.13 - crelt: 1.0.7 - '@codemirror/search@6.7.2': dependencies: '@codemirror/state': 6.7.6