diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0d3f62d9..907183e6 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -113,3 +113,40 @@ jobs:
access: public
package: packages/design-system/package.json
if: startsWith(github.ref, 'refs/tags/')
+
+ build-v3:
+ name: Build and Publish
+ runs-on: ubuntu-latest
+ needs: [check-ts, test]
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v3
+
+ - name: Use Node 20
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20.x
+
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 9.7.0
+ run_install: true
+
+ - name: Build
+ working-directory: packages/design-system-v3
+ run: pnpm run build
+
+ - uses: JS-DevTools/npm-publish@v3
+ with:
+ token: ${{ secrets.NPM_TOKEN }}
+ access: public
+ package: packages/design-system-v3/package.json
+ if: startsWith(github.ref, 'refs/tags/')
+
+ - uses: JS-DevTools/npm-publish@v3
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ registry: https://npm.pkg.github.com
+ access: public
+ package: packages/design-system-v3/package.json
+ if: startsWith(github.ref, 'refs/tags/')
diff --git a/packages/design-system-v3/.eslintignore b/packages/design-system-v3/.eslintignore
new file mode 100644
index 00000000..a7c47537
--- /dev/null
+++ b/packages/design-system-v3/.eslintignore
@@ -0,0 +1 @@
+types/
diff --git a/packages/design-system-v3/.eslintrc.cjs b/packages/design-system-v3/.eslintrc.cjs
new file mode 100644
index 00000000..1f6d4b46
--- /dev/null
+++ b/packages/design-system-v3/.eslintrc.cjs
@@ -0,0 +1,15 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': 'off',
+ },
+}
diff --git a/packages/design-system-v3/.gitattributes b/packages/design-system-v3/.gitattributes
new file mode 100644
index 00000000..fcadb2cf
--- /dev/null
+++ b/packages/design-system-v3/.gitattributes
@@ -0,0 +1 @@
+* text eol=lf
diff --git a/packages/design-system-v3/.gitignore b/packages/design-system-v3/.gitignore
new file mode 100644
index 00000000..b4ca1ed5
--- /dev/null
+++ b/packages/design-system-v3/.gitignore
@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+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/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.cache/
+build/
+.vercel/
diff --git a/packages/design-system-v3/.ladle/components.tsx b/packages/design-system-v3/.ladle/components.tsx
new file mode 100644
index 00000000..e366257e
--- /dev/null
+++ b/packages/design-system-v3/.ladle/components.tsx
@@ -0,0 +1,10 @@
+// organize-imports-ignore
+import type { GlobalProvider } from '@ladle/react'
+import React from 'react'
+
+import 'tailwindcss/src/css/preflight.css'
+import 'tailwindcss/tailwind.css'
+
+export const Provider: GlobalProvider = ({ children, globalState }) => (
+ <>{children}>
+)
diff --git a/packages/design-system-v3/.ladle/head.html b/packages/design-system-v3/.ladle/head.html
new file mode 100644
index 00000000..f0391a58
--- /dev/null
+++ b/packages/design-system-v3/.ladle/head.html
@@ -0,0 +1,79 @@
+
+
+
+
+
diff --git a/packages/design-system-v3/.prettierignore b/packages/design-system-v3/.prettierignore
new file mode 100644
index 00000000..6bc14429
--- /dev/null
+++ b/packages/design-system-v3/.prettierignore
@@ -0,0 +1,2 @@
+dist/
+types/
diff --git a/packages/design-system-v3/.prettierrc.cjs b/packages/design-system-v3/.prettierrc.cjs
new file mode 100644
index 00000000..94293e5d
--- /dev/null
+++ b/packages/design-system-v3/.prettierrc.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ semi: false,
+ singleQuote: true,
+ trailingComma: 'es5',
+ plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],
+}
diff --git a/packages/design-system-v3/README.md b/packages/design-system-v3/README.md
new file mode 100644
index 00000000..e1cdc89d
--- /dev/null
+++ b/packages/design-system-v3/README.md
@@ -0,0 +1,30 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
+
+- Configure the top-level `parserOptions` property like this:
+
+```js
+export default {
+ // other rules...
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: __dirname,
+ },
+}
+```
+
+- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
+- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
+- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
diff --git a/packages/design-system-v3/components.json b/packages/design-system-v3/components.json
new file mode 100644
index 00000000..4797f70b
--- /dev/null
+++ b/packages/design-system-v3/components.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "src/tailwind.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/ui",
+ "utils": "@/lib/utils",
+ "ui": "@/ui"
+ }
+}
diff --git a/packages/design-system-v3/package.json b/packages/design-system-v3/package.json
new file mode 100644
index 00000000..f17699f8
--- /dev/null
+++ b/packages/design-system-v3/package.json
@@ -0,0 +1,112 @@
+{
+ "name": "@uzh-df/design-system",
+ "version": "3.0.0-alpha.25",
+ "main": "dist/index.js",
+ "files": [
+ "dist",
+ "src"
+ ],
+ "dependencies": {
+ "@hookform/resolvers": "3.9.0",
+ "@radix-ui/react-accordion": "1.2.0",
+ "@radix-ui/react-alert-dialog": "1.1.1",
+ "@radix-ui/react-aspect-ratio": "1.1.0",
+ "@radix-ui/react-avatar": "1.1.0",
+ "@radix-ui/react-checkbox": "1.1.1",
+ "@radix-ui/react-collapsible": "1.0.3",
+ "@radix-ui/react-context-menu": "2.2.1",
+ "@radix-ui/react-dialog": "1.0.4",
+ "@radix-ui/react-dropdown-menu": "2.0.5",
+ "@radix-ui/react-hover-card": "1.1.1",
+ "@radix-ui/react-label": "2.1.0",
+ "@radix-ui/react-menubar": "1.1.1",
+ "@radix-ui/react-navigation-menu": "1.2.0",
+ "@radix-ui/react-popover": "1.1.1",
+ "@radix-ui/react-progress": "1.0.3",
+ "@radix-ui/react-radio-group": "1.2.0",
+ "@radix-ui/react-scroll-area": "1.1.0",
+ "@radix-ui/react-select": "1.2.2",
+ "@radix-ui/react-separator": "1.1.0",
+ "@radix-ui/react-slider": "1.1.2",
+ "@radix-ui/react-slot": "1.1.0",
+ "@radix-ui/react-switch": "1.0.3",
+ "@radix-ui/react-tabs": "1.0.4",
+ "@radix-ui/react-toast": "1.1.4",
+ "@radix-ui/react-toggle": "1.1.0",
+ "@radix-ui/react-toggle-group": "1.1.0",
+ "@radix-ui/react-tooltip": "1.0.6",
+ "cmdk": "1.0.0",
+ "date-fns": "3.6.0",
+ "embla-carousel-react": "8.1.8",
+ "input-otp": "1.2.4",
+ "lucide-react": "0.438.0",
+ "next-themes": "0.3.0",
+ "react-day-picker": "8.10.1",
+ "react-hook-form": "7.52.2",
+ "react-resizable-panels": "2.0.22",
+ "recharts": "2.12.7",
+ "sonner": "1.5.0",
+ "vaul": "0.9.1",
+ "zod": "3.23.8"
+ },
+ "devDependencies": {
+ "@ladle/react": "4.1.0",
+ "@tailwindcss/aspect-ratio": "0.4.2",
+ "@tailwindcss/forms": "0.5.7",
+ "@tailwindcss/typography": "0.5.13",
+ "@types/node": "^20.14.14",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@typescript-eslint/eslint-plugin": "7.15.0",
+ "@typescript-eslint/parser": "7.15.0",
+ "@vitejs/plugin-react-swc": "3.7.0",
+ "autoprefixer": "10.4.20",
+ "eslint": "8.57.0",
+ "eslint-plugin-react-hooks": "4.6.2",
+ "eslint-plugin-react-refresh": "0.4.9",
+ "npm-run-all": "4.1.5",
+ "postcss": "8.4.41",
+ "prettier": "3.3.3",
+ "prettier-plugin-organize-imports": "4.0.0",
+ "prettier-plugin-tailwindcss": "0.6.5",
+ "tailwindcss": "3.4.10",
+ "tailwindcss-animate": "1.0.7",
+ "tailwindcss-radix": "3.0.4",
+ "tsx": "4.16.5",
+ "typescript": "5.4.2",
+ "vite": "5.4.0",
+ "vite-plugin-dts": "4.0.1"
+ },
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "^6.6.0",
+ "@fortawesome/free-regular-svg-icons": "^6.6.0",
+ "@fortawesome/free-solid-svg-icons": "^6.6.0",
+ "@fortawesome/react-fontawesome": "^0.2.2",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "postcss": "^8.4.41",
+ "postcss-import": "^16.1.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "tailwind-merge": "^2.4.0"
+ },
+ "scripts": {
+ "build": "run-s build:tsc build:vite build:copy",
+ "build:copy": "tsx src/copy.ts",
+ "build:ladle": "ladle build",
+ "build:tsc": "tsc -b",
+ "build:vite": "vite build",
+ "dev": "ladle serve",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "ladle preview"
+ },
+ "engines": {
+ "node": "=20"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "type": "module"
+}
diff --git a/packages/design-system-v3/postcss.config.js b/packages/design-system-v3/postcss.config.js
new file mode 100644
index 00000000..a8c52eea
--- /dev/null
+++ b/packages/design-system-v3/postcss.config.js
@@ -0,0 +1,7 @@
+export default {
+ plugins: {
+ 'postcss-import': {},
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/packages/design-system-v3/src/constants.ts b/packages/design-system-v3/src/constants.ts
new file mode 100644
index 00000000..2cb3bd6d
--- /dev/null
+++ b/packages/design-system-v3/src/constants.ts
@@ -0,0 +1,154 @@
+import { fontFamily } from 'tailwindcss/defaultTheme.js'
+
+export const TailwindAnimations = {
+ keyframes: {
+ 'enter-from-right': {
+ '0%': { transform: 'translateX(200px)', opacity: 0 },
+ '100%': { transform: 'translateX(0)', opacity: 1 },
+ },
+ 'enter-from-left': {
+ '0%': { transform: 'translateX(-200px)', opacity: 0 },
+ '100%': { transform: 'translateX(0)', opacity: 1 },
+ },
+ 'exit-to-right': {
+ '0%': { transform: 'translateX(0)', opacity: 1 },
+ '100%': { transform: 'translateX(200px)', opacity: 0 },
+ },
+ 'exit-to-left': {
+ '0%': { transform: 'translateX(0)', opacity: 1 },
+ '100%': { transform: 'translateX(-200px)', opacity: 0 },
+ },
+ 'scale-in-content': {
+ '0%': { transform: 'rotateX(-30deg) scale(0.9)', opacity: 0 },
+ '100%': { transform: 'rotateX(0deg) scale(1)', opacity: 1 },
+ },
+ 'scale-out-content': {
+ '0%': { transform: 'rotateX(0deg) scale(1)', opacity: 1 },
+ '100%': { transform: 'rotateX(-10deg) scale(0.95)', opacity: 0 },
+ },
+ 'fade-in': {
+ '0%': { opacity: 0 },
+ '100%': { opacity: 1 },
+ },
+ 'fade-out': {
+ '0%': { opacity: 1 },
+ '100%': { opacity: 0 },
+ },
+ },
+ animation: {
+ 'enter-from-right': 'enter-from-right 0.25s ease',
+ 'enter-from-left': 'enter-from-left 0.25s ease',
+ 'exit-to-right': 'exit-to-right 0.25s ease',
+ 'exit-to-left': 'exit-to-left 0.25s ease',
+ 'scale-in-content': 'scale-in-content 0.2s ease',
+ 'scale-out-content': 'scale-out-content 0.2s ease',
+ 'fade-in': 'fade-in 0.2s ease',
+ 'fade-out': 'fade-out 0.2s ease',
+ },
+}
+
+export const TailwindColorsUZH = {
+ 'uzh-blue-100': '#0028a5',
+ 'uzh-blue-80': '#3353b7',
+ 'uzh-blue-60': '#667ec9',
+ 'uzh-blue-40': '#99a9db',
+ 'uzh-blue-20': '#ccd4ed',
+ 'uzh-grey-100': '#a3adb7',
+ 'uzh-grey-80': '#b5bdc5',
+ 'uzh-grey-60': '#c8ced4',
+ 'uzh-grey-40': '#dadee2',
+ 'uzh-grey-20': '#edeff1',
+ 'uzh-red-100': '#dc6027',
+ 'uzh-red-80': '#e38052',
+ 'uzh-red-60': '#eaa07d',
+ 'uzh-red-40': '#f1bfa9',
+ 'uzh-red-20': '#f8dfd4',
+ 'uzh-yellow-100': '#fede00',
+ 'uzh-yellow-80': '#fbe651',
+ 'uzh-yellow-60': '#fcec7c',
+ 'uzh-yellow-40': '#fdf3a8',
+ 'uzh-yellow-20': '#fef9d3',
+ 'uzh-lightgreen-100': '#91c34a',
+ 'uzh-lightgreen-80': '#aad470',
+ 'uzh-lightgreen-60': '#bfdfg4',
+ 'uzh-lightgreen-40': '#d5e9b7',
+ 'uzh-lightgreen-20': '#eaf4db',
+ 'uzh-darkgreen-100': '#2a7f62',
+ 'uzh-darkgreen-80': '#569d85',
+ 'uzh-darkgreen-60': '#80b6a4',
+ 'uzh-darkgreen-40': '#abcec2',
+ 'uzh-darkgreen-20': '#d5e7e1',
+ 'uzh-turqoise-100': '#0b82a0',
+ 'uzh-turqoise-80': '#3c9fb6',
+ 'uzh-turqoise-60': '#6bb7c7',
+ 'uzh-turqoise-40': '#9ed0d9',
+ 'uzh-turqoise-20': '#cfe8ec',
+ 'primary-100': 'var(--theme-color-primary)',
+ 'primary-80': 'var(--theme-color-primary-80)',
+ 'primary-60': 'var(--theme-color-primary-60)',
+ 'primary-40': 'var(--theme-color-primary-40)',
+ 'primary-20': 'var(--theme-color-primary-20)',
+ 'secondary-100': 'var(--theme-color-secondary)',
+ 'secondary-80': 'var(--theme-color-secondary-80)',
+ 'secondary-60': 'var(--theme-color-secondary-60)',
+ 'secondary-40': 'var(--theme-color-secondary-40)',
+ 'secondary-20': 'var(--theme-color-secondary-20)',
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))',
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))',
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))',
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))',
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))',
+ },
+}
+
+export const TailwindFonts = {
+ sans: [
+ 'var(--theme-font-primary)',
+ 'var(--source-sans-pro)',
+ '"Source Sans 3"',
+ '"Source Sans Pro"',
+ ...fontFamily.sans,
+ ],
+}
+
+export const TailwindBasePlugins = {
+ 'postcss-import': {},
+ 'tailwindcss/nesting': {},
+ tailwindcss: {},
+ autoprefixer: {},
+}
+
+export const TailwindProdPlugins = {
+ ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
+}
+
+export const ESLintConfig = {
+ extends: ['next', 'next/core-web-vitals'],
+}
diff --git a/packages/design-system-v3/src/copy.ts b/packages/design-system-v3/src/copy.ts
new file mode 100644
index 00000000..579d7612
--- /dev/null
+++ b/packages/design-system-v3/src/copy.ts
@@ -0,0 +1,11 @@
+import fs from 'fs'
+import path, { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+
+fs.copyFileSync(
+ path.resolve(__dirname, '../node_modules/tailwindcss/src/css/preflight.css'),
+ path.resolve(__dirname, '../dist/preflight.css')
+)
diff --git a/packages/design-system-v3/src/hooks/use-toast.ts b/packages/design-system-v3/src/hooks/use-toast.ts
new file mode 100644
index 00000000..bb8f8a4e
--- /dev/null
+++ b/packages/design-system-v3/src/hooks/use-toast.ts
@@ -0,0 +1,188 @@
+import * as React from 'react'
+
+import type { ToastActionElement, ToastProps } from '@/ui/toast'
+
+const TOAST_LIMIT = 1
+const TOAST_REMOVE_DELAY = 1000000
+
+type ToasterToast = ToastProps & {
+ id: string
+ title?: React.ReactNode
+ description?: React.ReactNode
+ action?: ToastActionElement
+}
+
+const actionTypes = {
+ ADD_TOAST: 'ADD_TOAST',
+ UPDATE_TOAST: 'UPDATE_TOAST',
+ DISMISS_TOAST: 'DISMISS_TOAST',
+ REMOVE_TOAST: 'REMOVE_TOAST',
+} as const
+
+let count = 0
+
+function genId() {
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
+ return count.toString()
+}
+
+type ActionType = typeof actionTypes
+
+type Action =
+ | {
+ type: ActionType['ADD_TOAST']
+ toast: ToasterToast
+ }
+ | {
+ type: ActionType['UPDATE_TOAST']
+ toast: Partial
+ }
+ | {
+ type: ActionType['DISMISS_TOAST']
+ toastId?: ToasterToast['id']
+ }
+ | {
+ type: ActionType['REMOVE_TOAST']
+ toastId?: ToasterToast['id']
+ }
+
+interface State {
+ toasts: ToasterToast[]
+}
+
+const toastTimeouts = new Map>()
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId)
+ dispatch({
+ type: 'REMOVE_TOAST',
+ toastId: toastId,
+ })
+ }, TOAST_REMOVE_DELAY)
+
+ toastTimeouts.set(toastId, timeout)
+}
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case 'ADD_TOAST':
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ }
+
+ case 'UPDATE_TOAST':
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
+ ),
+ }
+
+ case 'DISMISS_TOAST': {
+ const { toastId } = action
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId)
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id)
+ })
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t
+ ),
+ }
+ }
+ case 'REMOVE_TOAST':
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ }
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ }
+ }
+}
+
+const listeners: Array<(state: State) => void> = []
+
+let memoryState: State = { toasts: [] }
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action)
+ listeners.forEach((listener) => {
+ listener(memoryState)
+ })
+}
+
+type Toast = Omit
+
+function toast({ ...props }: Toast) {
+ const id = genId()
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: 'UPDATE_TOAST',
+ toast: { ...props, id },
+ })
+ const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
+
+ dispatch({
+ type: 'ADD_TOAST',
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss()
+ },
+ },
+ })
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ }
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState)
+
+ React.useEffect(() => {
+ listeners.push(setState)
+ return () => {
+ const index = listeners.indexOf(setState)
+ if (index > -1) {
+ listeners.splice(index, 1)
+ }
+ }
+ }, [state])
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
+ }
+}
+
+export { toast, useToast }
diff --git a/packages/design-system-v3/src/index.ts b/packages/design-system-v3/src/index.ts
new file mode 100644
index 00000000..5a98dc07
--- /dev/null
+++ b/packages/design-system-v3/src/index.ts
@@ -0,0 +1,49 @@
+import './tailwind.css'
+
+export * from './ui/accordion'
+export * from './ui/alert'
+export * from './ui/alert-dialog'
+export * from './ui/aspect-ratio'
+export * from './ui/avatar'
+export * from './ui/badge'
+export * from './ui/breadcrumb'
+export * from './ui/button'
+export * from './ui/calendar'
+export * from './ui/card'
+export * from './ui/carousel'
+export * from './ui/chart'
+export * from './ui/checkbox'
+export * from './ui/collapsible'
+export * from './ui/context-menu'
+export * from './ui/dialog'
+export * from './ui/drawer'
+export * from './ui/dropdown-menu'
+export * from './ui/form'
+export * from './ui/hover-card'
+export * from './ui/input'
+export * from './ui/input-otp'
+export * from './ui/label'
+// export * from './ui/menubar'
+export * from './ui/navigation-menu'
+export * from './ui/pagination'
+export * from './ui/popover'
+export * from './ui/progress'
+export * from './ui/radio-group'
+export * from './ui/resizable'
+export * from './ui/scroll-area'
+export * from './ui/select'
+export * from './ui/separator'
+export * from './ui/sheet'
+export * from './ui/skeleton'
+export * from './ui/slider'
+export { Toaster as Sonner } from './ui/sonner'
+export * from './ui/switch'
+export * from './ui/table'
+export * from './ui/tabs'
+export * from './ui/textarea'
+export * from './ui/toast'
+export * from './ui/toaster'
+export * from './ui/toggle'
+export * from './ui/toggle-group'
+export * from './ui/tooltip'
+export * from './ui/use-toast'
diff --git a/packages/design-system-v3/src/lib/utils.ts b/packages/design-system-v3/src/lib/utils.ts
new file mode 100644
index 00000000..fed2fe91
--- /dev/null
+++ b/packages/design-system-v3/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from 'clsx'
+import { twMerge } from 'tailwind-merge'
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/packages/design-system-v3/src/tailwind.css b/packages/design-system-v3/src/tailwind.css
new file mode 100644
index 00000000..958fed41
--- /dev/null
+++ b/packages/design-system-v3/src/tailwind.css
@@ -0,0 +1,81 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+ --radius: 0.3rem;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ }
+}
+
+@layer base {
+ * {
+ /* @apply border-border; */
+ border-color: hsl(var(--border));
+ }
+ body {
+ /* @apply bg-background text-foreground; */
+ background-color: hsl(var(--background));
+ color: hsl(var(--foreground));
+ font-feature-settings:
+ 'rlig' 1,
+ 'calt' 1;
+ }
+ html,
+ body,
+ #__next {
+ @apply h-full;
+ @apply font-sans;
+ }
+}
diff --git a/packages/design-system-v3/src/ui/accordion.stories.tsx b/packages/design-system-v3/src/ui/accordion.stories.tsx
new file mode 100644
index 00000000..e7d07900
--- /dev/null
+++ b/packages/design-system-v3/src/ui/accordion.stories.tsx
@@ -0,0 +1,36 @@
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from '@/ui/accordion'
+
+import type { Story, StoryDefault } from '@ladle/react'
+
+export default {
+ title: 'Shadcn/Accordion',
+} satisfies StoryDefault
+
+export const Default: Story = () => (
+
+
+ Is it accessible?
+
+ Yes. It adheres to the WAI-ARIA design pattern.
+
+
+
+ Is it styled?
+
+ Yes. It comes with default styles that matches the other
+ components' aesthetic.
+
+
+
+ Is it animated?
+
+ Yes. It's animated by default, but you can disable it if you prefer.
+
+
+
+)
diff --git a/packages/design-system-v3/src/ui/accordion.tsx b/packages/design-system-v3/src/ui/accordion.tsx
new file mode 100644
index 00000000..a8d51beb
--- /dev/null
+++ b/packages/design-system-v3/src/ui/accordion.tsx
@@ -0,0 +1,56 @@
+import * as AccordionPrimitive from '@radix-ui/react-accordion'
+import { ChevronDown } from 'lucide-react'
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = 'AccordionItem'
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180',
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
diff --git a/packages/design-system-v3/src/ui/alert-dialog.stories.tsx b/packages/design-system-v3/src/ui/alert-dialog.stories.tsx
new file mode 100644
index 00000000..45e81376
--- /dev/null
+++ b/packages/design-system-v3/src/ui/alert-dialog.stories.tsx
@@ -0,0 +1,38 @@
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/ui/alert-dialog'
+import { Button } from '@/ui/button'
+import type { Story, StoryDefault } from '@ladle/react'
+
+export default {
+ title: 'Shadcn/AlertDialog',
+} satisfies StoryDefault
+
+export const Default: Story = () => (
+
+
+
+
+
+
+ Are you absolutely sure?
+
+ This action cannot be undone. This will permanently delete your
+ account and remove your data from our servers.
+
+
+
+ Cancel
+ Continue
+
+
+
+)
diff --git a/packages/design-system-v3/src/ui/alert-dialog.tsx b/packages/design-system-v3/src/ui/alert-dialog.tsx
new file mode 100644
index 00000000..aa223362
--- /dev/null
+++ b/packages/design-system-v3/src/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+import { buttonVariants } from '@/ui/button'
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = 'AlertDialogHeader'
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = 'AlertDialogFooter'
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+}
diff --git a/packages/design-system-v3/src/ui/alert.stories.tsx b/packages/design-system-v3/src/ui/alert.stories.tsx
new file mode 100644
index 00000000..f2b662e2
--- /dev/null
+++ b/packages/design-system-v3/src/ui/alert.stories.tsx
@@ -0,0 +1,17 @@
+import { Alert, AlertDescription, AlertTitle } from '@/ui/alert'
+import type { Story, StoryDefault } from '@ladle/react'
+import { Terminal } from 'lucide-react'
+
+export default {
+ title: 'Shadcn/Alert',
+} satisfies StoryDefault
+
+export const Default: Story = () => (
+
+
+ Heads up!
+
+ You can add components to your app using the cli.
+
+
+)
diff --git a/packages/design-system-v3/src/ui/alert.tsx b/packages/design-system-v3/src/ui/alert.tsx
new file mode 100644
index 00000000..47ab0da9
--- /dev/null
+++ b/packages/design-system-v3/src/ui/alert.tsx
@@ -0,0 +1,59 @@
+import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+const alertVariants = cva(
+ 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
+ {
+ variants: {
+ variant: {
+ default: 'bg-background text-foreground',
+ destructive:
+ 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = 'Alert'
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = 'AlertTitle'
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = 'AlertDescription'
+
+export { Alert, AlertDescription, AlertTitle }
diff --git a/packages/design-system-v3/src/ui/aspect-ratio.tsx b/packages/design-system-v3/src/ui/aspect-ratio.tsx
new file mode 100644
index 00000000..07bc6747
--- /dev/null
+++ b/packages/design-system-v3/src/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/packages/design-system-v3/src/ui/avatar.stories.tsx b/packages/design-system-v3/src/ui/avatar.stories.tsx
new file mode 100644
index 00000000..53594517
--- /dev/null
+++ b/packages/design-system-v3/src/ui/avatar.stories.tsx
@@ -0,0 +1,13 @@
+import { Avatar, AvatarFallback, AvatarImage } from '@/ui/avatar'
+import type { Story, StoryDefault } from '@ladle/react'
+
+export default {
+ title: 'Shadcn/Avatar',
+} satisfies StoryDefault
+
+export const Default: Story = () => (
+
+
+ CN
+
+)
diff --git a/packages/design-system-v3/src/ui/avatar.tsx b/packages/design-system-v3/src/ui/avatar.tsx
new file mode 100644
index 00000000..408995ba
--- /dev/null
+++ b/packages/design-system-v3/src/ui/avatar.tsx
@@ -0,0 +1,48 @@
+import * as AvatarPrimitive from '@radix-ui/react-avatar'
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarFallback, AvatarImage }
diff --git a/packages/design-system-v3/src/ui/badge.stories.tsx b/packages/design-system-v3/src/ui/badge.stories.tsx
new file mode 100644
index 00000000..f91b0486
--- /dev/null
+++ b/packages/design-system-v3/src/ui/badge.stories.tsx
@@ -0,0 +1,8 @@
+import { Badge } from '@/ui/badge'
+import type { Story, StoryDefault } from '@ladle/react'
+
+export default {
+ title: 'Shadcn/Badge',
+} satisfies StoryDefault
+
+export const Default: Story = () => Badge
diff --git a/packages/design-system-v3/src/ui/badge.tsx b/packages/design-system-v3/src/ui/badge.tsx
new file mode 100644
index 00000000..e2657183
--- /dev/null
+++ b/packages/design-system-v3/src/ui/badge.tsx
@@ -0,0 +1,36 @@
+import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+const badgeVariants = cva(
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ destructive:
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ outline: 'text-foreground',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/packages/design-system-v3/src/ui/breadcrumb.tsx b/packages/design-system-v3/src/ui/breadcrumb.tsx
new file mode 100644
index 00000000..2b4d52ab
--- /dev/null
+++ b/packages/design-system-v3/src/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import { Slot } from '@radix-ui/react-slot'
+import { ChevronRight, MoreHorizontal } from 'lucide-react'
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<'nav'> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = 'Breadcrumb'
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<'ol'>
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = 'BreadcrumbList'
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<'li'>
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = 'BreadcrumbItem'
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<'a'> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'a'
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = 'BreadcrumbLink'
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<'span'>
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = 'BreadcrumbPage'
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) => (
+ svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = 'BreadcrumbSeparator'
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis'
+
+export {
+ Breadcrumb,
+ BreadcrumbEllipsis,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+}
diff --git a/packages/design-system-v3/src/ui/button.stories.tsx b/packages/design-system-v3/src/ui/button.stories.tsx
new file mode 100644
index 00000000..4b01da96
--- /dev/null
+++ b/packages/design-system-v3/src/ui/button.stories.tsx
@@ -0,0 +1,9 @@
+import { Button } from '@/ui/button'
+
+import type { Story, StoryDefault } from '@ladle/react'
+
+export default {
+ title: 'Shadcn/Button',
+} satisfies StoryDefault
+
+export const Default: Story = () =>
diff --git a/packages/design-system-v3/src/ui/button.tsx b/packages/design-system-v3/src/ui/button.tsx
new file mode 100644
index 00000000..91b6f5c6
--- /dev/null
+++ b/packages/design-system-v3/src/ui/button.tsx
@@ -0,0 +1,56 @@
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+ outline:
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-10 px-4 py-2',
+ sm: 'h-9 rounded-md px-3',
+ lg: 'h-11 rounded-md px-8',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button'
+ return (
+
+ )
+ }
+)
+Button.displayName = 'Button'
+
+export { Button, buttonVariants }
diff --git a/packages/design-system-v3/src/ui/calendar.stories.tsx b/packages/design-system-v3/src/ui/calendar.stories.tsx
new file mode 100644
index 00000000..6867b15b
--- /dev/null
+++ b/packages/design-system-v3/src/ui/calendar.stories.tsx
@@ -0,0 +1,20 @@
+import { Calendar } from '@/ui/calendar'
+import type { Story, StoryDefault } from '@ladle/react'
+import React from 'react'
+
+export default {
+ title: 'Shadcn/Calendar',
+} satisfies StoryDefault
+
+export const Default: Story = () => {
+ const [date, setDate] = React.useState(new Date())
+
+ return (
+
+ )
+}
diff --git a/packages/design-system-v3/src/ui/calendar.tsx b/packages/design-system-v3/src/ui/calendar.tsx
new file mode 100644
index 00000000..553fbd2e
--- /dev/null
+++ b/packages/design-system-v3/src/ui/calendar.tsx
@@ -0,0 +1,64 @@
+import { ChevronLeft, ChevronRight } from 'lucide-react'
+import * as React from 'react'
+import { DayPicker } from 'react-day-picker'
+
+import { cn } from '@/lib/utils'
+import { buttonVariants } from '@/ui/button'
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: () => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = 'Calendar'
+
+export { Calendar }
diff --git a/packages/design-system-v3/src/ui/card.tsx b/packages/design-system-v3/src/ui/card.tsx
new file mode 100644
index 00000000..f7dfb0c2
--- /dev/null
+++ b/packages/design-system-v3/src/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = 'Card'
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = 'CardHeader'
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = 'CardTitle'
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = 'CardDescription'
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = 'CardContent'
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = 'CardFooter'
+
+export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
diff --git a/packages/design-system-v3/src/ui/carousel.tsx b/packages/design-system-v3/src/ui/carousel.tsx
new file mode 100644
index 00000000..2c7862c0
--- /dev/null
+++ b/packages/design-system-v3/src/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from 'embla-carousel-react'
+import { ArrowLeft, ArrowRight } from 'lucide-react'
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+import { Button } from '@/ui/button'
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: 'horizontal' | 'vertical'
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error('useCarousel must be used within a ')
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = 'horizontal',
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === 'horizontal' ? 'x' : 'y',
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === 'ArrowLeft') {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === 'ArrowRight') {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on('reInit', onSelect)
+ api.on('select', onSelect)
+
+ return () => {
+ api?.off('select', onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = 'Carousel'
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = 'CarouselContent'
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = 'CarouselItem'
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = 'CarouselPrevious'
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = 'CarouselNext'
+
+export {
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselNext,
+ CarouselPrevious,
+ type CarouselApi,
+}
diff --git a/packages/design-system-v3/src/ui/chart.tsx b/packages/design-system-v3/src/ui/chart.tsx
new file mode 100644
index 00000000..50c1c7e7
--- /dev/null
+++ b/packages/design-system-v3/src/ui/chart.tsx
@@ -0,0 +1,363 @@
+import * as React from 'react'
+import * as RechartsPrimitive from 'recharts'
+
+import { cn } from '@/lib/utils'
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: '', dark: '.dark' } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error('useChart must be used within a ')
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<'div'> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >['children']
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = 'Chart'
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+