Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions invokeai/frontend/webv2/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"active": "Active",
"add": "Add",
"all": "All",
"apply": "Apply",
"archived": "Archived",
"asc": "Asc",
"assets": "Assets",
Expand All @@ -15,6 +16,7 @@
"close": "Close",
"configure": "Configure",
"copy": "Copy",
"crop": "Crop",
"clipboardBlocked": "Clipboard access was blocked by the browser.",
"couldNotCopy": "Could not copy",
"countOfTotal": "{{count}} of {{total}}",
Expand Down Expand Up @@ -53,6 +55,7 @@
"required": "Required",
"refresh": "Refresh",
"reload": "Reload",
"reset": "Reset",
"retry": "Retry",
"saved": "Saved",
"save": "Save",
Expand Down Expand Up @@ -673,6 +676,34 @@
"promptTemplate": "Prompt Template",
"promptTriggerOptions": "Prompt trigger options",
"random": "Random",
"activeSteps": "Active steps",
"clearReferenceImages": "Remove all",
"collapseReferenceImage": "Collapse reference image",
"cropReferenceImage": "Crop reference image",
"cropReferenceImageHelp": "Drag the crop box to move it, or drag its handles to resize. Apply to create a cropped reference image.",
"disableReferenceImage": "Disable reference image",
"enableReferenceImage": "Enable reference image",
"expandReferenceImage": "Expand reference image",
"imageInfluence": "Image influence",
"mode": "Mode",
"modeBoth": "Both",
"modeComposition": "Composition",
"modeStyle": "Style",
"pullBboxIntoReferenceImage": "Pull Bbox into Reference Image",
"referenceImage": "Reference Image",
"referenceImageModel": "Reference image model",
"referenceImages": "Reference images",
"referenceImagesHelp": "Upload an image or drag images from Gallery here.",
"referenceImagesUnsupported": "Reference images are not supported by the selected model.",
"removeReferenceImage": "Remove reference image",
"styleVariant": "Style variant",
"styleVariantPrecise": "Precise",
"styleVariantPreciseDesc": "Applies a precise visual style, eliminating subject influence.",
"styleVariantStandard": "Standard",
"styleVariantStandardDesc": "Applies visual style (colors, textures) without considering its layout. Previously called Style Only.",
"styleVariantStrong": "Strong",
"styleVariantStrongDesc": "Applies a strong visual style, with a slightly reduced composition influence.",
"useSize": "Use size",
"removeConcept": "Remove concept",
"removeConceptNamed": "Remove {{name}}",
"resizeNegativePrompt": "Resize negative prompt",
Expand Down Expand Up @@ -709,8 +740,10 @@
"useModelDefaultWidth": "Use model default width",
"usePrompt": "Use prompt",
"usingBundledVae": "Using the VAE bundled with the model.",
"upload": "Upload",
"vae": "VAE",
"vaePrecision": "VAE precision",
"weight": "Weight",
"width": "Width",
"xAxis": "X axis",
"yAxis": "Y axis"
Expand Down
40 changes: 32 additions & 8 deletions invokeai/frontend/webv2/src/theme/recipes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ const dropdownContent = {

const dropdownItem = {
_highlighted: { bg: 'bg.subtle' },
_focusVisible: { outline: '2px solid', outlineColor: 'accent.solid', outlineOffset: '-2px' },
_focusVisible: {
outline: '2px solid',
outlineColor: 'accent.solid',
outlineOffset: '-2px',
},
};

const dropdownGroupLabel = {
Expand Down Expand Up @@ -223,15 +227,27 @@ export const rowRecipe = defineRecipe({
transition: 'background var(--wb-motion-duration-fast) ease, color var(--wb-motion-duration-fast) ease',
w: 'full',
_hover: { bg: 'bg.muted' },
_focusVisible: { outline: '2px solid', outlineColor: 'accent.solid', outlineOffset: '-2px' },
_focusVisible: {
outline: '2px solid',
outlineColor: 'accent.solid',
outlineOffset: '-2px',
},
_disabled: { cursor: 'not-allowed', opacity: 0.5 },
},
variants: {
active: {
none: {},
muted: { bg: 'bg.muted' },
brand: { bg: 'brand.subtle', color: 'brand.fg', _hover: { bg: 'brand.subtle' } },
accent: { bg: 'accent.solid', color: 'accent.contrast', _hover: { bg: 'accent.solid' } },
brand: {
bg: 'brand.subtle',
color: 'brand.fg',
_hover: { bg: 'brand.subtle' },
},
accent: {
bg: 'accent.solid',
color: 'accent.contrast',
_hover: { bg: 'accent.solid' },
},
},
},
defaultVariants: { active: 'none' },
Expand Down Expand Up @@ -270,8 +286,7 @@ export const fieldLabelRecipe = defineRecipe({
color: 'fg.muted',
fontSize: '2xs',
fontWeight: '600',
letterSpacing: '0.02em',
textTransform: 'uppercase',
letterSpacing: '0.03em',
},
});

Expand All @@ -295,7 +310,11 @@ export const themeCardRecipe = defineSlotRecipe({
transition:
'border-color var(--wb-motion-duration-fast) ease, background var(--wb-motion-duration-fast) ease, transform var(--wb-motion-duration-fast) ease',
_hover: { borderColor: 'border.emphasized' },
_focusVisible: { outline: '2px solid', outlineColor: 'accent.solid', outlineOffset: '2px' },
_focusVisible: {
outline: '2px solid',
outlineColor: 'accent.solid',
outlineOffset: '2px',
},
},
preview: {
borderColor: 'border.subtle',
Expand All @@ -306,7 +325,12 @@ export const themeCardRecipe = defineSlotRecipe({
overflow: 'hidden',
},
swatch: { flex: '1' },
body: { alignItems: 'flex-start', display: 'flex', flexDirection: 'column', gap: '0.5' },
body: {
alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
gap: '0.5',
},
name: { color: 'fg', fontSize: 'sm', fontWeight: '600' },
description: { color: 'fg.subtle', fontSize: '2xs', lineHeight: '1.3' },
indicator: {
Expand Down
118 changes: 97 additions & 21 deletions invokeai/frontend/webv2/src/workbench/components/ui/Field.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ReactNode } from 'react';

import { HStack, Stack, Text, useRecipe, type StackProps } from '@chakra-ui/react';
import { Field as ChakraField, HStack, Stack, Text, useRecipe, type StackProps } from '@chakra-ui/react';
import { fieldLabelRecipe } from '@theme/recipes';
import { useMemo } from 'react';

/**
* The shared, theme-aware uppercase field label. Backed by `fieldLabelRecipe` so
Expand All @@ -18,31 +19,106 @@ export const FieldLabel = ({ children }: { children: ReactNode }) => {
);
};

export interface FieldProps extends Omit<StackProps, 'title'> {
export interface FieldProps extends Omit<StackProps, 'disabled' | 'id' | 'readOnly' | 'required' | 'title'> {
id?: string;
label: string;
labelEnd?: ReactNode;
/** Validation error, shown in place of `helpText`. Mark the control itself invalid via `aria-invalid`. */
disabled?: boolean;
orientation?: 'horizontal' | 'vertical';
invalid?: boolean;
readOnly?: boolean;
required?: boolean;
/** Validation error, shown in place of `helpText`; also marks the field invalid by default. */
error?: string | null;
helpText?: string;
children: ReactNode;
}

/** A labelled form field: an uppercase label stacked above its control, with an optional help/error line below. */
export const Field = ({ children, error, helpText, label, labelEnd, ...rest }: FieldProps) => (
<Stack flex="1" gap="1.5" minW="0" {...rest}>
<HStack align="center" justify="space-between" minW="0">
<FieldLabel>{label}</FieldLabel>
{labelEnd}
</HStack>
{children}
{error ? (
<Text color="fg.error" fontSize="2xs" role="alert">
{error}
</Text>
) : helpText ? (
<Text color="fg.subtle" fontSize="2xs">
{helpText}
</Text>
) : null}
</Stack>
);
export const Field = ({
children,
disabled,
error,
helpText,
id,
invalid,
label,
labelEnd,
orientation = 'vertical',
readOnly,
required,
...rest
}: FieldProps) => {
const recipe = useRecipe({ recipe: fieldLabelRecipe });
const isHorizontal = orientation === 'horizontal';
const isInvalid = invalid ?? Boolean(error);
const ids = useMemo(
() =>
id
? {
errorText: `${id}-error`,
helperText: `${id}-help`,
label: `${id}-label`,
}
: undefined,
[id]
);
const labelContent = <ChakraField.Label css={recipe()}>{label}</ChakraField.Label>;
const message = error ? (
<ChakraField.ErrorText color="fg.error" fontSize="2xs" role="alert">
{error}
</ChakraField.ErrorText>
) : helpText ? (
<ChakraField.HelperText color="fg.subtle" fontSize="2xs">
{helpText}
</ChakraField.HelperText>
) : null;

if (!isHorizontal) {
return (
<ChakraField.Root
asChild
disabled={disabled}
id={id}
ids={ids}
invalid={isInvalid}
readOnly={readOnly}
required={required}
unstyled
>
<Stack flex="1" gap="1.5" minW="0" {...rest}>
<HStack align="center" justify="space-between" minW="0">
{labelContent}
{labelEnd}
</HStack>
{children}
{message}
</Stack>
</ChakraField.Root>
);
}

return (
<ChakraField.Root
asChild
disabled={disabled}
id={id}
ids={ids}
invalid={isInvalid}
readOnly={readOnly}
required={required}
unstyled
>
<Stack align="flex-start" direction="row" flex="1" gap="1.5" minW="0" {...rest}>
<HStack align="center" flexShrink="0" justify="space-between" minH="8" minW="0">
{labelContent}
{labelEnd}
</HStack>
<Stack flex="1" gap="1.5" minW="0" w="full">
{children}
{message}
</Stack>
</Stack>
</ChakraField.Root>
);
};
77 changes: 77 additions & 0 deletions invokeai/frontend/webv2/src/workbench/components/ui/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type {
CollectionItem,
SelectContentProps,
SelectIndicatorGroupProps,
SelectPositionerProps,
SelectRootProps,
SelectTriggerProps,
SelectValueTextProps,
} from '@chakra-ui/react';
import type { Key, ReactNode } from 'react';

import { Portal, Select as ChakraSelect } from '@chakra-ui/react';

const getDefaultItemKey = <T extends CollectionItem>(item: T, index: number): Key => {
const keyedItem = item as { id?: Key; value?: Key };

return keyedItem.value ?? keyedItem.id ?? index;
};

const renderDefaultItem = <T extends CollectionItem>(item: T): ReactNode => {
const labelledItem = item as { label?: ReactNode; value?: ReactNode };

return labelledItem.label ?? labelledItem.value;
};

export interface SelectProps<T extends CollectionItem> extends Omit<SelectRootProps<T>, 'children'> {
contentProps?: SelectContentProps;
getItemKey?: (item: T, index: number) => Key;
indicatorGroupProps?: SelectIndicatorGroupProps;
itemIndicator?: boolean;
portalled?: boolean;
positionerProps?: SelectPositionerProps;
renderItem?: (item: T) => ReactNode;
triggerProps?: SelectTriggerProps;
valueText?: ReactNode;
valueTextProps?: SelectValueTextProps;
}

/** Workbench Select: Chakra's custom Select with the standard trigger, portal, and item markup pre-wired. */
export const Select = <T extends CollectionItem>({
collection,
contentProps,
getItemKey = getDefaultItemKey,
indicatorGroupProps,
itemIndicator = true,
portalled = true,
positionerProps,
renderItem = renderDefaultItem,
triggerProps,
valueText,
valueTextProps,
...rootProps
}: SelectProps<T>) => (
<ChakraSelect.Root collection={collection} {...rootProps}>
<ChakraSelect.HiddenSelect />
<ChakraSelect.Control>
<ChakraSelect.Trigger {...triggerProps}>
<ChakraSelect.ValueText {...valueTextProps}>{valueText}</ChakraSelect.ValueText>
</ChakraSelect.Trigger>
<ChakraSelect.IndicatorGroup {...indicatorGroupProps}>
<ChakraSelect.Indicator />
</ChakraSelect.IndicatorGroup>
</ChakraSelect.Control>
<Portal disabled={!portalled}>
<ChakraSelect.Positioner {...positionerProps}>
<ChakraSelect.Content {...contentProps}>
{collection.items.map((item, index) => (
<ChakraSelect.Item key={getItemKey(item, index)} item={item}>
<ChakraSelect.ItemText>{renderItem(item)}</ChakraSelect.ItemText>
{itemIndicator ? <ChakraSelect.ItemIndicator /> : null}
</ChakraSelect.Item>
))}
</ChakraSelect.Content>
</ChakraSelect.Positioner>
</Portal>
</ChakraSelect.Root>
);
Loading
Loading