maz-ui
Version:
A standalone components library for Vue.Js 3 & Nuxt.Js 3
437 lines (436 loc) • 15.9 kB
JavaScript
import { ref, nextTick, watch, onMounted, computed, toValue } from "vue";
import { MazUiTranslations } from "@maz-ui/translations";
function isServer() {
return typeof document > "u" || typeof globalThis.window > "u";
}
function parseHSL(hsl) {
const match = hsl.match(/^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/);
if (!match)
throw new Error(`Invalid HSL format: ${hsl}`);
return {
h: Number.parseFloat(match[1]),
s: Number.parseFloat(match[2]),
l: Number.parseFloat(match[3])
};
}
function formatHSL(h, s, l) {
const roundedH = Math.round(h * 10) / 10, roundedS = Math.round(s * 10) / 10, roundedL = Math.round(l * 10) / 10;
return `${roundedH} ${roundedS}% ${roundedL}%`;
}
const LUMINOSITY_OFFSETS = {
50: 37.5,
100: 30,
200: 22.5,
300: 15,
400: 7.5,
500: 0,
600: -7.5,
700: -15,
800: -22.5,
900: -30,
950: -37.5
};
function calculateSaturationMultiplier(baseVariant, targetVariant, baseSaturation) {
if (targetVariant === baseVariant)
return 1;
const saturationFactor = Math.min(baseSaturation / 100, 1), variantDiff = Math.abs(targetVariant - baseVariant);
if (targetVariant < baseVariant) {
const reduction = variantDiff / 500 * 0.25 * saturationFactor;
return Math.max(0.3, 1 - reduction);
} else {
const increase = variantDiff / 400 * 0.15 * saturationFactor;
return Math.min(1.3, 1 + increase);
}
}
function generateColorScale(baseColor) {
const { h, s, l } = parseHSL(baseColor), baseVariant = 500, baseLuminosity = l, variants = Object.keys(LUMINOSITY_OFFSETS).map(Number), scale = {};
return variants.forEach((variant) => {
if (variant === baseVariant)
scale[variant] = formatHSL(h, s, l);
else {
const isUnderBase = variant < baseVariant, isOverBase = variant > baseVariant, luminosityOffset = LUMINOSITY_OFFSETS[variant];
let targetLuminosity;
isUnderBase && l >= 100 ? targetLuminosity = baseLuminosity : targetLuminosity = baseLuminosity + luminosityOffset, isOverBase && l <= 0 && (targetLuminosity = 0), targetLuminosity = Math.min(100, Math.max(0, targetLuminosity));
const saturationMultiplier = calculateSaturationMultiplier(baseVariant, variant, s), adjustedSaturation = Math.min(100, Math.max(5, s * saturationMultiplier));
scale[variant] = formatHSL(h, adjustedSaturation, targetLuminosity);
}
}), scale;
}
const DEFAULT_CRITICAL_COLORS = [
"background",
"foreground",
"primary",
"primary-foreground",
"secondary",
"secondary-foreground",
"accent",
"accent-foreground",
"destructive",
"destructive-foreground",
"success",
"success-foreground",
"warning",
"warning-foreground",
"info",
"info-foreground",
"contrast",
"contrast-foreground",
"muted",
"shadow",
"border"
], DEFAULT_CRITICAL_FOUNDATION = [
"radius",
"font-family",
"base-font-size",
"border-width"
], scaleColors = ["primary", "secondary", "accent", "destructive", "success", "warning", "info", "contrast", "background", "foreground", "border", "muted", "overlay", "shadow"];
function generateCSS(preset, options = {
onlyCritical: !1,
mode: "both",
darkSelectorStrategy: "class",
darkClass: "dark"
}) {
const {
onlyCritical = !1,
criticalColors = DEFAULT_CRITICAL_COLORS,
criticalFoundation = DEFAULT_CRITICAL_FOUNDATION,
mode,
darkSelectorStrategy,
prefix = "maz",
includeColorScales = !0,
darkClass = "dark"
} = options;
let css = `@layer maz-ui-theme {
`;
return (mode === "light" || mode === "both") && (css += generateLightThemeVariables(preset, {
onlyCritical,
criticalColors,
criticalFoundation,
prefix,
includeColorScales
})), (mode === "dark" || mode === "both") && (css += generateDarkThemeVariables(preset, {
onlyCritical,
criticalColors,
criticalFoundation,
mode,
darkSelectorStrategy,
prefix,
includeColorScales,
darkClass
})), css += `}
`, css;
}
function generateLightThemeVariables(preset, options) {
const { onlyCritical, criticalColors, criticalFoundation, prefix, includeColorScales } = options, lightColors = onlyCritical ? extractCriticalVariables(preset.colors.light, criticalColors) : preset.colors.light, lightFoundation = onlyCritical ? extractCriticalFoundation(preset.foundation, criticalFoundation) : preset.foundation;
return generateVariablesBlock({
selector: ":root",
colors: lightColors,
foundation: lightFoundation,
prefix,
includeScales: !onlyCritical && includeColorScales,
preset: onlyCritical ? void 0 : preset
});
}
function generateDarkThemeVariables(preset, options) {
const { onlyCritical, criticalColors, criticalFoundation, mode, darkSelectorStrategy, prefix, includeColorScales, darkClass } = options, darkColors = onlyCritical ? extractCriticalVariables(preset.colors.dark, criticalColors) : preset.colors.dark, darkFoundation = getDarkFoundation(onlyCritical, mode, preset.foundation, criticalFoundation);
return generateVariablesBlock({
selector: darkSelectorStrategy === "media" ? ":root" : `.${darkClass}`,
mediaQuery: darkSelectorStrategy === "media" ? "@media (prefers-color-scheme: dark)" : void 0,
colors: darkColors,
foundation: darkFoundation,
prefix,
includeScales: !onlyCritical && includeColorScales,
preset: onlyCritical ? void 0 : preset,
isDark: !0
});
}
function getDarkFoundation(onlyCritical, mode, foundation, criticalFoundation) {
return onlyCritical ? extractCriticalFoundation(foundation, criticalFoundation) : mode === "dark" ? foundation : void 0;
}
function extractCriticalVariables(colors, criticalKeys) {
return Object.fromEntries(
criticalKeys.filter((key) => colors[key]).map((key) => [key, colors[key]])
);
}
function extractCriticalFoundation(foundation, criticalKeys) {
return foundation ? Object.fromEntries(
criticalKeys.filter((key) => foundation[key]).map((key) => [key, foundation[key]])
) : {};
}
function generateVariablesBlock({
selector,
mediaQuery,
colors,
foundation,
prefix,
includeScales = !1,
preset,
isDark = !1
}) {
const variables = [];
if (colors && Object.entries(colors).forEach(([key, value]) => {
value && variables.push(` --${prefix}-${key}: ${value};`);
}), foundation && Object.entries(foundation).forEach(([key, value]) => {
value && variables.push(` --${prefix}-${key}: ${value};`);
}), includeScales && preset) {
const sourceColors = isDark ? preset.colors.dark : preset.colors.light, colorScales = generateAllColorScales(sourceColors, prefix);
variables.push(...colorScales);
}
const content = variables.join(`
`);
return mediaQuery ? `
${mediaQuery} {
${selector} {
${content.replace(/^/gm, " ")}
}
}
` : `
${selector} {
${content}
}
`;
}
function generateAllColorScales(colors, prefix) {
const colorScales = [];
return scaleColors.forEach((colorName) => {
const baseColor = colors[colorName];
if (baseColor) {
const scale = generateColorScale(baseColor);
Object.entries(scale).forEach(([scaleKey, scaleValue]) => {
colorScales.push(` --${prefix}-${colorName}-${scaleKey}: ${scaleValue};`);
});
}
}), colorScales;
}
const CSS_ID = "maz-theme-css";
function injectCSS(id = CSS_ID, css) {
if (isServer())
return;
const styleElements = document.querySelectorAll(`#${id}`);
if (!styleElements || styleElements.length === 0) {
const element = document.createElement("style");
element.id = id, document.head.appendChild(element), element.textContent = css;
return;
}
if (styleElements.length === 1) {
styleElements[0].textContent = css;
return;
}
if (styleElements.length > 1) {
for (let i = 0; i < styleElements.length - 1; i++)
styleElements[i].remove();
styleElements[styleElements.length - 1].textContent = css;
}
}
function getCookie(key) {
if (isServer())
return null;
const cookie = document.cookie.split(";").find((c) => c.trim().startsWith(`${key}=`));
return cookie ? decodeURIComponent(cookie.split("=")[1]) : null;
}
function getSavedColorMode() {
const savedMode = getCookie("maz-color-mode");
if (savedMode && ["light", "dark", "auto"].includes(savedMode))
return savedMode;
}
function getColorMode(colorMode) {
return colorMode && ["light", "dark"].includes(colorMode) ? colorMode : getSavedColorMode() || "auto";
}
function getSystemColorMode() {
return isServer() || typeof globalThis.matchMedia != "function" ? "light" : globalThis.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function noTransition(fn) {
if (isServer()) {
fn();
return;
}
const style = document.createElement("style");
style.textContent = `.no-transitions *,
.no-transitions *::before,
.no-transitions *::after {
transition-property: transform, opacity, scale, rotate, translate !important;
}`, document.head.appendChild(style), document.documentElement.classList.add("no-transitions"), fn(), setTimeout(() => {
document.documentElement.classList.remove("no-transitions"), style.remove();
}, 500);
}
function updateDocumentClass(isDark, state) {
typeof document > "u" || !state || state.darkModeStrategy === "media" || state.mode === "light" || noTransition(() => {
isDark ? document.documentElement.classList.add(state.darkClass) : document.documentElement.classList.remove(state.darkClass);
});
}
function isPresetObject(preset) {
return typeof preset == "object" && preset !== null && !!preset.name;
}
async function getPreset(preset) {
if (isPresetObject(preset))
return preset;
if (preset === "mazUi" || !preset || preset === "maz-ui") {
const { mazUi } = await import("../chunks/mazUi.raWwR54O.js");
return mazUi;
}
if (preset === "ocean") {
const { ocean } = await import("../chunks/ocean.pHrY5H_S.js");
return ocean;
}
if (preset === "pristine") {
const { pristine } = await import("../chunks/pristine.CxBZzgUG.js");
return pristine;
}
if (preset === "obsidian") {
const { obsidian } = await import("../chunks/obsidian.Cy0R8RHy.js");
return obsidian;
}
throw new TypeError(`[@maz-ui/themes] Preset ${preset} not found`);
}
function mergePresets(base, overrides) {
return {
name: overrides.name || base.name,
foundation: {
...base.foundation,
...overrides.foundation
},
colors: {
light: mergeColors(base.colors.light, overrides.colors?.light),
dark: mergeColors(base.colors.dark, overrides.colors?.dark)
}
};
}
function mergeColors(base, overrides) {
return overrides ? {
...base,
...overrides
} : base;
}
function isClient() {
return typeof document < "u";
}
function truthyFilter(value) {
return !!value;
}
function useMutationObserver(target, callback, options = {}) {
const {
internalWindow = isClient() ? globalThis : void 0,
...mutationOptions
} = options;
let observer;
const isSupported = ref((internalWindow && "MutationObserver" in internalWindow) ?? !1);
isSupported.value || onMounted(() => {
isSupported.value = (internalWindow && "MutationObserver" in internalWindow) ?? !1;
});
const cleanup = () => {
observer && (observer.disconnect(), observer = void 0);
}, targets = computed(() => {
const value = toValue(target);
let element;
return value && "$el" in value ? element = value.$el : value && (element = value), new Set([element].filter(truthyFilter));
}), stopWatch = watch(
[targets, isSupported],
([newTargets, isSupported2]) => {
cleanup(), isSupported2 && newTargets.size && (observer = new MutationObserver(callback), newTargets.forEach((el) => observer?.observe(el, mutationOptions)));
},
{ immediate: !0, flush: "post" }
);
return {
isSupported,
stop: () => {
stopWatch(), cleanup();
},
takeRecords: () => observer?.takeRecords()
};
}
function injectThemeCSS(finalPreset, config) {
if (typeof document > "u")
return;
const cssOptions = {
mode: config.mode,
darkSelectorStrategy: config.darkModeStrategy,
darkClass: config.darkClass
};
if (config.injectCriticalCSS) {
const criticalCSS = generateCSS(finalPreset, {
...cssOptions,
onlyCritical: !0
});
injectCSS(CSS_ID, criticalCSS);
}
if (!config.injectFullCSS)
return;
const fullCSS = generateCSS(finalPreset, cssOptions);
config.strategy === "runtime" ? injectCSS(CSS_ID, fullCSS) : config.strategy === "hybrid" && (typeof requestIdleCallback < "u" ? requestIdleCallback(() => {
injectCSS(CSS_ID, fullCSS);
}, { timeout: 100 }) : nextTick(() => {
injectCSS(CSS_ID, fullCSS);
}));
}
function injectThemeState(app, themeState) {
app.provide("mazThemeState", themeState), app.config.globalProperties.$mazThemeState = themeState;
}
function watchColorSchemeFromMedia(themeState) {
if (!isServer()) {
if (themeState.value && themeState.value.colorMode === "auto") {
const mediaQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"), updateFromMedia = () => {
if (themeState.value.colorMode === "auto") {
const newColorMode = mediaQuery.matches ? "dark" : "light";
updateDocumentClass(newColorMode === "dark", themeState.value), themeState.value.isDark = newColorMode === "dark";
}
};
mediaQuery.addEventListener("change", updateFromMedia);
}
watch(() => themeState.value.colorMode, (colorMode) => {
updateDocumentClass(
colorMode === "auto" ? getSystemColorMode() === "dark" : colorMode === "dark",
themeState.value
);
});
}
}
function watchMutationClassOnHtmlElement(themeState) {
isServer() || useMutationObserver(
document.documentElement,
() => {
if (isServer() || !themeState.value)
return;
const activeColorMode = document.documentElement.classList.contains(themeState.value.darkClass) ? "dark" : "light";
themeState.value.isDark = activeColorMode === "dark", themeState.value.colorMode !== activeColorMode && themeState.value.colorMode !== "auto" && (themeState.value.colorMode = activeColorMode);
},
{
attributes: !0
}
);
}
const MazUiTheme = {
async install(app, options) {
const config = {
strategy: "hybrid",
overrides: {},
darkModeStrategy: "class",
preset: void 0,
injectCriticalCSS: !0,
injectFullCSS: !0,
mode: "both",
darkClass: "dark",
...options,
colorMode: getSavedColorMode() ?? options.colorMode ?? (options.mode === "dark" ? "dark" : "auto")
}, isDark = config.colorMode === "auto" && config.mode === "both" ? getSystemColorMode() === "dark" || getColorMode(config.colorMode) === "dark" : getColorMode(config.colorMode) === "dark" || config.mode === "dark", themeState = ref({
strategy: config.strategy,
darkClass: config.darkClass,
darkModeStrategy: config.darkModeStrategy,
colorMode: config.colorMode,
mode: config.mode,
preset: void 0,
// @ts-expect-error _isDark is a private property
isDark: options._isDark || isDark
});
injectThemeState(app, themeState), updateDocumentClass(themeState.value.isDark, themeState.value);
const preset = config.strategy === "buildtime" ? config.preset : await getPreset(config.preset), finalPreset = Object.keys(config.overrides).length > 0 && preset ? mergePresets(preset, config.overrides) : preset;
finalPreset && (themeState.value.preset = finalPreset), !(config.strategy === "buildtime" || !finalPreset) && (injectThemeCSS(finalPreset, config), watchColorSchemeFromMedia(themeState), watchMutationClassOnHtmlElement(themeState));
}
}, MazUi = {
install(app, options) {
const { theme, translations } = options;
app.use(MazUiTheme, theme), app.use(MazUiTranslations, translations);
}
};
export {
MazUi
};