oss-sync 29ccaf1e92
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (dfadcd5f)
2026-06-23 06:38:48 +00:00

173 lines
6.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>