sync: Focus Panel UX Refresh from private repo
- Three-tab interface: Notes | Desc | Source - Markdown preview with node tokens (react-markdown + remark-gfm) - Formatting toolbar (H1/H2/H3/Bold/Italic) - @mention functionality for linking nodes - Description edit with 280 char limit Run `npm install` to get new dependencies.
This commit is contained in:
+1257
-541
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,176 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Bold, Italic, Heading1, Heading2, Heading3 } from 'lucide-react';
|
||||
|
||||
interface FormattingToolbarProps {
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
value: string;
|
||||
onChange: (newValue: string) => void;
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
export default function FormattingToolbar({ textareaRef, value, onChange, inline = false }: FormattingToolbarProps) {
|
||||
const applyFormatting = (
|
||||
type: 'h1' | 'h2' | 'h3' | 'bold' | 'italic'
|
||||
) => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = value.substring(start, end);
|
||||
|
||||
let newText = value;
|
||||
let newCursorPos = end;
|
||||
|
||||
switch (type) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3': {
|
||||
const prefix = type === 'h1' ? '# ' : type === 'h2' ? '## ' : '### ';
|
||||
// Find the start of the current line
|
||||
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
|
||||
const lineEnd = value.indexOf('\n', end);
|
||||
const actualLineEnd = lineEnd === -1 ? value.length : lineEnd;
|
||||
const currentLine = value.substring(lineStart, actualLineEnd);
|
||||
|
||||
// Check if line already has a heading prefix
|
||||
const headingMatch = currentLine.match(/^(#{1,3})\s/);
|
||||
if (headingMatch) {
|
||||
// Replace existing heading
|
||||
const newLine = prefix + currentLine.replace(/^#{1,3}\s/, '');
|
||||
newText = value.substring(0, lineStart) + newLine + value.substring(actualLineEnd);
|
||||
newCursorPos = lineStart + newLine.length;
|
||||
} else {
|
||||
// Add heading prefix
|
||||
newText = value.substring(0, lineStart) + prefix + value.substring(lineStart);
|
||||
newCursorPos = start + prefix.length;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'bold': {
|
||||
if (selectedText) {
|
||||
// Wrap selection with **
|
||||
newText = value.substring(0, start) + '**' + selectedText + '**' + value.substring(end);
|
||||
newCursorPos = end + 4;
|
||||
} else {
|
||||
// Insert ** and place cursor in middle
|
||||
newText = value.substring(0, start) + '****' + value.substring(end);
|
||||
newCursorPos = start + 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'italic': {
|
||||
if (selectedText) {
|
||||
// Wrap selection with *
|
||||
newText = value.substring(0, start) + '*' + selectedText + '*' + value.substring(end);
|
||||
newCursorPos = end + 2;
|
||||
} else {
|
||||
// Insert ** and place cursor in middle
|
||||
newText = value.substring(0, start) + '**' + value.substring(end);
|
||||
newCursorPos = start + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onChange(newText);
|
||||
|
||||
// Restore focus and cursor position
|
||||
setTimeout(() => {
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const buttonStyle: React.CSSProperties = {
|
||||
background: 'transparent',
|
||||
border: '1px solid #2a2a2a',
|
||||
borderRadius: '4px',
|
||||
padding: '4px 8px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#888',
|
||||
transition: 'all 0.15s'
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
e.currentTarget.style.color = '#ccc';
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = '#888';
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
alignItems: 'center',
|
||||
...(inline ? {} : {
|
||||
marginBottom: '8px',
|
||||
paddingBottom: '8px',
|
||||
borderBottom: '1px solid #1a1a1a'
|
||||
})
|
||||
}}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyFormatting('h1')}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={buttonStyle}
|
||||
title="Heading 1 (# )"
|
||||
>
|
||||
<Heading1 size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyFormatting('h2')}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={buttonStyle}
|
||||
title="Heading 2 (## )"
|
||||
>
|
||||
<Heading2 size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyFormatting('h3')}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={buttonStyle}
|
||||
title="Heading 3 (### )"
|
||||
>
|
||||
<Heading3 size={14} />
|
||||
</button>
|
||||
<div style={{ width: '1px', background: '#2a2a2a', margin: '0 4px' }} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyFormatting('bold')}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={buttonStyle}
|
||||
title="Bold (**text**)"
|
||||
>
|
||||
<Bold size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyFormatting('italic')}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={buttonStyle}
|
||||
title="Italic (*text*)"
|
||||
>
|
||||
<Italic size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user