(frontend) add a user menu

We are now using the UserMenu component from
the ui-kit. It is a dropdown displaying the user's
name and email, along with a logout button and a
language picker. To fit the design of the user menu,
we adapted the LanguagePicker adn are now using the
ui-kit language picker component.
This commit is contained in:
Anthony LC
2026-07-01 17:56:46 +02:00
parent acadbc33e1
commit 50b696cba1
21 changed files with 199 additions and 176 deletions
+1
View File
@@ -10,6 +10,7 @@ and this project adheres to
- ✨(y-provider) preserve callouts, PDFs, page breaks, interlinking
links and commented text on HTML/markdown export #2296
- ✨(frontend) add a user menu #2463
### Changed
@@ -24,10 +24,11 @@ const saveStorageState = async (
await SignIn(page, browserName);
/**
* If the grid is displayed, it means the user is logged in and the storage state can be saved.
*/
await expect(
page.locator('header').first().getByRole('button', {
name: 'Logout',
}),
page.getByRole('heading', { name: 'All docs', level: 2 }),
).toBeVisible({ timeout: 10000 });
await page.context().storageState({
@@ -17,6 +17,7 @@ import {
updateRoleUser,
updateShareLink,
} from './utils-share';
import { logOut } from './utils-signin';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -312,7 +313,7 @@ test.describe('Doc Comments', () => {
await updateShareLink(page, 'Public', 'Editing');
// Anonymous user can see and add comments
await otherPage.getByRole('button', { name: 'Logout' }).click();
await logOut(otherPage);
await expect(
otherPage
@@ -9,7 +9,7 @@ import {
} from './utils-common';
import { writeInEditor } from './utils-editor';
import { connectOtherUserToDoc, updateRoleUser } from './utils-share';
import { SignIn } from './utils-signin';
import { SignIn, logOut } from './utils-signin';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Document create member', () => {
@@ -371,11 +371,7 @@ test.describe('Document create member: Multiple login', () => {
const urlDoc = page.url();
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await logOut(page);
const otherBrowser = BROWSERS.find((b) => b !== browserName);
@@ -407,21 +407,23 @@ test.describe('Doc Tree', () => {
await expect(selectedSubDoc).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByLabel('User menu')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByLabel('Open help menu')).toBeFocused();
await page.keyboard.press('Tab');
await expect(
page.locator('[data-panel-resize-handle-id]').first(),
).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(page.getByLabel('Open help menu')).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(page.getByLabel('User menu')).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(selectedSubDoc).toBeFocused();
});
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
import { BROWSERS, createDoc, verifyDocName } from './utils-common';
import { getEditor, writeInEditor } from './utils-editor';
import { addNewMember, connectOtherUserToDoc } from './utils-share';
import { SignIn, expectLoginPage } from './utils-signin';
import { SignIn, expectLoginPage, logOut } from './utils-signin';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc Visibility', () => {
@@ -81,13 +81,7 @@ test.describe('Doc Visibility: Restricted', () => {
await verifyDocName(page, docTitle);
const urlDoc = page.url();
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await logOut(page);
await expectLoginPage(page);
await page.goto(urlDoc);
@@ -112,11 +106,7 @@ test.describe('Doc Visibility: Restricted', () => {
const urlDoc = page.url();
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await logOut(page);
const otherBrowser = BROWSERS.find((b) => b !== browserName);
if (!otherBrowser) {
@@ -1,7 +1,6 @@
import { expect, test } from '@playwright/test';
import { overrideConfig } from './utils-common';
import { SignIn, expectLoginPage } from './utils-signin';
test.describe('Header', () => {
test('checks all the elements are visible', async ({ page }) => {
@@ -14,14 +13,6 @@ test.describe('Header', () => {
'font-family',
/Roboto/i,
);
await expect(
header.getByRole('button', {
name: 'Logout',
}),
).toBeVisible();
await expect(header.getByText('English')).toBeVisible();
});
test('checks all the elements are visible with DSFR theme', async ({
@@ -106,21 +97,3 @@ test.describe('Header', () => {
await expect(logoImage).toHaveAttribute('alt', '');
});
});
test.describe('Header: Log out', () => {
test.use({ storageState: { cookies: [], origins: [] } });
// eslint-disable-next-line playwright/expect-expect
test('checks logout button', async ({ page, browserName }) => {
await page.goto('/');
await SignIn(page, browserName);
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await expectLoginPage(page);
});
});
@@ -40,75 +40,71 @@ test.describe('Language', () => {
});
test('checks language switching', async ({ page }) => {
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
await expect(page.locator('html')).toHaveAttribute('lang', 'en-us');
// initial language should be english
await expect(
page.getByRole('link', {
name: 'New',
exact: true,
}),
).toBeVisible();
// switch to french
await waitForLanguageSwitch(page, TestLanguage.French);
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await page.getByLabel('Menu utilisateur').click();
await expect(
header.getByRole('button').getByText('Français'),
page.getByRole('button', { name: 'Language: Français' }),
).toBeVisible();
await expect(page.getByLabel('Se déconnecter')).toBeVisible();
await expect(page.getByText('Déconnexion')).toBeVisible();
await page.keyboard.press('Escape');
// Switch to German using the utility function for consistency
await waitForLanguageSwitch(page, TestLanguage.German);
await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible();
await waitForLanguageSwitch(page, TestLanguage.German, 'Menu utilisateur');
await expect(page.getByLabel('Abmelden')).toBeVisible();
await page.getByLabel('User menu').click();
await expect(
page.getByRole('button', { name: 'Language: Deutsch' }),
).toBeVisible();
await expect(page.getByText('Logout', { exact: true })).toBeVisible();
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
await languagePicker.click();
await expect(page.locator('[role="menu"]')).toBeVisible();
const menuItems = page.locator('[role="menuitemradio"]');
await expect(menuItems.first()).toBeVisible();
await menuItems.first().click();
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
await expect(languagePicker).toContainText('English');
});
test('can switch language using only keyboard', async ({ page }) => {
await page.goto('/');
await waitForLanguageSwitch(page, TestLanguage.English);
await waitForLanguageSwitch(page, TestLanguage.English, 'User menu', false);
const languagePicker = page.getByRole('button', {
name: /select language/i,
});
await expect(languagePicker).toBeVisible();
await page.getByLabel('User menu').click();
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
const menu = page.getByRole('menu');
await expect(menu).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'English' })).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Français' }),
).toBeVisible();
await page.waitForTimeout(300);
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect(page.locator('html')).not.toHaveAttribute('lang', 'en-us');
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await expect(
page.getByRole('button', { name: 'Déconnexion' }),
).toBeVisible();
await page.keyboard.press('Escape');
await page.getByLabel('Menu utilisateur').click();
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowUp');
await page.keyboard.press('Enter');
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible();
});
test('checks that backend uses the same language as the frontend', async ({
@@ -171,7 +167,7 @@ test.describe('Language', () => {
/**
* Swedish is not yet supported in the BlockNote locales, so it should fallback to English
*/
await waitForLanguageSwitch(page, TestLanguage.Swedish);
await waitForLanguageSwitch(page, TestLanguage.Swedish, 'Menu utilisateur');
await openSuggestionMenu({ page });
await expect(
suggestionMenu.getByText('Headings', { exact: true }),
@@ -7,6 +7,7 @@ import {
verifyDocName,
} from './utils-common';
import { tryFocusEditorContent } from './utils-editor';
import { SignIn, expectLoginPage, logOut } from './utils-signin';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Left panel desktop', () => {
@@ -19,6 +20,8 @@ test.describe('Left panel desktop', () => {
await expect(page.getByTestId('left-panel-mobile')).toBeHidden();
await expect(page.getByTestId('home-button')).toBeHidden();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
await expect(page.getByLabel('User menu')).toBeVisible();
await expect(page.getByLabel('Open help menu')).toBeVisible();
await goToGridDoc(page);
@@ -178,6 +181,7 @@ test.describe('Left panel desktop', () => {
test.describe('Left panel responsive', () => {
test('checks elements visibility on different screen sizes', async ({
page,
browserName,
}) => {
await page.setViewportSize({ width: 500, height: 1200 });
await page.goto('/');
@@ -188,26 +192,22 @@ test.describe('Left panel responsive', () => {
const header = page.locator('header').first();
const homeButton = page.getByTestId('home-button');
const newDocButton = page.getByTestId('new-doc-button');
const languageButton = page.getByRole('button', {
name: 'Select language',
const userMenu = page.getByRole('button', {
name: 'User menu',
});
const logoutButton = page.getByRole('button', { name: 'Logout' });
await expect(homeButton).not.toBeInViewport();
await expect(newDocButton).not.toBeInViewport();
await expect(languageButton).not.toBeInViewport();
await expect(logoutButton).not.toBeInViewport();
await expect(userMenu).not.toBeInViewport();
const title = await goToGridDoc(page);
await verifyDocName(page, title);
await createDoc(page, 'mobile-doc-test', browserName, 1, true);
await header.getByLabel('Open the header menu').click();
await expect(page.getByTestId('left-panel-mobile')).toBeInViewport();
await expect(homeButton).toBeInViewport();
await expect(newDocButton).toBeInViewport();
await expect(languageButton).toBeInViewport();
await expect(logoutButton).toBeInViewport();
await expect(userMenu).toBeInViewport();
await header.getByLabel('Close the header menu').click();
@@ -217,8 +217,7 @@ test.describe('Left panel responsive', () => {
await expect(page.getByRole('link', { name: 'All docs' })).toBeInViewport();
await expect(newDocButton).toBeInViewport();
await expect(languageButton).toBeInViewport();
await expect(logoutButton).toBeInViewport();
await expect(userMenu).toBeInViewport();
await expect(header.getByLabel('Open the header menu')).toBeHidden();
});
@@ -309,3 +308,16 @@ test.describe('Left panel responsive', () => {
await expect(leftPanel).toBeHidden();
});
});
test.describe('Left Panel: Log out', () => {
test.use({ storageState: { cookies: [], origins: [] } });
// eslint-disable-next-line playwright/expect-expect
test('checks logout button', async ({ page, browserName }) => {
await page.goto('/');
await SignIn(page, browserName);
await logOut(page);
await expectLoginPage(page);
});
});
@@ -399,9 +399,11 @@ type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];
export async function waitForLanguageSwitch(
page: Page,
lang: TestLanguageValue,
labelUserMenu = 'User menu',
withMocking = true,
) {
await page.route(/\**\/api\/v1.0\/users\/\**/, async (route, request) => {
if (request.method().includes('PATCH')) {
if (request.method().includes('PATCH') && withMocking) {
await route.fulfill({
json: {
language: lang.expectedLocale[0],
@@ -412,19 +414,21 @@ export async function waitForLanguageSwitch(
}
});
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
await page.getByLabel(labelUserMenu).click();
const languagePicker = page.getByRole('button', { name: /Language/ });
const isAlreadyTargetLanguage = await languagePicker
.innerText()
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
if (isAlreadyTargetLanguage) {
await page.keyboard.press('Escape');
return;
}
await languagePicker.click();
await page.getByRole('menuitemradio', { name: lang.label }).click();
await page.getByRole('menuitem', { name: lang.label }).click();
await page.keyboard.press('Escape');
}
export const clickInEditorShareButton = async (page: Page) => {
@@ -13,6 +13,11 @@ export const SignIn = async (
await keycloakSignIn(page, browserName, fromHome);
};
export const logOut = async (page: Page) => {
await page.getByLabel('User menu').click();
await page.getByText('Logout', { exact: true }).click();
};
export const customSignIn = async (
page: Page,
browserName: string,
+1 -1
View File
@@ -42,7 +42,7 @@
"@fontsource/material-icons": "5.2.7",
"@gouvfr-lasuite/cunningham-react": "4.3.0",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.23.2",
"@gouvfr-lasuite/ui-kit": "0.25.0",
"@hocuspocus/provider": "3.4.4",
"@mantine/core": "9.3.1",
"@mantine/hooks": "9.3.1",
@@ -838,6 +838,9 @@
--c--components--button--border-radius: 4px;
--c--components--button--border-radius--active: 4px;
--c--components--button--border-radius--focus: 4px;
--c--components--resize-handle--hover--color: var(
--c--contextuals--border--surface--primary
);
--c--components--datagrid--header--color: var(
--c--contextuals--content--semantic--neutral--primary
);
@@ -2539,6 +2542,9 @@
--c--components--button--border-radius: 4px;
--c--components--button--border-radius--active: 4px;
--c--components--button--border-radius--focus: 4px;
--c--components--resize-handle--hover--color: var(
--c--contextuals--border--surface--primary
);
--c--components--datagrid--header--color: var(
--c--contextuals--content--semantic--neutral--primary
);
@@ -657,6 +657,7 @@ export const tokens = {
'border-radius--active': '4px',
'border-radius--focus': '4px',
},
'resize-handle': { 'hover--color': '#E2E2EA' },
datagrid: {
'header--color': '#25252F',
'header--size': '12px',
@@ -1961,6 +1962,7 @@ export const tokens = {
'border-radius--active': '4px',
'border-radius--focus': '4px',
},
'resize-handle': { 'hover--color': '#E2E2EA' },
datagrid: {
'header--color': '#25252F',
'header--size': '12px',
@@ -2,51 +2,30 @@ import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, BoxButton } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { BoxButton } from '@/components';
import ProConnectImg from '../assets/button-proconnect.svg';
import { useAuth } from '../hooks';
import { gotoLogin, gotoLogout } from '../utils';
import { gotoLogin } from '../utils';
export const ButtonLogin = () => {
const { t } = useTranslation();
const { authenticated } = useAuth();
const { colorsTokens } = useCunninghamTheme();
if (!authenticated) {
return (
<Button
onClick={() => gotoLogin()}
color="brand"
variant="tertiary"
aria-label={t('Login')}
className="--docs--button-login"
>
{t('Login')}
</Button>
);
if (authenticated) {
return null;
}
return (
<Box
$css={css`
.--docs--button-logout:focus-visible {
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']} !important;
border-radius: var(--c--globals--spacings--st);
}
`}
<Button
onClick={() => gotoLogin()}
color="brand"
size="small"
aria-label={t('Sign in')}
className="--docs--button-login"
>
<Button
onClick={gotoLogout}
color="brand"
variant="tertiary"
aria-label={t('Logout')}
className="--docs--button-logout"
>
{t('Logout')}
</Button>
</Box>
{t('Sign in')}
</Button>
);
};
@@ -8,14 +8,14 @@ import { useAuthQuery } from '../api';
const regexpUrlsAuth = [/\/docs\/$/g, /\/docs$/g, /^\/$/g];
export const useAuth = () => {
const { data: user, ...authStates } = useAuthQuery();
const { data: user, isFetched, isLoading, isSuccess } = useAuthQuery();
const { pathname } = useRouter();
const { trackEvent } = useAnalytics();
const [hasTracked, setHasTracked] = useState(authStates.isFetched);
const isAuthLoading =
authStates.fetchStatus !== 'idle' || authStates.isLoading;
const [hasTracked, setHasTracked] = useState(isFetched);
const isAuthLoading = isLoading;
const hasInitiallyLoaded = useRef(false);
if (authStates.isFetched) {
if (isFetched) {
hasInitiallyLoaded.current = true;
}
const [pathAllowed, setPathAllowed] = useState<boolean>(
@@ -27,7 +27,7 @@ export const useAuth = () => {
}, [pathname]);
useEffect(() => {
if (!hasTracked && user && authStates.isSuccess) {
if (!hasTracked && user && isSuccess) {
trackEvent({
eventName: 'user',
id: user?.id || '',
@@ -35,14 +35,13 @@ export const useAuth = () => {
});
setHasTracked(true);
}
}, [hasTracked, authStates.isSuccess, user, trackEvent]);
}, [hasTracked, isSuccess, user, trackEvent]);
return {
user,
authenticated: !!user && authStates.isSuccess,
authenticated: !!user && isSuccess,
pathAllowed,
hasInitiallyLoaded: hasInitiallyLoaded.current,
isAuthLoading,
...authStates,
};
};
@@ -6,7 +6,6 @@ import { Box, SkipToContent, StyledLink } from '@/components/';
import { useConfig } from '@/core/config';
import { useCunninghamTheme } from '@/cunningham';
import { ButtonLogin } from '@/features/auth';
import { LanguagePicker } from '@/features/language';
import { LeftPanelToggleMobile } from '@/features/left-panel';
import { useResponsiveStore } from '@/stores';
@@ -89,7 +88,6 @@ export const Header = () => {
$direction="row"
>
<ButtonLogin />
<LanguagePicker />
</Box>
)}
</Box>
@@ -5,7 +5,7 @@ import { Waffle } from '@/components/Waffle';
import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { Title } from '@/features/header';
import { LanguagePicker } from '@/features/language';
import { LanguagePickerLegacy } from '@/features/language';
import { LeftPanelToggleMobile } from '@/features/left-panel';
import { useResponsiveStore } from '@/stores';
@@ -80,7 +80,7 @@ export const HomeHeader = () => {
</Box>
{!isSmallMobile && (
<Box $direction="row" $gap="1rem" $align="center">
<LanguagePicker />
<LanguagePickerLegacy />
<Waffle />
</Box>
)}
@@ -1,3 +1,7 @@
import {
LanguagePicker as LanguagePickerUIKit,
LanguagesOption,
} from '@gouvfr-lasuite/ui-kit';
import { announce } from '@react-aria/live-announcer';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,7 +15,13 @@ import {
useSynchronizedLanguage,
} from '@/features/language';
export const LanguagePicker = () => {
/**
* LanguagePickerLegacy component for selecting language.
* We still have some legacy code that uses this component, so we keep it for now.
* @deprecated Use LanguagePicker instead.
* @returns JSX.Element
*/
export const LanguagePickerLegacy = () => {
const { t, i18n } = useTranslation();
const { data: conf } = useConfig();
const { data: user } = useAuthQuery();
@@ -85,3 +95,43 @@ export const LanguagePicker = () => {
</DropdownMenu>
);
};
export const LanguagePicker = () => {
const { t, i18n } = useTranslation();
const { data: conf } = useConfig();
const { data: user } = useAuthQuery();
const { changeLanguageSynchronized } = useSynchronizedLanguage();
const language = i18n.language;
const languages: LanguagesOption[] = useMemo(() => {
const backendOptions = conf?.LANGUAGES ?? [[language, language]];
return backendOptions.map(([backendLocale, backendLabel]) => ({
label: backendLabel,
value: backendLocale,
isChecked: getMatchingLocales([backendLocale], [language]).length > 0,
}));
}, [conf?.LANGUAGES, language]);
const onChange = (value: string) => {
const lang = conf?.LANGUAGES?.find(([code]) => code === value);
const backendLabel = lang?.[1] ?? value;
void changeLanguageSynchronized(value, user).then(() => {
announce(
t('Language changed to {{language}}', {
language: backendLabel,
defaultValue: `Language changed to ${backendLabel}`,
}),
'polite',
);
});
};
return (
<LanguagePickerUIKit
languages={languages}
size="small"
onChange={onChange}
compact
/>
);
};
@@ -1,12 +1,24 @@
import { UserMenu } from '@gouvfr-lasuite/ui-kit';
import { useTranslation } from 'react-i18next';
import { Box, SeparatedSection } from '@/components';
import { Waffle } from '@/components/Waffle';
import { ButtonLogin } from '@/features/auth';
import { useAuth } from '@/features/auth/hooks/useAuth';
import { gotoLogout } from '@/features/auth/utils';
import { HelpMenu } from '@/features/help';
import { LanguagePicker } from '@/features/language';
import { LanguagePicker } from '@/features/language/components/LanguagePicker';
import { useResponsiveStore } from '@/stores';
export const LeftPanelFooter = () => {
const { isLargeScreen } = useResponsiveStore();
const { t } = useTranslation();
const { user } = useAuth();
const userMenu = user || {
full_name: t('Guest'),
email: '',
};
return (
<SeparatedSection showSeparator="top">
@@ -15,14 +27,15 @@ export const LeftPanelFooter = () => {
$justify="space-between"
$direction="row"
>
<Box $direction="row">
<Box $direction="row" $align="center" $gap="0.2rem">
<UserMenu
user={userMenu}
logout={user ? gotoLogout : undefined}
actions={<LanguagePicker />}
withMobileView={false}
/>
<Waffle />
{!isLargeScreen && (
<>
<ButtonLogin />
<LanguagePicker />
</>
)}
{!isLargeScreen && <ButtonLogin />}
</Box>
<HelpMenu />
</Box>
+4 -9
View File
@@ -2128,10 +2128,10 @@
resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/integration/-/integration-1.0.3.tgz#7aca824ba61d343a7905dc90c8a8bbdbce8f9a09"
integrity sha512-OgP28CqlPi35wQPul1Dr52SngACXAk8buLGqHYXDp23fbTOJThqarrZE/pgJHoc9Ndwiu7ngwBSO4rZ7OPyMpA==
"@gouvfr-lasuite/ui-kit@0.23.2":
version "0.23.2"
resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/ui-kit/-/ui-kit-0.23.2.tgz#9a7531fb35821b75b786bfc3167ad53aab116c4c"
integrity sha512-o6bQjqJadBbWMbFiwDYwVeNKYAD/EGeU2W0/omqNgTEYWjKQCGYwgcxgeYR+SZN9GnWKCXEARs18aYgBl+oRbQ==
"@gouvfr-lasuite/ui-kit@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/ui-kit/-/ui-kit-0.25.0.tgz#9feb7da860ea610cb5f171b90ee074fe39271940"
integrity sha512-yvP1/rpckaDPFGcuNiZqwUn43asnSq8ffS660Oikxrq/m+/5oMtRsApHjkt2Wtn1HfUWGvqazN8wSrr1yEwG0Q==
dependencies:
"@dnd-kit/core" "6.3.1"
"@dnd-kit/modifiers" "9.0.0"
@@ -8323,11 +8323,6 @@ crelt@^1.0.0:
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
crisp-sdk-web@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/crisp-sdk-web/-/crisp-sdk-web-1.1.2.tgz#e5b31ee784cef678a711170f54eca68e28968a22"
integrity sha512-qxSpiT4EGHnVEO6J/t1UURY6fg1ojgquLU0IIr7RNjZxXZs1R2e6AhAiFqxiGZ13Vb4o5VoUpATFU2TzX/ppOQ==
cross-env@10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-10.1.0.tgz#cfd2a6200df9ed75bfb9cb3d7ce609c13ea21783"