(null);
@@ -595,7 +848,9 @@ function App() {
{error && {error}
}
- {currentRoute !== "pets" ? (
+ {currentRoute === "settings" ? (
+
+ ) : currentRoute !== "pets" ? (
) : (
diff --git a/apps/desktop/src/renderer/src/styles.css b/apps/desktop/src/renderer/src/styles.css
index 0e8b5de2..d500bfce 100644
--- a/apps/desktop/src/renderer/src/styles.css
+++ b/apps/desktop/src/renderer/src/styles.css
@@ -72,12 +72,12 @@
.btn-success:hover:not(:disabled) { background: linear-gradient(180deg, #6ee7b7, #059669); }
.btn-warning { border: 1px solid rgba(217, 119, 6, 0.32); background: linear-gradient(180deg, #fbbf24, #d97706); color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10); }
.btn-warning:hover:not(:disabled) { background: linear-gradient(180deg, #fcd34d, #d97706); }
-
+
.btn-icon-wrapper { @apply flex items-center justify-center shrink-0; }
.btn-icon { @apply h-4 w-4 stroke-[2.5]; }
.pager-text { @apply text-xs font-bold uppercase tracking-wider text-slatecopy/80; }
.error { @apply mb-4 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-red-700; }
-
+
.nav-bar { @apply mb-5 flex items-center gap-1.5 border-b border-blue-200/30 pb-3.5 flex-wrap; }
.nav-tab { @apply rounded-xl border px-3.5 py-2 font-monoDisplay text-xs font-black uppercase tracking-wider text-slate-600 active:scale-[0.96] transition-[transform,background-color,border-color,box-shadow] duration-150 flex items-center gap-2 cursor-pointer select-none h-9 shadow-sm; border-color: rgba(126, 161, 210, 0.25); background: linear-gradient(180deg, rgba(255, 255, 255, 0.8) 0%, rgba(241, 245, 249, 0.8) 100%); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.04); }
.nav-tab:hover:not(.active) { @apply text-brand; border-color: rgba(37, 99, 235, 0.3); background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(239, 246, 255, 0.8) 100%); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.06); }
@@ -85,4 +85,53 @@
.nav-tab.active:hover { background: linear-gradient(180deg, #55a6ff 0%, #176df2 100%); }
.nav-icon { @apply h-3.5 w-3.5 stroke-[2.5] opacity-75 shrink-0; }
.nav-tab.active .nav-icon { @apply opacity-100; }
+
+ .settings-layout { @apply relative flex flex-col gap-6 overflow-y-auto pr-2 h-full; overscroll-behavior: contain; }
+ .settings-container { @apply grid grid-cols-[280px_1fr] gap-6 min-h-0 flex-1; }
+ .settings-sidebar { @apply flex flex-col gap-2; }
+ .settings-content { @apply flex flex-col gap-6 overflow-y-auto pr-2 pb-8; overscroll-behavior: contain; }
+ .settings-nav-item { @apply flex items-center gap-3 px-4 py-3 rounded-2xl border border-transparent text-sm font-bold text-slatecopy transition-all duration-150 cursor-pointer select-none; }
+ .settings-nav-item:hover { @apply bg-white/50 text-brand border-blue-100/50; }
+ .settings-nav-item.active { @apply bg-white text-brand border-blue-200 shadow-sm ring-4 ring-blue-100/30; }
+ .settings-nav-icon { @apply w-4 h-4 opacity-70; }
+ .settings-nav-item.active .settings-nav-icon { @apply opacity-100; }
+
+ .settings-section { @apply flex flex-col gap-4; }
+ .settings-section-title { @apply m-0 font-monoDisplay text-2xl font-black text-navy; }
+
+ .settings-group { @apply flex flex-col overflow-hidden rounded-[24px] border border-blue-100/70 bg-white/55 shadow-sm; }
+ .settings-row { @apply flex items-center justify-between gap-4 p-5 transition-colors hover:bg-white/80 border-b border-blue-50 last:border-b-0; }
+ .settings-row-info { @apply flex-1 min-w-0; }
+ .settings-row-info strong { @apply block text-sm font-bold text-navy; }
+ .settings-row-info small { @apply mt-0.5 block text-xs leading-relaxed text-slatecopy; }
+
+ .settings-toggle { @apply h-6 w-11 shrink-0 cursor-pointer accent-brand disabled:cursor-not-allowed; }
+ .settings-select { @apply min-h-10 min-w-[140px] rounded-xl border border-blue-200/80 bg-white/80 px-3 py-2 text-sm font-semibold text-navy outline-none focus:border-brand focus:ring-4 focus:ring-brand/15 disabled:cursor-not-allowed disabled:opacity-60; }
+
+ .settings-system-footer { @apply mt-2 flex items-center justify-between gap-4 p-4 rounded-2xl bg-blue-50/40 border border-blue-100/50; }
+ .settings-system-info { @apply flex items-center gap-3 text-xs font-bold text-slatecopy; }
+ .settings-system-version { @apply px-2 py-0.5 rounded-md bg-blue-100/50 text-brand font-mono; }
+
+ .reaction-grid { @apply grid grid-cols-1 gap-px bg-blue-50; }
+ .reaction-row { @apply flex items-center gap-4 bg-white/80 p-4 transition-colors hover:bg-white; }
+ .reaction-preview-box { @apply flex h-14 w-14 shrink-0 items-center justify-center rounded-xl bg-blue-50/50 border border-blue-100/50 overflow-hidden; }
+ .reaction-preview-sprite-shell { @apply flex h-12 w-12 items-center justify-center; }
+ .reaction-preview-sprite { display: block; width: 36px; height: 36px; flex: 0 0 auto; overflow: hidden; image-rendering: pixelated; }
+ .reaction-preview-sprite image { pointer-events: none; user-select: none; }
+ .reaction-info { @apply flex-1 min-w-0; }
+ .reaction-info strong { @apply block text-sm font-bold text-navy; }
+ .reaction-info small { @apply mt-0.5 block text-xs leading-relaxed text-slatecopy; }
+
+ .settings-panel { @apply flex flex-col gap-4 p-6; }
+ .settings-panel h2 { @apply m-0 font-monoDisplay text-2xl font-black text-navy; }
+ .settings-wide { @apply w-full; }
+ .settings-actions { @apply flex flex-wrap gap-3 mt-2; }
+ .settings-message { @apply fixed bottom-8 left-1/2 z-20 w-[min(360px,calc(100vw-3rem))] -translate-x-1/2 text-center shadow-lg; }
+ .settings-success { @apply mb-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-emerald-700; }
+ .update-card-content { @apply p-4 rounded-2xl bg-brand/5 border border-brand/10; }
+ .update-card-eyebrow { @apply text-xs text-brand font-bold uppercase tracking-wider mb-2; }
+ .update-card-text { @apply text-sm text-navy mb-4; }
+ .reaction-header { @apply flex items-center justify-between mb-2; }
+ .reaction-desc { @apply desc mb-2; }
+
}
diff --git a/apps/desktop/src/windows.ts b/apps/desktop/src/windows.ts
index fac83c60..f4e02a32 100644
--- a/apps/desktop/src/windows.ts
+++ b/apps/desktop/src/windows.ts
@@ -87,6 +87,21 @@ function getPetsStateSnapshot(): { preferences: { defaultPetId: string }; pets:
return { preferences: { defaultPetId: state.preferences.defaultPetId }, pets: state.pets };
}
+function getSettingsStateSnapshot(): {
+ preferences: Pick["preferences"], "openDefaultPetOnLaunch" | "petScale" | "reactionAnimationOverrides">;
+ petScaleOptions: typeof petScaleOptions;
+} {
+ const state = getAppStateSnapshot();
+ return {
+ preferences: {
+ openDefaultPetOnLaunch: state.preferences.openDefaultPetOnLaunch,
+ petScale: state.preferences.petScale,
+ reactionAnimationOverrides: state.preferences.reactionAnimationOverrides,
+ },
+ petScaleOptions,
+ };
+}
+
export function installInternalUiHandlers(): void {
if (internalUiHandlersInstalled) {
return;
@@ -104,8 +119,13 @@ export function installInternalUiHandlers(): void {
return getPetsStateSnapshot();
});
+ ipcMain.handle("openpets:get-settings-state", (event) => {
+ assertAllowedSender(event, ["control-center"]);
+ return getSettingsStateSnapshot();
+ });
+
ipcMain.handle("openpets:get-reaction-animation-settings", async (event) => {
- assertAllowedSender(event, ["settings"]);
+ assertAllowedSender(event, ["settings", "control-center"]);
return getReactionAnimationSettingsSnapshot();
});
@@ -217,7 +237,7 @@ export function installInternalUiHandlers(): void {
});
ipcMain.handle("openpets:update-preferences", (event, patch: unknown) => {
- assertAllowedSender(event, ["settings"]);
+ assertAllowedSender(event, ["settings", "control-center"]);
const previousScale = getAppStateSnapshot().preferences.petScale;
const previousOverrides = JSON.stringify(getAppStateSnapshot().preferences.reactionAnimationOverrides ?? {});
const state = updatePreferences(validatePreferencePatch(patch));
@@ -226,16 +246,16 @@ export function installInternalUiHandlers(): void {
refreshDefaultPetContent();
refreshAgentPetContent();
}
- return state;
+ return getInternalUiWindowKindForWebContents(event.sender.id) === "control-center" ? getSettingsStateSnapshot() : state;
});
ipcMain.handle("openpets:get-launch-at-login", (event) => {
- assertAllowedSender(event, ["settings"]);
+ assertAllowedSender(event, ["settings", "control-center"]);
return getLaunchAtLoginState();
});
ipcMain.handle("openpets:set-launch-at-login", (event, enabled: unknown) => {
- assertAllowedSender(event, ["settings"]);
+ assertAllowedSender(event, ["settings", "control-center"]);
if (typeof enabled !== "boolean") throw new Error("Invalid launch-at-login value.");
if (!isLaunchAtLoginSupported()) return getLaunchAtLoginState();
app.setLoginItemSettings({ openAtLogin: enabled, openAsHidden: true });
@@ -243,12 +263,12 @@ export function installInternalUiHandlers(): void {
});
ipcMain.handle("openpets:get-update-status", (event) => {
- assertAllowedSender(event, ["settings"]);
+ assertAllowedSender(event, ["settings", "control-center"]);
return getUpdateStatus();
});
ipcMain.handle("openpets:check-for-updates", async (event) => {
- assertAllowedSender(event, ["settings"]);
+ assertAllowedSender(event, ["settings", "control-center"]);
const status = await checkForGitHubReleaseUpdate();
const { refreshTrayMenu } = await import("./tray.js");
refreshTrayMenu();
@@ -256,7 +276,7 @@ export function installInternalUiHandlers(): void {
});
ipcMain.handle("openpets:open-update-release-page", async (event) => {
- assertAllowedSender(event, ["settings"]);
+ assertAllowedSender(event, ["settings", "control-center"]);
await openUpdateReleasePage();
});
@@ -305,9 +325,9 @@ export function installInternalUiHandlers(): void {
});
ipcMain.handle("openpets:reset-default-pet-position", (event) => {
- assertAllowedSender(event, ["settings"]);
+ assertAllowedSender(event, ["settings", "control-center"]);
resetDefaultPetToInitialPosition();
- return getAppStateSnapshot();
+ return getInternalUiWindowKindForWebContents(event.sender.id) === "control-center" ? getSettingsStateSnapshot() : getAppStateSnapshot();
});
ipcMain.handle("openpets:agent-setup-snapshot", async (event, selectedPetId: unknown, commandMode: unknown) => {
diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts
index b0585967..277ee967 100644
--- a/apps/desktop/vite.config.ts
+++ b/apps/desktop/vite.config.ts
@@ -1,8 +1,8 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
-const productionCsp = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data: https://openpets.dev openpets-codex:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-src 'none'";
-const devCsp = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://openpets.dev openpets-codex:; connect-src 'self' http://127.0.0.1:5173 ws://127.0.0.1:5173; base-uri 'none'; form-action 'none'; frame-src 'none'";
+const productionCsp = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data: https://openpets.dev openpets-codex: openpets-pet-preview:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-src 'none'";
+const devCsp = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://openpets.dev openpets-codex: openpets-pet-preview:; connect-src 'self' http://127.0.0.1:5173 ws://127.0.0.1:5173; base-uri 'none'; form-action 'none'; frame-src 'none'";
export default defineConfig(({ command }) => ({
root: "src/renderer",