87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | )
17 | Drawer.displayName = "Drawer"
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal
22 |
23 | const DrawerClose = DrawerPrimitive.Close
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ))
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ))
56 | DrawerContent.displayName = "DrawerContent"
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | )
67 | DrawerHeader.displayName = "DrawerHeader"
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | )
78 | DrawerFooter.displayName = "DrawerFooter"
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ))
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ))
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | }
119 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Google Docs Clone
2 |
3 | 
4 |
5 | This is a Google Docs clone built with **Next.js 15**, **React**, **Tailwind CSS**, and real-time collaboration features using **Liveblocks**, **Convex**, and **Clerk** for authentication.
6 |
7 | ⭐ **Please give a star to my repo; it will help me a lot!** ⭐
8 |
9 | ---
10 |
11 | ## Features
12 |
13 | - **Real-time collaboration**
14 | - **Rich text editing** with support for:
15 | - Tables
16 | - Images with resizing
17 | - Task lists
18 | - and many more...
19 | - **Customizable themes** with Tailwind CSS
20 | - **Responsive design**
21 | - **Toolbar for common actions**
22 |
23 | ---
24 |
25 | ## Getting Started
26 |
27 | ### Prerequisites
28 |
29 | Before you begin, ensure you have the following:
30 |
31 | - **Node.js** installed on your machine
32 | - An account on [Convex](https://convex.dev)
33 | - An account on [Clerk](https://clerk.dev)
34 | - An account on [Liveblocks](https://liveblocks.io)
35 |
36 | ### Setup
37 |
38 | 1. **Clone the repository:**
39 |
40 | ```bash
41 | git clone https://github.com/sufiprog/docs-clone.git
42 | cd docs-clone
43 | npm install
44 | ```
45 |
46 | 2. **Set up Convex:**
47 | - Create an account on Convex and create a new project.
48 | - Follow the instructions to set up your database.
49 | - Copy the Convex deployment URL and secret code.
50 |
51 | 3. **Set up Clerk:**
52 | - Create an account on Clerk and create a new application.
53 | - Copy the Clerk publishable key and secret key.
54 |
55 | 4. **Set up Liveblocks:**
56 | - Create an account on Liveblocks and create a new project.
57 | - Copy the Liveblocks public key and secret key.
58 |
59 | 5. **Create a `.env.local` file:**
60 | - Add the following environment variables to the file in the root directory of your project:
61 |
62 | ```env
63 | CONVEX_DEPLOYMENT=your_convex_secret_code
64 | NEXT_PUBLIC_CONVEX_URL=your_convex_public_url
65 |
66 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key
67 | CLERK_SECRET_KEY=your_clerk_secret_key
68 |
69 | LIVE_BLOCK_SECRET_KEY=your_liveblocks_secret_key
70 | ```
71 |
72 | 6. **Run the development server:**
73 |
74 | ```bash
75 | npm run dev
76 | ```
77 |
78 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
79 |
80 | ---
81 |
82 | ## Deployment
83 |
84 | The easiest way to deploy your Next.js app is to use the **Vercel Platform**, created by the Next.js team.
85 |
86 | ### Deploy on Vercel:
87 |
88 | 1. Sign up for a [Vercel](https://vercel.com) account if you don’t have one.
89 | 2. Import your project from GitHub.
90 | 3. Add the same environment variables in the Vercel dashboard.
91 | 4. Deploy your project.
92 |
93 | ---
94 |
95 | ## Learn More
96 |
97 | To learn more about the technologies used in this project, take a look at the following resources:
98 |
99 | - [Next.js Documentation](https://nextjs.org/docs) - Learn about Next.js features and API.
100 | - [Convex Documentation](https://docs.convex.dev) - Learn about Convex features and API.
101 | - [Clerk Documentation](https://clerk.dev/docs) - Learn about Clerk features and API.
102 | - [Liveblocks Documentation](https://liveblocks.io/docs) - Learn about Liveblocks features and API.
103 |
104 | ---
105 |
106 | ## Contributing
107 |
108 | Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.
109 |
110 | ---
111 |
112 | ## License
113 |
114 | This project is licensed under the **MIT License**.
115 |
116 |
--------------------------------------------------------------------------------
/convex/_generated/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated utilities for implementing server-side Convex query and mutation functions.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import {
12 | actionGeneric,
13 | httpActionGeneric,
14 | queryGeneric,
15 | mutationGeneric,
16 | internalActionGeneric,
17 | internalMutationGeneric,
18 | internalQueryGeneric,
19 | } from "convex/server";
20 |
21 | /**
22 | * Define a query in this Convex app's public API.
23 | *
24 | * This function will be allowed to read your Convex database and will be accessible from the client.
25 | *
26 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
27 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
28 | */
29 | export const query = queryGeneric;
30 |
31 | /**
32 | * Define a query that is only accessible from other Convex functions (but not from the client).
33 | *
34 | * This function will be allowed to read from your Convex database. It will not be accessible from the client.
35 | *
36 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
37 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
38 | */
39 | export const internalQuery = internalQueryGeneric;
40 |
41 | /**
42 | * Define a mutation in this Convex app's public API.
43 | *
44 | * This function will be allowed to modify your Convex database and will be accessible from the client.
45 | *
46 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
47 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
48 | */
49 | export const mutation = mutationGeneric;
50 |
51 | /**
52 | * Define a mutation that is only accessible from other Convex functions (but not from the client).
53 | *
54 | * This function will be allowed to modify your Convex database. It will not be accessible from the client.
55 | *
56 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
57 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
58 | */
59 | export const internalMutation = internalMutationGeneric;
60 |
61 | /**
62 | * Define an action in this Convex app's public API.
63 | *
64 | * An action is a function which can execute any JavaScript code, including non-deterministic
65 | * code and code with side-effects, like calling third-party services.
66 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
67 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
68 | *
69 | * @param func - The action. It receives an {@link ActionCtx} as its first argument.
70 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
71 | */
72 | export const action = actionGeneric;
73 |
74 | /**
75 | * Define an action that is only accessible from other Convex functions (but not from the client).
76 | *
77 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
78 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
79 | */
80 | export const internalAction = internalActionGeneric;
81 |
82 | /**
83 | * Define a Convex HTTP action.
84 | *
85 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
86 | * as its second.
87 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
88 | */
89 | export const httpAction = httpActionGeneric;
90 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs-clone",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@clerk/nextjs": "^6.9.0",
13 | "@hookform/resolvers": "^3.9.1",
14 | "@liveblocks/client": "^2.14.0",
15 | "@liveblocks/node": "^2.14.0",
16 | "@liveblocks/react": "^2.14.0",
17 | "@liveblocks/react-tiptap": "^2.14.0",
18 | "@liveblocks/react-ui": "^2.14.0",
19 | "@radix-ui/react-accordion": "^1.2.1",
20 | "@radix-ui/react-alert-dialog": "^1.1.2",
21 | "@radix-ui/react-aspect-ratio": "^1.1.0",
22 | "@radix-ui/react-avatar": "^1.1.1",
23 | "@radix-ui/react-checkbox": "^1.1.2",
24 | "@radix-ui/react-collapsible": "^1.1.1",
25 | "@radix-ui/react-context-menu": "^2.2.2",
26 | "@radix-ui/react-dialog": "^1.1.2",
27 | "@radix-ui/react-dropdown-menu": "^2.1.2",
28 | "@radix-ui/react-hover-card": "^1.1.2",
29 | "@radix-ui/react-label": "^2.1.0",
30 | "@radix-ui/react-menubar": "^1.1.2",
31 | "@radix-ui/react-navigation-menu": "^1.2.1",
32 | "@radix-ui/react-popover": "^1.1.2",
33 | "@radix-ui/react-progress": "^1.1.0",
34 | "@radix-ui/react-radio-group": "^1.2.1",
35 | "@radix-ui/react-scroll-area": "^1.2.1",
36 | "@radix-ui/react-select": "^2.1.2",
37 | "@radix-ui/react-separator": "^1.1.0",
38 | "@radix-ui/react-slider": "^1.2.1",
39 | "@radix-ui/react-slot": "^1.1.0",
40 | "@radix-ui/react-switch": "^1.1.1",
41 | "@radix-ui/react-tabs": "^1.1.1",
42 | "@radix-ui/react-toast": "^1.2.2",
43 | "@radix-ui/react-toggle": "^1.1.0",
44 | "@radix-ui/react-toggle-group": "^1.1.0",
45 | "@radix-ui/react-tooltip": "^1.1.4",
46 | "@tiptap/extension-collaboration": "^2.10.3",
47 | "@tiptap/extension-collaboration-cursor": "^2.10.3",
48 | "@tiptap/extension-color": "^2.10.2",
49 | "@tiptap/extension-font-family": "^2.10.2",
50 | "@tiptap/extension-heading": "^2.10.2",
51 | "@tiptap/extension-highlight": "^2.10.2",
52 | "@tiptap/extension-image": "^2.10.2",
53 | "@tiptap/extension-link": "^2.10.2",
54 | "@tiptap/extension-table": "^2.10.2",
55 | "@tiptap/extension-table-cell": "^2.10.2",
56 | "@tiptap/extension-table-header": "^2.10.2",
57 | "@tiptap/extension-table-row": "^2.10.2",
58 | "@tiptap/extension-task-item": "^2.10.2",
59 | "@tiptap/extension-task-list": "^2.10.2",
60 | "@tiptap/extension-text-align": "^2.10.2",
61 | "@tiptap/extension-text-style": "^2.10.2",
62 | "@tiptap/extension-underline": "^2.10.2",
63 | "@tiptap/pm": "^2.10.2",
64 | "@tiptap/react": "^2.10.2",
65 | "@tiptap/starter-kit": "^2.10.2",
66 | "class-variance-authority": "^0.7.1",
67 | "clsx": "^2.1.1",
68 | "cmdk": "^1.0.0",
69 | "convex": "^1.17.3",
70 | "date-fns": "^4.1.0",
71 | "embla-carousel-react": "^8.5.1",
72 | "input-otp": "^1.4.1",
73 | "lucide-react": "^0.468.0",
74 | "next": "15.0.4",
75 | "next-themes": "^0.4.4",
76 | "nuqs": "^2.2.3",
77 | "react": "^19.0.0",
78 | "react-color": "^2.19.3",
79 | "react-day-picker": "^8.10.1",
80 | "react-dom": "^19.0.0",
81 | "react-hook-form": "^7.54.0",
82 | "react-icons": "^5.3.0",
83 | "react-resizable-panels": "^2.1.7",
84 | "recharts": "^2.14.1",
85 | "sonner": "^1.7.1",
86 | "tailwind-merge": "^2.5.5",
87 | "tailwindcss-animate": "^1.0.7",
88 | "tiptap-extension-resize-image": "^1.2.1",
89 | "vaul": "^1.1.1",
90 | "y-protocols": "^1.0.6",
91 | "zod": "^3.23.8",
92 | "zustand": "^5.0.1"
93 | },
94 | "devDependencies": {
95 | "@types/node": "^20",
96 | "@types/react": "^19",
97 | "@types/react-color": "^3.0.12",
98 | "@types/react-dom": "^19",
99 | "eslint": "^8",
100 | "eslint-config-next": "15.0.4",
101 | "postcss": "^8",
102 | "tailwindcss": "^3.4.1",
103 | "typescript": "^5"
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/constants/templates.ts:
--------------------------------------------------------------------------------
1 | export const templates = [
2 | {
3 | id: "blank",
4 | label: "Blank Document",
5 | imageUrl: "/blank-document.svg",
6 | initialContent: ""
7 | },
8 | {
9 | id: "software-proposal",
10 | label: "Software development proposal",
11 | imageUrl: "/software-proposal.svg",
12 | initialContent: `
13 | Software Development Proposal
14 | Project Overview:
15 | Provide a brief description of the project, its goals, and objectives.
16 | Scope of Work:
17 |
18 | - Task 1
19 | - Task 2
20 | - Task 3
21 |
22 | Timeline:
23 | Start Date: [Enter start date]
24 | End Date: [Enter end date]
25 | Budget:
26 | [Enter budget]
27 | `
28 | },
29 | {
30 | id: "project-proposal",
31 | label: "Project proposal",
32 | imageUrl: "/project-proposal.svg",
33 | initialContent: `
34 | Project Proposal
35 | Project Name: [Enter project name]
36 | Overview:
37 | [Enter project overview]
38 | Objectives:
39 |
40 | - Objective 1
41 | - Objective 2
42 | - Objective 3
43 |
44 | Estimated Timeline:
45 | Start Date: [Enter start date]
46 | End Date: [Enter end date]
47 | `
48 | },
49 | {
50 | id: "business-letter",
51 | label: "Business letter",
52 | imageUrl: "/business-letter.svg",
53 | initialContent: `
54 | Business Letter
55 | Recipient's Name: [Enter recipient's name]
56 | Company Name: [Enter company name]
57 | Subject: [Enter subject]
58 | [Dear recipient,]
59 | [Enter letter content here]
60 | Sincerely,
61 | [Your Name]
62 | [Your Position]
63 | [Company Name]
64 | `
65 | },
66 | {
67 | id: "resume",
68 | label: "Resume",
69 | imageUrl: "/resume.svg",
70 | initialContent: `
71 | Resume
72 | Contact Information
73 | Name: [Your Name]
74 | Email: [Your Email]
75 | Phone: [Your Phone Number]
76 | LinkedIn: [Your LinkedIn Profile]
77 | Professional Experience
78 | Job Title: [Your Job Title]
79 | Company: [Company Name]
80 | Duration: [Start Date] - [End Date]
81 | [Job Responsibilities]
82 | Education
83 | Degree: [Degree]
84 | Institution: [University Name]
85 | Graduation Year: [Year]
86 | `
87 | },
88 | {
89 | id: "cover-letter",
90 | label: "Cover letter",
91 | imageUrl: "/cover-letter.svg",
92 | initialContent: `
93 | Cover Letter
94 | Recipient's Name: [Enter recipient's name]
95 | Company Name: [Enter company name]
96 | Subject: [Enter subject]
97 | [Dear Hiring Manager,]
98 | [Enter letter content here]
99 | Sincerely,
100 | [Your Name]
101 | `
102 | },
103 | {
104 | id: "letter",
105 | label: "Letter",
106 | imageUrl: "/letter.svg",
107 | initialContent: `
108 | Letter
109 | Recipient's Name: [Enter recipient's name]
110 | Address: [Enter address]
111 | Subject: [Enter subject]
112 | [Dear recipient,]
113 | [Enter letter content here]
114 | Sincerely,
115 | [Your Name]
116 | `
117 | }
118 | ];
119 |
--------------------------------------------------------------------------------
/src/app/documents/[documentId]/editor.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEditor, EditorContent } from '@tiptap/react'
4 | import StarterKit from '@tiptap/starter-kit'
5 | import TaskItem from '@tiptap/extension-task-item'
6 | import TaskList from '@tiptap/extension-task-list'
7 | import Table from '@tiptap/extension-table'
8 | import TableCell from '@tiptap/extension-table-cell'
9 | import TableHeader from '@tiptap/extension-table-header'
10 | import TableRow from '@tiptap/extension-table-row'
11 | import Image from '@tiptap/extension-image'
12 | import ImageResize from 'tiptap-extension-resize-image'
13 | import Underline from '@tiptap/extension-underline'
14 | import FontFamily from '@tiptap/extension-font-family'
15 | import TextStyle from '@tiptap/extension-text-style'
16 | import Highlight from '@tiptap/extension-highlight'
17 | import Link from '@tiptap/extension-link'
18 | import TextAlign from '@tiptap/extension-text-align'
19 | import { Color } from '@tiptap/extension-color'
20 | import { useStorage } from '@liveblocks/react';
21 |
22 | import { FontSizeExtension } from '@/extensions/font-size';
23 | import { LineHeightExtension } from '@/extensions/line-height';
24 | import { useEditorStore } from '@/store/use-editor-store';
25 | import { Ruler } from './ruler';
26 | import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
27 | import { Threads } from './threads';
28 |
29 | interface EditorProps {
30 | initialContent?: string | undefined;
31 | }
32 |
33 | export const Editor = ({ initialContent }: EditorProps) => {
34 | const leftMargin = useStorage((root) => root.leftMargin)
35 | const rightMargin = useStorage((root) => root.rightMargin)
36 | const liveblocks = useLiveblocksExtension({
37 | initialContent,
38 | offlineSupport_experimental: true
39 | });
40 | const { setEditor } = useEditorStore()
41 |
42 | const editor = useEditor({
43 | immediatelyRender: false,
44 |
45 | onCreate({ editor }) {
46 | setEditor(editor);
47 | },
48 | onDestroy() {
49 | setEditor(null)
50 | },
51 | onUpdate({ editor }) {
52 | setEditor(editor)
53 | },
54 | onSelectionUpdate({ editor }) {
55 | setEditor(editor)
56 | },
57 | onTransaction({ editor }) {
58 | setEditor(editor)
59 | },
60 | onFocus({ editor }) {
61 | setEditor(editor)
62 | },
63 | onBlur({ editor }) {
64 | setEditor(editor)
65 | },
66 | onContentError({ editor }) {
67 | setEditor(editor)
68 | },
69 | editorProps: {
70 | attributes: {
71 | style: `padding-left: ${leftMargin ?? 56}px; padding-right: ${rightMargin ?? 56}px;`,
72 | class: "focus:outline-none print:border-0 bg-white border border-[#C7C7C7] flex flex-col min-h-[1054px] w-[816px] pt-10 pr-14 pb-10 cursor-text"
73 | },
74 | },
75 | extensions: [
76 | liveblocks,
77 | StarterKit.configure({
78 | history: false,
79 | }),
80 | Color,
81 | FontFamily,
82 | TextStyle,
83 | TaskList,
84 | Table,
85 | TableCell,
86 | TableHeader,
87 | TableRow,
88 | ImageResize,
89 | Underline,
90 | FontSizeExtension,
91 | LineHeightExtension.configure({
92 | types: ['heading', 'paragraph'],
93 | }),
94 | TextAlign.configure({
95 | types: ['heading', 'paragraph'],
96 | }),
97 | Link.configure({
98 | openOnClick: false,
99 | autolink: true,
100 | defaultProtocol: "https",
101 | }),
102 | Highlight.configure({
103 | multicolor: true,
104 | }),
105 | TaskItem.configure({
106 | nested: true,
107 | }),
108 | Image.configure({
109 | inline: true,
110 | }),
111 | ],
112 | content: '',
113 | })
114 |
115 | return (
116 |
117 |
118 |
120 | 
121 |
122 |
123 |
124 |
125 |
126 | );
127 | }
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/convex/documents.ts:
--------------------------------------------------------------------------------
1 | import { ConvexError, v } from "convex/values";
2 | import { paginationOptsValidator } from "convex/server";
3 |
4 | import { mutation, query } from "./_generated/server";
5 |
6 | export const getByIds = query({
7 | args: { ids: v.array(v.id("documents")) },
8 | handler: async (ctx, { ids }) => {
9 | const documents = []
10 |
11 | for (const id of ids) {
12 | const document = await ctx.db.get(id)
13 |
14 | if (document) {
15 | documents.push({ id: document._id, name: document.title })
16 | } else {
17 | documents.push({ id, name: "[Removed]" })
18 | }
19 | }
20 | return documents;
21 | }
22 | })
23 |
24 |
25 | export const create = mutation({
26 | args: { title: v.optional(v.string()), initialContent: v.optional(v.string()) },
27 | handler: async (ctx, args) => {
28 | const user = await ctx.auth.getUserIdentity();
29 |
30 | if (!user) {
31 | throw new ConvexError("Unauthorized");
32 | }
33 |
34 | const organizationId = (user.organization_id ?? undefined) as
35 | | string
36 | | undefined;
37 |
38 | return await ctx.db.insert("documents", {
39 | title: args.title ?? "Untitled document",
40 | ownerId: user.subject,
41 | organizationId,
42 | initialContent: args.initialContent,
43 | });
44 | },
45 | });
46 |
47 | export const get = query({
48 | args: { paginationOpts: paginationOptsValidator, search: v.optional(v.string()) },
49 | handler: async (ctx, { search, paginationOpts }) => {
50 | const user = await ctx.auth.getUserIdentity();
51 |
52 | if (!user) {
53 | throw new ConvexError("Unauthorized")
54 | }
55 |
56 | const organizationId = (user.organization_id ?? undefined) as
57 | | string
58 | | undefined;
59 |
60 | if (search && organizationId) {
61 | return await ctx.db
62 | .query("documents")
63 | .withSearchIndex("search_title", (q) =>
64 | q.search("title", search).eq("organizationId", organizationId)
65 | )
66 | .paginate(paginationOpts)
67 | }
68 |
69 |
70 | if (search) {
71 | return await ctx.db
72 | .query("documents")
73 | .withSearchIndex("search_title", (q) =>
74 | q.search("title", search).eq("ownerId", user.subject))
75 | .paginate(paginationOpts);
76 | }
77 |
78 | if (organizationId) {
79 | return await ctx.db
80 | .query("documents")
81 | .withIndex("by_organization_id", (q) => q.eq("organizationId", organizationId))
82 | .paginate(paginationOpts);
83 | }
84 |
85 | return await ctx.db
86 | .query("documents")
87 | .withIndex("by_owner_id", (q) => q.eq("ownerId", user.subject))
88 | .paginate(paginationOpts);
89 | },
90 | }
91 |
92 | );
93 |
94 | export const removeById = mutation({
95 | args: { id: v.id("documents") },
96 | handler: async (ctx, args) => {
97 | const user = await ctx.auth.getUserIdentity();
98 | if (!user) {
99 | throw new ConvexError("Unauthorized");
100 | }
101 |
102 | const organizationId = (user.organization_id ?? undefined) as
103 | | string
104 | | undefined;
105 |
106 | const document = await ctx.db.get(args.id);
107 | if (!document) {
108 | throw new ConvexError("Document not found");
109 | }
110 |
111 | const isOwner = document.ownerId === user.subject;
112 | const isOrganizationMember = !!(document.organizationId && document.organizationId === organizationId)
113 |
114 | if (!isOwner && !isOrganizationMember) {
115 | throw new ConvexError("Unauthorized");
116 | }
117 | return await ctx.db.delete(args.id);
118 | },
119 | });
120 |
121 | export const updateById = mutation({
122 | args: { id: v.id("documents"), title: v.string() },
123 | handler: async (ctx, args) => {
124 | const user = await ctx.auth.getUserIdentity();
125 | if (!user) {
126 | throw new ConvexError("Unauthorized");
127 | }
128 | const organizationId = (user.organization_id ?? undefined) as
129 | | string
130 | | undefined;
131 |
132 | const document = await ctx.db.get(args.id);
133 | if (!document) {
134 | throw new ConvexError("Document not found");
135 | }
136 |
137 | const isOwner = document.ownerId === user.subject;
138 | const isOrganizationMember = !!(document.organizationId && document.organizationId === organizationId)
139 |
140 | if (!isOwner && !isOrganizationMember) {
141 | throw new ConvexError("Unauthorized");
142 | }
143 | return await ctx.db.patch(args.id, { title: args.title });
144 | },
145 | });
146 |
147 | export const getById = query({
148 | args: { id: v.id("documents") },
149 | handler: async (ctx, { id }) => {
150 | const document = await ctx.db.get(id)
151 |
152 | if (!document) {
153 | throw new ConvexError("Document not found")
154 | }
155 |
156 | return document
157 | },
158 | })
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */ // If you want to disable the unused variable warning globally
2 |
3 | "use client";
4 |
5 | // Inspired by react-hot-toast library
6 | import * as React from "react";
7 |
8 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
9 |
10 | const TOAST_LIMIT = 1;
11 | const TOAST_REMOVE_DELAY = 1000000;
12 |
13 | type ToasterToast = ToastProps & {
14 | id: string;
15 | title?: React.ReactNode;
16 | description?: React.ReactNode;
17 | action?: ToastActionElement;
18 | };
19 |
20 | const actionTypes = {
21 | ADD_TOAST: "ADD_TOAST",
22 | UPDATE_TOAST: "UPDATE_TOAST",
23 | DISMISS_TOAST: "DISMISS_TOAST",
24 | REMOVE_TOAST: "REMOVE_TOAST",
25 | } as const;
26 |
27 | let count = 0;
28 |
29 | function genId() {
30 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
31 | return count.toString();
32 | }
33 |
34 | type ActionType = typeof actionTypes;
35 |
36 | type Action =
37 | | {
38 | type: ActionType["ADD_TOAST"];
39 | toast: ToasterToast;
40 | }
41 | | {
42 | type: ActionType["UPDATE_TOAST"];
43 | toast: Partial;
44 | }
45 | | {
46 | type: ActionType["DISMISS_TOAST"];
47 | toastId?: ToasterToast["id"];
48 | }
49 | | {
50 | type: ActionType["REMOVE_TOAST"];
51 | toastId?: ToasterToast["id"];
52 | };
53 |
54 | interface State {
55 | toasts: ToasterToast[];
56 | }
57 |
58 | const toastTimeouts = new Map>();
59 |
60 | const addToRemoveQueue = (toastId: string) => {
61 | if (toastTimeouts.has(toastId)) {
62 | return;
63 | }
64 |
65 | const timeout = setTimeout(() => {
66 | toastTimeouts.delete(toastId);
67 | dispatch({
68 | type: actionTypes.REMOVE_TOAST, // Use actionTypes here
69 | toastId: toastId,
70 | });
71 | }, TOAST_REMOVE_DELAY);
72 |
73 | toastTimeouts.set(toastId, timeout);
74 | };
75 |
76 | export const reducer = (state: State, action: Action): State => {
77 | switch (action.type) {
78 | case actionTypes.ADD_TOAST: // Use actionTypes here
79 | return {
80 | ...state,
81 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
82 | };
83 |
84 | case actionTypes.UPDATE_TOAST: // Use actionTypes here
85 | return {
86 | ...state,
87 | toasts: state.toasts.map((t) =>
88 | t.id === action.toast.id ? { ...t, ...action.toast } : t
89 | ),
90 | };
91 |
92 | case actionTypes.DISMISS_TOAST: {
93 | const { toastId } = action;
94 |
95 | // ! Side effects ! - This could be extracted into a dismissToast() action,
96 | // but I'll keep it here for simplicity
97 | if (toastId) {
98 | addToRemoveQueue(toastId);
99 | } else {
100 | state.toasts.forEach((toast) => {
101 | addToRemoveQueue(toast.id);
102 | });
103 | }
104 |
105 | return {
106 | ...state,
107 | toasts: state.toasts.map((t) =>
108 | t.id === toastId || toastId === undefined
109 | ? {
110 | ...t,
111 | open: false,
112 | }
113 | : t
114 | ),
115 | };
116 | }
117 | case actionTypes.REMOVE_TOAST: // Use actionTypes here
118 | if (action.toastId === undefined) {
119 | return {
120 | ...state,
121 | toasts: [],
122 | };
123 | }
124 | return {
125 | ...state,
126 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
127 | };
128 | }
129 | };
130 |
131 | const listeners: Array<(state: State) => void> = [];
132 |
133 | let memoryState: State = { toasts: [] };
134 |
135 | function dispatch(action: Action) {
136 | memoryState = reducer(memoryState, action);
137 | listeners.forEach((listener) => {
138 | listener(memoryState);
139 | });
140 | }
141 |
142 | type Toast = Omit;
143 |
144 | function toast({ ...props }: Toast) {
145 | const id = genId();
146 |
147 | const update = (props: ToasterToast) =>
148 | dispatch({
149 | type: actionTypes.UPDATE_TOAST, // Use actionTypes here
150 | toast: { ...props, id },
151 | });
152 | const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id }); // Use actionTypes here
153 |
154 | dispatch({
155 | type: actionTypes.ADD_TOAST, // Use actionTypes here
156 | toast: {
157 | ...props,
158 | id,
159 | open: true,
160 | onOpenChange: (open) => {
161 | if (!open) dismiss();
162 | },
163 | },
164 | });
165 |
166 | return {
167 | id: id,
168 | dismiss,
169 | update,
170 | };
171 | }
172 |
173 | function useToast() {
174 | const [state, setState] = React.useState(memoryState);
175 |
176 | React.useEffect(() => {
177 | listeners.push(setState);
178 | return () => {
179 | const index = listeners.indexOf(setState);
180 | if (index > -1) {
181 | listeners.splice(index, 1);
182 | }
183 | };
184 | }, [state]);
185 |
186 | return {
187 | ...state,
188 | toast,
189 | dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }), // Use actionTypes here
190 | };
191 | }
192 |
193 | export { useToast, toast };
194 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | const CommandDialog = ({ children, ...props }: DialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/src/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/src/app/documents/[documentId]/ruler.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react';
2 | import { FaCaretDown } from 'react-icons/fa'
3 | import { useStorage, useMutation } from '@liveblocks/react';
4 |
5 | const markers = Array.from({ length: 83 }, (_, i) => i)
6 |
7 | export const Ruler = () => {
8 | const leftMargin = useStorage((root) => root.leftMargin) ?? 56
9 | const setLeftMargin = useMutation(({ storage }, position: number) => {
10 | storage.set("leftMargin", position)
11 | }, [])
12 |
13 | const rightMargin = useStorage((root) => root.rightMargin) ?? 56
14 | const setRightMargin = useMutation(({ storage }, position: number) => {
15 | storage.set("rightMargin", position)
16 | }, [])
17 |
18 | const [isDraggingLeft, setIsDraggingLeft] = useState(false)
19 | const [isDraggingRight, setIsDraggingRight] = useState(false)
20 | const rulerRef = useRef(null);
21 |
22 | const handleLeftMouseDown = () => {
23 | setIsDraggingLeft(true)
24 | }
25 |
26 | const handleRightMouseDown = () => {
27 | setIsDraggingRight(true)
28 | }
29 |
30 | const handleMouseMove = (e: React.MouseEvent) => {
31 | const PAGE_WIDTH = 816;
32 | const MINIMUM_SPACE = 100;
33 |
34 | if ((isDraggingLeft || isDraggingRight) && rulerRef.current) {
35 | const container = rulerRef.current?.querySelector("#ruler-container")
36 | if (container) {
37 | const containerRect = container.getBoundingClientRect()
38 | const relativeX = e.clientX - containerRect.left
39 | const rawPosition = Math.max(0, Math.min(PAGE_WIDTH, relativeX));
40 |
41 | if (isDraggingLeft) {
42 | const maxLeftPosition = PAGE_WIDTH - rightMargin - MINIMUM_SPACE;
43 | const newLeftPosition = Math.min(rawPosition, maxLeftPosition)
44 | setLeftMargin(newLeftPosition) //Todo: Make Collabarative
45 | } else if (isDraggingRight) {
46 | const maxRightPosition = PAGE_WIDTH - (leftMargin + MINIMUM_SPACE)
47 | const newRightPosition = Math.max(PAGE_WIDTH - rawPosition, 0);
48 | const constrainedRightPosition = Math.min(newRightPosition, maxRightPosition)
49 | setRightMargin(constrainedRightPosition)
50 | }
51 | }
52 | }
53 | }
54 |
55 | const handleMouseUp = () => {
56 | setIsDraggingLeft(false)
57 | setIsDraggingRight(false)
58 | }
59 |
60 | const handleLeftDoubleClick = () => {
61 | setLeftMargin(56)
62 | }
63 |
64 | const handleRightDoubleClick = () => {
65 | setRightMargin(56)
66 | }
67 |
68 | return (
69 |
76 |
80 |
87 |
94 |
95 |
96 | {markers.map((marker) => {
97 | const position = (marker * 816 / 82)
98 | return (
99 |
104 | {marker % 10 === 0 && (
105 | <>
106 |
107 |
108 | {marker / 10 + 1}
109 |
110 | >
111 | )}
112 | {marker % 5 === 0 && marker % 10 !== 0 && (
113 |
114 | )}
115 | {marker % 5 !== 0 && marker % 10 !== 0 && (
116 |
117 | )}
118 |
119 | )
120 | })}
121 |
122 |
123 |
124 |
125 | );
126 | }
127 |
128 | interface MarkerProps {
129 | position: number;
130 | isLeft: boolean;
131 | isDragging: boolean;
132 | onMouseDown: () => void;
133 | onDoubleClick: () => void;
134 | }
135 |
136 | const Marker = ({
137 | position,
138 | isLeft,
139 | isDragging,
140 | onMouseDown,
141 | onDoubleClick
142 | }: MarkerProps) => {
143 | return (
144 |
162 | )
163 | }
--------------------------------------------------------------------------------
/convex/_generated/server.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated utilities for implementing server-side Convex query and mutation functions.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import {
12 | ActionBuilder,
13 | HttpActionBuilder,
14 | MutationBuilder,
15 | QueryBuilder,
16 | GenericActionCtx,
17 | GenericMutationCtx,
18 | GenericQueryCtx,
19 | GenericDatabaseReader,
20 | GenericDatabaseWriter,
21 | } from "convex/server";
22 | import type { DataModel } from "./dataModel.js";
23 |
24 | /**
25 | * Define a query in this Convex app's public API.
26 | *
27 | * This function will be allowed to read your Convex database and will be accessible from the client.
28 | *
29 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
30 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
31 | */
32 | export declare const query: QueryBuilder;
33 |
34 | /**
35 | * Define a query that is only accessible from other Convex functions (but not from the client).
36 | *
37 | * This function will be allowed to read from your Convex database. It will not be accessible from the client.
38 | *
39 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
40 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
41 | */
42 | export declare const internalQuery: QueryBuilder;
43 |
44 | /**
45 | * Define a mutation in this Convex app's public API.
46 | *
47 | * This function will be allowed to modify your Convex database and will be accessible from the client.
48 | *
49 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
50 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
51 | */
52 | export declare const mutation: MutationBuilder;
53 |
54 | /**
55 | * Define a mutation that is only accessible from other Convex functions (but not from the client).
56 | *
57 | * This function will be allowed to modify your Convex database. It will not be accessible from the client.
58 | *
59 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
60 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
61 | */
62 | export declare const internalMutation: MutationBuilder;
63 |
64 | /**
65 | * Define an action in this Convex app's public API.
66 | *
67 | * An action is a function which can execute any JavaScript code, including non-deterministic
68 | * code and code with side-effects, like calling third-party services.
69 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
70 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
71 | *
72 | * @param func - The action. It receives an {@link ActionCtx} as its first argument.
73 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
74 | */
75 | export declare const action: ActionBuilder;
76 |
77 | /**
78 | * Define an action that is only accessible from other Convex functions (but not from the client).
79 | *
80 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
81 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
82 | */
83 | export declare const internalAction: ActionBuilder;
84 |
85 | /**
86 | * Define an HTTP action.
87 | *
88 | * This function will be used to respond to HTTP requests received by a Convex
89 | * deployment if the requests matches the path and method where this action
90 | * is routed. Be sure to route your action in `convex/http.js`.
91 | *
92 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
93 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
94 | */
95 | export declare const httpAction: HttpActionBuilder;
96 |
97 | /**
98 | * A set of services for use within Convex query functions.
99 | *
100 | * The query context is passed as the first argument to any Convex query
101 | * function run on the server.
102 | *
103 | * This differs from the {@link MutationCtx} because all of the services are
104 | * read-only.
105 | */
106 | export type QueryCtx = GenericQueryCtx;
107 |
108 | /**
109 | * A set of services for use within Convex mutation functions.
110 | *
111 | * The mutation context is passed as the first argument to any Convex mutation
112 | * function run on the server.
113 | */
114 | export type MutationCtx = GenericMutationCtx;
115 |
116 | /**
117 | * A set of services for use within Convex action functions.
118 | *
119 | * The action context is passed as the first argument to any Convex action
120 | * function run on the server.
121 | */
122 | export type ActionCtx = GenericActionCtx;
123 |
124 | /**
125 | * An interface to read from the database within Convex query functions.
126 | *
127 | * The two entry points are {@link DatabaseReader.get}, which fetches a single
128 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts
129 | * building a query.
130 | */
131 | export type DatabaseReader = GenericDatabaseReader;
132 |
133 | /**
134 | * An interface to read from and write to the database within Convex mutation
135 | * functions.
136 | *
137 | * Convex guarantees that all writes within a single mutation are
138 | * executed atomically, so you never have to worry about partial writes leaving
139 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
140 | * for the guarantees Convex provides your functions.
141 | */
142 | export type DatabaseWriter = GenericDatabaseWriter;
143 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 | {children}
132 |
133 | ))
134 | SelectItem.displayName = SelectPrimitive.Item.displayName
135 |
136 | const SelectSeparator = React.forwardRef<
137 | React.ElementRef,
138 | React.ComponentPropsWithoutRef
139 | >(({ className, ...props }, ref) => (
140 |
145 | ))
146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
147 |
148 | export {
149 | Select,
150 | SelectGroup,
151 | SelectValue,
152 | SelectTrigger,
153 | SelectContent,
154 | SelectLabel,
155 | SelectItem,
156 | SelectSeparator,
157 | SelectScrollUpButton,
158 | SelectScrollDownButton,
159 | }
160 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | body::-webkit-scrollbar {
10 | display: none;
11 | }
12 |
13 | @layer base {
14 | :root {
15 | --background: 0 0% 100%;
16 | --foreground: 0 0% 3.9%;
17 | --card: 0 0% 100%;
18 | --card-foreground: 0 0% 3.9%;
19 | --popover: 0 0% 100%;
20 | --popover-foreground: 0 0% 3.9%;
21 | --primary: 0 0% 9%;
22 | --primary-foreground: 0 0% 98%;
23 | --secondary: 0 0% 96.1%;
24 | --secondary-foreground: 0 0% 9%;
25 | --muted: 0 0% 96.1%;
26 | --muted-foreground: 0 0% 45.1%;
27 | --accent: 0 0% 96.1%;
28 | --accent-foreground: 0 0% 9%;
29 | --destructive: 0 84.2% 60.2%;
30 | --destructive-foreground: 0 0% 98%;
31 | --border: 0 0% 89.8%;
32 | --input: 0 0% 89.8%;
33 | --ring: 0 0% 3.9%;
34 | --chart-1: 12 76% 61%;
35 | --chart-2: 173 58% 39%;
36 | --chart-3: 197 37% 24%;
37 | --chart-4: 43 74% 66%;
38 | --chart-5: 27 87% 67%;
39 | --radius: 0.5rem;
40 | --sidebar-background: 0 0% 98%;
41 | --sidebar-foreground: 240 5.3% 26.1%;
42 | --sidebar-primary: 240 5.9% 10%;
43 | --sidebar-primary-foreground: 0 0% 98%;
44 | --sidebar-accent: 240 4.8% 95.9%;
45 | --sidebar-accent-foreground: 240 5.9% 10%;
46 | --sidebar-border: 220 13% 91%;
47 | --sidebar-ring: 217.2 91.2% 59.8%;
48 | }
49 |
50 | .dark {
51 | --background: 0 0% 3.9%;
52 | --foreground: 0 0% 98%;
53 | --card: 0 0% 3.9%;
54 | --card-foreground: 0 0% 98%;
55 | --popover: 0 0% 3.9%;
56 | --popover-foreground: 0 0% 98%;
57 | --primary: 0 0% 98%;
58 | --primary-foreground: 0 0% 9%;
59 | --secondary: 0 0% 14.9%;
60 | --secondary-foreground: 0 0% 98%;
61 | --muted: 0 0% 14.9%;
62 | --muted-foreground: 0 0% 63.9%;
63 | --accent: 0 0% 14.9%;
64 | --accent-foreground: 0 0% 98%;
65 | --destructive: 0 62.8% 30.6%;
66 | --destructive-foreground: 0 0% 98%;
67 | --border: 0 0% 14.9%;
68 | --input: 0 0% 14.9%;
69 | --ring: 0 0% 83.1%;
70 | --chart-1: 220 70% 50%;
71 | --chart-2: 160 60% 45%;
72 | --chart-3: 30 80% 55%;
73 | --chart-4: 280 65% 60%;
74 | --chart-5: 340 75% 55%;
75 | --sidebar-background: 240 5.9% 10%;
76 | --sidebar-foreground: 240 4.8% 95.9%;
77 | --sidebar-primary: 224.3 76.3% 48%;
78 | --sidebar-primary-foreground: 0 0% 100%;
79 | --sidebar-accent: 240 3.7% 15.9%;
80 | --sidebar-accent-foreground: 240 4.8% 95.9%;
81 | --sidebar-border: 240 3.7% 15.9%;
82 | --sidebar-ring: 217.2 91.2% 59.8%;
83 | }
84 | }
85 |
86 | @layer base {
87 | * {
88 | @apply border-border;
89 | }
90 |
91 | body {
92 | @apply bg-background text-foreground;
93 | ;
94 | }
95 | }
96 |
97 | .tiptap {
98 |
99 | /* Link styles */
100 | a {
101 | @apply text-blue-600;
102 | cursor: pointer;
103 |
104 | &:hover {
105 | @apply underline;
106 | }
107 | }
108 |
109 | /* Image */
110 | img {
111 | display: block;
112 | height: auto;
113 | margin: 1.5rem 0;
114 | max-width: 100%;
115 |
116 | &.ProseMirror-selectednode {
117 | outline: 3px solid var(--primary);
118 | }
119 | }
120 |
121 | /* Table-specific styling */
122 | table {
123 | border-collapse: collapse;
124 | margin: 0;
125 | overflow: hidden;
126 | table-layout: fixed;
127 | width: 100%;
128 |
129 | td,
130 | th {
131 | border: 1px solid black;
132 | box-sizing: border-box;
133 | min-width: 1em;
134 | padding: 6px 8px;
135 | position: relative;
136 | vertical-align: top;
137 |
138 | >* {
139 | margin-bottom: 0;
140 | }
141 | }
142 |
143 | th {
144 | background-color: #c7c7c7;
145 | font-weight: bold;
146 | text-align: left;
147 | }
148 |
149 | .selectedCell:after {
150 | background: #959596;
151 | content: "";
152 | left: 0;
153 | right: 0;
154 | top: 0;
155 | bottom: 0;
156 | pointer-events: none;
157 | position: absolute;
158 | z-index: 2;
159 | }
160 |
161 | .column-resize-handle {
162 | background-color: var(--primary);
163 | bottom: -2px;
164 | pointer-events: none;
165 | position: absolute;
166 | right: -2px;
167 | top: 0;
168 | width: 4px;
169 | }
170 | }
171 |
172 | .tableWrapper {
173 | margin: 1.5rem 0;
174 | overflow-x: auto;
175 | }
176 |
177 | &.resize-cursor {
178 | cursor: ew-resize;
179 | cursor: col-resize;
180 | }
181 |
182 |
183 | /* Heading styles */
184 | h1,
185 | h2,
186 | h3,
187 | h4,
188 | h5,
189 | h6 {
190 | line-height: 1.1;
191 | margin-top: 2.5rem;
192 | text-wrap: pretty;
193 | }
194 |
195 | h1,
196 | h2 {
197 | margin-top: 3.5rem;
198 | margin-bottom: 1.5rem;
199 | }
200 |
201 | h1 {
202 | font-size: 35px;
203 | }
204 |
205 | h2 {
206 | font-size: 30px;
207 | }
208 |
209 | h3 {
210 | font-size: 24px;
211 | }
212 |
213 | h4 {
214 | font-size: 18.5px;
215 | }
216 |
217 | h5 {
218 | font-size: 17px;
219 | }
220 |
221 | h6 {
222 | font-size: 16px;
223 | }
224 |
225 |
226 | /* List styles */
227 | ul,
228 | ol {
229 | padding: 0 1rem;
230 | margin: 1.25rem 1rem 1.25rem 0.4rem;
231 | }
232 |
233 | ul li {
234 | list-style-type: disc;
235 |
236 | p {
237 | margin-top: 0.25em;
238 | margin-bottom: 0.25em;
239 | }
240 | }
241 |
242 | ol li {
243 | list-style-type: decimal;
244 |
245 | p {
246 | margin-top: 0.25em;
247 | margin-bottom: 0.25em;
248 | }
249 | }
250 |
251 | /* Task list specific styles */
252 | ul[data-type="taskList"] {
253 | list-style: none;
254 | margin-left: 0;
255 | padding: 0;
256 |
257 | li {
258 | align-items: flex-start;
259 | display: flex;
260 |
261 | >label {
262 | flex: 0 0 auto;
263 | margin-right: 0.5rem;
264 | user-select: none;
265 | }
266 |
267 | >div {
268 | flex: 1 1 auto;
269 | }
270 | }
271 |
272 | input[type="checkbox"] {
273 | cursor: pointer;
274 | }
275 |
276 | ul[data-type="taskList"] {
277 | margin: 0;
278 | }
279 | }
280 | }
281 |
282 | /* For mobile */
283 | .floating-threads {
284 | display: none;
285 | }
286 |
287 | /* For desktop */
288 | .anchored-threads {
289 | display: block;
290 | max-width: 300px;
291 | width: 100%;
292 | position: absolute;
293 | right: 12px;
294 | }
295 |
296 | @media (max-width: 640px) {
297 | .floating-threads {
298 | display: block;
299 | }
300 |
301 | .anchored-threads {
302 | display: none;
303 | }
304 | }
305 |
306 | div[data-radix-popper-content-wrapper]{
307 | z-index: 50 !important
308 | }
--------------------------------------------------------------------------------
|