// 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 (
{t.label}
); }; return (
{renderTab(TAB5[0])} {renderTab(TAB5[1])}
Create
{renderTab(TAB5[2])} {renderTab(TAB5[3])}
); }; // ───────────────────────────────────────────────────────────── // Full-viewport greeting hero. Earned midnight: greetingGradient lives // only here. One eyebrow, one narrative line, one signature metric, one // thin stat row — nothing else competes in the first viewport. // ───────────────────────────────────────────────────────────── const TodayHero = ({ name = 'Lukas', date = 'Sun · Mar 16', greeting, narrative = 'Three things matter today.', metricLabel = "Tonight's bedtime", metric = '10:30', metricUnit = 'PM', metricSub = 'Worked back from tomorrow — wake 6:15, an 8-hour night.', stats = [], empty = false, showMoon = true, grad, }) => (
{date}
{name[0]}
{greeting || `Good morning, ${name}`}
{narrative}
{showMoon &&
{metric}{metricUnit && {metricUnit}}
{metricSub &&
{metricSub}
} {stats.length > 0 && (
{stats.map((s, i) => (
{s.l}
{s.v}
{s.s}
))}
)}
); // ───────────────────────────────────────────────────────────── // Section header // ───────────────────────────────────────────────────────────── const TSection = ({ title, count, see }) => (
{title}{count !== undefined && {count}}
{see && {see} }
); // ───────────────────────────────────────────────────────────── // Today's arc — today-only timeline, past dimmed, Now marker. // ───────────────────────────────────────────────────────────── const TArcRow = ({ kind = 'next', time, suffix, title, location, feed }) => (
{time} {suffix}
{title}
{location &&
{location}
}
); const TArc = ({ items, nowLabel = '10:14', nowAfter }) => { let idx = nowAfter; if (idx === undefined) { const firstNext = items.findIndex(i => i.kind !== 'past'); idx = firstNext - 1; } return (
{items.map((it, i) => ( {i === idx && (
{nowLabel}
)} ))}
); }; // ───────────────────────────────────────────────────────────── // Module tile — chip + label + exactly one live datum. Never an icon // alone: the datum is what makes the shelf a command centre. // ───────────────────────────────────────────────────────────── const ModuleTile = ({ icon, chip, label, datumValue, datumRest, empty = false }) => (
{label}
{datumValue} {datumRest && {datumRest}}
); // ───────────────────────────────────────────────────────────── // Create sheet option row // ───────────────────────────────────────────────────────────── const CreateOpt = ({ icon, color, title, sub }) => (
{title}
{sub}
); Object.assign(window, { TAB5, TabBar5, TodayHero, TSection, TArc, TArcRow, ModuleTile, CreateOpt, HERO_PALETTES, HERO_PALETTE_LIST, HERO_PALETTE_REPS, gradientFor, greetingForTod, resolveTimeOfDay, paletteNameFrom, });