Created
October 15, 2021 23:29
-
-
Save chanced/b47add1fe4c6d84804da99690e29bdd7 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