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>
This commit is contained in:
218
craft/src/ui/BorderControl.tsx
Normal file
218
craft/src/ui/BorderControl.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
interface BorderControlProps {
|
||||
style: CSSProperties;
|
||||
onChange: (updates: CSSProperties) => void;
|
||||
}
|
||||
|
||||
const BORDER_STYLES = ['none', 'solid', 'dashed', 'dotted'] 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 btnStyle = (active: boolean): React.CSSProperties => ({
|
||||
padding: '4px 8px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: active ? '#3b82f6' : '#27272a',
|
||||
color: active ? '#fff' : '#a1a1aa',
|
||||
flex: 1, textTransform: 'capitalize',
|
||||
});
|
||||
|
||||
const sideLabel: React.CSSProperties = {
|
||||
fontSize: 9, color: '#71717a', textAlign: 'center', marginTop: 2, textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
};
|
||||
|
||||
interface FourSidedValue {
|
||||
top: string;
|
||||
right: string;
|
||||
bottom: string;
|
||||
left: string;
|
||||
}
|
||||
|
||||
function parseFourSided(val: string | undefined): FourSidedValue {
|
||||
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] };
|
||||
}
|
||||
|
||||
function fourSidedToString(val: FourSidedValue): 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}`;
|
||||
}
|
||||
|
||||
function getNumeric(val: string): string {
|
||||
return val.replace(/[^0-9.]/g, '') || '0';
|
||||
}
|
||||
|
||||
export const BorderControl: React.FC<BorderControlProps> = ({ style, onChange }) => {
|
||||
const [widthLinked, setWidthLinked] = useState(true);
|
||||
const [radiusLinked, setRadiusLinked] = useState(true);
|
||||
|
||||
const currentBorderStyle = (style.borderStyle as string) || 'none';
|
||||
const currentBorderColor = (style.borderColor as string) || '#3f3f46';
|
||||
|
||||
// Border width as 4-sided
|
||||
const borderWidth = parseFourSided(style.borderWidth as string);
|
||||
|
||||
// Border radius as 4 corners (TL, TR, BR, BL)
|
||||
const borderRadius = parseFourSided(style.borderRadius as string);
|
||||
|
||||
const handleWidthChange = (side: keyof FourSidedValue, raw: string) => {
|
||||
const num = raw.replace(/[^0-9.]/g, '');
|
||||
const newVal = num ? `${num}px` : '0';
|
||||
let updated: FourSidedValue;
|
||||
if (widthLinked) {
|
||||
updated = { top: newVal, right: newVal, bottom: newVal, left: newVal };
|
||||
} else {
|
||||
updated = { ...borderWidth, [side]: newVal };
|
||||
}
|
||||
onChange({ borderWidth: fourSidedToString(updated) });
|
||||
};
|
||||
|
||||
const handleRadiusChange = (corner: keyof FourSidedValue, raw: string) => {
|
||||
const num = raw.replace(/[^0-9.]/g, '');
|
||||
const newVal = num ? `${num}px` : '0';
|
||||
let updated: FourSidedValue;
|
||||
if (radiusLinked) {
|
||||
updated = { top: newVal, right: newVal, bottom: newVal, left: newVal };
|
||||
} else {
|
||||
updated = { ...borderRadius, [corner]: newVal };
|
||||
}
|
||||
onChange({ borderRadius: fourSidedToString(updated) });
|
||||
};
|
||||
|
||||
const widthSides: { key: keyof FourSidedValue; label: string }[] = [
|
||||
{ key: 'top', label: 'T' },
|
||||
{ key: 'right', label: 'R' },
|
||||
{ key: 'bottom', label: 'B' },
|
||||
{ key: 'left', label: 'L' },
|
||||
];
|
||||
|
||||
const radiusCorners: { key: keyof FourSidedValue; label: string }[] = [
|
||||
{ key: 'top', label: 'TL' },
|
||||
{ key: 'right', label: 'TR' },
|
||||
{ key: 'bottom', label: 'BR' },
|
||||
{ key: 'left', label: 'BL' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
{/* Border Style */}
|
||||
<div>
|
||||
<label style={labelStyle}>Border Style</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{BORDER_STYLES.map((bs) => (
|
||||
<button
|
||||
key={bs}
|
||||
onClick={() => onChange({ borderStyle: bs })}
|
||||
style={btnStyle(currentBorderStyle === bs)}
|
||||
>{bs}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Border Width (4-sided) */}
|
||||
{currentBorderStyle !== 'none' && (
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<label style={{ ...labelStyle, marginBottom: 0 }}>Border Width</label>
|
||||
<button
|
||||
onClick={() => setWidthLinked(!widthLinked)}
|
||||
title={widthLinked ? 'Unlink sides' : 'Link all sides'}
|
||||
style={{
|
||||
padding: '2px 6px', fontSize: 11, borderRadius: 3, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: widthLinked ? '#3b82f6' : '#27272a',
|
||||
color: widthLinked ? '#fff' : '#71717a',
|
||||
}}
|
||||
>
|
||||
<i className={`fa ${widthLinked ? 'fa-link' : 'fa-unlink'}`} style={{ fontSize: 9 }} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 4 }}>
|
||||
{widthSides.map((s) => (
|
||||
<div key={s.key}>
|
||||
<input
|
||||
type="text"
|
||||
value={getNumeric(borderWidth[s.key])}
|
||||
onChange={(e) => handleWidthChange(s.key, e.target.value)}
|
||||
style={inputStyle}
|
||||
/>
|
||||
<div style={sideLabel}>{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Border Color */}
|
||||
{currentBorderStyle !== 'none' && (
|
||||
<div>
|
||||
<label style={labelStyle}>Border Color</label>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<input
|
||||
type="color"
|
||||
value={currentBorderColor}
|
||||
onChange={(e) => onChange({ borderColor: e.target.value })}
|
||||
style={{ width: 32, height: 28, border: '1px solid #3f3f46', borderRadius: 4, background: 'none', cursor: 'pointer', padding: 0 }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={currentBorderColor}
|
||||
onChange={(e) => onChange({ borderColor: e.target.value })}
|
||||
style={{ ...inputStyle, flex: 1, textAlign: 'left' }}
|
||||
placeholder="#000000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Border Radius (4 corners) */}
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<label style={{ ...labelStyle, marginBottom: 0 }}>Border Radius</label>
|
||||
<button
|
||||
onClick={() => setRadiusLinked(!radiusLinked)}
|
||||
title={radiusLinked ? 'Unlink corners' : 'Link all corners'}
|
||||
style={{
|
||||
padding: '2px 6px', fontSize: 11, borderRadius: 3, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: radiusLinked ? '#3b82f6' : '#27272a',
|
||||
color: radiusLinked ? '#fff' : '#71717a',
|
||||
}}
|
||||
>
|
||||
<i className={`fa ${radiusLinked ? 'fa-link' : 'fa-unlink'}`} style={{ fontSize: 9 }} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 4 }}>
|
||||
{radiusCorners.map((c) => (
|
||||
<div key={c.key}>
|
||||
<input
|
||||
type="text"
|
||||
value={getNumeric(borderRadius[c.key])}
|
||||
onChange={(e) => handleRadiusChange(c.key, e.target.value)}
|
||||
style={inputStyle}
|
||||
/>
|
||||
<div style={sideLabel}>{c.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user