summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.demo/index.html48
-rw-r--r--esbuild.config.js6
-rw-r--r--package-lock.json253
-rw-r--r--package.json6
-rw-r--r--src/css/style.css121
-rw-r--r--src/index.html21
-rw-r--r--src/ts/editor.ts279
-rw-r--r--src/ts/script.ts113
8 files changed, 712 insertions, 135 deletions
diff --git a/.demo/index.html b/.demo/index.html
deleted file mode 100644
index 578c883..0000000
--- a/.demo/index.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Liz Dot Coffee!</title>
- <link rel="stylesheet" href="/bundle.css" />
- <link rel="icon" href="/img/favicon.ico" />
- </head>
- <body class="demo-page">
- <header>
- <nav>
- <a href="/demo.html">Home</a>
- <a href="/">Components</a>
- <a href="#about">About</a>
- <a href="#archive">Archive</a>
- </nav>
- <input type="checkbox" id="theme-toggle" class="toggle" aria-label="Toggle dark mode" />
- </header>
-
- <main>
- <article>
- <h2>Welcome to the 90s</h2>
- <p><small>Posted on December 9, 2025</small></p>
- <p>
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
- eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
- minim veniam, quis nostrud exercitation ullamco laboris nisi ut
- aliquip ex ea commodo consequat.
- </p>
- <p>
- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
- dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
- proident, sunt in culpa qui officia deserunt mollit anim id est
- laborum.
- </p>
- <a href="#read-more">Read more →</a>
- </article>
-
- </main>
-
- <footer>
- <p>&copy; 2025 My Retro Blog. Made with coffee and nostalgia.</p>
- </footer>
-
- <script src="/bundle.js"></script>
- </body>
-</html>
diff --git a/esbuild.config.js b/esbuild.config.js
index 4adb86a..9d1c3df 100644
--- a/esbuild.config.js
+++ b/esbuild.config.js
@@ -10,7 +10,6 @@ const paths = {
cssEntry: 'src/css/style.css',
componentsHtml: 'src/index.html',
demoHtml: '.demo/index.html',
- outJs: 'dist/bundle.js',
outCss: 'dist/bundle.css',
outComponentsHtml: 'dist/index.html',
outDemoHtml: 'dist/demo.html',
@@ -25,10 +24,13 @@ function buildJavaScript() {
return esbuild.build({
entryPoints: [paths.jsEntry],
bundle: true,
+ splitting: true,
+ format: 'esm',
minify: isProduction,
sourcemap: true,
target: 'es2020',
- outfile: paths.outJs,
+ outdir: paths.distDir,
+ chunkNames: 'chunks/[name]-[hash]',
logLevel: 'info',
});
}
diff --git a/package-lock.json b/package-lock.json
index 1c771dc..e8490eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,12 @@
"name": "adelie",
"version": "0.0.1",
"dependencies": {
+ "@codemirror/lang-css": "^6.3.1",
+ "@codemirror/lang-html": "^6.4.11",
+ "@codemirror/lang-javascript": "^6.2.4",
+ "@codemirror/theme-one-dark": "^6.1.3",
+ "@replit/codemirror-vim": "^6.3.0",
+ "codemirror": "^6.0.2",
"prismjs": "^1.29.0"
},
"devDependencies": {
@@ -183,6 +189,144 @@
"node": ">=14.21.3"
}
},
+ "node_modules/@codemirror/autocomplete": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
+ "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/commands": {
+ "version": "6.10.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz",
+ "integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.4.0",
+ "@codemirror/view": "^6.27.0",
+ "@lezer/common": "^1.1.0"
+ }
+ },
+ "node_modules/@codemirror/lang-css": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
+ "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.2",
+ "@lezer/css": "^1.1.7"
+ }
+ },
+ "node_modules/@codemirror/lang-html": {
+ "version": "6.4.11",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
+ "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/lang-css": "^6.0.0",
+ "@codemirror/lang-javascript": "^6.0.0",
+ "@codemirror/language": "^6.4.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/css": "^1.1.0",
+ "@lezer/html": "^1.3.12"
+ }
+ },
+ "node_modules/@codemirror/lang-javascript": {
+ "version": "6.2.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
+ "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.6.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/javascript": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.12.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
+ "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
+ "@lezer/common": "^1.5.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/lint": {
+ "version": "6.9.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz",
+ "integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.35.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/search": {
+ "version": "6.5.11",
+ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
+ "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.5.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.3.tgz",
+ "integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@marijn/find-cluster-break": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/theme-one-dark": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
+ "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/highlight": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.39.8",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.8.tgz",
+ "integrity": "sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.5.0",
+ "crelt": "^1.0.6",
+ "style-mod": "^4.1.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -625,6 +769,82 @@
"node": ">=18"
}
},
+ "node_modules/@lezer/common": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz",
+ "integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
+ "license": "MIT"
+ },
+ "node_modules/@lezer/css": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz",
+ "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
+ "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/html": {
+ "version": "1.3.13",
+ "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
+ "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/javascript": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
+ "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.1.3",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz",
+ "integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@marijn/find-cluster-break": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
+ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
+ "license": "MIT"
+ },
+ "node_modules/@replit/codemirror-vim": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz",
+ "integrity": "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@codemirror/commands": "6.x.x",
+ "@codemirror/language": "6.x.x",
+ "@codemirror/search": "6.x.x",
+ "@codemirror/state": "6.x.x",
+ "@codemirror/view": "6.x.x"
+ }
+ },
"node_modules/@types/node": {
"version": "22.19.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
@@ -719,6 +939,21 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/codemirror": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
+ "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/commands": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/search": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -749,6 +984,12 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/crelt": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1303,6 +1544,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/style-mod": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
+ "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
+ "license": "MIT"
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -1366,6 +1613,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT"
+ },
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
diff --git a/package.json b/package.json
index 08a6855..097cc1e 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,12 @@
"check": "biome check src"
},
"dependencies": {
+ "@codemirror/lang-css": "^6.3.1",
+ "@codemirror/lang-html": "^6.4.11",
+ "@codemirror/lang-javascript": "^6.2.4",
+ "@codemirror/theme-one-dark": "^6.1.3",
+ "@replit/codemirror-vim": "^6.3.0",
+ "codemirror": "^6.0.2",
"prismjs": "^1.29.0"
},
"devDependencies": {
diff --git a/src/css/style.css b/src/css/style.css
index 5076da9..48d964e 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -1352,108 +1352,63 @@ button:disabled {
margin-bottom: 0;
}
-/* demo page */
-
-.demo-page .demo-section {
- margin-bottom: var(--space-xl);
-}
-
-.demo-page .component-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
- gap: var(--space-md);
- margin: var(--space-md) 0;
-}
+/* fairy dust */
-.demo-page .component-box {
- border: var(--border-width) solid var(--border);
- padding: var(--space-md);
- background: var(--surface);
- box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark),
- var(--shadow-box);
+.fairy-dust {
+ position: fixed;
+ pointer-events: none;
+ z-index: 9001;
+ line-height: 1;
+ animation: fairy-float 0.8s ease-out forwards;
+ will-change: transform, opacity;
}
-.demo-page .component-box > :last-child {
- margin-bottom: 0;
+@keyframes fairy-float {
+ 0% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(40px) scale(0.5);
+ }
}
-.demo-page .component-label {
- display: block;
- font-size: 0.75rem;
- color: var(--muted);
- margin-bottom: var(--space-xs);
- text-transform: uppercase;
- letter-spacing: 0.05em;
-}
+/* code editor */
-.demo-page .button-row {
+.editor-controls {
display: flex;
+ align-items: center;
gap: var(--space-sm);
- flex-wrap: wrap;
- margin: var(--space-md) 0;
-}
-
-.demo-page .form-group {
margin-bottom: var(--space-md);
}
-.demo-page code {
- background: var(--bg);
- padding: 0.25rem 0.5rem;
- border: 1px solid var(--border-light);
- font-size: 0.875rem;
- font-family: var(--font-mono);
+.editor-controls label {
+ margin: 0;
+ font-weight: 600;
}
-.demo-page pre code {
- background: transparent;
- border: none;
- padding: 0;
+.editor-controls select {
+ flex: 0 0 auto;
+ width: auto;
+ min-width: 150px;
}
-.demo-page .color-swatch {
- display: inline-block;
- width: 40px;
- height: 40px;
+.code-editor-container {
border: var(--border-width) solid var(--border);
- margin-right: var(--space-sm);
- vertical-align: middle;
+ border-radius: var(--border-radius);
+ overflow: hidden;
box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark),
- var(--shadow-sm);
-}
-
-.demo-page .demo-swatch-row {
- display: flex;
- align-items: center;
-}
-
-.demo-page .demo-inline-label {
- display: inline;
- margin: 0;
-}
-
-.demo-page .demo-example-box {
- max-width: 400px;
+ var(--shadow-box);
+ background: var(--surface);
}
-/* fairy dust */
-
-.fairy-dust {
- position: fixed;
- pointer-events: none;
- z-index: 9001;
- line-height: 1;
- animation: fairy-float 0.8s ease-out forwards;
- will-change: transform, opacity;
+.code-editor-container .cm-editor {
+ min-height: 300px;
+ max-height: 500px;
+ font-size: 0.875rem;
}
-@keyframes fairy-float {
- 0% {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
- 100% {
- opacity: 0;
- transform: translateY(40px) scale(0.5);
- }
+.code-editor-container .cm-scroller {
+ overflow: auto;
}
diff --git a/src/index.html b/src/index.html
index 5deda56..511e113 100644
--- a/src/index.html
+++ b/src/index.html
@@ -18,7 +18,7 @@
<main>
<!-- Color Palette -->
- <article>
+ <article style="margin-bottom: var(--space-xl);">
<h2>Color Palette</h2>
<p>
A retro-themed minimal CSS framework with carefully selected colors for light
@@ -425,6 +425,23 @@
retro(); // "That's totally rad!"</code></pre>
</article>
+ <!-- Live Code Editor -->
+ <article>
+ <h2>Live Code Editor</h2>
+ <p>Try out the interactive code editor powered by CodeMirror:</p>
+
+ <div class="editor-controls">
+ <label for="language-select">Language:</label>
+ <select id="language-select">
+ <option value="javascript">JavaScript</option>
+ <option value="css">CSS</option>
+ <option value="html">HTML</option>
+ </select>
+ </div>
+
+ <div id="code-editor" class="code-editor-container"></div>
+ </article>
+
<!-- Dividers -->
<article>
<h2>Dividers</h2>
@@ -523,6 +540,6 @@ retro(); // "That's totally rad!"</code></pre>
<p>&copy; 2025 Liz CSS Framework. Made with coffee and retro vibes.</p>
</footer>
- <script src="/bundle.js"></script>
+ <script type="module" src="/script.js"></script>
</body>
</html>
diff --git a/src/ts/editor.ts b/src/ts/editor.ts
new file mode 100644
index 0000000..2d2a3da
--- /dev/null
+++ b/src/ts/editor.ts
@@ -0,0 +1,279 @@
+import { EditorView, minimalSetup } from 'codemirror';
+import { javascript } from '@codemirror/lang-javascript';
+import { css } from '@codemirror/lang-css';
+import { html } from '@codemirror/lang-html';
+import { EditorState, Compartment } from '@codemirror/state';
+import { syntaxHighlighting, HighlightStyle } from '@codemirror/language';
+import { tags } from '@lezer/highlight';
+import { vim } from '@replit/codemirror-vim';
+import { ViewPlugin, Decoration, DecorationSet, gutter, GutterMarker } from '@codemirror/view';
+import { RangeSetBuilder } from '@codemirror/state';
+import { closeBrackets } from '@codemirror/autocomplete';
+
+const themeConfig = new Compartment();
+
+// Whitespace visualization - only show in selected text
+function highlightWhitespace() {
+ const whitespaceDecoration = Decoration.mark({
+ class: 'cm-whitespace',
+ attributes: { 'aria-hidden': 'true' },
+ });
+
+ return ViewPlugin.fromClass(
+ class {
+ decorations: DecorationSet;
+
+ constructor(view: EditorView) {
+ this.decorations = this.buildDecorations(view);
+ }
+
+ update(update: any) {
+ if (update.docChanged || update.viewportChanged || update.selectionSet) {
+ this.decorations = this.buildDecorations(update.view);
+ }
+ }
+
+ buildDecorations(view: EditorView) {
+ const builder = new RangeSetBuilder<Decoration>();
+ const selection = view.state.selection.main;
+
+ // Only show whitespace in selected ranges
+ if (!selection.empty) {
+ const text = view.state.doc.sliceString(selection.from, selection.to);
+ let pos = selection.from;
+
+ for (let i = 0; i < text.length; i++) {
+ const char = text[i];
+ if (char === ' ' || char === '\t') {
+ builder.add(pos + i, pos + i + 1, whitespaceDecoration);
+ }
+ }
+ }
+
+ return builder.finish();
+ }
+ },
+ {
+ decorations: (v) => v.decorations,
+ }
+ );
+}
+
+// Relative line numbers that update on cursor movement
+class RelativeLineNumberMarker extends GutterMarker {
+ constructor(private lineNo: number, private currentLine: number) {
+ super();
+ }
+
+ eq(other: RelativeLineNumberMarker) {
+ return this.lineNo === other.lineNo && this.currentLine === other.currentLine;
+ }
+
+ toDOM() {
+ const relative = Math.abs(this.lineNo - this.currentLine);
+ const display = this.lineNo === this.currentLine ? String(this.lineNo) : String(relative);
+ return document.createTextNode(display);
+ }
+}
+
+const relativeLineNumberGutter = ViewPlugin.fromClass(
+ class {
+ constructor(private view: EditorView) {}
+
+ update(update: any) {
+ // Force update on selection change
+ if (update.selectionSet || update.docChanged || update.viewportChanged) {
+ this.view.requestMeasure();
+ }
+ }
+ }
+);
+
+function relativeLineNumbers() {
+ return [
+ relativeLineNumberGutter,
+ gutter({
+ class: 'cm-lineNumbers',
+ lineMarker(view, line) {
+ const currentLine = view.state.doc.lineAt(view.state.selection.main.head).number;
+ const lineNo = view.state.doc.lineAt(line.from).number;
+ return new RelativeLineNumberMarker(lineNo, currentLine);
+ },
+ lineMarkerChange: (update) => update.selectionSet,
+ initialSpacer: () => new RelativeLineNumberMarker(1, 1),
+ }),
+ ];
+}
+
+// Match Prism.js colors exactly
+const lightHighlighting = HighlightStyle.define([
+ { tag: tags.comment, color: '#6a5550', fontStyle: 'italic' },
+ { tag: tags.punctuation, color: '#2a1f1d' },
+ { tag: [tags.propertyName, tags.attributeName], color: '#8f78d6' },
+ { tag: tags.tagName, color: '#8f78d6' },
+ { tag: tags.bool, color: '#8f78d6' },
+ { tag: tags.number, color: '#8f78d6' },
+ { tag: tags.literal, color: '#8f78d6' },
+ { tag: [tags.string, tags.character, tags.special(tags.string)], color: '#7a5245' },
+ { tag: tags.operator, color: '#c84d86' },
+ { tag: tags.keyword, color: '#c84d86', fontWeight: '700' },
+ { tag: tags.function(tags.variableName), color: '#e56aa6', fontWeight: '700' },
+ { tag: tags.className, color: '#e56aa6', fontWeight: '700' },
+ { tag: tags.variableName, color: '#b88a68' },
+]);
+
+// Match Prism.js dark theme colors
+const darkHighlighting = HighlightStyle.define([
+ { tag: tags.comment, color: '#cbb8b1', fontStyle: 'italic' },
+ { tag: tags.punctuation, color: '#f9efea' },
+ { tag: [tags.propertyName, tags.attributeName], color: '#d7c8ff' },
+ { tag: tags.tagName, color: '#d7c8ff' },
+ { tag: tags.bool, color: '#d7c8ff' },
+ { tag: tags.number, color: '#d7c8ff' },
+ { tag: tags.literal, color: '#d7c8ff' },
+ { tag: [tags.string, tags.character, tags.special(tags.string)], color: '#c9936a' },
+ { tag: tags.operator, color: '#ff97c8' },
+ { tag: tags.keyword, color: '#ff97c8', fontWeight: '700' },
+ { tag: tags.function(tags.variableName), color: '#f06aa6', fontWeight: '700' },
+ { tag: tags.className, color: '#f06aa6', fontWeight: '700' },
+ { tag: tags.variableName, color: '#a57353' },
+]);
+
+const lightTheme = EditorView.theme({
+ '&': {
+ backgroundColor: 'var(--surface)',
+ color: 'var(--text)',
+ },
+ '.cm-content': {
+ caretColor: 'var(--primary)',
+ fontFamily: 'var(--font-mono)',
+ fontSize: '0.875rem',
+ paddingLeft: '0',
+ },
+ '.cm-gutters': {
+ backgroundColor: 'var(--bg)',
+ color: 'var(--muted)',
+ border: 'none',
+ borderRight: '1px solid var(--border)',
+ },
+ '.cm-activeLineGutter': {
+ backgroundColor: 'var(--primary-light)',
+ color: 'var(--text)',
+ },
+ '.cm-activeLine': {
+ backgroundColor: 'rgba(229, 106, 166, 0.08)',
+ },
+ '.cm-selectionBackground': {
+ backgroundColor: 'rgba(229, 106, 166, 0.5) !important',
+ },
+ '&.cm-focused .cm-selectionBackground': {
+ backgroundColor: 'rgba(229, 106, 166, 0.5) !important',
+ },
+ '&.cm-focused .cm-selectionMatch': {
+ backgroundColor: 'rgba(142, 120, 214, 0.3) !important',
+ },
+ '.cm-cursor': {
+ borderLeftColor: 'var(--primary)',
+ },
+ '.cm-whitespace::before': {
+ content: '"·"',
+ position: 'absolute',
+ opacity: '0.3',
+ color: 'var(--muted)',
+ pointerEvents: 'none',
+ },
+});
+
+const darkTheme = EditorView.theme({
+ '&': {
+ backgroundColor: 'var(--surface)',
+ color: 'var(--text)',
+ },
+ '.cm-content': {
+ caretColor: 'var(--primary)',
+ fontFamily: 'var(--font-mono)',
+ fontSize: '0.875rem',
+ paddingLeft: '0',
+ },
+ '.cm-gutters': {
+ backgroundColor: 'var(--bg)',
+ color: 'var(--muted)',
+ border: 'none',
+ borderRight: '1px solid var(--border)',
+ },
+ '.cm-activeLineGutter': {
+ backgroundColor: 'rgba(229, 106, 166, 0.2)',
+ color: 'var(--text)',
+ },
+ '.cm-activeLine': {
+ backgroundColor: 'rgba(240, 106, 166, 0.08)',
+ },
+ '.cm-selectionBackground': {
+ backgroundColor: 'rgba(240, 106, 166, 0.45) !important',
+ },
+ '&.cm-focused .cm-selectionBackground': {
+ backgroundColor: 'rgba(240, 106, 166, 0.45) !important',
+ },
+ '&.cm-focused .cm-selectionMatch': {
+ backgroundColor: 'rgba(182, 156, 255, 0.35) !important',
+ },
+ '.cm-cursor': {
+ borderLeftColor: 'var(--primary)',
+ },
+ '.cm-whitespace::before': {
+ content: '"·"',
+ position: 'absolute',
+ opacity: '0.25',
+ color: 'var(--muted)',
+ pointerEvents: 'none',
+ },
+}, { dark: true });
+
+interface EditorOptions {
+ parent: HTMLElement;
+ language?: 'javascript' | 'css' | 'html';
+ initialCode?: string;
+ theme?: 'light' | 'dark';
+}
+
+export function createEditor(options: EditorOptions): EditorView {
+ const { parent, language = 'javascript', initialCode = '', theme = 'light' } = options;
+
+ const languageExtension = {
+ javascript,
+ css,
+ html,
+ }[language];
+
+ const state = EditorState.create({
+ doc: initialCode,
+ extensions: [
+ minimalSetup,
+ closeBrackets(),
+ relativeLineNumbers(),
+ highlightWhitespace(),
+ vim(),
+ languageExtension(),
+ themeConfig.of(theme === 'dark'
+ ? [darkTheme, syntaxHighlighting(darkHighlighting)]
+ : [lightTheme, syntaxHighlighting(lightHighlighting)]
+ ),
+ ],
+ });
+
+ const view = new EditorView({
+ state,
+ parent,
+ });
+
+ return view;
+}
+
+export function setEditorTheme(view: EditorView, theme: 'light' | 'dark'): void {
+ view.dispatch({
+ effects: themeConfig.reconfigure(theme === 'dark'
+ ? [darkTheme, syntaxHighlighting(darkHighlighting)]
+ : [lightTheme, syntaxHighlighting(lightHighlighting)]
+ ),
+ });
+}
diff --git a/src/ts/script.ts b/src/ts/script.ts
index 79a9a75..e8502c8 100644
--- a/src/ts/script.ts
+++ b/src/ts/script.ts
@@ -5,10 +5,14 @@ import 'prismjs/components/prism-markup';
import { initOneko } from './oneko';
type Theme = 'light' | 'dark';
+type EditorView = any;
const THEME_STORAGE_KEY = 'theme';
const DARK_THEME_ATTRIBUTE = 'data-theme';
+let editorInstance: EditorView | null = null;
+let editorModule: typeof import('./editor') | null = null;
+
function detectAssetBase(): string {
const currentScript = document.currentScript as HTMLScriptElement | null;
const scriptSrc =
@@ -58,6 +62,10 @@ function initThemeToggle(): void {
const nextTheme: Theme = toggleButton.checked ? 'dark' : 'light';
applyTheme(nextTheme);
sessionStorage.setItem(THEME_STORAGE_KEY, nextTheme);
+
+ if (editorInstance && editorModule) {
+ editorModule.setEditorTheme(editorInstance, nextTheme);
+ }
});
}
@@ -142,11 +150,116 @@ function initFileInputs(): void {
});
}
+async function loadEditor() {
+ if (!editorModule) {
+ editorModule = await import('./editor');
+ }
+ return editorModule;
+}
+
+function initCodeEditor(): void {
+ const editorContainer = document.getElementById('code-editor');
+ const languageSelect = document.getElementById('language-select');
+
+ if (!editorContainer || !(languageSelect instanceof HTMLSelectElement)) return;
+
+ const initialCode = {
+ javascript: `// Welcome to the live code editor!
+function greet(name) {
+ return \`Hello, \${name}! 👋\`;
+}
+
+console.log(greet('World'));`,
+ css: `/* Try editing this CSS! */
+.retro-box {
+ background: var(--primary);
+ color: var(--text);
+ padding: var(--space-md);
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow-box);
+}`,
+ html: `<!-- Try editing this HTML! -->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Retro Page</title>
+</head>
+<body>
+ <h1>Hello, World!</h1>
+ <p>Welcome to the retro web.</p>
+</body>
+</html>`,
+ };
+
+ let editorLoaded = false;
+
+ const currentTheme = getStoredTheme() ?? getSystemTheme();
+
+ async function createEditorInstance() {
+ if (editorLoaded) return;
+ editorLoaded = true;
+
+ const { createEditor } = await loadEditor();
+ const language = languageSelect.value as 'javascript' | 'css' | 'html';
+
+ editorInstance = createEditor({
+ parent: editorContainer,
+ language,
+ initialCode: initialCode[language],
+ theme: currentTheme,
+ });
+ }
+
+ // Lazy load on intersection (when editor scrolls into view)
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ createEditorInstance();
+ observer.disconnect();
+ }
+ });
+ },
+ { rootMargin: '100px' }
+ );
+
+ observer.observe(editorContainer);
+
+ // Also load on language change
+ languageSelect.addEventListener('change', async () => {
+ const language = languageSelect.value as 'javascript' | 'css' | 'html';
+
+ if (!editorModule) {
+ await createEditorInstance();
+ return;
+ }
+
+ if (editorInstance && editorContainer) {
+ editorContainer.innerHTML = '';
+
+ const { createEditor } = editorModule;
+ editorInstance = createEditor({
+ parent: editorContainer,
+ language,
+ initialCode: initialCode[language],
+ theme: currentTheme,
+ });
+ }
+ });
+}
+
function init(): void {
setAssetBase();
initThemeToggle();
initFairyDust();
initFileInputs();
+
+ // Only initialize code editor if the container exists
+ if (document.getElementById('code-editor')) {
+ initCodeEditor();
+ }
+
Prism.highlightAll();
initOneko();
}