New src/ui/Modal.tsx ({ open, onClose, title?, children, width?,
closeOnEscape?, closeOnBackdropClick?, backdropStyle?, backdropProps? })
owns the backdrop, Escape-to-close, backdrop-click-to-close, and a new
body-scroll-lock while open. Adopted by TemplateModal, HeadCodeModal, and
SitesmithModal; each keeps its own panel styling/header/footer as children
since those differ per modal.
- TemplateModal: Escape/backdrop-click still close the confirm-template
sub-dialog first via a wrapped onClose passed to Modal; the header's X
button keeps using the raw onClose prop (always fully closes), matching
prior asymmetric behavior.
- HeadCodeModal: straightforward adoption, no prior custom close logic.
- SitesmithModal: previously had no `open` prop, no Escape-to-close, and no
backdrop-click-to-close. Preserved via open (always mounted-open by its
parent already), closeOnEscape={false}, closeOnBackdropClick={false}; role
and aria-modal are passed through via backdropProps to keep them on the
same backdrop element as before.
Body scroll-lock while a modal is open is a small new addition (requested by
the task) applied uniformly; it has no visible effect since each modal's
opaque fixed-position backdrop already fully covers the viewport.
Added a light Modal.test.tsx (renders children, Escape/backdrop-click close
behavior, scroll-lock) using the existing react-dom/client + act harness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
137 lines
4.4 KiB
TypeScript
137 lines
4.4 KiB
TypeScript
import React from 'react';
|
|
import { useSiteDesign } from '../../state/SiteDesignContext';
|
|
import { Modal } from '../../ui/Modal';
|
|
|
|
interface HeadCodeModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const HeadCodeModal: React.FC<HeadCodeModalProps> = ({ open, onClose }) => {
|
|
const { design, updateDesign } = useSiteDesign();
|
|
|
|
return (
|
|
<Modal open={open} onClose={onClose}>
|
|
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
|
|
{/* Header */}
|
|
<div style={modalHeaderStyle}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<i className="fa fa-code" style={{ color: 'var(--color-accent)', fontSize: 16 }} />
|
|
<div>
|
|
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-text)' }}>
|
|
Custom Head Code
|
|
</div>
|
|
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2 }}>
|
|
Add tracking scripts, meta tags, or custom CSS to your site's <head> section.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} style={closeButtonStyle}>
|
|
<i className="fa fa-times" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div style={{ padding: 20, flex: 1, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<div style={{
|
|
padding: '10px 14px',
|
|
background: 'rgba(59,130,246,0.08)',
|
|
border: '1px solid rgba(59,130,246,0.2)',
|
|
borderRadius: 6,
|
|
fontSize: 12,
|
|
color: 'var(--color-text-muted)',
|
|
lineHeight: 1.5,
|
|
}}>
|
|
<i className="fa fa-info-circle" style={{ color: 'var(--color-accent)', marginRight: 6 }} />
|
|
Code added here will be injected into the <code style={{ background: 'rgba(255,255,255,0.08)', padding: '1px 4px', borderRadius: 3, fontSize: 11 }}><head></code> of every page on your site. Use it for analytics, custom fonts, or global CSS.
|
|
</div>
|
|
|
|
<textarea
|
|
value={design.headCode || ''}
|
|
onChange={(e) => updateDesign({ headCode: e.target.value })}
|
|
placeholder={"<!-- Google Analytics -->\n<script async src=\"https://...\"></script>\n\n<!-- Custom Fonts -->\n<link href=\"https://fonts.googleapis.com/...\" rel=\"stylesheet\">\n\n<style>\n /* Global CSS overrides */\n body { }\n</style>"}
|
|
style={{
|
|
flex: 1,
|
|
minHeight: 300,
|
|
padding: 14,
|
|
background: '#0d0d0f',
|
|
color: '#e4e4e7',
|
|
border: '1px solid #3f3f46',
|
|
borderRadius: 8,
|
|
fontFamily: 'Source Code Pro, Consolas, monospace',
|
|
fontSize: 13,
|
|
lineHeight: 1.6,
|
|
resize: 'vertical',
|
|
outline: 'none',
|
|
tabSize: 2,
|
|
}}
|
|
spellCheck={false}
|
|
/>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div style={{
|
|
padding: '12px 20px',
|
|
borderTop: '1px solid var(--color-border)',
|
|
display: 'flex',
|
|
justifyContent: 'flex-end',
|
|
gap: 8,
|
|
}}>
|
|
<button onClick={onClose} style={doneButtonStyle}>
|
|
Done
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
/* ---------- Styles ---------- */
|
|
|
|
const modalStyle: React.CSSProperties = {
|
|
width: '90vw',
|
|
maxWidth: 700,
|
|
maxHeight: '80vh',
|
|
backgroundColor: 'var(--color-bg-surface)',
|
|
borderRadius: 12,
|
|
border: '1px solid var(--color-border)',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
overflow: 'hidden',
|
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
|
};
|
|
|
|
const modalHeaderStyle: React.CSSProperties = {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: '16px 20px',
|
|
borderBottom: '1px solid var(--color-border)',
|
|
flexShrink: 0,
|
|
};
|
|
|
|
const closeButtonStyle: React.CSSProperties = {
|
|
width: 32,
|
|
height: 32,
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'none',
|
|
border: '1px solid var(--color-border)',
|
|
borderRadius: 6,
|
|
color: 'var(--color-text-muted)',
|
|
cursor: 'pointer',
|
|
fontSize: 14,
|
|
};
|
|
|
|
const doneButtonStyle: React.CSSProperties = {
|
|
padding: '8px 24px',
|
|
fontSize: 13,
|
|
fontWeight: 600,
|
|
background: 'var(--color-accent)',
|
|
color: '#fff',
|
|
border: 'none',
|
|
borderRadius: 6,
|
|
cursor: 'pointer',
|
|
};
|