├── public ├── gradient.jpg ├── r │ ├── search-and-replace-toolbar.json │ ├── toolbar-provider.json │ ├── search-and-replace-toolbar-example.json │ ├── image-placeholder-toolbar-example.json │ ├── color-and-highlight-toolbar-example.json │ ├── search-and-replace-example.json │ ├── image-placeholder-example.json │ ├── color-highlight-example.json │ ├── starter-kit-toolbar-example.json │ └── starter-kit-example.json ├── niaz-component.json ├── vercel.svg └── next.svg ├── src ├── app │ ├── favicon.ico │ ├── page.tsx │ ├── docs │ │ ├── layout.tsx │ │ └── [[...slug]] │ │ │ └── page.tsx │ ├── extensions │ │ ├── search-and-replace │ │ │ └── page.mdx │ │ ├── image-placeholder │ │ │ └── page.mdx │ │ ├── color-highlight │ │ │ └── page.mdx │ │ └── image │ │ │ └── page.mdx │ ├── layout.tsx │ ├── _components │ │ └── hero-section.tsx │ └── globals.css ├── components │ ├── ui │ │ ├── aspect-ratio.tsx │ │ ├── collapsible.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── checkbox.tsx │ │ ├── tooltip.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── alert.tsx │ │ ├── scroll-area.tsx │ │ ├── button.tsx │ │ ├── tabs.tsx │ │ ├── accordion.tsx │ │ └── sheet.tsx │ ├── theme-provider.tsx │ ├── callout.tsx │ ├── component-source.tsx │ ├── main-nav.tsx │ ├── theme-switch.tsx │ ├── motion-primitives │ │ └── border-trail.tsx │ ├── header.tsx │ ├── code-block-wrapper.tsx │ ├── pager.tsx │ ├── sidebar-nav.tsx │ ├── toc.tsx │ ├── component-example.tsx │ ├── mobile-nav.tsx │ ├── component-preview.tsx │ └── copy-button.tsx ├── hooks │ └── use-mounted.ts ├── lib │ ├── highlight-code.ts │ ├── utils.ts │ ├── toc.ts │ └── rehype-npm-command.ts ├── registry │ ├── index.ts │ ├── toolbars │ │ ├── toolbar-provider.tsx │ │ ├── hard-break.tsx │ │ ├── undo.tsx │ │ ├── redo.tsx │ │ ├── horizontal-rule.tsx │ │ ├── code.tsx │ │ ├── image-placeholder-toolbar.tsx │ │ ├── code-block.tsx │ │ ├── subscript.tsx │ │ ├── bullet-list.tsx │ │ ├── blockquote.tsx │ │ ├── superscript.tsx │ │ ├── ordered-list.tsx │ │ ├── italic.tsx │ │ ├── underline.tsx │ │ ├── strikethrough.tsx │ │ ├── bold.tsx │ │ ├── alignment.tsx │ │ ├── link.tsx │ │ └── color-and-highlight.tsx │ ├── registry-toolbar-examples.ts │ ├── registry-exmples.ts │ ├── toolbar-examples │ │ ├── search-and-replace-toolbar-example.tsx │ │ ├── image-placeholder-toolbar-example.tsx │ │ ├── color-and-highlight-toolbar-example.tsx │ │ └── starter-kit-toolbar-example.tsx │ ├── registry-toolbars.ts │ ├── examples │ │ ├── search-and-replace-example.tsx │ │ ├── image-placeholder-example.tsx │ │ ├── color-highlight-example.tsx │ │ └── starter-kit-example.tsx │ └── schema.ts ├── config │ ├── site.ts │ └── docs.ts ├── types │ ├── nav.ts │ └── unist.ts ├── scripts │ └── fix-import.mts ├── __registry__ │ └── toolbars │ │ ├── toolbar-provider.tsx │ │ └── bold.tsx ├── content │ └── docs │ │ ├── index.mdx │ │ ├── toolbars │ │ └── image-placeholder.mdx │ │ ├── installation.mdx │ │ └── extensions │ │ └── color-and-highlight.mdx └── styles │ └── mdx.css ├── postcss.config.mjs ├── tsconfig.scripts.json ├── components.json ├── lefthook.yml ├── .gitignore ├── next.config.mjs ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── tsconfig.json ├── .vscode └── settings.json ├── README.md ├── LICENSE ├── biome.json ├── .husky ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── tailwind.config.ts ├── package.json └── contentlayer.config.js /public/gradient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NiazMorshed2007/shadcn-tiptap/HEAD/public/gradient.jpg -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NiazMorshed2007/shadcn-tiptap/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /public/r/search-and-replace-toolbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search-and-replace-toolbar", 3 | "type": "registry:block", 4 | "description": "A toolbar for searching and replacing text in the editor.", 5 | "files": [] 6 | } -------------------------------------------------------------------------------- /src/hooks/use-mounted.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function useMounted() { 4 | const [mounted, setMounted] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | setMounted(true); 8 | }, []); 9 | 10 | return mounted; 11 | } 12 | -------------------------------------------------------------------------------- /public/niaz-component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "niaz-component", 3 | "type": "registry:ui", 4 | "registryDependencies": [], 5 | "dependencies": [], 6 | "devDependencies": [], 7 | "files": [ 8 | { 9 | "path": "niaz-component.tsx", 10 | "content": "export const NiazComponent = () => {\n\treturn
NiazComponent
;\n};\n", 11 | "target": "" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import type { ThemeProviderProps } from "next-themes/dist/types"; 5 | import * as React from "react"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /tsconfig.scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "isolatedModules": false 10 | }, 11 | "include": [".contentlayer/generated", "scripts/**/*.ts"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /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": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/lib/highlight-code.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { codeToHtml } from "shiki"; 4 | 5 | export async function highlightCode(code: string) { 6 | const html = codeToHtml(code, { 7 | lang: "typescript", 8 | theme: "github-dark-default", 9 | transformers: [ 10 | { 11 | code(node) { 12 | node.properties["data-line-numbers"] = ""; 13 | }, 14 | }, 15 | ], 16 | }); 17 | 18 | return html; 19 | } 20 | -------------------------------------------------------------------------------- /src/registry/index.ts: -------------------------------------------------------------------------------- 1 | import { examples } from "@/registry/registry-exmples"; 2 | import type { Registry } from "@/registry/schema"; 3 | import { extensions } from "./registry-extensions"; 4 | import { toolbarExamples } from "./registry-toolbar-examples"; 5 | import { toolbars } from "./registry-toolbars"; 6 | 7 | export const registry: Registry = [ 8 | ...examples, 9 | ...extensions, 10 | ...toolbars, 11 | ...toolbarExamples, 12 | ]; 13 | -------------------------------------------------------------------------------- /src/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "shadcn-tiptap", 3 | url: "https://tiptap.niazmorshed.dev", 4 | ogImage: "https://tiptap.niazmorshed.dev/og.jpg", 5 | description: 6 | "Set of custom extensions & toolbars for tiptap, made with shadcn/ui.", 7 | links: { 8 | twitter: "https://twitter.com/niazmorshed_", 9 | github: "https://github.com/NiazMorshed2007/shadcn-tiptap", 10 | }, 11 | }; 12 | 13 | export type SiteConfig = typeof siteConfig; 14 | -------------------------------------------------------------------------------- /src/types/nav.ts: -------------------------------------------------------------------------------- 1 | import type { Icons } from "@/components/icons"; 2 | 3 | export interface NavItem { 4 | title: string; 5 | href?: string; 6 | disabled?: boolean; 7 | external?: boolean; 8 | icon?: keyof typeof Icons; 9 | label?: string; 10 | } 11 | 12 | export interface NavItemWithChildren extends NavItem { 13 | items: NavItemWithChildren[]; 14 | } 15 | 16 | export interface MainNavItem extends NavItem {} 17 | 18 | export interface SidebarNavItem extends NavItemWithChildren {} 19 | -------------------------------------------------------------------------------- /src/components/callout.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 2 | 3 | interface CalloutProps { 4 | icon?: string; 5 | title?: string; 6 | children?: React.ReactNode; 7 | } 8 | 9 | export function Callout({ title, children, icon, ...props }: CalloutProps) { 10 | return ( 11 | 12 | {icon && {icon}} 13 | {title && {title}} 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { HeroSection } from "./_components/hero-section"; 2 | 3 | async function fetchStarCount(repo: string): Promise { 4 | const response = await fetch(`https://api.github.com/repos/${repo}`); 5 | const data = await response.json(); 6 | return data.stargazers_count; 7 | } 8 | 9 | export default async function Home() { 10 | const starCount = await fetchStarCount("NiazMorshed2007/shadcn-tiptap"); 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | commit-msg: 2 | commands: 3 | lint-commit-msg: 4 | run: npx commitlint --edit 5 | 6 | pre-commit: 7 | parallel: true 8 | commands: 9 | format: 10 | glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" 11 | run: npx biome check --apply --no-errors-on-unmatched --files-ignore-unknown=true {staged_files} && git update-index --again 12 | branch-name: 13 | run: pnpm enforce-branch-name '^(build|chore|ci|docs|feat|fix|hotfix|perf|refactor|revert|style|test|setup|backup)\/[a-z-]+$' --ignore '(staging|develop|master|main|dev)' 14 | -------------------------------------------------------------------------------- /.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .contentlayer -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createContentlayerPlugin } from "next-contentlayer2"; 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | reactStrictMode: true, 6 | swcMinify: true, 7 | images: { 8 | remotePatterns: [ 9 | { 10 | protocol: "https", 11 | hostname: "avatars.githubusercontent.com", 12 | }, 13 | { 14 | protocol: "https", 15 | hostname: "images.unsplash.com", 16 | }, 17 | ], 18 | }, 19 | redirects() { 20 | return []; 21 | }, 22 | }; 23 | 24 | const withContentlayer = createContentlayerPlugin({ 25 | // Additional Contentlayer config options 26 | }); 27 | 28 | export default withContentlayer(nextConfig); 29 | -------------------------------------------------------------------------------- /src/components/component-source.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type * as React from "react"; 4 | 5 | import { CodeBlockWrapper } from "@/components/code-block-wrapper"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | interface ComponentSourceProps extends React.HTMLAttributes { 9 | src: string; 10 | } 11 | 12 | export function ComponentSource({ 13 | children, 14 | className, 15 | ...props 16 | }: ComponentSourceProps) { 17 | return ( 18 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/scripts/fix-import.mts: -------------------------------------------------------------------------------- 1 | export function fixImport(content: string) { 2 | const regex = /@\/(.+?)\/((?:.*?\/)?(?:components|ui|hooks|lib))\/([\w-]+)/g; 3 | 4 | const replacement = ( 5 | match: string, 6 | _: string, 7 | type: string, 8 | component: string, 9 | ) => { 10 | if (type.endsWith("components")) { 11 | return `@/components/${component}`; 12 | } 13 | if (type.endsWith("ui")) { 14 | return `@/components/ui/${component}`; 15 | } 16 | if (type.endsWith("hooks")) { 17 | return `@/hooks/${component}`; 18 | } 19 | if (type.endsWith("lib")) { 20 | return `@/lib/${component}`; 21 | } 22 | 23 | return match; 24 | }; 25 | 26 | return content.replace(regex, replacement); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | import { docsConfig } from "@/config/docs"; 2 | import Link from "next/link"; 3 | import { Separator } from "./ui/separator"; 4 | 5 | export const MainNav = () => { 6 | const { mainNav } = docsConfig; 7 | return ( 8 |
9 | 10 | Shadcn Tiptap 11 | 12 | 13 | {mainNav?.map((item) => ( 14 | 19 | {item.title} 20 | 21 | ))} 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/types/unist.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import type { Node } from "unist-builder"; 3 | 4 | export interface UnistNode extends Node { 5 | type: string; 6 | name?: string; 7 | tagName?: string; 8 | value?: string; 9 | properties?: { 10 | __rawString__?: string; 11 | __className__?: string; 12 | __event__?: string; 13 | [key: string]: unknown; 14 | } & NpmCommands; 15 | attributes?: { 16 | name: string; 17 | value: unknown; 18 | type?: string; 19 | }[]; 20 | children?: UnistNode[]; 21 | } 22 | 23 | export interface UnistTree extends Node { 24 | children: UnistNode[]; 25 | } 26 | 27 | export interface NpmCommands { 28 | __npmCommand__?: string; 29 | __yarnCommand__?: string; 30 | __pnpmCommand__?: string; 31 | __bunCommand__?: string; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /src/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DocsSidebarNav } from "@/components/sidebar-nav"; 2 | import { ScrollArea } from "@/components/ui/scroll-area"; 3 | import { docsConfig } from "@/config/docs"; 4 | 5 | interface DocsLayoutProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export default function DocsLayout({ children }: DocsLayoutProps) { 10 | return ( 11 |
12 |
13 | 18 | {children} 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "target": "ES5", 6 | "lib": ["dom", "dom.iterable", "esnext"], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ], 23 | "paths": { 24 | "@/*": ["./src/*"], 25 | "contentlayer/generated": ["./.contentlayer/generated"] 26 | } 27 | }, 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx", 32 | ".next/types/**/*.ts", 33 | ".contentlayer/generated" 34 | ], 35 | "exclude": ["node_modules"] 36 | } 37 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /src/registry/toolbars/toolbar-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { Editor } from "@tiptap/react"; 4 | import React from "react"; 5 | 6 | export interface ToolbarContextProps { 7 | editor: Editor; 8 | } 9 | 10 | export const ToolbarContext = React.createContext( 11 | null, 12 | ); 13 | 14 | interface ToolbarProviderProps { 15 | editor: Editor; 16 | children: React.ReactNode; 17 | } 18 | 19 | export const ToolbarProvider = ({ editor, children }: ToolbarProviderProps) => { 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export const useToolbar = () => { 28 | const context = React.useContext(ToolbarContext); 29 | 30 | if (!context) { 31 | throw new Error("useToolbar must be used within a ToolbarProvider"); 32 | } 33 | 34 | return context; 35 | }; 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnPaste": true, 4 | "editor.codeActionsOnSave": { 5 | "quickfix.biome": "explicit", 6 | "source.organizeImports.biome": "explicit" 7 | }, 8 | "editor.defaultFormatter": "biomejs.biome", 9 | "[css]": { 10 | "editor.defaultFormatter": "biomejs.biome" 11 | }, 12 | "[javascript]": { 13 | "editor.defaultFormatter": "biomejs.biome" 14 | }, 15 | "[typescript]": { 16 | "editor.defaultFormatter": "biomejs.biome" 17 | }, 18 | "[typescriptreact]": { 19 | "editor.defaultFormatter": "biomejs.biome" 20 | }, 21 | "[json]": { 22 | "editor.defaultFormatter": "biomejs.biome" 23 | }, 24 | "tailwindCSS.experimental.classRegex": [ 25 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 26 | ["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] 27 | ], 28 | "typescript.tsdk": "node_modules/typescript/lib" 29 | } 30 | -------------------------------------------------------------------------------- /src/__registry__/toolbars/toolbar-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { Editor } from "@tiptap/react"; 4 | import React from "react"; 5 | 6 | export interface ToolbarContextProps { 7 | editor: Editor; 8 | } 9 | 10 | export const ToolbarContext = React.createContext( 11 | null, 12 | ); 13 | 14 | interface ToolbarProviderProps { 15 | editor: Editor; 16 | children: React.ReactNode; 17 | } 18 | 19 | export const ToolbarProvider = ({ editor, children }: ToolbarProviderProps) => { 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export const useToolbar = () => { 28 | const context = React.useContext(ToolbarContext); 29 | 30 | if (!context) { 31 | throw new Error("useToolbar must be used within a ToolbarProvider"); 32 | } 33 | 34 | return context; 35 | }; 36 | -------------------------------------------------------------------------------- /src/app/extensions/search-and-replace/page.mdx: -------------------------------------------------------------------------------- 1 | import CodeBlock from '../../_components/code-block'; 2 | import ComponentCodePreview from '../../_components/component-code-preview'; 3 | import SearchReplacePlayground from "@/components/examples/search-and-replace-playground"; 4 | 5 | 6 | # Search & Replace 7 | 8 | The Search & Replace extension for TipTap allows to efficiently find and replace specific content within the editor. Key features include search functionality, case sensitivity, sequential navigation & replace option. 9 | 10 | ## Playground 11 | 12 | } 14 | /> 15 | 16 | ## Installation code 17 | 18 | 19 | 20 | 21 | ## Dependencies 22 | 23 | - [@tiptap/core](https://www.npmjs.com/package/@tiptap/core) 24 | - [@tiptap/pm](https://www.npmjs.com/package/@tiptap/pm) -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref, 15 | ) => ( 16 | 27 | ), 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /src/app/extensions/image-placeholder/page.mdx: -------------------------------------------------------------------------------- 1 | import CodeBlock from '../../_components/code-block'; 2 | import ComponentCodePreview from '../../_components/component-code-preview'; 3 | import ImagePlaceholderPlayground from "@/components/examples/image-placeholder-playground"; 4 | 5 | 6 | # Image placeholder 7 | 8 | A custom extension for image placeholders like Notion. Enabling to embed image with a link or upload. 9 | 10 | ## Playground 11 | } 13 | /> 14 | 15 | ## Installation code 16 | 17 | 18 | 19 | ## Dependencies 20 | 21 | - [@tiptap/react](https://www.npmjs.com/package/@tiptap/react) 22 | - [react-dropzone](https://react-dropzone.js.org/) 23 | - [Button](https://ui.shadcn.com/docs/components/button) 24 | - [Input](https://ui.shadcn.com/docs/components/input) 25 | - [Popover](https://ui.shadcn.com/docs/components/popover) 26 | - [Tabs](https://ui.shadcn.com/docs/components/tabs) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/app/extensions/color-highlight/page.mdx: -------------------------------------------------------------------------------- 1 | import CodeBlock from '../../_components/code-block'; 2 | import ComponentCodePreview from '../../_components/component-code-preview'; 3 | import ColorHighlightExample from "@/components/examples/color-highlight-example"; 4 | 5 | 6 | # Color & Highlight 7 | 8 | An implementation of the color and highlight extensions for TipTap. Adding a toolbar for color and highlight using the css variables. 9 | 10 | ## Playground 11 | } 13 | /> 14 | 15 | ## Installation code 16 | 17 | 18 | 19 | ## Related Extension 20 | 21 | - See [Image placeholder](/docs/extensions/image-placeholder) extension. 22 | 23 | ## Dependencies 24 | 25 | - [@tiptap/react](https://www.npmjs.com/package/@tiptap/react) 26 | - [@tiptap/extension-image](https://tiptap.dev/docs/editor/extensions/nodes/image) 27 | - [Button](https://ui.shadcn.com/docs/components/button) 28 | - [Dropdown](https://ui.shadcn.com/docs/components/dropdown) 29 | - [Separator](https://ui.shadcn.com/docs/components/separator) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Shadcn tiptap 2 | 3 | Sets of custom extensions & toolbars for tiptap editor, ready to use with shadcn/ui components. 4 | 5 | Buy Me A Coffee 6 | 7 | 8 | 9 | ## Tech Stack 10 | 11 | - **Framework:** [Next.js](https://nextjs.org) 12 | - **Styling:** [Tailwind CSS](https://tailwindcss.com) 13 | - **UI Components:** [shadcn/ui](https://ui.shadcn.com) 14 | - **Tiptap (Editor):** [Tiptap](https://tiptap.dev) 15 | 16 | ## Running Locally 17 | 18 | 1. Clone the repository 19 | 20 | ```bash 21 | git clone https://github.com/NiazMorshed2007/shadcn-tiptap.git 22 | ``` 23 | 24 | 2. Install dependencies using pnpm 25 | 26 | ```bash 27 | pnpm install 28 | ``` 29 | 30 | 3. Start the development server 31 | 32 | ```bash 33 | pnpm run dev 34 | ``` 35 | -------------------------------------------------------------------------------- /src/registry/registry-toolbar-examples.ts: -------------------------------------------------------------------------------- 1 | import type { Registry } from "./schema"; 2 | 3 | export const toolbarExamples: Registry = [ 4 | { 5 | name: "starter-kit-toolbar-example", 6 | type: "registry:example", 7 | files: [ 8 | { 9 | path: "toolbar-examples/starter-kit-toolbar-example.tsx", 10 | type: "registry:example", 11 | }, 12 | ], 13 | }, 14 | { 15 | name: "search-and-replace-toolbar-example", 16 | type: "registry:example", 17 | files: [ 18 | { 19 | path: "toolbar-examples/search-and-replace-toolbar-example.tsx", 20 | type: "registry:example", 21 | }, 22 | ], 23 | }, 24 | { 25 | name: "image-placeholder-toolbar-example", 26 | type: "registry:example", 27 | files: [ 28 | { 29 | path: "toolbar-examples/image-placeholder-toolbar-example.tsx", 30 | type: "registry:example", 31 | }, 32 | ], 33 | }, 34 | { 35 | name: "color-and-highlight-toolbar-example", 36 | type: "registry:example", 37 | files: [ 38 | { 39 | path: "toolbar-examples/color-and-highlight-toolbar-example.tsx", 40 | type: "registry:example", 41 | }, 42 | ], 43 | }, 44 | ]; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Niaz Morshed 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/r/toolbar-provider.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toolbar-provider", 3 | "type": "registry:block", 4 | "description": "", 5 | "dependencies": [ 6 | "@tiptap/react" 7 | ], 8 | "files": [ 9 | { 10 | "path": "toolbars/toolbar-provider.tsx", 11 | "type": "registry:component", 12 | "content": "\"use client\";\n\nimport type { Editor } from \"@tiptap/react\";\nimport React from \"react\";\n\nexport interface ToolbarContextProps {\n\teditor: Editor;\n}\n\nexport const ToolbarContext = React.createContext(\n\tnull,\n);\n\ninterface ToolbarProviderProps {\n\teditor: Editor;\n\tchildren: React.ReactNode;\n}\n\nexport const ToolbarProvider = ({ editor, children }: ToolbarProviderProps) => {\n\treturn (\n\t\t\n\t\t\t{children}\n\t\t\n\t);\n};\n\nexport const useToolbar = () => {\n\tconst context = React.useContext(ToolbarContext);\n\n\tif (!context) {\n\t\tthrow new Error(\"useToolbar must be used within a ToolbarProvider\");\n\t}\n\n\treturn context;\n};\n", 13 | "target": "components/toolbars/toolbar-provider.tsx" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | [Provide a detailed description of the changes made in this Pull Request. Include relevant information to understand the nature and purpose of the changes.] 4 | 5 | ## Proposed Changes 6 | 7 | [List the specific changes made in this Pull Request, including any code additions, modifications, or deletions. You can use lists, bullet points, or code snippets as needed.] 8 | 9 | ## Screenshots (if applicable) 10 | 11 | [If the changes are visual or affect the user interface, include screenshots or animated GIFs that demonstrate the changes.] 12 | 13 | ## Test Verification 14 | 15 | [Describe the tests performed to ensure that the changes work correctly. Include details about unit tests, integration tests, or user tests you've conducted. If necessary, provide specific commands for others to run the tests.] 16 | 17 | ## Steps to Test 18 | 19 | [Detailed instructions on how to review and test the changes in this Pull Request. Include any necessary setup or configurations.] 20 | 21 | ## Additional Information 22 | 23 | [Any additional information or relevant context that should be considered by reviewers.] 24 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { CheckIcon } from "@radix-ui/react-icons"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "style": { 11 | "useBlockStatements": "error", 12 | "noUnusedTemplateLiteral": "off" 13 | }, 14 | "a11y": { 15 | "noSvgWithoutTitle": "off", 16 | "useKeyWithClickEvents": "off" 17 | }, 18 | "complexity": { 19 | "useLiteralKeys": "off" 20 | }, 21 | "suspicious": { 22 | "noConsoleLog": "error", 23 | "noArrayIndexKey": "off", 24 | "noAssignInExpressions": "off" 25 | }, 26 | "correctness": { 27 | "noUnusedVariables": "error" 28 | } 29 | }, 30 | "ignore": ["src/__registry__/**/*"] 31 | }, 32 | "overrides": [ 33 | { 34 | "include": ["src/registry/extensions/**/*", "src/registry/toolbars/**/*"], 35 | "linter": { 36 | "rules": { 37 | "suspicious": { 38 | "noExplicitAny": "off" 39 | }, 40 | "correctness": { 41 | "noUnusedVariables": "off" 42 | } 43 | } 44 | } 45 | } 46 | ], 47 | "formatter": { 48 | "ignore": ["src/__registry__/**/*"] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter as FontSans } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Header } from "@/components/header"; 5 | import { ThemeProvider } from "@/components/theme-provider"; 6 | import { TooltipProvider } from "@/components/ui/tooltip"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | export const metadata: Metadata = { 10 | title: "Shadcn Tiptap Search & Replace", 11 | description: 12 | "An custom extension for tiptap editor to search and replace text. Made with shadcn/ui components", 13 | }; 14 | 15 | const fontSans = FontSans({ 16 | subsets: ["latin"], 17 | variable: "--font-sans", 18 | }); 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 33 | 34 | 35 |
36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/extensions/image/page.mdx: -------------------------------------------------------------------------------- 1 | import CodeBlock from '../../_components/code-block'; 2 | import ComponentCodePreview from '../../_components/component-code-preview'; 3 | import ImageExtendedPlayground from "@/components/examples/image-extended-playground"; 4 | 5 | 6 | # Image (Extended) 7 | 8 | This extension enhances the standard image functionality provided by TipTap. This extension allows for more comprehensive interactions with image elements, enabling features such as resizing, positioning, and user interactions. 9 | 10 | ## Playground 11 | } 13 | /> 14 | 15 | ## Installation code 16 | 17 | 18 | 19 | ## Related Extension 20 | 21 | - See [Image placeholder](/docs/extensions/image-placeholder) extension. 22 | 23 | ## Dependencies 24 | 25 | - [@tiptap/react](https://www.npmjs.com/package/@tiptap/react) 26 | - [@tiptap/extension-image](https://tiptap.dev/docs/editor/extensions/nodes/image) 27 | - [Button](https://ui.shadcn.com/docs/components/button) 28 | - [Dropdown](https://ui.shadcn.com/docs/components/dropdown) 29 | - [Separator](https://ui.shadcn.com/docs/components/separator) 30 | -------------------------------------------------------------------------------- /src/registry/registry-exmples.ts: -------------------------------------------------------------------------------- 1 | import type { Registry } from "./schema"; 2 | 3 | export const examples: Registry = [ 4 | { 5 | name: "starter-kit-example", 6 | type: "registry:example", 7 | files: [ 8 | { 9 | path: "examples/starter-kit-example.tsx", 10 | type: "registry:example", 11 | }, 12 | ], 13 | }, 14 | { 15 | name: "search-and-replace-example", 16 | type: "registry:example", 17 | files: [ 18 | { 19 | path: "examples/search-and-replace-example.tsx", 20 | type: "registry:example", 21 | }, 22 | ], 23 | }, 24 | { 25 | name: "color-highlight-example", 26 | type: "registry:example", 27 | files: [ 28 | { 29 | path: "examples/color-highlight-example.tsx", 30 | type: "registry:example", 31 | }, 32 | ], 33 | }, 34 | { 35 | name: "image-placeholder-example", 36 | type: "registry:example", 37 | files: [ 38 | { 39 | path: "examples/image-placeholder-example.tsx", 40 | type: "registry:example", 41 | }, 42 | ], 43 | }, 44 | { 45 | name: "image-extended-example", 46 | type: "registry:example", 47 | files: [ 48 | { 49 | path: "examples/image-extended-example.tsx", 50 | type: "registry:example", 51 | }, 52 | ], 53 | }, 54 | ]; 55 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@tiptap/core"; 2 | import { type ClassValue, clsx } from "clsx"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | 9 | export const NODE_HANDLES_SELECTED_STYLE_CLASSNAME = 10 | "node-handles-selected-style"; 11 | 12 | export function isValidUrl(url: string) { 13 | return /^https?:\/\/\S+$/.test(url); 14 | } 15 | 16 | export const duplicateContent = (editor: Editor) => { 17 | const { view } = editor; 18 | const { state } = view; 19 | const { selection } = state; 20 | 21 | editor 22 | .chain() 23 | .insertContentAt( 24 | selection.to, 25 | selection.content().content.firstChild?.toJSON(), 26 | { 27 | updateSelection: true, 28 | }, 29 | ) 30 | .focus(selection.to) 31 | .run(); 32 | }; 33 | 34 | export function getUrlFromString(str: string) { 35 | if (isValidUrl(str)) { 36 | return str; 37 | } 38 | try { 39 | if (str.includes(".") && !str.includes(" ")) { 40 | return new URL(`https://${str}`).toString(); 41 | } 42 | } catch { 43 | return null; 44 | } 45 | } 46 | 47 | export function absoluteUrl(path: string) { 48 | return `${process.env.NEXT_PUBLIC_APP_URL}${path}`; 49 | } 50 | -------------------------------------------------------------------------------- /src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction to shadcn-tiptap 3 | description: A collection of extensions and toolbars for Tiptap, built with shadcn/ui 4 | --- 5 | 6 | ## Overview 7 | 8 | This project provides: 9 | 10 | 1. A set of custom-built extensions for Tiptap 11 | 2. Ready-to-use toolbars crafted with [shadcn/ui](https://ui.shadcn.com/) components 12 | 3. Easy integration using the [shadcn CLI](https://ui.shadcn.com/docs/cli) 13 | 14 | 15 | 16 | ## Getting Started 17 | 18 | To get started with shadcn-tiptap, please refer to [Installation Guide](/docs/installation). 19 | 20 | ## Contribute 21 | 22 | Any contributions are welcome! Open an issue or a PR if you have any suggestions or improvements on [GitHub](https://github.com/NiazMorshed2007/shadcn-tiptap). 23 | 24 | ## Support 25 | 26 | I appreciate any support you can offer. 27 | 28 | 36 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )); 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 31 | -------------------------------------------------------------------------------- /src/components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MoonIcon, SunIcon } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuTrigger, 10 | } from "./ui/dropdown-menu"; 11 | 12 | export default function ThemeSwitch() { 13 | const { theme, setTheme } = useTheme(); 14 | 15 | return ( 16 | 17 | 18 | 30 | 31 | 32 | setTheme("light")}> 33 | Light 34 | 35 | setTheme("dark")}> 36 | Dark 37 | 38 | setTheme("system")}> 39 | System 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/registry/toolbars/hard-break.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { WrapText } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const HardBreakToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 34 | 35 | 36 | Hard break 37 | 38 | 39 | ); 40 | }, 41 | ); 42 | 43 | HardBreakToolbar.displayName = "HardBreakToolbar"; 44 | 45 | export { HardBreakToolbar }; 46 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md 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", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/registry/toolbars/undo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button, type ButtonProps } from "@/components/ui/button"; 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipTrigger, 8 | } from "@/components/ui/tooltip"; 9 | import { cn } from "@/lib/utils"; 10 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 11 | import { CornerUpLeft } from "lucide-react"; 12 | import React from "react"; 13 | 14 | const UndoToolbar = React.forwardRef( 15 | ({ className, onClick, children, ...props }, ref) => { 16 | const { editor } = useToolbar(); 17 | 18 | return ( 19 | 20 | 21 | 35 | 36 | 37 | Undo 38 | 39 | 40 | ); 41 | }, 42 | ); 43 | 44 | UndoToolbar.displayName = "UndoToolbar"; 45 | 46 | export { UndoToolbar }; 47 | -------------------------------------------------------------------------------- /src/registry/toolbars/redo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CornerUpRight } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const RedoToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | 19 | return ( 20 | 21 | 22 | 36 | 37 | 38 | Redo 39 | 40 | 41 | ); 42 | }, 43 | ); 44 | 45 | RedoToolbar.displayName = "RedoToolbar"; 46 | 47 | export { RedoToolbar }; 48 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/registry/toolbars/horizontal-rule.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SeparatorHorizontal } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const HorizontalRuleToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 34 | 35 | 36 | Horizontal Rule 37 | 38 | 39 | ); 40 | }, 41 | ); 42 | 43 | HorizontalRuleToolbar.displayName = "HorizontalRuleToolbar"; 44 | 45 | export { HorizontalRuleToolbar }; 46 | -------------------------------------------------------------------------------- /src/registry/toolbar-examples/search-and-replace-toolbar-example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import SearchAndReplace from "@/registry/extensions/search-and-replace"; 4 | import { SearchAndReplaceToolbar } from "@/registry/toolbars/search-and-replace-toolbar"; 5 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider"; 6 | import { useEditor } from "@tiptap/react"; 7 | import StarterKit from "@tiptap/starter-kit"; 8 | 9 | const extensions = [ 10 | StarterKit.configure({ 11 | orderedList: { 12 | HTMLAttributes: { 13 | class: "list-decimal", 14 | }, 15 | }, 16 | blockquote: { 17 | HTMLAttributes: { 18 | class: "text-accent-foreground p-2", 19 | }, 20 | }, 21 | bulletList: { 22 | HTMLAttributes: { 23 | class: "list-disc", 24 | }, 25 | }, 26 | heading: { 27 | levels: [1, 2, 3, 4], 28 | HTMLAttributes: { 29 | class: "tiptap-heading", 30 | }, 31 | }, 32 | }), 33 | SearchAndReplace, 34 | ]; 35 | 36 | const SearchReplaceExample = () => { 37 | const editor = useEditor({ 38 | extensions, 39 | immediatelyRender: false, 40 | }); 41 | 42 | if (!editor) { 43 | return null; 44 | } 45 | return ( 46 |
47 | 48 | 49 | 50 |
51 | ); 52 | }; 53 | 54 | export default SearchReplaceExample; 55 | -------------------------------------------------------------------------------- /src/registry/toolbars/code.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Code2 } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const CodeToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 39 | 40 | 41 | Code 42 | 43 | 44 | ); 45 | }, 46 | ); 47 | 48 | CodeToolbar.displayName = "CodeToolbar"; 49 | 50 | export { CodeToolbar }; 51 | -------------------------------------------------------------------------------- /src/registry/toolbars/image-placeholder-toolbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Image } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const ImagePlaceholderToolbar = React.forwardRef< 16 | HTMLButtonElement, 17 | ButtonProps 18 | >(({ className, onClick, children, ...props }, ref) => { 19 | const { editor } = useToolbar(); 20 | return ( 21 | 22 | 23 | 40 | 41 | 42 | Image 43 | 44 | 45 | ); 46 | }); 47 | 48 | ImagePlaceholderToolbar.displayName = "ImagePlaceholderToolbar"; 49 | 50 | export { ImagePlaceholderToolbar }; 51 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor; 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )); 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 34 | -------------------------------------------------------------------------------- /src/components/motion-primitives/border-trail.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import { type Transition, motion } from "framer-motion"; 4 | 5 | type BorderTrailProps = { 6 | className?: string; 7 | size?: number; 8 | transition?: Transition; 9 | delay?: number; 10 | onAnimationComplete?: () => void; 11 | style?: React.CSSProperties; 12 | }; 13 | 14 | export function BorderTrail({ 15 | className, 16 | size = 60, 17 | transition, 18 | delay, 19 | onAnimationComplete, 20 | style, 21 | }: BorderTrailProps) { 22 | const BASE_TRANSITION = { 23 | repeat: Number.POSITIVE_INFINITY, 24 | duration: 5, 25 | ease: "linear", 26 | }; 27 | 28 | return ( 29 |
30 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/registry/toolbars/code-block.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Code, Code2 } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const CodeBlockToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 39 | 40 | 41 | Code Block 42 | 43 | 44 | ); 45 | }, 46 | ); 47 | 48 | CodeBlockToolbar.displayName = "CodeBlockToolbar"; 49 | 50 | export { CodeBlockToolbar }; 51 | -------------------------------------------------------------------------------- /src/registry/toolbars/subscript.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Subscript } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const SubscriptToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 39 | 40 | 41 | Subscript 42 | 43 | 44 | ); 45 | }, 46 | ); 47 | 48 | SubscriptToolbar.displayName = "SubscriptToolbar"; 49 | 50 | export { SubscriptToolbar }; 51 | -------------------------------------------------------------------------------- /src/registry/toolbars/bullet-list.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { List } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const BulletListToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | 19 | return ( 20 | 21 | 22 | 40 | 41 | 42 | Bullet list 43 | 44 | 45 | ); 46 | }, 47 | ); 48 | 49 | BulletListToolbar.displayName = "BulletListToolbar"; 50 | 51 | export { BulletListToolbar }; 52 | -------------------------------------------------------------------------------- /src/registry/toolbars/blockquote.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { TextQuote } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const BlockquoteToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 39 | 40 | 41 | Blockquote 42 | 43 | 44 | ); 45 | }, 46 | ); 47 | 48 | BlockquoteToolbar.displayName = "BlockquoteToolbar"; 49 | 50 | export { BlockquoteToolbar }; 51 | -------------------------------------------------------------------------------- /src/registry/toolbars/superscript.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Superscript } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const SuperscriptToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 39 | 40 | 41 | Superscript 42 | 43 | 44 | ); 45 | }, 46 | ); 47 | 48 | SuperscriptToolbar.displayName = "SuperscriptToolbar"; 49 | 50 | export { SuperscriptToolbar }; 51 | -------------------------------------------------------------------------------- /src/registry/toolbars/ordered-list.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ListOrdered } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const OrderedListToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 39 | 40 | 41 | Ordered list 42 | 43 | 44 | ); 45 | }, 46 | ); 47 | 48 | OrderedListToolbar.displayName = "OrderedListToolbar"; 49 | 50 | export { OrderedListToolbar }; 51 | -------------------------------------------------------------------------------- /src/registry/toolbars/italic.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ItalicIcon } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const ItalicToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 39 | 40 | 41 | Italic 42 | (cmd + i) 43 | 44 | 45 | ); 46 | }, 47 | ); 48 | 49 | ItalicToolbar.displayName = "ItalicToolbar"; 50 | 51 | export { ItalicToolbar }; 52 | -------------------------------------------------------------------------------- /src/registry/toolbar-examples/image-placeholder-toolbar-example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ImageExtension } from "@/registry/extensions/image"; 4 | import { ImagePlaceholder } from "@/registry/extensions/image-placeholder"; 5 | import { ImagePlaceholderToolbar } from "@/registry/toolbars/image-placeholder-toolbar"; 6 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider"; 7 | import { useEditor } from "@tiptap/react"; 8 | import StarterKit from "@tiptap/starter-kit"; 9 | 10 | const extensions = [ 11 | StarterKit.configure({ 12 | orderedList: { 13 | HTMLAttributes: { 14 | class: "list-decimal", 15 | }, 16 | }, 17 | blockquote: { 18 | HTMLAttributes: { 19 | class: "text-accent-foreground p-2", 20 | }, 21 | }, 22 | bulletList: { 23 | HTMLAttributes: { 24 | class: "list-disc", 25 | }, 26 | }, 27 | heading: { 28 | levels: [1, 2, 3, 4], 29 | HTMLAttributes: { 30 | class: "tiptap-heading", 31 | }, 32 | }, 33 | }), 34 | ImageExtension, 35 | ImagePlaceholder, 36 | ]; 37 | 38 | const ImagePlaceholderToolbarExample = () => { 39 | const editor = useEditor({ 40 | extensions, 41 | immediatelyRender: false, 42 | }); 43 | 44 | if (!editor) { 45 | return null; 46 | } 47 | return ( 48 |
49 | 50 | 51 | 52 |
53 | ); 54 | }; 55 | 56 | export default ImagePlaceholderToolbarExample; 57 | -------------------------------------------------------------------------------- /src/registry/toolbars/underline.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { UnderlineIcon } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const UnderlineToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 39 | 40 | 41 | Underline 42 | (cmd + u) 43 | 44 | 45 | ); 46 | }, 47 | ); 48 | 49 | UnderlineToolbar.displayName = "UnderlineToolbar"; 50 | 51 | export { UnderlineToolbar }; 52 | -------------------------------------------------------------------------------- /src/registry/toolbars/strikethrough.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Strikethrough } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | 15 | const StrikeThroughToolbar = React.forwardRef( 16 | ({ className, onClick, children, ...props }, ref) => { 17 | const { editor } = useToolbar(); 18 | return ( 19 | 20 | 21 | 39 | 40 | 41 | Strikethrough 42 | (cmd + shift + x) 43 | 44 | 45 | ); 46 | }, 47 | ); 48 | 49 | StrikeThroughToolbar.displayName = "StrikeThroughToolbar"; 50 | 51 | export { StrikeThroughToolbar }; 52 | -------------------------------------------------------------------------------- /src/registry/toolbar-examples/color-and-highlight-toolbar-example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColorHighlightToolbar } from "@/registry/toolbars/color-and-highlight"; 4 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider"; 5 | import Color from "@tiptap/extension-color"; 6 | import Highlight from "@tiptap/extension-highlight"; 7 | import TextStyle from "@tiptap/extension-text-style"; 8 | import { useEditor } from "@tiptap/react"; 9 | import StarterKit from "@tiptap/starter-kit"; 10 | 11 | const extensions = [ 12 | StarterKit.configure({ 13 | orderedList: { 14 | HTMLAttributes: { 15 | class: "list-decimal", 16 | }, 17 | }, 18 | blockquote: { 19 | HTMLAttributes: { 20 | class: "text-accent-foreground p-2", 21 | }, 22 | }, 23 | bulletList: { 24 | HTMLAttributes: { 25 | class: "list-disc", 26 | }, 27 | }, 28 | heading: { 29 | levels: [1, 2, 3, 4], 30 | HTMLAttributes: { 31 | class: "tiptap-heading", 32 | }, 33 | }, 34 | }), 35 | TextStyle, 36 | Color, 37 | Highlight.configure({ 38 | multicolor: true, 39 | }), 40 | ]; 41 | 42 | const ColorHighlightToolbarExample = () => { 43 | const editor = useEditor({ 44 | extensions, 45 | immediatelyRender: false, 46 | }); 47 | 48 | if (!editor) { 49 | return null; 50 | } 51 | return ( 52 |
53 | 54 | 55 | 56 |
57 | ); 58 | }; 59 | 60 | export default ColorHighlightToolbarExample; 61 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { GitHubLogoIcon, TwitterLogoIcon } from "@radix-ui/react-icons"; 2 | import { Coffee } from "lucide-react"; 3 | import Link from "next/link"; 4 | import { MainNav } from "./main-nav"; 5 | import { MobileNav } from "./mobile-nav"; 6 | import ThemeSwitch from "./theme-switch"; 7 | import { Button } from "./ui/button"; 8 | 9 | export const Header = () => { 10 | return ( 11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 | 24 | 29 | 37 | 38 |
39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/registry/registry-toolbars.ts: -------------------------------------------------------------------------------- 1 | import type { Registry } from "./schema"; 2 | 3 | export const toolbars: Registry = [ 4 | { 5 | name: "toolbar-provider", 6 | type: "registry:block", 7 | description: "A toolbar provider that serves editor to its children.", 8 | dependencies: ["@tiptap/react"], 9 | files: [ 10 | { 11 | path: "toolbars/toolbar-provider.tsx", 12 | type: "registry:component", 13 | target: "components/toolbars/toolbar-provider.tsx", 14 | }, 15 | ], 16 | }, 17 | { 18 | name: "search-and-replace-toolbar", 19 | type: "registry:block", 20 | description: "A toolbar for searching and replacing text in the editor.", 21 | files: [ 22 | { 23 | path: "toolbars/search-and-replace.tsx", 24 | type: "registry:component", 25 | target: "components/toolbars/search-and-replace.tsx", 26 | }, 27 | ], 28 | }, 29 | { 30 | name: "image-placeholder-toolbar", 31 | type: "registry:component", 32 | description: "A toolbar for adding image placeholder.", 33 | files: [ 34 | { 35 | path: "toolbars/image-placeholder-toolbar.tsx", 36 | type: "registry:component", 37 | target: "components/toolbars/image-placeholder-toolbar.tsx", 38 | }, 39 | ], 40 | }, 41 | { 42 | name: "color-and-highlight-toolbar", 43 | type: "registry:component", 44 | description: "A toolbar for adding color and highlight.", 45 | files: [ 46 | { 47 | path: "toolbars/color-and-highlight.tsx", 48 | type: "registry:component", 49 | target: "components/toolbars/color-and-highlight.tsx", 50 | }, 51 | ], 52 | }, 53 | ]; 54 | -------------------------------------------------------------------------------- /public/r/search-and-replace-toolbar-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search-and-replace-toolbar-example", 3 | "type": "registry:example", 4 | "files": [ 5 | { 6 | "path": "toolbar-examples/search-and-replace-toolbar-example.tsx", 7 | "type": "registry:example", 8 | "content": "\"use client\";\n\nimport SearchAndReplace from \"@/components/extensions/search-and-replace\";\nimport { SearchAndReplaceToolbar } from \"@/components/toolbars/search-and-replace-toolbar\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport { useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\tblockquote: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"text-accent-foreground p-2\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t}),\n\tSearchAndReplace,\n];\n\nconst SearchReplaceExample = () => {\n\tconst editor = useEditor({\n\t\textensions,\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t
\n\t);\n};\n\nexport default SearchReplaceExample;\n", 9 | "target": "components/search-and-replace-toolbar-example.tsx" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /src/registry/toolbars/bold.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { BoldIcon } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | import type { Extension } from "@tiptap/core"; 15 | import type { StarterKitOptions } from "@tiptap/starter-kit"; 16 | 17 | type StarterKitExtensions = Extension; 18 | 19 | const BoldToolbar = React.forwardRef( 20 | ({ className, onClick, children, ...props }, ref) => { 21 | const { editor } = useToolbar(); 22 | return ( 23 | 24 | 25 | 43 | 44 | 45 | Bold 46 | (cmd + b) 47 | 48 | 49 | ); 50 | }, 51 | ); 52 | 53 | BoldToolbar.displayName = "BoldToolbar"; 54 | 55 | export { BoldToolbar }; 56 | -------------------------------------------------------------------------------- /src/__registry__/toolbars/bold.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { BoldIcon } from "lucide-react"; 4 | import React from "react"; 5 | 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger, 11 | } from "@/components/ui/tooltip"; 12 | import { cn } from "@/lib/utils"; 13 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 14 | import type { Extension } from "@tiptap/core"; 15 | import type { StarterKitOptions } from "@tiptap/starter-kit"; 16 | 17 | type StarterKitExtensions = Extension; 18 | 19 | const BoldToolbar = React.forwardRef( 20 | ({ className, onClick, children, ...props }, ref) => { 21 | const { editor } = useToolbar(); 22 | return ( 23 | 24 | 25 | 43 | 44 | 45 | Bold 46 | (cmd + b) 47 | 48 | 49 | ); 50 | }, 51 | ); 52 | 53 | BoldToolbar.displayName = "BoldToolbar"; 54 | 55 | export { BoldToolbar }; 56 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then 4 | set -x 5 | fi 6 | 7 | if [ "$LEFTHOOK" = "0" ]; then 8 | exit 0 9 | fi 10 | 11 | call_lefthook() 12 | { 13 | dir="$(git rev-parse --show-toplevel)" 14 | osArch=$(uname | tr '[:upper:]' '[:lower:]') 15 | cpuArch=$(uname -m | sed 's/aarch64/arm64/') 16 | 17 | if test -n "$LEFTHOOK_BIN" 18 | then 19 | "$LEFTHOOK_BIN" "$@" 20 | elif lefthook -h >/dev/null 2>&1 21 | then 22 | lefthook "$@" 23 | elif test -f "$dir/node_modules/lefthook/bin/index.js" 24 | then 25 | "$dir/node_modules/lefthook/bin/index.js" "$@" 26 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" 27 | then 28 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@" 29 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" 30 | then 31 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@" 32 | 33 | elif bundle exec lefthook -h >/dev/null 2>&1 34 | then 35 | bundle exec lefthook "$@" 36 | elif yarn lefthook -h >/dev/null 2>&1 37 | then 38 | yarn lefthook "$@" 39 | elif pnpm lefthook -h >/dev/null 2>&1 40 | then 41 | pnpm lefthook "$@" 42 | elif swift package plugin lefthook >/dev/null 2>&1 43 | then 44 | swift package --disable-sandbox plugin lefthook "$@" 45 | elif command -v npx >/dev/null 2>&1 46 | then 47 | npx lefthook "$@" 48 | else 49 | echo "Can't find lefthook in PATH" 50 | fi 51 | } 52 | 53 | call_lefthook run "commit-msg" "$@" 54 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then 4 | set -x 5 | fi 6 | 7 | if [ "$LEFTHOOK" = "0" ]; then 8 | exit 0 9 | fi 10 | 11 | call_lefthook() 12 | { 13 | dir="$(git rev-parse --show-toplevel)" 14 | osArch=$(uname | tr '[:upper:]' '[:lower:]') 15 | cpuArch=$(uname -m | sed 's/aarch64/arm64/') 16 | 17 | if test -n "$LEFTHOOK_BIN" 18 | then 19 | "$LEFTHOOK_BIN" "$@" 20 | elif lefthook -h >/dev/null 2>&1 21 | then 22 | lefthook "$@" 23 | elif test -f "$dir/node_modules/lefthook/bin/index.js" 24 | then 25 | "$dir/node_modules/lefthook/bin/index.js" "$@" 26 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" 27 | then 28 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@" 29 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" 30 | then 31 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@" 32 | 33 | elif bundle exec lefthook -h >/dev/null 2>&1 34 | then 35 | bundle exec lefthook "$@" 36 | elif yarn lefthook -h >/dev/null 2>&1 37 | then 38 | yarn lefthook "$@" 39 | elif pnpm lefthook -h >/dev/null 2>&1 40 | then 41 | pnpm lefthook "$@" 42 | elif swift package plugin lefthook >/dev/null 2>&1 43 | then 44 | swift package --disable-sandbox plugin lefthook "$@" 45 | elif command -v npx >/dev/null 2>&1 46 | then 47 | npx lefthook "$@" 48 | else 49 | echo "Can't find lefthook in PATH" 50 | fi 51 | } 52 | 53 | call_lefthook run "pre-commit" "$@" 54 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then 4 | set -x 5 | fi 6 | 7 | if [ "$LEFTHOOK" = "0" ]; then 8 | exit 0 9 | fi 10 | 11 | call_lefthook() 12 | { 13 | dir="$(git rev-parse --show-toplevel)" 14 | osArch=$(uname | tr '[:upper:]' '[:lower:]') 15 | cpuArch=$(uname -m | sed 's/aarch64/arm64/') 16 | 17 | if test -n "$LEFTHOOK_BIN" 18 | then 19 | "$LEFTHOOK_BIN" "$@" 20 | elif lefthook -h >/dev/null 2>&1 21 | then 22 | lefthook "$@" 23 | elif test -f "$dir/node_modules/lefthook/bin/index.js" 24 | then 25 | "$dir/node_modules/lefthook/bin/index.js" "$@" 26 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" 27 | then 28 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@" 29 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" 30 | then 31 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@" 32 | 33 | elif bundle exec lefthook -h >/dev/null 2>&1 34 | then 35 | bundle exec lefthook "$@" 36 | elif yarn lefthook -h >/dev/null 2>&1 37 | then 38 | yarn lefthook "$@" 39 | elif pnpm lefthook -h >/dev/null 2>&1 40 | then 41 | pnpm lefthook "$@" 42 | elif swift package plugin lefthook >/dev/null 2>&1 43 | then 44 | swift package --disable-sandbox plugin lefthook "$@" 45 | elif command -v npx >/dev/null 2>&1 46 | then 47 | npx lefthook "$@" 48 | else 49 | echo "Can't find lefthook in PATH" 50 | fi 51 | } 52 | 53 | call_lefthook run "prepare-commit-msg" "$@" 54 | -------------------------------------------------------------------------------- /src/components/code-block-wrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Collapsible, 9 | CollapsibleContent, 10 | CollapsibleTrigger, 11 | } from "@/components/ui/collapsible"; 12 | 13 | interface CodeBlockProps extends React.HTMLAttributes { 14 | expandButtonTitle?: string; 15 | } 16 | 17 | export function CodeBlockWrapper({ 18 | expandButtonTitle = "View Code", 19 | className, 20 | children, 21 | ...props 22 | }: CodeBlockProps) { 23 | const [isOpened, setIsOpened] = React.useState(false); 24 | 25 | return ( 26 | 27 |
28 | 32 |
38 | {children} 39 |
40 |
41 |
47 | 48 | 51 | 52 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /public/r/image-placeholder-toolbar-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-placeholder-toolbar-example", 3 | "type": "registry:example", 4 | "files": [ 5 | { 6 | "path": "toolbar-examples/image-placeholder-toolbar-example.tsx", 7 | "type": "registry:example", 8 | "content": "\"use client\";\n\nimport { ImageExtension } from \"@/components/extensions/image\";\nimport { ImagePlaceholder } from \"@/components/extensions/image-placeholder\";\nimport { ImagePlaceholderToolbar } from \"@/components/toolbars/image-placeholder-toolbar\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport { useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\tblockquote: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"text-accent-foreground p-2\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t}),\n\tImageExtension,\n\tImagePlaceholder,\n];\n\nconst ImagePlaceholderToolbarExample = () => {\n\tconst editor = useEditor({\n\t\textensions,\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t
\n\t);\n};\n\nexport default ImagePlaceholderToolbarExample;\n", 9 | "target": "components/image-placeholder-toolbar-example.tsx" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /public/r/color-and-highlight-toolbar-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color-and-highlight-toolbar-example", 3 | "type": "registry:example", 4 | "files": [ 5 | { 6 | "path": "toolbar-examples/color-and-highlight-toolbar-example.tsx", 7 | "type": "registry:example", 8 | "content": "\"use client\";\n\nimport { ColorHighlightToolbar } from \"@/components/toolbars/color-and-highlight\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport Color from \"@tiptap/extension-color\";\nimport Highlight from \"@tiptap/extension-highlight\";\nimport TextStyle from \"@tiptap/extension-text-style\";\nimport { useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\tblockquote: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"text-accent-foreground p-2\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t}),\n\tTextStyle,\n\tColor,\n\tHighlight.configure({\n\t\tmulticolor: true,\n\t}),\n];\n\nconst ColorHighlightToolbarExample = () => {\n\tconst editor = useEditor({\n\t\textensions,\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t
\n\t);\n};\n\nexport default ColorHighlightToolbarExample;\n", 9 | "target": "components/color-and-highlight-toolbar-example.tsx" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /src/styles/mdx.css: -------------------------------------------------------------------------------- 1 | [data-theme="light"] { 2 | display: block; 3 | } 4 | 5 | [data-theme="dark"] { 6 | display: none; 7 | } 8 | 9 | .dark [data-theme="light"] { 10 | display: none; 11 | } 12 | 13 | .dark [data-theme="dark"] { 14 | display: block; 15 | } 16 | 17 | [data-rehype-pretty-code-fragment] { 18 | @apply relative text-white; 19 | } 20 | 21 | [data-rehype-pretty-code-fragment] code { 22 | @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0; 23 | counter-reset: line; 24 | box-decoration-break: clone; 25 | } 26 | 27 | [data-rehype-pretty-code-fragment] .line { 28 | @apply px-4 min-h-[1rem] py-0.5 w-full inline-block; 29 | } 30 | 31 | [data-rehype-pretty-code-fragment] [data-line-numbers] .line { 32 | @apply px-2; 33 | } 34 | 35 | [data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before { 36 | @apply text-zinc-50/40 text-xs; 37 | counter-increment: line; 38 | content: counter(line); 39 | display: inline-block; 40 | width: 1.8rem; 41 | margin-right: 1.4rem; 42 | text-align: right; 43 | } 44 | 45 | [data-rehype-pretty-code-fragment] .line--highlighted { 46 | @apply bg-zinc-700/50; 47 | } 48 | 49 | [data-rehype-pretty-code-fragment] .line-highlighted span { 50 | @apply relative; 51 | } 52 | 53 | [data-rehype-pretty-code-fragment] .word--highlighted { 54 | @apply rounded-md bg-zinc-700/50 border-zinc-700/70 p-1; 55 | } 56 | 57 | .dark [data-rehype-pretty-code-fragment] .word--highlighted { 58 | @apply bg-zinc-900; 59 | } 60 | 61 | [data-rehype-pretty-code-title] { 62 | @apply mt-2 pt-6 px-4 text-sm font-medium text-foreground; 63 | } 64 | 65 | [data-rehype-pretty-code-title] + pre { 66 | @apply mt-2; 67 | } 68 | 69 | .mdx > .steps:first-child > h3:first-child { 70 | @apply mt-0; 71 | } 72 | 73 | .steps > h3 { 74 | @apply mt-8 mb-4 text-base font-semibold; 75 | } 76 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /src/lib/toc.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // TODO: I'll fix this later. 3 | 4 | import { toc } from "mdast-util-toc"; 5 | import { remark } from "remark"; 6 | import { visit } from "unist-util-visit"; 7 | 8 | const textTypes = ["text", "emphasis", "strong", "inlineCode"]; 9 | 10 | function flattenNode(node) { 11 | const p = []; 12 | visit(node, (node) => { 13 | if (!textTypes.includes(node.type)) { 14 | return; 15 | } 16 | p.push(node.value); 17 | }); 18 | return p.join(""); 19 | } 20 | 21 | interface Item { 22 | title: string; 23 | url: string; 24 | items?: Item[]; 25 | } 26 | 27 | interface Items { 28 | items?: Item[]; 29 | } 30 | 31 | function getItems(node, current): Items { 32 | if (!node) { 33 | return {}; 34 | } 35 | 36 | if (node.type === "paragraph") { 37 | visit(node, (item) => { 38 | if (item.type === "link") { 39 | current.url = item.url; 40 | current.title = flattenNode(node); 41 | } 42 | 43 | if (item.type === "text") { 44 | current.title = flattenNode(node); 45 | } 46 | }); 47 | 48 | return current; 49 | } 50 | 51 | if (node.type === "list") { 52 | current.items = node.children.map((i) => getItems(i, {})); 53 | 54 | return current; 55 | } 56 | if (node.type === "listItem") { 57 | const heading = getItems(node.children[0], {}); 58 | 59 | if (node.children.length > 1) { 60 | getItems(node.children[1], heading); 61 | } 62 | 63 | return heading; 64 | } 65 | 66 | return {}; 67 | } 68 | 69 | const getToc = () => (node, file) => { 70 | const table = toc(node); 71 | const items = getItems(table.map, {}); 72 | 73 | file.data = items; 74 | }; 75 | 76 | export type TableOfContents = Items; 77 | 78 | export async function getTableOfContents( 79 | content: string, 80 | ): Promise { 81 | const result = await remark().use(getToc).process(content); 82 | 83 | return result.data; 84 | } 85 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /src/config/docs.ts: -------------------------------------------------------------------------------- 1 | import type { MainNavItem, SidebarNavItem } from "@/types/nav"; 2 | 3 | export interface DocsConfig { 4 | mainNav: MainNavItem[]; 5 | sidebarNav: SidebarNavItem[]; 6 | } 7 | 8 | export const docsConfig: DocsConfig = { 9 | mainNav: [ 10 | { 11 | title: "Documentation", 12 | href: "/docs", 13 | }, 14 | { 15 | title: "Extensions", 16 | href: "/docs/extensions/search-and-replace", 17 | }, 18 | ], 19 | sidebarNav: [ 20 | { 21 | title: "Getting Started", 22 | items: [ 23 | { 24 | title: "Introduction", 25 | href: "/docs", 26 | items: [], 27 | }, 28 | { 29 | title: "Installation", 30 | href: "/docs/installation", 31 | items: [], 32 | }, 33 | ], 34 | }, 35 | { 36 | title: "Extensions", 37 | items: [ 38 | { 39 | title: "StarterKit", 40 | href: "/docs/extensions/starter-kit", 41 | items: [], 42 | }, 43 | { 44 | title: "Search & Replace", 45 | href: "/docs/extensions/search-and-replace", 46 | items: [], 47 | label: "Custom", 48 | }, 49 | { 50 | title: "Image (extended)", 51 | href: "/docs/extensions/image", 52 | items: [], 53 | }, 54 | { 55 | title: "Image placeholder", 56 | href: "/docs/extensions/image-placeholder", 57 | items: [], 58 | label: "Custom", 59 | }, 60 | { 61 | title: "Color and Highlight", 62 | href: "/docs/extensions/color-and-highlight", 63 | items: [], 64 | }, 65 | ], 66 | }, 67 | { 68 | title: "Toolbars", 69 | items: [ 70 | { 71 | title: "Search & Replace", 72 | href: "/docs/toolbars/search-and-replace", 73 | items: [], 74 | }, 75 | { 76 | title: "Image placeholder", 77 | href: "/docs/toolbars/image-placeholder", 78 | items: [], 79 | }, 80 | { 81 | title: "Color and Highlight", 82 | href: "/docs/toolbars/color-and-highlight", 83 | items: [], 84 | }, 85 | ], 86 | }, 87 | ], 88 | }; 89 | -------------------------------------------------------------------------------- /src/components/pager.tsx: -------------------------------------------------------------------------------- 1 | import type { NavItem, NavItemWithChildren } from "@/types/nav"; 2 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"; 3 | import type { Doc } from "contentlayer/generated"; 4 | import Link from "next/link"; 5 | 6 | import { docsConfig } from "@/config/docs"; 7 | import { cn } from "@/lib/utils"; 8 | import { buttonVariants } from "./ui/button"; 9 | 10 | interface DocsPagerProps { 11 | doc: Doc; 12 | } 13 | 14 | export function DocsPager({ doc }: DocsPagerProps) { 15 | const pager = getPagerForDoc(doc); 16 | 17 | if (!pager) { 18 | return null; 19 | } 20 | 21 | return ( 22 |
23 | {pager?.prev?.href && ( 24 | 28 | 29 | {pager.prev.title} 30 | 31 | )} 32 | {pager?.next?.href && ( 33 | 37 | {pager.next.title} 38 | 39 | 40 | )} 41 |
42 | ); 43 | } 44 | 45 | export function getPagerForDoc(doc: Doc) { 46 | const nav = docsConfig.sidebarNav; 47 | const flattenedLinks = [null, ...flatten(nav), null]; 48 | const activeIndex = flattenedLinks.findIndex( 49 | (link) => doc.slug === link?.href, 50 | ); 51 | const prev = activeIndex !== 0 ? flattenedLinks[activeIndex - 1] : null; 52 | const next = 53 | activeIndex !== flattenedLinks.length - 1 54 | ? flattenedLinks[activeIndex + 1] 55 | : null; 56 | return { 57 | prev, 58 | next, 59 | }; 60 | } 61 | 62 | export function flatten(links: NavItemWithChildren[]): NavItem[] { 63 | return links 64 | .reduce((flat, link) => { 65 | return flat.concat(link.items?.length ? flatten(link.items) : link); 66 | }, []) 67 | .filter((link) => !link?.disabled); 68 | } 69 | -------------------------------------------------------------------------------- /src/registry/examples/search-and-replace-example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import SearchAndReplace from "@/registry/extensions/search-and-replace"; 4 | import { SearchAndReplaceToolbar } from "@/registry/toolbars/search-and-replace-toolbar"; 5 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider"; 6 | import { EditorContent, useEditor } from "@tiptap/react"; 7 | import StarterKit from "@tiptap/starter-kit"; 8 | 9 | const extensions = [ 10 | StarterKit.configure({ 11 | orderedList: { 12 | HTMLAttributes: { 13 | class: "list-decimal", 14 | }, 15 | }, 16 | blockquote: { 17 | HTMLAttributes: { 18 | class: "text-accent-foreground p-2", 19 | }, 20 | }, 21 | bulletList: { 22 | HTMLAttributes: { 23 | class: "list-disc", 24 | }, 25 | }, 26 | heading: { 27 | levels: [1, 2, 3, 4], 28 | HTMLAttributes: { 29 | class: "tiptap-heading", 30 | }, 31 | }, 32 | }), 33 | SearchAndReplace, 34 | ]; 35 | 36 | const content = ` 37 |

Try searching for the word "the".

38 |

"The only thing we have to fear is fear itself." - Franklin D. Roosevelt

39 |

The quick brown fox jumps over the lazy dog.

40 |

In the end, we only regret the chances we didn't take.

`; 41 | 42 | const SearchReplaceExample = () => { 43 | const editor = useEditor({ 44 | extensions, 45 | content, 46 | immediatelyRender: false, 47 | }); 48 | 49 | if (!editor) { 50 | return null; 51 | } 52 | return ( 53 |
54 |
55 | 56 | 57 | 58 |
59 |
{ 61 | editor?.chain().focus().run(); 62 | }} 63 | className="cursor-text min-h-[18rem] bg-background" 64 | > 65 | 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default SearchReplaceExample; 72 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /src/lib/rehype-npm-command.ts: -------------------------------------------------------------------------------- 1 | import type { UnistNode, UnistTree } from "@/types/unist"; 2 | import { visit } from "unist-util-visit"; 3 | 4 | export function rehypeNpmCommand() { 5 | return (tree: UnistTree) => { 6 | // @ts-ignore 7 | visit(tree, (node: UnistNode) => { 8 | if (node.type !== "element" || node?.tagName !== "pre") { 9 | return; 10 | } 11 | 12 | // npm install. 13 | if (node.properties?.["__rawString__"]?.startsWith("npm install")) { 14 | const npmCommand = node.properties?.["__rawString__"]; 15 | node.properties["__npmCommand__"] = npmCommand; 16 | node.properties["__yarnCommand__"] = npmCommand.replace( 17 | "npm install", 18 | "yarn add", 19 | ); 20 | node.properties["__pnpmCommand__"] = npmCommand.replace( 21 | "npm install", 22 | "pnpm add", 23 | ); 24 | node.properties["__bunCommand__"] = npmCommand.replace( 25 | "npm install", 26 | "bun add", 27 | ); 28 | } 29 | 30 | // npx create. 31 | if (node.properties?.["__rawString__"]?.startsWith("npx create-")) { 32 | const npmCommand = node.properties?.["__rawString__"]; 33 | node.properties["__npmCommand__"] = npmCommand; 34 | node.properties["__yarnCommand__"] = npmCommand.replace( 35 | "npx create-", 36 | "yarn create ", 37 | ); 38 | node.properties["__pnpmCommand__"] = npmCommand.replace( 39 | "npx create-", 40 | "pnpm create ", 41 | ); 42 | node.properties["__bunCommand__"] = npmCommand.replace( 43 | "npx", 44 | "bunx --bun", 45 | ); 46 | } 47 | 48 | // npx. 49 | if ( 50 | node.properties?.["__rawString__"]?.startsWith("npx") && 51 | !node.properties?.["__rawString__"]?.startsWith("npx create-") 52 | ) { 53 | const npmCommand = node.properties?.["__rawString__"]; 54 | node.properties["__npmCommand__"] = npmCommand; 55 | node.properties["__yarnCommand__"] = npmCommand; 56 | node.properties["__pnpmCommand__"] = npmCommand.replace( 57 | "npx", 58 | "pnpm dlx", 59 | ); 60 | node.properties["__bunCommand__"] = npmCommand.replace( 61 | "npx", 62 | "bunx --bun", 63 | ); 64 | } 65 | }); 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /src/registry/examples/image-placeholder-example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ImageExtension } from "@/registry/extensions/image"; 4 | import { ImagePlaceholder } from "@/registry/extensions/image-placeholder"; 5 | import { ImagePlaceholderToolbar } from "@/registry/toolbars/image-placeholder-toolbar"; 6 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider"; 7 | import TextAlign from "@tiptap/extension-text-align"; 8 | import { EditorContent, useEditor } from "@tiptap/react"; 9 | import StarterKit from "@tiptap/starter-kit"; 10 | 11 | const extensions = [ 12 | StarterKit.configure({ 13 | orderedList: { 14 | HTMLAttributes: { 15 | class: "list-decimal", 16 | }, 17 | }, 18 | blockquote: { 19 | HTMLAttributes: { 20 | class: " text-accent p-2", 21 | }, 22 | }, 23 | bulletList: { 24 | HTMLAttributes: { 25 | class: "list-disc", 26 | }, 27 | }, 28 | heading: { 29 | levels: [1, 2, 3, 4], 30 | HTMLAttributes: { 31 | class: "tiptap-heading", 32 | }, 33 | }, 34 | }), 35 | TextAlign.configure({ 36 | types: ["heading", "paragraph"], 37 | }), 38 | ImageExtension, 39 | ImagePlaceholder, 40 | ]; 41 | 42 | const content = ` 43 |

A notion style image placeholder.

44 |

Try adding a image here by clicking the media icon 👆

45 | `; 46 | 47 | const ImagePlaceholderPlayground = () => { 48 | const editor = useEditor({ 49 | extensions, 50 | content, 51 | immediatelyRender: false, 52 | }); 53 | 54 | if (!editor) { 55 | return null; 56 | } 57 | return ( 58 |
59 |
60 | 61 | 62 | 63 |
64 |
{ 66 | editor?.chain().focus().run(); 67 | }} 68 | className="cursor-text min-h-[18rem] bg-background" 69 | > 70 | 71 |
72 |
73 | ); 74 | }; 75 | 76 | export default ImagePlaceholderPlayground; 77 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDownIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 56 | 57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 58 | -------------------------------------------------------------------------------- /public/r/search-and-replace-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search-and-replace-example", 3 | "type": "registry:example", 4 | "files": [ 5 | { 6 | "path": "examples/search-and-replace-example.tsx", 7 | "type": "registry:example", 8 | "content": "\"use client\";\n\nimport SearchAndReplace from \"@/components/extensions/search-and-replace\";\nimport { SearchAndReplaceToolbar } from \"@/components/toolbars/search-and-replace-toolbar\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport { EditorContent, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\tblockquote: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"text-accent-foreground p-2\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t}),\n\tSearchAndReplace,\n];\n\nconst content = `\n

Try searching for the word \"the\".

\n

\"The only thing we have to fear is fear itself.\" - Franklin D. Roosevelt

\n

The quick brown fox jumps over the lazy dog.

\n

In the end, we only regret the chances we didn't take.

`;\n\nconst SearchReplaceExample = () => {\n\tconst editor = useEditor({\n\t\textensions,\n\t\tcontent,\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\t\t\t {\n\t\t\t\t\teditor?.chain().focus().run();\n\t\t\t\t}}\n\t\t\t\tclassName=\"cursor-text min-h-[18rem] bg-background\"\n\t\t\t>\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n};\n\nexport default SearchReplaceExample;\n", 9 | "target": "components/search-and-replace-example.tsx" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /public/r/image-placeholder-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-placeholder-example", 3 | "type": "registry:example", 4 | "files": [ 5 | { 6 | "path": "examples/image-placeholder-example.tsx", 7 | "type": "registry:example", 8 | "content": "\"use client\";\n\nimport { ImageExtension } from \"@/components/extensions/image\";\nimport { ImagePlaceholder } from \"@/components/extensions/image-placeholder\";\nimport { ImagePlaceholderToolbar } from \"@/components/toolbars/image-placeholder-toolbar\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport TextAlign from \"@tiptap/extension-text-align\";\nimport { EditorContent, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\tblockquote: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \" text-accent p-2\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t}),\n\tTextAlign.configure({\n\t\ttypes: [\"heading\", \"paragraph\"],\n\t}),\n\tImageExtension,\n\tImagePlaceholder,\n];\n\nconst content = `\n

A notion style image placeholder.

\n

Try adding a image here by clicking the media icon 👆

\n`;\n\nconst ImagePlaceholderPlayground = () => {\n\tconst editor = useEditor({\n\t\textensions,\n\t\tcontent,\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\t\t\t {\n\t\t\t\t\teditor?.chain().focus().run();\n\t\t\t\t}}\n\t\t\t\tclassName=\"cursor-text min-h-[18rem] bg-background\"\n\t\t\t>\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n};\n\nexport default ImagePlaceholderPlayground;\n", 9 | "target": "components/image-placeholder-example.tsx" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{ts,tsx}", 7 | "./components/**/*.{ts,tsx}", 8 | "./app/**/*.{ts,tsx}", 9 | "./src/**/*.{ts,tsx}", 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { 64 | height: "0", 65 | }, 66 | to: { 67 | height: "var(--radix-accordion-content-height)", 68 | }, 69 | }, 70 | "accordion-up": { 71 | from: { 72 | height: "var(--radix-accordion-content-height)", 73 | }, 74 | to: { 75 | height: "0", 76 | }, 77 | }, 78 | }, 79 | animation: { 80 | "accordion-down": "accordion-down 0.2s ease-out", 81 | "accordion-up": "accordion-up 0.2s ease-out", 82 | }, 83 | }, 84 | }, 85 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], 86 | } satisfies Config; 87 | 88 | export default config; 89 | -------------------------------------------------------------------------------- /src/content/docs/toolbars/image-placeholder.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Image placeholder toolbar 3 | description: A toolbar for adding image placeholder. 4 | --- 5 | 6 | 7 | **Note:** This toolbar requires the [Image placeholder](/docs/extensions/image-placeholder) extension to be installed manually. 8 | 9 | 10 | 11 | 12 | 13 | ## Installation 14 | 15 | 16 | 17 | 18 | CLI 19 | Manual 20 | 21 | 22 | 23 | ```bash 24 | npx shadcn add https://tiptap.niazmorshed.dev/r/image-placeholder-toolbar.json 25 | ``` 26 | 27 | 28 | 29 | Install [Button](https://ui.shadcn.com/docs/components/button), [Tooltip](https://ui.shadcn.com/docs/components/tooltip) components from @shadcn/ui 30 | Copy & paste the following code under `components/toolbars/image-placeholder-toolbar.tsx`. 31 | ```jsx 32 | "use client"; 33 | 34 | import { Image } from "lucide-react"; 35 | import React from "react"; 36 | 37 | import { Button, type ButtonProps } from "@/components/ui/button"; 38 | import { 39 | Tooltip, 40 | TooltipContent, 41 | TooltipTrigger, 42 | } from "@/components/ui/tooltip"; 43 | import { cn } from "@/lib/utils"; 44 | import { useToolbar } from "@/components/toolbars/toolbar-provider"; 45 | 46 | const ImagePlaceholderToolbar = React.forwardRef< 47 | HTMLButtonElement, 48 | ButtonProps 49 | >(({ className, onClick, children, ...props }, ref) => { 50 | const { editor } = useToolbar(); 51 | return ( 52 | 53 | 54 | 71 | 72 | 73 | Image 74 | 75 | 76 | ); 77 | }); 78 | 79 | ImagePlaceholderToolbar.displayName = "ImagePlaceholderToolbar"; 80 | 81 | export { ImagePlaceholderToolbar }; 82 | 83 | ``` 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/registry/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const blockChunkSchema = z.object({ 4 | name: z.string(), 5 | description: z.string(), 6 | component: z.any(), 7 | file: z.string(), 8 | code: z.string().optional(), 9 | container: z 10 | .object({ 11 | className: z.string().nullish(), 12 | }) 13 | .optional(), 14 | }); 15 | 16 | export const registryItemTypeSchema = z.enum([ 17 | "registry:lib", 18 | "registry:example", 19 | "registry:block", 20 | "registry:component", 21 | "registry:hook", 22 | "registry:page", 23 | ]); 24 | 25 | const registryItemFileSchema = z.object({ 26 | path: z.string(), 27 | type: registryItemTypeSchema, 28 | content: z.string().optional(), 29 | target: z.string().optional(), 30 | }); 31 | 32 | export const registryItemTailwindSchema = z.object({ 33 | config: z.object({ 34 | content: z.array(z.string()).optional(), 35 | theme: z.record(z.string(), z.any()).optional(), 36 | plugins: z.array(z.string()).optional(), 37 | }), 38 | }); 39 | 40 | export const registryItemCssVarsSchema = z.object({ 41 | light: z.record(z.string(), z.string()).optional(), 42 | dark: z.record(z.string(), z.string()).optional(), 43 | }); 44 | 45 | export const registryEntrySchema = z.object({ 46 | name: z.string(), 47 | type: registryItemTypeSchema, 48 | description: z.string().optional(), 49 | dependencies: z.array(z.string()).optional(), 50 | devDependencies: z.array(z.string()).optional(), 51 | registryDependencies: z.array(z.string()).optional(), 52 | files: z.array(registryItemFileSchema).optional(), 53 | tailwind: registryItemTailwindSchema.optional(), 54 | cssVars: registryItemCssVarsSchema.optional(), 55 | source: z.string().optional(), 56 | category: z.string().optional(), 57 | subcategory: z.string().optional(), 58 | chunks: z.array(blockChunkSchema).optional(), 59 | docs: z.string().optional(), 60 | }); 61 | 62 | export const registrySchema = z.array(registryEntrySchema); 63 | 64 | export type RegistryEntry = z.infer; 65 | 66 | export type Registry = z.infer; 67 | 68 | export const blockSchema = registryEntrySchema.extend({ 69 | type: z.literal("registry:block"), 70 | component: z.any(), 71 | container: z 72 | .object({ 73 | height: z.string().nullish(), 74 | className: z.string().nullish(), 75 | }) 76 | .optional(), 77 | code: z.string(), 78 | highlightedCode: z.string(), 79 | }); 80 | 81 | export type Block = z.infer; 82 | 83 | export type BlockChunk = z.infer; 84 | -------------------------------------------------------------------------------- /src/components/sidebar-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { SidebarNavItem } from "@/types/nav"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | import type { DocsConfig } from "@/config/docs"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | export interface DocsSidebarNavProps { 11 | config: DocsConfig; 12 | } 13 | 14 | export function DocsSidebarNav({ config }: DocsSidebarNavProps) { 15 | const pathname = usePathname(); 16 | 17 | const items = config.sidebarNav; 18 | 19 | return items.length ? ( 20 |
21 | {items.map((item, index) => ( 22 |
23 |

24 | {item.title} 25 |

26 | {item?.items?.length && ( 27 | 28 | )} 29 |
30 | ))} 31 |
32 | ) : null; 33 | } 34 | 35 | interface DocsSidebarNavItemsProps { 36 | items: SidebarNavItem[]; 37 | pathname: string | null; 38 | } 39 | 40 | export function DocsSidebarNavItems({ 41 | items, 42 | pathname, 43 | }: DocsSidebarNavItemsProps) { 44 | return items?.length ? ( 45 |
46 | {items.map((item, index) => 47 | item.href && !item.disabled ? ( 48 | 61 | {item.title} 62 | {item.label && ( 63 | 64 | {item.label} 65 | 66 | )} 67 | 68 | ) : ( 69 | 76 | {item.title} 77 | {item.label && ( 78 | 79 | {item.label} 80 | 81 | )} 82 | 83 | ), 84 | )} 85 |
86 | ) : null; 87 | } 88 | -------------------------------------------------------------------------------- /src/registry/examples/color-highlight-example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColorHighlightToolbar } from "@/registry/toolbars/color-and-highlight"; 4 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider"; 5 | import Color from "@tiptap/extension-color"; 6 | import Highlight from "@tiptap/extension-highlight"; 7 | import TextAlign from "@tiptap/extension-text-align"; 8 | import TextStyle from "@tiptap/extension-text-style"; 9 | import { EditorContent, useEditor } from "@tiptap/react"; 10 | import StarterKit from "@tiptap/starter-kit"; 11 | 12 | const extensions = [ 13 | StarterKit.configure({ 14 | orderedList: { 15 | HTMLAttributes: { 16 | class: "list-decimal", 17 | }, 18 | }, 19 | heading: { 20 | levels: [1, 2, 3, 4], 21 | HTMLAttributes: { 22 | class: "tiptap-heading", 23 | }, 24 | }, 25 | blockquote: { 26 | HTMLAttributes: { 27 | class: "text-accent-foreground p-2", 28 | }, 29 | }, 30 | bulletList: { 31 | HTMLAttributes: { 32 | class: "list-disc", 33 | }, 34 | }, 35 | }), 36 | TextAlign.configure({ 37 | types: ["heading", "paragraph"], 38 | }), 39 | TextStyle, 40 | Color, 41 | Highlight.configure({ 42 | multicolor: true, 43 | }), 44 | ]; 45 | 46 | const content = ` 47 |

Exploring Text Colors and Highlights 🎨

48 |

Welcome to our color and highlight demonstration!

49 |

Here's some red text to catch your eye.

50 |

And now, let's try a yellow highlight for emphasis.

51 |

We can also combine blue text with an orange background.

52 |

Feel free to experiment with the color toolbar above to create your own colorful content!

53 |

Remember, judicious use of color can enhance readability and highlight key points in your document.

54 | `; 55 | 56 | const ColorHighlightExample = () => { 57 | const editor = useEditor({ 58 | extensions, 59 | content, 60 | immediatelyRender: false, 61 | }); 62 | 63 | if (!editor) { 64 | return null; 65 | } 66 | return ( 67 |
68 |
69 | 70 | 71 | 72 |
73 |
{ 75 | editor?.chain().focus().run(); 76 | }} 77 | className="cursor-text min-h-[18rem] bg-background" 78 | > 79 | 80 |
81 |
82 | ); 83 | }; 84 | 85 | export default ColorHighlightExample; 86 | -------------------------------------------------------------------------------- /src/registry/toolbar-examples/starter-kit-toolbar-example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Separator } from "@/components/ui/separator"; 4 | import { BlockquoteToolbar } from "@/registry/toolbars/blockquote"; 5 | import { BoldToolbar } from "@/registry/toolbars/bold"; 6 | import { BulletListToolbar } from "@/registry/toolbars/bullet-list"; 7 | import { CodeToolbar } from "@/registry/toolbars/code"; 8 | import { CodeBlockToolbar } from "@/registry/toolbars/code-block"; 9 | import { HardBreakToolbar } from "@/registry/toolbars/hard-break"; 10 | import { HorizontalRuleToolbar } from "@/registry/toolbars/horizontal-rule"; 11 | import { ItalicToolbar } from "@/registry/toolbars/italic"; 12 | import { OrderedListToolbar } from "@/registry/toolbars/ordered-list"; 13 | import { RedoToolbar } from "@/registry/toolbars/redo"; 14 | import { StrikeThroughToolbar } from "@/registry/toolbars/strikethrough"; 15 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider"; 16 | import { UndoToolbar } from "@/registry/toolbars/undo"; 17 | import { type Extension, useEditor } from "@tiptap/react"; 18 | import StarterKit from "@tiptap/starter-kit"; 19 | 20 | const extensions = [ 21 | StarterKit.configure({ 22 | orderedList: { 23 | HTMLAttributes: { 24 | class: "list-decimal", 25 | }, 26 | }, 27 | bulletList: { 28 | HTMLAttributes: { 29 | class: "list-disc", 30 | }, 31 | }, 32 | code: { 33 | HTMLAttributes: { 34 | class: "bg-accent rounded-md p-1", 35 | }, 36 | }, 37 | horizontalRule: { 38 | HTMLAttributes: { 39 | class: "my-2", 40 | }, 41 | }, 42 | codeBlock: { 43 | HTMLAttributes: { 44 | class: "bg-primary text-primary-foreground p-2 text-sm rounded-md p-1", 45 | }, 46 | }, 47 | heading: { 48 | levels: [1, 2, 3, 4], 49 | HTMLAttributes: { 50 | class: "tiptap-heading", 51 | }, 52 | }, 53 | }), 54 | ]; 55 | 56 | const StarterKitToolbarExample = () => { 57 | const editor = useEditor({ 58 | extensions: extensions as Extension[], 59 | immediatelyRender: false, 60 | }); 61 | 62 | if (!editor) { 63 | return null; 64 | } 65 | return ( 66 |
67 | 68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 |
84 |
85 | ); 86 | }; 87 | 88 | export default StarterKitToolbarExample; 89 | -------------------------------------------------------------------------------- /src/components/toc.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | "use client"; 3 | 4 | import * as React from "react"; 5 | 6 | import { useMounted } from "@/hooks/use-mounted"; 7 | import type { TableOfContents } from "@/lib/toc"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | interface TocProps { 11 | toc: TableOfContents; 12 | } 13 | 14 | export function DashboardTableOfContents({ toc }: TocProps) { 15 | const itemIds = React.useMemo( 16 | () => 17 | toc.items 18 | ? toc.items 19 | .flatMap((item) => [item.url, item?.items?.map((item) => item.url)]) 20 | .flat() 21 | .filter(Boolean) 22 | .map((id) => id?.split("#")[1]) 23 | : [], 24 | [toc], 25 | ); 26 | const activeHeading = useActiveItem(itemIds); 27 | const _ = useMounted(); 28 | 29 | if (!toc?.items?.length) { 30 | return null; 31 | } 32 | 33 | return ( 34 |
35 |

On This Page

36 | 37 |
38 | ); 39 | } 40 | 41 | function useActiveItem(itemIds: string[]) { 42 | const [activeId, setActiveId] = React.useState(null); 43 | 44 | React.useEffect(() => { 45 | const observer = new IntersectionObserver( 46 | (entries) => { 47 | entries.map((entry) => { 48 | if (entry.isIntersecting) { 49 | setActiveId(entry.target.id); 50 | } 51 | }); 52 | }, 53 | { rootMargin: `0% 0% -80% 0%` }, 54 | ); 55 | 56 | itemIds?.map((id) => { 57 | const element = document.getElementById(id); 58 | if (element) { 59 | observer.observe(element); 60 | } 61 | }); 62 | 63 | return () => { 64 | itemIds?.map((id) => { 65 | const element = document.getElementById(id); 66 | if (element) { 67 | observer.unobserve(element); 68 | } 69 | }); 70 | }; 71 | }, [itemIds]); 72 | 73 | return activeId; 74 | } 75 | 76 | interface TreeProps { 77 | tree: TableOfContents; 78 | level?: number; 79 | activeItem?: string; 80 | } 81 | 82 | function Tree({ tree, level = 1, activeItem }: TreeProps) { 83 | return tree?.items?.length && level < 3 ? ( 84 |
    85 | {tree.items.map((item, index) => { 86 | return ( 87 |
  • 88 | 97 | {item.title} 98 | 99 | {item.items?.length ? ( 100 | 101 | ) : null} 102 |
  • 103 | ); 104 | })} 105 |
106 | ) : null; 107 | } 108 | -------------------------------------------------------------------------------- /src/app/_components/hero-section.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import TiptapEditor from "@/components/editor"; 4 | import { Button } from "@/components/ui/button"; 5 | import { GitHubLogoIcon } from "@radix-ui/react-icons"; 6 | import { motion } from "framer-motion"; 7 | import { Coffee } from "lucide-react"; 8 | import Link from "next/link"; 9 | 10 | export const HeroSection = ({ starCount: _ }: { starCount: number }) => { 11 | const staggeredAnimation = { 12 | hidden: { opacity: 0, y: -20, filter: "blur(4px)" }, 13 | visible: { 14 | opacity: 1, 15 | y: 0, 16 | filter: "blur(0px)", 17 | transition: { 18 | when: "beforeChildren", 19 | staggerChildren: 0.1, 20 | }, 21 | }, 22 | }; 23 | 24 | const childAnimation = { 25 | hidden: { opacity: 0, y: -20, filter: "blur(4px)" }, 26 | visible: { 27 | opacity: 1, 28 | y: 0, 29 | filter: "blur(0px)", 30 | transition: { 31 | type: "spring", 32 | bounce: 0, 33 | }, 34 | }, 35 | }; 36 | 37 | return ( 38 | 44 |
45 | 46 | Shadcn Tiptap 47 | 48 | 52 | Collection of custom extensions and toolbars for Tiptap editor.
{" "} 53 | Ready to copy & paste and use with shadcn/ui components 54 |
55 | 59 | 62 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 |
79 | Made with ❤️ by{" "} 80 | 81 | Niaz Morshed 82 | 83 |
84 | 90 |
91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /public/r/color-highlight-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color-highlight-example", 3 | "type": "registry:example", 4 | "files": [ 5 | { 6 | "path": "examples/color-highlight-example.tsx", 7 | "type": "registry:example", 8 | "content": "\"use client\";\n\nimport { ColorHighlightToolbar } from \"@/components/toolbars/color-and-highlight\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport Color from \"@tiptap/extension-color\";\nimport Highlight from \"@tiptap/extension-highlight\";\nimport TextAlign from \"@tiptap/extension-text-align\";\nimport TextStyle from \"@tiptap/extension-text-style\";\nimport { EditorContent, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t\tblockquote: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"text-accent-foreground p-2\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t}),\n\tTextAlign.configure({\n\t\ttypes: [\"heading\", \"paragraph\"],\n\t}),\n\tTextStyle,\n\tColor,\n\tHighlight.configure({\n\t\tmulticolor: true,\n\t}),\n];\n\nconst content = `\n

Exploring Text Colors and Highlights 🎨

\n

Welcome to our color and highlight demonstration!

\n

Here's some red text to catch your eye.

\n

And now, let's try a yellow highlight for emphasis.

\n

We can also combine blue text with an orange background.

\n

Feel free to experiment with the color toolbar above to create your own colorful content!

\n

Remember, judicious use of color can enhance readability and highlight key points in your document.

\n`;\n\nconst ColorHighlightExample = () => {\n\tconst editor = useEditor({\n\t\textensions,\n\t\tcontent,\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\t\t\t {\n\t\t\t\t\teditor?.chain().focus().run();\n\t\t\t\t}}\n\t\t\t\tclassName=\"cursor-text min-h-[18rem] bg-background\"\n\t\t\t>\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n};\n\nexport default ColorHighlightExample;\n", 9 | "target": "components/color-highlight-example.tsx" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /public/r/starter-kit-toolbar-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-kit-toolbar-example", 3 | "type": "registry:example", 4 | "files": [ 5 | { 6 | "path": "toolbar-examples/starter-kit-toolbar-example.tsx", 7 | "type": "registry:example", 8 | "content": "\"use client\";\n\nimport { Separator } from \"@/components/ui/separator\";\nimport { BlockquoteToolbar } from \"@/components/toolbars/blockquote\";\nimport { BoldToolbar } from \"@/components/toolbars/bold\";\nimport { BulletListToolbar } from \"@/components/toolbars/bullet-list\";\nimport { CodeToolbar } from \"@/components/toolbars/code\";\nimport { CodeBlockToolbar } from \"@/components/toolbars/code-block\";\nimport { HardBreakToolbar } from \"@/components/toolbars/hard-break\";\nimport { HorizontalRuleToolbar } from \"@/components/toolbars/horizontal-rule\";\nimport { ItalicToolbar } from \"@/components/toolbars/italic\";\nimport { OrderedListToolbar } from \"@/components/toolbars/ordered-list\";\nimport { RedoToolbar } from \"@/components/toolbars/redo\";\nimport { StrikeThroughToolbar } from \"@/components/toolbars/strikethrough\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport { UndoToolbar } from \"@/components/toolbars/undo\";\nimport { type Extension, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t\tcode: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"bg-accent rounded-md p-1\",\n\t\t\t},\n\t\t},\n\t\thorizontalRule: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"my-2\",\n\t\t\t},\n\t\t},\n\t\tcodeBlock: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"bg-primary text-primary-foreground p-2 text-sm rounded-md p-1\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t}),\n];\n\nconst StarterKitToolbarExample = () => {\n\tconst editor = useEditor({\n\t\textensions: extensions as Extension[],\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t
button]:shrink-0 items-center gap-2\">\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t);\n};\n\nexport default StarterKitToolbarExample;\n", 9 | "target": "components/starter-kit-toolbar-example.tsx" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /src/content/docs/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: Installation guide for shadcn tiptap. 4 | --- 5 | 6 | 7 | 8 | 9 | Install core packages 10 | 11 | ```bash 12 | npm install @tiptap/react @tiptap/core @tiptap/pm 13 | ``` 14 | 15 | Install `ToolbarProvider` component 16 | 17 | `ToolbarProvider` accepts editor as a prop and serves it to its children using react context. 18 | Think of this as a provider for your toolbar components. 19 | 20 | **Install with `shadcn` cli:** 21 | 22 | ```bash 23 | npx shadcn add https://tiptap.niazmorshed.dev/r/toolbar-provider.json 24 | ``` 25 | 26 | **Or manually copy & paste**, the following code under `components/toolbars/toolbar-provider.tsx`. 27 | 28 | ```jsx 29 | "use client"; 30 | 31 | import type { Editor } from "@tiptap/react"; 32 | import React from "react"; 33 | 34 | export interface ToolbarContextProps { 35 | editor: Editor; 36 | } 37 | 38 | export const ToolbarContext = React.createContext( 39 | null, 40 | ); 41 | 42 | interface ToolbarProviderProps { 43 | editor: Editor; 44 | children: React.ReactNode; 45 | } 46 | 47 | export const ToolbarProvider = ({ editor, children }: ToolbarProviderProps) => { 48 | return ( 49 | 50 | {children} 51 | 52 | ); 53 | }; 54 | 55 | export const useToolbar = () => { 56 | const context = React.useContext(ToolbarContext); 57 | 58 | if (!context) { 59 | throw new Error("useToolbar must be used within a ToolbarProvider"); 60 | } 61 | 62 | return context; 63 | }; 64 | 65 | ``` 66 | 67 | Copy & paste the following code under `src/app/globals.css`. 68 | 69 | 70 | It's important to be careful about what class names you use for the editor. 71 | 72 | 73 | ```css 74 | .ProseMirror { 75 | @apply px-4 pt-2; 76 | outline: none !important; 77 | } 78 | 79 | h1.tiptap-heading { 80 | @apply mb-6 mt-8 text-4xl font-bold; 81 | } 82 | 83 | h2.tiptap-heading { 84 | @apply mb-4 mt-6 text-3xl font-bold; 85 | } 86 | 87 | h3.tiptap-heading { 88 | @apply mb-3 mt-4 text-xl font-bold; 89 | } 90 | 91 | h1.tiptap-heading:first-child, 92 | h2.tiptap-heading:first-child, 93 | h3.tiptap-heading:first-child { 94 | margin-top: 0; 95 | } 96 | 97 | h1.tiptap-heading + h2.tiptap-heading, 98 | h1.tiptap-heading + h3.tiptap-heading, 99 | h2.tiptap-heading + h1.tiptap-heading, 100 | h2.tiptap-heading + h3.tiptap-heading, 101 | h3.tiptap-heading + h1.tiptap-heading, 102 | h3.tiptap-heading + h2.tiptap-heading { 103 | margin-top: 0; 104 | } 105 | 106 | .tiptap p.is-editor-empty:first-child::before { 107 | @apply pointer-events-none float-left h-0 text-accent-foreground; 108 | content: attr(data-placeholder); 109 | } 110 | 111 | .tiptap ul, 112 | .tiptap ol { 113 | padding: 0 1rem; 114 | } 115 | 116 | .tiptap blockquote { 117 | border-left: 3px solid gray; 118 | margin: 1.5rem 0; 119 | padding-left: 1rem; 120 | } 121 | ``` 122 | 123 | That's it! Now go ahead and install any extensions and toolbars you want. 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/registry/examples/starter-kit-example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Separator } from "@/components/ui/separator"; 4 | import { BlockquoteToolbar } from "@/registry/toolbars/blockquote"; 5 | import { BoldToolbar } from "@/registry/toolbars/bold"; 6 | import { BulletListToolbar } from "@/registry/toolbars/bullet-list"; 7 | import { CodeToolbar } from "@/registry/toolbars/code"; 8 | import { CodeBlockToolbar } from "@/registry/toolbars/code-block"; 9 | import { HardBreakToolbar } from "@/registry/toolbars/hard-break"; 10 | import { HorizontalRuleToolbar } from "@/registry/toolbars/horizontal-rule"; 11 | import { ItalicToolbar } from "@/registry/toolbars/italic"; 12 | import { OrderedListToolbar } from "@/registry/toolbars/ordered-list"; 13 | import { RedoToolbar } from "@/registry/toolbars/redo"; 14 | import { StrikeThroughToolbar } from "@/registry/toolbars/strikethrough"; 15 | import { ToolbarProvider } from "@/registry/toolbars/toolbar-provider"; 16 | import { UndoToolbar } from "@/registry/toolbars/undo"; 17 | import { EditorContent, type Extension, useEditor } from "@tiptap/react"; 18 | import StarterKit from "@tiptap/starter-kit"; 19 | 20 | const extensions = [ 21 | StarterKit.configure({ 22 | orderedList: { 23 | HTMLAttributes: { 24 | class: "list-decimal", 25 | }, 26 | }, 27 | bulletList: { 28 | HTMLAttributes: { 29 | class: "list-disc", 30 | }, 31 | }, 32 | code: { 33 | HTMLAttributes: { 34 | class: "bg-accent rounded-md p-1", 35 | }, 36 | }, 37 | horizontalRule: { 38 | HTMLAttributes: { 39 | class: "my-2", 40 | }, 41 | }, 42 | codeBlock: { 43 | HTMLAttributes: { 44 | class: "bg-primary text-primary-foreground p-2 text-sm rounded-md p-1", 45 | }, 46 | }, 47 | heading: { 48 | levels: [1, 2, 3, 4], 49 | HTMLAttributes: { 50 | class: "tiptap-heading", 51 | }, 52 | }, 53 | }), 54 | ]; 55 | 56 | const content = ` 57 |

Hello world 🌍

58 | `; 59 | 60 | const StarterKitExample = () => { 61 | const editor = useEditor({ 62 | extensions: extensions as Extension[], 63 | content, 64 | immediatelyRender: false, 65 | }); 66 | 67 | if (!editor) { 68 | return null; 69 | } 70 | return ( 71 |
72 |
73 | 74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |
90 |
91 |
{ 93 | editor?.chain().focus().run(); 94 | }} 95 | className="cursor-text min-h-[18rem] bg-background" 96 | > 97 | 98 |
99 |
100 | ); 101 | }; 102 | 103 | export default StarterKitExample; 104 | -------------------------------------------------------------------------------- /src/registry/toolbars/alignment.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | AlignCenter, 5 | AlignJustify, 6 | AlignLeft, 7 | AlignRight, 8 | Check, 9 | ChevronDown, 10 | } from "lucide-react"; 11 | 12 | import { Button } from "@/components/ui/button"; 13 | import { 14 | DropdownMenu, 15 | DropdownMenuContent, 16 | DropdownMenuGroup, 17 | DropdownMenuItem, 18 | DropdownMenuTrigger, 19 | } from "@/components/ui/dropdown-menu"; 20 | import { 21 | Tooltip, 22 | TooltipContent, 23 | TooltipTrigger, 24 | } from "@/components/ui/tooltip"; 25 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 26 | 27 | export const AlignmentTooolbar = () => { 28 | const { editor } = useToolbar(); 29 | const handleAlign = (value: string) => { 30 | editor?.chain().focus().setTextAlign(value).run(); 31 | }; 32 | 33 | const isDisabled = 34 | (editor?.isActive("image") || editor?.isActive("video") || !editor) ?? 35 | false; 36 | 37 | const currentTextAlign = () => { 38 | if (editor?.isActive({ textAlign: "left" })) { 39 | return "left"; 40 | } 41 | if (editor?.isActive({ textAlign: "center" })) { 42 | return "center"; 43 | } 44 | if (editor?.isActive({ textAlign: "right" })) { 45 | return "right"; 46 | } 47 | if (editor?.isActive({ textAlign: "justify" })) { 48 | return "justify"; 49 | } 50 | 51 | return "left"; 52 | }; 53 | 54 | const alignmentOptions = [ 55 | { 56 | name: "Left Align", 57 | value: "left", 58 | icon: , 59 | }, 60 | { 61 | name: "Center Align", 62 | value: "center", 63 | icon: , 64 | }, 65 | { 66 | name: "Right Align", 67 | value: "right", 68 | icon: , 69 | }, 70 | { 71 | name: "Justify Align", 72 | value: "justify", 73 | icon: , 74 | }, 75 | ]; 76 | 77 | const findIndex = (value: string) => { 78 | return alignmentOptions.findIndex((option) => option.value === value); 79 | }; 80 | 81 | return ( 82 | 83 | 84 | 85 | 86 | 93 | 94 | 95 | Text Alignment 96 | 97 | { 100 | e.preventDefault(); 101 | }} 102 | > 103 | 104 | {alignmentOptions.map((option, index) => ( 105 | { 107 | handleAlign(option.value); 108 | }} 109 | key={index} 110 | > 111 | {option.icon} 112 | {option.name} 113 | 114 | {option.value === currentTextAlign() && ( 115 | 116 | )} 117 | 118 | ))} 119 | 120 | 121 | 122 | ); 123 | }; 124 | -------------------------------------------------------------------------------- /public/r/starter-kit-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-kit-example", 3 | "type": "registry:example", 4 | "files": [ 5 | { 6 | "path": "examples/starter-kit-example.tsx", 7 | "type": "registry:example", 8 | "content": "\"use client\";\n\nimport { Separator } from \"@/components/ui/separator\";\nimport { BlockquoteToolbar } from \"@/components/toolbars/blockquote\";\nimport { BoldToolbar } from \"@/components/toolbars/bold\";\nimport { BulletListToolbar } from \"@/components/toolbars/bullet-list\";\nimport { CodeToolbar } from \"@/components/toolbars/code\";\nimport { CodeBlockToolbar } from \"@/components/toolbars/code-block\";\nimport { HardBreakToolbar } from \"@/components/toolbars/hard-break\";\nimport { HorizontalRuleToolbar } from \"@/components/toolbars/horizontal-rule\";\nimport { ItalicToolbar } from \"@/components/toolbars/italic\";\nimport { OrderedListToolbar } from \"@/components/toolbars/ordered-list\";\nimport { RedoToolbar } from \"@/components/toolbars/redo\";\nimport { StrikeThroughToolbar } from \"@/components/toolbars/strikethrough\";\nimport { ToolbarProvider } from \"@/components/toolbars/toolbar-provider\";\nimport { UndoToolbar } from \"@/components/toolbars/undo\";\nimport { EditorContent, type Extension, useEditor } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\n\nconst extensions = [\n\tStarterKit.configure({\n\t\torderedList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-decimal\",\n\t\t\t},\n\t\t},\n\t\tbulletList: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"list-disc\",\n\t\t\t},\n\t\t},\n\t\tcode: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"bg-accent rounded-md p-1\",\n\t\t\t},\n\t\t},\n\t\thorizontalRule: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"my-2\",\n\t\t\t},\n\t\t},\n\t\tcodeBlock: {\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"bg-primary text-primary-foreground p-2 text-sm rounded-md p-1\",\n\t\t\t},\n\t\t},\n\t\theading: {\n\t\t\tlevels: [1, 2, 3, 4],\n\t\t\tHTMLAttributes: {\n\t\t\t\tclass: \"tiptap-heading\",\n\t\t\t},\n\t\t},\n\t}),\n];\n\nconst content = `\n

Hello world 🌍

\n`;\n\nconst StarterKitExample = () => {\n\tconst editor = useEditor({\n\t\textensions: extensions as Extension[],\n\t\tcontent,\n\t\timmediatelyRender: false,\n\t});\n\n\tif (!editor) {\n\t\treturn null;\n\t}\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t\t {\n\t\t\t\t\teditor?.chain().focus().run();\n\t\t\t\t}}\n\t\t\t\tclassName=\"cursor-text min-h-[18rem] bg-background\"\n\t\t\t>\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t);\n};\n\nexport default StarterKitExample;\n", 9 | "target": "components/starter-kit-example.tsx" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /src/components/component-example.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | import { CopyButton, CopyWithClassNames } from "@/components/copy-button"; 7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 8 | 9 | interface ComponentExampleProps extends React.HTMLAttributes { 10 | extractClassname?: boolean; 11 | extractedClassNames?: string; 12 | align?: "center" | "start" | "end"; 13 | src?: string; 14 | } 15 | 16 | export function ComponentExample({ 17 | children, 18 | className, 19 | extractClassname, 20 | extractedClassNames, 21 | align = "center", 22 | src: _, 23 | ...props 24 | }: ComponentExampleProps) { 25 | const [Example, Code, ...Children] = React.Children.toArray( 26 | children, 27 | ) as React.ReactElement[]; 28 | 29 | const codeString = React.useMemo(() => { 30 | if ( 31 | typeof Code?.props["data-rehype-pretty-code-fragment"] !== "undefined" 32 | ) { 33 | const [, Button] = React.Children.toArray( 34 | Code.props.children, 35 | ) as React.ReactElement[]; 36 | return Button?.props?.value || Button?.props?.__rawString__ || null; 37 | } 38 | }, [Code]); 39 | 40 | return ( 41 |
45 | 46 |
47 | 48 | 52 | Preview 53 | 54 | 58 | Code 59 | 60 | 61 | {extractedClassNames ? ( 62 | 67 | ) : ( 68 | codeString && ( 69 | 73 | ) 74 | )} 75 |
76 | 77 |
84 | {Example} 85 |
86 |
87 | 88 |
89 |
90 | {Code} 91 |
92 | {Children?.length ? ( 93 |
94 | {Children} 95 |
96 | ) : null} 97 |
98 |
99 |
100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/registry/toolbars/link.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { PopoverClose } from "@radix-ui/react-popover"; 4 | import { Trash2, X } from "lucide-react"; 5 | 6 | import React, { type FormEvent } from "react"; 7 | 8 | import { Button, type ButtonProps } from "@/components/ui/button"; 9 | import { Input } from "@/components/ui/input"; 10 | import { Label } from "@/components/ui/label"; 11 | import { 12 | Tooltip, 13 | TooltipContent, 14 | TooltipTrigger, 15 | } from "@/components/ui/tooltip"; 16 | import { cn, getUrlFromString } from "@/lib/utils"; 17 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 18 | import { 19 | Popover, 20 | PopoverContent, 21 | PopoverTrigger, 22 | } from "../../components/ui/popover"; 23 | 24 | const LinkToolbar = React.forwardRef( 25 | ({ className, ...props }, ref) => { 26 | const { editor } = useToolbar(); 27 | const [link, setLink] = React.useState(""); 28 | 29 | const handleSubmit = (e: FormEvent) => { 30 | e.preventDefault(); 31 | const url = getUrlFromString(link); 32 | url && editor?.chain().focus().setLink({ href: url }).run(); 33 | }; 34 | 35 | React.useEffect(() => { 36 | setLink(editor?.getAttributes("link").href || ""); 37 | }, [editor]); 38 | 39 | return ( 40 | 41 | 42 | 43 | 47 | 63 | 64 | 65 | 66 | Link 67 | 68 | 69 | 70 | { 72 | e.preventDefault(); 73 | }} 74 | asChild 75 | className="relative px-3 py-2.5" 76 | > 77 |
78 | 79 | 80 | 81 |
82 | 83 |

84 | Attach a link to the selected text 85 |

86 |
87 | { 90 | setLink(e.target.value); 91 | }} 92 | className="w-full" 93 | placeholder="https://example.com" 94 | /> 95 |
96 | {editor?.getAttributes("link").href && ( 97 | 110 | )} 111 | 114 |
115 |
116 |
117 |
118 |
119 |
120 | ); 121 | }, 122 | ); 123 | 124 | LinkToolbar.displayName = "LinkToolbar"; 125 | 126 | export { LinkToolbar }; 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadcn-tiptap", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "contentlayer2 build && pnpm build:registry && next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "build:registry": "npx tsx --tsconfig ./tsconfig.scripts.json ./src/scripts/build-registry.mts", 11 | "build:docs": "contentlayer2 build" 12 | }, 13 | "dependencies": { 14 | "@commitlint/cli": "^19.3.0", 15 | "@commitlint/config-conventional": "^19.2.2", 16 | "@emotion/is-prop-valid": "^1.3.1", 17 | "@evilmartians/lefthook": "^1.7.1", 18 | "@opentelemetry/api": "^1.8.0", 19 | "@radix-ui/react-accordion": "^1.2.1", 20 | "@radix-ui/react-aspect-ratio": "^1.1.0", 21 | "@radix-ui/react-checkbox": "^1.1.1", 22 | "@radix-ui/react-collapsible": "^1.1.1", 23 | "@radix-ui/react-dialog": "^1.1.2", 24 | "@radix-ui/react-dropdown-menu": "^2.1.1", 25 | "@radix-ui/react-icons": "^1.3.0", 26 | "@radix-ui/react-label": "^2.1.0", 27 | "@radix-ui/react-popover": "^1.1.1", 28 | "@radix-ui/react-scroll-area": "^1.2.0", 29 | "@radix-ui/react-separator": "^1.1.0", 30 | "@radix-ui/react-slot": "^1.1.0", 31 | "@radix-ui/react-tabs": "^1.1.1", 32 | "@radix-ui/react-tooltip": "^1.1.2", 33 | "@shikijs/compat": "^1.22.1", 34 | "@shikijs/core": "^1.22.0", 35 | "@tiptap/core": "^2.9.1", 36 | "@tiptap/extension-blockquote": "^2.9.1", 37 | "@tiptap/extension-color": "^2.9.1", 38 | "@tiptap/extension-heading": "^2.9.1", 39 | "@tiptap/extension-highlight": "^2.9.1", 40 | "@tiptap/extension-image": "^2.9.1", 41 | "@tiptap/extension-link": "^2.9.1", 42 | "@tiptap/extension-subscript": "^2.9.1", 43 | "@tiptap/extension-superscript": "^2.9.1", 44 | "@tiptap/extension-text-align": "^2.9.1", 45 | "@tiptap/extension-text-style": "^2.9.1", 46 | "@tiptap/extension-underline": "^2.9.1", 47 | "@tiptap/pm": "^2.9.1", 48 | "@tiptap/react": "^2.9.1", 49 | "@tiptap/starter-kit": "^2.9.1", 50 | "aria-hidden": "^1.2.4", 51 | "class-variance-authority": "^0.7.0", 52 | "clsx": "^2.1.1", 53 | "contentlayer2": "^0.5.1", 54 | "enforce-branch-name": "^1.1.2", 55 | "framer-motion": "^11.11.9", 56 | "linkifyjs": "^4.1.3", 57 | "lodash": "^4.17.21", 58 | "lodash.template": "^4.5.0", 59 | "lucide-react": "^0.407.0", 60 | "mdast-util-toc": "^7.1.0", 61 | "next": "14.2.4", 62 | "next-contentlayer2": "^0.5.1", 63 | "next-themes": "^0.3.0", 64 | "prosemirror-commands": "^1.6.2", 65 | "prosemirror-dropcursor": "^1.8.1", 66 | "prosemirror-gapcursor": "^1.3.2", 67 | "prosemirror-history": "^1.4.1", 68 | "prosemirror-keymap": "^1.2.2", 69 | "prosemirror-model": "^1.23.0", 70 | "prosemirror-schema-list": "^1.4.1", 71 | "prosemirror-state": "^1.4.3", 72 | "prosemirror-transform": "^1.10.2", 73 | "prosemirror-view": "^1.34.3", 74 | "react": "^18", 75 | "react-dom": "^18", 76 | "react-dropzone": "^14.2.10", 77 | "react-remove-scroll": "^2.6.0", 78 | "react-wrap-balancer": "^1.1.1", 79 | "rehype-autolink-headings": "^7.1.0", 80 | "rehype-pretty-code": "^0.14.0", 81 | "rehype-slug": "^6.0.0", 82 | "remark": "^15.0.1", 83 | "remark-code-import": "^1.2.0", 84 | "remark-gfm": "^4.0.0", 85 | "rimraf": "^6.0.1", 86 | "shiki": "^1.22.0", 87 | "tailwind-merge": "^2.4.0", 88 | "tailwindcss-animate": "^1.0.7", 89 | "ts-morph": "^24.0.0", 90 | "unist-builder": "^4.0.0", 91 | "unist-util-visit": "^5.0.0", 92 | "zod": "^3.23.8" 93 | }, 94 | "devDependencies": { 95 | "@biomejs/biome": "^1.8.3", 96 | "@tailwindcss/typography": "^0.5.15", 97 | "@types/node": "^20", 98 | "@types/react": "^18", 99 | "@types/react-dom": "^18", 100 | "postcss": "^8", 101 | "tailwindcss": "^3.4.1", 102 | "typescript": "^5" 103 | }, 104 | "peerDependencies": { 105 | "react": "^18", 106 | "react-dom": "^18" 107 | }, 108 | "pnpm": { 109 | "overrides": { 110 | "@opentelemetry/api": "1.8.0", 111 | "@opentelemetry/core": "1.8.0", 112 | "@opentelemetry/resources": "1.8.0", 113 | "@opentelemetry/sdk-trace-base": "1.8.0", 114 | "@opentelemetry/sdk-metrics": "1.8.0", 115 | "@opentelemetry/sdk-logs": "0.39.1" 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/components/mobile-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link, { type LinkProps } from "next/link"; 4 | import { useRouter } from "next/navigation"; 5 | import * as React from "react"; 6 | 7 | import { Icons } from "@/components/icons"; 8 | import { Button } from "@/components/ui/button"; 9 | import { ScrollArea } from "@/components/ui/scroll-area"; 10 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 11 | import { docsConfig } from "@/config/docs"; 12 | import { siteConfig } from "@/config/site"; 13 | import { cn } from "@/lib/utils"; 14 | 15 | export function MobileNav() { 16 | const [open, setOpen] = React.useState(false); 17 | 18 | return ( 19 | 20 | 21 | 56 | 57 | 58 | 63 | 64 | {siteConfig.name} 65 | 66 | 67 |
68 | {docsConfig.mainNav?.map( 69 | (item) => 70 | item.href && ( 71 | 76 | {item.title} 77 | 78 | ), 79 | )} 80 |
81 |
82 | {docsConfig.sidebarNav.map((item, index) => ( 83 |
84 |

{item.title}

85 | {item?.items?.length && 86 | item.items.map((item) => ( 87 | 88 | {!item.disabled && 89 | (item.href ? ( 90 | 95 | {item.title} 96 | {item.label && ( 97 | 98 | {item.label} 99 | 100 | )} 101 | 102 | ) : ( 103 | item.title 104 | ))} 105 | 106 | ))} 107 |
108 | ))} 109 |
110 |
111 |
112 |
113 | ); 114 | } 115 | 116 | interface MobileLinkProps extends LinkProps { 117 | onOpenChange?: (open: boolean) => void; 118 | children: React.ReactNode; 119 | className?: string; 120 | } 121 | 122 | function MobileLink({ 123 | href, 124 | onOpenChange, 125 | className, 126 | children, 127 | ...props 128 | }: MobileLinkProps) { 129 | const router = useRouter(); 130 | return ( 131 | { 134 | router.push(href.toString()); 135 | onOpenChange?.(false); 136 | }} 137 | className={cn(className)} 138 | {...props} 139 | > 140 | {children} 141 | 142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /src/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { allDocs } from "contentlayer/generated"; 2 | import { notFound } from "next/navigation"; 3 | 4 | import "@/styles/mdx.css"; 5 | import { ChevronRightIcon, ExternalLinkIcon } from "@radix-ui/react-icons"; 6 | import type { Metadata } from "next"; 7 | import Link from "next/link"; 8 | import Balancer from "react-wrap-balancer"; 9 | 10 | import { Mdx } from "@/components/mdx-components"; 11 | import { DocsPager } from "@/components/pager"; 12 | import { DashboardTableOfContents } from "@/components/toc"; 13 | import { badgeVariants } from "@/components/ui/badge"; 14 | import { ScrollArea } from "@/components/ui/scroll-area"; 15 | import { siteConfig } from "@/config/site"; 16 | import { getTableOfContents } from "@/lib/toc"; 17 | import { absoluteUrl, cn } from "@/lib/utils"; 18 | 19 | interface DocPageProps { 20 | params: { 21 | slug: string[]; 22 | }; 23 | } 24 | 25 | async function getDocFromParams({ params }: DocPageProps) { 26 | const slug = params.slug?.join("/") || ""; 27 | const doc = allDocs.find((doc) => doc.slugAsParams === slug); 28 | 29 | if (!doc) { 30 | return null; 31 | } 32 | 33 | return doc; 34 | } 35 | 36 | export async function generateMetadata({ 37 | params, 38 | }: DocPageProps): Promise { 39 | const doc = await getDocFromParams({ params }); 40 | 41 | if (!doc) { 42 | return {}; 43 | } 44 | 45 | return { 46 | title: doc.title, 47 | description: doc.description, 48 | openGraph: { 49 | title: doc.title, 50 | description: doc.description, 51 | type: "article", 52 | url: absoluteUrl(doc.slug), 53 | images: [ 54 | { 55 | url: siteConfig.ogImage, 56 | width: 1200, 57 | height: 630, 58 | alt: siteConfig.name, 59 | }, 60 | ], 61 | }, 62 | twitter: { 63 | card: "summary_large_image", 64 | title: doc.title, 65 | description: doc.description, 66 | images: [siteConfig.ogImage], 67 | creator: "@shadcn", 68 | }, 69 | }; 70 | } 71 | 72 | export async function generateStaticParams(): Promise< 73 | DocPageProps["params"][] 74 | > { 75 | return allDocs.map((doc) => ({ 76 | slug: doc.slugAsParams.split("/"), 77 | })); 78 | } 79 | 80 | export default async function DocPage({ params }: DocPageProps) { 81 | const doc = await getDocFromParams({ params }); 82 | 83 | if (!doc) { 84 | notFound(); 85 | } 86 | 87 | const toc = await getTableOfContents(doc.body.raw); 88 | 89 | return ( 90 |
91 |
92 |
93 |
Docs
94 | 95 |
{doc.title}
96 |
97 |
98 |

99 | {doc.title} 100 |

101 | {doc.description && ( 102 |

103 | {doc.description} 104 |

105 | )} 106 |
107 | {doc.links ? ( 108 |
109 | {doc.links?.doc && ( 110 | 116 | Docs 117 | 118 | 119 | )} 120 | {doc.links?.api && ( 121 | 127 | API Reference 128 | 129 | 130 | )} 131 |
132 | ) : null} 133 |
134 | 135 |
136 | 137 |
138 |
139 |
140 | 141 | {doc.toc && } 142 | 143 |
144 |
145 |
146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /contentlayer.config.js: -------------------------------------------------------------------------------- 1 | import { getHighlighter } from "@shikijs/compat"; 2 | import { 3 | defineDocumentType, 4 | defineNestedType, 5 | makeSource, 6 | } from "contentlayer2/source-files"; 7 | import rehypeAutolinkHeadings from "rehype-autolink-headings"; 8 | import rehypePrettyCode from "rehype-pretty-code"; 9 | import rehypeSlug from "rehype-slug"; 10 | import { codeImport } from "remark-code-import"; 11 | import remarkGfm from "remark-gfm"; 12 | import { visit } from "unist-util-visit"; 13 | 14 | import { rehypeComponent } from "./src/lib/rehype-component"; 15 | import { rehypeNpmCommand } from "./src/lib/rehype-npm-command"; 16 | 17 | /** @type {import('contentlayer/source-files').ComputedFields} */ 18 | const computedFields = { 19 | slug: { 20 | type: "string", 21 | resolve: (doc) => `/${doc._raw.flattenedPath}`, 22 | }, 23 | slugAsParams: { 24 | type: "string", 25 | resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/"), 26 | }, 27 | }; 28 | 29 | const LinksProperties = defineNestedType(() => ({ 30 | name: "LinksProperties", 31 | fields: { 32 | doc: { 33 | type: "string", 34 | }, 35 | api: { 36 | type: "string", 37 | }, 38 | }, 39 | })); 40 | 41 | export const Doc = defineDocumentType(() => ({ 42 | name: "Doc", 43 | filePathPattern: `docs/**/*.mdx`, 44 | contentType: "mdx", 45 | 46 | fields: { 47 | title: { 48 | type: "string", 49 | required: true, 50 | }, 51 | description: { 52 | type: "string", 53 | required: true, 54 | }, 55 | published: { 56 | type: "boolean", 57 | default: true, 58 | }, 59 | links: { 60 | type: "nested", 61 | of: LinksProperties, 62 | }, 63 | featured: { 64 | type: "boolean", 65 | default: false, 66 | required: false, 67 | }, 68 | component: { 69 | type: "boolean", 70 | default: false, 71 | required: false, 72 | }, 73 | toc: { 74 | type: "boolean", 75 | default: true, 76 | required: false, 77 | }, 78 | }, 79 | computedFields, 80 | })); 81 | 82 | export default makeSource({ 83 | contentDirPath: "./src/content", 84 | documentTypes: [Doc], 85 | mdx: { 86 | remarkPlugins: [remarkGfm, codeImport], 87 | rehypePlugins: [ 88 | rehypeSlug, 89 | rehypeComponent, 90 | () => (tree) => { 91 | visit(tree, (node) => { 92 | if (node?.type === "element" && node?.tagName === "pre") { 93 | const [codeEl] = node.children; 94 | if (codeEl.tagName !== "code") { 95 | return; 96 | } 97 | 98 | if (codeEl.data?.meta) { 99 | // Extract event from meta and pass it down the tree. 100 | const regex = /event="([^"]*)"/; 101 | const match = codeEl.data?.meta.match(regex); 102 | if (match) { 103 | node.__event__ = match ? match[1] : null; 104 | codeEl.data.meta = codeEl.data.meta.replace(regex, ""); 105 | } 106 | } 107 | 108 | node.__rawString__ = codeEl.children?.[0].value; 109 | node.__src__ = node.properties?.__src__; 110 | } 111 | }); 112 | }, 113 | [ 114 | rehypePrettyCode, 115 | { 116 | theme: "github-dark", 117 | getHighlighter, 118 | onVisitLine(node) { 119 | // Prevent lines from collapsing in `display: grid` mode, and allow empty 120 | // lines to be copy/pasted 121 | if (node.children.length === 0) { 122 | node.children = [{ type: "text", value: " " }]; 123 | } 124 | }, 125 | onVisitHighlightedLine(node) { 126 | node.properties.className.push("line--highlighted"); 127 | }, 128 | onVisitHighlightedWord(node) { 129 | node.properties.className = ["word--highlighted"]; 130 | }, 131 | }, 132 | ], 133 | () => (tree) => { 134 | visit(tree, (node) => { 135 | if (node?.type === "element") { 136 | const preElement = node.children.at(-1); 137 | if (preElement.tagName !== "pre") { 138 | return; 139 | } 140 | 141 | preElement.properties["__withMeta__"] = 142 | node.children.at(0).tagName === "div"; 143 | preElement.properties["__rawString__"] = node.__rawString__; 144 | 145 | if (node.__src__) { 146 | preElement.properties["__src__"] = node.__src__; 147 | } 148 | } 149 | }); 150 | }, 151 | rehypeNpmCommand, 152 | [ 153 | rehypeAutolinkHeadings, 154 | { 155 | properties: { 156 | className: ["subheading-anchor"], 157 | ariaLabel: "Link to section", 158 | }, 159 | }, 160 | ], 161 | ], 162 | }, 163 | }); 164 | -------------------------------------------------------------------------------- /src/components/component-preview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Index } from "@/__registry__"; 4 | import * as React from "react"; 5 | 6 | import { CopyButton } from "@/components/copy-button"; 7 | import { Icons } from "@/components/icons"; 8 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 9 | import { cn } from "@/lib/utils"; 10 | 11 | interface ComponentPreviewProps extends React.HTMLAttributes { 12 | name: string; 13 | extractClassname?: boolean; 14 | extractedClassNames?: string; 15 | align?: "center" | "start" | "end"; 16 | description?: string; 17 | hideCode?: boolean; 18 | type?: "block" | "component" | "example"; 19 | } 20 | 21 | export function ComponentPreview({ 22 | name, 23 | type, 24 | children, 25 | className, 26 | extractClassname, 27 | extractedClassNames, 28 | align = "center", 29 | description, 30 | hideCode = false, 31 | ...props 32 | }: ComponentPreviewProps) { 33 | const Codes = React.Children.toArray(children) as React.ReactElement[]; 34 | const Code = Codes[0]; 35 | 36 | const Preview = React.useMemo(() => { 37 | const Component = Index[name]?.component; 38 | 39 | if (!Component) { 40 | return ( 41 |

42 | Component{" "} 43 | 44 | {name} 45 | {" "} 46 | not found in registry. 47 |

48 | ); 49 | } 50 | 51 | return ; 52 | }, [name]); 53 | 54 | const codeString = React.useMemo(() => { 55 | if ( 56 | typeof Code?.props["data-rehype-pretty-code-fragment"] !== "undefined" 57 | ) { 58 | const [Button] = React.Children.toArray( 59 | Code.props.children, 60 | ) as React.ReactElement[]; 61 | return Button?.props?.value || Button?.props?.__rawString__ || null; 62 | } 63 | }, [Code]); 64 | 65 | return ( 66 |
70 | 71 |
72 | {!hideCode && ( 73 | 74 | 78 | Preview 79 | 80 | 84 | Code 85 | 86 | 87 | )} 88 |
89 | 90 |
91 | 96 |
97 |
107 | 110 | 111 | Loading... 112 |
113 | } 114 | > 115 | {Preview} 116 | 117 |
118 | 119 | 120 |
121 |
122 | {Code} 123 |
124 |
125 |
126 | 127 | 128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { Cross2Icon } from "@radix-ui/react-icons" 6 | import { cva, type VariantProps } from "class-variance-authority" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const Sheet = SheetPrimitive.Root 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger 13 | 14 | const SheetClose = SheetPrimitive.Close 15 | 16 | const SheetPortal = SheetPrimitive.Portal 17 | 18 | const SheetOverlay = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 30 | )) 31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 32 | 33 | const sheetVariants = cva( 34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", 35 | { 36 | variants: { 37 | side: { 38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 39 | bottom: 40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 42 | right: 43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 44 | }, 45 | }, 46 | defaultVariants: { 47 | side: "right", 48 | }, 49 | } 50 | ) 51 | 52 | interface SheetContentProps 53 | extends React.ComponentPropsWithoutRef, 54 | VariantProps {} 55 | 56 | const SheetContent = React.forwardRef< 57 | React.ElementRef, 58 | SheetContentProps 59 | >(({ side = "right", className, children, ...props }, ref) => ( 60 | 61 | 62 | 67 | 68 | 69 | Close 70 | 71 | {children} 72 | 73 | 74 | )) 75 | SheetContent.displayName = SheetPrimitive.Content.displayName 76 | 77 | const SheetHeader = ({ 78 | className, 79 | ...props 80 | }: React.HTMLAttributes) => ( 81 |
88 | ) 89 | SheetHeader.displayName = "SheetHeader" 90 | 91 | const SheetFooter = ({ 92 | className, 93 | ...props 94 | }: React.HTMLAttributes) => ( 95 |
102 | ) 103 | SheetFooter.displayName = "SheetFooter" 104 | 105 | const SheetTitle = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )) 115 | SheetTitle.displayName = SheetPrimitive.Title.displayName 116 | 117 | const SheetDescription = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, ...props }, ref) => ( 121 | 126 | )) 127 | SheetDescription.displayName = SheetPrimitive.Description.displayName 128 | 129 | export { 130 | Sheet, 131 | SheetPortal, 132 | SheetOverlay, 133 | SheetTrigger, 134 | SheetClose, 135 | SheetContent, 136 | SheetHeader, 137 | SheetFooter, 138 | SheetTitle, 139 | SheetDescription, 140 | } 141 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 10% 3.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | 33 | --text-default: #000000; 34 | --text-gray: #6b7280; 35 | --text-brown: #92400e; 36 | --text-orange: #ea580c; 37 | --text-yellow: #ca8a04; 38 | --text-green: #16a34a; 39 | --text-blue: #2563eb; 40 | --text-purple: #9333ea; 41 | --text-pink: #db2777; 42 | --text-red: #dc2626; 43 | 44 | --highlight-default: #ffffff; 45 | --highlight-gray: #f3f4f6; 46 | --highlight-brown: #fef3c7; 47 | --highlight-orange: #ffedd5; 48 | --highlight-yellow: #fef9c3; 49 | --highlight-green: #dcfce7; 50 | --highlight-blue: #dbeafe; 51 | --highlight-purple: #f3e8ff; 52 | --highlight-pink: #fce7f3; 53 | --highlight-red: #fee2e2; 54 | } 55 | 56 | .dark { 57 | --background: 240 10% 3.9%; 58 | --foreground: 0 0% 98%; 59 | --card: 240 10% 3.9%; 60 | --card-foreground: 0 0% 98%; 61 | --popover: 240 10% 3.9%; 62 | --popover-foreground: 0 0% 98%; 63 | --primary: 0 0% 98%; 64 | --primary-foreground: 240 5.9% 10%; 65 | --secondary: 240 3.7% 15.9%; 66 | --secondary-foreground: 0 0% 98%; 67 | --muted: 240 3.7% 15.9%; 68 | --muted-foreground: 240 5% 64.9%; 69 | --accent: 240 3.7% 15.9%; 70 | --accent-foreground: 0 0% 98%; 71 | --destructive: 0 62.8% 30.6%; 72 | --destructive-foreground: 0 0% 98%; 73 | --border: 240 3.7% 15.9%; 74 | --input: 240 3.7% 15.9%; 75 | --ring: 240 4.9% 83.9%; 76 | --chart-1: 220 70% 50%; 77 | --chart-2: 160 60% 45%; 78 | --chart-3: 30 80% 55%; 79 | --chart-4: 280 65% 60%; 80 | --chart-5: 340 75% 55%; 81 | 82 | --text-default: #ffffff; 83 | --text-gray: #9ca3af; 84 | --text-brown: #d97706; 85 | --text-orange: #f97316; 86 | --text-yellow: #eab308; 87 | --text-green: #22c55e; 88 | --text-blue: #3b82f6; 89 | --text-purple: #a855f7; 90 | --text-pink: #ec4899; 91 | --text-red: #ef4444; 92 | 93 | --highlight-default: #1f2937; 94 | --highlight-gray: #374151; 95 | --highlight-brown: #78350f; 96 | --highlight-orange: #9a3412; 97 | --highlight-yellow: #854d0e; 98 | --highlight-green: #166534; 99 | --highlight-blue: #1e40af; 100 | --highlight-purple: #6b21a8; 101 | --highlight-pink: #9d174d; 102 | --highlight-red: #991b1b; 103 | } 104 | } 105 | 106 | @layer base { 107 | * { 108 | @apply border-border; 109 | } 110 | body { 111 | @apply bg-background text-foreground; 112 | } 113 | } 114 | 115 | @layer utilities { 116 | .step { 117 | counter-increment: step; 118 | } 119 | 120 | .step:before { 121 | @apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background; 122 | @apply ml-[-50px] mt-[-4px]; 123 | content: counter(step); 124 | } 125 | 126 | .chunk-container { 127 | @apply shadow-none; 128 | } 129 | 130 | .chunk-container::after { 131 | content: ""; 132 | @apply absolute -inset-4 shadow-xl rounded-xl border; 133 | } 134 | } 135 | 136 | .ProseMirror { 137 | @apply px-4 pt-2; 138 | outline: none !important; 139 | } 140 | 141 | h1.tiptap-heading { 142 | @apply mb-6 mt-8 text-4xl font-bold; 143 | } 144 | 145 | h2.tiptap-heading { 146 | @apply mb-4 mt-6 text-3xl font-bold; 147 | } 148 | 149 | h3.tiptap-heading { 150 | @apply mb-3 mt-4 text-xl font-bold; 151 | } 152 | 153 | h1.tiptap-heading:first-child, 154 | h2.tiptap-heading:first-child, 155 | h3.tiptap-heading:first-child { 156 | margin-top: 0; 157 | } 158 | 159 | h1.tiptap-heading + h2.tiptap-heading, 160 | h1.tiptap-heading + h3.tiptap-heading, 161 | h2.tiptap-heading + h1.tiptap-heading, 162 | h2.tiptap-heading + h3.tiptap-heading, 163 | h3.tiptap-heading + h1.tiptap-heading, 164 | h3.tiptap-heading + h2.tiptap-heading { 165 | margin-top: 0; 166 | } 167 | 168 | .tiptap p.is-editor-empty:first-child::before { 169 | @apply pointer-events-none float-left h-0 text-accent-foreground; 170 | content: attr(data-placeholder); 171 | } 172 | 173 | .tiptap ul, 174 | .tiptap ol { 175 | padding: 0 1rem; 176 | } 177 | 178 | .tiptap blockquote { 179 | border-left: 3px solid gray; 180 | margin: 1.5rem 0; 181 | padding-left: 1rem; 182 | } 183 | -------------------------------------------------------------------------------- /src/components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { NpmCommands } from "@/types/unist"; 4 | import type { DropdownMenuTriggerProps } from "@radix-ui/react-dropdown-menu"; 5 | import { CheckIcon, ClipboardIcon } from "lucide-react"; 6 | import * as React from "react"; 7 | 8 | import { Button, type ButtonProps } from "@/components/ui/button"; 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuTrigger, 14 | } from "@/components/ui/dropdown-menu"; 15 | import { cn } from "@/lib/utils"; 16 | 17 | interface CopyButtonProps extends ButtonProps { 18 | value: string; 19 | src?: string; 20 | } 21 | 22 | export async function copyToClipboardWithMeta(value: string) { 23 | navigator.clipboard.writeText(value); 24 | } 25 | 26 | export function CopyButton({ 27 | value, 28 | className, 29 | src, 30 | variant = "ghost", 31 | ...props 32 | }: CopyButtonProps) { 33 | const [hasCopied, setHasCopied] = React.useState(false); 34 | 35 | React.useEffect(() => { 36 | setTimeout(() => { 37 | setHasCopied(false); 38 | }, 2000); 39 | }, []); 40 | 41 | return ( 42 | 58 | ); 59 | } 60 | 61 | interface CopyWithClassNamesProps extends DropdownMenuTriggerProps { 62 | value: string; 63 | classNames: string; 64 | className?: string; 65 | } 66 | 67 | export function CopyWithClassNames({ 68 | value, 69 | classNames, 70 | className, 71 | ...props 72 | }: CopyWithClassNamesProps) { 73 | const [hasCopied, setHasCopied] = React.useState(false); 74 | 75 | // biome-ignore lint/correctness/useExhaustiveDependencies: 76 | React.useEffect(() => { 77 | setTimeout(() => { 78 | setHasCopied(false); 79 | }, 2000); 80 | }, [hasCopied]); 81 | 82 | const copyToClipboard = React.useCallback((value: string) => { 83 | copyToClipboardWithMeta(value); 84 | setHasCopied(true); 85 | }, []); 86 | 87 | return ( 88 | 89 | 90 | 106 | 107 | 108 | copyToClipboard(value)}> 109 | Component 110 | 111 | copyToClipboard(classNames)}> 112 | Classname 113 | 114 | 115 | 116 | ); 117 | } 118 | 119 | interface CopyNpmCommandButtonProps extends DropdownMenuTriggerProps { 120 | commands: Required; 121 | } 122 | 123 | export function CopyNpmCommandButton({ 124 | commands, 125 | className, 126 | ...props 127 | }: CopyNpmCommandButtonProps) { 128 | const [hasCopied, setHasCopied] = React.useState(false); 129 | 130 | // biome-ignore lint/correctness/useExhaustiveDependencies: 131 | React.useEffect(() => { 132 | setTimeout(() => { 133 | setHasCopied(false); 134 | }, 2000); 135 | }, [hasCopied]); 136 | 137 | const copyCommand = React.useCallback((value: string) => { 138 | copyToClipboardWithMeta(value); 139 | setHasCopied(true); 140 | }, []); 141 | 142 | return ( 143 | 144 | 145 | 161 | 162 | 163 | copyCommand(commands.__npmCommand__)}> 164 | npm 165 | 166 | copyCommand(commands.__yarnCommand__)}> 167 | yarn 168 | 169 | copyCommand(commands.__pnpmCommand__)}> 170 | pnpm 171 | 172 | copyCommand(commands.__bunCommand__)}> 173 | bun 174 | 175 | 176 | 177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /src/content/docs/extensions/color-and-highlight.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Color and Highlight 3 | description: Color and highlight extensions for Tiptap 4 | links: 5 | --- 6 | 7 | 8 | 9 | 10 | ## Installation 11 | 12 | 13 | 14 | 15 | 16 | CLI 17 | Manual 18 | 19 | 20 | 21 | 22 | 23 | Run the following command to install associated toolbar component. 24 | ```bash 25 | npx shadcn add https://tiptap.niazmorshed.dev/r/color-and-highlight.json 26 | ``` 27 | 28 | Add the following colors to your CSS file 29 | 30 | The command above should install the colors for you. If not, copy and paste the following in your CSS file. 31 | 32 | ```css title="app/globals.css" 33 | @layer base { 34 | :root { 35 | --text-default: #000000; 36 | --text-gray: #6b7280; 37 | --text-brown: #92400e; 38 | --text-orange: #ea580c; 39 | --text-yellow: #ca8a04; 40 | --text-green: #16a34a; 41 | --text-blue: #2563eb; 42 | --text-purple: #9333ea; 43 | --text-pink: #db2777; 44 | --text-red: #dc2626; 45 | 46 | --highlight-default: #ffffff; 47 | --highlight-gray: #f3f4f6; 48 | --highlight-brown: #fef3c7; 49 | --highlight-orange: #ffedd5; 50 | --highlight-yellow: #fef9c3; 51 | --highlight-green: #dcfce7; 52 | --highlight-blue: #dbeafe; 53 | --highlight-purple: #f3e8ff; 54 | --highlight-pink: #fce7f3; 55 | --highlight-red: #fee2e2; 56 | } 57 | 58 | .dark { 59 | --text-default: #ffffff; 60 | --text-gray: #9ca3af; 61 | --text-brown: #d97706; 62 | --text-orange: #f97316; 63 | --text-yellow: #eab308; 64 | --text-green: #22c55e; 65 | --text-blue: #3b82f6; 66 | --text-purple: #a855f7; 67 | --text-pink: #ec4899; 68 | --text-red: #ef4444; 69 | 70 | --highlight-default: #1f2937; 71 | --highlight-gray: #374151; 72 | --highlight-brown: #78350f; 73 | --highlight-orange: #9a3412; 74 | --highlight-yellow: #854d0e; 75 | --highlight-green: #166534; 76 | --highlight-blue: #1e40af; 77 | --highlight-purple: #6b21a8; 78 | --highlight-pink: #9d174d; 79 | --highlight-red: #991b1b; 80 | } 81 | } 82 | ``` 83 | 84 | Enable `multicolor` for Highlight extension when using it. 85 | ```jsx 86 | extensions: [ 87 | Highlight.configure({ 88 | multicolor: true, 89 | }), 90 | ] 91 | ``` 92 | 93 | 94 | 95 | 96 | 97 | Install the following dependencies. 98 | 99 | ```bash 100 | npm install @tiptap/extension-text-style @tiptap/extension-highlight @tiptap/extension-color 101 | ``` 102 | 103 | Copy & paste the css variables. 104 | 105 | ```css title="app/globals.css" 106 | @layer base { 107 | :root { 108 | --text-default: #000000; 109 | --text-gray: #6b7280; 110 | --text-brown: #92400e; 111 | --text-orange: #ea580c; 112 | --text-yellow: #ca8a04; 113 | --text-green: #16a34a; 114 | --text-blue: #2563eb; 115 | --text-purple: #9333ea; 116 | --text-pink: #db2777; 117 | --text-red: #dc2626; 118 | 119 | --highlight-default: #ffffff; 120 | --highlight-gray: #f3f4f6; 121 | --highlight-brown: #fef3c7; 122 | --highlight-orange: #ffedd5; 123 | --highlight-yellow: #fef9c3; 124 | --highlight-green: #dcfce7; 125 | --highlight-blue: #dbeafe; 126 | --highlight-purple: #f3e8ff; 127 | --highlight-pink: #fce7f3; 128 | --highlight-red: #fee2e2; 129 | } 130 | 131 | .dark { 132 | --text-default: #ffffff; 133 | --text-gray: #9ca3af; 134 | --text-brown: #d97706; 135 | --text-orange: #f97316; 136 | --text-yellow: #eab308; 137 | --text-green: #22c55e; 138 | --text-blue: #3b82f6; 139 | --text-purple: #a855f7; 140 | --text-pink: #ec4899; 141 | --text-red: #ef4444; 142 | 143 | --highlight-default: #1f2937; 144 | --highlight-gray: #374151; 145 | --highlight-brown: #78350f; 146 | --highlight-orange: #9a3412; 147 | --highlight-yellow: #854d0e; 148 | --highlight-green: #166534; 149 | --highlight-blue: #1e40af; 150 | --highlight-purple: #6b21a8; 151 | --highlight-pink: #9d174d; 152 | --highlight-red: #991b1b; 153 | } 154 | } 155 | ``` 156 | 157 | Add the [Color & Highlight Toolbar](/docs/toolbars/color-and-highlight) to your editor. 158 | 159 | Enable `multicolor` for Highlight extension when using it. 160 | ```jsx 161 | extensions: [ 162 | Highlight.configure({ 163 | multicolor: true, 164 | }), 165 | ] 166 | ``` 167 | 168 | 169 | 170 | 171 | ## Usage 172 | 173 | ```jsx 174 | const extensions = [ 175 | StarterKit.configure({ 176 | // ... 177 | }), 178 | TextStyle, 179 | Color, 180 | Highlight.configure({ 181 | multicolor: true, 182 | }), 183 | ]; 184 | 185 | const editor = useEditor({ 186 | extensions, 187 | }); 188 | ``` 189 | 190 | 191 | ## API Reference 192 | 193 | - [TextStyle](https://tiptap.dev/docs/editor/extensions/marks/text-style#page-title) 194 | - [Highlight](https://tiptap.dev/docs/editor/extensions/marks/highlight#page-title) 195 | - [Color](https://tiptap.dev/docs/editor/extensions/functionality/color#page-title) 196 | -------------------------------------------------------------------------------- /src/registry/toolbars/color-and-highlight.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Popover, 6 | PopoverContent, 7 | PopoverTrigger, 8 | } from "@/components/ui/popover"; 9 | import { ScrollArea } from "@/components/ui/scroll-area"; 10 | import { Separator } from "@/components/ui/separator"; 11 | import { 12 | Tooltip, 13 | TooltipContent, 14 | TooltipTrigger, 15 | } from "@/components/ui/tooltip"; 16 | import { cn } from "@/lib/utils"; 17 | import { useToolbar } from "@/registry/toolbars/toolbar-provider"; 18 | import type { Extension } from "@tiptap/core"; 19 | import type { ColorOptions } from "@tiptap/extension-color"; 20 | import type { HighlightOptions } from "@tiptap/extension-highlight"; 21 | import { Check, ChevronDown } from "lucide-react"; 22 | 23 | type TextStylingExtensions = 24 | | Extension 25 | | Extension; 26 | 27 | const TEXT_COLORS = [ 28 | { name: "Default", color: "var(--text-default)" }, 29 | { name: "Gray", color: "var(--text-gray)" }, 30 | { name: "Brown", color: "var(--text-brown)" }, 31 | { name: "Orange", color: "var(--text-orange)" }, 32 | { name: "Yellow", color: "var(--text-yellow)" }, 33 | { name: "Green", color: "var(--text-green)" }, 34 | { name: "Blue", color: "var(--text-blue)" }, 35 | { name: "Purple", color: "var(--text-purple)" }, 36 | { name: "Pink", color: "var(--text-pink)" }, 37 | { name: "Red", color: "var(--text-red)" }, 38 | ]; 39 | 40 | const HIGHLIGHT_COLORS = [ 41 | { name: "Default", color: "var(--highlight-default)" }, 42 | { name: "Gray", color: "var(--highlight-gray)" }, 43 | { name: "Brown", color: "var(--highlight-brown)" }, 44 | { name: "Orange", color: "var(--highlight-orange)" }, 45 | { name: "Yellow", color: "var(--highlight-yellow)" }, 46 | { name: "Green", color: "var(--highlight-green)" }, 47 | { name: "Blue", color: "var(--highlight-blue)" }, 48 | { name: "Purple", color: "var(--highlight-purple)" }, 49 | { name: "Pink", color: "var(--highlight-pink)" }, 50 | { name: "Red", color: "var(--highlight-red)" }, 51 | ]; 52 | 53 | interface ColorHighlightButtonProps { 54 | name: string; 55 | color: string; 56 | isActive: boolean; 57 | onClick: () => void; 58 | isHighlight?: boolean; 59 | } 60 | 61 | const ColorHighlightButton = ({ 62 | name, 63 | color, 64 | isActive, 65 | onClick, 66 | isHighlight, 67 | }: ColorHighlightButtonProps) => ( 68 | 84 | ); 85 | 86 | export const ColorHighlightToolbar = () => { 87 | const { editor } = useToolbar(); 88 | 89 | const currentColor = editor?.getAttributes("textStyle").color; 90 | const currentHighlight = editor?.getAttributes("highlight").color; 91 | 92 | const handleSetColor = (color: string) => { 93 | editor 94 | ?.chain() 95 | .focus() 96 | .setColor(color === currentColor ? "" : color) 97 | .run(); 98 | }; 99 | 100 | const handleSetHighlight = (color: string) => { 101 | editor 102 | ?.chain() 103 | .focus() 104 | .setHighlight(color === currentHighlight ? { color: "" } : { color }) 105 | .run(); 106 | }; 107 | 108 | const isDisabled = 109 | !editor?.can().chain().setHighlight().run() || 110 | !editor?.can().chain().setColor("").run(); 111 | 112 | return ( 113 | 114 |
115 | 116 | 117 | 118 | 129 | 130 | 131 | Text Color & Highlight 132 | 133 | 134 | 135 | 136 |
Color
137 | {TEXT_COLORS.map(({ name, color }) => ( 138 | handleSetColor(color)} 144 | /> 145 | ))} 146 | 147 | 148 | 149 |
150 | Background 151 |
152 | {HIGHLIGHT_COLORS.map(({ name, color }) => ( 153 | handleSetHighlight(color)} 159 | isHighlight 160 | /> 161 | ))} 162 |
163 |
164 |
165 |
166 | ); 167 | }; 168 | --------------------------------------------------------------------------------