Cool-light theme, Google event colors, photo frame contain+lower-right clock

- Main app swapped from warm cream/sage to cool slate light theme with
  blue accent; dark-* tokens and text-white/gray overrides remapped in
  tailwind.config.js and index.css
- Calendar events now render in Google's 11-color palette (Tomato through
  Graphite), deterministically hashed from the event summary so the same
  event always gets the same color
- PhotoFrame uses object-contain (whole photo shown, letterboxed) instead
  of object-cover; clock + date moved to lower-right, same white color,
  text-shadow for readability over any photo
- EMAIL_UPLOAD.md / PHOTO_FRAME.md / iCloud sync script and systemd timer
  remain unchanged
This commit is contained in:
root
2026-04-15 02:18:43 -05:00
parent d0db8c55b3
commit 7b36551c32
5 changed files with 105 additions and 65 deletions

View File

@@ -2,10 +2,15 @@ import { useState, useMemo, useCallback, useEffect } from 'react';
import { format, startOfMonth, endOfMonth, startOfWeek, endOfWeek, addDays, isSameMonth, isSameDay, isToday } from 'date-fns';
import { useCalendar, CalendarEvent, formatEventTime } from '@/hooks/useCalendar';
import { VirtualKeyboard } from '@/components/keyboard';
import { getEventColor } from './eventColors';
function EventItem({ event }: { event: CalendarEvent }) {
const color = getEventColor(event.summary);
return (
<div className="text-[0.6rem] truncate px-1 py-0.5 rounded bg-accent/20 text-accent-light">
<div
className="text-[0.6rem] truncate px-1 py-0.5 rounded"
style={{ backgroundColor: color.bg, color: color.text }}
>
{formatEventTime(event)} {event.summary}
</div>
);
@@ -74,21 +79,28 @@ function EventDetails({ date, events, onAddEvent }: { date: Date; events: Calend
<p className="text-xs text-gray-500">No events</p>
) : (
<div className="space-y-1.5 max-h-24 overflow-y-auto">
{events.map((event) => (
<div key={event.id} className="text-xs">
<div className="flex items-center gap-2">
<span className="text-accent font-medium">
{formatEventTime(event)}
</span>
<span className="flex-1 truncate">{event.summary}</span>
</div>
{event.location && (
<div className="text-[0.65rem] text-gray-500 truncate ml-12">
{event.location}
{events.map((event) => {
const color = getEventColor(event.summary);
return (
<div key={event.id} className="text-xs">
<div className="flex items-center gap-2">
<span
className="inline-block w-2 h-2 rounded-full shrink-0"
style={{ backgroundColor: color.bg }}
/>
<span className="font-medium" style={{ color: color.bg }}>
{formatEventTime(event)}
</span>
<span className="flex-1 truncate">{event.summary}</span>
</div>
)}
</div>
))}
{event.location && (
<div className="text-[0.65rem] text-gray-500 truncate ml-12">
{event.location}
</div>
)}
</div>
);
})}
</div>
)}
</div>

View File

@@ -0,0 +1,32 @@
// Google Calendar's 11 event colors, values lifted from the web client
// color picker. Colors are assigned deterministically by hashing the event
// summary so each recurring event always renders in the same color.
const GOOGLE_EVENT_COLORS = [
{ name: 'Tomato', bg: '#d50000', text: '#ffffff' },
{ name: 'Flamingo', bg: '#e67c73', text: '#ffffff' },
{ name: 'Tangerine', bg: '#f4511e', text: '#ffffff' },
{ name: 'Banana', bg: '#f6bf26', text: '#202124' },
{ name: 'Sage', bg: '#33b679', text: '#ffffff' },
{ name: 'Basil', bg: '#0b8043', text: '#ffffff' },
{ name: 'Peacock', bg: '#039be5', text: '#ffffff' },
{ name: 'Blueberry', bg: '#3f51b5', text: '#ffffff' },
{ name: 'Lavender', bg: '#7986cb', text: '#ffffff' },
{ name: 'Grape', bg: '#8e24aa', text: '#ffffff' },
{ name: 'Graphite', bg: '#616161', text: '#ffffff' },
] as const;
export type EventColor = (typeof GOOGLE_EVENT_COLORS)[number];
function hash(str: string): number {
let h = 2166136261;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
export function getEventColor(key: string): EventColor {
const idx = hash(key.toLowerCase().trim()) % GOOGLE_EVENT_COLORS.length;
return GOOGLE_EVENT_COLORS[idx];
}

View File

@@ -137,7 +137,7 @@ export function PhotoFrame({ intervalMs = 15_000, transitionMs = 1_200 }: PhotoF
key={`prev-${prevSrc}`}
src={prevSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover animate-ken-burns"
className="absolute inset-0 w-full h-full object-contain animate-ken-burns"
style={{ opacity: 1 }}
/>
)}
@@ -146,20 +146,18 @@ export function PhotoFrame({ intervalMs = 15_000, transitionMs = 1_200 }: PhotoF
key={`cur-${index}`}
src={currentSrc}
alt=""
className="absolute inset-0 w-full h-full object-cover animate-ken-burns"
className="absolute inset-0 w-full h-full object-contain animate-ken-burns"
style={{
opacity: 0,
animation: `photo-fade-in ${transitionMs}ms ease-out forwards, ken-burns 20s ease-in-out infinite alternate`,
}}
/>
)}
{/* Gradient + centered clock/date overlay (Skylight style) */}
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-56 bg-gradient-to-t from-black/75 via-black/30 to-transparent" />
<div className="pointer-events-none absolute inset-x-0 bottom-14 flex justify-center">
<div className="px-10 py-5 rounded-3xl bg-black/25 backdrop-blur-md border border-white/10 text-white text-center drop-shadow-2xl">
<div className="text-7xl md:text-8xl font-light tracking-tight leading-none">{timeStr}</div>
<div className="text-2xl md:text-3xl mt-3 capitalize font-light opacity-95">{dateStr}</div>
</div>
{/* Lower-right clock + date overlay */}
<div className="pointer-events-none absolute bottom-8 right-10 text-right text-white leading-tight"
style={{ textShadow: '0 2px 12px rgba(0,0,0,0.9), 0 0 4px rgba(0,0,0,0.7)' }}>
<div className="text-6xl md:text-7xl font-light tracking-tight">{timeStr}</div>
<div className="text-xl md:text-2xl mt-1 capitalize font-light">{dateStr}</div>
</div>
<style>{`
@keyframes photo-fade-in {

View File

@@ -266,23 +266,21 @@
}
/*
* Theme remap: existing components use text-white and text-gray-* for
* dark-theme text. For the Skylight light theme, rebind those utilities
* to the warm ink scale so every component becomes readable on cream bg
* without touching hundreds of component files.
* Theme remap (cool light): existing components use text-white and
* text-gray-* for dark-theme text. Rebind to cool slate ink so the UI
* is readable on the slate-100 bg without touching every component.
*/
.text-white { color: #3a322a; }
.text-gray-300 { color: #4a4239; }
.text-gray-400 { color: #6b5f51; }
.text-gray-500 { color: #8a7d6e; }
.text-gray-600 { color: #a0937f; }
.text-white { color: #0f172a; }
.text-gray-300 { color: #1e293b; }
.text-gray-400 { color: #475569; }
.text-gray-500 { color: #64748b; }
.text-gray-600 { color: #94a3b8; }
/* Dark backgrounds that were nearly-black in the old theme */
.bg-imperial-black { background-color: #faf6f0; }
.bg-black { background-color: #2a2520; }
.bg-imperial-black { background-color: #f1f5f9; }
.bg-black { background-color: #0f172a; }
.text-ink { color: #3a322a; }
.text-ink-muted { color: #6b5f51; }
.text-ink-subtle { color: #8a7d6e; }
.text-ink-faint { color: #b0a494; }
.text-ink { color: #0f172a; }
.text-ink-muted { color: #475569; }
.text-ink-subtle { color: #64748b; }
.text-ink-faint { color: #94a3b8; }
}