UNPKG

@zeix/ui-element

Version:

UIElement - minimal reactive framework based on Web Components

127 lines (117 loc) 3.56 kB
import { type Component, type Context, type SignalProducer, type State, component, provide, state, } from "../../../"; export type MediaContextProps = { "media-motion": boolean; "media-theme": "light" | "dark"; "media-viewport": "xs" | "sm" | "md" | "lg" | "xl"; "media-orientation": "portrait" | "landscape"; }; /* === Exported Contexts === */ export const MEDIA_MOTION = "media-motion" as Context< "media-motion", State<boolean> >; export const MEDIA_THEME = "media-theme" as Context< "media-theme", State<"light" | "dark"> >; export const MEDIA_VIEWPORT = "media-viewport" as Context< "media-viewport", State<"xs" | "sm" | "md" | "lg" | "xl"> >; export const MEDIA_ORIENTATION = "media-orientation" as Context< "media-orientation", State<"portrait" | "landscape"> >; /* === Component === */ export default component( "media-context", { // Context for reduced motion preference [MEDIA_MOTION]: (() => { const mql = matchMedia("(prefers-reduced-motion: reduce)"); const reducedMotion = state(mql.matches); mql.addEventListener("change", (e) => { reducedMotion.set(e.matches); }); return reducedMotion; }) as SignalProducer<HTMLElement, boolean>, // Context for preferred color scheme [MEDIA_THEME]: (() => { const mql = matchMedia("(prefers-color-scheme: dark)"); const colorScheme = state(mql.matches ? "dark" : "light"); mql.addEventListener("change", (e) => { colorScheme.set(e.matches ? "dark" : "light"); }); return colorScheme; }) as SignalProducer<HTMLElement, "light" | "dark">, // Context for screen viewport size [MEDIA_VIEWPORT]: ((el) => { const getBreakpoint = (attr: string, fallback: string) => { const value = el.getAttribute(attr); const trimmed = value?.trim(); if (!trimmed) return fallback; const unit = trimmed.match(/em$/) ? "em" : "px"; const v = parseFloat(trimmed); return Number.isFinite(v) ? v + unit : fallback; }; const mqlSM = matchMedia( `(min-width: ${getBreakpoint("sm", "32em")})`, ); const mqlMD = matchMedia( `(min-width: ${getBreakpoint("md", "48em")})`, ); const mqlLG = matchMedia( `(min-width: ${getBreakpoint("lg", "72em")})`, ); const mqlXL = matchMedia( `(min-width: ${getBreakpoint("xl", "104em")})`, ); const getViewport = () => { if (mqlXL.matches) return "xl"; if (mqlLG.matches) return "lg"; if (mqlMD.matches) return "md"; if (mqlSM.matches) return "sm"; return "xs"; }; const viewport = state(getViewport()); mqlSM.addEventListener("change", () => { viewport.set(getViewport()); }); mqlMD.addEventListener("change", () => { viewport.set(getViewport()); }); mqlLG.addEventListener("change", () => { viewport.set(getViewport()); }); mqlXL.addEventListener("change", () => { viewport.set(getViewport()); }); return viewport; }) as SignalProducer<HTMLElement, "xs" | "sm" | "md" | "lg" | "xl">, // Context for screen orientation [MEDIA_ORIENTATION]: (() => { const mql = matchMedia("(orientation: landscape)"); const orientation = state(mql.matches ? "landscape" : "portrait"); mql.addEventListener("change", (e) => { orientation.set(e.matches ? "landscape" : "portrait"); }); return orientation; }) as SignalProducer<HTMLElement, "landscape" | "portrait">, }, () => [ provide([MEDIA_MOTION, MEDIA_THEME, MEDIA_VIEWPORT, MEDIA_ORIENTATION]), ], ); declare global { interface HTMLElementTagNameMap { "media-context": Component<MediaContextProps>; } }