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:
@@ -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>
|
||||
|
||||
32
src/components/calendar/eventColors.ts
Normal file
32
src/components/calendar/eventColors.ts
Normal 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];
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -7,44 +7,44 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Skylight-style warm light theme.
|
||||
// Cool light theme (slate-based neutrals + blue accent).
|
||||
// Token names kept as "dark.*" so existing components don't need class changes.
|
||||
dark: {
|
||||
primary: '#faf6f0', // page bg - warm cream
|
||||
primary: '#f1f5f9', // page bg - slate-100
|
||||
secondary: '#ffffff', // card bg - white
|
||||
tertiary: '#f5ede1', // subtle panels / compact rows
|
||||
tertiary: '#e2e8f0', // subtle panels - slate-200
|
||||
elevated: '#ffffff', // elevated cards
|
||||
hover: '#ede2cf', // hover states
|
||||
border: '#ebe0cc', // soft warm borders
|
||||
'border-light': '#d9cab0',
|
||||
hover: '#e2e8f0',
|
||||
border: '#e2e8f0', // cool soft border
|
||||
'border-light': '#cbd5e1',
|
||||
},
|
||||
// Sage accent (Skylight uses muted greens/peach, not blue)
|
||||
// Blue accent
|
||||
accent: {
|
||||
DEFAULT: '#7fa894', // sage
|
||||
light: '#a0c2b1',
|
||||
dark: '#5e8a76',
|
||||
DEFAULT: '#3b82f6', // blue-500
|
||||
light: '#60a5fa',
|
||||
dark: '#2563eb',
|
||||
},
|
||||
// Warm ink text scale
|
||||
// Cool slate ink text scale
|
||||
ink: {
|
||||
DEFAULT: '#3a322a', // primary dark text
|
||||
muted: '#6b5f51',
|
||||
subtle: '#8a7d6e',
|
||||
faint: '#b0a494',
|
||||
DEFAULT: '#0f172a', // slate-900
|
||||
muted: '#475569', // slate-600
|
||||
subtle: '#64748b', // slate-500
|
||||
faint: '#94a3b8', // slate-400
|
||||
},
|
||||
status: {
|
||||
success: '#4a9d7a', // muted green
|
||||
warning: '#d99a4a', // warm amber
|
||||
error: '#c75b5b', // muted brick
|
||||
success: '#16a34a', // green-600
|
||||
warning: '#d97706', // amber-600
|
||||
error: '#dc2626', // red-600
|
||||
},
|
||||
// Legacy imperial tokens retained so old references don't break
|
||||
imperial: {
|
||||
black: '#faf6f0',
|
||||
dark: '#f5ede1',
|
||||
medium: '#ebe0cc',
|
||||
light: '#d9cab0',
|
||||
red: '#c75b5b',
|
||||
'red-dark': '#a84545',
|
||||
'red-light': '#d97777',
|
||||
black: '#f1f5f9',
|
||||
dark: '#e2e8f0',
|
||||
medium: '#cbd5e1',
|
||||
light: '#94a3b8',
|
||||
red: '#dc2626',
|
||||
'red-dark': '#b91c1c',
|
||||
'red-light': '#ef4444',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
|
||||
Reference in New Issue
Block a user