1265 lines
62 KiB
HTML
1265 lines
62 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ja">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="robots" content="noindex, nofollow">
|
||
|
||
<!--
|
||
╔══════════════════════════════════════════════════════════╗
|
||
║ POSIMAI APP TEMPLATE v2.0 ║
|
||
╠══════════════════════════════════════════════════════════╣
|
||
║ 置換対象: ║
|
||
║ APP_NAME → アプリ名(例: Posimai Brain) ║
|
||
║ APP_ID → 識別子(例: posimai-brain) ║
|
||
║ APP_DESCRIPTION → 説明文 ║
|
||
╠══════════════════════════════════════════════════════════╣
|
||
║ デザインルール: ║
|
||
║ 絵文字禁止 / アイコンは Lucide のみ ║
|
||
║ アクセントは --accent 1色のみ ║
|
||
║ 全コンポーネントは data-i18n 属性を付与 ║
|
||
╚══════════════════════════════════════════════════════════╝
|
||
-->
|
||
|
||
<!-- ① テーマ・ロケール初期化(FOUC 防止 — CSS より前に実行) -->
|
||
<script>
|
||
(function () {
|
||
// Theme
|
||
var t = localStorage.getItem('APP_ID-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);
|
||
|
||
// Locale
|
||
var loc = localStorage.getItem('APP_ID-locale');
|
||
if (!loc) loc = navigator.language && navigator.language.startsWith('en') ? 'en' : 'ja';
|
||
document.documentElement.lang = loc;
|
||
document.documentElement.setAttribute('data-locale', loc);
|
||
|
||
// Magic Link
|
||
var p = new URLSearchParams(location.search);
|
||
var k = p.get('init_key');
|
||
if (k) {
|
||
localStorage.setItem('posimai_api_key', k);
|
||
p.delete('init_key');
|
||
var u = location.pathname + (p.toString() ? '?' + p.toString() : '') + location.hash;
|
||
history.replaceState({}, '', u);
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
<!-- PWA meta -->
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||
<meta name="description" content="APP_DESCRIPTION">
|
||
<meta name="color-scheme" content="dark light">
|
||
<meta name="theme-color" content="#0D0D0D" media="(prefers-color-scheme: dark)" id="themeColorDark">
|
||
<meta name="theme-color" content="#F9FAFB" media="(prefers-color-scheme: light)" id="themeColorLight">
|
||
<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="APP_NAME">
|
||
<link rel="manifest" href="/manifest.json">
|
||
<link rel="icon" type="image/png" href="/logo.png">
|
||
<link rel="apple-touch-icon" href="/logo.png">
|
||
|
||
<title>APP_NAME</title>
|
||
|
||
<!-- ② フォント(@import より高速) -->
|
||
<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">
|
||
|
||
<!-- Lucide Icons — これ以外のアイコンライブラリ使用禁止 -->
|
||
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
|
||
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
/* ════════════════════════════════════════
|
||
POSIMAI DESIGN TOKENS — Dark (default)
|
||
════════════════════════════════════════ */
|
||
:root, [data-theme="dark"] {
|
||
--bg: #0D0D0D;
|
||
--surface: #1A1A1A;
|
||
--surface2: #252525;
|
||
--border: #2D2D2D;
|
||
--text: #F3F4F6;
|
||
--text2: #9CA3AF;
|
||
--text3: #6B7280;
|
||
--accent: #6EE7B7;
|
||
--accent-dim: rgba(110,231,183,0.1);
|
||
--accent-border: rgba(110,231,183,0.25);
|
||
--header-bg: rgba(13,13,13,0.85);
|
||
--sidebar-bg: rgba(13,13,13,0.88);
|
||
--overlay-bg: rgba(0,0,0,0.6);
|
||
--shadow-lg: 0 8px 32px rgba(0,0,0,0.5);
|
||
--shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
|
||
color-scheme: dark;
|
||
}
|
||
|
||
/* ════════════════════════════════════════
|
||
POSIMAI DESIGN TOKENS — Light
|
||
════════════════════════════════════════ */
|
||
[data-theme="light"] {
|
||
--bg: #F9FAFB;
|
||
--surface: #FFFFFF;
|
||
--surface2: #F3F4F6;
|
||
--border: #E5E7EB;
|
||
--text: #111827;
|
||
--text2: #4B5563;
|
||
--text3: #9CA3AF;
|
||
--accent: #059669; /* emerald-600: 読みやすい明度 */
|
||
--accent-dim: rgba(5,150,105,0.08);
|
||
--accent-border: rgba(5,150,105,0.2);
|
||
--header-bg: rgba(249,250,251,0.85);
|
||
--sidebar-bg: rgba(249,250,251,0.92);
|
||
--overlay-bg: rgba(0,0,0,0.4);
|
||
--shadow-lg: 0 8px 32px rgba(0,0,0,0.15);
|
||
--shadow-sm: 0 2px 8px rgba(0,0,0,0.08);
|
||
color-scheme: light;
|
||
}
|
||
|
||
/* ════════════════════════════════════════
|
||
COMMON TOKENS (theme-independent)
|
||
════════════════════════════════════════ */
|
||
:root {
|
||
--radius: 12px;
|
||
--radius-sm: 8px;
|
||
--header-h: 52px;
|
||
--sidebar-w: 240px;
|
||
--ease: cubic-bezier(.4, 0, .2, 1);
|
||
--dur: 0.2s;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', -apple-system, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
.app { display: flex; min-height: 100dvh; }
|
||
|
||
/* ════════════════════════════════════════
|
||
SKIP LINK (アクセシビリティ)
|
||
════════════════════════════════════════ */
|
||
.skip-link {
|
||
position: absolute;
|
||
top: -100%; left: 8px;
|
||
background: var(--accent); color: #0D0D0D;
|
||
padding: 8px 16px; border-radius: var(--radius-sm);
|
||
font-weight: 600; font-size: 13px; z-index: 10000;
|
||
text-decoration: none;
|
||
transition: top .15s var(--ease);
|
||
}
|
||
.skip-link:focus { top: 8px; }
|
||
|
||
/* ════════════════════════════════════════
|
||
FOCUS VISIBLE
|
||
════════════════════════════════════════ */
|
||
:focus-visible {
|
||
outline: 2px solid var(--accent);
|
||
outline-offset: 2px;
|
||
border-radius: 4px;
|
||
}
|
||
:focus:not(:focus-visible) { outline: none; }
|
||
|
||
/* ════════════════════════════════════════
|
||
SIDEBAR
|
||
════════════════════════════════════════ */
|
||
.sidebar {
|
||
position: fixed;
|
||
top: 0; left: 0; bottom: 0;
|
||
width: var(--sidebar-w);
|
||
background: var(--sidebar-bg);
|
||
backdrop-filter: blur(20px);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
z-index: 200;
|
||
transform: translateX(-100%);
|
||
transition: transform .25s var(--ease);
|
||
}
|
||
.sidebar.open {
|
||
transform: translateX(0);
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
|
||
.sidebar-header {
|
||
height: var(--header-h);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 12px 0 16px;
|
||
flex-shrink: 0;
|
||
/* border-bottom なし — border-right との交差(格子模様)を防ぐ */
|
||
}
|
||
.brand { display: flex; align-items: center; gap: 10px; }
|
||
.brand-logo {
|
||
width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: linear-gradient(135deg, #6EE7B7, #047857);
|
||
}
|
||
[data-theme="light"] .brand-logo {
|
||
background: linear-gradient(135deg, #34D399, #059669);
|
||
}
|
||
.brand-text { display: flex; flex-direction: column; gap: 0; }
|
||
.brand-name { font-size: 13px; font-weight: 600; letter-spacing: -0.01em; line-height: 1.3; }
|
||
.brand-org { font-size: 10px; font-weight: 500; color: var(--text3); text-transform: uppercase; letter-spacing: 0.1em; }
|
||
|
||
/* サイドバーナビ */
|
||
.sidebar-nav { flex: 1; overflow-y: auto; padding: 4px 8px 0; }
|
||
.sidebar-nav::-webkit-scrollbar { width: 0; }
|
||
|
||
.nav-section-label {
|
||
font-size: 10px; font-weight: 600; color: var(--text3);
|
||
text-transform: uppercase; letter-spacing: 0.1em;
|
||
padding: 12px 10px 4px;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
}
|
||
.nav-section-toggle {
|
||
background: none; border: none; cursor: pointer; color: var(--text3);
|
||
display: flex; align-items: center; padding: 2px; border-radius: 4px;
|
||
transition: color var(--dur), background var(--dur);
|
||
}
|
||
.nav-section-toggle:hover { color: var(--text2); background: var(--surface2); }
|
||
.nav-accordion { overflow: hidden; transition: max-height .2s var(--ease); }
|
||
|
||
.nav-item {
|
||
display: flex; align-items: center; gap: 9px;
|
||
padding: 9px 10px; min-height: 40px;
|
||
border-radius: var(--radius-sm);
|
||
border: 1px solid transparent;
|
||
cursor: pointer; color: var(--text2); font-size: 13px; font-weight: 500;
|
||
transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
|
||
text-decoration: none; user-select: none;
|
||
}
|
||
.nav-item:hover { background: var(--surface2); color: var(--text); }
|
||
.nav-item.active { background: var(--accent-dim); color: var(--accent); border-color: var(--accent-border); }
|
||
.nav-item svg { flex-shrink: 0; opacity: .8; }
|
||
.nav-item.active svg { opacity: 1; }
|
||
.nav-item-label { flex: 1; }
|
||
.nav-chevron { display: none; flex-shrink: 0; opacity: 0.6; }
|
||
.nav-item.active .nav-chevron { display: flex; }
|
||
|
||
.nav-count {
|
||
font-size: 11px; font-weight: 600; color: var(--text3);
|
||
background: var(--surface2); border-radius: 999px;
|
||
padding: 1px 6px; min-width: 20px; text-align: center; line-height: 1.6;
|
||
}
|
||
.nav-item.active .nav-count { background: var(--accent-dim); color: var(--accent); }
|
||
|
||
/* サイドバーフッター */
|
||
.sidebar-footer { flex-shrink: 0; padding: 8px; border-top: 1px solid var(--border); }
|
||
.user-row {
|
||
display: flex; align-items: center; gap: 9px;
|
||
padding: 8px 10px; border-radius: var(--radius-sm);
|
||
cursor: pointer; transition: background var(--dur);
|
||
}
|
||
.user-row:hover { background: var(--surface2); }
|
||
.user-avatar {
|
||
width: 28px; height: 28px; border-radius: 50%;
|
||
background: var(--accent-dim); border: 1px solid var(--accent-border);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 11px; font-weight: 600; color: var(--accent); flex-shrink: 0;
|
||
}
|
||
.user-info { flex: 1; min-width: 0; }
|
||
.user-name {
|
||
font-size: 12px; font-weight: 500; color: var(--text);
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.user-role { font-size: 11px; color: var(--text3); }
|
||
.settings-trigger {
|
||
background: none; border: none; cursor: pointer; color: var(--text3);
|
||
display: flex; align-items: center; padding: 6px;
|
||
border-radius: var(--radius-sm);
|
||
transition: color var(--dur), background var(--dur); flex-shrink: 0;
|
||
}
|
||
.settings-trigger:hover { color: var(--text2); background: var(--surface2); }
|
||
|
||
/* ════════════════════════════════════════
|
||
SETTINGS PANEL(右スライドアウト)
|
||
════════════════════════════════════════ */
|
||
.settings-panel {
|
||
position: fixed; top: 0; right: 0; bottom: 0;
|
||
width: 280px; background: var(--surface);
|
||
border-left: 1px solid var(--border);
|
||
display: flex; flex-direction: column;
|
||
z-index: 300; transform: translateX(100%);
|
||
transition: transform .25s var(--ease);
|
||
}
|
||
.settings-panel.open {
|
||
transform: translateX(0);
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
.settings-panel-header {
|
||
height: var(--header-h);
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0 12px 0 16px; flex-shrink: 0; border-bottom: 1px solid var(--border);
|
||
}
|
||
.settings-panel-title { font-size: 14px; font-weight: 600; }
|
||
.settings-panel-body {
|
||
flex: 1; overflow-y: auto; padding: 16px;
|
||
display: flex; flex-direction: column; gap: 24px;
|
||
}
|
||
|
||
.settings-group { display: flex; flex-direction: column; }
|
||
.settings-group-label {
|
||
font-size: 10px; font-weight: 600; color: var(--text3);
|
||
text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 10px;
|
||
}
|
||
.settings-item {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border);
|
||
}
|
||
.settings-item:last-child { border-bottom: none; }
|
||
.settings-item-label { font-size: 13px; color: var(--text); }
|
||
.settings-item-desc { font-size: 11px; color: var(--text3); margin-top: 1px; }
|
||
|
||
/* トグルスイッチ */
|
||
.toggle { position: relative; width: 36px; height: 20px; flex-shrink: 0; }
|
||
.toggle input { opacity: 0; width: 0; height: 0; }
|
||
.toggle-track {
|
||
position: absolute; inset: 0;
|
||
background: var(--surface2); border: 1px solid var(--border);
|
||
border-radius: 999px; cursor: pointer; transition: background var(--dur);
|
||
}
|
||
.toggle input:checked + .toggle-track { background: var(--accent); border-color: var(--accent); }
|
||
.toggle-track::after {
|
||
content: ''; position: absolute; top: 2px; left: 2px;
|
||
width: 14px; height: 14px; background: #fff; border-radius: 50%;
|
||
transition: transform var(--dur);
|
||
}
|
||
.toggle input:checked + .toggle-track::after { transform: translateX(16px); }
|
||
|
||
/* テーマセレクター(3択ボタングループ) */
|
||
.theme-selector {
|
||
display: flex; gap: 4px;
|
||
background: var(--surface2); border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm); padding: 3px;
|
||
}
|
||
.theme-btn {
|
||
flex: 1; display: flex; align-items: center; justify-content: center; gap: 5px;
|
||
padding: 6px 8px; border-radius: 6px; border: none; cursor: pointer;
|
||
font-family: inherit; font-size: 11px; font-weight: 500; color: var(--text3);
|
||
background: none; transition: all var(--dur) var(--ease); white-space: nowrap;
|
||
}
|
||
.theme-btn:hover { color: var(--text2); }
|
||
.theme-btn.active {
|
||
background: var(--surface); color: var(--text);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
.theme-btn svg { flex-shrink: 0; }
|
||
|
||
/* 言語セレクター */
|
||
.lang-selector {
|
||
display: flex; gap: 4px;
|
||
background: var(--surface2); border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm); padding: 3px;
|
||
}
|
||
.lang-btn {
|
||
flex: 1; padding: 7px; border-radius: 6px; border: none; cursor: pointer;
|
||
font-family: inherit; font-size: 12px; font-weight: 600; color: var(--text3);
|
||
background: none; transition: all var(--dur) var(--ease); text-align: center;
|
||
}
|
||
.lang-btn:hover { color: var(--text2); }
|
||
.lang-btn.active {
|
||
background: var(--surface); color: var(--text);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
/* ════════════════════════════════════════
|
||
OVERLAY
|
||
════════════════════════════════════════ */
|
||
.overlay {
|
||
display: none; position: fixed; inset: 0;
|
||
background: var(--overlay-bg); backdrop-filter: blur(2px);
|
||
z-index: 150;
|
||
}
|
||
.overlay.open { display: block; }
|
||
|
||
/* ════════════════════════════════════════
|
||
MAIN AREA
|
||
════════════════════════════════════════ */
|
||
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||
|
||
/* ════════════════════════════════════════
|
||
HEADER
|
||
════════════════════════════════════════ */
|
||
.header {
|
||
height: var(--header-h);
|
||
display: flex; align-items: center; gap: 8px; padding: 0 16px;
|
||
background: var(--header-bg);
|
||
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
|
||
border-bottom: 1px solid var(--border);
|
||
position: sticky; top: 0; z-index: 100;
|
||
}
|
||
.page-title { font-size: 14px; font-weight: 600; letter-spacing: -0.01em; flex: 1; }
|
||
.mobile-brand { display: flex; align-items: center; gap: 7px; flex: 1; }
|
||
.mobile-brand-dot { width: 7px; height: 7px; background: var(--accent); border-radius: 50%; }
|
||
.mobile-brand-name { font-size: 14px; font-weight: 600; }
|
||
|
||
.icon-btn {
|
||
background: none; border: none; cursor: pointer; color: var(--text2);
|
||
min-width: 44px; min-height: 44px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
border-radius: var(--radius-sm);
|
||
transition: color var(--dur), background var(--dur);
|
||
flex-shrink: 0; margin: 0 -8px;
|
||
}
|
||
.icon-btn:hover { color: var(--text); background: var(--surface2); }
|
||
|
||
/* ライブステータスドット */
|
||
.dot-live {
|
||
width: 6px; height: 6px; border-radius: 50%;
|
||
background: var(--accent); flex-shrink: 0;
|
||
animation: dot-pulse 2.4s ease-in-out infinite;
|
||
}
|
||
@keyframes dot-pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.3; }
|
||
}
|
||
|
||
/* ステータスチップ */
|
||
.status-chip {
|
||
display: none; align-items: center; gap: 6px;
|
||
padding: 4px 10px; border-radius: var(--radius-sm);
|
||
background: var(--surface); border: 1px solid var(--border);
|
||
font-size: 11px; font-weight: 500; color: var(--text3);
|
||
}
|
||
@media (min-width: 640px) { .status-chip { display: inline-flex; } }
|
||
|
||
/* ════════════════════════════════════════
|
||
CONTENT
|
||
════════════════════════════════════════ */
|
||
.content {
|
||
flex: 1; padding: 24px 20px calc(40px + env(safe-area-inset-bottom));
|
||
max-width: 960px; width: 100%; margin: 0 auto;
|
||
}
|
||
|
||
/* ════════════════════════════════════════
|
||
PC: サイドバー常時表示
|
||
════════════════════════════════════════ */
|
||
@media (min-width: 1024px) {
|
||
.sidebar {
|
||
transform: translateX(0) !important;
|
||
box-shadow: none !important;
|
||
transition: transform .25s var(--ease) !important;
|
||
}
|
||
.main { margin-left: var(--sidebar-w); transition: margin-left .25s var(--ease); }
|
||
.mobile-brand { display: none; }
|
||
.page-title { display: block; }
|
||
#menuBtn { display: none; }
|
||
.overlay { display: none !important; }
|
||
|
||
body.sidebar-collapsed .sidebar { transform: translateX(-100%) !important; }
|
||
body.sidebar-collapsed .main { margin-left: 0; }
|
||
body.sidebar-collapsed #menuBtn { display: flex !important; }
|
||
body.sidebar-collapsed .mobile-brand { display: flex !important; }
|
||
body.sidebar-collapsed .page-title { display: none; }
|
||
|
||
.overlay.settings-overlay { display: none !important; }
|
||
}
|
||
@media (max-width: 1023px) {
|
||
.page-title { display: none; }
|
||
.mobile-brand { display: flex; }
|
||
}
|
||
|
||
/* ════════════════════════════════════════
|
||
COMPONENTS
|
||
════════════════════════════════════════ */
|
||
|
||
/* カード */
|
||
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
|
||
|
||
/* セクション */
|
||
.section { margin-top: 28px; }
|
||
.section:first-child { margin-top: 0; }
|
||
.section-label {
|
||
font-size: 11px; font-weight: 600; color: var(--text3);
|
||
text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 10px;
|
||
}
|
||
|
||
/* リストアイテム */
|
||
.list { display: flex; flex-direction: column; }
|
||
.list-item {
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 11px 12px; min-height: 52px; border-radius: var(--radius-sm);
|
||
cursor: pointer; transition: background var(--dur);
|
||
}
|
||
.list-item:hover { background: var(--surface2); }
|
||
.list-item-icon {
|
||
width: 34px; height: 34px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: var(--surface2); border-radius: var(--radius-sm);
|
||
flex-shrink: 0; color: var(--text2);
|
||
}
|
||
.list-item-body { flex: 1; min-width: 0; }
|
||
.list-item-title {
|
||
font-size: 13px; font-weight: 500;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.list-item-sub { font-size: 12px; color: var(--text2); margin-top: 1px; }
|
||
|
||
/* ボタン */
|
||
.btn {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 8px 14px; min-height: 36px; border-radius: var(--radius-sm); border: none;
|
||
cursor: pointer; font-family: inherit; font-size: 13px; font-weight: 500;
|
||
transition: opacity var(--dur), background var(--dur); white-space: nowrap;
|
||
}
|
||
.btn-primary { background: var(--accent); color: #0D0D0D; font-weight: 600; }
|
||
.btn-primary:hover { opacity: .85; }
|
||
.btn-ghost { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
|
||
.btn-ghost:hover { background: var(--border); }
|
||
.btn-danger { background: rgba(239,68,68,.1); color: #f87171; border: 1px solid rgba(239,68,68,.2); }
|
||
.btn-danger:hover { background: rgba(239,68,68,.18); }
|
||
|
||
/* バッジ */
|
||
.badge { display: inline-flex; align-items: center; padding: 2px 7px; border-radius: 999px; font-size: 11px; font-weight: 600; }
|
||
.badge-stable { background: rgba(52,211,153,.1); color: #34d399; border: 1px solid rgba(52,211,153,.2); }
|
||
.badge-progress { background: rgba(251,191,36,.1); color: #fbbf24; border: 1px solid rgba(251,191,36,.2); }
|
||
.badge-planned { background: rgba(161,161,170,.1); color: #a1a1aa; border: 1px solid rgba(161,161,170,.2); }
|
||
|
||
/* 入力 */
|
||
.input {
|
||
width: 100%; background: var(--surface); border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm); color: var(--text);
|
||
font-family: inherit; font-size: 13px; padding: 9px 12px; min-height: 40px;
|
||
outline: none; transition: border-color var(--dur);
|
||
}
|
||
.input::placeholder { color: var(--text3); }
|
||
.input:focus { border-color: var(--accent-border); }
|
||
|
||
/* API キー入力(モノスペース) */
|
||
.input-mono { font-family: 'Menlo', 'Monaco', monospace; font-size: 12px; }
|
||
|
||
/* 空状態 */
|
||
.empty {
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||
gap: 12px; padding: 56px 16px; color: var(--text3); text-align: center;
|
||
}
|
||
.empty-title { font-size: 14px; font-weight: 500; color: var(--text2); }
|
||
.empty-desc { font-size: 13px; margin-top: -4px; }
|
||
|
||
/* トースト */
|
||
#toast {
|
||
position: fixed;
|
||
bottom: max(24px, calc(env(safe-area-inset-bottom) + 16px));
|
||
left: 50%;
|
||
transform: translateX(-50%) translateY(16px);
|
||
background: var(--surface2); border: 1px solid var(--border); color: var(--text);
|
||
font-size: 13px; font-weight: 500; padding: 9px 18px; border-radius: 999px;
|
||
box-shadow: var(--shadow-lg);
|
||
opacity: 0; transition: opacity .2s var(--ease), transform .2s var(--ease);
|
||
pointer-events: none; z-index: 9999; white-space: nowrap;
|
||
}
|
||
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||
|
||
/* ユーティリティ */
|
||
.flex { display: flex; }
|
||
.items-center { align-items: center; }
|
||
.gap-2 { gap: 8px; }
|
||
.mt-4 { margin-top: 16px; }
|
||
.w-full { width: 100%; }
|
||
.text2 { color: var(--text2); }
|
||
.text3 { color: var(--text3); }
|
||
.text-sm { font-size: 13px; }
|
||
|
||
/* ════════════════════════════════════════
|
||
prefers-reduced-motion
|
||
════════════════════════════════════════ */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.sidebar, .settings-panel { transition: none; }
|
||
.nav-accordion { transition: none; }
|
||
* { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
|
||
#toast { transform: translateX(-50%) !important; transition: opacity .15s !important; }
|
||
}
|
||
|
||
/* ── コンパクト表示 ─────────────────────────────────── */
|
||
body.compact .list-item { padding: 7px 12px; min-height: 40px; }
|
||
body.compact .card { padding: 12px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- スキップリンク -->
|
||
<a href="#content" class="skip-link" data-i18n="a11y.skipLink">コンテンツへスキップ</a>
|
||
|
||
<div class="app">
|
||
|
||
<!-- ══ サイドバー ══════════════════════════════════════ -->
|
||
<aside class="sidebar" id="sidebar" aria-label="サイドバーナビゲーション">
|
||
<div class="sidebar-header">
|
||
<div class="brand">
|
||
<div class="brand-logo" aria-hidden="true">
|
||
<i data-lucide="layers" style="width:14px;height:14px;stroke-width:2;color:#0D0D0D"></i>
|
||
</div>
|
||
<div class="brand-text">
|
||
<span class="brand-name">APP_NAME</span>
|
||
<span class="brand-org">Posimai</span>
|
||
</div>
|
||
</div>
|
||
<button class="icon-btn" id="closeBtn" aria-label="サイドバーを閉じる" aria-expanded="true">
|
||
<i data-lucide="x" style="width:15px;height:15px;stroke-width:2"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<nav class="sidebar-nav" aria-label="メインナビゲーション">
|
||
<div class="nav-section-label">
|
||
<span data-i18n="nav.section.main">メインメニュー</span>
|
||
<button class="nav-section-toggle" data-accordion="main" aria-expanded="true" aria-label="折りたたむ">
|
||
<i data-lucide="chevron-down" style="width:12px;height:12px;stroke-width:2"></i>
|
||
</button>
|
||
</div>
|
||
<div class="nav-accordion" id="accordion-main" role="list">
|
||
<a class="nav-item active" data-view="home" data-title-ja="ホーム" data-title-en="Home" href="#" aria-current="page" role="listitem">
|
||
<i data-lucide="home" style="width:15px;height:15px;stroke-width:1.75"></i>
|
||
<span class="nav-item-label" data-i18n="nav.home">ホーム</span>
|
||
<i data-lucide="chevron-right" style="width:11px;height:11px;stroke-width:2.5" class="nav-chevron"></i>
|
||
</a>
|
||
<a class="nav-item" data-view="list" data-title-ja="一覧" data-title-en="List" href="#" role="listitem">
|
||
<i data-lucide="list" style="width:15px;height:15px;stroke-width:1.75"></i>
|
||
<span class="nav-item-label" data-i18n="nav.list">一覧</span>
|
||
<span class="nav-count" id="count-list" aria-label="0件">0</span>
|
||
<i data-lucide="chevron-right" style="width:11px;height:11px;stroke-width:2.5" class="nav-chevron"></i>
|
||
</a>
|
||
</div>
|
||
|
||
<div class="nav-section-label" style="margin-top:4px">
|
||
<span data-i18n="nav.section.sub">その他</span>
|
||
<button class="nav-section-toggle" data-accordion="sub" aria-expanded="true" aria-label="折りたたむ">
|
||
<i data-lucide="chevron-down" style="width:12px;height:12px;stroke-width:2"></i>
|
||
</button>
|
||
</div>
|
||
<div class="nav-accordion" id="accordion-sub" role="list">
|
||
<a class="nav-item" data-view="archive" data-title-ja="アーカイブ" data-title-en="Archive" href="#" role="listitem">
|
||
<i data-lucide="archive" style="width:15px;height:15px;stroke-width:1.75"></i>
|
||
<span class="nav-item-label" data-i18n="nav.archive">アーカイブ</span>
|
||
<span class="nav-count" id="count-archive" aria-label="0件">0</span>
|
||
<i data-lucide="chevron-right" style="width:11px;height:11px;stroke-width:2.5" class="nav-chevron"></i>
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="sidebar-footer">
|
||
<div class="user-row">
|
||
<div class="user-avatar" id="userAvatar" aria-hidden="true">P</div>
|
||
<div class="user-info">
|
||
<div class="user-name" id="userName">Posimai</div>
|
||
<div class="user-role" data-i18n="user.role">メンバー</div>
|
||
</div>
|
||
<button class="settings-trigger" id="settingsBtn" aria-label="設定を開く" aria-expanded="false" aria-controls="settingsPanel">
|
||
<i data-lucide="settings" style="width:15px;height:15px;stroke-width:1.75"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ══ オーバーレイ ════════════════════════════════════ -->
|
||
<div class="overlay" id="overlay" aria-hidden="true"></div>
|
||
|
||
<!-- ══ 設定パネル ══════════════════════════════════════ -->
|
||
<aside class="settings-panel" id="settingsPanel"
|
||
role="dialog" aria-modal="true" aria-labelledby="settingsPanelTitle">
|
||
<div class="settings-panel-header">
|
||
<span class="settings-panel-title" id="settingsPanelTitle" data-i18n="settings.title">設定</span>
|
||
<button class="icon-btn" id="settingsCloseBtn" aria-label="設定を閉じる">
|
||
<i data-lucide="x" style="width:15px;height:15px;stroke-width:2"></i>
|
||
</button>
|
||
</div>
|
||
<div class="settings-panel-body">
|
||
|
||
<!-- テーマ -->
|
||
<div class="settings-group">
|
||
<p class="settings-group-label" data-i18n="settings.group.appearance">外観</p>
|
||
<div class="settings-item" style="flex-direction:column; align-items:stretch; gap:8px;">
|
||
<div class="settings-item-label" data-i18n="settings.theme">テーマ</div>
|
||
<div class="theme-selector" role="group" aria-label="テーマ選択">
|
||
<button class="theme-btn" data-theme-val="dark" aria-pressed="false">
|
||
<i data-lucide="moon" style="width:12px;height:12px;stroke-width:1.75"></i>
|
||
<span data-i18n="settings.theme.dark">ダーク</span>
|
||
</button>
|
||
<button class="theme-btn" data-theme-val="light" aria-pressed="false">
|
||
<i data-lucide="sun" style="width:12px;height:12px;stroke-width:1.75"></i>
|
||
<span data-i18n="settings.theme.light">ライト</span>
|
||
</button>
|
||
<button class="theme-btn" data-theme-val="system" aria-pressed="false">
|
||
<i data-lucide="monitor" style="width:12px;height:12px;stroke-width:1.75"></i>
|
||
<span data-i18n="settings.theme.system">自動</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="settings-item" style="flex-direction:column; align-items:stretch; gap:8px;">
|
||
<div class="settings-item-label" data-i18n="settings.language">言語 / Language</div>
|
||
<div class="lang-selector" role="group" aria-label="言語選択">
|
||
<button class="lang-btn" data-lang-val="ja" aria-pressed="false">日本語</button>
|
||
<button class="lang-btn" data-lang-val="en" aria-pressed="false">English</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 表示設定 -->
|
||
<div class="settings-group">
|
||
<p class="settings-group-label" data-i18n="settings.group.display">表示</p>
|
||
<div class="settings-item">
|
||
<div>
|
||
<div class="settings-item-label" data-i18n="settings.compact">コンパクト表示</div>
|
||
<div class="settings-item-desc" data-i18n="settings.compact.desc">リストの行間を詰める</div>
|
||
</div>
|
||
<label class="toggle" aria-label="コンパクト表示">
|
||
<input type="checkbox" id="toggle-compact">
|
||
<span class="toggle-track"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 接続 -->
|
||
<div class="settings-group">
|
||
<p class="settings-group-label" data-i18n="settings.group.connection">接続</p>
|
||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||
<p class="settings-item-label" style="font-size:12px;color:var(--text2);" data-i18n="settings.apiKey">Posimai API Key</p>
|
||
<input type="password" class="input input-mono" id="apiKeyInput"
|
||
placeholder="pk_xxxx_..." data-i18n-placeholder="settings.apiKey.placeholder">
|
||
<p style="font-size:11px;color:var(--text3);line-height:1.5;" data-i18n="settings.apiKey.desc">
|
||
Brain への保存に使用されます。キーはブラウザにのみ保存されます。
|
||
</p>
|
||
<button class="btn btn-ghost w-full" id="saveApiKeyBtn" data-i18n="settings.save">保存</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- アカウント -->
|
||
<div class="settings-group">
|
||
<p class="settings-group-label" data-i18n="settings.group.account">アカウント</p>
|
||
<button class="btn btn-danger w-full" id="clearDataBtn">
|
||
<i data-lucide="trash-2" style="width:13px;height:13px;stroke-width:1.75"></i>
|
||
<span data-i18n="settings.clearData">データをすべて削除</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- バージョン情報 -->
|
||
<div style="margin-top:auto;padding-top:16px;border-top:1px solid var(--border);">
|
||
<p style="font-size:11px;color:var(--text3);text-align:center;">APP_NAME · Posimai</p>
|
||
</div>
|
||
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ══ メインエリア ════════════════════════════════════ -->
|
||
<div class="main" role="main">
|
||
|
||
<header class="header">
|
||
<button class="icon-btn" id="menuBtn" aria-label="メニューを開く" aria-expanded="false" aria-controls="sidebar">
|
||
<i data-lucide="menu" style="width:18px;height:18px;stroke-width:1.75"></i>
|
||
</button>
|
||
|
||
<div class="mobile-brand">
|
||
<div class="mobile-brand-dot"></div>
|
||
<span class="mobile-brand-name">APP_NAME</span>
|
||
</div>
|
||
|
||
<span class="page-title" id="pageTitle" aria-live="polite">ホーム</span>
|
||
|
||
<div style="flex:1"></div>
|
||
|
||
<div class="status-chip" aria-hidden="true">
|
||
<span class="dot-live"></span>
|
||
<span id="statusChipText">APP_NAME</span>
|
||
</div>
|
||
|
||
<button class="icon-btn" aria-label="検索" id="searchBtn">
|
||
<i data-lucide="search" style="width:16px;height:16px;stroke-width:1.75"></i>
|
||
</button>
|
||
</header>
|
||
|
||
<!-- コンテンツ -->
|
||
<main class="content" id="content">
|
||
|
||
<!-- ホームビュー -->
|
||
<div id="view-home">
|
||
<div class="section">
|
||
<p class="section-label" data-i18n="section.recent">最近</p>
|
||
<div class="card">
|
||
<div class="list">
|
||
<div class="list-item">
|
||
<div class="list-item-icon">
|
||
<i data-lucide="file-text" style="width:15px;height:15px;stroke-width:1.75"></i>
|
||
</div>
|
||
<div class="list-item-body">
|
||
<div class="list-item-title">サンプルアイテム 1</div>
|
||
<div class="list-item-sub">2026年3月12日</div>
|
||
</div>
|
||
<span class="badge badge-stable" data-i18n="badge.done">完了</span>
|
||
</div>
|
||
<div class="list-item">
|
||
<div class="list-item-icon">
|
||
<i data-lucide="file-text" style="width:15px;height:15px;stroke-width:1.75"></i>
|
||
</div>
|
||
<div class="list-item-body">
|
||
<div class="list-item-title">サンプルアイテム 2</div>
|
||
<div class="list-item-sub">2026年3月11日</div>
|
||
</div>
|
||
<span class="badge badge-progress" data-i18n="badge.inProgress">進行中</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<p class="section-label" data-i18n="section.actions">クイックアクション</p>
|
||
<div class="flex gap-2" style="flex-wrap:wrap">
|
||
<button class="btn btn-primary" id="addBtn">
|
||
<i data-lucide="plus" style="width:14px;height:14px;stroke-width:2"></i>
|
||
<span data-i18n="action.add">新規追加</span>
|
||
</button>
|
||
<button class="btn btn-ghost" id="exportBtn">
|
||
<i data-lucide="download" style="width:14px;height:14px;stroke-width:1.75"></i>
|
||
<span data-i18n="action.export">エクスポート</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 一覧ビュー -->
|
||
<div id="view-list" hidden>
|
||
<div class="empty">
|
||
<i data-lucide="inbox" style="width:36px;height:36px;stroke-width:1;color:var(--border)"></i>
|
||
<p class="empty-title" data-i18n="empty.list.title">データがありません</p>
|
||
<p class="empty-desc" data-i18n="empty.list.desc">最初のアイテムを追加してください</p>
|
||
<button class="btn btn-ghost mt-4">
|
||
<i data-lucide="plus" style="width:13px;height:13px;stroke-width:2"></i>
|
||
<span data-i18n="action.add">新規追加</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- アーカイブビュー -->
|
||
<div id="view-archive" hidden>
|
||
<div class="empty">
|
||
<i data-lucide="archive" style="width:36px;height:36px;stroke-width:1;color:var(--border)"></i>
|
||
<p class="empty-title" data-i18n="empty.archive.title">アーカイブは空です</p>
|
||
<p class="empty-desc" data-i18n="empty.archive.desc">完了したアイテムがここに表示されます</p>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- トースト(aria-live で読み上げ対応) -->
|
||
<div id="toast" role="status" aria-live="polite" aria-atomic="true"></div>
|
||
|
||
<script>
|
||
// ════════════════════════════════════════════════════════
|
||
// i18n
|
||
// ════════════════════════════════════════════════════════
|
||
const STRINGS = {
|
||
ja: {
|
||
'a11y.skipLink': 'コンテンツへスキップ',
|
||
'nav.section.main': 'メインメニュー',
|
||
'nav.section.sub': 'その他',
|
||
'nav.home': 'ホーム',
|
||
'nav.list': '一覧',
|
||
'nav.archive': 'アーカイブ',
|
||
'user.role': 'メンバー',
|
||
'settings.title': '設定',
|
||
'settings.group.appearance':'外観',
|
||
'settings.group.display': '表示',
|
||
'settings.group.connection':'接続',
|
||
'settings.group.account': 'アカウント',
|
||
'settings.theme': 'テーマ',
|
||
'settings.theme.dark': 'ダーク',
|
||
'settings.theme.light': 'ライト',
|
||
'settings.theme.system': '自動',
|
||
'settings.language': '言語 / Language',
|
||
'settings.compact': 'コンパクト表示',
|
||
'settings.compact.desc': 'リストの行間を詰める',
|
||
'settings.apiKey': 'Posimai API Key',
|
||
'settings.apiKey.placeholder': 'pk_xxxx_...',
|
||
'settings.apiKey.desc': 'Brain への保存に使用されます。キーはブラウザにのみ保存されます。',
|
||
'settings.save': '保存',
|
||
'settings.clearData': 'データをすべて削除',
|
||
'section.recent': '最近',
|
||
'section.actions': 'クイックアクション',
|
||
'badge.done': '完了',
|
||
'badge.inProgress': '進行中',
|
||
'empty.list.title': 'データがありません',
|
||
'empty.list.desc': '最初のアイテムを追加してください',
|
||
'empty.archive.title': 'アーカイブは空です',
|
||
'empty.archive.desc': '完了したアイテムがここに表示されます',
|
||
'action.add': '新規追加',
|
||
'action.export': 'エクスポート',
|
||
'toast.saved': '保存しました',
|
||
'toast.apiKeySaved': 'API キーを保存しました',
|
||
'toast.dataCleared': 'データを削除しました',
|
||
'toast.update': '更新があります',
|
||
'confirm.clearData': 'すべてのデータを削除しますか?この操作は元に戻せません。',
|
||
},
|
||
en: {
|
||
'a11y.skipLink': 'Skip to content',
|
||
'nav.section.main': 'Main Menu',
|
||
'nav.section.sub': 'More',
|
||
'nav.home': 'Home',
|
||
'nav.list': 'List',
|
||
'nav.archive': 'Archive',
|
||
'user.role': 'Member',
|
||
'settings.title': 'Settings',
|
||
'settings.group.appearance':'Appearance',
|
||
'settings.group.display': 'Display',
|
||
'settings.group.connection':'Connection',
|
||
'settings.group.account': 'Account',
|
||
'settings.theme': 'Theme',
|
||
'settings.theme.dark': 'Dark',
|
||
'settings.theme.light': 'Light',
|
||
'settings.theme.system': 'Auto',
|
||
'settings.language': '言語 / Language',
|
||
'settings.compact': 'Compact view',
|
||
'settings.compact.desc': 'Reduce row spacing in lists',
|
||
'settings.apiKey': 'Posimai API Key',
|
||
'settings.apiKey.placeholder': 'pk_xxxx_...',
|
||
'settings.apiKey.desc': 'Used for saving to Brain. Stored in browser only.',
|
||
'settings.save': 'Save',
|
||
'settings.clearData': 'Delete all data',
|
||
'section.recent': 'Recent',
|
||
'section.actions': 'Quick Actions',
|
||
'badge.done': 'Done',
|
||
'badge.inProgress': 'In Progress',
|
||
'empty.list.title': 'Nothing here',
|
||
'empty.list.desc': 'Add your first item to get started',
|
||
'empty.archive.title': 'Archive is empty',
|
||
'empty.archive.desc': 'Completed items will appear here',
|
||
'action.add': 'New',
|
||
'action.export': 'Export',
|
||
'toast.saved': 'Saved',
|
||
'toast.apiKeySaved': 'API key saved',
|
||
'toast.dataCleared': 'Data deleted',
|
||
'toast.update': 'Update available',
|
||
'confirm.clearData': 'Delete all data? This cannot be undone.',
|
||
}
|
||
};
|
||
|
||
let _locale = document.documentElement.getAttribute('data-locale') || 'ja';
|
||
|
||
function t(key) {
|
||
return (STRINGS[_locale] || STRINGS.ja)[key] || (STRINGS.ja[key] || key);
|
||
}
|
||
|
||
function applyLocale(locale) {
|
||
_locale = locale;
|
||
document.documentElement.lang = locale;
|
||
document.documentElement.setAttribute('data-locale', locale);
|
||
localStorage.setItem('APP_ID-locale', locale);
|
||
|
||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||
const k = el.dataset.i18n;
|
||
const s = t(k);
|
||
if (s !== k) el.textContent = s;
|
||
});
|
||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||
const k = el.dataset.i18nPlaceholder;
|
||
const s = t(k);
|
||
if (s !== k) el.placeholder = s;
|
||
});
|
||
|
||
// ナビページタイトルも更新
|
||
const activeNav = document.querySelector('.nav-item.active');
|
||
if (activeNav) {
|
||
const title = locale === 'en'
|
||
? activeNav.dataset.titleEn
|
||
: activeNav.dataset.titleJa;
|
||
if (title) document.getElementById('pageTitle').textContent = title;
|
||
}
|
||
|
||
// 言語ボタン状態更新
|
||
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||
const active = btn.dataset.langVal === locale;
|
||
btn.classList.toggle('active', active);
|
||
btn.setAttribute('aria-pressed', String(active));
|
||
});
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════
|
||
// テーマ
|
||
// ════════════════════════════════════════════════════════
|
||
let _themePref = document.documentElement.getAttribute('data-theme-pref') || 'system';
|
||
|
||
const _mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||
|
||
function applyTheme(pref) {
|
||
_themePref = pref;
|
||
localStorage.setItem('APP_ID-theme', pref);
|
||
document.documentElement.setAttribute('data-theme-pref', pref);
|
||
|
||
const dark = pref === 'dark' || (pref === 'system' && _mql.matches);
|
||
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||
|
||
// テーマボタン状態更新
|
||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||
const active = btn.dataset.themeVal === pref;
|
||
btn.classList.toggle('active', active);
|
||
btn.setAttribute('aria-pressed', String(active));
|
||
});
|
||
}
|
||
|
||
// OSのテーマ変更を検知(system モード時のみ反映)
|
||
_mql.addEventListener('change', () => {
|
||
if (_themePref === 'system') applyTheme('system');
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════
|
||
// サイドバー
|
||
// ════════════════════════════════════════════════════════
|
||
const sidebar = document.getElementById('sidebar');
|
||
const overlay = document.getElementById('overlay');
|
||
const menuBtn = document.getElementById('menuBtn');
|
||
const closeBtn = document.getElementById('closeBtn');
|
||
const SIDEBAR_KEY = 'APP_ID-sidebar-collapsed';
|
||
|
||
const isDesktop = () => window.matchMedia('(min-width: 1024px)').matches;
|
||
|
||
function openSidebar() {
|
||
if (isDesktop()) {
|
||
document.body.classList.remove('sidebar-collapsed');
|
||
localStorage.removeItem(SIDEBAR_KEY);
|
||
menuBtn.setAttribute('aria-expanded', 'true');
|
||
} else {
|
||
sidebar.classList.add('open');
|
||
overlay.classList.add('open');
|
||
overlay.setAttribute('aria-hidden', 'false');
|
||
closeBtn.setAttribute('aria-expanded', 'true');
|
||
menuBtn.setAttribute('aria-expanded', 'true');
|
||
}
|
||
}
|
||
function closeSidebar() {
|
||
if (isDesktop()) {
|
||
document.body.classList.add('sidebar-collapsed');
|
||
localStorage.setItem(SIDEBAR_KEY, '1');
|
||
menuBtn.setAttribute('aria-expanded', 'false');
|
||
} else {
|
||
sidebar.classList.remove('open');
|
||
overlay.classList.remove('open');
|
||
overlay.setAttribute('aria-hidden', 'true');
|
||
closeBtn.setAttribute('aria-expanded', 'false');
|
||
menuBtn.setAttribute('aria-expanded', 'false');
|
||
}
|
||
}
|
||
|
||
if (isDesktop() && localStorage.getItem(SIDEBAR_KEY)) {
|
||
document.body.classList.add('sidebar-collapsed');
|
||
}
|
||
|
||
menuBtn.addEventListener('click', openSidebar);
|
||
closeBtn.addEventListener('click', closeSidebar);
|
||
overlay.addEventListener('click', () => {
|
||
closeSidebar();
|
||
closeSettings();
|
||
});
|
||
|
||
// ── Accordion ──────────────────────────────────────────
|
||
document.querySelectorAll('.nav-section-toggle').forEach(btn => {
|
||
const id = btn.dataset.accordion;
|
||
const accordion = document.getElementById('accordion-' + id);
|
||
if (!accordion) return;
|
||
|
||
accordion.style.maxHeight = accordion.scrollHeight + 'px';
|
||
|
||
btn.addEventListener('click', () => {
|
||
const isCollapsed = accordion.classList.contains('collapsed');
|
||
if (isCollapsed) {
|
||
accordion.classList.remove('collapsed');
|
||
accordion.style.maxHeight = accordion.scrollHeight + 'px';
|
||
btn.style.transform = '';
|
||
btn.setAttribute('aria-expanded', 'true');
|
||
} else {
|
||
accordion.classList.add('collapsed');
|
||
accordion.style.maxHeight = '0px';
|
||
btn.style.transform = 'rotate(-90deg)';
|
||
btn.setAttribute('aria-expanded', 'false');
|
||
}
|
||
btn.style.transition = 'transform .2s var(--ease)';
|
||
});
|
||
});
|
||
|
||
// ── ナビゲーション ──────────────────────────────────────
|
||
document.querySelectorAll('.nav-item').forEach(item => {
|
||
item.addEventListener('click', e => {
|
||
e.preventDefault();
|
||
const view = item.dataset.view;
|
||
const title = _locale === 'en' ? item.dataset.titleEn : item.dataset.titleJa;
|
||
|
||
document.querySelectorAll('.nav-item').forEach(n => {
|
||
n.classList.remove('active');
|
||
n.removeAttribute('aria-current');
|
||
});
|
||
item.classList.add('active');
|
||
item.setAttribute('aria-current', 'page');
|
||
|
||
if (title) document.getElementById('pageTitle').textContent = title;
|
||
|
||
document.querySelectorAll('[id^="view-"]').forEach(v => v.hidden = true);
|
||
const el = document.getElementById('view-' + view);
|
||
if (el) el.hidden = false;
|
||
|
||
if (!isDesktop()) closeSidebar();
|
||
});
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════
|
||
// 設定パネル
|
||
// ════════════════════════════════════════════════════════
|
||
const settingsPanel = document.getElementById('settingsPanel');
|
||
const settingsBtn = document.getElementById('settingsBtn');
|
||
const settingsCloseBtn = document.getElementById('settingsCloseBtn');
|
||
|
||
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])';
|
||
|
||
function trapFocus(container, trigger) {
|
||
const els = [...container.querySelectorAll(FOCUSABLE)];
|
||
if (!els.length) return () => {};
|
||
els[0].focus();
|
||
function handler(e) {
|
||
if (e.key === 'Tab') {
|
||
const first = els[0], last = els[els.length - 1];
|
||
if (e.shiftKey && document.activeElement === first) {
|
||
e.preventDefault(); last.focus();
|
||
} else if (!e.shiftKey && document.activeElement === last) {
|
||
e.preventDefault(); first.focus();
|
||
}
|
||
}
|
||
}
|
||
container.addEventListener('keydown', handler);
|
||
return () => container.removeEventListener('keydown', handler);
|
||
}
|
||
|
||
let _removeFocusTrap = null;
|
||
|
||
function openSettings() {
|
||
// 設定値をフォームに反映
|
||
const savedKey = localStorage.getItem('posimai_api_key') || '';
|
||
document.getElementById('apiKeyInput').value = savedKey;
|
||
|
||
settingsPanel.classList.add('open');
|
||
settingsBtn.setAttribute('aria-expanded', 'true');
|
||
if (!isDesktop()) {
|
||
overlay.classList.add('open');
|
||
sidebar.classList.remove('open');
|
||
}
|
||
_removeFocusTrap = trapFocus(settingsPanel, settingsBtn);
|
||
}
|
||
|
||
function closeSettings() {
|
||
settingsPanel.classList.remove('open');
|
||
settingsBtn.setAttribute('aria-expanded', 'false');
|
||
if (!isDesktop()) overlay.classList.remove('open');
|
||
if (_removeFocusTrap) { _removeFocusTrap(); _removeFocusTrap = null; }
|
||
settingsBtn.focus();
|
||
}
|
||
|
||
function toggleSettings() {
|
||
settingsPanel.classList.contains('open') ? closeSettings() : openSettings();
|
||
}
|
||
settingsBtn.addEventListener('click', toggleSettings);
|
||
settingsCloseBtn.addEventListener('click', closeSettings);
|
||
|
||
// user-row 全体でもトグル(gear icon が小さいため)
|
||
document.querySelector('.user-row').addEventListener('click', (e) => {
|
||
if (!e.target.closest('button') || e.target.closest('#settingsBtn')) toggleSettings();
|
||
});
|
||
|
||
// Escape で閉じる(グローバル)
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key !== 'Escape') return;
|
||
if (settingsPanel.classList.contains('open')) { closeSettings(); return; }
|
||
if (!isDesktop() && sidebar.classList.contains('open')) { closeSidebar(); menuBtn.focus(); }
|
||
});
|
||
|
||
// ── テーマ切り替え ─────────────────────────────────────
|
||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => applyTheme(btn.dataset.themeVal));
|
||
});
|
||
|
||
// ── 言語切り替え ───────────────────────────────────────
|
||
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => applyLocale(btn.dataset.langVal));
|
||
});
|
||
|
||
// ── コンパクト切り替え ─────────────────────────────────
|
||
const compactToggle = document.getElementById('toggle-compact');
|
||
compactToggle.checked = !!localStorage.getItem('APP_ID-compact');
|
||
document.body.classList.toggle('compact', compactToggle.checked);
|
||
compactToggle.addEventListener('change', () => {
|
||
document.body.classList.toggle('compact', compactToggle.checked);
|
||
localStorage.setItem('APP_ID-compact', compactToggle.checked ? '1' : '');
|
||
});
|
||
|
||
// ── API キー保存 ───────────────────────────────────────
|
||
document.getElementById('saveApiKeyBtn').addEventListener('click', () => {
|
||
const val = document.getElementById('apiKeyInput').value.trim();
|
||
if (val) {
|
||
localStorage.setItem('posimai_api_key', val);
|
||
} else {
|
||
localStorage.removeItem('posimai_api_key');
|
||
}
|
||
showToast(t('toast.apiKeySaved'));
|
||
closeSettings();
|
||
});
|
||
|
||
// ── データ削除 ─────────────────────────────────────────
|
||
document.getElementById('clearDataBtn').addEventListener('click', () => {
|
||
if (!confirm(t('confirm.clearData'))) return;
|
||
// posimai_api_key と テーマ・ロケール設定は保持
|
||
const keep = ['posimai_api_key', 'APP_ID-theme', 'APP_ID-locale', 'APP_ID-sidebar-collapsed'];
|
||
const saved = {};
|
||
keep.forEach(k => { const v = localStorage.getItem(k); if (v) saved[k] = v; });
|
||
localStorage.clear();
|
||
Object.entries(saved).forEach(([k, v]) => localStorage.setItem(k, v));
|
||
showToast(t('toast.dataCleared'));
|
||
setTimeout(() => location.reload(), 800);
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════
|
||
// サンプルボタン
|
||
// ════════════════════════════════════════════════════════
|
||
document.getElementById('addBtn').addEventListener('click', () => showToast(t('toast.saved')));
|
||
document.getElementById('exportBtn').addEventListener('click', () => showToast(t('toast.saved')));
|
||
|
||
// ════════════════════════════════════════════════════════
|
||
// トースト
|
||
// ════════════════════════════════════════════════════════
|
||
let _toastTimer;
|
||
function showToast(msg, duration = 2500) {
|
||
const el = document.getElementById('toast');
|
||
el.textContent = msg;
|
||
el.classList.add('show');
|
||
clearTimeout(_toastTimer);
|
||
_toastTimer = setTimeout(() => el.classList.remove('show'), duration);
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════
|
||
// Service Worker
|
||
// ════════════════════════════════════════════════════════
|
||
if ('serviceWorker' in navigator) {
|
||
navigator.serviceWorker.register('/sw.js').then(reg => {
|
||
reg.addEventListener('updatefound', () => {
|
||
const nw = reg.installing;
|
||
nw.addEventListener('statechange', () => {
|
||
if (nw.state === 'installed' && navigator.serviceWorker.controller) {
|
||
showToast(t('toast.update'), 8000);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════
|
||
// 初期化
|
||
// ════════════════════════════════════════════════════════
|
||
lucide.createIcons();
|
||
applyTheme(_themePref);
|
||
applyLocale(_locale);
|
||
</script>
|
||
</body>
|
||
</html>
|