diff --git a/package-lock.json b/package-lock.json
index 89ed976..d050460 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,8 @@
"framer-motion": "^12",
"lucide-react": "^0.525",
"react": "^19.2.7",
- "react-dom": "^19.2.7"
+ "react-dom": "^19.2.7",
+ "zustand": "^5.0.14"
},
"devDependencies": {
"@tailwindcss/vite": "^4",
@@ -1330,7 +1331,7 @@
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1376,7 +1377,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/detect-libc": {
@@ -2117,6 +2118,35 @@
"optional": true
}
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.14",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+ "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 93e17de..d034bf8 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
"framer-motion": "^12",
"lucide-react": "^0.525",
"react": "^19.2.7",
- "react-dom": "^19.2.7"
+ "react-dom": "^19.2.7",
+ "zustand": "^5.0.14"
},
"devDependencies": {
"@tailwindcss/vite": "^4",
diff --git a/src/App.tsx b/src/App.tsx
index 1d69ea4..0ea1920 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -26,6 +26,8 @@ import { CalendarWidget } from "./components/CalendarWidget";
import { WorldBuilder } from "./components/WorldBuilder";
import { ItemForge } from "./components/ItemForge";
import { QuestDesigner } from "./components/QuestDesigner";
+import { Soundboard } from "./components/Soundboard";
+import { ToastContainer } from "./components/Toast";
export type View =
| "dashboard"
@@ -77,16 +79,7 @@ function renderView(view: View): React.ReactNode {
case "items":
return ;
case "sound":
- return (
-
-
-
-
Soundboard
-
Local audio clips for ambience
-
Coming soon
-
-
- );
+ return ;
default:
return null;
}
@@ -239,6 +232,9 @@ export default function App() {
) : null}
+
+ {/* Toast notifications */}
+
);
}
\ No newline at end of file
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx
index 2fe1bd5..2979d14 100644
--- a/src/components/Dashboard.tsx
+++ b/src/components/Dashboard.tsx
@@ -158,9 +158,16 @@ export function Dashboard({ onNavigate }: DashboardProps) {
{/* Soundboard — 1×1 */}
} span="col-span-1 row-span-1">
-
-
-
Soundboard coming soon
+
+
+
+
+
@@ -192,4 +199,5 @@ import { RandomTables } from "./RandomTables";
import { CalendarWidget } from "./CalendarWidget";
import { WorldBuilder } from "./WorldBuilder";
import { QuestDesigner } from "./QuestDesigner";
-import { ItemForge } from "./ItemForge";
\ No newline at end of file
+import { ItemForge } from "./ItemForge";
+import { Soundboard } from "./Soundboard";
\ No newline at end of file
diff --git a/src/components/Soundboard.tsx b/src/components/Soundboard.tsx
new file mode 100644
index 0000000..94473d5
--- /dev/null
+++ b/src/components/Soundboard.tsx
@@ -0,0 +1,327 @@
+import { useState, useRef, useCallback } from "react";
+import { useToast } from "./Toast";
+
+// Ambient sound types with their oscillator configs
+const AMBIENT_SOUNDS = [
+ { id: "rain", label: "Rain", icon: "🌧", type: "brown" as const, freq: 0, filterFreq: 800 },
+ { id: "wind", label: "Wind", icon: "💨", type: "brown" as const, freq: 0, filterFreq: 400 },
+ { id: "fire", label: "Fire", icon: "🔥", type: "brown" as const, freq: 0, filterFreq: 200 },
+ { id: "ocean", label: "Ocean", icon: "🌊", type: "sine" as const, freq: 80, filterFreq: 1200 },
+ { id: "forest", label: "Forest", icon: "🌲", type: "brown" as const, freq: 0, filterFreq: 3000 },
+ { id: "tavern", label: "Tavern", icon: "🍺", type: "brown" as const, freq: 0, filterFreq: 600 },
+ { id: "thunder", label: "Thunder", icon: "⛈", type: "sawtooth" as const, freq: 40, filterFreq: 200 },
+ { id: "cave", label: "Cave", icon: "🪨", type: "sine" as const, freq: 55, filterFreq: 150 },
+];
+
+// Sound effect one-shots
+const SFX_SOUNDS = [
+ { id: "sword", label: "Sword", icon: "⚔" },
+ { id: "bow", label: "Bow", icon: "🏹" },
+ { id: "spell", label: "Spell", icon: "✨" },
+ { id: "door", label: "Door", icon: "🚪" },
+ { id: "coins", label: "Coins", icon: "💰" },
+ { id: "horn", label: "Horn", icon: "📯" },
+ { id: "dice", label: "Dice", icon: "🎲" },
+ { id: "footsteps", label: "Steps", icon: "👣" },
+];
+
+export function Soundboard() {
+ const [activeAmbients, setActiveAmbients] = useState
>(new Set());
+ const [volume, setVolume] = useState(0.5);
+ const audioCtxRef = useRef(null);
+ const nodesRef = useRef