diff --git a/apps/web/components/Objects/Activities/DynamicCanva/CustomHeadingExtenstion.tsx b/apps/web/components/Objects/Activities/DynamicCanva/CustomHeadingExtenstion.tsx
new file mode 100644
index 00000000..2e655434
--- /dev/null
+++ b/apps/web/components/Objects/Activities/DynamicCanva/CustomHeadingExtenstion.tsx
@@ -0,0 +1,29 @@
+import Heading from '@tiptap/extension-heading'
+
+// Custom Heading extension that adds IDs
+export const CustomHeading = Heading.extend({
+ renderHTML({ node, HTMLAttributes }: { node: any; HTMLAttributes: any }) {
+ const hasLevel = this.options.levels.includes(node.attrs.level)
+ const level = hasLevel ? node.attrs.level : this.options.levels[0]
+
+ // Generate ID from heading text
+ const headingText = node.textContent || ''
+ const slug = headingText
+ .toLowerCase()
+ .trim()
+ .replace(/[^\w\s-]/g, '') // Remove special characters
+ .replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphens
+ .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
+
+ const id = slug ? `heading-${slug}` : `heading-${Math.random().toString(36).substr(2, 9)}`
+
+ return [
+ `h${level}`,
+ {
+ ...HTMLAttributes,
+ id,
+ },
+ 0,
+ ]
+ },
+})
\ No newline at end of file
diff --git a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx
index 1c07b5eb..f2f84439 100644
--- a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx
+++ b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx
@@ -33,12 +33,16 @@ import TableRow from '@tiptap/extension-table-row'
import TableCell from '@tiptap/extension-table-cell'
import UserBlock from '@components/Objects/Editor/Extensions/Users/UserBlock'
import { getLinkExtension } from '@components/Objects/Editor/EditorConf'
+import TableOfContents from './TableOfContents'
+import { CustomHeading } from './CustomHeadingExtenstion'
interface Editor {
content: string
activity: any
}
+
+
function Canva(props: Editor) {
/**
* Important Note : This is a workaround to enable user interaction features to be implemented easily, like text selection, AI features and other planned features, this is set to true but otherwise it should be set to false.
@@ -59,6 +63,7 @@ function Canva(props: Editor) {
editable: isEditable,
extensions: [
StarterKit.configure({
+ heading: false,
bulletList: {
HTMLAttributes: {
class: 'bullet-list',
@@ -70,6 +75,7 @@ function Canva(props: Editor) {
},
},
}),
+ CustomHeading,
NoTextInput,
// Custom Extensions
InfoCallout.configure({
@@ -137,7 +143,10 @@ function Canva(props: Editor) {
-
+
+
+
+
)
@@ -146,33 +155,17 @@ function Canva(props: Editor) {
const CanvaWrapper = styled.div`
width: 100%;
margin: 0 auto;
+`
- .bubble-menu {
- display: flex;
- background-color: #0d0d0d;
- padding: 0.2rem;
- border-radius: 0.5rem;
-
- button {
- border: none;
- background: none;
- color: #fff;
- font-size: 0.85rem;
- font-weight: 500;
- padding: 0 0.2rem;
- opacity: 0.6;
-
- &:hover,
- &.is-active {
- opacity: 1;
- }
- }
- }
-
- // disable chrome outline
+const ContentWrapper = styled.div`
+ display: flex;
+ width: 100%;
+ height: 100%;
.ProseMirror {
- // Workaround to disable editor from being edited by the user.
+ flex: 1;
+ padding: 1rem;
+ // disable chrome outline
caret-color: transparent;
h1 {
diff --git a/apps/web/components/Objects/Activities/DynamicCanva/TableOfContents.tsx b/apps/web/components/Objects/Activities/DynamicCanva/TableOfContents.tsx
new file mode 100644
index 00000000..72243266
--- /dev/null
+++ b/apps/web/components/Objects/Activities/DynamicCanva/TableOfContents.tsx
@@ -0,0 +1,133 @@
+import { useEffect, useState } from 'react'
+import styled from 'styled-components'
+import { Editor } from '@tiptap/react'
+import { Check } from 'lucide-react'
+
+interface TableOfContentsProps {
+ editor: Editor | null
+}
+
+interface HeadingItem {
+ level: number
+ text: string
+ id: string
+}
+
+
+const TableOfContents = ({ editor }: TableOfContentsProps) => {
+ const [headings, setHeadings] = useState([])
+ const [open, setOpen] = useState(true)
+
+ useEffect(() => {
+ if (!editor) return
+
+ const updateHeadings = () => {
+ const items: HeadingItem[] = []
+ editor.state.doc.descendants((node) => {
+ if (node.type.name.startsWith('heading')) {
+ const level = node.attrs.level || 1
+ const headingText = node.textContent || ''
+
+ // Create slug from heading text (same logic as CustomHeading in DynamicCanva)
+ const slug = headingText
+ .toLowerCase()
+ .trim()
+ .replace(/[^\w\s-]/g, '') // Remove special characters
+ .replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphens
+ .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
+
+ const id = slug ? `heading-${slug}` : `heading-${Math.random().toString(36).substr(2, 9)}`
+
+ items.push({
+ level,
+ text: node.textContent,
+ id,
+ })
+ }
+ })
+ setHeadings(items)
+ }
+
+ editor.on('update', updateHeadings)
+ updateHeadings()
+
+ return () => {
+ editor.off('update', updateHeadings)
+ }
+ }, [editor])
+
+ if (headings.length === 0) return null
+
+ return (
+
+
+ {headings.map((heading, index) => (
+
+
+ {heading.text}
+
+ ))}
+
+
+ )
+}
+
+const TOCCard = styled.div`
+ width: 20%;
+ background: none;
+ border: none;
+ box-shadow: none;
+ padding: 0;
+ margin: 0;
+ font-family: inherit;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ height: fit-content;
+`
+
+const TOCList = styled.ul`
+ list-style: none !important;
+ padding: 0 !important;
+ margin: 0;
+`
+
+const TOCItem = styled.li<{ level: number }>`
+ margin: 0.5rem 0;
+ padding-left: ${({ level }) => `${(level - 1) * 1.2}rem`};
+ list-style: none !important;
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+
+ .toc-check {
+ display: flex;
+ align-items: center;
+ color: #23272f;
+ flex-shrink: 0;
+ margin-top: 0.1rem;
+ }
+
+ .toc-link {
+ color: #23272f;
+ text-decoration: none;
+ display: block;
+ font-size: ${({ level }) => (level === 1 ? '1rem' : level === 2 ? '0.97rem' : '0.95rem')};
+ font-weight: ${({ level }) => (level === 1 ? 500 : 400)};
+ line-height: 1.4;
+ padding: 0;
+ background: none;
+ border-radius: 0;
+ transition: none;
+ flex: 1;
+ min-width: 0;
+ word-break: break-word;
+ hyphens: auto;
+
+ &:hover {
+ color: #007acc;
+ }
+ }
+`
+
+export default TableOfContents
\ No newline at end of file
diff --git a/apps/web/package.json b/apps/web/package.json
index 5fd4bd82..9ea07c5e 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -35,6 +35,7 @@
"@tiptap/core": "^2.11.7",
"@tiptap/extension-bullet-list": "^2.11.7",
"@tiptap/extension-code-block-lowlight": "^2.11.7",
+ "@tiptap/extension-heading": "^2.12.0",
"@tiptap/extension-link": "^2.11.7",
"@tiptap/extension-list-item": "^2.11.7",
"@tiptap/extension-ordered-list": "^2.11.7",
diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml
index b7290c38..cda60768 100644
--- a/apps/web/pnpm-lock.yaml
+++ b/apps/web/pnpm-lock.yaml
@@ -84,6 +84,9 @@ importers:
'@tiptap/extension-code-block-lowlight':
specifier: ^2.11.7
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/extension-code-block@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(highlight.js@11.11.1)(lowlight@3.3.0)
+ '@tiptap/extension-heading':
+ specifier: ^2.12.0
+ version: 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
'@tiptap/extension-link':
specifier: ^2.11.7
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
@@ -1590,8 +1593,8 @@ packages:
peerDependencies:
'@tiptap/core': ^2.7.0
- '@tiptap/extension-heading@2.11.7':
- resolution: {integrity: sha512-8kWh7y4Rd2fwxfWOhFFWncHdkDkMC1Z60yzIZWjIu72+6yQxvo8w3yeb7LI7jER4kffbMmadgcfhCHC/fkObBA==}
+ '@tiptap/extension-heading@2.12.0':
+ resolution: {integrity: sha512-9DfES4Wd5TX1foI70N9sAL+35NN1UHrtzDYN2+dTHupnmKir9RaMXyZcbkUb4aDVzYrGxIqxJzHBVkquKIlTrw==}
peerDependencies:
'@tiptap/core': ^2.7.0
@@ -3441,8 +3444,8 @@ packages:
tailwind-merge@2.6.0:
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
- tailwind-merge@3.2.0:
- resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==}
+ tailwind-merge@3.3.0:
+ resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==}
tailwindcss-animate@1.0.7:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
@@ -4860,7 +4863,7 @@ snapshots:
dependencies:
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
- '@tiptap/extension-heading@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
+ '@tiptap/extension-heading@2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
dependencies:
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
@@ -4980,7 +4983,7 @@ snapshots:
'@tiptap/extension-dropcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@tiptap/extension-gapcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@tiptap/extension-hard-break': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
- '@tiptap/extension-heading': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
+ '@tiptap/extension-heading': 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
'@tiptap/extension-history': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@tiptap/extension-horizontal-rule': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
'@tiptap/extension-italic': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
@@ -5499,7 +5502,7 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-easy-sort: 1.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
- tailwind-merge: 3.2.0
+ tailwind-merge: 3.3.0
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
@@ -7021,7 +7024,7 @@ snapshots:
tailwind-merge@2.6.0: {}
- tailwind-merge@3.2.0: {}
+ tailwind-merge@3.3.0: {}
tailwindcss-animate@1.0.7(tailwindcss@4.1.3):
dependencies: