2026-03-21 01:57:57 +00:00
|
|
|
|
<!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>
|
2026-03-21 02:10:24 +00:00
|
|
|
|
/* ── Clock zone ── */
|
|
|
|
|
|
.veil-clock {
|
2026-03-21 02:31:26 +00:00
|
|
|
|
padding: max(20px, env(safe-area-inset-top)) 16px 8px;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
.veil-time {
|
2026-03-21 02:31:26 +00:00
|
|
|
|
font-size: clamp(56px, 18vw, 80px);
|
2026-03-21 02:10:24 +00:00
|
|
|
|
font-weight: 300;
|
2026-03-21 02:31:26 +00:00
|
|
|
|
letter-spacing: -0.03em;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
line-height: 1;
|
|
|
|
|
|
color: var(--text);
|
2026-03-21 02:31:26 +00:00
|
|
|
|
font-variant-numeric: tabular-nums;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
.veil-sub {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-03-21 02:31:26 +00:00
|
|
|
|
gap: 8px;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--text2);
|
2026-03-21 02:31:26 +00:00
|
|
|
|
margin-top: 4px;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
}
|
2026-03-21 02:31:26 +00:00
|
|
|
|
.veil-sep { color: var(--border); }
|
2026-03-21 02:10:24 +00:00
|
|
|
|
.veil-weather {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.veil-weather-icon {
|
2026-03-21 02:31:26 +00:00
|
|
|
|
width: 13px;
|
|
|
|
|
|
height: 13px;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
stroke: var(--text2);
|
2026-03-21 02:31:26 +00:00
|
|
|
|
stroke-width: 2;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
.veil-greeting {
|
2026-03-21 02:31:26 +00:00
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 300;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
color: var(--text3);
|
|
|
|
|
|
margin-top: 4px;
|
2026-03-21 02:31:26 +00:00
|
|
|
|
min-height: 16px;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
.veil-settings-btn {
|
|
|
|
|
|
position: absolute;
|
2026-03-21 02:31:26 +00:00
|
|
|
|
top: max(16px, env(safe-area-inset-top));
|
2026-03-21 02:10:24 +00:00
|
|
|
|
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); }
|
|
|
|
|
|
|
2026-03-21 11:49:14 +00:00
|
|
|
|
/* ── Toolbar (カテゴリ + アクションボタン統合行) ── */
|
|
|
|
|
|
.toolbar-wrap {
|
2026-03-21 01:57:57 +00:00
|
|
|
|
position: sticky;
|
2026-03-21 02:10:24 +00:00
|
|
|
|
top: 0;
|
2026-03-21 11:49:14 +00:00
|
|
|
|
z-index: 9;
|
2026-03-21 01:57:57 +00:00
|
|
|
|
background: var(--bg);
|
|
|
|
|
|
}
|
2026-03-21 11:49:14 +00:00
|
|
|
|
.toolbar {
|
2026-03-21 01:57:57 +00:00
|
|
|
|
display: flex;
|
2026-03-21 11:49:14 +00:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 6px 16px;
|
2026-03-21 01:57:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
.cat-scroll {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
scrollbar-width: none;
|
2026-03-21 11:49:14 +00:00
|
|
|
|
flex: 1;
|
2026-03-21 01:57:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
.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);
|
2026-03-25 07:28:03 +00:00
|
|
|
|
color: var(--bg);
|
2026-03-21 01:57:57 +00:00
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
2026-03-21 11:49:14 +00:00
|
|
|
|
.toolbar-actions {
|
2026-03-21 01:57:57 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-03-21 11:49:14 +00:00
|
|
|
|
gap: 6px;
|
|
|
|
|
|
flex-shrink: 0;
|
2026-03-21 02:23:56 +00:00
|
|
|
|
}
|
2026-03-21 11:49:14 +00:00
|
|
|
|
.toolbar-search-btn {
|
|
|
|
|
|
width: 30px;
|
|
|
|
|
|
height: 30px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--text2);
|
2026-03-21 02:23:56 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-03-21 11:49:14 +00:00
|
|
|
|
justify-content: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
transition: color 0.12s, border-color 0.12s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toolbar-search-btn.active {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Search expand (折りたたみ) ── */
|
|
|
|
|
|
.search-expand {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
max-height: 0;
|
|
|
|
|
|
padding: 0 16px;
|
|
|
|
|
|
transition: max-height 0.2s cubic-bezier(0.2, 0.9, 0.2, 1),
|
|
|
|
|
|
padding 0.2s cubic-bezier(0.2, 0.9, 0.2, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.search-expand.open {
|
|
|
|
|
|
max-height: 56px;
|
|
|
|
|
|
padding: 4px 16px 8px;
|
2026-03-21 01:57:57 +00:00
|
|
|
|
}
|
2026-03-21 11:49:14 +00:00
|
|
|
|
.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;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
transition: border-color 0.15s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.search-input:focus { border-color: var(--accent); }
|
2026-03-21 01:57:57 +00:00
|
|
|
|
.edit-toggle-btn {
|
2026-03-21 12:05:10 +00:00
|
|
|
|
width: 30px;
|
|
|
|
|
|
height: 30px;
|
|
|
|
|
|
border-radius: 50%;
|
2026-03-21 01:57:57 +00:00
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--text2);
|
2026-03-21 12:05:10 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-03-21 01:57:57 +00:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.12s;
|
2026-03-21 02:23:56 +00:00
|
|
|
|
flex-shrink: 0;
|
2026-03-21 01:57:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
.edit-toggle-btn.active {
|
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
/* ── 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); }
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
/* ── 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
/* ── 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
.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; }
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
/* ── 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; }
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
/* ── 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; }
|
|
|
|
|
|
|
2026-03-21 02:06:48 +00:00
|
|
|
|
/* ── 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%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
/* ── 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);
|
2026-03-25 07:28:03 +00:00
|
|
|
|
color: var(--bg);
|
2026-03-21 01:57:57 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
/* ── 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);
|
2026-03-25 07:28:03 +00:00
|
|
|
|
color: var(--bg);
|
2026-03-21 02:23:56 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 04:17:53 +00:00
|
|
|
|
/* ── 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;
|
|
|
|
|
|
}
|
2026-03-21 01:57:57 +00:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<a href="#main-content" class="skip-link" tabindex="0"
|
2026-03-25 07:28:03 +00:00
|
|
|
|
style="position:absolute;top:-100%;left:8px;background:var(--accent);color:var(--bg);padding:8px 16px;border-radius:8px;font-weight:600;font-size:13px;z-index:10000;text-decoration:none">
|
2026-03-21 01:57:57 +00:00
|
|
|
|
コンテンツへスキップ
|
|
|
|
|
|
</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>
|
|
|
|
|
|
|
2026-03-21 02:06:48 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
<main id="main-content">
|
|
|
|
|
|
|
2026-03-21 02:10:24 +00:00
|
|
|
|
<!-- 時計・天気・挨拶ゾーン -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-03-21 11:49:14 +00:00
|
|
|
|
<div class="toolbar-wrap">
|
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
|
<div class="cat-scroll" id="catScroll" role="tablist" aria-label="カテゴリフィルタ"></div>
|
|
|
|
|
|
<div class="toolbar-actions">
|
|
|
|
|
|
<button class="toolbar-search-btn" id="searchBtn" aria-label="検索">
|
|
|
|
|
|
<i data-lucide="search" style="width:15px;height:15px;stroke-width:1.75"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="add-app-btn" id="addAppBtn" aria-label="アプリを追加">
|
|
|
|
|
|
<i data-lucide="plus" style="width:13px;height:13px;stroke-width:2.5"></i>
|
|
|
|
|
|
アプリを追加
|
|
|
|
|
|
</button>
|
2026-03-21 12:05:10 +00:00
|
|
|
|
<button class="edit-toggle-btn" id="editToggleBtn" aria-label="編集">
|
|
|
|
|
|
<i data-lucide="square-pen" style="width:15px;height:15px;stroke-width:1.75"></i>
|
|
|
|
|
|
</button>
|
2026-03-21 11:49:14 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="search-expand" id="searchExpand">
|
2026-03-21 01:57:57 +00:00
|
|
|
|
<input class="search-input" id="searchInput" type="search"
|
|
|
|
|
|
placeholder="アプリを検索" autocomplete="off" autocorrect="off" autocapitalize="off">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</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 — アプリランチャー
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-21 02:06:48 +00:00
|
|
|
|
const ENABLED_KEY = 'posimai-veil-enabled';
|
2026-03-21 02:23:56 +00:00
|
|
|
|
const CUSTOM_KEY = 'posimai-veil-custom';
|
2026-03-21 02:06:48 +00:00
|
|
|
|
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: {
|
2026-03-21 02:23:56 +00:00
|
|
|
|
posimai: '#6EE7B7',
|
|
|
|
|
|
sns: '#60A5FA',
|
|
|
|
|
|
media: '#F472B6',
|
|
|
|
|
|
news: '#FB923C',
|
|
|
|
|
|
tools: '#94A3B8',
|
|
|
|
|
|
nav: '#4ADE80',
|
|
|
|
|
|
shop: '#FBBF24',
|
2026-03-21 02:06:48 +00:00
|
|
|
|
},
|
|
|
|
|
|
light: {
|
2026-03-21 02:23:56 +00:00
|
|
|
|
posimai: '#059669',
|
|
|
|
|
|
sns: '#2563EB',
|
|
|
|
|
|
media: '#DB2777',
|
|
|
|
|
|
news: '#EA580C',
|
|
|
|
|
|
tools: '#475569',
|
|
|
|
|
|
nav: '#16A34A',
|
|
|
|
|
|
shop: '#D97706',
|
2026-03-21 02:06:48 +00:00
|
|
|
|
},
|
|
|
|
|
|
};
|
2026-03-21 01:57:57 +00:00
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
// ── アイコンピッカー候補リスト ──────────────────────────────
|
|
|
|
|
|
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',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
// ── アプリ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'];
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
// ── カスタムアプリ ──────────────────────────────────────────
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
// ── 状態 ────────────────────────────────────────────────────
|
|
|
|
|
|
let enabledIds = loadEnabled();
|
|
|
|
|
|
let activeCat = 'all';
|
|
|
|
|
|
let searchQ = '';
|
|
|
|
|
|
let editMode = false;
|
2026-03-21 02:23:56 +00:00
|
|
|
|
let colorMode = localStorage.getItem(COLOR_MODE_KEY) || 'accent';
|
2026-03-21 01:57:57 +00:00
|
|
|
|
let apiKey = localStorage.getItem(API_KEY_KEY) || '';
|
|
|
|
|
|
|
2026-03-21 02:06:48 +00:00
|
|
|
|
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)';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
function loadEnabled() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const raw = localStorage.getItem(ENABLED_KEY);
|
|
|
|
|
|
if (raw) return new Set(JSON.parse(raw));
|
|
|
|
|
|
} catch {}
|
2026-03-21 02:23:56 +00:00
|
|
|
|
// デフォルト: 全 ON(組み込み + カスタム)
|
|
|
|
|
|
const base = new Set(APP_DB.map(a => a.id));
|
|
|
|
|
|
customApps.forEach(a => base.add(a.id));
|
|
|
|
|
|
return base;
|
2026-03-21 01:57:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function saveEnabled() {
|
|
|
|
|
|
localStorage.setItem(ENABLED_KEY, JSON.stringify([...enabledIds]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
function allApps() {
|
|
|
|
|
|
return [...APP_DB, ...customApps];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
// ── アプリ起動 ──────────────────────────────────────────────
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
// ── カスタムアプリ削除 ──────────────────────────────────────
|
|
|
|
|
|
function deleteCustomApp(id) {
|
|
|
|
|
|
customApps = customApps.filter(a => a.id !== id);
|
|
|
|
|
|
saveCustomApps();
|
|
|
|
|
|
enabledIds.delete(id);
|
|
|
|
|
|
saveEnabled();
|
|
|
|
|
|
renderApps();
|
|
|
|
|
|
showToast('削除しました');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
// ── カテゴリタブ描画 ────────────────────────────────────────
|
|
|
|
|
|
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() {
|
2026-03-21 02:23:56 +00:00
|
|
|
|
const container = document.getElementById('appSections');
|
2026-03-21 11:49:14 +00:00
|
|
|
|
const editBtn = document.getElementById('editToggleBtn');
|
|
|
|
|
|
const addBtn = document.getElementById('addAppBtn');
|
2026-03-21 01:57:57 +00:00
|
|
|
|
|
|
|
|
|
|
editBtn.classList.toggle('active', editMode);
|
2026-03-21 02:23:56 +00:00
|
|
|
|
addBtn.classList.toggle('visible', editMode);
|
2026-03-21 01:57:57 +00:00
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
const q = searchQ.toLowerCase();
|
|
|
|
|
|
const source = allApps();
|
|
|
|
|
|
const filtered = source.filter(app => {
|
2026-03-21 01:57:57 +00:00
|
|
|
|
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 => {
|
2026-03-21 02:23:56 +00:00
|
|
|
|
const apps = groups[cat];
|
|
|
|
|
|
const color = getCatColor(cat);
|
2026-03-21 02:06:48 +00:00
|
|
|
|
const isColorful = colorMode === 'colorful';
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
return `
|
|
|
|
|
|
<div class="app-section">
|
2026-03-21 02:06:48 +00:00
|
|
|
|
<div class="section-label"
|
|
|
|
|
|
style="${isColorful ? `color:${color};opacity:.9` : ''}">${CAT_LABELS[cat] || cat}</div>
|
2026-03-21 01:57:57 +00:00
|
|
|
|
<div class="app-grid">
|
|
|
|
|
|
${apps.map(app => {
|
2026-03-21 02:23:56 +00:00
|
|
|
|
const on = enabledIds.has(app.id);
|
2026-03-21 01:57:57 +00:00
|
|
|
|
const hidden = !editMode && !on ? ' hidden' : '';
|
|
|
|
|
|
const editCls = editMode ? ` edit-mode ${on ? 'on' : 'off'}` : '';
|
2026-03-21 02:06:48 +00:00
|
|
|
|
const bgStyle = isColorful
|
|
|
|
|
|
? `background:${color}14;border-color:${color}38;`
|
|
|
|
|
|
: '';
|
2026-03-21 02:23:56 +00:00
|
|
|
|
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}を削除">✕</button>`
|
|
|
|
|
|
: '';
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
return `
|
|
|
|
|
|
<div class="app-item${editCls}${hidden}"
|
|
|
|
|
|
data-id="${app.id}" role="button" tabindex="0"
|
2026-03-21 02:06:48 +00:00
|
|
|
|
aria-label="${app.label}"
|
2026-03-21 02:23:56 +00:00
|
|
|
|
style="${bgStyle}">
|
|
|
|
|
|
${delBtn}
|
|
|
|
|
|
${iconHTML}
|
2026-03-21 01:57:57 +00:00
|
|
|
|
<span class="app-label">${app.label}</span>
|
2026-03-21 02:06:48 +00:00
|
|
|
|
<span class="check-badge" aria-hidden="true"
|
|
|
|
|
|
style="background:${color}">
|
2026-03-21 01:57:57 +00:00
|
|
|
|
<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;
|
2026-03-21 02:23:56 +00:00
|
|
|
|
const app = allApps().find(a => a.id === id);
|
2026-03-21 01:57:57 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
container.querySelectorAll('.custom-del-btn').forEach(btn => {
|
|
|
|
|
|
btn.addEventListener('click', e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
deleteCustomApp(btn.dataset.delId);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
lucide.createIcons();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 編集モードトグル ────────────────────────────────────────
|
|
|
|
|
|
document.getElementById('editToggleBtn').addEventListener('click', () => {
|
|
|
|
|
|
editMode = !editMode;
|
|
|
|
|
|
renderApps();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-21 11:49:14 +00:00
|
|
|
|
// ── 検索(折りたたみ) ──────────────────────────────────────
|
|
|
|
|
|
const searchBtn = document.getElementById('searchBtn');
|
|
|
|
|
|
const searchExpand = document.getElementById('searchExpand');
|
|
|
|
|
|
const searchInput = document.getElementById('searchInput');
|
|
|
|
|
|
|
|
|
|
|
|
searchBtn.addEventListener('click', () => {
|
|
|
|
|
|
const open = searchExpand.classList.toggle('open');
|
|
|
|
|
|
searchBtn.classList.toggle('active', open);
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
searchInput.focus();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
searchInput.value = '';
|
|
|
|
|
|
searchQ = '';
|
|
|
|
|
|
renderCatTabs();
|
|
|
|
|
|
renderApps();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
searchInput.addEventListener('input', e => {
|
2026-03-21 01:57:57 +00:00
|
|
|
|
searchQ = e.target.value;
|
|
|
|
|
|
if (searchQ) activeCat = 'all';
|
|
|
|
|
|
renderCatTabs();
|
|
|
|
|
|
renderApps();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-21 02:06:48 +00:00
|
|
|
|
// ── カラーモード切替 ────────────────────────────────────────
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
// ── 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', () => {
|
2026-03-21 02:23:56 +00:00
|
|
|
|
enabledIds = new Set(allApps().map(a => a.id));
|
2026-03-21 01:57:57 +00:00
|
|
|
|
saveEnabled();
|
|
|
|
|
|
renderApps();
|
|
|
|
|
|
showToast('リセットしました');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── トースト ────────────────────────────────────────────────
|
|
|
|
|
|
function showToast(msg) {
|
|
|
|
|
|
const el = document.getElementById('toast');
|
|
|
|
|
|
el.textContent = msg;
|
|
|
|
|
|
el.classList.add('show');
|
|
|
|
|
|
setTimeout(() => el.classList.remove('show'), 2200);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 02:23:56 +00:00
|
|
|
|
// ── アプリ追加モーダル ───────────────────────────────────────
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-03-21 02:10:24 +00:00
|
|
|
|
// ── 時計 ────────────────────────────────────────────────────
|
|
|
|
|
|
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)}¤t=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';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
// ── SW 登録 ─────────────────────────────────────────────────
|
|
|
|
|
|
if ('serviceWorker' in navigator) {
|
|
|
|
|
|
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 初期化 ──────────────────────────────────────────────────
|
2026-03-21 02:10:24 +00:00
|
|
|
|
updateClock();
|
|
|
|
|
|
setInterval(updateClock, 1000);
|
|
|
|
|
|
fetchWeather();
|
|
|
|
|
|
setInterval(fetchWeather, 30 * 60 * 1000);
|
|
|
|
|
|
|
2026-03-21 01:57:57 +00:00
|
|
|
|
renderCatTabs();
|
|
|
|
|
|
renderApps();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|