UNPKG

maz-ui

Version:

A standalone components library for Vue.Js 3 & Nuxt.Js 3

437 lines (436 loc) 15.9 kB
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 };