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..8137d91 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +set -e +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/docs/next-steps.md b/docs/next-steps.md index cdb8b53..0c103af 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -2,22 +2,10 @@ Focused follow-up work for `@knighted/develop`. -1. **Grid-first header/layout cleanup** - - Refactor panel header layout to use CSS Grid as the primary layout mechanism. - - Reduce wrapper rows where possible and place controls explicitly in grid areas. - - Preserve existing semantics and accessibility behavior while simplifying structure. - - Validate desktop/mobile breakpoints and keep visual behavior parity. - -2. **Style isolation behavior docs** +1. **Style isolation behavior docs** - Document ShadowRoot on/off behavior and how style isolation changes in light DOM mode. - Clarify that light DOM preview can inherit shell styles and include recommendations for scoping. -3. **Preview UX polish** +2. **Preview UX polish** - Keep tooltip affordances for mode-specific behavior. - Continue tightening panel control alignment and spacing without introducing extra markup. - -4. **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/package-lock.json b/package-lock.json index 89e44fd..98df061 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,14 @@ "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" + }, + "engines": { + "node": ">=22.22.1" } }, "node_modules/@babel/code-frame": { @@ -944,6 +949,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 +1337,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 +1391,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 +1479,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 +1845,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 +1884,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 +1976,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 +2085,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 +2548,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 +2631,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 +2758,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 +2847,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 +2963,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 +3072,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..4cca463 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,13 @@ "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", "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 +37,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/app.js b/src/app.js index 92650bb..35662f7 100644 --- a/src/app.js +++ b/src/app.js @@ -3,6 +3,9 @@ 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 appThemeButtons = document.querySelectorAll('[data-app-theme]') const renderMode = document.getElementById('render-mode') const autoRenderToggle = document.getElementById('auto-render') const renderButton = document.getElementById('render-button') @@ -17,6 +20,9 @@ const cssEditor = document.getElementById('css-editor') const styleWarning = document.getElementById('style-warning') const cdnLoading = document.getElementById('cdn-loading') const previewBgColorInput = document.getElementById('preview-bg-color') +const clearConfirmDialog = document.getElementById('clear-confirm-dialog') +const clearConfirmTitle = document.getElementById('clear-confirm-title') +const clearConfirmCopy = document.getElementById('clear-confirm-copy') jsxEditor.value = defaultJsx cssEditor.value = defaultCss @@ -37,9 +43,13 @@ let compiledStylesCache = { key: null, value: null, } +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', @@ -138,6 +148,78 @@ 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 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 @@ -170,6 +252,7 @@ const setCssSource = value => { const clearComponentSource = () => { setJsxSource('') + setStatus('Component cleared') if (!jsxCodeEditor) { maybeRender() } @@ -177,11 +260,45 @@ const clearComponentSource = () => { const clearStylesSource = () => { setCssSource('') + setStatus('Styles cleared') if (!cssCodeEditor) { maybeRender() } } +const confirmClearSource = ({ label, onConfirm }) => { + const supportsModalDialog = + clearConfirmDialog instanceof HTMLDialogElement && + typeof clearConfirmDialog.showModal === 'function' + + if (!supportsModalDialog) { + if ( + window.confirm( + `Clear ${label.toLowerCase()} source? This action will remove all text from the editor.`, + ) + ) { + onConfirm() + } + return + } + + if (clearConfirmDialog.open) { + return + } + + if (clearConfirmTitle) { + clearConfirmTitle.textContent = `Clear ${label} source?` + } + + if (clearConfirmCopy) { + clearConfirmCopy.textContent = + 'This action will remove all text from the editor. This cannot be undone.' + } + + pendingClearAction = onConfirm + clearConfirmDialog.showModal() +} + const copyTextToClipboard = async text => { if (!clipboardSupported) { throw new Error('Clipboard API is not available in this browser context.') @@ -247,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 = () => { @@ -256,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) }) } @@ -273,9 +409,7 @@ const recreatePreviewHost = () => { previewHost.replaceWith(nextHost) previewHost = nextHost - if (previewBackgroundColor) { - applyPreviewBackgroundColor(previewBackgroundColor) - } + applyPreviewBackgroundColor(previewBackgroundColor) } const getRenderTarget = () => { @@ -828,11 +962,54 @@ if (clipboardSupported) { copyComponentButton.hidden = true copyStylesButton.hidden = true } -clearComponentButton.addEventListener('click', clearComponentSource) -clearStylesButton.addEventListener('click', clearStylesSource) +if (clearConfirmDialog instanceof HTMLDialogElement) { + clearConfirmDialog.addEventListener('close', () => { + if (clearConfirmDialog.returnValue === 'confirm') { + pendingClearAction?.() + } + pendingClearAction = null + }) +} + +clearComponentButton.addEventListener('click', () => { + confirmClearSource({ + label: 'Component', + onConfirm: clearComponentSource, + }) +}) + +clearStylesButton.addEventListener('click', () => { + confirmClearSource({ + label: 'Styles', + onConfirm: clearStylesSource, + }) +}) 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) + }) +} + +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) setCdnLoading(true) 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 d5a18c7..8e72a39 100644 --- a/src/index.html +++ b/src/index.html @@ -18,51 +18,130 @@

>@knighted/develop

-

Develop UI components directly in the browser with live previews.

+

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

Idle
+
+ + + + +
+ + +
+
+
-
-
-

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 0170403..868ca87 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,13 +1,175 @@ :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-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-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 +192,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 +201,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 +225,7 @@ body { .app-header p { margin: 0; - color: #9aa3b2; + color: var(--text-muted); font-size: 0.95rem; } @@ -70,20 +233,85 @@ 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 { 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; +} + +.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 var(--border-control); + border-radius: 10px; + background: var(--surface-control); + color: var(--icon-color); + cursor: pointer; +} + +.layout-toggle svg { + width: 18px; + height: 18px; + fill: none; + stroke: currentColor; + stroke-width: 1.5; +} + +.layout-toggle:hover { + background: var(--surface-control-hover); +} + +.layout-toggle[aria-pressed='true'] { + 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 var(--focus-ring); + outline-offset: 2px; +} + .component-panel { grid-area: component; max-height: min(64vh, 620px); @@ -105,15 +333,26 @@ body { .app-grid { grid-template-columns: minmax(0, 1fr); grid-template-areas: + 'layout-controls' 'component' 'styles' 'preview'; } + + .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; @@ -130,8 +369,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; } @@ -141,23 +380,32 @@ body { font-size: 1rem; } -.panel-header--stack { - align-items: stretch; - gap: 10px; +.panel-header--grid { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: + 'title quick' + 'actions actions'; + align-items: start; + column-gap: 12px; + row-gap: 10px; } -.panel-header-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; +.panel-header--grid h2 { + grid-area: title; } -.panel-header-row--actions { - justify-content: flex-start; +.panel-header-quick-actions { + grid-area: quick; + justify-self: center; +} + +.panel-header-main-actions { + grid-area: actions; + justify-self: start; } -.panel-header-row--quick-actions { +.panel-header-main-actions .controls { justify-content: flex-start; } @@ -166,7 +414,7 @@ body { gap: 12px; align-items: center; font-size: 0.85rem; - color: #b9c1cf; + color: var(--text-controls); flex-wrap: wrap; } @@ -187,36 +435,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; @@ -258,16 +506,16 @@ textarea:focus { .panel-footer { padding: 10px 18px 16px; font-size: 0.85rem; - color: #f6b56a; + color: var(--warning); } .preview-host { - flex: 0 1 auto; + flex: 1 1 auto; min-height: 180px; padding: 18px; overflow: auto; position: relative; - background: #12141c; + background: var(--surface-preview); z-index: 1; } @@ -278,9 +526,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; @@ -295,8 +543,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; } @@ -324,7 +572,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; @@ -340,7 +588,7 @@ textarea:focus { } .toggle input { - accent-color: #7a6bff; + accent-color: var(--accent); } .hint-icon { @@ -349,8 +597,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; @@ -383,7 +631,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; } @@ -396,14 +644,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; } @@ -418,14 +666,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; @@ -433,13 +681,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; @@ -450,11 +698,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; } @@ -468,13 +716,88 @@ textarea:focus { stroke-linejoin: round; } -@media (max-width: 900px) { - .panel-header-row { - align-items: flex-start; - flex-direction: column; - } +.confirm-dialog { + border: none; + padding: 0; + background: transparent; + color: inherit; +} + +.confirm-dialog:modal { + width: min(460px, calc(100vw - 32px)); + border: 1px solid var(--border-tooltip); + border-radius: 14px; + background: var(--surface-dialog); + color: var(--dialog-text); + box-shadow: 0 18px 42px var(--shadow-elev-1); +} + +.confirm-dialog::backdrop { + background: var(--surface-loading); + backdrop-filter: blur(2px); +} - .panel-header-row--actions, +.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: var(--dialog-muted); + 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: var(--border-control); + background: var(--surface-control); + color: var(--select-option-text); +} + +.confirm-dialog__button--secondary:hover { + background: var(--surface-control-hover); +} + +.confirm-dialog__button--danger { + 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(var(--danger-rgb), 0.32); +} + +.confirm-dialog__button:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 1px; +} + +@media (max-width: 900px) { + .panel-header-main-actions .controls, .controls--actions { justify-content: flex-start; } @@ -482,7 +805,7 @@ textarea:focus { .app-footer { padding: 12px 24px 24px; - color: #9aa3b2; + color: var(--text-muted); font-size: 0.85rem; } @@ -492,7 +815,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; } @@ -505,9 +828,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; } @@ -517,20 +840,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); }