strapi-to-lokalise-plugin
Version:
Preview and sync Lokalise translations from Strapi admin
593 lines (555 loc) • 14.4 kB
JSX
import React, { useEffect, useMemo, useRef } from 'react';
const baseFont = 'Segoe UI, Inter, -apple-system, BlinkMacSystemFont, sans-serif';
const textColorDefault = '#1c1c1c';
const mutedText = '#666687';
const borderColor = '#dcddea';
const backgroundNeutral = '#f6f6f9';
const buttonVariants = {
primary: {
backgroundColor: '#4945ff',
color: '#fff',
border: '1px solid #4945ff',
},
secondary: {
backgroundColor: '#fff',
color: '#4945ff',
border: `1px solid ${borderColor}`,
},
tertiary: {
backgroundColor: '#fff',
color: '#4945ff',
border: `1px solid ${borderColor}`,
},
danger: {
backgroundColor: '#fceaea',
color: '#d02b20',
border: '1px solid #f5c0b8',
},
};
const buttonSizes = {
S: { padding: '4px 12px', fontSize: 13 },
M: { padding: '8px 16px', fontSize: 14 },
L: { padding: '12px 20px', fontSize: 15 },
};
export const Box = ({
as: Comp = 'div',
padding,
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
margin,
marginTop,
marginBottom,
marginLeft,
marginRight,
background,
hasRadius,
shadow,
style = {},
children,
...rest
}) => {
const paddingMap = {
1: '4px',
2: '8px',
3: '12px',
4: '16px',
5: '20px',
6: '24px',
7: '28px',
8: '32px',
};
const backgroundMap = {
neutral0: '#fff',
neutral100: '#f6f6f9',
neutral150: '#eaeaef',
neutral200: '#dcddea',
neutral600: '#666687',
neutral700: '#32324d',
neutral800: '#212134',
};
const shadowMap = {
tableShadow: '0 1px 4px rgba(33, 33, 52, 0.08)',
filterShadow: '0 2px 8px rgba(33, 33, 52, 0.12)',
};
const computedStyle = {
...(padding && { padding: typeof padding === 'number' ? paddingMap[padding] : padding }),
...(paddingTop && { paddingTop: typeof paddingTop === 'number' ? paddingMap[paddingTop] : paddingTop }),
...(paddingBottom && { paddingBottom: typeof paddingBottom === 'number' ? paddingMap[paddingBottom] : paddingBottom }),
...(paddingLeft && { paddingLeft: typeof paddingLeft === 'number' ? paddingMap[paddingLeft] : paddingLeft }),
...(paddingRight && { paddingRight: typeof paddingRight === 'number' ? paddingMap[paddingRight] : paddingRight }),
...(margin && { margin: typeof margin === 'number' ? paddingMap[margin] : margin }),
...(marginTop && { marginTop: typeof marginTop === 'number' ? paddingMap[marginTop] : marginTop }),
...(marginBottom && { marginBottom: typeof marginBottom === 'number' ? paddingMap[marginBottom] : marginBottom }),
...(marginLeft && { marginLeft: typeof marginLeft === 'number' ? paddingMap[marginLeft] : marginLeft }),
...(marginRight && { marginRight: typeof marginRight === 'number' ? paddingMap[marginRight] : marginRight }),
...(background && { backgroundColor: backgroundMap[background] || background }),
...(hasRadius && { borderRadius: 8 }),
...(shadow && { boxShadow: shadowMap[shadow] || shadow }),
...style,
};
return (
<Comp style={computedStyle} {...rest}>
{children}
</Comp>
);
};
export const Flex = ({
as: Comp = 'div',
alignItems,
justifyContent,
gap,
direction,
wrap,
style = {},
children,
...rest
}) => (
<Comp
style={{
display: 'flex',
alignItems,
justifyContent,
gap,
flexDirection: direction,
flexWrap: wrap,
...style,
}}
{...rest}
>
{children}
</Comp>
);
export const Typography = ({
as: Comp = 'p',
variant,
textColor = '#1c1c1c',
fontWeight,
style = {},
children,
...rest
}) => {
const fontSize =
variant === 'alpha'
? 26
: variant === 'beta'
? 20
: variant === 'epsilon'
? 13
: variant === 'pi'
? 12
: 16;
const fontWeightMap = {
bold: 700,
semiBold: 600,
normal: 400,
};
return (
<Comp
style={{
fontFamily: baseFont,
fontSize,
color: textColor === 'neutral600' ? mutedText : textColor === 'neutral700' ? '#32324d' : textColor,
marginTop: 0,
marginBottom: variant === 'alpha' ? '1rem' : variant === 'beta' ? '0.75rem' : '0.5rem',
fontWeight: fontWeight ? (fontWeightMap[fontWeight] || fontWeight) : variant === 'alpha' || variant === 'beta' || variant === 'epsilon' ? 600 : 400,
lineHeight: 1.5,
...style,
}}
{...rest}
>
{children}
</Comp>
);
};
export const Button = ({
type = 'button',
variant = 'primary',
size = 'M',
startIcon,
endIcon,
loading,
disabled,
fullWidth,
children,
style = {},
...rest
}) => {
const variantStyle = buttonVariants[variant] || buttonVariants.primary;
const sizeStyle = buttonSizes[size] || buttonSizes.M;
const handleMouseEnter = (e) => {
if (disabled || loading) return;
if (variant === 'tertiary' || variant === 'secondary') {
e.currentTarget.style.backgroundColor = '#f6f6f9';
e.currentTarget.style.borderColor = '#c0c0cf';
} else if (variant === 'primary') {
e.currentTarget.style.backgroundColor = '#3733b5';
}
};
const handleMouseLeave = (e) => {
if (variant === 'tertiary' || variant === 'secondary') {
e.currentTarget.style.backgroundColor = variantStyle.backgroundColor;
e.currentTarget.style.borderColor = variantStyle.border;
} else if (variant === 'primary') {
e.currentTarget.style.backgroundColor = variantStyle.backgroundColor;
}
};
return (
<button
type={type}
disabled={disabled || loading}
style={{
fontFamily: baseFont,
fontWeight: 600,
borderRadius: 6,
cursor: disabled || loading ? 'not-allowed' : 'pointer',
opacity: disabled || loading ? 0.6 : 1,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
width: fullWidth ? '100%' : undefined,
transition: 'background-color 120ms ease, color 120ms ease, border-color 120ms ease',
textTransform: 'none',
...variantStyle,
...sizeStyle,
...style,
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...rest}
>
{loading && <Loader size="S" />}
{startIcon}
{children}
{endIcon}
</button>
);
};
export const IconButton = ({ label, children, style = {}, disabled, ...rest }) => {
const handleMouseEnter = (e) => {
if (!disabled) {
e.currentTarget.style.backgroundColor = '#f6f6f9';
e.currentTarget.style.borderColor = '#c0c0cf';
}
};
const handleMouseLeave = (e) => {
if (!disabled) {
e.currentTarget.style.backgroundColor = '#fff';
e.currentTarget.style.borderColor = '#dcdcdc';
}
};
return (
<button
type="button"
aria-label={label}
disabled={disabled}
style={{
border: '1px solid #dcdcdc',
borderRadius: 6,
background: '#fff',
width: 32,
height: 32,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.6 : 1,
transition: 'background-color 120ms ease, border-color 120ms ease',
...style,
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...rest}
>
{children}
</button>
);
};
export const Table = ({ children, style = {}, ...rest }) => (
<div style={{ marginTop: 16, marginBottom: 16 }}>
<table
style={{
width: '100%',
borderCollapse: 'collapse',
fontFamily: baseFont,
backgroundColor: '#fff',
borderRadius: 8,
overflow: 'hidden',
boxShadow: '0 2px 8px rgba(15, 15, 15, 0.06)',
...style,
}}
{...rest}
>
{children}
</table>
</div>
);
export const Thead = ({ children, ...rest }) => (
<thead style={{ backgroundColor: backgroundNeutral, borderBottom: `1px solid ${borderColor}` }} {...rest}>
{children}
</thead>
);
export const Tbody = ({ children, ...rest }) => <tbody {...rest}>{children}</tbody>;
export const Tr = ({ children, style = {}, ...rest }) => (
<tr
style={{
borderBottom: '1px solid #f0f0f3',
transition: 'background-color 0.15s ease',
...style,
}}
{...rest}
>
{children}
</tr>
);
export const Th = ({ children, align = 'left', width, ...rest }) => (
<th
style={{
textAlign: align,
padding: '14px 16px',
fontSize: 12,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 0.4,
color: mutedText,
fontFamily: baseFont,
width,
}}
{...rest}
>
{children}
</th>
);
export const Td = ({ children, align = 'left', width, ...rest }) => (
<td
style={{
textAlign: align,
padding: '14px 16px',
fontSize: 14,
color: textColorDefault,
borderBottom: `1px solid ${borderColor}`,
fontFamily: baseFont,
width,
verticalAlign: 'top',
}}
{...rest}
>
{children}
</td>
);
export const Checkbox = ({
indeterminate,
children,
onCheckedChange,
onChange,
label,
style = {},
...rest
}) => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
ref.current.indeterminate = Boolean(indeterminate);
}
}, [indeterminate]);
const handleChange = (event) => {
onChange?.(event);
onCheckedChange?.(event.target.checked);
};
return (
<label
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 8,
cursor: 'pointer',
fontFamily: baseFont,
fontSize: 14,
color: textColorDefault,
...style,
}}
>
<input
ref={ref}
type="checkbox"
onChange={handleChange}
style={{ width: 16, height: 16, accentColor: '#4945ff' }}
{...rest}
/>
<span>{label ?? children}</span>
</label>
);
};
export const Loader = ({ size = 'M', small }) => {
const spinnerSize = small ? 12 : size === 'S' ? 12 : size === 'L' ? 24 : 16;
const borderWidth = spinnerSize / 6;
return (
<>
<style>
{`
@keyframes lokalise-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`}
</style>
<span
style={{
width: spinnerSize,
height: spinnerSize,
borderRadius: '50%',
border: `${borderWidth}px solid #dcddea`,
borderTopColor: '#4945ff',
display: 'inline-block',
animation: 'lokalise-spin 0.8s linear infinite',
}}
/>
</>
);
};
export const TextInput = ({
label,
hint,
error,
startAction,
endAction,
style = {},
...rest
}) => (
<label style={{ display: 'block', marginBottom: 0, width: '100%' }}>
{label && (
<span
style={{
fontFamily: baseFont,
fontSize: 13,
fontWeight: 600,
display: 'block',
marginBottom: 4,
}}
>
{label}
</span>
)}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
{startAction}
<input
style={{
flex: 1,
minWidth: 0,
width: '100%',
border: `1px solid ${error ? '#f03a2f' : '#dcddea'}`,
borderRadius: 6,
padding: '8px 12px',
fontFamily: baseFont,
fontSize: 14,
transition: 'border-color 150ms ease, box-shadow 150ms ease',
outline: 'none',
...style,
}}
onFocus={(e) => {
if (!error) {
e.currentTarget.style.borderColor = '#4945ff';
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(73, 69, 255, 0.1)';
}
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = error ? '#f03a2f' : '#dcddea';
e.currentTarget.style.boxShadow = 'none';
}}
{...rest}
/>
{endAction}
</div>
{hint && (
<span style={{ display: 'block', marginTop: 4, fontSize: 12, color: '#8e8ea9' }}>{hint}</span>
)}
{error && (
<span style={{ display: 'block', marginTop: 4, fontSize: 12, color: '#f03a2f' }}>{error}</span>
)}
</label>
);
export const Tag = ({ children, size = 'M', variant = 'secondary', style = {}, ...rest }) => {
const padding = size === 'S' ? '2px 8px' : '4px 12px';
const colors =
variant === 'success'
? { backgroundColor: '#eafbe7', color: '#2f8144' }
: variant === 'danger'
? { backgroundColor: '#fceaea', color: '#d02b20' }
: { backgroundColor: '#f6f6f9', color: '#4945ff' };
return (
<span
style={{
fontFamily: baseFont,
fontWeight: 600,
fontSize: 12,
borderRadius: 999,
padding,
...colors,
...style,
}}
{...rest}
>
{children}
</span>
);
};
export const Alert = ({ title, children, variant = 'neutral', style = {}, ...rest }) => {
const colors =
variant === 'danger'
? { borderColor: '#f5c0b8', backgroundColor: '#fef4f2' }
: variant === 'success'
? { borderColor: '#b5e4ca', backgroundColor: '#f6fffa' }
: { borderColor: '#dcddea', backgroundColor: '#f9f9ff' };
return (
<div
style={{
border: '1px solid',
borderRadius: 8,
padding: 16,
fontFamily: baseFont,
...colors,
...style,
}}
{...rest}
>
{title && (
<Typography as="h4" style={{ marginBottom: 8 }}>
{title}
</Typography>
)}
{children}
</div>
);
};
export const EmptyStateLayout = ({ icon, content, action, style = {}, ...rest }) => (
<div
style={{
border: '1px dashed #dcddea',
borderRadius: 12,
padding: 32,
textAlign: 'center',
fontFamily: baseFont,
display: 'flex',
flexDirection: 'column',
gap: 12,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fff',
...style,
}}
{...rest}
>
{icon}
{content &&
(typeof content === 'string' ? <Typography>{content}</Typography> : content)}
{action}
</div>
);