// home/lib.jsx — "Today" tab primitives (locked redesign spec). // // Exports (window.*): // TabBar5 — the single global navigator: Today · Calendar · ✛ · Health · You // TodayHero — full-viewport greeting hero: eyebrow · narrative · signature // metric · one thin stat row // TSection — Today section header (editorial) // TArc — today-only event timeline with a Now marker // ModuleTile — shelf tile: domain chip + label + exactly one live datum // CreateOpt — a row inside the Create sheet // // Voice: quiet, plain, declarative. Sentence case. Em-dashes for pivots. // ──────────────────────────────────────────────────────────── // Time-of-day greeting gradients. The hero's midnight identity shifts gently // through the day — the same idea as the web Home greeting. Each palette is a // soft, mid-tone family (no near-black) so white type rests on it without a // harsh edge. gradientFor(palette, timeOfDay) → a CSS background string. // Palettes are exposed as a Tweak; "Auto" reads the real clock. // ──────────────────────────────────────────────────────────── const HERO_PALETTES = { 'Soft purple': { rep: ['#6E5C99', '#4E4391', '#2F2866'], dawn: ['#4A3B6E', '#5B4A82', '#6E5C99'], morning: ['#3E3470', '#4E4391', '#6256A8'], afternoon: ['#3A3E80', '#4A52A0', '#5E6BBE'], evening: ['#46306E', '#5E3F88', '#7A52A0'], night: ['#241E4E', '#2F2866', '#3C3480'], }, 'Twilight': { rep: ['#9A5E72', '#6A4A7E', '#3E2A55'], dawn: ['#5E3A63', '#7A4A6E', '#9A5E72'], morning: ['#4E3A66', '#6A4A7E', '#8A5E92'], afternoon: ['#4A3E72', '#64508E', '#8466A8'], evening: ['#5A2E5E', '#7E3E72', '#A05284'], night: ['#2E1E44', '#3E2A55', '#4E3568'], }, 'Cool indigo': { rep: ['#5E73A6', '#3E4E8E', '#26305E'], dawn: ['#39456E', '#4A5A88', '#5E73A6'], morning: ['#2E3A66', '#3E4E8E', '#5266B0'], afternoon: ['#2C4078', '#3A55A0', '#4E72C4'], evening: ['#33356E', '#454088', '#5A52A8'], night: ['#1C2244', '#26305E', '#323F7A'], }, }; const HERO_PALETTE_LIST = Object.keys(HERO_PALETTES); const HERO_PALETTE_REPS = HERO_PALETTE_LIST.map(function (n) { return HERO_PALETTES[n].rep; }); const HERO_REP_TO_NAME = {}; HERO_PALETTE_LIST.forEach(function (n) { HERO_REP_TO_NAME[JSON.stringify(HERO_PALETTES[n].rep).toLowerCase()] = n; }); function _heroHexToRgb(h) { h = String(h).replace('#', ''); if (h.length === 3) h = h.split('').map(function (c) { return c + c; }).join(''); const n = parseInt(h, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; } function _heroLightenRgba(h, amt, a) { const c = _heroHexToRgb(h); const f = function (v) { return Math.round(v + (255 - v) * amt); }; return 'rgba(' + f(c[0]) + ',' + f(c[1]) + ',' + f(c[2]) + ',' + a + ')'; } function resolveTimeOfDay(tod) { if (tod && tod !== 'Auto') return String(tod).toLowerCase(); const h = new Date().getHours(); if (h < 5) return 'night'; if (h < 8) return 'dawn'; if (h < 12) return 'morning'; if (h < 17) return 'afternoon'; if (h < 21) return 'evening'; return 'night'; } function paletteNameFrom(palette) { if (Array.isArray(palette)) return HERO_REP_TO_NAME[JSON.stringify(palette).toLowerCase()] || 'Soft purple'; return HERO_PALETTES[palette] ? palette : 'Soft purple'; } function gradientFor(palette, tod) { const name = paletteNameFrom(palette); const key = resolveTimeOfDay(tod); const stops = HERO_PALETTES[name][key] || HERO_PALETTES[name].morning; const a = stops[0], b = stops[1], c = stops[2]; const topGlow = _heroLightenRgba(c, 0.32, 0.42); const botGlow = _heroLightenRgba(b, 0.06, 0.30); return 'radial-gradient(120% 75% at 82% -8%, ' + topGlow + ', transparent 58%), ' + 'radial-gradient(90% 70% at 6% 108%, ' + botGlow + ', transparent 60%), ' + 'linear-gradient(157deg, ' + a + ' 0%, ' + b + ' 52%, ' + c + ' 100%)'; } function greetingForTod(tod, name) { const key = resolveTimeOfDay(tod); const g = key === 'afternoon' ? 'Good afternoon' : key === 'evening' ? 'Good evening' : key === 'night' ? 'Winding down' : 'Good morning'; return g + ', ' + name; } // ───────────────────────────────────────────────────────────── // 5-item tab bar. The center ✛ is a primary action (blue), not a // destination — no selected state. Destination tabs swap to a filled // glyph when active. Active tint defaults to a single blue accent // (single-accent discipline); `tint="domain"` previews per-domain. // ───────────────────────────────────────────────────────────── const TAB5 = [ { id: 'today', label: 'Today', icon: 'home', iconActive: 'home-filled', domain: 'var(--io-indigo)' }, { id: 'calendar', label: 'Calendar', icon: 'calendar', iconActive: 'calendar-filled', domain: 'var(--io-blue)' }, { id: 'health', label: 'Health', icon: 'heart', iconActive: 'heart-filled', domain: 'var(--io-red)' }, { id: 'you', label: 'You', icon: 'user', iconActive: 'person-filled', domain: 'var(--io-label)' }, ]; const TabBar5 = ({ active = 'today', tint = 'blue', onCreate }) => { const renderTab = (t) => { const isActive = t.id === active; const perDomain = tint === 'domain'; const style = perDomain && isActive ? { '--tab-tint': t.domain } : undefined; return (