refactor: migrate params editor drawer to custom component

This commit is contained in:
Carlos Valente
2025-06-20 22:23:21 +02:00
committed by Carlos Valente
parent 4827b295fa
commit fdd81354cc
13 changed files with 281 additions and 97 deletions
+1
View File
@@ -4,6 +4,7 @@
"private": true,
"type": "module",
"dependencies": {
"@base-ui-components/react": "1.0.0-beta.0",
"@chakra-ui/react": "^2.7.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -57,5 +57,9 @@
}
.large {
height: 3.5rem;
height: 2.5rem;
}
.xlarge {
height: 3rem;
}
@@ -6,7 +6,7 @@ import style from './Button.module.scss';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'subtle' | 'primary';
size?: 'medium' | 'large';
size?: 'medium' | 'large' | 'xlarge';
}
export default function Button(props: ButtonProps) {
@@ -2,8 +2,6 @@
.baseIconButton {
aspect-ratio: 1;
height: 2rem;
width: 2rem;
display: grid;
place-content: center;
@@ -65,3 +63,18 @@
border-color: $red-900;
}
}
.medium {
height: 2rem;
width: 2rem;
}
.large {
height: 2.5rem;
width: 2.5rem;
}
.xlarge {
height: 3rem;
height: 3rem;
}
@@ -6,13 +6,18 @@ import style from './IconButton.module.scss';
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'subtle' | 'subtle-white' | 'destructive';
size?: 'medium' | 'large' | 'xlarge';
}
export default function IconButton(props: IconButtonProps) {
const { className, children, variant = 'subtle', ...buttonProps } = props;
const { className, children, variant = 'subtle', size = 'medium', ...buttonProps } = props;
return (
<button className={cx([style.baseIconButton, style[variant], className])} type='button' {...buttonProps}>
<button
className={cx([style.baseIconButton, style[variant], style[size], className])}
type='button'
{...buttonProps}
>
{children}
</button>
);
@@ -11,15 +11,31 @@ interface ViewNavigationMenuProps {
isLockable?: boolean;
}
function ViewNavigationMenu(props: ViewNavigationMenuProps) {
const { isLockable } = props;
export default memo(ViewNavigationMenu);
function ViewNavigationMenu({ isLockable }: ViewNavigationMenuProps) {
const { isOpen: isMenuOpen, onOpen: onMenuOpen, onClose: onMenuClose } = useDisclosure();
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable });
const toggleMenu = () => (isMenuOpen ? onMenuClose() : onMenuOpen());
useHotkeys([['mod + ,', () => toggleMenu()]]);
useHotkeys([
[
'Space',
() => {
if (isViewLocked) return;
toggleMenu();
},
{ preventDefault: true },
],
[
'mod + ,',
() => {
if (isViewLocked) return;
showEditFormDrawer();
},
{ preventDefault: true },
],
]);
if (isViewLocked) {
return <ViewLockedIcon />;
@@ -32,5 +48,3 @@ function ViewNavigationMenu(props: ViewNavigationMenuProps) {
</>
);
}
export default memo(ViewNavigationMenu);
@@ -8,12 +8,81 @@
}
}
.backdrop {
position: fixed;
inset: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.7);
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
}
}
.drawer {
box-sizing: border-box;
position: fixed;
top: 0;
right: 0;
bottom: 0;
z-index: 11;
width: 40rem;
height: 100vh;
display: flex;
flex-direction: column;
padding: 1rem 1.5rem;
background-color: $gray-1250;
color: $ui-white;
&[data-open] {
transform: translateX(0%);
transition: transform 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
}
&[data-starting-style],
&[data-ending-style] {
transform: translateX(100%);
transition: transform 500ms cubic-bezier(0.45, 1.005, 0, 1.005);
}
// take the whole screen in mobile devices
@media (max-width: $min-tablet) {
width: 100vw;
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 3.5rem;
font-weight: 600;
font-size: 1.25rem;
}
.body {
flex: 1;
padding-bottom: 10vh;
overflow-y: scroll;
}
.footer {
display: flex;
gap: 1rem;
align-items: center;
justify-content: flex-end;
}
.sectionList {
display: flex;
flex-direction: column;
min-height: 100%;
gap: 2rem;
overflow-y: scroll;
padding-right: 0.5rem;
}
@@ -1,18 +1,12 @@
import { FormEvent, memo, useEffect } from 'react';
import { IoClose } from 'react-icons/io5';
import { useSearchParams } from 'react-router-dom';
import {
Button,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
useDisclosure,
} from '@chakra-ui/react';
import { Dialog } from '@base-ui-components/react/dialog';
import { useDisclosure } from '@mantine/hooks';
import useViewSettings from '../../hooks-query/useViewSettings';
import Button from '../buttons/Button';
import IconButton from '../buttons/IconButton';
import Info from '../info/Info';
import { ViewOption } from './viewParams.types';
@@ -31,27 +25,27 @@ function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
const [searchParams, setSearchParams] = useSearchParams();
const { data: viewSettings } = useViewSettings();
const { isOpen, onClose, onOpen } = useDisclosure();
const [isOpen, handlers] = useDisclosure(false);
// handle opening the drawer
useEffect(() => {
const isEditing = searchParams.get('edit');
if (isEditing === 'true') {
return onOpen();
return handlers.open();
}
}, [searchParams, onOpen]);
}, [searchParams, handlers]);
const handleClose = () => {
searchParams.delete('edit');
setSearchParams(searchParams);
onClose();
handlers.close();
};
const resetParams = () => {
setSearchParams();
onClose();
handlers.close();
};
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
@@ -63,39 +57,48 @@ function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
};
return (
<Drawer isOpen={isOpen} placement='right' onClose={handleClose} variant='ontime' size='lg'>
<DrawerOverlay />
<DrawerContent>
<DrawerHeader>
<DrawerCloseButton size='lg' />
Customise
</DrawerHeader>
<DrawerBody>
{viewSettings.overrideStyles && (
<Info className={style.info}>This view style is being modified by a custom CSS file.</Info>
)}
<form id='edit-params-form' onSubmit={onParamsFormSubmit} className={style.sectionList}>
{viewOptions.map((section) => (
<ViewParamsSection
key={section.title}
title={section.title}
collapsible={section.collapsible}
options={section.options}
/>
))}
</form>
</DrawerBody>
<DrawerFooter className={style.drawerFooter}>
<Button variant='ontime-ghosted' onClick={resetParams} type='reset'>
Reset to default
</Button>
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
Save
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
<Dialog.Root
open={isOpen}
onOpenChange={(open) => {
if (!open) {
handleClose();
}
}}
>
<Dialog.Portal>
<Dialog.Backdrop className={style.backdrop} />
<Dialog.Popup className={style.drawer}>
<div className={style.header}>
<Dialog.Title>Customise</Dialog.Title>
<IconButton variant='subtle-white' size='large' onClick={handleClose}>
<IoClose />
</IconButton>
</div>
<div className={style.body}>
{viewSettings.overrideStyles && (
<Info className={style.info}>This view style is being modified by a custom CSS file.</Info>
)}
<form id='edit-params-form' onSubmit={onParamsFormSubmit} className={style.sectionList}>
{viewOptions.map((section) => (
<ViewParamsSection
key={section.title}
title={section.title}
collapsible={section.collapsible}
options={section.options}
/>
))}
</form>
</div>
<div className={style.footer}>
<Button variant='subtle' size='large' onClick={resetParams} type='reset'>
Reset to default
</Button>
<Button variant='primary' size='large' form='edit-params-form' type='submit'>
Save
</Button>
</div>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
);
}
@@ -114,7 +114,7 @@ function CountdownContents({ playableEvents, selectedId, subscriptions, time, go
return (
<div className='empty-container'>
<Empty text={getLocalizedString('countdown.select_event')} className='empty-container' />
<Button variant='primary' size='large' onClick={goToEditMode}>
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
<IoAdd /> Add
</Button>
</div>
@@ -127,7 +127,7 @@ function CountdownContents({ playableEvents, selectedId, subscriptions, time, go
return (
<div className='empty-container'>
<Empty text={getLocalizedString('countdown.select_event')} className='empty-container' />
<Button variant='primary' size='large' onClick={goToEditMode}>
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
<IoAdd /> Add
</Button>
</div>
@@ -83,13 +83,13 @@ export default function CountdownSelect({ events, subscriptions, disableEdit }:
})}
<div className='fab-container'>
<Button variant='subtle' size='large' onClick={disableEdit}>
<Button variant='subtle' size='xlarge' onClick={disableEdit}>
<IoArrowBack /> Go back
</Button>
<Button variant='subtle' size='large' onClick={() => setSelected([])} disabled={selected.length === 0}>
<Button variant='subtle' size='xlarge' onClick={() => setSelected([])} disabled={selected.length === 0}>
<IoClose /> Clear
</Button>
<Button variant='primary' size='large' disabled={events.length < 1} onClick={applySelection}>
<Button variant='primary' size='xlarge' disabled={events.length < 1} onClick={applySelection}>
<IoSaveOutline /> Save
</Button>
</div>
@@ -113,7 +113,7 @@ export default function CountdownSubscriptions({
);
})}
<div className={cx(['fab-container', !showFab && 'fab-container--hidden'])}>
<Button variant='primary' size='large' onClick={goToEditMode}>
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
<IoPencil /> Edit
</Button>
</div>
+11 -11
View File
@@ -3,34 +3,34 @@ import { expect, type Page, test } from '@playwright/test';
test.describe('test view navigation feature', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:4001/');
page.locator('data-test-id=timer-view');
await expect(page.locator('data-testid=timer-view')).toBeVisible();
});
test('Minimal', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Minimal Timer' }).click();
page.locator('data-test-id=minimal-timer');
await expect(page.locator('data-testid=minimal-timer')).toBeVisible();
await expect(page).toHaveURL('http://localhost:4001/minimal');
});
test('Wall Clock', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Wall Clock', exact: true }).click();
page.locator('data-test-id=clock-view');
await expect(page.locator('data-testid=clock-view')).toBeVisible();
await expect(page).toHaveURL('http://localhost:4001/clock');
});
test('Timeline', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Timeline' }).click();
page.locator('data-test-id=timeline-view');
page.locator('data-testid=timeline-view');
await expect(page).toHaveURL('http://localhost:4001/timeline');
});
test('Backstage', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Backstage' }).click();
page.locator('data-test-id=backstage-view');
page.locator('data-testid=backstage-view');
await expect(page).toHaveURL('http://localhost:4001/backstage');
});
@@ -38,39 +38,39 @@ test.describe('test view navigation feature', () => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Lower Thirds' }).click();
await expect(page).toHaveURL('http://localhost:4001/lower');
const errorBoundary = page.locator('data-test-id=error-container');
const errorBoundary = page.locator('data-testid=error-container');
await expect(errorBoundary).toHaveCount(0);
});
test('Studio Clock', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Studio Clock' }).click();
page.locator('data-test-id=studio-view');
page.locator('data-testid=studio-view');
await expect(page).toHaveURL('http://localhost:4001/studio');
});
test('Countdown', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Countdown' }).click();
page.locator('data-test-id=countdown-view');
page.locator('data-testid=countdown-view');
await expect(page).toHaveURL('http://localhost:4001/countdown');
});
test('Project Info', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Project Info' }).click();
page.locator('data-test-id=project-view');
page.locator('data-testid=project-view');
await expect(page).toHaveURL('http://localhost:4001/info');
});
test('Timer', async ({ page }) => {
await openNavigationMenu(page);
await page.getByRole('link', { name: 'Timer', exact: true }).click();
page.locator('data-test-id=timer-view');
page.locator('data-testid=timer-view');
await expect(page).toHaveURL('http://localhost:4001/timer');
});
});
async function openNavigationMenu(page: Page) {
await page.keyboard.press('ControlOrMeta + ,');
await page.keyboard.press('Space');
}
+91 -16
View File
@@ -86,6 +86,9 @@ importers:
apps/client:
dependencies:
'@base-ui-components/react':
specifier: 1.0.0-beta.0
version: 1.0.0-beta.0(@types/react@18.0.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@chakra-ui/react':
specifier: ^2.7.0
version: 2.7.0(@emotion/react@11.10.6(@types/react@18.0.26)(react@18.3.1))(@emotion/styled@11.10.6(@emotion/react@11.10.6(@types/react@18.0.26)(react@18.3.1))(@types/react@18.0.26)(react@18.3.1))(@types/react@18.0.26)(framer-motion@10.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -178,7 +181,7 @@ importers:
version: 3.1.1
zustand:
specifier: ^5.0.3
version: 5.0.3(@types/react@18.0.26)(react@18.3.1)(use-sync-external-store@1.2.0(react@18.3.1))
version: 5.0.3(@types/react@18.0.26)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1))
devDependencies:
'@sentry/vite-plugin':
specifier: ^2.16.1
@@ -299,7 +302,7 @@ importers:
version: 2.8.5
dotenv:
specifier: ^16.0.1
version: 16.3.1
version: 16.5.0
express:
specifier: 5.1.0
version: 5.1.0
@@ -618,6 +621,10 @@ packages:
resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.27.6':
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
engines: {node: '>=6.9.0'}
'@babel/template@7.22.15':
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
@@ -642,6 +649,17 @@ packages:
resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==}
engines: {node: '>=6.9.0'}
'@base-ui-components/react@1.0.0-beta.0':
resolution: {integrity: sha512-lPw5/40g/TbpSG1e1g4drl10kaSY2VBOFFQ9axmGhwPGqrQmTuW42jcUq/7OPdXQAyMakfWMWLSXyk3NXbRk+Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@types/react':
optional: true
'@chakra-ui/accordion@2.2.0':
resolution: {integrity: sha512-2IK1iLzTZ22u8GKPPPn65mqJdZidn4AvkgAbv17ISdKA07VHJ8jSd4QF1T5iCXjKfZ0XaXozmhP4kDhjwF2IbQ==}
peerDependencies:
@@ -1704,6 +1722,27 @@ packages:
resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@floating-ui/core@1.7.1':
resolution: {integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==}
'@floating-ui/dom@1.7.1':
resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==}
'@floating-ui/react-dom@2.1.3':
resolution: {integrity: sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/react@0.27.12':
resolution: {integrity: sha512-kKlWNrpIQxF1B/a2MZvE0/uyKby4960yjO91W7nVyNKmmfNi62xU9HCjL1M1eWzx/LFj/VPSwJVbwQk9Pq/68A==}
peerDependencies:
react: '>=17.0.0'
react-dom: '>=17.0.0'
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@fontsource/open-sans@5.0.28':
resolution: {integrity: sha512-hBvJHY76pJT/JynGUB5EXWhnzjYfLdcMn655J5p1v9lTT9HdQSy+keq2KPVXO2Htlg998BBa3p6u/jlrZ6w0kg==}
@@ -3108,10 +3147,6 @@ packages:
dotenv-expand@5.1.0:
resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==}
dotenv@16.3.1:
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
engines: {node: '>=12'}
dotenv@16.5.0:
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
engines: {node: '>=12'}
@@ -5114,6 +5149,9 @@ packages:
resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==}
engines: {node: ^14.18.0 || >=16.0.0}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tar-mini@0.2.0:
resolution: {integrity: sha512-+qfUHz700DWnRutdUsxRRVZ38G1Qr27OetwaMYTdg8hcPxf46U0S1Zf76dQMWRBmusOt2ZCK5kbIaiLkoGO7WQ==}
@@ -5391,10 +5429,10 @@ packages:
'@types/react':
optional: true
use-sync-external-store@1.2.0:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
use-sync-external-store@1.5.0:
resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
utf8-byte-length@1.0.4:
resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==}
@@ -5873,6 +5911,8 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.27.6': {}
'@babel/template@7.22.15':
dependencies:
'@babel/code-frame': 7.24.2
@@ -5923,6 +5963,17 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@base-ui-components/react@1.0.0-beta.0(@types/react@18.0.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.27.6
'@floating-ui/react': 0.27.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@floating-ui/utils': 0.2.9
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
use-sync-external-store: 1.5.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.0.26
'@chakra-ui/accordion@2.2.0(@chakra-ui/system@2.5.8(@emotion/react@11.10.6(@types/react@18.0.26)(react@18.3.1))(@emotion/styled@11.10.6(@emotion/react@11.10.6(@types/react@18.0.26)(react@18.3.1))(@types/react@18.0.26)(react@18.3.1))(react@18.3.1))(framer-motion@10.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies:
'@chakra-ui/descendant': 3.0.14(react@18.3.1)
@@ -7101,6 +7152,31 @@ snapshots:
'@eslint/js@8.56.0': {}
'@floating-ui/core@1.7.1':
dependencies:
'@floating-ui/utils': 0.2.9
'@floating-ui/dom@1.7.1':
dependencies:
'@floating-ui/core': 1.7.1
'@floating-ui/utils': 0.2.9
'@floating-ui/react-dom@2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/dom': 1.7.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@floating-ui/react@0.27.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react-dom': 2.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@floating-ui/utils': 0.2.9
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tabbable: 6.2.0
'@floating-ui/utils@0.2.9': {}
'@fontsource/open-sans@5.0.28': {}
'@gar/promisify@1.1.3': {}
@@ -7338,7 +7414,7 @@ snapshots:
'@babel/core': 7.23.6
'@sentry/babel-plugin-component-annotate': 2.16.1
'@sentry/cli': 2.23.0(encoding@0.1.13)
dotenv: 16.3.1
dotenv: 16.5.0
find-up: 5.0.0
glob: 9.3.2
magic-string: 0.30.8
@@ -8721,8 +8797,6 @@ snapshots:
dotenv-expand@5.1.0: {}
dotenv@16.3.1: {}
dotenv@16.5.0: {}
dotenv@9.0.2: {}
@@ -11014,6 +11088,8 @@ snapshots:
'@pkgr/core': 0.1.1
tslib: 2.6.2
tabbable@6.2.0: {}
tar-mini@0.2.0: {}
tar-stream@2.2.0:
@@ -11270,10 +11346,9 @@ snapshots:
optionalDependencies:
'@types/react': 18.0.26
use-sync-external-store@1.2.0(react@18.3.1):
use-sync-external-store@1.5.0(react@18.3.1):
dependencies:
react: 18.3.1
optional: true
utf8-byte-length@1.0.4: {}
@@ -11547,8 +11622,8 @@ snapshots:
compress-commons: 4.1.2
readable-stream: 3.6.2
zustand@5.0.3(@types/react@18.0.26)(react@18.3.1)(use-sync-external-store@1.2.0(react@18.3.1)):
zustand@5.0.3(@types/react@18.0.26)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)):
optionalDependencies:
'@types/react': 18.0.26
react: 18.3.1
use-sync-external-store: 1.2.0(react@18.3.1)
use-sync-external-store: 1.5.0(react@18.3.1)