Files
site-builder/craft/src/ui/SpacingInput.tsx
T
shadowdao 91a6b6f34b Add Craft.js site builder (v2) - complete rebuild from GrapesJS
Rebuilt the visual site builder from scratch using Craft.js, React 18,
and TypeScript. The new editor renders directly in the DOM (no iframe),
supports 40+ components, multi-page with shared header/footer, 16
templates, full-spectrum color/gradient controls, custom head code
injection, save/publish workflow, and auto-save.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:31:16 -07:00

142 lines
5.2 KiB
TypeScript

import React, { useState } from 'react';
export interface SpacingValue {
top: string;
right: string;
bottom: string;
left: string;
}
interface SpacingInputProps {
label: string;
value: SpacingValue;
onChange: (value: SpacingValue) => void;
}
const UNITS = ['px', 'em', '%'] as const;
const labelStyle: React.CSSProperties = {
fontSize: 11, fontWeight: 600, color: '#a1a1aa', display: 'block', marginBottom: 6,
textTransform: 'uppercase', letterSpacing: '0.3px',
};
const inputStyle: React.CSSProperties = {
width: '100%', padding: '4px 6px', background: '#27272a', color: '#e4e4e7',
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11, textAlign: 'center',
};
const sideLabel: React.CSSProperties = {
fontSize: 9, color: '#71717a', textAlign: 'center', marginTop: 2, textTransform: 'uppercase',
letterSpacing: '0.5px',
};
function parseValue(val: string): { num: string; unit: string } {
const match = val.match(/^(-?\d*\.?\d+)\s*(px|em|%|rem)?$/);
if (match) return { num: match[1], unit: match[2] || 'px' };
return { num: val.replace(/[^0-9.-]/g, '') || '0', unit: 'px' };
}
export const SpacingInput: React.FC<SpacingInputProps> = ({ label, value, onChange }) => {
const [linked, setLinked] = useState(false);
const [unit, setUnit] = useState<string>(() => parseValue(value.top).unit || 'px');
const handleSideChange = (side: keyof SpacingValue, raw: string) => {
const numericPart = raw.replace(/[^0-9.-]/g, '');
const newVal = numericPart ? `${numericPart}${unit}` : '0';
if (linked) {
onChange({ top: newVal, right: newVal, bottom: newVal, left: newVal });
} else {
onChange({ ...value, [side]: newVal });
}
};
const handleUnitChange = (newUnit: string) => {
setUnit(newUnit);
// Re-apply current numeric values with new unit
const updated: SpacingValue = { top: '', right: '', bottom: '', left: '' };
for (const side of ['top', 'right', 'bottom', 'left'] as const) {
const { num } = parseValue(value[side]);
updated[side] = num && num !== '0' ? `${num}${newUnit}` : '0';
}
onChange(updated);
};
const sides: { key: keyof SpacingValue; label: string }[] = [
{ key: 'top', label: 'T' },
{ key: 'right', label: 'R' },
{ key: 'bottom', label: 'B' },
{ key: 'left', label: 'L' },
];
return (
<div style={{ marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
<label style={{ ...labelStyle, marginBottom: 0 }}>{label}</label>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{/* Unit selector */}
<div style={{ display: 'flex', gap: 2 }}>
{UNITS.map((u) => (
<button
key={u}
onClick={() => handleUnitChange(u)}
style={{
padding: '1px 5px', fontSize: 9, borderRadius: 3, cursor: 'pointer',
border: '1px solid #3f3f46',
background: unit === u ? '#3b82f6' : '#27272a',
color: unit === u ? '#fff' : '#71717a',
}}
>{u}</button>
))}
</div>
{/* Link toggle */}
<button
onClick={() => setLinked(!linked)}
title={linked ? 'Unlink sides' : 'Link all sides'}
style={{
padding: '2px 6px', fontSize: 11, borderRadius: 3, cursor: 'pointer',
border: '1px solid #3f3f46',
background: linked ? '#3b82f6' : '#27272a',
color: linked ? '#fff' : '#71717a',
}}
>
<i className={`fa ${linked ? 'fa-link' : 'fa-unlink'}`} style={{ fontSize: 9 }} />
</button>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 4 }}>
{sides.map((s) => (
<div key={s.key}>
<input
type="text"
value={parseValue(value[s.key]).num}
onChange={(e) => handleSideChange(s.key, e.target.value)}
style={inputStyle}
/>
<div style={sideLabel}>{s.label}</div>
</div>
))}
</div>
</div>
);
};
/** Parse a CSS shorthand like "10px 20px 10px 20px" or "10px" into SpacingValue */
export function parseSpacingShorthand(val: string | undefined): SpacingValue {
if (!val) return { top: '0', right: '0', bottom: '0', left: '0' };
const parts = val.trim().split(/\s+/);
if (parts.length === 1) return { top: parts[0], right: parts[0], bottom: parts[0], left: parts[0] };
if (parts.length === 2) return { top: parts[0], right: parts[1], bottom: parts[0], left: parts[1] };
if (parts.length === 3) return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[1] };
return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[3] };
}
/** Convert SpacingValue back to CSS shorthand */
export function spacingToShorthand(val: SpacingValue): string {
const { top, right, bottom, left } = val;
if (top === right && right === bottom && bottom === left) return top || '0';
if (top === bottom && right === left) return `${top} ${right}`;
if (right === left) return `${top} ${right} ${bottom}`;
return `${top} ${right} ${bottom} ${left}`;
}