UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

245 lines 9.47 kB
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