v0.5.0-dm-tools: Initiative tracker, random tables, encounter builder, calendar
- Initiative Tracker: add/remove combatants, roll initiative, HP bars with color transitions (green→gold→red), condition badges, round counter, next-turn cycling, gold active-state border - Random Tables: 4 built-in tables (Tavern Names, Weather, NPC Quirks, Treasure), dice notation parser, roll history, highlight matching result - Encounter Builder: AI-generated encounters via LLM, party level/size, terrain selector, JSON response parsing, difficulty badge - Calendar Widget: Faerûn calendar (Hammer–Nightal), event creation per day, event indicators, month navigation, year display - All glass-card wrapped with framer-motion hover/tap animations - Full production build clean: tsc, vite, cargo, tauri build
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const MONTHS = [
|
||||
"Hammer", "Alturiak", "Ches", "Tarsakh", "Mirtul", "Kythorn",
|
||||
"Flamerule", "Eleasis", "Eleint", "Marpenoth", "Uktar", "Nightal",
|
||||
];
|
||||
|
||||
interface CalendarEvent {
|
||||
day: number;
|
||||
month: number;
|
||||
text: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const COLORS = ["var(--color-gold-bright)", "var(--color-info)", "var(--color-success)", "var(--color-danger)"];
|
||||
|
||||
export function CalendarWidget() {
|
||||
const [month, setMonth] = useState(0);
|
||||
const [year, setYear] = useState(1492);
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||||
const [selectedDay, setSelectedDay] = useState<number | null>(null);
|
||||
const [newEvent, setNewEvent] = useState("");
|
||||
|
||||
const today = { day: 15, month: 5 }; // 15th of Kythorn (arbitrary "today")
|
||||
|
||||
function addEvent() {
|
||||
if (!newEvent.trim() || selectedDay === null) return;
|
||||
setEvents((prev) => [
|
||||
...prev,
|
||||
{ day: selectedDay, month, text: newEvent.trim(), color: COLORS[events.length % COLORS.length] },
|
||||
]);
|
||||
setNewEvent("");
|
||||
}
|
||||
|
||||
function removeEvent(idx: number) {
|
||||
setEvents((prev) => prev.filter((_, i) => i !== idx));
|
||||
}
|
||||
|
||||
const dayEvents = events.filter((e) => e.day === selectedDay && e.month === month);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 h-full">
|
||||
{/* Month navigation */}
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => { if (month === 0) { setMonth(11); setYear(year - 1); } else setMonth(month - 1); }}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-gold-bright)] cursor-pointer text-sm"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<div className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold">
|
||||
{MONTHS[month]}
|
||||
</div>
|
||||
<div className="text-[var(--color-text-dim)] text-[10px]">{year} DR</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { if (month === 11) { setMonth(0); setYear(year + 1); } else setMonth(month + 1); }}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-gold-bright)] cursor-pointer text-sm"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Day grid */}
|
||||
<div className="grid grid-cols-7 gap-0.5">
|
||||
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((d) => (
|
||||
<div key={d} className="text-[var(--color-text-dim)] text-[9px] text-center font-medium">
|
||||
{d}
|
||||
</div>
|
||||
))}
|
||||
{/* 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 hasEvent = events.some((e) => e.day === dayNum && e.month === month);
|
||||
const isSelected = dayNum === selectedDay;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={dayNum}
|
||||
onClick={() => setSelectedDay(dayNum)}
|
||||
className={`rounded text-[10px] py-1 cursor-pointer transition-colors ${
|
||||
isToday
|
||||
? "bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] font-bold"
|
||||
: isSelected
|
||||
? "bg-[var(--color-bg-card-hover)] text-[var(--color-text-primary)]"
|
||||
: "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
|
||||
}`}
|
||||
>
|
||||
{dayNum}
|
||||
{hasEvent && !isToday && (
|
||||
<span className="block w-1 h-1 rounded-full bg-[var(--color-gold-bright)] mx-auto mt-0" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
{dayEvents.length === 0 && (
|
||||
<div className="text-[var(--color-text-dim)] text-[10px] italic">No events</div>
|
||||
)}
|
||||
{dayEvents.map((e, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5 py-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: e.color }} />
|
||||
<span className="text-[var(--color-text-primary)] text-[10px] flex-1">{e.text}</span>
|
||||
<button
|
||||
onClick={() => removeEvent(events.indexOf(e))}
|
||||
className="text-[var(--color-text-dim)] hover:text-[var(--color-danger)] text-[10px] cursor-pointer"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-1 mt-1">
|
||||
<input
|
||||
className="flex-1 min-w-0 rounded bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-2 py-1 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-[10px]"
|
||||
value={newEvent}
|
||||
onChange={(e) => setNewEvent(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && addEvent()}
|
||||
placeholder="Add event…"
|
||||
/>
|
||||
<button
|
||||
onClick={addEvent}
|
||||
className="rounded bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-2 py-1 text-[10px] font-semibold cursor-pointer hover:bg-[var(--color-gold-muted)] transition-colors"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user