diff --git a/.gitignore b/.gitignore index 2e7b3b0..b14cbc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.claude/settings.local.json .superpowers .brainstorming .worktrees diff --git a/README.md b/README.md index 86a15f7..717b04b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,38 @@ +

+ Synthpunk icon +

+ # Synthpunk High-energy synthwave-inspired color themes for VS Code, Zed, Neovim, WezTerm, and Starship. Four variants: Pastel Dark/Light and Neon Dark/Light. -![Synthpunk Pastel Dark](assets/pastel-dark.png) +![Synthpunk — the same code across all four variants](assets/hero.png) + +## Preview + +Syntax highlighting shown in the **Neon Dark** variant: + +### React + +![Synthpunk Neon Dark — React/TSX](assets/sample-react.png) + +### Python + +![Synthpunk Neon Dark — Python](assets/sample-python.png) + +### Rust + +![Synthpunk Neon Dark — Rust](assets/sample-rust.png) + +### C# + +![Synthpunk Neon Dark — C#](assets/sample-csharp.png) + +### Markdown + +![Synthpunk Neon Dark — Markdown](assets/sample-markdown.png) + +Full palette swatches for every variant: [Pastel Dark](assets/palette-preview-dark.png) · [Pastel Light](assets/palette-preview-light.png) · [Neon Dark](assets/palette-preview-neon-dark.png) · [Neon Light](assets/palette-preview-neon-light.png). Preview images are generated by [`assets/generate/`](assets/generate/). ## Installation diff --git a/assets/generate/README.md b/assets/generate/README.md new file mode 100644 index 0000000..b222c17 --- /dev/null +++ b/assets/generate/README.md @@ -0,0 +1,40 @@ +# Preview image generator + +Generates the marketing images used by the top-level [`README.md`](../../README.md): + +| Output (`assets/`) | What it is | +| -------------------- | ----------------------------------------------------------------- | +| `icon.png` | Angular-synthwave neon icon (1024×1024) | +| `hero.png` | The same snippet rendered across all four variants (2×2 grid) | +| `sample-react.png` | React/TSX preview, Neon Dark | +| `sample-python.png` | Python preview, Neon Dark | +| `sample-rust.png` | Rust preview, Neon Dark | +| `sample-csharp.png` | C# preview, Neon Dark | +| `sample-markdown.png`| Markdown preview, Neon Dark | + +Highlighting is done with [shiki](https://shiki.style) loaded with the repo's own +generated VS Code themes (`themes/vscode/themes/*.json`), so the colors match the +editor exactly. The highlighted HTML is screenshotted with the system Chrome via +`puppeteer-core`. + +## Regenerate + +Run after changing palette colors (and rebuilding the VS Code themes with +`bun run build` from the repo root): + +```sh +cd assets/generate +bun install +bun run generate # or: bun run generate.ts +``` + +Requires a Chromium-based browser. The script auto-detects Google Chrome, Chromium, +and Edge in the usual macOS/Linux locations; set `CHROME_PATH` to override: + +```sh +CHROME_PATH="/path/to/chrome" bun run generate +``` + +The code shown in each image lives in [`snippets/`](snippets/) — edit those files to +change the sample code. This directory is excluded from the repo's `tsc`/biome checks +(see `tsconfig.json` and `biome.json`); its `node_modules/` is git-ignored. diff --git a/assets/generate/bun.lock b/assets/generate/bun.lock new file mode 100644 index 0000000..0e2adea --- /dev/null +++ b/assets/generate/bun.lock @@ -0,0 +1,156 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "synthpunk-preview-generator", + "dependencies": { + "puppeteer-core": "^25.1.0", + "shiki": "^4.2.0", + }, + }, + }, + "packages": { + "@puppeteer/browsers": ["@puppeteer/browsers@3.0.4", "", { "dependencies": { "modern-tar": "^0.7.6", "yargs": "^17.7.2" }, "peerDependencies": { "proxy-agent": ">=8.0.1" }, "optionalPeers": ["proxy-agent"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA=="], + + "@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g=="], + + "@shikijs/langs": ["@shikijs/langs@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA=="], + + "@shikijs/themes": ["@shikijs/themes@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w=="], + + "@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "chromium-bidi": ["chromium-bidi@16.0.1", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "devtools-protocol": ["devtools-protocol@0.0.1624250", "", {}, "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + + "modern-tar": ["modern-tar@0.7.6", "", {}, "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "puppeteer-core": ["puppeteer-core@25.1.0", "", { "dependencies": { "@puppeteer/browsers": "3.0.4", "chromium-bidi": "16.0.1", "devtools-protocol": "0.0.1624250", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.0" } }, "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + } +} diff --git a/assets/generate/generate.ts b/assets/generate/generate.ts new file mode 100644 index 0000000..91b8bd4 --- /dev/null +++ b/assets/generate/generate.ts @@ -0,0 +1,208 @@ +/** + * Generates the README marketing images from the Synthpunk VS Code themes: + * - assets/icon.png angular-synthwave neon icon + * - assets/sample-*.png Neon Dark syntax-highlight previews (react/python/rust/csharp/markdown) + * - assets/hero.png 2x2 grid of one snippet across all four variants + * + * Highlighting uses shiki loaded with the repo's own VS Code theme JSON, so the + * colors are exactly what the editor renders. Rendering is done by screenshotting + * HTML with the system Chrome via puppeteer-core. + * + * Run: bun install && bun run generate.ts (set CHROME_PATH to override the browser) + */ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import puppeteer from "puppeteer-core"; +import { createHighlighter, type ThemeRegistrationRaw } from "shiki"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO = resolve(HERE, "..", ".."); +const THEMES_DIR = resolve(REPO, "themes/vscode/themes"); +const SNIPPETS = resolve(HERE, "snippets"); +const OUT = resolve(REPO, "assets"); + +// --- theme loading --------------------------------------------------------- + +const VARIANT_FILES = { + "pastel-dark": "synthpunk-pastel-dark-color-theme.json", + "pastel-light": "synthpunk-pastel-light-color-theme.json", + "neon-dark": "synthpunk-neon-dark-color-theme.json", + "neon-light": "synthpunk-neon-light-color-theme.json", +} as const; +type VariantKey = keyof typeof VARIANT_FILES; + +type Theme = { + name: string; + type: string; + colors: Record; + tokenColors: unknown[]; +}; + +const themes = Object.fromEntries( + (Object.keys(VARIANT_FILES) as VariantKey[]).map((key) => [ + key, + JSON.parse(readFileSync(resolve(THEMES_DIR, VARIANT_FILES[key]), "utf8")) as Theme, + ]), +) as Record; + +// --- browser / chrome ------------------------------------------------------ + +function chromePath(): string { + const candidates = [ + process.env.CHROME_PATH, + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + "/usr/bin/google-chrome", + "/usr/bin/chromium", + ].filter((p): p is string => Boolean(p)); + const found = candidates.find((p) => existsSync(p)); + if (!found) { + throw new Error("No Chrome/Chromium found. Set CHROME_PATH to a browser executable."); + } + return found; +} + +// --- html templating ------------------------------------------------------- + +const FONT_STACK = "'Cascadia Code', 'Cascadia Mono', Menlo, Consolas, monospace"; +const UI_FONT = "-apple-system, 'Segoe UI', system-ui, sans-serif"; + +const PAGE_CSS = ` + * { box-sizing: border-box; } + html, body { margin: 0; padding: 0; background: transparent; } + #shot { display: inline-block; padding: 56px; background: transparent; } + .window { + border-radius: 12px; + overflow: hidden; + box-shadow: 0 30px 70px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.05); + } + .titlebar { display: flex; align-items: center; gap: 9px; padding: 13px 16px; } + .dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; } + .filename { margin-left: 10px; font: 500 13px ${UI_FONT}; opacity: 0.85; } + .code { padding: 22px 26px; overflow: hidden; } + .code pre.shiki { margin: 0; background: transparent !important; } + .code pre.shiki code { font: 15px/1.65 ${FONT_STACK}; } + .code .line { white-space: pre; } + .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 36px; } + .cell .label { margin: 0 0 11px 3px; font: 600 14px ${UI_FONT}; color: #9a8fb0; letter-spacing: 0.4px; } +`; + +function page(inner: string, pad = 56): string { + return `
${inner}
`; +} + +function windowHtml(codeHtml: string, theme: Theme, filename: string): string { + const bg = theme.colors["editor.background"]; + const titleBg = theme.colors["titleBar.activeBackground"] ?? bg; + const nameColor = theme.colors["tab.activeForeground"] ?? theme.colors.foreground ?? "#888"; + return `
+
+ + + + ${filename} +
+
${codeHtml}
+
`; +} + +const ICON_HTML = `
+
+
+
+
+
+
+
`; + +// --- rendering ------------------------------------------------------------- + +async function main() { + const highlighter = await createHighlighter({ + themes: Object.values(themes) as ThemeRegistrationRaw[], + langs: ["tsx", "python", "rust", "csharp", "markdown", "typescript"], + }); + + const browser = await puppeteer.launch({ + executablePath: chromePath(), + headless: true, + args: ["--force-color-profile=srgb", "--font-render-hinting=none", "--hide-scrollbars"], + }); + + async function shoot(html: string, outFile: string, selector = "#shot", scale = 2) { + const p = await browser.newPage(); + await p.setViewport({ width: 1700, height: 1100, deviceScaleFactor: scale }); + await p.setContent(html, { waitUntil: "load" }); + await p.evaluate(async () => { + await document.fonts.ready; + }); + const el = await p.$(selector); + if (!el) throw new Error(`selector ${selector} not found`); + await el.screenshot({ path: outFile, omitBackground: true }); + await p.close(); + console.log("wrote", outFile.replace(`${REPO}/`, "")); + } + + function read(name: string): string { + return readFileSync(resolve(SNIPPETS, name), "utf8").replace(/\n+$/, ""); + } + + // Per-language samples in Neon Dark. + const neon = themes["neon-dark"]; + const samples = [ + { out: "sample-react.png", file: "App.tsx", lang: "tsx", snippet: "App.tsx" }, + { out: "sample-python.png", file: "pipeline.py", lang: "python", snippet: "pipeline.py" }, + { out: "sample-rust.png", file: "lib.rs", lang: "rust", snippet: "lib.rs" }, + { out: "sample-csharp.png", file: "Service.cs", lang: "csharp", snippet: "Service.cs" }, + { out: "sample-markdown.png", file: "README.md", lang: "markdown", snippet: "sample.md" }, + ]; + for (const s of samples) { + const codeHtml = highlighter.codeToHtml(read(s.snippet), { lang: s.lang, theme: neon.name }); + await shoot(page(windowHtml(codeHtml, neon, s.file)), resolve(OUT, s.out)); + } + + // Four-variant hero from one shared snippet. + const heroCode = read("hero.ts"); + const order: Array<[VariantKey, string]> = [ + ["pastel-dark", "Pastel Dark"], + ["pastel-light", "Pastel Light"], + ["neon-dark", "Neon Dark"], + ["neon-light", "Neon Light"], + ]; + const cells = order + .map(([key, label]) => { + const codeHtml = highlighter.codeToHtml(heroCode, { lang: "typescript", theme: themes[key].name }); + return `
${label}
${windowHtml(codeHtml, themes[key], "theme.ts")}
`; + }) + .join(""); + await shoot(page(`
${cells}
`, 56), resolve(OUT, "hero.png")); + + // Icon. + await shoot(ICON_HTML, resolve(OUT, "icon.png"), ".icon"); + + await browser.close(); + console.log("done."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/assets/generate/package.json b/assets/generate/package.json new file mode 100644 index 0000000..0c8a1b0 --- /dev/null +++ b/assets/generate/package.json @@ -0,0 +1,14 @@ +{ + "name": "synthpunk-preview-generator", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Generates README marketing images (icon + syntax-highlight previews) from the Synthpunk VS Code themes.", + "scripts": { + "generate": "bun run generate.ts" + }, + "dependencies": { + "puppeteer-core": "^25.1.0", + "shiki": "^4.2.0" + } +} diff --git a/assets/generate/snippets/App.tsx b/assets/generate/snippets/App.tsx new file mode 100644 index 0000000..20da713 --- /dev/null +++ b/assets/generate/snippets/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from "react"; + +interface CounterProps { + label: string; + initial?: number; +} + +// A tiny counter that shifts hue as it climbs. +export function Counter({ label, initial = 0 }: CounterProps) { + const [count, setCount] = useState(initial); + const hue = (count * 24) % 360; + + useEffect(() => { + document.title = `${label}: ${count}`; + }, [label, count]); + + return ( + + ); +} diff --git a/assets/generate/snippets/Service.cs b/assets/generate/snippets/Service.cs new file mode 100644 index 0000000..3f6ecb1 --- /dev/null +++ b/assets/generate/snippets/Service.cs @@ -0,0 +1,22 @@ +using System.Linq; +using System.Threading.Tasks; + +namespace Synthpunk.Audio; + +/// Streams and ranks tracks by tempo. +public sealed class TrackService(IClock clock) +{ + public required string Channel { get; init; } + + public async Task CountUptempoAsync(Track[] tracks) + { + await Task.Delay(10); + return tracks.Count(t => t.Bpm >= 128); + } + + // Format a track for the now-playing line. + public string Tag(Track t) => + $"[{clock.Now:HH:mm}] {t.Title} — {t.Bpm} bpm"; +} + +public record Track(string Title, int Bpm); diff --git a/assets/generate/snippets/hero.ts b/assets/generate/snippets/hero.ts new file mode 100644 index 0000000..d536200 --- /dev/null +++ b/assets/generate/snippets/hero.ts @@ -0,0 +1,10 @@ +type Variant = "pastel" | "neon"; + +// Build a Synthpunk theme id from its parts. +function themeId(variant: Variant, dark: boolean): string { + const mode = dark ? "dark" : "light"; + return `synthpunk-${variant}-${mode}`; +} + +const current = themeId("neon", true); +console.log(`active → ${current}`); diff --git a/assets/generate/snippets/lib.rs b/assets/generate/snippets/lib.rs new file mode 100644 index 0000000..0ce45b1 --- /dev/null +++ b/assets/generate/snippets/lib.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; + +/// A neon palette keyed by semantic role. +#[derive(Debug, Clone, Default)] +pub struct Palette { + colors: HashMap, +} + +impl Palette { + pub fn new() -> Self { + Self::default() + } + + /// Insert a color and return `self` for chaining. + pub fn set(&mut self, role: &str, hex: u32) -> &mut Self { + self.colors.insert(role.to_string(), hex); + self + } + + pub fn get(&self, role: &str) -> Option { + self.colors.get(role).copied() + } +} + +fn main() { + let mut p = Palette::new(); + p.set("keyword", 0xFF5CA8).set("string", 0x40FF80); + println!("keyword = {:#08X}", p.get("keyword").unwrap_or(0)); +} diff --git a/assets/generate/snippets/pipeline.py b/assets/generate/snippets/pipeline.py new file mode 100644 index 0000000..a432ee9 --- /dev/null +++ b/assets/generate/snippets/pipeline.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from functools import cache + + +@dataclass(frozen=True) +class Track: + title: str + bpm: int = 120 + + @property + def is_uptempo(self) -> bool: + return self.bpm >= 128 + + +@cache +def neon_label(track: Track) -> str: + """Return a short, human label for a track.""" + mood = "drive" if track.is_uptempo else "chill" + return f"{track.title} ({track.bpm} bpm, {mood})" + + +if __name__ == "__main__": + deck = [Track("Outrun", 140), Track("Midnight", 96)] + for t in deck: + print(neon_label(t)) diff --git a/assets/generate/snippets/sample.md b/assets/generate/snippets/sample.md new file mode 100644 index 0000000..a600e22 --- /dev/null +++ b/assets/generate/snippets/sample.md @@ -0,0 +1,17 @@ +# Synthpunk + +High-energy **synthwave** color themes for your editor. + +> Four variants — *Pastel* and *Neon*, each in light and dark. + +## Install + +1. Open the **Extensions** panel +2. Search for `Synthpunk` +3. Pick a theme via `Color Theme` + +```ts +const theme = "Synthpunk Neon Dark"; +``` + +See the [documentation](https://example.com) for more. diff --git a/assets/hero.png b/assets/hero.png new file mode 100644 index 0000000..4f837e5 Binary files /dev/null and b/assets/hero.png differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..223a678 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/palette-preview-dark.png b/assets/palette-preview-dark.png deleted file mode 100644 index 57fbb9f..0000000 Binary files a/assets/palette-preview-dark.png and /dev/null differ diff --git a/assets/palette-preview-light.png b/assets/palette-preview-light.png deleted file mode 100644 index 7841b28..0000000 Binary files a/assets/palette-preview-light.png and /dev/null differ diff --git a/assets/palette-preview-neon-dark.png b/assets/palette-preview-neon-dark.png deleted file mode 100644 index 62f07d1..0000000 Binary files a/assets/palette-preview-neon-dark.png and /dev/null differ diff --git a/assets/palette-preview-neon-light.png b/assets/palette-preview-neon-light.png deleted file mode 100644 index 4ce5a11..0000000 Binary files a/assets/palette-preview-neon-light.png and /dev/null differ diff --git a/assets/sample-csharp.png b/assets/sample-csharp.png new file mode 100644 index 0000000..7ab699e Binary files /dev/null and b/assets/sample-csharp.png differ diff --git a/assets/sample-markdown.png b/assets/sample-markdown.png new file mode 100644 index 0000000..58121cd Binary files /dev/null and b/assets/sample-markdown.png differ diff --git a/assets/sample-python.png b/assets/sample-python.png new file mode 100644 index 0000000..d323a16 Binary files /dev/null and b/assets/sample-python.png differ diff --git a/assets/sample-react.png b/assets/sample-react.png new file mode 100644 index 0000000..f7b1c7c Binary files /dev/null and b/assets/sample-react.png differ diff --git a/assets/sample-rust.png b/assets/sample-rust.png new file mode 100644 index 0000000..1175f40 Binary files /dev/null and b/assets/sample-rust.png differ diff --git a/biome.json b/biome.json index f6e1f23..32765c9 100644 --- a/biome.json +++ b/biome.json @@ -6,7 +6,8 @@ "useIgnoreFile": true }, "files": { - "ignoreUnknown": false + "ignoreUnknown": false, + "includes": ["**", "!assets/generate"] }, "formatter": { "enabled": true, diff --git a/tsconfig.json b/tsconfig.json index 21eef14..f413a23 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "types": ["bun"] }, "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "assets/generate"] }