Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 325 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,25 +47,30 @@
"cmdk": "^1.1.1",
"framer-motion": "^12.16.0",
"geist": "^1.3.0",
"lodash": "^4.17.21",
"lodash.throttle": "^4.1.1",
"lucide-react": "^0.460.0",
"next": "^15.0.1",
"next": "15.0.7",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"server-only": "^0.0.1",
"shiki": "^3.9.2",
"shiki": "^3.20.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.27",
"zod": "^3.23.8",
"zustand": "^5.0.4"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/lodash": "^4.17.21",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
5 changes: 3 additions & 2 deletions src/app/_components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import { type Document } from "~/server/api/routers/document"
import { useToast } from "../../hooks/use-toast"
import { NavUser } from "./nav-user"
import { useCommandMenuStore } from "~/hooks/use-command-menu"
import { useUserProfile } from "~/hooks/use-user-profile"

interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
initialDocuments: { id: string; name: string }[]
Expand DownExpand Up@@ -63,8 +64,8 @@ export function AppSidebar({ initialDocuments, ...props }: AppSidebarProps) {
setShowBottomShadow(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
}, [documents]);

// Fetch user profile and email
const { data: userProfile, isLoading: userLoading } = api.user.getCurrentUserProfile.useQuery();
// Fetch user profile and email (shared query, automatically deduplicated by React Query)
const { data: userProfile, isLoading: userLoading } = useUserProfile();
const { data: userEmail } = api.user.getCurrentUser.useQuery();

const createDocument = api.document.createDocument.useMutation({
Expand Down
55 changes: 55 additions & 0 deletions src/app/_components/editor/codeBlockSyntaxHighlighter.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { createHighlighter } from "shiki";

// Code block language configuration
// Add or remove languages here - changes will be applied to both BlockNote and Shiki
const codeBlockLanguages = {
javascript: { name: "JavaScript", aliases: ["js"] },
typescript: { name: "TypeScript", aliases: ["ts"] },
python: { name: "Python", aliases: ["py"] },
java: { name: "Java" },
c: { name: "C" },
cpp: { name: "C++" },
go: { name: "Go" },
ruby: { name: "Ruby" },
php: { name: "PHP" },
rust: { name: "Rust" },
kotlin: { name: "Kotlin" },
swift: { name: "Swift" },
csharp: { name: "C#", aliases: ["cs"] },
scala: { name: "Scala" },
html: { name: "HTML" },
css: { name: "CSS" },
json: { name: "JSON" },
yaml: { name: "YAML", aliases: ["yml"] },
markdown: { name: "Markdown", aliases: ["md"] },
sql: { name: "SQL" },
bash: { name: "Bash", aliases: ["sh"] },
powershell: { name: "PowerShell", aliases: ["ps"] },
dart: { name: "Dart" },
"objective-c": { name: "Objective-C", aliases: ["objc"] },
} as const;

// Derive the supported languages object for BlockNote
export const supportedLanguages = Object.fromEntries(
Object.entries(codeBlockLanguages).map(([key, value]) => {
const langConfig =
"aliases" in value && value.aliases
? { name: value.name, aliases: [...value.aliases] } // Convert readonly array to mutable
: { name: value.name };
return [key, langConfig];
}),
) as Record<string, { name: string; aliases?: string[] }>;

// Extract language IDs for Shiki highlighter
const shikiLanguageIds = Object.keys(codeBlockLanguages);

// Create the highlighter factory function for BlockNote
// Type mismatch between shiki and @blocknote/core shiki types - both use different versions of @shikijs/types
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
export const createCodeBlockHighlighter = () =>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
}) as any;
Comment on lines +49 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Find the codeBlockSyntaxHighlighter.ts file
find . -name "codeBlockSyntaxHighlighter.ts" -type f 2>/dev/null | head -5

Repository: Matia-R/Chptr

Length of output: 115


🏁 Script executed:

cat -n ./src/app/_components/editor/codeBlockSyntaxHighlighter.ts

Repository: Matia-R/Chptr

Length of output: 2544


🏁 Script executed:

find . -name "editor.tsx" -type f 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -100 {}'

Repository: Matia-R/Chptr

Length of output: 2647


🏁 Script executed:

rg "currentTheme|theme.*light|theme.*dark" --type ts --type tsx -C 2 | head -80

Repository: Matia-R/Chptr

Length of output: 84


🏁 Script executed:

rg "codeToHtml|highlighter.*theme|theme.*highlighter" ./src --type ts --type tsx -A 2 -B 2 | head -60

Repository: Matia-R/Chptr

Length of output: 84


Load both light and dark themes in the code highlighter.

Currently only "github-dark" is loaded, causing users in light mode to see dark syntax highlighting in code blocks. Shiki supports loading multiple themes which are switched using CSS variables.

The editor already passes the current theme to BlockNoteView (editor.tsx line 87: theme={currentTheme as "light" | "dark"}), and BlockNote automatically selects the appropriate theme based on the user's system preference. Shiki handles the light/dark switching through CSS variables automatically—no additional theme-selection code is needed.

Apply this diff:

 export const createCodeBlockHighlighter = () =>
createHighlighter({
- themes: ["github-dark"],+ themes: ["github-light", "github-dark"],
langs: shikiLanguageIds,
}) as any;
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
})asany;
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-light","github-dark"],
langs: shikiLanguageIds,
})asany;
🤖 Prompt for AI Agents
In src/app/_components/editor/codeBlockSyntaxHighlighter.ts around lines 49 to
53, the highlighter currently loads only the "github-dark" theme causing
light-mode users to see dark highlighting; update the createHighlighter call to
include both the light and dark themes (e.g., "github-light" and "github-dark")
in the themes array so Shiki can switch via CSS variables automatically; keep
the return type cast as any and do not add runtime theme-selection logic.

/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */

38 changes: 38 additions & 0 deletions src/app/_components/editor/cursor-renderer.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
type CollaborationUser = {
name: string;
color: string;
[key: string]: string;
};

// Custom cursor renderer for Google Docs-like appearance
export const renderCursor = (user: CollaborationUser): HTMLElement => {
const cursorElement = document.createElement("span");
cursorElement.classList.add("bn-collaboration-cursor__base");

// Create the caret (vertical line)
const caretElement = document.createElement("span");
caretElement.setAttribute("contenteditable", "false");
caretElement.classList.add("bn-collaboration-cursor__caret");
caretElement.setAttribute(
"style",
`border-left: 2.5px solid ${user.color}; height: 1.25em; position: relative; display: inline-block; vertical-align: text-bottom;`
);

// Create the label
const labelElement = document.createElement("span");
labelElement.classList.add("bn-collaboration-cursor__label");
labelElement.setAttribute(
"style",
`background-color: ${user.color}; color: #ffffff; padding: 3px 7px; font-size: 11px; font-weight: 600; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 1px rgba(0, 0, 0, 0.15); position: absolute; left: -2.5px; top: -1.85em; white-space: nowrap; z-index: 10; pointer-events: none; user-select: none;`
);
labelElement.textContent = user.name;

// Assemble the cursor
caretElement.appendChild(labelElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space
cursorElement.appendChild(caretElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space

return cursorElement;
};

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Realtime doc sync yjs by Matia-R · Pull Request #20 · Matia-R/Chptr · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 325 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,25 +47,30 @@
"cmdk": "^1.1.1",
"framer-motion": "^12.16.0",
"geist": "^1.3.0",
"lodash": "^4.17.21",
"lodash.throttle": "^4.1.1",
"lucide-react": "^0.460.0",
"next": "^15.0.1",
"next": "15.0.7",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"server-only": "^0.0.1",
"shiki": "^3.9.2",
"shiki": "^3.20.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.27",
"zod": "^3.23.8",
"zustand": "^5.0.4"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/lodash": "^4.17.21",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
5 changes: 3 additions & 2 deletions src/app/_components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import { type Document } from "~/server/api/routers/document"
import { useToast } from "../../hooks/use-toast"
import { NavUser } from "./nav-user"
import { useCommandMenuStore } from "~/hooks/use-command-menu"
import { useUserProfile } from "~/hooks/use-user-profile"

interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
initialDocuments: { id: string; name: string }[]
Expand DownExpand Up@@ -63,8 +64,8 @@ export function AppSidebar({ initialDocuments, ...props }: AppSidebarProps) {
setShowBottomShadow(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
}, [documents]);

// Fetch user profile and email
const { data: userProfile, isLoading: userLoading } = api.user.getCurrentUserProfile.useQuery();
// Fetch user profile and email (shared query, automatically deduplicated by React Query)
const { data: userProfile, isLoading: userLoading } = useUserProfile();
const { data: userEmail } = api.user.getCurrentUser.useQuery();

const createDocument = api.document.createDocument.useMutation({
Expand Down
55 changes: 55 additions & 0 deletions src/app/_components/editor/codeBlockSyntaxHighlighter.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { createHighlighter } from "shiki";

// Code block language configuration
// Add or remove languages here - changes will be applied to both BlockNote and Shiki
const codeBlockLanguages = {
javascript: { name: "JavaScript", aliases: ["js"] },
typescript: { name: "TypeScript", aliases: ["ts"] },
python: { name: "Python", aliases: ["py"] },
java: { name: "Java" },
c: { name: "C" },
cpp: { name: "C++" },
go: { name: "Go" },
ruby: { name: "Ruby" },
php: { name: "PHP" },
rust: { name: "Rust" },
kotlin: { name: "Kotlin" },
swift: { name: "Swift" },
csharp: { name: "C#", aliases: ["cs"] },
scala: { name: "Scala" },
html: { name: "HTML" },
css: { name: "CSS" },
json: { name: "JSON" },
yaml: { name: "YAML", aliases: ["yml"] },
markdown: { name: "Markdown", aliases: ["md"] },
sql: { name: "SQL" },
bash: { name: "Bash", aliases: ["sh"] },
powershell: { name: "PowerShell", aliases: ["ps"] },
dart: { name: "Dart" },
"objective-c": { name: "Objective-C", aliases: ["objc"] },
} as const;

// Derive the supported languages object for BlockNote
export const supportedLanguages = Object.fromEntries(
Object.entries(codeBlockLanguages).map(([key, value]) => {
const langConfig =
"aliases" in value && value.aliases
? { name: value.name, aliases: [...value.aliases] } // Convert readonly array to mutable
: { name: value.name };
return [key, langConfig];
}),
) as Record<string, { name: string; aliases?: string[] }>;

// Extract language IDs for Shiki highlighter
const shikiLanguageIds = Object.keys(codeBlockLanguages);

// Create the highlighter factory function for BlockNote
// Type mismatch between shiki and @blocknote/core shiki types - both use different versions of @shikijs/types
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
export const createCodeBlockHighlighter = () =>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
}) as any;
Comment on lines +49 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Find the codeBlockSyntaxHighlighter.ts file
find . -name "codeBlockSyntaxHighlighter.ts" -type f 2>/dev/null | head -5

Repository: Matia-R/Chptr

Length of output: 115


🏁 Script executed:

cat -n ./src/app/_components/editor/codeBlockSyntaxHighlighter.ts

Repository: Matia-R/Chptr

Length of output: 2544


🏁 Script executed:

find . -name "editor.tsx" -type f 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -100 {}'

Repository: Matia-R/Chptr

Length of output: 2647


🏁 Script executed:

rg "currentTheme|theme.*light|theme.*dark" --type ts --type tsx -C 2 | head -80

Repository: Matia-R/Chptr

Length of output: 84


🏁 Script executed:

rg "codeToHtml|highlighter.*theme|theme.*highlighter" ./src --type ts --type tsx -A 2 -B 2 | head -60

Repository: Matia-R/Chptr

Length of output: 84


Load both light and dark themes in the code highlighter.

Currently only "github-dark" is loaded, causing users in light mode to see dark syntax highlighting in code blocks. Shiki supports loading multiple themes which are switched using CSS variables.

The editor already passes the current theme to BlockNoteView (editor.tsx line 87: theme={currentTheme as "light" | "dark"}), and BlockNote automatically selects the appropriate theme based on the user's system preference. Shiki handles the light/dark switching through CSS variables automatically—no additional theme-selection code is needed.

Apply this diff:

 export const createCodeBlockHighlighter = () =>
createHighlighter({
- themes: ["github-dark"],+ themes: ["github-light", "github-dark"],
langs: shikiLanguageIds,
}) as any;
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
})asany;
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-light","github-dark"],
langs: shikiLanguageIds,
})asany;
🤖 Prompt for AI Agents
In src/app/_components/editor/codeBlockSyntaxHighlighter.ts around lines 49 to
53, the highlighter currently loads only the "github-dark" theme causing
light-mode users to see dark highlighting; update the createHighlighter call to
include both the light and dark themes (e.g., "github-light" and "github-dark")
in the themes array so Shiki can switch via CSS variables automatically; keep
the return type cast as any and do not add runtime theme-selection logic.

/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */

38 changes: 38 additions & 0 deletions src/app/_components/editor/cursor-renderer.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
type CollaborationUser = {
name: string;
color: string;
[key: string]: string;
};

// Custom cursor renderer for Google Docs-like appearance
export const renderCursor = (user: CollaborationUser): HTMLElement => {
const cursorElement = document.createElement("span");
cursorElement.classList.add("bn-collaboration-cursor__base");

// Create the caret (vertical line)
const caretElement = document.createElement("span");
caretElement.setAttribute("contenteditable", "false");
caretElement.classList.add("bn-collaboration-cursor__caret");
caretElement.setAttribute(
"style",
`border-left: 2.5px solid ${user.color}; height: 1.25em; position: relative; display: inline-block; vertical-align: text-bottom;`
);

// Create the label
const labelElement = document.createElement("span");
labelElement.classList.add("bn-collaboration-cursor__label");
labelElement.setAttribute(
"style",
`background-color: ${user.color}; color: #ffffff; padding: 3px 7px; font-size: 11px; font-weight: 600; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 1px rgba(0, 0, 0, 0.15); position: absolute; left: -2.5px; top: -1.85em; white-space: nowrap; z-index: 10; pointer-events: none; user-select: none;`
);
labelElement.textContent = user.name;

// Assemble the cursor
caretElement.appendChild(labelElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space
cursorElement.appendChild(caretElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space

return cursorElement;
};

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Realtime doc sync yjs by Matia-R · Pull Request #20 · Matia-R/Chptr · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 325 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,25 +47,30 @@
"cmdk": "^1.1.1",
"framer-motion": "^12.16.0",
"geist": "^1.3.0",
"lodash": "^4.17.21",
"lodash.throttle": "^4.1.1",
"lucide-react": "^0.460.0",
"next": "^15.0.1",
"next": "15.0.7",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"server-only": "^0.0.1",
"shiki": "^3.9.2",
"shiki": "^3.20.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.27",
"zod": "^3.23.8",
"zustand": "^5.0.4"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/lodash": "^4.17.21",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
5 changes: 3 additions & 2 deletions src/app/_components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import { type Document } from "~/server/api/routers/document"
import { useToast } from "../../hooks/use-toast"
import { NavUser } from "./nav-user"
import { useCommandMenuStore } from "~/hooks/use-command-menu"
import { useUserProfile } from "~/hooks/use-user-profile"

interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
initialDocuments: { id: string; name: string }[]
Expand DownExpand Up@@ -63,8 +64,8 @@ export function AppSidebar({ initialDocuments, ...props }: AppSidebarProps) {
setShowBottomShadow(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
}, [documents]);

// Fetch user profile and email
const { data: userProfile, isLoading: userLoading } = api.user.getCurrentUserProfile.useQuery();
// Fetch user profile and email (shared query, automatically deduplicated by React Query)
const { data: userProfile, isLoading: userLoading } = useUserProfile();
const { data: userEmail } = api.user.getCurrentUser.useQuery();

const createDocument = api.document.createDocument.useMutation({
Expand Down
55 changes: 55 additions & 0 deletions src/app/_components/editor/codeBlockSyntaxHighlighter.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { createHighlighter } from "shiki";

// Code block language configuration
// Add or remove languages here - changes will be applied to both BlockNote and Shiki
const codeBlockLanguages = {
javascript: { name: "JavaScript", aliases: ["js"] },
typescript: { name: "TypeScript", aliases: ["ts"] },
python: { name: "Python", aliases: ["py"] },
java: { name: "Java" },
c: { name: "C" },
cpp: { name: "C++" },
go: { name: "Go" },
ruby: { name: "Ruby" },
php: { name: "PHP" },
rust: { name: "Rust" },
kotlin: { name: "Kotlin" },
swift: { name: "Swift" },
csharp: { name: "C#", aliases: ["cs"] },
scala: { name: "Scala" },
html: { name: "HTML" },
css: { name: "CSS" },
json: { name: "JSON" },
yaml: { name: "YAML", aliases: ["yml"] },
markdown: { name: "Markdown", aliases: ["md"] },
sql: { name: "SQL" },
bash: { name: "Bash", aliases: ["sh"] },
powershell: { name: "PowerShell", aliases: ["ps"] },
dart: { name: "Dart" },
"objective-c": { name: "Objective-C", aliases: ["objc"] },
} as const;

// Derive the supported languages object for BlockNote
export const supportedLanguages = Object.fromEntries(
Object.entries(codeBlockLanguages).map(([key, value]) => {
const langConfig =
"aliases" in value && value.aliases
? { name: value.name, aliases: [...value.aliases] } // Convert readonly array to mutable
: { name: value.name };
return [key, langConfig];
}),
) as Record<string, { name: string; aliases?: string[] }>;

// Extract language IDs for Shiki highlighter
const shikiLanguageIds = Object.keys(codeBlockLanguages);

// Create the highlighter factory function for BlockNote
// Type mismatch between shiki and @blocknote/core shiki types - both use different versions of @shikijs/types
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
export const createCodeBlockHighlighter = () =>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
}) as any;
Comment on lines +49 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Find the codeBlockSyntaxHighlighter.ts file
find . -name "codeBlockSyntaxHighlighter.ts" -type f 2>/dev/null | head -5

Repository: Matia-R/Chptr

Length of output: 115


🏁 Script executed:

cat -n ./src/app/_components/editor/codeBlockSyntaxHighlighter.ts

Repository: Matia-R/Chptr

Length of output: 2544


🏁 Script executed:

find . -name "editor.tsx" -type f 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -100 {}'

Repository: Matia-R/Chptr

Length of output: 2647


🏁 Script executed:

rg "currentTheme|theme.*light|theme.*dark" --type ts --type tsx -C 2 | head -80

Repository: Matia-R/Chptr

Length of output: 84


🏁 Script executed:

rg "codeToHtml|highlighter.*theme|theme.*highlighter" ./src --type ts --type tsx -A 2 -B 2 | head -60

Repository: Matia-R/Chptr

Length of output: 84


Load both light and dark themes in the code highlighter.

Currently only "github-dark" is loaded, causing users in light mode to see dark syntax highlighting in code blocks. Shiki supports loading multiple themes which are switched using CSS variables.

The editor already passes the current theme to BlockNoteView (editor.tsx line 87: theme={currentTheme as "light" | "dark"}), and BlockNote automatically selects the appropriate theme based on the user's system preference. Shiki handles the light/dark switching through CSS variables automatically—no additional theme-selection code is needed.

Apply this diff:

 export const createCodeBlockHighlighter = () =>
createHighlighter({
- themes: ["github-dark"],+ themes: ["github-light", "github-dark"],
langs: shikiLanguageIds,
}) as any;
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
})asany;
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-light","github-dark"],
langs: shikiLanguageIds,
})asany;
🤖 Prompt for AI Agents
In src/app/_components/editor/codeBlockSyntaxHighlighter.ts around lines 49 to
53, the highlighter currently loads only the "github-dark" theme causing
light-mode users to see dark highlighting; update the createHighlighter call to
include both the light and dark themes (e.g., "github-light" and "github-dark")
in the themes array so Shiki can switch via CSS variables automatically; keep
the return type cast as any and do not add runtime theme-selection logic.

/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */

38 changes: 38 additions & 0 deletions src/app/_components/editor/cursor-renderer.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
type CollaborationUser = {
name: string;
color: string;
[key: string]: string;
};

// Custom cursor renderer for Google Docs-like appearance
export const renderCursor = (user: CollaborationUser): HTMLElement => {
const cursorElement = document.createElement("span");
cursorElement.classList.add("bn-collaboration-cursor__base");

// Create the caret (vertical line)
const caretElement = document.createElement("span");
caretElement.setAttribute("contenteditable", "false");
caretElement.classList.add("bn-collaboration-cursor__caret");
caretElement.setAttribute(
"style",
`border-left: 2.5px solid ${user.color}; height: 1.25em; position: relative; display: inline-block; vertical-align: text-bottom;`
);

// Create the label
const labelElement = document.createElement("span");
labelElement.classList.add("bn-collaboration-cursor__label");
labelElement.setAttribute(
"style",
`background-color: ${user.color}; color: #ffffff; padding: 3px 7px; font-size: 11px; font-weight: 600; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 1px rgba(0, 0, 0, 0.15); position: absolute; left: -2.5px; top: -1.85em; white-space: nowrap; z-index: 10; pointer-events: none; user-select: none;`
);
labelElement.textContent = user.name;

// Assemble the cursor
caretElement.appendChild(labelElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space
cursorElement.appendChild(caretElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space

return cursorElement;
};

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Realtime doc sync yjs by Matia-R · Pull Request #20 · Matia-R/Chptr · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 325 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,25 +47,30 @@
"cmdk": "^1.1.1",
"framer-motion": "^12.16.0",
"geist": "^1.3.0",
"lodash": "^4.17.21",
"lodash.throttle": "^4.1.1",
"lucide-react": "^0.460.0",
"next": "^15.0.1",
"next": "15.0.7",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"server-only": "^0.0.1",
"shiki": "^3.9.2",
"shiki": "^3.20.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.27",
"zod": "^3.23.8",
"zustand": "^5.0.4"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/lodash": "^4.17.21",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
5 changes: 3 additions & 2 deletions src/app/_components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import { type Document } from "~/server/api/routers/document"
import { useToast } from "../../hooks/use-toast"
import { NavUser } from "./nav-user"
import { useCommandMenuStore } from "~/hooks/use-command-menu"
import { useUserProfile } from "~/hooks/use-user-profile"

interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
initialDocuments: { id: string; name: string }[]
Expand DownExpand Up@@ -63,8 +64,8 @@ export function AppSidebar({ initialDocuments, ...props }: AppSidebarProps) {
setShowBottomShadow(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
}, [documents]);

// Fetch user profile and email
const { data: userProfile, isLoading: userLoading } = api.user.getCurrentUserProfile.useQuery();
// Fetch user profile and email (shared query, automatically deduplicated by React Query)
const { data: userProfile, isLoading: userLoading } = useUserProfile();
const { data: userEmail } = api.user.getCurrentUser.useQuery();

const createDocument = api.document.createDocument.useMutation({
Expand Down
55 changes: 55 additions & 0 deletions src/app/_components/editor/codeBlockSyntaxHighlighter.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { createHighlighter } from "shiki";

// Code block language configuration
// Add or remove languages here - changes will be applied to both BlockNote and Shiki
const codeBlockLanguages = {
javascript: { name: "JavaScript", aliases: ["js"] },
typescript: { name: "TypeScript", aliases: ["ts"] },
python: { name: "Python", aliases: ["py"] },
java: { name: "Java" },
c: { name: "C" },
cpp: { name: "C++" },
go: { name: "Go" },
ruby: { name: "Ruby" },
php: { name: "PHP" },
rust: { name: "Rust" },
kotlin: { name: "Kotlin" },
swift: { name: "Swift" },
csharp: { name: "C#", aliases: ["cs"] },
scala: { name: "Scala" },
html: { name: "HTML" },
css: { name: "CSS" },
json: { name: "JSON" },
yaml: { name: "YAML", aliases: ["yml"] },
markdown: { name: "Markdown", aliases: ["md"] },
sql: { name: "SQL" },
bash: { name: "Bash", aliases: ["sh"] },
powershell: { name: "PowerShell", aliases: ["ps"] },
dart: { name: "Dart" },
"objective-c": { name: "Objective-C", aliases: ["objc"] },
} as const;

// Derive the supported languages object for BlockNote
export const supportedLanguages = Object.fromEntries(
Object.entries(codeBlockLanguages).map(([key, value]) => {
const langConfig =
"aliases" in value && value.aliases
? { name: value.name, aliases: [...value.aliases] } // Convert readonly array to mutable
: { name: value.name };
return [key, langConfig];
}),
) as Record<string, { name: string; aliases?: string[] }>;

// Extract language IDs for Shiki highlighter
const shikiLanguageIds = Object.keys(codeBlockLanguages);

// Create the highlighter factory function for BlockNote
// Type mismatch between shiki and @blocknote/core shiki types - both use different versions of @shikijs/types
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
export const createCodeBlockHighlighter = () =>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
}) as any;
Comment on lines +49 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Find the codeBlockSyntaxHighlighter.ts file
find . -name "codeBlockSyntaxHighlighter.ts" -type f 2>/dev/null | head -5

Repository: Matia-R/Chptr

Length of output: 115


🏁 Script executed:

cat -n ./src/app/_components/editor/codeBlockSyntaxHighlighter.ts

Repository: Matia-R/Chptr

Length of output: 2544


🏁 Script executed:

find . -name "editor.tsx" -type f 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -100 {}'

Repository: Matia-R/Chptr

Length of output: 2647


🏁 Script executed:

rg "currentTheme|theme.*light|theme.*dark" --type ts --type tsx -C 2 | head -80

Repository: Matia-R/Chptr

Length of output: 84


🏁 Script executed:

rg "codeToHtml|highlighter.*theme|theme.*highlighter" ./src --type ts --type tsx -A 2 -B 2 | head -60

Repository: Matia-R/Chptr

Length of output: 84


Load both light and dark themes in the code highlighter.

Currently only "github-dark" is loaded, causing users in light mode to see dark syntax highlighting in code blocks. Shiki supports loading multiple themes which are switched using CSS variables.

The editor already passes the current theme to BlockNoteView (editor.tsx line 87: theme={currentTheme as "light" | "dark"}), and BlockNote automatically selects the appropriate theme based on the user's system preference. Shiki handles the light/dark switching through CSS variables automatically—no additional theme-selection code is needed.

Apply this diff:

 export const createCodeBlockHighlighter = () =>
createHighlighter({
- themes: ["github-dark"],+ themes: ["github-light", "github-dark"],
langs: shikiLanguageIds,
}) as any;
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
})asany;
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-light","github-dark"],
langs: shikiLanguageIds,
})asany;
🤖 Prompt for AI Agents
In src/app/_components/editor/codeBlockSyntaxHighlighter.ts around lines 49 to
53, the highlighter currently loads only the "github-dark" theme causing
light-mode users to see dark highlighting; update the createHighlighter call to
include both the light and dark themes (e.g., "github-light" and "github-dark")
in the themes array so Shiki can switch via CSS variables automatically; keep
the return type cast as any and do not add runtime theme-selection logic.

/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */

38 changes: 38 additions & 0 deletions src/app/_components/editor/cursor-renderer.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
type CollaborationUser = {
name: string;
color: string;
[key: string]: string;
};

// Custom cursor renderer for Google Docs-like appearance
export const renderCursor = (user: CollaborationUser): HTMLElement => {
const cursorElement = document.createElement("span");
cursorElement.classList.add("bn-collaboration-cursor__base");

// Create the caret (vertical line)
const caretElement = document.createElement("span");
caretElement.setAttribute("contenteditable", "false");
caretElement.classList.add("bn-collaboration-cursor__caret");
caretElement.setAttribute(
"style",
`border-left: 2.5px solid ${user.color}; height: 1.25em; position: relative; display: inline-block; vertical-align: text-bottom;`
);

// Create the label
const labelElement = document.createElement("span");
labelElement.classList.add("bn-collaboration-cursor__label");
labelElement.setAttribute(
"style",
`background-color: ${user.color}; color: #ffffff; padding: 3px 7px; font-size: 11px; font-weight: 600; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 1px rgba(0, 0, 0, 0.15); position: absolute; left: -2.5px; top: -1.85em; white-space: nowrap; z-index: 10; pointer-events: none; user-select: none;`
);
labelElement.textContent = user.name;

// Assemble the cursor
caretElement.appendChild(labelElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space
cursorElement.appendChild(caretElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space

return cursorElement;
};

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Realtime doc sync yjs by Matia-R · Pull Request #20 · Matia-R/Chptr · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 325 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,25 +47,30 @@
"cmdk": "^1.1.1",
"framer-motion": "^12.16.0",
"geist": "^1.3.0",
"lodash": "^4.17.21",
"lodash.throttle": "^4.1.1",
"lucide-react": "^0.460.0",
"next": "^15.0.1",
"next": "15.0.7",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"server-only": "^0.0.1",
"shiki": "^3.9.2",
"shiki": "^3.20.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.27",
"zod": "^3.23.8",
"zustand": "^5.0.4"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/lodash": "^4.17.21",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
5 changes: 3 additions & 2 deletions src/app/_components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import { type Document } from "~/server/api/routers/document"
import { useToast } from "../../hooks/use-toast"
import { NavUser } from "./nav-user"
import { useCommandMenuStore } from "~/hooks/use-command-menu"
import { useUserProfile } from "~/hooks/use-user-profile"

interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
initialDocuments: { id: string; name: string }[]
Expand DownExpand Up@@ -63,8 +64,8 @@ export function AppSidebar({ initialDocuments, ...props }: AppSidebarProps) {
setShowBottomShadow(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
}, [documents]);

// Fetch user profile and email
const { data: userProfile, isLoading: userLoading } = api.user.getCurrentUserProfile.useQuery();
// Fetch user profile and email (shared query, automatically deduplicated by React Query)
const { data: userProfile, isLoading: userLoading } = useUserProfile();
const { data: userEmail } = api.user.getCurrentUser.useQuery();

const createDocument = api.document.createDocument.useMutation({
Expand Down
55 changes: 55 additions & 0 deletions src/app/_components/editor/codeBlockSyntaxHighlighter.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { createHighlighter } from "shiki";

// Code block language configuration
// Add or remove languages here - changes will be applied to both BlockNote and Shiki
const codeBlockLanguages = {
javascript: { name: "JavaScript", aliases: ["js"] },
typescript: { name: "TypeScript", aliases: ["ts"] },
python: { name: "Python", aliases: ["py"] },
java: { name: "Java" },
c: { name: "C" },
cpp: { name: "C++" },
go: { name: "Go" },
ruby: { name: "Ruby" },
php: { name: "PHP" },
rust: { name: "Rust" },
kotlin: { name: "Kotlin" },
swift: { name: "Swift" },
csharp: { name: "C#", aliases: ["cs"] },
scala: { name: "Scala" },
html: { name: "HTML" },
css: { name: "CSS" },
json: { name: "JSON" },
yaml: { name: "YAML", aliases: ["yml"] },
markdown: { name: "Markdown", aliases: ["md"] },
sql: { name: "SQL" },
bash: { name: "Bash", aliases: ["sh"] },
powershell: { name: "PowerShell", aliases: ["ps"] },
dart: { name: "Dart" },
"objective-c": { name: "Objective-C", aliases: ["objc"] },
} as const;

// Derive the supported languages object for BlockNote
export const supportedLanguages = Object.fromEntries(
Object.entries(codeBlockLanguages).map(([key, value]) => {
const langConfig =
"aliases" in value && value.aliases
? { name: value.name, aliases: [...value.aliases] } // Convert readonly array to mutable
: { name: value.name };
return [key, langConfig];
}),
) as Record<string, { name: string; aliases?: string[] }>;

// Extract language IDs for Shiki highlighter
const shikiLanguageIds = Object.keys(codeBlockLanguages);

// Create the highlighter factory function for BlockNote
// Type mismatch between shiki and @blocknote/core shiki types - both use different versions of @shikijs/types
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
export const createCodeBlockHighlighter = () =>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
}) as any;
Comment on lines +49 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Find the codeBlockSyntaxHighlighter.ts file
find . -name "codeBlockSyntaxHighlighter.ts" -type f 2>/dev/null | head -5

Repository: Matia-R/Chptr

Length of output: 115


🏁 Script executed:

cat -n ./src/app/_components/editor/codeBlockSyntaxHighlighter.ts

Repository: Matia-R/Chptr

Length of output: 2544


🏁 Script executed:

find . -name "editor.tsx" -type f 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -100 {}'

Repository: Matia-R/Chptr

Length of output: 2647


🏁 Script executed:

rg "currentTheme|theme.*light|theme.*dark" --type ts --type tsx -C 2 | head -80

Repository: Matia-R/Chptr

Length of output: 84


🏁 Script executed:

rg "codeToHtml|highlighter.*theme|theme.*highlighter" ./src --type ts --type tsx -A 2 -B 2 | head -60

Repository: Matia-R/Chptr

Length of output: 84


Load both light and dark themes in the code highlighter.

Currently only "github-dark" is loaded, causing users in light mode to see dark syntax highlighting in code blocks. Shiki supports loading multiple themes which are switched using CSS variables.

The editor already passes the current theme to BlockNoteView (editor.tsx line 87: theme={currentTheme as "light" | "dark"}), and BlockNote automatically selects the appropriate theme based on the user's system preference. Shiki handles the light/dark switching through CSS variables automatically—no additional theme-selection code is needed.

Apply this diff:

 export const createCodeBlockHighlighter = () =>
createHighlighter({
- themes: ["github-dark"],+ themes: ["github-light", "github-dark"],
langs: shikiLanguageIds,
}) as any;
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
})asany;
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-light","github-dark"],
langs: shikiLanguageIds,
})asany;
🤖 Prompt for AI Agents
In src/app/_components/editor/codeBlockSyntaxHighlighter.ts around lines 49 to
53, the highlighter currently loads only the "github-dark" theme causing
light-mode users to see dark highlighting; update the createHighlighter call to
include both the light and dark themes (e.g., "github-light" and "github-dark")
in the themes array so Shiki can switch via CSS variables automatically; keep
the return type cast as any and do not add runtime theme-selection logic.

/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */

38 changes: 38 additions & 0 deletions src/app/_components/editor/cursor-renderer.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
type CollaborationUser = {
name: string;
color: string;
[key: string]: string;
};

// Custom cursor renderer for Google Docs-like appearance
export const renderCursor = (user: CollaborationUser): HTMLElement => {
const cursorElement = document.createElement("span");
cursorElement.classList.add("bn-collaboration-cursor__base");

// Create the caret (vertical line)
const caretElement = document.createElement("span");
caretElement.setAttribute("contenteditable", "false");
caretElement.classList.add("bn-collaboration-cursor__caret");
caretElement.setAttribute(
"style",
`border-left: 2.5px solid ${user.color}; height: 1.25em; position: relative; display: inline-block; vertical-align: text-bottom;`
);

// Create the label
const labelElement = document.createElement("span");
labelElement.classList.add("bn-collaboration-cursor__label");
labelElement.setAttribute(
"style",
`background-color: ${user.color}; color: #ffffff; padding: 3px 7px; font-size: 11px; font-weight: 600; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 1px rgba(0, 0, 0, 0.15); position: absolute; left: -2.5px; top: -1.85em; white-space: nowrap; z-index: 10; pointer-events: none; user-select: none;`
);
labelElement.textContent = user.name;

// Assemble the cursor
caretElement.appendChild(labelElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space
cursorElement.appendChild(caretElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space

return cursorElement;
};

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Realtime doc sync yjs by Matia-R · Pull Request #20 · Matia-R/Chptr · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 325 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,25 +47,30 @@
"cmdk": "^1.1.1",
"framer-motion": "^12.16.0",
"geist": "^1.3.0",
"lodash": "^4.17.21",
"lodash.throttle": "^4.1.1",
"lucide-react": "^0.460.0",
"next": "^15.0.1",
"next": "15.0.7",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"server-only": "^0.0.1",
"shiki": "^3.9.2",
"shiki": "^3.20.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.27",
"zod": "^3.23.8",
"zustand": "^5.0.4"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/lodash": "^4.17.21",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
5 changes: 3 additions & 2 deletions src/app/_components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import { type Document } from "~/server/api/routers/document"
import { useToast } from "../../hooks/use-toast"
import { NavUser } from "./nav-user"
import { useCommandMenuStore } from "~/hooks/use-command-menu"
import { useUserProfile } from "~/hooks/use-user-profile"

interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
initialDocuments: { id: string; name: string }[]
Expand DownExpand Up@@ -63,8 +64,8 @@ export function AppSidebar({ initialDocuments, ...props }: AppSidebarProps) {
setShowBottomShadow(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
}, [documents]);

// Fetch user profile and email
const { data: userProfile, isLoading: userLoading } = api.user.getCurrentUserProfile.useQuery();
// Fetch user profile and email (shared query, automatically deduplicated by React Query)
const { data: userProfile, isLoading: userLoading } = useUserProfile();
const { data: userEmail } = api.user.getCurrentUser.useQuery();

const createDocument = api.document.createDocument.useMutation({
Expand Down
55 changes: 55 additions & 0 deletions src/app/_components/editor/codeBlockSyntaxHighlighter.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { createHighlighter } from "shiki";

// Code block language configuration
// Add or remove languages here - changes will be applied to both BlockNote and Shiki
const codeBlockLanguages = {
javascript: { name: "JavaScript", aliases: ["js"] },
typescript: { name: "TypeScript", aliases: ["ts"] },
python: { name: "Python", aliases: ["py"] },
java: { name: "Java" },
c: { name: "C" },
cpp: { name: "C++" },
go: { name: "Go" },
ruby: { name: "Ruby" },
php: { name: "PHP" },
rust: { name: "Rust" },
kotlin: { name: "Kotlin" },
swift: { name: "Swift" },
csharp: { name: "C#", aliases: ["cs"] },
scala: { name: "Scala" },
html: { name: "HTML" },
css: { name: "CSS" },
json: { name: "JSON" },
yaml: { name: "YAML", aliases: ["yml"] },
markdown: { name: "Markdown", aliases: ["md"] },
sql: { name: "SQL" },
bash: { name: "Bash", aliases: ["sh"] },
powershell: { name: "PowerShell", aliases: ["ps"] },
dart: { name: "Dart" },
"objective-c": { name: "Objective-C", aliases: ["objc"] },
} as const;

// Derive the supported languages object for BlockNote
export const supportedLanguages = Object.fromEntries(
Object.entries(codeBlockLanguages).map(([key, value]) => {
const langConfig =
"aliases" in value && value.aliases
? { name: value.name, aliases: [...value.aliases] } // Convert readonly array to mutable
: { name: value.name };
return [key, langConfig];
}),
) as Record<string, { name: string; aliases?: string[] }>;

// Extract language IDs for Shiki highlighter
const shikiLanguageIds = Object.keys(codeBlockLanguages);

// Create the highlighter factory function for BlockNote
// Type mismatch between shiki and @blocknote/core shiki types - both use different versions of @shikijs/types
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
export const createCodeBlockHighlighter = () =>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
}) as any;
Comment on lines +49 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Find the codeBlockSyntaxHighlighter.ts file
find . -name "codeBlockSyntaxHighlighter.ts" -type f 2>/dev/null | head -5

Repository: Matia-R/Chptr

Length of output: 115


🏁 Script executed:

cat -n ./src/app/_components/editor/codeBlockSyntaxHighlighter.ts

Repository: Matia-R/Chptr

Length of output: 2544


🏁 Script executed:

find . -name "editor.tsx" -type f 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -100 {}'

Repository: Matia-R/Chptr

Length of output: 2647


🏁 Script executed:

rg "currentTheme|theme.*light|theme.*dark" --type ts --type tsx -C 2 | head -80

Repository: Matia-R/Chptr

Length of output: 84


🏁 Script executed:

rg "codeToHtml|highlighter.*theme|theme.*highlighter" ./src --type ts --type tsx -A 2 -B 2 | head -60

Repository: Matia-R/Chptr

Length of output: 84


Load both light and dark themes in the code highlighter.

Currently only "github-dark" is loaded, causing users in light mode to see dark syntax highlighting in code blocks. Shiki supports loading multiple themes which are switched using CSS variables.

The editor already passes the current theme to BlockNoteView (editor.tsx line 87: theme={currentTheme as "light" | "dark"}), and BlockNote automatically selects the appropriate theme based on the user's system preference. Shiki handles the light/dark switching through CSS variables automatically—no additional theme-selection code is needed.

Apply this diff:

 export const createCodeBlockHighlighter = () =>
createHighlighter({
- themes: ["github-dark"],+ themes: ["github-light", "github-dark"],
langs: shikiLanguageIds,
}) as any;
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
})asany;
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-light","github-dark"],
langs: shikiLanguageIds,
})asany;
🤖 Prompt for AI Agents
In src/app/_components/editor/codeBlockSyntaxHighlighter.ts around lines 49 to
53, the highlighter currently loads only the "github-dark" theme causing
light-mode users to see dark highlighting; update the createHighlighter call to
include both the light and dark themes (e.g., "github-light" and "github-dark")
in the themes array so Shiki can switch via CSS variables automatically; keep
the return type cast as any and do not add runtime theme-selection logic.

/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */

38 changes: 38 additions & 0 deletions src/app/_components/editor/cursor-renderer.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
type CollaborationUser = {
name: string;
color: string;
[key: string]: string;
};

// Custom cursor renderer for Google Docs-like appearance
export const renderCursor = (user: CollaborationUser): HTMLElement => {
const cursorElement = document.createElement("span");
cursorElement.classList.add("bn-collaboration-cursor__base");

// Create the caret (vertical line)
const caretElement = document.createElement("span");
caretElement.setAttribute("contenteditable", "false");
caretElement.classList.add("bn-collaboration-cursor__caret");
caretElement.setAttribute(
"style",
`border-left: 2.5px solid ${user.color}; height: 1.25em; position: relative; display: inline-block; vertical-align: text-bottom;`
);

// Create the label
const labelElement = document.createElement("span");
labelElement.classList.add("bn-collaboration-cursor__label");
labelElement.setAttribute(
"style",
`background-color: ${user.color}; color: #ffffff; padding: 3px 7px; font-size: 11px; font-weight: 600; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 1px rgba(0, 0, 0, 0.15); position: absolute; left: -2.5px; top: -1.85em; white-space: nowrap; z-index: 10; pointer-events: none; user-select: none;`
);
labelElement.textContent = user.name;

// Assemble the cursor
caretElement.appendChild(labelElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space
cursorElement.appendChild(caretElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space

return cursorElement;
};

Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Realtime doc sync yjs by Matia-R · Pull Request #20 · Matia-R/Chptr · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 325 additions & 79 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,25 +47,30 @@
"cmdk": "^1.1.1",
"framer-motion": "^12.16.0",
"geist": "^1.3.0",
"lodash": "^4.17.21",
"lodash.throttle": "^4.1.1",
"lucide-react": "^0.460.0",
"next": "^15.0.1",
"next": "15.0.7",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"server-only": "^0.0.1",
"shiki": "^3.9.2",
"shiki": "^3.20.0",
"superjson": "^2.2.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.27",
"zod": "^3.23.8",
"zustand": "^5.0.4"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/lodash": "^4.17.21",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
5 changes: 3 additions & 2 deletions src/app/_components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import { type Document } from "~/server/api/routers/document"
import { useToast } from "../../hooks/use-toast"
import { NavUser } from "./nav-user"
import { useCommandMenuStore } from "~/hooks/use-command-menu"
import { useUserProfile } from "~/hooks/use-user-profile"

interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
initialDocuments: { id: string; name: string }[]
Expand DownExpand Up@@ -63,8 +64,8 @@ export function AppSidebar({ initialDocuments, ...props }: AppSidebarProps) {
setShowBottomShadow(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
}, [documents]);

// Fetch user profile and email
const { data: userProfile, isLoading: userLoading } = api.user.getCurrentUserProfile.useQuery();
// Fetch user profile and email (shared query, automatically deduplicated by React Query)
const { data: userProfile, isLoading: userLoading } = useUserProfile();
const { data: userEmail } = api.user.getCurrentUser.useQuery();

const createDocument = api.document.createDocument.useMutation({
Expand Down
55 changes: 55 additions & 0 deletions src/app/_components/editor/codeBlockSyntaxHighlighter.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
import { createHighlighter } from "shiki";

// Code block language configuration
// Add or remove languages here - changes will be applied to both BlockNote and Shiki
const codeBlockLanguages = {
javascript: { name: "JavaScript", aliases: ["js"] },
typescript: { name: "TypeScript", aliases: ["ts"] },
python: { name: "Python", aliases: ["py"] },
java: { name: "Java" },
c: { name: "C" },
cpp: { name: "C++" },
go: { name: "Go" },
ruby: { name: "Ruby" },
php: { name: "PHP" },
rust: { name: "Rust" },
kotlin: { name: "Kotlin" },
swift: { name: "Swift" },
csharp: { name: "C#", aliases: ["cs"] },
scala: { name: "Scala" },
html: { name: "HTML" },
css: { name: "CSS" },
json: { name: "JSON" },
yaml: { name: "YAML", aliases: ["yml"] },
markdown: { name: "Markdown", aliases: ["md"] },
sql: { name: "SQL" },
bash: { name: "Bash", aliases: ["sh"] },
powershell: { name: "PowerShell", aliases: ["ps"] },
dart: { name: "Dart" },
"objective-c": { name: "Objective-C", aliases: ["objc"] },
} as const;

// Derive the supported languages object for BlockNote
export const supportedLanguages = Object.fromEntries(
Object.entries(codeBlockLanguages).map(([key, value]) => {
const langConfig =
"aliases" in value && value.aliases
? { name: value.name, aliases: [...value.aliases] } // Convert readonly array to mutable
: { name: value.name };
return [key, langConfig];
}),
) as Record<string, { name: string; aliases?: string[] }>;

// Extract language IDs for Shiki highlighter
const shikiLanguageIds = Object.keys(codeBlockLanguages);

// Create the highlighter factory function for BlockNote
// Type mismatch between shiki and @blocknote/core shiki types - both use different versions of @shikijs/types
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
export const createCodeBlockHighlighter = () =>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
}) as any;
Comment on lines +49 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Find the codeBlockSyntaxHighlighter.ts file
find . -name "codeBlockSyntaxHighlighter.ts" -type f 2>/dev/null | head -5

Repository: Matia-R/Chptr

Length of output: 115


🏁 Script executed:

cat -n ./src/app/_components/editor/codeBlockSyntaxHighlighter.ts

Repository: Matia-R/Chptr

Length of output: 2544


🏁 Script executed:

find . -name "editor.tsx" -type f 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -100 {}'

Repository: Matia-R/Chptr

Length of output: 2647


🏁 Script executed:

rg "currentTheme|theme.*light|theme.*dark" --type ts --type tsx -C 2 | head -80

Repository: Matia-R/Chptr

Length of output: 84


🏁 Script executed:

rg "codeToHtml|highlighter.*theme|theme.*highlighter" ./src --type ts --type tsx -A 2 -B 2 | head -60

Repository: Matia-R/Chptr

Length of output: 84


Load both light and dark themes in the code highlighter.

Currently only "github-dark" is loaded, causing users in light mode to see dark syntax highlighting in code blocks. Shiki supports loading multiple themes which are switched using CSS variables.

The editor already passes the current theme to BlockNoteView (editor.tsx line 87: theme={currentTheme as "light" | "dark"}), and BlockNote automatically selects the appropriate theme based on the user's system preference. Shiki handles the light/dark switching through CSS variables automatically—no additional theme-selection code is needed.

Apply this diff:

 export const createCodeBlockHighlighter = () =>
createHighlighter({
- themes: ["github-dark"],+ themes: ["github-light", "github-dark"],
langs: shikiLanguageIds,
}) as any;
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-dark"],
langs: shikiLanguageIds,
})asany;
exportconstcreateCodeBlockHighlighter=()=>
createHighlighter({
themes: ["github-light","github-dark"],
langs: shikiLanguageIds,
})asany;
🤖 Prompt for AI Agents
In src/app/_components/editor/codeBlockSyntaxHighlighter.ts around lines 49 to
53, the highlighter currently loads only the "github-dark" theme causing
light-mode users to see dark highlighting; update the createHighlighter call to
include both the light and dark themes (e.g., "github-light" and "github-dark")
in the themes array so Shiki can switch via CSS variables automatically; keep
the return type cast as any and do not add runtime theme-selection logic.

/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */

38 changes: 38 additions & 0 deletions src/app/_components/editor/cursor-renderer.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
type CollaborationUser = {
name: string;
color: string;
[key: string]: string;
};

// Custom cursor renderer for Google Docs-like appearance
export const renderCursor = (user: CollaborationUser): HTMLElement => {
const cursorElement = document.createElement("span");
cursorElement.classList.add("bn-collaboration-cursor__base");

// Create the caret (vertical line)
const caretElement = document.createElement("span");
caretElement.setAttribute("contenteditable", "false");
caretElement.classList.add("bn-collaboration-cursor__caret");
caretElement.setAttribute(
"style",
`border-left: 2.5px solid ${user.color}; height: 1.25em; position: relative; display: inline-block; vertical-align: text-bottom;`
);

// Create the label
const labelElement = document.createElement("span");
labelElement.classList.add("bn-collaboration-cursor__label");
labelElement.setAttribute(
"style",
`background-color: ${user.color}; color: #ffffff; padding: 3px 7px; font-size: 11px; font-weight: 600; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 1px rgba(0, 0, 0, 0.15); position: absolute; left: -2.5px; top: -1.85em; white-space: nowrap; z-index: 10; pointer-events: none; user-select: none;`
);
labelElement.textContent = user.name;

// Assemble the cursor
caretElement.appendChild(labelElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space
cursorElement.appendChild(caretElement);
cursorElement.appendChild(document.createTextNode("\u2060")); // Non-breaking space

return cursorElement;
};

Loading