context-media.ts
ts
import {
type Component,
type Context,
type State,
component,
provideContexts,
state,
} from '../../../'
export type ContextMediaProps = {
'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(
'context-media',
{
// 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
},
// 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
},
// Context for screen viewport size
[MEDIA_VIEWPORT]: (el: HTMLElement) => {
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
},
// 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
},
},
() => [
provideContexts([
MEDIA_MOTION,
MEDIA_THEME,
MEDIA_VIEWPORT,
MEDIA_ORIENTATION,
]),
],
)
declare global {
interface HTMLElementTagNameMap {
'context-media': Component<ContextMediaProps>
}
}