diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2026-01-04 20:58:28 -0800 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2026-01-04 20:58:28 -0800 |
| commit | acfea7c9e0129168205c374783e7036e5018c9a5 (patch) | |
| tree | c94ca7170552ef59425203c9150f0523cdd514dd | |
| parent | 4dd5994b27bb32d93efacd6d3a42b130f81425df (diff) | |
| download | adelie-acfea7c9e0129168205c374783e7036e5018c9a5.tar.gz adelie-acfea7c9e0129168205c374783e7036e5018c9a5.zip | |
Claude: first attempt at code editor
| -rw-r--r-- | .demo/index.html | 48 | ||||
| -rw-r--r-- | esbuild.config.js | 6 | ||||
| -rw-r--r-- | package-lock.json | 253 | ||||
| -rw-r--r-- | package.json | 6 | ||||
| -rw-r--r-- | src/css/style.css | 121 | ||||
| -rw-r--r-- | src/index.html | 21 | ||||
| -rw-r--r-- | src/ts/editor.ts | 279 | ||||
| -rw-r--r-- | src/ts/script.ts | 113 |
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>© 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>© 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(); } |
