e.stopPropagation()}
+ {...modalProps("Settings")}
className="bg-[#0d0d0f] border border-neutral-800 rounded-2xl shadow-2xl w-full max-w-5xl flex flex-col max-h-[90vh] overflow-hidden"
>
diff --git a/electron/after-pack.js b/electron/after-pack.js
index 45eda26..48bc9ea 100644
--- a/electron/after-pack.js
+++ b/electron/after-pack.js
@@ -9,7 +9,19 @@ const path = require("path");
exports.default = async function afterPack(context) {
const projectRoot = context.packager.info.projectDir;
const appOutDir = context.appOutDir;
- const dest = path.join(appOutDir, "resources", "app", ".next", "standalone");
+ const productName = context.packager.appInfo.productName;
+
+ // On macOS, electron-builder places the .app bundle at
+ // appOutDir/
.app/Contents/Resources/, NOT at
+ // appOutDir/resources/. The old path left the standalone server outside the
+ // .app bundle, so process.resourcesPath could not find it and the embedded
+ // Next.js server never started.
+ const resourcesDir =
+ process.platform === "darwin"
+ ? path.join(appOutDir, `${productName}.app`, "Contents", "Resources")
+ : path.join(appOutDir, "resources");
+
+ const dest = path.join(resourcesDir, "app", ".next", "standalone");
// Wipe whatever electron-builder copied first (it may have partial contents).
if (fs.existsSync(dest)) {
diff --git a/electron/main.js b/electron/main.js
index 74748ec..bf9e4bd 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -371,6 +371,11 @@ if (!hasSingleInstanceLock) {
});
app.whenReady().then(async () => {
+ // Must run after ready. Without it, Chromium only builds the accessibility
+ // tree when a screen reader is already running at launch, so a reader
+ // started after the app (e.g. VoiceOver) would see an empty tree.
+ app.setAccessibilitySupportEnabled(true);
+
await createMainWindow();
await createMiniWindow();
diff --git a/lib/a11y/index.ts b/lib/a11y/index.ts
new file mode 100644
index 0000000..c434df0
--- /dev/null
+++ b/lib/a11y/index.ts
@@ -0,0 +1,80 @@
+/**
+ * Accessibility helpers for War Room.
+ *
+ * Small prop factories so screen-reader semantics (dialog roles, live regions,
+ * expand/collapse state) stay consistent across components instead of being
+ * hand-rolled at each call site.
+ */
+
+/**
+ * Props for an accessible modal dialog.
+ * Usage:
+ */
+export function modalProps(title: string): {
+ role: "dialog";
+ "aria-modal": true;
+ "aria-label": string;
+} {
+ return { role: "dialog", "aria-modal": true, "aria-label": title };
+}
+
+/**
+ * Props for a collapsible section toggle button.
+ * Usage: