Dev #2
@@ -26,6 +26,7 @@ import {
|
|||||||
Italic,
|
Italic,
|
||||||
Strikethrough,
|
Strikethrough,
|
||||||
Link,
|
Link,
|
||||||
|
FileText,
|
||||||
Table as TableIcon,
|
Table as TableIcon,
|
||||||
Percent,
|
Percent,
|
||||||
Minus,
|
Minus,
|
||||||
@@ -68,13 +69,17 @@ interface FolderTreeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SlashCommandItem {
|
interface SlashCommandItem {
|
||||||
type: BlockType;
|
type?: BlockType;
|
||||||
|
action?: 'insert_link' | 'insert_note' | 'insert_weblink';
|
||||||
label: string;
|
label: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SLASH_COMMANDS: SlashCommandItem[] = [
|
const SLASH_COMMANDS: SlashCommandItem[] = [
|
||||||
|
{ action: 'insert_link', label: '/link', desc: 'Webseite oder Notiz verlinken', icon: '🔗' },
|
||||||
|
{ action: 'insert_note', label: '/notiz', desc: 'Notiz verlinken [[Notiz]]', icon: '📄' },
|
||||||
|
{ action: 'insert_weblink', label: '/web', desc: 'Externe Webseite verlinken [Name](url)', icon: '🌐' },
|
||||||
{ type: 'todo', label: '/todo', desc: 'Aufgabe mit Checkbox', icon: '☑️' },
|
{ type: 'todo', label: '/todo', desc: 'Aufgabe mit Checkbox', icon: '☑️' },
|
||||||
{ type: 'h1', label: '/h1', desc: 'Große Überschrift', icon: 'H1' },
|
{ type: 'h1', label: '/h1', desc: 'Große Überschrift', icon: 'H1' },
|
||||||
{ type: 'h2', label: '/h2', desc: 'Abschnitts-Überschrift', icon: 'H2' },
|
{ type: 'h2', label: '/h2', desc: 'Abschnitts-Überschrift', icon: 'H2' },
|
||||||
@@ -135,6 +140,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const [slashSelectedIndex, setSlashSelectedIndex] = useState(0);
|
const [slashSelectedIndex, setSlashSelectedIndex] = useState(0);
|
||||||
const slashMenuRef = useRef<HTMLDivElement | null>(null);
|
const slashMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// Link Insertion Modal state (/link, /notiz, /web)
|
||||||
|
const [linkModalBlockId, setLinkModalBlockId] = useState<string | null>(null);
|
||||||
|
const [linkModalTab, setLinkModalTab] = useState<'note' | 'web'>('note');
|
||||||
|
const [linkSearchQuery, setLinkSearchQuery] = useState('');
|
||||||
|
const [linkWebTitle, setLinkWebTitle] = useState('');
|
||||||
|
const [linkWebUrl, setLinkWebUrl] = useState('');
|
||||||
|
|
||||||
// Drag and Drop reordering state for blocks
|
// Drag and Drop reordering state for blocks
|
||||||
const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null);
|
const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -1154,7 +1166,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const selected = filteredSlashCommands[slashSelectedIndex];
|
const selected = filteredSlashCommands[slashSelectedIndex];
|
||||||
if (selected) {
|
if (selected) {
|
||||||
applySlashCommand(block.id, selected.type);
|
applySlashCommand(block.id, selected);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1236,17 +1248,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
}, [slashSelectedIndex, slashBlockId]);
|
}, [slashSelectedIndex, slashBlockId]);
|
||||||
|
|
||||||
const applySlashCommand = (blockId: string, type: BlockType) => {
|
const focusBlock = (blockId: string) => {
|
||||||
if (isReadOnly || !selectedDoc) return;
|
setLastFocusedBlockId(blockId);
|
||||||
const block = selectedDoc.blocks.find((b) => b.id === blockId);
|
const tryFocus = () => {
|
||||||
if (!block) return;
|
|
||||||
const cleanContent = block.content.replace(/\/([a-z0-9]*)$/i, '').trim();
|
|
||||||
updateBlock(blockId, { type, content: cleanContent, checked: type === 'todo' ? false : undefined });
|
|
||||||
setSlashBlockId(null);
|
|
||||||
setSlashSearch('');
|
|
||||||
|
|
||||||
// Re-focus immediately into the block's text field and position cursor at end
|
|
||||||
const focusBlock = () => {
|
|
||||||
const el = blockInputRefs.current[blockId];
|
const el = blockInputRefs.current[blockId];
|
||||||
if (el) {
|
if (el) {
|
||||||
el.focus();
|
el.focus();
|
||||||
@@ -1259,14 +1263,79 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
setLastFocusedBlockId(blockId);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!focusBlock()) {
|
if (!tryFocus()) {
|
||||||
setTimeout(focusBlock, 30);
|
setTimeout(tryFocus, 30);
|
||||||
}
|
}
|
||||||
}, 20);
|
}, 20);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applySlashCommand = (blockId: string, cmd: SlashCommandItem) => {
|
||||||
|
if (isReadOnly || !selectedDoc) return;
|
||||||
|
const block = selectedDoc.blocks.find((b) => b.id === blockId);
|
||||||
|
if (!block) return;
|
||||||
|
const cleanContent = block.content.replace(/\/([a-z0-9]*)$/i, '').trim();
|
||||||
|
|
||||||
|
setSlashBlockId(null);
|
||||||
|
setSlashSearch('');
|
||||||
|
|
||||||
|
if (cmd.action) {
|
||||||
|
updateBlock(blockId, { content: cleanContent });
|
||||||
|
setLinkModalBlockId(blockId);
|
||||||
|
setLinkModalTab(cmd.action === 'insert_weblink' ? 'web' : 'note');
|
||||||
|
setLinkSearchQuery('');
|
||||||
|
setLinkWebTitle('');
|
||||||
|
setLinkWebUrl('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd.type) {
|
||||||
|
updateBlock(blockId, { type: cmd.type, content: cleanContent, checked: cmd.type === 'todo' ? false : undefined });
|
||||||
|
focusBlock(blockId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInsertNoteLink = (docTitle: string) => {
|
||||||
|
if (!linkModalBlockId || !selectedDoc) return;
|
||||||
|
const block = selectedDoc.blocks.find((b) => b.id === linkModalBlockId);
|
||||||
|
if (block) {
|
||||||
|
const space = block.content && !block.content.endsWith(' ') ? ' ' : '';
|
||||||
|
const newContent = `${block.content}${space}[[${docTitle}]] `;
|
||||||
|
updateBlock(linkModalBlockId, { content: newContent });
|
||||||
|
}
|
||||||
|
const targetBlockId = linkModalBlockId;
|
||||||
|
setLinkModalBlockId(null);
|
||||||
|
focusBlock(targetBlockId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInsertWebLink = () => {
|
||||||
|
if (!linkModalBlockId || !selectedDoc) return;
|
||||||
|
const block = selectedDoc.blocks.find((b) => b.id === linkModalBlockId);
|
||||||
|
let title = linkWebTitle.trim();
|
||||||
|
let url = linkWebUrl.trim();
|
||||||
|
if (!url) return;
|
||||||
|
if (!/^https?:\/\//i.test(url)) {
|
||||||
|
url = `https://${url}`;
|
||||||
|
}
|
||||||
|
if (!title) {
|
||||||
|
try {
|
||||||
|
title = new URL(url).hostname;
|
||||||
|
} catch {
|
||||||
|
title = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (block) {
|
||||||
|
const space = block.content && !block.content.endsWith(' ') ? ' ' : '';
|
||||||
|
const newContent = `${block.content}${space}[${title}](${url}) `;
|
||||||
|
updateBlock(linkModalBlockId, { content: newContent });
|
||||||
|
}
|
||||||
|
const targetBlockId = linkModalBlockId;
|
||||||
|
setLinkModalBlockId(null);
|
||||||
|
setLinkWebTitle('');
|
||||||
|
setLinkWebUrl('');
|
||||||
|
focusBlock(targetBlockId);
|
||||||
|
};
|
||||||
|
|
||||||
const getDirectChildTodos = (blocks: DocumentBlock[], parentIndex: number): DocumentBlock[] => {
|
const getDirectChildTodos = (blocks: DocumentBlock[], parentIndex: number): DocumentBlock[] => {
|
||||||
const parent = blocks[parentIndex];
|
const parent = blocks[parentIndex];
|
||||||
if (!parent) return [];
|
if (!parent) return [];
|
||||||
@@ -1423,9 +1492,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to render text with clickable Wiki-Links and Hashtags
|
// Helper to render text with clickable Wiki-Links, Web-Links and Hashtags
|
||||||
const renderFormattedText = (text: string) => {
|
const renderFormattedText = (text: string) => {
|
||||||
const parts = text.split(/(\[\[.*?\]\]|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g);
|
const parts = text.split(/(\[\[.*?\]\]|\[.*?\]\(https?:\/\/[^\s)]+\)|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g);
|
||||||
return parts.map((part, i) => {
|
return parts.map((part, i) => {
|
||||||
if (part.startsWith('[[') && part.endsWith(']]')) {
|
if (part.startsWith('[[') && part.endsWith(']]')) {
|
||||||
const linkTitle = part.slice(2, -2).trim();
|
const linkTitle = part.slice(2, -2).trim();
|
||||||
@@ -1454,6 +1523,26 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const webMatch = part.match(/^\[(.*?)\]\((https?:\/\/[^\s)]+)\)$/);
|
||||||
|
if (webMatch) {
|
||||||
|
const title = webMatch[1] || webMatch[2];
|
||||||
|
const url = webMatch[2];
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={i}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="inline-flex items-center gap-1 px-1.5 py-0.2 mx-0.5 bg-sky-500/10 border border-sky-500/30 text-sky-500 dark:text-sky-400 hover:bg-sky-500/20 text-xs font-medium rounded-btn transition-colors"
|
||||||
|
title={`Öffne: ${url}`}
|
||||||
|
>
|
||||||
|
<span>🌐</span>
|
||||||
|
<span>{title}</span>
|
||||||
|
<ExternalLink className="w-2.5 h-2.5 opacity-60" />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (part.startsWith('#') && part.length > 1) {
|
if (part.startsWith('#') && part.length > 1) {
|
||||||
const tagWord = part.slice(1);
|
const tagWord = part.slice(1);
|
||||||
return (
|
return (
|
||||||
@@ -2548,7 +2637,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(block.content.includes('[[') || block.content.includes('#')) && (
|
{(block.content.includes('[[') || block.content.includes('#') || (block.content.includes('[') && block.content.includes(']('))) && (
|
||||||
<div className="text-xs text-muted flex flex-wrap items-center gap-1 pt-0.5 pl-6">
|
<div className="text-xs text-muted flex flex-wrap items-center gap-1 pt-0.5 pl-6">
|
||||||
{renderFormattedText(block.content)}
|
{renderFormattedText(block.content)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2754,7 +2843,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
isReadOnly ? 'cursor-default' : ''
|
isReadOnly ? 'cursor-default' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{(block.content.includes('[[') || block.content.includes('#')) && (
|
{(block.content.includes('[[') || block.content.includes('#') || (block.content.includes('[') && block.content.includes(']('))) && (
|
||||||
<div className="text-xs text-muted flex flex-wrap items-center gap-1 pt-0.5">
|
<div className="text-xs text-muted flex flex-wrap items-center gap-1 pt-0.5">
|
||||||
{renderFormattedText(block.content)}
|
{renderFormattedText(block.content)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2771,11 +2860,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<div ref={slashMenuRef} className="relative max-h-48 overflow-y-auto space-y-0.5">
|
<div ref={slashMenuRef} className="relative max-h-48 overflow-y-auto space-y-0.5">
|
||||||
{filteredSlashCommands.map((cmd, idx) => (
|
{filteredSlashCommands.map((cmd, idx) => (
|
||||||
<button
|
<button
|
||||||
key={cmd.type}
|
key={cmd.label || idx}
|
||||||
type="button"
|
type="button"
|
||||||
data-slash-index={idx}
|
data-slash-index={idx}
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={() => applySlashCommand(block.id, cmd.type)}
|
onClick={() => applySlashCommand(block.id, cmd)}
|
||||||
className={`w-full text-left px-2 py-1.5 rounded-btn text-xs flex items-center justify-between transition-colors ${
|
className={`w-full text-left px-2 py-1.5 rounded-btn text-xs flex items-center justify-between transition-colors ${
|
||||||
slashSelectedIndex === idx
|
slashSelectedIndex === idx
|
||||||
? 'bg-accent text-on-accent'
|
? 'bg-accent text-on-accent'
|
||||||
@@ -3436,6 +3525,220 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* LINK INSERTION MODAL (/link, /notiz, /web) */}
|
||||||
|
{linkModalBlockId && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"
|
||||||
|
onClick={() => {
|
||||||
|
const blockId = linkModalBlockId;
|
||||||
|
setLinkModalBlockId(null);
|
||||||
|
if (blockId) focusBlock(blockId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-panel border border-border rounded-xl shadow-panel w-full max-w-md overflow-hidden animate-in fade-in zoom-in-95 duration-150"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-1.5 rounded-btn bg-accent/15 text-accent">
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-semibold text-main">Link einfügen</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const blockId = linkModalBlockId;
|
||||||
|
setLinkModalBlockId(null);
|
||||||
|
if (blockId) focusBlock(blockId);
|
||||||
|
}}
|
||||||
|
className="p-1 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Switcher: Notiz vs Webseite */}
|
||||||
|
<div className="flex border-b border-border bg-panel-elevated/40 p-1 gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLinkModalTab('note')}
|
||||||
|
className={`flex-1 py-1.5 px-3 rounded-btn text-xs font-medium flex items-center justify-center gap-1.5 transition-colors cursor-pointer ${
|
||||||
|
linkModalTab === 'note'
|
||||||
|
? 'bg-panel text-main shadow-xs font-semibold'
|
||||||
|
: 'text-muted hover:text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
<span>Notiz verlinken</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLinkModalTab('web')}
|
||||||
|
className={`flex-1 py-1.5 px-3 rounded-btn text-xs font-medium flex items-center justify-center gap-1.5 transition-colors cursor-pointer ${
|
||||||
|
linkModalTab === 'web'
|
||||||
|
? 'bg-panel text-main shadow-xs font-semibold'
|
||||||
|
: 'text-muted hover:text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
<span>Webseite verlinken</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="p-4">
|
||||||
|
{linkModalTab === 'note' ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="w-4 h-4 absolute left-3 top-2.5 text-muted pointer-events-none" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
placeholder="Notiz nach Titel oder Ordner filtern..."
|
||||||
|
value={linkSearchQuery}
|
||||||
|
onChange={(e) => setLinkSearchQuery(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const blockId = linkModalBlockId;
|
||||||
|
setLinkModalBlockId(null);
|
||||||
|
if (blockId) focusBlock(blockId);
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
const filtered = docs.filter(
|
||||||
|
(d) =>
|
||||||
|
d.title.toLowerCase().includes(linkSearchQuery.toLowerCase()) ||
|
||||||
|
(d.folderPath && d.folderPath.toLowerCase().includes(linkSearchQuery.toLowerCase()))
|
||||||
|
);
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
handleInsertNoteLink(filtered[0].title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="input-field w-full pl-9 h-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-60 overflow-y-auto space-y-1 divide-y divide-border/30">
|
||||||
|
{docs
|
||||||
|
.filter(
|
||||||
|
(d) =>
|
||||||
|
!linkSearchQuery.trim() ||
|
||||||
|
d.title.toLowerCase().includes(linkSearchQuery.toLowerCase()) ||
|
||||||
|
(d.folderPath && d.folderPath.toLowerCase().includes(linkSearchQuery.toLowerCase()))
|
||||||
|
)
|
||||||
|
.slice(0, 20)
|
||||||
|
.map((d) => (
|
||||||
|
<button
|
||||||
|
key={d.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleInsertNoteLink(d.title)}
|
||||||
|
className="w-full text-left px-2.5 py-2 rounded-btn text-xs hover:bg-panel-elevated flex items-center justify-between gap-2 transition-colors cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="text-base flex-shrink-0">{d.icon || '📄'}</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="font-medium text-main truncate group-hover:text-accent">
|
||||||
|
{d.title}
|
||||||
|
</div>
|
||||||
|
{d.folderPath && (
|
||||||
|
<div className="text-[10px] text-faint truncate">
|
||||||
|
{d.folderPath}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-faint group-hover:text-accent flex-shrink-0 font-mono">
|
||||||
|
[[{d.title}]]
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{docs.filter(
|
||||||
|
(d) =>
|
||||||
|
!linkSearchQuery.trim() ||
|
||||||
|
d.title.toLowerCase().includes(linkSearchQuery.toLowerCase())
|
||||||
|
).length === 0 && (
|
||||||
|
<div className="py-6 text-center text-xs text-faint">
|
||||||
|
Keine passende Notiz gefunden
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleInsertWebLink();
|
||||||
|
}}
|
||||||
|
className="space-y-3"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[11px] font-medium text-muted">Titel / Name der Verlinkung</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
placeholder="z.B. Wikipedia oder Referenzseite"
|
||||||
|
value={linkWebTitle}
|
||||||
|
onChange={(e) => setLinkWebTitle(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const blockId = linkModalBlockId;
|
||||||
|
setLinkModalBlockId(null);
|
||||||
|
if (blockId) focusBlock(blockId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="input-field w-full h-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[11px] font-medium text-muted">Web-Adresse (URL) *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="https://example.com"
|
||||||
|
value={linkWebUrl}
|
||||||
|
onChange={(e) => setLinkWebUrl(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const blockId = linkModalBlockId;
|
||||||
|
setLinkModalBlockId(null);
|
||||||
|
if (blockId) focusBlock(blockId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="input-field w-full h-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2 flex justify-end gap-2 border-t border-border">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const blockId = linkModalBlockId;
|
||||||
|
setLinkModalBlockId(null);
|
||||||
|
if (blockId) focusBlock(blockId);
|
||||||
|
}}
|
||||||
|
className="btn-action h-8 px-3 text-xs cursor-pointer"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!linkWebUrl.trim()}
|
||||||
|
className="btn-primary h-8 px-4 text-xs font-medium cursor-pointer disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Link einfügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user