posimai-root/posimai-dev/index.html

650 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ja" data-app-id="posimai-dev">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<script>
(function () {
var t = localStorage.getItem('posimai-dev-theme') || 'dark';
document.documentElement.setAttribute('data-theme', t === 'light' ? 'light' : 'dark');
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="posimai development portal">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0C1221">
<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-dev">
<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-dev</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&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://posimai-ui.vercel.app/v1/base.css">
<link rel="stylesheet" href="https://unpkg.com/@xterm/xterm@6.0.0/css/xterm.css" integrity="sha384-n2n7twoohnW+d3myBKaUgl7DSiwidw6MkQy9oesGzkPpMjejKRR3XlnD+5yCdtBD" crossorigin="anonymous">
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js" integrity="sha384-tTkFttkBclaU1cloKwOi9xk3pbao3VZxTjLNBt8iFABWDBQibbAbWpVmO28zMuxq" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@xterm/xterm@6.0.0/lib/xterm.js" integrity="sha384-f/1U6Z9wM4D71a5eRXEZnyOTMOvjqxr2XLwh+Go1OvIl3L3tOcvUrzudnhbECwl4" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@xterm/addon-fit@0.11.0/lib/addon-fit.js" integrity="sha384-txoiwu4RR2GD3qySbaj+BbzibkLbSJRcfqGYMu6z1EqHil4A2dyBiBW5dlacG6OR" crossorigin="anonymous"></script>
<style>
:root {
--accent: #A78BFA;
--accent-dim: rgba(167, 139, 250, 0.15);
--dev-bg: #0C1221;
}
[data-theme="light"] { --accent: #7C3AED; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
display: flex;
flex-direction: column;
height: 100dvh;
overflow: hidden;
background: var(--dev-bg);
font-family: Inter, sans-serif;
}
/* ── Header ── */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 48px;
border-bottom: 1px solid rgba(255,255,255,0.06);
flex-shrink: 0;
background: rgba(12, 18, 33, 0.8);
backdrop-filter: blur(12px);
}
.header-left { display: flex; align-items: center; gap: 10px; }
.header-dot {
width: 8px; height: 8px; border-radius: 50%;
background: #4B5563;
transition: background 0.4s;
flex-shrink: 0;
}
.header-dot.connected {
background: #6EE7B7;
animation: dot-pulse 2.5s ease-in-out infinite;
}
.header-dot.disconnected { background: #F87171; animation: none; }
@keyframes dot-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(110,231,183,0.5); }
50% { box-shadow: 0 0 0 5px rgba(110,231,183,0); }
}
.header-title { font-size: 14px; font-weight: 600; color: #F3F4F6; letter-spacing: -0.01em; }
.header-right { display: flex; align-items: center; gap: 4px; }
/* status-badge は dot に統合したため非表示 */
.status-badge { display: none; }
/* Claude 開始ボタン */
.claude-btn {
width: 36px; height: 36px; border-radius: 8px; border: none; cursor: pointer;
background: var(--accent-dim); color: var(--accent);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; transition: background 0.15s;
}
.claude-btn:hover { background: rgba(167,139,250,0.25); }
/* icon-btn */
.icon-btn { color: rgba(255,255,255,0.5); }
.icon-btn:hover { color: #F3F4F6; background: rgba(255,255,255,0.06); }
/* ── Terminal ── */
#terminal-container {
flex: 1;
overflow: hidden;
padding: 12px 12px 0;
min-height: 0;
background:
radial-gradient(ellipse at 15% 60%, rgba(34,211,238,0.14) 0%, transparent 55%),
radial-gradient(ellipse at 85% 25%, rgba(167,139,250,0.14) 0%, transparent 55%),
var(--dev-bg);
}
#terminal-container .xterm { height: 100%; }
#terminal-container .xterm-viewport { border-radius: 8px 8px 0 0; }
/* xterm 内部の不透明レイヤーを透明化 → aurora が透ける */
#terminal-container .xterm-viewport,
#terminal-container .xterm-screen,
#terminal-container .xterm-screen canvas { background: transparent !important; }
/* スクロールバー — 右端に固定、細く鮮明に */
.xterm-viewport { scrollbar-width: thin; scrollbar-color: rgba(167,139,250,0.35) transparent; }
.xterm-viewport::-webkit-scrollbar { width: 5px; }
.xterm-viewport::-webkit-scrollbar-track { background: rgba(255,255,255,0.03); border-radius: 3px; }
.xterm-viewport::-webkit-scrollbar-thumb { background: rgba(167,139,250,0.4); border-radius: 3px; }
.xterm-viewport::-webkit-scrollbar-thumb:hover { background: rgba(167,139,250,0.7); }
/* ── Input wrapper (relative anchor for slash popup) ── */
.input-wrapper {
position: relative;
flex-shrink: 0;
background: rgba(12, 18, 33, 0.9);
border-top: 1px solid rgba(255,255,255,0.06);
}
/* ── Quick chips ── */
.chips-bar {
display: flex;
align-items: center;
padding: 8px 12px 4px;
gap: 6px;
overflow-x: auto;
scrollbar-width: none;
}
.chips-bar::-webkit-scrollbar { display: none; }
.chip {
display: flex; align-items: center; gap: 5px;
padding: 4px 10px; border-radius: 20px;
border: 1px solid rgba(255,255,255,0.07);
background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.45);
font-size: 11px; font-weight: 500; font-family: Inter, sans-serif;
cursor: pointer; white-space: nowrap; flex-shrink: 0;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.chip:hover { background: var(--accent-dim); color: var(--accent); border-color: rgba(167,139,250,0.2); }
/* ── Slash command popup ── */
.slash-popup {
position: absolute;
bottom: 100%;
left: 12px; right: 12px;
background: rgba(10, 14, 28, 0.98);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 -8px 32px rgba(0,0,0,0.5);
display: none;
z-index: 100;
}
.slash-popup.open { display: block; }
.slash-item {
display: flex; align-items: center; gap: 12px;
padding: 10px 14px; cursor: pointer;
transition: background 0.1s;
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.slash-item:last-child { border-bottom: none; }
.slash-item:hover, .slash-item.active { background: rgba(167,139,250,0.1); }
.slash-cmd {
font-family: monospace; font-size: 12px; font-weight: 600;
color: var(--accent); min-width: 82px;
}
.slash-label { font-size: 12px; color: rgba(255,255,255,0.35); }
/* ── Chat bar ── */
.chat-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
padding-bottom: max(10px, env(safe-area-inset-bottom));
}
.chat-input {
flex: 1;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
padding: 9px 14px;
color: #F3F4F6;
font-size: 14px;
font-family: Inter, sans-serif;
outline: none;
transition: border-color 0.15s;
}
.chat-input:focus { border-color: rgba(167,139,250,0.4); }
.mic-btn {
width: 38px; height: 38px; border-radius: 10px; border: none;
background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.35); cursor: pointer;
display: none; align-items: center; justify-content: center;
flex-shrink: 0; transition: background 0.15s, color 0.15s;
}
.mic-btn.available { display: flex; }
.mic-btn:hover { background: rgba(255,255,255,0.1); color: #F3F4F6; }
.mic-btn.listening {
background: rgba(239,68,68,0.15); color: #F87171;
animation: mic-pulse 1s ease-in-out infinite;
}
@keyframes mic-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.send-btn {
width: 38px; height: 38px; border-radius: 10px; border: none;
background: var(--accent); color: #0C1221; cursor: pointer;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; transition: opacity 0.15s;
}
.send-btn:hover { opacity: 0.85; }
.send-btn:disabled { opacity: 0.3; cursor: default; }
/* ── Settings panel extras ── */
.overlay { display: none; }
.settings-panel { z-index: 1000; }
.session-info-row {
display: flex; align-items: center; gap: 8px; margin-top: 8px;
font-size: 11px; color: rgba(255,255,255,0.35); font-family: monospace;
background: rgba(255,255,255,0.04); border-radius: 8px; padding: 8px 10px;
}
.session-info-row.hidden { display: none; }
/* Toggle switch */
.toggle-row {
display: flex; align-items: center; justify-content: space-between;
margin-top: 10px; cursor: pointer; user-select: none;
}
.toggle-label { font-size: 13px; color: rgba(255,255,255,0.55); }
.toggle-wrap { position: relative; display: inline-block; width: 38px; height: 22px; flex-shrink: 0; }
.toggle-wrap input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle-track {
position: absolute; inset: 0; border-radius: 11px;
background: rgba(255,255,255,0.1); transition: background 0.2s; cursor: pointer;
}
.toggle-track::after {
content: ''; position: absolute; top: 3px; left: 3px;
width: 16px; height: 16px; border-radius: 50%;
background: rgba(255,255,255,0.35); transition: transform 0.2s, background 0.2s;
}
.toggle-wrap input:checked + .toggle-track { background: rgba(167,139,250,0.3); }
.toggle-wrap input:checked + .toggle-track::after { transform: translateX(16px); background: var(--accent); }
</style>
</head>
<body>
<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="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 style="margin-top:20px">
<div class="settings-group-label">音声入力</div>
<label class="toggle-row">
<span class="toggle-label">認識後に自動送信</span>
<span class="toggle-wrap">
<input type="checkbox" id="voiceAutosend">
<span class="toggle-track"></span>
</span>
</label>
</div>
<div style="margin-top:20px">
<div class="settings-group-label">セッション</div>
<div class="session-info-row hidden" id="settingsSessionBadge">
<i data-lucide="terminal" style="width:12px;height:12px;stroke-width:1.75;color:var(--accent);flex-shrink:0"></i>
<span id="settingsSessionId"></span>
</div>
<a href="/sessions.html" style="font-size:13px;color:var(--accent);text-decoration:none;display:flex;align-items:center;gap:6px;margin-top:10px" rel="noopener noreferrer">
<i data-lucide="history" style="width:14px;height:14px;stroke-width:1.75"></i>
過去のセッションを見る
</a>
</div>
</div>
</aside>
<div class="overlay" id="overlay" aria-hidden="true"></div>
<header class="header">
<div class="header-left">
<div class="header-dot" aria-hidden="true"></div>
<span class="header-title">posimai-dev</span>
<span class="status-badge disconnected" id="statusBadge">接続中...</span>
</div>
<div class="header-right">
<button class="claude-btn" id="claudeBtn" aria-label="Claude 開始">
<i data-lucide="bot" style="width:20px;height:20px;stroke-width:1.75"></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 id="terminal-container"></div>
<div class="input-wrapper">
<!-- スラッシュコマンドポップアップ -->
<div class="slash-popup" id="slashPopup" role="listbox"></div>
<!-- クイックチップス -->
<div class="chips-bar" id="chipsBar"></div>
<!-- チャットバー -->
<div class="chat-bar">
<input
type="text"
class="chat-input"
id="chatInput"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
>
<button class="mic-btn" id="micBtn" aria-label="音声入力">
<i data-lucide="mic" style="width:16px;height:16px;stroke-width:1.75"></i>
</button>
<button class="send-btn" id="sendBtn" disabled aria-label="送信">
<i data-lucide="arrow-up" style="width:16px;height:16px;stroke-width:2.5"></i>
</button>
</div>
</div>
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
<script>
(function () {
// ── Elements ──
const statusBadge = document.getElementById('statusBadge'); // hidden, kept for compat
const headerDot = document.querySelector('.header-dot');
const settingsSessionRow = document.getElementById('settingsSessionBadge');
const settingsSessionId = document.getElementById('settingsSessionId');
const container = document.getElementById('terminal-container');
const chatInput = document.getElementById('chatInput');
const sendBtn = document.getElementById('sendBtn');
const claudeBtn = document.getElementById('claudeBtn');
const micBtn = document.getElementById('micBtn');
const slashPopup = document.getElementById('slashPopup');
const chipsBar = document.getElementById('chipsBar');
const voiceAutosend = document.getElementById('voiceAutosend');
// ── Slash commands & chips ──
const COMMANDS = [
{ cmd: '/todo', label: '今日のタスク', prompt: 'おはよう。git logと変更ファイルを確認して、今日やるべき作業を整理して提案して' },
{ cmd: '/status', label: '状況確認', prompt: 'git statusと直近のコミットを確認して、今の作業状態をわかりやすく教えて' },
{ cmd: '/commit', label: 'コミット', prompt: '変更内容を確認してコミットメッセージの案を出して、問題なければコミットして' },
{ cmd: '/deploy', label: 'デプロイ', prompt: 'npm run deployを実行してデプロイして' },
{ cmd: '/fix', label: 'エラー修正', prompt: '直前のエラーを確認して原因と修正方法を教えて、修正できるなら直して' },
{ cmd: '/explain', label: 'コード説明', prompt: '今の変更内容またはエラーをわかりやすく説明して' },
{ cmd: '/test', label: 'テスト', prompt: 'テストを実行して結果を教えて' },
{ cmd: '/ls', label: 'ファイル確認', prompt: 'カレントディレクトリの構造を確認して教えて' },
];
// クイックチップス最初の5件を常時表示
const CHIPS = COMMANDS.slice(0, 5);
CHIPS.forEach((c) => {
const btn = document.createElement('button');
btn.className = 'chip';
btn.textContent = c.label;
btn.addEventListener('click', () => {
sendInput(c.prompt + '\n');
});
chipsBar.appendChild(btn);
});
// スラッシュポップアップ描画
let slashActiveIndex = -1;
function renderSlashPopup(query) {
const q = query.slice(1).toLowerCase();
const matches = COMMANDS.filter((c) => c.cmd.includes(q) || c.label.includes(q));
if (!matches.length) { closeSlash(); return; }
slashPopup.innerHTML = '';
slashActiveIndex = -1;
matches.forEach((c, i) => {
const item = document.createElement('div');
item.className = 'slash-item';
item.setAttribute('role', 'option');
item.innerHTML = `<span class="slash-cmd">${c.cmd}</span><span class="slash-label">${c.label}</span>`;
item.addEventListener('mousedown', (e) => {
e.preventDefault();
selectSlash(c);
});
slashPopup.appendChild(item);
});
slashPopup.classList.add('open');
return matches;
}
function closeSlash() {
slashPopup.classList.remove('open');
slashPopup.innerHTML = '';
slashActiveIndex = -1;
}
function selectSlash(cmd) {
chatInput.value = cmd.prompt;
closeSlash();
chatInput.focus();
}
// ── xterm.js ──
const term = new Terminal({
fontFamily: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
fontSize: 14,
lineHeight: 1.4,
cursorBlink: true,
allowTransparency: true,
theme: {
background: 'transparent',
foreground: '#F3F4F6',
cursor: '#A78BFA',
selectionBackground: 'rgba(167,139,250,0.3)',
black: '#1A1A1A', brightBlack: '#4B5563',
red: '#F87171', brightRed: '#FCA5A5',
green: '#6EE7B7', brightGreen: '#A7F3D0',
yellow: '#FCD34D',brightYellow: '#FDE68A',
blue: '#60A5FA', brightBlue: '#93C5FD',
magenta: '#A78BFA', brightMagenta: '#C4B5FD',
cyan: '#22D3EE', brightCyan: '#67E8F9',
white: '#F3F4F6', brightWhite: '#FFFFFF'
}
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(container);
fitAddon.fit();
// 右クリックでターミナルにクリップボード貼り付け
container.addEventListener('contextmenu', async (e) => {
e.preventDefault();
try {
const text = await navigator.clipboard.readText();
if (text) sendInput(text);
} catch {}
});
// ── WebSocket ──
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
let ws;
function sendInput(text) {
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'input', data: text }));
}
function connect() {
ws = new WebSocket(`${proto}//${location.host}/terminal`);
ws.onopen = () => {
headerDot.classList.add('connected');
headerDot.classList.remove('disconnected');
headerDot.title = '接続済み';
sendBtn.disabled = false;
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
};
ws.onmessage = (e) => {
try {
const msg = JSON.parse(e.data);
if (msg.type === 'output') term.write(msg.data);
if (msg.type === 'session') {
settingsSessionId.textContent = msg.id;
settingsSessionRow.classList.remove('hidden');
}
} catch {}
};
ws.onclose = () => {
headerDot.classList.remove('connected');
headerDot.classList.add('disconnected');
headerDot.title = '切断';
sendBtn.disabled = true;
term.write('\r\n\x1b[31m[切断されました。再接続中...]\x1b[0m\r\n');
setTimeout(connect, 3000);
};
ws.onerror = () => ws.close();
}
connect();
term.onData((data) => sendInput(data));
// ── 送信(フォーカスをチャット欄に保持) ──
const inputHistory = [];
let historyIndex = -1;
function submitChat() {
const text = chatInput.value.trim();
if (!text) return;
closeSlash();
sendInput(text + '\n');
inputHistory.unshift(text);
if (inputHistory.length > 50) inputHistory.pop();
historyIndex = -1;
chatInput.value = '';
// フォーカスをチャット欄に残すCtrl+V がそのまま使える)
chatInput.focus();
}
sendBtn.addEventListener('click', submitChat);
chatInput.addEventListener('keydown', (e) => {
// 送信
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submitChat(); return; }
// スラッシュポップアップのキーボード操作
if (slashPopup.classList.contains('open')) {
const items = slashPopup.querySelectorAll('.slash-item');
if (e.key === 'ArrowDown') {
e.preventDefault();
slashActiveIndex = Math.min(slashActiveIndex + 1, items.length - 1);
items.forEach((el, i) => el.classList.toggle('active', i === slashActiveIndex));
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
slashActiveIndex = Math.max(slashActiveIndex - 1, 0);
items.forEach((el, i) => el.classList.toggle('active', i === slashActiveIndex));
return;
}
if (e.key === 'Enter' && slashActiveIndex >= 0) {
e.preventDefault();
const match = COMMANDS.filter((c) =>
c.cmd.includes(chatInput.value.slice(1).toLowerCase()) ||
c.label.includes(chatInput.value.slice(1).toLowerCase())
);
if (match[slashActiveIndex]) selectSlash(match[slashActiveIndex]);
return;
}
if (e.key === 'Escape') { closeSlash(); return; }
}
// 入力履歴ナビ(スラッシュポップアップが閉じている時)
if (e.key === 'ArrowUp' && !slashPopup.classList.contains('open')) {
e.preventDefault();
if (historyIndex < inputHistory.length - 1) {
historyIndex++;
chatInput.value = inputHistory[historyIndex];
}
return;
}
if (e.key === 'ArrowDown' && !slashPopup.classList.contains('open')) {
e.preventDefault();
if (historyIndex > 0) { historyIndex--; chatInput.value = inputHistory[historyIndex]; }
else { historyIndex = -1; chatInput.value = ''; }
return;
}
});
chatInput.addEventListener('input', () => {
const val = chatInput.value;
if (val.startsWith('/')) {
renderSlashPopup(val);
} else {
closeSlash();
}
});
// ── Claude 開始ボタン ──
claudeBtn.addEventListener('click', () => {
sendInput('claude\n');
chatInput.focus();
});
// ── 音声入力 ──
voiceAutosend.checked = localStorage.getItem('posimai-dev-voice-autosend') === '1';
voiceAutosend.addEventListener('change', () => {
localStorage.setItem('posimai-dev-voice-autosend', voiceAutosend.checked ? '1' : '0');
});
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SR) {
micBtn.classList.add('available');
const recognition = new SR();
recognition.lang = 'ja-JP';
recognition.continuous = false;
recognition.interimResults = false;
let listening = false;
micBtn.addEventListener('click', () => {
if (listening) { recognition.stop(); return; }
recognition.start();
});
recognition.onstart = () => { listening = true; micBtn.classList.add('listening'); };
recognition.onend = () => { listening = false; micBtn.classList.remove('listening'); };
recognition.onerror = () => { listening = false; micBtn.classList.remove('listening'); };
recognition.onresult = (e) => {
const transcript = e.results[0][0].transcript;
chatInput.value = transcript;
if (voiceAutosend.checked) {
submitChat();
} else {
chatInput.focus();
}
};
}
// ── Resize ──
const ro = new ResizeObserver(() => {
fitAddon.fit();
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
});
ro.observe(container);
window.addEventListener('resize', () => fitAddon.fit());
// ── Settings panel ──
const settingsBtn = document.getElementById('settingsBtn');
const settingsPanel = document.getElementById('settingsPanel');
settingsBtn.addEventListener('click', () => {
settingsPanel.classList.toggle('open');
settingsBtn.setAttribute('aria-expanded', settingsPanel.classList.contains('open'));
});
document.getElementById('settingsCloseBtn').addEventListener('click', () => {
settingsPanel.classList.remove('open');
settingsBtn.setAttribute('aria-expanded', 'false');
});
lucide.createIcons();
})();
</script>
</body>
</html>