mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
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:
committed by
Kent Keirsey
parent
a47d91f0e7
commit
f0b102d830
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user