-
+
);
}
diff --git a/apps/landing/src/app/(public)/_components/theme-toggle.tsx b/apps/landing/src/app/(public)/_components/theme-toggle.tsx
index 6eac564..f6f0feb 100644
--- a/apps/landing/src/app/(public)/_components/theme-toggle.tsx
+++ b/apps/landing/src/app/(public)/_components/theme-toggle.tsx
@@ -1,8 +1,9 @@
'use client';
-import { Button, MoonIcon, SunIcon } from '@components';
+import { Button } from '@components';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
+import { LuMoon, LuSun } from 'react-icons/lu';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
@@ -28,9 +29,9 @@ export function ThemeToggle() {
className="focus-visible:ring-0 cursor-pointer"
>
{theme === 'dark' ? (
-
+
) : (
-
+
)}
Toggle theme
diff --git a/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx b/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx
new file mode 100644
index 0000000..e39d837
--- /dev/null
+++ b/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import { RequestFeaturePopup } from './request-feature-popup';
+
+export function FeatureRequestCTA() {
+ return (
+
+
+
+ Punya Ide Bagus untuk IMPHNEN?
+
+
+ Bagikan ide fitur yang kamu inginkan dan vote untuk membuatnya nyata!
+
+
+
+
+ );
+}
diff --git a/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx b/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx
new file mode 100644
index 0000000..fdc015f
--- /dev/null
+++ b/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx
@@ -0,0 +1,239 @@
+'use client';
+
+import { Button, Card, CardContent, CardHeader, CardTitle } from '@components';
+import { AnimatePresence, motion } from 'framer-motion';
+import { useState } from 'react';
+import { BiUpvote } from 'react-icons/bi';
+import { FiCheckCircle } from 'react-icons/fi';
+import { MdOutlineOpenInNew } from 'react-icons/md';
+
+export default function ProjectsVote() {
+ const [upcomingItems, setUpcomingItems] = useState([
+ {
+ title: 'IMPHNEN Project Showcase',
+ description: 'Showcase projectmu ke member lain dan dapatkan feedback',
+ votes: 42,
+ voted: false,
+ },
+ {
+ title: 'IMPHNEN Meme Generator',
+ description: 'Bikin meme kocak kapanpun dengan mudah',
+ votes: 42,
+ voted: false,
+ },
+ ]);
+
+ const inProgressItems = [
+ {
+ title: 'IMPHNEN Twibbon',
+ description: 'Buat Twibbon kece untuk profile media sosialmu',
+ },
+ {
+ title: 'IMPHNEN Certificate',
+ description: 'Cetak sertifikat keren secara instan untuk anggota IMPHNEN',
+ },
+ ];
+
+ const completedItems = [
+ {
+ title: 'IMPHNEN List Event',
+ description:
+ 'Koleksi daftar event dan kolaborasi seru yang bisa kamu ikuti',
+ },
+ {
+ title: 'IMPHNEN Testimoni',
+ description: 'Berikan testimonial gokil buat komunitas IMPHNEN',
+ },
+ {
+ title: 'IMPHNEN Roadmap by Vote',
+ description: 'Usulkan ide fitur seru dan ajak anggota lain buat voting',
+ },
+ ];
+
+ const handleVote = (index: number) => {
+ const newItems = [...upcomingItems];
+ newItems[index] = {
+ ...newItems[index],
+ votes: newItems[index].voted
+ ? newItems[index].votes - 1
+ : newItems[index].votes + 1,
+ voted: !newItems[index].voted,
+ };
+ setUpcomingItems(newItems);
+ };
+
+ const containerVariants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.05,
+ },
+ },
+ };
+
+ const itemVariants = {
+ hidden: { opacity: 0, y: 10 },
+ visible: { opacity: 1, y: 0, transition: { duration: 0.2 } },
+ };
+
+ return (
+
+ {/* Vote Now Column */}
+
+
+
+
+
+ {upcomingItems.map((item, index) => (
+
+
+
+
+ {item.title}
+
+
+
+
+ {item.description}
+
+
+
handleVote(index)}
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
+ item.voted
+ ? 'bg-primary-500 text-white hover:bg-primary-600'
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
+ }`}
+ >
+
+
+
+ {item.voted ? 'Voted' : 'Vote'}
+
+
+
+
+ {item.votes}
+
+
+
+
+
+
+ ))}
+
+
+
+
+ {/* In Progress Column */}
+
+
+
+ 🧑🔧
+
+
In Progress
+
+
+
+
+ {inProgressItems.map((item, index) => (
+
+
+
+
+ {item.title}
+
+
+
+ {item.description}
+
+
+
+ {index === 0 ? 'Development started' : 'In development'}
+
+
+
+
+
+ ))}
+
+
+
+
+ {/* Completed Column */}
+
+
+
+
+
+ {completedItems.map((item, index) => (
+
+
+
+
+ {item.title}
+
+
+
+
+ {item.description}
+
+
+
+
+ Implemented
+
+
+ Coba sekarang
+
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx b/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx
new file mode 100644
index 0000000..13dbfdf
--- /dev/null
+++ b/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx
@@ -0,0 +1,120 @@
+'use client';
+
+import * as React from 'react';
+
+import { useMediaQuery } from '@/hooks/use-media-query';
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+ Input,
+ Label,
+ Textarea,
+} from '@components';
+import { cn } from '@utils';
+
+export function RequestFeaturePopup() {
+ const [open, setOpen] = React.useState(false);
+ const isDesktop = useMediaQuery('(min-width: 768px)');
+
+ if (isDesktop) {
+ return (
+
+
+ Request Fitur
+
+
+
+ Request Fitur
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Request Fitur
+
+
+
+ Request Fitur
+
+
+
+
+ Cancel
+
+
+
+
+ );
+}
+
+interface ProfileFormProps extends React.ComponentProps<'form'> {
+ showFooterButton?: boolean;
+}
+
+function ProfileForm({
+ className,
+ showFooterButton = false,
+}: ProfileFormProps) {
+ const [description, setDescription] = React.useState('');
+ const maxDescriptionLength = 10000;
+
+ return (
+
+ );
+}
diff --git a/apps/landing/src/app/(public)/roadmap/page.tsx b/apps/landing/src/app/(public)/roadmap/page.tsx
new file mode 100644
index 0000000..dc4a82f
--- /dev/null
+++ b/apps/landing/src/app/(public)/roadmap/page.tsx
@@ -0,0 +1,11 @@
+import { FeatureRequestCTA } from './_components/feature-request-cta';
+import ProjectsVote from './_components/projects-vote';
+
+export default function Page() {
+ return (
+
+ );
+}
diff --git a/apps/landing/src/app/(public)/roadmaps/page.tsx b/apps/landing/src/app/(public)/roadmaps/page.tsx
deleted file mode 100644
index 7d1e0b7..0000000
--- a/apps/landing/src/app/(public)/roadmaps/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Page() {
- return <>>;
-}
diff --git a/apps/landing/src/app/(public)/articles/page.tsx b/apps/landing/src/app/(public)/teams/page.tsx
similarity index 100%
rename from apps/landing/src/app/(public)/articles/page.tsx
rename to apps/landing/src/app/(public)/teams/page.tsx
diff --git a/apps/landing/src/data/navigations.json b/apps/landing/src/data/navigations.json
index 2db1c6d..012c973 100644
--- a/apps/landing/src/data/navigations.json
+++ b/apps/landing/src/data/navigations.json
@@ -13,10 +13,10 @@
},
{
"title": "Roadmap",
- "link": "/roadmaps"
+ "link": "/roadmap"
},
{
- "title": "Artikel",
- "link": "/articles"
+ "title": "Team",
+ "link": "/teams"
}
]
diff --git a/apps/landing/src/hooks/use-media-query.ts b/apps/landing/src/hooks/use-media-query.ts
new file mode 100644
index 0000000..f6205cc
--- /dev/null
+++ b/apps/landing/src/hooks/use-media-query.ts
@@ -0,0 +1,33 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+export function useMediaQuery(query: string): boolean {
+ // 1) Always default to `false`—this guarantees SSR and the very first client render match.
+ const [matches, setMatches] = useState(false);
+
+ useEffect(() => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ const mediaQueryList = window.matchMedia(query);
+
+ // 2) On mount, immediately set to the real match value (true or false).
+ setMatches(mediaQueryList.matches);
+
+ // 3) Subscribe to changes.
+ const listener = (event: MediaQueryListEvent) => {
+ setMatches(event.matches);
+ };
+ mediaQueryList.addEventListener('change', listener);
+
+ return () => {
+ mediaQueryList.removeEventListener('change', listener);
+ };
+ }, [query]);
+
+ return matches;
+}
+
+export default useMediaQuery;
diff --git a/docker-compose-landing.yml b/docker-compose-landing.yml
new file mode 100644
index 0000000..34feeca
--- /dev/null
+++ b/docker-compose-landing.yml
@@ -0,0 +1,10 @@
+version: '3.8'
+services:
+ landing:
+ build:
+ context: .
+ dockerfile: docker/landing.Dockerfile
+ ports:
+ - "3000:3000"
+ restart: unless-stopped
+ container_name: landing
diff --git a/docker/backoffice.Dockerfile b/docker/backoffice.Dockerfile
new file mode 100644
index 0000000..ef68f85
--- /dev/null
+++ b/docker/backoffice.Dockerfile
@@ -0,0 +1,16 @@
+FROM node:22-alpine AS deps
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm install
+
+FROM node:22-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN npm run backoffice:build
+
+FROM nginx:alpine AS runner
+RUN rm -rf /usr/share/nginx/html/*
+COPY --from=builder /app/dist/apps/backoffice /usr/share/nginx/html
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/docker/dimentorin.Dockerfile b/docker/dimentorin.Dockerfile
new file mode 100644
index 0000000..7c832d3
--- /dev/null
+++ b/docker/dimentorin.Dockerfile
@@ -0,0 +1,16 @@
+FROM node:22-alpine AS deps
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm install
+
+FROM node:22-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN npm run dimentorin:build
+
+FROM nginx:alpine AS runner
+RUN rm -rf /usr/share/nginx/html/*
+COPY --from=builder /app/dist/apps/dimentorin /usr/share/nginx/html
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/docker/gacha.Dockerfile b/docker/gacha.Dockerfile
new file mode 100644
index 0000000..0531586
--- /dev/null
+++ b/docker/gacha.Dockerfile
@@ -0,0 +1,16 @@
+FROM node:22-alpine AS deps
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm install
+
+FROM node:22-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN npm run gacha:build
+
+FROM nginx:alpine AS runner
+RUN rm -rf /usr/share/nginx/html/*
+COPY --from=builder /app/dist/apps/gacha /usr/share/nginx/html
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/docker/landing.Dockerfile b/docker/landing.Dockerfile
new file mode 100644
index 0000000..462c476
--- /dev/null
+++ b/docker/landing.Dockerfile
@@ -0,0 +1,17 @@
+FROM node:22-alpine AS builder
+WORKDIR /app
+
+COPY . .
+RUN npm install
+RUN npm run landing:build
+
+FROM node:22-alpine AS runner
+WORKDIR /app
+
+COPY --from=builder /app/dist/apps/landing/.next/standalone .
+COPY --from=builder /app/dist/apps/landing/public apps/landing/public
+COPY --from=builder /app/dist/apps/landing/.next/static dist/apps/landing/.next/static
+
+EXPOSE 3000
+
+ENTRYPOINT ["node", "apps/landing/server.js"]
\ No newline at end of file
diff --git a/libs/shadcn-ui/package-lock.json b/libs/shadcn-ui/package-lock.json
index 50115cc..a22185a 100644
--- a/libs/shadcn-ui/package-lock.json
+++ b/libs/shadcn-ui/package-lock.json
@@ -19,6 +19,7 @@
"react-icons": "^5.5.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
+ "vaul": "^1.1.2",
"zod": "^3.25.28"
},
"devDependencies": {
@@ -462,6 +463,12 @@
"react-hook-form": "^7.55.0"
}
},
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -477,6 +484,142 @@
}
}
},
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
+ "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+ "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-label": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
@@ -500,6 +643,54 @@
}
}
},
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
@@ -541,6 +732,91 @@
}
}
},
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz",
@@ -1088,6 +1364,18 @@
"license": "MIT",
"peer": true
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -1118,6 +1406,12 @@
"node": ">=8"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -1202,6 +1496,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1570,6 +1873,75 @@
"react": "*"
}
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.0.tgz",
+ "integrity": "sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/rollup": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
@@ -1669,6 +2041,12 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/tw-animate-css": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.8.tgz",
@@ -1679,6 +2057,62 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vaul": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
+ "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/vite": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
diff --git a/libs/shadcn-ui/package.json b/libs/shadcn-ui/package.json
index 7616d4a..b800b77 100644
--- a/libs/shadcn-ui/package.json
+++ b/libs/shadcn-ui/package.json
@@ -21,6 +21,7 @@
"react-icons": "^5.5.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
+ "vaul": "^1.1.2",
"zod": "^3.25.28"
},
"devDependencies": {
diff --git a/libs/shadcn-ui/src/atoms/dialog.tsx b/libs/shadcn-ui/src/atoms/dialog.tsx
new file mode 100644
index 0000000..2743c62
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/dialog.tsx
@@ -0,0 +1,142 @@
+'use client';
+
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import * as React from 'react';
+import { LuX } from 'react-icons/lu';
+import { cn } from '../lib';
+
+function Dialog({
+ ...props
+}: React.ComponentProps
) {
+ return ;
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean;
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ );
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+};
diff --git a/libs/shadcn-ui/src/atoms/drawer.tsx b/libs/shadcn-ui/src/atoms/drawer.tsx
new file mode 100644
index 0000000..0250d21
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/drawer.tsx
@@ -0,0 +1,131 @@
+'use client';
+
+import * as React from 'react';
+import { Drawer as DrawerPrimitive } from 'vaul';
+import { cn } from '../lib';
+
+function Drawer({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DrawerTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DrawerPortal({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DrawerClose({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DrawerOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DrawerContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+ );
+}
+
+function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DrawerTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DrawerDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerOverlay,
+ DrawerPortal,
+ DrawerTitle,
+ DrawerTrigger,
+};
diff --git a/libs/shadcn-ui/src/atoms/icons.tsx b/libs/shadcn-ui/src/atoms/icons.tsx
deleted file mode 100644
index 9843457..0000000
--- a/libs/shadcn-ui/src/atoms/icons.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import {
- LuArrowDown as ArrowDownIcon,
- LuBookOpen as BookOpenIcon,
- LuCode as CodeIcon,
- LuFacebook as FacebookIcon,
- LuInstagram as InstagramIcon,
- LuLaptop as LaptopIcon,
- LuMenu as MenuIcon,
- LuMessageCircle as MessageCircleIcon,
- LuMoon as MoonIcon,
- LuQuote as QuoteIcon,
- LuShare2 as Share2Icon,
- LuSparkles as SparklesIcon,
- LuSun as SunIcon,
- LuUsers as UsersIcon,
- LuVideo as VideoIcon,
- LuX as XIcon,
-} from 'react-icons/lu';
-
-export {
- ArrowDownIcon,
- BookOpenIcon,
- CodeIcon,
- FacebookIcon,
- InstagramIcon,
- LaptopIcon,
- MenuIcon,
- MessageCircleIcon,
- MoonIcon,
- QuoteIcon,
- Share2Icon,
- SparklesIcon,
- SunIcon,
- UsersIcon,
- VideoIcon,
- XIcon,
-};
diff --git a/libs/shadcn-ui/src/atoms/index.ts b/libs/shadcn-ui/src/atoms/index.ts
index 82b2d15..4bb8f35 100644
--- a/libs/shadcn-ui/src/atoms/index.ts
+++ b/libs/shadcn-ui/src/atoms/index.ts
@@ -1,6 +1,8 @@
export * from './button';
export * from './card';
+export * from './dialog';
+export * from './drawer';
export * from './form';
-export * from './icons';
export * from './input';
export * from './label';
+export * from './textarea';
diff --git a/libs/shadcn-ui/src/atoms/textarea.tsx b/libs/shadcn-ui/src/atoms/textarea.tsx
new file mode 100644
index 0000000..d189fbf
--- /dev/null
+++ b/libs/shadcn-ui/src/atoms/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+
+import { cn } from '../lib';
+
+function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
+ return (
+
+ );
+}
+
+export { Textarea };
diff --git a/libs/ui/src/molecules/accordion/accordion.tsx b/libs/ui/src/molecules/accordion/accordion.tsx
new file mode 100644
index 0000000..5064814
--- /dev/null
+++ b/libs/ui/src/molecules/accordion/accordion.tsx
@@ -0,0 +1,53 @@
+import { cn, Show } from "@imphnen-frontend-service/utils"
+import { DetailedHTMLProps, FC, HTMLAttributes, useState } from "react"
+import { AnimatePresence, motion, Variants } from "framer-motion"
+import { MinusOutlined, PlusOutlined } from "@ant-design/icons"
+
+export type AccordionOptions = {
+ title: string
+ description: string
+}
+
+export type AccordionProps = DetailedHTMLProps<
+ HTMLAttributes,
+ HTMLDivElement
+> & AccordionOptions & {
+ titleClassName?: string
+ contentClassName?: string
+}
+
+export const Accordion: FC = ({ title, description, className, titleClassName, contentClassName, ...rest }) => {
+ const [expand, setExpand] = useState(false)
+
+ const variants: Variants = {
+ expand: { opacity: 1, y: 0, marginTop: 16, height: 'auto' },
+ collapse: { opacity: 0, y: -10, marginTop: 0, height: 0 },
+ }
+
+ return (
+
+
setExpand(prev => !prev)}>
+
{title}
+
+ }>
+
+
+
+
+
+ {expand && (
+
+ {description}
+
+ )}
+
+
+ )
+}
diff --git a/libs/ui/src/molecules/accordion/index.ts b/libs/ui/src/molecules/accordion/index.ts
new file mode 100644
index 0000000..ce956fe
--- /dev/null
+++ b/libs/ui/src/molecules/accordion/index.ts
@@ -0,0 +1 @@
+export * from './accordion';
diff --git a/libs/ui/src/molecules/index.ts b/libs/ui/src/molecules/index.ts
index 1bc94a6..ad99b97 100644
--- a/libs/ui/src/molecules/index.ts
+++ b/libs/ui/src/molecules/index.ts
@@ -4,3 +4,4 @@ export * from './input-field';
export * from './pagination';
export * from './modal/modal';
export * from './stepper';
+export * from './accordion';
\ No newline at end of file
diff --git a/libs/utils/src/index.ts b/libs/utils/src/index.ts
index 722f7b8..93a8043 100644
--- a/libs/utils/src/index.ts
+++ b/libs/utils/src/index.ts
@@ -6,3 +6,4 @@ export * from './local-storage';
export * from './cookies';
export * from './session';
export * from './constants';
+export * from './logic';
diff --git a/libs/utils/src/logic/for.tsx b/libs/utils/src/logic/for.tsx
new file mode 100644
index 0000000..abdf388
--- /dev/null
+++ b/libs/utils/src/logic/for.tsx
@@ -0,0 +1,27 @@
+export interface ForProps {
+ data: readonly T[]
+ children: (item: T, index: number) => U | null
+ fallback?: React.ReactNode | null
+}
+
+/**
+ * A functional component that renders a list of children components from a given
+ * array of data.
+ *
+ * @param data - The array of data to render
+ * @param children - A function that takes the current item and index and returns
+ * the child component to render
+ * @param fallback - An optional fallback component to render when the data is empty
+ *
+ * @returns An array of rendered child components if the data is not empty, otherwise
+ * the fallback component if it is provided, null otherwise
+ */
+export function For({
+ data,
+ children,
+ fallback = null
+}: ForProps): (U | null)[] | React.ReactNode | null {
+ if (!Array.isArray(data) || !data?.length) return fallback
+
+ return data.map((item, idx) => children(item, idx))
+}
diff --git a/libs/utils/src/logic/index.ts b/libs/utils/src/logic/index.ts
new file mode 100644
index 0000000..c3c1579
--- /dev/null
+++ b/libs/utils/src/logic/index.ts
@@ -0,0 +1,2 @@
+export * from './for'
+export * from './show'
diff --git a/libs/utils/src/logic/show.tsx b/libs/utils/src/logic/show.tsx
new file mode 100644
index 0000000..c8305cc
--- /dev/null
+++ b/libs/utils/src/logic/show.tsx
@@ -0,0 +1,19 @@
+export interface ShowProps {
+ condition: boolean
+ children: React.ReactNode
+ fallback?: React.ReactNode | null
+}
+
+/**
+ * Conditionally renders the `children` component if `condition` is true. If
+ * `condition` is false, renders the `fallback` component instead.
+ *
+ * If `fallback` is `null`, renders nothing.
+ *
+ * @param condition - The condition to check
+ * @param children - The component to render if `condition` is true
+ * @param fallback - The component to render if `condition` is false
+ */
+export function Show({ condition, children, fallback = null }: ShowProps) {
+ return condition ? children : fallback
+}
diff --git a/package-lock.json b/package-lock.json
index b1a656b..1b89f05 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,6 +37,7 @@
"sharp": "^0.34.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.0.2",
+ "vaul": "^1.1.2",
"zod": "^3.24.2",
"zustand": "^5.0.5"
},
@@ -7591,6 +7592,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -7606,6 +7613,142 @@
}
}
},
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
+ "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+ "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-label": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
@@ -7629,6 +7772,54 @@
}
}
},
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
@@ -7670,6 +7861,91 @@
}
}
},
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rc-component/util": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.2.1.tgz",
@@ -13378,6 +13654,18 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -16863,6 +17151,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/detect-port": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz",
@@ -19589,6 +19883,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -28359,6 +28662,53 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.0.tgz",
+ "integrity": "sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-router": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz",
@@ -28407,6 +28757,28 @@
"node": ">=18"
}
},
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -33090,6 +33462,49 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
@@ -33203,6 +33618,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/vaul": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
+ "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/verdaccio": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-6.1.2.tgz",
diff --git a/package.json b/package.json
index c35b39d..c306c58 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"sharp": "^0.34.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.0.2",
+ "vaul": "^1.1.2",
"zod": "^3.24.2",
"zustand": "^5.0.5"
},