posimai-root/_template/index.html

1265 lines
62 KiB
HTML
Raw Normal View History

<!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>