@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
165 lines • 7.59 kB
JavaScript
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