diff --git a/components/package.json b/components/package.json index 23c8fc4c..e381084a 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "@ensdomains/thorin", - "version": "0.6.17", + "version": "0.6.18", "description": "A web3 native design system", "main": "./dist/index.cjs.js", "module": "./dist/index.es.js", diff --git a/components/src/components/atoms/Avatar/Avatar.tsx b/components/src/components/atoms/Avatar/Avatar.tsx index 1a01b08c..a7e89325 100644 --- a/components/src/components/atoms/Avatar/Avatar.tsx +++ b/components/src/components/atoms/Avatar/Avatar.tsx @@ -61,8 +61,8 @@ const Container = styled.div( `, ) -const Placeholder = styled.div<{ $url?: string }>( - ({ theme, $url }) => css` +const Placeholder = styled.div<{ $url?: string; $disabled: boolean }>( + ({ theme, $url, $disabled }) => css` background: ${$url || theme.colors.gradients.blue}; display: flex; @@ -70,11 +70,16 @@ const Placeholder = styled.div<{ $url?: string }>( justify-content: center; width: 100%; height: 100%; + + ${$disabled && + css` + filter: grayscale(1); + `} `, ) -const Img = styled.img<{ $shown: boolean }>( - ({ $shown }) => css` +const Img = styled.img<{ $shown: boolean; $disabled: boolean }>( + ({ $shown, $disabled }) => css` height: 100%; width: 100%; object-fit: cover; @@ -84,6 +89,11 @@ const Img = styled.img<{ $shown: boolean }>( css` display: block; `} + + ${$disabled && + css` + filter: grayscale(1); + `} `, ) @@ -98,6 +108,8 @@ export type Props = { shape?: Shape /** A placeholder for the image to use when not loaded, in css format (e.g. url("https://example.com")) */ placeholder?: string + /** If true sets the component into disabled format. */ + disabled?: boolean } & Omit export const Avatar = ({ @@ -107,6 +119,7 @@ export const Avatar = ({ src, placeholder, decoding = 'async', + disabled = false, ...props }: Props) => { const ref = React.useRef(null) @@ -138,9 +151,16 @@ export const Avatar = ({ return ( - {!showImage && } + {!showImage && ( + + )} {label} +type Size = 'small' | 'medium' + type Props = { /** Total number of pages */ total: number current: number /** Maximum number of buttons to show */ max?: number + size?: Size alwaysShowFirst?: boolean alwaysShowLast?: boolean + showEllipsis?: boolean onChange: (value: number) => void } & Omit +enum Marker { + ellipsis = -1, +} + const Container = styled.div( ({ theme }) => css` display: flex; @@ -27,8 +35,8 @@ const Container = styled.div( `, ) -const PageButton = styled.button<{ $selected?: boolean }>( - ({ theme, $selected }) => css` +const PageButton = styled.button<{ $selected?: boolean; $size: Size }>( + ({ theme, $selected, $size }) => css` background-color: transparent; transition: all 0.15s ease-in-out; cursor: pointer; @@ -37,21 +45,26 @@ const PageButton = styled.button<{ $selected?: boolean }>( background-color: ${theme.colors.background}; cursor: default; pointer-events: none; + color: ${theme.colors.accent}; ` : css` + color: ${theme.colors.text}; &:hover { background-color: ${theme.colors.foregroundSecondary}; } `} - border-radius: ${theme.radii['extraLarge']}; + border-radius: ${$size === 'small' + ? theme.space['2'] + : theme.radii['extraLarge']}; border: 1px solid ${theme.colors.borderSecondary}; - min-width: ${theme.space['10']}; + min-width: ${$size === 'small' ? theme.space['9'] : theme.space['10']}; padding: ${theme.space['2']}; - height: ${theme.space['10']}; - font-size: ${theme.fontSizes['small']}; + height: ${$size === 'small' ? theme.space['9'] : theme.space['10']}; + font-size: ${$size === 'small' + ? theme.space['3.5'] + : theme.fontSizes['small']}; font-weight: ${theme.fontWeights['medium']}; - color: ${theme.colors.text}; `, ) @@ -67,8 +80,10 @@ export const PageButtons = ({ total, current, max = 5, + size = 'medium', alwaysShowFirst, alwaysShowLast, + showEllipsis = true, onChange, ...props }: Props) => { @@ -77,23 +92,31 @@ export const PageButtons = ({ Math.min(Math.max(current - maxPerSide, 1), total - max + 1), 1, ) - const array = Array.from({ length: max }, (_, i) => start + i).filter( + const pageNumbers = Array.from({ length: max }, (_, i) => start + i).filter( (x) => x <= total, ) if (total > max) { if (alwaysShowFirst && start > 1) { - array[0] = -1 - array.unshift(1) - } else if (start > 1) { - array.unshift(-1) + if (showEllipsis) { + pageNumbers[0] = Marker.ellipsis + pageNumbers.unshift(1) + } else { + pageNumbers[0] = 1 + } + } else if (showEllipsis && start > 1) { + pageNumbers.unshift(Marker.ellipsis) } if (alwaysShowLast && total > current + maxPerSide) { - array[array.length - 1] = -1 * total - array.push(total) - } else if (total > current + maxPerSide) { - array.push(-1 * total) + if (showEllipsis) { + pageNumbers[pageNumbers.length - 1] = Marker.ellipsis + pageNumbers.push(total) + } else { + pageNumbers[pageNumbers.length - 1] = total + } + } else if (showEllipsis && total > current + maxPerSide) { + pageNumbers.push(Marker.ellipsis) } } @@ -101,14 +124,16 @@ export const PageButtons = ({ - {array.map((value) => - 0 > value ? ( - + {pageNumbers.map((value, i) => + value === Marker.ellipsis ? ( + // eslint-disable-next-line react/no-array-index-key + ... ) : ( ', () => { , ) userEvent.click(screen.getByTestId('select-container')) - userEvent.click(screen.getByText('One')) - expect(screen.getByTestId('selected').innerHTML).toEqual('One') + userEvent.click(screen.getByTestId('select-option-1')) + expect(screen.getByTestId('selected').innerHTML).toContain('One') }) it('should call onChange when selection made', async () => { @@ -64,7 +64,7 @@ describe('', () => { ) await waitFor(() => { - expect(screen.getByTestId('selected').innerHTML).toEqual('One') + expect(screen.getByTestId('selected').innerHTML).toContain('One') }) }) @@ -214,7 +214,7 @@ describe('', () => { userEvent.type(screen.getByTestId('select-input'), 'onsies') expect(screen.getByTestId('select-input')).toHaveValue('onsies') - const create = await screen.findByText('Add "onsies"') + const create = await screen.findByTestId( + 'select-option-CREATE_OPTION_VALUE', + ) userEvent.click(create) await waitFor(() => { diff --git a/components/src/components/molecules/Select/Select.tsx b/components/src/components/molecules/Select/Select.tsx index ff04a614..63a698ec 100644 --- a/components/src/components/molecules/Select/Select.tsx +++ b/components/src/components/molecules/Select/Select.tsx @@ -15,13 +15,19 @@ import { Space } from '@/src/tokens' const CREATE_OPTION_VALUE = 'CREATE_OPTION_VALUE' -type Size = 'small' | 'medium' +type Size = 'small' | 'medium' | 'large' -const SelectContainer = styled.div<{ $disabled?: boolean; $size: Size }>( - ({ theme, $disabled, $size }) => css` - background: ${theme.colors.background}; - border-color: ${theme.colors.backgroundHide}; - border-width: ${theme.space['px']}; +const SelectContainer = styled.div<{ + $disabled?: boolean + $size: Size + $showBorder: boolean +}>( + ({ theme, $disabled, $size, $showBorder }) => css` + background: ${theme.colors.backgroundSecondary}; + ${$showBorder && + css` + border: 1px solid ${theme.colors.backgroundHide}; + `}; cursor: pointer; position: relative; display: flex; @@ -30,14 +36,20 @@ const SelectContainer = styled.div<{ $disabled?: boolean; $size: Size }>( justify-content: space-between; z-index: 10; overflow: hidden; - ${$size === 'medium' + ${$size === 'small' ? css` - border-radius: ${theme.radii['2xLarge']}; - height: ${theme.space['14']}; + border-radius: ${theme.space['2']}; + height: ${theme.space['9']}; + font-size: ${theme.space['3.5']}; ` - : css` + : $size === 'medium' + ? css` border-radius: ${theme.radii['almostExtraLarge']}; height: ${theme.space['10']}; + ` + : css` + border-radius: ${theme.radii['2xLarge']}; + height: ${theme.space['14']}; `} ${$disabled && @@ -51,6 +63,12 @@ const SelectContainer = styled.div<{ $disabled?: boolean; $size: Size }>( const SelectContentContainer = styled.div( () => css` flex: 1; + overflow: hidden; + display: flex; + + svg { + display: block; + } `, ) @@ -63,6 +81,15 @@ const SelectActionContainer = styled.div( `, ) +const SelectLabel = styled.div( + () => css` + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + line-height: 1.4; + `, +) + const OptionElementContainer = styled.div<{ $padding: Space; $gap: Space }>( ({ theme, $padding, $gap }) => css` align-items: center; @@ -72,6 +99,7 @@ const OptionElementContainer = styled.div<{ $padding: Space; $gap: Space }>( gap: ${theme.space[$gap]}; padding: ${theme.space[$padding]}; padding-right: 0; + overflow: hidden; `, ) @@ -79,21 +107,25 @@ const NoOptionContainer = styled.div<{ $padding: Space }>( ({ theme, $padding }) => css` padding: ${theme.space[$padding]}; padding-right: 0; - font-style: italic; + color: ${theme.colors.textPlaceholder}; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; `, ) const SelectInput = styled.input<{ $padding: Space }>( ({ theme, $padding }) => css` padding: ${theme.space[$padding]}; + background: transparent; padding-right: 0; width: 100%; height: 100%; `, ) -const SelectActionButton = styled.button<{ $padding: Space }>( - ({ theme, $padding }) => css` +const SelectActionButton = styled.button<{ $padding: Space; $size: Size }>( + ({ theme, $padding, $size }) => css` display: flex; justify-content: center; align-items: center; @@ -102,7 +134,7 @@ const SelectActionButton = styled.button<{ $padding: Space }>( padding: ${theme.space[$padding]}; svg { display: block; - width: 12px; + width: ${$size === 'small' ? theme.space['2'] : theme.space['3']}; path { color: ${theme.colors.textSecondary}; } @@ -117,7 +149,6 @@ const Chevron = styled(IconDownIndicatorSvg)<{ }>( ({ theme, $open, $disabled, $direction }) => css` margin-left: ${theme.space['1']}; - width: ${theme.space['3']}; margin-right: ${theme.space['0.5']}; transition-duration: ${theme.transitionDuration['200']}; transition-property: all; @@ -148,8 +179,10 @@ const SelectOptionContainer = styled.div<{ $state?: TransitionState $direction?: Direction $rows?: number + $size?: Size + $align?: 'left' | 'right' }>( - ({ theme, $state, $direction, $rows }) => css` + ({ theme, $state, $direction, $rows, $size, $align }) => css` display: ${$state === 'exited' ? 'none' : 'block'}; position: absolute; visibility: hidden; @@ -159,11 +192,23 @@ const SelectOptionContainer = styled.div<{ margin-top: ${theme.space['1.5']}; padding: ${theme.space['1.5']}; min-width: ${theme.space['full']}; + ${$align === 'right' + ? css` + right: 0; + ` + : css` + left: 0; + `} border-radius: ${theme.radii['medium']}; box-shadow: ${theme.boxShadows['0.02']}; background: ${theme.colors.background}; transition: all 0.3s cubic-bezier(1, 0, 0.22, 1.6), z-index 0.3s linear; + ${$size === 'small' && + css` + font-size: ${theme.space['3.5']}; + `} + ${$state === 'entered' ? css` z-index: 20; @@ -292,6 +337,10 @@ const SelectOption = styled.div<{ background-color: ${theme.colors.transparent}; } `} + + svg { + display: block; + } `, ) @@ -372,8 +421,6 @@ export type SelectProps = { direction?: Direction /** The string or component to prefix the value in the create value option. */ createablePrefix?: string - /** The message for when there is no option selected */ - noSelectionMessage?: string /** The handler for change events. */ onChange?: NativeSelectProps['onChange'] /** The tabindex attribute for */ @@ -398,6 +445,10 @@ export type SelectProps = { padding?: Space | { outer?: Space; inner?: Space } /** The size attribute for input element. Useful for controlling input size in flexboxes. */ inputSize?: number | { max?: number; min?: number } + /** If true, show a border around the select component **/ + showBorder?: boolean + /** If the option list is wider than the select, which */ + align?: 'left' | 'right' } & FieldBaseProps & Omit< NativeDivProps, @@ -442,7 +493,7 @@ export const Select = React.forwardRef( autocomplete = false, createable = false, createablePrefix = 'Add ', - noSelectionMessage, + placeholder, direction = 'down', error, hideLabel, @@ -465,6 +516,8 @@ export const Select = React.forwardRef( size = 'medium', padding: paddingProp, inputSize: inputSizeProps, + showBorder = false, + align, ...props }: SelectProps, ref: React.Ref, @@ -615,7 +668,7 @@ export const Select = React.forwardRef( if (!menuOpen && state === 'unmounted') handleReset() }, [menuOpen, state]) - const defaultPadding = size === 'medium' ? '4' : '2' + const defaultPadding = size === 'small' ? '3' : '4' const outerPadding = getPadding('outer', defaultPadding, paddingProp) const innerPadding = getPadding('inner', defaultPadding, paddingProp) @@ -698,7 +751,9 @@ export const Select = React.forwardRef( option ? ( {option.prefix &&
{option.prefix}
} - {option.node ? option.node : option.label || option.value} + + {option.node ? option.node : option.label || option.value} +
) : null @@ -729,6 +784,7 @@ export const Select = React.forwardRef( onKeyDown: handleKeydown, }} $disabled={disabled} + $showBorder={showBorder} $size={size} id={`combo-${id}`} ref={displayRef} @@ -763,9 +819,9 @@ export const Select = React.forwardRef( > - ) : noSelectionMessage ? ( + ) : placeholder ? ( - {noSelectionMessage} + {placeholder} ) : null} @@ -773,13 +829,18 @@ export const Select = React.forwardRef( {showClearButton ? ( ) : ( - + ``` +## Disabled + +The placeholder is shown if the src property is undefined or if an error occurs loading the src. + +```tsx live=true + + Placeholder +
+ +
+ Avatar +
+ +
+
+``` +