minor UI fixes
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { usePersistentState } from "../lib/usePersistentState";
|
||||
|
||||
const MONTHS = [
|
||||
"Hammer", "Alturiak", "Ches", "Tarsakh", "Mirtul", "Kythorn",
|
||||
@@ -17,11 +18,52 @@ const COLORS = ["var(--color-gold-bright)", "var(--color-info)", "var(--color-su
|
||||
export function CalendarWidget() {
|
||||
const [month, setMonth] = useState(0);
|
||||
const [year, setYear] = useState(1492);
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||||
const [events, setEvents] = usePersistentState<CalendarEvent[]>("calendar.events", []);
|
||||
const [selectedDay, setSelectedDay] = useState<number | null>(null);
|
||||
const [newEvent, setNewEvent] = useState("");
|
||||
// ponytail: the campaign's current date, persisted so it survives reloads.
|
||||
// Advances with the "Next day" button; "Today" jumps the view to it.
|
||||
const [today, setToday] = usePersistentState<{ day: number; month: number; year: number }>(
|
||||
"calendar.today",
|
||||
{ day: 15, month: 5, year: 1492 },
|
||||
);
|
||||
|
||||
const today = { day: 15, month: 5 }; // 15th of Kythorn (arbitrary "today")
|
||||
// ponytail: 8 moon phases cycling on a ~30-day lunar cycle. Derived from
|
||||
// a continuous day index so the phase is stable for any date.
|
||||
const MOON_PHASES = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"];
|
||||
function moonPhase(day: number, month: number, year: number): string {
|
||||
const idx = Math.floor(((year * 360 + month * 30 + (day - 1)) % 30) / 30 * 8);
|
||||
return MOON_PHASES[((idx % 8) + 8) % 8];
|
||||
}
|
||||
|
||||
// ponytail: deterministic weather per day (stable for a given date).
|
||||
// A simple PRNG seeded by the date index so the forecast doesn't reshuffle
|
||||
// every render. Coarse by design — a DM can override narratively.
|
||||
const WEATHER = [
|
||||
"☀ Clear", "⛅ Fair", "☁ Overcast", "🌧 Light rain", "⛈ Storm",
|
||||
"❄ Snow", "🌫 Fog", "💨 Windy", "🌀 Unnatural",
|
||||
];
|
||||
function weatherFor(day: number, month: number, year: number): string {
|
||||
const seed = (year * 360 + month * 30 + (day - 1)) % 97;
|
||||
return WEATHER[seed % WEATHER.length];
|
||||
}
|
||||
|
||||
// Advance the campaign date by one day (30-day months, 12-month year).
|
||||
function nextDay() {
|
||||
setToday((t) => {
|
||||
let { day, month, year } = t;
|
||||
day += 1;
|
||||
if (day > 30) { day = 1; month += 1; }
|
||||
if (month > 11) { month = 0; year += 1; }
|
||||
return { day, month, year };
|
||||
});
|
||||
}
|
||||
|
||||
function jumpToToday() {
|
||||
setMonth(today.month);
|
||||
setYear(today.year);
|
||||
setSelectedDay(today.day);
|
||||
}
|
||||
|
||||
function addEvent() {
|
||||
if (!newEvent.trim() || selectedDay === null) return;
|
||||
@@ -62,6 +104,29 @@ export function CalendarWidget() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Campaign date controls */}
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="text-[10px] text-[var(--color-text-secondary)]">
|
||||
Today: {MONTHS[today.month]} {today.day}, {today.year} DR {moonPhase(today.day, today.month, today.year)}
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={jumpToToday}
|
||||
className="rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-1.5 py-0.5 text-[9px] cursor-pointer hover:text-[var(--color-gold-bright)]"
|
||||
title="Jump the view to today"
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
<button
|
||||
onClick={nextDay}
|
||||
className="rounded bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-1.5 py-0.5 text-[9px] font-semibold cursor-pointer hover:bg-[var(--color-gold-muted)]"
|
||||
title="Advance the campaign date by one day"
|
||||
>
|
||||
Next day →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Day grid */}
|
||||
<div className="grid grid-cols-7 gap-0.5">
|
||||
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((d) => (
|
||||
@@ -72,7 +137,7 @@ export function CalendarWidget() {
|
||||
{/* Start offset - Faerûn calendar starts on first of month */}
|
||||
{Array.from({ length: 30 }, (_, i) => {
|
||||
const dayNum = i + 1;
|
||||
const isToday = dayNum === today.day && month === today.month;
|
||||
const isToday = dayNum === today.day && month === today.month && year === today.year;
|
||||
const hasEvent = events.some((e) => e.day === dayNum && e.month === month);
|
||||
const isSelected = dayNum === selectedDay;
|
||||
|
||||
@@ -92,6 +157,7 @@ export function CalendarWidget() {
|
||||
{hasEvent && !isToday && (
|
||||
<span className="block w-1 h-1 rounded-full bg-[var(--color-gold-bright)] mx-auto mt-0" />
|
||||
)}
|
||||
<span className="block text-[7px] leading-none opacity-60">{moonPhase(dayNum, month, year)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -100,8 +166,13 @@ export function CalendarWidget() {
|
||||
{/* Events for selected day */}
|
||||
{selectedDay !== null && (
|
||||
<div className="flex-1 overflow-y-auto border-t border-[var(--color-border-subtle)] pt-2">
|
||||
<div className="text-[10px] font-medium text-[var(--color-text-secondary)] mb-1">
|
||||
{MONTHS[month]} {selectedDay}
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="text-[10px] font-medium text-[var(--color-text-secondary)]">
|
||||
{MONTHS[month]} {selectedDay}
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--color-gold-bright)]" title="Weather for this day">
|
||||
{weatherFor(selectedDay, month, year)}
|
||||
</span>
|
||||
</div>
|
||||
{dayEvents.length === 0 && (
|
||||
<div className="text-[var(--color-text-dim)] text-[10px] italic">No events</div>
|
||||
|
||||
Reference in New Issue
Block a user