Skip to content

Commit edb4911

Browse files
refactor: some improved ux.
1 parent 0dd4269 commit edb4911

4 files changed

Lines changed: 297 additions & 35 deletions

File tree

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
{
22
"name": "@knighted/develop",
33
"version": "0.1.0",
4-
"description": "Develop UI components directly in the browser.",
4+
"description": "Develop UI components directly in the browser using JSX and CSS.",
55
"keywords": [
66
"ui",
77
"components",
88
"realtime",
99
"browser",
10-
"development"
10+
"development",
11+
"jsx",
12+
"css",
13+
"importmap",
14+
"cdn"
1115
],
1216
"license": "MIT",
1317
"author": "KCM <knightedcodemonkey@gmail.com>",

src/app.js

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ const statusNode = document.getElementById('status')
55
const renderMode = document.getElementById('render-mode')
66
const autoRenderToggle = document.getElementById('auto-render')
77
const renderButton = document.getElementById('render-button')
8+
const copyComponentButton = document.getElementById('copy-component')
9+
const clearComponentButton = document.getElementById('clear-component')
810
const styleMode = document.getElementById('style-mode')
11+
const copyStylesButton = document.getElementById('copy-styles')
12+
const clearStylesButton = document.getElementById('clear-styles')
913
const shadowToggle = document.getElementById('shadow-toggle')
1014
const jsxEditor = document.getElementById('jsx-editor')
1115
const cssEditor = document.getElementById('css-editor')
12-
const previewHost = document.getElementById('preview-host')
1316
const styleWarning = document.getElementById('style-warning')
1417
const cdnLoading = document.getElementById('cdn-loading')
1518

@@ -66,6 +69,7 @@ button:focus-visible {
6669
jsxEditor.value = defaultJsx
6770
cssEditor.value = defaultCss
6871

72+
let previewHost = document.getElementById('preview-host')
6973
let jsxCodeEditor = null
7074
let cssCodeEditor = null
7175
let getJsxSource = () => jsxEditor.value
@@ -196,7 +200,87 @@ const debounceRender = () => {
196200
scheduled = setTimeout(renderPreview, 200)
197201
}
198202

199-
const getShadowRoot = () => {
203+
const setJsxSource = value => {
204+
if (jsxCodeEditor) {
205+
jsxCodeEditor.setValue(value)
206+
}
207+
jsxEditor.value = value
208+
}
209+
210+
const setCssSource = value => {
211+
if (cssCodeEditor) {
212+
cssCodeEditor.setValue(value)
213+
}
214+
cssEditor.value = value
215+
}
216+
217+
const clearComponentSource = () => {
218+
setJsxSource('')
219+
maybeRender()
220+
}
221+
222+
const clearStylesSource = () => {
223+
setCssSource('')
224+
maybeRender()
225+
}
226+
227+
const copyTextToClipboard = async text => {
228+
if (navigator.clipboard?.writeText) {
229+
await navigator.clipboard.writeText(text)
230+
return
231+
}
232+
233+
const fallbackInput = document.createElement('textarea')
234+
fallbackInput.value = text
235+
fallbackInput.setAttribute('readonly', 'true')
236+
fallbackInput.style.position = 'absolute'
237+
fallbackInput.style.left = '-9999px'
238+
document.body.append(fallbackInput)
239+
fallbackInput.select()
240+
const ok = document.execCommand('copy')
241+
fallbackInput.remove()
242+
243+
if (!ok) {
244+
throw new Error('Clipboard copy is not available in this browser context.')
245+
}
246+
}
247+
248+
const copyComponentSource = async () => {
249+
try {
250+
await copyTextToClipboard(getJsxSource())
251+
setStatus('Component copied')
252+
} catch {
253+
setStatus('Copy failed')
254+
}
255+
}
256+
257+
const copyStylesSource = async () => {
258+
try {
259+
await copyTextToClipboard(getCssSource())
260+
setStatus('Styles copied')
261+
} catch {
262+
setStatus('Copy failed')
263+
}
264+
}
265+
266+
const recreatePreviewHost = () => {
267+
const nextHost = document.createElement('div')
268+
nextHost.id = 'preview-host'
269+
nextHost.className = previewHost.className
270+
previewHost.replaceWith(nextHost)
271+
previewHost = nextHost
272+
}
273+
274+
const getRenderTarget = () => {
275+
if (!shadowToggle.checked && previewHost.shadowRoot) {
276+
/* ShadowRoot cannot be detached, so recreate the host for light DOM mode. */
277+
if (reactRoot) {
278+
reactRoot.unmount()
279+
reactRoot = null
280+
}
281+
recreatePreviewHost()
282+
}
283+
200284
if (shadowToggle.checked) {
201285
if (!previewHost.shadowRoot) {
202286
previewHost.attachShadow({ mode: 'open' })
@@ -605,7 +689,7 @@ const ensureReactRuntime = async () => {
605689

606690
const renderDom = async () => {
607691
const { jsx } = await ensureCoreRuntime()
608-
const target = getShadowRoot()
692+
const target = getRenderTarget()
609693
clearTarget(target)
610694
const compiledStyles = await compileStyles()
611695
applyStyles(target, compiledStyles.css)
@@ -627,7 +711,7 @@ const renderDom = async () => {
627711
}
628712

629713
const renderReact = async () => {
630-
const target = getShadowRoot()
714+
const target = getRenderTarget()
631715
clearTarget(target)
632716
const compiledStyles = await compileStyles()
633717
applyStyles(target, compiledStyles.css)
@@ -666,7 +750,7 @@ const renderPreview = async () => {
666750
setStatus('Rendered')
667751
} catch (error) {
668752
setStatus('Error')
669-
const target = getShadowRoot()
753+
const target = getRenderTarget()
670754
clearTarget(target)
671755
const message = document.createElement('pre')
672756
message.textContent = error instanceof Error ? error.message : String(error)
@@ -686,6 +770,10 @@ const maybeRender = () => {
686770
}
687771
}
688772

773+
const updateRenderButtonVisibility = () => {
774+
renderButton.hidden = autoRenderToggle.checked
775+
}
776+
689777
renderMode.addEventListener('change', maybeRender)
690778
styleMode.addEventListener('change', () => {
691779
if (cssCodeEditor) {
@@ -695,14 +783,24 @@ styleMode.addEventListener('change', () => {
695783
})
696784
shadowToggle.addEventListener('change', maybeRender)
697785
autoRenderToggle.addEventListener('change', () => {
786+
updateRenderButtonVisibility()
698787
if (autoRenderToggle.checked) {
699788
renderPreview()
700789
}
701790
})
702791
renderButton.addEventListener('click', renderPreview)
792+
copyComponentButton.addEventListener('click', () => {
793+
void copyComponentSource()
794+
})
795+
clearComponentButton.addEventListener('click', clearComponentSource)
796+
copyStylesButton.addEventListener('click', () => {
797+
void copyStylesSource()
798+
})
799+
clearStylesButton.addEventListener('click', clearStylesSource)
703800
jsxEditor.addEventListener('input', maybeRender)
704801
cssEditor.addEventListener('input', maybeRender)
705802

803+
updateRenderButtonVisibility()
706804
setStyleCompiling(false)
707805
setCdnLoading(true)
708806
void initializeCodeEditors()

src/index.html

Lines changed: 103 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,39 +25,115 @@ <h1>
2525

2626
<main class="app-grid">
2727
<section class="panel component-panel">
28-
<div class="panel-header">
29-
<h2>Component</h2>
30-
<div class="controls">
31-
<label>
32-
Render mode
33-
<select id="render-mode">
34-
<option value="dom" selected>DOM (default)</option>
35-
<option value="react">React</option>
36-
</select>
37-
</label>
38-
<label class="toggle">
39-
<input id="auto-render" type="checkbox" checked />
40-
Auto render
41-
</label>
42-
<button class="render-button" id="render-button" type="button">Render</button>
28+
<div class="panel-header panel-header--stack">
29+
<div class="panel-header-row">
30+
<h2>Component</h2>
31+
</div>
32+
<div class="panel-header-row panel-header-row--quick-actions">
33+
<div class="controls controls--actions controls--quick-actions">
34+
<button
35+
class="icon-button"
36+
id="copy-component"
37+
type="button"
38+
title="Copy component source"
39+
aria-label="Copy component source"
40+
>
41+
<svg viewBox="0 0 24 24" aria-hidden="true">
42+
<rect x="9" y="9" width="10" height="10" rx="2"></rect>
43+
<path
44+
d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"
45+
></path>
46+
</svg>
47+
</button>
48+
<button
49+
class="icon-button"
50+
id="clear-component"
51+
type="button"
52+
title="Clear component source"
53+
aria-label="Clear component source"
54+
>
55+
<svg viewBox="0 0 24 24" aria-hidden="true">
56+
<path d="M4 7h16"></path>
57+
<path d="M10 11v6"></path>
58+
<path d="M14 11v6"></path>
59+
<path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"></path>
60+
<path d="M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"></path>
61+
</svg>
62+
</button>
63+
</div>
64+
</div>
65+
<div class="panel-header-row panel-header-row--actions">
66+
<div class="controls controls--actions">
67+
<button class="render-button" id="render-button" type="button">
68+
Render
69+
</button>
70+
<label class="toggle">
71+
<input id="auto-render" type="checkbox" checked />
72+
Auto render
73+
</label>
74+
<label>
75+
<span class="sr-only">Render mode</span>
76+
<select id="render-mode">
77+
<option value="dom" selected>DOM</option>
78+
<option value="react">React</option>
79+
</select>
80+
</label>
81+
</div>
4382
</div>
4483
</div>
4584
<textarea id="jsx-editor" spellcheck="false"></textarea>
4685
</section>
4786

4887
<section class="panel styles-panel">
49-
<div class="panel-header">
50-
<h2>Styles</h2>
51-
<div class="controls">
52-
<label>
53-
Style mode
54-
<select id="style-mode">
55-
<option value="css" selected>Native CSS</option>
56-
<option value="module">CSS Modules</option>
57-
<option value="less">Less</option>
58-
<option value="sass">Sass</option>
59-
</select>
60-
</label>
88+
<div class="panel-header panel-header--stack">
89+
<div class="panel-header-row">
90+
<h2>Styles</h2>
91+
</div>
92+
<div class="panel-header-row panel-header-row--quick-actions">
93+
<div class="controls controls--actions controls--quick-actions">
94+
<button
95+
class="icon-button"
96+
id="copy-styles"
97+
type="button"
98+
title="Copy styles source"
99+
aria-label="Copy styles source"
100+
>
101+
<svg viewBox="0 0 24 24" aria-hidden="true">
102+
<rect x="9" y="9" width="10" height="10" rx="2"></rect>
103+
<path
104+
d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"
105+
></path>
106+
</svg>
107+
</button>
108+
<button
109+
class="icon-button"
110+
id="clear-styles"
111+
type="button"
112+
title="Clear styles source"
113+
aria-label="Clear styles source"
114+
>
115+
<svg viewBox="0 0 24 24" aria-hidden="true">
116+
<path d="M4 7h16"></path>
117+
<path d="M10 11v6"></path>
118+
<path d="M14 11v6"></path>
119+
<path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"></path>
120+
<path d="M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"></path>
121+
</svg>
122+
</button>
123+
</div>
124+
</div>
125+
<div class="panel-header-row panel-header-row--actions">
126+
<div class="controls controls--actions">
127+
<label>
128+
<span class="sr-only">Style mode</span>
129+
<select id="style-mode">
130+
<option value="css" selected>Native CSS</option>
131+
<option value="module">CSS Modules</option>
132+
<option value="less">Less</option>
133+
<option value="sass">Sass</option>
134+
</select>
135+
</label>
136+
</div>
61137
</div>
62138
</div>
63139
<textarea id="css-editor" spellcheck="false"></textarea>

0 commit comments

Comments
 (0)