refactor: migrate related components

This commit is contained in:
Carlos Valente
2025-07-03 09:00:00 +02:00
committed by Carlos Valente
parent cd8e014f08
commit 84b73fc02a
23 changed files with 346 additions and 277 deletions
@@ -81,6 +81,25 @@
}
.ghosted {
background: transparent;
color: $blue-500;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
color: $blue-500;
}
&:active:not(:disabled) {
background: $gray-1100;
border-color: $gray-1250;
}
&:disabled {
opacity: $opacity-disabled;
}
}
.ghosted-white {
background: transparent;
color: $ui-white;
@@ -2,6 +2,7 @@
@import './BaseButtonStyles.module.scss';
.baseButton {
position: relative;
display: flex;
align-items: center;
justify-content: center;
@@ -25,6 +26,36 @@
outline: 2px solid $blue-500;
outline-offset: 2px;
}
&.loading {
cursor: wait;
.content {
opacity: 0;
}
}
}
.content {
display: flex;
align-items: center;
gap: inherit;
}
.loadingOverlay {
position: absolute;
display: grid;
place-content: center;
}
.spinner {
animation: spin 1s linear infinite;
stroke-dasharray: 8;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
.small {
@@ -1,29 +1,44 @@
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { IoEllipseOutline } from 'react-icons/io5';
import { cx } from '../../utils/styleUtils';
import style from './Button.module.scss';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'subtle' | 'subtle-white' | 'destructive' | 'subtle-destructive' | 'ghosted';
variant?: 'primary' | 'subtle' | 'subtle-white' | 'destructive' | 'subtle-destructive' | 'ghosted' | 'ghosted-white';
size?: 'small' | 'medium' | 'large' | 'xlarge';
fluid?: boolean;
loading?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const { className, children, variant = 'subtle', size = 'medium', fluid, ...buttonProps } = props;
return (
<button
ref={ref}
className={cx([style.baseButton, style[variant], style[size], fluid && style.fluid, className])}
type='button'
{...buttonProps}
>
{children}
</button>
);
});
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, children, variant = 'subtle', size = 'medium', fluid, loading, ...buttonProps }, ref) => {
return (
<button
ref={ref}
className={cx([
style.baseButton,
style[variant],
style[size],
fluid && style.fluid,
loading && style.loading,
className,
])}
type='button'
disabled={loading || buttonProps.disabled}
{...buttonProps}
>
<span className={style.content}>{children}</span>
{loading && (
<div className={style.loadingOverlay}>
<IoEllipseOutline className={style.spinner} />
</div>
)}
</button>
);
},
);
Button.displayName = 'Button';
@@ -5,7 +5,7 @@ import { cx } from '../../utils/styleUtils';
import style from './IconButton.module.scss';
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'subtle' | 'subtle-white' | 'destructive' | 'subtle-destructive' | 'ghosted';
variant?: 'primary' | 'subtle' | 'subtle-white' | 'destructive' | 'subtle-destructive' | 'ghosted' | 'ghosted-white';
size?: 'small' | 'medium' | 'large' | 'xlarge';
}
@@ -0,0 +1,47 @@
@use '../../../../theme/viewerDefs' as *;
.textarea {
box-sizing: border-box;
display: block;
font-size: 1rem;
font-weight: 400;
color: $gray-200;
border-radius: $component-border-radius-md;
border: 1px solid transparent;
padding-inline: 0.5em;
outline: none;
&:hover:not(:disabled) {
background-color: $gray-1100;
}
&:focus:not(:read-only) {
background-color: $gray-1000;
border: 1px solid $blue-500;
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
&::placeholder {
color: $gray-500;
letter-spacing: 0;
}
}
.subtle {
background-color: $gray-1200;
}
.ghosted {
background-color: transparent;
padding: 0;
}
.fluid {
width: 100%;
}
@@ -0,0 +1,31 @@
import { forwardRef, TextareaHTMLAttributes } from 'react';
import { cx } from '../../../utils/styleUtils';
import style from './Textarea.module.scss';
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
variant?: 'subtle' | 'ghosted';
fluid?: boolean;
resize?: 'none' | 'both' | 'horizontal' | 'vertical';
}
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function TextArea(
{ className, variant = 'subtle', fluid, rows = 5, resize = 'none', style: customStyle, ...textareaProps },
ref,
) {
return (
<textarea
ref={ref}
autoCorrect='off'
autoComplete='off'
spellCheck='false'
rows={rows}
style={{ ...customStyle, resize }}
className={cx([style.textarea, style[variant], fluid && style.fluid, className])}
{...textareaProps}
/>
);
});
export default Textarea;
@@ -17,11 +17,11 @@
font-size: calc(1rem - 2px);
white-space: nowrap;
&:hover:not(:disabled) {
&:hover:not([data-disabled]) {
background-color: $gray-1100;
}
&:active {
&:active:not([data-disabled]) {
background-color: $gray-1000;
}
@@ -29,7 +29,7 @@
background-color: $gray-1000;
}
&:disabled {
&[data-disabled] {
opacity: 0.4;
cursor: not-allowed;
}
@@ -4,25 +4,17 @@ import { Select as BaseSelect } from '@base-ui-components/react/select';
import styles from './Select.module.scss';
interface SelectProps<T extends string | null = string> {
defaultValue?: T;
interface SelectProps<T> extends Omit<BaseSelect.Root.Props<T>, 'items'> {
// overload items to not allow undefined values
options: {
value: NonNullable<T>;
value: T;
label: string;
disabled?: boolean; // exposed to allow creating a non-selectable option
}[];
value?: T;
onChange?: (value: NonNullable<T>) => void;
}
export default function Select<T extends string | null = string>({
defaultValue,
options,
value,
onChange,
}: SelectProps<T>) {
export default function Select<T>({ options, ...selectRootProps }: SelectProps<T>) {
return (
<BaseSelect.Root items={options} defaultValue={defaultValue} onValueChange={onChange} value={value}>
<BaseSelect.Root items={options} {...selectRootProps}>
<BaseSelect.Trigger className={styles.select}>
<BaseSelect.Value />
<BaseSelect.Icon className={styles.selectIcon}>
@@ -33,16 +25,14 @@ export default function Select<T extends string | null = string>({
<BaseSelect.Positioner side='bottom' align='start'>
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
<BaseSelect.Popup className={styles.popup}>
{options.map((option) => {
return (
<BaseSelect.Item key={option.value} className={styles.item} value={option.value}>
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
<IoCheckmark className={styles.itemIndicatorIcon} />
</BaseSelect.ItemIndicator>
<BaseSelect.ItemText className={styles.itemLabel}>{option.label}</BaseSelect.ItemText>
</BaseSelect.Item>
);
})}
{options.map(({ label, value }) => (
<BaseSelect.Item key={String(value)} className={styles.item} value={value}>
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
<IoCheckmark className={styles.itemIndicatorIcon} />
</BaseSelect.ItemIndicator>
<BaseSelect.ItemText className={styles.itemLabel}>{label}</BaseSelect.ItemText>
</BaseSelect.Item>
))}
</BaseSelect.Popup>
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
</BaseSelect.Positioner>