posimai-root/_template/index.html

1265 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="ja">
<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>