173 lines
6.6 KiB
HTML
173 lines
6.6 KiB
HTML
<!doctype html>
|
||
<html lang="ja">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>ファイル・ノート</title>
|
||
<style>
|
||
/* 自己完結: 外部 CSS/フォント禁止(CSP で遮断される)。インラインのみ。 */
|
||
:root { color-scheme: light dark; }
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0; padding: 16px;
|
||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||
line-height: 1.5; color: #1e293b; background: #f8fafc;
|
||
}
|
||
h1 { font-size: 16px; margin: 0 0 12px; }
|
||
.row { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
|
||
.panel { flex: 1 1 280px; min-width: 240px; }
|
||
.card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; }
|
||
ul { list-style: none; margin: 0; padding: 0; max-height: 220px; overflow: auto; }
|
||
li { margin: 0; }
|
||
.file-btn {
|
||
width: 100%; text-align: left; border: 0; background: transparent;
|
||
padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 13px; color: #334155;
|
||
}
|
||
.file-btn:hover { background: #f1f5f9; }
|
||
.file-btn[aria-current="true"] { background: #e0e7ff; font-weight: 600; }
|
||
pre {
|
||
margin: 8px 0 0; padding: 8px; background: #f1f5f9; border-radius: 6px;
|
||
font-size: 12px; max-height: 200px; overflow: auto; white-space: pre-wrap; word-break: break-word;
|
||
}
|
||
textarea {
|
||
width: 100%; min-height: 120px; padding: 8px; border: 1px solid #cbd5e1;
|
||
border-radius: 6px; font-family: inherit; font-size: 13px; resize: vertical;
|
||
}
|
||
button.primary {
|
||
margin-top: 8px; border: 0; background: #4f46e5; color: #fff; font-weight: 700;
|
||
padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 13px;
|
||
}
|
||
button.primary:disabled { opacity: .5; cursor: default; }
|
||
.muted { color: #64748b; font-size: 12px; }
|
||
.status { margin-top: 8px; font-size: 12px; min-height: 1.2em; }
|
||
.status.ok { color: #047857; }
|
||
.status.err { color: #dc2626; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>ファイル・ノート</h1>
|
||
<p class="muted">このワークスペースの <code>output/</code> を一覧・閲覧し、メモを <code>output/note.md</code> に保存します。</p>
|
||
|
||
<div class="row">
|
||
<div class="panel">
|
||
<div class="card">
|
||
<strong style="font-size:13px;">output/ のファイル</strong>
|
||
<ul id="files"><li class="muted" style="padding:6px 8px;">読み込み中…</li></ul>
|
||
</div>
|
||
</div>
|
||
<div class="panel">
|
||
<div class="card">
|
||
<strong style="font-size:13px;">プレビュー</strong>
|
||
<pre id="preview" class="muted">左の一覧からファイルを選んでください。</pre>
|
||
</div>
|
||
<div class="card" style="margin-top:12px;">
|
||
<strong style="font-size:13px;">メモ(output/note.md に保存)</strong>
|
||
<textarea id="note" placeholder="ここにメモを書いて「保存」を押すと output/note.md に保存されます。"></textarea>
|
||
<button id="save" class="primary">保存</button>
|
||
<div id="status" class="status"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ── postMessage ブリッジ(自己完結 / 外部ネットワーク無し)─────────────────
|
||
// 親 (AppRunner) が、ユーザーのセッションでファイル I/O を代理する。
|
||
// 詳細仕様: docs/workspace-apps-bridge.md
|
||
let seq = 0;
|
||
const pending = new Map();
|
||
function call(req) {
|
||
return new Promise((resolve, reject) => {
|
||
const id = ++seq;
|
||
pending.set(id, { resolve, reject });
|
||
window.parent.postMessage({ ...req, id }, '*');
|
||
});
|
||
}
|
||
window.addEventListener('message', (e) => {
|
||
const r = e.data;
|
||
const p = r && pending.get(r.id);
|
||
if (!p) return;
|
||
pending.delete(r.id);
|
||
r.ok ? p.resolve(r.data) : p.reject(new Error(r.error || 'request failed'));
|
||
});
|
||
|
||
const $files = document.getElementById('files');
|
||
const $preview = document.getElementById('preview');
|
||
const $note = document.getElementById('note');
|
||
const $save = document.getElementById('save');
|
||
const $status = document.getElementById('status');
|
||
|
||
function setStatus(msg, kind) {
|
||
$status.textContent = msg;
|
||
$status.className = 'status' + (kind ? ' ' + kind : '');
|
||
}
|
||
|
||
async function loadFiles() {
|
||
try {
|
||
const { entries } = await call({ type: 'listFiles', dir: 'output' });
|
||
const files = (entries || []).filter((e) => e.kind !== 'directory');
|
||
$files.innerHTML = '';
|
||
if (files.length === 0) {
|
||
$files.innerHTML = '<li class="muted" style="padding:6px 8px;">output/ は空です。</li>';
|
||
return;
|
||
}
|
||
for (const f of files) {
|
||
const li = document.createElement('li');
|
||
const btn = document.createElement('button');
|
||
btn.className = 'file-btn';
|
||
btn.textContent = f.name;
|
||
btn.onclick = () => selectFile(f, btn);
|
||
li.appendChild(btn);
|
||
$files.appendChild(li);
|
||
}
|
||
} catch (err) {
|
||
// output/ が無い等。空表示にして致命化しない。
|
||
$files.innerHTML = '<li class="muted" style="padding:6px 8px;">output/ がまだありません。</li>';
|
||
}
|
||
}
|
||
|
||
async function selectFile(file, btn) {
|
||
for (const b of $files.querySelectorAll('.file-btn')) b.removeAttribute('aria-current');
|
||
btn.setAttribute('aria-current', 'true');
|
||
$preview.className = 'muted';
|
||
$preview.textContent = '読み込み中…';
|
||
try {
|
||
const { content } = await call({ type: 'readFile', path: 'output/' + file.name });
|
||
$preview.className = '';
|
||
$preview.textContent = content.length > 4000 ? content.slice(0, 4000) + '\n…(省略)' : content;
|
||
} catch (err) {
|
||
$preview.className = 'muted';
|
||
$preview.textContent = '読み込めませんでした(テキストファイルのみ対応)。';
|
||
}
|
||
}
|
||
|
||
// 既存の note.md があれば編集欄に読み込む(無ければ空のまま)。
|
||
async function loadExistingNote() {
|
||
try {
|
||
const { content } = await call({ type: 'readFile', path: 'output/note.md' });
|
||
$note.value = content;
|
||
} catch {
|
||
/* 無ければ何もしない */
|
||
}
|
||
}
|
||
|
||
$save.onclick = async () => {
|
||
$save.disabled = true;
|
||
setStatus('保存中…');
|
||
try {
|
||
// output/ への書き込みは確認不要(親のサイレント許可ポリシー)。
|
||
await call({ type: 'writeFile', path: 'output/note.md', content: $note.value });
|
||
setStatus('output/note.md に保存しました。', 'ok');
|
||
await loadFiles();
|
||
} catch (err) {
|
||
setStatus('保存に失敗しました: ' + err.message, 'err');
|
||
} finally {
|
||
$save.disabled = false;
|
||
}
|
||
};
|
||
|
||
loadFiles();
|
||
loadExistingNote();
|
||
</script>
|
||
</body>
|
||
</html>
|