Add fullscreen editor modal and secure external links with target=_blank

This commit is contained in:
2026-04-28 00:19:42 -04:00
parent ddb28cf8cf
commit 088f6f6e4b
3 changed files with 119 additions and 28 deletions

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Archive, Pin, RefreshCw, Sparkles } from 'lucide-react'; import { createPortal } from 'react-dom';
import { Archive, Maximize2, Minimize2, Pin, RefreshCw, Sparkles } from 'lucide-react';
import { researchBridge } from '../../lib/researchBridge'; import { researchBridge } from '../../lib/researchBridge';
import type { import type {
GhostNote, GhostNote,
@@ -84,6 +85,7 @@ export const ResearchInspectorPane: React.FC<ResearchInspectorPaneProps> = ({
const [draftBodyHtml, setDraftBodyHtml] = useState(''); const [draftBodyHtml, setDraftBodyHtml] = useState('');
const [draftEmbeds, setDraftEmbeds] = useState<NoteEmbedInput[]>([]); const [draftEmbeds, setDraftEmbeds] = useState<NoteEmbedInput[]>([]);
const [draftType, setDraftType] = useState<NoteType>('claim'); const [draftType, setDraftType] = useState<NoteType>('claim');
const [isEditorFullscreen, setIsEditorFullscreen] = useState(false);
useEffect(() => { useEffect(() => {
if (!note) { if (!note) {
@@ -97,6 +99,15 @@ export const ResearchInspectorPane: React.FC<ResearchInspectorPaneProps> = ({
setDraftType(note.noteType); setDraftType(note.noteType);
}, [note]); }, [note]);
useEffect(() => {
if (!isEditorFullscreen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setIsEditorFullscreen(false);
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isEditorFullscreen]);
if (!note && !ghost) { if (!note && !ghost) {
return ( return (
<div className="flex h-full flex-col justify-between p-4 sm:p-5"> <div className="flex h-full flex-col justify-between p-4 sm:p-5">
@@ -218,9 +229,20 @@ export const ResearchInspectorPane: React.FC<ResearchInspectorPaneProps> = ({
/> />
</label> </label>
<label className="flex flex-col gap-2"> <label className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-[10px] uppercase tracking-[0.18em] text-term-text-tertiary"> <span className="text-[10px] uppercase tracking-[0.18em] text-term-text-tertiary">
Body Body
</span> </span>
<button
type="button"
onClick={() => setIsEditorFullscreen(true)}
className="inline-flex items-center gap-1 rounded-lg border border-term-border bg-term-surface px-1.5 py-1 text-term-text-tertiary transition-colors hover:border-info/60 hover:text-term-text"
aria-label="Expand editor"
>
<Maximize2 className="h-3 w-3" />
</button>
</div>
{!isEditorFullscreen && (
<ResearchEditor <ResearchEditor
content={draftBodyHtml} content={draftBodyHtml}
embeds={draftEmbeds} embeds={draftEmbeds}
@@ -236,6 +258,7 @@ export const ResearchInspectorPane: React.FC<ResearchInspectorPaneProps> = ({
setDraftEmbeds(embeds); setDraftEmbeds(embeds);
}} }}
/> />
)}
</label> </label>
<label className="flex flex-col gap-2"> <label className="flex flex-col gap-2">
<span className="text-[10px] uppercase tracking-[0.18em] text-term-text-tertiary"> <span className="text-[10px] uppercase tracking-[0.18em] text-term-text-tertiary">
@@ -359,6 +382,8 @@ export const ResearchInspectorPane: React.FC<ResearchInspectorPaneProps> = ({
{source.url ? ( {source.url ? (
<a <a
href={source.url} href={source.url}
target="_blank"
rel="noopener noreferrer"
className="block truncate text-info underline decoration-info/40 underline-offset-2" className="block truncate text-info underline decoration-info/40 underline-offset-2"
> >
{source.url} {source.url}
@@ -397,6 +422,68 @@ export const ResearchInspectorPane: React.FC<ResearchInspectorPaneProps> = ({
</div> </div>
</> </>
) : null} ) : null}
{isEditorFullscreen && note
? createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
onClick={() => setIsEditorFullscreen(false)}
role="dialog"
aria-modal="true"
aria-label="Expanded note editor"
>
<div
className="flex max-h-[90vh] w-full max-w-4xl flex-col overflow-hidden rounded-[24px] border border-term-border bg-term-elevated shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between border-b border-term-border bg-term-surface px-6 py-4">
<div>
<h2 className="text-lg font-semibold text-term-text">Edit Note</h2>
<p className="mt-1 text-xs text-term-text-muted">{draftTitle || 'Untitled'}</p>
</div>
<button
type="button"
onClick={() => setIsEditorFullscreen(false)}
className="inline-flex items-center gap-1.5 rounded-xl border border-term-border bg-term-bg px-2.5 py-2 text-term-text-tertiary transition-colors hover:border-info/60 hover:text-term-text"
aria-label="Collapse editor"
>
<Minimize2 className="h-4 w-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6">
<ResearchEditor
content={draftBodyHtml}
embeds={draftEmbeds}
minRows={18}
className="rounded-2xl"
onPreviewLink={async (url) => {
const result = await researchBridge.previewNoteLink({ url, kind: 'article' });
return result.embed;
}}
onChange={(html, text, embeds) => {
setDraftBodyHtml(html);
setDraftBody(text);
setDraftEmbeds(embeds);
}}
/>
</div>
<div className="flex items-center justify-between border-t border-term-border bg-term-surface px-6 py-4">
<p className="text-[10px] text-term-text-tertiary">
Press <kbd className="rounded border border-term-border bg-term-bg px-1 py-0.5">Esc</kbd> to collapse
</p>
<button
type="button"
onClick={() => setIsEditorFullscreen(false)}
className="rounded border border-info bg-info px-4 py-2 text-xs font-semibold text-term-bg transition-colors hover:brightness-110"
>
Done
</button>
</div>
</div>
</div>,
document.body,
)
: null}
</div> </div>
); );
}; };

View File

@@ -118,6 +118,8 @@ const EmbeddedLinkCard = Node.create({
'a', 'a',
{ {
href: HTMLAttributes.url, href: HTMLAttributes.url,
target: '_blank',
rel: 'noopener noreferrer',
class: 'mt-2 block truncate text-xs text-info underline decoration-info/40 underline-offset-2', class: 'mt-2 block truncate text-xs text-info underline decoration-info/40 underline-offset-2',
}, },
HTMLAttributes.url, HTMLAttributes.url,
@@ -187,7 +189,7 @@ export const ResearchEditor: React.FC<ResearchEditorProps> = ({
Link.configure({ Link.configure({
openOnClick: false, openOnClick: false,
autolink: true, autolink: true,
HTMLAttributes: { class: 'text-info underline decoration-info/40 underline-offset-2' }, HTMLAttributes: { class: 'text-info underline decoration-info/40 underline-offset-2', target: '_blank', rel: 'noopener noreferrer' },
}), }),
EmbeddedLinkCard, EmbeddedLinkCard,
], ],

View File

@@ -51,6 +51,8 @@ export const EvidenceTracePanel: React.FC<{ auditTrail: NoteAuditTrail | null }>
{source.url ? ( {source.url ? (
<a <a
href={source.url} href={source.url}
target="_blank"
rel="noopener noreferrer"
className="mt-2 block truncate text-xs text-info underline decoration-info/40 underline-offset-2" className="mt-2 block truncate text-xs text-info underline decoration-info/40 underline-offset-2"
> >
{source.url} {source.url}