feat: ダイアリーパネル追加 — localhost:2626連携・localStorage fallback

This commit is contained in:
posimai 2026-04-03 09:47:33 +09:00
parent af382b8a0d
commit 40dace3ddd
1 changed files with 210 additions and 3 deletions

View File

@ -232,6 +232,87 @@
} }
.md-body a:hover { opacity: .8; } .md-body a:hover { opacity: .8; }
/* ── 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 {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.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; }
@media (max-width: 640px) {
.diary-panel { width: 100%; }
}
/* ── Mobile ── */ /* ── Mobile ── */
@media (max-width: 640px) { @media (max-width: 640px) {
.pane-wrap { grid-template-columns: 1fr; } .pane-wrap { grid-template-columns: 1fr; }
@ -291,9 +372,14 @@
<div class="header-dot" aria-hidden="true"></div> <div class="header-dot" aria-hidden="true"></div>
<span class="header-title">posimai log</span> <span class="header-title">posimai log</span>
</div> </div>
<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"> <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> <i data-lucide="settings" style="width:18px;height:18px;stroke-width:1.5"></i>
</button> </button>
</div>
</header> </header>
<div class="pane-wrap" id="main-content"> <div class="pane-wrap" id="main-content">
@ -328,6 +414,29 @@
</div> </div>
</div> </div>
<!-- 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">
<span class="diary-sync-status" id="diarySyncStatus">---</span>
<span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);" id="diaryCharCount">0 chars</span>
</div>
</aside>
<div id="toast" role="status" aria-live="polite"></div> <div id="toast" role="status" aria-live="polite"></div>
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script> <script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
@ -415,6 +524,104 @@
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {}); navigator.serviceWorker.register('/sw.js').catch(() => {});
} }
// ── Diary panel ──────────────────────────────────────────
const DIARY_URL = 'http://localhost:2626/diary';
const DIARY_LS_KEY = 'posimai-log-diary';
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');
let serverOnline = false;
let saveTimer = null;
function setDiaryStatus(state, text) {
diarySyncStatus.className = 'diary-sync-status ' + state;
diarySyncStatus.textContent = text;
diaryDot.className = 'diary-status-dot ' + (serverOnline ? 'connected' : state === 'pending' ? 'pending' : '');
}
// サーバー接続確認 & 未同期ローカルデータのプッシュ
async function checkServer() {
try {
const res = await fetch(DIARY_URL, { signal: AbortSignal.timeout(800) });
if (!res.ok) throw new Error();
serverOnline = true;
diaryDot.className = 'diary-status-dot connected';
// ローカルに保留中のデータがあればサーバーに送る
const local = localStorage.getItem(DIARY_LS_KEY);
if (local !== null) {
const serverData = await res.json();
if (local !== serverData.content) {
await fetch(DIARY_URL, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ content: local }) });
}
}
} catch {
serverOnline = false;
diaryDot.className = 'diary-status-dot' + (localStorage.getItem(DIARY_LS_KEY) ? ' pending' : '');
}
}
// パネルを開くときにコンテンツをロード
async function openDiary() {
diaryPanel.classList.add('open');
diaryBtn.setAttribute('aria-expanded', 'true');
setDiaryStatus('', 'connecting...');
await checkServer();
try {
if (serverOnline) {
const res = await fetch(DIARY_URL, { signal: AbortSignal.timeout(800) });
const { content } = await res.json();
diaryTextarea.value = content;
localStorage.setItem(DIARY_LS_KEY, content);
setDiaryStatus('saved', 'synced');
} else {
diaryTextarea.value = localStorage.getItem(DIARY_LS_KEY) || '';
setDiaryStatus('pending', 'offline — saved locally');
}
} 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');
}
// 入力のたびに debounce 保存 (1s)
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 () => {
if (serverOnline) {
try {
await fetch(DIARY_URL, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ content }) });
setDiaryStatus('saved', 'saved to diary.md');
} catch {
serverOnline = false;
setDiaryStatus('pending', 'offline — saved locally');
}
} else {
setDiaryStatus('pending', 'offline — saved locally');
}
}, 1000);
});
diaryBtn.addEventListener('click', openDiary);
diaryCloseBtn.addEventListener('click', closeDiary);
// overlay クリックでも閉じる
document.getElementById('overlay').addEventListener('click', closeDiary);
})(); })();
</script> </script>
</body> </body>