diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 23e2b31f..5a676587 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -39,7 +39,7 @@ .light .sb-show-main.sb-main-centered { color: #11111F; - background: #E6E6F0; + background: #F7F7FA; } .dark .sb-show-main.sb-main-centered { diff --git a/src/form/Range/Range.story.tsx b/src/form/Range/Range.story.tsx index 175e26e8..2a2a47b0 100644 --- a/src/form/Range/Range.story.tsx +++ b/src/form/Range/Range.story.tsx @@ -4,14 +4,7 @@ import { RangeSingle } from './RangeSingle'; export default { title: 'Components/Form/Range', - component: RangeSingle, - decorators: [ - Story => ( -
- -
- ) - ] + component: RangeSingle }; export const Single = () => { diff --git a/src/form/Select/Select.tsx b/src/form/Select/Select.tsx index 1472dc57..347d0662 100644 --- a/src/form/Select/Select.tsx +++ b/src/form/Select/Select.tsx @@ -151,6 +151,11 @@ export interface SelectProps { */ menuDisabled?: boolean; + /** + * The size of the select. + */ + size?: 'small' | 'medium' | 'large' | string; + /** * When the input receives a key down event. */ @@ -244,6 +249,7 @@ export const Select: FC = ({ value, defaultFilterValue, required, + size = 'medium', input = , menu = , onRefresh, @@ -761,6 +767,7 @@ export const Select: FC = ({ inputSearchText={keyword} loading={loading} filterable={filterable} + size={size} onSelectedChange={onMenuSelectedChange} /> )} @@ -789,6 +796,7 @@ export const Select: FC = ({ selectedOption={selectedOption} clearable={clearable} menuDisabled={menuDisabled} + size={size} onSelectedChange={toggleSelectedOption} onExpandClick={onInputExpanded} onKeyDown={onInputKeyedDown} diff --git a/src/form/Select/SelectInput/SelectInput.tsx b/src/form/Select/SelectInput/SelectInput.tsx index 22b94344..957dc3b1 100644 --- a/src/form/Select/SelectInput/SelectInput.tsx +++ b/src/form/Select/SelectInput/SelectInput.tsx @@ -15,8 +15,7 @@ import { CloseIcon } from '@/form/Select/icons/CloseIcon'; import { DotsLoader } from '@/elements/Loader/DotsLoader'; import { RefreshIcon } from '@/form/Select/icons/RefreshIcon'; import { SelectInputChip, SelectInputChipProps } from './SelectInputChip'; -import { twMerge } from 'tailwind-merge'; -import { useComponentTheme } from '@/utils'; +import { cn, useComponentTheme } from '@/utils'; import { SelectTheme } from '@/form/Select/SelectTheme'; import { CloneElement } from '@/utils'; @@ -131,6 +130,11 @@ export interface SelectInputProps { */ menuDisabled?: boolean; + /** + * The size of the select input. + */ + size?: 'small' | 'medium' | 'large' | string; + /** * The theme of the select input. */ @@ -250,6 +254,7 @@ export const SelectInput: FC = ({ error, menuDisabled, menuOpen, + size, refreshIcon = , closeIcon = , expandIcon = , @@ -433,11 +438,9 @@ export const SelectInput: FC = ({ if (multipleOptions?.length) { return (
{multipleOptions.map(option => ( @@ -459,7 +462,7 @@ export const SelectInput: FC = ({ if (singleOption?.inputLabel && !inputText) { return (
= ({ ]); return ( -
+
- {renderPrefix()} - -
-
- {refreshable && !loading && ( - - )} - {loading &&
{loadingIcon}
} - {showClear && ( - - )} - {!menuDisabled && ( - - )} + value={inputTextValue} + autoCorrect="off" + spellCheck="false" + autoComplete="off" + onKeyDown={onInputKeyDown} + onKeyUp={onInputKeyUp} + onChange={onChange} + onFocus={onInputFocus} + onBlur={onBlur} + onPaste={onPaste} + placeholderIsMinWidth={false} + /> +
+
+ {refreshable && !loading && ( + + )} + {loading &&
{loadingIcon}
} + {showClear && ( + + )} + {!menuDisabled && ( + + )} +
); diff --git a/src/form/Select/SelectInput/SelectInputTheme.ts b/src/form/Select/SelectInput/SelectInputTheme.ts index 8095b283..dc477f7f 100644 --- a/src/form/Select/SelectInput/SelectInputTheme.ts +++ b/src/form/Select/SelectInput/SelectInputTheme.ts @@ -1,4 +1,5 @@ export interface SelectInputTheme { + container: string; base: string; inputContainer: string; input: string; @@ -32,14 +33,21 @@ export interface SelectInputTheme { disabled: string; removeButton: string; }; + size: { + small: string; + medium: string; + large: string; + [key: string]: string; + }; } const baseTheme: SelectInputTheme = { - base: 'flex flex-nowrap items-center box-border border rounded py-1.5 px-3 ', + base: 'flex flex-nowrap items-center box-border border rounded', + container: 'relative', inputContainer: 'flex-wrap flex items-center overflow-hidden flex-1 max-w-full [&>div]:max-w-full', input: - 'p-0 bg-transparent text-base text-ellipsis align-middle max-w-full read-only:cursor-not-allowed focus:outline-none disabled:text-disabled', + 'p-0 bg-transparent text-ellipsis align-middle max-w-full read-only:cursor-not-allowed focus:outline-none disabled:text-disabled', placeholder: '', prefix: 'overflow-hidden whitespace-nowrap text-ellipsis', suffix: { @@ -52,7 +60,7 @@ const baseTheme: SelectInputTheme = { 'mr-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:fill-panel-secondary-content', expand: '[&>svg]:w-4 [&>svg]:h-4 [&>svg]:fill-panel-secondary-content' }, - disabled: 'cursor-not-allowed text-disabled', + disabled: 'cursor-not-allowed text-disabled hover:after:content-none', unfilterable: 'caret-transparent', error: 'border border-solid', open: 'rounded rounded-ee-none rounded-es-none', @@ -72,6 +80,11 @@ const baseTheme: SelectInputTheme = { disabled: 'disabled:cursor-not-allowed', removeButton: 'cursor-pointer leading-[0] ml-1 p-0 border-0 [&>svg]:w-3 [&>svg]:h-3 [&>svg]:align-baseline [&>svg]:pointer-events-none' + }, + size: { + small: 'py-1 px-2 text-sm min-h-8', + medium: 'py-2 px-3 text-base min-h-[35px]', + large: 'py-2 px-3 text-lg min-h-[42px]' } }; @@ -79,20 +92,36 @@ export const selectInputTheme: SelectInputTheme = { ...baseTheme, base: [ baseTheme.base, - 'bg-panel text-panel-content border-panel-accent border-solid' + 'bg-panel text-panel-content border-panel-accent border-solid hover:border-panel-accent light:hover:border-panel-accent', + 'hover:after:bg-[radial-gradient(circle,_#105EFF_0%,_#105EFF_36%,_#242433_100%)] light:hover:after:bg-[radial-gradient(circle,_#105EFF_0%,_#105EFF_36%,_#E6E6F0_100%)]', + 'focus-within:after:bg-[radial-gradient(circle,_#93B6FF_0%,_#105EFF_36%,_#3D3D4D_90%,_#242433_100%)] light:focus-within:after:bg-[radial-gradient(circle,_#105EFF_10%,_#93B6FF_36%,_#E6E6F0_90%)]', + 'hover:after:content-[""] hover:after:absolute hover:after:mx-1 hover:after:h-px after:z-[2] hover:after:rounded hover:after:-bottom-[0px] hover:after:inset-x-0.5', + 'focus-within:after:content-[""] focus-within:after:absolute focus-within:after:mx-0 focus-within:after:h-px after:z-[2] focus-within:after:rounded focus-within:after:-bottom-[0px] focus-within:after:inset-x-0.5' + ].join(' '), + placeholder: [ + baseTheme.placeholder, + 'placeholder:text-secondary-content' + ].join(' '), + disabled: [ + baseTheme.disabled, + 'text-panel-secondary-content/40 border-surface light:hover:border-surface' ].join(' '), - disabled: [baseTheme.disabled, 'opacity-75'].join(' '), - error: [baseTheme.error, 'border-error'].join(' '), + error: [baseTheme.error, 'border-error light:border-error/20'].join(' '), + suffix: { + ...baseTheme.suffix, + button: [baseTheme.suffix.button, 'hover:cursor-pointer'].join(' ') + }, chip: { ...baseTheme.chip, - base: [baseTheme.chip.base, 'bg-panel-accent text-surface-content'].join( - ' ' - ), + base: [ + baseTheme.chip.base, + '[&>svg]:fill-panel-content disabled:[&>svg]:fill-panel-secondary-content/40' + ].join(' '), hover: [baseTheme.chip.hover, 'hover:brightness-150'].join(' '), focused: [baseTheme.chip.focused, 'border-panel-accent'].join(' '), removeButton: [ baseTheme.chip.removeButton, - '[&>svg]:fill-panel-content' + '[&>svg]:fill-panel-content disabled:[&>svg]:fill-panel-secondary-content/40' ].join(' ') } }; diff --git a/src/form/Select/SelectMenu/SelectMenu.tsx b/src/form/Select/SelectMenu/SelectMenu.tsx index 855bf590..ab7b04d3 100644 --- a/src/form/Select/SelectMenu/SelectMenu.tsx +++ b/src/form/Select/SelectMenu/SelectMenu.tsx @@ -4,9 +4,9 @@ import { SelectOptionProps, SelectValue } from '@/form/Select/SelectOption'; import Highlighter from 'react-highlight-words'; import { GroupOptions, GroupOption } from '@/form/Select/utils'; import { List, ListItem } from '@/layout'; -import { useComponentTheme } from '@/utils'; +import { cn, useComponentTheme } from '@/utils'; import { SelectTheme } from '@/form/Select/SelectTheme'; -import { twMerge } from 'tailwind-merge'; +import { CheckIcon } from '@/form/Select/icons/CheckIcon'; export interface SelectMenuProps { /** @@ -74,6 +74,11 @@ export interface SelectMenuProps { */ loading?: boolean; + /** + * The size of the select menu. + */ + size?: 'small' | 'medium' | 'large' | string; + /** * Event fired when the selected option(s) change. */ @@ -98,6 +103,7 @@ export const SelectMenu: FC = ({ groups, multiple, inputSearchText, + size, onSelectedChange, theme: customTheme }) => { @@ -128,13 +134,17 @@ export const SelectMenu: FC = ({ items.map((o, i) => ( { event.preventDefault(); event.stopPropagation(); @@ -150,6 +160,9 @@ export const SelectMenu: FC = ({ textToHighlight={o.children} /> )} + {Boolean(multiple && checkOptionSelected(o)) && ( + + )} )), [ @@ -157,15 +170,18 @@ export const SelectMenu: FC = ({ disabled, index, inputSearchText, + size, + multiple, onSelectedChange, - theme.option + theme.option, + theme.size ] ); return ( = ({ renderListItems(g.items, g) ) : (

diff --git a/src/form/Select/SelectMenu/SelectMenuTheme.ts b/src/form/Select/SelectMenu/SelectMenuTheme.ts index d3b5c6a4..91f21bdc 100644 --- a/src/form/Select/SelectMenu/SelectMenuTheme.ts +++ b/src/form/Select/SelectMenu/SelectMenuTheme.ts @@ -1,26 +1,56 @@ export interface SelectMenuTheme { base: string; - groupItem: string; - groupTitle: string; + groupItem: { + base: string; + title: string; + size: { + small: string; + medium: string; + large: string; + [key: string]: string; + }; + }; option: { base: string; hover: string; selected: string; active: string; disabled: string; + checkIcon: string; + content: string; + }; + size: { + small: string; + medium: string; + large: string; + [key: string]: string; }; } const baseTheme: SelectMenuTheme = { base: 'border border-solid rounded-b-md text-center will-change-[transform,opacity] min-w-[112px] max-h-[300px] overflow-y-auto text-left box-border', - groupItem: 'p-0 border-0', - groupTitle: 'text-sm font-bold uppercase m-0 px-1.5 py-2.5', + groupItem: { + base: 'p-0 border-0 first:pt-2 last:pb-2', + title: 'font-bold uppercase m-0 px-1.5 py-2.5', + size: { + small: 'px-2.5 text-sm', + medium: 'px-3 text-sm', + large: 'px-3.5 text-base' + } + }, option: { - base: 'text-sm flex-1 whitespace-break-spaces break-words py-1.5 px-2.5', + base: 'flex-1 whitespace-break-spaces break-words py-1.5 px-2.5', hover: '', selected: '', active: '', - disabled: '' + disabled: '', + checkIcon: 'ml-1', + content: 'flex flex-row justify-between' + }, + size: { + small: 'px-2.5 py-1.5 text-sm', + medium: 'px-4 py-2 text-base', + large: 'px-5 py-3 text-lg' } }; @@ -30,16 +60,19 @@ export const selectMenuTheme: SelectMenuTheme = { baseTheme.base, 'bg-panel text-panel-content border-panel-accent border-t-transparent' ].join(' '), - groupTitle: [baseTheme.groupTitle, 'text-panel-secondary-content'].join(' '), + groupItem: { + ...baseTheme.groupItem, + title: [baseTheme.groupItem.title, 'text-surface-content'].join(' ') + }, option: { ...baseTheme.option, - base: [baseTheme.option.base, 'text-surface-content'].join(' '), - hover: [baseTheme.option.hover, 'hover:bg-panel-accent'].join(' '), - active: [baseTheme.option.active, 'bg-panel-accent'].join(' '), - selected: [ - baseTheme.option.selected, - 'bg-primary-active hover:bg-primary-hover light:bg-primary-active light:[&>div>span]:invert' - ].join(' ') + base: [baseTheme.option.base, 'text-panel-secondary-content '].join(' '), + hover: [ + baseTheme.option.hover, + 'hover:bg-vulcan hover:text-mystic light:hover:bg-vulcan/5 light:hover:text-panel-secondary-content' + ].join(' '), + active: [baseTheme.option.active, 'bg-vulcan hover:text-mystic'].join(' '), + selected: [baseTheme.option.selected, 'text-primary-active'].join(' ') } }; @@ -49,7 +82,10 @@ export const cssVarsSelectMenuTheme: SelectMenuTheme = { baseTheme.base, 'bg-[var(--select-menu-background)] [border:_var(--select-menu-border)] rounded-[var(--select-menu-border-radius)]' ].join(' '), - groupTitle: [baseTheme.groupTitle, 'text-gray-600'].join(' '), + groupItem: { + ...baseTheme.groupItem, + title: [baseTheme.groupItem.title, 'text-gray-600'].join(' ') + }, option: { ...baseTheme.option, base: [ diff --git a/src/form/Select/SingleSelect.story.tsx b/src/form/Select/SingleSelect.story.tsx index 6af5e804..d1b37ac3 100644 --- a/src/form/Select/SingleSelect.story.tsx +++ b/src/form/Select/SingleSelect.story.tsx @@ -3,6 +3,8 @@ import { Select } from './Select'; import { SelectOption } from './SelectOption'; import { SelectMenu } from './SelectMenu'; import { SelectInput, SelectInputChip } from './SelectInput'; +import { Stack } from '@/layout'; +import { Label } from '@/layout/Block/Block.story'; export default { title: 'Components/Form/Select/Single', @@ -43,6 +45,66 @@ export const Basic = () => { ); }; +export const Sizes = () => { + const [value, setValue] = useState(null); + return ( +
+ + + + + + + + + + + + + + +
+ ); +}; + export const Fonts = () => { const [value, setValue] = useState(null); return ( diff --git a/src/form/Select/icons/CheckIcon.tsx b/src/form/Select/icons/CheckIcon.tsx new file mode 100644 index 00000000..5a64f0dd --- /dev/null +++ b/src/form/Select/icons/CheckIcon.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; + +export type CheckIconProps = { + className?: string; +}; + +export const CheckIcon: FC = ({ className }) => ( + + + + + +); diff --git a/src/form/Select/icons/CloseIcon.tsx b/src/form/Select/icons/CloseIcon.tsx index 8a787099..272a6dd8 100644 --- a/src/form/Select/icons/CloseIcon.tsx +++ b/src/form/Select/icons/CloseIcon.tsx @@ -5,16 +5,19 @@ export type CloseIconProps = { width?: number; }; -export const CloseIcon: FC = ({ height = 32, width = 32 }) => ( +export const CloseIcon: FC = ({ height = 16, width = 16 }) => ( - + ); diff --git a/src/form/Select/icons/DownArrowIcon.tsx b/src/form/Select/icons/DownArrowIcon.tsx index 24db6771..e510b076 100644 --- a/src/form/Select/icons/DownArrowIcon.tsx +++ b/src/form/Select/icons/DownArrowIcon.tsx @@ -5,10 +5,13 @@ export const DownArrowIcon: FC = () => ( xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" - width="50" - height="50" - viewBox="0 0 32 32" + width="16" + height="16" + viewBox="0 0 16 16" > - + ); diff --git a/src/form/Select/icons/RefreshIcon.tsx b/src/form/Select/icons/RefreshIcon.tsx index 2c51d8ef..fdf1b254 100644 --- a/src/form/Select/icons/RefreshIcon.tsx +++ b/src/form/Select/icons/RefreshIcon.tsx @@ -7,6 +7,9 @@ export const RefreshIcon: FC = () => ( width="64px" height="64px" > - + ); diff --git a/src/layout/List/ListItem/ListItem.tsx b/src/layout/List/ListItem/ListItem.tsx index addbc8fb..25b0d725 100644 --- a/src/layout/List/ListItem/ListItem.tsx +++ b/src/layout/List/ListItem/ListItem.tsx @@ -1,7 +1,6 @@ import React, { FC, InputHTMLAttributes, LegacyRef, forwardRef } from 'react'; -import { twMerge } from 'tailwind-merge'; import { ListTheme } from '@/layout/List/ListTheme'; -import { useComponentTheme } from '@/utils'; +import { cn, useComponentTheme } from '@/utils'; export interface ListItemProps extends InputHTMLAttributes { /** @@ -29,6 +28,11 @@ export interface ListItemProps extends InputHTMLAttributes { */ dense?: boolean; + /** + * Class name for the content element. + */ + contentClassName?: string; + /** * A start component for the list item. */ @@ -59,6 +63,7 @@ export const ListItem: FC = forwardRef< ( { className, + contentClassName, children, active, disabled, @@ -82,7 +87,7 @@ export const ListItem: FC = forwardRef< role={onClick ? 'button' : 'listitem'} tabIndex={onClick ? 0 : undefined} onClick={e => !disabled && onClick?.(e)} - className={twMerge( + className={cn( theme.listItem.base, dense && theme.listItem.dense.base, disabled && theme.listItem.disabled, @@ -95,29 +100,32 @@ export const ListItem: FC = forwardRef< > {start && (
{start}
)}
{children}
{end && (
{end} diff --git a/src/layout/List/ListTheme.ts b/src/layout/List/ListTheme.ts index 5f279d50..4fba6355 100644 --- a/src/layout/List/ListTheme.ts +++ b/src/layout/List/ListTheme.ts @@ -47,7 +47,7 @@ const baseTheme: ListTheme = { end: 'pl-1', svg: 'fill-current' }, - content: 'text-sm overflow-wrap break-word word-wrap break-all flex-1' + content: 'overflow-wrap break-word word-wrap break-all flex-1' } };