• TS

    media-context.ts ts

    import { UIElement, type Context } from '../../../'
    
    /* === Provided Context Keys === */
    
    const MEDIA_MOTION = 'media-motion'
    const MEDIA_THEME = 'media-theme'
    const MEDIA_VIEWPORT = 'media-viewport'
    const MEDIA_ORIENTATION = 'media-orientation'
    
    /* === Pure Functions === */
    
    const parseBreakpoint = (value: string | null, fallback: string) => {
    	const attr = value?.trim()
    	if (!attr) return fallback
    	const unit = attr.match(/em$/) ? 'em' : 'px'
    	const v = parseFloat(attr)
    	return Number.isFinite(v) ? v + unit : fallback
    }
    
    /* === Class Definition === */
    
    export class MediaContext extends UIElement<{
    	'media-motion': boolean,
        'media-theme': string,
        'media-viewport': string,
        'media-orientation': string,
    }> {
    	static readonly localName ='media-context'
    	static providedContexts = [
    		MEDIA_MOTION as Context<string, boolean>,
    		MEDIA_THEME as Context<string, string>,
    		MEDIA_VIEWPORT as Context<string, string>,
    		MEDIA_ORIENTATION as Context<string, string>,
    	]
    
    	connectedCallback() {
    		super.connectedCallback()
    
    		const THEME_LIGHT = 'light'
    		const THEME_DARK = 'dark'
    		const VIEWPORT_XS = 'xs'
    		const VIEWPORT_SM = 'sm'
    		const VIEWPORT_MD = 'md'
    		const VIEWPORT_LG = 'lg'
    		const VIEWPORT_XL = 'xl'
    		const ORIENTATION_LANDSCAPE = 'landscape'
    		const ORIENTATION_PORTRAIT = 'portrait'
    
    		const getBreakpoints = () => ({
    			sm: parseBreakpoint(this.getAttribute(VIEWPORT_SM), '32em'),
    			md: parseBreakpoint(this.getAttribute(VIEWPORT_MD), '48em'),
    			lg: parseBreakpoint(this.getAttribute(VIEWPORT_LG), '72em'),
    			xl: parseBreakpoint(this.getAttribute(VIEWPORT_XL), '108em'),
    		})
    		const breakpoints = getBreakpoints()
    
    		const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)')
    		const colorScheme = matchMedia('(prefers-color-scheme: dark)')
    		const screenSmall = matchMedia(`(min-width: ${breakpoints.sm})`)
    		const screenMedium = matchMedia(`(min-width: ${breakpoints.md})`)
    		const screenLarge = matchMedia(`(min-width: ${breakpoints.lg})`)
    		const screenXLarge = matchMedia(`(min-width: ${breakpoints.xl})`)
    		const screenOrientation = matchMedia('(orientation: landscape)')
    
    		const getViewport = () => {
    			if (screenXLarge.matches) return VIEWPORT_XL
                if (screenLarge.matches) return VIEWPORT_LG
                if (screenMedium.matches) return VIEWPORT_MD
                if (screenSmall.matches) return VIEWPORT_SM
                return VIEWPORT_XS
    		}
    
    		// set initial values
    		this.set(MEDIA_MOTION, reducedMotion.matches)
    		this.set(MEDIA_THEME, colorScheme.matches ? THEME_DARK : THEME_LIGHT)
    		this.set(MEDIA_VIEWPORT, getViewport())
    		this.set(MEDIA_ORIENTATION, screenOrientation.matches ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT)
    
    		// event listeners
    		reducedMotion.addEventListener('change', e => this.set(MEDIA_MOTION, e.matches))
    		colorScheme.addEventListener('change', e => this.set(MEDIA_THEME, e.matches ? THEME_DARK : THEME_LIGHT))
    		screenSmall.addEventListener('change', () => this.set(MEDIA_VIEWPORT, getViewport()))
    		screenMedium.addEventListener('change', () => this.set(MEDIA_VIEWPORT, getViewport()))
    		screenLarge.addEventListener('change', () => this.set(MEDIA_VIEWPORT, getViewport()))
    		screenXLarge.addEventListener('change', () => this.set(MEDIA_VIEWPORT, getViewport()))
    		screenOrientation.addEventListener('change', e => this.set(MEDIA_THEME, e.matches ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT))
    	}
    }
    MediaContext.define()