@senka-ai/ui
Version:
A modern, type-safe Svelte 5 UI component library with full theme support, accessibility standards, and robust state management patterns
347 lines (346 loc) • 9.49 kB
JavaScript
/**
* Component composition and rendering utilities
*
* These utilities simplify common rendering patterns and conditional logic
* used throughout components, making templates cleaner and more readable.
*/
/**
* Conditionally render content based on a boolean condition
* Useful for complex conditional rendering logic
*/
export function renderIf(condition, content) {
return condition ? content : null;
}
/**
* Render content with a fallback if the primary content is falsy
* Useful for optional content with defaults
*/
export function renderWithFallback(primary, fallback) {
return primary || fallback;
}
/**
* Render content only if all conditions are true
* Useful for complex conditional rendering
*/
export function renderIfAll(conditions, content) {
return conditions.every(Boolean) ? content : null;
}
/**
* Render content if any condition is true
* Useful for OR-based conditional rendering
*/
export function renderIfAny(conditions, content) {
return conditions.some(Boolean) ? content : null;
}
/**
* Choose between multiple rendering options based on a value
* Useful for switch-like rendering logic
*/
export function renderSwitch(value, options, fallback) {
return options[value] || fallback || null;
}
/**
* Utilities for icon rendering patterns
*/
export const IconRenderer = {
/**
* Check if an icon should be rendered
*/
shouldRender(icon, showIcon = true) {
return showIcon && icon !== undefined && icon !== null && icon !== '';
},
/**
* Determine if icon is a string (emoji) or component
*/
isStringIcon(icon) {
return typeof icon === 'string';
},
/**
* Render an icon with fallback logic
*/
render(icon, fallback) {
if (!icon)
return fallback || null;
return icon;
},
/**
* Get appropriate CSS classes for icon positioning
*/
getPositionClasses(position, hasIcon) {
if (!hasIcon)
return '';
const classes = {
left: 'pl-10',
right: 'pr-10',
};
return classes[position] || '';
},
};
/**
* Utilities for form field rendering patterns
*/
export const FormRenderer = {
/**
* Determine if a label should be shown
*/
shouldShowLabel(label, showLabel = true) {
return showLabel && Boolean(label);
},
/**
* Determine if helper text should be shown
*/
shouldShowHelperText(helperText, showHelperText = true) {
return showHelperText && Boolean(helperText);
},
/**
* Determine if error should be shown
*/
shouldShowError(error) {
return Boolean(error);
},
/**
* Get the appropriate input state for styling
*/
getInputState(focused, error, disabled = false) {
if (disabled)
return 'disabled';
if (error)
return 'error';
if (focused)
return 'focused';
return 'default';
},
};
/**
* Utilities for avatar rendering patterns
*/
export const AvatarRenderer = {
/**
* Generate initials from a name
* Updated to handle edge cases properly
*/
getInitials(name) {
if (!name || name.trim() === '')
return '';
return name
.trim()
.split(' ')
.filter((word) => word.length > 0)
.map((word) => word.charAt(0))
.join('')
.toUpperCase()
.slice(0, 2);
},
/**
* Determine what to render based on available props
*/
getRenderType(src, name) {
if (src)
return 'image';
if (name)
return 'initials';
return 'placeholder';
},
/**
* Get status indicator classes
* Updated to match Avatar component's original design
*/
getStatusClasses(status, size = 'medium') {
if (!status)
return '';
const baseClasses = 'absolute rounded-full border-2 border-white';
const statusColors = {
online: 'bg-success',
offline: 'bg-neutral',
away: 'bg-warning',
busy: 'bg-error',
};
const sizeClasses = {
xs: 'h-2.5 w-2.5 -bottom-0.5 -right-0.5',
small: 'h-3 w-3 -bottom-0.5 -right-0.5',
medium: 'h-3.5 w-3.5 -bottom-1 -right-1',
large: 'h-4 w-4 -bottom-0.5 -right-0.5',
};
return `${baseClasses} ${statusColors[status]} ${sizeClasses[size]}`;
},
};
/**
* Utilities for list rendering patterns
*/
export const ListRenderer = {
/**
* Determine if a divider should be shown
*/
shouldShowDivider(index, total, showDividers = true) {
return showDividers && index < total - 1;
},
/**
* Get list item classes based on position
*/
getItemClasses(index, total, variant = 'default') {
const baseClasses = variant === 'compact' ? 'py-2 px-3' : 'py-3 px-4';
const positionClasses = [];
if (index === 0)
positionClasses.push('rounded-t-xl');
if (index === total - 1)
positionClasses.push('rounded-b-xl');
return `${baseClasses} ${positionClasses.join(' ')}`;
},
};
/**
* Utilities for badge rendering patterns
*/
export const BadgeRenderer = {
/**
* Format badge number with max value
*/
formatNumber(value, max = 99) {
return value > max ? `${max}+` : value.toString();
},
/**
* Determine badge type from props
*/
getBadgeType(number, icon, dot = false) {
if (dot)
return 'dot';
if (number !== undefined)
return 'number';
if (icon)
return 'icon';
return 'dot';
},
/**
* Get appropriate size classes for badge content
* Supports xs, small, medium, and large sizes
*/
getContentSize(size, type) {
if (type === 'dot')
return '';
const iconSizes = {
xs: 'h-2 w-2',
small: 'h-2.5 w-2.5',
medium: 'h-3 w-3',
large: 'h-3.5 w-3.5',
};
return type === 'icon' ? iconSizes[size] : '';
},
};
/**
* Utilities for dropdown/select rendering patterns
*/
export const DropdownRenderer = {
/**
* Find selected option from options array
*/
findSelectedOption(options, selectedValue) {
return options.find((option) => option.value === selectedValue);
},
/**
* Get display text for selected option
*/
getDisplayText(options, selectedValue, placeholder = 'Select option') {
const selected = this.findSelectedOption(options, selectedValue);
return selected?.label || placeholder;
},
/**
* Determine if dropdown should show as open
*/
shouldShowOpen(isOpen, disabled = false) {
return isOpen && !disabled;
},
};
/**
* Utilities for button rendering patterns
*/
export const ButtonRenderer = {
/**
* Determine if button should show loading state
*/
shouldShowLoading(loading = false, disabled = false) {
return loading && !disabled;
},
/**
* Get content to render based on loading state
*/
getContent(loading, children, loadingText) {
return loading ? loadingText || 'Loading...' : children;
},
/**
* Determine if button is effectively disabled
*/
isEffectivelyDisabled(disabled = false, loading = false) {
return disabled || loading;
},
};
/**
* Utilities for media rendering patterns
*/
export const MediaRenderer = {
/**
* Determine aspect ratio classes
*/
getAspectRatioClasses(aspectRatio) {
const ratios = {
'1:1': 'aspect-square',
'16:9': 'aspect-video',
'4:3': 'aspect-[4/3]',
'3:2': 'aspect-[3/2]',
};
return aspectRatio ? ratios[aspectRatio] : '';
},
/**
* Get rounded corner classes
*/
getRoundedClasses(rounded = false) {
if (!rounded)
return '';
if (rounded === true)
return 'rounded-xl';
const sizes = {
small: 'rounded-lg',
medium: 'rounded-xl',
large: 'rounded-2xl',
};
return sizes[rounded] || '';
},
};
/**
* Generic utilities for common patterns
*/
export const GenericRenderer = {
/**
* Safely render children with null check
*/
renderChildren(children) {
return children || null;
},
/**
* Get HTML attributes for spreading
*/
getRestProps(allProps, excludeKeys) {
const restProps = {};
Object.keys(allProps).forEach((key) => {
if (!excludeKeys.includes(key)) {
restProps[key] = allProps[key];
}
});
return restProps;
},
/**
* Create accessibility attributes
*/
getA11yProps(options) {
const props = {};
if (options.role)
props.role = options.role;
if (options.ariaLabel)
props['aria-label'] = options.ariaLabel;
if (options.ariaDescribedBy)
props['aria-describedby'] = options.ariaDescribedBy;
if (options.ariaPressed !== undefined)
props['aria-pressed'] = options.ariaPressed;
if (options.ariaExpanded !== undefined)
props['aria-expanded'] = options.ariaExpanded;
return props;
},
};