11import { cdnImports , importFromCdnWithFallback } from './cdn.js'
22import { createCodeMirrorEditor } from './editor-codemirror.js'
3+ import { defaultCss , defaultJsx } from './defaults.js'
34
45const statusNode = document . getElementById ( 'status' )
56const renderMode = document . getElementById ( 'render-mode' )
@@ -15,56 +16,7 @@ const jsxEditor = document.getElementById('jsx-editor')
1516const cssEditor = document . getElementById ( 'css-editor' )
1617const styleWarning = document . getElementById ( 'style-warning' )
1718const 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
6921jsxEditor . value = defaultJsx
7022cssEditor . value = defaultCss
@@ -86,6 +38,8 @@ let compiledStylesCache = {
8638 value : null ,
8739}
8840let hasCompletedInitialRender = false
41+ let previewBackgroundColor = null
42+ const clipboardSupported = Boolean ( navigator . clipboard ?. writeText )
8943
9044const styleLabels = {
9145 css : 'Native CSS' ,
@@ -216,33 +170,24 @@ const setCssSource = value => {
216170
217171const clearComponentSource = ( ) => {
218172 setJsxSource ( '' )
219- maybeRender ( )
173+ if ( ! jsxCodeEditor ) {
174+ maybeRender ( )
175+ }
220176}
221177
222178const clearStylesSource = ( ) => {
223179 setCssSource ( '' )
224- maybeRender ( )
180+ if ( ! cssCodeEditor ) {
181+ maybeRender ( )
182+ }
225183}
226184
227185const 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
248193const 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 ( / ^ # [ \d a - f ] { 6 } $ / i. test ( colorValue ) ) {
219+ return colorValue . toLowerCase ( )
220+ }
221+
222+ if ( / ^ # [ \d a - 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+
266269const 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
274281const 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} )
791819renderButton . 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+ }
795831clearComponentButton . addEventListener ( 'click' , clearComponentSource )
796- copyStylesButton . addEventListener ( 'click' , ( ) => {
797- void copyStylesSource ( )
798- } )
799832clearStylesButton . addEventListener ( 'click' , clearStylesSource )
800833jsxEditor . addEventListener ( 'input' , maybeRender )
801834cssEditor . addEventListener ( 'input' , maybeRender )
802835
803836updateRenderButtonVisibility ( )
804837setStyleCompiling ( false )
805838setCdnLoading ( true )
839+ initializePreviewBackgroundPicker ( )
806840void initializeCodeEditors ( )
807841renderPreview ( )
0 commit comments