diff --git a/docs/.gitignore b/docs/.gitignore index 8fa55a69b3..c12953bff8 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -29,4 +29,4 @@ next-env.d.ts /content/examples/*/* /components/example/generated/ -sqlite.db \ No newline at end of file +sqlite.db diff --git a/docs/app/(home)/_components/BlockCatalog.tsx b/docs/app/(home)/_components/BlockCatalog.tsx new file mode 100644 index 0000000000..dda86af75c --- /dev/null +++ b/docs/app/(home)/_components/BlockCatalog.tsx @@ -0,0 +1,108 @@ +"use client"; +import { + AudioWaveform, + ChevronRight, + Code2, + FileText, + Heading, + Image, + List, + ListOrdered, + ListTodo, + Minus, + Pilcrow, + Puzzle, + Quote, + Table, + Video, +} from "lucide-react"; +import React from "react"; + +const BlockCatalogItem: React.FC<{ name: string; icon: React.ReactNode }> = ({ + name, + icon, +}) => ( +
+
+
+ {icon} +
+ + {name} + +
+); + +export const BlockCatalog: React.FC = () => { + return ( +
+ {/* Subtle decorative elements */} +
+
+
+
+
+ +
+
+
+ 🧩 +
+

+ A universe of blocks. +

+

+ Every BlockNote document is a collection of blocks—headings, lists, + images, and more. Use the built-in blocks, customize them to fit + your needs, or create entirely new ones. +

+
+ +
+ } + /> + } + /> + } /> + } + /> + } + /> + } + /> + } /> + } /> + } + /> + } /> + } /> + } /> + } + /> + } + /> + } + /> +
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/DigitalCommons.tsx b/docs/app/(home)/_components/DigitalCommons.tsx new file mode 100644 index 0000000000..4ada09fd6a --- /dev/null +++ b/docs/app/(home)/_components/DigitalCommons.tsx @@ -0,0 +1,119 @@ +"use client"; +import Link from "next/link"; +import React, { useRef, useState } from "react"; + +export const DigitalCommons: React.FC = () => { + const videoRef = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); + + const handlePlayPause = () => { + if (videoRef.current) { + if (isPlaying) { + videoRef.current.pause(); + } else { + videoRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }; + + return ( +
+ {/* Warm gradient overlay to harmonize with cream hero */} +
+ {/* Top edge gradient for smoother transition */} +
+ +
+ {/* Asymmetric layout: content + video (vertically centered) */} +
+ {/* Left: Editorial content */} +
+ {/* Eyebrow with EU flag only */} +
+ 🇪🇺 + + Digital Commons + +
+ + {/* Headline - editorial style */} +

+ Three nations choose +
+ + open source + {" "} + to power +
+ their digital future. +

+ + {/* Short punchy copy */} +

+ France, Germany, and the Netherlands partner to build{" "} + Docs — a collaborative + writing tool for thousands of public servants.{" "} + BlockNote is the engine. +

+ + {/* Compelling social proof - simpler */} +

+ "Building Digital Commons means better tools, data sovereignty, + and shared progress." +

+ + {/* CTA */} + + Watch the story + + +
+ + {/* Right: Video - vertically centered */} +
+ {/* Glow effect */} +
+ +
+ + + {/* Play button overlay */} + {!isPlaying && ( + + )} +
+
+
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/FAQ.tsx b/docs/app/(home)/_components/FAQ.tsx new file mode 100644 index 0000000000..158c5c691a --- /dev/null +++ b/docs/app/(home)/_components/FAQ.tsx @@ -0,0 +1,51 @@ +import React from "react"; + +const faqs = [ + { + question: "Isn't it easier to use a Headless editor framework?", + answer: + "There are a number of really powerful headless text editor frameworks available. In fact, BlockNote is built on Prosemirror and TipTap. However, even when using a headless library, it takes several months and requires deep expertise to build a fully-featured editor with a polished UI that your users expect.", + }, + { + question: "Is BlockNote ready for production use?", + answer: + "BlockNote is used by dozens of companies in production, ranging from startups to large enterprises and public institutions. Also, we didn't reinvent the wheel. The core editor is built on top of Prosemirror - a battle tested framework that powers software from Atlassian, Gitlab, the New York Times, and many others.", + }, + { + question: "Can I add my own extensions to BlockNote?", + answer: + "BlockNote comes with lot of functionality out-of-the-box, but we understand that every use case is different. You can easily customize the built-in UI Components, or create your own custom Blocks, Inline Content, and Styles. If you want to go even further, you can extend the core editor with additional Prosemirror or TipTap plugins.", + }, + { + question: "Is BlockNote really free?", + answer: + "100% of BlockNote is open source. We offer consultancy, support services and commercial licenses for specific XL packages to help sustain BlockNote. Explore our pricing page for more details.", + }, +]; + +export const FAQ: React.FC = () => { + return ( +
+
+
+

+ Questions? +

+
+ +
+ {faqs.map((faq, index) => ( +
+

+ {faq.question} +

+

+ {faq.answer} +

+
+ ))} +
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/FeatureAI.tsx b/docs/app/(home)/_components/FeatureAI.tsx new file mode 100644 index 0000000000..e1061534c5 --- /dev/null +++ b/docs/app/(home)/_components/FeatureAI.tsx @@ -0,0 +1,108 @@ +"use client"; +import React, { useState } from "react"; +import { FeatureSection } from "./FeatureSection"; +import { MockEditor } from "./Shared"; + +export const FeatureAI: React.FC = () => { + const [activeTab, setActiveTab] = useState<"toolbar" | "models" | "human">( + "toolbar", + ); + + const tabs = [ + { + id: "toolbar", + icon: , + label: "AI in the Editor", + description: + "Context-aware completions and edits directly in the document.", + }, + { + id: "models", + icon: 🔌, + label: "Bring Any Model", + description: "Connect OpenAI, Anthropic, or your own endpoints.", + }, + { + id: "human", + icon: 🤝, + label: "Human in the Loop", + description: "Users accept, reject, or refine AI suggestions.", + }, + ]; + + const getEditorState = () => { + switch (activeTab) { + case "toolbar": + return { + body: ( + <> + + BlockNote is a text editor. + + + ), + cursorVisible: false, + aiSelection: true, + aiPopupVisible: true, + }; + case "models": + return { + body: ( + <> + Connect to OpenAI, Anthropic, or + your own inference endpoints. + + ), + cursorVisible: false, + aiSelection: false, + aiPopupVisible: false, + }; + case "human": + return { + body: ( + <> +
+ AI Suggestion: Improved + version of your text... +
+
+ + +
+ + ), + cursorVisible: false, + aiSelection: false, + aiPopupVisible: false, + }; + default: + return {}; + } + }; + + const editorState = getEditorState(); + + return ( + setActiveTab(id as any)} + reverse={true} + > + + + ); +}; diff --git a/docs/app/(home)/_components/FeatureCollab.tsx b/docs/app/(home)/_components/FeatureCollab.tsx new file mode 100644 index 0000000000..55c68a6687 --- /dev/null +++ b/docs/app/(home)/_components/FeatureCollab.tsx @@ -0,0 +1,113 @@ +"use client"; +import React, { useState } from "react"; +import { FeatureSection } from "./FeatureSection"; + +export const FeatureCollab: React.FC = () => { + const [activeTab, setActiveTab] = useState< + "realtime" | "comments" | "suggestions" + >("realtime"); + + const content = { + realtime: { + file: "CollaborativeEditor.tsx", + code: `import * as Y from "yjs"; +import { WebrtcProvider } from "y-webrtc"; + +const doc = new Y.Doc(); +const provider = new WebrtcProvider("room-id", doc); + +const editor = useCreateBlockNote({ + collaboration: { + fragment: doc.getXmlFragment("document"), + user: { name: "Alice", color: "#ff0000" }, + provider, + } +}); + +// Cursors and presence included`, + }, + comments: { + file: "Comments.tsx", + code: `// Add inline comment threads +editor.comments.add({ + blockId: "block-123", + content: "Can we clarify this section?", + author: currentUser, +}); + +// Subscribe to comment updates +editor.comments.onChange((comments) => { + updateCommentSidebar(comments); +});`, + }, + suggestions: { + file: "Suggestions.tsx", + code: `// Enable suggestion mode +editor.suggestions.enable(); + +// All edits are now tracked as suggestions +const pending = editor.suggestions.getPending(); + +// Accept or reject changes +editor.suggestions.accept(pending[0].id); +editor.suggestions.reject(pending[1].id); + +// Review history and versions`, + }, + }; + + const tabs = [ + { + id: "realtime", + icon: 👯, + label: "Real-Time Sync", + description: "Yjs-powered with automatic conflict resolution.", + }, + { + id: "comments", + icon: 💬, + label: "Comments", + description: "Inline threads and mentions keep conversations in context.", + }, + { + id: "suggestions", + icon: 📝, + label: "Suggestions & Versioning", + description: + "Track changes, accept or reject edits. Full document history.", + }, + ]; + + return ( + setActiveTab(id as any)} + reverse={false} + > + {/* Window Chrome */} +
+
+
+
+
+
+ + {content[activeTab].file} + +
+
+ + {/* Code */} +
+
+          {content[activeTab].code}
+        
+
+ +
+
+ ); +}; diff --git a/docs/app/(home)/_components/FeatureDX.tsx b/docs/app/(home)/_components/FeatureDX.tsx new file mode 100644 index 0000000000..cf9c4ddbb4 --- /dev/null +++ b/docs/app/(home)/_components/FeatureDX.tsx @@ -0,0 +1,117 @@ +"use client"; +import React, { useState } from "react"; +import { FeatureSection } from "./FeatureSection"; + +export const FeatureDX: React.FC = () => { + const [activeTab, setActiveTab] = useState<"types" | "theming" | "extend">( + "types", + ); + + const content = { + types: { + file: "schema.ts", + code: `import { BlockNoteSchema } from "@blocknote/core"; + +// Define your custom block schema +const schema = BlockNoteSchema.create({ + blockSpecs: { + alert: AlertBlock, + callout: CalloutBlock, + }, +}); + +// Full type inference for your blocks +type MyBlock = typeof schema.Block; +// ^? { type: "alert" | "callout" | ... }`, + }, + theming: { + file: "Editor.tsx", + code: `import { useCreateBlockNote } from "@blocknote/react"; +import { ShadCNComponents } from "@blocknote/shadcn"; + +// Use your design system +const editor = useCreateBlockNote(); + +return ( + +);`, + }, + extend: { + file: "CustomBlock.tsx", + code: `import { createReactBlockSpec } from "@blocknote/react"; + +// Create custom blocks with React +export const AlertBlock = createReactBlockSpec({ + type: "alert", + propSchema: { + type: { default: "warning" }, + }, + content: "inline", +}, { + render: (props) => ( +
+ {props.contentRef} +
+ ), +});`, + }, + }; + + const tabs = [ + { + id: "types", + icon: 📐, + label: "Type-Safe", + description: "Full autocompletion and type inference for custom schemas.", + }, + { + id: "theming", + icon: 🎨, + label: "Use Your Stack", + description: "Works with Mantine, Shadcn/ui, or go headless.", + }, + { + id: "extend", + icon: 🔧, + label: "Extend Everything", + description: "Create custom blocks, inline content, and plugins.", + }, + ]; + + return ( + setActiveTab(id as any)} + reverse={true} + > + {/* Window Chrome */} +
+
+
+
+
+
+ + {content[activeTab].file} + +
+
+ + {/* Code */} +
+
+          {content[activeTab].code}
+        
+
+ +
+
+ ); +}; diff --git a/docs/app/(home)/_components/FeatureSection.tsx b/docs/app/(home)/_components/FeatureSection.tsx new file mode 100644 index 0000000000..71fcd5d22a --- /dev/null +++ b/docs/app/(home)/_components/FeatureSection.tsx @@ -0,0 +1,101 @@ +import React from "react"; + +interface FeatureTab { + id: string; + icon: React.ReactNode; + label: string; + description: string; +} + +interface FeatureSectionProps { + title: string; + description: string; + tabs: FeatureTab[]; + activeTabId: string; + onTabChange: (id: string) => void; + // The content to display on the right side (Visual or Code) + children: React.ReactNode; + // Optional: Swap order for visual variety (Left/Right) + reverse?: boolean; +} + +export const FeatureSection: React.FC = ({ + title, + description, + tabs, + activeTabId, + onTabChange, + children, + reverse = false, +}) => { + return ( +
+ {/* Left Text & Tabs */} +
+

+ {title} +

+

+ {description} +

+ +
+ {tabs.map((tab) => { + const isActive = activeTabId === tab.id; + // Dynamic styles based on active state could be passed or handled here + // For simplicity, we'll use a generic active style or specific color logic if needed. + // But CodePlayground had specific colors (purple, amber, blue). + // Let's rely on the parent or use a generic active style here for now, + // or we can add a 'color' prop to FeatureTab if we want distinct colors per tab. + + return ( + + ); + })} +
+
+ + {/* Right Visual */} +
+
+
+ {children} +
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/FeatureUX.tsx b/docs/app/(home)/_components/FeatureUX.tsx new file mode 100644 index 0000000000..7e0e9c878d --- /dev/null +++ b/docs/app/(home)/_components/FeatureUX.tsx @@ -0,0 +1,100 @@ +"use client"; +import React, { useState } from "react"; +import { FeatureSection } from "./FeatureSection"; +import { MockEditor } from "./Shared"; + +export const FeatureUX: React.FC = () => { + const [activeTab, setActiveTab] = useState<"components" | "ai" | "blocks">( + "components", + ); + + const tabs = [ + { + id: "components", + icon: ⚡️, + label: "Ready to Use", + description: + "Slash menus, formatting toolbars, and drag handles work instantly.", + }, + { + id: "ai", + icon: , + label: "AI Assistance", + description: "Select text, ask AI. Completions and edits built-in.", + }, + { + id: "blocks", + icon: 🧱, + label: "Block-Based", + description: "Drag, drop, and nest content blocks.", + }, + ]; + + const getEditorState = () => { + switch (activeTab) { + case "components": + return { + body: ( + <> + Type '/' for commands... + + ), + cursorVisible: true, + aiSelection: false, + aiPopupVisible: false, + }; + case "ai": + return { + body: ( + <> + + BlockNote is a text editor. + + + ), + cursorVisible: false, + aiSelection: true, + aiPopupVisible: true, + }; + case "blocks": + return { + body: ( + <> +
+ ⋮⋮ + Drag blocks to reorder content. +
+
Nest blocks to create structure.
+ + ), + cursorVisible: false, + aiSelection: false, + aiPopupVisible: false, + }; + default: + return {}; + } + }; + + const editorState = getEditorState(); + + return ( + setActiveTab(id as any)} + reverse={false} + > + + + ); +}; diff --git a/docs/app/(home)/_components/FrameworkPill.tsx b/docs/app/(home)/_components/FrameworkPill.tsx new file mode 100644 index 0000000000..eef63da2ba --- /dev/null +++ b/docs/app/(home)/_components/FrameworkPill.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export const FrameworkPill: React.FC<{ + name: string; + color: string; + icon?: React.ReactNode; +}> = ({ name, color, icon }) => ( +
+ {icon ||
} + {name} +
+); diff --git a/docs/app/(home)/_components/HomeContent.tsx b/docs/app/(home)/_components/HomeContent.tsx new file mode 100644 index 0000000000..2c326a0380 --- /dev/null +++ b/docs/app/(home)/_components/HomeContent.tsx @@ -0,0 +1,248 @@ +"use client"; +import Image from "next/image"; +import Link from "next/link"; +import React from "react"; +import { BlockCatalog } from "./BlockCatalog"; +import { DigitalCommons } from "./DigitalCommons"; +import { FAQ } from "./FAQ"; +import { FeatureAI } from "./FeatureAI"; +import { FeatureCollab } from "./FeatureCollab"; +import { FeatureDX } from "./FeatureDX"; +import { FeatureUX } from "./FeatureUX"; +import { Letter } from "./Letter"; +import { Marquee } from "./Marquee"; +import { OpenSource } from "./OpenSource"; +import { Pricing } from "./Pricing"; +import { SpotlightCard } from "./SpotlightCard"; +import { Testimonials } from "./Testimonials"; +import { TextLoop } from "./TextLoop"; + +export const HomeContent: React.FC = () => { + const BADGES = [ + { icon: "⭐️", text: "100k+ weekly installs" }, + { icon: "🛡️", text: "100% Open source & self-hostable" }, + { icon: "✨", text: "AI Ready" }, + ]; + + return ( +
+ {/* Styles for custom animations */} + +
+ {/* Hero Section */} +
+ {/* Passive Neural Background */} + +
+ {/* Badge */} + + {BADGES.map((badge, index) => ( +
+ + {badge.icon} + {badge.text} + +
+ ))} +
+ +

+ Build a Notion-quality{" "} + editor in minutes. +

+

+ The AI-native, open source rich + text editor for React. Add a{" "} + fully customizable modern block-based editing + experience to your product that users will love. +

+ +
+ + View Demo + + → + + + + Documentation + +
+
+ +
+ {/* Editor Placeholder */} + {/* Editor Preview */} + +
+ BlockNote Editor Demo +
+ +
+
+
+ {/* Tech Stack Marquee */} +
+
+
+ + Integrates with your stack (TBD) + +
+ + + + + {/* Feature Grid with Spotlight Effect */} +
+
+
+

+ ?? The editor you'd build, if you had the time. +

+

+ BlockNote combines a premium editing experience with the + flexibility of open standards. Zero compromise. +

+
+ +
+ +
+ ✨ +
+

+ ?? Notion-Quality UX +

+

+ Give your users the modern, block-based experience they expect. + Slash commands, drag-and-drop, and real-time collaboration. +

+
+ /image +
+ Uploading... +
+
+ + +
+ 🛡️ +
+

+ ?? Sovereign Infrastructure +

+

+ ?? 100% open source and self-hostable. Own your data, extend the + core, and never worry about platform risk. +

+
+ + MIT / MPL + + + Local-First + +
+
+ + +
+ 🧠 +
+

+ ?? Intelligence You Own +

+

+ Add AI features like autocomplete and rewriting without leaking + data. Bring your own model, run it anywhere. +

+
+
+
+
+
+
+
+
+
+ + {/* Digital Commons */} + + + {/* Feature Pillars */} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {/* Testimonials */} + + {/* Open Source */} + + {/* Pricing */} + + {/* Blocks Catalog */} + + {/* Letter from Creators */} + + + {/* FAQ */} + + + ); +}; diff --git a/docs/app/(home)/_components/Letter.tsx b/docs/app/(home)/_components/Letter.tsx new file mode 100644 index 0000000000..6817789316 --- /dev/null +++ b/docs/app/(home)/_components/Letter.tsx @@ -0,0 +1,110 @@ +import React from "react"; + +export const Letter: React.FC = () => { + return ( +
+ {/* Background Decor - Subtle Grid */} +
+ +
+
+
+

+ Let's build. +

+
+ +
+
+
+

+ Building a rich text editor is one of the hardest engineering + challenges on the web. It used to take months of specialized + work. +

+

+ We believe that great tools should be{" "} + sovereign by default. You shouldn't have to + choose between a cohesive UX and owning your infrastructure. +

+

+ That's why we built BlockNote. A{" "} + batteries-included editor that gives you a + Notion-quality experience in minutes, while staying grounded + in open standards like{" "} + + ProseMirror + {" "} + and Yjs. +

+
+
+

+ Whether you're a startup or a public institution, you deserve + software that lasts. Join us to{" "} + + shape the future + + + + {" "} + of the open web. +

+
+
+ +
+ {/* Floating "Card" for Impact - DARK MODE */} +
+
+
+

+ Enter BlockNote. +

+

+ Forget low-level details. Work with a strongly typed API. + Get modern UI components out-of-the-box. +

+
+ +
+
+
+
+
+
+
+ + The Team + + + BlockNote Creators + +
+
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/Marquee.tsx b/docs/app/(home)/_components/Marquee.tsx new file mode 100644 index 0000000000..a5b4e3f190 --- /dev/null +++ b/docs/app/(home)/_components/Marquee.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { FrameworkPill } from "./FrameworkPill"; + +export const Marquee: React.FC = () => ( +
+
+ + + + + + + + {/* Duplicates */} + + + + + + + +
+
+ + + + + + + + {/* Duplicates */} + + + + + + + +
+
+); diff --git a/docs/app/(home)/_components/OpenSource.tsx b/docs/app/(home)/_components/OpenSource.tsx new file mode 100644 index 0000000000..bc6961ab38 --- /dev/null +++ b/docs/app/(home)/_components/OpenSource.tsx @@ -0,0 +1,126 @@ +"use client"; +import React from "react"; + +const pillars = [ + { + icon: "🏛️", + title: "Built on Giants", + description: + "ProseMirror and Yjs are battle-tested foundations trusted by teams worldwide. We build on top of them, not around them.", + }, + { + icon: "🤝", + title: "Community First", + description: + "We collaborate closely with the Yjs team and contribute back to the ecosystem. Open source thrives on shared innovation.", + }, + { + icon: "🔓", + title: "Yours to Own", + description: + "No vendor lock-in. Self-host, fork, extend. Your editing infrastructure, under your control.", + }, + { + icon: "🇪🇺", + title: "Digital Autonomy", + description: + "Partnering with DINUM (France) and Zendis (Germany) to build open European alternatives — reducing dependencies on big tech.", + }, + { + icon: "⬆️", + title: "Contributing Upstream", + description: + "We're significant contributors to Yjs, Hocuspocus, and Tiptap. When we improve the ecosystem, everyone benefits.", + }, + { + icon: "🌱", + title: "Sustainable by Design", + description: + "Bootstrapped and independent. We're building for the long term, not the next funding round.", + }, +]; + +export const OpenSource: React.FC = () => { + return ( +
+ {/* Subtle grid background */} +
+ + + + + + +
+ +
+
+

+ Committed to open source. +

+

+ Document editing is foundational infrastructure for the modern + workforce. We believe the tools we use to create and share knowledge + should be open, transparent, and free from lock-in. That's why + everything we build is open source. +

+
+ +
+ {pillars.map((pillar, index) => ( +
+
{pillar.icon}
+

{pillar.title}

+

+ {pillar.description} +

+
+ ))} +
+ + {/* Founder Quote */} +
+
+

+ "Here we could put a quote about our open source commitment." +

+
— Cool person
+
+
+ + +
+
+ ); +}; diff --git a/docs/app/(home)/_components/Pricing.tsx b/docs/app/(home)/_components/Pricing.tsx new file mode 100644 index 0000000000..94980dd71c --- /dev/null +++ b/docs/app/(home)/_components/Pricing.tsx @@ -0,0 +1,466 @@ +"use client"; +import { InfiniteSlider } from "@/components/InfiniteSlider"; +import Link from "next/link"; +import React from "react"; + +// ============================================ +// DESIGN A: Three-tier pricing cards +// ============================================ +const tiersA = [ + { + name: "Free", + tagline: "For everyone", + icon: "💚", + description: + "The complete BlockNote editor, including XL packages under GPL-3.0 for open source projects.", + features: [ + "All blocks & UI components", + "Real-time collaboration", + "Comments & suggestions", + "AI integration", + "Multi-column layouts", + "Export to PDF, Docx, ODT", + ], + license: "MIT + GPL-3.0", + highlight: false, + }, + { + name: "Pro", + tagline: "For commercial use", + icon: "⚡", + description: + "Commercial license for XL packages in closed-source projects.", + features: [ + "AI integration", + "Multi-column layouts", + "Export to PDF, Docx, ODT, Email", + "Startup discounts available", + ], + license: "Commercial License", + highlight: true, + }, + { + name: "Enterprise", + tagline: "For teams at scale", + icon: "🏢", + description: "Priority support, SLAs, and design partnership.", + features: [ + "Priority support & SLAs", + "Design partnership", + "Custom development", + "Dedicated onboarding", + ], + license: "Custom terms", + highlight: false, + }, +]; + +const PricingDesignA: React.FC = () => { + return ( +
+
+
+ + Design A — Three Tiers + +
+ + {/* Header */} +
+

+ Pricing +

+

+ 100% open source. Fair pricing. +

+

+ Everything is open source. Use it free under GPL-3.0, or get a + commercial license for closed-source projects. +

+
+ + {/* Tiers */} +
+ {tiersA.map((tier, index) => ( +
+ {tier.highlight && ( +
+ + Commercial + +
+ )} +
{tier.icon}
+

+ {tier.name} +

+

+ {tier.tagline} +

+

+ {tier.description} +

+
    + {tier.features.map((feature, featureIndex) => ( +
  • + + + + {feature} +
  • + ))} +
+

+ {tier.license} +

+
+ ))} +
+ + {/* No limits note */} +

+ No limits on documents, users, or features across all plans. +

+ + {/* CTA */} +
+ + View Pricing + + + + + + Licensing FAQ + +
+
+
+ ); +}; + +// ============================================ +// DESIGN B: Simple two-part explainer +// ============================================ +const sponsors = [ + { + name: "Semrush", + logo: ( + <> + Semrush + Semrush + + ), + }, + { + name: "NLnet", + logo: ( + <> + NLnet + NLnet + + ), + }, + { + name: "DINUM", + logo: ( + <> + DINUM + DINUM + + ), + }, + { + name: "ZenDiS", + logo: ZenDiS, + }, + { + name: "OpenProject", + logo: ( + OpenProject + ), + }, + { + name: "Poggio", + logo: ( + <> + Poggio + Poggio + + ), + }, + { + name: "Capitol", + logo: ( + <> + Capitol + Capitol + + ), + }, + { + name: "Twenty", + logo: ( + <> + Twenty + Twenty + + ), + }, + { + name: "Deep Origin", + logo: ( + Deep Origin + ), + }, + { + name: "Krisp", + logo: Krisp, + }, + { + name: "Claimer", + logo: Claimer, + }, + { + name: "Atlas", + logo: Atlas, + }, + { + name: "Juma", + logo: Juma, + }, + { + name: "Atuin", + logo: Atuin, + }, + { + name: "Cella", + logo: Cella, + }, + { + name: "Illumi", + logo: Illumi, + }, + { + name: "Agree", + logo: Agree, + }, +]; + +const PricingDesignB: React.FC = () => { + return ( +
+
+ {/* Header */} +
+

+ Transparent pricing +

+

+ Subscribe to BlockNote XL. +

+

+ BlockNote is 100% open source. Here's how licensing works. +

+
+ + {/* Two-part explainer */} +
+ {/* Core */} +
+
+ 💚 +

+ Core Editor +

+
+

+ The majority of BlockNote (including all blocks, real-time + collaboration, comments, and UI components) are liberally + licensed. +

+

+ Free to use in any project; personal, open source, or commercial. +

+

+ ✓ Free for everyone +

+
+ + {/* XL */} +
+
+ +

+ XL Packages +

+
+

+ Advanced features like AI integration,{" "} + PDF / Word / ODT exports, and{" "} + multi-column layouts. +

+

+ Free for open source projects under GPL-3.0. Closed source + projects require a subscription. +

+

+ ✓ Free for open source +

+
+
+ + {/* CTA */} +
+ + View Pricing + + + + +
+ + {/* Sponsors */} +
+

+ Thanks to our supporters for helping us build sustainable open + source software. +

+ + {sponsors.map((sponsor, index) => ( +
+ {sponsor.logo ? ( + sponsor.logo + ) : ( + + {sponsor.name} + + )} +
+ ))} +
+
+
+
+ ); +}; + +// ============================================ +// Export both for comparison +// ============================================ +export const Pricing: React.FC = () => { + return ( + <> + {/* */} + + + ); +}; diff --git a/docs/app/(home)/_components/Shared.tsx b/docs/app/(home)/_components/Shared.tsx new file mode 100644 index 0000000000..b79b7f5265 --- /dev/null +++ b/docs/app/(home)/_components/Shared.tsx @@ -0,0 +1,81 @@ +"use client"; +import ThemedImage from "@/components/ThemedImage"; +import LogoDark from "@/public/img/logos/banner.dark.svg"; +import LogoLight from "@/public/img/logos/banner.svg"; +import React from "react"; + +export const Logo: React.FC<{ className?: string }> = ({ className }) => { + return ( + + ); +}; + +interface MockEditorProps { + variant?: string; + title?: string; + body?: React.ReactNode; + cursorVisible?: boolean; + aiSelection?: boolean; + aiPopupVisible?: boolean; + aiThinking?: boolean; + className?: string; +} + +export const MockEditor: React.FC = ({ + title, + body, + cursorVisible, + aiSelection, + aiPopupVisible, + className, +}) => { + return ( +
+
+
+
+
+
+
+
{title}
+
+
+
+ + {body} + + {cursorVisible && ( + + )} + + {aiPopupVisible && ( +
+
+ ✨ AI Assistant +
+
+
+ Fix grammar +
+
+ Make shorter +
+
+ Change tone... +
+
+
+ )} +
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/SpotlightCard.tsx b/docs/app/(home)/_components/SpotlightCard.tsx new file mode 100644 index 0000000000..8ee0b46adf --- /dev/null +++ b/docs/app/(home)/_components/SpotlightCard.tsx @@ -0,0 +1,40 @@ +"use client"; +import React, { useRef, useState } from "react"; + +export const SpotlightCard: React.FC<{ + children: React.ReactNode; + className?: string; +}> = ({ children, className = "" }) => { + const divRef = useRef(null); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [opacity, setOpacity] = useState(0); + + const handleMouseMove = (e: React.MouseEvent) => { + if (!divRef.current) return; + const rect = divRef.current.getBoundingClientRect(); + setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }); + setOpacity(1); + }; + + const handleMouseLeave = () => { + setOpacity(0); + }; + + return ( +
+
+
{children}
+
+ ); +}; diff --git a/docs/app/(home)/_components/Testimonials.tsx b/docs/app/(home)/_components/Testimonials.tsx new file mode 100644 index 0000000000..e48dc43bbe --- /dev/null +++ b/docs/app/(home)/_components/Testimonials.tsx @@ -0,0 +1,101 @@ +"use client"; +import Link from "next/link"; +import React from "react"; + +interface Testimonial { + company: string; + quote: string; + author: string; + role: string; +} + +const testimonials: Testimonial[] = [ + { + company: "Acme Corp", + quote: + "BlockNote let us ship a polished editor in days instead of months. Our users love it.", + author: "Sarah Chen", + role: "VP Engineering", + }, + { + company: "Startup Inc", + quote: + "We evaluated every rich text editor on the market. BlockNote was the only one that felt modern.", + author: "Marcus Johnson", + role: "CTO", + }, + { + company: "Enterprise Co", + quote: + "The TypeScript support is exceptional. Our team was productive from day one.", + author: "Elena Rodriguez", + role: "Lead Developer", + }, +]; + +import { SpotlightCard } from "./SpotlightCard"; + +const TestimonialCard: React.FC<{ testimonial: Testimonial }> = ({ + testimonial, +}) => ( + +
+ {testimonial.company} +
+

+ "{testimonial.quote}" +

+
+
+ {testimonial.author + .split(" ") + .map((n) => n[0]) + .join("")} +
+
+
+ {testimonial.author} +
+
+ {testimonial.role}, {testimonial.company} +
+
+
+
+); + +export const Testimonials: React.FC = () => { + return ( +
+
+
+

+ Trusted by teams everywhere. +

+

+ From startups to enterprises, teams choose BlockNote to build their + document experiences. +

+
+ +
+ {testimonials.map((testimonial, index) => ( + + ))} +
+ +
+ + See who's using BlockNote + + → + + +
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/TextLoop.tsx b/docs/app/(home)/_components/TextLoop.tsx new file mode 100644 index 0000000000..5947d0772a --- /dev/null +++ b/docs/app/(home)/_components/TextLoop.tsx @@ -0,0 +1,74 @@ +// https://motion-primitives.com/docs/text-loop + +"use client"; +import { cn } from "@/lib/fumadocs/cn"; +import { + AnimatePresence, + AnimatePresenceProps, + motion, + Transition, + Variants, +} from "motion/react"; +import { Children, useEffect, useState } from "react"; + +export type TextLoopProps = { + children: React.ReactNode[]; + className?: string; + interval?: number; + transition?: Transition; + variants?: Variants; + onIndexChange?: (index: number) => void; + trigger?: boolean; + mode?: AnimatePresenceProps["mode"]; +}; + +export function TextLoop({ + children, + className, + interval = 2, + transition = { duration: 0.3 }, + variants, + onIndexChange, + trigger = true, + mode = "popLayout", +}: TextLoopProps) { + const [currentIndex, setCurrentIndex] = useState(0); + const items = Children.toArray(children); + + useEffect(() => { + if (!trigger) return; + + const intervalMs = interval * 1000; + const timer = setInterval(() => { + setCurrentIndex((current) => { + const next = (current + 1) % items.length; + onIndexChange?.(next); + return next; + }); + }, intervalMs); + return () => clearInterval(timer); + }, [items.length, interval, onIndexChange, trigger]); + + const motionVariants: Variants = { + initial: { y: 20, opacity: 0 }, + animate: { y: 0, opacity: 1 }, + exit: { y: -20, opacity: 0 }, + }; + + return ( +
+ + + {items[currentIndex]} + + +
+ ); +} diff --git a/docs/app/(home)/page.tsx b/docs/app/(home)/page.tsx index 8728ad7a73..d1d48e5b2e 100644 --- a/docs/app/(home)/page.tsx +++ b/docs/app/(home)/page.tsx @@ -1,30 +1,9 @@ -import { FadeIn } from "@/components/FadeIn"; -import { FAQ } from "./faq/FAQ"; -import { Community } from "./community/Community"; -import { Features } from "./features/Features"; -import { Hero } from "./hero/Hero"; -import { Letter } from "./letter/Letter"; +import { HomeContent } from "./_components/HomeContent"; export default function Home() { return (
- - -
- - - -
- - - -
- - - -
- - +
); } diff --git a/docs/app/demo/_components/DemoEditor.tsx b/docs/app/demo/_components/DemoEditor.tsx new file mode 100644 index 0000000000..4783b92725 --- /dev/null +++ b/docs/app/demo/_components/DemoEditor.tsx @@ -0,0 +1,405 @@ +"use client"; + +import { combineByGroup } from "@blocknote/core"; +import { + CommentsExtension, + DefaultThreadStoreAuth, + YjsThreadStore, +} from "@blocknote/core/comments"; +import { filterSuggestionItems } from "@blocknote/core/extensions"; +import "@blocknote/core/fonts/inter.css"; +import * as locales from "@blocknote/core/locales"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { + BlockNoteViewEditor, + FloatingComposerController, + FloatingThreadController, + FormattingToolbar, + FormattingToolbarController, + SuggestionMenuController, + ThreadsSidebar, + getDefaultReactSlashMenuItems, + getFormattingToolbarItems, + useCreateBlockNote, +} from "@blocknote/react"; +import { + AIExtension, + AIMenuController, + AIToolbarButton, + getAISlashMenuItems, +} from "@blocknote/xl-ai"; +import { en as aiEnFile } from "@blocknote/xl-ai/locales"; +import "@blocknote/xl-ai/style.css"; +import { + DOCXExporter, + docxDefaultSchemaMappings, +} from "@blocknote/xl-docx-exporter"; +import { + ODTExporter, + odtDefaultSchemaMappings, +} from "@blocknote/xl-odt-exporter"; +import { + PDFExporter, + pdfDefaultSchemaMappings, +} from "@blocknote/xl-pdf-exporter"; +import { pdf } from "@react-pdf/renderer"; +import { DefaultChatTransport } from "ai"; +import { useTheme } from "next-themes"; +import { useEffect, useMemo, useState } from "react"; +import YPartyKitProvider from "y-partykit/provider"; +import * as Y from "yjs"; +import { EditorMenu } from "./EditorMenu"; +import { HARDCODED_USERS, resolveUsers, uploadFile } from "./utils"; + +import "./styles.css"; + +const AI_API_URL = "http://localhost:3000/api/ai/regular/streamText"; + +// Formatting toolbar with AI button +function FormattingToolbarWithAI() { + return ( + ( + + {...getFormattingToolbarItems()} + + + )} + /> + ); +} + +export function DemoEditor() { + const [roomId, setRoomId] = useState(null); + const [isNewRoom, setIsNewRoom] = useState(false); + + useEffect(() => { + if (typeof window !== "undefined") { + const handleHashChange = () => { + const hash = window.location.hash.replace("#", ""); + if (hash) { + setRoomId(hash); + setIsNewRoom(false); // Manually changed hash -> mostly existing or user choice + } + }; + + // Initial check + const hash = window.location.hash.replace("#", ""); + if (hash) { + setRoomId(hash); + setIsNewRoom(false); + } else { + const newId = Math.random().toString(36).substring(2, 7); + window.location.hash = newId; + setRoomId(newId); + setIsNewRoom(true); + } + + window.addEventListener("hashchange", handleHashChange); + return () => { + window.removeEventListener("hashchange", handleHashChange); + }; + } + }, []); + + const url = + typeof window !== "undefined" && roomId + ? `${window.location.origin}${window.location.pathname}#${roomId}` + : ""; + + const content = !roomId ? ( +
+ {}} + user={HARDCODED_USERS[0]} + setUser={() => {}} + sidebarOpen={true} + onToggleSidebar={() => {}} + disabled={true} + /> +
+
Loading...
+
+
+ ) : ( + + ); + + return ( +
+
+ + ⚡️ Collaborate live! Share this URL: + +
+ e.currentTarget.select()} + /> + +
+
+ + {content} + + +
+ ); +} + +function DemoEditorInner({ + roomId, + isNewRoom, +}: { + roomId: string; + isNewRoom: boolean; +}) { + const { resolvedTheme } = useTheme(); + // const [mounted, setMounted] = useState(false); + const [activeUser, setActiveUser] = useState(HARDCODED_USERS[0]); + const [sidebarOpen, setSidebarOpen] = useState(true); + + const { doc, provider } = useMemo(() => { + const doc = new Y.Doc(); + console.log("open room", "demo/" + roomId); + const provider = new YPartyKitProvider( + "blocknote-dev.yousefed.partykit.dev", + "demo-" + roomId, + doc, + ); + return { doc, provider }; + }, [roomId, activeUser]); + // Thread Store + const threadStore = useMemo(() => { + return new YjsThreadStore( + activeUser.id, + doc.getMap("threads"), + new DefaultThreadStoreAuth(activeUser.id, "editor"), + ); + }, [activeUser, doc]); + + console.log(activeUser); + const editor = useCreateBlockNote( + { + // Schema with MultiColumn & PageBreak + // schema: withMultiColumn(withPageBreak(BlockNoteSchema.create())), + // dropCursor: multiColumnDropCursor, + + // Locales + dictionary: { + ...locales.en, + // multi_column: multiColumnLocales.en, + ai: aiEnFile, + }, + + // Collaboration + collaboration: { + provider, + fragment: doc.getXmlFragment("blocknote"), + user: { color: activeUser.color, name: activeUser.username }, + }, + + // Extensions: AI, Comments + extensions: [ + AIExtension({ + transport: new DefaultChatTransport({ + api: AI_API_URL, + }), + }), + CommentsExtension({ threadStore, resolveUsers }), + ], + + uploadFile, + }, + [activeUser, threadStore, provider, doc], + ); + + const getSlashMenuItems = useMemo( + () => async (query: string) => + filterSuggestionItems( + combineByGroup( + getDefaultReactSlashMenuItems(editor), + // getPageBreakReactSlashMenuItems(editor), + // getMultiColumnSlashMenuItems(editor), + getAISlashMenuItems(editor), + ), + query, + ), + [editor], + ); + + useEffect(() => { + if (editor && isNewRoom) { + editor.replaceBlocks(editor.document, [ + { + type: "heading", + content: "Welcome to BlockNote!", + }, + { + type: "paragraph", + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "This is a demo of BlockNote with ", + styles: {}, + }, + { + type: "text", + text: "multi-cursor collaboration", + styles: { bold: true }, + }, + { + type: "text", + text: ", ", + styles: {}, + }, + { + type: "text", + text: "live comments", + styles: { bold: true }, + }, + { + type: "text", + text: ", and ", + styles: {}, + }, + { + type: "text", + text: "AI features", + styles: { bold: true }, + }, + { + type: "text", + text: ".", + styles: {}, + }, + ], + }, + { + type: "paragraph", + }, + { + type: "paragraph", + content: "Share the URL above to collaborate with others ⚡️", + }, + { + type: "paragraph", + }, + ]); + } + }, [editor, isNewRoom]); + + const handleExport = async (format: "pdf" | "docx" | "odt") => { + let blob: Blob; + let filename = `blocknote-export.${format}`; + + if (format === "pdf") { + const exporter = new PDFExporter(editor.schema, pdfDefaultSchemaMappings); + const pdfDocs = await exporter.toReactPDFDocument(editor.document); + blob = await pdf(pdfDocs).toBlob(); + } else if (format === "docx") { + const exporter = new DOCXExporter( + editor.schema, + docxDefaultSchemaMappings, + ); + blob = await exporter.toBlob(editor.document); + } else if (format === "odt") { + const exporter = new ODTExporter(editor.schema, odtDefaultSchemaMappings); + blob = await exporter.toODTDocument(editor.document); + } else { + return; + } + + const link = document.createElement("a"); + link.href = window.URL.createObjectURL(blob); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + if (!editor) { + return null; + } + + return ( +
+
+ setSidebarOpen(!sidebarOpen)} + /> +
+ +
+ + + + + +
+
+
+ +
+ + {!sidebarOpen && } +
+ +
+
+ Comments +
+
+ +
+
+
+
+
+
+ ); +} diff --git a/docs/app/demo/_components/EditorMenu.tsx b/docs/app/demo/_components/EditorMenu.tsx new file mode 100644 index 0000000000..df7c23b64b --- /dev/null +++ b/docs/app/demo/_components/EditorMenu.tsx @@ -0,0 +1,118 @@ +import { Download, PanelRight, Share, User } from "lucide-react"; +import { HARDCODED_USERS } from "./utils"; + +interface EditorMenuProps { + onExport: (format: "pdf" | "docx" | "odt") => void; + user: { id: string; username: string; color: string }; + setUser: (user: { + id: string; + username: string; + color: string; + avatarUrl: string; + }) => void; + sidebarOpen: boolean; + onToggleSidebar: () => void; + disabled?: boolean; +} + +export function EditorMenu({ + onExport, + user, + setUser, + sidebarOpen, + onToggleSidebar, + disabled, +}: EditorMenuProps) { + return ( +
+
+
+
+
+
+ +
+
+ + +
+ +
+ +
+ +
+ +
+ + + +
+
+
+ +
+ + +
+
+ ); +} diff --git a/docs/app/demo/_components/styles.css b/docs/app/demo/_components/styles.css new file mode 100644 index 0000000000..dfa306fbc8 --- /dev/null +++ b/docs/app/demo/_components/styles.css @@ -0,0 +1,14 @@ +/* Custom scrollbar for sidebar */ +.bn-threads-sidebar { + height: 100%; +} + +/* Ensure editor content takes full height */ +.bn-editor { + min-height: 100%; +} + +.bn-threads-sidebar .bn-thread { + /* todo: fix in blocknote */ + min-width: auto !important; +} diff --git a/docs/app/demo/_components/utils.ts b/docs/app/demo/_components/utils.ts new file mode 100644 index 0000000000..385318cd43 --- /dev/null +++ b/docs/app/demo/_components/utils.ts @@ -0,0 +1,80 @@ +export const HARDCODED_USERS = [ + { + id: "user-1", + username: "Kev", + color: "#ff0000", + avatarUrl: "https://github.com/shadcn.png", + }, + { + id: "user-2", + username: "Matt", + color: "#00ff00", + avatarUrl: "https://github.com/shadcn.png", + }, + { + id: "user-3", + username: "Sef", + color: "#0000ff", + avatarUrl: "https://github.com/shadcn.png", + }, + { + id: "user-4", + username: "Nicky", + color: "#ff00ff", + avatarUrl: "https://github.com/shadcn.png", + }, + { + id: "user-5", + username: "Sam", + color: "#00ffff", + avatarUrl: "https://github.com/shadcn.png", + }, + { + id: "user-6", + username: "Walter", + color: "#ffff00", + avatarUrl: "https://github.com/shadcn.png", + }, + { + id: "user-7", + username: "Amanda", + color: "#ffa500", + avatarUrl: "https://github.com/shadcn.png", + }, + { + id: "user-8", + username: "Sara", + color: "#800080", + avatarUrl: "https://github.com/shadcn.png", + }, +]; + +export const getRandomUser = () => { + return HARDCODED_USERS[Math.floor(Math.random() * HARDCODED_USERS.length)]; +}; + +// The resolveUsers function fetches information about your users +// (e.g. their name, avatar, etc.). Usually, you'd fetch this from your +// own database or user management system. +// Here, we just return the hardcoded users +export async function resolveUsers(userIds: string[]) { + // fake a (slow) network request + await new Promise((resolve) => setTimeout(resolve, 500)); + + return HARDCODED_USERS.filter((user) => userIds.includes(user.id)); +} + +// Uploads a file to tmpfiles.org and returns the URL to the uploaded file. +export async function uploadFile(file: File) { + const body = new FormData(); + body.append("file", file); + + const ret = await fetch("https://tmpfiles.org/api/v1/upload", { + method: "POST", + body: body, + }); + return (await ret.json()).data.url.replace( + "tmpfiles.org/", + "tmpfiles.org/dl/", + ); +} diff --git a/docs/app/demo/layout.tsx b/docs/app/demo/layout.tsx new file mode 100644 index 0000000000..be4a00255b --- /dev/null +++ b/docs/app/demo/layout.tsx @@ -0,0 +1,6 @@ +import { HomeLayout } from "@/components/fumadocs/layout/home"; +import { baseOptions } from "@/lib/layout.shared"; + +export default function Layout({ children }: LayoutProps<"/">) { + return {children}; +} diff --git a/docs/app/demo/page.tsx b/docs/app/demo/page.tsx new file mode 100644 index 0000000000..6e7a5833f2 --- /dev/null +++ b/docs/app/demo/page.tsx @@ -0,0 +1,14 @@ +import { DemoEditor } from "./_components/DemoEditor"; + +export default function DemoPage() { + return ( +
+
+

+ Try BlockNote +

+
+ +
+ ); +} diff --git a/docs/app/pricing/page.tsx b/docs/app/pricing/page.tsx index e7095d8f07..b90b775a7b 100644 --- a/docs/app/pricing/page.tsx +++ b/docs/app/pricing/page.tsx @@ -1,6 +1,12 @@ import { FAQ } from "@/app/pricing/faq"; import { Tier, Tiers } from "@/app/pricing/tiers"; -import { SectionSubHeader } from "@/components/Headings"; +import { InfiniteSlider } from "@/components/InfiniteSlider"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { getFullMetadata } from "@/lib/getFullMetadata"; import Link from "next/link"; @@ -9,103 +15,178 @@ export const metadata = getFullMetadata({ path: "/pricing", }); +const sponsors = [ + { name: "Semrush", logo: "/img/sponsors/semrush.light.png" }, + { name: "NLnet", logo: "/img/sponsors/nlnetLight.svg" }, + { name: "DINUM", logo: "/img/sponsors/dinumLight.svg" }, + { name: "ZenDiS", logo: "/img/sponsors/zendis.svg" }, + { name: "OpenProject", logo: "/img/sponsors/openproject.svg" }, + { name: "Poggio", logo: "/img/sponsors/poggioLight.svg" }, + { name: "Capitol", logo: "/img/sponsors/capitolLight.svg" }, + { name: "Twenty", logo: "/img/sponsors/twentyLight.png" }, + { name: "Deep Origin", logo: "/img/sponsors/deepOrigin.svg" }, + { name: "Krisp", logo: "/img/sponsors/krisp.svg" }, +]; + const tiers: Tier[] = [ - // { - // id: "tier-free", - // title: "Community", - // description: "Everything necessary to get started for individuals and non-commercial projects.", - // price: "Free", - // features: [ - // "Use BlockNote for free (open source license)", - // "Community Discord for help & feedback", - // ], - // href: "/docs/", - // }, { - id: "starter", - mostPopular: false, - title: "Starter", - description: - "Best for individuals and Open Source projects looking to support BlockNote.", - price: { month: 90, year: 24 }, + id: "free", + title: "Community", + icon: "💚", + tagline: "Get Started", + description: ( + <> + Everything you need to get started.{" "} + + + + Liberally licensed + + + BlockNote is MPL-licensed. This is close to MIT and free for any + use. The key difference is a "share-alike" requirement: if you + modify BlockNote's internal files, you must share those specific + changes. + + + {" "} + and free for any project. + + ), + price: "Free", features: [ - "Access to all Pro Examples", - "Prioritized Bug Reports on GitHub", - "Support maintenance and new versions of our open source library", - "XL packages only available for open source projects under GPL-3.0 or early stage startups", + "All blocks & UI components", + + "Drag-and-drop editing", + "Slash commands & menus", + "Real-time collaboration", + "Comments", + + XL Packages free for OSS under GPL-3.0 + , ], + cta: "Get Started", + href: "/docs", }, { id: "business", title: "Business", + icon: "⚡", + tagline: "Go premium", mostPopular: true, + badge: "Recommended", description: - // "Best for companies who need a commercial license for XL features.", - - "Best for companies that want a direct line to the team and a commercial license.", + "Commercial license for access to advanced features and technical support.", price: { month: 390, year: 48 }, features: [ - Commercial license for XL packages:, - "- AI integration", - "- Multi-column layouts", - "- Export to PDF, Docx, ODT, Email", - "Access to all Pro Examples", - "Prioritized Bug Reports on GitHub", - "Support maintenance and new versions of our open source library", - "Logo on our website and repositories", + + Commercial license for XL packages: + , + + • AI integration + , + + • Multi-column layouts + , + + • Export to PDF, Docx, ODT, Email + , + "Logo on website and repositories", - Standard Support included ( - see SLA) + Standard Support ( + + see SLA + + ) , ], + cta: "Start free trial", }, { id: "enterprise", title: "Enterprise", - description: - "Collaborate directly with the BlockNote team for dedicated consulting and support.", - price: "Tailored pricing", + icon: "🏢", + tagline: "Sustainable partnerships", + description: "Custom licensing, dedicated support, and design partnership.", + price: "Custom", features: [ - "Development of BlockNote features required for your organization", - "Access to a private Slack channel with the maintainers", - "Guidance on integrating BlockNote into your project", - Commercial license for XL packages:, - "- AI integration", - "- Multi-column layouts", - "- Export to PDF, Docx, ODT, Email", - "Access to all Pro Examples", - "Prioritized Bug Reports and Feature Requests on GitHub", - "Support maintenance and new versions of our open source library", - "Logo on our website and repositories", + + Everything in Business, plus: + , + "Custom BlockNote feature development", + "Private Slack channel with maintainers", + "Onboarding and integration guidance", - Priority Support included ( - see SLA) + Priority Support ( + + see SLA + + ) , ], href: "/about/", + cta: "Contact us", }, ]; export default function Pricing() { return ( -
-
-

- BlockNote Pro -

- Upgrade your BlockNote experience -

- Get direct support from the maintainers, access Pro Examples -
and get your commercial license for XL Packages such as - BlockNote AI. -
- Your subscription helps us to maintain and develop BlockNote. -

+
+
+ {/* Header */} +
+

+ Pricing +

+

+ 100% Open Source. +
+ + Fair Pricing. + +

+

+ The majority of BlockNote is liberally licensed and free to use for + any purpose. The dual-licensed XL features (like AI) are free for + open source projects, but require a commercial license for + closed-source applications. +

+
+ + {/* Pricing Tiers */} -

- BlockNote is 100% open source software that organizations of all sizes - are using to add polished editing experiences to their apps. -

+ + {/* Social proof */} +
+

+ Trusted by teams building the future of collaboration +

+ + {sponsors.map((sponsor) => ( +
+ {sponsor.name} +
+ ))} +
+
+ + {/* FAQ */}
diff --git a/docs/app/pricing/tiers.tsx b/docs/app/pricing/tiers.tsx index 1dd63e8039..dc67426726 100644 --- a/docs/app/pricing/tiers.tsx +++ b/docs/app/pricing/tiers.tsx @@ -11,57 +11,32 @@ type Frequency = "month" | "year"; export type Tier = { id: string; mostPopular?: boolean; + theme?: "green" | "purple" | "default"; + badge?: string; + icon?: string; title: string; - description: string; + tagline?: string; + description: React.ReactNode; price: Record | string; features: React.ReactNode[]; href?: string; + cta?: string; }; -function TierTitle({ tier }: { tier: Tier }) { - return ( -

- {tier.title} -

- ); -} - -function TierPrice({ tier, frequency }: { tier: Tier; frequency: Frequency }) { - return ( -

- {typeof tier.price === "string" - ? tier.price - : `$${tier.price[frequency]} / ${frequency}`} -

- ); -} - -function TierHeader({ tier, frequency }: { tier: Tier; frequency: Frequency }) { - return ( -
- - -
- ); -} - -function TierDescription({ tier }: { tier: Tier }) { - return

{tier.description}

; -} +// ... (TierCTAButton and TierFeature remain unchanged, I'll use context to skip them or just target the Tier definition and Tiers component if possible, but replace_file_content is convenient for small files) +// Wait, replace_file_content replaces a contiguous block. I need to replace Tier type AND the rendering. They are far apart. +// I'll use multi_replace_file_content or just replace the Tier type first, then the rendering. +// tiers.tsx is small enough, maybe I can do it in one go if I include everything between. +// But Tier rendering is at the bottom. +// I will use multi_replace_file_content. function TierCTAButton({ tier }: { tier: Tier }) { const { data: session } = useSession(); - let text = "Sign up"; + let text = tier.cta || "Sign up"; if (session) { if (session.planType === "free") { - text = "Buy now"; + text = tier.cta || "Buy now"; } else { text = session.planType === tier.id @@ -69,19 +44,34 @@ function TierCTAButton({ tier }: { tier: Tier }) { : "Update subscription"; } } + + // Theme-based button styles + const isGreen = tier.theme === "green"; + const isPurple = tier.mostPopular; // Keep purple for most popular + + const buttonClasses = cn( + "group inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl py-3.5 text-center text-sm font-semibold transition-all shadow-sm", + isPurple && + "bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-purple-500/25 hover:shadow-xl hover:shadow-purple-500/30", + isGreen && + "bg-white border-2 border-green-200 text-green-700 hover:border-green-300 hover:shadow-md hover:shadow-green-500/10", + !isPurple && + !isGreen && + "bg-white border border-stone-300 text-stone-900 hover:border-purple-300 hover:text-purple-600", + ); + return ( { - // This event is deprecated, but we keep it to use as a baseline for analytics track("Signup", { tier: tier.id }); - + // ... rest of analytic logic kept simple for brevity in replacement, + // in real implementation we keep the existing logic. + // Re-injecting existing analytics logic below to ensure no regression. if (!session) { Sentry.captureEvent({ message: "click-pricing-signup", level: "info", - extra: { - tier: tier.id, - }, + extra: { tier: tier.id }, }); track("click-pricing-signup", { tier: tier.id }); return; @@ -91,33 +81,25 @@ function TierCTAButton({ tier }: { tier: Tier }) { Sentry.captureEvent({ message: "click-pricing-buy-now", level: "info", - extra: { - tier: tier.id, - }, + extra: { tier: tier.id }, }); track("click-pricing-buy-now", { tier: tier.id }); e.preventDefault(); e.stopPropagation(); - await authClient.checkout({ - slug: tier.id, - }); + await authClient.checkout({ slug: tier.id }); } else { if (session.planType === tier.id) { Sentry.captureEvent({ message: "click-pricing-manage-subscription", level: "info", - extra: { - tier: tier.id, - }, + extra: { tier: tier.id }, }); track("click-pricing-manage-subscription", { tier: tier.id }); } else { Sentry.captureEvent({ message: "click-pricing-update-subscription", level: "info", - extra: { - tier: tier.id, - }, + extra: { tier: tier.id }, }); track("click-pricing-update-subscription", { tier: tier.id }); } @@ -128,37 +110,44 @@ function TierCTAButton({ tier }: { tier: Tier }) { }} href={tier.href ?? (session ? undefined : "/signup")} aria-describedby={tier.id} - className={cn( - tier.mostPopular - ? "text-fd-background dark:text-fd-foreground bg-indigo-600 shadow-sm hover:bg-indigo-500" - : "text-indigo-600 ring-1 ring-inset ring-indigo-600 hover:text-indigo-500 hover:ring-indigo-500", - "mt-8 block cursor-pointer rounded-md px-3 py-2 text-center text-sm font-semibold leading-6 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", - )} + className={buttonClasses} > - {tier.id === "enterprise" ? "Get in touch" : text} + {tier.id === "enterprise" ? tier.cta || "Contact us" : text} + + → + ); } function TierFeature({ feature }: { feature: React.ReactNode }) { - return ( -
  • -
  • - ); -} + const isSubFeature = + typeof feature === "object" && + feature !== null && + "props" in feature && + (feature as { props?: { className?: string } }).props?.className?.includes( + "ml-4", + ); -function TierFeatures({ tier }: { tier: Tier }) { return ( -
      - {tier.features.map((feature, index) => ( - - ))} -
    +
  • + {!isSubFeature && ( +
    + +
    + )} + {feature} +
  • ); } @@ -170,21 +159,82 @@ export function Tiers({ frequency: Frequency; }) { return ( -
    - {tiers.map((tier) => ( -
    - - - - -
    - ))} +
    + {tiers.map((tier) => { + const isGreen = tier.theme === "green"; + const isPurple = tier.mostPopular; + + return ( +
    + {/* Popular badge */} + {tier.mostPopular && ( +
    + + {tier.badge ?? "Most Popular"} + +
    + )} + + {/* Header */} +
    + {tier.icon &&
    {tier.icon}
    } +

    + {tier.title} +

    + {tier.tagline && ( +

    + {tier.tagline} +

    + )} +
    + + {/* Price */} +
    + {typeof tier.price === "string" ? ( + + {tier.price} + + ) : ( +
    + + ${tier.price[frequency]} + + + /{frequency} + +
    + )} +
    + + {/* Description */} +

    + {tier.description} +

    + + {/* CTA */} +
    + +
    + + {/* Features */} +
      + {tier.features.map((feature, index) => ( + + ))} +
    +
    + ); + })}
    ); } diff --git a/docs/components/Footer.tsx b/docs/components/Footer.tsx index 5d9c683e59..7313cb1374 100644 --- a/docs/components/Footer.tsx +++ b/docs/components/Footer.tsx @@ -9,7 +9,7 @@ import ThemedImage from "@/components/ThemedImage"; function FooterLink({ href, children }: { href: string; children: ReactNode }) { const classes = - "text-sm text-fd-muted-foreground no-underline transition hover:text-fd-foreground"; + "text-sm text-stone-500 no-underline transition-colors hover:text-purple-600 block py-1"; if (href.startsWith("http")) { return ( @@ -25,7 +25,9 @@ function FooterLink({ href, children }: { href: string; children: ReactNode }) { } function FooterHeader({ children }: { children: ReactNode }) { - return

    {children}

    ; + return ( +

    {children}

    + ); } const navigation = { @@ -69,86 +71,75 @@ export function FooterContent() {
    -
    - {/* Subscribe to our newsletter */} +
    -

    +

    BlockNote is an extensible React rich text editor with support for - block-based editing, collaboration and comes with ready-to-use - customizable UI components. + block-based editing, real-time collaboration, and comes with + ready-to-use customizable UI components.

    - {/* */}
    -
    -
    -
    - Learn -
      - {navigation.general.map((item) => ( -
    • - {item.name} -
    • - ))} -
    -
    -
    - Collaborate -
      - {navigation.collaborate().map((item) => ( -
    • - {item.name} -
    • - ))} -
    -
    -
    - Community -
      - {navigation.community.map((item) => ( -
    • - {item.name} -
    • - ))} -
    -
    -
    - Legal -
      -
    • - - Terms & Conditions - +
      +
      + Learn +
        + {navigation.general.map((item) => ( +
      • + {item.name}
      • -
      • - - Privacy Policy - + ))} +
      +
      +
      + Collaborate +
        + {navigation.collaborate().map((item) => ( +
      • + {item.name}
      • -
      -
      -
      - Theme -
        -
      • - + ))} +
      +
      +
      + Community +
        + {navigation.community.map((item) => ( +
      • + {item.name}
      • -
      -
      + ))} +
    +
    +
    + Legal & Theme +
      +
    • + + Terms & Conditions + +
    • +
    • + + Privacy Policy + +
    • +
    • + +
    • +
    -
    -
    -

    - © {new Date().getFullYear()} BlockNote maintainers. All - rights reserved. -

    -
    +
    +

    + © {new Date().getFullYear()} BlockNote maintainers. All rights + reserved. +

    @@ -157,19 +148,10 @@ export function FooterContent() { export function Footer({ menu }: { menu?: boolean }): ReactElement { return ( -