Files
wursor/packages/app/src/components/inspector/sections/BoxModelSection.tsx
T
2026-05-03 20:34:18 +01:00

238 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
// ── Box Model Section — Margin / Padding visual diagram ───────────────────────
// Renders a concentric-rectangle diagram (like browser DevTools' box model view).
// Outer ring = margin, middle ring = border, inner ring = padding, center = W×H.
// Each editable quad shows the current value; clicking focuses an inline input.
import { useState } from 'react';
import { useCanvasTheme } from '@/store/canvasTheme';
import { SectionHeader, HSep, parseCssNum, parseCssUnit } from '../DesignInputs';
interface BoxModelSectionProps {
styles: Record<string, string>;
onPatch: (prop: string, val: string) => void;
}
// Parse a shorthand-or-individual CSS value for one side.
function getSide(styles: Record<string, string>, base: string, side: string): string {
const individual = styles[`${base}-${side}`];
if (individual) return individual;
return styles[base] ?? '0px';
}
// Tiny inline editable cell for a box model value.
function BoxCell({
prop,
value,
onPatch,
mini = false,
style: styleProp,
}: {
prop: string;
value: string;
onPatch: (prop: string, val: string) => void;
mini?: boolean;
style?: React.CSSProperties;
}) {
const T = useCanvasTheme();
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState('');
const num = parseCssNum(value);
const unit = parseCssUnit(value);
const display = num || '0';
function commit(raw: string) {
const n = parseFloat(raw);
if (!isNaN(n)) onPatch(prop, `${n}${unit || 'px'}`);
setEditing(false);
}
if (editing) {
return (
<input
autoFocus
value={draft}
onChange={e => setDraft(e.target.value)}
onBlur={() => commit(draft)}
onKeyDown={e => {
if (e.key === 'Enter') commit(draft);
if (e.key === 'Escape') setEditing(false);
}}
style={{
width: mini ? 28 : 36,
textAlign: 'center',
fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.5625rem',
background: T.bgDeep,
border: `1px solid ${T.accent}`,
borderRadius: 3,
color: T.fg,
outline: 'none',
padding: '1px 3px',
...styleProp,
}}
/>
);
}
return (
<span
onClick={() => { setEditing(true); setDraft(num || '0'); }}
title={`${prop}: ${value}`}
style={{
fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.5625rem',
color: T.fg,
cursor: 'text',
minWidth: mini ? 20 : 28,
textAlign: 'center',
display: 'inline-block',
padding: '1px 2px',
borderRadius: 2,
transition: 'background 0.1s',
...styleProp,
}}
onMouseEnter={e => (e.currentTarget.style.background = T.hoverBg ?? 'rgba(255,255,255,0.06)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
{display}
</span>
);
}
export function BoxModelSection({ styles, onPatch }: BoxModelSectionProps) {
const T = useCanvasTheme();
const [open, setOpen] = useState(false);
// Margin values
const mt = getSide(styles, 'margin', 'top');
const mr = getSide(styles, 'margin', 'right');
const mb = getSide(styles, 'margin', 'bottom');
const ml = getSide(styles, 'margin', 'left');
// Padding values
const pt = getSide(styles, 'padding', 'top');
const pr = getSide(styles, 'padding', 'right');
const pb = getSide(styles, 'padding', 'bottom');
const pl = getSide(styles, 'padding', 'left');
// Border values (display only — editable via StrokeSection)
const bw = styles['border-width'] ?? '0px';
// Dimensions
const w = parseCssNum(styles['width'] ?? '0px') || '—';
const h = parseCssNum(styles['height'] ?? '0px') || '—';
// Shared ring styles
const ringBase: React.CSSProperties = {
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
const labelStyle: React.CSSProperties = {
fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.4375rem',
color: T.dim,
letterSpacing: '0.06em',
textTransform: 'uppercase',
position: 'absolute',
top: 4,
left: 6,
userSelect: 'none',
pointerEvents: 'none',
};
return (
<>
<SectionHeader label="Box Model" expanded={open} onToggle={() => setOpen(!open)} />
{open && (
<div style={{ padding: '8px 14px 12px' }}>
{/* Outermost = margin */}
<div style={{
...ringBase,
background: 'rgba(255,200,100,0.07)',
border: `1px solid rgba(255,200,100,0.18)`,
borderRadius: 5,
padding: '18px 20px',
minHeight: 130,
}}>
<span style={labelStyle}>margin</span>
{/* Top margin */}
<BoxCell prop="margin-top" value={mt} onPatch={onPatch}
mini style={{ position: 'absolute', top: 6, left: '50%', transform: 'translateX(-50%)' } as React.CSSProperties} />
{/* Bottom margin */}
<BoxCell prop="margin-bottom" value={mb} onPatch={onPatch}
mini style={{ position: 'absolute', bottom: 6, left: '50%', transform: 'translateX(-50%)' } as React.CSSProperties} />
{/* Left margin */}
<BoxCell prop="margin-left" value={ml} onPatch={onPatch}
mini style={{ position: 'absolute', left: 4, top: '50%', transform: 'translateY(-50%)' } as React.CSSProperties} />
{/* Right margin */}
<BoxCell prop="margin-right" value={mr} onPatch={onPatch}
mini style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)' } as React.CSSProperties} />
{/* Border ring */}
<div style={{
...ringBase,
background: 'rgba(130,170,255,0.07)',
border: `1px solid rgba(130,170,255,0.22)`,
borderRadius: 4,
width: '100%',
minHeight: 96,
padding: '14px 16px',
}}>
<span style={{ ...labelStyle, color: 'rgba(130,170,255,0.55)' }}>border</span>
{/* Border width — read-only hint */}
<span style={{
position: 'absolute', top: 6, left: '50%', transform: 'translateX(-50%)',
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5rem',
color: 'rgba(130,170,255,0.55)',
}}>
{parseCssNum(bw) || '0'}
</span>
{/* Padding ring */}
<div style={{
...ringBase,
background: 'rgba(100,200,130,0.07)',
border: `1px solid rgba(100,200,130,0.22)`,
borderRadius: 3,
width: '100%',
minHeight: 60,
padding: '10px 12px',
}}>
<span style={{ ...labelStyle, color: 'rgba(100,200,130,0.55)' }}>padding</span>
<BoxCell prop="padding-top" value={pt} onPatch={onPatch}
mini style={{ position: 'absolute', top: 4, left: '50%', transform: 'translateX(-50%)' } as React.CSSProperties} />
<BoxCell prop="padding-bottom" value={pb} onPatch={onPatch}
mini style={{ position: 'absolute', bottom: 4, left: '50%', transform: 'translateX(-50%)' } as React.CSSProperties} />
<BoxCell prop="padding-left" value={pl} onPatch={onPatch}
mini style={{ position: 'absolute', left: 2, top: '50%', transform: 'translateY(-50%)' } as React.CSSProperties} />
<BoxCell prop="padding-right" value={pr} onPatch={onPatch}
mini style={{ position: 'absolute', right: 2, top: '50%', transform: 'translateY(-50%)' } as React.CSSProperties} />
{/* Content dimensions */}
<span style={{
fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.5625rem',
color: T.fg,
opacity: 0.7,
whiteSpace: 'nowrap',
}}>
{w} × {h}
</span>
</div>
</div>
</div>
</div>
)}
<HSep />
</>
);
}