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) =>