@jschofield/play-ground 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @jschofield/play-ground
2
+
3
+ Interactive code editor and live preview web component built with [Lit](https://lit.dev/) and [CodeMirror 6](https://codemirror.net/). A split-pane playground for HTML/CSS/JS — like a lightweight CodePen you can embed anywhere.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @jschofield/play-ground
9
+ ```
10
+
11
+ ### Peer dependencies
12
+
13
+ This component requires the following peer dependencies:
14
+
15
+ ```bash
16
+ npm install lit prettier codemirror @codemirror/commands @codemirror/lang-html @codemirror/lang-javascript @codemirror/language @codemirror/state @codemirror/view @replit/codemirror-vim
17
+ ```
18
+
19
+ | Peer | Version |
20
+ |---|---|
21
+ | `lit` | `^3.0.0` |
22
+ | `prettier` | `^3.0.0` |
23
+ | `codemirror` | `^6.0.1` |
24
+ | `@codemirror/commands` | `^6.3.3` |
25
+ | `@codemirror/lang-html` | `^6.4.8` |
26
+ | `@codemirror/lang-javascript` | `^6.2.1` |
27
+ | `@codemirror/language` | `^6.10.1` |
28
+ | `@codemirror/state` | `^6.4.1` |
29
+ | `@codemirror/view` | `^6.24.1` |
30
+ | `@replit/codemirror-vim` | `^6.3.0` |
31
+
32
+ ## Usage
33
+
34
+ ```html
35
+ <script type="module">
36
+ import '@jschofield/play-ground';
37
+ </script>
38
+
39
+ <play-ground>
40
+ <template>
41
+ <style>
42
+ h1 { color: tomato; }
43
+ </style>
44
+ <h1>Hello!</h1>
45
+ </template>
46
+ </play-ground>
47
+ ```
48
+
49
+ ### Inline HTML attribute
50
+
51
+ ```html
52
+ <play-ground html="<h1>Quick example</h1>"></play-ground>
53
+ ```
54
+
55
+ ### Code folding
56
+
57
+ Collapse specific lines on initial load (useful for hiding boilerplate):
58
+
59
+ ```html
60
+ <play-ground fold="1,5">
61
+ <template>
62
+ <!-- line 1 is folded -->
63
+ ...
64
+ </template>
65
+ </play-ground>
66
+ ```
67
+
68
+ ### Without a bundler
69
+
70
+ This component has many CodeMirror peer dependencies, so a bundler is strongly recommended. If you must go without one, you'll need an import map for all peers:
71
+
72
+ ```html
73
+ <script type="importmap">
74
+ {
75
+ "imports": {
76
+ "lit": "https://esm.run/lit",
77
+ "lit/": "https://esm.run/lit/",
78
+ "prettier/standalone": "https://esm.run/prettier/standalone",
79
+ "prettier/plugins/": "https://esm.run/prettier/plugins/",
80
+ "codemirror": "https://esm.run/codemirror",
81
+ "@codemirror/commands": "https://esm.run/@codemirror/commands",
82
+ "@codemirror/lang-html": "https://esm.run/@codemirror/lang-html",
83
+ "@codemirror/lang-javascript": "https://esm.run/@codemirror/lang-javascript",
84
+ "@codemirror/language": "https://esm.run/@codemirror/language",
85
+ "@codemirror/state": "https://esm.run/@codemirror/state",
86
+ "@codemirror/view": "https://esm.run/@codemirror/view",
87
+ "@replit/codemirror-vim": "https://esm.run/@replit/codemirror-vim"
88
+ }
89
+ }
90
+ </script>
91
+ <script type="module" src="https://esm.run/@jschofield/play-ground"></script>
92
+ ```
93
+
94
+ ## Attributes
95
+
96
+ | Attribute | Type | Default | Description |
97
+ |---|---|---|---|
98
+ | `html` | `string` | `""` | Initial HTML content (alternative to `<template>`) |
99
+ | `fold` | `string` | `""` | Comma-separated line numbers to fold on load |
100
+
101
+ ## Features
102
+
103
+ - Live preview updates as you type (200ms debounce)
104
+ - Format button (Prettier with HTML/CSS/JS support)
105
+ - Vim mode toggle (persists across page via shared state)
106
+ - Sandboxed iframe preview (`allow-scripts allow-forms allow-same-origin`)
107
+ - Responsive: stacks vertically below 900px
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,171 @@
1
+ import{LitElement as e,css as t,html as n}from"lit";import*as r from"prettier/standalone";import i from"prettier/plugins/babel";import a from"prettier/plugins/postcss";import o from"prettier/plugins/estree";import s from"prettier/plugins/html";import{EditorView as c,drawSelection as l,gutters as u,keymap as d,lineNumbers as f}from"@codemirror/view";import{EditorState as p}from"@codemirror/state";import{basicSetup as m}from"codemirror";import{customElement as h,property as g,state as _}from"lit/decorators.js";import{defaultKeymap as v,indentWithTab as y}from"@codemirror/commands";import{html as b}from"@codemirror/lang-html";import{vim as x}from"@replit/codemirror-vim";import{foldCode as S}from"@codemirror/language";(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var C=class{static{this.STORAGE_KEY=`repl-playground-state`}static{this.STATE_CHANGE_EVENT=`repl-playground-state-change`}static{this.subscribers=new Set}static getState(){try{let e=localStorage.getItem(this.STORAGE_KEY);if(e)return JSON.parse(e)}catch(e){console.warn(`Failed to parse repl-playground state from localStorage:`,e)}return{vimMode:`disabled`}}static getVimMode(){return this.getState().vimMode===`enabled`}static setVimMode(e){let t={...this.getState(),vimMode:e?`enabled`:`disabled`};try{localStorage.setItem(this.STORAGE_KEY,JSON.stringify(t))}catch(e){console.warn(`Failed to save repl-playground state to localStorage:`,e)}this.notifySubscribers(t),window.dispatchEvent(new CustomEvent(this.STATE_CHANGE_EVENT,{detail:t}))}static subscribe(e){this.subscribers.add(e)}static unsubscribe(e){this.subscribers.delete(e)}static notifySubscribers(e){this.subscribers.forEach(t=>{try{t(e)}catch(e){console.warn(`Error in state change callback:`,e)}})}static initializeStorageListener(){window.addEventListener(`storage`,e=>{if(e.key===this.STORAGE_KEY&&e.newValue)try{let t=JSON.parse(e.newValue);this.notifySubscribers(t)}catch(e){console.warn(`Failed to parse state change from storage event:`,e)}}),window.addEventListener(this.STATE_CHANGE_EVENT,(()=>{}))}},w=(e,t)=>{let n;return((...r)=>{clearTimeout(n),n=setTimeout(()=>e(...r),t)})};function T(e,t,n,r){var i=arguments.length,a=i<3?t:r===null?r=Object.getOwnPropertyDescriptor(t,n):r,o;if(typeof Reflect==`object`&&typeof Reflect.decorate==`function`)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(o=e[s])&&(a=(i<3?o(a):i>3?o(t,n,a):o(t,n))||a);return i>3&&a&&Object.defineProperty(t,n,a),a}var E=[i,o,s,a],D=class extends e{constructor(...e){super(...e),this.docContents=``,this.vimMode=!1,this.html=``,this.fold=``,this.handleDocUpdate=()=>{this.docContents=this.editorView.state.doc.toString().trim(),this.updateIframeContent()},this.debouncedHandleDocUpdate=w(this.handleDocUpdate,200),this.onEditorUpdate=e=>{e.docChanged&&this.debouncedHandleDocUpdate()}}get template(){return this.querySelector(`template`)}connectedCallback(){super.connectedCallback(),C.initializeStorageListener(),this.vimMode=C.getVimMode(),this.stateChangeCallback=e=>{let t=e.vimMode===`enabled`;this.vimMode!==t&&(this.vimMode=t,this.buildEditor())},C.subscribe(this.stateChangeCallback)}async initEditorView(){let e=await this.getTemplate();this.docContents=e,this.buildEditor(e),this.fold&&this.foldLines()}buildEditor(e){let t=this.shadowRoot?.querySelector(`#editor`);if(!t)return;let n=[];this.vimMode&&n.push(x()),n.push(l()),n.push(m,d.of([...v,y]),b(),f(),u(),c.updateListener.of(this.onEditorUpdate));let r=p.create({doc:e??this.editorView?.state.doc.toString()??``,extensions:n});this.editorView&&this.editorView.destroy(),this.editorView=new c({state:r,parent:t})}foldLines(){this.fold.split(`,`).map(e=>Number.parseInt(e)).forEach(e=>{let t=this.editorView.state.doc.line(e);this.editorView.dispatch({selection:{anchor:t.from}}),S(this.editorView)})}async getTemplate(){if(this.html)return await this.format(this.html);let e=this.template?.innerHTML.trim();return await this.format(e)}async firstUpdated(){await this.initEditorView(),this.setupIframe()}disconnectedCallback(){super.disconnectedCallback(),this.stateChangeCallback&&C.unsubscribe(this.stateChangeCallback),this.editorView?.destroy()}setupIframe(){this.iframeContainer=this.shadowRoot?.querySelector(`#view-container`),this.iframeContainer&&this.updateIframeContent()}updateIframeContent(){if(!this.iframeContainer)return;let e=(this.docContents||`<!DOCTYPE html><p>Loading...</p>`).replace(`shadowrootmode="open."`,`shadowrootmode="open"`);this.iframe&&this.iframe.remove(),this.iframe=document.createElement(`iframe`),this.iframe.sandbox.add(`allow-scripts`,`allow-forms`,`allow-same-origin`),this.iframe.srcdoc=e,this.iframeContainer.appendChild(this.iframe)}async format(e=``){let t=await r.format(e,{parser:`html`,plugins:E}),n=[`open`,`disabled`,`checked`,`selected`,`readonly`,`required`,`autofocus`,`autoplay`,`controls`,`defer`,`hidden`,`loop`,`multiple`,`muted`,`reversed`,`scoped`,`async`,`default`,`visible`,`active`,`expanded`,`loading`,`invalid`,`complete`,`bool-attr`],i=t;return n.forEach(e=>{let t=RegExp(`\\s${e}=""`,`g`);i=i.replace(t,` ${e}`)}),i}toggleVimMode(){C.setVimMode(!this.vimMode)}async doFormat(){let e=await this.format(this.docContents),{state:t}=this.editorView,n=t.update({changes:{from:0,to:t.doc.length,insert:e}});this.editorView.update([n])}render(){return n`<div part="container" class="query-container">
2
+ <div class="editor-container">
3
+ <div class="editor-wrapper">
4
+ <div id="editor"></div>
5
+ <div class="controls">
6
+ <button
7
+ part="editor-format-button"
8
+ class="format-button"
9
+ type="button"
10
+ @click=${this.doFormat}
11
+ >
12
+ Format
13
+ </button>
14
+ <label class="vim-toggle">
15
+ <input
16
+ type="checkbox"
17
+ class="sr-only"
18
+ .checked=${this.vimMode}
19
+ @change=${this.toggleVimMode}
20
+ />
21
+ <span class="toggle-track ${this.vimMode?`active`:``}">
22
+ <span class="toggle-thumb"></span>
23
+ </span>
24
+ Vim mode
25
+ </label>
26
+ </div>
27
+ </div>
28
+ <div id="view-container"></div>
29
+ </div>
30
+ </div>`}static{this.styles=t`
31
+ :host {
32
+ display: block;
33
+ }
34
+
35
+ #editor {
36
+ font-size: 14px;
37
+ }
38
+
39
+ .query-container {
40
+ container-type: inline-size;
41
+ border: 1px solid #333;
42
+ border-radius: 8px;
43
+ overflow: hidden;
44
+ }
45
+
46
+ .editor-container {
47
+ position: relative;
48
+ display: flex;
49
+
50
+ & > * {
51
+ width: 50%;
52
+ }
53
+ }
54
+
55
+ #view-container {
56
+ display: flex;
57
+ border-left: 1px solid #333;
58
+ }
59
+
60
+ #view-container iframe {
61
+ width: 100%;
62
+ border: none;
63
+ }
64
+
65
+ .editor-wrapper {
66
+ display: flex;
67
+ flex-direction: column;
68
+ }
69
+
70
+ .controls {
71
+ display: flex;
72
+ gap: 8px;
73
+ padding: 6px 8px;
74
+ border-top: 1px solid #333;
75
+ background: var(--seasalt, #fafafa);
76
+ }
77
+
78
+ .controls button {
79
+ padding: 0.25em 1em 0.2em;
80
+ font-size: 0.833rem;
81
+ cursor: pointer;
82
+ border: none;
83
+ border-radius: 8px;
84
+ background: var(--gunmetal, #1b2f36);
85
+ color: var(--seasalt, #fafafa);
86
+ box-shadow: var(--shadow, 0 4px 8px rgba(0, 0, 0, 0.2));
87
+ transition: color 0.2s;
88
+ }
89
+
90
+ .controls button:hover,
91
+ .controls button:active {
92
+ background: var(--gradient, var(--gunmetal, #1b2f36));
93
+ color: var(--seasalt, #fafafa);
94
+ }
95
+
96
+ .controls button:focus-visible {
97
+ outline: 2px solid var(--raw-umber, #906b56);
98
+ outline-offset: 1px;
99
+ background: var(--gradient, var(--gunmetal, #1b2f36));
100
+ color: var(--seasalt, #fafafa);
101
+ }
102
+
103
+ .sr-only {
104
+ position: absolute;
105
+ width: 1px;
106
+ height: 1px;
107
+ padding: 0;
108
+ margin: -1px;
109
+ overflow: hidden;
110
+ clip: rect(0, 0, 0, 0);
111
+ white-space: nowrap;
112
+ border: 0;
113
+ }
114
+
115
+ .vim-toggle {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 6px;
119
+ margin-left: auto;
120
+ cursor: pointer;
121
+ font-size: 0.833rem;
122
+ color: var(--gunmetal, #1b2f36);
123
+ user-select: none;
124
+ }
125
+
126
+ .sr-only:focus-visible + .toggle-track {
127
+ outline: 2px solid var(--raw-umber, #906b56);
128
+ outline-offset: 2px;
129
+ }
130
+
131
+ .toggle-track {
132
+ display: inline-block;
133
+ position: relative;
134
+ width: 32px;
135
+ height: 18px;
136
+ border-radius: 9px;
137
+ background: #d5d5d5;
138
+ border: 1.5px solid var(--gunmetal, #1b2f36);
139
+ box-sizing: border-box;
140
+ transition: background 0.2s;
141
+ }
142
+
143
+ .toggle-track.active {
144
+ background: var(--gunmetal, #1b2f36);
145
+ }
146
+
147
+ .toggle-thumb {
148
+ position: absolute;
149
+ top: 1.5px;
150
+ left: 1.5px;
151
+ width: 12px;
152
+ height: 12px;
153
+ border-radius: 50%;
154
+ background: var(--seasalt, #fafafa);
155
+ transition: transform 0.2s;
156
+ }
157
+
158
+ .toggle-track.active .toggle-thumb {
159
+ transform: translateX(14px);
160
+ }
161
+
162
+ @container (max-width: 900px) {
163
+ .editor-container {
164
+ flex-direction: column;
165
+
166
+ & > * {
167
+ width: initial;
168
+ }
169
+ }
170
+ }
171
+ `}};T([_()],D.prototype,`docContents`,void 0),T([_()],D.prototype,`vimMode`,void 0),T([g()],D.prototype,`html`,void 0),T([g()],D.prototype,`fold`,void 0),D=T([h(`play-ground`)],D);
@@ -0,0 +1,73 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Vite + Lit + TS</title>
7
+ <script type="module" crossorigin src="/assets/index.js"></script>
8
+ </head>
9
+ <body>
10
+ <play-ground fold="7">
11
+ <template slot="html">
12
+ <h1>Hello World!</h1>
13
+ <html>
14
+ <img src="https://picsum.photos/id/12/300/200" />
15
+ <script>
16
+ const someJavascript = document.createElement("span");
17
+ </script>
18
+ <style>
19
+ h1 {
20
+ font-weight: bold;
21
+ }
22
+ </style>
23
+ </html>
24
+ </template>
25
+ </play-ground>
26
+ <play-ground fold="3,9">
27
+ <template>
28
+ <link href="./example.css" rel="stylesheet" />
29
+ <div>Div outside the shadow DOM!</div>
30
+ <my-element>
31
+ <template shadowrootmode="open.">
32
+ testing
33
+ <link href="./example.css" rel="stylesheet" />
34
+ </template>
35
+ </my-element>
36
+ <script>
37
+ class MyElement extends HTMLElement {
38
+ connectedCallback() {
39
+ const div = document.createElement("div");
40
+ div.innerHTML = `
41
+ Div inside the shadow DOM!
42
+ `;
43
+ this.shadowRoot.append(div);
44
+ }
45
+ }
46
+ customElements.define("my-element", MyElement);
47
+ </script>
48
+ </template>
49
+ </play-ground>
50
+
51
+ <play-ground
52
+ html='
53
+ <my-element>
54
+ Here is text being passed into the default slot!
55
+ </my-element>
56
+ <script type="module">
57
+ import {
58
+ LitElement, html,
59
+ } from "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js";
60
+
61
+ class MyElement extends LitElement {
62
+ render() {
63
+ return html`
64
+ <slot></slot>
65
+ `;
66
+ }
67
+ }
68
+
69
+ customElements.define("my-element", MyElement);
70
+ </script>
71
+ '></play-ground>
72
+ </body>
73
+ </html>
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@jschofield/play-ground",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "./dist/assets/index.js",
6
+ "exports": {
7
+ ".": "./dist/assets/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "description": "Interactive code editor/playground web component built with Lit and CodeMirror",
13
+ "author": "Jim Schofield",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/JimSchofield/jschof.dev",
18
+ "directory": "component/play-ground"
19
+ },
20
+ "dependencies": {},
21
+ "peerDependencies": {
22
+ "@codemirror/commands": "^6.3.3",
23
+ "@codemirror/lang-html": "^6.4.8",
24
+ "@codemirror/lang-javascript": "^6.2.1",
25
+ "@codemirror/language": "^6.10.1",
26
+ "@codemirror/state": "^6.4.1",
27
+ "@codemirror/view": "^6.24.1",
28
+ "@replit/codemirror-vim": "^6.3.0",
29
+ "codemirror": "^6.0.1",
30
+ "lit": "^3.0.0",
31
+ "prettier": "^3.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^6.0.2",
35
+ "vite": "^8.0.3"
36
+ },
37
+ "scripts": {
38
+ "dev": "vite",
39
+ "build": "tsc && vite build",
40
+ "preview": "vite preview"
41
+ }
42
+ }