goobs-frontend
Version:
A comprehensive React-based libary for building modern web applications
389 lines (350 loc) • 10.6 kB
text/typescript
// --------------------------------------------------------------------------
// SHARED FORM FIELD THEME SYSTEM
// --------------------------------------------------------------------------
import React from 'react'
export interface FormFieldTheme {
background: string
border: {
default: string
focused: string
error: string
}
text: string
label: {
default: string
focused: string
error: string
shrunkBackground: string
}
adornment: {
default: string
focused: string
}
footerText: {
default: string
error: string
info: string
}
fontFamily: string
}
export interface FormFieldStyles {
// Theme selection
theme?: 'light' | 'dark' | 'sacred'
// Custom colors (all must be rgba format)
backgroundColor?: string
borderColor?: string
borderFocusedColor?: string
borderErrorColor?: string
textColor?: string
labelColor?: string
labelFocusedColor?: string
labelErrorColor?: string
labelShrunkBackgroundColor?: string
adornmentColor?: string
adornmentFocusedColor?: string
footerTextColor?: string
footerTextErrorColor?: string
footerTextInfoColor?: string
fontFamily?: string
// Required field styling
requiredIndicatorColor?: string
requiredIndicatorText?: string
// Field state
disabled?: boolean
required?: boolean
helperTextType?: 'error' | 'info'
// Layout and spacing
padding?: string
paddingLeft?: string
paddingRight?: string
paddingTop?: string
paddingBottom?: string
margin?: string
marginTop?: string
marginBottom?: string
marginLeft?: string
marginRight?: string
// Border and shape
borderRadius?: string
borderWidth?: string
// Typography
fontSize?: string
fontWeight?: string | number
lineHeight?: string
// Dimensions
width?: string
height?: string
minWidth?: string
maxWidth?: string
minHeight?: string
maxHeight?: string
// Adornment positioning
startAdornmentOffset?: string
endAdornmentOffset?: string
// Arrow positioning (for dropdowns)
arrowTop?: string
arrowRight?: string
arrowBottom?: string
arrowLeft?: string
arrowPadding?: string
// Label positioning
labelOffset?: string
labelShrunkOffset?: string
// Footer spacing
footerMarginTop?: string
footerFontSize?: string
// Transitions
transitionDuration?: string
transitionEasing?: string
}
export const formFieldThemes: Record<
'light' | 'dark' | 'sacred',
FormFieldTheme
> = {
light: {
background: 'rgba(255, 255, 255, 0.95)',
border: {
default: 'rgba(209, 213, 219, 1)', // #D1D5DB
focused: 'rgba(59, 130, 246, 1)', // #3B82F6
error: 'rgba(239, 68, 68, 1)', // #EF4444
},
text: 'rgba(31, 41, 55, 1)',
label: {
default: 'rgba(107, 114, 128, 1)', // #6B7280
focused: 'rgba(59, 130, 246, 1)', // #3B82F6
error: 'rgba(239, 68, 68, 1)', // #EF4444
shrunkBackground: 'rgba(255, 255, 255, 0.95)',
},
adornment: {
default: 'rgba(107, 114, 128, 1)', // #6B7280
focused: 'rgba(59, 130, 246, 1)', // #3B82F6
},
footerText: {
default: 'rgba(107, 114, 128, 1)', // #6B7280
error: 'rgba(239, 68, 68, 1)', // #EF4444
info: 'rgba(59, 130, 246, 1)', // #3B82F6
},
fontFamily: '"Inter", sans-serif',
},
dark: {
background: 'rgba(31, 41, 55, 0.95)',
border: {
default: 'rgba(75, 85, 99, 1)', // #4B5563
focused: 'rgba(96, 165, 250, 1)', // #60A5FA
error: 'rgba(239, 68, 68, 1)', // #EF4444
},
text: 'rgba(255, 255, 255, 1)',
label: {
default: 'rgba(156, 163, 175, 1)', // #9CA3AF
focused: 'rgba(96, 165, 250, 1)', // #60A5FA
error: 'rgba(239, 68, 68, 1)', // #EF4444
shrunkBackground: 'rgba(31, 41, 55, 0.95)',
},
adornment: {
default: 'rgba(156, 163, 175, 1)', // #9CA3AF
focused: 'rgba(96, 165, 250, 1)', // #60A5FA
},
footerText: {
default: 'rgba(156, 163, 175, 1)', // #9CA3AF
error: 'rgba(239, 68, 68, 1)', // #EF4444
info: 'rgba(96, 165, 250, 1)', // #60A5FA
},
fontFamily: '"Inter", sans-serif',
},
sacred: {
background: 'rgba(10, 10, 10, 0.9)',
border: {
default: 'rgba(255, 215, 0, 0.4)', // Gold with transparency
focused: 'rgba(255, 215, 0, 1)', // #FFD700
error: 'rgba(239, 68, 68, 1)', // #EF4444
},
text: 'rgba(255, 215, 0, 1)', // #FFD700
label: {
default: 'rgba(107, 114, 128, 1)', // #6B7280
focused: 'rgba(255, 215, 0, 1)', // #FFD700
error: 'rgba(239, 68, 68, 1)', // #EF4444
shrunkBackground: 'rgba(10, 10, 10, 0.9)',
},
adornment: {
default: 'rgba(107, 114, 128, 1)', // #6B7280
focused: 'rgba(255, 215, 0, 1)', // #FFD700
},
footerText: {
default: 'rgba(107, 114, 128, 1)', // #6B7280
error: 'rgba(239, 68, 68, 1)', // #EF4444
info: 'rgba(255, 215, 0, 0.8)', // Gold with slight transparency
},
fontFamily: '"Cinzel", serif',
},
}
// Helper function to get computed theme with custom style overrides
export const getFormFieldTheme = (styles?: FormFieldStyles): FormFieldTheme => {
const theme = styles?.theme || 'light'
const baseTheme = formFieldThemes[theme]
if (!styles) {
return baseTheme
}
return {
background: styles.backgroundColor || baseTheme.background,
border: {
default: styles.borderColor || baseTheme.border.default,
focused: styles.borderFocusedColor || baseTheme.border.focused,
error: styles.borderErrorColor || baseTheme.border.error,
},
text: styles.textColor || baseTheme.text,
label: {
default: styles.labelColor || baseTheme.label.default,
focused: styles.labelFocusedColor || baseTheme.label.focused,
error: styles.labelErrorColor || baseTheme.label.error,
shrunkBackground:
styles.labelShrunkBackgroundColor || baseTheme.label.shrunkBackground,
},
adornment: {
default: styles.adornmentColor || baseTheme.adornment.default,
focused: styles.adornmentFocusedColor || baseTheme.adornment.focused,
},
footerText: {
default: styles.footerTextColor || baseTheme.footerText.default,
error: styles.footerTextErrorColor || baseTheme.footerText.error,
info: styles.footerTextInfoColor || baseTheme.footerText.info,
},
fontFamily: styles.fontFamily || baseTheme.fontFamily,
}
}
// --------------------------------------------------------------------------
// SHARED STYLE GENERATORS
// --------------------------------------------------------------------------
export const getSharedFormFieldStyles = (
styles?: FormFieldStyles,
isFocused?: boolean
) => {
const themeConfig = getFormFieldTheme(styles)
// Determine helper text type
const helperTextType = styles?.helperTextType || 'info'
const isError = helperTextType === 'error'
const borderColor = isError
? themeConfig.border.error
: isFocused
? themeConfig.border.focused
: themeConfig.border.default
const labelColor = isError
? themeConfig.label.error
: themeConfig.label.default
const adornmentColor = isFocused
? themeConfig.adornment.focused
: themeConfig.adornment.default
const footerTextColor =
helperTextType === 'error'
? themeConfig.footerText.error
: helperTextType === 'info'
? themeConfig.footerText.info
: themeConfig.footerText.default
const transition = styles?.transitionDuration
? `all ${styles.transitionDuration} ${styles.transitionEasing || 'cubic-bezier(0.4, 0, 0.2, 1)'}`
: 'all 200ms cubic-bezier(0.4, 0, 0.2, 1)'
return {
themeConfig,
borderColor,
labelColor,
adornmentColor,
footerTextColor,
transition,
isError,
helperTextType,
}
}
export const getSharedLabelStyles = (
labelColor: string,
themeConfig: FormFieldTheme
): React.CSSProperties => ({
display: 'block',
marginBottom: '6px',
fontSize: '14px',
color: labelColor,
fontFamily: themeConfig.fontFamily,
fontWeight: 600,
lineHeight: '1.2',
letterSpacing: '0.01em',
pointerEvents: 'auto',
})
export const getSharedContainerStyles = (
styles?: FormFieldStyles
): React.CSSProperties => ({
position: 'relative',
width: styles?.width || '100%',
minWidth: styles?.minWidth,
maxWidth: styles?.maxWidth,
height: styles?.height || 'auto',
minHeight: styles?.minHeight,
maxHeight: styles?.maxHeight,
marginTop: styles?.marginTop || '0',
marginBottom: styles?.marginBottom,
marginLeft: styles?.marginLeft,
marginRight: styles?.marginRight,
margin: styles?.margin,
})
export const getSharedFooterTextStyles = (
footerTextColor: string,
themeConfig: FormFieldTheme,
styles?: FormFieldStyles
): React.CSSProperties => ({
display: 'block',
marginTop: styles?.footerMarginTop || '8px',
fontSize: styles?.footerFontSize || '12px',
color: footerTextColor,
fontFamily: themeConfig.fontFamily,
lineHeight: '1.3',
fontWeight: 400,
})
export const getSharedAdornmentStyles = (
adornmentColor: string
): React.CSSProperties => ({
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
display: 'flex',
alignItems: 'center',
color: adornmentColor,
})
// --------------------------------------------------------------------------
// SHARED REQUIRED FIELD UTILITIES
// --------------------------------------------------------------------------
export interface SharedFormFieldProps {
/** The label for the input. Can be a string or a React node. */
label?: React.ReactNode
/** Error message to display. */
error?: string
/** Whether the field is required. */
required?: boolean
/** Whether the field is disabled. */
disabled?: boolean
/** Comprehensive styling options including theme, custom colors, and layout properties. */
styles?: FormFieldStyles
}
export const getRequiredLabelText = (
label: string,
required?: boolean,
styles?: FormFieldStyles
): string => {
if (!label || !required) return label
const indicator = styles?.requiredIndicatorText || ' *'
return `${label}${indicator}`
}
export const getRequiredIndicatorStyle = (
styles?: FormFieldStyles
): React.CSSProperties => ({
color: styles?.requiredIndicatorColor || 'rgba(239, 68, 68, 1)',
})
export const getRequiredProps = (required?: boolean) => ({
required: !!required,
'aria-required': !!required,
})
export const validateRequired = (
value: string,
required?: boolean
): string | undefined => {
if (required && (!value || value.trim() === '')) {
return 'This field is required'
}
return undefined
}