Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
Son Of Anton
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Son Of Anton
Commits
347bafba
Commit
347bafba
authored
Mar 30, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Create frontend/src/components/UIPreview.jsx via Son of Anton
parent
9442fe1d
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
641 additions
and
0 deletions
+641
-0
UIPreview.jsx
frontend/src/components/UIPreview.jsx
+641
-0
No files found.
frontend/src/components/UIPreview.jsx
0 → 100644
View file @
347bafba
import
React
,
{
useState
,
useEffect
,
useRef
,
useCallback
,
useMemo
}
from
"react"
;
import
{
X
,
Monitor
,
Tablet
,
Smartphone
,
Maximize2
,
RefreshCw
,
Download
,
Copy
,
Check
,
Code2
,
Eye
,
Columns
,
ZoomIn
,
ZoomOut
,
ExternalLink
,
RotateCcw
,
Sun
,
Moon
,
Paintbrush
,
}
from
"lucide-react"
;
/* ═══════════════════════════════════════════════════
VIEWPORT PRESETS
═══════════════════════════════════════════════════ */
const
VIEWPORTS
=
[
{
id
:
"desktop"
,
label
:
"Desktop"
,
width
:
1440
,
height
:
900
,
icon
:
Monitor
},
{
id
:
"tablet"
,
label
:
"Tablet"
,
width
:
768
,
height
:
1024
,
icon
:
Tablet
},
{
id
:
"mobile"
,
label
:
"Mobile"
,
width
:
375
,
height
:
812
,
icon
:
Smartphone
},
{
id
:
"full"
,
label
:
"Responsive"
,
width
:
null
,
height
:
null
,
icon
:
Maximize2
},
];
const
ZOOM_LEVELS
=
[
25
,
50
,
75
,
100
,
125
,
150
,
200
];
/* ═══════════════════════════════════════════════════
HTML BUILDER — Combines code blocks into a page
═══════════════════════════════════════════════════ */
const
TAILWIND_RE
=
/class="
[^
"
]
*
(?:
flex|grid|p-
\d
|m-
\d
|bg-|text-
(?:
sm|lg|xl|
\w
+-
\d)
|rounded|shadow|border|w-|h-|gap-|space-|items-|justify-
)
/
;
const
REACT_IMPORT_RE
=
/
(?:
import
\s
+.*from
\s
+
[
'"
]
react|React
\.
|useState|useEffect|useRef|jsx|<
\w
+
[
A-Z
])
/
;
export
function
buildPreviewHTML
(
blocks
)
{
if
(
!
blocks
||
!
blocks
.
length
)
return
null
;
const
htmlBlocks
=
blocks
.
filter
(
(
b
)
=>
b
.
language
===
"html"
||
b
.
filename
?.
match
(
/
\.
html
?
$/
)
);
const
cssBlocks
=
blocks
.
filter
(
(
b
)
=>
b
.
language
===
"css"
||
b
.
language
===
"scss"
||
b
.
filename
?.
match
(
/
\.
s
?
css$/
)
);
const
jsBlocks
=
blocks
.
filter
(
(
b
)
=>
[
"javascript"
,
"js"
].
includes
(
b
.
language
)
||
(
b
.
filename
?.
match
(
/
\.
m
?
js$/
)
&&
!
b
.
filename
?.
match
(
/
\.
jsx$/
))
);
const
reactBlocks
=
blocks
.
filter
(
(
b
)
=>
[
"jsx"
,
"tsx"
,
"react"
].
includes
(
b
.
language
)
||
b
.
filename
?.
match
(
/
\.(
jsx|tsx
)
$/
)
||
([
"javascript"
,
"js"
,
"typescript"
,
"ts"
].
includes
(
b
.
language
)
&&
REACT_IMPORT_RE
.
test
(
b
.
code
))
);
const
vueBlocks
=
blocks
.
filter
(
(
b
)
=>
b
.
language
===
"vue"
||
b
.
filename
?.
match
(
/
\.
vue$/
)
);
const
svelteBlocks
=
blocks
.
filter
(
(
b
)
=>
b
.
language
===
"svelte"
||
b
.
filename
?.
match
(
/
\.
svelte$/
)
);
// ── Case 1: Complete HTML file ──
if
(
htmlBlocks
.
length
)
{
let
html
=
htmlBlocks
[
0
].
code
;
// Check if it's a full document or a fragment
const
isFullDoc
=
/<!DOCTYPE|<html/i
.
test
(
html
);
if
(
!
isFullDoc
)
{
html
=
_wrapFragment
(
html
,
cssBlocks
,
jsBlocks
);
}
else
{
// Inject additional CSS blocks
if
(
cssBlocks
.
length
)
{
const
css
=
cssBlocks
.
map
((
b
)
=>
b
.
code
).
join
(
"
\n\n
"
);
if
(
html
.
includes
(
"</head>"
))
{
html
=
html
.
replace
(
"</head>"
,
`<style>\n
${
css
}
\n</style>\n</head>`
);
}
else
{
html
=
`<style>\n
${
css
}
\n</style>\n`
+
html
;
}
}
// Inject additional JS blocks
if
(
jsBlocks
.
length
)
{
const
js
=
jsBlocks
.
map
((
b
)
=>
b
.
code
).
join
(
"
\n\n
"
);
if
(
html
.
includes
(
"</body>"
))
{
html
=
html
.
replace
(
"</body>"
,
`<script>\n
${
js
}
\n</script>\n</body>`
);
}
else
{
html
+=
`\n<script>\n
${
js
}
\n</script>`
;
}
}
// Auto-inject Tailwind CDN if needed
html
=
_injectTailwindIfNeeded
(
html
);
}
return
html
;
}
// ── Case 2: React/JSX ──
if
(
reactBlocks
.
length
)
{
return
_buildReactPreview
(
reactBlocks
,
cssBlocks
,
jsBlocks
);
}
// ── Case 3: Vue SFC ──
if
(
vueBlocks
.
length
)
{
return
_buildVuePreview
(
vueBlocks
,
cssBlocks
);
}
// ── Case 4: CSS only (showcase) ──
if
(
cssBlocks
.
length
&&
!
jsBlocks
.
length
)
{
return
_buildCSSShowcase
(
cssBlocks
);
}
// ── Case 5: JS only ──
if
(
jsBlocks
.
length
)
{
return
_wrapFragment
(
""
,
cssBlocks
,
jsBlocks
);
}
return
null
;
}
function
_wrapFragment
(
bodyHTML
,
cssBlocks
,
jsBlocks
)
{
const
css
=
cssBlocks
.
map
((
b
)
=>
b
.
code
).
join
(
"
\n\n
"
);
const
js
=
jsBlocks
.
map
((
b
)
=>
b
.
code
).
join
(
"
\n\n
"
);
const
combined
=
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UI Preview</title>
${
css
?
`<style>\n
${
css
}
\n</style>`
:
""
}
</head>
<body>
${
bodyHTML
}
${
js
?
`<script>\n
${
js
}
\n</script>`
:
""
}
</body>
</html>`
;
return
_injectTailwindIfNeeded
(
combined
);
}
function
_buildReactPreview
(
reactBlocks
,
cssBlocks
,
jsBlocks
)
{
const
css
=
cssBlocks
.
map
((
b
)
=>
b
.
code
).
join
(
"
\n\n
"
);
const
jsx
=
reactBlocks
.
map
((
b
)
=>
b
.
code
).
join
(
"
\n\n
"
);
const
js
=
jsBlocks
.
map
((
b
)
=>
b
.
code
).
join
(
"
\n\n
"
);
// Strip import/export statements for browser execution
const
cleanJSX
=
jsx
.
replace
(
/^import
\s
+.*
?
from
\s
+
[
'"
][^
'"
]
+
[
'"
]
;
?\s
*$/gm
,
""
)
.
replace
(
/^export
\s
+default
\s
+/gm
,
"const __DefaultExport__ = "
)
.
replace
(
/^export
\s
+/gm
,
""
);
// Find the main component name
const
componentMatch
=
cleanJSX
.
match
(
/
(?:
function|const|class
)\s
+
([
A-Z
]\w
+
)
/
);
const
mainComponent
=
componentMatch
?
componentMatch
[
1
]
:
"__DefaultExport__"
;
const
html
=
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React Preview</title>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin><\/script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin><\/script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"><\/script>
${
css
?
`<style>\n
${
css
}
\n</style>`
:
""
}
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
</style>
</head>
<body>
<div id="root"></div>
${
js
?
`<script>\n
${
js
}
\n<\/script>`
:
""
}
<script type="text/babel">
const { useState, useEffect, useRef, useCallback, useMemo, createContext, useContext, useReducer } = React;
${
cleanJSX
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(
${
mainComponent
}
));
<\/script>
</body>
</html>`
;
return
_injectTailwindIfNeeded
(
html
);
}
function
_buildVuePreview
(
vueBlocks
,
cssBlocks
)
{
const
vue
=
vueBlocks
[
0
].
code
;
const
css
=
cssBlocks
.
map
((
b
)
=>
b
.
code
).
join
(
"
\n\n
"
);
// Extract template, script, style from SFC
const
templateMatch
=
vue
.
match
(
/<template>
([\s\S]
*
?)
<
\/
template>/
);
const
scriptMatch
=
vue
.
match
(
/<script
[^
>
]
*>
([\s\S]
*
?)
<
\/
script>/
);
const
styleMatch
=
vue
.
match
(
/<style
[^
>
]
*>
([\s\S]
*
?)
<
\/
style>/
);
const
template
=
templateMatch
?
templateMatch
[
1
]
:
"<div>Vue Component</div>"
;
const
script
=
scriptMatch
?
scriptMatch
[
1
]
:
""
;
const
style
=
styleMatch
?
styleMatch
[
1
]
:
""
;
return
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue Preview</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"><\/script>
<style>
${
css
}
\n
${
style
}
</style>
</head>
<body>
<div id="app">
${
template
}
</div>
<script>
${
script
.
replace
(
/export
\s
+default
\s
*/
,
"const __comp__ = "
)}
Vue.createApp(typeof __comp__ !== 'undefined' ? __comp__ : {}).mount('#app');
<\/script>
</body>
</html>`
;
}
function
_buildCSSShowcase
(
cssBlocks
)
{
const
css
=
cssBlocks
.
map
((
b
)
=>
b
.
code
).
join
(
"
\n\n
"
);
return
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS Preview</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 2rem; }
${
css
}
</style>
</head>
<body>
<div class="preview-container">
<h1>CSS Preview</h1>
<p>Your styles are applied to this page. Add HTML elements to see them in action.</p>
</div>
</body>
</html>`
;
}
function
_injectTailwindIfNeeded
(
html
)
{
if
(
!
TAILWIND_RE
.
test
(
html
))
return
html
;
if
(
html
.
includes
(
"tailwindcss"
)
||
html
.
includes
(
"tailwind."
))
return
html
;
const
tailwindScript
=
'<script src="https://cdn.tailwindcss.com"><
\
/script>'
;
if
(
html
.
includes
(
"</head>"
))
{
return
html
.
replace
(
"</head>"
,
`
${
tailwindScript
}
\n</head>`
);
}
return
tailwindScript
+
"
\n
"
+
html
;
}
export
function
isPreviewable
(
blocks
)
{
if
(
!
blocks
||
!
blocks
.
length
)
return
false
;
const
langs
=
new
Set
(
blocks
.
map
((
b
)
=>
b
.
language
));
const
files
=
blocks
.
map
((
b
)
=>
b
.
filename
||
""
).
join
(
" "
);
if
(
langs
.
has
(
"html"
)
||
files
.
match
(
/
\.
html
?
/
))
return
true
;
if
(
langs
.
has
(
"jsx"
)
||
langs
.
has
(
"tsx"
)
||
files
.
match
(
/
\.(
jsx|tsx
)
/
))
return
true
;
if
(
langs
.
has
(
"vue"
)
||
files
.
match
(
/
\.
vue/
))
return
true
;
if
(
langs
.
has
(
"svelte"
)
||
files
.
match
(
/
\.
svelte/
))
return
true
;
// CSS + JS combo
if
(
(
langs
.
has
(
"css"
)
||
langs
.
has
(
"scss"
))
&&
(
langs
.
has
(
"javascript"
)
||
langs
.
has
(
"js"
))
)
return
true
;
// Single HTML-like code that starts with tags
for
(
const
b
of
blocks
)
{
if
(
b
.
code
.
trim
().
match
(
/^<!DOCTYPE|^<html|^<div|^<section|^<main|^<header/i
))
return
true
;
}
return
false
;
}
/* ═══════════════════════════════════════════════════
MAIN COMPONENT
═══════════════════════════════════════════════════ */
export
default
function
UIPreview
({
html
:
initialHtml
,
title
,
onClose
})
{
const
[
html
,
setHtml
]
=
useState
(
initialHtml
);
const
[
editHtml
,
setEditHtml
]
=
useState
(
initialHtml
);
const
[
viewport
,
setViewport
]
=
useState
(
"full"
);
const
[
viewMode
,
setViewMode
]
=
useState
(
"preview"
);
const
[
zoom
,
setZoom
]
=
useState
(
100
);
const
[
copied
,
setCopied
]
=
useState
(
false
);
const
[
blobUrl
,
setBlobUrl
]
=
useState
(
null
);
const
[
iframeKey
,
setIframeKey
]
=
useState
(
0
);
const
[
darkBg
,
setDarkBg
]
=
useState
(
false
);
const
iframeRef
=
useRef
(
null
);
const
containerRef
=
useRef
(
null
);
// Create blob URL for iframe
useEffect
(()
=>
{
if
(
!
html
)
return
;
const
blob
=
new
Blob
([
html
],
{
type
:
"text/html;charset=utf-8"
});
const
url
=
URL
.
createObjectURL
(
blob
);
setBlobUrl
(
url
);
return
()
=>
URL
.
revokeObjectURL
(
url
);
},
[
html
,
iframeKey
]);
// Escape key to close
useEffect
(()
=>
{
const
onKey
=
(
e
)
=>
{
if
(
e
.
key
===
"Escape"
)
onClose
?.();
};
window
.
addEventListener
(
"keydown"
,
onKey
);
return
()
=>
window
.
removeEventListener
(
"keydown"
,
onKey
);
},
[
onClose
]);
const
handleRefresh
=
useCallback
(()
=>
{
setIframeKey
((
k
)
=>
k
+
1
);
},
[]);
const
handleApplyCode
=
useCallback
(()
=>
{
setHtml
(
editHtml
);
setIframeKey
((
k
)
=>
k
+
1
);
},
[
editHtml
]);
const
handleCopy
=
useCallback
(()
=>
{
navigator
.
clipboard
.
writeText
(
html
);
setCopied
(
true
);
setTimeout
(()
=>
setCopied
(
false
),
2000
);
},
[
html
]);
const
handleDownload
=
useCallback
(()
=>
{
const
blob
=
new
Blob
([
html
],
{
type
:
"text/html"
});
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"a"
);
a
.
href
=
url
;
const
safeName
=
(
title
||
"design"
)
.
replace
(
/
[^\w\s
-
]
/g
,
""
)
.
trim
()
.
replace
(
/
\s
+/g
,
"-"
)
.
slice
(
0
,
50
);
a
.
download
=
`
${
safeName
||
"design"
}
.html`
;
a
.
click
();
URL
.
revokeObjectURL
(
url
);
},
[
html
,
title
]);
const
handleOpenExternal
=
useCallback
(()
=>
{
if
(
!
html
)
return
;
const
blob
=
new
Blob
([
html
],
{
type
:
"text/html;charset=utf-8"
});
const
url
=
URL
.
createObjectURL
(
blob
);
window
.
open
(
url
,
"_blank"
);
// Don't revoke immediately — let the new tab load
setTimeout
(()
=>
URL
.
revokeObjectURL
(
url
),
5000
);
},
[
html
]);
const
handleZoomIn
=
()
=>
setZoom
((
z
)
=>
Math
.
min
(
z
+
25
,
ZOOM_LEVELS
[
ZOOM_LEVELS
.
length
-
1
]));
const
handleZoomOut
=
()
=>
setZoom
((
z
)
=>
Math
.
max
(
z
-
25
,
ZOOM_LEVELS
[
0
]));
const
currentVP
=
VIEWPORTS
.
find
((
v
)
=>
v
.
id
===
viewport
);
const
iframeWidth
=
currentVP
?.
width
||
"100%"
;
const
iframeHeight
=
currentVP
?.
height
||
"100%"
;
const
showPreview
=
viewMode
===
"preview"
||
viewMode
===
"split"
;
const
showCode
=
viewMode
===
"code"
||
viewMode
===
"split"
;
return
(
<
div
className=
"fixed inset-0 z-[100] bg-black/90 backdrop-blur-sm flex flex-col animate-fade-in"
>
{
/* ═══ HEADER ═══ */
}
<
div
className=
"flex items-center gap-2 px-3 py-2 bg-anton-surface border-b border-anton-border shrink-0 flex-wrap"
>
{
/* Close + Title */
}
<
button
onClick=
{
onClose
}
className=
"p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"
>
<
X
size=
{
18
}
/>
</
button
>
<
div
className=
"flex items-center gap-2 min-w-0 mr-2"
>
<
Paintbrush
size=
{
16
}
className=
"text-anton-accent shrink-0"
/>
<
span
className=
"text-sm font-semibold text-white truncate max-w-[200px]"
>
{
title
||
"UI Preview"
}
</
span
>
</
div
>
{
/* View Mode Tabs */
}
<
div
className=
"flex items-center bg-anton-bg rounded-lg p-0.5 border border-anton-border"
>
{
[
{
id
:
"preview"
,
label
:
"Preview"
,
icon
:
Eye
},
{
id
:
"code"
,
label
:
"Code"
,
icon
:
Code2
},
{
id
:
"split"
,
label
:
"Split"
,
icon
:
Columns
},
].
map
(({
id
,
label
,
icon
:
Icon
})
=>
(
<
button
key=
{
id
}
onClick=
{
()
=>
setViewMode
(
id
)
}
className=
{
`flex items-center gap-1 px-2.5 py-1.5 rounded-md text-xs font-medium transition ${
viewMode === id
? "bg-anton-accent text-white shadow"
: "text-anton-muted hover:text-white"
}`
}
>
<
Icon
size=
{
13
}
/>
<
span
className=
"hidden sm:inline"
>
{
label
}
</
span
>
</
button
>
))
}
</
div
>
{
/* Viewport Controls */
}
<
div
className=
"flex items-center gap-0.5 ml-2"
>
{
VIEWPORTS
.
map
(({
id
,
label
,
icon
:
Icon
,
width
})
=>
(
<
button
key=
{
id
}
onClick=
{
()
=>
setViewport
(
id
)
}
className=
{
`p-1.5 rounded-lg transition relative group ${
viewport === id
? "bg-anton-accent/20 text-anton-accent"
: "text-anton-muted hover:text-white hover:bg-anton-card"
}`
}
title=
{
`${label}${width ? `
(
$
{
width
}
px
)
` : ""}`
}
>
<
Icon
size=
{
15
}
/>
</
button
>
))
}
</
div
>
{
/* Zoom */
}
<
div
className=
"flex items-center gap-0.5 ml-2"
>
<
button
onClick=
{
handleZoomOut
}
className=
"p-1 rounded text-anton-muted hover:text-white transition"
disabled=
{
zoom
<=
ZOOM_LEVELS
[
0
]
}
>
<
ZoomOut
size=
{
14
}
/>
</
button
>
<
span
className=
"text-[10px] text-anton-muted font-mono w-8 text-center"
>
{
zoom
}
%
</
span
>
<
button
onClick=
{
handleZoomIn
}
className=
"p-1 rounded text-anton-muted hover:text-white transition"
disabled=
{
zoom
>=
ZOOM_LEVELS
[
ZOOM_LEVELS
.
length
-
1
]
}
>
<
ZoomIn
size=
{
14
}
/>
</
button
>
</
div
>
{
/* Spacer */
}
<
div
className=
"flex-1"
/>
{
/* Actions */
}
<
div
className=
"flex items-center gap-1"
>
<
button
onClick=
{
()
=>
setDarkBg
(
!
darkBg
)
}
className=
{
`p-1.5 rounded-lg transition ${
darkBg
? "bg-gray-700 text-yellow-300"
: "text-anton-muted hover:text-white hover:bg-anton-card"
}`
}
title=
"Toggle background"
>
{
darkBg
?
<
Moon
size=
{
14
}
/>
:
<
Sun
size=
{
14
}
/>
}
</
button
>
<
button
onClick=
{
handleRefresh
}
className=
"p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"
title=
"Refresh preview"
>
<
RefreshCw
size=
{
14
}
/>
</
button
>
<
button
onClick=
{
handleOpenExternal
}
className=
"p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"
title=
"Open in new tab"
>
<
ExternalLink
size=
{
14
}
/>
</
button
>
<
button
onClick=
{
handleCopy
}
className=
"p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"
title=
"Copy HTML"
>
{
copied
?
(
<
Check
size=
{
14
}
className=
"text-green-400"
/>
)
:
(
<
Copy
size=
{
14
}
/>
)
}
</
button
>
<
button
onClick=
{
handleDownload
}
className=
"p-1.5 rounded-lg text-anton-muted hover:text-white hover:bg-anton-card transition"
title=
"Download HTML"
>
<
Download
size=
{
14
}
/>
</
button
>
</
div
>
</
div
>
{
/* ═══ VIEWPORT INDICATOR ═══ */
}
{
viewport
!==
"full"
&&
(
<
div
className=
"text-center py-1 text-[10px] text-anton-muted bg-anton-bg/50 border-b border-anton-border shrink-0"
>
{
currentVP
?.
label
}
—
{
currentVP
?.
width
}
×
{
currentVP
?.
height
}
px
{
zoom
!==
100
&&
` @ ${zoom}%`
}
</
div
>
)
}
{
/* ═══ MAIN CONTENT ═══ */
}
<
div
ref=
{
containerRef
}
className=
"flex-1 flex overflow-hidden min-h-0"
>
{
/* CODE PANEL */
}
{
showCode
&&
(
<
div
className=
{
`flex flex-col bg-[#1a1b26] border-r border-anton-border overflow-hidden ${
viewMode === "split" ? "w-1/2" : "w-full"
}`
}
>
<
div
className=
"flex items-center justify-between px-3 py-1.5 border-b border-anton-border bg-anton-surface"
>
<
span
className=
"text-[10px] text-anton-accent font-mono uppercase"
>
HTML
</
span
>
<
div
className=
"flex gap-1"
>
<
button
onClick=
{
()
=>
setEditHtml
(
initialHtml
)
}
className=
"text-[10px] text-anton-muted hover:text-white px-1.5 py-0.5 rounded transition flex items-center gap-1"
title=
"Reset to original"
>
<
RotateCcw
size=
{
10
}
/>
Reset
</
button
>
<
button
onClick=
{
handleApplyCode
}
className=
"text-[10px] bg-anton-accent text-white px-2 py-0.5 rounded hover:opacity-80 transition"
>
Apply Changes
</
button
>
</
div
>
</
div
>
<
textarea
value=
{
editHtml
}
onChange=
{
(
e
)
=>
setEditHtml
(
e
.
target
.
value
)
}
className=
"flex-1 w-full bg-transparent text-[12px] text-[#d4d4d4] font-mono p-3 resize-none focus:outline-none leading-relaxed"
spellCheck=
{
false
}
wrap=
"off"
/>
</
div
>
)
}
{
/* PREVIEW PANEL */
}
{
showPreview
&&
(
<
div
className=
{
`flex-1 flex items-start justify-center overflow-auto ${
darkBg ? "bg-[#0a0a0a]" : "bg-[#f0f0f0]"
} ${viewMode === "split" ? "w-1/2" : "w-full"}`
}
style=
{
{
padding
:
viewport
===
"full"
?
0
:
"24px"
}
}
>
<
div
className=
"relative transition-all duration-300 ease-out"
style=
{
{
width
:
viewport
===
"full"
?
"100%"
:
`${(currentVP?.width || 1440) * (zoom / 100)}px`
,
height
:
viewport
===
"full"
?
"100%"
:
"auto"
,
maxWidth
:
"100%"
,
}
}
>
{
/* Device Frame */
}
{
viewport
!==
"full"
&&
(
<
div
className=
{
`rounded-xl overflow-hidden shadow-2xl ${
darkBg ? "shadow-black/50" : "shadow-gray-400/30"
}`
}
style=
{
{
border
:
`${viewport === "mobile" ? 8 : viewport === "tablet" ? 6 : 2}px solid ${
darkBg ? "#222" : "#ccc"
}`
,
borderRadius
:
viewport
===
"mobile"
?
"32px"
:
viewport
===
"tablet"
?
"20px"
:
"8px"
,
}
}
>
{
/* Notch for mobile */
}
{
viewport
===
"mobile"
&&
(
<
div
className=
"flex justify-center py-1"
style=
{
{
background
:
darkBg
?
"#222"
:
"#ccc"
,
}
}
>
<
div
className=
"rounded-full"
style=
{
{
width
:
"80px"
,
height
:
"6px"
,
background
:
darkBg
?
"#333"
:
"#aaa"
,
}
}
/>
</
div
>
)
}
{
blobUrl
&&
(
<
iframe
ref=
{
iframeRef
}
key=
{
`frame-${iframeKey}`
}
src=
{
blobUrl
}
sandbox=
"allow-scripts allow-popups allow-forms allow-modals"
style=
{
{
width
:
`${currentVP.width}px`
,
height
:
`${currentVP.height}px`
,
transform
:
`scale(${zoom / 100})`
,
transformOrigin
:
"top left"
,
border
:
"none"
,
display
:
"block"
,
background
:
"white"
,
}
}
title=
"UI Preview"
/>
)
}
</
div
>
)
}
{
/* Full responsive mode */
}
{
viewport
===
"full"
&&
blobUrl
&&
(
<
iframe
ref=
{
iframeRef
}
key=
{
`frame-${iframeKey}`
}
src=
{
blobUrl
}
sandbox=
"allow-scripts allow-popups allow-forms allow-modals"
className=
"w-full border-none"
style=
{
{
height
:
"100%"
,
minHeight
:
"calc(100vh - 120px)"
,
transform
:
zoom
!==
100
?
`scale(${zoom / 100})`
:
undefined
,
transformOrigin
:
"top left"
,
background
:
"white"
,
}
}
title=
"UI Preview"
/>
)
}
</
div
>
</
div
>
)
}
</
div
>
{
/* ═══ FOOTER STATUS BAR ═══ */
}
<
div
className=
"flex items-center justify-between px-3 py-1.5 bg-anton-surface border-t border-anton-border text-[10px] text-anton-muted shrink-0"
>
<
span
>
{
html
.
length
.
toLocaleString
()
}
chars • Sandbox: scripts only
</
span
>
<
span
>
Son of Anton UI Preview •
{
" "
}
<
kbd
className=
"px-1 py-0.5 bg-anton-bg rounded text-[9px]"
>
Esc
</
kbd
>
{
" "
}
to close
</
span
>
</
div
>
</
div
>
);
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment