|
|
|
@@ -63,6 +63,17 @@
|
|
|
|
|
let highlightDropdown = $state(false);
|
|
|
|
|
let alignDropdown = $state(false);
|
|
|
|
|
let insertDropdown = $state(false);
|
|
|
|
|
|
|
|
|
|
function closeAllDropdowns() {
|
|
|
|
|
headingDropdown = false;
|
|
|
|
|
colorDropdown = false;
|
|
|
|
|
highlightDropdown = false;
|
|
|
|
|
alignDropdown = false;
|
|
|
|
|
insertDropdown = false;
|
|
|
|
|
tablePickerOpen = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let anyDropdownOpen = $derived(headingDropdown || colorDropdown || highlightDropdown || alignDropdown || insertDropdown || tablePickerOpen);
|
|
|
|
|
let editorState = $state(0);
|
|
|
|
|
|
|
|
|
|
// AI
|
|
|
|
@@ -93,6 +104,14 @@
|
|
|
|
|
let historySelected = $state<VersionEntry | null>(null);
|
|
|
|
|
let historyLoading = $state(false);
|
|
|
|
|
|
|
|
|
|
// In-note search
|
|
|
|
|
let noteSearchOpen = $state(false);
|
|
|
|
|
let noteSearchQuery = $state('');
|
|
|
|
|
let noteSearchIndex = $state(0);
|
|
|
|
|
let noteSearchResults = $state<{from: number, to: number}[]>([]);
|
|
|
|
|
let noteSearchInput = $state<HTMLInputElement>(null!);
|
|
|
|
|
const noteSearchPluginKey = new PluginKey('noteSearch');
|
|
|
|
|
|
|
|
|
|
// Slash commands
|
|
|
|
|
let slashMenu = $state<{ x: number; y: number; query: string; from: number; to: number } | null>(null);
|
|
|
|
|
let slashSelectedIndex = $state(0);
|
|
|
|
@@ -267,6 +286,31 @@
|
|
|
|
|
codeLangDropdown = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── In-note search extension ──
|
|
|
|
|
const NoteSearchExtension = Extension.create({
|
|
|
|
|
name: 'noteSearch',
|
|
|
|
|
addProseMirrorPlugins() {
|
|
|
|
|
return [
|
|
|
|
|
new Plugin({
|
|
|
|
|
key: noteSearchPluginKey,
|
|
|
|
|
state: {
|
|
|
|
|
init() { return DecorationSet.empty; },
|
|
|
|
|
apply(tr, old) {
|
|
|
|
|
const meta = tr.getMeta(noteSearchPluginKey);
|
|
|
|
|
if (meta !== undefined) return meta;
|
|
|
|
|
return old.map(tr.mapping, tr.doc);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
props: {
|
|
|
|
|
decorations(state) {
|
|
|
|
|
return this.getState(state);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
];
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const CodeBlockLanguageSelect = Extension.create({
|
|
|
|
|
name: 'codeBlockLanguageSelect',
|
|
|
|
|
addProseMirrorPlugins() {
|
|
|
|
@@ -334,6 +378,9 @@
|
|
|
|
|
closeSlashMenu();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track whether the user just typed a slash (vs cursor moving into existing text)
|
|
|
|
|
let slashTypedByUser = false;
|
|
|
|
|
|
|
|
|
|
function closeSlashMenu() {
|
|
|
|
|
slashMenu = null;
|
|
|
|
|
slashSelectedIndex = 0;
|
|
|
|
@@ -363,6 +410,13 @@
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only open the menu if the user typed the slash, or the menu is already open
|
|
|
|
|
// This prevents triggering when clicking/arrowing into existing paths like /usr/local/bin
|
|
|
|
|
if (!slashMenu && !slashTypedByUser) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
slashTypedByUser = false;
|
|
|
|
|
|
|
|
|
|
const query = match[2];
|
|
|
|
|
const slashOffset = textBefore.length - match[0].length + (match[1].length); // position of "/"
|
|
|
|
|
const from = resolvedFrom.start() + slashOffset;
|
|
|
|
@@ -389,6 +443,12 @@
|
|
|
|
|
new Plugin({
|
|
|
|
|
key: new PluginKey('slashCommands'),
|
|
|
|
|
props: {
|
|
|
|
|
handleTextInput: (_view, _from, _to, text) => {
|
|
|
|
|
if (text === '/') {
|
|
|
|
|
slashTypedByUser = true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
},
|
|
|
|
|
handleKeyDown: (_view, event) => {
|
|
|
|
|
if (!slashMenu) return false;
|
|
|
|
|
if (slashTablePicker) {
|
|
|
|
@@ -397,7 +457,14 @@
|
|
|
|
|
closeSlashMenu();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (event.key === 'ArrowRight' || event.key === 'Tab') {
|
|
|
|
|
if (event.key === 'Tab') {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (slashTableHover.rows > 0 && slashTableHover.cols > 0) {
|
|
|
|
|
slashInsertTable(slashTableHover.rows, slashTableHover.cols);
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (event.key === 'ArrowRight') {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
slashTableHover = { rows: Math.max(1, slashTableHover.rows), cols: Math.min(10, (slashTableHover.cols || 0) + 1) };
|
|
|
|
|
return true;
|
|
|
|
@@ -436,7 +503,7 @@
|
|
|
|
|
slashSelectedIndex = (slashSelectedIndex - 1 + slashFiltered.length) % Math.max(1, slashFiltered.length);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (event.key === 'Enter') {
|
|
|
|
|
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
|
|
|
if (slashFiltered.length > 0) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
executeSlashCommand(slashSelectedIndex);
|
|
|
|
@@ -656,6 +723,8 @@
|
|
|
|
|
closeWikiLinkMenu();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Refresh titles when the menu first opens so newly created notes are found
|
|
|
|
|
if (!wikiLinkMenu) refreshWikiLinkTitles();
|
|
|
|
|
const query = match[1];
|
|
|
|
|
const bracketOffset = textBefore.length - match[0].length;
|
|
|
|
|
const from = resolvedFrom.start() + bracketOffset;
|
|
|
|
@@ -1002,14 +1071,23 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function prosemirrorToMarkdown(doc: any): string {
|
|
|
|
|
const listTypes = new Set(['bulletList', 'orderedList', 'taskList']);
|
|
|
|
|
const parts: string[] = [];
|
|
|
|
|
let prevType = '';
|
|
|
|
|
let preserveEmptyParas = false;
|
|
|
|
|
doc.forEach((node: any) => {
|
|
|
|
|
// Skip empty paragraphs right after code blocks (TipTap cursor placeholder)
|
|
|
|
|
if (node.type.name === 'paragraph' && node.childCount === 0 && prevType === 'codeBlock') {
|
|
|
|
|
const isEmpty = node.type.name === 'paragraph' && node.childCount === 0;
|
|
|
|
|
// After a list or code block, preserve empty paragraphs as HTML comments
|
|
|
|
|
// so markdown-it doesn't merge adjacent lists or collapse spacing
|
|
|
|
|
if (listTypes.has(prevType) || prevType === 'codeBlock') {
|
|
|
|
|
preserveEmptyParas = true;
|
|
|
|
|
}
|
|
|
|
|
if (isEmpty && preserveEmptyParas) {
|
|
|
|
|
parts.push('<!-- -->');
|
|
|
|
|
prevType = node.type.name;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
preserveEmptyParas = false;
|
|
|
|
|
parts.push(serializeNode(node));
|
|
|
|
|
prevType = node.type.name;
|
|
|
|
|
});
|
|
|
|
@@ -1038,9 +1116,12 @@
|
|
|
|
|
return '```' + lang + '\n' + code + '\n```\n';
|
|
|
|
|
}
|
|
|
|
|
case 'blockquote': {
|
|
|
|
|
const inner: string[] = [];
|
|
|
|
|
node.forEach((child: any) => inner.push(serializeNode(child)));
|
|
|
|
|
return inner.join('').split('\n').filter((l: string) => l !== '').map((l: string) => '> ' + l).join('\n') + '\n';
|
|
|
|
|
const blocks: string[] = [];
|
|
|
|
|
node.forEach((child: any) => {
|
|
|
|
|
const lines = serializeNode(child).replace(/\n$/, '').split('\n');
|
|
|
|
|
blocks.push(lines.map((l: string) => '> ' + l).join('\n'));
|
|
|
|
|
});
|
|
|
|
|
return blocks.join('\n>\n') + '\n';
|
|
|
|
|
}
|
|
|
|
|
case 'bulletList': {
|
|
|
|
|
const items: string[] = [];
|
|
|
|
@@ -1125,7 +1206,15 @@
|
|
|
|
|
case 'underline': text = `<u>${text}</u>`; break;
|
|
|
|
|
case 'subscript': text = `~${text}~`; break;
|
|
|
|
|
case 'superscript': text = `^${text}^`; break;
|
|
|
|
|
case 'highlight': text = `==${text}==`; break;
|
|
|
|
|
case 'highlight': {
|
|
|
|
|
const color = mark.attrs?.color;
|
|
|
|
|
if (color) {
|
|
|
|
|
text = `<mark data-color="${color}">${text}</mark>`;
|
|
|
|
|
} else {
|
|
|
|
|
text = `==${text}==`;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'link': text = `[${text}](${mark.attrs.href})`; break;
|
|
|
|
|
case 'wikiLink': text = `[[${mark.attrs.title || text}]]`; break;
|
|
|
|
|
}
|
|
|
|
@@ -1144,6 +1233,91 @@
|
|
|
|
|
return parts.join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function autofocus(el: HTMLElement) {
|
|
|
|
|
requestAnimationFrame(() => el.focus());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── In-note search functions ──
|
|
|
|
|
let noteSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
|
|
|
|
function updateNoteSearch(query: string) {
|
|
|
|
|
if (noteSearchTimer) clearTimeout(noteSearchTimer);
|
|
|
|
|
if (!editor) return;
|
|
|
|
|
if (!query) {
|
|
|
|
|
noteSearchResults = [];
|
|
|
|
|
noteSearchIndex = 0;
|
|
|
|
|
const tr = editor.state.tr.setMeta(noteSearchPluginKey, DecorationSet.empty);
|
|
|
|
|
editor.view.dispatch(tr);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
noteSearchTimer = setTimeout(() => {
|
|
|
|
|
if (!editor) return;
|
|
|
|
|
const results: {from: number, to: number}[] = [];
|
|
|
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
|
editor.state.doc.descendants((node, pos) => {
|
|
|
|
|
if (!node.isText || !node.text) return;
|
|
|
|
|
const text = node.text.toLowerCase();
|
|
|
|
|
let idx = text.indexOf(lowerQuery);
|
|
|
|
|
while (idx !== -1) {
|
|
|
|
|
results.push({ from: pos + idx, to: pos + idx + query.length });
|
|
|
|
|
idx = text.indexOf(lowerQuery, idx + 1);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
noteSearchResults = results;
|
|
|
|
|
if (noteSearchIndex >= results.length) noteSearchIndex = 0;
|
|
|
|
|
applySearchDecorations();
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applySearchDecorations() {
|
|
|
|
|
if (!editor) return;
|
|
|
|
|
const decorations = noteSearchResults.map((m, i) =>
|
|
|
|
|
Decoration.inline(m.from, m.to, { class: i === noteSearchIndex ? 'note-search-match note-search-active' : 'note-search-match' })
|
|
|
|
|
);
|
|
|
|
|
const decoSet = DecorationSet.create(editor.state.doc, decorations);
|
|
|
|
|
const tr = editor.state.tr.setMeta(noteSearchPluginKey, decoSet);
|
|
|
|
|
editor.view.dispatch(tr);
|
|
|
|
|
scrollToCurrentMatch();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scrollToCurrentMatch() {
|
|
|
|
|
if (!editor || noteSearchResults.length === 0) return;
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
const el = editor?.view.dom.querySelector('.note-search-active');
|
|
|
|
|
if (el) {
|
|
|
|
|
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function noteSearchNext() {
|
|
|
|
|
if (noteSearchResults.length === 0) return;
|
|
|
|
|
noteSearchIndex = (noteSearchIndex + 1) % noteSearchResults.length;
|
|
|
|
|
applySearchDecorations();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function noteSearchPrev() {
|
|
|
|
|
if (noteSearchResults.length === 0) return;
|
|
|
|
|
noteSearchIndex = (noteSearchIndex - 1 + noteSearchResults.length) % noteSearchResults.length;
|
|
|
|
|
applySearchDecorations();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function openNoteSearch() {
|
|
|
|
|
noteSearchOpen = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeNoteSearch() {
|
|
|
|
|
noteSearchOpen = false;
|
|
|
|
|
noteSearchQuery = '';
|
|
|
|
|
noteSearchResults = [];
|
|
|
|
|
noteSearchIndex = 0;
|
|
|
|
|
if (editor) {
|
|
|
|
|
const tr = editor.state.tr.setMeta(noteSearchPluginKey, DecorationSet.empty);
|
|
|
|
|
editor.view.dispatch(tr);
|
|
|
|
|
editor.commands.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stripAssetSrc(src: string): string {
|
|
|
|
|
// blob: URLs are not persistable — they were temporary browser references
|
|
|
|
|
if (src.startsWith('blob:')) return '';
|
|
|
|
@@ -1203,6 +1377,7 @@
|
|
|
|
|
md = md.replace(/<sub>(.*?)<\/sub>/gi, '~$1~');
|
|
|
|
|
md = md.replace(/<sup>(.*?)<\/sup>/gi, '^$1^');
|
|
|
|
|
md = md.replace(/<code>(.*?)<\/code>/gi, '`$1`');
|
|
|
|
|
md = md.replace(/<mark data-color="([^"]*)">(.*?)<\/mark>/gi, '<mark data-color="$1">$2</mark>');
|
|
|
|
|
md = md.replace(/<mark>(.*?)<\/mark>/gi, '==$1==');
|
|
|
|
|
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
|
|
|
|
|
return content
|
|
|
|
@@ -1310,8 +1485,10 @@
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Pre-process: convert task list syntax before markdown-it (it doesn't know TipTap's format)
|
|
|
|
|
src = src.replace(/^- \[x\]\s+(.+)$/gm, '- <tiptask checked="true">$1</tiptask>');
|
|
|
|
|
src = src.replace(/^- \[ \]\s+(.+)$/gm, '- <tiptask checked="false">$1</tiptask>');
|
|
|
|
|
src = src.replace(/^- \[x\][^\S\n]+(.+)$/gm, '- <tiptask checked="true">$1</tiptask>');
|
|
|
|
|
src = src.replace(/^- \[x\][^\S\n]*$/gm, '- <tiptask checked="true"> </tiptask>');
|
|
|
|
|
src = src.replace(/^- \[ \][^\S\n]+(.+)$/gm, '- <tiptask checked="false">$1</tiptask>');
|
|
|
|
|
src = src.replace(/^- \[ \][^\S\n]*$/gm, '- <tiptask checked="false"> </tiptask>');
|
|
|
|
|
|
|
|
|
|
// Run markdown-it (single-pass parser — handles headings, bold, italic, strike, code, blockquote, lists, links, images, hr, tables, raw HTML)
|
|
|
|
|
let html = mdit.render(src);
|
|
|
|
@@ -1320,8 +1497,11 @@
|
|
|
|
|
html = html.replace(/<code([^>]*)>\n?/g, '<code$1>');
|
|
|
|
|
html = html.replace(/\n<\/code>/g, '</code>');
|
|
|
|
|
|
|
|
|
|
// Post-process: convert task list items to TipTap format
|
|
|
|
|
html = html.replace(/<li><tiptask checked="(true|false)">([\s\S]*?)<\/tiptask><\/li>/gi, (_, checked, content) => {
|
|
|
|
|
// Post-process: convert list-separator comments back to empty paragraphs for TipTap
|
|
|
|
|
html = html.replace(/<!-- -->/g, '<p></p>');
|
|
|
|
|
|
|
|
|
|
// Post-process: convert task list items to TipTap format (handle both tight and loose lists — loose lists wrap content in <p> tags)
|
|
|
|
|
html = html.replace(/<li>\s*(?:<p>)?\s*<tiptask checked="(true|false)">([\s\S]*?)<\/tiptask>\s*(?:<\/p>)?\s*<\/li>/gi, (_, checked, content) => {
|
|
|
|
|
return `<li data-type="taskItem" data-checked="${checked}">${content}</li>`;
|
|
|
|
|
});
|
|
|
|
|
html = html.replace(/<ul>\s*(<li data-type="taskItem")/gi, '<ul data-type="taskList">$1');
|
|
|
|
@@ -1353,6 +1533,19 @@
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close formatting dropdowns when clicking outside the formatting bar
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (!anyDropdownOpen) return;
|
|
|
|
|
function onClickAway(e: MouseEvent) {
|
|
|
|
|
const bar = document.querySelector('.editor-formatting-bar');
|
|
|
|
|
if (bar && !bar.contains(e.target as Node)) {
|
|
|
|
|
closeAllDropdowns();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener('mousedown', onClickAway);
|
|
|
|
|
return () => document.removeEventListener('mousedown', onClickAway);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// React to activeNotePath changes from external sources (e.g. search panel)
|
|
|
|
|
$effect(() => {
|
|
|
|
|
const path = $activeNotePath;
|
|
|
|
@@ -1418,6 +1611,7 @@
|
|
|
|
|
DetailsContent,
|
|
|
|
|
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
|
|
|
|
SlashCommands,
|
|
|
|
|
NoteSearchExtension,
|
|
|
|
|
...($appConfig?.enable_wiki_links ? [WikiLink, WikiLinkAutocomplete] : []),
|
|
|
|
|
],
|
|
|
|
|
content: html,
|
|
|
|
@@ -1579,7 +1773,7 @@
|
|
|
|
|
let x = event.clientX;
|
|
|
|
|
let y = event.clientY;
|
|
|
|
|
const menuWidth = 220;
|
|
|
|
|
const menuHeight = 640;
|
|
|
|
|
const menuHeight = 740;
|
|
|
|
|
if (x + menuWidth > window.innerWidth) x = window.innerWidth - menuWidth - 8;
|
|
|
|
|
if (y + menuHeight > window.innerHeight) y = window.innerHeight - menuHeight - 8;
|
|
|
|
|
if (x < 4) x = 4;
|
|
|
|
@@ -2294,6 +2488,16 @@
|
|
|
|
|
{#if readOnly}
|
|
|
|
|
<span class="readonly-indicator">View Mode</span>
|
|
|
|
|
{/if}
|
|
|
|
|
<button
|
|
|
|
|
class="icon-btn"
|
|
|
|
|
class:active={noteSearchOpen}
|
|
|
|
|
onclick={() => noteSearchOpen ? closeNoteSearch() : openNoteSearch()}
|
|
|
|
|
title="Find in note (Ctrl+F)"
|
|
|
|
|
>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="icon-btn"
|
|
|
|
|
class:active={readOnly}
|
|
|
|
@@ -2401,6 +2605,41 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="editor-body-wrapper">
|
|
|
|
|
{#if noteSearchOpen}
|
|
|
|
|
<div class="note-search-bar">
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.5;flex-shrink:0"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
|
|
|
<input
|
|
|
|
|
bind:this={noteSearchInput}
|
|
|
|
|
type="text"
|
|
|
|
|
class="note-search-input"
|
|
|
|
|
placeholder="Find in note..."
|
|
|
|
|
bind:value={noteSearchQuery}
|
|
|
|
|
oninput={() => updateNoteSearch(noteSearchQuery)}
|
|
|
|
|
onkeydown={(e) => {
|
|
|
|
|
if (e.key === 'Enter') { e.preventDefault(); e.shiftKey ? noteSearchPrev() : noteSearchNext(); }
|
|
|
|
|
if (e.key === 'Escape') { e.preventDefault(); closeNoteSearch(); }
|
|
|
|
|
}}
|
|
|
|
|
use:autofocus
|
|
|
|
|
/>
|
|
|
|
|
<span class="note-search-count">
|
|
|
|
|
{#if noteSearchQuery && noteSearchResults.length > 0}
|
|
|
|
|
{noteSearchIndex + 1} / {noteSearchResults.length}
|
|
|
|
|
{:else if noteSearchQuery}
|
|
|
|
|
No results
|
|
|
|
|
{/if}
|
|
|
|
|
</span>
|
|
|
|
|
<button class="note-search-btn" onclick={noteSearchPrev} title="Previous (Shift+Enter)">
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="note-search-btn" onclick={noteSearchNext} title="Next (Enter)">
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="note-search-btn" onclick={closeNoteSearch} title="Close (Esc)">
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
<div class="editor-body-row">
|
|
|
|
|
<div class="editor-body">
|
|
|
|
|
{#if $sourceMode}
|
|
|
|
|
<textarea
|
|
|
|
@@ -2471,6 +2710,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if editorReady && !$sourceMode}
|
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
|
@@ -2478,37 +2718,37 @@
|
|
|
|
|
<!-- Insert (+) dropdown -->
|
|
|
|
|
<div class="fmt-dropdown-wrap">
|
|
|
|
|
<button class="fmt-btn insert-btn" onclick={(e) => { e.stopPropagation(); insertDropdown = !insertDropdown; headingDropdown = false; colorDropdown = false; highlightDropdown = false; tablePickerOpen = false; alignDropdown = false; }} title="Insert">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
{#if insertDropdown}
|
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
|
|
<div class="fmt-dropdown insert-dropdown" onclick={(e) => e.stopPropagation()}>
|
|
|
|
|
<button onclick={() => { insertDropdown = false; document.querySelector<HTMLInputElement>('#insert-image-input')?.click(); }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 00-2.828 0L6 21"/></svg>
|
|
|
|
|
Image
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick={() => { insertDropdown = false; document.querySelector<HTMLInputElement>('#insert-file-input')?.click(); }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 01-2-2V4a2 2 0 012-2h8a2.4 2.4 0 011.704.706l3.588 3.588A2.4 2.4 0 0120 8v12a2 2 0 01-2 2z"/><path d="M14 2v5a1 1 0 001 1h5"/></svg>
|
|
|
|
|
File
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick={() => { insertDropdown = false; tablePickerOpen = true; }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/></svg>
|
|
|
|
|
Table
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick={() => { insertDropdown = false; editor?.chain().focus().setHorizontalRule().run(); }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="12" x2="21" y2="12"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14"/></svg>
|
|
|
|
|
Horizontal Rule
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick={() => { insertDropdown = false; editor?.chain().focus().toggleCodeBlock().run(); }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m10 9-3 3 3 3"/><path d="m14 15 3-3-3-3"/><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
|
|
|
|
|
Code Block
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick={() => { insertDropdown = false; editor?.chain().focus().toggleBlockquote().run(); }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M3 6h4v4l-2 6H3l2-6H3V6zm10 0h4v4l-2 6h-2l2-6h-2V6z"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 5H3"/><path d="M21 12H8"/><path d="M21 19H8"/><path d="M3 12v7"/></svg>
|
|
|
|
|
Quote
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick={() => { insertDropdown = false; editor?.chain().focus().setDetails().run(); }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><polyline points="10 8 14 12 10 16"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="13" height="7" x="8" y="3" rx="1"/><path d="m2 9 3 3-3 3"/><rect width="13" height="7" x="8" y="14" rx="1"/></svg>
|
|
|
|
|
Collapsible Section
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
@@ -2520,7 +2760,7 @@
|
|
|
|
|
<!-- Heading dropdown -->
|
|
|
|
|
<div class="fmt-dropdown-wrap">
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('heading'))} onclick={(e) => { e.stopPropagation(); headingDropdown = !headingDropdown; colorDropdown = false; highlightDropdown = false; tablePickerOpen = false; alignDropdown = false; insertDropdown = false; }} title="Heading">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16M4 6v12M20 6v12"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h12"/><path d="M6 20V4"/><path d="M18 20V4"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
{#if headingDropdown}
|
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
|
@@ -2538,22 +2778,22 @@
|
|
|
|
|
|
|
|
|
|
<!-- Text formatting -->
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('bold'))} onclick={() => editor?.chain().focus().toggleBold().run()} title="Bold (Ctrl+B)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6zm0 8h9a4 4 0 014 4 4 4 0 01-4 4H6z" stroke="currentColor" stroke-width="1" fill="none"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12h9a4 4 0 010 8H7a1 1 0 01-1-1V5a1 1 0 011-1h7a4 4 0 010 8"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('italic'))} onclick={() => editor?.chain().focus().toggleItalic().run()} title="Italic (Ctrl+I)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" x2="10" y1="4" y2="4"/><line x1="14" x2="5" y1="20" y2="20"/><line x1="15" x2="9" y1="4" y2="20"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('underline'))} onclick={() => editor?.chain().focus().toggleUnderline().run()} title="Underline (Ctrl+U)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3v7a6 6 0 006 6 6 6 0 006-6V3"/><line x1="4" y1="21" x2="20" y2="21"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4v6a6 6 0 0012 0V4"/><line x1="4" x2="20" y1="20" y2="20"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('strike'))} onclick={() => editor?.chain().focus().toggleStrike().run()} title="Strikethrough (Ctrl+Shift+X)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4H9a3 3 0 00-3 3 3 3 0 003 3h6"/><line x1="4" y1="12" x2="20" y2="12"/><path d="M8 20h7a3 3 0 003-3 3 3 0 00-3-3H8"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4H9a3 3 0 00-2.83 4"/><path d="M14 12a4 4 0 010 8H6"/><line x1="4" x2="20" y1="12" y2="12"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Text color -->
|
|
|
|
|
<div class="fmt-dropdown-wrap">
|
|
|
|
|
<button class="fmt-btn" onclick={(e) => { e.stopPropagation(); colorDropdown = !colorDropdown; headingDropdown = false; highlightDropdown = false; tablePickerOpen = false; alignDropdown = false; insertDropdown = false; }} title="Text Color">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 2l5 0M12 2l-5 17M17 19H5M15 7l4 12"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16"/><path d="m6 16 6-12 6 12"/><path d="M8 12h8"/></svg>
|
|
|
|
|
<span class="color-indicator" style="background: {editor.getAttributes('textStyle').color || 'var(--accent)'}"></span>
|
|
|
|
|
</button>
|
|
|
|
|
{#if colorDropdown}
|
|
|
|
@@ -2581,49 +2821,49 @@
|
|
|
|
|
|
|
|
|
|
<!-- Lists -->
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('bulletList'))} onclick={() => editor?.chain().focus().toggleBulletList().run()} title="Bullet List (Ctrl+Shift+8)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="3.5" cy="6" r="1.5" fill="currentColor"/><circle cx="3.5" cy="12" r="1.5" fill="currentColor"/><circle cx="3.5" cy="18" r="1.5" fill="currentColor"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h.01"/><path d="M3 12h.01"/><path d="M3 19h.01"/><path d="M8 5h13"/><path d="M8 12h13"/><path d="M8 19h13"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('orderedList'))} onclick={() => editor?.chain().focus().toggleOrderedList().run()} title="Ordered List (Ctrl+Shift+7)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><text x="1" y="8" font-size="8" fill="currentColor" stroke="none" font-weight="600">1</text><text x="1" y="14" font-size="8" fill="currentColor" stroke="none" font-weight="600">2</text><text x="1" y="20" font-size="8" fill="currentColor" stroke="none" font-weight="600">3</text></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5h10"/><path d="M11 12h10"/><path d="M11 19h10"/><path d="M4 4h1v5"/><path d="M4 9h2"/><path d="M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 00-2.6-1.02"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('taskList'))} onclick={() => editor?.chain().focus().toggleTaskList().run()} title="Task List (Ctrl+Shift+9)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5"/><polyline points="4.5 6.5 6 8 8.5 4.5"/><line x1="13" y1="6.5" x2="21" y2="6.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><line x1="13" y1="17.5" x2="21" y2="17.5"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 5h8"/><path d="M13 12h8"/><path d="M13 19h8"/><path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div class="fmt-sep"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Undo / Redo -->
|
|
|
|
|
<button class="fmt-btn" onclick={() => editor?.chain().focus().undo().run()} title="Undo (Ctrl+Z)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 102.13-9.36L1 10"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 015.5 5.5 5.5 5.5 0 01-5.5 5.5H11"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="fmt-btn" onclick={() => editor?.chain().focus().redo().run()} title="Redo (Ctrl+Shift+Z)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.13-9.36L23 10"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 14 5-5-5-5"/><path d="M20 9H9.5A5.5 5.5 0 004 14.5 5.5 5.5 0 009.5 20H13"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div class="fmt-sep"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Code & Code Block -->
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('code'))} onclick={() => editor?.chain().focus().toggleCode().run()} title="Inline Code (Ctrl+E)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('codeBlock'))} onclick={() => editor?.chain().focus().toggleCodeBlock().run()} title="Code Block (Ctrl+Alt+C)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><polyline points="9 8 5 12 9 16"/><polyline points="15 8 19 12 15 16"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m10 9-3 3 3 3"/><path d="m14 15 3-3-3-3"/><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Blockquote -->
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('blockquote'))} onclick={() => editor?.chain().focus().toggleBlockquote().run()} title="Quote (Ctrl+Shift+B)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 6h4v4l-2 6H3l2-6H3V6zm10 0h4v4l-2 6h-2l2-6h-2V6z"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 5H3"/><path d="M21 12H8"/><path d="M21 19H8"/><path d="M3 12v7"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Collapsible Section -->
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('details'))} onclick={() => editor?.chain().focus().setDetails().run()} title="Collapsible Section">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><polyline points="10 8 14 12 10 16"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="13" height="7" x="8" y="3" rx="1"/><path d="m2 9 3 3-3 3"/><rect width="13" height="7" x="8" y="14" rx="1"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Table -->
|
|
|
|
|
<div class="fmt-dropdown-wrap">
|
|
|
|
|
<button class="fmt-btn" onclick={(e) => { e.stopPropagation(); tablePickerOpen = !tablePickerOpen; headingDropdown = false; colorDropdown = false; highlightDropdown = false; alignDropdown = false; insertDropdown = false; }} title="Insert Table">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
{#if tablePickerOpen}
|
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
|
@@ -2650,7 +2890,7 @@
|
|
|
|
|
|
|
|
|
|
<!-- Horizontal Rule -->
|
|
|
|
|
<button class="fmt-btn" onclick={() => editor?.chain().focus().setHorizontalRule().run()} title="Horizontal Rule">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="12" x2="21" y2="12"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div class="fmt-sep"></div>
|
|
|
|
@@ -2658,7 +2898,7 @@
|
|
|
|
|
<!-- Highlight -->
|
|
|
|
|
<div class="fmt-dropdown-wrap">
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('highlight'))} onclick={(e) => { e.stopPropagation(); highlightDropdown = !highlightDropdown; headingDropdown = false; colorDropdown = false; tablePickerOpen = false; alignDropdown = false; insertDropdown = false; }} title="Highlight (Ctrl+Shift+H)">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/><path d="M15 5l3 3"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a2 2 0 01-2.8 0l-5.2-5.2a2 2 0 010-2.8L14 4"/></svg>
|
|
|
|
|
<span class="color-indicator" style="background: {editor.getAttributes('highlight').color || 'var(--accent)'}"></span>
|
|
|
|
|
</button>
|
|
|
|
|
{#if highlightDropdown}
|
|
|
|
@@ -2684,10 +2924,10 @@
|
|
|
|
|
|
|
|
|
|
<!-- Subscript & Superscript -->
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('subscript'))} onclick={() => editor?.chain().focus().toggleSubscript().run()} title="Subscript">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><text x="2" y="14" font-size="14" fill="currentColor" stroke="none" font-weight="600">x</text><text x="14" y="20" font-size="10" fill="currentColor" stroke="none">2</text></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m4 5 8 8"/><path d="m12 5-8 8"/><path d="M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 00-2.62-.44c-.42.24-.74.62-.9 1.07"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="fmt-btn" class:active={(editorState, editor.isActive('superscript'))} onclick={() => editor?.chain().focus().toggleSuperscript().run()} title="Superscript">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><text x="2" y="16" font-size="14" fill="currentColor" stroke="none" font-weight="600">x</text><text x="14" y="10" font-size="10" fill="currentColor" stroke="none">2</text></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m4 19 8-8"/><path d="m12 19-8-8"/><path d="M20 12h-4c0-1.5.442-2 1.5-2.5S20 8.334 20 7.002c0-.472-.17-.93-.484-1.29a2.105 2.105 0 00-2.617-.436c-.42.239-.738.614-.899 1.06"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div class="fmt-sep"></div>
|
|
|
|
@@ -2696,32 +2936,32 @@
|
|
|
|
|
<div class="fmt-dropdown-wrap">
|
|
|
|
|
<button class="fmt-btn" onclick={(e) => { e.stopPropagation(); alignDropdown = !alignDropdown; headingDropdown = false; colorDropdown = false; highlightDropdown = false; tablePickerOpen = false; insertDropdown = false; }} title="Text Alignment">
|
|
|
|
|
{#if (editorState, editor.isActive({ textAlign: 'center' }))}
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="6" y1="10" x2="18" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="6" y1="18" x2="18" y2="18"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 5H3"/><path d="M17 12H7"/><path d="M19 19H5"/></svg>
|
|
|
|
|
{:else if (editorState, editor.isActive({ textAlign: 'right' }))}
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="9" y1="10" x2="21" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="9" y1="18" x2="21" y2="18"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 5H3"/><path d="M21 12H9"/><path d="M21 19H7"/></svg>
|
|
|
|
|
{:else if (editorState, editor.isActive({ textAlign: 'justify' }))}
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h18"/><path d="M3 12h18"/><path d="M3 19h18"/></svg>
|
|
|
|
|
{:else}
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="10" x2="15" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="3" y1="18" x2="15" y2="18"/></svg>
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 5H3"/><path d="M15 12H3"/><path d="M17 19H3"/></svg>
|
|
|
|
|
{/if}
|
|
|
|
|
</button>
|
|
|
|
|
{#if alignDropdown}
|
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
|
|
<div class="fmt-dropdown align-dropdown" onclick={(e) => e.stopPropagation()}>
|
|
|
|
|
<button class:active={(editorState, editor.isActive({ textAlign: 'left' }))} onclick={() => { editor?.chain().focus().setTextAlign('left').run(); alignDropdown = false; }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="10" x2="15" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="3" y1="18" x2="15" y2="18"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 5H3"/><path d="M15 12H3"/><path d="M17 19H3"/></svg>
|
|
|
|
|
Left
|
|
|
|
|
</button>
|
|
|
|
|
<button class:active={(editorState, editor.isActive({ textAlign: 'center' }))} onclick={() => { editor?.chain().focus().setTextAlign('center').run(); alignDropdown = false; }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="6" y1="10" x2="18" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="6" y1="18" x2="18" y2="18"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 5H3"/><path d="M17 12H7"/><path d="M19 19H5"/></svg>
|
|
|
|
|
Center
|
|
|
|
|
</button>
|
|
|
|
|
<button class:active={(editorState, editor.isActive({ textAlign: 'right' }))} onclick={() => { editor?.chain().focus().setTextAlign('right').run(); alignDropdown = false; }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="9" y1="10" x2="21" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="9" y1="18" x2="21" y2="18"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 5H3"/><path d="M21 12H9"/><path d="M21 19H7"/></svg>
|
|
|
|
|
Right
|
|
|
|
|
</button>
|
|
|
|
|
<button class:active={(editorState, editor.isActive({ textAlign: 'justify' }))} onclick={() => { editor?.chain().focus().setTextAlign('justify').run(); alignDropdown = false; }}>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="3" y1="14" x2="21" y2="14"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
|
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h18"/><path d="M3 12h18"/><path d="M3 19h18"/></svg>
|
|
|
|
|
Justify
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
@@ -3184,6 +3424,7 @@
|
|
|
|
|
placeholder="Tell AI what to do with the selected text..."
|
|
|
|
|
bind:value={aiCustomPrompt}
|
|
|
|
|
onkeydown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); runAiAction('custom', aiCustomPrompt); } }}
|
|
|
|
|
use:autofocus
|
|
|
|
|
></textarea>
|
|
|
|
|
<button class="ai-custom-submit" onclick={() => runAiAction('custom', aiCustomPrompt)} disabled={!aiCustomPrompt.trim()}>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" /></svg>
|
|
|
|
@@ -3212,6 +3453,7 @@
|
|
|
|
|
placeholder="Describe the note you want to create..."
|
|
|
|
|
bind:value={aiCustomPrompt}
|
|
|
|
|
onkeydown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); runAiAction('custom', aiCustomPrompt); } }}
|
|
|
|
|
use:autofocus
|
|
|
|
|
></textarea>
|
|
|
|
|
<button class="ai-custom-submit" onclick={() => runAiAction('custom', aiCustomPrompt)} disabled={!aiCustomPrompt.trim()}>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4l-2-2"/><rect x="4" y="8" width="16" height="12" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M9 13v2"/><path d="M15 13v2"/></svg>
|
|
|
|
@@ -3600,9 +3842,66 @@
|
|
|
|
|
.editor-body-wrapper {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.note-search-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.note-search-input {
|
|
|
|
|
flex: 1;
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
outline: none;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
.note-search-input::placeholder {
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
.note-search-count {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
.note-search-btn {
|
|
|
|
|
background: none;
|
|
|
|
|
border: none;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: 2px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.note-search-btn:hover {
|
|
|
|
|
background: var(--bg-hover);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
:global(.note-search-match) {
|
|
|
|
|
background: rgba(255, 200, 0, 0.3);
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
}
|
|
|
|
|
:global(.note-search-active) {
|
|
|
|
|
background: rgba(255, 150, 0, 0.6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.editor-body-row {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.editor-body {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
@@ -4199,9 +4498,11 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.tiptap-wrapper .tiptap mark) {
|
|
|
|
|
padding: 1px 3px;
|
|
|
|
|
padding: 0px 5px 2px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
color: #333 !important;
|
|
|
|
|
box-decoration-break: clone;
|
|
|
|
|
-webkit-box-decoration-break: clone;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.dark .tiptap-wrapper .tiptap mark) {
|
|
|
|
|