@venix-sistemas/jade 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +481 -0
- package/dist/module.d.mts +26 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +249 -0
- package/dist/runtime/components/VenixIcon.d.vue.ts +14 -0
- package/dist/runtime/components/VenixIcon.vue +37 -0
- package/dist/runtime/components/VenixIcon.vue.d.ts +14 -0
- package/dist/runtime/components/VenixThemeSwitcher.d.vue.ts +9 -0
- package/dist/runtime/components/VenixThemeSwitcher.vue +191 -0
- package/dist/runtime/components/VenixThemeSwitcher.vue.d.ts +9 -0
- package/dist/runtime/composables/useThemeColors.d.ts +10 -0
- package/dist/runtime/composables/useThemeColors.js +25 -0
- package/dist/runtime/composables/useThemeCookies.d.ts +7 -0
- package/dist/runtime/composables/useThemeCookies.js +33 -0
- package/dist/runtime/composables/useThemeLocale.d.ts +12 -0
- package/dist/runtime/composables/useThemeLocale.js +87 -0
- package/dist/runtime/composables/useThemeSeasonal.d.ts +4 -0
- package/dist/runtime/composables/useThemeSeasonal.js +7 -0
- package/dist/runtime/composables/useVenixIcon.d.ts +3 -0
- package/dist/runtime/composables/useVenixIcon.js +11 -0
- package/dist/runtime/composables/useVenixTheme.d.ts +33 -0
- package/dist/runtime/composables/useVenixTheme.js +186 -0
- package/dist/runtime/nitro/theme-init.d.ts +2 -0
- package/dist/runtime/nitro/theme-init.js +11 -0
- package/dist/runtime/plugin.d.ts +2 -0
- package/dist/runtime/plugin.js +3 -0
- package/dist/runtime/plugins/theme-init.server.d.ts +2 -0
- package/dist/runtime/plugins/theme-init.server.js +37 -0
- package/dist/runtime/plugins/vuetify-theme.d.ts +2 -0
- package/dist/runtime/plugins/vuetify-theme.js +42 -0
- package/dist/runtime/public/fonts/Iceberg/Iceberg-Regular.woff2 +0 -0
- package/dist/runtime/public/images/ui/cursor/cursor-hand.png +0 -0
- package/dist/runtime/public/images/ui/cursor/cursor-text.png +0 -0
- package/dist/runtime/public/images/ui/cursor/cursor.png +0 -0
- package/dist/runtime/scripts/theme-init.d.ts +27 -0
- package/dist/runtime/scripts/theme-init.js +89 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/shared/constants.d.ts +28 -0
- package/dist/shared/constants.js +40 -0
- package/dist/shared/css/colors.d.ts +2 -0
- package/dist/shared/css/colors.js +19 -0
- package/dist/shared/css/cursor.d.ts +2 -0
- package/dist/shared/css/cursor.js +74 -0
- package/dist/shared/css/index.d.ts +8 -0
- package/dist/shared/css/index.js +31 -0
- package/dist/shared/css/scrollbar.d.ts +2 -0
- package/dist/shared/css/scrollbar.js +51 -0
- package/dist/shared/css/transition.d.ts +2 -0
- package/dist/shared/css/transition.js +20 -0
- package/dist/shared/css/typography.d.ts +3 -0
- package/dist/shared/css/typography.js +62 -0
- package/dist/shared/theme.json +193 -0
- package/dist/shared/types/color-options.d.ts +14 -0
- package/dist/shared/types/color-options.js +0 -0
- package/dist/shared/types/colors.d.ts +38 -0
- package/dist/shared/types/colors.js +0 -0
- package/dist/shared/types/cursor.d.ts +7 -0
- package/dist/shared/types/cursor.js +0 -0
- package/dist/shared/types/icon.d.ts +7 -0
- package/dist/shared/types/icon.js +0 -0
- package/dist/shared/types/index.d.ts +10 -0
- package/dist/shared/types/index.js +1 -0
- package/dist/shared/types/runtime-config.d.ts +33 -0
- package/dist/shared/types/runtime-config.js +1 -0
- package/dist/shared/types/scrollbar.d.ts +12 -0
- package/dist/shared/types/scrollbar.js +0 -0
- package/dist/shared/types/theme.d.ts +11 -0
- package/dist/shared/types/theme.js +0 -0
- package/dist/shared/types/translation.d.ts +16 -0
- package/dist/shared/types/translation.js +0 -0
- package/dist/shared/types/typography.d.ts +15 -0
- package/dist/shared/types/typography.js +0 -0
- package/dist/shared/types/vuetify.d.ts +3 -0
- package/dist/shared/types/vuetify.js +0 -0
- package/dist/shared/unocss-preset.d.ts +32 -0
- package/dist/shared/unocss-preset.js +13 -0
- package/dist/shared/utils/consent.d.ts +8 -0
- package/dist/shared/utils/consent.js +12 -0
- package/dist/shared/utils/icon-refs.d.ts +10 -0
- package/dist/shared/utils/icon-refs.js +24 -0
- package/dist/shared/utils/icon.d.ts +27 -0
- package/dist/shared/utils/icon.js +20 -0
- package/dist/shared/utils/index.d.ts +9 -0
- package/dist/shared/utils/index.js +12 -0
- package/dist/shared/utils/load.d.ts +4 -0
- package/dist/shared/utils/load.js +24 -0
- package/dist/shared/utils/normalize.d.ts +20 -0
- package/dist/shared/utils/normalize.js +38 -0
- package/dist/shared/utils/options.d.ts +10 -0
- package/dist/shared/utils/options.js +12 -0
- package/dist/shared/utils/theme-config.d.ts +14 -0
- package/dist/shared/utils/theme-config.js +22 -0
- package/dist/shared/utils/theme-resolve.d.ts +24 -0
- package/dist/shared/utils/theme-resolve.js +45 -0
- package/dist/types.d.mts +3 -0
- package/package.json +100 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { watch } from "vue";
|
|
2
|
+
import { useRequestHeaders, useState, useRoute } from "#app";
|
|
3
|
+
import { DEFAULT_LOCALE } from "../../shared/constants";
|
|
4
|
+
import { normalizeLocale, extractFirstLocale } from "../../shared/utils/normalize";
|
|
5
|
+
import { hasCookieConsent } from "../../shared/utils/consent";
|
|
6
|
+
export const useThemeLocale = (theme, localeCookie, options = {}) => {
|
|
7
|
+
const { enabled = true, forcedLocale, defaultLocale } = options;
|
|
8
|
+
const headers = useRequestHeaders(["accept-language"]);
|
|
9
|
+
const route = useRoute();
|
|
10
|
+
const fallbackLocale = defaultLocale || DEFAULT_LOCALE;
|
|
11
|
+
const detectLocale = () => {
|
|
12
|
+
const getAvailableLocales = () => {
|
|
13
|
+
const availableLocales = /* @__PURE__ */ new Set();
|
|
14
|
+
Object.values(theme.colors.themes).forEach((themeConfig) => {
|
|
15
|
+
if (themeConfig.translations) {
|
|
16
|
+
Object.keys(themeConfig.translations).forEach((locale) => {
|
|
17
|
+
availableLocales.add(normalizeLocale(locale));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return Array.from(availableLocales);
|
|
22
|
+
};
|
|
23
|
+
const findLocaleInAvailable = (requestedLocale) => {
|
|
24
|
+
const availableLocales = getAvailableLocales();
|
|
25
|
+
const normalized = normalizeLocale(requestedLocale);
|
|
26
|
+
if (availableLocales.includes(normalized)) return normalized;
|
|
27
|
+
const baseLocale = normalized.split("-")[0];
|
|
28
|
+
return availableLocales.find((locale) => locale.split("-")[0] === baseLocale) || null;
|
|
29
|
+
};
|
|
30
|
+
const findBestLocale = (requestedLocale) => findLocaleInAvailable(requestedLocale) || fallbackLocale;
|
|
31
|
+
if (forcedLocale) return findBestLocale(forcedLocale);
|
|
32
|
+
if (!enabled) return fallbackLocale;
|
|
33
|
+
if (localeCookie.value) return findBestLocale(localeCookie.value);
|
|
34
|
+
const pathSegment = route.path.split("/").find(Boolean);
|
|
35
|
+
if (pathSegment) {
|
|
36
|
+
const pathLocale = findLocaleInAvailable(pathSegment);
|
|
37
|
+
if (pathLocale) return pathLocale;
|
|
38
|
+
}
|
|
39
|
+
const acceptLanguageHeader = headers["accept-language"];
|
|
40
|
+
if (acceptLanguageHeader) {
|
|
41
|
+
const acceptLanguage = Array.isArray(acceptLanguageHeader) ? acceptLanguageHeader[0] : acceptLanguageHeader;
|
|
42
|
+
if (acceptLanguage) {
|
|
43
|
+
const firstLocale = extractFirstLocale(acceptLanguage);
|
|
44
|
+
if (firstLocale) return findBestLocale(firstLocale);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (import.meta.client && typeof navigator !== "undefined") {
|
|
48
|
+
return findBestLocale(navigator.language || fallbackLocale);
|
|
49
|
+
}
|
|
50
|
+
return fallbackLocale;
|
|
51
|
+
};
|
|
52
|
+
const currentLocale = useState("venix-theme-locale", () => detectLocale());
|
|
53
|
+
if (enabled && !forcedLocale && localeCookie.value) {
|
|
54
|
+
const cookieLocale = normalizeLocale(localeCookie.value);
|
|
55
|
+
if (cookieLocale !== currentLocale.value) {
|
|
56
|
+
currentLocale.value = cookieLocale;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
watch(localeCookie, (newLocale) => {
|
|
60
|
+
if (enabled && newLocale && !forcedLocale) {
|
|
61
|
+
currentLocale.value = normalizeLocale(newLocale);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const translate = (translations, fallback) => {
|
|
65
|
+
if (!translations) return fallback || "";
|
|
66
|
+
const locale = currentLocale.value;
|
|
67
|
+
if (translations[locale]) return translations[locale];
|
|
68
|
+
const baseLocale = locale.split("-")[0];
|
|
69
|
+
if (baseLocale && translations[baseLocale]) return translations[baseLocale];
|
|
70
|
+
if (translations[fallbackLocale]) return translations[fallbackLocale];
|
|
71
|
+
if (translations["en-US"]) return translations["en-US"];
|
|
72
|
+
const firstTranslation = Object.values(translations)[0];
|
|
73
|
+
if (firstTranslation) return firstTranslation;
|
|
74
|
+
return fallback || "";
|
|
75
|
+
};
|
|
76
|
+
const setLocale = (locale) => {
|
|
77
|
+
currentLocale.value = normalizeLocale(locale);
|
|
78
|
+
if (enabled && !forcedLocale && hasCookieConsent()) {
|
|
79
|
+
localeCookie.value = normalizeLocale(locale);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
currentLocale,
|
|
84
|
+
translate,
|
|
85
|
+
setLocale
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getActiveSeasonalTheme } from "../../shared/utils/theme-resolve";
|
|
2
|
+
export const useThemeSeasonal = (theme) => {
|
|
3
|
+
const getActiveSeasonalThemeFor = (prefersDark) => getActiveSeasonalTheme(theme.colors.themes, prefersDark);
|
|
4
|
+
return {
|
|
5
|
+
getActiveSeasonalTheme: getActiveSeasonalThemeFor
|
|
6
|
+
};
|
|
7
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useRuntimeConfig } from "#app";
|
|
3
|
+
import { resolveIcon } from "../../shared/utils/icon";
|
|
4
|
+
export function useVenixIcon(icon) {
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const aliases = config.public.venixTheme?.icon?.aliases || {};
|
|
7
|
+
return computed(() => {
|
|
8
|
+
const value = typeof icon === "string" ? icon : icon.value;
|
|
9
|
+
return resolveIcon(value, aliases);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const useVenixTheme: () => {
|
|
2
|
+
theme: {
|
|
3
|
+
value: import("vue").ComputedRef<string>;
|
|
4
|
+
preference: import("vue").Ref<string, string>;
|
|
5
|
+
data: import("vue").ComputedRef<{
|
|
6
|
+
value: string;
|
|
7
|
+
name: string;
|
|
8
|
+
icon: import("../../shared/utils/icon.js").ResolvedThemeIcon | null;
|
|
9
|
+
} | undefined>;
|
|
10
|
+
toggle: (forceTheme?: string) => void;
|
|
11
|
+
isSeasonalActive: import("vue").ComputedRef<boolean>;
|
|
12
|
+
activeSeasonalTheme: import("vue").ComputedRef<string | null>;
|
|
13
|
+
shouldApplyColors: boolean;
|
|
14
|
+
colors: import("../../shared/types/index.js").ColorsConfig;
|
|
15
|
+
};
|
|
16
|
+
themes: import("vue").ComputedRef<{
|
|
17
|
+
value: string;
|
|
18
|
+
name: string;
|
|
19
|
+
icon: import("../../shared/utils/icon.js").ResolvedThemeIcon | null;
|
|
20
|
+
}[]>;
|
|
21
|
+
locale: {
|
|
22
|
+
current: import("vue").Ref<string, string>;
|
|
23
|
+
set: (locale: string) => void;
|
|
24
|
+
translate: (translations?: Record<string, string>, fallback?: string) => string;
|
|
25
|
+
};
|
|
26
|
+
persistence: {
|
|
27
|
+
hasConsent: import("vue").Ref<boolean, boolean>;
|
|
28
|
+
grant: () => void;
|
|
29
|
+
revoke: () => void;
|
|
30
|
+
enable: () => void;
|
|
31
|
+
disable: () => void;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { computed, watch, onMounted, onUnmounted, getCurrentInstance } from "vue";
|
|
2
|
+
import { useRuntimeConfig, useState } from "#app";
|
|
3
|
+
import { DEFAULT_THEME, DEFAULT_LOCALE, COOKIE_PREFERENCES_UPDATED_EVENT, COOKIE_CONSENT_STORAGE_KEY, THEME_APPLIED_EVENT } from "../../shared/constants";
|
|
4
|
+
import { useThemeCookies } from "./useThemeCookies.js";
|
|
5
|
+
import { useThemeLocale } from "./useThemeLocale.js";
|
|
6
|
+
import { useThemeSeasonal } from "./useThemeSeasonal.js";
|
|
7
|
+
import { useThemeColors } from "./useThemeColors.js";
|
|
8
|
+
import { hasCookieConsent } from "../../shared/utils/consent";
|
|
9
|
+
import { buildResolvedTheme } from "../../shared/utils/theme-config";
|
|
10
|
+
import { resolveThemePreference } from "../../shared/utils/theme-resolve";
|
|
11
|
+
export const useVenixTheme = () => {
|
|
12
|
+
const config = useRuntimeConfig();
|
|
13
|
+
const themeConfig = config.public.venixTheme;
|
|
14
|
+
const theme = buildResolvedTheme(themeConfig);
|
|
15
|
+
const defaultTheme = theme.colors.defaultColor || DEFAULT_THEME;
|
|
16
|
+
const shouldApplyColors = themeConfig?.applyColors !== false && theme.colors.defaults !== false;
|
|
17
|
+
const cookies = useThemeCookies(themeConfig?.localeCookie);
|
|
18
|
+
const hasConsent = useState(
|
|
19
|
+
"venix-cookie-consent",
|
|
20
|
+
() => typeof window !== "undefined" && hasCookieConsent()
|
|
21
|
+
);
|
|
22
|
+
const locale = useThemeLocale(theme, cookies.localeCookie, {
|
|
23
|
+
enabled: themeConfig?.enabled?.translation !== false,
|
|
24
|
+
forcedLocale: themeConfig?.locale,
|
|
25
|
+
defaultLocale: themeConfig?.defaultLocale || DEFAULT_LOCALE
|
|
26
|
+
});
|
|
27
|
+
if (themeConfig?.enabled?.manageHtmlLang) {
|
|
28
|
+
if (typeof document !== "undefined") {
|
|
29
|
+
document.documentElement.lang = locale.currentLocale.value;
|
|
30
|
+
}
|
|
31
|
+
watch(locale.currentLocale, (newLocale) => {
|
|
32
|
+
if (typeof document !== "undefined") document.documentElement.lang = newLocale;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const seasonal = useThemeSeasonal(theme);
|
|
36
|
+
const colors = useThemeColors(theme, locale.translate, themeConfig?.iconFormat || "svg");
|
|
37
|
+
const initialPreference = cookies.preferenceCookie.value || defaultTheme;
|
|
38
|
+
const preference = useState("venix-theme-preference", () => initialPreference || "system");
|
|
39
|
+
const getResolvedTheme = (pref) => {
|
|
40
|
+
const prefersDark = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)").matches : true;
|
|
41
|
+
return resolveThemePreference(pref, theme.colors.themes, defaultTheme, prefersDark);
|
|
42
|
+
};
|
|
43
|
+
const apply = (pref) => {
|
|
44
|
+
if (!shouldApplyColors) return;
|
|
45
|
+
const resolved = getResolvedTheme(pref);
|
|
46
|
+
if (typeof document !== "undefined") {
|
|
47
|
+
const html = document.documentElement;
|
|
48
|
+
html.classList.remove(...colors.themeValues.value);
|
|
49
|
+
html.classList.add(resolved);
|
|
50
|
+
html.setAttribute("data-theme", resolved);
|
|
51
|
+
window.__VENIX_INITIAL_THEME__ = resolved;
|
|
52
|
+
window.dispatchEvent(new CustomEvent(THEME_APPLIED_EVENT, { detail: resolved }));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const applyWithTransition = (pref) => {
|
|
56
|
+
const canAnimate = typeof document !== "undefined" && typeof document.startViewTransition === "function" && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
57
|
+
if (!canAnimate) {
|
|
58
|
+
apply(pref);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const body = document.body;
|
|
62
|
+
const hasCustomCursor = theme.customCursor?.enabled !== false && !!theme.customCursor?.cursors;
|
|
63
|
+
const currentCursor = hasCustomCursor ? getComputedStyle(body).cursor : null;
|
|
64
|
+
if (currentCursor && currentCursor !== "auto") {
|
|
65
|
+
body.style.setProperty("cursor", currentCursor, "important");
|
|
66
|
+
}
|
|
67
|
+
const restoreCursor = () => body.style.removeProperty("cursor");
|
|
68
|
+
const transition = document.startViewTransition(() => apply(pref));
|
|
69
|
+
transition.ready.catch(() => {
|
|
70
|
+
});
|
|
71
|
+
transition.finished.then(restoreCursor).catch(restoreCursor);
|
|
72
|
+
};
|
|
73
|
+
if (typeof document !== "undefined" && shouldApplyColors) {
|
|
74
|
+
if (cookies.preferenceCookie.value && cookies.preferenceCookie.value !== preference.value) {
|
|
75
|
+
preference.value = cookies.preferenceCookie.value;
|
|
76
|
+
}
|
|
77
|
+
const currentTheme = document.documentElement.getAttribute("data-theme");
|
|
78
|
+
if (currentTheme) {
|
|
79
|
+
if (!cookies.preferenceCookie.value) {
|
|
80
|
+
preference.value = currentTheme;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
apply(preference.value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const syncPersistence = () => {
|
|
87
|
+
hasConsent.value = hasCookieConsent();
|
|
88
|
+
if (hasConsent.value) {
|
|
89
|
+
cookies.persistIfConsented(preference.value, getResolvedTheme(preference.value));
|
|
90
|
+
} else {
|
|
91
|
+
cookies.disablePersistence();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const grantPersistence = () => {
|
|
95
|
+
if (typeof localStorage === "undefined") return;
|
|
96
|
+
localStorage.setItem(COOKIE_CONSENT_STORAGE_KEY, JSON.stringify({ functionality: true }));
|
|
97
|
+
syncPersistence();
|
|
98
|
+
window.dispatchEvent(new Event(COOKIE_PREFERENCES_UPDATED_EVENT));
|
|
99
|
+
};
|
|
100
|
+
const revokePersistence = () => {
|
|
101
|
+
if (typeof localStorage === "undefined") return;
|
|
102
|
+
localStorage.setItem(COOKIE_CONSENT_STORAGE_KEY, JSON.stringify({ functionality: false }));
|
|
103
|
+
syncPersistence();
|
|
104
|
+
window.dispatchEvent(new Event(COOKIE_PREFERENCES_UPDATED_EVENT));
|
|
105
|
+
};
|
|
106
|
+
const instance = getCurrentInstance();
|
|
107
|
+
const runOnClientMount = (fn) => {
|
|
108
|
+
if (instance) {
|
|
109
|
+
onMounted(fn);
|
|
110
|
+
} else if (typeof window !== "undefined") {
|
|
111
|
+
queueMicrotask(fn);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
runOnClientMount(() => {
|
|
115
|
+
hasConsent.value = hasCookieConsent();
|
|
116
|
+
if (!shouldApplyColors) return;
|
|
117
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
118
|
+
const handleSystemThemeChange = () => {
|
|
119
|
+
if (preference.value === "system") applyWithTransition("system");
|
|
120
|
+
};
|
|
121
|
+
mq.addEventListener("change", handleSystemThemeChange);
|
|
122
|
+
const checkSeasonalChange = () => {
|
|
123
|
+
const now = /* @__PURE__ */ new Date();
|
|
124
|
+
const msToMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).getTime() - now.getTime();
|
|
125
|
+
return setTimeout(() => {
|
|
126
|
+
if (preference.value === "system") applyWithTransition("system");
|
|
127
|
+
checkSeasonalChange();
|
|
128
|
+
}, msToMidnight + 1e3);
|
|
129
|
+
};
|
|
130
|
+
const midnightTimeout = checkSeasonalChange();
|
|
131
|
+
window.addEventListener(COOKIE_PREFERENCES_UPDATED_EVENT, syncPersistence);
|
|
132
|
+
if (instance) {
|
|
133
|
+
onUnmounted(() => {
|
|
134
|
+
mq.removeEventListener("change", handleSystemThemeChange);
|
|
135
|
+
clearTimeout(midnightTimeout);
|
|
136
|
+
window.removeEventListener(COOKIE_PREFERENCES_UPDATED_EVENT, syncPersistence);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
watch(preference, (newPref) => {
|
|
141
|
+
if (!shouldApplyColors) return;
|
|
142
|
+
applyWithTransition(newPref);
|
|
143
|
+
cookies.persistIfConsented(newPref, getResolvedTheme(newPref));
|
|
144
|
+
});
|
|
145
|
+
const toggle = (forceTheme) => {
|
|
146
|
+
const newTheme = forceTheme ?? (preference.value === "light" ? "dark" : "light");
|
|
147
|
+
preference.value = newTheme;
|
|
148
|
+
cookies.persistIfConsented(newTheme, getResolvedTheme(newTheme));
|
|
149
|
+
};
|
|
150
|
+
const data = computed(
|
|
151
|
+
() => colors.themes.value.find((t) => t.value === preference.value) ?? colors.themes.value[0]
|
|
152
|
+
);
|
|
153
|
+
const isSeasonalActive = computed(() => {
|
|
154
|
+
if (preference.value !== "system") return false;
|
|
155
|
+
return seasonal.getActiveSeasonalTheme() !== null;
|
|
156
|
+
});
|
|
157
|
+
const activeSeasonalTheme = computed(() => {
|
|
158
|
+
if (preference.value !== "system") return null;
|
|
159
|
+
return seasonal.getActiveSeasonalTheme();
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
theme: {
|
|
163
|
+
value: computed(() => getResolvedTheme(preference.value)),
|
|
164
|
+
preference,
|
|
165
|
+
data,
|
|
166
|
+
toggle,
|
|
167
|
+
isSeasonalActive,
|
|
168
|
+
activeSeasonalTheme,
|
|
169
|
+
shouldApplyColors,
|
|
170
|
+
colors: theme.colors
|
|
171
|
+
},
|
|
172
|
+
themes: colors.themes,
|
|
173
|
+
locale: {
|
|
174
|
+
current: locale.currentLocale,
|
|
175
|
+
set: locale.setLocale,
|
|
176
|
+
translate: locale.translate
|
|
177
|
+
},
|
|
178
|
+
persistence: {
|
|
179
|
+
hasConsent,
|
|
180
|
+
grant: grantPersistence,
|
|
181
|
+
revoke: revokePersistence,
|
|
182
|
+
enable: () => cookies.persistIfConsented(preference.value, getResolvedTheme(preference.value)),
|
|
183
|
+
disable: cookies.disablePersistence
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime";
|
|
2
|
+
import initTheme from "../scripts/theme-init.js";
|
|
3
|
+
import { buildResolvedTheme } from "../../shared/utils/theme-config";
|
|
4
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
5
|
+
const theme = buildResolvedTheme(useRuntimeConfig().public.venixTheme);
|
|
6
|
+
const inlineScript = `<script>(${initTheme.toString()})(${JSON.stringify(theme)})<\/script>`;
|
|
7
|
+
const hookRenderHtml = nitroApp.hooks.hook;
|
|
8
|
+
hookRenderHtml("render:html", (html) => {
|
|
9
|
+
html.head.unshift(inlineScript);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig, useCookie, useHead } from "#app";
|
|
2
|
+
import { THEME_PREFERENCE_COOKIE, THEME_RESOLVED_COOKIE, DEFAULT_LOCALE_COOKIE_NAME, DEFAULT_LOCALE, DEFAULT_THEME } from "../../shared/constants";
|
|
3
|
+
import { useThemeLocale } from "../composables/useThemeLocale.js";
|
|
4
|
+
import { buildResolvedTheme } from "../../shared/utils/theme-config";
|
|
5
|
+
import { isColorTheme, resolveThemePreference } from "../../shared/utils/theme-resolve";
|
|
6
|
+
export default defineNuxtPlugin({
|
|
7
|
+
name: "venix-theme-init-server",
|
|
8
|
+
enforce: "pre",
|
|
9
|
+
setup() {
|
|
10
|
+
const config = useRuntimeConfig();
|
|
11
|
+
const themeConfig = config.public.venixTheme;
|
|
12
|
+
const resolvedCookie = useCookie(THEME_RESOLVED_COOKIE);
|
|
13
|
+
const preferenceCookie = useCookie(THEME_PREFERENCE_COOKIE);
|
|
14
|
+
const theme = buildResolvedTheme(themeConfig);
|
|
15
|
+
const defaultTheme = theme.colors.defaultColor || DEFAULT_THEME;
|
|
16
|
+
let resolvedTheme = defaultTheme;
|
|
17
|
+
if (resolvedCookie.value && isColorTheme(theme.colors.themes[resolvedCookie.value])) {
|
|
18
|
+
resolvedTheme = resolvedCookie.value;
|
|
19
|
+
} else if (preferenceCookie.value) {
|
|
20
|
+
resolvedTheme = resolveThemePreference(preferenceCookie.value, theme.colors.themes, defaultTheme, true);
|
|
21
|
+
}
|
|
22
|
+
const htmlAttrs = {
|
|
23
|
+
"data-theme": resolvedTheme,
|
|
24
|
+
"class": resolvedTheme
|
|
25
|
+
};
|
|
26
|
+
if (themeConfig?.enabled?.manageHtmlLang) {
|
|
27
|
+
const localeCookie = useCookie(themeConfig?.localeCookie || DEFAULT_LOCALE_COOKIE_NAME);
|
|
28
|
+
const locale = useThemeLocale(theme, localeCookie, {
|
|
29
|
+
enabled: themeConfig?.enabled?.translation !== false,
|
|
30
|
+
forcedLocale: themeConfig?.locale,
|
|
31
|
+
defaultLocale: themeConfig?.defaultLocale || DEFAULT_LOCALE
|
|
32
|
+
});
|
|
33
|
+
htmlAttrs.lang = locale.currentLocale.value;
|
|
34
|
+
}
|
|
35
|
+
useHead({ htmlAttrs });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig, useCookie } from "#app";
|
|
2
|
+
import { THEME_PREFERENCE_COOKIE, THEME_RESOLVED_COOKIE, THEME_APPLIED_EVENT, DEFAULT_THEME } from "../../shared/constants";
|
|
3
|
+
import { buildResolvedTheme } from "../../shared/utils/theme-config";
|
|
4
|
+
function applyVuetifyTheme(vuetify, name) {
|
|
5
|
+
if (typeof vuetify.theme.change === "function") {
|
|
6
|
+
vuetify.theme.change(name);
|
|
7
|
+
} else {
|
|
8
|
+
vuetify.theme.global.name.value = name;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export default defineNuxtPlugin({
|
|
12
|
+
name: "venix-theme-vuetify-sync",
|
|
13
|
+
enforce: "pre",
|
|
14
|
+
setup(nuxtApp) {
|
|
15
|
+
const config = useRuntimeConfig();
|
|
16
|
+
const themeConfig = config.public.venixTheme;
|
|
17
|
+
const resolvedCookie = useCookie(THEME_RESOLVED_COOKIE);
|
|
18
|
+
const preferenceCookie = useCookie(THEME_PREFERENCE_COOKIE);
|
|
19
|
+
const theme = buildResolvedTheme(themeConfig);
|
|
20
|
+
const defaultTheme = theme.colors.defaultColor || DEFAULT_THEME;
|
|
21
|
+
const hookBeforeCreate = nuxtApp.hook;
|
|
22
|
+
hookBeforeCreate("vuetify:before-create", ({ vuetifyOptions }) => {
|
|
23
|
+
const themes = vuetifyOptions?.theme?.themes;
|
|
24
|
+
if (!vuetifyOptions?.theme || !themes) return;
|
|
25
|
+
let resolved = defaultTheme;
|
|
26
|
+
if (resolvedCookie.value && themes[resolvedCookie.value]) {
|
|
27
|
+
resolved = resolvedCookie.value;
|
|
28
|
+
} else if (preferenceCookie.value && themes[preferenceCookie.value]) {
|
|
29
|
+
resolved = preferenceCookie.value;
|
|
30
|
+
}
|
|
31
|
+
vuetifyOptions.theme.defaultTheme = resolved;
|
|
32
|
+
});
|
|
33
|
+
if (import.meta.client) {
|
|
34
|
+
const hookReady = nuxtApp.hook;
|
|
35
|
+
hookReady("vuetify:ready", (vuetify) => {
|
|
36
|
+
window.addEventListener(THEME_APPLIED_EVENT, ((event) => {
|
|
37
|
+
applyVuetifyTheme(vuetify, event.detail);
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ThemeConfig } from '../../shared/types/index.js';
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
__VENIX_INITIAL_THEME__?: string;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Runs as an inline script in `<head>`, injected via Nitro's `render:html`
|
|
9
|
+
* hook by the `runtime/nitro/theme-init.ts` plugin (see
|
|
10
|
+
* `setup/register-plugins.ts`) — BEFORE the Vue bundle, to avoid a
|
|
11
|
+
* wrong-theme flash before hydration (SSR doesn't know the user's real
|
|
12
|
+
* light/dark preference when the preference is 'system', nor does it have
|
|
13
|
+
* `matchMedia`).
|
|
14
|
+
*
|
|
15
|
+
* Because it runs this way, this function (only this function — the rest of
|
|
16
|
+
* the file, including the type imports, is erased by TypeScript and never
|
|
17
|
+
* reaches the browser) has to be 100% self-contained: `initTheme.toString()`
|
|
18
|
+
* gets serialized and executed as a standalone IIFE, so any import from
|
|
19
|
+
* another module would become a `ReferenceError` on the client. That's why
|
|
20
|
+
* the cookie names below and the theme-resolution rule
|
|
21
|
+
* (`resolveThemePreference`/`getActiveSeasonalTheme`) are deliberately
|
|
22
|
+
* duplicated — the canonical implementation, shared between the composable
|
|
23
|
+
* and the SSR plugin, lives in `shared/constants.ts` and
|
|
24
|
+
* `shared/utils/theme-resolve.ts`. Keep the two sides in sync when changing
|
|
25
|
+
* either one.
|
|
26
|
+
*/
|
|
27
|
+
export default function initTheme(themeData: ThemeConfig): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export default function initTheme(themeData) {
|
|
2
|
+
const THEME_PREFERENCE_COOKIE = "venix-theme-preference";
|
|
3
|
+
const THEME_RESOLVED_COOKIE = "venix-theme-resolved";
|
|
4
|
+
const COOKIE_CONSENT_STORAGE_KEY = "venix-cookie-consent";
|
|
5
|
+
function hasCookieConsent() {
|
|
6
|
+
if (typeof localStorage === "undefined") return false;
|
|
7
|
+
const consentData = localStorage.getItem(COOKIE_CONSENT_STORAGE_KEY);
|
|
8
|
+
if (!consentData) return false;
|
|
9
|
+
try {
|
|
10
|
+
const consent = JSON.parse(consentData);
|
|
11
|
+
return consent?.functionality === true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function getCookie(name) {
|
|
17
|
+
const match = document.cookie.match(new RegExp("(^|; )" + name + "=([^;]+)"));
|
|
18
|
+
const cookieValue = match?.[2];
|
|
19
|
+
return cookieValue ? decodeURIComponent(cookieValue) : null;
|
|
20
|
+
}
|
|
21
|
+
function setCookieIfConsented(name, value) {
|
|
22
|
+
if (hasCookieConsent()) {
|
|
23
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=31536000; SameSite=Lax`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isColorTheme(themeConfig) {
|
|
27
|
+
if (!themeConfig) return false;
|
|
28
|
+
return !!themeConfig.primary || !!themeConfig.background;
|
|
29
|
+
}
|
|
30
|
+
function isDateInSeasonalRange(themeConfig, now) {
|
|
31
|
+
if (themeConfig.seasonal !== true || !themeConfig.dateRange) return false;
|
|
32
|
+
const startParts = themeConfig.dateRange.start.split("-").map(Number);
|
|
33
|
+
const endParts = themeConfig.dateRange.end.split("-").map(Number);
|
|
34
|
+
if (startParts.length !== 2 || endParts.length !== 2) return false;
|
|
35
|
+
const [startMonth, startDay] = startParts;
|
|
36
|
+
const [endMonth, endDay] = endParts;
|
|
37
|
+
if (startMonth === void 0 || startDay === void 0 || endMonth === void 0 || endDay === void 0) return false;
|
|
38
|
+
const current = (now.getMonth() + 1) * 100 + now.getDate();
|
|
39
|
+
const startNum = startMonth * 100 + startDay;
|
|
40
|
+
const endNum = endMonth * 100 + endDay;
|
|
41
|
+
return startNum <= endNum ? current >= startNum && current <= endNum : current >= startNum || current <= endNum;
|
|
42
|
+
}
|
|
43
|
+
function getActiveSeasonalTheme(themes2, prefersDark) {
|
|
44
|
+
const now = /* @__PURE__ */ new Date();
|
|
45
|
+
for (const [themeName, themeConfig] of Object.entries(themes2)) {
|
|
46
|
+
if (themeName === "system") continue;
|
|
47
|
+
if (!isColorTheme(themeConfig)) continue;
|
|
48
|
+
if (!isDateInSeasonalRange(themeConfig, now)) continue;
|
|
49
|
+
if (themeConfig.dark !== prefersDark) continue;
|
|
50
|
+
return themeName;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function resolveThemePreference(preference2, themes2, defaultTheme2, prefersDark) {
|
|
55
|
+
let resolved;
|
|
56
|
+
if (preference2 === "system") {
|
|
57
|
+
resolved = getActiveSeasonalTheme(themes2, prefersDark) || (prefersDark ? "dark" : "light");
|
|
58
|
+
} else if (preference2 && isColorTheme(themes2[preference2])) {
|
|
59
|
+
resolved = preference2;
|
|
60
|
+
} else {
|
|
61
|
+
resolved = defaultTheme2;
|
|
62
|
+
}
|
|
63
|
+
if (!isColorTheme(themes2[resolved])) {
|
|
64
|
+
resolved = isColorTheme(themes2[defaultTheme2]) ? defaultTheme2 : "dark";
|
|
65
|
+
}
|
|
66
|
+
return resolved;
|
|
67
|
+
}
|
|
68
|
+
const html = document.documentElement;
|
|
69
|
+
const themes = themeData.colors?.themes || {};
|
|
70
|
+
const defaultTheme = themeData.colors?.defaultColor || "dark";
|
|
71
|
+
const preference = getCookie(THEME_PREFERENCE_COOKIE);
|
|
72
|
+
const resolvedCookie = getCookie(THEME_RESOLVED_COOKIE);
|
|
73
|
+
const systemPrefersDark = typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
74
|
+
let theme;
|
|
75
|
+
if (resolvedCookie && isColorTheme(themes[resolvedCookie])) {
|
|
76
|
+
theme = resolvedCookie;
|
|
77
|
+
} else {
|
|
78
|
+
theme = resolveThemePreference(preference || void 0, themes, defaultTheme, systemPrefersDark);
|
|
79
|
+
}
|
|
80
|
+
html.classList.add(theme);
|
|
81
|
+
html.setAttribute("data-theme", theme);
|
|
82
|
+
window.__VENIX_INITIAL_THEME__ = theme;
|
|
83
|
+
if (!resolvedCookie || resolvedCookie !== theme) {
|
|
84
|
+
setCookieIfConsented(THEME_RESOLVED_COOKIE, theme);
|
|
85
|
+
}
|
|
86
|
+
if (!preference) {
|
|
87
|
+
setCookieIfConsented(THEME_PREFERENCE_COOKIE, theme);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const THEME_PREFERENCE_COOKIE = "venix-theme-preference";
|
|
2
|
+
export declare const THEME_RESOLVED_COOKIE = "venix-theme-resolved";
|
|
3
|
+
export declare const COOKIE_PREFERENCES_UPDATED_EVENT = "venix-cookie-preferences-updated";
|
|
4
|
+
/**
|
|
5
|
+
* Dispatched synchronously by `apply()` (useVenixTheme.ts), inside the same
|
|
6
|
+
* `document.startViewTransition()` callback — BEFORE the View Transition
|
|
7
|
+
* captures the new state. Integrations that need to react to theme changes
|
|
8
|
+
* (e.g. `vuetify-theme.ts`) should listen for this event instead of watching
|
|
9
|
+
* `theme.preference` on their own: a separate `watch()` runs asynchronously,
|
|
10
|
+
* decoupled from the transition (Vue's timing isn't the browser's), so the
|
|
11
|
+
* integration's DOM mutation ends up competing with the View Transition's
|
|
12
|
+
* snapshot capture for the same frame — in practice this can cause a real,
|
|
13
|
+
* noticeable freeze in apps with a lot of theme-reactive DOM (e.g. Vuetify
|
|
14
|
+
* recomputing CSS vars for every component). Listening for this event
|
|
15
|
+
* guarantees the integration runs in the same synchronous tick as the
|
|
16
|
+
* `data-theme` change, as part of the same transition.
|
|
17
|
+
*/
|
|
18
|
+
export declare const THEME_APPLIED_EVENT = "venix-theme-applied";
|
|
19
|
+
/** Read (never written) by this module — the consumer app's cookie-consent UI writes here. */
|
|
20
|
+
export declare const COOKIE_CONSENT_STORAGE_KEY = "venix-cookie-consent";
|
|
21
|
+
export declare const DEFAULT_LOCALE = "en-US";
|
|
22
|
+
export declare const DEFAULT_LOCALE_COOKIE_NAME = "i18n_redirected";
|
|
23
|
+
export declare const LOCALE_MAP: Record<string, string>;
|
|
24
|
+
export declare const DEFAULT_THEME = "dark";
|
|
25
|
+
export declare const SYSTEM_THEME = "system";
|
|
26
|
+
export declare const COLOR_PROPERTIES: readonly ["primary", "secondary", "accent", "error", "info", "success", "warning", "background", "background2", "background3"];
|
|
27
|
+
export declare const NON_COLOR_PROPERTIES: readonly ["dark", "seasonal", "dateRange", "translations", "icon"];
|
|
28
|
+
export declare const CSS_HEADER = "/* ============================================ */";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const THEME_PREFERENCE_COOKIE = "venix-theme-preference";
|
|
2
|
+
export const THEME_RESOLVED_COOKIE = "venix-theme-resolved";
|
|
3
|
+
export const COOKIE_PREFERENCES_UPDATED_EVENT = "venix-cookie-preferences-updated";
|
|
4
|
+
export const THEME_APPLIED_EVENT = "venix-theme-applied";
|
|
5
|
+
export const COOKIE_CONSENT_STORAGE_KEY = "venix-cookie-consent";
|
|
6
|
+
export const DEFAULT_LOCALE = "en-US";
|
|
7
|
+
export const DEFAULT_LOCALE_COOKIE_NAME = "i18n_redirected";
|
|
8
|
+
export const LOCALE_MAP = {
|
|
9
|
+
pt: "pt-BR",
|
|
10
|
+
en: "en-US",
|
|
11
|
+
es: "es-ES",
|
|
12
|
+
fr: "fr-FR",
|
|
13
|
+
de: "de-DE",
|
|
14
|
+
it: "it-IT",
|
|
15
|
+
ja: "ja-JP",
|
|
16
|
+
ko: "ko-KR",
|
|
17
|
+
zh: "zh-CN"
|
|
18
|
+
};
|
|
19
|
+
export const DEFAULT_THEME = "dark";
|
|
20
|
+
export const SYSTEM_THEME = "system";
|
|
21
|
+
export const COLOR_PROPERTIES = [
|
|
22
|
+
"primary",
|
|
23
|
+
"secondary",
|
|
24
|
+
"accent",
|
|
25
|
+
"error",
|
|
26
|
+
"info",
|
|
27
|
+
"success",
|
|
28
|
+
"warning",
|
|
29
|
+
"background",
|
|
30
|
+
"background2",
|
|
31
|
+
"background3"
|
|
32
|
+
];
|
|
33
|
+
export const NON_COLOR_PROPERTIES = [
|
|
34
|
+
"dark",
|
|
35
|
+
"seasonal",
|
|
36
|
+
"dateRange",
|
|
37
|
+
"translations",
|
|
38
|
+
"icon"
|
|
39
|
+
];
|
|
40
|
+
export const CSS_HEADER = "/* ============================================ */";
|