1
0
Fork 0

add settings slice and allow resizing image history

This commit is contained in:
Sean Sube 2024-01-11 21:53:02 -06:00
parent 2b562d9464
commit 5888cae298
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
16 changed files with 242 additions and 81 deletions

1
api/gui/.gitignore vendored
View File

@ -1,3 +1,4 @@
bundle/main.css
bundle/main.js bundle/main.js
config.json config.json
index.html index.html

View File

@ -22,6 +22,7 @@ docs-local:
build: deps build: deps
yarn tsc yarn tsc
cp -v src/components/main.css out/src/components/
build-shebang: build build-shebang: build
sed -i '1s;^;#! /usr/bin/env node\n\n;g' $(shell pwd)/out/src/main.js sed -i '1s;^;#! /usr/bin/env node\n\n;g' $(shell pwd)/out/src/main.js
@ -35,6 +36,7 @@ bundle: build
# copy everything into the server's default path # copy everything into the server's default path
cp -v src/index.html ../api/gui/ cp -v src/index.html ../api/gui/
cp -v src/config.json ../api/gui/ cp -v src/config.json ../api/gui/
cp -v out/bundle/main.css ../api/gui/bundle/
cp -v out/bundle/main.js ../api/gui/bundle/ cp -v out/bundle/main.js ../api/gui/bundle/
COVER_OPTS := --all \ COVER_OPTS := --all \

View File

@ -16,6 +16,7 @@
"@tanstack/react-query": "^4.0.5", "@tanstack/react-query": "^4.0.5",
"@types/lodash": "^4.14.192", "@types/lodash": "^4.14.192",
"@types/node": "^18.15.11", "@types/node": "^18.15.11",
"allotment": "^1.19.5",
"browser-bunyan": "^1.8.0", "browser-bunyan": "^1.8.0",
"exifreader": "^4.13.0", "exifreader": "^4.13.0",
"i18next": "^22.4.14", "i18next": "^22.4.14",

View File

@ -48,8 +48,13 @@ export function ImageHistory(props: ImageHistoryProps) {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-magic-numbers return <Grid
return <Grid container spacing={2}>{children.map(([key, child]) => <Grid item key={key} xs={12 / width}>{child}</Grid>)}</Grid>; container
spacing={2}
>{
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
children.map(([key, child]) => <Grid item key={key} xs={12 / width}>{child}</Grid>)
}</Grid>;
} }
export function selectActions(state: OnnxState) { export function selectActions(state: OnnxState) {

View File

@ -1,13 +1,16 @@
/* eslint-disable @typescript-eslint/no-magic-numbers */ /* eslint-disable @typescript-eslint/no-magic-numbers */
import { mustExist } from '@apextoaster/js-utils'; import { mustExist } from '@apextoaster/js-utils';
import { TabContext, TabList, TabPanel } from '@mui/lab'; import { TabContext, TabList, TabPanel } from '@mui/lab';
import { Box, Button, Container, CssBaseline, Divider, Stack, Tab, useMediaQuery } from '@mui/material'; import { Box, Container, CssBaseline, Divider, Stack, Tab, useMediaQuery } from '@mui/material';
import { Breakpoint, SxProps, Theme, ThemeProvider, createTheme } from '@mui/material/styles'; import { Breakpoint, SxProps, Theme, ThemeProvider, createTheme } from '@mui/material/styles';
import { Allotment } from 'allotment';
import * as React from 'react'; import * as React from 'react';
import { useContext, useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { useHash } from 'react-use/lib/useHash'; import { useHash } from 'react-use/lib/useHash';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { shallow } from 'zustand/shallow';
import { Motd } from '../Motd.js';
import { OnnxState, StateContext } from '../state/full.js'; import { OnnxState, StateContext } from '../state/full.js';
import { ImageHistory } from './ImageHistory.js'; import { ImageHistory } from './ImageHistory.js';
import { Logo } from './Logo.js'; import { Logo } from './Logo.js';
@ -19,7 +22,9 @@ import { Settings } from './tab/Settings.js';
import { Txt2Img } from './tab/Txt2Img.js'; import { Txt2Img } from './tab/Txt2Img.js';
import { Upscale } from './tab/Upscale.js'; import { Upscale } from './tab/Upscale.js';
import { TAB_LABELS, getTab, getTheme } from './utils.js'; import { TAB_LABELS, getTab, getTheme } from './utils.js';
import { Motd } from '../Motd.js';
import 'allotment/dist/style.css';
import './main.css';
export interface OnnxWebProps { export interface OnnxWebProps {
motd: boolean; motd: boolean;
@ -30,7 +35,7 @@ export function OnnxWeb(props: OnnxWebProps) {
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const store = mustExist(useContext(StateContext)); const store = mustExist(useContext(StateContext));
const stateTheme = useStore(store, selectTheme); const stateTheme = useStore(store, selectTheme);
const layout = useStore(store, selectLayout); const layout = useStore(store, selectLayout, shallow);
const theme = useMemo( const theme = useMemo(
() => createTheme({ () => createTheme({
@ -41,13 +46,7 @@ export function OnnxWeb(props: OnnxWebProps) {
[prefersDarkMode, stateTheme], [prefersDarkMode, stateTheme],
); );
const [hash, setHash] = useHash(); const historyStyle: SxProps<Theme> = LAYOUT_STYLES[layout.direction].history.style;
const historyStyle: SxProps<Theme> = {
mx: 4,
my: 4,
...LAYOUT_STYLES[layout.direction].history.style,
};
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
@ -57,44 +56,7 @@ export function OnnxWeb(props: OnnxWebProps) {
<Logo /> <Logo />
</Box> </Box>
{props.motd && <Motd />} {props.motd && <Motd />}
<Stack direction={LAYOUT_STYLES[layout.direction].direction} spacing={2}> {layout.direction === 'vertical' ? <VerticalBody layout={layout} style={historyStyle} /> : <HorizontalBody layout={layout} style={historyStyle} />}
<Stack direction='column'>
<TabContext value={getTab(hash)}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<TabList onChange={(_e, idx) => {
setHash(idx);
}}>
{TAB_LABELS.map((name) => <Tab key={name} label={name} value={name} />)}
</TabList>
</Box>
<TabPanel value='txt2img'>
<Txt2Img />
</TabPanel>
<TabPanel value='img2img'>
<Img2Img />
</TabPanel>
<TabPanel value='inpaint'>
<Inpaint />
</TabPanel>
<TabPanel value='upscale'>
<Upscale />
</TabPanel>
<TabPanel value='blend'>
<Blend />
</TabPanel>
<TabPanel value='models'>
<Models />
</TabPanel>
<TabPanel value='settings'>
<Settings />
</TabPanel>
</TabContext>
</Stack>
<Divider flexItem variant='middle' orientation={LAYOUT_STYLES[layout.direction].divider} />
<Box sx={historyStyle}>
<ImageHistory width={layout.width} />
</Box>
</Stack>
</Container> </Container>
</ThemeProvider> </ThemeProvider>
); );
@ -114,10 +76,14 @@ export function selectLayout(state: OnnxState) {
export const LAYOUT_STYLES = { export const LAYOUT_STYLES = {
horizontal: { horizontal: {
container: false, container: false,
control: {
width: '30%',
},
direction: 'row', direction: 'row',
divider: 'vertical', divider: 'vertical',
history: { history: {
style: { style: {
marginLeft: 4,
maxHeight: '85vb', maxHeight: '85vb',
overflowY: 'auto', overflowY: 'auto',
}, },
@ -126,11 +92,91 @@ export const LAYOUT_STYLES = {
}, },
vertical: { vertical: {
container: 'lg' as Breakpoint, container: 'lg' as Breakpoint,
control: {
width: undefined,
},
direction: 'column', direction: 'column',
divider: 'horizontal', divider: 'horizontal',
history: { history: {
style: {}, style: {
mx: 4,
my: 4,
},
width: 2, width: 2,
}, },
}, },
} as const; } as const;
// used for both horizontal and vertical
export interface BodyProps {
layout: ReturnType<typeof selectLayout>;
style: SxProps<Theme>;
}
export function HorizontalBody(props: BodyProps) {
const store = mustExist(useContext(StateContext));
const {direction} = useStore(store, selectLayout, shallow);
const layout = LAYOUT_STYLES[direction];
return <Allotment separator className='body-allotment' minSize={300}>
<TabGroup />
<Box sx={layout.history.style}>
<ImageHistory width={props.layout.width} />
</Box>
</Allotment>;
}
export function VerticalBody(props: BodyProps) {
const store = mustExist(useContext(StateContext));
const {direction, width} = useStore(store, selectLayout, shallow);
const layout = LAYOUT_STYLES[direction];
return <Stack direction={layout.direction} spacing={2}>
<TabGroup />
<Divider flexItem variant='middle' orientation={layout.divider} />
<Box sx={layout.history.style}>
<ImageHistory width={width} />
</Box>
</Stack>;
}
export function TabGroup() {
const store = mustExist(useContext(StateContext));
const {direction} = useStore(store, selectLayout, shallow);
const layout = LAYOUT_STYLES[direction];
const [hash, setHash] = useHash();
return <Stack direction='column' minWidth={layout.control.width} sx={{ mx: 4 }}>
<TabContext value={getTab(hash)}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<TabList onChange={(_e, idx) => {
setHash(idx);
}}>
{TAB_LABELS.map((name) => <Tab key={name} label={name} value={name} />)}
</TabList>
</Box>
<TabPanel value='txt2img'>
<Txt2Img />
</TabPanel>
<TabPanel value='img2img'>
<Img2Img />
</TabPanel>
<TabPanel value='inpaint'>
<Inpaint />
</TabPanel>
<TabPanel value='upscale'>
<Upscale />
</TabPanel>
<TabPanel value='blend'>
<Blend />
</TabPanel>
<TabPanel value='models'>
<Models />
</TabPanel>
<TabPanel value='settings'>
<Settings />
</TabPanel>
</TabContext>
</Stack>;
}

View File

@ -25,6 +25,7 @@ import { OnnxState, StateContext } from '../state/full.js';
import { ImageMetadata } from '../types/api.js'; import { ImageMetadata } from '../types/api.js';
import { DeepPartial } from '../types/model.js'; import { DeepPartial } from '../types/model.js';
import { BaseImgParams, HighresParams, ModelParams, Txt2ImgParams, UpscaleParams } from '../types/params.js'; import { BaseImgParams, HighresParams, ModelParams, Txt2ImgParams, UpscaleParams } from '../types/params.js';
import { downloadAsJson } from '../utils.js';
export const ALLOWED_EXTENSIONS = ['.json','.jpg','.jpeg','.png','.txt','.webp']; export const ALLOWED_EXTENSIONS = ['.json','.jpg','.jpeg','.png','.txt','.webp'];
export const EXTENSION_FILTER = ALLOWED_EXTENSIONS.join(','); export const EXTENSION_FILTER = ALLOWED_EXTENSIONS.join(',');
@ -119,10 +120,10 @@ export function Profiles(props: ProfilesProps) {
onClick={() => { onClick={() => {
const state = store.getState(); const state = store.getState();
saveProfile({ saveProfile({
model: props.selectModel(state),
params: props.selectParams(state),
name: profileName, name: profileName,
highres: props.selectHighres(state), highres: props.selectHighres(state),
model: props.selectModel(state),
params: props.selectParams(state),
upscale: props.selectUpscale(state), upscale: props.selectUpscale(state),
}); });
setDialogOpen(false); setDialogOpen(false);
@ -166,9 +167,9 @@ export function Profiles(props: ProfilesProps) {
</Button> </Button>
<Button component='label' variant='contained' onClick={() => { <Button component='label' variant='contained' onClick={() => {
const state = store.getState(); const state = store.getState();
downloadParamsAsFile({ downloadAsJson({
// TODO: save model parameters name: 'web-ui-profile',
// model: props.selectModel(state), model: props.selectModel(state),
params: props.selectParams(state), params: props.selectParams(state),
highres: props.selectHighres(state), highres: props.selectHighres(state),
upscale: props.selectUpscale(state), upscale: props.selectUpscale(state),
@ -210,19 +211,6 @@ export async function loadParamsFromFile(file: File): Promise<DeepPartial<ImageM
} }
} }
/**
* from https://stackoverflow.com/a/30800715
*/
export function downloadParamsAsFile(data: DeepPartial<ImageMetadata>): void {
const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(data));
const elem = document.createElement('a');
elem.setAttribute('href', dataStr);
elem.setAttribute('download', 'parameters.json');
document.body.appendChild(elem); // required for firefox
elem.click();
elem.remove();
}
export async function parseImageParams(file: File): Promise<DeepPartial<ImageMetadata>> { export async function parseImageParams(file: File): Promise<DeepPartial<ImageMetadata>> {
const tags = await ExifReader.load(file); const tags = await ExifReader.load(file);

View File

@ -0,0 +1,3 @@
.body-allotment {
height: 85vb;
}

View File

@ -1,5 +1,5 @@
import { doesExist, mustExist } from '@apextoaster/js-utils'; import { doesExist, mustExist } from '@apextoaster/js-utils';
import { Refresh } from '@mui/icons-material'; import { Download, Refresh } from '@mui/icons-material';
import { Alert, Button, FormControlLabel, Stack, Switch, TextField, useMediaQuery } from '@mui/material'; import { Alert, Button, FormControlLabel, Stack, Switch, TextField, useMediaQuery } from '@mui/material';
import * as React from 'react'; import * as React from 'react';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
@ -10,6 +10,7 @@ import { getApiRoot } from '../../config.js';
import { ConfigContext, StateContext, STATE_KEY } from '../../state/full.js'; import { ConfigContext, StateContext, STATE_KEY } from '../../state/full.js';
import { getTheme } from '../utils.js'; import { getTheme } from '../utils.js';
import { NumericField } from '../input/NumericField.js'; import { NumericField } from '../input/NumericField.js';
import { downloadAsJson } from '../../utils.js';
function removeBlobs(key: string, value: unknown): unknown { function removeBlobs(key: string, value: unknown): unknown {
if (value instanceof Blob || value instanceof File) { if (value instanceof Blob || value instanceof File) {
@ -55,7 +56,7 @@ export function Settings() {
value={state.historyWidth} value={state.historyWidth}
onChange={(value) => state.setWidth(value)} onChange={(value) => state.setWidth(value)}
/> />
<Button onClick={() => state.setLayout(state.layout === 'horizontal' ? 'vertical' : 'horizontal')}>Toggle Layout</Button> <Button variant='contained' onClick={() => state.setLayout(state.layout === 'horizontal' ? 'vertical' : 'horizontal')}>Toggle Layout</Button>
<TextField variant='outlined' label={t('setting.prompt')} value={state.defaults.prompt} onChange={(event) => { <TextField variant='outlined' label={t('setting.prompt')} value={state.defaults.prompt} onChange={(event) => {
state.setDefaults({ state.setDefaults({
prompt: event.target.value, prompt: event.target.value,
@ -82,14 +83,19 @@ export function Settings() {
</Alert> </Alert>
</Stack> </Stack>
<Stack direction='row' spacing={2}> <Stack direction='row' spacing={2}>
<TextField variant='outlined' label={t('setting.state')} value={json} onChange={(event) => { <TextField variant='outlined' label={t('setting.state.label')} value={json} onChange={(event) => {
setJson(event.target.value); setJson(event.target.value);
}} /> }} />
<Button variant='contained' startIcon={<Refresh />} onClick={() => { <Button variant='contained' startIcon={<Refresh />} onClick={() => {
window.localStorage.setItem(STATE_KEY, json); window.localStorage.setItem(STATE_KEY, json);
window.location.reload(); window.location.reload();
}}> }}>
{t('setting.loadState')} {t('setting.state.load')}
</Button>
<Button variant='contained' startIcon={<Download />} onClick={() => {
downloadAsJson(state, 'state.json');
}}>
{t('setting.state.save')}
</Button> </Button>
</Stack> </Stack>
<FormControlLabel control={ <FormControlLabel control={

View File

@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>ONNX Web</title> <title>ONNX Web</title>
<link href="./bundle/main.css" rel="stylesheet">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

34
gui/src/state/settings.ts Normal file
View File

@ -0,0 +1,34 @@
import { Slice } from './types.js';
export type Layout = 'horizontal' | 'vertical';
export const DEFAULT_LAYOUT = {
historyWidth: 4,
layout: 'vertical' as Layout,
} as const;
export interface SettingsSlice {
historyWidth: number;
layout: Layout;
setLayout(layout: Layout): void;
setWidth(width: number): void;
}
export function createSettingsSlice<TState extends SettingsSlice>(): Slice<TState, SettingsSlice> {
return (set) => ({
...DEFAULT_LAYOUT,
setLayout(layout) {
set((prev) => ({
...prev,
layout,
}));
},
setWidth(width) {
set((prev) => ({
...prev,
historyWidth: width,
}));
},
});
}

View File

@ -216,7 +216,6 @@ export const I18N_STRINGS_DE = {
limit: 'Bildgeschichte', limit: 'Bildgeschichte',
width: '', width: '',
}, },
loadState: 'Laden',
prompt: 'Standard-Eingabeaufforderung', prompt: 'Standard-Eingabeaufforderung',
reset: { reset: {
all: 'Alles zurücksetzen', all: 'Alles zurücksetzen',
@ -226,7 +225,11 @@ export const I18N_STRINGS_DE = {
}, },
scheduler: 'Standardplaner', scheduler: 'Standardplaner',
server: 'API-Server', server: 'API-Server',
state: 'Kundenstatus', state: {
label: 'Kundenstatus',
load: 'Laden',
save: '',
},
darkMode: 'Dunkelmodus', darkMode: 'Dunkelmodus',
}, },
sourceFilter: { sourceFilter: {

View File

@ -279,7 +279,6 @@ export const I18N_STRINGS_EN = {
limit: 'Image History Length', limit: 'Image History Length',
width: 'Image History Width', width: 'Image History Width',
}, },
loadState: 'Load',
prompt: 'Default Prompt', prompt: 'Default Prompt',
reset: { reset: {
all: 'Reset All', all: 'Reset All',
@ -289,7 +288,11 @@ export const I18N_STRINGS_EN = {
}, },
scheduler: 'Default Scheduler', scheduler: 'Default Scheduler',
server: 'API Server', server: 'API Server',
state: 'Client State', state: {
label: 'Client State',
save: 'Save',
load: 'Load',
},
darkMode: 'Dark Mode', darkMode: 'Dark Mode',
}, },
scheduler: { scheduler: {

View File

@ -216,7 +216,6 @@ export const I18N_STRINGS_ES = {
limit: 'Historia de la imagen', limit: 'Historia de la imagen',
width: '', width: '',
}, },
loadState: 'Carga estado',
prompt: 'Solicitud predeterminada', prompt: 'Solicitud predeterminada',
reset: { reset: {
all: 'Resetear todo', all: 'Resetear todo',
@ -226,7 +225,11 @@ export const I18N_STRINGS_ES = {
}, },
scheduler: 'Programador predeterminado', scheduler: 'Programador predeterminado',
server: 'Servidor API', server: 'Servidor API',
state: 'Estado del cliente', state: {
label: 'Estado del cliente',
load: 'Carga estado',
save: '',
},
darkMode: 'Modo Oscuro', darkMode: 'Modo Oscuro',
}, },
sourceFilter: { sourceFilter: {

View File

@ -216,7 +216,6 @@ export const I18N_STRINGS_FR = {
limit: '', limit: '',
width: '', width: '',
}, },
loadState: '',
prompt: '', prompt: '',
reset: { reset: {
all: '', all: '',
@ -226,7 +225,11 @@ export const I18N_STRINGS_FR = {
}, },
scheduler: '', scheduler: '',
server: '', server: '',
state: '', state: {
label: '',
load: '',
save: '',
},
darkMode: 'Mode Sombre', darkMode: 'Mode Sombre',
}, },
sourceFilter: { sourceFilter: {

View File

@ -26,3 +26,16 @@ export function trimHash(val: string): string {
return val; return val;
} }
/**
* from https://stackoverflow.com/a/30800715
*/
export function downloadAsJson(data: object, filename = 'parameters.json'): void {
const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(data));
const elem = document.createElement('a');
elem.setAttribute('href', dataStr);
elem.setAttribute('download', filename);
document.body.appendChild(elem); // required for firefox
elem.click();
elem.remove();
}

View File

@ -389,6 +389,11 @@
"@jridgewell/resolve-uri" "3.1.0" "@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14" "@jridgewell/sourcemap-codec" "1.4.14"
"@juggle/resize-observer@^3.3.1":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@mochajs/multi-reporter@^1.1.0": "@mochajs/multi-reporter@^1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@mochajs/multi-reporter/-/multi-reporter-1.1.0.tgz#378aafd9b9ecbd612753899a3be35026b79b62a5" resolved "https://registry.yarnpkg.com/@mochajs/multi-reporter/-/multi-reporter-1.1.0.tgz#378aafd9b9ecbd612753899a3be35026b79b62a5"
@ -806,6 +811,18 @@ ajv@^6.10.0, ajv@^6.12.4:
json-schema-traverse "^0.4.1" json-schema-traverse "^0.4.1"
uri-js "^4.2.2" uri-js "^4.2.2"
allotment@^1.19.5:
version "1.19.5"
resolved "https://registry.yarnpkg.com/allotment/-/allotment-1.19.5.tgz#238e5457cba8ebfc5a6a59c411b4c563048b38ed"
integrity sha512-lQDeuqMkEEzITT56NAXNOWxRXycqVyV62w3X7jVZQS9n+cznDx6RLJMD1cnWcRMfCdPUmAj07FAIR69ueytxDQ==
dependencies:
classnames "^2.3.0"
eventemitter3 "^5.0.0"
lodash.clamp "^4.0.0"
lodash.debounce "^4.0.0"
lodash.isequal "^4.5.0"
use-resize-observer "^9.0.0"
ansi-colors@4.1.1: ansi-colors@4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
@ -1056,6 +1073,11 @@ chokidar@3.5.3, chokidar@^3.5.3:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
classnames@^2.3.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
cliui@^7.0.2: cliui@^7.0.2:
version "7.0.4" version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
@ -1568,6 +1590,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
eventemitter3@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4"
integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==
exifreader@^4.13.0: exifreader@^4.13.0:
version "4.13.0" version "4.13.0"
resolved "https://registry.yarnpkg.com/exifreader/-/exifreader-4.13.0.tgz#f380b33cfc85630a0dbd56edd41e28710a9e9679" resolved "https://registry.yarnpkg.com/exifreader/-/exifreader-4.13.0.tgz#f380b33cfc85630a0dbd56edd41e28710a9e9679"
@ -2233,11 +2260,26 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" p-locate "^5.0.0"
lodash.clamp@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash.clamp/-/lodash.clamp-4.0.3.tgz#5c24bedeeeef0753560dc2b4cb4671f90a6ddfaa"
integrity sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==
lodash.debounce@^4.0.0:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.get@^4.4.2: lodash.get@^4.4.2:
version "4.4.2" version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
lodash.merge@^4.6.2: lodash.merge@^4.6.2:
version "4.6.2" version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@ -3101,6 +3143,13 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
use-resize-observer@^9.0.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-9.1.0.tgz#14735235cf3268569c1ea468f8a90c5789fc5c6c"
integrity sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==
dependencies:
"@juggle/resize-observer" "^3.3.1"
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"