diff --git a/astro.config.mjs b/astro.config.mjs
index 1595b47..940ceb1 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -7,5 +7,16 @@ export default defineConfig({
base: '/',
vite: {
plugins: [tailwindcss()],
+ worker: {
+ format: 'es',
+ },
+ assetsInclude: ['**/*.wasm'],
+ optimizeDeps: {
+ exclude: ['@postcss-go/core'],
+ },
+ build: {
+ // Keep the classic WASM worker as a file. A data: URL cannot importScripts wasm_exec.js.
+ assetsInlineLimit: 0,
+ },
},
});
diff --git a/package.json b/package.json
index af2605e..c8c16b5 100644
--- a/package.json
+++ b/package.json
@@ -25,8 +25,13 @@
"*.{json,md,yml,yaml,css}": "prettier --write"
},
"dependencies": {
+ "@postcss-go/core": "^0.0.1",
"@tailwindcss/vite": "^4.3.3",
- "astro": "^7.1.3"
+ "astro": "^7.1.3",
+ "autoprefixer": "^10.5.4",
+ "monaco-editor": "^0.56.0",
+ "postcss": "^8.5.26",
+ "postcss-nested": "^8.0.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 43855f7..76fe917 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,12 +8,27 @@ importers:
.:
dependencies:
+ '@postcss-go/core':
+ specifier: ^0.0.1
+ version: 0.0.1
'@tailwindcss/vite':
specifier: ^4.3.3
version: 4.3.3(vite@8.2.1(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))
astro:
specifier: ^7.1.3
version: 7.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(jiti@2.7.0)(yaml@2.9.0)
+ autoprefixer:
+ specifier: ^10.5.4
+ version: 10.5.4(postcss@8.5.26)
+ monaco-editor:
+ specifier: ^0.56.0
+ version: 0.56.0
+ postcss:
+ specifier: ^8.5.26
+ version: 8.5.26
+ postcss-nested:
+ specifier: ^8.0.1
+ version: 8.0.1(postcss@8.5.26)
devDependencies:
'@eslint/js':
specifier: ^10.0.1
@@ -666,6 +681,41 @@ packages:
'@oxc-project/types@0.144.0':
resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==}
+ '@postcss-go/core@0.0.1':
+ resolution: {integrity: sha512-ruprOBNp8yo9DHaZmf1d7+qMKr6Ofp64JPliWawLGZGn0KQrSs8r5Ovb4NqWm5zc97UayAKsppGDWQ/C/RDp5g==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ '@postcss-go/native-darwin-arm64@0.0.1':
+ resolution: {integrity: sha512-OQALbux4tEr1n7dRPKumQ4QfBNmK/kEWUbRLRJaRZXy9x1ezBCJ0Yj8ZACzE5KG0pFgKKmNzsBPDUjC49lgKmw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@postcss-go/native-darwin-x64@0.0.1':
+ resolution: {integrity: sha512-QMp0FvJ48FFPLXaou8Ngp5h3HrINN+VRBHFR9quFOC+tKEreSR7vylsXRq1Sg1oDR040bZv7mA9+CWp3i3nhlQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@postcss-go/native-linux-arm64-gnu@0.0.1':
+ resolution: {integrity: sha512-QgaLCSVcBNe/Z/KA6X94vCsvApF+d9v8yNJFcINy1uqZ3jcN6XxKL9GbVhfYSeGjBzfx1rMTsv51l8VHqrxuvg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@postcss-go/native-linux-x64-gnu@0.0.1':
+ resolution: {integrity: sha512-aadqh6WjOgzyy+UA7g/H8NbImS168r/Xr11TAuzQcgMb3OO72UzN/HdkMt4HcsrbQKMOsgI8YKjlcORN3wdS0g==}
+ cpu: [x64]
+ os: [linux]
+
+ '@postcss-go/native-win32-arm64-msvc@0.0.1':
+ resolution: {integrity: sha512-BjGwIy8BgEKC6Lhp0/IVPOWa/r6ccnM6hTaD1dJeA6tLx7xm7W/gDkEOaKohFqCMXF/lNAW+B5uXPgg5W/dQkQ==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@postcss-go/native-win32-x64-msvc@0.0.1':
+ resolution: {integrity: sha512-sXTETqxi+6YRhaimZtPGvR3sDYLdAB9Mpj+SSzHrBPu8e15zExfWjZBHUAjzHr5j8YTLnw+UndgyP/fIB4Nclg==}
+ cpu: [x64]
+ os: [win32]
+
'@rolldown/binding-android-arm64@1.2.4':
resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -898,6 +948,9 @@ packages:
'@types/nlcst@2.0.3':
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
+ '@types/trusted-types@2.0.7':
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@@ -1009,6 +1062,13 @@ packages:
'@astrojs/markdown-remark':
optional: true
+ autoprefixer@10.5.4:
+ resolution: {integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
@@ -1020,6 +1080,15 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
+ baseline-browser-mapping@2.11.15:
+ resolution: {integrity: sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
@@ -1027,6 +1096,18 @@ packages:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
engines: {node: 20 || >=22}
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.28.8:
+ resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ caniuse-lite@1.0.30001809:
+ resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==}
+
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -1040,6 +1121,10 @@ packages:
character-entities-legacy@3.0.0:
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+ chokidar@3.6.0:
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+ engines: {node: '>= 8.10.0'}
+
chokidar@5.0.0:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'}
@@ -1127,6 +1212,10 @@ packages:
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
+ dependency-graph@1.0.0:
+ resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==}
+ engines: {node: '>=4'}
+
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@@ -1158,6 +1247,9 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
+ dompurify@3.4.8:
+ resolution: {integrity: sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==}
+
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -1165,6 +1257,9 @@ packages:
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
engines: {node: '>=4'}
+ electron-to-chromium@1.5.411:
+ resolution: {integrity: sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==}
+
enhanced-resolve@5.24.5:
resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==}
engines: {node: '>=10.13.0'}
@@ -1185,6 +1280,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -1290,6 +1389,10 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
find-process@2.1.1:
resolution: {integrity: sha512-SrQDx3QhlmHM90iqn9rdjCQcw/T+WlpOkHFsjoRgB+zTpDfltNA1VSNYeYELwhUTJy12UFxqjWhmhOrJc+o4sA==}
hasBin: true
@@ -1316,6 +1419,9 @@ packages:
resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==}
engines: {node: '>=20'}
+ fraction.js@5.3.4:
+ resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1328,6 +1434,10 @@ packages:
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
@@ -1393,6 +1503,10 @@ packages:
iron-webcrypto@1.2.1:
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
+ is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+
is-docker@4.0.0:
resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==}
engines: {node: '>=20'}
@@ -1406,6 +1520,10 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
@@ -1606,6 +1724,11 @@ packages:
magicast@0.5.4:
resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==}
+ marked@14.0.0:
+ resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==}
+ engines: {node: '>= 18'}
+ hasBin: true
+
mdast-util-to-hast@13.2.1:
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
@@ -1634,6 +1757,9 @@ packages:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22}
+ monaco-editor@0.56.0:
+ resolution: {integrity: sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==}
+
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -1662,6 +1788,10 @@ packages:
node-mock-http@1.0.5:
resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==}
+ node-releases@2.0.53:
+ resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==}
+ engines: {node: '>=18'}
+
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -1737,10 +1867,19 @@ packages:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
+ postcss-nested@8.0.1:
+ resolution: {integrity: sha512-PvSIwDVh1NTw939iu4FBK+oeZfSm0cbErFAjXCTPiStammzobBNE2SiQNezy8Xp+Oud2uLXaLpTauv3UdxWjbQ==}
+ engines: {node: ^22.0.0 || ^24.0.0 || >= 26.0.0}
+ peerDependencies:
+ postcss: ^8.2.14
+
postcss-selector-parser@7.1.5:
resolution: {integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==}
engines: {node: '>=4'}
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
postcss@8.5.26:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -1758,6 +1897,10 @@ packages:
engines: {node: '>=14'}
hasBin: true
+ pretty-hrtime@1.0.3:
+ resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==}
+ engines: {node: '>= 0.8'}
+
prismjs@1.30.0:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'}
@@ -1776,6 +1919,13 @@ packages:
radix3@1.1.2:
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
+ read-cache@1.0.2:
+ resolution: {integrity: sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==}
+
+ readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+
readdirp@5.1.1:
resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==}
engines: {node: '>= 20.19.0'}
@@ -1842,6 +1992,10 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+ slash@5.1.0:
+ resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
+ engines: {node: '>=14.16'}
+
smol-toml@1.8.0:
resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==}
engines: {node: '>= 18'}
@@ -1894,6 +2048,10 @@ packages:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@@ -2021,6 +2179,12 @@ packages:
uploadthing:
optional: true
+ update-browserslist-db@1.3.1:
+ resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -2613,6 +2777,42 @@ snapshots:
'@oxc-project/types@0.144.0': {}
+ '@postcss-go/core@0.0.1':
+ dependencies:
+ chokidar: 3.6.0
+ dependency-graph: 1.0.0
+ picocolors: 1.1.1
+ pretty-hrtime: 1.0.3
+ read-cache: 1.0.2
+ slash: 5.1.0
+ source-map-js: 1.2.1
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@postcss-go/native-darwin-arm64': 0.0.1
+ '@postcss-go/native-darwin-x64': 0.0.1
+ '@postcss-go/native-linux-arm64-gnu': 0.0.1
+ '@postcss-go/native-linux-x64-gnu': 0.0.1
+ '@postcss-go/native-win32-arm64-msvc': 0.0.1
+ '@postcss-go/native-win32-x64-msvc': 0.0.1
+
+ '@postcss-go/native-darwin-arm64@0.0.1':
+ optional: true
+
+ '@postcss-go/native-darwin-x64@0.0.1':
+ optional: true
+
+ '@postcss-go/native-linux-arm64-gnu@0.0.1':
+ optional: true
+
+ '@postcss-go/native-linux-x64-gnu@0.0.1':
+ optional: true
+
+ '@postcss-go/native-win32-arm64-msvc@0.0.1':
+ optional: true
+
+ '@postcss-go/native-win32-x64-msvc@0.0.1':
+ optional: true
+
'@rolldown/binding-android-arm64@1.2.4':
optional: true
@@ -2792,6 +2992,9 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ '@types/trusted-types@2.0.7':
+ optional: true
+
'@types/unist@3.0.3': {}
'@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)':
@@ -3024,18 +3227,45 @@ snapshots:
- uploadthing
- yaml
+ autoprefixer@10.5.4(postcss@8.5.26):
+ dependencies:
+ browserslist: 4.28.8
+ caniuse-lite: 1.0.30001809
+ fraction.js: 5.3.4
+ picocolors: 1.1.1
+ postcss: 8.5.26
+ postcss-value-parser: 4.2.0
+
axobject-query@4.1.0: {}
bail@2.0.2: {}
balanced-match@4.0.4: {}
+ baseline-browser-mapping@2.11.15: {}
+
+ binary-extensions@2.3.0: {}
+
boolbase@1.0.0: {}
brace-expansion@5.0.9:
dependencies:
balanced-match: 4.0.4
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.28.8:
+ dependencies:
+ baseline-browser-mapping: 2.11.15
+ caniuse-lite: 1.0.30001809
+ electron-to-chromium: 1.5.411
+ node-releases: 2.0.53
+ update-browserslist-db: 1.3.1(browserslist@4.28.8)
+
+ caniuse-lite@1.0.30001809: {}
+
ccount@2.0.1: {}
chalk@4.1.2:
@@ -3047,6 +3277,18 @@ snapshots:
character-entities-legacy@3.0.0: {}
+ chokidar@3.6.0:
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.3
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
chokidar@5.0.0:
dependencies:
readdirp: 5.1.1
@@ -3117,6 +3359,8 @@ snapshots:
defu@6.1.7: {}
+ dependency-graph@1.0.0: {}
+
dequal@2.0.3: {}
destr@2.0.5: {}
@@ -3143,6 +3387,10 @@ snapshots:
dependencies:
domelementtype: 2.3.0
+ dompurify@3.4.8:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
@@ -3151,6 +3399,8 @@ snapshots:
dset@3.1.4: {}
+ electron-to-chromium@1.5.411: {}
+
enhanced-resolve@5.24.5:
dependencies:
graceful-fs: 4.2.11
@@ -3191,6 +3441,8 @@ snapshots:
'@esbuild/win32-ia32': 0.28.2
'@esbuild/win32-x64': 0.28.2
+ escalade@3.2.0: {}
+
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@10.8.1(jiti@2.7.0)):
@@ -3310,6 +3562,10 @@ snapshots:
dependencies:
flat-cache: 4.0.1
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
find-process@2.1.1:
dependencies:
chalk: 4.1.2
@@ -3338,6 +3594,8 @@ snapshots:
dependencies:
tiny-inflate: 1.0.3
+ fraction.js@5.3.4: {}
+
fsevents@2.3.3:
optional: true
@@ -3347,6 +3605,10 @@ snapshots:
github-slugger@2.0.0: {}
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
@@ -3435,6 +3697,10 @@ snapshots:
iron-webcrypto@1.2.1: {}
+ is-binary-path@2.1.0:
+ dependencies:
+ binary-extensions: 2.3.0
+
is-docker@4.0.0: {}
is-extglob@2.1.1: {}
@@ -3443,6 +3709,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
+ is-number@7.0.0: {}
+
is-plain-obj@4.1.0: {}
isexe@2.0.0: {}
@@ -3598,6 +3866,8 @@ snapshots:
'@babel/types': 7.29.8
source-map-js: 1.2.1
+ marked@14.0.0: {}
+
mdast-util-to-hast@13.2.1:
dependencies:
'@types/hast': 3.0.5
@@ -3635,6 +3905,11 @@ snapshots:
dependencies:
brace-expansion: 5.0.9
+ monaco-editor@0.56.0:
+ dependencies:
+ dompurify: 3.4.8
+ marked: 14.0.0
+
mrmime@2.0.1: {}
ms@2.1.3: {}
@@ -3653,6 +3928,8 @@ snapshots:
node-mock-http@1.0.5: {}
+ node-releases@2.0.53: {}
+
normalize-path@3.0.0: {}
nth-check@2.1.1:
@@ -3723,11 +4000,18 @@ snapshots:
picomatch@4.0.5: {}
+ postcss-nested@8.0.1(postcss@8.5.26):
+ dependencies:
+ postcss: 8.5.26
+ postcss-selector-parser: 7.1.5
+
postcss-selector-parser@7.1.5:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
+ postcss-value-parser@4.2.0: {}
+
postcss@8.5.26:
dependencies:
nanoid: 3.3.18
@@ -3744,6 +4028,8 @@ snapshots:
prettier@3.9.6: {}
+ pretty-hrtime@1.0.3: {}
+
prismjs@1.30.0: {}
process-ancestry@0.1.0: {}
@@ -3754,6 +4040,12 @@ snapshots:
radix3@1.1.2: {}
+ read-cache@1.0.2: {}
+
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.2
+
readdirp@5.1.1: {}
regex-recursion@6.0.2:
@@ -3873,6 +4165,8 @@ snapshots:
sisteransi@1.0.5: {}
+ slash@5.1.0: {}
+
smol-toml@1.8.0: {}
source-map-js@1.2.1: {}
@@ -3919,6 +4213,10 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.5)
picomatch: 4.0.5
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
trim-lines@3.0.1: {}
trough@2.2.0: {}
@@ -4005,6 +4303,12 @@ snapshots:
ofetch: 1.5.1
ufo: 1.6.4
+ update-browserslist-db@1.3.1(browserslist@4.28.8):
+ dependencies:
+ browserslist: 4.28.8
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
diff --git a/src/components/CodeSample.astro b/src/components/CodeSample.astro
new file mode 100644
index 0000000..641bf09
--- /dev/null
+++ b/src/components/CodeSample.astro
@@ -0,0 +1,26 @@
+---
+interface Props {
+ filename: string;
+ code: string;
+}
+
+const { filename, code } = Astro.props;
+---
+
+
+
+ {filename}
+ Copy
+
+
{code}
+
diff --git a/src/components/CssPlayground.astro b/src/components/CssPlayground.astro
new file mode 100644
index 0000000..c6ff51a
--- /dev/null
+++ b/src/components/CssPlayground.astro
@@ -0,0 +1,62 @@
+---
+interface Props {
+ id: string;
+ nested?: boolean;
+ autoprefixer?: boolean;
+}
+
+const { id, nested = true, autoprefixer = true } = Astro.props;
+---
+
+
+
+
diff --git a/src/components/PlaygroundCta.astro b/src/components/PlaygroundCta.astro
new file mode 100644
index 0000000..b57a15f
--- /dev/null
+++ b/src/components/PlaygroundCta.astro
@@ -0,0 +1,12 @@
+---
+interface Props {
+ id: string;
+}
+
+const { id } = Astro.props;
+const href = `${import.meta.env.BASE_URL}playground/${id}/`;
+---
+
+
+ Open live playground →
+
diff --git a/src/components/SiteNav.astro b/src/components/SiteNav.astro
index ab4361e..aee4978 100644
--- a/src/components/SiteNav.astro
+++ b/src/components/SiteNav.astro
@@ -3,9 +3,11 @@ const base = import.meta.env.BASE_URL;
const pathname = Astro.url.pathname;
const homeHref = base;
const architectureHref = `${base}architecture/`;
-const progressHref = `${base}progress/`;
+const examplesHref = `${base}examples/default/`;
+const playgroundHref = `${base}playground/default/`;
const guideHref = `${base}guide/get-started/`;
-const isProgress = pathname === `${base}progress/` || pathname === `${base}progress`;
+const isExamples = pathname === `${base}examples/` || pathname.startsWith(`${base}examples`);
+const isPlayground = pathname === `${base}playground/` || pathname.startsWith(`${base}playground`);
const isGuide = pathname.startsWith(`${base}guide`);
const isArchitecture = pathname === `${base}architecture/` || pathname === `${base}architecture`;
const navLink = 'rounded-full border border-transparent px-3 py-2 transition hover:text-acid';
@@ -68,9 +70,14 @@ const githubStars = await (async () => {
aria-current={isGuide ? 'page' : undefined}>Guide
Progress Examples
+ Playground
+
+declare module '*?url' {
+ const value: string;
+ export default value;
+}
+
+declare module '*?worker' {
+ const WorkerFactory: {
+ new (): Worker;
+ };
+ export default WorkerFactory;
+}
diff --git a/src/layouts/ExamplesLayout.astro b/src/layouts/ExamplesLayout.astro
new file mode 100644
index 0000000..8bb1e0e
--- /dev/null
+++ b/src/layouts/ExamplesLayout.astro
@@ -0,0 +1,139 @@
+---
+import '../styles/global.css';
+import PageHead from '../components/PageHead.astro';
+import SiteNav from '../components/SiteNav.astro';
+import { exampleGroups } from '../lib/examples';
+
+interface Props {
+ title?: string;
+ section?: string;
+ frontmatter?: {
+ title?: string;
+ section?: string;
+ };
+}
+
+const { frontmatter, title: propTitle, section: propSection } = Astro.props;
+const title = frontmatter?.title ?? propTitle ?? '';
+const section = frontmatter?.section ?? propSection ?? '';
+const base = import.meta.env.BASE_URL;
+const groups = exampleGroups(base);
+---
+
+
+
+
+
+
+
+
+
+
+
+
+ Examples
+
+
+ {
+ groups.map((group) => (
+
+ ))
+ }
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/PlaygroundLayout.astro b/src/layouts/PlaygroundLayout.astro
new file mode 100644
index 0000000..25850af
--- /dev/null
+++ b/src/layouts/PlaygroundLayout.astro
@@ -0,0 +1,64 @@
+---
+import '../styles/global.css';
+import PageHead from '../components/PageHead.astro';
+import SiteNav from '../components/SiteNav.astro';
+import { playgroundGroups } from '../lib/playgrounds';
+
+interface Props {
+ title?: string;
+ description?: string;
+ section?: string;
+}
+
+const { title = '', description = title, section = '' } = Astro.props;
+const base = import.meta.env.BASE_URL;
+const groups = playgroundGroups(base);
+---
+
+
+
+
+
+
+
+
+
+
+
+
+ Playground
+
+
+ {
+ groups.map((group) => (
+
+ ))
+ }
+
+
+
+
+
+
+
+
diff --git a/src/lib/examples.ts b/src/lib/examples.ts
new file mode 100644
index 0000000..7db1043
--- /dev/null
+++ b/src/lib/examples.ts
@@ -0,0 +1,23 @@
+export interface ExampleLink {
+ label: string;
+ href: string;
+ key: string;
+}
+
+export interface ExampleGroup {
+ label: string;
+ items: ExampleLink[];
+}
+
+export function exampleGroups(base: string): ExampleGroup[] {
+ return [
+ {
+ label: 'Usage',
+ items: [{ label: 'Default', href: `${base}examples/default/`, key: 'default' }],
+ },
+ {
+ label: 'Frameworks',
+ items: [{ label: 'webpack', href: `${base}examples/webpack/`, key: 'webpack' }],
+ },
+ ];
+}
diff --git a/src/lib/monaco-env.ts b/src/lib/monaco-env.ts
new file mode 100644
index 0000000..bd154b5
--- /dev/null
+++ b/src/lib/monaco-env.ts
@@ -0,0 +1,17 @@
+import EditorWorker from 'monaco-editor/editor/editor.worker?worker';
+import CssWorker from 'monaco-editor/language/css/css.worker?worker';
+
+const global = globalThis as typeof globalThis & {
+ MonacoEnvironment?: {
+ getWorker: (id: string, label: string) => Worker;
+ };
+};
+
+global.MonacoEnvironment = {
+ getWorker(_id, label) {
+ if (label === 'css' || label === 'scss' || label === 'less') {
+ return new CssWorker();
+ }
+ return new EditorWorker();
+ },
+};
diff --git a/src/lib/playground-app.ts b/src/lib/playground-app.ts
new file mode 100644
index 0000000..683159c
--- /dev/null
+++ b/src/lib/playground-app.ts
@@ -0,0 +1,226 @@
+import './monaco-env';
+import * as monaco from 'monaco-editor/editor/editor.api';
+import 'monaco-editor/language/css/monaco.contribution';
+
+import { processCss } from './playground-process';
+import { playgroundPreset, type PluginFlags } from './playgrounds';
+
+const DARK: monaco.editor.IStandaloneThemeData = {
+ base: 'vs-dark',
+ inherit: true,
+ rules: [],
+ colors: {
+ 'editor.background': '#0a0b0d',
+ 'editor.foreground': '#f5f5f0',
+ 'editorLineNumber.foreground': '#56606b',
+ 'editorCursor.foreground': '#c8ff3d',
+ focusBorder: '#c8ff3d',
+ },
+};
+
+const LIGHT: monaco.editor.IStandaloneThemeData = {
+ base: 'vs',
+ inherit: true,
+ rules: [],
+ colors: {
+ 'editor.background': '#f5f5f0',
+ 'editor.foreground': '#0a0b0d',
+ 'editorCursor.foreground': '#527800',
+ },
+};
+
+function currentTheme() {
+ return document.documentElement.dataset.theme === 'light'
+ ? 'postcss-go-light'
+ : 'postcss-go-dark';
+}
+
+function decodeHash() {
+ const raw = location.hash.replace(/^#/, '');
+ if (!raw) return '';
+ const css = new URLSearchParams(raw).get('css');
+ if (!css) return '';
+ try {
+ return new TextDecoder().decode(
+ Uint8Array.from(atob(css.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0)),
+ );
+ } catch {
+ return '';
+ }
+}
+
+function encodeCss(css: string) {
+ const bytes = new TextEncoder().encode(css);
+ let binary = '';
+ bytes.forEach((byte) => {
+ binary += String.fromCharCode(byte);
+ });
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+}
+
+function pluginFlags(root: HTMLElement): PluginFlags {
+ return {
+ nested: Boolean(root.querySelector('[data-plugin="nested"]')?.checked),
+ autoprefixer: Boolean(
+ root.querySelector('[data-plugin="autoprefixer"]')?.checked,
+ ),
+ };
+}
+
+function previewDocument(css: string, body: string) {
+ const light = document.documentElement.dataset.theme === 'light';
+ const background = light ? '#f5f5f0' : '#0a0b0d';
+ const color = light ? '#0a0b0d' : '#f5f5f0';
+ return `${body}`;
+}
+
+export function bootPlayground(root: HTMLElement) {
+ const id = root.dataset.presetId ?? 'default';
+ const preset = playgroundPreset(id);
+ const inputHost = root.querySelector('[data-editor="input"]');
+ const outputHost = root.querySelector('[data-editor="output"]');
+ const preview = root.querySelector('[data-preview]');
+ const status = root.querySelector('[data-status]');
+ const errorBox = root.querySelector('[data-error]');
+ const share = root.querySelector('[data-share]');
+ const reset = root.querySelector('[data-reset]');
+ const copy = root.querySelector('[data-copy]');
+ if (!inputHost || !outputHost) return;
+
+ monaco.editor.defineTheme('postcss-go-dark', DARK);
+ monaco.editor.defineTheme('postcss-go-light', LIGHT);
+
+ const options: monaco.editor.IStandaloneEditorConstructionOptions = {
+ language: 'css',
+ theme: currentTheme(),
+ fontSize: 13,
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
+ minimap: { enabled: false },
+ scrollBeyondLastLine: false,
+ automaticLayout: true,
+ padding: { top: 12, bottom: 12 },
+ wordWrap: 'on',
+ tabSize: 2,
+ renderLineHighlight: 'line',
+ };
+
+ const input = monaco.editor.create(inputHost, {
+ ...options,
+ value: decodeHash() || preset.input,
+ });
+ const output = monaco.editor.create(outputHost, {
+ ...options,
+ value: '',
+ readOnly: true,
+ domReadOnly: true,
+ });
+
+ let timer = 0;
+ let latestCss = '';
+
+ const observer = new MutationObserver(() => {
+ const theme = currentTheme();
+ monaco.editor.setTheme(theme);
+ input.updateOptions({ theme });
+ output.updateOptions({ theme });
+ if (latestCss && preview) {
+ preview.srcdoc = previewDocument(latestCss, preset.previewHtml);
+ }
+ });
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
+
+ async function run() {
+ const css = input.getValue();
+ status && (status.textContent = 'Running…');
+ errorBox && (errorBox.hidden = true);
+ monaco.editor.setModelMarkers(input.getModel()!, 'postcss-go', []);
+ try {
+ const result = await processCss(css, pluginFlags(root));
+ latestCss = result.css;
+ output.setValue(result.css);
+ if (preview) {
+ preview.srcdoc = previewDocument(result.css, preset.previewHtml);
+ }
+ if (status) {
+ status.textContent = `${result.backend} · ${result.ms.toFixed(1)}ms`;
+ }
+ } catch (error) {
+ const err = error as { message?: string; reason?: string; line?: number; column?: number };
+ const message = err.reason || err.message || String(error);
+ latestCss = '';
+ output.setValue('');
+ if (errorBox) {
+ errorBox.hidden = false;
+ errorBox.textContent = message;
+ }
+ if (status) status.textContent = 'error';
+ if (err.line && input.getModel()) {
+ monaco.editor.setModelMarkers(input.getModel()!, 'postcss-go', [
+ {
+ startLineNumber: err.line,
+ startColumn: err.column || 1,
+ endLineNumber: err.line,
+ endColumn: (err.column || 1) + 1,
+ message,
+ severity: monaco.MarkerSeverity.Error,
+ },
+ ]);
+ }
+ }
+ }
+
+ function schedule() {
+ window.clearTimeout(timer);
+ timer = window.setTimeout(() => {
+ void run();
+ }, 200);
+ }
+
+ input.onDidChangeModelContent(schedule);
+ root.querySelectorAll('[data-plugin]').forEach((box) => {
+ box.addEventListener('change', () => {
+ void run();
+ });
+ });
+
+ root.querySelectorAll('[data-tab]').forEach((button) => {
+ button.addEventListener('click', () => {
+ const tab = button.dataset.tab;
+ root.querySelectorAll('[data-tab]').forEach((item) => {
+ item.setAttribute('aria-selected', String(item === button));
+ });
+ root.querySelectorAll('[data-panel]').forEach((panel) => {
+ panel.hidden = panel.dataset.panel !== tab;
+ });
+ });
+ });
+
+ share?.addEventListener('click', async () => {
+ const url = new URL(location.href);
+ url.hash = new URLSearchParams({ css: encodeCss(input.getValue()) }).toString();
+ await navigator.clipboard.writeText(url.toString());
+ share.textContent = 'Copied link';
+ window.setTimeout(() => {
+ share.textContent = 'Share';
+ }, 1400);
+ });
+
+ reset?.addEventListener('click', () => {
+ history.replaceState(null, '', location.pathname + location.search);
+ input.setValue(preset.input);
+ void run();
+ });
+
+ copy?.addEventListener('click', async () => {
+ await navigator.clipboard.writeText(latestCss);
+ copy.textContent = 'Copied';
+ window.setTimeout(() => {
+ copy.textContent = 'Copy output';
+ }, 1400);
+ });
+
+ void run();
+}
diff --git a/src/lib/playground-process.ts b/src/lib/playground-process.ts
new file mode 100644
index 0000000..db2aff5
--- /dev/null
+++ b/src/lib/playground-process.ts
@@ -0,0 +1,41 @@
+import { createBrowserProcessor, type BrowserProcessor } from '@postcss-go/core/wasm';
+import workerUrl from '@postcss-go/core/wasm/worker?url';
+import wasmUrl from '@postcss-go/core/wasm/postcss-go.wasm?url';
+import wasmExecUrl from '@postcss-go/core/wasm/wasm_exec.js?url';
+import autoprefixer from 'autoprefixer';
+import nested from 'postcss-nested';
+
+import type { PluginFlags } from './playgrounds';
+
+type BrowserPlugins = NonNullable[0]>;
+
+let processor: BrowserProcessor | undefined;
+let processorKey = '';
+
+function pluginList(flags: PluginFlags): BrowserPlugins {
+ const plugins: BrowserPlugins = [];
+ if (flags.nested) plugins.push(nested());
+ if (flags.autoprefixer) plugins.push(autoprefixer());
+ return plugins;
+}
+
+export async function processCss(css: string, flags: PluginFlags) {
+ const key = `${flags.nested}:${flags.autoprefixer}`;
+ if (!processor || processorKey !== key) {
+ await processor?.close();
+ processor = createBrowserProcessor(pluginList(flags), {
+ workerUrl,
+ wasmUrl,
+ wasmExecUrl,
+ });
+ processorKey = key;
+ }
+
+ const started = performance.now();
+ const result = await processor.process(css, { from: 'input.css', map: false });
+ return {
+ css: result.css,
+ backend: result.backend ?? 'wasm-worker',
+ ms: performance.now() - started,
+ };
+}
diff --git a/src/lib/playgrounds.ts b/src/lib/playgrounds.ts
new file mode 100644
index 0000000..58ed996
--- /dev/null
+++ b/src/lib/playgrounds.ts
@@ -0,0 +1,276 @@
+export interface PlaygroundLink {
+ id: string;
+ label: string;
+}
+
+export interface PluginFlags {
+ nested: boolean;
+ autoprefixer: boolean;
+}
+
+export interface PlaygroundPreset {
+ id: string;
+ title: string;
+ description: string;
+ exampleId: string;
+ input: string;
+ previewHtml: string;
+ plugins: PluginFlags;
+}
+
+export const PLAYGROUND_LINKS: PlaygroundLink[] = [
+ { id: 'default', label: 'Default' },
+ { id: 'tailwindcss', label: 'Tailwind CSS' },
+ { id: 'vite', label: 'Vite' },
+ { id: 'nextjs', label: 'Next.js' },
+ { id: 'nuxt', label: 'Nuxt' },
+ { id: 'vue', label: 'Vue' },
+ { id: 'astro', label: 'Astro' },
+ { id: 'sveltekit', label: 'SvelteKit' },
+ { id: 'webpack', label: 'webpack' },
+];
+
+export function playgroundGroups(base: string) {
+ return [
+ {
+ label: 'Frameworks',
+ items: PLAYGROUND_LINKS.map((item) => ({
+ label: item.label,
+ href: `${base}playground/${item.id}/`,
+ key: item.id,
+ })),
+ },
+ ];
+}
+
+export function playgroundPreset(id: string): PlaygroundPreset {
+ const preset = PLAYGROUND_PRESETS[id] ?? PLAYGROUND_PRESETS.default;
+ if (!preset) {
+ throw new Error(`Unknown playground: ${id}`);
+ }
+ return preset;
+}
+
+const demoInput = `.hero {
+ display: flex;
+ gap: 1rem;
+ user-select: none;
+ backdrop-filter: blur(12px);
+
+ & h1 {
+ margin: 0;
+ letter-spacing: -0.06em;
+ }
+
+ & .cta {
+ appearance: none;
+ border-radius: 999px;
+ }
+}
+`;
+
+export const PLAYGROUND_PRESETS: Record = {
+ default: {
+ id: 'default',
+ title: 'Default',
+ description: 'Parse, run plugins, stringify — the same pipeline as the CLI.',
+ exampleId: 'default',
+ plugins: { nested: true, autoprefixer: true },
+ input: demoInput,
+ previewHtml: `PostCSS, rebuilt. Get started `,
+ },
+ tailwindcss: {
+ id: 'tailwindcss',
+ title: 'Tailwind CSS',
+ description:
+ 'Sample CSS through nested + Autoprefixer. Tailwind itself is not compiled here — it needs a project file tree.',
+ exampleId: 'tailwindcss',
+ plugins: { nested: true, autoprefixer: true },
+ input: `.page {
+ display: grid;
+ min-height: 100vh;
+ place-items: center;
+ background: #0a0b0d;
+ color: #f5f5f0;
+
+ & .mark {
+ color: #c8ff3d;
+ }
+
+ & .btn {
+ appearance: none;
+ user-select: none;
+ border-radius: 999px;
+ background: #c8ff3d;
+ color: #0a0b0d;
+ }
+}
+`,
+ previewHtml: `Tailwind-shaped CSS
Utility first. Try a class `,
+ },
+ vite: {
+ id: 'vite',
+ title: 'Vite',
+ description: 'Imported CSS as Vite would send it through PostCSS.',
+ exampleId: 'vite',
+ plugins: { nested: true, autoprefixer: true },
+ input: `#app {
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+
+ & .card {
+ display: flex;
+ gap: 0.75rem;
+ backdrop-filter: blur(8px);
+ user-select: none;
+ }
+}
+`,
+ previewHtml: ``,
+ },
+ nextjs: {
+ id: 'nextjs',
+ title: 'Next.js',
+ description: 'Global stylesheet shape used from app/layout.',
+ exampleId: 'nextjs',
+ plugins: { nested: true, autoprefixer: true },
+ input: `:root {
+ --fg: #f5f5f0;
+ --bg: #0a0b0d;
+}
+
+body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--fg);
+}
+
+.hero {
+ display: grid;
+ min-height: 100vh;
+ place-items: center;
+
+ & p {
+ user-select: none;
+ }
+}
+`,
+ previewHtml: ` `,
+ },
+ nuxt: {
+ id: 'nuxt',
+ title: 'Nuxt',
+ description: 'Asset CSS with nested rules, as Nuxt PostCSS would see it.',
+ exampleId: 'nuxt',
+ plugins: { nested: true, autoprefixer: true },
+ input: `body {
+ margin: 0;
+ font-family: system-ui, sans-serif;
+}
+
+.hero {
+ display: flex;
+ min-height: 100vh;
+ align-items: center;
+ justify-content: center;
+
+ & h1 {
+ letter-spacing: -0.05em;
+ }
+}
+`,
+ previewHtml: `Nuxt + PostCSS `,
+ },
+ vue: {
+ id: 'vue',
+ title: 'Vue',
+ description: 'Component-style nested CSS, including scoped-looking selectors.',
+ exampleId: 'vue',
+ plugins: { nested: true, autoprefixer: true },
+ input: `.hero {
+ display: grid;
+ min-height: 100vh;
+ place-items: center;
+
+ & .title {
+ user-select: none;
+ letter-spacing: 0.02em;
+ }
+}
+`,
+ previewHtml: `Vue scoped styles
`,
+ },
+ astro: {
+ id: 'astro',
+ title: 'Astro',
+ description: 'Global CSS plus nested blocks, matching Astro style pipelines.',
+ exampleId: 'astro',
+ plugins: { nested: true, autoprefixer: true },
+ input: `:root {
+ --bg: #0a0b0d;
+}
+
+html {
+ background: var(--bg);
+}
+
+.hero {
+ display: grid;
+ min-height: 100vh;
+ place-items: center;
+
+ & .card {
+ appearance: none;
+ user-select: none;
+ }
+}
+`,
+ previewHtml: `src/styles/global.css
`,
+ },
+ sveltekit: {
+ id: 'sveltekit',
+ title: 'SvelteKit',
+ description: 'Imported app.css after vitePreprocess would run PostCSS.',
+ exampleId: 'sveltekit',
+ plugins: { nested: true, autoprefixer: true },
+ input: `:root {
+ font-family: system-ui, sans-serif;
+}
+
+.hero {
+ display: flex;
+ min-height: 100vh;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+
+ & h1 {
+ letter-spacing: -0.06em;
+ }
+}
+`,
+ previewHtml: `SvelteKit `,
+ },
+ webpack: {
+ id: 'webpack',
+ title: 'webpack',
+ description: 'CSS as @postcss-go/webpack-loader would hand to the engine.',
+ exampleId: 'webpack',
+ plugins: { nested: true, autoprefixer: true },
+ input: `.hero {
+ display: flex;
+ gap: 0.5rem;
+ min-height: 100vh;
+ align-items: center;
+ justify-content: center;
+ user-select: none;
+
+ & h1 span {
+ color: #c8ff3d;
+ }
+}
+`,
+ previewHtml: `webpack + PostCSS `,
+ },
+};
diff --git a/src/pages/examples/default.astro b/src/pages/examples/default.astro
new file mode 100644
index 0000000..0e85d3b
--- /dev/null
+++ b/src/pages/examples/default.astro
@@ -0,0 +1,110 @@
+---
+import ExamplesLayout from '../../layouts/ExamplesLayout.astro';
+import CodeSample from '../../components/CodeSample.astro';
+import PlaygroundCta from '../../components/PlaygroundCta.astro';
+
+const install = 'pnpm add -D @postcss-go/core autoprefixer';
+const config = `export default {
+ plugins: {
+ autoprefixer: {},
+ },
+};`;
+const functionConfig = `export default (ctx) => ({
+ map: ctx.env === 'production' ? { inline: false } : true,
+ plugins: {
+ autoprefixer: {},
+ },
+});`;
+const css = `:root {
+ color-scheme: dark;
+}
+
+.button {
+ display: flex;
+ gap: 0.5rem;
+}`;
+const scripts = `{
+ "scripts": {
+ "build:css": "postcss-go src/index.css -o dist/index.css",
+ "watch:css": "postcss-go src/index.css -o dist/index.css -w"
+ }
+}`;
+const api = `import { readFile, writeFile } from 'node:fs/promises';
+import postcss from '@postcss-go/core';
+import autoprefixer from 'autoprefixer';
+
+const css = await readFile('src/index.css', 'utf8');
+const result = await postcss([autoprefixer()]).process(css, {
+ from: 'src/index.css',
+ to: 'dist/index.css',
+});
+
+await writeFile('dist/index.css', result.css);`;
+---
+
+
+ Default
+
+ The default setup is a postcss.config.js plus the postcss-go CLI. No bundler
+ is required. The same config file is what Vite, Next.js, and other tools already look for.
+
+
+
+ Install
+
+
+ Configuration
+
+ Object form is enough for a fixed plugin list. false disables an entry;
+ true calls the plugin with no options.
+
+
+
+ Use a function when maps or plugins depend on NODE_ENV or the current file.
+ ctx.env is NODE_ENV, then development.
+
+
+
+ Input and scripts
+
+
+
+ JavaScript API
+
+ Skip the CLI when you already own the file I/O. process() always returns a Promise —
+ there is no implicit LazyResult.
+
+
+
+ CLI
+
+
+
+ Task
+ Command
+
+
+
+
+ One file
+ postcss-go src/index.css -o dist/index.css
+
+
+ Directory / glob
+ postcss-go src/**/*.css --base src --dir dist
+
+
+ Watch
+ postcss-go src/index.css -o dist/index.css -w
+
+
+ Plugin without a config
+ postcss-go src/index.css -u autoprefixer -o dist/index.css
+
+
+ Disable maps
+ postcss-go src/index.css -o dist/index.css --no-map
+
+
+
+
diff --git a/src/pages/examples/index.astro b/src/pages/examples/index.astro
new file mode 100644
index 0000000..9c690c4
--- /dev/null
+++ b/src/pages/examples/index.astro
@@ -0,0 +1,3 @@
+---
+return Astro.redirect(`${import.meta.env.BASE_URL}examples/default/`);
+---
diff --git a/src/pages/examples/webpack.astro b/src/pages/examples/webpack.astro
new file mode 100644
index 0000000..96541b3
--- /dev/null
+++ b/src/pages/examples/webpack.astro
@@ -0,0 +1,82 @@
+---
+import ExamplesLayout from '../../layouts/ExamplesLayout.astro';
+import CodeSample from '../../components/CodeSample.astro';
+import PlaygroundCta from '../../components/PlaygroundCta.astro';
+
+const install =
+ 'pnpm add -D @postcss-go/core @postcss-go/webpack-loader autoprefixer webpack webpack-cli css-loader style-loader';
+const config = `export default {
+ plugins: {
+ autoprefixer: {},
+ },
+};`;
+const webpackConfig = `export default {
+ module: {
+ rules: [
+ {
+ test: /\\.css$/i,
+ use: ['style-loader', 'css-loader', '@postcss-go/webpack-loader'],
+ },
+ ],
+ },
+};`;
+const webpackConfigInline = `import autoprefixer from 'autoprefixer';
+
+export default {
+ module: {
+ rules: [
+ {
+ test: /\\.css$/i,
+ use: [
+ 'style-loader',
+ 'css-loader',
+ {
+ loader: '@postcss-go/webpack-loader',
+ options: {
+ sourceMap: true,
+ postcssOptions: {
+ config: false,
+ plugins: [autoprefixer()],
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+};`;
+const css = `.button {
+ display: flex;
+ gap: 0.5rem;
+}`;
+const scripts = `{
+ "scripts": {
+ "build": "webpack --mode production",
+ "build:css": "postcss-go src/**/*.css --base src --dir dist/css --no-map"
+ }
+}`;
+---
+
+
+ webpack
+
+ Add @postcss-go/webpack-loader to your CSS rule. It calls the Go engine directly and
+ does not depend on postcss or the official postcss-loader. By default
+ it discovers postcss.config.js from the input file, same as the CLI.
+
+
+
+
+
+
+ Pass postcssOptions inline when you want to disable config lookup or set plugins in the
+ webpack config. postcssOptions can also be a function that receives mode,
+ env, file, and the loader context.
+
+
+
+ You can still run the CLI for a bundler-free CSS pass:
+
+
diff --git a/src/pages/guide/get-started.md b/src/pages/guide/get-started.md
index 426297b..f16b00d 100644
--- a/src/pages/guide/get-started.md
+++ b/src/pages/guide/get-started.md
@@ -50,6 +50,8 @@ file or environment:
terminal Copy
pnpm postcss-go src/index.css -o dist/index.css
+For the default config, see [Examples](../../examples/default/). For webpack,
+see the [webpack example](../../examples/webpack/).
For a gradual migration, see the [migration and compatibility guide](../migration/).
For browsers, import `@postcss-go/core/wasm` and follow the
[Browser and WASM](../browser-wasm/) guide.
diff --git a/src/pages/index.astro b/src/pages/index.astro
index b060655..c538d0a 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -67,7 +67,12 @@ import BenchmarkChart from '../components/BenchmarkChart.astro';
Get started
- Progress
+ Examples
+ Playground
MIT licensed
diff --git a/src/pages/playground/[id].astro b/src/pages/playground/[id].astro
new file mode 100644
index 0000000..3e15f39
--- /dev/null
+++ b/src/pages/playground/[id].astro
@@ -0,0 +1,24 @@
+---
+import PlaygroundLayout from '../../layouts/PlaygroundLayout.astro';
+import CssPlayground from '../../components/CssPlayground.astro';
+import { PLAYGROUND_LINKS, playgroundPreset } from '../../lib/playgrounds';
+
+export function getStaticPaths() {
+ return PLAYGROUND_LINKS.map((item) => ({ params: { id: item.id } }));
+}
+
+const id = Astro.params.id ?? 'default';
+const spec = playgroundPreset(id);
+const exampleHref = `${import.meta.env.BASE_URL}examples/${spec.exampleId}/`;
+---
+
+
+
+
+
diff --git a/src/pages/playground/index.astro b/src/pages/playground/index.astro
new file mode 100644
index 0000000..52653ab
--- /dev/null
+++ b/src/pages/playground/index.astro
@@ -0,0 +1,3 @@
+---
+return Astro.redirect(`${import.meta.env.BASE_URL}playground/default/`);
+---
diff --git a/src/styles/global.css b/src/styles/global.css
index 805159d..3969df2 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -335,3 +335,230 @@ html[data-theme='light'] .site-nav__github:hover {
html[data-theme='light'] .site-nav__github {
border-color: rgba(10, 11, 13, 0.2) !important;
}
+
+.playground-page {
+ display: flex;
+ min-height: 100dvh;
+ flex-direction: column;
+}
+
+@media (min-width: 761px) {
+ .playground-page {
+ height: 100dvh;
+ overflow: hidden;
+ }
+}
+
+.playground-shell {
+ display: grid;
+ width: 100%;
+ max-width: 100rem;
+ min-height: 0;
+ flex: 1;
+ grid-template-columns: 1fr;
+ gap: clamp(1.5rem, 4vw, 2.5rem);
+ padding: 0 1.5rem 1.5rem;
+}
+
+@media (min-width: 761px) {
+ .playground-shell {
+ grid-template-columns: 13rem minmax(0, 1fr);
+ padding: 0 2.5rem 1.25rem;
+ }
+}
+
+.playground-aside {
+ min-width: 0;
+}
+
+@media (min-width: 761px) {
+ .playground-aside {
+ overflow: auto;
+ padding-bottom: 0.5rem;
+ }
+}
+
+.playground-main {
+ display: flex;
+ min-width: 0;
+ min-height: 0;
+ flex-direction: column;
+}
+
+.playground-intro h1 {
+ margin: 0;
+ font-size: clamp(1.35rem, 2.4vw, 1.85rem);
+ letter-spacing: -0.04em;
+ line-height: 1.15;
+}
+
+.playground-intro p {
+ margin: 0.45rem 0 0.9rem;
+ max-width: 46rem;
+ color: var(--copy-muted);
+ font-size: 0.9rem;
+ line-height: 1.55;
+}
+
+.playground-intro code {
+ border: 1px solid var(--line);
+ border-radius: 0.3rem;
+ background: var(--surface);
+ padding: 0.08rem 0.32rem;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+ font-size: 0.86em;
+ color: var(--accent-readable);
+}
+
+.playground {
+ display: flex;
+ min-height: 28rem;
+ flex: 1;
+ flex-direction: column;
+ overflow: hidden;
+ border: 1px solid var(--line);
+ border-radius: 0.85rem;
+ background: var(--surface-soft);
+}
+
+@media (min-width: 761px) {
+ .playground {
+ min-height: 0;
+ }
+}
+
+.playground__toolbar,
+.playground__footer {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem 1rem;
+ padding: 0.65rem 0.9rem;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+ font-size: 0.68rem;
+}
+
+.playground__toolbar {
+ border-bottom: 1px solid var(--line-soft);
+}
+
+.playground__footer {
+ border-top: 1px solid var(--line-soft);
+ color: var(--copy-faint);
+}
+
+.playground__plugins,
+.playground__actions,
+.playground__tabs {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.65rem 0.85rem;
+}
+
+.playground__check {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ color: var(--copy-muted);
+ cursor: pointer;
+ letter-spacing: 0.04em;
+ text-transform: lowercase;
+}
+
+.playground__check input {
+ accent-color: var(--accent-readable);
+}
+
+.playground__actions button,
+.playground__tabs button {
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ background: transparent;
+ padding: 0.28rem 0.75rem;
+ color: var(--copy-muted);
+ cursor: pointer;
+ font: inherit;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ transition:
+ border-color 150ms,
+ color 150ms;
+}
+
+.playground__actions button:hover,
+.playground__tabs button:hover,
+.playground__tabs button[aria-selected='true'] {
+ border-color: var(--accent-readable);
+ color: var(--accent-readable);
+}
+
+.playground__panes {
+ display: grid;
+ min-height: 0;
+ flex: 1;
+ grid-template-columns: 1fr;
+}
+
+@media (min-width: 900px) {
+ .playground__panes {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+.playground__pane {
+ display: flex;
+ min-height: 16rem;
+ min-width: 0;
+ flex-direction: column;
+}
+
+@media (min-width: 900px) {
+ .playground__pane {
+ min-height: 0;
+ }
+
+ .playground__pane + .playground__pane {
+ border-left: 1px solid var(--line-soft);
+ }
+}
+
+.playground__label {
+ padding: 0.5rem 0.9rem 0.35rem;
+ color: var(--copy-faint);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+ font-size: 0.65rem;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+.playground__label--tabs {
+ padding-bottom: 0.25rem;
+}
+
+.playground__editor,
+.playground__preview {
+ min-height: 0;
+ flex: 1;
+}
+
+.playground__preview {
+ width: 100%;
+ border: 0;
+ background: var(--canvas);
+}
+
+.playground__error {
+ color: #ff6b6b;
+}
+
+.playground .monaco-editor,
+.playground .monaco-editor-background,
+.playground .margin {
+ background: transparent !important;
+}
+
+html[data-theme='light'] .playground__error {
+ color: #b42318;
+}