feat(config): add shared browser cookie policy
Quality / quality (ubuntu-latest) (push) Failing after 9m46s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-12 21:53:07 +05:30
parent a0dab308f0
commit 5de792f359
15 changed files with 198 additions and 22 deletions
+85 -4
View File
@@ -111,6 +111,54 @@ export interface ThemeCookieConfig {
sameSite?: "Strict" | "Lax" | "None";
/** Force or disable Secure. When omitted it follows the current protocol. */
secure?: boolean;
/** Cookie lifetime in seconds. Defaults to one year. */
maxAge?: number;
}
export type BrowserCookieOptions = ThemeCookieConfig;
/** Shared browser-cookie policy with optional per-preference overrides. */
export interface BrowserCookiesConfig {
defaults?: BrowserCookieOptions;
theme?: BrowserCookieOptions;
accent?: BrowserCookieOptions;
language?: BrowserCookieOptions;
}
export type BrowserCookiePreference = "theme" | "accent" | "language";
export interface BrowserCookieApi {
readonly config: BrowserCookiesConfig;
get(name: string): string | null;
options(
preference?: BrowserCookiePreference | BrowserCookieOptions,
override?: BrowserCookieOptions,
): BrowserCookieOptions;
set(
name: string,
value: string,
preference?: BrowserCookiePreference | BrowserCookieOptions,
override?: BrowserCookieOptions,
): void;
remove(
name: string,
preference?: BrowserCookiePreference | BrowserCookieOptions,
override?: BrowserCookieOptions,
): void;
}
declare global {
interface Window {
wrnCookies: BrowserCookieApi;
}
}
export function resolveBrowserCookieOptions(
config: BrowserCookiesConfig | undefined,
preference: BrowserCookiePreference,
override?: BrowserCookieOptions,
): BrowserCookieOptions {
return { ...(config?.defaults ?? {}), ...(config?.[preference] ?? {}), ...(override ?? {}) };
}
export interface ThemeConfig {
@@ -131,6 +179,8 @@ export interface ResolvedTheme {
defaultAccent?: ThemePaletteName;
accentNames: ThemePaletteName[];
accentCookie?: ThemeCookieConfig;
themeCookie?: ThemeCookieConfig;
cookies?: BrowserCookiesConfig;
}
/** Cookies used by the SSR renderer and client runtime. */
@@ -477,7 +527,10 @@ function themeScheme(tokens: ThemeTokens): ColorScheme {
}
/** Merge the user's theme config over the built-in defaults. */
export function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme {
export function resolveThemeConfig(
config?: ThemeConfig,
cookies?: BrowserCookiesConfig,
): ResolvedTheme {
const palette = resolvePalette(config?.palette);
const themes: Record<string, ThemeTokens> = {};
const names = new Set<string>([
@@ -508,7 +561,9 @@ export function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme {
themes,
defaultAccent,
accentNames,
accentCookie: config?.accent?.cookie,
themeCookie: resolveBrowserCookieOptions(cookies, "theme"),
accentCookie: resolveBrowserCookieOptions(cookies, "accent", config?.accent?.cookie),
cookies,
};
}
@@ -637,6 +692,8 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
const accentNames = JSON.stringify(theme.accentNames);
const defaultAccent = JSON.stringify(theme.defaultAccent ?? null);
const cssPrefix = JSON.stringify(THEME_CSS_PREFIX);
const cookieConfig = JSON.stringify(theme.cookies ?? {});
const themeCookie = JSON.stringify(theme.themeCookie ?? {});
const accentCookie = JSON.stringify(theme.accentCookie ?? {});
return `(function(){
@@ -647,6 +704,8 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
var DEFAULT_THEME=${JSON.stringify(theme.default)};
var DEFAULT_ACCENT=${defaultAccent};
var THEME_CSS_PREFIX=${cssPrefix};
var COOKIE_CONFIG=${cookieConfig};
var THEME_COOKIE_OPTIONS=${themeCookie};
var ACCENT_COOKIE_OPTIONS=${accentCookie};
var MAX_AGE=31536000;
var el=document.documentElement;
@@ -678,14 +737,36 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
}
function writeCookie(name,value,options){
options=options||{};
var maxAge=options.maxAge===undefined?MAX_AGE:Math.max(0,Math.floor(options.maxAge));
document.cookie=encodeURIComponent(name)+"="+encodeURIComponent(value)+
";max-age="+MAX_AGE+cookieOptions(options);
";max-age="+maxAge+cookieOptions(options);
}
function deleteCookie(name,options){
document.cookie=encodeURIComponent(name)+"=;max-age=0"+cookieOptions(options);
}
function configuredCookieOptions(preference,override){
var resolved=Object.assign({},COOKIE_CONFIG.defaults||{});
if(typeof preference==="string")Object.assign(resolved,COOKIE_CONFIG[preference]||{});
else if(preference)Object.assign(resolved,preference);
if(override)Object.assign(resolved,override);
return resolved;
}
window.wrnCookies={
config:COOKIE_CONFIG,
get:readCookie,
options:configuredCookieOptions,
set:function(name,value,preference,override){
writeCookie(name,value,configuredCookieOptions(preference,override));
},
remove:function(name,preference,override){
deleteCookie(name,configuredCookieOptions(preference,override));
}
};
function syncStylesheet(themeName,accentName){
var link=document.querySelector("link[data-wrnexus-theme]");
if(!link)return;
@@ -720,7 +801,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
function setTheme(name){
if(THEMES.indexOf(name)<0)return;
el.setAttribute("data-theme",name);
writeCookie(THEME_COOKIE,name);
writeCookie(THEME_COOKIE,name,THEME_COOKIE_OPTIONS);
syncStylesheet(name,getAccent());
syncThemeButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:name}}));