feat: sidebar navigation to dedicated detail views

- Sidebar icons now navigate to full-screen views for each tool
- Dashboard bento cards have 'Expand' buttons that open the detail view
- Back arrow (←) in title bar returns to dashboard
- Detail views wrapped in a glass-card with max-w-2xl layout
- Settings page gets its own full layout with heading
- World Map, Quest Designer, Soundboard show placeholder screens with icons
- All nav items in sidebar: World, NPCs, Encounter, Dice, Session, Sound,
  Initiative, Random Tables, Calendar, Settings
This commit is contained in:
itsamejms
2026-06-28 22:43:08 +01:00
parent 4d4518aec0
commit a44de0e4bb
2 changed files with 318 additions and 87 deletions
+162 -20
View File
@@ -7,48 +7,129 @@ import {
Volume2,
Settings,
ChevronDown,
ChevronLeft,
Sparkles,
Timer,
Calendar,
} from "lucide-react";
import { useState } from "react";
import { Dashboard } from "./components/Dashboard";
import { SettingsPanel } from "./components/SettingsPanel";
import { NpcGenerator } from "./components/NpcGenerator";
import { DiceRoller } from "./components/DiceRoller";
import { SessionLogger } from "./components/SessionLogger";
import { EncounterBuilder } from "./components/EncounterBuilder";
import { InitiativeTracker } from "./components/InitiativeTracker";
import { RandomTables } from "./components/RandomTables";
import { CalendarWidget } from "./components/CalendarWidget";
type View = "dashboard" | "settings";
export type View =
| "dashboard"
| "world"
| "npcs"
| "encounter"
| "dice"
| "session"
| "sound"
| "initiative"
| "tables"
| "calendar"
| "quest"
| "settings";
const navItems = [
{ icon: Map, label: "World" },
{ icon: User, label: "NPCs" },
{ icon: Swords, label: "Encounter" },
{ icon: Dice5, label: "Dice" },
{ icon: ScrollText, label: "Session" },
{ icon: Volume2, label: "Sound" },
const navItems: { icon: typeof Map; label: string; view: View }[] = [
{ icon: Map, label: "World", view: "world" },
{ icon: User, label: "NPCs", view: "npcs" },
{ icon: Swords, label: "Encounter", view: "encounter" },
{ icon: Dice5, label: "Dice", view: "dice" },
{ icon: ScrollText, label: "Session", view: "session" },
{ icon: Volume2, label: "Sound", view: "sound" },
];
function renderView(view: View): React.ReactNode {
switch (view) {
case "npcs":
return <NpcGenerator />;
case "dice":
return <DiceRoller />;
case "session":
return <SessionLogger />;
case "encounter":
return <EncounterBuilder />;
case "initiative":
return <InitiativeTracker />;
case "tables":
return <RandomTables />;
case "calendar":
return <CalendarWidget />;
case "settings":
return <SettingsPanel />;
case "world":
return (
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)]">
<div className="text-center">
<Map size={64} className="mx-auto mb-4 opacity-30" />
<p className="text-lg font-heading">World Map</p>
<p className="text-sm mt-2">Interactive map canvas coming soon</p>
<p className="text-xs mt-1 opacity-60">react-konva integration pending</p>
</div>
</div>
);
case "quest":
return (
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)]">
<div className="text-center">
<Sparkles size={64} className="mx-auto mb-4 opacity-30" />
<p className="text-lg font-heading">Quest Designer</p>
<p className="text-sm mt-2">Branching flowchart coming soon</p>
<p className="text-xs mt-1 opacity-60">react-flow integration pending</p>
</div>
</div>
);
case "sound":
return (
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)]">
<div className="text-center">
<Volume2 size={64} className="mx-auto mb-4 opacity-30" />
<p className="text-lg font-heading">Soundboard</p>
<p className="text-sm mt-2">Local audio clips for ambience</p>
<p className="text-xs mt-1 opacity-60">Coming soon</p>
</div>
</div>
);
default:
return null;
}
}
export default function App() {
const [view, setView] = useState<View>("dashboard");
const [activeNav, setActiveNav] = useState(0);
const isDetailView = view !== "dashboard" && view !== "settings";
return (
<div className="flex h-screen w-screen overflow-hidden bg-[var(--color-bg-deep)]">
{/* Left Rail */}
<nav className="flex flex-col items-center w-14 bg-[var(--color-bg-surface)] border-r border-[var(--color-border-subtle)] py-3 gap-1 shrink-0">
{/* App Logo */}
{/* App Logo — goes to dashboard */}
<button
onClick={() => setView("dashboard")}
className="w-8 h-8 rounded-lg bg-[var(--color-gold-bright)] flex items-center justify-center mb-4 text-[var(--color-bg-deep)] font-heading font-bold text-lg cursor-pointer"
title="DM-Pal"
className={`w-8 h-8 rounded-lg flex items-center justify-center mb-4 font-heading font-bold text-lg cursor-pointer transition-colors ${
view === "dashboard"
? "bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)]"
: "bg-[var(--color-bg-card)] text-[var(--color-gold-bright)] hover:bg-[var(--color-gold-bright)] hover:text-[var(--color-bg-deep)]"
}`}
title="Dashboard"
>
</button>
{navItems.map((item, i) => (
{navItems.map((item) => (
<button
key={item.label}
onClick={() => {
setActiveNav(i);
setView("dashboard");
}}
onClick={() => setView(item.view)}
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors cursor-pointer ${
i === activeNav && view === "dashboard"
view === item.view
? "bg-[var(--color-bg-card)] text-[var(--color-gold-bright)]"
: "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
}`}
@@ -61,7 +142,44 @@ export default function App() {
{/* Spacer */}
<div className="flex-1" />
{/* Settings at bottom */}
{/* Extra nav items */}
<button
onClick={() => setView("initiative")}
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors cursor-pointer ${
view === "initiative"
? "bg-[var(--color-bg-card)] text-[var(--color-gold-bright)]"
: "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
}`}
title="Initiative"
>
<Timer size={18} />
</button>
<button
onClick={() => setView("tables")}
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors cursor-pointer ${
view === "tables"
? "bg-[var(--color-bg-card)] text-[var(--color-gold-bright)]"
: "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
}`}
title="Random Tables"
>
<Sparkles size={18} />
</button>
<button
onClick={() => setView("calendar")}
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors cursor-pointer ${
view === "calendar"
? "bg-[var(--color-bg-card)] text-[var(--color-gold-bright)]"
: "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
}`}
title="Calendar"
>
<Calendar size={18} />
</button>
<div className="w-6 h-px bg-[var(--color-border-subtle)] my-1" />
{/* Settings */}
<button
onClick={() => setView(view === "settings" ? "dashboard" : "settings")}
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors cursor-pointer ${
@@ -82,6 +200,15 @@ export default function App() {
className="flex items-center h-10 px-4 bg-[var(--color-bg-surface)] border-b border-[var(--color-border-subtle)] shrink-0"
data-tauri-drag-region
>
{isDetailView && (
<button
onClick={() => setView("dashboard")}
className="mr-3 text-[var(--color-text-secondary)] hover:text-[var(--color-gold-bright)] transition-colors cursor-pointer"
title="Back to dashboard"
>
<ChevronLeft size={18} />
</button>
)}
<span className="font-heading text-[var(--color-gold-bright)] text-base font-bold tracking-wider">
DM-Pal
</span>
@@ -93,7 +220,22 @@ export default function App() {
{/* Main Content */}
<main className="flex-1 overflow-auto">
{view === "dashboard" ? <Dashboard /> : <SettingsPanel />}
{view === "dashboard" ? (
<Dashboard onNavigate={setView} />
) : view === "settings" ? (
<div className="max-w-lg mx-auto p-6">
<h2 className="font-heading text-[var(--color-gold-bright)] text-lg font-semibold mb-4">
Settings
</h2>
<SettingsPanel />
</div>
) : isDetailView ? (
<div className="max-w-2xl mx-auto p-6">
<div className="glass-card p-6">
{renderView(view)}
</div>
</div>
) : null}
</main>
</div>
</div>
+156 -67
View File
@@ -8,78 +8,167 @@ import {
Timer,
Volume2,
Calendar,
Maximize2,
} from "lucide-react";
import { BentoCard } from "./BentoCard";
import type { View } from "../App";
interface DashboardProps {
onNavigate: (view: View) => void;
}
export function Dashboard({ onNavigate }: DashboardProps) {
return (
<div className="grid grid-cols-4 grid-rows-4 gap-3 p-4 h-full bg-[var(--color-bg-deep)]">
{/* World Map — 2×2 */}
<BentoCard title="World Map" icon={<Map size={16} />} span="col-span-2 row-span-2">
<div className="flex flex-col items-center justify-center h-full text-[var(--color-text-dim)] text-sm gap-2">
<Map size={48} className="opacity-30" />
<p>Map canvas coming soon</p>
<button
onClick={() => onNavigate("world")}
className="text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] text-xs flex items-center gap-1 cursor-pointer transition-colors"
>
<Maximize2 size={12} /> Open full view
</button>
</div>
</BentoCard>
{/* NPC Sheet — 1×1 */}
<BentoCard title="NPC Sheet" icon={<User size={16} />} span="col-span-1 row-span-1">
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
<NpcGenerator />
</div>
<button
onClick={() => onNavigate("npcs")}
className="text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] text-xs flex items-center justify-center gap-1 cursor-pointer transition-colors mt-1 pt-2 border-t border-[var(--color-border-subtle)]"
>
<Maximize2 size={12} /> Expand
</button>
</div>
</BentoCard>
{/* Dice Roller — 1×1 */}
<BentoCard title="Dice Roller" icon={<Dice5 size={16} />} span="col-span-1 row-span-1">
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
<DiceRoller />
</div>
<button
onClick={() => onNavigate("dice")}
className="text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] text-xs flex items-center justify-center gap-1 cursor-pointer transition-colors mt-1 pt-2 border-t border-[var(--color-border-subtle)]"
>
<Maximize2 size={12} /> Expand
</button>
</div>
</BentoCard>
{/* Encounter Builder — 1×1 */}
<BentoCard title="Encounter" icon={<Swords size={16} />} span="col-span-1 row-span-1">
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
<EncounterBuilder />
</div>
<button
onClick={() => onNavigate("encounter")}
className="text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] text-xs flex items-center justify-center gap-1 cursor-pointer transition-colors mt-1 pt-2 border-t border-[var(--color-border-subtle)]"
>
<Maximize2 size={12} /> Expand
</button>
</div>
</BentoCard>
{/* Session Log — 2×1 */}
<BentoCard title="Session Log" icon={<ScrollText size={16} />} span="col-span-2 row-span-1">
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
<SessionLogger />
</div>
<button
onClick={() => onNavigate("session")}
className="text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] text-xs flex items-center justify-center gap-1 cursor-pointer transition-colors mt-1 pt-2 border-t border-[var(--color-border-subtle)]"
>
<Maximize2 size={12} /> Expand
</button>
</div>
</BentoCard>
{/* Quest Designer — 2×1 */}
<BentoCard title="Quest Designer" icon={<Sparkles size={16} />} span="col-span-2 row-span-1">
<div className="flex flex-col items-center justify-center h-full text-[var(--color-text-dim)] text-sm gap-2">
<Sparkles size={32} className="opacity-30" />
<p>Quest flowchart coming soon</p>
<button
onClick={() => onNavigate("quest")}
className="text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] text-xs flex items-center gap-1 cursor-pointer transition-colors"
>
<Maximize2 size={12} /> Open
</button>
</div>
</BentoCard>
{/* Initiative Tracker — 1×1 */}
<BentoCard title="Initiative" icon={<Timer size={16} />} span="col-span-1 row-span-1">
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
<InitiativeTracker />
</div>
<button
onClick={() => onNavigate("initiative")}
className="text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] text-xs flex items-center justify-center gap-1 cursor-pointer transition-colors mt-1 pt-2 border-t border-[var(--color-border-subtle)]"
>
<Maximize2 size={12} /> Expand
</button>
</div>
</BentoCard>
{/* Random Tables — 1×1 */}
<BentoCard title="Random Tables" icon={<Sparkles size={16} />} span="col-span-1 row-span-1">
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
<RandomTables />
</div>
<button
onClick={() => onNavigate("tables")}
className="text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] text-xs flex items-center justify-center gap-1 cursor-pointer transition-colors mt-1 pt-2 border-t border-[var(--color-border-subtle)]"
>
<Maximize2 size={12} /> Expand
</button>
</div>
</BentoCard>
{/* Calendar — 1×1 */}
<BentoCard title="Calendar" icon={<Calendar size={16} />} span="col-span-1 row-span-1">
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
<CalendarWidget />
</div>
<button
onClick={() => onNavigate("calendar")}
className="text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] text-xs flex items-center justify-center gap-1 cursor-pointer transition-colors mt-1 pt-2 border-t border-[var(--color-border-subtle)]"
>
<Maximize2 size={12} /> Expand
</button>
</div>
</BentoCard>
{/* Soundboard — 1×1 */}
<BentoCard title="Soundboard" icon={<Volume2 size={16} />} span="col-span-1 row-span-1">
<div className="flex flex-col items-center justify-center h-full text-[var(--color-text-dim)] text-sm gap-2">
<Volume2 size={32} className="opacity-30" />
<p>Soundboard coming soon</p>
</div>
</BentoCard>
</div>
);
}
// Inline imports for dashboard cards (compact versions)
import { NpcGenerator } from "./NpcGenerator";
import { DiceRoller } from "./DiceRoller";
import { SessionLogger } from "./SessionLogger";
import { EncounterBuilder } from "./EncounterBuilder";
import { InitiativeTracker } from "./InitiativeTracker";
import { RandomTables } from "./RandomTables";
import { CalendarWidget } from "./CalendarWidget";
export function Dashboard() {
return (
<div className="grid grid-cols-4 grid-rows-4 gap-3 p-4 h-full bg-[var(--color-bg-deep)]">
{/* World Map — 2×2 */}
<BentoCard title="World Map" icon={<Map size={16} />} span="col-span-2 row-span-2">
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
<div className="text-center">
<Map size={48} className="mx-auto mb-2 opacity-30" />
<p>Map canvas coming soon</p>
<p className="text-xs mt-1">react-konva integration pending</p>
</div>
</div>
</BentoCard>
{/* NPC Sheet — 1×1 */}
<BentoCard title="NPC Sheet" icon={<User size={16} />} span="col-span-1 row-span-1">
<NpcGenerator />
</BentoCard>
{/* Dice Roller — 1×1 */}
<BentoCard title="Dice Roller" icon={<Dice5 size={16} />} span="col-span-1 row-span-1">
<DiceRoller />
</BentoCard>
{/* Encounter Builder — 1×1 */}
<BentoCard title="Encounter" icon={<Swords size={16} />} span="col-span-1 row-span-1">
<EncounterBuilder />
</BentoCard>
{/* Session Log — 2×1 */}
<BentoCard title="Session Log" icon={<ScrollText size={16} />} span="col-span-2 row-span-1">
<SessionLogger />
</BentoCard>
{/* Quest Designer — 2×1 */}
<BentoCard title="Quest Designer" icon={<Sparkles size={16} />} span="col-span-2 row-span-1">
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
Quest flowchart coming soon react-flow integration pending
</div>
</BentoCard>
{/* Initiative Tracker — 1×1 */}
<BentoCard title="Initiative" icon={<Timer size={16} />} span="col-span-1 row-span-1">
<InitiativeTracker />
</BentoCard>
{/* Random Tables — 1×1 */}
<BentoCard title="Random Tables" icon={<Sparkles size={16} />} span="col-span-1 row-span-1">
<RandomTables />
</BentoCard>
{/* Calendar — 1×1 */}
<BentoCard title="Calendar" icon={<Calendar size={16} />} span="col-span-1 row-span-1">
<CalendarWidget />
</BentoCard>
{/* Soundboard — 1×1 */}
<BentoCard title="Soundboard" icon={<Volume2 size={16} />} span="col-span-1 row-span-1">
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
Soundboard coming soon
</div>
</BentoCard>
</div>
);
}
import { CalendarWidget } from "./CalendarWidget";