goobs-frontend
Version:
A comprehensive React-based libary for building modern web applications
734 lines (690 loc) • 21.1 kB
text/typescript
// --------------------------------------------------------------------------
// DROPDOWN THEME SYSTEM
// --------------------------------------------------------------------------
import React from 'react'
import { TRANSITIONS, SHADOWS } from './shared'
export interface DropdownTheme {
// Container styling
container: {
position: string
width: string
minWidth: string
maxWidth: string
height: string
minHeight: string
maxHeight: string
fontFamily: string
}
// Label styling
label: {
color: string
fontSize: string
fontWeight: string | number
marginBottom: string
fontFamily: string
}
// Trigger/Input styling
trigger: {
background: string
border: string
borderRadius: string
borderColor: string
borderFocusedColor: string
borderErrorColor: string
color: string
fontSize: string
fontFamily: string
padding: string
minHeight: string
cursor: string
transition: string
height: string
}
// Dropdown menu styling
dropdown: {
background: string
border: string
borderRadius: string
boxShadow: string
marginTop: string
maxHeight: string
zIndex: number
}
// Option styling
option: {
background: string
color: string
fontSize: string
fontFamily: string
padding: string
cursor: string
transition: string
hoverBackground: string
hoverColor: string
}
// Arrow/Icon styling
icon: {
color: string
size: string
transition: string
}
// Footer text styling
footerText: {
color: string
fontSize: string
fontFamily: string
marginTop: string
}
// Disabled states
disabled: {
background: string
color: string
borderColor: string
cursor: string
iconColor: string
}
// Scrollbar styling
scrollbar: {
width: string
trackBackground: string
thumbBackground: string
thumbHoverBackground: string
borderRadius: string
}
// Sacred theme specific
sacred: {
glow: string
textShadow: string
}
}
export interface DropdownStyles {
// Theme selection
theme?: 'light' | 'dark' | 'sacred'
// Container dimensions
width?: string
minWidth?: string
maxWidth?: string
height?: string
minHeight?: string
maxHeight?: string
// Styling overrides
backgroundColor?: string
borderColor?: string
borderFocusedColor?: string
borderErrorColor?: string
borderRadius?: string
borderWidth?: string
textColor?: string
fontSize?: string
fontFamily?: string
padding?: string
// Label styling
labelColor?: string
labelFontSize?: string
labelFontWeight?: string | number
labelMarginBottom?: string
// Dropdown menu styling
dropdownBackground?: string
dropdownBorderColor?: string
dropdownBorderRadius?: string
dropdownBoxShadow?: string
dropdownMarginTop?: string
dropdownMaxHeight?: string
dropdownZIndex?: number
// Option styling
optionBackground?: string
optionColor?: string
optionFontSize?: string
optionPadding?: string
optionHoverBackground?: string
optionHoverColor?: string
// Icon styling
iconColor?: string
iconSize?: string
// Footer text styling
footerTextColor?: string
footerTextFontSize?: string
footerTextMarginTop?: string
// States
disabled?: boolean
required?: boolean
helperTextType?: 'error' | 'info'
// Spacing
margin?: string
marginTop?: string
marginBottom?: string
marginLeft?: string
marginRight?: string
// Transitions
transitionDuration?: string
transitionEasing?: string
// Required field styling
requiredIndicatorColor?: string
requiredIndicatorText?: string
}
export const dropdownThemes: Record<
'light' | 'dark' | 'sacred',
DropdownTheme
> = {
light: {
container: {
position: 'relative',
width: '100%',
minWidth: 'auto',
maxWidth: 'none',
height: 'auto',
minHeight: 'auto',
maxHeight: 'none',
fontFamily: '"Inter", sans-serif',
},
label: {
color: 'rgba(107, 114, 128, 1)',
fontSize: '14px',
fontWeight: 600,
marginBottom: '6px',
fontFamily: '"Inter", sans-serif',
},
trigger: {
background: 'rgba(255, 255, 255, 0.95)',
border: '1px solid rgba(209, 213, 219, 1)',
borderRadius: '8px',
borderColor: 'rgba(209, 213, 219, 1)',
borderFocusedColor: 'rgba(59, 130, 246, 1)',
borderErrorColor: 'rgba(239, 68, 68, 1)',
color: 'rgba(31, 41, 55, 1)',
fontSize: '16px',
fontFamily: '"Inter", sans-serif',
padding: '8px 16px',
minHeight: '40px',
cursor: 'text',
transition: TRANSITIONS.medium,
height: '40px',
},
dropdown: {
background: 'rgba(255, 255, 255, 0.95)',
border: '1px solid rgba(209, 213, 219, 1)',
borderRadius: '8px',
boxShadow: SHADOWS.light.medium,
marginTop: '4px',
maxHeight: '240px',
zIndex: 1000,
},
option: {
background: 'transparent',
color: 'rgba(31, 41, 55, 1)',
fontSize: '14px',
fontFamily: '"Inter", sans-serif',
padding: '8px 16px',
cursor: 'pointer',
transition: TRANSITIONS.medium,
hoverBackground: 'rgba(243, 244, 246, 1)',
hoverColor: 'rgba(31, 41, 55, 1)',
},
icon: {
color: 'rgba(31, 41, 55, 1)',
size: '20px',
transition: TRANSITIONS.medium,
},
footerText: {
color: 'rgba(107, 114, 128, 1)',
fontSize: '12px',
fontFamily: '"Inter", sans-serif',
marginTop: '8px',
},
disabled: {
background: 'rgba(224, 224, 224, 1)',
color: 'rgba(158, 158, 158, 1)',
borderColor: 'rgba(189, 189, 189, 1)',
cursor: 'not-allowed',
iconColor: 'rgba(158, 158, 158, 1)',
},
scrollbar: {
width: '8px',
trackBackground: 'rgba(243, 244, 246, 0.3)',
thumbBackground: 'rgba(156, 163, 175, 0.6)',
thumbHoverBackground: 'rgba(156, 163, 175, 0.8)',
borderRadius: '4px',
},
sacred: {
glow: '0 0 0 transparent',
textShadow: 'none',
},
},
dark: {
container: {
position: 'relative',
width: '100%',
minWidth: 'auto',
maxWidth: 'none',
height: 'auto',
minHeight: 'auto',
maxHeight: 'none',
fontFamily: '"Inter", sans-serif',
},
label: {
color: 'rgba(156, 163, 175, 1)',
fontSize: '14px',
fontWeight: 600,
marginBottom: '6px',
fontFamily: '"Inter", sans-serif',
},
trigger: {
background: 'rgba(31, 41, 55, 0.95)',
border: '1px solid rgba(75, 85, 99, 1)',
borderRadius: '8px',
borderColor: 'rgba(75, 85, 99, 1)',
borderFocusedColor: 'rgba(96, 165, 250, 1)',
borderErrorColor: 'rgba(239, 68, 68, 1)',
color: 'rgba(255, 255, 255, 1)',
fontSize: '16px',
fontFamily: '"Inter", sans-serif',
padding: '8px 16px',
minHeight: '40px',
cursor: 'text',
transition: TRANSITIONS.medium,
height: '40px',
},
dropdown: {
background: 'rgba(31, 41, 55, 0.95)',
border: '1px solid rgba(75, 85, 99, 1)',
borderRadius: '8px',
boxShadow: SHADOWS.dark.medium,
marginTop: '4px',
maxHeight: '240px',
zIndex: 1000,
},
option: {
background: 'transparent',
color: 'rgba(255, 255, 255, 1)',
fontSize: '14px',
fontFamily: '"Inter", sans-serif',
padding: '8px 16px',
cursor: 'pointer',
transition: TRANSITIONS.medium,
hoverBackground: 'rgba(75, 85, 99, 1)',
hoverColor: 'rgba(255, 255, 255, 1)',
},
icon: {
color: 'rgba(255, 255, 255, 1)',
size: '20px',
transition: TRANSITIONS.medium,
},
footerText: {
color: 'rgba(156, 163, 175, 1)',
fontSize: '12px',
fontFamily: '"Inter", sans-serif',
marginTop: '8px',
},
disabled: {
background: 'rgba(55, 65, 81, 1)',
color: 'rgba(107, 114, 128, 1)',
borderColor: 'rgba(75, 85, 99, 1)',
cursor: 'not-allowed',
iconColor: 'rgba(107, 114, 128, 1)',
},
scrollbar: {
width: '8px',
trackBackground: 'rgba(75, 85, 99, 0.3)',
thumbBackground: 'rgba(156, 163, 175, 0.6)',
thumbHoverBackground: 'rgba(156, 163, 175, 0.8)',
borderRadius: '4px',
},
sacred: {
glow: '0 0 0 transparent',
textShadow: 'none',
},
},
sacred: {
container: {
position: 'relative',
width: '100%',
minWidth: 'auto',
maxWidth: 'none',
height: 'auto',
minHeight: 'auto',
maxHeight: 'none',
fontFamily: '"Cinzel", serif',
},
label: {
color: 'rgba(107, 114, 128, 1)',
fontSize: '14px',
fontWeight: 600,
marginBottom: '6px',
fontFamily: '"Cinzel", serif',
},
trigger: {
background: 'rgba(10, 10, 10, 0.9)',
border: '1px solid rgba(255, 215, 0, 0.4)',
borderRadius: '8px',
borderColor: 'rgba(255, 215, 0, 0.4)',
borderFocusedColor: 'rgba(255, 215, 0, 1)',
borderErrorColor: 'rgba(239, 68, 68, 1)',
color: 'rgba(255, 215, 0, 1)',
fontSize: '16px',
fontFamily: '"Cinzel", serif',
padding: '8px 16px',
minHeight: '40px',
cursor: 'text',
transition: TRANSITIONS.medium,
height: '40px',
},
dropdown: {
background: 'rgba(10, 10, 10, 0.9)',
border: '1px solid rgba(255, 215, 0, 0.4)',
borderRadius: '8px',
boxShadow: '0 10px 30px rgba(255, 215, 0, 0.3)',
marginTop: '4px',
maxHeight: '240px',
zIndex: 1000,
},
option: {
background: 'transparent',
color: 'rgba(255, 215, 0, 1)',
fontSize: '14px',
fontFamily: '"Cinzel", serif',
padding: '8px 16px',
cursor: 'pointer',
transition: TRANSITIONS.medium,
hoverBackground: 'rgba(255, 215, 0, 0.1)',
hoverColor: 'rgba(255, 215, 0, 1)',
},
icon: {
color: 'rgba(255, 215, 0, 1)',
size: '20px',
transition: TRANSITIONS.medium,
},
footerText: {
color: 'rgba(107, 114, 128, 1)',
fontSize: '12px',
fontFamily: '"Cinzel", serif',
marginTop: '8px',
},
disabled: {
background: 'rgba(30, 30, 30, 0.8)',
color: 'rgba(107, 114, 128, 1)',
borderColor: 'rgba(255, 215, 0, 0.2)',
cursor: 'not-allowed',
iconColor: 'rgba(107, 114, 128, 1)',
},
scrollbar: {
width: '8px',
trackBackground: 'rgba(255, 215, 0, 0.1)',
thumbBackground: 'rgba(255, 215, 0, 0.6)',
thumbHoverBackground: 'rgba(255, 215, 0, 0.8)',
borderRadius: '4px',
},
sacred: {
glow: '0 0 10px rgba(255, 215, 0, 0.4)',
textShadow: '0 0 5px rgba(255, 215, 0, 0.5)',
},
},
}
// Helper function to get computed theme with custom style overrides
export const getDropdownTheme = (styles?: DropdownStyles): DropdownTheme => {
const theme = styles?.theme || 'light'
const baseTheme = dropdownThemes[theme]
if (!styles) {
return baseTheme
}
return {
container: {
...baseTheme.container,
width: styles.width || baseTheme.container.width,
minWidth: styles.minWidth || baseTheme.container.minWidth,
maxWidth: styles.maxWidth || baseTheme.container.maxWidth,
height: styles.height || baseTheme.container.height,
minHeight: styles.minHeight || baseTheme.container.minHeight,
maxHeight: styles.maxHeight || baseTheme.container.maxHeight,
fontFamily: styles.fontFamily || baseTheme.container.fontFamily,
},
label: {
...baseTheme.label,
color: styles.labelColor || baseTheme.label.color,
fontSize: styles.labelFontSize || baseTheme.label.fontSize,
fontWeight: styles.labelFontWeight || baseTheme.label.fontWeight,
marginBottom: styles.labelMarginBottom || baseTheme.label.marginBottom,
fontFamily: styles.fontFamily || baseTheme.label.fontFamily,
},
trigger: {
...baseTheme.trigger,
background: styles.backgroundColor || baseTheme.trigger.background,
borderColor: styles.borderColor || baseTheme.trigger.borderColor,
borderFocusedColor:
styles.borderFocusedColor || baseTheme.trigger.borderFocusedColor,
borderErrorColor:
styles.borderErrorColor || baseTheme.trigger.borderErrorColor,
borderRadius: styles.borderRadius || baseTheme.trigger.borderRadius,
color: styles.textColor || baseTheme.trigger.color,
fontSize: styles.fontSize || baseTheme.trigger.fontSize,
fontFamily: styles.fontFamily || baseTheme.trigger.fontFamily,
padding: styles.padding || baseTheme.trigger.padding,
minHeight: styles.minHeight || baseTheme.trigger.minHeight,
border: `${styles.borderWidth || '1px'} solid ${styles.borderColor || baseTheme.trigger.borderColor}`,
transition: styles.transitionDuration
? `all ${styles.transitionDuration} ${styles.transitionEasing || 'cubic-bezier(0.4, 0, 0.2, 1)'}`
: baseTheme.trigger.transition,
},
dropdown: {
...baseTheme.dropdown,
background: styles.dropdownBackground || baseTheme.dropdown.background,
border: `1px solid ${styles.dropdownBorderColor || styles.borderColor || baseTheme.dropdown.border.split(' ')[2]}`,
borderRadius:
styles.dropdownBorderRadius ||
styles.borderRadius ||
baseTheme.dropdown.borderRadius,
boxShadow: styles.dropdownBoxShadow || baseTheme.dropdown.boxShadow,
marginTop: styles.dropdownMarginTop || baseTheme.dropdown.marginTop,
maxHeight: styles.dropdownMaxHeight || baseTheme.dropdown.maxHeight,
zIndex: styles.dropdownZIndex || baseTheme.dropdown.zIndex,
},
option: {
...baseTheme.option,
background: styles.optionBackground || baseTheme.option.background,
color: styles.optionColor || styles.textColor || baseTheme.option.color,
fontSize: styles.optionFontSize || baseTheme.option.fontSize,
fontFamily: styles.fontFamily || baseTheme.option.fontFamily,
padding: styles.optionPadding || baseTheme.option.padding,
hoverBackground:
styles.optionHoverBackground || baseTheme.option.hoverBackground,
hoverColor: styles.optionHoverColor || baseTheme.option.hoverColor,
},
icon: {
...baseTheme.icon,
color: styles.iconColor || styles.textColor || baseTheme.icon.color,
size: styles.iconSize || baseTheme.icon.size,
},
footerText: {
...baseTheme.footerText,
color: styles.footerTextColor || baseTheme.footerText.color,
fontSize: styles.footerTextFontSize || baseTheme.footerText.fontSize,
fontFamily: styles.fontFamily || baseTheme.footerText.fontFamily,
marginTop: styles.footerTextMarginTop || baseTheme.footerText.marginTop,
},
disabled: baseTheme.disabled,
scrollbar: baseTheme.scrollbar,
sacred: baseTheme.sacred,
}
}
// Main style generator function
export const getDropdownStyles = (
styles?: DropdownStyles,
isOpen?: boolean,
isFocused?: boolean
) => {
const themeConfig = getDropdownTheme(styles)
const isError = styles?.helperTextType === 'error'
const isSacred = styles?.theme === 'sacred'
// Determine border color based on state
const borderColor = isError
? themeConfig.trigger.borderErrorColor
: isFocused || isOpen
? themeConfig.trigger.borderFocusedColor
: themeConfig.trigger.borderColor
const containerStyle: React.CSSProperties = {
position: themeConfig.container.position as any,
width: themeConfig.container.width,
minWidth: themeConfig.container.minWidth,
maxWidth: themeConfig.container.maxWidth,
height: themeConfig.container.height,
minHeight: themeConfig.container.minHeight,
maxHeight: themeConfig.container.maxHeight,
fontFamily: themeConfig.container.fontFamily,
margin: styles?.margin,
marginTop: styles?.marginTop,
marginBottom: styles?.marginBottom,
marginLeft: styles?.marginLeft,
marginRight: styles?.marginRight,
}
const labelStyle: React.CSSProperties = {
display: 'block',
color: themeConfig.label.color,
fontSize: themeConfig.label.fontSize,
fontWeight: themeConfig.label.fontWeight,
marginBottom: themeConfig.label.marginBottom,
fontFamily: themeConfig.label.fontFamily,
lineHeight: '1.2',
letterSpacing: '0.01em',
}
const triggerStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
height: styles?.height || themeConfig.trigger.minHeight,
minHeight: styles?.minHeight || themeConfig.trigger.minHeight,
padding: themeConfig.trigger.padding,
backgroundColor: themeConfig.trigger.background,
border: `1px solid ${borderColor}`,
borderRadius: themeConfig.trigger.borderRadius,
color: themeConfig.trigger.color,
fontSize: themeConfig.trigger.fontSize,
fontFamily: themeConfig.trigger.fontFamily,
cursor: themeConfig.trigger.cursor,
outline: 'none',
transition: themeConfig.trigger.transition,
...(styles?.disabled && {
backgroundColor: themeConfig.disabled.background,
color: themeConfig.disabled.color,
borderColor: themeConfig.disabled.borderColor,
cursor: themeConfig.disabled.cursor,
}),
...(isSacred && {
textShadow: themeConfig.sacred.textShadow,
boxShadow: themeConfig.sacred.glow,
}),
}
const inputStyle: React.CSSProperties = {
border: 'none',
outline: 'none',
background: 'transparent',
color: 'inherit',
fontSize: 'inherit',
fontFamily: 'inherit',
width: '100%',
padding: 0,
height: '100%',
cursor: 'inherit',
...(styles?.disabled && {
cursor: themeConfig.disabled.cursor,
}),
}
const arrowButtonStyle: React.CSSProperties = {
border: 'none',
background: 'transparent',
cursor: 'pointer',
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: themeConfig.icon.color,
minWidth: themeConfig.icon.size,
height: '100%',
transition: themeConfig.icon.transition,
...(styles?.disabled && {
color: themeConfig.disabled.iconColor,
cursor: themeConfig.disabled.cursor,
}),
}
const dropdownStyle: React.CSSProperties = {
position: 'absolute',
top: '100%',
left: 0,
right: 0,
backgroundColor: themeConfig.dropdown.background,
border: themeConfig.dropdown.border,
borderRadius: themeConfig.dropdown.borderRadius,
boxShadow: themeConfig.dropdown.boxShadow,
marginTop: themeConfig.dropdown.marginTop,
maxHeight: themeConfig.dropdown.maxHeight,
overflowY: 'auto',
zIndex: themeConfig.dropdown.zIndex,
scrollbarWidth: 'thin',
scrollbarColor: `${themeConfig.scrollbar.thumbBackground} ${themeConfig.scrollbar.trackBackground}`,
}
const optionStyle: React.CSSProperties = {
display: 'block',
width: '100%',
padding: themeConfig.option.padding,
backgroundColor: themeConfig.option.background,
color: themeConfig.option.color,
fontSize: themeConfig.option.fontSize,
fontFamily: themeConfig.option.fontFamily,
cursor: themeConfig.option.cursor,
border: 'none',
outline: 'none',
transition: themeConfig.option.transition,
textAlign: 'left',
}
const footerTextStyle: React.CSSProperties = {
display: 'block',
color: themeConfig.footerText.color,
fontSize: themeConfig.footerText.fontSize,
fontFamily: themeConfig.footerText.fontFamily,
marginTop: themeConfig.footerText.marginTop,
lineHeight: '1.3',
}
// Generate scrollbar styles
const scrollbarStyles = `
.dropdown-listbox::-webkit-scrollbar {
width: ${themeConfig.scrollbar.width};
}
.dropdown-listbox::-webkit-scrollbar-track {
background: ${themeConfig.scrollbar.trackBackground};
border-radius: ${themeConfig.scrollbar.borderRadius};
}
.dropdown-listbox::-webkit-scrollbar-thumb {
background: ${themeConfig.scrollbar.thumbBackground};
border-radius: ${themeConfig.scrollbar.borderRadius};
}
.dropdown-listbox::-webkit-scrollbar-thumb:hover {
background: ${themeConfig.scrollbar.thumbHoverBackground};
}
`
return {
container: containerStyle,
label: labelStyle,
trigger: triggerStyle,
input: inputStyle,
arrowButton: arrowButtonStyle,
dropdown: dropdownStyle,
option: optionStyle,
footerText: footerTextStyle,
scrollbarStyles,
theme: themeConfig,
// Helper function to get option hover styles
getOptionHoverStyle: (): React.CSSProperties => ({
backgroundColor: themeConfig.option.hoverBackground,
color: themeConfig.option.hoverColor,
}),
}
}
// Required field utilities
export const getRequiredIndicatorStyle = (
styles?: DropdownStyles
): React.CSSProperties => ({
color: styles?.requiredIndicatorColor || '#EF4444',
fontWeight: 'bold',
})
export const getRequiredProps = (required?: boolean) => ({
...(required && { 'aria-required': true }),
})