From e4b9cfa371c234f58b3b6c83fe75197ca6550657 Mon Sep 17 00:00:00 2001 From: KCM Date: Mon, 16 Mar 2026 09:58:53 -0500 Subject: [PATCH 1/6] chore: lint staged. --- .husky/pre-commit | 2 + .husky/pre-push | 3 + .prettierignore | 2 + README.md | 2 +- docs/article.md | 75 +++++++++++ package-lock.json | 330 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 11 ++ src/index.html | 2 +- 8 files changed, 425 insertions(+), 2 deletions(-) create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100644 .prettierignore create mode 100644 docs/article.md diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..4e51b48 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +npm exec lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..7c4e303 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +npm run lint +npm run build:esm diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/README.md b/README.md index 9b45078..b66108c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @knighted/develop -A small in-browser playground for experimenting with `@knighted/jsx` and `@knighted/css`. +Compiler-as-a-Service (at the edge of your browser) with `@knighted/jsx` and `@knighted/css`. > ⚠️ Early project status: this package is pre-`1.0.0` and still actively evolving. diff --git a/docs/article.md b/docs/article.md new file mode 100644 index 0000000..2cf8e02 --- /dev/null +++ b/docs/article.md @@ -0,0 +1,75 @@ +# Forget The Build Step: Building A Compiler-as-a-Service Playground + +In modern frontend development, we have normalized a heavy local setup cost. Want JSX and modern CSS dialects? Install a large dependency graph, start a dev server, and wait for transpilation loops before you can really iterate. + +I wanted to test a different path: what if we removed the terminal from the inner loop? + +That experiment became @knighted/develop, a browser-native playground that treats your tab as a real-time compiler host. + +## The Core Idea + +Most playgrounds rely on a backend build service. @knighted/develop flips that model: + +- JSX compilation and execution happen in the browser. +- CSS transforms (including CSS Modules, Less, and Sass) run in the browser. +- Compilers are loaded on demand from CDN sources. + +The result is a development loop that feels direct: type, compile, render, repeat. + +## The Stack Behind It + +Two libraries power the runtime: + +- @knighted/jsx: JSX that resolves to real DOM nodes. + - No virtual DOM requirement. + - You can use declarative JSX and imperative DOM APIs in the same flow. +- @knighted/css: A browser-capable CSS compiler pipeline. + - Supports native CSS, CSS Modules, Less, and Sass. + - Uses WASM-backed tooling for modern transforms. + +Under the hood, the app leans on CDN resolution and lazy loading, so it fetches compiler/runtime pieces only when a mode needs them. + +## Why "Compiler-as-a-Service"? + +Compiler-as-a-Service here does not mean a remote build cluster. + +It means the service boundary is split between: + +- global CDN infrastructure (module and WASM delivery), and +- the user device (actual compilation and execution). + +If you switch into Sass mode, the browser loads Sass support. If you stay in native CSS mode, it does not pay that cost. The compiler behaves like an on-demand service, but the work stays local to the tab. + +## What This Enables + +- Fast feedback loops + - Rendering updates track edits with minimal overhead. +- Mixed declarative and imperative workflows + - Useful for low-level UI experiments and DOM-heavy component prototypes. +- Isolation testing with ShadowRoot + - Toggle encapsulation to verify style boundary behavior. +- Zero install inner loop + - Open a page and start building. + +## Why This Matters + +The point is not to replace every production build pipeline. + +The point is to prove a stronger baseline: modern browsers are now capable enough to host substantial parts of the authoring and compile cycle directly, without defaulting to local toolchain setup for every experiment. + +For prototyping and component iteration, that changes the cost model dramatically. + +## Try It + +- Live playground: https://knightedcodemonkey.github.io/develop/ +- Source repository: https://github.com/knightedcodemonkey/develop + +## Notes For Publishing + +If you post this on Medium (or similar), include a short screen recording that shows: + +- switching style modes (CSS -> Modules -> Less -> Sass), +- toggling ShadowRoot on and off, and +- immediate preview updates while typing. + +That visual sequence communicates the Compiler-as-a-Service model faster than any architecture diagram. diff --git a/package-lock.json b/package-lock.json index 89e44fd..4e91331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "license": "MIT", "devDependencies": { "http-server": "^14.1.1", + "husky": "^9.1.7", "jspm": "^4.4.0", + "lint-staged": "^16.4.0", "oxlint": "^1.55.0", "prettier": "^3.8.1" } @@ -944,6 +946,22 @@ "node": ">=0.4.0" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1316,6 +1334,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1336,6 +1388,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -1417,6 +1476,19 @@ "dev": true, "license": "MIT" }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1770,6 +1842,22 @@ "node": ">=12" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -1793,6 +1881,22 @@ "node": ">=0.8.19" } }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -1869,6 +1973,65 @@ "jspm": "dist/jspm.js" } }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/locko": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/locko/-/locko-1.1.0.tgz", @@ -1919,6 +2082,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2332,6 +2545,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/portfinder": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", @@ -2402,6 +2628,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2522,6 +2755,36 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2581,6 +2844,16 @@ "text-decoder": "^1.1.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -2687,6 +2960,16 @@ "b4a": "^1.6.4" } }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/union": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", @@ -2786,6 +3069,53 @@ "engines": { "node": ">=12" } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/package.json b/package.json index 6924c0a..5415038 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "main": "index.js", "scripts": { "dev": "http-server . -a localhost -c-1 -o src/index.html", + "prepare": "husky", "build:prepare": "node scripts/build-prepare.js", "build:importmap": "node scripts/build-importmap.js", "build": "npm run build:prepare && npm run build:importmap", @@ -32,10 +33,20 @@ }, "devDependencies": { "http-server": "^14.1.1", + "husky": "^9.1.7", "jspm": "^4.4.0", + "lint-staged": "^16.4.0", "oxlint": "^1.55.0", "prettier": "^3.8.1" }, + "lint-staged": { + "*": [ + "prettier --write --ignore-unknown" + ], + "{src,scripts}/**/*.{js,mjs,cjs,jsx,ts,tsx}": [ + "oxlint" + ] + }, "prettier": { "arrowParens": "avoid", "printWidth": 90, diff --git a/src/index.html b/src/index.html index d5a18c7..9ce13bd 100644 --- a/src/index.html +++ b/src/index.html @@ -18,7 +18,7 @@

>@knighted/develop

-

Develop UI components directly in the browser with live previews.

+

Compiler-as-a-Service (at the edge of your browser).

Idle
From d1873ce3185a398c84e24f11c722639e67f9b3bc Mon Sep 17 00:00:00 2001 From: KCM Date: Mon, 16 Mar 2026 10:11:18 -0500 Subject: [PATCH 2/6] refactor: grid based layout for controls. --- src/index.html | 136 ++++++++++++++++++++++--------------------------- src/styles.css | 38 +++++++------- 2 files changed, 83 insertions(+), 91 deletions(-) diff --git a/src/index.html b/src/index.html index 9ce13bd..fabdcf2 100644 --- a/src/index.html +++ b/src/index.html @@ -25,44 +25,38 @@

-
-
-

Component

-
-
-
- - -
+
+

Component

+
+ +
-
+
-
-
-

Styles

-
-
-
- - -
+
+

Styles

+
+ +
-
+
+ +
+

Clear source?

+

This action will remove all text from the editor.

+ + + + +
+
+ diff --git a/src/styles.css b/src/styles.css index a8ece51..860855a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -477,6 +477,86 @@ textarea:focus { stroke-linejoin: round; } +.confirm-dialog { + border: none; + padding: 0; + background: transparent; + color: inherit; +} + +.confirm-dialog:modal { + width: min(460px, calc(100vw - 32px)); + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 14px; + background: rgba(13, 16, 24, 0.97); + color: #e7edf9; + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.45); +} + +.confirm-dialog::backdrop { + background: rgba(6, 9, 15, 0.66); + backdrop-filter: blur(2px); +} + +.confirm-dialog__form { + margin: 0; + padding: 18px; +} + +.confirm-dialog__form h3 { + margin: 0; + font-size: 1.04rem; +} + +.confirm-dialog__form p { + margin: 10px 0 0; + color: #b8c3d9; + font-size: 0.9rem; +} + +.confirm-dialog__actions { + margin: 18px 0 0; + padding: 0; + list-style: none; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.confirm-dialog__button { + border-radius: 10px; + border: 1px solid transparent; + min-height: 34px; + padding: 6px 14px; + cursor: pointer; + font-weight: 600; +} + +.confirm-dialog__button--secondary { + border-color: rgba(255, 255, 255, 0.16); + background: rgba(255, 255, 255, 0.06); + color: #eef3ff; +} + +.confirm-dialog__button--secondary:hover { + background: rgba(255, 255, 255, 0.14); +} + +.confirm-dialog__button--danger { + border-color: rgba(250, 126, 138, 0.48); + background: rgba(250, 126, 138, 0.2); + color: #ffe7ea; +} + +.confirm-dialog__button--danger:hover { + background: rgba(250, 126, 138, 0.32); +} + +.confirm-dialog__button:focus-visible { + outline: 2px solid rgba(122, 107, 255, 0.88); + outline-offset: 1px; +} + @media (max-width: 900px) { .panel-header-main-actions .controls, .controls--actions { From fd9e261fb4478320b7dc3cbcec064a866cc5017a Mon Sep 17 00:00:00 2001 From: KCM Date: Mon, 16 Mar 2026 10:36:30 -0500 Subject: [PATCH 4/6] feat: layout options. --- src/app.js | 52 ++++++++++++++++++++++++++++++++++++++++ src/index.html | 48 +++++++++++++++++++++++++++++++++++++ src/styles.css | 64 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 163 insertions(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index a90f907..03b4b87 100644 --- a/src/app.js +++ b/src/app.js @@ -3,6 +3,8 @@ import { createCodeMirrorEditor } from './editor-codemirror.js' import { defaultCss, defaultJsx } from './defaults.js' const statusNode = document.getElementById('status') +const appGrid = document.querySelector('.app-grid') +const appGridLayoutButtons = document.querySelectorAll('[data-app-grid-layout]') const renderMode = document.getElementById('render-mode') const autoRenderToggle = document.getElementById('auto-render') const renderButton = document.getElementById('render-button') @@ -44,6 +46,7 @@ let pendingClearAction = null let hasCompletedInitialRender = false let previewBackgroundColor = null const clipboardSupported = Boolean(navigator.clipboard?.writeText) +const appGridLayoutStorageKey = 'knighted-develop:app-grid-layout' const styleLabels = { css: 'Native CSS', @@ -142,6 +145,43 @@ const setStatus = text => { statusNode.textContent = text } +const appGridLayouts = ['default', 'preview-right', 'preview-left'] + +const applyAppGridLayout = (layout, { persist = true } = {}) => { + if (!appGrid || !appGridLayouts.includes(layout)) { + return + } + + appGrid.classList.toggle('app-grid--preview-right', layout === 'preview-right') + appGrid.classList.toggle('app-grid--preview-left', layout === 'preview-left') + + for (const button of appGridLayoutButtons) { + const isActive = button.dataset.appGridLayout === layout + button.setAttribute('aria-pressed', isActive ? 'true' : 'false') + } + + if (persist) { + try { + localStorage.setItem(appGridLayoutStorageKey, layout) + } catch { + /* Ignore storage write errors in restricted browsing modes. */ + } + } +} + +const getInitialAppGridLayout = () => { + try { + const value = localStorage.getItem(appGridLayoutStorageKey) + if (appGridLayouts.includes(value)) { + return value + } + } catch { + /* Ignore storage read errors in restricted browsing modes. */ + } + + return 'default' +} + const setCdnLoading = isLoading => { if (!cdnLoading) return cdnLoading.hidden = !isLoading @@ -892,6 +932,18 @@ clearStylesButton.addEventListener('click', () => { jsxEditor.addEventListener('input', maybeRender) cssEditor.addEventListener('input', maybeRender) +for (const button of appGridLayoutButtons) { + button.addEventListener('click', () => { + const nextLayout = button.dataset.appGridLayout + if (!nextLayout) { + return + } + applyAppGridLayout(nextLayout) + }) +} + +applyAppGridLayout(getInitialAppGridLayout(), { persist: false }) + updateRenderButtonVisibility() setStyleCompiling(false) setCdnLoading(true) diff --git a/src/index.html b/src/index.html index 09911cb..3e89e02 100644 --- a/src/index.html +++ b/src/index.html @@ -24,6 +24,54 @@

+
+ + + +
+

Component

diff --git a/src/styles.css b/src/styles.css index 860855a..545d8a2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -78,12 +78,69 @@ body { display: grid; grid-template-columns: repeat(2, minmax(320px, 1fr)); grid-template-areas: + 'layout-controls layout-controls' 'component styles' 'preview preview'; gap: 18px; padding: 24px; } +.app-grid--preview-right { + grid-template-areas: + 'layout-controls layout-controls' + 'component preview' + 'styles preview'; +} + +.app-grid--preview-left { + grid-template-areas: + 'layout-controls layout-controls' + 'preview component' + 'preview styles'; +} + +.app-grid-layout-controls { + grid-area: layout-controls; + display: inline-flex; + justify-self: end; + gap: 10px; +} + +.layout-toggle { + display: inline-grid; + place-content: center; + width: 36px; + height: 36px; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 10px; + background: rgba(255, 255, 255, 0.06); + color: #d8e0ef; + cursor: pointer; +} + +.layout-toggle svg { + width: 18px; + height: 18px; + fill: none; + stroke: currentColor; + stroke-width: 1.5; +} + +.layout-toggle:hover { + background: rgba(255, 255, 255, 0.12); +} + +.layout-toggle[aria-pressed='true'] { + border-color: rgba(122, 107, 255, 0.65); + background: rgba(122, 107, 255, 0.2); + color: #f1f5ff; +} + +.layout-toggle:focus-visible { + outline: 2px solid rgba(122, 107, 255, 0.85); + outline-offset: 2px; +} + .component-panel { grid-area: component; max-height: min(64vh, 620px); @@ -105,10 +162,15 @@ body { .app-grid { grid-template-columns: minmax(0, 1fr); grid-template-areas: + 'layout-controls' 'component' 'styles' 'preview'; } + + .app-grid-layout-controls { + justify-self: start; + } } .panel { @@ -271,7 +333,7 @@ textarea:focus { } .preview-host { - flex: 0 1 auto; + flex: 1 1 auto; min-height: 180px; padding: 18px; overflow: auto; From ed96a82b173d7e6627f9e8ee10aca372a4b05d37 Mon Sep 17 00:00:00 2001 From: KCM Date: Mon, 16 Mar 2026 10:49:14 -0500 Subject: [PATCH 5/6] feat: theming. --- docs/next-steps.md | 6 - src/app.js | 78 ++++++++- src/editor-codemirror.js | 94 +++++++---- src/index.html | 37 +++++ src/styles.css | 341 +++++++++++++++++++++++++++++---------- 5 files changed, 432 insertions(+), 124 deletions(-) diff --git a/docs/next-steps.md b/docs/next-steps.md index ee7034d..0c103af 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -9,9 +9,3 @@ Focused follow-up work for `@knighted/develop`. 2. **Preview UX polish** - Keep tooltip affordances for mode-specific behavior. - Continue tightening panel control alignment and spacing without introducing extra markup. - -3. **Theming (light + dark)** - - Keep the existing dark mode as the baseline and add a first-class light theme. - - Move key colors to semantic CSS variables and define both theme palettes. - - Ensure component panels, controls, editor chrome, preview shell, and tooltips all have complete light-mode coverage. - - Verify contrast/accessibility across both themes and preserve visual hierarchy parity. diff --git a/src/app.js b/src/app.js index 03b4b87..35662f7 100644 --- a/src/app.js +++ b/src/app.js @@ -5,6 +5,7 @@ import { defaultCss, defaultJsx } from './defaults.js' const statusNode = document.getElementById('status') const appGrid = document.querySelector('.app-grid') const appGridLayoutButtons = document.querySelectorAll('[data-app-grid-layout]') +const appThemeButtons = document.querySelectorAll('[data-app-theme]') const renderMode = document.getElementById('render-mode') const autoRenderToggle = document.getElementById('auto-render') const renderButton = document.getElementById('render-button') @@ -45,8 +46,10 @@ let compiledStylesCache = { let pendingClearAction = null let hasCompletedInitialRender = false let previewBackgroundColor = null +let previewBackgroundCustomized = false const clipboardSupported = Boolean(navigator.clipboard?.writeText) const appGridLayoutStorageKey = 'knighted-develop:app-grid-layout' +const appThemeStorageKey = 'knighted-develop:theme' const styleLabels = { css: 'Native CSS', @@ -182,6 +185,41 @@ const getInitialAppGridLayout = () => { return 'default' } +const applyTheme = (theme, { persist = true } = {}) => { + if (!['dark', 'light'].includes(theme)) { + return + } + + document.documentElement.dataset.theme = theme + syncPreviewBackgroundPickerFromTheme() + + for (const button of appThemeButtons) { + const isActive = button.dataset.appTheme === theme + button.setAttribute('aria-pressed', isActive ? 'true' : 'false') + } + + if (persist) { + try { + localStorage.setItem(appThemeStorageKey, theme) + } catch { + /* Ignore storage write errors in restricted browsing modes. */ + } + } +} + +const getInitialTheme = () => { + try { + const value = localStorage.getItem(appThemeStorageKey) + if (value === 'dark' || value === 'light') { + return value + } + } catch { + /* Ignore storage read errors in restricted browsing modes. */ + } + + return 'dark' +} + const setCdnLoading = isLoading => { if (!cdnLoading) return cdnLoading.hidden = !isLoading @@ -326,7 +364,24 @@ const applyPreviewBackgroundColor = color => { return } - previewHost.style.backgroundColor = color + if (typeof color === 'string' && color.length > 0) { + previewHost.style.backgroundColor = color + return + } + + previewHost.style.removeProperty('background-color') +} + +const syncPreviewBackgroundPickerFromTheme = () => { + if (!previewBgColorInput || !previewHost || previewBackgroundCustomized) { + return + } + + previewBackgroundColor = null + applyPreviewBackgroundColor(null) + previewBgColorInput.value = normalizeColorToHex( + getComputedStyle(previewHost).backgroundColor, + ) } const initializePreviewBackgroundPicker = () => { @@ -335,12 +390,14 @@ const initializePreviewBackgroundPicker = () => { } const initialColor = normalizeColorToHex(getComputedStyle(previewHost).backgroundColor) - previewBackgroundColor = initialColor + previewBackgroundColor = null + previewBackgroundCustomized = false previewBgColorInput.value = initialColor - applyPreviewBackgroundColor(initialColor) + applyPreviewBackgroundColor(null) previewBgColorInput.addEventListener('input', () => { previewBackgroundColor = previewBgColorInput.value + previewBackgroundCustomized = true applyPreviewBackgroundColor(previewBackgroundColor) }) } @@ -352,9 +409,7 @@ const recreatePreviewHost = () => { previewHost.replaceWith(nextHost) previewHost = nextHost - if (previewBackgroundColor) { - applyPreviewBackgroundColor(previewBackgroundColor) - } + applyPreviewBackgroundColor(previewBackgroundColor) } const getRenderTarget = () => { @@ -942,7 +997,18 @@ for (const button of appGridLayoutButtons) { }) } +for (const button of appThemeButtons) { + button.addEventListener('click', () => { + const nextTheme = button.dataset.appTheme + if (!nextTheme) { + return + } + applyTheme(nextTheme) + }) +} + applyAppGridLayout(getInitialAppGridLayout(), { persist: false }) +applyTheme(getInitialTheme(), { persist: false }) updateRenderButtonVisibility() setStyleCompiling(false) diff --git a/src/editor-codemirror.js b/src/editor-codemirror.js index 8a59fcb..99953a9 100644 --- a/src/editor-codemirror.js +++ b/src/editor-codemirror.js @@ -118,17 +118,49 @@ export const createCodeMirrorEditor = async ({ onFocus, }) => { const runtime = await ensureCodeMirrorRuntime() + const editorColors = { + keyword: 'var(--cm-keyword)', + name: 'var(--cm-name)', + property: 'var(--cm-property)', + fn: 'var(--cm-function)', + constant: 'var(--cm-constant)', + definition: 'var(--cm-definition)', + type: 'var(--cm-type)', + number: 'var(--cm-number)', + operator: 'var(--cm-operator)', + string: 'var(--cm-string)', + comment: 'var(--cm-comment)', + link: 'var(--cm-link)', + heading: 'var(--cm-heading)', + atom: 'var(--cm-atom)', + invalid: 'var(--cm-invalid)', + text: 'var(--cm-text)', + caret: 'var(--cm-caret)', + gutterBg: 'var(--cm-gutter-bg)', + gutterBorder: 'var(--cm-gutter-border)', + gutterText: 'var(--cm-gutter-text)', + selection: 'var(--cm-selection)', + activeLine: 'var(--cm-active-line)', + focusRing: 'var(--cm-focus-ring)', + tooltipBg: 'var(--cm-tooltip-bg)', + tooltipText: 'var(--cm-tooltip-text)', + tooltipBorder: 'var(--cm-tooltip-border)', + tooltipItem: 'var(--cm-tooltip-item)', + tooltipItemSelectedBg: 'var(--cm-tooltip-item-selected-bg)', + tooltipItemSelectedText: 'var(--cm-tooltip-item-selected-text)', + } + const languageCompartment = new runtime.Compartment() const editorHighlightStyle = runtime.HighlightStyle.define([ - { tag: runtime.tags.keyword, color: '#ff7fb3', fontWeight: '600' }, - { tag: [runtime.tags.name, runtime.tags.deleted], color: '#e7ecf9' }, + { tag: runtime.tags.keyword, color: editorColors.keyword, fontWeight: '600' }, + { tag: [runtime.tags.name, runtime.tags.deleted], color: editorColors.name }, { tag: [runtime.tags.character, runtime.tags.propertyName, runtime.tags.macroName], - color: '#3fd6a6', + color: editorColors.property, }, { tag: [runtime.tags.function(runtime.tags.variableName), runtime.tags.labelName], - color: '#8dc8ff', + color: editorColors.fn, }, { tag: [ @@ -136,15 +168,15 @@ export const createCodeMirrorEditor = async ({ runtime.tags.constant(runtime.tags.name), runtime.tags.standard(runtime.tags.name), ], - color: '#7fd7ff', + color: editorColors.constant, }, { tag: [runtime.tags.definition(runtime.tags.name), runtime.tags.separator], - color: '#dce4f6', + color: editorColors.definition, }, { tag: [runtime.tags.className, runtime.tags.typeName], - color: '#8eb8ff', + color: editorColors.type, fontWeight: '600', }, { @@ -156,19 +188,19 @@ export const createCodeMirrorEditor = async ({ runtime.tags.self, runtime.tags.namespace, ], - color: '#ffcb82', + color: editorColors.number, }, { tag: [runtime.tags.operator, runtime.tags.operatorKeyword], - color: '#d5def0', + color: editorColors.operator, }, { tag: [runtime.tags.string, runtime.tags.special(runtime.tags.string)], - color: '#ffd38e', + color: editorColors.string, }, { tag: [runtime.tags.meta, runtime.tags.comment], - color: '#94a2bb', + color: editorColors.comment, fontStyle: 'italic', }, { @@ -181,12 +213,12 @@ export const createCodeMirrorEditor = async ({ }, { tag: runtime.tags.link, - color: '#88b6ff', + color: editorColors.link, textDecoration: 'underline', }, { tag: runtime.tags.heading, - color: '#f2f5ff', + color: editorColors.heading, fontWeight: '700', }, { @@ -195,19 +227,19 @@ export const createCodeMirrorEditor = async ({ runtime.tags.bool, runtime.tags.special(runtime.tags.variableName), ], - color: '#b8a8ff', + color: editorColors.atom, }, { tag: runtime.tags.invalid, - color: '#ff8fa1', - textDecoration: 'underline wavy #ff8fa1', + color: editorColors.invalid, + textDecoration: `underline wavy ${editorColors.invalid}`, }, ]) const editorTheme = runtime.EditorView.theme({ '&': { height: '100%', backgroundColor: 'transparent', - color: '#edf2ff', + color: editorColors.text, fontSize: '0.9rem', fontFamily: "'JetBrains Mono', 'Fira Code', monospace", }, @@ -218,39 +250,39 @@ export const createCodeMirrorEditor = async ({ '.cm-content': { padding: '16px 18px', minHeight: '100%', - caretColor: '#f1f5ff', + caretColor: editorColors.caret, }, '.cm-gutters': { - backgroundColor: 'rgba(255, 255, 255, 0.045)', - borderRight: '1px solid rgba(255, 255, 255, 0.13)', - color: '#98a8c4', + backgroundColor: editorColors.gutterBg, + borderRight: `1px solid ${editorColors.gutterBorder}`, + color: editorColors.gutterText, }, '.cm-lineNumbers .cm-gutterElement': { padding: '0 10px 0 14px', }, '&.cm-focused .cm-cursor': { - borderLeftColor: '#f1f5ff', + borderLeftColor: editorColors.caret, }, '&.cm-focused .cm-selectionBackground, ::selection': { - backgroundColor: 'rgba(122, 107, 255, 0.36)', + backgroundColor: editorColors.selection, }, '&.cm-focused .cm-activeLine': { - backgroundColor: 'rgba(255, 255, 255, 0.08)', + backgroundColor: editorColors.activeLine, }, '&.cm-focused': { - outline: '1px solid rgba(122, 107, 255, 0.62)', + outline: `1px solid ${editorColors.focusRing}`, }, '.cm-tooltip': { - backgroundColor: '#1b2233', - color: '#edf2ff', - border: '1px solid rgba(152, 168, 196, 0.32)', + backgroundColor: editorColors.tooltipBg, + color: editorColors.tooltipText, + border: `1px solid ${editorColors.tooltipBorder}`, }, '.cm-tooltip-autocomplete > ul > li': { - color: '#dce6fa', + color: editorColors.tooltipItem, }, '.cm-tooltip-autocomplete > ul > li[aria-selected]': { - backgroundColor: 'rgba(122, 107, 255, 0.34)', - color: '#f4f7ff', + backgroundColor: editorColors.tooltipItemSelectedBg, + color: editorColors.tooltipItemSelectedText, }, }) const updateListener = runtime.EditorView.updateListener.of(update => { diff --git a/src/index.html b/src/index.html index 3e89e02..568e10a 100644 --- a/src/index.html +++ b/src/index.html @@ -70,6 +70,43 @@

+ +
+ + +

diff --git a/src/styles.css b/src/styles.css index 545d8a2..d23ed2b 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,13 +1,177 @@ :root { - color-scheme: dark light; + color-scheme: dark; + --app-bg-start: #0f1115; + --app-bg-end: #12141b; + --shell-text: #e6e7eb; + --text-muted: #9aa3b2; + --text-subtle: #cdd4df; + --text-controls: #b9c1cf; + --panel-text: #e8ebf3; + --surface-app-header: rgba(16, 18, 24, 0.9); + --surface-panel: rgba(18, 20, 28, 0.9); + --surface-panel-header: rgba(15, 17, 23, 0.9); + --surface-chip: rgba(255, 255, 255, 0.08); + --surface-control: rgba(255, 255, 255, 0.06); + --surface-control-hover: rgba(255, 255, 255, 0.12); + --surface-select: rgba(255, 255, 255, 0.08); + --surface-select-option: #151a25; + --surface-preview: #12141c; + --surface-overlay: rgba(10, 12, 18, 0.72); + --surface-tooltip: rgba(9, 12, 20, 0.96); + --surface-dialog: rgba(13, 16, 24, 0.97); + --surface-loading: rgba(7, 10, 16, 0.82); + --surface-loading-card: rgba(13, 16, 24, 0.96); + --border-subtle: rgba(255, 255, 255, 0.08); + --border-control: rgba(255, 255, 255, 0.16); + --border-strong: rgba(255, 255, 255, 0.18); + --border-tooltip: rgba(255, 255, 255, 0.12); + --border-loading-card: rgba(255, 255, 255, 0.12); + --accent: #7a6bff; + --accent-rgb: 122, 107, 255; + --accent-soft: rgba(122, 107, 255, 0.2); + --accent-soft-hover: rgba(122, 107, 255, 0.35); + --accent-strong: rgba(122, 107, 255, 0.8); + --focus-ring: rgba(122, 107, 255, 0.85); + --danger-rgb: 250, 126, 138; + --danger-text: #ffe7ea; + --warning: #f6b56a; + --shadow-elev-1: rgba(0, 0, 0, 0.45); + --shadow-elev-2: rgba(0, 0, 0, 0.35); + --loading-spinner-border: rgba(255, 255, 255, 0.24); + --select-arrow-color: #d6def0; + --select-text: #f1f5ff; + --select-option-text: #eef3ff; + --select-option-disabled: #95a1b9; + --tooltip-text: #dfe6f7; + --dialog-text: #e7edf9; + --dialog-muted: #b8c3d9; + --loading-title: #edf1ff; + --loading-copy: #b7c1d4; + --icon-color: #d8e0ef; + --hint-icon: #dbe5ff; + --preview-spinner: #8f83ff; + --cm-keyword: #ff7fb3; + --cm-name: #e7ecf9; + --cm-property: #3fd6a6; + --cm-function: #8dc8ff; + --cm-constant: #7fd7ff; + --cm-definition: #dce4f6; + --cm-type: #8eb8ff; + --cm-number: #ffcb82; + --cm-operator: #d5def0; + --cm-string: #ffd38e; + --cm-comment: #94a2bb; + --cm-link: #88b6ff; + --cm-heading: #f2f5ff; + --cm-atom: #b8a8ff; + --cm-invalid: #ff8fa1; + --cm-text: #edf2ff; + --cm-caret: #f1f5ff; + --cm-gutter-bg: rgba(255, 255, 255, 0.045); + --cm-gutter-border: rgba(255, 255, 255, 0.13); + --cm-gutter-text: #98a8c4; + --cm-selection: rgba(122, 107, 255, 0.36); + --cm-active-line: rgba(255, 255, 255, 0.08); + --cm-focus-ring: rgba(122, 107, 255, 0.62); + --cm-tooltip-bg: #1b2233; + --cm-tooltip-text: #edf2ff; + --cm-tooltip-border: rgba(152, 168, 196, 0.32); + --cm-tooltip-item: #dce6fa; + --cm-tooltip-item-selected-bg: rgba(122, 107, 255, 0.34); + --cm-tooltip-item-selected-text: #f4f7ff; + --control-color-scheme: dark; font-family: 'Inter', system-ui, -apple-system, sans-serif; line-height: 1.4; - background: #0f1115; - color: #e6e7eb; + background: var(--app-bg-start); + color: var(--shell-text); +} + +:root[data-theme='light'] { + color-scheme: light; + --app-bg-start: #f3f6fc; + --app-bg-end: #e8eef8; + --shell-text: #1f2937; + --text-muted: #475569; + --text-subtle: #334155; + --text-controls: #4b5563; + --panel-text: #1f2937; + --surface-app-header: rgba(245, 248, 255, 0.92); + --surface-panel: rgba(255, 255, 255, 0.92); + --surface-panel-header: rgba(248, 251, 255, 0.95); + --surface-chip: rgba(15, 23, 42, 0.08); + --surface-control: rgba(15, 23, 42, 0.06); + --surface-control-hover: rgba(15, 23, 42, 0.12); + --surface-select: rgba(15, 23, 42, 0.06); + --surface-select-option: #ffffff; + --surface-preview: #f6f9ff; + --surface-overlay: rgba(240, 245, 255, 0.84); + --surface-tooltip: rgba(22, 34, 52, 0.96); + --surface-dialog: rgba(255, 255, 255, 0.98); + --surface-loading: rgba(234, 241, 252, 0.78); + --surface-loading-card: rgba(255, 255, 255, 0.97); + --border-subtle: rgba(15, 23, 42, 0.12); + --border-control: rgba(15, 23, 42, 0.22); + --border-strong: rgba(15, 23, 42, 0.24); + --border-tooltip: rgba(203, 213, 225, 0.7); + --border-loading-card: rgba(148, 163, 184, 0.42); + --accent: #5f57f0; + --accent-rgb: 95, 87, 240; + --accent-soft: rgba(95, 87, 240, 0.16); + --accent-soft-hover: rgba(95, 87, 240, 0.26); + --accent-strong: rgba(95, 87, 240, 0.72); + --focus-ring: rgba(95, 87, 240, 0.7); + --danger-rgb: 225, 66, 86; + --danger-text: #7f1d1d; + --warning: #b45309; + --shadow-elev-1: rgba(15, 23, 42, 0.2); + --shadow-elev-2: rgba(15, 23, 42, 0.16); + --loading-spinner-border: rgba(15, 23, 42, 0.24); + --select-arrow-color: #334155; + --select-text: #111827; + --select-option-text: #111827; + --select-option-disabled: #64748b; + --tooltip-text: #f8fafc; + --dialog-text: #1e293b; + --dialog-muted: #475569; + --loading-title: #0f172a; + --loading-copy: #334155; + --icon-color: #334155; + --hint-icon: #334155; + --preview-spinner: #5f57f0; + --cm-keyword: #b42364; + --cm-name: #0f172a; + --cm-property: #047857; + --cm-function: #1d4ed8; + --cm-constant: #0e7490; + --cm-definition: #1e293b; + --cm-type: #4338ca; + --cm-number: #b45309; + --cm-operator: #334155; + --cm-string: #92400e; + --cm-comment: #64748b; + --cm-link: #1d4ed8; + --cm-heading: #0f172a; + --cm-atom: #7c3aed; + --cm-invalid: #be123c; + --cm-text: #0f172a; + --cm-caret: #0f172a; + --cm-gutter-bg: rgba(15, 23, 42, 0.05); + --cm-gutter-border: rgba(15, 23, 42, 0.16); + --cm-gutter-text: #64748b; + --cm-selection: rgba(95, 87, 240, 0.2); + --cm-active-line: rgba(15, 23, 42, 0.08); + --cm-focus-ring: rgba(95, 87, 240, 0.5); + --cm-tooltip-bg: #f8fafc; + --cm-tooltip-text: #0f172a; + --cm-tooltip-border: rgba(15, 23, 42, 0.18); + --cm-tooltip-item: #1e293b; + --cm-tooltip-item-selected-bg: rgba(95, 87, 240, 0.2); + --cm-tooltip-item-selected-text: #0f172a; + --control-color-scheme: light; } * { @@ -30,7 +194,8 @@ body { margin: 0; padding: 0; min-height: 100vh; - background: linear-gradient(180deg, #0f1115 0%, #12141b 100%); + background: linear-gradient(180deg, var(--app-bg-start) 0%, var(--app-bg-end) 100%); + color: var(--shell-text); } .app-header { @@ -38,8 +203,8 @@ body { align-items: center; justify-content: space-between; padding: 20px 28px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(16, 18, 24, 0.9); + border-bottom: 1px solid var(--border-subtle); + background: var(--surface-app-header); position: sticky; top: 0; z-index: 10; @@ -62,7 +227,7 @@ body { .app-header p { margin: 0; - color: #9aa3b2; + color: var(--text-muted); font-size: 0.95rem; } @@ -70,8 +235,8 @@ body { font-size: 0.9rem; padding: 8px 12px; border-radius: 999px; - background: rgba(255, 255, 255, 0.08); - color: #cdd4df; + background: var(--surface-chip); + color: var(--text-subtle); } .app-grid { @@ -106,15 +271,23 @@ body { gap: 10px; } +.app-grid-theme-controls { + display: inline-flex; + gap: 10px; + margin-left: 8px; + padding-left: 10px; + border-left: 1px solid var(--border-subtle); +} + .layout-toggle { display: inline-grid; place-content: center; width: 36px; height: 36px; - border: 1px solid rgba(255, 255, 255, 0.16); + border: 1px solid var(--border-control); border-radius: 10px; - background: rgba(255, 255, 255, 0.06); - color: #d8e0ef; + background: var(--surface-control); + color: var(--icon-color); cursor: pointer; } @@ -127,17 +300,17 @@ body { } .layout-toggle:hover { - background: rgba(255, 255, 255, 0.12); + background: var(--surface-control-hover); } .layout-toggle[aria-pressed='true'] { - border-color: rgba(122, 107, 255, 0.65); - background: rgba(122, 107, 255, 0.2); - color: #f1f5ff; + border-color: color-mix(in srgb, var(--accent) 65%, transparent); + background: var(--accent-soft); + color: var(--select-text); } .layout-toggle:focus-visible { - outline: 2px solid rgba(122, 107, 255, 0.85); + outline: 2px solid var(--focus-ring); outline-offset: 2px; } @@ -171,11 +344,17 @@ body { .app-grid-layout-controls { justify-self: start; } + + .app-grid-theme-controls { + margin-left: 0; + padding-left: 0; + border-left: none; + } } .panel { - background: rgba(18, 20, 28, 0.9); - border: 1px solid rgba(255, 255, 255, 0.08); + background: var(--surface-panel); + border: 1px solid var(--border-subtle); border-radius: 14px; display: flex; flex-direction: column; @@ -192,8 +371,8 @@ body { align-items: center; justify-content: space-between; padding: 16px 18px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(15, 17, 23, 0.9); + border-bottom: 1px solid var(--border-subtle); + background: var(--surface-panel-header); position: relative; z-index: 2; } @@ -237,7 +416,7 @@ body { gap: 12px; align-items: center; font-size: 0.85rem; - color: #b9c1cf; + color: var(--text-controls); flex-wrap: wrap; } @@ -258,36 +437,36 @@ body { .controls select { appearance: none; -webkit-appearance: none; - background-color: rgba(255, 255, 255, 0.08); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%23d6def0' d='M1.2 0 5 3.8 8.8 0 10 1.2 5 6 0 1.2z'/%3E%3C/svg%3E"); + background-color: var(--surface-select); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='currentColor' d='M1.2 0 5 3.8 8.8 0 10 1.2 5 6 0 1.2z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; - color: #f1f5ff; - border: 1px solid rgba(255, 255, 255, 0.16); + color: var(--select-text); + border: 1px solid var(--border-control); border-radius: 8px; padding: 6px 30px 6px 10px; - color-scheme: dark; + color-scheme: var(--control-color-scheme); } .controls select:focus-visible { - outline: 2px solid rgba(122, 107, 255, 0.8); + outline: 2px solid var(--focus-ring); outline-offset: 1px; } .controls select option, .controls select optgroup { - background: #151a25; - color: #eef3ff; + background: var(--surface-select-option); + color: var(--select-option-text); } .controls select option:disabled { - color: #95a1b9; + color: var(--select-option-disabled); } textarea { flex: 1; background: transparent; - color: #e8ebf3; + color: var(--panel-text); border: none; padding: 16px 18px; font-family: 'JetBrains Mono', 'Fira Code', monospace; @@ -329,7 +508,7 @@ textarea:focus { .panel-footer { padding: 10px 18px 16px; font-size: 0.85rem; - color: #f6b56a; + color: var(--warning); } .preview-host { @@ -338,7 +517,7 @@ textarea:focus { padding: 18px; overflow: auto; position: relative; - background: #12141c; + background: var(--surface-preview); z-index: 1; } @@ -349,9 +528,9 @@ textarea:focus { display: grid; place-content: center; padding-top: 34px; - background: rgba(10, 12, 18, 0.72); + background: var(--surface-overlay); backdrop-filter: blur(2px); - color: #d9e0ee; + color: var(--panel-text); font-size: 0.88rem; letter-spacing: 0.01em; z-index: 2; @@ -366,8 +545,8 @@ textarea:focus { height: 20px; margin-left: -10px; border-radius: 50%; - border: 2px solid rgba(255, 255, 255, 0.28); - border-top-color: #8f83ff; + border: 2px solid var(--loading-spinner-border); + border-top-color: var(--preview-spinner); animation: preview-spin 0.7s linear infinite; z-index: 3; } @@ -395,7 +574,7 @@ textarea:focus { width: 34px; height: 24px; padding: 0; - border: 1px solid rgba(255, 255, 255, 0.18); + border: 1px solid var(--border-strong); border-radius: 8px; background: transparent; cursor: pointer; @@ -411,7 +590,7 @@ textarea:focus { } .toggle input { - accent-color: #7a6bff; + accent-color: var(--accent); } .hint-icon { @@ -420,8 +599,8 @@ textarea:focus { width: 16px; height: 16px; border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.34); - color: #dbe5ff; + border: 1px solid var(--border-strong); + color: var(--hint-icon); font-size: 0.68rem; font-weight: 700; line-height: 1; @@ -454,7 +633,7 @@ textarea:focus { right: 4px; border-left: 6px solid transparent; border-right: 6px solid transparent; - border-bottom: 6px solid rgba(9, 12, 20, 0.96); + border-bottom: 6px solid var(--surface-tooltip); transform: translateY(-4px); z-index: 31; } @@ -467,14 +646,14 @@ textarea:focus { width: min(320px, calc(100vw - 36px)); padding: 10px 12px; border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(9, 12, 20, 0.96); - color: #dfe6f7; + border: 1px solid var(--border-tooltip); + background: var(--surface-tooltip); + color: var(--tooltip-text); font-size: 0.78rem; font-weight: 500; line-height: 1.35; text-align: left; - box-shadow: 0 12px 24px rgba(0, 0, 0, 0.45); + box-shadow: 0 12px 24px var(--shadow-elev-1); transform: translateY(-4px); z-index: 30; } @@ -489,14 +668,14 @@ textarea:focus { } .shadow-hint:focus-visible { - outline: 2px solid rgba(122, 107, 255, 0.9); + outline: 2px solid var(--focus-ring); outline-offset: 2px; } .render-button { - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(122, 107, 255, 0.2); - color: #f1f5ff; + border: 1px solid var(--border-subtle); + background: var(--accent-soft); + color: var(--select-text); padding: 6px 14px; border-radius: 999px; cursor: pointer; @@ -504,13 +683,13 @@ textarea:focus { } .render-button:hover { - background: rgba(122, 107, 255, 0.35); + background: var(--accent-soft-hover); } .icon-button { - border: 1px solid rgba(255, 255, 255, 0.15); - background: rgba(255, 255, 255, 0.06); - color: #d8e0ef; + border: 1px solid var(--border-control); + background: var(--surface-control); + color: var(--icon-color); width: 32px; height: 32px; padding: 0; @@ -521,11 +700,11 @@ textarea:focus { } .icon-button:hover { - background: rgba(255, 255, 255, 0.13); + background: var(--surface-control-hover); } .icon-button:focus-visible { - outline: 2px solid rgba(122, 107, 255, 0.8); + outline: 2px solid var(--focus-ring); outline-offset: 1px; } @@ -548,15 +727,15 @@ textarea:focus { .confirm-dialog:modal { width: min(460px, calc(100vw - 32px)); - border: 1px solid rgba(255, 255, 255, 0.14); + border: 1px solid var(--border-tooltip); border-radius: 14px; - background: rgba(13, 16, 24, 0.97); - color: #e7edf9; - box-shadow: 0 18px 42px rgba(0, 0, 0, 0.45); + background: var(--surface-dialog); + color: var(--dialog-text); + box-shadow: 0 18px 42px var(--shadow-elev-1); } .confirm-dialog::backdrop { - background: rgba(6, 9, 15, 0.66); + background: var(--surface-loading); backdrop-filter: blur(2px); } @@ -572,7 +751,7 @@ textarea:focus { .confirm-dialog__form p { margin: 10px 0 0; - color: #b8c3d9; + color: var(--dialog-muted); font-size: 0.9rem; } @@ -595,27 +774,27 @@ textarea:focus { } .confirm-dialog__button--secondary { - border-color: rgba(255, 255, 255, 0.16); - background: rgba(255, 255, 255, 0.06); - color: #eef3ff; + border-color: var(--border-control); + background: var(--surface-control); + color: var(--select-option-text); } .confirm-dialog__button--secondary:hover { - background: rgba(255, 255, 255, 0.14); + background: var(--surface-control-hover); } .confirm-dialog__button--danger { - border-color: rgba(250, 126, 138, 0.48); - background: rgba(250, 126, 138, 0.2); - color: #ffe7ea; + border-color: rgba(var(--danger-rgb), 0.48); + background: rgba(var(--danger-rgb), 0.2); + color: var(--danger-text); } .confirm-dialog__button--danger:hover { - background: rgba(250, 126, 138, 0.32); + background: rgba(var(--danger-rgb), 0.32); } .confirm-dialog__button:focus-visible { - outline: 2px solid rgba(122, 107, 255, 0.88); + outline: 2px solid var(--focus-ring); outline-offset: 1px; } @@ -628,7 +807,7 @@ textarea:focus { .app-footer { padding: 12px 24px 24px; - color: #9aa3b2; + color: var(--text-muted); font-size: 0.85rem; } @@ -638,7 +817,7 @@ textarea:focus { z-index: 100; display: grid; place-items: center; - background: rgba(7, 10, 16, 0.82); + background: var(--surface-loading); backdrop-filter: blur(6px); transition: opacity 160ms ease; } @@ -651,9 +830,9 @@ textarea:focus { .cdn-loading-card { width: min(560px, calc(100% - 40px)); border-radius: 14px; - border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(13, 16, 24, 0.96); - box-shadow: 0 20px 42px rgba(0, 0, 0, 0.35); + border: 1px solid var(--border-loading-card); + background: var(--surface-loading-card); + box-shadow: 0 20px 42px var(--shadow-elev-2); padding: 22px 20px; text-align: center; } @@ -663,20 +842,20 @@ textarea:focus { height: 28px; margin: 0 auto 12px; border-radius: 50%; - border: 2px solid rgba(255, 255, 255, 0.24); - border-top-color: #8f83ff; + border: 2px solid var(--loading-spinner-border); + border-top-color: var(--preview-spinner); animation: preview-spin 0.7s linear infinite; } .cdn-loading-title { margin: 0; font-size: 1rem; - color: #edf1ff; + color: var(--loading-title); font-weight: 700; } .cdn-loading-copy { margin: 8px 0 0; font-size: 0.9rem; - color: #b7c1d4; + color: var(--loading-copy); } From b86c8643aec97c0f9b8249582141420fdf17cbba Mon Sep 17 00:00:00 2001 From: KCM Date: Mon, 16 Mar 2026 11:17:27 -0500 Subject: [PATCH 6/6] refactor: address pr comments. --- .husky/pre-push | 1 + package-lock.json | 3 +++ package.json | 6 +++++- src/index.html | 7 ++++++- src/styles.css | 2 -- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.husky/pre-push b/.husky/pre-push index 7c4e303..8137d91 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,3 +1,4 @@ #!/usr/bin/env sh +set -e npm run lint npm run build:esm diff --git a/package-lock.json b/package-lock.json index 4e91331..98df061 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,9 @@ "lint-staged": "^16.4.0", "oxlint": "^1.55.0", "prettier": "^3.8.1" + }, + "engines": { + "node": ">=22.22.1" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index 5415038..4cca463 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,10 @@ "author": "KCM ", "type": "module", "main": "index.js", + "engines": { + "node": ">=22.22.1" + }, + "engineStrict": true, "scripts": { "dev": "http-server . -a localhost -c-1 -o src/index.html", "prepare": "husky", @@ -40,7 +44,7 @@ "prettier": "^3.8.1" }, "lint-staged": { - "*": [ + "**/*": [ "prettier --write --ignore-unknown" ], "{src,scripts}/**/*.{js,mjs,cjs,jsx,ts,tsx}": [ diff --git a/src/index.html b/src/index.html index 568e10a..8e72a39 100644 --- a/src/index.html +++ b/src/index.html @@ -256,7 +256,12 @@

Preview

- +

Clear source?

This action will remove all text from the editor.

diff --git a/src/styles.css b/src/styles.css index d23ed2b..868ca87 100644 --- a/src/styles.css +++ b/src/styles.css @@ -38,7 +38,6 @@ --shadow-elev-1: rgba(0, 0, 0, 0.45); --shadow-elev-2: rgba(0, 0, 0, 0.35); --loading-spinner-border: rgba(255, 255, 255, 0.24); - --select-arrow-color: #d6def0; --select-text: #f1f5ff; --select-option-text: #eef3ff; --select-option-disabled: #95a1b9; @@ -130,7 +129,6 @@ --shadow-elev-1: rgba(15, 23, 42, 0.2); --shadow-elev-2: rgba(15, 23, 42, 0.16); --loading-spinner-border: rgba(15, 23, 42, 0.24); - --select-arrow-color: #334155; --select-text: #111827; --select-option-text: #111827; --select-option-disabled: #64748b;