This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '../../i18n';
|
||||
import { Marked, Renderer } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import mermaid from 'mermaid';
|
||||
@@ -229,6 +231,7 @@ interface MarkdownPreviewProps {
|
||||
}
|
||||
|
||||
export function MarkdownPreview({ content, imageBaseUrl, taskId, showOutline = false }: MarkdownPreviewProps): JSX.Element {
|
||||
const { t } = useTranslation('files');
|
||||
const truncated = content.slice(0, 100000);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [activeSlug, setActiveSlug] = useState<string>('');
|
||||
@@ -346,7 +349,7 @@ export function MarkdownPreview({ content, imageBaseUrl, taskId, showOutline = f
|
||||
return (
|
||||
<div className="flex gap-4 items-start">
|
||||
<aside className="mdxg-outline hidden md:block flex-shrink-0 sticky top-0 max-h-[68vh] overflow-y-auto pr-2 border-r border-hairline" style={{ width: '200px' }}>
|
||||
<div className="text-2xs font-semibold text-slate-500 uppercase tracking-wide px-2 py-1.5">目次</div>
|
||||
<div className="text-2xs font-semibold text-slate-500 uppercase tracking-wide px-2 py-1.5">{t('toc')}</div>
|
||||
<nav>
|
||||
{outline.map(h => (
|
||||
<a
|
||||
@@ -585,7 +588,7 @@ function renderJsonl(content: string): JSX.Element {
|
||||
}
|
||||
|
||||
if (records.length === 0) {
|
||||
return <p className="text-slate-400 text-sm">表示できるレコードがありません</p>;
|
||||
return <p className="text-slate-400 text-sm">{i18n.t('files:records.empty')}</p>;
|
||||
}
|
||||
|
||||
const columns = [...new Set(records.flatMap(r => Object.keys(r)))];
|
||||
@@ -620,6 +623,7 @@ function renderJsonl(content: string): JSX.Element {
|
||||
|
||||
// --- FilePreview ---
|
||||
export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onClose, taskId, section, filePath, editable }: FilePreviewProps) {
|
||||
const { t } = useTranslation('files');
|
||||
const [mode, setMode] = useState<'view' | 'edit'>('view');
|
||||
const [editContent, setEditContent] = useState(content);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -639,14 +643,14 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
|
||||
const html = await buildPrintHtml(currentContent, { title: name, imageBaseUrl: markdownImageBaseUrl });
|
||||
const win = window.open('', '_blank');
|
||||
if (!win) {
|
||||
setError('印刷ウィンドウを開けませんでした。ポップアップブロックを解除してください。');
|
||||
setError(t('print.popupBlocked'));
|
||||
return;
|
||||
}
|
||||
win.document.open();
|
||||
win.document.write(html);
|
||||
win.document.close();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '印刷の準備に失敗しました');
|
||||
setError(err instanceof Error ? err.message : t('print.prepFailed'));
|
||||
} finally {
|
||||
setPrinting(false);
|
||||
}
|
||||
@@ -682,14 +686,14 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
|
||||
onClick={() => { setMode('view'); setError(''); }}
|
||||
className="px-3 h-8 text-xs rounded-md border border-hairline bg-canvas text-slate-700 hover:bg-surface transition-colors"
|
||||
>
|
||||
キャンセル
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-3 h-8 text-xs font-semibold rounded-md bg-accent text-accent-fg hover:bg-accent-deep disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
{saving ? t('saving') : t('save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -702,7 +706,12 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
|
||||
return (
|
||||
<iframe
|
||||
src={imageSrc}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
// No allow-same-origin / allow-scripts: workspace HTML is
|
||||
// untrusted (agent- or web-sourced). The pair would let embedded
|
||||
// scripts run on our origin and ride the viewer's session. Render
|
||||
// the markup in an opaque, scriptless sandbox; the raw endpoint
|
||||
// also sends `Content-Security-Policy: sandbox` as a second layer.
|
||||
sandbox=""
|
||||
className="w-full rounded-lg border-0"
|
||||
style={{ height: '80vh' }}
|
||||
title={name}
|
||||
@@ -751,13 +760,13 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
disabled={printing}
|
||||
title="ブラウザの印刷ダイアログから PDF として保存または印刷"
|
||||
title={t('print.tooltip')}
|
||||
className="inline-flex items-center gap-1 px-2.5 h-7 text-2xs font-medium rounded-md border border-hairline bg-canvas text-slate-700 hover:bg-surface disabled:opacity-50 transition-colors"
|
||||
>
|
||||
<svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M4 5V2h8v3M4 11H2.5A1.5 1.5 0 0 1 1 9.5v-3A1.5 1.5 0 0 1 2.5 5h11A1.5 1.5 0 0 1 15 6.5v3a1.5 1.5 0 0 1-1.5 1.5H12M4 9.5h8v4.5H4z" />
|
||||
</svg>
|
||||
{printing ? '準備中...' : 'PDF / 印刷'}
|
||||
{printing ? t('print.preparing') : t('print.button')}
|
||||
</button>
|
||||
)}
|
||||
{canEdit && mode === 'view' && (
|
||||
@@ -768,13 +777,13 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
|
||||
<svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M11.5 2.5l2 2L5 13l-2.5.5L3 11l8.5-8.5z" />
|
||||
</svg>
|
||||
編集
|
||||
{t('edit')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="inline-flex items-center justify-center w-7 h-7 rounded-md text-slate-400 hover:text-slate-700 hover:bg-surface-2 transition-colors"
|
||||
aria-label="プレビューを閉じる"
|
||||
aria-label={t('close')}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round">
|
||||
<path d="M4 4l8 8M12 4l-8 8" />
|
||||
|
||||
Reference in New Issue
Block a user