vira
Version:
A simple and highly versatile design system using element-vir.
455 lines (435 loc) • 20.8 kB
JavaScript
import { check } from '@augment-vir/assert';
import { arrayToObject, getObjectTypedKeys, } from '@augment-vir/common';
import { ContrastLevelName } from '@electrovir/color/dist/data/contrast/contrast.js';
import { css, defineElementEvent, html, listen, unsafeCSS } from 'element-vir';
import { themeDefaultKey } from 'theme-vir/dist/color-theme/color-theme.js';
import { Check16Icon } from '../icons/icon-svgs/16/check-16.icon.js';
import { X16Icon } from '../icons/icon-svgs/16/x-16.icon.js';
import { viraFormCssVars } from '../styles/form-styles.js';
import { standaloneThemeColorNames, ViraColorVariant, viraColorVariantToHostClassKey, ViraEmphasis, viraEmphasisVariants, ViraSize, viraSizeHeights, viraSizeVariants, } from '../styles/form-variants.js';
import { noNativeFormStyles } from '../styles/native-styles.js';
import { noUserSelect } from '../styles/user-select.js';
import { viraThemeByKeys, ViraThemeColorName } from '../styles/vira-color-theme-object.js';
import { viraTheme } from '../styles/vira-color-theme.js';
import { defineViraElement } from '../util/define-vira-element.js';
import { ViraIcon } from './vira-icon.element.js';
const transparentColor = {
value: css `transparent`,
};
function buildThemedTagColors(colorName) {
const behindBg = viraThemeByKeys[colorName]['behind-bg'];
const onSelf = viraThemeByKeys[colorName]['on-self'];
return {
[ViraEmphasis.Standard]: {
idle: {
textColor: behindBg[ContrastLevelName.NonBodyText].foreground,
backgroundColor: behindBg[ContrastLevelName.NonBodyText].background,
borderColor: behindBg[ContrastLevelName.NonBodyText].background,
},
hover: {
textColor: behindBg[ContrastLevelName.Header].foreground,
backgroundColor: behindBg[ContrastLevelName.Header].background,
borderColor: behindBg[ContrastLevelName.Header].background,
},
active: {
textColor: behindBg[ContrastLevelName.NonBodyText].foreground,
backgroundColor: behindBg[ContrastLevelName.NonBodyText].background,
borderColor: behindBg[ContrastLevelName.NonBodyText].background,
},
},
[ViraEmphasis.Subtle]: {
idle: {
textColor: onSelf[ContrastLevelName.BodyText].foreground,
backgroundColor: onSelf[ContrastLevelName.BodyText].background,
borderColor: onSelf[ContrastLevelName.BodyText].background,
},
hover: {
textColor: onSelf[ContrastLevelName.NonBodyText].foreground,
backgroundColor: onSelf[ContrastLevelName.NonBodyText].background,
borderColor: onSelf[ContrastLevelName.NonBodyText].background,
},
active: {
textColor: onSelf[ContrastLevelName.BodyText].foreground,
backgroundColor: onSelf[ContrastLevelName.BodyText].background,
borderColor: onSelf[ContrastLevelName.BodyText].background,
},
},
};
}
function buildThemedNotCheckedColors(colorName) {
const onSelfBodyText = viraThemeByKeys[colorName]['on-self'][ContrastLevelName.BodyText];
return {
idle: {
textColor: onSelfBodyText.foreground,
backgroundColor: transparentColor,
borderColor: onSelfBodyText.background,
},
hover: {
textColor: onSelfBodyText.foreground,
backgroundColor: viraThemeByKeys[colorName]['behind-bg'][ContrastLevelName.Invisible].background,
borderColor: onSelfBodyText.background,
},
active: {
textColor: onSelfBodyText.foreground,
backgroundColor: viraThemeByKeys[colorName]['behind-bg'][ContrastLevelName.Decoration].background,
borderColor: onSelfBodyText.background,
},
};
}
const plainTagColors = {
[ViraEmphasis.Standard]: {
idle: {
backgroundColor: viraTheme.colors[themeDefaultKey].foreground,
textColor: viraTheme.colors[themeDefaultKey].background,
borderColor: viraTheme.colors[themeDefaultKey].foreground,
},
hover: {
backgroundColor: viraTheme.colors['vira-grey-behind-bg-body'].background,
textColor: viraTheme.colors['vira-grey-behind-bg-body'].foreground,
borderColor: viraTheme.colors['vira-grey-behind-bg-body'].background,
},
active: {
backgroundColor: viraTheme.colors[themeDefaultKey].foreground,
textColor: viraTheme.colors[themeDefaultKey].background,
borderColor: viraTheme.colors[themeDefaultKey].foreground,
},
},
[ViraEmphasis.Subtle]: {
idle: {
backgroundColor: transparentColor,
textColor: viraTheme.colors[themeDefaultKey].foreground,
borderColor: transparentColor,
},
hover: {
backgroundColor: viraTheme.colors['vira-grey-behind-fg-small-body'].background,
textColor: viraTheme.colors['vira-grey-behind-fg-small-body'].foreground,
borderColor: viraTheme.colors['vira-grey-behind-fg-small-body'].background,
},
active: {
backgroundColor: viraTheme.colors['vira-grey-behind-fg-body'].background,
textColor: viraTheme.colors['vira-grey-behind-fg-body'].foreground,
borderColor: viraTheme.colors['vira-grey-behind-fg-body'].background,
},
},
};
const plainNotCheckedColors = {
idle: {
textColor: viraTheme.colors[themeDefaultKey].foreground,
backgroundColor: transparentColor,
borderColor: viraTheme.colors['vira-grey-on-self-body'].background,
},
hover: {
backgroundColor: viraTheme.colors['vira-grey-behind-fg-small-body'].background,
textColor: viraTheme.colors['vira-grey-behind-fg-small-body'].foreground,
borderColor: viraTheme.colors['vira-grey-on-self-body'].background,
},
active: {
backgroundColor: viraTheme.colors['vira-grey-behind-fg-body'].background,
textColor: viraTheme.colors['vira-grey-behind-fg-body'].foreground,
borderColor: viraTheme.colors['vira-grey-on-self-body'].background,
},
};
/**
* A "tag" or "label" or "pill" element. Supports many variations including non-clickable,
* selectable, and cancellable variations.
*
* @category Elements
* @see https://electrovir.github.io/vira/book/elements/vira-tag
*/
export const ViraTag = defineViraElement()({
tagName: 'vira-tag',
cssVars: {
'vira-tag-text-color': 'transparent',
'vira-tag-background-color': 'transparent',
'vira-tag-border-color': 'transparent',
'vira-tag-hover-text-color': 'transparent',
'vira-tag-hover-background-color': 'transparent',
'vira-tag-hover-border-color': 'transparent',
'vira-tag-active-text-color': 'transparent',
'vira-tag-active-background-color': 'transparent',
'vira-tag-active-border-color': 'transparent',
'vira-tag-disabled-text-color': viraTheme.colors['vira-grey-behind-bg-decoration'].foreground.value,
'vira-tag-disabled-background-color': viraTheme.colors['vira-grey-behind-bg-decoration'].background.value,
'vira-tag-disabled-border-color': viraTheme.colors['vira-grey-behind-bg-decoration'].background.value,
'vira-tag-border-radius': '1000px',
'vira-tag-gap': '6px',
'vira-tag-horizontal-padding': '12px',
'vira-tag-border-width': '1px',
},
events: {
toggle: defineElementEvent(),
cancel: defineElementEvent(),
},
hostClasses: {
'vira-tag-selectable': ({ inputs }) => check.isBoolean(inputs.isClickable?.selected),
'vira-tag-checked': ({ inputs }) => !!inputs.isClickable?.selected,
'vira-tag-not-checked': ({ inputs }) => inputs.isClickable?.selected === false,
'vira-tag-cancellable': ({ inputs }) => !!inputs.isClickable?.cancellable,
'vira-tag-not-clickable': ({ inputs }) => !inputs.isClickable,
'vira-tag-disabled': ({ inputs }) => !!inputs.disabled,
'vira-tag-size-large': ({ inputs }) => inputs.size === ViraSize.Large,
'vira-tag-size-medium': ({ inputs }) => !inputs.size || inputs.size === ViraSize.Medium,
'vira-tag-size-small': ({ inputs }) => inputs.size === ViraSize.Small,
'vira-tag-emphasis-standard': ({ inputs }) => !inputs.emphasis || inputs.emphasis === ViraEmphasis.Standard,
'vira-tag-emphasis-subtle': ({ inputs }) => inputs.emphasis === ViraEmphasis.Subtle,
...arrayToObject(getObjectTypedKeys(viraColorVariantToHostClassKey), (colorVariant) => {
const colorKey = viraColorVariantToHostClassKey[colorVariant];
return {
key: `vira-tag-color-${colorKey}`,
value: ({ inputs, }) => {
return inputs.color === colorVariant || inputs.color === colorKey;
},
};
}, {
useRequired: true,
}),
'vira-tag-color-plain': ({ inputs, }) => !inputs.color || inputs.color === ViraColorVariant.Plain,
'vira-tag-color-neutral': ({ inputs, }) => inputs.color === ViraColorVariant.Neutral,
...arrayToObject(standaloneThemeColorNames, (colorName) => {
return {
key: `vira-tag-color-${colorName}`,
value: ({ inputs, }) => {
return inputs.color === colorName;
},
};
}, {
useRequired: true,
}),
},
styles: ({ cssVars, hostClasses }) => {
function buildVariantCssRule({ variantSelector, emphasisSelector, colors, }) {
return css `
${variantSelector}${emphasisSelector} {
${cssVars['vira-tag-background-color'].name}: ${colors.idle.backgroundColor
.value};
${cssVars['vira-tag-text-color'].name}: ${colors.idle.textColor.value};
${cssVars['vira-tag-border-color'].name}: ${colors.idle.borderColor.value};
${cssVars['vira-tag-hover-background-color'].name}: ${colors.hover
.backgroundColor.value};
${cssVars['vira-tag-hover-text-color'].name}: ${colors.hover.textColor.value};
${cssVars['vira-tag-hover-border-color'].name}: ${colors.hover.borderColor
.value};
${cssVars['vira-tag-active-background-color'].name}: ${colors.active
.backgroundColor.value};
${cssVars['vira-tag-active-text-color'].name}: ${colors.active.textColor.value};
${cssVars['vira-tag-active-border-color'].name}: ${colors.active.borderColor
.value};
}
`;
}
function generateVariantCss() {
const allStyles = viraEmphasisVariants.flatMap((emphasis) => {
const emphasisSelector = hostClasses[`vira-tag-emphasis-${emphasis}`].selector;
const themedStyles = getObjectTypedKeys(viraColorVariantToHostClassKey).map((colorVariant) => {
const colorKey = viraColorVariantToHostClassKey[colorVariant];
const colors = buildThemedTagColors(colorKey)[emphasis];
const variantSelector = hostClasses[`vira-tag-color-${colorKey}`].selector;
return buildVariantCssRule({
variantSelector,
emphasisSelector,
colors,
});
});
const plainStyle = buildVariantCssRule({
variantSelector: hostClasses['vira-tag-color-plain'].selector,
emphasisSelector,
colors: plainTagColors[emphasis],
});
const neutralStyle = buildVariantCssRule({
variantSelector: hostClasses['vira-tag-color-neutral'].selector,
emphasisSelector,
colors: buildThemedTagColors(ViraThemeColorName.grey)[emphasis],
});
const standaloneStyles = standaloneThemeColorNames.map((colorName) => {
const colors = buildThemedTagColors(colorName)[emphasis];
const variantSelector = hostClasses[`vira-tag-color-${colorName}`].selector;
return buildVariantCssRule({
variantSelector,
emphasisSelector,
colors,
});
});
return [
...themedStyles,
plainStyle,
neutralStyle,
...standaloneStyles,
];
});
return unsafeCSS(allStyles.join('\n'));
}
function buildNotCheckedCssRule(variantSelector, colors) {
const notCheckedSelector = hostClasses['vira-tag-not-checked'].selector;
return css `
${variantSelector}${notCheckedSelector}${notCheckedSelector}${notCheckedSelector} {
${cssVars['vira-tag-background-color'].name}: ${colors.idle.backgroundColor
.value};
${cssVars['vira-tag-text-color'].name}: ${colors.idle.textColor.value};
${cssVars['vira-tag-border-color'].name}: ${colors.idle.borderColor.value};
${cssVars['vira-tag-hover-background-color'].name}: ${colors.hover
.backgroundColor.value};
${cssVars['vira-tag-hover-text-color'].name}: ${colors.hover.textColor.value};
${cssVars['vira-tag-hover-border-color'].name}: ${colors.hover.borderColor
.value};
${cssVars['vira-tag-active-background-color'].name}: ${colors.active
.backgroundColor.value};
${cssVars['vira-tag-active-text-color'].name}: ${colors.active.textColor.value};
${cssVars['vira-tag-active-border-color'].name}: ${colors.active.borderColor
.value};
}
`;
}
function generateNotCheckedCss() {
const themedStyles = getObjectTypedKeys(viraColorVariantToHostClassKey).map((colorVariant) => {
const colorKey = viraColorVariantToHostClassKey[colorVariant];
const colors = buildThemedNotCheckedColors(colorKey);
const variantSelector = hostClasses[`vira-tag-color-${colorKey}`].selector;
return buildNotCheckedCssRule(variantSelector, colors);
});
const plainStyle = buildNotCheckedCssRule(hostClasses['vira-tag-color-plain'].selector, plainNotCheckedColors);
const neutralStyle = buildNotCheckedCssRule(hostClasses['vira-tag-color-neutral'].selector, buildThemedNotCheckedColors(ViraThemeColorName.grey));
const standaloneStyles = standaloneThemeColorNames.map((colorName) => {
const colors = buildThemedNotCheckedColors(colorName);
const variantSelector = hostClasses[`vira-tag-color-${colorName}`].selector;
return buildNotCheckedCssRule(variantSelector, colors);
});
return unsafeCSS([
...themedStyles,
plainStyle,
neutralStyle,
...standaloneStyles,
].join('\n'));
}
function generateSizeVariantCss() {
const styles = viraSizeVariants.map((sizeVariant) => {
return css `
${hostClasses[`vira-tag-size-${sizeVariant}`].selector} button {
height: ${viraSizeHeights[sizeVariant]}px;
font-size: ${viraFormCssVars[`vira-form-${sizeVariant}-text-size`].value};
}
`;
});
return unsafeCSS(styles.join('\n'));
}
return css `
:host {
display: inline-flex;
}
${generateSizeVariantCss()}
${generateVariantCss()}
${generateNotCheckedCss()}
button {
${noNativeFormStyles}
flex-shrink: 0;
cursor: pointer;
display: flex;
align-items: center;
gap: ${cssVars['vira-tag-gap'].value};
border-radius: ${cssVars['vira-tag-border-radius'].value};
border-width: ${cssVars['vira-tag-border-width'].value};
border-style: solid;
border-color: ${cssVars['vira-tag-border-color'].value};
color: ${cssVars['vira-tag-text-color'].value};
background-color: ${cssVars['vira-tag-background-color'].value};
box-sizing: border-box;
padding: 0 ${cssVars['vira-tag-horizontal-padding'].value};
&[disabled] {
cursor: default;
pointer-events: none;
}
}
button:hover {
background-color: ${cssVars['vira-tag-hover-background-color'].value};
color: ${cssVars['vira-tag-hover-text-color'].value};
border-color: ${cssVars['vira-tag-hover-border-color'].value};
}
button:active {
background-color: ${cssVars['vira-tag-active-background-color'].value};
color: ${cssVars['vira-tag-active-text-color'].value};
border-color: ${cssVars['vira-tag-active-border-color'].value};
}
.cancel-x,
.selected-check,
.text {
height: 0;
display: flex;
align-items: center;
}
.cancel-x {
display: none;
margin-right: -2px;
}
.selected-check {
margin-left: -2px;
display: none;
visibility: hidden;
}
${hostClasses['vira-tag-selectable'].selector} {
& button {
padding: 0 calc(${cssVars['vira-tag-horizontal-padding'].value} + 5px);
margin-left: calc(4px + ${cssVars['vira-tag-gap'].value});
}
}
${hostClasses['vira-tag-checked'].selector} {
& button {
padding: 0 ${cssVars['vira-tag-horizontal-padding'].value};
margin-left: 0;
}
& .selected-check {
display: flex;
visibility: visible;
}
}
${hostClasses['vira-tag-cancellable'].selector} .cancel-x {
display: flex;
}
${hostClasses['vira-tag-size-small'].selector} {
${cssVars['vira-tag-gap'].name}: 4px;
${cssVars['vira-tag-horizontal-padding'].name}: 8px;
}
${hostClasses['vira-tag-size-large'].selector} {
${cssVars['vira-tag-horizontal-padding'].name}: 16px;
}
${hostClasses['vira-tag-disabled'].selector} {
cursor: not-allowed;
${noUserSelect}
& button {
color: ${cssVars['vira-tag-disabled-text-color'].value};
background-color: ${cssVars['vira-tag-disabled-background-color'].value};
border-color: ${cssVars['vira-tag-disabled-border-color'].value};
}
}
`;
},
render({ inputs, dispatch, events }) {
const disabled = !inputs.isClickable || !!inputs.disabled;
return html `
<button
?disabled=${disabled}
${listen('click', () => {
if (disabled) {
return;
}
else if (inputs.isClickable?.selected != undefined) {
dispatch(new events.toggle(!inputs.isClickable.selected));
}
else if (inputs.isClickable?.cancellable) {
dispatch(new events.cancel());
}
})}
>
<${ViraIcon.assign({
icon: Check16Icon,
})}
class="selected-check"
></${ViraIcon}>
<span class="text">${String(inputs.text)}</span>
<${ViraIcon.assign({
icon: X16Icon,
})}
class="cancel-x"
></${ViraIcon}>
</button>
`;
},
});