import { css } from '@tesseract-design/goofy-goober'; export enum TextControlSize { SMALL = 'small', MEDIUM = 'medium', LARGE = 'large', } export enum TextControlStyle { DEFAULT = 'default', ALTERNATE = 'alternate', } export const MIN_HEIGHTS: Record = { [TextControlSize.SMALL]: '2.5rem', [TextControlSize.MEDIUM]: '3rem', [TextControlSize.LARGE]: '4rem', }; const LABEL_VERTICAL_PADDING_SIZES: Record = { [TextControlSize.SMALL]: '0.125rem', [TextControlSize.MEDIUM]: '0.25rem', [TextControlSize.LARGE]: '0.375rem', }; const INPUT_FONT_SIZES: Record = { [TextControlSize.SMALL]: '0.75em', [TextControlSize.MEDIUM]: '0.85em', [TextControlSize.LARGE]: '1em', }; const SECONDARY_TEXT_SIZES: Record = { [TextControlSize.SMALL]: '0.6em', [TextControlSize.MEDIUM]: '0.725em', [TextControlSize.LARGE]: '0.85em', }; const MULTILINE_VERTICAL_PADDING_FACTORS: Record = { [TextControlSize.SMALL]: '1.25', [TextControlSize.MEDIUM]: '1.2', [TextControlSize.LARGE]: '1.45', }; const ALTERNATE_VERTICAL_PADDING_FACTORS: Record = { [TextControlSize.SMALL]: '1.75', [TextControlSize.MEDIUM]: '1.35', [TextControlSize.LARGE]: '1.25', }; export type TextControlBaseArgs = { /** * Will the component occupy the whole width of its container? */ block: boolean, /** * Stylistic variant of the component. */ style: TextControlStyle, /** * Will the component display a surrounding border? */ border: boolean, /** * Does the component include an additional indicator for labels? */ indicator: boolean, /** * Size of the component. */ size: TextControlSize, /** * Can the size of the component be changed? */ resizable: boolean, /** * Does this component have predefined values? */ predefinedValues: boolean, } export const Root = ({ block, }: TextControlBaseArgs): string => css.cx( css` vertical-align: middle; position: relative; border-radius: 0.25rem; font-family: var(--font-family-base, sans-serif); max-width: 100%; &:focus-within { --color-accent: var(--color-hover, red); } & > span { border-color: var(--color-accent, blue); box-sizing: border-box; display: inline-block; border-width: 0.125rem; border-style: solid; position: absolute; top: 0; left: 0; width: 100%; height: 100%; border-radius: inherit; z-index: 2; pointer-events: none; transition-property: border-color; } & > span::before { position: absolute; top: 0; left: 0; width: 100%; height: 100%; content: ''; border-radius: 0.125rem; opacity: 0.5; pointer-events: none; box-shadow: 0 0 0 0 var(--color-accent, blue); transition-property: box-shadow; transition-duration: 150ms; transition-timing-function: linear; } &:focus-within > span::before { box-shadow: 0 0 0 0.375rem var(--color-accent, blue); } `, css.dynamic({ display: block ? 'block' : 'inline-block', }), ); export const LabelWrapper = ({ style, border, indicator, size, }: TextControlBaseArgs): string => css.cx( css` color: var(--color-accent, blue); box-sizing: border-box; position: absolute; top: 0; left: 0; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: bolder; z-index: 1; pointer-events: none; transition-property: color; line-height: 0.65; user-select: none; `, css.dynamic({ 'padding-bottom': LABEL_VERTICAL_PADDING_SIZES[size], 'font-size': SECONDARY_TEXT_SIZES[size], }), css.if (border) ( css` background-color: var(--color-bg, white); ` ), css.if (style === TextControlStyle.ALTERNATE) ( css.dynamic({ 'padding-top': `calc(${LABEL_VERTICAL_PADDING_SIZES[size]} * 0.5)`, }), css.if (border) ( css` padding-left: 0.5rem; `, css.dynamic({ 'padding-right': indicator ? MIN_HEIGHTS[size] : '0.5rem', }), ), css.if (!border && indicator) ( css.dynamic({ 'padding-right': MIN_HEIGHTS[size], }), ), ), css.if (style === TextControlStyle.DEFAULT) ( css` padding-left: 0.5rem; `, css.dynamic({ 'padding-top': LABEL_VERTICAL_PADDING_SIZES[size], 'padding-right': !indicator ? '0.5rem' : MIN_HEIGHTS[size], }), ), ) export const Input = ({ style, size, indicator, border, resizable, predefinedValues, }: TextControlBaseArgs): string => css.cx( css` appearance: none; display: block; box-sizing: border-box; position: relative; border: 0; border-radius: inherit; margin: 0; font-family: inherit; min-width: 8rem; max-width: 100%; width: 100%; z-index: 1; transition-property: background-color, color; &:focus { outline: 0; color: var(--color-fg, black); } &:disabled { cursor: not-allowed; opacity: 0.5; } &:disabled ~ * { opacity: 0.5; } `, css.media('only screen') ( css` background-color: var(--color-bg, white); color: var(--color-fg, black); ` ), css.dynamic({ 'min-height': MIN_HEIGHTS[size], 'font-size': INPUT_FONT_SIZES[size], }), css.if (resizable) ( css` resize: vertical; ` ), css.if (predefinedValues) ( css` cursor: pointer; ` ), css.if (border) ( css` background-color: var(--color-bg, white); ` ), css.if (style === TextControlStyle.ALTERNATE) ( css` padding-bottom: 0; `, css.dynamic({ 'padding-top': resizable ? `calc(${SECONDARY_TEXT_SIZES[size]} * 2.5)` : `calc(${SECONDARY_TEXT_SIZES[size]} * 2)`, 'line-height': `calc(${MULTILINE_VERTICAL_PADDING_FACTORS[size]} * 1.1)`, }), css.if (border) ( css` padding-left: 0.5rem; `, css.dynamic({ 'padding-right': indicator ? MIN_HEIGHTS[size] : '0.5rem', }), ), css.if (!border && indicator) ( css.dynamic({ 'padding-right': MIN_HEIGHTS[size], }), ) ), css.if (style === TextControlStyle.DEFAULT) ( css` padding-left: 1rem; `, css.dynamic({ 'padding-right': !indicator ? '1rem' : MIN_HEIGHTS[size], 'line-height': `calc(${MULTILINE_VERTICAL_PADDING_FACTORS[size]} * 1.1)`, }), css.if (resizable) ( css.dynamic({ 'padding-top': `calc(${SECONDARY_TEXT_SIZES[size]} * ${MULTILINE_VERTICAL_PADDING_FACTORS[size]})`, 'padding-bottom': `calc(${SECONDARY_TEXT_SIZES[size]} * ${MULTILINE_VERTICAL_PADDING_FACTORS[size]})`, }) ), css.if (!resizable) ( css.dynamic({ 'padding-bottom': `calc(${SECONDARY_TEXT_SIZES[size]} * ${MULTILINE_VERTICAL_PADDING_FACTORS[size]} * 0.5)`, }) ) ), ) export const HintWrapper = ({ style, size, border, }: TextControlBaseArgs): string => css.cx( css` box-sizing: border-box; position: absolute; left: 0; font-size: 0.85em; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; z-index: 1; pointer-events: none; user-select: none; line-height: 0; `, css.if (border) ( css` background-color: var(--color-bg, white); ` ), css.if (style === TextControlStyle.ALTERNATE) ( css` line-height: 1.25; `, css.dynamic({ top: `calc(${SECONDARY_TEXT_SIZES[size]} * ${ALTERNATE_VERTICAL_PADDING_FACTORS[size]})`, 'font-size': SECONDARY_TEXT_SIZES[size], }), css.if (border) ( css` padding-left: 0.5rem; &:last-child { padding-right: 0.5rem; } `, css.dynamic({ 'padding-right': MIN_HEIGHTS[size], }) ) ), css.if (style === TextControlStyle.DEFAULT) ( css` bottom: 0; padding-left: 1rem; line-height: 1.25; &:last-child { padding-right: 1rem; } `, css.dynamic({ 'padding-bottom': `calc(${LABEL_VERTICAL_PADDING_SIZES[size]} * 0.9)`, 'padding-right': MIN_HEIGHTS[size], 'font-size': SECONDARY_TEXT_SIZES[size], }) ) ) export const Hint = (): string => css.cx( css` opacity: 0.5; ` ); export const IndicatorWrapper = ({ size }: TextControlBaseArgs): string => css.cx( css` color: var(--color-accent, blue); box-sizing: border-box; position: absolute; bottom: 0; right: 0; display: grid; place-content: center; padding: 0 1rem; z-index: 2; pointer-events: none; transition-property: color; line-height: 1; user-select: none; `, css.dynamic({ width: MIN_HEIGHTS[size], height: MIN_HEIGHTS[size], }), ); export const Indicator = (): string => css.cx( css` width: 1.5em; height: 1.5em; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; `, );