From edb4911750220981a0cab6af59ed607b7ba75c85 Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 15 Mar 2026 20:59:48 -0500 Subject: [PATCH 1/2] refactor: some improved ux. --- package.json | 8 ++- src/app.js | 108 ++++++++++++++++++++++++++++++++++++++-- src/index.html | 130 +++++++++++++++++++++++++++++++++++++++---------- src/styles.css | 86 +++++++++++++++++++++++++++++++- 4 files changed, 297 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index bc509c5..6924c0a 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,17 @@ { "name": "@knighted/develop", "version": "0.1.0", - "description": "Develop UI components directly in the browser.", + "description": "Develop UI components directly in the browser using JSX and CSS.", "keywords": [ "ui", "components", "realtime", "browser", - "development" + "development", + "jsx", + "css", + "importmap", + "cdn" ], "license": "MIT", "author": "KCM ", diff --git a/src/app.js b/src/app.js index 989877b..5dd89fc 100644 --- a/src/app.js +++ b/src/app.js @@ -5,11 +5,14 @@ const statusNode = document.getElementById('status') const renderMode = document.getElementById('render-mode') const autoRenderToggle = document.getElementById('auto-render') const renderButton = document.getElementById('render-button') +const copyComponentButton = document.getElementById('copy-component') +const clearComponentButton = document.getElementById('clear-component') const styleMode = document.getElementById('style-mode') +const copyStylesButton = document.getElementById('copy-styles') +const clearStylesButton = document.getElementById('clear-styles') const shadowToggle = document.getElementById('shadow-toggle') const jsxEditor = document.getElementById('jsx-editor') const cssEditor = document.getElementById('css-editor') -const previewHost = document.getElementById('preview-host') const styleWarning = document.getElementById('style-warning') const cdnLoading = document.getElementById('cdn-loading') @@ -66,6 +69,7 @@ button:focus-visible { jsxEditor.value = defaultJsx cssEditor.value = defaultCss +let previewHost = document.getElementById('preview-host') let jsxCodeEditor = null let cssCodeEditor = null let getJsxSource = () => jsxEditor.value @@ -196,7 +200,87 @@ const debounceRender = () => { scheduled = setTimeout(renderPreview, 200) } -const getShadowRoot = () => { +const setJsxSource = value => { + if (jsxCodeEditor) { + jsxCodeEditor.setValue(value) + } + jsxEditor.value = value +} + +const setCssSource = value => { + if (cssCodeEditor) { + cssCodeEditor.setValue(value) + } + cssEditor.value = value +} + +const clearComponentSource = () => { + setJsxSource('') + maybeRender() +} + +const clearStylesSource = () => { + setCssSource('') + maybeRender() +} + +const copyTextToClipboard = async text => { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text) + return + } + + const fallbackInput = document.createElement('textarea') + fallbackInput.value = text + fallbackInput.setAttribute('readonly', 'true') + fallbackInput.style.position = 'absolute' + fallbackInput.style.left = '-9999px' + document.body.append(fallbackInput) + fallbackInput.select() + const ok = document.execCommand('copy') + fallbackInput.remove() + + if (!ok) { + throw new Error('Clipboard copy is not available in this browser context.') + } +} + +const copyComponentSource = async () => { + try { + await copyTextToClipboard(getJsxSource()) + setStatus('Component copied') + } catch { + setStatus('Copy failed') + } +} + +const copyStylesSource = async () => { + try { + await copyTextToClipboard(getCssSource()) + setStatus('Styles copied') + } catch { + setStatus('Copy failed') + } +} + +const recreatePreviewHost = () => { + const nextHost = document.createElement('div') + nextHost.id = 'preview-host' + nextHost.className = previewHost.className + previewHost.replaceWith(nextHost) + previewHost = nextHost +} + +const getRenderTarget = () => { + if (!shadowToggle.checked && previewHost.shadowRoot) { + /* ShadowRoot cannot be detached, so recreate the host for light DOM mode. */ + if (reactRoot) { + reactRoot.unmount() + reactRoot = null + } + recreatePreviewHost() + } + if (shadowToggle.checked) { if (!previewHost.shadowRoot) { previewHost.attachShadow({ mode: 'open' }) @@ -605,7 +689,7 @@ const ensureReactRuntime = async () => { const renderDom = async () => { const { jsx } = await ensureCoreRuntime() - const target = getShadowRoot() + const target = getRenderTarget() clearTarget(target) const compiledStyles = await compileStyles() applyStyles(target, compiledStyles.css) @@ -627,7 +711,7 @@ const renderDom = async () => { } const renderReact = async () => { - const target = getShadowRoot() + const target = getRenderTarget() clearTarget(target) const compiledStyles = await compileStyles() applyStyles(target, compiledStyles.css) @@ -666,7 +750,7 @@ const renderPreview = async () => { setStatus('Rendered') } catch (error) { setStatus('Error') - const target = getShadowRoot() + const target = getRenderTarget() clearTarget(target) const message = document.createElement('pre') message.textContent = error instanceof Error ? error.message : String(error) @@ -686,6 +770,10 @@ const maybeRender = () => { } } +const updateRenderButtonVisibility = () => { + renderButton.hidden = autoRenderToggle.checked +} + renderMode.addEventListener('change', maybeRender) styleMode.addEventListener('change', () => { if (cssCodeEditor) { @@ -695,14 +783,24 @@ styleMode.addEventListener('change', () => { }) shadowToggle.addEventListener('change', maybeRender) autoRenderToggle.addEventListener('change', () => { + updateRenderButtonVisibility() if (autoRenderToggle.checked) { renderPreview() } }) renderButton.addEventListener('click', renderPreview) +copyComponentButton.addEventListener('click', () => { + void copyComponentSource() +}) +clearComponentButton.addEventListener('click', clearComponentSource) +copyStylesButton.addEventListener('click', () => { + void copyStylesSource() +}) +clearStylesButton.addEventListener('click', clearStylesSource) jsxEditor.addEventListener('input', maybeRender) cssEditor.addEventListener('input', maybeRender) +updateRenderButtonVisibility() setStyleCompiling(false) setCdnLoading(true) void initializeCodeEditors() diff --git a/src/index.html b/src/index.html index ed5af10..be17545 100644 --- a/src/index.html +++ b/src/index.html @@ -25,39 +25,115 @@

-
-

Component

-
- - - +
+
+

Component

+
+
+
+ + +
+
+
+
+ + + +
-
-

Styles

-
- +
+
+

Styles

+
+
+
+ + +
+
+
+
+ +
diff --git a/src/styles.css b/src/styles.css index 75fc4c8..f80f486 100644 --- a/src/styles.css +++ b/src/styles.css @@ -14,6 +14,18 @@ box-sizing: border-box; } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + body { margin: 0; padding: 0; @@ -126,6 +138,26 @@ body { font-size: 1rem; } +.panel-header--stack { + align-items: stretch; + gap: 10px; +} + +.panel-header-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.panel-header-row--actions { + justify-content: flex-start; +} + +.panel-header-row--quick-actions { + justify-content: flex-start; +} + .controls { display: flex; gap: 12px; @@ -135,6 +167,14 @@ body { flex-wrap: wrap; } +.controls--actions { + justify-content: flex-start; +} + +.controls--quick-actions { + gap: 10px; +} + .controls label { display: flex; flex-direction: column; @@ -262,7 +302,7 @@ textarea:focus { } } -.toggle { +.controls label.toggle { flex-direction: row; align-items: center; gap: 8px; @@ -287,6 +327,50 @@ textarea:focus { background: rgba(122, 107, 255, 0.35); } +.icon-button { + border: 1px solid rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.06); + color: #d8e0ef; + width: 32px; + height: 32px; + padding: 0; + border-radius: 999px; + cursor: pointer; + display: inline-grid; + place-content: center; +} + +.icon-button:hover { + background: rgba(255, 255, 255, 0.13); +} + +.icon-button:focus-visible { + outline: 2px solid rgba(122, 107, 255, 0.8); + outline-offset: 1px; +} + +.icon-button svg { + width: 16px; + height: 16px; + stroke: currentColor; + fill: none; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +@media (max-width: 900px) { + .panel-header-row { + align-items: flex-start; + flex-direction: column; + } + + .panel-header-row--actions, + .controls--actions { + justify-content: flex-start; + } +} + .app-footer { padding: 12px 24px 24px; color: #9aa3b2; From bbe5a02c3d19b8d55d6a598301f5ef14cc6dcdc8 Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 15 Mar 2026 23:43:48 -0500 Subject: [PATCH 2/2] refactor: address comments and more. --- docs/next-steps.md | 23 ++++++ src/app.js | 192 ++++++++++++++++++++++++++------------------- src/defaults.js | 114 +++++++++++++++++++++++++++ src/index.html | 12 +++ src/styles.css | 109 +++++++++++++++++++++++++ 5 files changed, 371 insertions(+), 79 deletions(-) create mode 100644 docs/next-steps.md create mode 100644 src/defaults.js diff --git a/docs/next-steps.md b/docs/next-steps.md new file mode 100644 index 0000000..cdb8b53 --- /dev/null +++ b/docs/next-steps.md @@ -0,0 +1,23 @@ +# Next Steps + +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** + - 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** + - 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/src/app.js b/src/app.js index 5dd89fc..92650bb 100644 --- a/src/app.js +++ b/src/app.js @@ -1,5 +1,6 @@ import { cdnImports, importFromCdnWithFallback } from './cdn.js' import { createCodeMirrorEditor } from './editor-codemirror.js' +import { defaultCss, defaultJsx } from './defaults.js' const statusNode = document.getElementById('status') const renderMode = document.getElementById('render-mode') @@ -15,56 +16,7 @@ const jsxEditor = document.getElementById('jsx-editor') const cssEditor = document.getElementById('css-editor') const styleWarning = document.getElementById('style-warning') const cdnLoading = document.getElementById('cdn-loading') - -const defaultJsx = [ - 'const Button = ({ onClick }) => {', - ' return ', - '}', - '', - 'const App = () => {', - ' const onClick = () => {', - " alert('clicked!')", - ' }', - '', - ' return
diff --git a/src/styles.css b/src/styles.css index f80f486..0170403 100644 --- a/src/styles.css +++ b/src/styles.css @@ -98,6 +98,7 @@ body { .preview-panel { grid-area: preview; + position: relative; } @media (max-width: 900px) { @@ -131,6 +132,8 @@ body { padding: 16px 18px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); background: rgba(15, 17, 23, 0.9); + position: relative; + z-index: 2; } .panel-header h2 { @@ -264,6 +267,8 @@ textarea:focus { padding: 18px; overflow: auto; position: relative; + background: #12141c; + z-index: 1; } .preview-host[data-style-compiling='true']::before { @@ -309,10 +314,114 @@ textarea:focus { cursor: pointer; } +.controls label.color-control { + flex-direction: row; + align-items: center; + gap: 8px; +} + +.controls input[type='color'] { + width: 34px; + height: 24px; + padding: 0; + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 8px; + background: transparent; + cursor: pointer; +} + +.controls input[type='color']::-webkit-color-swatch-wrapper { + padding: 0; +} + +.controls input[type='color']::-webkit-color-swatch { + border: none; + border-radius: 6px; +} + .toggle input { accent-color: #7a6bff; } +.hint-icon { + display: inline-grid; + place-content: center; + width: 16px; + height: 16px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.34); + color: #dbe5ff; + font-size: 0.68rem; + font-weight: 700; + line-height: 1; + opacity: 0.9; + background: transparent; +} + +.shadow-hint { + position: relative; + cursor: help; + border-width: 1px; + padding: 0; +} + +.shadow-hint::before, +.shadow-hint::after { + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: + opacity 120ms ease, + transform 120ms ease, + visibility 120ms ease; +} + +.shadow-hint::before { + content: ''; + position: absolute; + top: calc(100% + 4px); + right: 4px; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid rgba(9, 12, 20, 0.96); + transform: translateY(-4px); + z-index: 31; +} + +.shadow-hint::after { + content: attr(data-tooltip); + position: absolute; + top: calc(100% + 10px); + right: 0; + 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; + 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); + transform: translateY(-4px); + z-index: 30; +} + +.shadow-hint:hover::before, +.shadow-hint:hover::after, +.shadow-hint:focus-visible::before, +.shadow-hint:focus-visible::after { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.shadow-hint:focus-visible { + outline: 2px solid rgba(122, 107, 255, 0.9); + outline-offset: 2px; +} + .render-button { border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(122, 107, 255, 0.2);