Skip to content

Instantly share code, notes, and snippets.

@chanced
Created November 12, 2021 19:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chanced/479f45c053e04c6d4cbb382743f5cf54 to your computer and use it in GitHub Desktop.
Save chanced/479f45c053e04c6d4cbb382743f5cf54 to your computer and use it in GitHub Desktop.
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);
},
};
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