posimai-log/index.html

419 lines
15 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="ja" data-app-id="posimai-log">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<script>
(function () {
var t = localStorage.getItem('posimai-log-theme') || 'system';
var dark = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme:dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme-pref', t);
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="Architect without Code — AI駆動開発の軌跡">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0D0D0D" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#F9FAFB" media="(prefers-color-scheme: light)">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="posimai log">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" href="/logo.png">
<link rel="apple-touch-icon" href="/logo.png">
<title>posimai log</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://posimai-ui.vercel.app/v1/base.css">
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js"></script>
<style>
/* ── Layout ── */
body { overflow: hidden; }
.app-shell {
display: grid;
grid-template-rows: 48px 1fr;
height: 100dvh;
}
.pane-wrap {
display: grid;
grid-template-columns: 260px 1fr;
overflow: hidden;
}
/* ── Sidebar ── */
.sidebar {
border-right: 1px solid var(--border);
overflow-y: auto;
padding: 12px 0;
}
.sidebar-label {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 500;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--text3);
padding: 0 16px 8px;
}
.post-item {
display: block;
padding: 10px 16px;
cursor: pointer;
border-left: 2px solid transparent;
transition: background .12s, border-color .12s;
text-align: left;
width: 100%;
background: none;
border-top: none;
border-right: none;
border-bottom: none;
}
.post-item:hover { background: var(--surface2); }
.post-item.active {
border-left-color: var(--accent);
background: var(--surface2);
}
.post-item-date {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--accent);
margin-bottom: 3px;
}
.post-item-title {
font-size: 13px;
font-weight: 500;
color: var(--text1);
line-height: 1.4;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.sidebar-empty {
padding: 24px 16px;
font-size: 13px;
color: var(--text3);
line-height: 1.6;
}
.sidebar-empty code {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
background: var(--surface2);
padding: 1px 5px;
border-radius: 4px;
color: var(--accent);
}
/* ── Reader ── */
.reader {
overflow-y: auto;
padding: 40px 48px;
}
.reader-empty {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: var(--text3);
}
.reader-empty-label {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
letter-spacing: .06em;
}
.post-meta {
margin-bottom: 32px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.post-meta-date {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--accent);
margin-bottom: 8px;
}
.post-meta-title {
font-size: 22px;
font-weight: 600;
color: var(--text1);
line-height: 1.35;
}
/* ── Markdown content ── */
.md-body { max-width: 680px; }
.md-body h1 { display: none; } /* h1 is shown in post-meta */
.md-body h2 {
font-size: 17px;
font-weight: 600;
color: var(--text1);
margin: 32px 0 12px;
padding-bottom: 6px;
border-bottom: 1px solid var(--border);
}
.md-body h3 {
font-size: 15px;
font-weight: 600;
color: var(--text1);
margin: 24px 0 8px;
}
.md-body p {
font-size: 14px;
line-height: 1.8;
color: var(--text2);
margin-bottom: 16px;
}
.md-body ul, .md-body ol {
font-size: 14px;
line-height: 1.8;
color: var(--text2);
margin-bottom: 16px;
padding-left: 20px;
}
.md-body li { margin-bottom: 4px; }
.md-body strong { color: var(--text1); font-weight: 600; }
.md-body em { color: var(--text2); }
.md-body code {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
background: var(--surface2);
color: var(--accent);
padding: 2px 6px;
border-radius: 4px;
}
.md-body pre {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin-bottom: 16px;
}
.md-body pre code {
background: none;
padding: 0;
color: var(--text2);
font-size: 12px;
}
.md-body blockquote {
border-left: 3px solid var(--accent);
margin: 0 0 16px;
padding: 8px 16px;
background: var(--surface2);
border-radius: 0 6px 6px 0;
}
.md-body blockquote p { margin-bottom: 0; color: var(--text2); }
.md-body hr {
border: none;
border-top: 1px solid var(--border);
margin: 24px 0;
}
.md-body a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 3px;
}
.md-body a:hover { opacity: .8; }
/* ── Mobile ── */
@media (max-width: 640px) {
.pane-wrap { grid-template-columns: 1fr; }
.sidebar { display: block; }
.reader { padding: 24px 20px; }
.reader.hidden-mobile { display: none; }
.sidebar.hidden-mobile { display: none; }
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--accent);
background: none;
border: none;
padding: 0;
margin-bottom: 20px;
cursor: pointer;
}
}
@media (min-width: 641px) {
.back-btn { display: none; }
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link" tabindex="0" style="position:absolute;top:-100%;left:8px;background:var(--accent);color:#0D0D0D;padding:8px 16px;border-radius:8px;font-weight:600;font-size:13px;z-index:10000;text-decoration:none">コンテンツへスキップ</a>
<!-- Settings panel -->
<aside class="settings-panel" id="settingsPanel" role="complementary">
<div class="settings-panel-header">
<span class="settings-panel-title">設定</span>
<button class="icon-btn" id="settingsCloseBtn" aria-label="設定を閉じる">
<i data-lucide="x" style="width:18px;height:18px;stroke-width:1.75"></i>
</button>
</div>
<div class="settings-panel-body">
<div>
<div class="settings-group-label">外観</div>
<div class="settings-item">
<div class="settings-item-label">テーマ</div>
<div class="theme-selector">
<button class="theme-btn" data-theme-val="dark"><i data-lucide="moon" style="width:12px;height:12px;stroke-width:1.75"></i>ダーク</button>
<button class="theme-btn" data-theme-val="light"><i data-lucide="sun" style="width:12px;height:12px;stroke-width:1.75"></i>ライト</button>
<button class="theme-btn" data-theme-val="system"><i data-lucide="monitor" style="width:12px;height:12px;stroke-width:1.75"></i>自動</button>
</div>
</div>
</div>
</div>
</aside>
<div class="overlay" id="overlay" aria-hidden="true"></div>
<div class="app-shell">
<header class="header">
<div class="header-brand">
<div class="header-dot" aria-hidden="true"></div>
<span class="header-title">posimai log</span>
</div>
<button class="icon-btn" id="settingsBtn" aria-label="設定" aria-expanded="false">
<i data-lucide="settings" style="width:18px;height:18px;stroke-width:1.5"></i>
</button>
</header>
<div class="pane-wrap" id="main-content">
<!-- Sidebar: post list -->
<nav class="sidebar" id="sidebar" aria-label="記事一覧">
<div class="sidebar-label">log entries</div>
<div id="postList">
<div class="sidebar-empty">
ログを読み込み中...
</div>
</div>
</nav>
<!-- Main: reader -->
<main class="reader" id="reader">
<div class="reader-empty" id="readerEmpty">
<i data-lucide="terminal" style="width:32px;height:32px;stroke-width:1.25;opacity:.4"></i>
<span class="reader-empty-label">select a log entry</span>
</div>
<article id="readerContent" hidden>
<button class="back-btn" id="backBtn" aria-label="一覧に戻る">
<i data-lucide="arrow-left" style="width:14px;height:14px;stroke-width:2"></i>
一覧に戻る
</button>
<div class="post-meta">
<div class="post-meta-date" id="postDate"></div>
<div class="post-meta-title" id="postTitle"></div>
</div>
<div class="md-body" id="postBody"></div>
</article>
</main>
</div>
</div>
<div id="toast" role="status" aria-live="polite"></div>
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
<script>
(function () {
const postList = document.getElementById('postList');
const readerEmpty = document.getElementById('readerEmpty');
const readerContent = document.getElementById('readerContent');
const postDate = document.getElementById('postDate');
const postTitle = document.getElementById('postTitle');
const postBody = document.getElementById('postBody');
const sidebar = document.getElementById('sidebar');
const reader = document.getElementById('reader');
const backBtn = document.getElementById('backBtn');
let posts = [];
// --- Fetch index ---
fetch('/posts/index.json')
.then(r => r.ok ? r.json() : [])
.then(data => {
posts = data;
renderList(posts);
})
.catch(() => {
postList.innerHTML = '<div class="sidebar-empty">記事がまだありません。<br><code>npm run generate</code> で生成してください。</div>';
});
function renderList(items) {
if (!items.length) {
postList.innerHTML = '<div class="sidebar-empty">記事がまだありません。<br><code>npm run generate</code> で生成後、<code>posts/</code> に配置してください。</div>';
return;
}
postList.innerHTML = '';
items.forEach(p => {
const btn = document.createElement('button');
btn.className = 'post-item';
btn.dataset.slug = p.slug;
btn.innerHTML = `<div class="post-item-date">${p.date}</div><div class="post-item-title">${p.title}</div>`;
btn.addEventListener('click', () => loadPost(p));
postList.appendChild(btn);
});
}
function loadPost(p) {
// Update active state
document.querySelectorAll('.post-item').forEach(el => el.classList.toggle('active', el.dataset.slug === p.slug));
// Mobile: hide sidebar, show reader
sidebar.classList.add('hidden-mobile');
reader.classList.remove('hidden-mobile');
// Show loading state
readerEmpty.hidden = true;
readerContent.hidden = false;
postDate.textContent = p.date;
postTitle.textContent = p.title || '';
postBody.innerHTML = '<p style="color:var(--text3);font-size:13px;">読み込み中...</p>';
fetch(`/posts/${p.slug}.md`)
.then(r => r.ok ? r.text() : Promise.reject('not found'))
.then(md => {
// Extract title from first h1 if present, use it to update postTitle
const h1Match = md.match(/^#\s+(.+)$/m);
if (h1Match) postTitle.textContent = h1Match[1];
postBody.innerHTML = marked.parse(md);
// Security: ensure external links are safe
postBody.querySelectorAll('a[href^="http"]').forEach(a => {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
});
})
.catch(() => {
postBody.innerHTML = '<p style="color:var(--text3);font-size:13px;">記事の読み込みに失敗しました。</p>';
});
}
// Back button (mobile)
backBtn.addEventListener('click', () => {
sidebar.classList.remove('hidden-mobile');
reader.classList.add('hidden-mobile');
});
// Register SW
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {});
}
})();
</script>
</body>
</html>