feat: Ambient風ヘッダーに刷新(時計・日付・天気・挨拶)

- 標準ヘッダーを廃止、時計ゾーンに置き換え
- 大型時刻表示(68px / weight:300)
- 日付・天気(open-meteo API)・挨拶を Ambient から移植
- 設定ボタンを時計ゾーン右上の丸ボタンに
- 検索バーを top:0 で sticky 化、カテゴリタブを top:52px で sticky 化
This commit is contained in:
posimai 2026-03-21 11:10:24 +09:00
parent d85b825bba
commit 22bb95a10a
1 changed files with 171 additions and 11 deletions

View File

@ -31,11 +31,67 @@
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
<style>
/* ── Clock zone ── */
.veil-clock {
padding: 28px 20px 16px;
position: relative;
}
.veil-time {
font-size: 68px;
font-weight: 300;
letter-spacing: -3px;
line-height: 1;
color: var(--text);
}
.veil-sub {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
font-size: 13px;
color: var(--text2);
margin-top: 8px;
}
.veil-sep { color: var(--text3); }
.veil-weather {
display: inline-flex;
align-items: center;
gap: 4px;
}
.veil-weather-icon {
width: 14px;
height: 14px;
stroke: var(--text2);
stroke-width: 1.75;
}
.veil-greeting {
font-size: 13px;
color: var(--text3);
margin-top: 4px;
}
.veil-settings-btn {
position: absolute;
top: 20px;
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: 52px;
top: 0;
z-index: 10;
background: var(--bg);
}
@ -71,6 +127,10 @@
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 {
@ -372,18 +432,26 @@
</aside>
<div class="overlay" id="overlay" aria-hidden="true"></div>
<header class="header">
<div class="header-brand">
<div class="header-dot" aria-hidden="true"></div>
<span class="header-title">Veil</span>
</div>
<button class="icon-btn" id="settingsBtn" aria-label="設定" aria-expanded="false">
<i data-lucide="settings" style="width:18px;height:18px;stroke-width:1.5"></i>
</button>
</header>
<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">
@ -816,12 +884,104 @@ function showToast(msg) {
setTimeout(() => el.classList.remove('show'), 2200);
}
// ── 時計 ────────────────────────────────────────────────────
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>