diff --git a/src/components/forms/Input.tsx b/src/components/forms/Input.tsx index 890b9ce5..6de17da5 100644 --- a/src/components/forms/Input.tsx +++ b/src/components/forms/Input.tsx @@ -1,33 +1,30 @@ import styled from '@emotion/styled'; -import { InputHTMLAttributes, ReactNode } from 'react'; +import { CSSProperties, InputHTMLAttributes, ReactNode } from 'react'; + +import { FullSpinner } from '../index'; import { useFieldsContext } from './context/FieldsContext'; +type InputVariant = 'default' | 'small'; + interface StyledProps { - variant: 'default' | 'small'; + variant: InputVariant; hasLeading: boolean; hasTrailing: boolean; } -function getSpecialSize(props: Pick) { - return { - fontSize: props.variant === 'small' ? '1em' : '1.125em', - lineHeight: props.variant === 'small' ? '15px' : '17px', - }; -} - const LabelStyled = styled.label` padding: ${(props) => props.variant === 'default' ? props.hasTrailing - ? '4px 11px 4px 11px' - : '4px 11px' + ? '2px 9px 4px 9px' + : '2px 9px' : props.hasTrailing - ? '0px 7px 0px 7px' - : '0 7px'}; + ? '1px 7px 1px 7px' + : '1px 7px'}; - font-size: ${(props) => getSpecialSize(props).fontSize}; - line-height: ${(props) => getSpecialSize(props).lineHeight}; + font-size: ${(props) => (props.variant === 'small' ? '1em' : '1.125em')}; + line-height: '17px'; background-color: white; border-width: 1px; @@ -55,16 +52,43 @@ const LabelStyled = styled.label` border-color: var(--custom-border-color); `; -const GroupStyled = styled.div` +function getStyleColor(hasError: boolean, hasValid: boolean) { + if (hasError) { + return { + default: '#ffa39e', + hover: '#f95d55', + }; + } + + if (hasValid) { + return { + default: '#6adc24', + hover: '#62cb21', + }; + } + + return { + default: 'rgb(217, 217, 217)', + hover: '#4096ff', + }; +} + +const GroupStyled = styled.div<{ hasError: boolean; hasValid: boolean }>` display: flex; border-radius: 0.375rem; margin-top: 0.25rem; - --custom-border-color: rgb(217, 217, 217); + .addon { + color: ${({ hasError }) => hasError && '#f95d55'}; + } + + --custom-border-color: ${({ hasError, hasValid }) => + getStyleColor(hasError, hasValid).default}; :hover, :focus-within { - --custom-border-color: #4096ff; + --custom-border-color: ${({ hasError, hasValid }) => + getStyleColor(hasError, hasValid).hover}; } `; @@ -118,6 +142,12 @@ const TrailingInlineAddonStyled = styled.div` padding-left: 0.5rem; `; +const RootInput = styled.div` + display: flex; + flex-direction: column; + gap: 2px; +`; + interface RenderAddon { addon: ReactNode; inline?: boolean; @@ -128,41 +158,98 @@ export interface InputProps extends InputHTMLAttributes { leadingAddon?: RenderAddon; trailingAddon?: RenderAddon; + + help?: string; + error?: string; + valid?: true | string; + + loading?: boolean; } export function Input(props: InputProps) { - const { variant, trailingAddon, leadingAddon, ...otherProps } = props; + const { + variant: variantProps, + trailingAddon, + leadingAddon, + help, + error, + valid, + loading, + ...otherProps + } = props; const { name, variant: contextVariant } = useFieldsContext(); const hasLeading = (leadingAddon && !leadingAddon.inline) || false; const hasTrailing = (trailingAddon && !trailingAddon.inline) || false; + const variant = variantProps || contextVariant; return ( - - {leadingAddon && !leadingAddon.inline && ( - {leadingAddon.addon} - )} - - {leadingAddon?.inline && ( - - {leadingAddon.addon} - + + + {leadingAddon && !leadingAddon.inline && ( + {leadingAddon.addon} )} - - {trailingAddon?.inline && ( - - {trailingAddon.addon} - + + + {leadingAddon?.inline && ( + + {leadingAddon.addon} + + )} + + {trailingAddon?.inline && ( + + {trailingAddon.addon} + + )} + + {loading && ( + + + + )} + + + {trailingAddon && !trailingAddon.inline && ( + {trailingAddon.addon} )} - - {trailingAddon && !trailingAddon.inline && ( - {trailingAddon.addon} - )} - + + + + ); } + +function SubText(props: Pick) { + const { error, help, valid: validProps } = props; + + const valid = typeof validProps === 'string' ? validProps : undefined; + const text = error || valid || help; + + return

{text}

; +} + +function getColor( + error?: string, + valid?: true | string, +): CSSProperties['color'] { + if (error) { + return '#f95d55'; + } + + if (valid && typeof valid !== 'boolean') { + return '#62cb21'; + } + + return 'gray'; +} diff --git a/src/components/forms/context/FieldsContext.tsx b/src/components/forms/context/FieldsContext.tsx index 90594a45..6b66e74d 100644 --- a/src/components/forms/context/FieldsContext.tsx +++ b/src/components/forms/context/FieldsContext.tsx @@ -18,9 +18,30 @@ const context = createContext(null); const FieldContextRoot = styled.div` display: flex; - flex-direction: row; + flex-flow: row; + min-width: 0; + margin: 0; + padding: 0; gap: 5px; - align-items: center; +`; + +const Label = styled.label<{ variant: FieldProps['variant'] }>` + position: relative; + display: inline-flex; + max-width: 100%; + line-height: ${(props) => (props.variant === 'small' ? '28px' : '32px')}; + font-size: ${(props) => (props.variant === 'small' ? '1em' : '1.125em')}; + white-space: nowrap; + text-align: end; +`; + +const LabelContainer = styled.div` + flex-grow: 0; + display: inline-block; + overflow: hidden; + text-align: end; + vertical-align: middle; + white-space: nowrap; `; const FieldContextRequired = styled.span` @@ -47,9 +68,13 @@ export function Field(props: FieldProps) { return ( - + + + + {children} diff --git a/src/components/spinner/FullSpinner.tsx b/src/components/spinner/FullSpinner.tsx index df2085a3..bff479de 100644 --- a/src/components/spinner/FullSpinner.tsx +++ b/src/components/spinner/FullSpinner.tsx @@ -1,5 +1,5 @@ -/** @jsxImportSource @emotion/react */ -import { css, keyframes } from '@emotion/react'; +import { keyframes } from '@emotion/react'; +import styled from '@emotion/styled'; const spin = keyframes` 100% { @@ -7,13 +7,18 @@ const spin = keyframes` } `; -const spinnerStyle = css` - height: 40px; - width: 40px; +interface FullSpinnerProps { + width?: number; + height?: number; +} + +const Spinner = styled.svg` + height: ${({ height }) => `${height || 40}px`}; + width: ${({ width }) => `${width || 40}px`}; animation: ${spin} 0.8s linear infinite; `; -export function FullSpinner() { +export function FullSpinner(props: FullSpinnerProps) { // First div is used when using nextjs/dynamic even after component is loaded return (
@@ -26,8 +31,8 @@ export function FullSpinner() { justifyContent: 'center', }} > - - +
); diff --git a/stories/components/button.stories.tsx b/stories/components/button.stories.tsx index a3b38509..789d3194 100644 --- a/stories/components/button.stories.tsx +++ b/stories/components/button.stories.tsx @@ -11,8 +11,12 @@ export function Basic() { export function ButtonGroupBasic() { return ( - {}} /> - {}} /> + + ); } + +function noop() { + // Do nothing +} diff --git a/stories/components/dropdown.stories.tsx b/stories/components/dropdown.stories.tsx index c933bdef..e711bf68 100644 --- a/stories/components/dropdown.stories.tsx +++ b/stories/components/dropdown.stories.tsx @@ -27,6 +27,10 @@ const defaultOptions: MenuOptions = [ { label: 'Default workspace', type: 'option' }, ]; +function noop() { + // do nothing +} + const ButtonStyled = styled.div` box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); color: white; @@ -55,7 +59,7 @@ export function Dropdown() { }, []); return ( - {}} options={options}> + Default workspace ); @@ -82,7 +86,7 @@ export function ContextDropdown() { }, []); return ( - {}} options={options}> +

Hello, World!

@@ -100,7 +104,7 @@ export function WithIcon() { return ( - {}} /> + ); } @@ -112,7 +116,7 @@ export function WithoutIcon() { return ( - {}} /> + ); } @@ -127,7 +131,7 @@ export function WithDisabled() { return ( - {}} /> + ); } @@ -143,7 +147,7 @@ export function WithDivider() { return ( - {}} /> + ); } @@ -179,7 +183,7 @@ export function Complex() { return ( - {}} /> + ); } diff --git a/stories/components/input.stories.tsx b/stories/components/input.stories.tsx index 3cce0354..379db594 100644 --- a/stories/components/input.stories.tsx +++ b/stories/components/input.stories.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import { ChangeEvent, useState } from 'react'; -import { FaMeteor } from 'react-icons/fa'; +import { FaMeteor, FaUser, FaBolt, FaShieldAlt } from 'react-icons/fa'; import { Input, Field } from '../../src/components'; @@ -52,13 +52,8 @@ export function Required() { - - + + ); @@ -76,13 +71,8 @@ export function Label() { - - + + ); @@ -94,24 +84,24 @@ export function WithTrailingAddon() { }} + trailingAddon={{ addon: }} /> }} + trailingAddon={{ addon: }} variant="small" /> , inline: true }} + trailingAddon={{ addon: , inline: true }} /> , inline: true }} + trailingAddon={{ addon: , inline: true }} variant="small" /> @@ -125,24 +115,24 @@ export function WithLeadingAddon() { }} + leadingAddon={{ addon: }} /> }} + leadingAddon={{ addon: }} variant="small" /> , inline: true }} + leadingAddon={{ addon: , inline: true }} /> , inline: true }} + leadingAddon={{ addon: , inline: true }} variant="small" /> @@ -189,7 +179,7 @@ export function TwoInputWithOneLabel() { return ( - + @@ -199,3 +189,73 @@ export function TwoInputWithOneLabel() { ); } + +export function WithSpinner() { + return ( + + + + + ); +} + +export function WithSubtext() { + return ( + + + , inline: true }} + /> + , inline: true }} + /> + + + , inline: true }} + /> + , inline: true }} + /> + + + , inline: true }} + /> + , inline: true }} + /> + + + , inline: true }} + /> + , inline: true }} + /> + + + ); +}