mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
Require dashboard session cookies on protected management APIs and reject bearer API keys with explicit 403 responses to prevent privilege escalation across provider, settings, and model alias routes. Add a dedicated payload rules management surface with dashboard UI, OpenAPI documentation, route normalization, and tests for hot-reloaded runtime updates. Consolidate provider catalog metadata for dashboard pages, add Perplexity web-cookie provider support, retire the legacy provider creation page, and improve upstream proxy handling. Harden startup and runtime behavior by moving cloud sync bootstrap to server instrumentation, skipping background services during build/test, making models.dev sync abortable, pruning isolated build artifacts, and improving DB backup and recovery safeguards.
124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { create } from "zustand";
|
|
import { persist } from "zustand/middleware";
|
|
import { THEME_CONFIG } from "@/shared/constants/appConfig";
|
|
|
|
interface ThemeState {
|
|
theme: string;
|
|
colorTheme: string;
|
|
customColor: string;
|
|
setTheme: (theme: string) => void;
|
|
setColorTheme: (colorTheme: string) => void;
|
|
setCustomColorTheme: (color: string) => void;
|
|
toggleTheme: () => void;
|
|
initTheme: () => void;
|
|
}
|
|
|
|
const useThemeStore = create<ThemeState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
theme: THEME_CONFIG.defaultTheme,
|
|
colorTheme: "coral",
|
|
customColor: "#3b82f6",
|
|
|
|
setTheme: (theme) => {
|
|
set({ theme });
|
|
applyTheme(theme);
|
|
},
|
|
|
|
setColorTheme: (colorTheme) => {
|
|
set({ colorTheme });
|
|
applyColorTheme(colorTheme, get().customColor);
|
|
},
|
|
|
|
setCustomColorTheme: (color) => {
|
|
const normalized = normalizeHexColor(color);
|
|
set({ colorTheme: "custom", customColor: normalized });
|
|
applyColorTheme("custom", normalized);
|
|
},
|
|
|
|
toggleTheme: () => {
|
|
const currentTheme = get().theme;
|
|
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
|
set({ theme: newTheme });
|
|
applyTheme(newTheme);
|
|
},
|
|
|
|
initTheme: () => {
|
|
const { theme, colorTheme, customColor } = get();
|
|
applyTheme(theme);
|
|
applyColorTheme(colorTheme, customColor);
|
|
},
|
|
}),
|
|
{
|
|
name: THEME_CONFIG.storageKey,
|
|
}
|
|
)
|
|
);
|
|
|
|
export const COLOR_THEMES: Record<string, string> = {
|
|
coral: "#e54d5e",
|
|
blue: "#3b82f6",
|
|
red: "#ef4444",
|
|
green: "#22c55e",
|
|
violet: "#8b5cf6",
|
|
orange: "#f97316",
|
|
cyan: "#06b6d4",
|
|
};
|
|
|
|
// Apply light/dark theme to document
|
|
function applyTheme(theme: string) {
|
|
if (typeof window === "undefined") return;
|
|
|
|
const root = document.documentElement;
|
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
const effectiveTheme = theme === "system" ? systemTheme : theme;
|
|
|
|
if (effectiveTheme === "dark") {
|
|
root.classList.add("dark");
|
|
} else {
|
|
root.classList.remove("dark");
|
|
}
|
|
}
|
|
|
|
function applyColorTheme(colorTheme: string, customColor: string) {
|
|
if (typeof window === "undefined") return;
|
|
|
|
const root = document.documentElement;
|
|
const baseColor =
|
|
colorTheme === "custom"
|
|
? normalizeHexColor(customColor)
|
|
: COLOR_THEMES[colorTheme] || COLOR_THEMES.coral;
|
|
const hoverColor = shadeHexColor(baseColor, -0.14);
|
|
|
|
root.style.setProperty("--color-primary", baseColor);
|
|
root.style.setProperty("--color-primary-hover", hoverColor);
|
|
}
|
|
|
|
function normalizeHexColor(color: string) {
|
|
const value = (color || "").trim();
|
|
const hex = value.startsWith("#") ? value : `#${value}`;
|
|
const valid = /^#([0-9a-fA-F]{6})$/.test(hex);
|
|
return valid ? hex.toLowerCase() : "#3b82f6";
|
|
}
|
|
|
|
function shadeHexColor(hex: string, percent: number) {
|
|
const normalized = normalizeHexColor(hex).slice(1);
|
|
const r = parseInt(normalized.slice(0, 2), 16);
|
|
const g = parseInt(normalized.slice(2, 4), 16);
|
|
const b = parseInt(normalized.slice(4, 6), 16);
|
|
|
|
const shade = (channel: number) => {
|
|
const target = percent < 0 ? 0 : 255;
|
|
const amount = Math.round((target - channel) * Math.abs(percent));
|
|
const next = percent < 0 ? channel - amount : channel + amount;
|
|
return Math.max(0, Math.min(255, next));
|
|
};
|
|
|
|
const toHex = (channel: number) => channel.toString(16).padStart(2, "0");
|
|
return `#${toHex(shade(r))}${toHex(shade(g))}${toHex(shade(b))}`;
|
|
}
|
|
|
|
export default useThemeStore;
|