├── lib ├── flags.ts ├── source.ts ├── utils.ts ├── themes.ts ├── fonts.ts ├── config.ts ├── events.ts └── blocks.ts ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── avatars │ └── shadcn.jpg ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── vercel.svg ├── r │ ├── auto-focus-plugin.json │ ├── hashtag-plugin.json │ ├── actions-plugin.json │ └── rich-text-editor-plugin.json ├── window.svg ├── file.svg ├── site.webmanifest ├── schema │ └── registry.json ├── globe.svg └── next.svg ├── .prettierignore ├── content └── docs │ ├── meta.json │ ├── (root) │ └── meta.json │ └── plugins │ ├── emoji.mdx │ ├── tab-focus.mdx │ ├── toolbar.mdx │ ├── auto-focus.mdx │ ├── keywords.mdx │ ├── link.mdx │ ├── mention.mdx │ ├── typing-pref.mdx │ ├── actions.mdx │ ├── draggable-block.mdx │ ├── actions │ ├── clear-editor.mdx │ ├── tree-view.mdx │ ├── edit-mode-toggle.mdx │ ├── speech-to-text.mdx │ ├── max-length.mdx │ ├── counter-character.mdx │ ├── markdown-toggle.mdx │ ├── import-export.mdx │ └── share-content.mdx │ ├── component-picker-menu.mdx │ ├── toolbar │ ├── font-family-toolbar.mdx │ ├── font-size-toolbar.mdx │ ├── history-toolbar.mdx │ ├── subsuper-toolbar.mdx │ ├── font-format-toolbar.mdx │ ├── font-color-toolbar.mdx │ ├── block-format-toolbar.mdx │ ├── link-toolbar.mdx │ ├── clear-formatting-toolbar.mdx │ └── element-format-toolbar.mdx │ ├── layout.mdx │ ├── context-menu.mdx │ ├── hashtag.mdx │ ├── markdown.mdx │ ├── drag-drop-paste.mdx │ ├── horizontal-rule.mdx │ ├── rich-text-editor.mdx │ ├── image.mdx │ ├── table.mdx │ ├── autocomplete.mdx │ ├── code.mdx │ ├── auto-embed.mdx │ └── floating-text-format.mdx ├── postcss.config.mjs ├── registry ├── registry-categories.ts ├── new-york-v4 │ ├── editor │ │ ├── plugins │ │ │ ├── actions │ │ │ │ ├── actions-plugin.tsx │ │ │ │ ├── character-limit-plugin.tsx │ │ │ │ └── edit-mode-toggle-plugin.tsx │ │ │ ├── picker │ │ │ │ ├── check-list-picker-plugin.tsx │ │ │ │ ├── bulleted-list-picker-plugin.tsx │ │ │ │ ├── numbered-list-picker-plugin.tsx │ │ │ │ ├── divider-picker-plugin.tsx │ │ │ │ ├── image-picker-plugin.tsx │ │ │ │ ├── columns-layout-picker-plugin.tsx │ │ │ │ ├── paragraph-picker-plugin.tsx │ │ │ │ ├── quote-picker-plugin.tsx │ │ │ │ ├── embeds-picker-plugin.tsx │ │ │ │ ├── code-picker-plugin.tsx │ │ │ │ ├── heading-picker-plugin.tsx │ │ │ │ ├── alignment-picker-plugin.tsx │ │ │ │ ├── component-picker-option.tsx │ │ │ │ └── table-picker-plugin.tsx │ │ │ ├── link-plugin.tsx │ │ │ ├── code-highlight-plugin.tsx │ │ │ ├── toolbar │ │ │ │ ├── horizontal-rule-toolbar-plugin.tsx │ │ │ │ ├── table-toolbar-plugin.tsx │ │ │ │ ├── image-toolbar-plugin.tsx │ │ │ │ ├── block-insert-plugin.tsx │ │ │ │ ├── block-insert │ │ │ │ │ ├── insert-horizontal-rule.tsx │ │ │ │ │ ├── insert-table.tsx │ │ │ │ │ ├── insert-image.tsx │ │ │ │ │ ├── insert-columns-layout.tsx │ │ │ │ │ └── insert-embeds.tsx │ │ │ │ ├── block-format │ │ │ │ │ ├── format-quote.tsx │ │ │ │ │ ├── format-paragraph.tsx │ │ │ │ │ ├── block-format-data.tsx │ │ │ │ │ ├── format-heading.tsx │ │ │ │ │ ├── format-check-list.tsx │ │ │ │ │ ├── format-numbered-list.tsx │ │ │ │ │ ├── format-bulleted-list.tsx │ │ │ │ │ └── format-code-block.tsx │ │ │ │ └── toolbar-plugin.tsx │ │ │ ├── auto-link-plugin.tsx │ │ │ ├── embeds │ │ │ │ ├── twitter-plugin.tsx │ │ │ │ └── youtube-plugin.tsx │ │ │ ├── draggable-block-plugin.tsx │ │ │ └── drag-drop-paste-plugin.tsx │ │ ├── utils │ │ │ ├── is-mobile-width.ts │ │ │ ├── guard.ts │ │ │ ├── collapsible.ts │ │ │ ├── get-dom-range-rect.ts │ │ │ ├── get-selected-node.ts │ │ │ ├── url.ts │ │ │ └── set-floating-elem-position-for-link-editor.ts │ │ ├── shared │ │ │ ├── can-use-dom.ts │ │ │ ├── warn-only-once.ts │ │ │ ├── react-test-utils.ts │ │ │ ├── normalize-class-names.ts │ │ │ ├── use-layout-effect.ts │ │ │ ├── react-patches.ts │ │ │ ├── invariant.ts │ │ │ ├── caret-from-point.ts │ │ │ └── simple-diff-with-cursor.ts │ │ ├── transformers │ │ │ ├── markdown-emoji-transformer.ts │ │ │ ├── markdown-tweet-transformer.ts │ │ │ ├── markdown-image-transformer.ts │ │ │ └── markdown-hr-transformer.ts │ │ ├── editor-hooks │ │ │ ├── use-debounce.ts │ │ │ └── use-update-toolbar.ts │ │ ├── editor-ui │ │ │ └── content-editable.tsx │ │ └── context │ │ │ └── toolbar-context.tsx │ ├── lib │ │ └── utils.ts │ ├── ui │ │ ├── aspect-ratio.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── progress.tsx │ │ ├── collapsible.tsx │ │ ├── input.tsx │ │ ├── switch.tsx │ │ ├── avatar.tsx │ │ ├── checkbox.tsx │ │ ├── radio-group.tsx │ │ ├── hover-card.tsx │ │ └── toggle.tsx │ ├── blocks │ │ ├── editor-00 │ │ │ ├── nodes.ts │ │ │ ├── page.tsx │ │ │ └── plugins.tsx │ │ ├── editor-md │ │ │ ├── nodes.ts │ │ │ └── page.tsx │ │ └── editor-x │ │ │ └── page.tsx │ └── hooks │ │ └── use-mobile.ts ├── __blocks__.json └── index.ts ├── components ├── analytics.tsx ├── page-nav.tsx ├── lo-fi │ ├── alert.tsx │ ├── index.tsx │ ├── component.tsx │ ├── accordion.tsx │ └── atom.tsx ├── theme-provider.tsx ├── announcement.tsx ├── cards │ └── calendar.tsx ├── code-tabs.tsx ├── callout.tsx ├── tailwind-indicator.tsx ├── open-in-v0-button.tsx ├── main-nav.tsx ├── components-list.tsx ├── color-palette.tsx ├── block-image.tsx ├── site-config.tsx ├── open-in-v0-cta.tsx ├── github-link.tsx ├── site-footer.tsx ├── nav-header.tsx ├── colors-nav.tsx ├── active-theme.tsx ├── mode-switcher.tsx ├── page-header.tsx ├── blocks-nav.tsx ├── chart-copy-button.tsx └── chart-display.tsx ├── hooks ├── use-mounted.ts ├── use-is-mac.ts ├── use-config.ts ├── use-media-query.tsx ├── use-mutation-observer.ts ├── use-mobile.ts ├── use-meta-color.ts ├── use-colors.ts └── use-copy-to-clipboard.ts ├── tsconfig.scripts.json ├── app └── (app) │ ├── layout.tsx │ ├── blocks │ ├── page.tsx │ └── [...categories] │ │ └── page.tsx │ ├── docs │ └── layout.tsx │ └── (root) │ └── section.tsx ├── components.json ├── eslint.config.mjs ├── next.config.mjs ├── .gitignore ├── tsconfig.json ├── source.config.ts └── README.md /lib/flags.ts: -------------------------------------------------------------------------------- 1 | export const showMcpDocs = true 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmujahid/shadcn-editor/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build 5 | .contentlayer 6 | registry/__index__.tsx 7 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmujahid/shadcn-editor/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmujahid/shadcn-editor/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmujahid/shadcn-editor/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/avatars/shadcn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmujahid/shadcn-editor/HEAD/public/avatars/shadcn.jpg -------------------------------------------------------------------------------- /content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "pages": [ 4 | "(root)", 5 | "plugins" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmujahid/shadcn-editor/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmujahid/shadcn-editor/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | } 6 | export default config 7 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /registry/registry-categories.ts: -------------------------------------------------------------------------------- 1 | export const registryCategories = [ 2 | { 3 | name: "Editor", 4 | slug: "editor", 5 | hidden: false, 6 | }, 7 | ] 8 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/actions/actions-plugin.tsx: -------------------------------------------------------------------------------- 1 | export function ActionsPlugin({ children }: { children: React.ReactNode }) { 2 | return children 3 | } 4 | -------------------------------------------------------------------------------- /content/docs/(root)/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Get Started", 3 | "pages": [ 4 | "index", 5 | "[Installation](/docs/installation)", 6 | "[Plugins](/docs/plugins)" 7 | ] 8 | } -------------------------------------------------------------------------------- /components/analytics.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react" 4 | 5 | export function Analytics() { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /registry/new-york-v4/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /lib/source.ts: -------------------------------------------------------------------------------- 1 | import { docs } from "@/.source" 2 | import { loader } from "fumadocs-core/source" 3 | 4 | export const source: ReturnType = loader({ 5 | baseUrl: "/docs", 6 | source: docs.toFumadocsSource(), 7 | }) 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/utils/is-mobile-width.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | -------------------------------------------------------------------------------- /hooks/use-is-mac.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export function useIsMac() { 4 | const [isMac, setIsMac] = useState(true) 5 | 6 | useEffect(() => { 7 | setIsMac(navigator.platform.toUpperCase().includes("MAC")) 8 | }, []) 9 | 10 | return isMac 11 | } 12 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | export function absoluteUrl(path: string) { 9 | return `${process.env.NEXT_PUBLIC_APP_URL}${path}` 10 | } 11 | -------------------------------------------------------------------------------- /public/r/auto-focus-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "auto-focus-plugin", 4 | "type": "registry:ui", 5 | "title": "Auto Focus Plugin", 6 | "description": "A plugin for the auto focus.", 7 | "registryDependencies": [ 8 | "@shadcn-editor/rich-text-editor-plugin" 9 | ] 10 | } -------------------------------------------------------------------------------- /registry/__blocks__.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "editor", 4 | "description": "A basic editor block." 5 | }, 6 | { 7 | "name": "editor-x", 8 | "description": "An advanced editor block with all plugins included." 9 | }, 10 | { 11 | "name": "editor-md", 12 | "description": "A markdown editor block." 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /registry/new-york-v4/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | function AspectRatio({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return 9 | } 10 | 11 | export { AspectRatio } 12 | -------------------------------------------------------------------------------- /registry/new-york-v4/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/utils/guard.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | export function isHTMLElement(x: unknown): x is HTMLElement { 9 | return x instanceof HTMLElement 10 | } 11 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /registry/new-york-v4/blocks/editor-00/nodes.ts: -------------------------------------------------------------------------------- 1 | import { HeadingNode, QuoteNode } from "@lexical/rich-text" 2 | import { 3 | Klass, 4 | LexicalNode, 5 | LexicalNodeReplacement, 6 | ParagraphNode, 7 | TextNode, 8 | } from "lexical" 9 | 10 | export const nodes: ReadonlyArray | LexicalNodeReplacement> = 11 | [HeadingNode, ParagraphNode, TextNode, QuoteNode] 12 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "display": "standalone" 17 | } 18 | -------------------------------------------------------------------------------- /public/r/hashtag-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "hashtag-plugin", 4 | "type": "registry:ui", 5 | "title": "Hashtag Plugin", 6 | "description": "A plugin for the hashtag.", 7 | "dependencies": [ 8 | "@lexical/hashtag" 9 | ], 10 | "registryDependencies": [ 11 | "@shadcn-editor/rich-text-editor-plugin" 12 | ] 13 | } -------------------------------------------------------------------------------- /components/page-nav.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | export function PageNav({ 4 | children, 5 | className, 6 | ...props 7 | }: React.ComponentProps<"div">) { 8 | return ( 9 |
10 |
11 | {children} 12 |
13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "target": "ES2017", 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "isolatedModules": false, 10 | "allowImportingTsExtensions": true 11 | }, 12 | "include": ["scripts/**/*.{ts,mts}"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SiteFooter } from "@/components/site-footer" 2 | import { SiteHeader } from "@/components/site-header" 3 | 4 | export default function AppLayout({ children }: { children: React.ReactNode }) { 5 | return ( 6 |
7 | 8 |
{children}
9 | 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/shared/can-use-dom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | export const CAN_USE_DOM: boolean = 10 | typeof window !== "undefined" && 11 | typeof window.document !== "undefined" && 12 | typeof window.document.createElement !== "undefined" 13 | -------------------------------------------------------------------------------- /app/(app)/blocks/page.tsx: -------------------------------------------------------------------------------- 1 | import { BlockDisplay } from "@/components/block-display" 2 | 3 | export const dynamic = "force-static" 4 | export const revalidate = false 5 | 6 | const FEATURED_BLOCKS = ["editor-x", "editor-md"] 7 | 8 | export default async function BlocksPage() { 9 | return ( 10 |
11 | {FEATURED_BLOCKS.map((name) => ( 12 | 13 | ))} 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /hooks/use-config.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai" 2 | import { atomWithStorage } from "jotai/utils" 3 | 4 | type Config = { 5 | style: "new-york-v4" 6 | packageManager: "npm" | "yarn" | "pnpm" | "bun" 7 | installationType: "cli" | "manual" 8 | } 9 | 10 | const configAtom = atomWithStorage("config", { 11 | style: "new-york-v4", 12 | packageManager: "pnpm", 13 | installationType: "cli", 14 | }) 15 | 16 | export function useConfig() { 17 | return useAtom(configAtom) 18 | } 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/registry/new-york-v4/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /registry/new-york-v4/editor/utils/collapsible.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | export function setDomHiddenUntilFound(dom: HTMLElement): void { 10 | // @ts-expect-error 11 | dom.hidden = "until-found" 12 | } 13 | 14 | export function domOnBeforeMatch(dom: HTMLElement, callback: () => void): void { 15 | dom.onbeforematch = callback 16 | } 17 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/shared/warn-only-once.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | export function warnOnlyOnce(message: string) { 10 | if (process.env.NODE_ENV === "production") { 11 | return 12 | } 13 | let run = false 14 | return () => { 15 | if (!run) { 16 | console.warn(message) 17 | } 18 | run = true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /hooks/use-media-query.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function useMediaQuery(query: string) { 4 | const [value, setValue] = React.useState(false) 5 | 6 | React.useEffect(() => { 7 | function onChange(event: MediaQueryListEvent) { 8 | setValue(event.matches) 9 | } 10 | 11 | const result = matchMedia(query) 12 | result.addEventListener("change", onChange) 13 | setValue(result.matches) 14 | 15 | return () => result.removeEventListener("change", onChange) 16 | }, [query]) 17 | 18 | return value 19 | } 20 | -------------------------------------------------------------------------------- /lib/themes.ts: -------------------------------------------------------------------------------- 1 | export const THEMES = [ 2 | { 3 | name: "Default", 4 | value: "default", 5 | }, 6 | { 7 | name: "Neutral", 8 | value: "neutral", 9 | }, 10 | { 11 | name: "Stone", 12 | value: "stone", 13 | }, 14 | { 15 | name: "Zinc", 16 | value: "zinc", 17 | }, 18 | { 19 | name: "Gray", 20 | value: "gray", 21 | }, 22 | { 23 | name: "Slate", 24 | value: "slate", 25 | }, 26 | { 27 | name: "Scaled", 28 | value: "scaled", 29 | }, 30 | ] 31 | export type Theme = (typeof THEMES)[number] 32 | -------------------------------------------------------------------------------- /components/lo-fi/alert.tsx: -------------------------------------------------------------------------------- 1 | import { CircleAlertIcon } from "lucide-react" 2 | 3 | import { Atom } from "@/components/lo-fi/atom" 4 | 5 | export function AlertLoFi() { 6 | return ( 7 | 11 | 12 |
13 | 14 | 15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | {children} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path" 2 | import { fileURLToPath } from "url" 3 | import { FlatCompat } from "@eslint/eslintrc" 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = dirname(__filename) 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }) 11 | 12 | const eslintConfig = [ 13 | ...compat.config({ 14 | extends: ["next/core-web-vitals", "next/typescript"], 15 | rules: { 16 | "@next/next/no-duplicate-head": "off", 17 | }, 18 | }), 19 | ] 20 | 21 | export default eslintConfig 22 | -------------------------------------------------------------------------------- /components/announcement.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | import { ArrowRightIcon } from "lucide-react" 3 | 4 | import { Badge } from "@/registry/new-york-v4/ui/badge" 5 | 6 | export function Announcement() { 7 | return ( 8 | 9 | 10 | 11 | Undocx: Realtime Collaborative Rich Text Editor 12 | 13 | 14 | ) 15 | } -------------------------------------------------------------------------------- /public/schema/registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-07/schema#", 3 | "description": "A shadcn registry of components, hooks, pages, etc.", 4 | "type": "object", 5 | "properties": { 6 | "name": { 7 | "type": "string" 8 | }, 9 | "homepage": { 10 | "type": "string" 11 | }, 12 | "items": { 13 | "type": "array", 14 | "items": { 15 | "$ref": "https://ui.shadcn.com/schema/registry-item.json" 16 | } 17 | } 18 | }, 19 | "required": ["name", "homepage", "items"], 20 | "uniqueItems": true, 21 | "minItems": 1 22 | } 23 | -------------------------------------------------------------------------------- /hooks/use-mutation-observer.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export const useMutationObserver = ( 4 | ref: React.RefObject, 5 | callback: MutationCallback, 6 | options: MutationObserverInit = { 7 | attributes: true, 8 | characterData: true, 9 | childList: true, 10 | subtree: true, 11 | } 12 | ) => { 13 | React.useEffect(() => { 14 | if (ref.current) { 15 | const observer = new MutationObserver(callback) 16 | observer.observe(ref.current, options) 17 | return () => observer.disconnect() 18 | } 19 | }, [ref, callback, options]) 20 | } 21 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/picker/check-list-picker-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { INSERT_CHECK_LIST_COMMAND } from "@lexical/list" 2 | import { ListTodoIcon } from "lucide-react" 3 | 4 | import { ComponentPickerOption } from "@/registry/new-york-v4/editor/plugins/picker/component-picker-option" 5 | 6 | export function CheckListPickerPlugin() { 7 | return new ComponentPickerOption("Check List", { 8 | icon: , 9 | keywords: ["check list", "todo list"], 10 | onSelect: (_, editor) => 11 | editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Geist_Mono as FontMono, 3 | Geist as FontSans, 4 | Inter, 5 | } from "next/font/google" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const fontSans = FontSans({ 10 | subsets: ["latin"], 11 | variable: "--font-sans", 12 | }) 13 | 14 | const fontMono = FontMono({ 15 | subsets: ["latin"], 16 | variable: "--font-mono", 17 | weight: ["400"], 18 | }) 19 | 20 | const fontInter = Inter({ 21 | subsets: ["latin"], 22 | variable: "--font-inter", 23 | }) 24 | 25 | export const fontVariables = cn( 26 | fontSans.variable, 27 | fontMono.variable, 28 | fontInter.variable 29 | ) 30 | -------------------------------------------------------------------------------- /hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function useIsMobile(mobileBreakpoint = 768) { 4 | const [isMobile, setIsMobile] = React.useState(undefined) 5 | 6 | React.useEffect(() => { 7 | const mql = window.matchMedia(`(max-width: ${mobileBreakpoint - 1}px)`) 8 | const onChange = () => { 9 | setIsMobile(window.innerWidth < mobileBreakpoint) 10 | } 11 | mql.addEventListener("change", onChange) 12 | setIsMobile(window.innerWidth < mobileBreakpoint) 13 | return () => mql.removeEventListener("change", onChange) 14 | }, []) 15 | 16 | return !!isMobile 17 | } 18 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/picker/bulleted-list-picker-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list" 2 | import { ListIcon } from "lucide-react" 3 | 4 | import { ComponentPickerOption } from "@/registry/new-york-v4/editor/plugins/picker/component-picker-option" 5 | 6 | export function BulletedListPickerPlugin() { 7 | return new ComponentPickerOption("Bulleted List", { 8 | icon: , 9 | keywords: ["bulleted list", "unordered list", "ul"], 10 | onSelect: (_, editor) => 11 | editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/link-plugin.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | /** 4 | * Copyright (c) Meta Platforms, Inc. and affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | import * as React from "react" 11 | import { JSX } from "react" 12 | import { LinkPlugin as LexicalLinkPlugin } from "@lexical/react/LexicalLinkPlugin" 13 | 14 | import { validateUrl } from "@/registry/new-york-v4/editor/utils/url" 15 | 16 | export function LinkPlugin(): JSX.Element { 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/picker/numbered-list-picker-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { INSERT_ORDERED_LIST_COMMAND } from "@lexical/list" 2 | import { ListOrderedIcon } from "lucide-react" 3 | 4 | import { ComponentPickerOption } from "@/registry/new-york-v4/editor/plugins/picker/component-picker-option" 5 | 6 | export function NumberedListPickerPlugin() { 7 | return new ComponentPickerOption("Numbered List", { 8 | icon: , 9 | keywords: ["numbered list", "ordered list", "ol"], 10 | onSelect: (_, editor) => 11 | editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/picker/divider-picker-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { INSERT_HORIZONTAL_RULE_COMMAND } from "@lexical/react/LexicalHorizontalRuleNode" 2 | import { MinusIcon } from "lucide-react" 3 | 4 | import { ComponentPickerOption } from "@/registry/new-york-v4/editor/plugins/picker/component-picker-option" 5 | 6 | export function DividerPickerPlugin() { 7 | return new ComponentPickerOption("Divider", { 8 | icon: , 9 | keywords: ["horizontal rule", "divider", "hr"], 10 | onSelect: (_, editor) => 11 | editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined), 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/shared/react-test-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | import * as React from "react" 9 | import * as ReactTestUtils from "react-dom/test-utils" 10 | 11 | /** 12 | * React 19 moved act from react-dom/test-utils to react 13 | * https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-react-dom-test-utils 14 | */ 15 | export const act = 16 | "act" in React ? (React.act as typeof ReactTestUtils.act) : ReactTestUtils.act 17 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from "fumadocs-mdx/next" 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | devIndicators: false, 6 | typescript: { 7 | ignoreBuildErrors: true, 8 | }, 9 | outputFileTracingIncludes: { 10 | "/*": ["./registry/**/*"], 11 | }, 12 | images: { 13 | remotePatterns: [ 14 | { 15 | protocol: "https", 16 | hostname: "avatars.githubusercontent.com", 17 | }, 18 | { 19 | protocol: "https", 20 | hostname: "images.unsplash.com", 21 | }, 22 | ], 23 | }, 24 | } 25 | 26 | const withMDX = createMDX({}) 27 | 28 | export default withMDX(nextConfig) 29 | -------------------------------------------------------------------------------- /registry/new-york-v4/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/transformers/markdown-emoji-transformer.ts: -------------------------------------------------------------------------------- 1 | import { TextMatchTransformer } from "@lexical/markdown" 2 | import { $createTextNode } from "lexical" 3 | 4 | import emojiList from "@/registry/new-york-v4/editor/utils/emoji-list" 5 | 6 | export const EMOJI: TextMatchTransformer = { 7 | dependencies: [], 8 | export: () => null, 9 | importRegExp: /:([a-z0-9_]+):/, 10 | regExp: /:([a-z0-9_]+):/, 11 | replace: (textNode, [, name]) => { 12 | const emoji = emojiList.find((e) => e.aliases.includes(name))?.emoji 13 | if (emoji) { 14 | textNode.replace($createTextNode(emoji)) 15 | } 16 | }, 17 | trigger: ":", 18 | type: "text-match", 19 | } 20 | -------------------------------------------------------------------------------- /public/r/actions-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "actions-plugin", 4 | "type": "registry:ui", 5 | "title": "Actions Plugin", 6 | "description": "A plugin for the actions.", 7 | "registryDependencies": [ 8 | "@shadcn-editor/rich-text-editor-plugin" 9 | ], 10 | "files": [ 11 | { 12 | "path": "registry/new-york-v4/editor/plugins/actions/actions-plugin.tsx", 13 | "content": "export function ActionsPlugin({ children }: { children: React.ReactNode }) {\n return children\n}\n", 14 | "type": "registry:component", 15 | "target": "components/editor/plugins/actions/actions-plugin.tsx" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /registry/new-york-v4/editor/shared/normalize-class-names.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | export default function normalizeClassNames( 10 | ...classNames: Array 11 | ): Array { 12 | const rval = [] 13 | for (const className of classNames) { 14 | if (className && typeof className === "string") { 15 | for (const [s] of Array.from(className.matchAll(/\S+/g))) { 16 | rval.push(s) 17 | } 18 | } 19 | } 20 | return rval 21 | } 22 | -------------------------------------------------------------------------------- /registry/new-york-v4/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/picker/image-picker-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { ImageIcon } from "lucide-react" 2 | 3 | import { InsertImageDialog } from "@/registry/new-york-v4/editor/plugins/images-plugin" 4 | import { ComponentPickerOption } from "@/registry/new-york-v4/editor/plugins/picker/component-picker-option" 5 | 6 | export function ImagePickerPlugin() { 7 | return new ComponentPickerOption("Image", { 8 | icon: , 9 | keywords: ["image", "photo", "picture", "file"], 10 | onSelect: (_, editor, showModal) => 11 | showModal("Insert Image", (onClose) => ( 12 | 13 | )), 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/shared/use-layout-effect.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | import { useEffect, useLayoutEffect } from "react" 9 | 10 | import { CAN_USE_DOM } from "@/registry/new-york-v4/editor/shared/can-use-dom" 11 | 12 | // This workaround is no longer necessary in React 19, 13 | // but we currently support React >=17.x 14 | // https://github.com/facebook/react/pull/26395 15 | const useLayoutEffectImpl: typeof useLayoutEffect = CAN_USE_DOM 16 | ? useLayoutEffect 17 | : useEffect 18 | 19 | export default useLayoutEffectImpl 20 | -------------------------------------------------------------------------------- /components/cards/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { addDays } from "date-fns" 4 | 5 | import { Calendar } from "@/registry/new-york-v4/ui/calendar" 6 | import { Card, CardContent } from "@/registry/new-york-v4/ui/card" 7 | 8 | const start = new Date(2025, 5, 5) 9 | 10 | export function CardsCalendar() { 11 | return ( 12 | 13 | 14 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/actions/character-limit-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { CharacterLimitPlugin as LexicalCharacterLimitPlugin } from "@lexical/react/LexicalCharacterLimitPlugin" 2 | 3 | export function CharacterLimitPlugin({ 4 | maxLength, 5 | charset, 6 | }: { 7 | maxLength: number 8 | charset: "UTF-8" | "UTF-16" 9 | }) { 10 | return ( 11 | ( 15 |
18 | {number.remainingCharacters} 19 |
20 | )} 21 | /> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/code-highlight-plugin.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | /** 4 | * Copyright (c) Meta Platforms, Inc. and affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | import { JSX, useEffect } from "react" 11 | import { registerCodeHighlighting } from "@lexical/code" 12 | import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext" 13 | 14 | export function CodeHighlightPlugin(): JSX.Element | null { 15 | const [editor] = useLexicalComposerContext() 16 | 17 | useEffect(() => { 18 | return registerCodeHighlighting(editor) 19 | }, [editor]) 20 | 21 | return null 22 | } 23 | -------------------------------------------------------------------------------- /registry/new-york-v4/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /hooks/use-meta-color.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { useTheme } from "next-themes" 3 | 4 | export const META_THEME_COLORS = { 5 | light: "#ffffff", 6 | dark: "#0a0a0a", 7 | } 8 | 9 | export function useMetaColor() { 10 | const { resolvedTheme } = useTheme() 11 | 12 | const metaColor = React.useMemo(() => { 13 | return resolvedTheme !== "dark" 14 | ? META_THEME_COLORS.light 15 | : META_THEME_COLORS.dark 16 | }, [resolvedTheme]) 17 | 18 | const setMetaColor = React.useCallback((color: string) => { 19 | document 20 | .querySelector('meta[name="theme-color"]') 21 | ?.setAttribute("content", color) 22 | }, []) 23 | 24 | return { 25 | metaColor, 26 | setMetaColor, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/picker/columns-layout-picker-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { Columns3Icon } from "lucide-react" 2 | 3 | import { InsertLayoutDialog } from "@/registry/new-york-v4/editor/plugins/layout-plugin" 4 | import { ComponentPickerOption } from "@/registry/new-york-v4/editor/plugins/picker/component-picker-option" 5 | 6 | export function ColumnsLayoutPickerPlugin() { 7 | return new ComponentPickerOption("Columns Layout", { 8 | icon: , 9 | keywords: ["columns", "layout", "grid"], 10 | onSelect: (_, editor, showModal) => 11 | showModal("Insert Columns Layout", (onClose) => ( 12 | 13 | )), 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /components/code-tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | 5 | import { useConfig } from "@/hooks/use-config" 6 | import { Tabs } from "@/registry/new-york-v4/ui/tabs" 7 | 8 | export function CodeTabs({ children }: React.ComponentProps) { 9 | const [config, setConfig] = useConfig() 10 | 11 | const installationType = React.useMemo(() => { 12 | return config.installationType || "cli" 13 | }, [config]) 14 | 15 | return ( 16 | 19 | setConfig({ ...config, installationType: value as "cli" | "manual" }) 20 | } 21 | className="relative mt-6 w-full" 22 | > 23 | {children} 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/transformers/markdown-tweet-transformer.ts: -------------------------------------------------------------------------------- 1 | import { ElementTransformer } from "@lexical/markdown" 2 | 3 | import { 4 | $createTweetNode, 5 | $isTweetNode, 6 | TweetNode, 7 | } from "@/registry/new-york-v4/editor/nodes/embeds/tweet-node" 8 | 9 | export const TWEET: ElementTransformer = { 10 | dependencies: [TweetNode], 11 | export: (node) => { 12 | if (!$isTweetNode(node)) { 13 | return null 14 | } 15 | 16 | return `` 17 | }, 18 | regExp: /\s?$/, 19 | replace: (textNode, _1, match) => { 20 | const [, id] = match 21 | const tweetNode = $createTweetNode(id) 22 | textNode.replace(tweetNode) 23 | }, 24 | type: "element", 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | 44 | # generated content 45 | .contentlayer 46 | .content-collections 47 | .source 48 | -------------------------------------------------------------------------------- /components/callout.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import { 3 | Alert, 4 | AlertDescription, 5 | AlertTitle, 6 | } from "@/registry/new-york-v4/ui/alert" 7 | 8 | export function Callout({ 9 | title, 10 | children, 11 | icon, 12 | className, 13 | ...props 14 | }: React.ComponentProps & { icon?: React.ReactNode }) { 15 | return ( 16 | 23 | {icon} 24 | {title && {title}} 25 | 26 | {children} 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /lib/config.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "shadcn/editor", 3 | url: "https://shadcn-editor.vercel.app", 4 | ogImage: "https://shadcn-editor.vercel.app/og.jpg", 5 | description: 6 | "Lexical based text editor using shadcn/ui. Accessible. Customizable. Open Source.", 7 | links: { 8 | twitter: "https://twitter.com/htmujahid", 9 | github: "https://github.com/htmujahid/shadcn-editor", 10 | }, 11 | navItems: [ 12 | { 13 | label: "Docs", 14 | href: "/docs", 15 | }, 16 | { 17 | label: "Plugins", 18 | href: "/docs/plugins", 19 | }, 20 | { 21 | label: "Blocks", 22 | href: "/blocks", 23 | }, 24 | ], 25 | } 26 | 27 | export const META_THEME_COLORS = { 28 | light: "#ffffff", 29 | dark: "#09090b", 30 | } 31 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/shared/react-patches.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | import React from "react" 9 | 10 | // Webpack + React 17 fails to compile on the usage of `React.startTransition` or 11 | // `React["startTransition"]` even if it's behind a feature detection of 12 | // `"startTransition" in React`. Moving this to a constant avoids the issue :/ 13 | const START_TRANSITION = "startTransition" 14 | 15 | export function startTransition(callback: () => void) { 16 | if (START_TRANSITION in React) { 17 | React[START_TRANSITION](callback) 18 | } else { 19 | callback() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | const SHOW = false 2 | 3 | export function TailwindIndicator() { 4 | if (process.env.NODE_ENV === "production" || !SHOW) { 5 | return null 6 | } 7 | 8 | return ( 9 |
13 |
xs
14 |
sm
15 |
md
16 |
lg
17 |
xl
18 |
2xl
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/utils/get-dom-range-rect.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | export function getDOMRangeRect( 9 | nativeSelection: Selection, 10 | rootElement: HTMLElement 11 | ): DOMRect { 12 | const domRange = nativeSelection.getRangeAt(0) 13 | 14 | let rect 15 | 16 | if (nativeSelection.anchorNode === rootElement) { 17 | let inner = rootElement 18 | while (inner.firstElementChild != null) { 19 | inner = inner.firstElementChild as HTMLElement 20 | } 21 | rect = inner.getBoundingClientRect() 22 | } else { 23 | rect = domRange.getBoundingClientRect() 24 | } 25 | 26 | return rect 27 | } 28 | -------------------------------------------------------------------------------- /hooks/use-colors.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai" 2 | import { atomWithStorage } from "jotai/utils" 3 | 4 | import { ColorFormat } from "@/lib/colors" 5 | import { useMounted } from "@/hooks/use-mounted" 6 | 7 | type Config = { 8 | format: ColorFormat 9 | lastCopied: string 10 | } 11 | 12 | const colorsAtom = atomWithStorage("colors", { 13 | format: "hsl", 14 | lastCopied: "", 15 | }) 16 | 17 | export function useColors() { 18 | const [colors, setColors] = useAtom(colorsAtom) 19 | const mounted = useMounted() 20 | 21 | return { 22 | isLoading: !mounted, 23 | format: colors.format, 24 | lastCopied: colors.lastCopied, 25 | setFormat: (format: ColorFormat) => setColors({ ...colors, format }), 26 | setLastCopied: (lastCopied: string) => setColors({ ...colors, lastCopied }), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/toolbar/horizontal-rule-toolbar-plugin.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { INSERT_HORIZONTAL_RULE_COMMAND } from "@lexical/react/LexicalHorizontalRuleNode" 4 | import { ScissorsIcon } from "lucide-react" 5 | 6 | import { useToolbarContext } from "@/registry/new-york-v4/editor/context/toolbar-context" 7 | import { Button } from "@/registry/new-york-v4/ui/button" 8 | 9 | export function HorizontalRuleToolbarPlugin() { 10 | const { activeEditor } = useToolbarContext() 11 | 12 | return ( 13 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/(app)/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { docsConfig } from "@/lib/docs" 2 | import { DocsSidebar } from "@/components/docs-sidebar" 3 | import { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar" 4 | 5 | export default function DocsLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode 9 | }) { 10 | return ( 11 |
12 | 13 | 14 |
{children}
15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/picker/paragraph-picker-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { $setBlocksType } from "@lexical/selection" 2 | import { $createParagraphNode, $getSelection, $isRangeSelection } from "lexical" 3 | import { TextIcon } from "lucide-react" 4 | 5 | import { ComponentPickerOption } from "@/registry/new-york-v4/editor/plugins/picker/component-picker-option" 6 | 7 | export function ParagraphPickerPlugin() { 8 | return new ComponentPickerOption("Paragraph", { 9 | icon: , 10 | keywords: ["normal", "paragraph", "p", "text"], 11 | onSelect: (_, editor) => 12 | editor.update(() => { 13 | const selection = $getSelection() 14 | if ($isRangeSelection(selection)) { 15 | $setBlocksType(selection, () => $createParagraphNode()) 16 | } 17 | }), 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /registry/index.ts: -------------------------------------------------------------------------------- 1 | import { registryItemSchema, type Registry } from "shadcn/schema" 2 | import { z } from "zod" 3 | 4 | import { blocks } from "@/registry/registry-blocks" 5 | import { examples } from "@/registry/registry-examples" 6 | import { ui } from "@/registry/registry-ui" 7 | 8 | // Shared between index and style for backward compatibility. 9 | // const NEW_YORK_V4_STYLE = { 10 | // type: "registry:style", 11 | // dependencies: ["class-variance-authority", "lucide-react"], 12 | // devDependencies: ["tw-animate-css"], 13 | // registryDependencies: ["utils"], 14 | // cssVars: {}, 15 | // files: [], 16 | // } 17 | 18 | export const registry = { 19 | name: "shadcn/editor", 20 | homepage: "https://shadcn-editor.vercel.app", 21 | items: z.array(registryItemSchema).parse([...ui, ...blocks, ...examples]), 22 | } satisfies Registry 23 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/plugins/picker/quote-picker-plugin.tsx: -------------------------------------------------------------------------------- 1 | import { $createQuoteNode } from "@lexical/rich-text" 2 | import { $setBlocksType } from "@lexical/selection" 3 | import { $getSelection, $isRangeSelection } from "lexical" 4 | import { QuoteIcon } from "lucide-react" 5 | 6 | import { ComponentPickerOption } from "@/registry/new-york-v4/editor/plugins/picker/component-picker-option" 7 | 8 | export function QuotePickerPlugin() { 9 | return new ComponentPickerOption("Quote", { 10 | icon: , 11 | keywords: ["block quote"], 12 | onSelect: (_, editor) => 13 | editor.update(() => { 14 | const selection = $getSelection() 15 | if ($isRangeSelection(selection)) { 16 | $setBlocksType(selection, () => $createQuoteNode()) 17 | } 18 | }), 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /registry/new-york-v4/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 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /registry/new-york-v4/editor/shared/invariant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | // invariant(condition, message) will refine types based on "condition", and 10 | // if "condition" is false will throw an error. This function is special-cased 11 | // in flow itself, so we can't name it anything else. 12 | export function invariant( 13 | cond?: boolean, 14 | message?: string, 15 | ...args: string[] 16 | ): asserts cond { 17 | if (cond) { 18 | return 19 | } 20 | 21 | throw new Error( 22 | "Internal Lexical error: invariant() is meant to be replaced at compile " + 23 | "time. There is no runtime version. Error: " + 24 | message 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /registry/new-york-v4/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |