@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
245 lines • 9.47 kB
JavaScript
import { Shade, createComponent } from '@furystack/shades';
import { buildTransition, cssVariableTheme } from '../services/css-variable-theme.js';
import { paletteMainColors } from '../services/palette-css-vars.js';
import { clipboard } from './icons/icon-definitions.js';
import { Icon } from './icons/icon.js';
const variantDefs = {
h1: {
fontSize: cssVariableTheme.typography.fontSize.xxxxl,
fontWeight: cssVariableTheme.typography.fontWeight.bold,
lineHeight: cssVariableTheme.typography.lineHeight.tight,
letterSpacing: cssVariableTheme.typography.letterSpacing.tight,
marginBottom: '0.3em',
},
h2: {
fontSize: cssVariableTheme.typography.fontSize.xxxl,
fontWeight: cssVariableTheme.typography.fontWeight.bold,
lineHeight: cssVariableTheme.typography.lineHeight.tight,
letterSpacing: cssVariableTheme.typography.letterSpacing.dense,
marginTop: '1.5em',
marginBottom: '0.3em',
},
h3: {
fontSize: cssVariableTheme.typography.fontSize.xxl,
fontWeight: cssVariableTheme.typography.fontWeight.semibold,
lineHeight: cssVariableTheme.typography.lineHeight.tight,
letterSpacing: cssVariableTheme.typography.letterSpacing.normal,
marginTop: '1.25em',
marginBottom: '0.25em',
},
h4: {
fontSize: cssVariableTheme.typography.fontSize.xl,
fontWeight: cssVariableTheme.typography.fontWeight.semibold,
lineHeight: cssVariableTheme.typography.lineHeight.tight,
letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
marginTop: '1em',
marginBottom: '0.25em',
},
h5: {
fontSize: cssVariableTheme.typography.fontSize.lg,
fontWeight: cssVariableTheme.typography.fontWeight.medium,
lineHeight: cssVariableTheme.typography.lineHeight.normal,
letterSpacing: cssVariableTheme.typography.letterSpacing.normal,
marginTop: '0.75em',
marginBottom: '0.35em',
},
h6: {
fontSize: cssVariableTheme.typography.fontSize.md,
fontWeight: cssVariableTheme.typography.fontWeight.medium,
lineHeight: cssVariableTheme.typography.lineHeight.normal,
letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
marginTop: '0.5em',
marginBottom: '0.2em',
},
subtitle1: {
fontSize: cssVariableTheme.typography.fontSize.md,
fontWeight: cssVariableTheme.typography.fontWeight.medium,
lineHeight: cssVariableTheme.typography.lineHeight.normal,
letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
marginBottom: '0.35em',
},
subtitle2: {
fontSize: cssVariableTheme.typography.fontSize.sm,
fontWeight: cssVariableTheme.typography.fontWeight.medium,
lineHeight: cssVariableTheme.typography.lineHeight.normal,
letterSpacing: '0.1px',
marginBottom: '0.25em',
},
body1: {
fontSize: cssVariableTheme.typography.fontSize.md,
fontWeight: cssVariableTheme.typography.fontWeight.normal,
lineHeight: cssVariableTheme.typography.lineHeight.relaxed,
letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
marginBottom: '0.75em',
},
body2: {
fontSize: cssVariableTheme.typography.fontSize.sm,
fontWeight: cssVariableTheme.typography.fontWeight.normal,
lineHeight: cssVariableTheme.typography.lineHeight.relaxed,
letterSpacing: cssVariableTheme.typography.letterSpacing.wide,
marginBottom: '0.5em',
},
caption: {
fontSize: cssVariableTheme.typography.fontSize.xs,
fontWeight: cssVariableTheme.typography.fontWeight.normal,
lineHeight: cssVariableTheme.typography.lineHeight.normal,
letterSpacing: '0.4px',
},
overline: {
fontSize: cssVariableTheme.typography.fontSize.xs,
fontWeight: cssVariableTheme.typography.fontWeight.medium,
lineHeight: cssVariableTheme.typography.lineHeight.normal,
letterSpacing: cssVariableTheme.typography.letterSpacing.widest,
textTransform: 'uppercase',
marginBottom: '0.5em',
},
};
const buildVariantCssRules = () => {
const rules = {};
for (const [variant, def] of Object.entries(variantDefs)) {
const rule = {
fontSize: def.fontSize,
fontWeight: def.fontWeight,
lineHeight: def.lineHeight,
letterSpacing: def.letterSpacing,
};
if (def.textTransform) {
rule.textTransform = def.textTransform;
}
if (def.marginTop) {
rule.marginTop = def.marginTop;
}
if (def.marginBottom) {
rule.marginBottom = def.marginBottom;
}
rules[`&[data-variant="${variant}"]`] = rule;
}
return rules;
};
const colorToVar = (color) => {
if (color === 'textPrimary')
return cssVariableTheme.text.primary;
if (color === 'textSecondary')
return cssVariableTheme.text.secondary;
if (color === 'textDisabled')
return cssVariableTheme.text.disabled;
return paletteMainColors[color].main;
};
const variantToTag = (variant) => {
if (variant.startsWith('h'))
return variant;
if (variant === 'subtitle1' || variant === 'subtitle2')
return 'h6';
if (variant === 'body1' || variant === 'body2')
return 'p';
return 'span';
};
const typographyCss = {
display: 'block',
margin: '0',
padding: '0',
fontFamily: cssVariableTheme.typography.fontFamily,
color: 'var(--typo-color)',
textShadow: cssVariableTheme.typography.textShadow || 'none',
...buildVariantCssRules(),
'&[data-gutter-bottom]': {
marginBottom: '0.35em',
},
'&[data-align="left"]': { textAlign: 'left' },
'&[data-align="center"]': { textAlign: 'center' },
'&[data-align="right"]': { textAlign: 'right' },
'&[data-align="justify"]': { textAlign: 'justify' },
'&[data-ellipsis="true"]': {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
'&[data-ellipsis="multiline"]': {
overflow: 'hidden',
display: '-webkit-box',
},
'& .typo-copy-btn': {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
background: 'transparent',
border: 'none',
color: cssVariableTheme.text.secondary,
cursor: 'pointer',
padding: `0 ${cssVariableTheme.spacing.xs}`,
fontSize: '0.85em',
lineHeight: '1',
verticalAlign: 'middle',
borderRadius: cssVariableTheme.shape.borderRadius.sm,
transition: buildTransition(['color', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default], ['background', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default]),
},
'& .typo-copy-btn:hover': {
color: cssVariableTheme.text.primary,
background: cssVariableTheme.action.hoverBackground,
},
};
const typographyRender = ({ props, children, useHostProps }) => {
const { variant = 'body1', color = 'textPrimary', ellipsis, copyable, gutterBottom, align, style } = props;
const hostStyle = {
'--typo-color': colorToVar(color),
};
if (typeof ellipsis === 'number') {
hostStyle.webkitLineClamp = String(ellipsis);
hostStyle.webkitBoxOrient = 'vertical';
}
if (style) {
Object.assign(hostStyle, style);
}
useHostProps({
'data-gutter-bottom': gutterBottom ? '' : undefined,
'data-align': align || undefined,
'data-ellipsis': ellipsis === true ? 'true' : typeof ellipsis === 'number' ? 'multiline' : undefined,
'data-variant': variant,
style: hostStyle,
});
if (!copyable) {
return createComponent(createComponent, null, children);
}
const handleCopy = (e) => {
const btn = e.currentTarget;
const host = btn.parentElement;
if (!host)
return;
const clone = host.cloneNode(true);
clone.querySelectorAll('.typo-copy-btn').forEach((el) => el.remove());
navigator.clipboard.writeText(clone.textContent ?? '').catch(() => { });
};
return (createComponent(createComponent, null,
children,
createComponent("button", { type: "button", className: "typo-copy-btn", onclick: handleCopy, title: "Copy to clipboard" },
createComponent(Icon, { icon: clipboard, size: 14 }))));
};
const tagConfigs = [
['h1', HTMLHeadingElement],
['h2', HTMLHeadingElement],
['h3', HTMLHeadingElement],
['h4', HTMLHeadingElement],
['h5', HTMLHeadingElement],
['h6', HTMLHeadingElement],
['p', HTMLParagraphElement],
['span', HTMLSpanElement],
];
const shadesByTag = {};
for (const [tag, elementBase] of tagConfigs) {
shadesByTag[tag] = Shade({
customElementName: `shade-typography-${tag}`,
elementBase,
elementBaseName: tag,
css: typographyCss,
render: typographyRender,
});
}
/**
* Typography component for consistent text styling.
* Maps variants to semantic HTML tags and uses theme typography tokens.
*/
export const Typography = (props, children) => {
const tag = variantToTag(props.variant ?? 'body1');
return shadesByTag[tag](props, children);
};
//# sourceMappingURL=typography.js.map