diff --git a/.gitignore b/.gitignore
index 52f3fcf21..a0f19970c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,3 @@
.vscode
.DS_Store
-.vscode/extensions.json
-.vscode/settings.json
-.idea
-frontend/techpick/.env.production
-frontend/techpick/.env.development
-frontend/techpick-extension/.env.development
-frontend/techpick-extension/.env.production
+.idea
\ No newline at end of file
diff --git a/frontend/.gitignore b/frontend/.gitignore
index af5823b76..7cd95b789 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -19,13 +19,6 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
# Editor directories and files
.vscode/*
@@ -37,3 +30,13 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# Build output
+dist/
+
+# Dependencies
+node_modules/
+
+# ENV
+.env.development
+.env.production
\ No newline at end of file
diff --git a/frontend/techpick/package.json b/frontend/techpick/package.json
index fb6a972d9..654e12282 100644
--- a/frontend/techpick/package.json
+++ b/frontend/techpick/package.json
@@ -27,6 +27,8 @@
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.1.1",
+ "@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@sentry/nextjs": "8",
"@tanstack/react-query": "^5.59.0",
diff --git a/frontend/techpick/public/video/multiSelectPickMove.gif b/frontend/techpick/public/video/multiSelectPickMove.gif
new file mode 100644
index 000000000..6c350b34f
Binary files /dev/null and b/frontend/techpick/public/video/multiSelectPickMove.gif differ
diff --git a/frontend/techpick/public/video/recommendPickMove.gif b/frontend/techpick/public/video/recommendPickMove.gif
new file mode 100644
index 000000000..3bce6f161
Binary files /dev/null and b/frontend/techpick/public/video/recommendPickMove.gif differ
diff --git a/frontend/techpick/src/app/(signed)/mypage/page.css.ts b/frontend/techpick/src/app/(signed)/mypage/page.css.ts
index d98dc73f0..c38601c3e 100644
--- a/frontend/techpick/src/app/(signed)/mypage/page.css.ts
+++ b/frontend/techpick/src/app/(signed)/mypage/page.css.ts
@@ -8,13 +8,6 @@ export const myPageLayoutStyle = style({
backgroundColor: colorVars.gold2,
});
-export const buttonSectionLayout = style({
- display: 'flex',
- justifyContent: 'flex-end',
- alignItems: 'center',
- padding: '8px',
-});
-
export const logoutButtonStyle = style({
width: '120px',
height: '32px',
@@ -28,4 +21,27 @@ export const logoutButtonStyle = style({
':hover': {
backgroundColor: colorVars.red3,
},
+
+ ':focus': {
+ backgroundColor: colorVars.red3,
+ },
+});
+
+export const myPageContentContainerLayoutStyle = style({
+ display: 'flex',
+ alignItems: 'start',
+ justifyContent: 'space-between',
+});
+
+export const tutorialReplaySwitchLayoutStyle = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: '8px',
+ padding: '16px 0',
+});
+
+export const tutorialReplaySwitchLabelStyle = style({
+ fontSize: '12px',
+ cursor: 'pointer',
+ flexShrink: 0,
});
diff --git a/frontend/techpick/src/app/(signed)/mypage/page.tsx b/frontend/techpick/src/app/(signed)/mypage/page.tsx
index a52c8207d..c71b07f6b 100644
--- a/frontend/techpick/src/app/(signed)/mypage/page.tsx
+++ b/frontend/techpick/src/app/(signed)/mypage/page.tsx
@@ -3,11 +3,14 @@
import { postLogout } from '@/apis/postLogout';
import MyPageContentContainer from '@/components/MyPage/MyPageContentContainer';
import MyPageShareFolderContent from '@/components/MyPage/MyPageShareFolderContent';
+import { TutorialReplaySwitch } from '@/components/TutorialReplaySwitch';
import { ROUTES } from '@/constants';
import {
- buttonSectionLayout,
logoutButtonStyle,
+ myPageContentContainerLayoutStyle,
myPageLayoutStyle,
+ tutorialReplaySwitchLabelStyle,
+ tutorialReplaySwitchLayoutStyle,
} from './page.css';
export default function MyPage() {
@@ -22,17 +25,29 @@ export default function MyPage() {
return (
-
- 내 계정 정보
-
+
+
+
+
+
+
+
+
+
+
-
-
-
);
}
diff --git a/frontend/techpick/src/app/(signed)/recommend/page.tsx b/frontend/techpick/src/app/(signed)/recommend/page.tsx
index f7d722e84..e5c5a3d63 100644
--- a/frontend/techpick/src/app/(signed)/recommend/page.tsx
+++ b/frontend/techpick/src/app/(signed)/recommend/page.tsx
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { getSuggestionRankingPicks } from '@/apis/getSuggestionRankingPicks';
import { FolderContentLayout } from '@/components/FolderContentLayout';
import { RecommendedPickCarousel } from '@/components/RecommendedPickCarousel/RecommendedPickCarousel';
+import { TutorialDialog } from '@/components/TutorialDialog';
import {
useClearSelectedPickIdsOnMount,
useFetchTagList,
@@ -57,6 +58,8 @@ export default function RecommendPage() {
return (
+
+
🔥HOT TREND!🔥
diff --git a/frontend/techpick/src/components/FolderTree/folderTreeHeader.css.ts b/frontend/techpick/src/components/FolderTree/folderTreeHeader.css.ts
index 882cedac2..6b703b793 100644
--- a/frontend/techpick/src/components/FolderTree/folderTreeHeader.css.ts
+++ b/frontend/techpick/src/components/FolderTree/folderTreeHeader.css.ts
@@ -20,4 +20,5 @@ export const folderTreeHeaderTitleLayout = style({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
+ height: '30px',
});
diff --git a/frontend/techpick/src/components/TutorialDialog.tsx b/frontend/techpick/src/components/TutorialDialog.tsx
new file mode 100644
index 000000000..9b08d40b1
--- /dev/null
+++ b/frontend/techpick/src/components/TutorialDialog.tsx
@@ -0,0 +1,150 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import Image from 'next/image';
+import * as Dialog from '@radix-ui/react-dialog';
+import * as Tabs from '@radix-ui/react-tabs';
+import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
+import { IS_TUTORIAL_SEEN_LOCAL_STORAGE_KEY } from '@/constants';
+import { useDisclosure, useLocalStorage } from '@/hooks';
+import { Gap } from './Gap';
+import {
+ dialogCloseButtonStyle,
+ dialogContent,
+ overlayStyle,
+ pointTextStyle,
+ tabContentDescriptionStyle,
+ tabContentStyle,
+ tabListStyle,
+ tabTriggerButtonStyle,
+ tabTriggerLayoutStyle,
+} from './tutorialDialog.css';
+
+const tutorialStepList = ['tutorial-step-1', 'tutorial-step-2'] as const;
+type TutorialStepType = (typeof tutorialStepList)[number];
+
+export function TutorialDialog() {
+ const [tutorialStep, setTutorialStep] = useState
(
+ tutorialStepList[0]
+ );
+ const prevButtonRef = useRef(null);
+ const closeButtonRef = useRef(null);
+ const { isOpen, onClose, onOpen } = useDisclosure();
+
+ const {
+ storedValue: isTutorialSeen,
+ setValue: setIsTutorialSeen,
+ isStoredValueLoad,
+ } = useLocalStorage(IS_TUTORIAL_SEEN_LOCAL_STORAGE_KEY, false);
+
+ const onValueChange = (value: string) => {
+ setTutorialStep(value as TutorialStepType);
+ };
+
+ const handleMouseEnter = (ref: React.RefObject) => {
+ ref.current?.focus();
+ };
+
+ const onCloseTutorial = () => {
+ setIsTutorialSeen(true);
+ onClose();
+ };
+
+ useEffect(
+ function openTutorialForFirstTimeUser() {
+ if (isStoredValueLoad && !isTutorialSeen) {
+ onOpen();
+ }
+ },
+ [isStoredValueLoad, isTutorialSeen, onOpen]
+ );
+
+ return (
+
+
+
+
+
+ 튜토리얼
+ 튜토리얼 설명입니다.
+
+
+
+
+
+ 📌 추천 페이지에서
+ 원하는 걸 저장할 수
+ 있어요!
+
+
+
+
+
+
+
+
+
+ 저장한 북마크를 쉽게
+ 이동할 수 있어요!
+
+
+
+
+
+
+
+
+ {tutorialStep === tutorialStepList[0] ? (
+
+
+
+ ) : (
+
+ handleMouseEnter(prevButtonRef)}
+ asChild
+ >
+
+
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/frontend/techpick/src/components/TutorialReplaySwitch.tsx b/frontend/techpick/src/components/TutorialReplaySwitch.tsx
new file mode 100644
index 000000000..5dcb185b7
--- /dev/null
+++ b/frontend/techpick/src/components/TutorialReplaySwitch.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import * as Switch from '@radix-ui/react-switch';
+import { IS_TUTORIAL_SEEN_LOCAL_STORAGE_KEY } from '@/constants';
+import { useLocalStorage } from '@/hooks';
+import { switchRoot, switchThumb } from './tutorialReplaySwitch.css';
+
+export function TutorialReplaySwitch({
+ labelTargetId,
+}: TutorialReplaySwitchProps) {
+ const { setValue: setIsTutorialSeen } = useLocalStorage(
+ IS_TUTORIAL_SEEN_LOCAL_STORAGE_KEY,
+ false
+ );
+
+ return (
+ {
+ setIsTutorialSeen(!isTutorialSeen);
+ }}
+ id={labelTargetId}
+ >
+
+
+ );
+}
+
+interface TutorialReplaySwitchProps {
+ labelTargetId?: string;
+}
diff --git a/frontend/techpick/src/components/tutorialDialog.css.ts b/frontend/techpick/src/components/tutorialDialog.css.ts
new file mode 100644
index 000000000..849af2969
--- /dev/null
+++ b/frontend/techpick/src/components/tutorialDialog.css.ts
@@ -0,0 +1,109 @@
+import { keyframes, style } from '@vanilla-extract/css';
+import { colorVars } from 'techpick-shared';
+
+const contentShow = keyframes({
+ from: {
+ opacity: '0',
+ transform: 'translate(-50%, -48%) scale(0.96)',
+ },
+ to: {
+ opacity: '1',
+ transform: 'translate(-50%, -50%) scale(1)',
+ },
+});
+
+export const overlayStyle = style({
+ position: 'fixed',
+ inset: '0',
+ animation: 'overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
+ backgroundColor: colorVars.sand8,
+ opacity: 0.5,
+});
+
+export const dialogContent = style({
+ position: 'fixed',
+ top: '50%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ width: 'auto',
+ height: 'auto',
+ borderRadius: '8px',
+ boxShadow: `
+ hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
+ hsl(206 22% 7% / 20%) 0px 10px 20px -15px
+ `,
+ padding: '24px',
+ backgroundColor: colorVars.gold4,
+ animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
+});
+
+export const tabListStyle = style({
+ position: 'absolute',
+ top: '0',
+ right: '0px',
+});
+
+export const tabTriggerButtonStyle = style({
+ width: '56px',
+ height: '32px',
+ border: '1px solid',
+ borderColor: colorVars.orange8,
+ borderRadius: '4px',
+ backgroundColor: colorVars.orange1,
+ color: colorVars.primary,
+ cursor: 'pointer',
+ transition: 'background-color 0.3s ease',
+
+ ':hover': {
+ backgroundColor: colorVars.orange3,
+ },
+
+ ':focus': {
+ backgroundColor: colorVars.orange3,
+ },
+
+ selectors: {
+ '&[data-state="open"]': {
+ backgroundColor: colorVars.orange3,
+ },
+ },
+});
+
+export const tabContentDescriptionStyle = style({
+ height: '32px',
+ fontSize: '18px',
+ textDecoration: 'underline',
+ textDecorationColor: colorVars.primary,
+ textUnderlineOffset: '4px',
+});
+
+export const pointTextStyle = style({ color: colorVars.primary });
+
+export const dialogCloseButtonStyle = style({
+ width: '56px',
+ height: '32px',
+ border: '1px solid',
+ borderColor: colorVars.orange8,
+ borderRadius: '4px',
+ backgroundColor: colorVars.orange1,
+ color: colorVars.primary,
+ cursor: 'pointer',
+ transition: 'background-color 0.3s ease',
+
+ ':hover': {
+ backgroundColor: colorVars.orange3,
+ },
+
+ ':focus': {
+ backgroundColor: colorVars.orange3,
+ },
+});
+
+export const tabContentStyle = style({
+ position: 'relative',
+});
+
+export const tabTriggerLayoutStyle = style({ display: 'flex', gap: '8px' });
diff --git a/frontend/techpick/src/components/tutorialReplaySwitch.css.ts b/frontend/techpick/src/components/tutorialReplaySwitch.css.ts
new file mode 100644
index 000000000..d9e9ed274
--- /dev/null
+++ b/frontend/techpick/src/components/tutorialReplaySwitch.css.ts
@@ -0,0 +1,43 @@
+import { style } from '@vanilla-extract/css';
+import { colorVars } from 'techpick-shared';
+
+// 스위치 루트 스타일
+export const switchRoot = style({
+ position: 'relative',
+ flexShrink: '0',
+ width: '42px',
+ height: '25px',
+ borderRadius: '9999px',
+ border: 'none',
+ boxShadow: `0 2px 10px ${colorVars.gold7}`,
+ backgroundColor: colorVars.gold3,
+ WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)',
+ cursor: 'pointer',
+
+ ':focus': {
+ boxShadow: `0 0 0 2px ${colorVars.gold8}`,
+ },
+ selectors: {
+ '&[data-state="checked"]': {
+ backgroundColor: colorVars.gold5,
+ },
+ },
+});
+
+// 스위치 썸 스타일
+export const switchThumb = style({
+ display: 'block',
+ width: '21px',
+ height: '21px',
+ borderRadius: '9999px',
+ boxShadow: `0 2px 2px ${colorVars.gold7}`,
+ backgroundColor: colorVars.gold1,
+ transition: 'transform 100ms',
+ transform: 'translateX(2px)',
+ willChange: 'transform',
+ selectors: {
+ '&[data-state="checked"]': {
+ transform: 'translateX(19px)',
+ },
+ },
+});
diff --git a/frontend/techpick/src/constants/index.ts b/frontend/techpick/src/constants/index.ts
index 381ead73a..054be70fb 100644
--- a/frontend/techpick/src/constants/index.ts
+++ b/frontend/techpick/src/constants/index.ts
@@ -3,4 +3,5 @@ export const UNKNOWN_FOLDER_ID = -9999;
export { ROUTES } from './route';
export { COLOR_LIST } from './colorList';
export { ERROR_MESSAGE_JSON } from './errorMessageJson';
+export { IS_TUTORIAL_SEEN_LOCAL_STORAGE_KEY } from './isTutorialSeenLocalStorageKey';
export { NON_EXIST_FOLDER_ID } from './nonExistFolderId';
diff --git a/frontend/techpick/src/constants/isTutorialSeenLocalStorageKey.ts b/frontend/techpick/src/constants/isTutorialSeenLocalStorageKey.ts
new file mode 100644
index 000000000..6e12e4f74
--- /dev/null
+++ b/frontend/techpick/src/constants/isTutorialSeenLocalStorageKey.ts
@@ -0,0 +1 @@
+export const IS_TUTORIAL_SEEN_LOCAL_STORAGE_KEY = 'isTutorialSeen';
diff --git a/frontend/techpick/src/hooks/index.ts b/frontend/techpick/src/hooks/index.ts
index 8397f7aad..2ce8ca4bc 100644
--- a/frontend/techpick/src/hooks/index.ts
+++ b/frontend/techpick/src/hooks/index.ts
@@ -11,3 +11,4 @@ export { useGetDragOverStyle } from './useGetDragOverStyle';
export { useFetchPickRecordByFolderId } from './useFetchPickRecordByFolderId';
export { useDisclosure } from './useDisclosure';
export { useRecommendPickToFolderDndMonitor } from './useRecommendPickToFolderDndMonitor';
+export { useLocalStorage } from './useLocalStorage';
diff --git a/frontend/techpick/src/hooks/useLocalStorage.ts b/frontend/techpick/src/hooks/useLocalStorage.ts
new file mode 100644
index 000000000..4384e1419
--- /dev/null
+++ b/frontend/techpick/src/hooks/useLocalStorage.ts
@@ -0,0 +1,54 @@
+'use client';
+
+import { useState, useEffect, Dispatch, SetStateAction } from 'react';
+import { getItemFromLocalStorage, setItemToLocalStorage } from '@/utils';
+
+type SetValue = Dispatch>;
+
+/**
+ * @description useLocalStorage의 storedValue가 제대로 로드됐는지 확인하기 위해선
+ * isStoredValueLoad가 true인지 획인해야합니다.
+ *
+ * 이렇게 한 이유는 nextjs의 hydration failed error때문입니다.
+ * @returns
+ */
+export function useLocalStorage(key: string, initialValue: T) {
+ const [storedValue, setStoredValue] = useState(undefined);
+ const [isStoredValueLoad, setIsStoredValueLoad] = useState(false);
+
+ useEffect(() => {
+ try {
+ const item = getItemFromLocalStorage(key);
+ if (item !== null) {
+ setStoredValue(item);
+ } else {
+ setStoredValue(initialValue);
+ }
+ } catch {
+ setStoredValue(initialValue);
+ } finally {
+ setIsStoredValueLoad(true);
+ }
+ }, [key, initialValue]);
+
+ const setValue: SetValue = (value) => {
+ try {
+ const valueToStore =
+ value instanceof Function ? value(storedValue as T) : value;
+ setStoredValue(valueToStore);
+ setItemToLocalStorage(key, valueToStore);
+ } catch {
+ // 에러 처리
+ }
+ };
+
+ if (!isStoredValueLoad) {
+ return { isStoredValueLoad, setValue, storedValue: null };
+ }
+
+ return {
+ storedValue,
+ setValue,
+ isStoredValueLoad,
+ };
+}
diff --git a/frontend/techpick/src/utils/getItemFromLocalStorage.ts b/frontend/techpick/src/utils/getItemFromLocalStorage.ts
new file mode 100644
index 000000000..ce56501a1
--- /dev/null
+++ b/frontend/techpick/src/utils/getItemFromLocalStorage.ts
@@ -0,0 +1,10 @@
+export const getItemFromLocalStorage = (key: string) => {
+ const item = localStorage.getItem(key);
+
+ if (!item) {
+ return undefined;
+ }
+
+ const parseItem: ItemType | undefined = JSON.parse(item);
+ return parseItem;
+};
diff --git a/frontend/techpick/src/utils/index.ts b/frontend/techpick/src/utils/index.ts
index cd5bbd4c7..992706ee6 100644
--- a/frontend/techpick/src/utils/index.ts
+++ b/frontend/techpick/src/utils/index.ts
@@ -17,3 +17,5 @@ export { getFolderLinkByType } from './getFolderLinkByType';
export { getOrderedPickListByFolderId } from './getOrderedPickListByFolderId';
export { createSearchSelectOptions } from './createSearchSelectOptions';
export { isRecommendPickDraggableObject } from './isRecommendPickDraggableObject';
+export { getItemFromLocalStorage } from './getItemFromLocalStorage';
+export { setItemToLocalStorage } from './setItemToLocalStorage';
diff --git a/frontend/techpick/src/utils/setItemToLocalStorage.ts b/frontend/techpick/src/utils/setItemToLocalStorage.ts
new file mode 100644
index 000000000..2208d1f06
--- /dev/null
+++ b/frontend/techpick/src/utils/setItemToLocalStorage.ts
@@ -0,0 +1,4 @@
+export const setItemToLocalStorage = (key: string, item: unknown) => {
+ const jsonItem = JSON.stringify(item);
+ localStorage.setItem(key, jsonItem);
+};
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index d7233370e..9254d4570 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -4070,6 +4070,57 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-switch@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-switch@npm:1.1.1"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.0"
+ "@radix-ui/react-compose-refs": "npm:1.1.0"
+ "@radix-ui/react-context": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.0.0"
+ "@radix-ui/react-use-controllable-state": "npm:1.1.0"
+ "@radix-ui/react-use-previous": "npm:1.1.0"
+ "@radix-ui/react-use-size": "npm:1.1.0"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/8b61aa3bf80d3a2037d67495cf5de9e1ffc0d0843edc0cde5adc1ff1a9b99b0a6b63a85951c79769ab5a44d484611d90dc85933a86d71f28028caa53d8db177b
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-tabs@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-tabs@npm:1.1.1"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.0"
+ "@radix-ui/react-context": "npm:1.1.1"
+ "@radix-ui/react-direction": "npm:1.1.0"
+ "@radix-ui/react-id": "npm:1.1.0"
+ "@radix-ui/react-presence": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.0.0"
+ "@radix-ui/react-roving-focus": "npm:1.1.0"
+ "@radix-ui/react-use-controllable-state": "npm:1.1.0"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/86fa6beda5ac5fbc6cede483e198641fbba0b1e4ad30db3488fbfefdf460ca4e35d765f5b22f73ded1849252b2432cfa755783218f282721462f90f2ad1adf30
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-use-callback-ref@npm:1.1.0":
version: 1.1.0
resolution: "@radix-ui/react-use-callback-ref@npm:1.1.0"
@@ -15493,6 +15544,8 @@ __metadata:
"@radix-ui/react-select": "npm:^2.1.2"
"@radix-ui/react-separator": "npm:^1.1.0"
"@radix-ui/react-slot": "npm:^1.1.0"
+ "@radix-ui/react-switch": "npm:^1.1.1"
+ "@radix-ui/react-tabs": "npm:^1.1.1"
"@radix-ui/react-visually-hidden": "npm:^1.1.0"
"@sentry/nextjs": "npm:8"
"@storybook/addon-essentials": "npm:^8.2.9"