/* global React, ReactDOM, TweaksPanel, useTweaks, TweakSection, TweakSlider, TweakButton */
const { useEffect, useState } = React;
// Defaults — bloco persistente para o host gravar
const TWEAK_DEFAULS = /*EDITMODE-BEGIN*/{
// ── DESKTOP ──
"d_section_pad_y": 56,
"d_hero_pad_top": 96,
"d_hero_pad_bottom": 48,
"d_hero_grid_gap": 56,
"d_hero_media_gap": 14,
"d_hero_buy_gap": 22,
"d_bene_gap": 20,
"d_aut_gap": 64,
"d_r10_gap": 32,
"d_r10_head_mb": 44,
"d_founder_grid_gap": 14,
"d_founder_head_mb": 32,
"d_reviews_pad_y": 64,
"d_reviews_head_mb": 36,
"d_footer_pad_top": 56,
"d_footer_pad_bottom": 32,
// ── MOBILE ──
"m_section_pad_y": 48,
"m_hero_pad_top": 72,
"m_hero_pad_bottom": 24,
"m_hero_grid_gap": 18,
"m_hero_media_gap": 6,
"m_hero_buy_gap": 18,
"m_bene_gap": 14,
"m_aut_gap": 36,
"m_r10_gap": 20,
"m_r10_head_mb": 32,
"m_founder_grid_gap": 14,
"m_founder_head_mb": 24,
"m_reviews_pad_y": 48,
"m_reviews_head_mb": 24,
"m_footer_pad_top": 40,
"m_footer_pad_bottom": 24
}/*EDITMODE-END*/;
// ─────────────────────────────────────────────
// Mobile Preview Frame — abre a própria página dentro
// de um iframe de 390px de largura, sobreposto à página
// ─────────────────────────────────────────────
function MobilePreviewFrame({ open, onClose, cssText }){
const iframeRef = React.useRef(null);
// Sempre que cssText muda, manda pro iframe
useEffect(() => {
if (!open) return;
const iframe = iframeRef.current;
if (!iframe) return;
function injectCss(){
try {
const doc = iframe.contentDocument;
if (!doc) return;
let s = doc.getElementById('spacing-tweaks-style');
if (!s){
s = doc.createElement('style');
s.id = 'spacing-tweaks-style';
doc.head.appendChild(s);
}
s.textContent = cssText;
// marca o iframe como mobile-preview pra esconder o gate de senha
doc.documentElement.classList.add('mobile-preview-iframe');
} catch(_){}
}
if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete'){
injectCss();
}
iframe.addEventListener('load', injectCss);
return () => iframe.removeEventListener('load', injectCss);
}, [open, cssText]);
if (!open) return null;
return (
e.stopPropagation()}>
PREVIEW MOBILE · 390px
);
}
function SpacingTweaks(){
// values = valores salvos em disco (defaults)
// setSaved = grava em disco (apenas chamamos no Salvar)
const [savedValues, setSaved] = useTweaks(TWEAK_DEFAULS);
// draft = valores em edição (preview ao vivo, sem persistir)
const [draft, setDraft] = useState(savedValues);
const [mobileOpen, setMobileOpen] = useState(false);
const [promptOpen, setPromptOpen] = useState(false);
const [copied, setCopied] = useState(false);
// Sempre que os valores salvos mudarem (por exemplo após salvar),
// sincroniza o draft
useEffect(() => { setDraft(savedValues); }, [savedValues]);
// Detecta se há mudanças não salvas
const isDirty = React.useMemo(() => {
return Object.keys(draft).some(k => draft[k] !== savedValues[k]);
}, [draft, savedValues]);
// Aviso ao sair se houver pendências
useEffect(() => {
if (!isDirty) return;
const handler = (e) => {
e.preventDefault();
e.returnValue = '';
};
window.addEventListener('beforeunload', handler);
return () => window.removeEventListener('beforeunload', handler);
}, [isDirty]);
// Helper que substitui o setTweak — escreve só no draft
function setTweak(keyOrEdits, val){
const edits = (typeof keyOrEdits === 'object' && keyOrEdits !== null)
? keyOrEdits : { [keyOrEdits]: val };
setDraft(prev => ({ ...prev, ...edits }));
}
// Usa draft pra o preview
const t = draft;
// Gera o CSS baseado nos valores atuais (draft)
const cssText = React.useMemo(() => `
/* === DESKTOP === */
@media (min-width: 900px){
.section{ padding: ${t.d_section_pad_y}px 0 !important; }
.hero{ padding-top: ${t.d_hero_pad_top}px !important; padding-bottom: ${t.d_hero_pad_bottom}px !important; }
.hero-grid{ gap: ${t.d_hero_grid_gap}px !important; }
.hero-media{ gap: ${t.d_hero_media_gap}px !important; }
.hero-buy > * + *{ margin-top: ${t.d_hero_buy_gap}px; }
.bene-grid{ gap: ${t.d_bene_gap}px !important; }
.autoridade{ gap: ${t.d_aut_gap}px !important; }
.r10-steps{ gap: ${t.d_r10_gap}px !important; }
.r10-head{ margin-bottom: ${t.d_r10_head_mb}px !important; }
.founder-grid{ gap: ${t.d_founder_grid_gap}px !important; }
.founder-head{ margin-bottom: ${t.d_founder_head_mb}px !important; }
.reviews{ padding: ${t.d_reviews_pad_y}px 0 !important; }
.rev-head{ margin-bottom: ${t.d_reviews_head_mb}px !important; }
.footer{ padding-top: ${t.d_footer_pad_top}px !important; padding-bottom: ${t.d_footer_pad_bottom}px !important; }
}
/* === MOBILE === */
@media (max-width: 899px){
.section{ padding: ${t.m_section_pad_y}px 0 !important; }
.hero{ padding-top: ${t.m_hero_pad_top}px !important; padding-bottom: ${t.m_hero_pad_bottom}px !important; }
.hero-grid{ gap: ${t.m_hero_grid_gap}px !important; }
.hero-media{ gap: ${t.m_hero_media_gap}px !important; }
.hero-buy > * + *{ margin-top: ${t.m_hero_buy_gap}px; }
.bene-grid{ gap: ${t.m_bene_gap}px !important; }
.autoridade{ gap: ${t.m_aut_gap}px !important; }
.r10-steps{ gap: ${t.m_r10_gap}px !important; }
.r10-head{ margin-bottom: ${t.m_r10_head_mb}px !important; }
.founder-grid{ gap: ${t.m_founder_grid_gap}px !important; }
.founder-head{ margin-bottom: ${t.m_founder_head_mb}px !important; }
.reviews{ padding: ${t.m_reviews_pad_y}px 0 !important; }
.rev-head{ margin-bottom: ${t.m_reviews_head_mb}px !important; }
.footer{ padding-top: ${t.m_footer_pad_top}px !important; padding-bottom: ${t.m_footer_pad_bottom}px !important; }
}
`, [t]);
// Aplica os valores na página principal — só injeta o CSS quando o usuário
// efetivamente edita algo (draft difere dos valores salvos). Quando tudo
// está "Tudo salvo", deixamos o spacing-overrides.css estático mandar.
useEffect(() => {
let style = document.getElementById('spacing-tweaks-style');
const isClean = Object.keys(draft).every(k => draft[k] === savedValues[k]);
if (isClean){
// remove o override dinâmico para deixar o CSS estático prevalecer
if (style && style.parentNode){
style.parentNode.removeChild(style);
}
return;
}
if (!style){
style = document.createElement('style');
style.id = 'spacing-tweaks-style';
document.head.appendChild(style);
}
style.textContent = cssText;
}, [cssText, draft, savedValues]);
function reset(){
setDraft({ ...TWEAK_DEFAULS });
}
function save(){
// Abre modal com prompt pra copiar e colar no chat do Claude
setPromptOpen(true);
setCopied(false);
}
function discard(){
setDraft(savedValues);
}
// Gera o prompt pronto pra copiar e colar no chat
const promptText = React.useMemo(() => {
const lines = [];
lines.push('Aplique estes espaçamentos no styles.css (substitua o bloco atual):');
lines.push('');
lines.push('DESKTOP:');
lines.push(`- section padding-y: ${draft.d_section_pad_y}px`);
lines.push(`- hero padding-top: ${draft.d_hero_pad_top}px / padding-bottom: ${draft.d_hero_pad_bottom}px`);
lines.push(`- hero-grid gap: ${draft.d_hero_grid_gap}px`);
lines.push(`- hero-media gap: ${draft.d_hero_media_gap}px`);
lines.push(`- hero-buy gap entre blocos: ${draft.d_hero_buy_gap}px`);
lines.push(`- bene-grid gap: ${draft.d_bene_gap}px`);
lines.push(`- autoridade gap: ${draft.d_aut_gap}px`);
lines.push(`- r10-steps gap: ${draft.d_r10_gap}px`);
lines.push(`- r10-head margin-bottom: ${draft.d_r10_head_mb}px`);
lines.push(`- founder-grid gap: ${draft.d_founder_grid_gap}px`);
lines.push(`- founder-head margin-bottom: ${draft.d_founder_head_mb}px`);
lines.push(`- reviews padding-y: ${draft.d_reviews_pad_y}px`);
lines.push(`- rev-head margin-bottom: ${draft.d_reviews_head_mb}px`);
lines.push(`- footer padding-top: ${draft.d_footer_pad_top}px / padding-bottom: ${draft.d_footer_pad_bottom}px`);
lines.push('');
lines.push('MOBILE (max-width: 899px):');
lines.push(`- section padding-y: ${draft.m_section_pad_y}px`);
lines.push(`- hero padding-top: ${draft.m_hero_pad_top}px / padding-bottom: ${draft.m_hero_pad_bottom}px`);
lines.push(`- hero-grid gap: ${draft.m_hero_grid_gap}px`);
lines.push(`- hero-media gap: ${draft.m_hero_media_gap}px`);
lines.push(`- hero-buy gap entre blocos: ${draft.m_hero_buy_gap}px`);
lines.push(`- bene-grid gap: ${draft.m_bene_gap}px`);
lines.push(`- autoridade gap: ${draft.m_aut_gap}px`);
lines.push(`- r10-steps gap: ${draft.m_r10_gap}px`);
lines.push(`- r10-head margin-bottom: ${draft.m_r10_head_mb}px`);
lines.push(`- founder-grid gap: ${draft.m_founder_grid_gap}px`);
lines.push(`- founder-head margin-bottom: ${draft.m_founder_head_mb}px`);
lines.push(`- reviews padding-y: ${draft.m_reviews_pad_y}px`);
lines.push(`- rev-head margin-bottom: ${draft.m_reviews_head_mb}px`);
lines.push(`- footer padding-top: ${draft.m_footer_pad_top}px / padding-bottom: ${draft.m_footer_pad_bottom}px`);
lines.push('');
lines.push('SPACING_VALUES_JSON=' + JSON.stringify(draft));
return lines.join('\n');
}, [draft]);
function copyPrompt(){
const fallback = () => {
const ta = document.createElement('textarea');
ta.value = promptText;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
try {
if (navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(promptText).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}).catch(fallback);
} else { fallback(); }
} catch(_){ fallback(); }
}
// Estilos da barra fixa
const dirtyBarStyle = {
position: 'sticky',
top: 0,
zIndex: 5,
background: isDirty ? 'linear-gradient(180deg, #FFF6E5 0%, #FFEFD0 100%)' : '#F4F4F4',
border: '1px solid ' + (isDirty ? '#E5B96B' : '#E0E0E0'),
borderRadius: '8px',
padding: '10px 12px',
margin: '0 0 10px',
display: 'flex',
flexDirection: 'column',
gap: '8px',
};
const dirtyMsgStyle = {
fontSize: '12px',
fontWeight: 600,
color: isDirty ? '#8B5A1A' : '#666',
display: 'flex',
alignItems: 'center',
gap: '6px',
};
const btnRowStyle = {
display: 'flex',
gap: '6px',
};
const btnStyle = (variant) => ({
flex: 1,
padding: '8px 10px',
fontSize: '12px',
fontWeight: 700,
border: 0,
borderRadius: '6px',
cursor: isDirty ? 'pointer' : 'not-allowed',
opacity: isDirty ? 1 : 0.4,
transition: 'transform .15s, box-shadow .15s',
background: variant === 'primary' ? '#DC8B72' : '#fff',
color: variant === 'primary' ? '#fff' : '#333',
boxShadow: variant === 'primary' ? '0 2px 6px rgba(220,139,114,.4)' : '0 1px 2px rgba(0,0,0,.08)',
border: variant === 'primary' ? 0 : '1px solid #D0D0D0',
});
return (
<>
{isDirty ? 'Alterações pendentes' : 'Sem alterações'}
setMobileOpen(true)} />
setTweak('d_section_pad_y', v)} />
setTweak('d_hero_pad_top', v)} />
setTweak('d_hero_pad_bottom', v)} />
setTweak('d_hero_grid_gap', v)} />
setTweak('d_hero_media_gap', v)} />
setTweak('d_hero_buy_gap', v)} />
setTweak('d_bene_gap', v)} />
setTweak('d_aut_gap', v)} />
setTweak('d_r10_gap', v)} />
setTweak('d_r10_head_mb', v)} />
setTweak('d_founder_grid_gap', v)} />
setTweak('d_founder_head_mb', v)} />
setTweak('d_reviews_pad_y', v)} />
setTweak('d_reviews_head_mb', v)} />
setTweak('d_footer_pad_top', v)} />
setTweak('d_footer_pad_bottom', v)} />
setTweak('m_section_pad_y', v)} />
setTweak('m_hero_pad_top', v)} />
setTweak('m_hero_pad_bottom', v)} />
setTweak('m_hero_grid_gap', v)} />
setTweak('m_hero_media_gap', v)} />
setTweak('m_hero_buy_gap', v)} />
setTweak('m_bene_gap', v)} />
setTweak('m_aut_gap', v)} />
setTweak('m_r10_gap', v)} />
setTweak('m_r10_head_mb', v)} />
setTweak('m_founder_grid_gap', v)} />
setTweak('m_founder_head_mb', v)} />
setTweak('m_reviews_pad_y', v)} />
setTweak('m_reviews_head_mb', v)} />
setTweak('m_footer_pad_top', v)} />
setTweak('m_footer_pad_bottom', v)} />
setMobileOpen(false)} cssText={cssText} />
{promptOpen && (
setPromptOpen(false)}>
e.stopPropagation()}>
📋 Prompt pronto para enviar ao Claude
Copie o texto abaixo e cole no chat. O Claude lerá os valores e aplicará as alterações de espaçamento direto no styles.css.
{promptText}
Após colar no chat e o Claude aplicar, recarregue a página.
)}
>
);
}
const root = ReactDOM.createRoot(document.getElementById('spacing-tweaks-root'));
root.render();