@zeix/ui-element
Version:
UIElement - minimal reactive framework based on Web Components
127 lines (117 loc) • 3.56 kB
text/typescript
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>;
}
}