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,95 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
|
||||
import { InvSelect } from 'common/components/InvSelect/InvSelect';
|
||||
import type { InvSelectOption } from 'common/components/InvSelect/types';
|
||||
import { InvSlider } from 'common/components/InvSlider/InvSlider';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { InvControl } from './InvControl';
|
||||
import type { InvControlProps } from './types';
|
||||
|
||||
const meta: Meta<typeof InvControl> = {
|
||||
title: 'Primitives/InvControl',
|
||||
tags: ['autodocs'],
|
||||
component: InvControl,
|
||||
args: {
|
||||
label: 'My Control',
|
||||
isDisabled: false,
|
||||
isInvalid: false,
|
||||
w: 96,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof InvControl>;
|
||||
|
||||
const InvControlWithSliderComponent = (props: InvControlProps) => {
|
||||
const [value, setValue] = useState(0);
|
||||
return (
|
||||
<InvControl {...props}>
|
||||
<InvSlider value={value} min={0} max={10} step={1} onChange={setValue} />
|
||||
</InvControl>
|
||||
);
|
||||
};
|
||||
|
||||
const InvControlWithSliderAndHelperTextComponent = (props: InvControlProps) => {
|
||||
const [value, setValue] = useState(0);
|
||||
return (
|
||||
<InvControl {...props} helperText="This is some helpful text">
|
||||
<InvSlider value={value} min={0} max={10} step={1} onChange={setValue} />
|
||||
</InvControl>
|
||||
);
|
||||
};
|
||||
|
||||
const InvControlWithNumberInputComponent = (props: InvControlProps) => {
|
||||
const [value, setValue] = useState(0);
|
||||
return (
|
||||
<InvControl {...props}>
|
||||
<InvNumberInput
|
||||
value={value}
|
||||
min={0}
|
||||
max={10}
|
||||
step={1}
|
||||
onChange={setValue}
|
||||
/>
|
||||
</InvControl>
|
||||
);
|
||||
};
|
||||
|
||||
const options: InvSelectOption[] = [
|
||||
{
|
||||
value: 'chocolate',
|
||||
label: 'Chocolate',
|
||||
},
|
||||
{
|
||||
value: 'strawberry',
|
||||
label: 'Strawberry',
|
||||
},
|
||||
{
|
||||
value: 'vanilla',
|
||||
label: 'Vanilla',
|
||||
},
|
||||
];
|
||||
const InvControlWithSelectComponent = (props: InvControlProps) => {
|
||||
return (
|
||||
<InvControl {...props}>
|
||||
<InvSelect defaultValue={options[0]} options={options} />
|
||||
</InvControl>
|
||||
);
|
||||
};
|
||||
|
||||
export const InvControlWithSlider: Story = {
|
||||
render: InvControlWithSliderComponent,
|
||||
};
|
||||
|
||||
export const InvControlWithSliderAndHelperText: Story = {
|
||||
render: InvControlWithSliderAndHelperTextComponent,
|
||||
};
|
||||
|
||||
export const InvControlWithNumberInput: Story = {
|
||||
render: InvControlWithNumberInputComponent,
|
||||
};
|
||||
|
||||
export const InvControlWithSelect: Story = {
|
||||
render: InvControlWithSelectComponent,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
Flex,
|
||||
FormControl as ChakraFormControl,
|
||||
FormHelperText as ChakraFormHelperText,
|
||||
forwardRef,
|
||||
} from '@chakra-ui/react';
|
||||
import { InvControlGroupContext } from 'common/components/InvControl/InvControlGroup';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { InvLabel } from './InvLabel';
|
||||
import type { InvControlProps } from './types';
|
||||
|
||||
export const InvControl = forwardRef<InvControlProps, typeof ChakraFormControl>(
|
||||
(props: InvControlProps, ref) => {
|
||||
const {
|
||||
children,
|
||||
helperText,
|
||||
feature,
|
||||
orientation,
|
||||
renderInfoPopoverInPortal = true,
|
||||
isDisabled,
|
||||
labelProps,
|
||||
label,
|
||||
...formControlProps
|
||||
} = props;
|
||||
|
||||
const ctx = useContext(InvControlGroupContext);
|
||||
|
||||
if (helperText) {
|
||||
return (
|
||||
<ChakraFormControl
|
||||
ref={ref}
|
||||
variant="withHelperText"
|
||||
orientation={orientation ?? ctx.orientation}
|
||||
isDisabled={isDisabled ?? ctx.isDisabled}
|
||||
{...formControlProps}
|
||||
>
|
||||
<Flex>
|
||||
{label && (
|
||||
<InvLabel
|
||||
feature={feature}
|
||||
renderInfoPopoverInPortal={renderInfoPopoverInPortal}
|
||||
{...labelProps}
|
||||
>
|
||||
{label}
|
||||
</InvLabel>
|
||||
)}
|
||||
{children}
|
||||
</Flex>
|
||||
<ChakraFormHelperText>{helperText}</ChakraFormHelperText>
|
||||
</ChakraFormControl>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChakraFormControl
|
||||
ref={ref}
|
||||
isDisabled={isDisabled ?? ctx.isDisabled}
|
||||
orientation={orientation ?? ctx.orientation}
|
||||
{...formControlProps}
|
||||
>
|
||||
{label && (
|
||||
<InvLabel
|
||||
feature={feature}
|
||||
renderInfoPopoverInPortal={renderInfoPopoverInPortal}
|
||||
{...labelProps}
|
||||
>
|
||||
{label}
|
||||
</InvLabel>
|
||||
)}
|
||||
{children}
|
||||
</ChakraFormControl>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { FormLabelProps } from '@chakra-ui/react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type InvControlGroupProps = {
|
||||
labelProps?: FormLabelProps;
|
||||
isDisabled?: boolean;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
};
|
||||
|
||||
export const InvControlGroupContext = createContext<InvControlGroupProps>({});
|
||||
|
||||
export const InvControlGroup = ({
|
||||
children,
|
||||
...context
|
||||
}: PropsWithChildren<InvControlGroupProps>) => {
|
||||
return (
|
||||
<InvControlGroupContext.Provider value={context}>
|
||||
{children}
|
||||
</InvControlGroupContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Flex, FormLabel, forwardRef } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIInformationalPopover from 'common/components/IAIInformationalPopover/IAIInformationalPopover';
|
||||
import { InvControlGroupContext } from 'common/components/InvControl/InvControlGroup';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import type { InvLabelProps } from './types';
|
||||
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
({ system }) => system.shouldEnableInformationalPopovers
|
||||
);
|
||||
|
||||
export const InvLabel = forwardRef<InvLabelProps, typeof FormLabel>(
|
||||
(
|
||||
{ feature, renderInfoPopoverInPortal, children, ...rest }: InvLabelProps,
|
||||
ref
|
||||
) => {
|
||||
const shouldEnableInformationalPopovers = useAppSelector(selector);
|
||||
const ctx = useContext(InvControlGroupContext);
|
||||
if (feature && shouldEnableInformationalPopovers) {
|
||||
return (
|
||||
<IAIInformationalPopover
|
||||
feature={feature}
|
||||
inPortal={renderInfoPopoverInPortal}
|
||||
>
|
||||
<Flex as="span">
|
||||
<FormLabel ref={ref} {...ctx.labelProps} {...rest}>
|
||||
{children}
|
||||
</FormLabel>
|
||||
</Flex>
|
||||
</IAIInformationalPopover>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormLabel ref={ref} {...ctx.labelProps} {...rest}>
|
||||
{children}
|
||||
</FormLabel>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
import { formAnatomy as parts } from '@chakra-ui/anatomy';
|
||||
import {
|
||||
createMultiStyleConfigHelpers,
|
||||
defineStyle,
|
||||
defineStyleConfig,
|
||||
} from '@chakra-ui/styled-system';
|
||||
|
||||
const { definePartsStyle, defineMultiStyleConfig } =
|
||||
createMultiStyleConfigHelpers(parts.keys);
|
||||
|
||||
const formBaseStyle = definePartsStyle((props) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: props.orientation === 'vertical' ? 'column' : 'row',
|
||||
alignItems: props.orientation === 'vertical' ? 'flex-start' : 'center',
|
||||
gap: 4,
|
||||
// h: props.orientation === 'vertical' ? 'unset' : 8,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const withHelperText = definePartsStyle(() => ({
|
||||
container: {
|
||||
flexDirection: 'column',
|
||||
gap: 0,
|
||||
h: 'unset',
|
||||
'> div': {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
h: 8,
|
||||
w: 'full',
|
||||
},
|
||||
},
|
||||
helperText: {
|
||||
w: 'full',
|
||||
fontSize: 'sm',
|
||||
color: 'base.400',
|
||||
m: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
export const formTheme = defineMultiStyleConfig({
|
||||
baseStyle: formBaseStyle,
|
||||
variants: {
|
||||
withHelperText,
|
||||
},
|
||||
});
|
||||
|
||||
const formLabelBaseStyle = defineStyle(() => {
|
||||
return {
|
||||
fontSize: 'sm',
|
||||
marginEnd: 0,
|
||||
mb: 0,
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
fontWeight: 'semibold',
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: 'normal',
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none',
|
||||
_disabled: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
color: 'base.300',
|
||||
_invalid: {
|
||||
color: 'error.300',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const formLabelTheme = defineStyleConfig({
|
||||
baseStyle: formLabelBaseStyle,
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import type {
|
||||
FormControlProps as ChakraFormControlProps,
|
||||
FormLabelProps as ChakraFormLabelProps,
|
||||
} from '@chakra-ui/react';
|
||||
import type { Feature } from 'common/components/IAIInformationalPopover/constants';
|
||||
|
||||
export type InvControlProps = ChakraFormControlProps & {
|
||||
label?: string;
|
||||
helperText?: string;
|
||||
feature?: Feature;
|
||||
renderInfoPopoverInPortal?: boolean;
|
||||
labelProps?: Omit<
|
||||
InvLabelProps,
|
||||
'children' | 'feature' | 'renderInfoPopoverInPortal'
|
||||
>;
|
||||
};
|
||||
|
||||
export type InvLabelProps = ChakraFormLabelProps & {
|
||||
feature?: Feature;
|
||||
renderInfoPopoverInPortal?: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user