diff --git a/apps/artboard/src/pages/artboard.tsx b/apps/artboard/src/pages/artboard.tsx index 5b212cb0e..d2a874644 100644 --- a/apps/artboard/src/pages/artboard.tsx +++ b/apps/artboard/src/pages/artboard.tsx @@ -55,5 +55,11 @@ export const ArtboardPage = () => { } }, [metadata]); - return ; + return ( + <> + {metadata.css.visible && } + + + + ); }; diff --git a/apps/client/public/styles/prism-dark.css b/apps/client/public/styles/prism-dark.css new file mode 100644 index 000000000..89b6ba0c7 --- /dev/null +++ b/apps/client/public/styles/prism-dark.css @@ -0,0 +1,155 @@ +code[class*="language-"], +pre[class*="language-"] { + color: #f8f8f2; + background: none; + font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + border-radius: 0.3em; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #2b2b2b; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: 0.1em; + border-radius: 0.3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #d4d0ab; +} + +.token.punctuation { + color: #fefefe; +} + +.token.property, +.token.tag, +.token.constant, +.token.symbol, +.token.deleted { + color: #ffa07a; +} + +.token.boolean, +.token.number { + color: #00e0e0; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #abe338; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string, +.token.variable { + color: #00e0e0; +} + +.token.atrule, +.token.attr-value, +.token.function { + color: #ffd700; +} + +.token.keyword { + color: #00e0e0; +} + +.token.regex, +.token.important { + color: #ffd700; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +@media screen and (forced-colors: active) { + code[class*="language-"], + pre[class*="language-"] { + color: windowText; + background: window; + } + + :not(pre) > code[class*="language-"], + pre[class*="language-"] { + background: window; + } + + .token.important { + background: highlight; + color: window; + font-weight: normal; + } + + .token.atrule, + .token.attr-value, + .token.function, + .token.keyword, + .token.operator, + .token.selector { + font-weight: bold; + } + + .token.attr-value, + .token.comment, + .token.doctype, + .token.function, + .token.keyword, + .token.operator, + .token.property, + .token.string { + color: highlight; + } + + .token.attr-value, + .token.url { + font-weight: normal; + } +} diff --git a/apps/client/public/styles/prism-light.css b/apps/client/public/styles/prism-light.css new file mode 100644 index 000000000..d162b1617 --- /dev/null +++ b/apps/client/public/styles/prism-light.css @@ -0,0 +1,167 @@ +code[class*="language-"], +pre[class*="language-"] { + color: #393a34; + font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + font-size: 0.9em; + line-height: 1.2em; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre > code[class*="language-"] { + font-size: 1em; +} + +pre[class*="language-"]::-moz-selection, +pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, +code[class*="language-"] ::-moz-selection { + background: #c1def1; +} + +pre[class*="language-"]::selection, +pre[class*="language-"] ::selection, +code[class*="language-"]::selection, +code[class*="language-"] ::selection { + background: #c1def1; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + border: 1px solid #dddddd; + background-color: white; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: 0.2em; + padding-top: 1px; + padding-bottom: 1px; + background: #f8f8f8; + border: 1px solid #dddddd; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #008000; + font-style: italic; +} + +.token.namespace { + opacity: 0.7; +} + +.token.string { + color: #a31515; +} + +.token.punctuation, +.token.operator { + color: #393a34; /* no highlight */ +} + +.token.url, +.token.symbol, +.token.number, +.token.boolean, +.token.variable, +.token.constant, +.token.inserted { + color: #36acaa; +} + +.token.atrule, +.token.keyword, +.token.attr-value, +.language-autohotkey .token.selector, +.language-json .token.boolean, +.language-json .token.number, +code[class*="language-css"] { + color: #0000ff; +} + +.token.function { + color: #393a34; +} + +.token.deleted, +.language-autohotkey .token.tag { + color: #9a050f; +} + +.token.selector, +.language-autohotkey .token.keyword { + color: #00009f; +} + +.token.important { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.class-name, +.language-json .token.property { + color: #2b91af; +} + +.token.tag, +.token.selector { + color: #800000; +} + +.token.attr-name, +.token.property, +.token.regex, +.token.entity { + color: #ff0000; +} + +.token.directive.tag .tag { + background: #ffff00; + color: #393a34; +} + +/* overrides color-values for the Line Numbers plugin + * http://prismjs.com/plugins/line-numbers/ + */ +.line-numbers.line-numbers .line-numbers-rows { + border-right-color: #a5a5a5; +} + +.line-numbers .line-numbers-rows > span:before { + color: #2b91af; +} + +/* overrides color-values for the Line Highlight plugin +* http://prismjs.com/plugins/line-highlight/ +*/ +.line-highlight.line-highlight { + background: rgba(193, 222, 241, 0.2); + background: -webkit-linear-gradient(left, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0)); + background: linear-gradient(to right, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0)); +} diff --git a/apps/client/public/templates/json/azurill.json b/apps/client/public/templates/json/azurill.json index 8f045ea0b..a738b4257 100644 --- a/apps/client/public/templates/json/azurill.json +++ b/apps/client/public/templates/json/azurill.json @@ -289,7 +289,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/bronzor.json b/apps/client/public/templates/json/bronzor.json index aec1e43ef..20729a6a5 100644 --- a/apps/client/public/templates/json/bronzor.json +++ b/apps/client/public/templates/json/bronzor.json @@ -314,7 +314,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/chikorita.json b/apps/client/public/templates/json/chikorita.json index fdfff8181..f1f798c9f 100644 --- a/apps/client/public/templates/json/chikorita.json +++ b/apps/client/public/templates/json/chikorita.json @@ -314,7 +314,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/ditto.json b/apps/client/public/templates/json/ditto.json index 19c663e37..a58fc3409 100644 --- a/apps/client/public/templates/json/ditto.json +++ b/apps/client/public/templates/json/ditto.json @@ -315,7 +315,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/gengar.json b/apps/client/public/templates/json/gengar.json index b3f9ca7f5..9133f9f2e 100644 --- a/apps/client/public/templates/json/gengar.json +++ b/apps/client/public/templates/json/gengar.json @@ -289,7 +289,7 @@ [[], []] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/glalie.json b/apps/client/public/templates/json/glalie.json index 05fe2b2c0..8e07b00cc 100644 --- a/apps/client/public/templates/json/glalie.json +++ b/apps/client/public/templates/json/glalie.json @@ -288,7 +288,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/kakuna.json b/apps/client/public/templates/json/kakuna.json index fcebdd35a..0c5b50f82 100644 --- a/apps/client/public/templates/json/kakuna.json +++ b/apps/client/public/templates/json/kakuna.json @@ -287,7 +287,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/leafish.json b/apps/client/public/templates/json/leafish.json index a1a5745d1..97693bc1f 100644 --- a/apps/client/public/templates/json/leafish.json +++ b/apps/client/public/templates/json/leafish.json @@ -289,7 +289,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/nosepass.json b/apps/client/public/templates/json/nosepass.json index e662f3015..f4aca21cb 100644 --- a/apps/client/public/templates/json/nosepass.json +++ b/apps/client/public/templates/json/nosepass.json @@ -306,7 +306,7 @@ [["projects", "certifications", "skills", "languages", "references"], []] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/onyx.json b/apps/client/public/templates/json/onyx.json index f1dc5ebc6..98f671005 100644 --- a/apps/client/public/templates/json/onyx.json +++ b/apps/client/public/templates/json/onyx.json @@ -287,7 +287,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/pikachu.json b/apps/client/public/templates/json/pikachu.json index 35c74fa25..ccd7b59a0 100644 --- a/apps/client/public/templates/json/pikachu.json +++ b/apps/client/public/templates/json/pikachu.json @@ -315,7 +315,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/public/templates/json/rhyhorn.json b/apps/client/public/templates/json/rhyhorn.json index 3bb365406..a2213b3bc 100644 --- a/apps/client/public/templates/json/rhyhorn.json +++ b/apps/client/public/templates/json/rhyhorn.json @@ -288,7 +288,7 @@ ] ], "css": { - "value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + "value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", "visible": false }, "page": { diff --git a/apps/client/src/pages/builder/sidebars/right/index.tsx b/apps/client/src/pages/builder/sidebars/right/index.tsx index c925a2ca9..ba6dfd53d 100644 --- a/apps/client/src/pages/builder/sidebars/right/index.tsx +++ b/apps/client/src/pages/builder/sidebars/right/index.tsx @@ -5,6 +5,7 @@ import { useRef } from "react"; import { Copyright } from "@/client/components/copyright"; import { ThemeSwitch } from "@/client/components/theme-switch"; +import { CssSection } from "./sections/css"; import { ExportSection } from "./sections/export"; import { InformationSection } from "./sections/information"; import { LayoutSection } from "./sections/layout"; @@ -37,6 +38,8 @@ export const RightSidebar = () => { + + @@ -85,6 +88,13 @@ export const RightSidebar = () => { scrollIntoView("#theme"); }} /> + { + scrollIntoView("#css"); + }} + /> { + const { isDarkMode } = useTheme(); + + const setValue = useResumeStore((state) => state.setValue); + const css = useResumeStore((state) => state.resume.data.metadata.css); + + return ( +
+ + {isDarkMode && } + {!isDarkMode && } + + +
+
+ {getSectionIcon("css")} +

{t`Custom CSS`}

+
+
+ +
+
+ { + setValue("metadata.css.visible", checked); + }} + /> + +
+ +
+ Prism.highlight(code, Prism.languages.css, "css")} + onValueChange={(value) => { + setValue("metadata.css.value", value); + }} + /> +
+
+
+ ); +}; diff --git a/apps/client/src/pages/builder/sidebars/right/shared/section-icon.tsx b/apps/client/src/pages/builder/sidebars/right/shared/section-icon.tsx index 4a86ea702..38b7b8a14 100644 --- a/apps/client/src/pages/builder/sidebars/right/shared/section-icon.tsx +++ b/apps/client/src/pages/builder/sidebars/right/shared/section-icon.tsx @@ -1,4 +1,5 @@ import { + Code, DiamondsFour, DownloadSimple, IconProps, @@ -19,6 +20,7 @@ export type MetadataKey = | "layout" | "typography" | "theme" + | "css" | "page" | "locale" | "sharing" @@ -45,6 +47,9 @@ export const getSectionIcon = (id: MetadataKey, props: IconProps = {}) => { case "theme": { return ; } + case "css": { + return ; + } case "page": { return ; } diff --git a/apps/server/src/printer/printer.service.ts b/apps/server/src/printer/printer.service.ts index 262bac5c2..abc1fae2b 100644 --- a/apps/server/src/printer/printer.service.ts +++ b/apps/server/src/printer/printer.service.ts @@ -150,6 +150,17 @@ export class PrinterService { return temporaryHtml_; }, pageElement); + // Apply custom CSS if enabled + const css = resume.data.metadata.css; + + if (css.visible) { + await page.evaluate((cssValue: string) => { + const styleTag = document.createElement("style"); + styleTag.textContent = cssValue; + document.head.append(styleTag); + }, css.value); + } + const uint8array = await page.pdf({ width, height, printBackground: true }); const buffer = Buffer.from(uint8array); pagesBuffer.push(buffer); diff --git a/libs/schema/src/metadata/index.ts b/libs/schema/src/metadata/index.ts index bf50e56d4..24573360c 100644 --- a/libs/schema/src/metadata/index.ts +++ b/libs/schema/src/metadata/index.ts @@ -12,7 +12,7 @@ export const metadataSchema = z.object({ template: z.string().default("rhyhorn"), layout: z.array(z.array(z.array(z.string()))).default(defaultLayout), // pages -> columns -> sections css: z.object({ - value: z.string().default(".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}"), + value: z.string().default("* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}"), visible: z.boolean().default(false), }), page: z.object({ @@ -50,7 +50,7 @@ export const defaultMetadata: Metadata = { template: "rhyhorn", layout: defaultLayout, css: { - value: ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + value: "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", visible: false, }, page: { diff --git a/libs/schema/src/sample.ts b/libs/schema/src/sample.ts index c7896c5ec..22f319f49 100644 --- a/libs/schema/src/sample.ts +++ b/libs/schema/src/sample.ts @@ -308,7 +308,7 @@ export const sampleResume: ResumeData = { ], ], css: { - value: ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", + value: "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}", visible: false, }, page: { diff --git a/package.json b/package.json index 39bce4271..f05751008 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@reactive-resume/source", "description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.", - "version": "4.3.4", + "version": "4.3.5", "license": "MIT", "private": true, "author": { @@ -75,6 +75,7 @@ "@types/passport-github2": "^1.2.9", "@types/passport-google-oauth20": "^2.0.16", "@types/passport-local": "^1.0.38", + "@types/prismjs": "^1.26.5", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/react-is": "^18.3.1", @@ -215,6 +216,7 @@ "passport-local": "^1.0.0", "pdf-lib": "^1.17.1", "prisma": "^5.22.0", + "prismjs": "^1.29.0", "puppeteer": "^23.11.1", "qrcode.react": "^4.2.0", "react": "^18.3.1", @@ -225,6 +227,7 @@ "react-parallax-tilt": "^1.7.272", "react-resizable-panels": "^2.1.7", "react-router": "^7.1.1", + "react-simple-code-editor": "^0.14.1", "react-zoom-pan-pinch": "^3.6.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85c54548b..03e46cee9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -296,6 +296,9 @@ importers: prisma: specifier: ^5.22.0 version: 5.22.0 + prismjs: + specifier: ^1.29.0 + version: 1.29.0 puppeteer: specifier: ^23.11.1 version: 23.11.1(typescript@5.7.3) @@ -326,6 +329,9 @@ importers: react-router: specifier: ^7.1.1 version: 7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-simple-code-editor: + specifier: ^0.14.1 + version: 0.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-zoom-pan-pinch: specifier: ^3.6.1 version: 3.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -507,6 +513,9 @@ importers: '@types/passport-local': specifier: ^1.0.38 version: 1.0.38 + '@types/prismjs': + specifier: ^1.26.5 + version: 1.26.5 '@types/react': specifier: ^18.3.18 version: 18.3.18 @@ -4405,6 +4414,9 @@ packages: '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + '@types/prop-types@15.7.11': resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} @@ -9398,6 +9410,10 @@ packages: engines: {node: '>=16.13'} hasBin: true + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + proc-log@3.0.0: resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -9693,6 +9709,12 @@ packages: react-dom: optional: true + react-simple-code-editor@0.14.1: + resolution: {integrity: sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-style-singleton@2.2.1: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -16161,6 +16183,8 @@ snapshots: pg-protocol: 1.7.0 pg-types: 2.2.0 + '@types/prismjs@1.26.5': {} + '@types/prop-types@15.7.11': {} '@types/pug@2.0.10': @@ -22313,6 +22337,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + prismjs@1.29.0: {} + proc-log@3.0.0: {} process-nextick-args@2.0.1: {} @@ -22712,6 +22738,11 @@ snapshots: optionalDependencies: react-dom: 18.3.1(react@18.3.1) + react-simple-code-editor@0.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-style-singleton@2.2.1(@types/react@18.3.18)(react@18.3.1): dependencies: get-nonce: 1.0.1