posimai-veil/index.html

1458 lines
58 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="ja" data-app-id="posimai-veil">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<script>
(function () {
var t = localStorage.getItem('posimai-veil-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);
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="ホーム画面をシンプルに保つアプリランチャー">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0D0D0D" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#F9FAFB" media="(prefers-color-scheme: light)">
<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="Veil">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" href="/logo.png">
<link rel="apple-touch-icon" href="/logo.png">
<title>Posimai Veil</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://posimai-ui.vercel.app/v1/base.css">
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
<style>
/* ── Clock zone ── */
.veil-clock {
padding: max(20px, env(safe-area-inset-top)) 16px 8px;
position: relative;
}
.veil-time {
font-size: clamp(56px, 18vw, 80px);
font-weight: 300;
letter-spacing: -0.03em;
line-height: 1;
color: var(--text);
font-variant-numeric: tabular-nums;
}
.veil-sub {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 13px;
color: var(--text2);
margin-top: 4px;
}
.veil-sep { color: var(--border); }
.veil-weather {
display: inline-flex;
align-items: center;
gap: 4px;
}
.veil-weather-icon {
width: 13px;
height: 13px;
stroke: var(--text2);
stroke-width: 2;
}
.veil-greeting {
font-size: 11px;
font-weight: 300;
color: var(--text3);
margin-top: 4px;
min-height: 16px;
}
.veil-settings-btn {
position: absolute;
top: max(16px, env(safe-area-inset-top));
right: 12px;
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.12s;
}
.veil-settings-btn:active { background: var(--surface2); }
/* ── Search ── */
.search-wrap {
padding: 8px 16px 4px;
position: sticky;
top: 0;
z-index: 10;
background: var(--bg);
}
.search-wrap-inner { position: relative; }
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text3);
pointer-events: none;
display: flex;
}
.search-input {
width: 100%;
box-sizing: border-box;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-size: 14px;
padding: 9px 14px 9px 38px;
outline: none;
font-family: inherit;
transition: border-color 0.15s;
}
.search-input:focus { border-color: var(--accent); }
/* ── Category tabs ── */
.cat-scroll {
display: flex;
gap: 6px;
padding: 8px 16px;
overflow-x: auto;
scrollbar-width: none;
position: sticky;
top: 52px;
z-index: 9;
background: var(--bg);
}
.cat-scroll::-webkit-scrollbar { display: none; }
.cat-btn {
flex-shrink: 0;
padding: 5px 12px;
border-radius: 20px;
border: 1px solid var(--border);
background: transparent;
color: var(--text2);
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: background 0.12s, color 0.12s, border-color 0.12s;
white-space: nowrap;
}
.cat-btn.active {
background: var(--accent);
color: #0D0D0D;
border-color: var(--accent);
font-weight: 600;
}
/* ── Edit bar ── */
.edit-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px 0;
min-height: 36px;
gap: 8px;
}
.edit-bar-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.edit-bar-hint { font-size: 11px; color: var(--text3); }
.edit-toggle-btn {
font-size: 12px;
padding: 5px 12px;
border-radius: 20px;
border: 1px solid var(--border);
background: transparent;
color: var(--text2);
font-family: inherit;
cursor: pointer;
transition: all 0.12s;
flex-shrink: 0;
}
.edit-toggle-btn.active {
border-color: var(--accent);
color: var(--accent);
background: var(--surface);
}
/* ── Add app button ── */
.add-app-btn {
display: none;
align-items: center;
gap: 5px;
padding: 5px 11px 5px 8px;
border-radius: 20px;
border: 1px dashed var(--accent);
background: transparent;
color: var(--accent);
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: background 0.12s;
flex-shrink: 0;
}
.add-app-btn.visible { display: flex; }
.add-app-btn:active { background: var(--surface); }
/* ── App sections ── */
.app-section { padding: 0 16px 16px; }
.section-label {
font-size: 11px;
font-weight: 600;
color: var(--text3);
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 12px 0 8px;
}
.app-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
/* ── App item ── */
.app-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 14px 6px 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
color: var(--text);
transition: background 0.12s, border-color 0.12s, transform 0.1s;
-webkit-tap-highlight-color: transparent;
user-select: none;
position: relative;
}
.app-item:active { background: var(--surface2); transform: scale(0.94); }
.app-item.hidden { display: none; }
.app-item.edit-mode { cursor: pointer; }
.app-item.edit-mode.off { opacity: 0.35; }
.app-item.edit-mode.on { border-color: var(--accent); }
.app-icon {
width: 26px;
height: 26px;
stroke: var(--accent);
stroke-width: 1.5;
flex-shrink: 0;
}
.app-label {
font-size: 10px;
font-weight: 500;
color: var(--text2);
text-align: center;
line-height: 1.3;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Text initial badge ── */
.app-initial {
width: 26px;
height: 26px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
flex-shrink: 0;
}
.check-badge {
display: none;
position: absolute;
top: 5px;
right: 5px;
width: 14px;
height: 14px;
background: var(--accent);
border-radius: 50%;
align-items: center;
justify-content: center;
}
.app-item.edit-mode.on .check-badge { display: flex; }
/* ── Custom app delete button ── */
.custom-del-btn {
display: none;
position: absolute;
top: 4px;
left: 4px;
width: 16px;
height: 16px;
background: #EF4444;
border-radius: 50%;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
z-index: 2;
padding: 0;
line-height: 1;
color: #fff;
font-size: 10px;
font-weight: 700;
}
.app-item.edit-mode .custom-del-btn { display: flex; }
/* ── Empty state ── */
.empty-state {
text-align: center;
padding: 60px 24px;
color: var(--text3);
}
.empty-icon {
width: 36px;
height: 36px;
stroke-width: 1.25;
margin: 0 auto 12px;
display: block;
opacity: .45;
}
.empty-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.empty-sub { font-size: 13px; }
/* ── Color mode selector ── */
.color-mode-selector {
display: flex;
gap: 6px;
margin-top: 8px;
}
.color-mode-btn {
flex: 1;
padding: 7px 8px;
border-radius: 8px;
border: 1px solid var(--border);
background: transparent;
color: var(--text2);
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: all 0.12s;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.color-mode-btn.active {
background: var(--surface2);
border-color: var(--accent);
color: var(--accent);
font-weight: 600;
}
.color-dots {
display: flex;
gap: 2px;
}
.color-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
/* ── Settings extras ── */
.settings-field-label {
font-size: 12px;
color: var(--text2);
margin-top: 4px;
line-height: 1.5;
}
.settings-text-input {
width: 100%;
box-sizing: border-box;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 13px;
padding: 9px 12px;
outline: none;
font-family: inherit;
margin-top: 6px;
}
.settings-text-input:focus { border-color: var(--accent); }
.settings-action-btn {
width: 100%;
padding: 10px;
margin-top: 8px;
background: var(--accent);
color: #0D0D0D;
border: none;
border-radius: 8px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.settings-reset-btn {
width: 100%;
padding: 9px;
margin-top: 6px;
background: transparent;
color: var(--text3);
border: 1px solid var(--border);
border-radius: 8px;
font-family: inherit;
font-size: 12px;
cursor: pointer;
}
/* ── Add app modal ── */
.add-modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,.55);
z-index: 200;
backdrop-filter: blur(2px);
}
.add-modal-backdrop.open { display: block; }
.add-modal {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--surface);
border-radius: 16px 16px 0 0;
padding-bottom: max(24px, env(safe-area-inset-bottom));
z-index: 201;
max-height: 88vh;
overflow-y: auto;
transform: translateY(100%);
transition: transform 0.25s cubic-bezier(0.2,0.9,0.2,1);
}
.add-modal.open { transform: translateY(0); }
.add-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 16px 12px;
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
background: var(--surface);
z-index: 1;
}
.add-modal-title {
font-size: 15px;
font-weight: 600;
color: var(--text);
}
.add-modal-body {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.form-field-label {
font-size: 12px;
color: var(--text2);
font-weight: 500;
margin-bottom: 6px;
}
.form-text-input {
width: 100%;
box-sizing: border-box;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 14px;
padding: 9px 12px;
outline: none;
font-family: inherit;
transition: border-color 0.15s;
}
.form-text-input:focus { border-color: var(--accent); }
.form-select {
width: 100%;
box-sizing: border-box;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 14px;
padding: 9px 12px;
outline: none;
font-family: inherit;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239CA3AF' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
transition: border-color 0.15s;
}
.form-select:focus { border-color: var(--accent); }
.icon-picker-label {
font-size: 12px;
color: var(--text2);
font-weight: 500;
margin-bottom: 8px;
}
.icon-picker-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
}
.icon-pick-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 4px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text3);
cursor: pointer;
transition: all 0.12s;
aspect-ratio: 1;
}
.icon-pick-btn.selected {
border-color: var(--accent);
background: var(--surface2);
color: var(--accent);
}
.icon-pick-btn:active { background: var(--surface2); }
.icon-initial-pick {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text2);
cursor: pointer;
font-size: 12px;
font-family: inherit;
transition: all 0.12s;
margin-top: 6px;
width: 100%;
box-sizing: border-box;
}
.icon-initial-pick.selected {
border-color: var(--accent);
background: var(--surface2);
color: var(--accent);
}
.initial-preview {
width: 22px;
height: 22px;
border-radius: 5px;
background: var(--surface2);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.form-submit-btn {
width: 100%;
padding: 12px;
background: var(--accent);
color: #0D0D0D;
border: none;
border-radius: 8px;
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.form-submit-btn:disabled {
opacity: 0.38;
cursor: not-allowed;
}
/* ── main: base.css のデフォルト padding / max-width を打ち消す ── */
/* base.css は main に padding:24px 20px / max-width:860px を適用するが、
Veil は全幅ランチャーのため各セクションが自前でパディングを持つ */
main {
padding: 0 0 max(16px, env(safe-area-inset-bottom));
max-width: none;
margin: 0;
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link" tabindex="0"
style="position:absolute;top:-100%;left:8px;background:var(--accent);color:#0D0D0D;padding:8px 16px;border-radius:8px;font-weight:600;font-size:13px;z-index:10000;text-decoration:none">
コンテンツへスキップ
</a>
<!-- Settings panel -->
<aside class="settings-panel" id="settingsPanel" role="complementary">
<div class="settings-panel-header">
<span class="settings-panel-title">設定</span>
<button class="icon-btn" id="settingsCloseBtn" aria-label="設定を閉じる">
<i data-lucide="x" style="width:18px;height:18px;stroke-width:1.75"></i>
</button>
</div>
<div class="settings-panel-body">
<div>
<div class="settings-group-label">外観</div>
<div class="settings-item">
<div class="settings-item-label">テーマ</div>
<div class="theme-selector">
<button class="theme-btn" data-theme-val="dark">
<i data-lucide="moon" style="width:12px;height:12px;stroke-width:1.75"></i>ダーク
</button>
<button class="theme-btn" data-theme-val="light">
<i data-lucide="sun" style="width:12px;height:12px;stroke-width:1.75"></i>ライト
</button>
<button class="theme-btn" data-theme-val="system">
<i data-lucide="monitor" style="width:12px;height:12px;stroke-width:1.75"></i>自動
</button>
</div>
</div>
</div>
<div style="margin-top:20px">
<div class="settings-group-label">アイコンカラー</div>
<div class="color-mode-selector">
<button class="color-mode-btn" data-color-mode="accent" id="colorModeAccent">
<span class="color-dots">
<span class="color-dot" style="background:#6EE7B7"></span>
<span class="color-dot" style="background:#6EE7B7"></span>
<span class="color-dot" style="background:#6EE7B7"></span>
</span>
統一色
</button>
<button class="color-mode-btn" data-color-mode="colorful" id="colorModeColorful">
<span class="color-dots">
<span class="color-dot" style="background:#60A5FA"></span>
<span class="color-dot" style="background:#F472B6"></span>
<span class="color-dot" style="background:#4ADE80"></span>
</span>
カラフル
</button>
</div>
</div>
<div style="margin-top:20px">
<div class="settings-group-label">同期(オプション)</div>
<p class="settings-field-label">
Posimai API キーを設定すると、複数端末でアプリ設定を共有できます。
</p>
<input class="settings-text-input" id="apiKeyInput" type="password"
placeholder="posimai_api_key" autocomplete="off">
<button class="settings-action-btn" id="apiKeySave">保存</button>
</div>
<div style="margin-top:20px">
<div class="settings-group-label">リセット</div>
<button class="settings-reset-btn" id="resetBtn">表示アプリをすべてデフォルトに戻す</button>
</div>
</div>
</aside>
<div class="overlay" id="overlay" aria-hidden="true"></div>
<!-- Add app modal -->
<div class="add-modal-backdrop" id="addModalBackdrop"></div>
<div class="add-modal" id="addModal" role="dialog" aria-modal="true" aria-label="アプリを追加">
<div class="add-modal-header">
<span class="add-modal-title">アプリを追加</span>
<button class="icon-btn" id="addModalClose" aria-label="閉じる">
<i data-lucide="x" style="width:18px;height:18px;stroke-width:1.75"></i>
</button>
</div>
<div class="add-modal-body">
<div>
<div class="form-field-label">アプリ名 *</div>
<input class="form-text-input" id="addName" type="text"
placeholder="例: Notion" autocomplete="off" maxlength="30">
</div>
<div>
<div class="form-field-label">カテゴリ *</div>
<select class="form-select" id="addCat">
<option value="">選択してください</option>
<option value="sns">SNS・メッセージ</option>
<option value="media">動画・音楽</option>
<option value="news">ニュース</option>
<option value="shop">ショッピング・決済</option>
<option value="nav">マップ・移動</option>
<option value="tools">ツール</option>
<option value="posimai">Posimai</option>
</select>
</div>
<div>
<div class="form-field-label">アイコン *</div>
<div class="icon-picker-grid" id="iconPickerGrid"></div>
<button class="icon-initial-pick" id="iconInitialPick" type="button">
<span class="initial-preview" id="initialPreviewChar">A</span>
イニシャルバッジ(名前の頭文字を使う)
</button>
</div>
<div>
<div class="form-field-label">Android パッケージ名</div>
<input class="form-text-input" id="addAndroid" type="text"
placeholder="例: com.notion.id" autocomplete="off" autocapitalize="none" autocorrect="off">
</div>
<div>
<div class="form-field-label">iOS URL スキーム</div>
<input class="form-text-input" id="addIos" type="text"
placeholder="例: notion://" autocomplete="off" autocapitalize="none" autocorrect="off">
</div>
<div>
<div class="form-field-label">ウェブ URL</div>
<input class="form-text-input" id="addWeb" type="url"
placeholder="例: https://notion.so" autocomplete="off">
</div>
<button class="form-submit-btn" id="addSubmitBtn" disabled>追加する</button>
</div>
</div>
<main id="main-content">
<!-- 時計・天気・挨拶ゾーン -->
<div class="veil-clock">
<div class="veil-time" id="veilTime">--:--</div>
<div class="veil-sub">
<span id="veilDate">---</span>
<span class="veil-sep">·</span>
<span class="veil-weather" id="veilWeather">
<i data-lucide="cloud" class="veil-weather-icon" id="veilWeatherIcon"></i>
<span id="veilTemp">--°</span>
<span id="veilWeatherLabel"></span>
</span>
</div>
<div class="veil-greeting" id="veilGreeting"></div>
<button class="veil-settings-btn" id="settingsBtn" aria-label="設定" aria-expanded="false">
<i data-lucide="settings" style="width:16px;height:16px;stroke-width:1.5"></i>
</button>
</div>
<div class="search-wrap">
<div class="search-wrap-inner">
<span class="search-icon">
<i data-lucide="search" style="width:16px;height:16px;stroke-width:1.75"></i>
</span>
<input class="search-input" id="searchInput" type="search"
placeholder="アプリを検索" autocomplete="off" autocorrect="off" autocapitalize="off">
</div>
</div>
<div class="cat-scroll" id="catScroll" role="tablist" aria-label="カテゴリフィルタ"></div>
<div class="edit-bar">
<div class="edit-bar-left">
<button class="add-app-btn" id="addAppBtn" aria-label="アプリを追加">
<i data-lucide="plus" style="width:13px;height:13px;stroke-width:2.5"></i>
アプリを追加
</button>
<span class="edit-bar-hint" id="editBarHint"></span>
</div>
<button class="edit-toggle-btn" id="editToggleBtn">編集</button>
</div>
<div id="appSections"></div>
</main>
<div id="toast" role="status" aria-live="polite"></div>
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
<script defer>
// ============================================================
// Posimai Veil — アプリランチャー
// ============================================================
const ENABLED_KEY = 'posimai-veil-enabled';
const CUSTOM_KEY = 'posimai-veil-custom';
const API_KEY_KEY = 'posimai_api_key';
const COLOR_MODE_KEY = 'posimai-veil-color-mode';
const API_BASE = 'https://posimai-lab.tail72e846.ts.net/brain/api';
// ── カテゴリカラー(ダーク / ライト) ──────────────────────
const CAT_COLORS = {
dark: {
posimai: '#6EE7B7',
sns: '#60A5FA',
media: '#F472B6',
news: '#FB923C',
tools: '#94A3B8',
nav: '#4ADE80',
shop: '#FBBF24',
},
light: {
posimai: '#059669',
sns: '#2563EB',
media: '#DB2777',
news: '#EA580C',
tools: '#475569',
nav: '#16A34A',
shop: '#D97706',
},
};
// ── アイコンピッカー候補リスト ──────────────────────────────
const ICON_PICKER_ICONS = [
'message-circle','message-square','send','at-sign','phone','mail','users',
'play','music','film','tv','radio','headphones','mic','airplay',
'camera','image','aperture','eye','star','heart','bookmark',
'map-pin','map','navigation','compass','globe',
'shopping-cart','shopping-bag','tag','credit-card','wallet',
'newspaper','rss','trending-up','bar-chart-2',
'calendar','clock','bell','search','share',
'settings','lock','key','shield','zap',
'briefcase','book','coffee','home','layers',
'monitor','smartphone','cpu','hard-drive','file-text',
'calculator','activity','layout-dashboard','check-circle',
];
// ── アプリDB ────────────────────────────────────────────────
const APP_DB = [
// SNS・メッセージ
{ id:'line', label:'LINE', icon:'message-circle', cat:'sns',
android:'jp.naver.line.android', ios:'line://', web:'https://line.me' },
{ id:'twitter', label:'X', icon:'at-sign', cat:'sns',
android:'com.twitter.android', ios:'twitter://', web:'https://x.com' },
{ id:'instagram', label:'Instagram', icon:'aperture', cat:'sns',
android:'com.instagram.android', ios:'instagram://', web:'https://instagram.com' },
{ id:'tiktok', label:'TikTok', icon:'play-circle', cat:'sns',
android:'com.zhiliaoapp.musically', ios:'tiktok://', web:'https://tiktok.com' },
{ id:'facebook', label:'Facebook', icon:'users', cat:'sns',
android:'com.facebook.katana', ios:'fb://', web:'https://facebook.com' },
{ id:'whatsapp', label:'WhatsApp', icon:'message-square', cat:'sns',
android:'com.whatsapp', ios:'whatsapp://', web:'https://whatsapp.com' },
{ id:'discord', label:'Discord', icon:'headphones', cat:'sns',
android:'com.discord', ios:'discord://', web:'https://discord.com' },
{ id:'threads', label:'Threads', icon:'link-2', cat:'sns',
android:'com.instagram.barcelona', ios:null, web:'https://threads.net' },
// 動画・音楽
{ id:'youtube', label:'YouTube', icon:'play', cat:'media',
android:'com.google.android.youtube', ios:'youtube://', web:'https://youtube.com' },
{ id:'netflix', label:'Netflix', icon:'tv', cat:'media',
android:'com.netflix.mediaclient', ios:'nflx://', web:'https://netflix.com' },
{ id:'spotify', label:'Spotify', icon:'music', cat:'media',
android:'com.spotify.music', ios:'spotify://', web:'https://open.spotify.com' },
{ id:'abema', label:'AbemaTV', icon:'radio', cat:'media',
android:'tv.abema', ios:'abema://', web:'https://abema.tv' },
{ id:'tver', label:'TVer', icon:'film', cat:'media',
android:'jp.co.tver.app', ios:'tver://', web:'https://tver.jp' },
{ id:'disneyplus', label:'Disney+', icon:'star', cat:'media',
android:'com.disney.disneyplus', ios:'disneyplus://', web:'https://disneyplus.com' },
{ id:'primevideo', label:'Prime Video', icon:'tv-2', cat:'media',
android:'com.amazon.avod.thirdpartyclient', ios:'aiv://', web:'https://primevideo.com' },
{ id:'nhkplus', label:'NHK+', icon:'airplay', cat:'media',
android:'jp.nhk.player.nhkplus', ios:'nhkplus://', web:'https://plus.nhk.jp' },
// ニュース
{ id:'smartnews', label:'SmartNews', icon:'newspaper', cat:'news',
android:'jp.gocro.smartnews.android', ios:'smartnews://', web:'https://smartnews.com' },
{ id:'newspicks', label:'NewsPicks', icon:'trending-up', cat:'news',
android:'jp.co.uzabase.newspicks', ios:'newspicks://', web:'https://newspicks.com' },
{ id:'yahoonews', label:'Yahoo!ニュース', icon:'rss', cat:'news',
android:'jp.co.yahoo.android.yjtop', ios:'yjiosapp://', web:'https://news.yahoo.co.jp' },
// ショッピング・決済
{ id:'amazon', label:'Amazon', icon:'shopping-cart', cat:'shop',
android:'com.amazon.mShop.android.shopping', ios:'com.amazon.mobile.shopping://', web:'https://amazon.co.jp' },
{ id:'rakuten', label:'楽天市場', icon:'shopping-bag', cat:'shop',
android:'jp.co.rakuten.ichiba', ios:'rakuten-ichiba://', web:'https://rakuten.co.jp' },
{ id:'mercari', label:'メルカリ', icon:'tag', cat:'shop',
android:'com.kouzoh.mercari', ios:'mercari://', web:'https://mercari.com' },
{ id:'paypay', label:'PayPay', icon:'credit-card', cat:'shop',
android:'jp.ne.paypay.android', ios:'paypay://', web:'https://paypay.ne.jp' },
{ id:'rakupay', label:'楽天Pay', icon:'wallet', cat:'shop',
android:'jp.co.rakuten.pay', ios:'rakutenpay://', web:'https://pay.rakuten.co.jp' },
// マップ・移動
{ id:'gmaps', label:'Google Maps', icon:'map-pin', cat:'nav',
android:'com.google.android.apps.maps', ios:'comgooglemaps://', web:'https://maps.google.com' },
{ id:'yahoomap', label:'Yahoo!地図', icon:'map', cat:'nav',
android:'jp.co.yahoo.android.apps.ysm', ios:'yjiosapp://', web:'https://map.yahoo.co.jp' },
{ id:'navitime', label:'NAVITIME', icon:'navigation', cat:'nav',
android:'jp.co.navitime.android.townnavi', ios:'navitime://', web:'https://navitime.co.jp' },
// ツール
{ id:'gmail', label:'Gmail', icon:'mail', cat:'tools',
android:'com.google.android.gm', ios:'googlegmail://', web:'https://mail.google.com' },
{ id:'gcal', label:'カレンダー', icon:'calendar', cat:'tools',
android:'com.google.android.calendar', ios:'calshow://', web:'https://calendar.google.com' },
{ id:'gdrive', label:'Drive', icon:'hard-drive', cat:'tools',
android:'com.google.android.apps.docs', ios:'googledrive://', web:'https://drive.google.com' },
{ id:'gphoto', label:'フォト', icon:'image', cat:'tools',
android:'com.google.android.apps.photos', ios:'googlephotos://', web:'https://photos.google.com' },
{ id:'camera', label:'カメラ', icon:'camera', cat:'tools',
android:'_camera', ios:null, web:null },
{ id:'chrome', label:'Chrome', icon:'globe', cat:'tools',
android:'com.android.chrome', ios:'googlechromes://', web:'https://google.com' },
{ id:'keep', label:'Keep', icon:'file-text', cat:'tools',
android:'com.google.android.keep', ios:null, web:'https://keep.google.com' },
{ id:'phone', label:'電話', icon:'phone', cat:'tools',
android:'_phone', ios:'tel://', web:null },
{ id:'settings', label:'設定', icon:'settings-2', cat:'tools',
android:'_settings', ios:'app-settings://', web:null },
{ id:'clock', label:'時計', icon:'clock', cat:'tools',
android:'com.google.android.deskclock', ios:null, web:null },
{ id:'calc', label:'電卓', icon:'calculator', cat:'tools',
android:'com.google.android.calculator', ios:null, web:null },
// Posimai
{ id:'p-ambient', label:'Ambient', icon:'monitor', cat:'posimai',
android:null, ios:null, web:'https://posimai-ambient.vercel.app' },
{ id:'p-feed', label:'Feed', icon:'rss', cat:'posimai',
android:null, ios:null, web:'https://posimai-feed.vercel.app' },
{ id:'p-journal', label:'Journal', icon:'notebook-pen', cat:'posimai',
android:null, ios:null, web:'https://posimai-journal.vercel.app' },
{ id:'p-brain', label:'Brain', icon:'layers', cat:'posimai',
android:null, ios:null, web:'https://posimai-brain.vercel.app' },
{ id:'p-habit', label:'Habit', icon:'check-circle', cat:'posimai',
android:null, ios:null, web:'https://posimai-habit.vercel.app' },
{ id:'p-pulse', label:'Pulse', icon:'activity', cat:'posimai',
android:null, ios:null, web:'https://posimai-pulse.vercel.app' },
{ id:'p-think', label:'Think', icon:'cpu', cat:'posimai',
android:null, ios:null, web:'https://posimai-think.vercel.app' },
{ id:'p-daily', label:'Daily', icon:'mic', cat:'posimai',
android:null, ios:null, web:'https://posimai-daily.vercel.app' },
{ id:'p-timer', label:'Timer', icon:'clock', cat:'posimai',
android:null, ios:null, web:'https://posimai-timer.vercel.app' },
{ id:'p-dashboard',label:'Dashboard', icon:'layout-dashboard',cat:'posimai',
android:null, ios:null, web:'https://posimai-dashboard.vercel.app' },
];
const CATS = [
{ id:'all', label:'すべて' },
{ id:'posimai', label:'Posimai' },
{ id:'sns', label:'SNS' },
{ id:'media', label:'メディア' },
{ id:'news', label:'ニュース' },
{ id:'tools', label:'ツール' },
{ id:'nav', label:'マップ' },
{ id:'shop', label:'ショッピング' },
];
const CAT_LABELS = {
sns: 'SNS・メッセージ',
media: '動画・音楽',
news: 'ニュース',
shop: 'ショッピング・決済',
nav: 'マップ・移動',
tools: 'ツール',
posimai: 'Posimai',
};
const CAT_ORDER = ['posimai','sns','media','news','tools','nav','shop'];
// ── カスタムアプリ ──────────────────────────────────────────
function loadCustomApps() {
try {
const raw = localStorage.getItem(CUSTOM_KEY);
if (raw) return JSON.parse(raw);
} catch {}
return [];
}
function saveCustomApps() {
localStorage.setItem(CUSTOM_KEY, JSON.stringify(customApps));
}
let customApps = loadCustomApps();
// ── 状態 ────────────────────────────────────────────────────
let enabledIds = loadEnabled();
let activeCat = 'all';
let searchQ = '';
let editMode = false;
let colorMode = localStorage.getItem(COLOR_MODE_KEY) || 'accent';
let apiKey = localStorage.getItem(API_KEY_KEY) || '';
function getTheme() {
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
}
function getCatColor(cat) {
if (colorMode !== 'colorful') return 'var(--accent)';
return CAT_COLORS[getTheme()][cat] || 'var(--accent)';
}
function loadEnabled() {
try {
const raw = localStorage.getItem(ENABLED_KEY);
if (raw) return new Set(JSON.parse(raw));
} catch {}
// デフォルト: 全 ON組み込み + カスタム)
const base = new Set(APP_DB.map(a => a.id));
customApps.forEach(a => base.add(a.id));
return base;
}
function saveEnabled() {
localStorage.setItem(ENABLED_KEY, JSON.stringify([...enabledIds]));
}
function allApps() {
return [...APP_DB, ...customApps];
}
// ── アプリ起動 ──────────────────────────────────────────────
function launchApp(app) {
const ua = navigator.userAgent.toLowerCase();
const isA = /android/.test(ua);
const isI = /iphone|ipad|ipod/.test(ua);
if (isA && app.android) {
if (app.android === '_camera') {
location.href = 'intent:#Intent;action=android.media.action.IMAGE_CAPTURE;end';
} else if (app.android === '_settings') {
location.href = 'intent:#Intent;action=android.settings.SETTINGS;end';
} else if (app.android === '_phone') {
location.href = 'tel:';
} else {
location.href = `intent:#Intent;package=${app.android};action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;end`;
}
return;
}
if (isI && app.ios) {
location.href = app.ios;
if (app.web) setTimeout(() => { location.href = app.web; }, 2500);
return;
}
if (app.web) {
window.open(app.web, '_blank', 'noopener');
}
}
// ── 編集モード: 表示/非表示トグル ──────────────────────────
function toggleApp(id) {
if (enabledIds.has(id)) enabledIds.delete(id);
else enabledIds.add(id);
saveEnabled();
renderApps();
}
// ── カスタムアプリ削除 ──────────────────────────────────────
function deleteCustomApp(id) {
customApps = customApps.filter(a => a.id !== id);
saveCustomApps();
enabledIds.delete(id);
saveEnabled();
renderApps();
showToast('削除しました');
}
// ── カテゴリタブ描画 ────────────────────────────────────────
function renderCatTabs() {
const wrap = document.getElementById('catScroll');
wrap.innerHTML = CATS.map(c => `
<button class="cat-btn${activeCat === c.id ? ' active' : ''}"
data-cat="${c.id}" role="tab" aria-selected="${activeCat === c.id}">
${c.label}
</button>
`).join('');
wrap.querySelectorAll('.cat-btn').forEach(btn => {
btn.addEventListener('click', () => {
activeCat = btn.dataset.cat;
renderCatTabs();
renderApps();
});
});
}
// ── アプリグリッド描画 ──────────────────────────────────────
function renderApps() {
const container = document.getElementById('appSections');
const hint = document.getElementById('editBarHint');
const editBtn = document.getElementById('editToggleBtn');
const addBtn = document.getElementById('addAppBtn');
editBtn.classList.toggle('active', editMode);
hint.textContent = editMode ? 'タップで表示/非表示を切り替え' : '';
addBtn.classList.toggle('visible', editMode);
const q = searchQ.toLowerCase();
const source = allApps();
const filtered = source.filter(app => {
if (activeCat !== 'all' && app.cat !== activeCat) return false;
if (q && !app.label.toLowerCase().includes(q) && !app.id.includes(q)) return false;
return true;
});
if (!filtered.length) {
container.innerHTML = `
<div class="empty-state">
<i data-lucide="search" class="empty-icon"></i>
<div class="empty-title">見つかりません</div>
<div class="empty-sub">キーワードを変えてみてください</div>
</div>`;
lucide.createIcons();
return;
}
const groups = {};
filtered.forEach(app => {
if (!groups[app.cat]) groups[app.cat] = [];
groups[app.cat].push(app);
});
const orderedCats = CAT_ORDER.filter(c => groups[c]);
container.innerHTML = orderedCats.map(cat => {
const apps = groups[cat];
const color = getCatColor(cat);
const isColorful = colorMode === 'colorful';
return `
<div class="app-section">
<div class="section-label"
style="${isColorful ? `color:${color};opacity:.9` : ''}">${CAT_LABELS[cat] || cat}</div>
<div class="app-grid">
${apps.map(app => {
const on = enabledIds.has(app.id);
const hidden = !editMode && !on ? ' hidden' : '';
const editCls = editMode ? ` edit-mode ${on ? 'on' : 'off'}` : '';
const bgStyle = isColorful
? `background:${color}14;border-color:${color}38;`
: '';
const isCustom = !!app.custom;
const isInitial = app.icon === '_initial';
const iconHTML = isInitial
? `<div class="app-initial" style="background:${color}22;color:${color}">${app.label.charAt(0).toUpperCase()}</div>`
: `<i data-lucide="${app.icon}" class="app-icon" style="stroke:${color}"></i>`;
const delBtn = isCustom
? `<button class="custom-del-btn" data-del-id="${app.id}" aria-label="${app.label}を削除">&#x2715;</button>`
: '';
return `
<div class="app-item${editCls}${hidden}"
data-id="${app.id}" role="button" tabindex="0"
aria-label="${app.label}"
style="${bgStyle}">
${delBtn}
${iconHTML}
<span class="app-label">${app.label}</span>
<span class="check-badge" aria-hidden="true"
style="background:${color}">
<i data-lucide="check"
style="width:9px;height:9px;stroke-width:3;stroke:#0D0D0D"></i>
</span>
</div>`;
}).join('')}
</div>
</div>`;
}).join('');
container.querySelectorAll('.app-item').forEach(el => {
const id = el.dataset.id;
const app = allApps().find(a => a.id === id);
if (!app) return;
el.addEventListener('click', () => {
if (editMode) toggleApp(id);
else launchApp(app);
});
el.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (editMode) toggleApp(id);
else launchApp(app);
}
});
});
container.querySelectorAll('.custom-del-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
deleteCustomApp(btn.dataset.delId);
});
});
lucide.createIcons();
}
// ── 編集モードトグル ────────────────────────────────────────
document.getElementById('editToggleBtn').addEventListener('click', () => {
editMode = !editMode;
renderApps();
});
// ── 検索 ────────────────────────────────────────────────────
document.getElementById('searchInput').addEventListener('input', e => {
searchQ = e.target.value;
if (searchQ) activeCat = 'all';
renderCatTabs();
renderApps();
});
// ── カラーモード切替 ────────────────────────────────────────
function syncColorModeUI() {
document.getElementById('colorModeAccent').classList.toggle('active', colorMode === 'accent');
document.getElementById('colorModeColorful').classList.toggle('active', colorMode === 'colorful');
}
['colorModeAccent', 'colorModeColorful'].forEach(id => {
document.getElementById(id).addEventListener('click', () => {
colorMode = id === 'colorModeAccent' ? 'accent' : 'colorful';
localStorage.setItem(COLOR_MODE_KEY, colorMode);
syncColorModeUI();
renderApps();
});
});
syncColorModeUI();
// ── API キー保存 ────────────────────────────────────────────
document.getElementById('apiKeyInput').value = apiKey;
document.getElementById('apiKeySave').addEventListener('click', () => {
apiKey = document.getElementById('apiKeyInput').value.trim();
localStorage.setItem(API_KEY_KEY, apiKey);
showToast('保存しました');
// TODO: アーキテクチャ確定後に site_config 同期を追加
});
// ── リセット ────────────────────────────────────────────────
document.getElementById('resetBtn').addEventListener('click', () => {
enabledIds = new Set(allApps().map(a => a.id));
saveEnabled();
renderApps();
showToast('リセットしました');
});
// ── トースト ────────────────────────────────────────────────
function showToast(msg) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 2200);
}
// ── アプリ追加モーダル ───────────────────────────────────────
let selectedIcon = null;
function buildIconPicker() {
const grid = document.getElementById('iconPickerGrid');
grid.innerHTML = ICON_PICKER_ICONS.map(name => `
<button class="icon-pick-btn${selectedIcon === name ? ' selected' : ''}"
data-icon="${name}" type="button" aria-label="${name}">
<i data-lucide="${name}" style="width:17px;height:17px;stroke-width:1.5"></i>
</button>
`).join('');
grid.querySelectorAll('.icon-pick-btn').forEach(btn => {
btn.addEventListener('click', () => {
selectedIcon = btn.dataset.icon;
grid.querySelectorAll('.icon-pick-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
document.getElementById('iconInitialPick').classList.remove('selected');
validateAddForm();
});
});
lucide.createIcons();
}
function openAddModal() {
selectedIcon = null;
document.getElementById('addName').value = '';
document.getElementById('addCat').value = '';
document.getElementById('addAndroid').value = '';
document.getElementById('addIos').value = '';
document.getElementById('addWeb').value = '';
document.getElementById('addSubmitBtn').disabled = true;
document.getElementById('iconInitialPick').classList.remove('selected');
document.getElementById('initialPreviewChar').textContent = 'A';
buildIconPicker();
document.getElementById('addModalBackdrop').classList.add('open');
document.getElementById('addModal').classList.add('open');
document.getElementById('addName').focus();
}
function closeAddModal() {
document.getElementById('addModalBackdrop').classList.remove('open');
document.getElementById('addModal').classList.remove('open');
}
function validateAddForm() {
const name = document.getElementById('addName').value.trim();
const cat = document.getElementById('addCat').value;
const ok = name.length > 0 && cat.length > 0 && selectedIcon !== null;
document.getElementById('addSubmitBtn').disabled = !ok;
}
document.getElementById('addName').addEventListener('input', () => {
const firstChar = document.getElementById('addName').value.trim().charAt(0).toUpperCase() || 'A';
document.getElementById('initialPreviewChar').textContent = firstChar || 'A';
validateAddForm();
});
document.getElementById('addCat').addEventListener('change', validateAddForm);
document.getElementById('iconInitialPick').addEventListener('click', () => {
selectedIcon = '_initial';
document.getElementById('iconInitialPick').classList.add('selected');
document.getElementById('iconPickerGrid').querySelectorAll('.icon-pick-btn')
.forEach(b => b.classList.remove('selected'));
validateAddForm();
});
document.getElementById('addSubmitBtn').addEventListener('click', () => {
const name = document.getElementById('addName').value.trim();
const cat = document.getElementById('addCat').value;
const android = document.getElementById('addAndroid').value.trim() || null;
const ios = document.getElementById('addIos').value.trim() || null;
const web = document.getElementById('addWeb').value.trim() || null;
if (!name || !cat || !selectedIcon) return;
const newApp = {
id: `custom-${Date.now()}`,
label: name,
icon: selectedIcon,
cat: cat,
android: android,
ios: ios,
web: web,
custom: true,
};
customApps.push(newApp);
saveCustomApps();
enabledIds.add(newApp.id);
saveEnabled();
closeAddModal();
showToast(`${name} を追加しました`);
renderApps();
});
document.getElementById('addAppBtn').addEventListener('click', openAddModal);
document.getElementById('addModalClose').addEventListener('click', closeAddModal);
document.getElementById('addModalBackdrop').addEventListener('click', closeAddModal);
// ── 時計 ────────────────────────────────────────────────────
const DAYS_JP = ['日','月','火','水','木','金','土'];
const GREETING_POOL = {
morning: ['おはようございます','いい朝ですね','さあ、始めましょうか','今日もよろしく'],
afternoon: ['こんにちは','お昼どうでしたか','午後もがんばりましょう','ひと息ついて'],
evening: ['こんばんは','お疲れさまです','今日もよく頑張りました','ゆっくり振り返りましょう'],
night: ['お疲れさまでした','ゆっくり休んでくださいね','そろそろ休みましょうか'],
latenight: ['夜更かしですね','深夜のひとときを','静かな夜ですね'],
};
function getGreetingPool(hr) {
if (hr >= 5 && hr <= 11) return GREETING_POOL.morning;
if (hr >= 12 && hr <= 16) return GREETING_POOL.afternoon;
if (hr >= 17 && hr <= 20) return GREETING_POOL.evening;
if (hr >= 21 && hr <= 23) return GREETING_POOL.night;
return GREETING_POOL.latenight;
}
const _greetIdx = Math.floor(Math.random() * 4);
function updateClock() {
const now = new Date();
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
document.getElementById('veilTime').textContent = `${h}:${m}`;
const mo = now.getMonth() + 1;
const d = now.getDate();
const day = DAYS_JP[now.getDay()];
document.getElementById('veilDate').textContent = `${mo}${d}日(${day}`;
const pool = getGreetingPool(now.getHours());
document.getElementById('veilGreeting').textContent = pool[_greetIdx % pool.length];
}
// ── 天気 ────────────────────────────────────────────────────
const WMO_MAP = [
[0, 'sun', '晴れ'],
[3, 'cloud', '曇り'],
[48, 'cloud', '霧'],
[57, 'cloud-drizzle', '霧雨'],
[67, 'cloud-rain', '雨'],
[77, 'snowflake', '雪'],
[82, 'cloud-rain', '大雨'],
[86, 'snowflake', '大雪'],
[99, 'cloud-lightning','雷雨'],
];
function wmoToInfo(code) {
for (let i = WMO_MAP.length - 1; i >= 0; i--) {
if (code >= WMO_MAP[i][0]) return { icon: WMO_MAP[i][1], label: WMO_MAP[i][2] };
}
return { icon: 'cloud', label: '---' };
}
async function fetchWeather() {
try {
let lat = 34.6937, lon = 135.5023;
try {
const pos = await new Promise((res, rej) =>
navigator.geolocation.getCurrentPosition(res, rej, { timeout: 3000 })
);
lat = pos.coords.latitude;
lon = pos.coords.longitude;
} catch { /* デフォルト使用 */ }
const ac = new AbortController();
setTimeout(() => ac.abort(), 6000);
const r = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${lat.toFixed(4)}&longitude=${lon.toFixed(4)}&current=temperature_2m,weather_code&timezone=auto`,
{ signal: ac.signal }
);
if (!r.ok) throw new Error();
const data = await r.json();
const temp = Math.round(data.current.temperature_2m);
const info = wmoToInfo(data.current.weather_code);
document.getElementById('veilWeatherIcon').setAttribute('data-lucide', info.icon);
document.getElementById('veilTemp').textContent = `${temp}°`;
document.getElementById('veilWeatherLabel').textContent = info.label;
lucide.createIcons();
} catch {
document.getElementById('veilWeather').style.display = 'none';
}
}
// ── SW 登録 ─────────────────────────────────────────────────
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {});
}
// ── 初期化 ──────────────────────────────────────────────────
updateClock();
setInterval(updateClock, 1000);
fetchWeather();
setInterval(fetchWeather, 30 * 60 * 1000);
renderCatTabs();
renderApps();
</script>
</body>
</html>