├── .cursor └── mcp.json ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── components.json ├── eslint.config.mjs ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── prettier.config.mjs ├── src ├── app │ ├── editor │ │ └── page.tsx │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── editor │ │ ├── plate-editor.tsx │ │ └── use-create-editor.ts │ └── ui │ │ ├── editor-static.tsx │ │ └── editor.tsx └── lib │ └── utils.ts └── tsconfig.json /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "plate": { 4 | "description": "Plate editors, plugins and components", 5 | "type": "stdio", 6 | "command": "npx", 7 | "args": ["-y", "shadcn@canary", "registry:mcp"], 8 | "env": { 9 | "REGISTRY_URL": "https://platejs.org/r/registry.json" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | jobs: 14 | build: 15 | name: ${{ matrix.command }} 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@v4 20 | with: 21 | # Fetch all git history so that yarn workspaces --since can compare with the correct commits 22 | # @link https://github.com/actions/checkout#fetch-all-history-for-all-tags-and-branches 23 | fetch-depth: 0 24 | 25 | - uses: pnpm/action-setup@v2.4.1 26 | name: Install pnpm 27 | with: 28 | version: 8.6.1 29 | run_install: false 30 | 31 | - name: Use Node.js 18 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: 18 35 | cache: 'pnpm' 36 | 37 | - name: Get pnpm store directory 38 | id: pnpm-cache 39 | run: | 40 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 41 | - uses: actions/cache@v4 42 | name: Setup pnpm cache 43 | with: 44 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 45 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pnpm-store- 48 | 49 | - name: Install dependencies 50 | run: pnpm install --no-frozen-lockfile 51 | 52 | # Lint, typecheck, build 53 | - name: 🏗 Run build 54 | run: pnpm build 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | .env.local 35 | .env.development.local 36 | .env.test.local 37 | .env.production.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | next-env.d.ts -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | cache 2 | .cache 3 | package.json 4 | package-lock.json 5 | public 6 | CHANGELOG.md 7 | .yarn 8 | dist 9 | node_modules 10 | .next 11 | build 12 | .contentlayer -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Ziad Beyens, Dylan Schiemann, Joe Anderson, shadcn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playground Template 2 | 3 | A minimal template for building rich-text editors with [Plate](https://platejs.org/) and Next.js 15. 4 | 5 | ## Features 6 | 7 | - Next.js 15 App Directory 8 | - [Plate](https://platejs.org/) editor 9 | - [shadcn/ui](https://ui.shadcn.com/) 10 | - [MCP](https://platejs.org/docs/components/mcp) 11 | 12 | ## Requirements 13 | 14 | - Node.js 20+ 15 | - pnpm 9+ 16 | 17 | ## Installation 18 | 19 | Choose one of these methods: 20 | 21 | ### 1. Using CLI (Recommended) 22 | 23 | ```bash 24 | npx shadcn@latest add https://platejs.org/r/editor-basic 25 | ``` 26 | 27 | ### 2. Using Template 28 | 29 | [Use this template](https://github.com/udecode/plate-template/generate), then install dependencies: 30 | 31 | ```bash 32 | pnpm install 33 | ``` 34 | 35 | ## Development 36 | 37 | ```bash 38 | pnpm dev 39 | ``` 40 | 41 | Visit http://localhost:3000/editor to see the editor in action. 42 | 43 | ## Upgrade 44 | 45 | Using the CLI, you can upgrade to `editor-ai` by running: 46 | 47 | ```bash 48 | npx shadcn@latest add https://platejs.org/r/editor-ai -o 49 | ``` 50 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc'; 2 | import perfectionist from 'eslint-plugin-perfectionist'; 3 | import unusedImports from 'eslint-plugin-unused-imports'; 4 | import { dirname } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | }); 13 | 14 | const eslintConfig = [ 15 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 16 | { 17 | plugins: { 18 | 'unused-imports': unusedImports, 19 | }, 20 | rules: { 21 | '@next/next/no-html-link-for-pages': 'off', 22 | '@typescript-eslint/no-unused-vars': 'off', 23 | 'import/no-anonymous-default-export': 'off', 24 | 'linebreak-style': ['error', 'unix'], 25 | 'no-case-declarations': 'off', 26 | 'no-duplicate-imports': 'off', 27 | 'no-empty-function': 'off', 28 | 'no-prototype-builtins': 'off', 29 | 'no-unused-vars': 'off', 30 | 'react/display-name': 'off', 31 | 'react/jsx-curly-brace-presence': [ 32 | 'warn', 33 | { children: 'never', props: 'never' }, 34 | ], 35 | 'react/jsx-newline': ['off'], 36 | 'react/no-unescaped-entities': ['error', { forbid: ['>'] }], 37 | 'react/no-unknown-property': 'off', 38 | 'react/prop-types': 'off', 39 | 'react/react-in-jsx-scope': 'off', 40 | 'spaced-comment': [ 41 | 'error', 42 | 'always', 43 | { 44 | block: { 45 | balanced: true, 46 | exceptions: ['*'], 47 | markers: ['!'], 48 | }, 49 | line: { 50 | exceptions: ['-', '+'], 51 | markers: ['/'], 52 | }, 53 | }, 54 | ], 55 | 'unused-imports/no-unused-imports': 'error', 56 | 'unused-imports/no-unused-vars': 'off', 57 | }, 58 | }, 59 | perfectionist.configs['recommended-natural'], 60 | { 61 | rules: { 62 | '@typescript-eslint/adjacent-overload-signatures': 'off', 63 | 'perfectionist/sort-array-includes': [ 64 | 'warn', 65 | { 66 | groupKind: 'literals-first', 67 | type: 'natural', 68 | }, 69 | ], 70 | 'perfectionist/sort-classes': [ 71 | 'warn', 72 | { 73 | groups: [ 74 | 'index-signature', 75 | 'static-property', 76 | 'private-property', 77 | 'protected-property', 78 | 'property', 79 | 'constructor', 80 | 'static-method', 81 | 'private-method', 82 | 'protected-method', 83 | 'method', 84 | ['get-method', 'set-method'], 85 | 'static-block', 86 | 'unknown', 87 | ], 88 | type: 'natural', 89 | }, 90 | ], 91 | 'perfectionist/sort-decorators': [ 92 | 'warn', 93 | { 94 | type: 'natural', 95 | }, 96 | ], 97 | 'perfectionist/sort-enums': [ 98 | 'warn', 99 | { 100 | sortByValue: true, 101 | type: 'natural', 102 | }, 103 | ], 104 | 'perfectionist/sort-exports': [ 105 | 'warn', 106 | { 107 | groupKind: 'types-first', 108 | type: 'natural', 109 | }, 110 | ], 111 | 'perfectionist/sort-heritage-clauses': [ 112 | 'warn', 113 | { 114 | type: 'natural', 115 | }, 116 | ], 117 | 'perfectionist/sort-imports': [ 118 | 'warn', 119 | { 120 | customGroups: { 121 | type: { 122 | next: '^next$', 123 | react: '^react$', 124 | }, 125 | value: { 126 | next: ['^next$'], 127 | react: ['^react$', '^react-.*$'], 128 | }, 129 | }, 130 | groups: [ 131 | 'react', 132 | ['type', 'internal-type'], 133 | 'next', 134 | ['builtin', 'external'], 135 | 'internal', 136 | ['parent-type', 'sibling-type', 'index-type'], 137 | ['parent', 'sibling', 'index'], 138 | 'side-effect', 139 | 'style', 140 | 'object', 141 | 'unknown', 142 | ], 143 | internalPattern: ['^@/.*'], 144 | type: 'natural', 145 | }, 146 | ], 147 | 'perfectionist/sort-interfaces': [ 148 | 'warn', 149 | { 150 | customGroups: { 151 | key: ['^key$', '^keys$'], 152 | id: ['^id$', '^_id$'], 153 | }, 154 | groupKind: 'required-first', 155 | groups: ['key', 'id', 'unknown', 'method'], 156 | partitionByComment: true, 157 | 158 | type: 'natural', 159 | }, 160 | ], 161 | 'perfectionist/sort-intersection-types': 'off', 162 | 'perfectionist/sort-jsx-props': [ 163 | 'warn', 164 | { 165 | customGroups: { 166 | key: ['^key$', '^keys$'], 167 | id: ['^id$', '^name$', '^testId$', '^data-testid$'], 168 | accessibility: [ 169 | '^title$', 170 | '^alt$', 171 | '^placeholder$', 172 | '^label$', 173 | '^description$', 174 | '^fallback$', 175 | ], 176 | callback: ['^on[A-Z]', '^handle[A-Z]'], 177 | className: ['^className$', '^class$', '^style$'], 178 | control: ['^asChild$', '^as$'], 179 | data: ['^data-*', '^aria-*'], 180 | ref: ['^ref$', '^innerRef$'], 181 | state: [ 182 | '^value$', 183 | '^checked$', 184 | '^selected$', 185 | '^open$', 186 | '^defaultValue$', 187 | '^defaultChecked$', 188 | '^defaultOpen$', 189 | '^disabled$', 190 | '^required$', 191 | '^readOnly$', 192 | '^loading$', 193 | ], 194 | variant: ['^variant$', '^size$', '^orientation$', '^color$'], 195 | }, 196 | groups: [ 197 | 'id', 198 | 'key', 199 | 'ref', 200 | 'control', 201 | 'variant', 202 | 'className', 203 | 'state', 204 | 'callback', 205 | 'accessibility', 206 | 'data', 207 | 'unknown', 208 | 'shorthand', 209 | ], 210 | type: 'natural', 211 | }, 212 | ], 213 | 'perfectionist/sort-modules': [ 214 | 'warn', 215 | { 216 | groups: [ 217 | 'declare-enum', 218 | 'export-enum', 219 | 'enum', 220 | ['declare-interface', 'declare-type'], 221 | ['export-interface', 'export-type'], 222 | ['interface', 'type'], 223 | 'declare-class', 224 | 'class', 225 | 'export-class', 226 | 227 | // 'declare-function', 228 | // 'export-function', 229 | // 'function', 230 | 231 | // 'unknown', 232 | ], 233 | type: 'natural', 234 | }, 235 | ], 236 | 'perfectionist/sort-named-exports': [ 237 | 'warn', 238 | { 239 | groupKind: 'types-first', 240 | type: 'natural', 241 | }, 242 | ], 243 | 'perfectionist/sort-named-imports': [ 244 | 'warn', 245 | { 246 | groupKind: 'types-first', 247 | type: 'natural', 248 | }, 249 | ], 250 | 'perfectionist/sort-object-types': [ 251 | 'warn', 252 | { 253 | customGroups: { 254 | key: ['^key$', '^keys$'], 255 | id: ['^id$', '^_id$'], 256 | callback: ['^on[A-Z]', '^handle[A-Z]'], 257 | }, 258 | groupKind: 'required-first', 259 | groups: [ 260 | 'key', 261 | 'id', 262 | 'unknown', 263 | // 'multiline', 264 | 'method', 265 | 'callback', 266 | ], 267 | newlinesBetween: 'never', 268 | type: 'natural', 269 | }, 270 | ], 271 | 'perfectionist/sort-objects': [ 272 | 'warn', 273 | { 274 | customGroups: { 275 | key: ['^key$', '^keys$'], 276 | id: ['^id$', '^_id$'], 277 | callback: ['^on[A-Z]', '^handle[A-Z]'], 278 | }, 279 | groups: [ 280 | 'key', 281 | 'id', 282 | 'unknown', 283 | // 'multiline', 284 | 'method', 285 | 'callback', 286 | ], 287 | // newlinesBetween: 'never', 288 | type: 'natural', 289 | }, 290 | ], 291 | 'perfectionist/sort-sets': [ 292 | 'warn', 293 | { 294 | type: 'natural', 295 | }, 296 | ], 297 | 'perfectionist/sort-switch-case': [ 298 | 'warn', 299 | { 300 | type: 'natural', 301 | }, 302 | ], 303 | 'perfectionist/sort-union-types': [ 304 | 'warn', 305 | { 306 | groups: [ 307 | 'conditional', 308 | 'function', 309 | 'import', 310 | ['intersection', 'union'], 311 | 'named', 312 | 'operator', 313 | 'object', 314 | 'keyword', 315 | 'literal', 316 | 'tuple', 317 | 'nullish', 318 | 'unknown', 319 | ], 320 | type: 'natural', 321 | }, 322 | ], 323 | 'perfectionist/sort-variable-declarations': [ 324 | 'warn', 325 | { 326 | type: 'natural', 327 | }, 328 | ], 329 | 'react/jsx-sort-props': 'off', 330 | 'sort-imports': 'off', 331 | 332 | 'sort-keys': 'off', 333 | }, 334 | settings: { 335 | perfectionist: { 336 | ignoreCase: false, 337 | }, 338 | }, 339 | }, 340 | ]; 341 | 342 | export default eslintConfig; 343 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | async redirects() { 4 | return [ 5 | { 6 | destination: '/editor', 7 | permanent: false, 8 | source: '/', 9 | }, 10 | ]; 11 | }, 12 | }; 13 | 14 | export default nextConfig; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plate-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "depset": "pnpm dlx depset@latest @udecode --yes", 8 | "dev": "next dev --turbo", 9 | "lint": "next lint", 10 | "lint:fix": "next lint --fix && prettier --write . --log-level warn", 11 | "preview": "next build && next start", 12 | "start": "next start", 13 | "typecheck": "tsc --noEmit" 14 | }, 15 | "dependencies": { 16 | "@udecode/cn": "^48.0.3", 17 | "@udecode/plate": "^48.0.3", 18 | "@udecode/plate-basic-elements": "^48.0.0", 19 | "@udecode/plate-basic-marks": "^48.0.0", 20 | "class-variance-authority": "0.7.1", 21 | "clsx": "^2.1.1", 22 | "lucide-react": "0.509.0", 23 | "next": "^15.3.2", 24 | "react": "^19.1.0", 25 | "react-dom": "^19.1.0", 26 | "tailwind-merge": "3.3.0", 27 | "tailwind-scrollbar-hide": "^2.0.0", 28 | "tw-animate-css": "^1.2.9" 29 | }, 30 | "devDependencies": { 31 | "@eslint/eslintrc": "^3.3.1", 32 | "@tailwindcss/postcss": "4.1.6", 33 | "@types/node": "^22.15.17", 34 | "@types/react": "^19.1.3", 35 | "@types/react-dom": "^19.1.3", 36 | "eslint": "^9.26.0", 37 | "eslint-config-next": "15.3.2", 38 | "eslint-plugin-perfectionist": "4.12.3", 39 | "eslint-plugin-unused-imports": "^4.1.4", 40 | "postcss": "^8.5.3", 41 | "prettier": "^3.5.3", 42 | "prettier-plugin-packagejson": "^2.5.12", 43 | "prettier-plugin-tailwindcss": "^0.6.11", 44 | "tailwindcss": "4.1.6", 45 | "typescript": "5.8.3" 46 | }, 47 | "packageManager": "pnpm@9.2.0", 48 | "pnpm": { 49 | "peerDependencyRules": { 50 | "allowAny": [ 51 | "react", 52 | "react-dom" 53 | ], 54 | "ignoreMissing": [ 55 | "scheduler" 56 | ] 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { plugins: { '@tailwindcss/postcss': {} } }; 2 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | export default { 3 | endOfLine: 'lf', 4 | plugins: ['prettier-plugin-packagejson', 'prettier-plugin-tailwindcss'], 5 | semi: true, 6 | singleQuote: true, 7 | tabWidth: 2, 8 | tailwindFunctions: ['cn', 'cva', 'withCn'], 9 | tailwindStylesheet: './src/app/globals.css', 10 | trailingComma: 'es5', 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/editor/page.tsx: -------------------------------------------------------------------------------- 1 | import { PlateEditor } from '@/components/editor/plate-editor'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udecode/plate-template/b549d61f9f1bb9f505a9ffb783d708e114b5abf3/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udecode/plate-template/b549d61f9f1bb9f505a9ffb783d708e114b5abf3/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udecode/plate-template/b549d61f9f1bb9f505a9ffb783d708e114b5abf3/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "tailwind-scrollbar-hide"; 4 | @import "tw-animate-css"; 5 | 6 | @custom-variant dark (&:is(.dark *)); 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: 'var(--font-sans)', 'ui-sans-serif', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI Variable Display', 'Segoe UI', 'Helvetica', 'Apple Color Emoji', 'Arial', 'sans-serif', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 12 | --font-mono: 'var(--font-mono)', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; 13 | --color-sidebar-ring: var(--sidebar-ring); 14 | --color-sidebar-border: var(--sidebar-border); 15 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 16 | --color-sidebar-accent: var(--sidebar-accent); 17 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 18 | --color-sidebar-primary: var(--sidebar-primary); 19 | --color-sidebar-foreground: var(--sidebar-foreground); 20 | --color-sidebar: var(--sidebar); 21 | --color-chart-5: var(--chart-5); 22 | --color-chart-4: var(--chart-4); 23 | --color-chart-3: var(--chart-3); 24 | --color-chart-2: var(--chart-2); 25 | --color-chart-1: var(--chart-1); 26 | --color-ring: var(--ring); 27 | --color-input: var(--input); 28 | --color-border: var(--border); 29 | --color-destructive: var(--destructive); 30 | --color-accent-foreground: var(--accent-foreground); 31 | --color-accent: var(--accent); 32 | --color-muted-foreground: var(--muted-foreground); 33 | --color-muted: var(--muted); 34 | --color-secondary-foreground: var(--secondary-foreground); 35 | --color-secondary: var(--secondary); 36 | --color-primary-foreground: var(--primary-foreground); 37 | --color-primary: var(--primary); 38 | --color-popover-foreground: var(--popover-foreground); 39 | --color-popover: var(--popover); 40 | --color-card-foreground: var(--card-foreground); 41 | --color-card: var(--card); 42 | --radius-sm: calc(var(--radius) - 4px); 43 | --radius-md: calc(var(--radius) - 2px); 44 | --radius-lg: var(--radius); 45 | --radius-xl: calc(var(--radius) + 4px); 46 | --font-heading: 'var(--font-heading)', 'ui-sans-serif', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI Variable Display', 'Segoe UI', 'Helvetica', 'Apple Color Emoji', 'Arial', 'sans-serif', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 47 | --color-highlight: var(--highlight); 48 | --color-brand: var(--brand); 49 | } 50 | 51 | :root { 52 | --radius: 0.625rem; 53 | --background: oklch(1 0 0); 54 | --foreground: oklch(0.145 0 0); 55 | --card: oklch(1 0 0); 56 | --card-foreground: oklch(0.145 0 0); 57 | --popover: oklch(1 0 0); 58 | --popover-foreground: oklch(0.145 0 0); 59 | --primary: oklch(0.205 0 0); 60 | --primary-foreground: oklch(0.985 0 0); 61 | --secondary: oklch(0.97 0 0); 62 | --secondary-foreground: oklch(0.205 0 0); 63 | --muted: oklch(0.97 0 0); 64 | --muted-foreground: oklch(0.556 0 0); 65 | --accent: oklch(0.97 0 0); 66 | --accent-foreground: oklch(0.205 0 0); 67 | --destructive: oklch(0.577 0.245 27.325); 68 | --border: oklch(0.922 0 0); 69 | --input: oklch(0.922 0 0); 70 | --ring: oklch(0.708 0 0); 71 | --chart-1: oklch(0.646 0.222 41.116); 72 | --chart-2: oklch(0.6 0.118 184.704); 73 | --chart-3: oklch(0.398 0.07 227.392); 74 | --chart-4: oklch(0.828 0.189 84.429); 75 | --chart-5: oklch(0.769 0.188 70.08); 76 | --sidebar: oklch(0.985 0 0); 77 | --sidebar-foreground: oklch(0.145 0 0); 78 | --sidebar-primary: oklch(0.205 0 0); 79 | --sidebar-primary-foreground: oklch(0.985 0 0); 80 | --sidebar-accent: oklch(0.97 0 0); 81 | --sidebar-accent-foreground: oklch(0.205 0 0); 82 | --sidebar-border: oklch(0.922 0 0); 83 | --sidebar-ring: oklch(0.708 0 0); 84 | --brand: oklch(0.623 0.214 259.815); 85 | --highlight: oklch(0.852 0.199 91.936); 86 | } 87 | 88 | .dark { 89 | --background: oklch(0.145 0 0); 90 | --foreground: oklch(0.985 0 0); 91 | --card: oklch(0.205 0 0); 92 | --card-foreground: oklch(0.985 0 0); 93 | --popover: oklch(0.205 0 0); 94 | --popover-foreground: oklch(0.985 0 0); 95 | --primary: oklch(0.922 0 0); 96 | --primary-foreground: oklch(0.205 0 0); 97 | --secondary: oklch(0.269 0 0); 98 | --secondary-foreground: oklch(0.985 0 0); 99 | --muted: oklch(0.269 0 0); 100 | --muted-foreground: oklch(0.708 0 0); 101 | --accent: oklch(0.269 0 0); 102 | --accent-foreground: oklch(0.985 0 0); 103 | --destructive: oklch(0.704 0.191 22.216); 104 | --border: oklch(1 0 0 / 10%); 105 | --input: oklch(1 0 0 / 15%); 106 | --ring: oklch(0.556 0 0); 107 | --chart-1: oklch(0.488 0.243 264.376); 108 | --chart-2: oklch(0.696 0.17 162.48); 109 | --chart-3: oklch(0.769 0.188 70.08); 110 | --chart-4: oklch(0.627 0.265 303.9); 111 | --chart-5: oklch(0.645 0.246 16.439); 112 | --sidebar: oklch(0.205 0 0); 113 | --sidebar-foreground: oklch(0.985 0 0); 114 | --sidebar-primary: oklch(0.488 0.243 264.376); 115 | --sidebar-primary-foreground: oklch(0.985 0 0); 116 | --sidebar-accent: oklch(0.269 0 0); 117 | --sidebar-accent-foreground: oklch(0.985 0 0); 118 | --sidebar-border: oklch(1 0 0 / 10%); 119 | --sidebar-ring: oklch(0.556 0 0); 120 | --brand: oklch(0.707 0.165 254.624); 121 | --highlight: oklch(0.852 0.199 91.936); 122 | } 123 | 124 | @layer base { 125 | * { 126 | @apply border-border outline-ring/50; 127 | } 128 | body { 129 | @apply bg-background text-foreground; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | import localFont from 'next/font/local'; 4 | 5 | import './globals.css'; 6 | 7 | const geistSans = localFont({ 8 | src: './fonts/GeistVF.woff', 9 | variable: '--font-geist-sans', 10 | weight: '100 900', 11 | }); 12 | const geistMono = localFont({ 13 | src: './fonts/GeistMonoVF.woff', 14 | variable: '--font-geist-mono', 15 | weight: '100 900', 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | description: 'Generated by create next app', 20 | title: 'Create Next App', 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 33 | {children} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |
7 | Next.js logo 15 |
    16 |
  1. 17 | Get started by editing{' '} 18 | 19 | app/page.tsx 20 | 21 | . 22 |
  2. 23 |
  3. Save and see your changes instantly.
  4. 24 |
25 | 26 |
27 | 33 | Vercel logomark 40 | Deploy now 41 | 42 | 48 | Read our docs 49 | 50 |
51 |
52 | 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/components/editor/plate-editor.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Plate } from '@udecode/plate/react'; 4 | 5 | import { useCreateEditor } from '@/components/editor/use-create-editor'; 6 | import { Editor, EditorContainer } from '@/components/ui/editor'; 7 | 8 | export function PlateEditor() { 9 | const editor = useCreateEditor(); 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/editor/use-create-editor.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { withProps } from '@udecode/cn'; 4 | import { BasicElementsPlugin } from '@udecode/plate-basic-elements/react'; 5 | import { 6 | BasicMarksPlugin, 7 | BoldPlugin, 8 | ItalicPlugin, 9 | StrikethroughPlugin, 10 | UnderlinePlugin, 11 | } from '@udecode/plate-basic-marks/react'; 12 | import { 13 | ParagraphPlugin, 14 | PlateElement, 15 | PlateLeaf, 16 | usePlateEditor, 17 | } from '@udecode/plate/react'; 18 | 19 | export const useCreateEditor = () => { 20 | return usePlateEditor({ 21 | components: { 22 | blockquote: withProps(PlateElement, { 23 | as: 'blockquote', 24 | className: 'mb-4 border-l-4 border-[#d0d7de] pl-4 text-[#636c76]', 25 | }), 26 | [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }), 27 | h1: withProps(PlateElement, { 28 | as: 'h1', 29 | className: 30 | 'mb-4 mt-6 text-3xl font-semibold tracking-tight lg:text-4xl', 31 | }), 32 | h2: withProps(PlateElement, { 33 | as: 'h2', 34 | className: 'mb-4 mt-6 text-2xl font-semibold tracking-tight', 35 | }), 36 | h3: withProps(PlateElement, { 37 | as: 'h3', 38 | className: 'mb-4 mt-6 text-xl font-semibold tracking-tight', 39 | }), 40 | [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), 41 | [ParagraphPlugin.key]: withProps(PlateElement, { 42 | as: 'p', 43 | className: 'mb-4', 44 | }), 45 | [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }), 46 | [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }), 47 | }, 48 | plugins: [BasicElementsPlugin, BasicMarksPlugin], 49 | value: [ 50 | { 51 | children: [{ text: 'Basic Editor' }], 52 | type: 'h1', 53 | }, 54 | { 55 | children: [{ text: 'Heading 2' }], 56 | type: 'h2', 57 | }, 58 | { 59 | children: [{ text: 'Heading 3' }], 60 | type: 'h3', 61 | }, 62 | { 63 | children: [{ text: 'This is a blockquote element' }], 64 | type: 'blockquote', 65 | }, 66 | { 67 | children: [ 68 | { text: 'Basic marks: ' }, 69 | { bold: true, text: 'bold' }, 70 | { text: ', ' }, 71 | { italic: true, text: 'italic' }, 72 | { text: ', ' }, 73 | { text: 'underline', underline: true }, 74 | { text: ', ' }, 75 | { strikethrough: true, text: 'strikethrough' }, 76 | { text: '.' }, 77 | ], 78 | type: ParagraphPlugin.key, 79 | }, 80 | ], 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /src/components/ui/editor-static.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import type { VariantProps } from 'class-variance-authority'; 4 | 5 | import { type PlateStaticProps, PlateStatic } from '@udecode/plate'; 6 | import { cva } from 'class-variance-authority'; 7 | 8 | import { cn } from '@/lib/utils'; 9 | 10 | export const editorVariants = cva( 11 | cn( 12 | 'group/editor', 13 | 'relative w-full cursor-text overflow-x-hidden break-words whitespace-pre-wrap select-text', 14 | 'rounded-md ring-offset-background focus-visible:outline-none', 15 | 'placeholder:text-muted-foreground/80 **:data-slate-placeholder:top-[auto_!important] **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!', 16 | '[&_strong]:font-bold' 17 | ), 18 | { 19 | defaultVariants: { 20 | variant: 'none', 21 | }, 22 | variants: { 23 | disabled: { 24 | true: 'cursor-not-allowed opacity-50', 25 | }, 26 | focused: { 27 | true: 'ring-2 ring-ring ring-offset-2', 28 | }, 29 | variant: { 30 | ai: 'w-full px-0 text-base md:text-sm', 31 | aiChat: 32 | 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-5 py-3 text-base md:text-sm', 33 | default: 34 | 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]', 35 | demo: 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]', 36 | fullWidth: 'size-full px-16 pt-4 pb-72 text-base sm:px-24', 37 | none: '', 38 | select: 'px-3 py-2 text-base data-readonly:w-fit', 39 | }, 40 | }, 41 | } 42 | ); 43 | 44 | export function EditorStatic({ 45 | className, 46 | variant, 47 | ...props 48 | }: PlateStaticProps & VariantProps) { 49 | return ( 50 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/ui/editor.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import type { PlateContentProps } from '@udecode/plate/react'; 6 | import type { VariantProps } from 'class-variance-authority'; 7 | 8 | import { PlateContainer, PlateContent } from '@udecode/plate/react'; 9 | import { cva } from 'class-variance-authority'; 10 | 11 | import { cn } from '@/lib/utils'; 12 | 13 | const editorContainerVariants = cva( 14 | 'relative w-full cursor-text overflow-y-auto caret-primary select-text selection:bg-brand/25 focus-visible:outline-none [&_.slate-selection-area]:z-50 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15', 15 | { 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | variants: { 20 | variant: { 21 | comment: cn( 22 | 'flex flex-wrap justify-between gap-1 px-1 py-0.5 text-sm', 23 | 'rounded-md border-[1.5px] border-transparent bg-transparent', 24 | 'has-[[data-slate-editor]:focus]:border-brand/50 has-[[data-slate-editor]:focus]:ring-2 has-[[data-slate-editor]:focus]:ring-brand/30', 25 | 'has-aria-disabled:border-input has-aria-disabled:bg-muted' 26 | ), 27 | default: 'h-full', 28 | demo: 'h-[650px]', 29 | select: cn( 30 | 'group rounded-md border border-input ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2', 31 | 'has-data-readonly:w-fit has-data-readonly:cursor-default has-data-readonly:border-transparent has-data-readonly:focus-within:[box-shadow:none]' 32 | ), 33 | }, 34 | }, 35 | } 36 | ); 37 | 38 | export const EditorContainer = ({ 39 | className, 40 | variant, 41 | ...props 42 | }: React.ComponentProps<'div'> & 43 | VariantProps) => { 44 | return ( 45 | 53 | ); 54 | }; 55 | 56 | EditorContainer.displayName = 'EditorContainer'; 57 | 58 | const editorVariants = cva( 59 | cn( 60 | 'group/editor', 61 | 'relative w-full cursor-text overflow-x-hidden break-words whitespace-pre-wrap select-text', 62 | 'rounded-md ring-offset-background focus-visible:outline-none', 63 | 'placeholder:text-muted-foreground/80 **:data-slate-placeholder:top-[auto_!important] **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!', 64 | '[&_strong]:font-bold' 65 | ), 66 | { 67 | defaultVariants: { 68 | variant: 'default', 69 | }, 70 | variants: { 71 | disabled: { 72 | true: 'cursor-not-allowed opacity-50', 73 | }, 74 | focused: { 75 | true: 'ring-2 ring-ring ring-offset-2', 76 | }, 77 | variant: { 78 | ai: 'w-full px-0 text-base md:text-sm', 79 | aiChat: 80 | 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-base md:text-sm', 81 | comment: cn('rounded-none border-none bg-transparent text-sm'), 82 | default: 83 | 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]', 84 | demo: 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]', 85 | fullWidth: 'size-full px-16 pt-4 pb-72 text-base sm:px-24', 86 | none: '', 87 | select: 'px-3 py-2 text-base data-readonly:w-fit', 88 | }, 89 | }, 90 | } 91 | ); 92 | 93 | export type EditorProps = PlateContentProps & 94 | VariantProps; 95 | 96 | export const Editor = React.forwardRef( 97 | ({ className, disabled, focused, variant, ...props }, ref) => { 98 | return ( 99 | 113 | ); 114 | } 115 | ); 116 | 117 | Editor.displayName = 'Editor'; 118 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": false, 5 | "strictNullChecks": true, 6 | "allowUnusedLabels": false, 7 | "allowUnreachableCode": false, 8 | "exactOptionalPropertyTypes": false, 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitOverride": true, 11 | "noImplicitReturns": false, 12 | "noPropertyAccessFromIndexSignature": false, 13 | "noUncheckedIndexedAccess": false, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | 17 | "isolatedModules": true, 18 | 19 | "allowJs": true, 20 | "checkJs": false, 21 | 22 | "esModuleInterop": true, 23 | "skipLibCheck": true, 24 | "forceConsistentCasingInFileNames": true, 25 | 26 | "lib": ["dom", "dom.iterable", "esnext"], 27 | "module": "esnext", 28 | "target": "es2022", 29 | "moduleResolution": "bundler", 30 | "moduleDetection": "force", 31 | "allowImportingTsExtensions": false, 32 | "resolveJsonModule": true, 33 | "noEmit": true, 34 | "incremental": true, 35 | "declaration": false, 36 | "declarationMap": false, 37 | "sourceMap": true, 38 | "pretty": true, 39 | "preserveWatchOutput": true, 40 | 41 | "jsx": "preserve", 42 | "plugins": [ 43 | { 44 | "name": "next" 45 | } 46 | ], 47 | "baseUrl": ".", 48 | "paths": { 49 | "@/*": ["src/*"] 50 | } 51 | }, 52 | "include": [ 53 | "next-env.d.ts", 54 | ".next/types/**/*.ts", 55 | "**/*.ts", 56 | "**/*.tsx", 57 | "**/*.cts", 58 | "**/*.ctsx", 59 | "**/*.mts", 60 | "**/*.mtsx", 61 | "**/*.js", 62 | "**/*.jsx", 63 | "**/*.cjs", 64 | "**/*.cjsx", 65 | "**/*.mjs", 66 | "**/*.mjsx" 67 | ], 68 | "exclude": ["node_modules", "dist"] 69 | } 70 | --------------------------------------------------------------------------------