Skip to content

Commit bbe5a02

Browse files
refactor: address comments and more.
1 parent edb4911 commit bbe5a02

5 files changed

Lines changed: 371 additions & 79 deletions

File tree

docs/next-steps.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Next Steps
2+
3+
Focused follow-up work for `@knighted/develop`.
4+
5+
1. **Grid-first header/layout cleanup**
6+
- Refactor panel header layout to use CSS Grid as the primary layout mechanism.
7+
- Reduce wrapper rows where possible and place controls explicitly in grid areas.
8+
- Preserve existing semantics and accessibility behavior while simplifying structure.
9+
- Validate desktop/mobile breakpoints and keep visual behavior parity.
10+
11+
2. **Style isolation behavior docs**
12+
- Document ShadowRoot on/off behavior and how style isolation changes in light DOM mode.
13+
- Clarify that light DOM preview can inherit shell styles and include recommendations for scoping.
14+
15+
3. **Preview UX polish**
16+
- Keep tooltip affordances for mode-specific behavior.
17+
- Continue tightening panel control alignment and spacing without introducing extra markup.
18+
19+
4. **Theming (light + dark)**
20+
- Keep the existing dark mode as the baseline and add a first-class light theme.
21+
- Move key colors to semantic CSS variables and define both theme palettes.
22+
- Ensure component panels, controls, editor chrome, preview shell, and tooltips all have complete light-mode coverage.
23+
- Verify contrast/accessibility across both themes and preserve visual hierarchy parity.

src/app.js

Lines changed: 113 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { cdnImports, importFromCdnWithFallback } from './cdn.js'
22
import { createCodeMirrorEditor } from './editor-codemirror.js'
3+
import { defaultCss, defaultJsx } from './defaults.js'
34

45
const statusNode = document.getElementById('status')
56
const renderMode = document.getElementById('render-mode')
@@ -15,56 +16,7 @@ const jsxEditor = document.getElementById('jsx-editor')
1516
const cssEditor = document.getElementById('css-editor')
1617
const styleWarning = document.getElementById('style-warning')
1718
const cdnLoading = document.getElementById('cdn-loading')
18-
19-
const defaultJsx = [
20-
'const Button = ({ onClick }) => {',
21-
' return <button onClick={onClick}>click me</button>',
22-
'}',
23-
'',
24-
'const App = () => {',
25-
' const onClick = () => {',
26-
" alert('clicked!')",
27-
' }',
28-
'',
29-
' return <Button onClick={onClick} />',
30-
'}',
31-
'',
32-
].join('\n')
33-
34-
const defaultCss = `button {
35-
appearance: none;
36-
border: 1px solid rgba(122, 107, 255, 0.55);
37-
background: linear-gradient(135deg, #7a6bff, #5f4dff);
38-
color: #fff;
39-
padding: 10px 16px;
40-
border-radius: 10px;
41-
font-weight: 700;
42-
letter-spacing: 0.01em;
43-
cursor: pointer;
44-
transition:
45-
transform 120ms ease,
46-
box-shadow 120ms ease,
47-
filter 120ms ease;
48-
box-shadow:
49-
0 8px 20px rgba(95, 77, 255, 0.28),
50-
inset 0 1px 0 rgba(255, 255, 255, 0.18);
51-
}
52-
53-
button:hover {
54-
transform: translateY(-1px);
55-
filter: brightness(1.06);
56-
}
57-
58-
button:active {
59-
transform: translateY(0);
60-
filter: brightness(0.98);
61-
}
62-
63-
button:focus-visible {
64-
outline: 2px solid #9d91ff;
65-
outline-offset: 2px;
66-
}
67-
`
19+
const previewBgColorInput = document.getElementById('preview-bg-color')
6820

6921
jsxEditor.value = defaultJsx
7022
cssEditor.value = defaultCss
@@ -86,6 +38,8 @@ let compiledStylesCache = {
8638
value: null,
8739
}
8840
let hasCompletedInitialRender = false
41+
let previewBackgroundColor = null
42+
const clipboardSupported = Boolean(navigator.clipboard?.writeText)
8943

9044
const styleLabels = {
9145
css: 'Native CSS',
@@ -216,33 +170,24 @@ const setCssSource = value => {
216170

217171
const clearComponentSource = () => {
218172
setJsxSource('')
219-
maybeRender()
173+
if (!jsxCodeEditor) {
174+
maybeRender()
175+
}
220176
}
221177

222178
const clearStylesSource = () => {
223179
setCssSource('')
224-
maybeRender()
180+
if (!cssCodeEditor) {
181+
maybeRender()
182+
}
225183
}
226184

227185
const copyTextToClipboard = async text => {
228-
if (navigator.clipboard?.writeText) {
229-
await navigator.clipboard.writeText(text)
230-
return
186+
if (!clipboardSupported) {
187+
throw new Error('Clipboard API is not available in this browser context.')
231188
}
232189

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-
}
190+
await navigator.clipboard.writeText(text)
246191
}
247192

248193
const copyComponentSource = async () => {
@@ -263,12 +208,74 @@ const copyStylesSource = async () => {
263208
}
264209
}
265210

211+
const toHexChannel = value => value.toString(16).padStart(2, '0')
212+
213+
const normalizeColorToHex = colorValue => {
214+
if (typeof colorValue !== 'string' || colorValue.length === 0) {
215+
return '#12141c'
216+
}
217+
218+
if (/^#[\da-f]{6}$/i.test(colorValue)) {
219+
return colorValue.toLowerCase()
220+
}
221+
222+
if (/^#[\da-f]{3}$/i.test(colorValue)) {
223+
return colorValue
224+
.slice(1)
225+
.split('')
226+
.map(channel => channel + channel)
227+
.join('')
228+
.replace(/^/, '#')
229+
.toLowerCase()
230+
}
231+
232+
const channels = colorValue.match(/\d+/g)
233+
if (!channels || channels.length < 3) {
234+
return '#12141c'
235+
}
236+
237+
const [red, green, blue] = channels.slice(0, 3).map(value => Number.parseInt(value, 10))
238+
if ([red, green, blue].some(value => Number.isNaN(value))) {
239+
return '#12141c'
240+
}
241+
242+
return `#${toHexChannel(red)}${toHexChannel(green)}${toHexChannel(blue)}`
243+
}
244+
245+
const applyPreviewBackgroundColor = color => {
246+
if (!previewHost) {
247+
return
248+
}
249+
250+
previewHost.style.backgroundColor = color
251+
}
252+
253+
const initializePreviewBackgroundPicker = () => {
254+
if (!previewBgColorInput || !previewHost) {
255+
return
256+
}
257+
258+
const initialColor = normalizeColorToHex(getComputedStyle(previewHost).backgroundColor)
259+
previewBackgroundColor = initialColor
260+
previewBgColorInput.value = initialColor
261+
applyPreviewBackgroundColor(initialColor)
262+
263+
previewBgColorInput.addEventListener('input', () => {
264+
previewBackgroundColor = previewBgColorInput.value
265+
applyPreviewBackgroundColor(previewBackgroundColor)
266+
})
267+
}
268+
266269
const recreatePreviewHost = () => {
267270
const nextHost = document.createElement('div')
268271
nextHost.id = 'preview-host'
269272
nextHost.className = previewHost.className
270273
previewHost.replaceWith(nextHost)
271274
previewHost = nextHost
275+
276+
if (previewBackgroundColor) {
277+
applyPreviewBackgroundColor(previewBackgroundColor)
278+
}
272279
}
273280

274281
const getRenderTarget = () => {
@@ -318,7 +325,10 @@ const applyStyles = (target, cssText) => {
318325
if (!target) return
319326

320327
const styleTag = document.createElement('style')
321-
styleTag.textContent = cssText
328+
const isShadowTarget = target instanceof ShadowRoot
329+
styleTag.textContent = isShadowTarget
330+
? cssText
331+
: `@scope (#preview-host) {\n${cssText}\n}`
322332
target.append(styleTag)
323333
}
324334

@@ -638,15 +648,33 @@ const evaluateUserModule = async (helpers = {}) => {
638648
throw error
639649
}
640650

641-
const transpiledUserCode = transpileJsxSource(userCode, {
642-
sourceType: 'script',
643-
}).code
651+
const transpileMode = helpers.React && helpers.reactJsx ? 'react' : 'dom'
652+
const transpileOptionsByMode = {
653+
dom: {
654+
sourceType: 'script',
655+
createElement: 'jsx.createElement',
656+
fragment: 'jsx.Fragment',
657+
},
658+
react: {
659+
sourceType: 'script',
660+
createElement: 'React.createElement',
661+
fragment: 'React.Fragment',
662+
},
663+
}
664+
const transpiledUserCode = transpileJsxSource(
665+
userCode,
666+
transpileOptionsByMode[transpileMode],
667+
).code
644668
const moduleFactory = createUserModuleFactory(transpiledUserCode)
645669

646670
if (helpers.React && helpers.reactJsx) {
647671
return moduleFactory(helpers.jsx ?? jsx, helpers.reactJsx, helpers.React)
648672
}
649673

674+
if (transpileMode === 'dom') {
675+
return moduleFactory(helpers.jsx ?? jsx, helpers.reactJsx, helpers.React)
676+
}
677+
650678
const { React, reactJsx } = await ensureReactRuntime()
651679
return moduleFactory(helpers.jsx ?? jsx, helpers.reactJsx ?? reactJsx, React)
652680
}
@@ -717,7 +745,7 @@ const renderReact = async () => {
717745
applyStyles(target, compiledStyles.css)
718746

719747
const { reactJsx, createRoot, React } = await ensureReactRuntime()
720-
const renderFn = await evaluateUserModule({ jsx: reactJsx, reactJsx })
748+
const renderFn = await evaluateUserModule({ jsx: reactJsx, reactJsx, React })
721749
if (!renderFn) {
722750
throw new Error('Expected a render() function or a component named App/View.')
723751
}
@@ -789,19 +817,25 @@ autoRenderToggle.addEventListener('change', () => {
789817
}
790818
})
791819
renderButton.addEventListener('click', renderPreview)
792-
copyComponentButton.addEventListener('click', () => {
793-
void copyComponentSource()
794-
})
820+
if (clipboardSupported) {
821+
copyComponentButton.addEventListener('click', () => {
822+
void copyComponentSource()
823+
})
824+
copyStylesButton.addEventListener('click', () => {
825+
void copyStylesSource()
826+
})
827+
} else {
828+
copyComponentButton.hidden = true
829+
copyStylesButton.hidden = true
830+
}
795831
clearComponentButton.addEventListener('click', clearComponentSource)
796-
copyStylesButton.addEventListener('click', () => {
797-
void copyStylesSource()
798-
})
799832
clearStylesButton.addEventListener('click', clearStylesSource)
800833
jsxEditor.addEventListener('input', maybeRender)
801834
cssEditor.addEventListener('input', maybeRender)
802835

803836
updateRenderButtonVisibility()
804837
setStyleCompiling(false)
805838
setCdnLoading(true)
839+
initializePreviewBackgroundPicker()
806840
void initializeCodeEditors()
807841
renderPreview()

0 commit comments

Comments
 (0)