-
16 |
-
17 | Get started by editing{' '}
18 |
19 | app/page.tsx 20 |
21 | . 22 |
23 | - Save and see your changes instantly. 24 |
├── .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 | ///
19 | app/page.tsx
20 |
21 | .
22 |