This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user