Created
November 12, 2021 19:33
-
-
Save chanced/479f45c053e04c6d4cbb382743f5cf54 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type BreakpointName = "sm" | "md" | "lg" | "xl" | "2xl"; | |
export const breakpointNames: BreakpointName[] = ["sm", "md", "lg", "xl", "2xl"]; | |
const pxRegex = /^(\d+)\s*(?:px)?$/i; | |
const remRegex = /^(\d+)\s*(?:rem)/i; | |
export interface Breakpoint { | |
name: BreakpointName; | |
minPixels: number; | |
query(): string; | |
minREM: number; | |
minPxString(): string; | |
minREMString(): string; | |
} | |
function remBaseline(): number { | |
return typeof document !== "undefined" | |
? parseFloat(getComputedStyle(document.documentElement).fontSize) | |
: 16; | |
} | |
function pixelsFromREM(rem: number): number { | |
return rem / remBaseline(); | |
} | |
function remFromPixels(px: number): number { | |
return px * remBaseline(); | |
} | |
export interface Breakpoints { | |
sizes: Breakpoint[]; | |
named(name: BreakpointName): Breakpoint | undefined; | |
matches(size: number | string): Breakpoint[]; | |
} | |
function size(name: BreakpointName, minPixels: number): Breakpoint { | |
const value = { | |
name, | |
minPixels, | |
query(): string { | |
return `(min-width: ${minPixels}px)`; | |
}, | |
get minREM(): number { | |
return remFromPixels(this.minPixels); | |
}, | |
minPxString(): string { | |
return this.minPixels + "px"; | |
}, | |
minREMString(): string { | |
return this.minREM + "rem"; | |
}, | |
}; | |
return value; | |
} | |
export const breakpoints: Breakpoints = { | |
sizes: [ | |
size("2xl", 1536), | |
size("xl", 1280), | |
size("lg", 1024), | |
size("md", 768), | |
size("sm", 640), | |
], | |
named(sizeName: BreakpointName): Breakpoint | undefined { | |
return this.sizes.find(({ name }) => name === sizeName); | |
}, | |
matches(size: string | number): Breakpoint[] { | |
if (typeof size === "string") { | |
const matchesPx = size.match(pxRegex); | |
if (matchesPx?.length === 2) { | |
size = parseInt(matchesPx[1]); | |
} else { | |
const matchesREM = size.match(remRegex); | |
if (matchesREM?.length === 2) { | |
size = pixelsFromREM(parseInt(matchesREM[1])); | |
} | |
} | |
} | |
if (typeof size === "string") { | |
throw new Error(`"${size}" is not a properly formatted size`); | |
} | |
return this.sizes.filter(({ minPixels }) => minPixels <= size); | |
}, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { breakpoints, breakpointNames } from "$lib/breakpoints"; | |
import type { BreakpointName, Breakpoint } from "$lib/breakpoints"; | |
import { browser } from "$app/env"; | |
import { writable } from "svelte/store"; | |
import type { Readable } from "svelte/store"; | |
import { getContext, setContext } from "svelte"; | |
const key = {}; | |
export type ScreenSizeState = { [K in BreakpointName]: boolean }; | |
function initial(queries: Map<Breakpoint, MediaQueryList>): ScreenSizeState { | |
if (!browser) { | |
return breakpointNames.reduce( | |
(state, name) => ({ ...state, [name]: false }), | |
{} as Partial<ScreenSizeState>, | |
) as ScreenSizeState; | |
} | |
return breakpoints.sizes.reduce((state, bp) => { | |
const query = queries.get(bp); | |
if (!query) { | |
throw new Error(`breakpoint ${bp.name} not found`); | |
} | |
state[bp.name] = query.matches; | |
return state; | |
}, {} as ScreenSizeState); | |
} | |
type EventHandler = (this: MediaQueryList, ev: MediaQueryListEvent) => void; | |
function createMediaQueryMap(): Map<Breakpoint, MediaQueryList> { | |
if (!browser) { | |
return new Map(); | |
} | |
return breakpoints.sizes.reduce( | |
(acc, size) => acc.set(size, window.matchMedia(size.query())), | |
new Map(), | |
); | |
} | |
// eslint-disable-next-line @typescript-eslint/no-empty-interface | |
export interface ScreenSizeStore extends Readable<ScreenSizeState> {} | |
export function createScreenSizeStore(): ScreenSizeStore { | |
const mediaQueryMap = createMediaQueryMap(); | |
const { update, subscribe } = writable(initial(mediaQueryMap), () => { | |
if (!browser) { | |
return; | |
} | |
const handlers = new Map<Breakpoint, EventHandler>(); | |
for (const [bp] of mediaQueryMap) { | |
handlers.set(bp, function (ev) { | |
update(($state) => ({ ...$state, [bp.name]: ev.matches })); | |
}); | |
} | |
for (const [bp, query] of mediaQueryMap) { | |
const handler = handlers.get(bp); | |
if (!handler) { | |
throw new Error(`handler not found for breakpoint: ${bp.name}`); | |
} | |
query.addEventListener("change", handler); | |
} | |
return () => { | |
for (const [bp, query] of mediaQueryMap) { | |
const handler = handlers.get(bp); | |
if (!handler) { | |
continue; | |
} | |
query.removeEventListener("change", handler); | |
} | |
}; | |
}); | |
const store = { subscribe }; | |
setContext(key, store); | |
return store; | |
} | |
// should this be ScreenStore | undefined? Hopefully layout isnt getting overridden | |
export function screenStore(): ScreenSizeStore { | |
let store = getContext<ScreenSizeStore>(key); | |
if (store) { | |
return store; | |
} | |
store = createScreenSizeStore(); | |
setContext(key, store); | |
return store; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment