feat(ui): ux improvements & redesign

This is a squash merge of a bajillion messy small commits created while iterating on the UI component library and redesign.
This commit is contained in:
psychedelicious
2023-12-29 00:03:21 +11:00
committed by Kent Keirsey
parent a47d91f0e7
commit f0b102d830
889 changed files with 16645 additions and 15595 deletions

View File

@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { InvNumberInput } from './InvNumberInput';
import type { InvNumberInputProps } from './types';
const meta: Meta<typeof InvNumberInput> = {
title: 'Primitives/InvNumberInput',
tags: ['autodocs'],
component: InvNumberInput,
args: {
min: -10,
max: 10,
step: 1,
},
};
export default meta;
type Story = StoryObj<typeof InvNumberInput>;
const Component = (props: InvNumberInputProps) => {
const [value, setValue] = useState(0);
return <InvNumberInput {...props} value={value} onChange={setValue} />;
};
export const Default: Story = {
render: Component,
args: { fineStep: 0.1 },
};
export const Integer: Story = {
render: Component,
};

View File

@@ -0,0 +1,121 @@
import { forwardRef, NumberInput as ChakraNumberInput } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { $modifiers } from 'common/hooks/useGlobalModifiers';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { clamp } from 'lodash-es';
import type { FocusEventHandler } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { InvNumberInputField } from './InvNumberInputField';
import { InvNumberInputStepper } from './InvNumberInputStepper';
import type { InvNumberInputProps } from './types';
const isValidCharacter = (char: string) => /^[0-9\-.]$/i.test(char);
export const InvNumberInput = forwardRef<
InvNumberInputProps,
typeof ChakraNumberInput
>((props: InvNumberInputProps, ref) => {
const {
value,
min = 0,
max,
step: _step = 1,
fineStep: _fineStep,
onChange: _onChange,
numberInputFieldProps,
...rest
} = props;
const [valueAsString, setValueAsString] = useState<string>(String(value));
const [valueAsNumber, setValueAsNumber] = useState<number>(value);
const modifiers = useStore($modifiers);
const step = useMemo(
() => (modifiers.shift ? _fineStep ?? _step : _step),
[modifiers.shift, _fineStep, _step]
);
const isInteger = useMemo(
() => Number.isInteger(_step) && Number.isInteger(_fineStep ?? 1),
[_step, _fineStep]
);
const inputMode = useMemo(
() => (isInteger ? 'numeric' : 'decimal'),
[isInteger]
);
const precision = useMemo(() => (isInteger ? 0 : 3), [isInteger]);
const onChange = useCallback(
(valueAsString: string, valueAsNumber: number) => {
setValueAsString(valueAsString);
if (isNaN(valueAsNumber)) {
return;
}
setValueAsNumber(valueAsNumber);
_onChange(valueAsNumber);
},
[_onChange]
);
// This appears to be unnecessary? Cannot figure out what it did but leaving it here in case
// it was important.
// const onClickStepper = useCallback(
// () => _onChange(Number(valueAsString)),
// [_onChange, valueAsString]
// );
const onBlur: FocusEventHandler<HTMLInputElement> = useCallback(
(e) => {
console.log('blur!');
if (!e.target.value) {
// If the input is empty, we set it to the minimum value
onChange(String(min), min);
} else {
// Otherwise, we round the value to the nearest multiple if integer, else 3 decimals
const roundedValue = isInteger
? roundToMultiple(valueAsNumber, _fineStep ?? _step)
: Number(valueAsNumber.toFixed(precision));
// Clamp to min/max
const clampedValue = clamp(roundedValue, min, max);
onChange(String(clampedValue), clampedValue);
}
},
[_fineStep, _step, isInteger, max, min, onChange, precision, valueAsNumber]
);
/**
* When `value` changes (e.g. from a diff source than this component), we need
* to update the internal `valueAsString`, but only if the actual value is different
* from the current value.
*/
useEffect(() => {
if (value !== valueAsNumber) {
setValueAsString(String(value));
setValueAsNumber(value);
}
}, [value, valueAsNumber]);
return (
<ChakraNumberInput
ref={ref}
min={min}
max={max}
step={step}
value={valueAsString}
onChange={onChange}
clampValueOnBlur={false}
isValidCharacter={isValidCharacter}
focusInputOnChange={false}
onPaste={stopPastePropagation}
inputMode={inputMode}
precision={precision}
variant="filled"
{...rest}
>
<InvNumberInputField onBlur={onBlur} {...numberInputFieldProps} />
<InvNumberInputStepper />
</ChakraNumberInput>
);
});

View File

@@ -0,0 +1,32 @@
import { NumberInputField as ChakraNumberInputField } from '@chakra-ui/react';
import { useGlobalModifiersSetters } from 'common/hooks/useGlobalModifiers';
import type { KeyboardEventHandler } from 'react';
import { useCallback } from 'react';
import type { InvNumberInputFieldProps } from './types';
export const InvNumberInputField = (props: InvNumberInputFieldProps) => {
const { onKeyUp, onKeyDown, children, ...rest } = props;
const { setShift } = useGlobalModifiersSetters();
const _onKeyUp: KeyboardEventHandler<HTMLInputElement> = useCallback(
(e) => {
onKeyUp?.(e);
setShift(e.key === 'Shift');
},
[onKeyUp, setShift]
);
const _onKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback(
(e) => {
onKeyDown?.(e);
setShift(e.key === 'Shift');
},
[onKeyDown, setShift]
);
return (
<ChakraNumberInputField onKeyUp={_onKeyUp} onKeyDown={_onKeyDown} {...rest}>
{children}
</ChakraNumberInputField>
);
};

View File

@@ -0,0 +1,26 @@
import {
forwardRef,
Icon as ChakraIcon,
NumberDecrementStepper as ChakraNumberDecrementStepper,
NumberIncrementStepper as ChakraNumberIncrementStepper,
NumberInputStepper as ChakraNumberInputStepper,
} from '@chakra-ui/react';
import { FaMinus, FaPlus } from 'react-icons/fa6';
import type { InvNumberInputStepperProps } from './types';
export const InvNumberInputStepper = forwardRef<
InvNumberInputStepperProps,
typeof ChakraNumberInputStepper
>((props: InvNumberInputStepperProps, ref) => {
return (
<ChakraNumberInputStepper ref={ref} {...props}>
<ChakraNumberIncrementStepper>
<ChakraIcon as={FaPlus} boxSize={2} />
</ChakraNumberIncrementStepper>
<ChakraNumberDecrementStepper>
<ChakraIcon as={FaMinus} boxSize={2} />
</ChakraNumberDecrementStepper>
</ChakraNumberInputStepper>
);
});

View File

@@ -0,0 +1,84 @@
import { numberInputAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import { getInputFilledStyles } from 'theme/util/getInputFilledStyles';
import { getInputOutlineStyles } from 'theme/util/getInputOutlineStyles';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
const invokeAI = definePartsStyle(() => ({
root: {
// height: 8,
},
field: {
border: 'none',
fontWeight: 'semibold',
height: 'auto',
py: 1,
ps: 2,
pe: 6,
...getInputOutlineStyles(),
},
stepperGroup: {
display: 'flex',
},
stepper: {
border: 'none',
svg: {
color: 'base.300',
width: 2.5,
height: 2.5,
_hover: {
color: 'base.100',
},
},
},
}));
const filled = definePartsStyle((props) => {
return {
root: { h: '28px' },
field: { ...getInputFilledStyles(props), pe: 6, h: 'full' },
stepperGroup: {
border: 'none',
w: 6,
},
stepper: {
color: 'base.200',
_hover: {
bg: 'base.700',
color: 'base.100',
},
_disabled: {
_hover: {
bg: 'base.800',
color: 'base.200',
},
},
_first: {
border: 'none',
margin: 0,
borderTopEndRadius: 'base',
borderBottomStartRadius: 'base',
},
_last: {
border: 'none',
margin: 0,
borderBottomEndRadius: 'base',
borderTopStartRadius: 'base',
},
},
};
});
export const numberInputTheme = defineMultiStyleConfig({
variants: {
invokeAI,
filled,
darkFilled: filled,
},
defaultProps: {
size: 'sm',
variant: 'filled',
},
});

View File

@@ -0,0 +1,46 @@
import type {
NumberDecrementStepperProps as ChakraNumberDecrementStepperProps,
NumberIncrementStepperProps as ChakraNumberIncrementStepperProps,
NumberInputFieldProps as ChakraNumberInputFieldProps,
NumberInputProps as ChakraNumberInputProps,
NumberInputStepperProps as ChakraNumberInputStepperProps,
} from '@chakra-ui/react';
export type InvNumberInputFieldProps = ChakraNumberInputFieldProps;
export type InvNumberInputStepperProps = ChakraNumberInputStepperProps;
export type InvNumberIncrementStepperProps = ChakraNumberIncrementStepperProps;
export type InvNumberDecrementStepperProps = ChakraNumberDecrementStepperProps;
export type InvNumberInputProps = Omit<
ChakraNumberInputProps,
'onChange' | 'min' | 'max'
> & {
/**
* The value
*/
value: number;
/**
* The minimum value
*/
min: number;
/**
* The maximum value
*/
max: number;
/**
* The default step
*/
step?: number;
/**
* The fine step (used when shift is pressed)
*/
fineStep?: number;
/**
* The change handler
*/
onChange: (v: number) => void;
/**
* Override props for the Chakra NumberInputField component
*/
numberInputFieldProps?: InvNumberInputFieldProps;
};