feat: ダイアリーパネル追加 — localhost:2626連携・localStorage fallback
This commit is contained in:
parent
af382b8a0d
commit
40dace3ddd
207
index.html
207
index.html
|
|
@ -232,6 +232,87 @@
|
|||
}
|
||||
.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 ── */
|
||||
@media (max-width: 640px) {
|
||||
.pane-wrap { grid-template-columns: 1fr; }
|
||||
|
|
@ -291,9 +372,14 @@
|
|||
<div class="header-dot" aria-hidden="true"></div>
|
||||
<span class="header-title">posimai log</span>
|
||||
</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">
|
||||
<i data-lucide="settings" style="width:18px;height:18px;stroke-width:1.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="pane-wrap" id="main-content">
|
||||
|
|
@ -328,6 +414,29 @@
|
|||
</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>
|
||||
|
||||
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
|
||||
|
|
@ -415,6 +524,104 @@
|
|||
if ('serviceWorker' in navigator) {
|
||||
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>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Reference in New Issue