init: posimai log — AI駆動開発の軌跡ビューワー
This commit is contained in:
commit
458cc49d19
|
|
@ -0,0 +1,418 @@
|
||||||
|
<!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>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"id": "/posimai-log/",
|
||||||
|
"name": "posimai log",
|
||||||
|
"short_name": "log",
|
||||||
|
"description": "Architect without Code — AI駆動開発の軌跡",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"display_override": ["window-controls-overlay", "standalone"],
|
||||||
|
"background_color": "#0D0D0D",
|
||||||
|
"theme_color": "#0D0D0D",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"categories": ["productivity"],
|
||||||
|
"icons": [
|
||||||
|
{ "src": "/logo.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||||
|
{ "src": "/logo.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "posimai-log",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Architect without Code — AI駆動開発の軌跡",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"deploy": "git push gitea main && git push github main"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Posimai Log SW — stale-while-revalidate for static, network-first for posts
|
||||||
|
const CACHE = 'posimai-log-v1';
|
||||||
|
const STATIC = ['/', '/index.html', '/manifest.json', '/logo.png'];
|
||||||
|
|
||||||
|
self.addEventListener('install', e => {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.open(CACHE).then(c => c.addAll(STATIC))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', e => {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.keys().then(keys =>
|
||||||
|
Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
|
||||||
|
).then(() => self.clients.claim())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', e => {
|
||||||
|
if (e.request.method !== 'GET') return;
|
||||||
|
const url = e.request.url;
|
||||||
|
if (!url.startsWith(self.location.origin)) return;
|
||||||
|
|
||||||
|
// posts/ → network-first (content changes on every deploy)
|
||||||
|
if (url.includes('/posts/')) {
|
||||||
|
e.respondWith(
|
||||||
|
fetch(e.request).catch(() => caches.match(e.request))
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// static assets → stale-while-revalidate
|
||||||
|
e.respondWith(
|
||||||
|
caches.open(CACHE).then(cache =>
|
||||||
|
cache.match(e.request).then(cached => {
|
||||||
|
const network = fetch(e.request).then(res => {
|
||||||
|
if (res.ok && res.type === 'basic') cache.put(e.request, res.clone());
|
||||||
|
return res;
|
||||||
|
}).catch(() => cached);
|
||||||
|
return cached || network;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue