219 lines
8.0 KiB
TypeScript
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>
|
||
|
|
);
|
||
|
|
};
|