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; }
|
.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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue