UNPKG

analytica-frontend-lib

Version:

Repositório público dos componentes utilizados nas plataformas da Analytica Ensino

1 lines 16 kB
{"version":3,"sources":["../src/store/themeStore.ts","../src/utils/cookieUtils.ts","../src/store/themeStorage.ts"],"sourcesContent":["import { create } from 'zustand';\nimport { createJSONStorage, devtools, persist } from 'zustand/middleware';\nimport { themeCookieStorage } from './themeStorage';\n\nexport type ThemeMode = 'light' | 'dark' | 'system';\n\n/**\n * Theme store state interface\n */\nexport interface ThemeState {\n /**\n * Current theme mode\n */\n themeMode: ThemeMode;\n /**\n * Whether the current theme is dark\n */\n isDark: boolean;\n}\n\n/**\n * Theme store actions interface\n */\nexport interface ThemeActions {\n /**\n * Apply theme based on the mode selected\n */\n applyTheme: (mode: ThemeMode) => void;\n /**\n * Toggle between themes\n */\n toggleTheme: () => void;\n /**\n * Set a specific theme mode\n */\n setTheme: (mode: ThemeMode) => void;\n /**\n * Set the white-label theme from institution branding\n */\n setWhiteLabelTheme: (theme: string | null) => void;\n /**\n * Clear the white-label theme and revert to default\n */\n clearWhiteLabelTheme: () => void;\n /**\n * Initialize theme on app start\n */\n initializeTheme: () => void;\n /**\n * Handle system theme change\n */\n handleSystemThemeChange: () => void;\n}\n\nexport type ThemeStore = ThemeState & ThemeActions;\n\n/**\n * Mapa de tema light institucional → tema dark correspondente.\n * Adicionar entradas aqui ao introduzir novas instituições.\n */\nconst DARK_THEME_MAP: Record<string, string> = {\n 'base-light': 'base-dark',\n 'enem-parana-light': 'enem-parana-dark',\n 'enem-paraiba-light': 'enem-paraiba-dark',\n 'analytica-light': 'analytica-dark',\n 'papole-light': 'papole-dark',\n};\n\n/**\n * Resolve o seletor CSS dark concreto com base no tema institucional (light)\n * salvo em `data-original-theme`. Fallback: 'base-dark', que também responde\n * ao seletor legado [data-theme='dark'] preservando compat com apps antigos.\n */\nconst resolveDarkTheme = (originalTheme: string | undefined): string => {\n if (!originalTheme) return 'base-dark';\n return DARK_THEME_MAP[originalTheme] ?? 'base-dark';\n};\n\n/**\n * Apply theme to DOM based on mode\n */\nconst applyThemeToDOM = (mode: ThemeMode): boolean => {\n const htmlElement = document.documentElement;\n const originalTheme = htmlElement.dataset.originalTheme;\n\n if (mode === 'dark') {\n htmlElement.dataset.theme = resolveDarkTheme(originalTheme);\n return true;\n } else if (mode === 'light') {\n if (originalTheme) {\n htmlElement.dataset.theme = originalTheme;\n }\n return false;\n } else if (mode === 'system') {\n const isSystemDark = window.matchMedia(\n '(prefers-color-scheme: dark)'\n ).matches;\n if (isSystemDark) {\n htmlElement.dataset.theme = resolveDarkTheme(originalTheme);\n return true;\n } else if (originalTheme) {\n htmlElement.dataset.theme = originalTheme;\n return false;\n }\n }\n return false;\n};\n\n/**\n * Save original theme from white label (reads from HTML meta tag or data-theme attribute)\n */\nconst saveOriginalTheme = () => {\n const htmlElement = document.documentElement;\n const currentTheme =\n htmlElement.dataset.theme ||\n document.querySelector('meta[name=\"theme\"]')?.getAttribute('content');\n\n if (currentTheme && !htmlElement.dataset.originalTheme) {\n htmlElement.dataset.originalTheme = currentTheme;\n }\n};\n\n/**\n * Theme store using Zustand with persistence\n */\nexport const useThemeStore = create<ThemeStore>()(\n devtools(\n persist(\n (set, get) => ({\n // Initial state\n themeMode: 'system',\n isDark: false,\n\n // Actions\n applyTheme: (mode: ThemeMode) => {\n const isDark = applyThemeToDOM(mode);\n set({ isDark });\n },\n\n toggleTheme: () => {\n const { themeMode, applyTheme } = get();\n let newMode: ThemeMode;\n\n if (themeMode === 'light') {\n newMode = 'dark';\n } else if (themeMode === 'dark') {\n newMode = 'light';\n } else {\n // Se estiver em 'system', vai para 'dark'\n newMode = 'dark';\n }\n\n set({ themeMode: newMode });\n applyTheme(newMode);\n },\n\n setTheme: (mode: ThemeMode) => {\n const { applyTheme } = get();\n set({ themeMode: mode });\n applyTheme(mode);\n },\n\n setWhiteLabelTheme: (theme: string | null) => {\n const htmlElement = document.documentElement;\n\n if (theme) {\n // Set the white-label theme as the original theme\n htmlElement.dataset.originalTheme = theme;\n\n // Apply theme based on current mode\n const { themeMode, applyTheme } = get();\n if (themeMode === 'light' || themeMode === 'system') {\n htmlElement.dataset.theme = theme;\n }\n\n applyTheme(themeMode);\n }\n },\n\n clearWhiteLabelTheme: () => {\n const htmlElement = document.documentElement;\n\n // Remove white-label theme attributes\n delete htmlElement.dataset.originalTheme;\n delete htmlElement.dataset.theme;\n\n // Restore original theme from page metadata\n saveOriginalTheme();\n\n // Re-apply current theme mode to use defaults\n const { themeMode, applyTheme } = get();\n applyTheme(themeMode);\n },\n\n initializeTheme: () => {\n const { themeMode, applyTheme } = get();\n\n // Save original theme from white label\n saveOriginalTheme();\n\n // Apply the current theme mode\n applyTheme(themeMode);\n },\n\n handleSystemThemeChange: () => {\n const { themeMode, applyTheme } = get();\n // Only respond to system changes when in system mode\n if (themeMode === 'system') {\n applyTheme('system');\n }\n },\n }),\n {\n name: 'theme-store', // Storage key (cookie + localStorage)\n // Root-domain cookie so login and app subdomains share the theme\n storage: createJSONStorage(() => themeCookieStorage),\n partialize: (state) => ({\n themeMode: state.themeMode,\n }), // Só persiste o themeMode, não o isDark\n }\n ),\n {\n name: 'theme-store',\n }\n )\n);\n","/**\n * Options for writing a cookie\n */\nexport interface CookieOptions {\n /**\n * Cookie Domain attribute; omitted when undefined (host-only cookie)\n */\n domain?: string;\n /**\n * Cookie Path attribute (default '/')\n */\n path?: string;\n /**\n * Max-Age in seconds\n */\n maxAge?: number;\n /**\n * SameSite attribute (default 'Lax')\n */\n sameSite?: 'Lax' | 'Strict' | 'None';\n /**\n * Adds the Secure attribute when true\n */\n secure?: boolean;\n}\n\n/**\n * Reads a cookie value by name\n *\n * @param name - The cookie name\n * @returns The decoded cookie value, or null when absent or malformed\n */\nexport const getCookie = (name: string): string | null => {\n if (typeof document === 'undefined') {\n return null;\n }\n const prefix = `${encodeURIComponent(name)}=`;\n const match = document.cookie\n .split('; ')\n .find((entry) => entry.startsWith(prefix));\n if (!match) {\n return null;\n }\n try {\n return decodeURIComponent(match.slice(prefix.length));\n } catch {\n return null; // malformed percent-encoding\n }\n};\n\n/**\n * Writes a cookie with the given options\n *\n * @param name - The cookie name\n * @param value - The cookie value (URL-encoded before writing)\n * @param options - Cookie attributes (domain, path, maxAge, sameSite, secure)\n */\nexport const setCookie = (\n name: string,\n value: string,\n options: CookieOptions = {}\n): void => {\n if (typeof document === 'undefined') {\n return;\n }\n const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];\n parts.push(`Path=${options.path ?? '/'}`);\n if (options.maxAge !== undefined) {\n parts.push(`Max-Age=${options.maxAge}`);\n }\n if (options.domain) {\n parts.push(`Domain=${options.domain}`);\n }\n parts.push(`SameSite=${options.sameSite ?? 'Lax'}`);\n if (options.secure) {\n parts.push('Secure');\n }\n document.cookie = parts.join('; ');\n};\n\n/**\n * Removes a cookie by expiring it immediately\n *\n * @param name - The cookie name\n * @param options - Domain and path must match the cookie being removed\n */\nexport const removeCookie = (\n name: string,\n options: Pick<CookieOptions, 'domain' | 'path'> = {}\n): void => {\n setCookie(name, '', { ...options, maxAge: 0 });\n};\n","import type { StateStorage } from 'zustand/middleware';\nimport { getCookie, setCookie, removeCookie } from '../utils/cookieUtils';\nimport { resolveRootHostname } from '../utils/domainUtils';\n\nconst ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365;\n\n/**\n * Resolves the cookie Domain attribute for the current location.\n * Returns undefined on localhost / IP literals so the cookie stays host-only.\n *\n * @returns The root domain for the cookie, or undefined\n */\nconst cookieDomain = (): string | undefined =>\n resolveRootHostname(globalThis.location.hostname) ?? undefined;\n\n/**\n * Reads a value from localStorage without throwing\n * (storage can be disabled in privacy mode or sandboxed iframes)\n *\n * @param name - The storage key\n * @returns The stored value, or null when absent or unavailable\n */\nconst readLocalStorage = (name: string): string | null => {\n try {\n return globalThis.localStorage.getItem(name);\n } catch {\n return null;\n }\n};\n\n/**\n * Returns the value only when it contains valid JSON, null otherwise\n * (guards against corrupted persisted values)\n *\n * @param raw - The raw persisted value, or null when absent\n * @returns The same value when it is valid JSON, or null\n */\nconst validJsonOrNull = (raw: string | null): string | null => {\n if (raw === null) {\n return null;\n }\n try {\n JSON.parse(raw);\n return raw;\n } catch {\n return null;\n }\n};\n\n/**\n * Zustand StateStorage that persists theme state in a cookie scoped to the\n * root domain, so the login app (root domain) and the profile apps\n * (subdomains) all share the chosen theme. Reads fall back to localStorage\n * for users that persisted the theme before the cookie storage existed, and\n * writes go to both stores for backward compatibility with older library\n * versions that read only localStorage.\n */\nexport const themeCookieStorage: StateStorage = {\n getItem: (name: string): string | null => {\n if (typeof document === 'undefined') {\n return null;\n }\n // A corrupted cookie must not block the localStorage fallback\n return (\n validJsonOrNull(getCookie(name)) ??\n validJsonOrNull(readLocalStorage(name))\n );\n },\n setItem: (name: string, value: string): void => {\n if (typeof document === 'undefined') {\n return;\n }\n setCookie(name, value, {\n domain: cookieDomain(),\n path: '/',\n maxAge: ONE_YEAR_IN_SECONDS,\n sameSite: 'Lax',\n secure: globalThis.location.protocol === 'https:',\n });\n try {\n globalThis.localStorage.setItem(name, value);\n } catch {\n // localStorage unavailable; the cookie is the source of truth\n }\n },\n removeItem: (name: string): void => {\n if (typeof document === 'undefined') {\n return;\n }\n removeCookie(name, { domain: cookieDomain(), path: '/' });\n try {\n globalThis.localStorage.removeItem(name);\n } catch {\n // ignore: nothing to clean up when storage is unavailable\n }\n },\n};\n"],"mappings":"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","names":[]}