2026-04-02 15:55:31 +00:00
|
|
|
<!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>
|
2026-04-09 23:09:46 +00:00
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
2026-04-02 15:55:31 +00:00
|
|
|
<link rel="stylesheet" href="https://posimai-ui.vercel.app/v1/base.css">
|
2026-04-09 23:09:46 +00:00
|
|
|
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js" integrity="sha384-tTkFttkBclaU1cloKwOi9xk3pbao3VZxTjLNBt8iFABWDBQibbAbWpVmO28zMuxq" crossorigin="anonymous"></script>
|
2026-04-16 23:12:31 +00:00
|
|
|
<script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" integrity="sha384-QsSpx6a0USazT7nK7w8qXDgpSAPhFsb2XtpoLFQ5+X2yFN6hvCKnwEzN8M5FWaJb" crossorigin="anonymous"></script>
|
2026-04-02 15:55:31 +00:00
|
|
|
|
|
|
|
|
<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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 16:08:38 +00:00
|
|
|
/* hidden 属性が display:flex に上書きされるのを防ぐ */
|
|
|
|
|
[hidden] { display: none !important; }
|
|
|
|
|
|
2026-04-02 15:55:31 +00:00
|
|
|
/* ── 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; }
|
|
|
|
|
|
2026-04-03 00:47:33 +00:00
|
|
|
/* ── Diary panel ── */
|
|
|
|
|
.diary-panel {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0; right: 0;
|
|
|
|
|
width: 380px;
|
|
|
|
|
height: 100dvh;
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
border-left: 1px solid var(--border);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
z-index: 200;
|
|
|
|
|
transform: translateX(100%);
|
|
|
|
|
transition: transform .22s cubic-bezier(.4,0,.2,1);
|
|
|
|
|
}
|
|
|
|
|
.diary-panel.open { transform: translateX(0); }
|
|
|
|
|
|
|
|
|
|
.diary-panel-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 0 16px;
|
|
|
|
|
height: 48px;
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.diary-panel-title {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
letter-spacing: .1em;
|
|
|
|
|
color: var(--text1);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.diary-status-dot {
|
|
|
|
|
width: 7px; height: 7px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: var(--text3);
|
|
|
|
|
transition: background .3s;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.diary-status-dot.connected { background: var(--accent); }
|
|
|
|
|
.diary-status-dot.pending { background: #FCD34D; }
|
|
|
|
|
|
|
|
|
|
.diary-textarea {
|
|
|
|
|
flex: 1;
|
|
|
|
|
width: 100%;
|
|
|
|
|
resize: none;
|
|
|
|
|
border: none;
|
|
|
|
|
outline: none;
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: var(--text1);
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.75;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
.diary-textarea::placeholder { color: var(--text3); }
|
|
|
|
|
|
|
|
|
|
.diary-footer {
|
2026-04-03 06:53:08 +00:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.diary-apikey-row {
|
|
|
|
|
display: none;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
.diary-apikey-row.visible { display: flex; }
|
|
|
|
|
.diary-apikey-input {
|
|
|
|
|
flex: 1;
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
color: var(--text1);
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
.diary-apikey-input:focus { border-color: var(--accent); }
|
|
|
|
|
.diary-footer-main {
|
2026-04-03 00:47:33 +00:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
}
|
|
|
|
|
.diary-sync-status {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
color: var(--text3);
|
|
|
|
|
}
|
|
|
|
|
.diary-sync-status.saved { color: var(--accent); }
|
|
|
|
|
.diary-sync-status.pending { color: #FCD34D; }
|
|
|
|
|
|
2026-04-03 06:53:08 +00:00
|
|
|
.diary-action-btn {
|
|
|
|
|
display: none;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
padding: 3px 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: var(--text3);
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: color .2s, border-color .2s;
|
|
|
|
|
}
|
|
|
|
|
.diary-action-btn:hover { color: var(--text1); border-color: var(--text2); }
|
|
|
|
|
.diary-action-btn.visible { display: inline-flex; }
|
|
|
|
|
|
2026-04-03 00:47:33 +00:00
|
|
|
@media (max-width: 640px) {
|
|
|
|
|
.diary-panel { width: 100%; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 15:55:31 +00:00
|
|
|
/* ── 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>
|
2026-04-03 00:47:33 +00:00
|
|
|
<div style="display:flex;gap:4px;align-items:center;">
|
|
|
|
|
<button class="icon-btn" id="diaryBtn" aria-label="ダイアリー" aria-expanded="false">
|
|
|
|
|
<i data-lucide="pencil-line" style="width:18px;height:18px;stroke-width:1.5"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
2026-04-02 15:55:31 +00:00
|
|
|
</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>
|
|
|
|
|
|
2026-04-03 00:47:33 +00:00
|
|
|
<!-- Diary panel -->
|
|
|
|
|
<aside class="diary-panel" id="diaryPanel" aria-label="ダイアリー">
|
|
|
|
|
<div class="diary-panel-header">
|
|
|
|
|
<div class="diary-panel-title">
|
|
|
|
|
<span class="diary-status-dot" id="diaryDot"></span>
|
|
|
|
|
diary
|
|
|
|
|
</div>
|
|
|
|
|
<button class="icon-btn" id="diaryCloseBtn" aria-label="ダイアリーを閉じる">
|
|
|
|
|
<i data-lucide="x" style="width:18px;height:18px;stroke-width:1.75"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<textarea
|
|
|
|
|
class="diary-textarea"
|
|
|
|
|
id="diaryTextarea"
|
|
|
|
|
placeholder="今日の一言をどうぞ。generate の前に書けば記事に反映されます。"
|
|
|
|
|
spellcheck="false"
|
|
|
|
|
></textarea>
|
|
|
|
|
<div class="diary-footer">
|
2026-04-03 06:53:08 +00:00
|
|
|
<div class="diary-apikey-row" id="diaryApiKeyRow">
|
|
|
|
|
<input type="password" class="diary-apikey-input" id="diaryApiKeyInput" placeholder="posimai API key">
|
|
|
|
|
<button class="diary-action-btn visible" id="diaryApiKeySave" style="display:inline-flex;">set</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="diary-footer-main">
|
|
|
|
|
<span class="diary-sync-status" id="diarySyncStatus">---</span>
|
|
|
|
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
|
|
|
<button class="diary-action-btn" id="diaryReconnectBtn" aria-label="再試行">
|
|
|
|
|
<i data-lucide="refresh-cw" style="width:10px;height:10px;stroke-width:2"></i>
|
|
|
|
|
retry
|
|
|
|
|
</button>
|
|
|
|
|
<span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);" id="diaryCharCount">0 chars</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-03 00:47:33 +00:00
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
2026-04-02 15:55:31 +00:00
|
|
|
<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(() => {});
|
|
|
|
|
}
|
2026-04-03 00:47:33 +00:00
|
|
|
|
|
|
|
|
// ── Diary panel ──────────────────────────────────────────
|
2026-04-03 06:53:08 +00:00
|
|
|
const DIARY_GET_URL = 'https://api.soar-enrich.com/brain/api/site/config/public?user=maita';
|
|
|
|
|
const DIARY_POST_URL = 'https://api.soar-enrich.com/brain/api/site/config/diary_content';
|
|
|
|
|
const DIARY_LS_KEY = 'posimai-log-diary';
|
|
|
|
|
const APIKEY_LS_KEY = 'posimai_api_key';
|
|
|
|
|
|
|
|
|
|
const diaryPanel = document.getElementById('diaryPanel');
|
|
|
|
|
const diaryBtn = document.getElementById('diaryBtn');
|
|
|
|
|
const diaryCloseBtn = document.getElementById('diaryCloseBtn');
|
|
|
|
|
const diaryTextarea = document.getElementById('diaryTextarea');
|
|
|
|
|
const diaryDot = document.getElementById('diaryDot');
|
|
|
|
|
const diarySyncStatus = document.getElementById('diarySyncStatus');
|
|
|
|
|
const diaryCharCount = document.getElementById('diaryCharCount');
|
|
|
|
|
const diaryReconnectBtn = document.getElementById('diaryReconnectBtn');
|
|
|
|
|
const diaryApiKeyRow = document.getElementById('diaryApiKeyRow');
|
|
|
|
|
const diaryApiKeyInput = document.getElementById('diaryApiKeyInput');
|
|
|
|
|
const diaryApiKeySave = document.getElementById('diaryApiKeySave');
|
|
|
|
|
|
2026-04-03 00:47:33 +00:00
|
|
|
let saveTimer = null;
|
|
|
|
|
|
2026-04-03 06:53:08 +00:00
|
|
|
function getApiKey() { return localStorage.getItem(APIKEY_LS_KEY) || ''; }
|
|
|
|
|
|
2026-04-03 00:47:33 +00:00
|
|
|
function setDiaryStatus(state, text) {
|
|
|
|
|
diarySyncStatus.className = 'diary-sync-status ' + state;
|
|
|
|
|
diarySyncStatus.textContent = text;
|
2026-04-03 06:53:08 +00:00
|
|
|
diaryDot.className = 'diary-status-dot' +
|
|
|
|
|
(state === 'saved' ? ' connected' : state === 'pending' ? ' pending' : '');
|
|
|
|
|
diaryReconnectBtn.classList.toggle('visible', state === 'error');
|
|
|
|
|
if (!getApiKey()) diaryApiKeyRow.classList.add('visible');
|
2026-04-03 00:47:33 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 06:53:08 +00:00
|
|
|
async function fetchDiary() {
|
|
|
|
|
const res = await fetch(DIARY_GET_URL, { signal: AbortSignal.timeout(4000) });
|
|
|
|
|
if (!res.ok) throw new Error('fetch failed');
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
return data.config?.diary_content || '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveDiary(content) {
|
|
|
|
|
const key = getApiKey();
|
|
|
|
|
if (!key) throw Object.assign(new Error('no key'), { code: 'nokey' });
|
|
|
|
|
const res = await fetch(DIARY_POST_URL, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + key },
|
|
|
|
|
body: JSON.stringify({ value: content }),
|
|
|
|
|
signal: AbortSignal.timeout(5000)
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error('save failed');
|
2026-04-03 00:47:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openDiary() {
|
|
|
|
|
diaryPanel.classList.add('open');
|
|
|
|
|
diaryBtn.setAttribute('aria-expanded', 'true');
|
2026-04-03 06:53:08 +00:00
|
|
|
diaryApiKeyRow.classList.toggle('visible', !getApiKey());
|
|
|
|
|
setDiaryStatus('', 'loading...');
|
2026-04-03 00:47:33 +00:00
|
|
|
try {
|
2026-04-03 06:53:08 +00:00
|
|
|
const content = await fetchDiary();
|
|
|
|
|
diaryTextarea.value = content;
|
|
|
|
|
localStorage.setItem(DIARY_LS_KEY, content);
|
|
|
|
|
setDiaryStatus('saved', 'synced');
|
2026-04-03 00:47:33 +00:00
|
|
|
} catch {
|
|
|
|
|
diaryTextarea.value = localStorage.getItem(DIARY_LS_KEY) || '';
|
|
|
|
|
setDiaryStatus('pending', 'offline — saved locally');
|
|
|
|
|
}
|
|
|
|
|
diaryCharCount.textContent = diaryTextarea.value.length + ' chars';
|
|
|
|
|
diaryTextarea.focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeDiary() {
|
|
|
|
|
diaryPanel.classList.remove('open');
|
|
|
|
|
diaryBtn.setAttribute('aria-expanded', 'false');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
diaryTextarea.addEventListener('input', () => {
|
|
|
|
|
const content = diaryTextarea.value;
|
|
|
|
|
diaryCharCount.textContent = content.length + ' chars';
|
|
|
|
|
localStorage.setItem(DIARY_LS_KEY, content);
|
|
|
|
|
setDiaryStatus('pending', 'saving...');
|
|
|
|
|
clearTimeout(saveTimer);
|
|
|
|
|
saveTimer = setTimeout(async () => {
|
2026-04-03 06:53:08 +00:00
|
|
|
try {
|
|
|
|
|
await saveDiary(content);
|
|
|
|
|
setDiaryStatus('saved', 'saved');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (e.code === 'nokey') {
|
|
|
|
|
setDiaryStatus('', 'set API key to sync');
|
|
|
|
|
diaryApiKeyRow.classList.add('visible');
|
|
|
|
|
} else {
|
2026-04-03 00:47:33 +00:00
|
|
|
setDiaryStatus('pending', 'offline — saved locally');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 1000);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-03 06:53:08 +00:00
|
|
|
diaryReconnectBtn.addEventListener('click', async () => {
|
|
|
|
|
setDiaryStatus('', 'retrying...');
|
|
|
|
|
try {
|
|
|
|
|
const content = await fetchDiary();
|
|
|
|
|
diaryTextarea.value = content;
|
|
|
|
|
localStorage.setItem(DIARY_LS_KEY, content);
|
|
|
|
|
setDiaryStatus('saved', 'synced');
|
|
|
|
|
diaryCharCount.textContent = content.length + ' chars';
|
|
|
|
|
} catch {
|
|
|
|
|
setDiaryStatus('error', 'still offline');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
diaryApiKeySave.addEventListener('click', async () => {
|
|
|
|
|
const val = diaryApiKeyInput.value.trim();
|
|
|
|
|
if (!val) return;
|
|
|
|
|
localStorage.setItem(APIKEY_LS_KEY, val);
|
|
|
|
|
diaryApiKeyInput.value = '';
|
|
|
|
|
diaryApiKeyRow.classList.remove('visible');
|
|
|
|
|
setDiaryStatus('', 'connecting...');
|
|
|
|
|
try {
|
|
|
|
|
const content = await fetchDiary();
|
|
|
|
|
diaryTextarea.value = content;
|
|
|
|
|
localStorage.setItem(DIARY_LS_KEY, content);
|
|
|
|
|
setDiaryStatus('saved', 'synced');
|
|
|
|
|
diaryCharCount.textContent = content.length + ' chars';
|
|
|
|
|
} catch {
|
|
|
|
|
setDiaryStatus('pending', 'offline — saved locally');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-03 00:47:33 +00:00
|
|
|
diaryBtn.addEventListener('click', openDiary);
|
|
|
|
|
diaryCloseBtn.addEventListener('click', closeDiary);
|
|
|
|
|
document.getElementById('overlay').addEventListener('click', closeDiary);
|
2026-04-02 15:55:31 +00:00
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|