Files
site-builder/craft/src/ui/BorderControl.tsx
Josh Knapp 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

219 lines
8.0 KiB
TypeScript

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>
);
};