goobs-frontend
Version:
A comprehensive React-based libary for building modern web applications
553 lines (526 loc) • 15.7 kB
text/typescript
/**
* @fileoverview Tabs component theme system with light, dark, and sacred themes.
*/
import React from 'react'
import { TRANSITIONS, SHADOWS } from './shared'
export interface TabsTheme {
/** Main container styling */
container: React.CSSProperties
/** Tabs container styling */
tabsContainer: React.CSSProperties
/** Tabs inner container styling */
tabsInnerContainer: React.CSSProperties
/** Individual tab styling */
tab: React.CSSProperties
/** Tab hover styling */
tabHover: React.CSSProperties
/** Tab active styling */
tabActive: React.CSSProperties
/** Tab indicator styling */
tabIndicator: React.CSSProperties
/** Tab indicator active styling */
tabIndicatorActive: React.CSSProperties
/** Tab border styling */
border: React.CSSProperties
/** Sacred glyph styling */
glyph: React.CSSProperties
/** Tab content styling */
tabContent: React.CSSProperties
/** Transition duration */
transition: string
}
export interface TabsStyles {
// Theme selection
theme?: 'light' | 'dark' | 'sacred'
// Container styling
backgroundColor?: string
borderColor?: string
borderRadius?: string
borderWidth?: string
boxShadow?: string
backdropFilter?: string
backgroundImage?: string
height?: string
minHeight?: string
// Tab styling
tabBackgroundColor?: string
tabBorderColor?: string
tabColor?: string
tabFontFamily?: string
tabFontSize?: string
tabFontWeight?: string | number
tabLetterSpacing?: string
tabTextShadow?: string
tabTextTransform?: string
tabPadding?: string
// Tab hover states
tabHoverBackgroundColor?: string
tabHoverColor?: string
tabHoverTextShadow?: string
// Tab active states
tabActiveBackgroundColor?: string
tabActiveColor?: string
tabActiveFontWeight?: string | number
tabActiveTextShadow?: string
// Tab indicator styling
indicatorHeight?: string
indicatorColor?: string
indicatorBoxShadow?: string
// Sacred glyph styling
glyphColor?: string
glyphFontSize?: string
glyphOpacity?: number
glyphAnimation?: string
// Layout and spacing
gap?: string
margin?: string
marginTop?: string
marginBottom?: string
marginLeft?: string
marginRight?: string
// Transitions
transitionDuration?: string
transitionEasing?: string
// States
disabled?: boolean
outline?: boolean
// Dimensions
width?: string
maxWidth?: string
minWidth?: string
maxHeight?: string
// Alignment
alignment?: 'left' | 'center' | 'right' | 'justify'
// Tab border options
tabLeftBorder?: boolean
tabRightBorder?: boolean
tabLeftBorderColor?: string
tabRightBorderColor?: string
}
export const tabsThemes: Record<'light' | 'dark' | 'sacred', TabsTheme> = {
light: {
container: {
position: 'sticky',
top: 0,
zIndex: 50,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
color: 'rgb(55, 65, 81)',
borderBottom: '1px solid rgba(226, 232, 240, 0.8)',
backdropFilter: 'blur(8px)',
boxShadow: SHADOWS.light.small,
padding: '8px 16px',
minHeight: '56px',
},
tabsContainer: {
width: '100%',
height: '100%',
padding: '0 8px',
},
tabsInnerContainer: {
height: '100%',
display: 'flex',
position: 'relative',
gap: '4px',
},
tab: {
height: '100%',
padding: '12px 24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 500,
fontSize: '14px',
letterSpacing: '-0.025em',
transition: TRANSITIONS.medium,
position: 'relative',
boxSizing: 'border-box',
fontFamily: '"Inter", sans-serif',
border: 'none',
backgroundColor: 'transparent',
borderRadius: '8px',
color: 'rgb(55, 65, 81)',
cursor: 'pointer',
minHeight: '40px',
margin: '0 2px',
},
tabHover: {
backgroundColor: 'rgba(59, 130, 246, 0.1)',
color: 'rgb(29, 78, 216)',
transform: 'translateY(-1px)',
},
tabActive: {
backgroundColor: 'rgba(59, 130, 246, 0.15)',
color: 'rgb(29, 78, 216)',
fontWeight: 600,
boxShadow: '0 2px 4px rgba(59, 130, 246, 0.2)',
},
tabIndicator: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '2px',
transition: TRANSITIONS.medium,
backgroundColor: 'transparent',
},
tabIndicatorActive: {
backgroundColor: 'rgb(59, 130, 246)',
height: '2px',
boxShadow: '0 0 8px rgba(59, 130, 246, 0.4)',
},
border: {
borderLeft: '1px solid rgba(226, 232, 240, 0.8)',
},
glyph: {
fontSize: '12px',
opacity: 0.6,
animation: 'none',
},
tabContent: {
display: 'flex',
alignItems: 'center',
gap: '8px',
},
transition: TRANSITIONS.medium,
},
dark: {
container: {
position: 'sticky',
top: 0,
zIndex: 50,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(31, 41, 55, 0.95)',
color: 'rgb(243, 244, 246)',
borderBottom: '1px solid rgba(75, 85, 99, 0.8)',
backdropFilter: 'blur(8px)',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4)',
padding: '8px 16px',
minHeight: '56px',
},
tabsContainer: {
width: '100%',
height: '100%',
padding: '0 8px',
},
tabsInnerContainer: {
height: '100%',
display: 'flex',
position: 'relative',
gap: '4px',
},
tab: {
height: '100%',
padding: '12px 24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 500,
fontSize: '14px',
letterSpacing: '-0.025em',
transition: TRANSITIONS.medium,
position: 'relative',
boxSizing: 'border-box',
fontFamily: '"Inter", sans-serif',
border: 'none',
backgroundColor: 'transparent',
borderRadius: '8px',
color: 'rgb(243, 244, 246)',
cursor: 'pointer',
minHeight: '40px',
margin: '0 2px',
},
tabHover: {
backgroundColor: 'rgba(96, 165, 250, 0.15)',
color: 'rgb(96, 165, 250)',
transform: 'translateY(-1px)',
},
tabActive: {
backgroundColor: 'rgba(96, 165, 250, 0.2)',
color: 'rgb(96, 165, 250)',
fontWeight: 600,
boxShadow: '0 2px 4px rgba(96, 165, 250, 0.3)',
},
tabIndicator: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '2px',
transition: TRANSITIONS.medium,
backgroundColor: 'transparent',
},
tabIndicatorActive: {
backgroundColor: 'rgb(96, 165, 250)',
height: '2px',
boxShadow: '0 0 8px rgba(96, 165, 250, 0.4)',
},
border: {
borderLeft: '1px solid rgba(75, 85, 99, 0.8)',
},
glyph: {
fontSize: '12px',
opacity: 0.6,
animation: 'none',
},
tabContent: {
display: 'flex',
alignItems: 'center',
gap: '8px',
},
transition: TRANSITIONS.medium,
},
sacred: {
container: {
position: 'sticky',
top: 0,
zIndex: 50,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(10, 10, 10, 0.95)',
color: 'rgba(255, 215, 0, 0.9)',
borderBottom: '2px solid rgba(255, 215, 0, 0.4)',
backdropFilter: 'blur(8px)',
boxShadow: SHADOWS.sacred.small,
animation: 'sacred-glow-pulse 2s infinite alternate',
padding: '12px 20px',
minHeight: '64px',
},
tabsContainer: {
width: '100%',
height: '100%',
padding: '0 12px',
},
tabsInnerContainer: {
height: '100%',
display: 'flex',
position: 'relative',
gap: '6px',
},
tab: {
height: '100%',
padding: '14px 28px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 500,
fontSize: '15px',
letterSpacing: '0.05em',
transition: TRANSITIONS.premium,
position: 'relative',
boxSizing: 'border-box',
fontFamily: '"Cinzel", serif',
border: '1px solid rgba(255, 215, 0, 0.2)',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
borderRadius: '12px',
color: 'rgba(255, 215, 0, 0.8)',
cursor: 'pointer',
minHeight: '44px',
margin: '0 3px',
},
tabHover: {
backgroundColor: 'rgba(255, 215, 0, 0.05)',
borderColor: 'rgba(255, 215, 0, 0.4)',
color: '#FFD700',
textShadow: '0 0 12px rgba(255, 215, 0, 0.6)',
transform: 'translateY(-2px)',
animation: 'sacred-shimmer 1s forwards',
boxShadow: '0 4px 12px rgba(255, 215, 0, 0.2)',
},
tabActive: {
backgroundColor: 'rgba(255, 215, 0, 0.15)',
borderColor: 'rgba(255, 215, 0, 0.6)',
color: '#FFD700',
fontWeight: 700,
textShadow: '0 0 18px rgba(255, 215, 0, 0.9)',
boxShadow:
'0 4px 16px rgba(255, 215, 0, 0.3), inset 0 1px 0 rgba(255, 215, 0, 0.2)',
},
tabIndicator: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '2px',
transition: TRANSITIONS.premium,
backgroundColor: 'transparent',
},
tabIndicatorActive: {
backgroundColor: '#FFD700',
height: '4px',
boxShadow:
'0 0 15px rgba(255, 215, 0, 0.8), 0 0 30px rgba(255, 215, 0, 0.4)',
},
border: {
borderLeft: '1px solid rgba(255, 215, 0, 0.3)',
},
glyph: {
fontSize: '12px',
opacity: 0.6,
animation: 'glyph-rotate 10s linear infinite',
},
tabContent: {
display: 'flex',
alignItems: 'center',
gap: '8px',
},
transition: TRANSITIONS.premium,
},
}
// Helper function to get computed theme with custom style overrides
export const getTabsTheme = (styles?: TabsStyles): TabsTheme => {
const theme = styles?.theme || 'light'
const baseTheme = tabsThemes[theme]
if (!styles) {
return baseTheme
}
return {
container: {
...baseTheme.container,
backgroundColor:
styles.backgroundColor || baseTheme.container.backgroundColor,
borderBottom: styles.borderColor || baseTheme.container.borderBottom,
borderRadius: styles.borderRadius || baseTheme.container.borderRadius,
boxShadow: styles.boxShadow || baseTheme.container.boxShadow,
backdropFilter:
styles.backdropFilter || baseTheme.container.backdropFilter,
backgroundImage:
styles.backgroundImage || baseTheme.container.backgroundImage,
height: styles.height || baseTheme.container.height,
minHeight: styles.minHeight || baseTheme.container.minHeight,
},
tabsContainer: baseTheme.tabsContainer,
tabsInnerContainer: baseTheme.tabsInnerContainer,
tab: {
...baseTheme.tab,
backgroundColor:
styles.tabBackgroundColor || baseTheme.tab.backgroundColor,
borderColor: styles.tabBorderColor || baseTheme.tab.borderColor,
color: styles.tabColor || baseTheme.tab.color,
fontFamily: styles.tabFontFamily || baseTheme.tab.fontFamily,
fontSize: styles.tabFontSize || baseTheme.tab.fontSize,
fontWeight: styles.tabFontWeight || baseTheme.tab.fontWeight,
letterSpacing: styles.tabLetterSpacing || baseTheme.tab.letterSpacing,
textShadow: styles.tabTextShadow || baseTheme.tab.textShadow,
textTransform:
(styles.tabTextTransform as React.CSSProperties['textTransform']) ||
baseTheme.tab.textTransform,
padding: styles.tabPadding || baseTheme.tab.padding,
},
tabHover: {
...baseTheme.tabHover,
backgroundColor:
styles.tabHoverBackgroundColor || baseTheme.tabHover.backgroundColor,
color: styles.tabHoverColor || baseTheme.tabHover.color,
textShadow: styles.tabHoverTextShadow || baseTheme.tabHover.textShadow,
},
tabActive: {
...baseTheme.tabActive,
backgroundColor:
styles.tabActiveBackgroundColor || baseTheme.tabActive.backgroundColor,
color: styles.tabActiveColor || baseTheme.tabActive.color,
fontWeight: styles.tabActiveFontWeight || baseTheme.tabActive.fontWeight,
textShadow: styles.tabActiveTextShadow || baseTheme.tabActive.textShadow,
},
tabIndicator: baseTheme.tabIndicator,
tabIndicatorActive: {
...baseTheme.tabIndicatorActive,
height: styles.indicatorHeight || baseTheme.tabIndicatorActive.height,
backgroundColor:
styles.indicatorColor || baseTheme.tabIndicatorActive.backgroundColor,
boxShadow:
styles.indicatorBoxShadow || baseTheme.tabIndicatorActive.boxShadow,
},
border: baseTheme.border,
glyph: {
...baseTheme.glyph,
color: styles.glyphColor || baseTheme.glyph.color,
fontSize: styles.glyphFontSize || baseTheme.glyph.fontSize,
opacity: styles.glyphOpacity || baseTheme.glyph.opacity,
animation: styles.glyphAnimation || baseTheme.glyph.animation,
},
tabContent: {
...baseTheme.tabContent,
gap: styles.gap || baseTheme.tabContent.gap,
},
transition: styles.transitionDuration
? `all ${styles.transitionDuration} ${styles.transitionEasing || 'cubic-bezier(0.4, 0, 0.2, 1)'}`
: baseTheme.transition,
}
}
// Main style generator function
export const getTabsStyles = (
styles?: TabsStyles,
_hoveredTab?: string | null,
_activeTab?: string | null,
isDisabled?: boolean
) => {
const themeConfig = getTabsTheme(styles)
const alignment = styles?.alignment || 'left'
const alignmentStyles = {
left: { justifyContent: 'flex-start' },
center: { justifyContent: 'center' },
right: { justifyContent: 'flex-end' },
justify: { justifyContent: 'space-between' },
}
const containerStyle: React.CSSProperties = {
...themeConfig.container,
opacity: isDisabled ? 0.5 : 1,
pointerEvents: isDisabled ? 'none' : 'auto',
// Layout styling
margin: styles?.margin,
marginTop: styles?.marginTop,
marginBottom: styles?.marginBottom,
marginLeft: styles?.marginLeft,
marginRight: styles?.marginRight,
width: styles?.width,
maxWidth: styles?.maxWidth,
minWidth: styles?.minWidth,
maxHeight: styles?.maxHeight,
// Apply outline override
...(styles?.outline === false && {
border: 'none',
boxShadow: 'none',
}),
}
const tabsInnerContainerStyle: React.CSSProperties = {
...themeConfig.tabsInnerContainer,
...alignmentStyles[alignment],
}
// Get default border color from theme
const defaultBorderColor = 'rgba(226, 232, 240, 0.8)' // Light theme default
const tabLeftBorderStyle: React.CSSProperties = styles?.tabLeftBorder
? {
borderLeft: `1px solid ${styles.tabLeftBorderColor || defaultBorderColor}`,
}
: {}
const tabRightBorderStyle: React.CSSProperties = styles?.tabRightBorder
? {
borderRight: `1px solid ${styles.tabRightBorderColor || defaultBorderColor}`,
}
: {}
return {
container: containerStyle,
tabsContainer: themeConfig.tabsContainer,
tabsInnerContainer: tabsInnerContainerStyle,
tab: themeConfig.tab,
tabHover: themeConfig.tabHover,
tabActive: themeConfig.tabActive,
tabIndicator: themeConfig.tabIndicator,
tabIndicatorActive: themeConfig.tabIndicatorActive,
border: themeConfig.border,
tabLeftBorder: tabLeftBorderStyle,
tabRightBorder: tabRightBorderStyle,
glyph: themeConfig.glyph,
tabContent: themeConfig.tabContent,
transition: themeConfig.transition,
}
}