UNPKG

strapi-to-lokalise-plugin

Version:

Preview and sync Lokalise translations from Strapi admin

593 lines (555 loc) 14.4 kB
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> );