UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

165 lines 7.59 kB
import { Shade, createComponent } from '@furystack/shades'; import { buildTransition, cssVariableTheme } from '../services/css-variable-theme.js'; import { paletteFullColors } from '../services/palette-css-vars.js'; // Default colors when no color prop is specified const defaultColors = { main: cssVariableTheme.text.secondary, mainContrast: cssVariableTheme.background.default, light: cssVariableTheme.text.primary, dark: cssVariableTheme.button.disabledBackground, darkContrast: cssVariableTheme.text.primary, }; const ensureSpinnerKeyframes = () => { if (typeof document === 'undefined') return; if (document.querySelector('style[data-shades-button-spinner]')) return; const style = document.createElement('style'); style.setAttribute('data-shades-button-spinner', ''); style.textContent = '@keyframes shade-btn-spin { to { transform: rotate(360deg); } }'; document.head.appendChild(style); }; const spinnerStyle = { display: 'inline-block', width: '1em', height: '1em', border: '2px solid currentColor', borderRightColor: 'transparent', borderRadius: cssVariableTheme.shape.borderRadius.full, animation: 'shade-btn-spin 0.75s linear infinite', flexShrink: '0', }; const iconWrapperStyle = { display: 'inline-flex', alignItems: 'center', flexShrink: '0', }; export const Button = Shade({ customElementName: 'shade-button', elementBase: HTMLButtonElement, elementBaseName: 'button', css: { // Base styles (layout, typography) fontFamily: cssVariableTheme.typography.fontFamily, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: cssVariableTheme.spacing.xs, margin: cssVariableTheme.spacing.sm, padding: `${cssVariableTheme.spacing.sm} ${cssVariableTheme.spacing.lg}`, border: 'none', borderRadius: cssVariableTheme.shape.borderRadius.md, textTransform: 'uppercase', fontSize: cssVariableTheme.typography.fontSize.md, fontWeight: cssVariableTheme.typography.fontWeight.medium, letterSpacing: cssVariableTheme.typography.letterSpacing.wider, lineHeight: '1.75', minWidth: '64px', userSelect: 'none', cursor: 'pointer', boxShadow: 'none', background: 'transparent', transition: buildTransition(['background', cssVariableTheme.transitions.duration.normal, cssVariableTheme.transitions.easing.default], ['box-shadow', cssVariableTheme.transitions.duration.normal, cssVariableTheme.transitions.easing.default], ['color', cssVariableTheme.transitions.duration.normal, cssVariableTheme.transitions.easing.default], ['transform', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.easeOut], ['opacity', cssVariableTheme.transitions.duration.normal, cssVariableTheme.transitions.easing.default]), // Common states '&:active:not(:disabled)': { transform: 'scale(0.96)', }, '&:disabled': { cursor: 'not-allowed', opacity: cssVariableTheme.action.disabledOpacity, }, // ========================================== // FLAT / TEXT VARIANT (default - no data-variant) // Uses CSS custom properties set in render // ========================================== color: 'var(--btn-color-main)', '&:not([data-variant]):hover:not(:disabled)': { color: 'var(--btn-color-light)', background: 'color-mix(in srgb, var(--btn-color-main) 10%, transparent)', }, '&:not([data-variant]):disabled': { color: 'var(--btn-color-dark)', }, // ========================================== // CONTAINED VARIANT // ========================================== '&[data-variant="contained"]': { background: 'var(--btn-color-main)', color: 'var(--btn-color-main-contrast)', }, '&[data-variant="contained"]:hover:not(:disabled)': { background: 'var(--btn-color-dark)', color: 'var(--btn-color-dark-contrast)', }, '&[data-variant="contained"]:disabled': { background: 'var(--btn-color-dark)', color: 'var(--btn-color-dark-contrast)', }, // ========================================== // OUTLINED VARIANT // ========================================== '&[data-variant="outlined"]': { color: 'var(--btn-color-main)', boxShadow: '0px 0px 0px 1px var(--btn-color-main)', backdropFilter: 'blur(35px)', }, '&[data-variant="outlined"]:hover:not(:disabled)': { color: 'var(--btn-color-light)', boxShadow: '0px 0px 0px 1px var(--btn-color-light)', background: 'color-mix(in srgb, var(--btn-color-main) 10%, transparent)', }, '&[data-variant="outlined"]:disabled': { color: 'var(--btn-color-dark)', boxShadow: '0px 0px 0px 1px var(--btn-color-dark)', }, // ========================================== // SIZE VARIANTS // ========================================== '&[data-size="small"]': { padding: `${cssVariableTheme.spacing.xs} ${cssVariableTheme.spacing.sm}`, fontSize: cssVariableTheme.typography.fontSize.sm, minWidth: '48px', }, '&[data-size="large"]': { padding: `${cssVariableTheme.spacing.md} ${cssVariableTheme.spacing.xl}`, fontSize: cssVariableTheme.typography.fontSize.lg, minWidth: '80px', }, // ========================================== // LOADING STATE // ========================================== '&[data-loading]': { cursor: 'default', pointerEvents: 'none', opacity: '0.7', }, }, render: ({ props, children, useHostProps }) => { if (props.loading) { ensureSpinnerKeyframes(); } // Danger overrides color to error const effectiveColor = props.danger ? 'error' : props.color; // Set CSS custom properties for the button colors const colors = effectiveColor ? paletteFullColors[effectiveColor] : defaultColors; useHostProps({ 'data-variant': props.variant && props.variant !== 'text' ? props.variant : undefined, 'data-size': props.size && props.size !== 'medium' ? props.size : undefined, 'data-loading': props.loading ? '' : undefined, disabled: props.loading ? '' : undefined, style: { '--btn-color-main': colors.main, '--btn-color-main-contrast': colors.mainContrast, '--btn-color-light': colors.light, '--btn-color-dark': colors.dark, '--btn-color-dark-contrast': colors.darkContrast, ...props.style, }, }); return (createComponent(createComponent, null, props.loading ? (createComponent("span", { style: spinnerStyle, className: "shade-btn-spinner" })) : props.startIcon ? (createComponent("span", { style: iconWrapperStyle, className: "shade-btn-start-icon" }, props.startIcon)) : null, children, !props.loading && props.endIcon ? (createComponent("span", { style: iconWrapperStyle, className: "shade-btn-end-icon" }, props.endIcon)) : null)); }, }); //# sourceMappingURL=button.js.map