Skip to content

Instantly share code, notes, and snippets.

@samanpwbb
Last active March 6, 2025 03:57
Show Gist options
  • Save samanpwbb/67f546b598c89b1e6dddba7de21ac396 to your computer and use it in GitHub Desktop.
Save samanpwbb/67f546b598c89b1e6dddba7de21ac396 to your computer and use it in GitHub Desktop.
// ThemeContext.tsx
/**
* Calculates layout metrics based on screen dimensions and aspect ratio.
* Establishes a grid system with columns and gutters for responsive design.
*/
const wideBreakpoint = 1200;
const narrowBreakpoint = 800;
const tallBreakpoint = 1000;
export function getMetrics(
totalWidth: number,
totalHeight: number,
ratio: number,
) {
const baseYSafeZone =
Math.floor(totalHeight * 0.02) +
Math.max(0, (totalHeight - tallBreakpoint) / 2);
const baseXSafeZone =
Math.floor(totalWidth * 0.01) +
Math.max(0, (totalWidth - wideBreakpoint) / 2);
const width = totalWidth - baseXSafeZone * 2;
const height = totalHeight - baseYSafeZone * 2;
const isMobile = width > mobileBreakpoint;
let cols = 12;
// eventually support smaller screens as well
if (width < narrowBreakpoint) cols = 9;
const tilesPerCol = 4;
const gutters = cols - 1;
const tilesPerGutter = 1;
// Total horizontal tiles: (cols * tiles per col) + (gutters * tiles per gutter)
const xTiles = cols * tilesPerCol + gutters * tilesPerGutter;
// Calculate individual tile width by dividing screen width by total horizontal tiles
const tileWidth = Math.floor(width / xTiles);
// Calculate how many tiles fit vertically based on the tile width
const yTiles = Math.floor(height / tileWidth);
// Calculate safe zones (unused space) to center the grid
const ySafeZone =
baseYSafeZone + Math.floor((height - yTiles * tileWidth) / 2);
const xSafeZone =
baseXSafeZone + Math.floor((width - xTiles * tileWidth) / 2);
// TODO:
// - conditional layout for mobile
return {
isMobile, // for now, mobile unsupported.
isNarrowScreen: width < narrowBreakpoint,
screenWidth: width,
screenHeight: height,
aspectRatio: ratio,
colWidth: tilesPerCol * tileWidth, // Width of each column in pixels
tileWidth,
gutterWidth: tilesPerGutter * tileWidth, // Width of each gutter in pixels
cols,
textM: tileWidth * 0.9,
textSm: tileWidth * 0.75,
textLg: tileWidth * 1.5,
xTiles,
yTiles,
xActiveZone: xTiles * tileWidth,
yActiveZone: yTiles * tileWidth,
xSafeZone,
ySafeZone,
};
}
/**
* Type definition for the theme object based on the metrics calculation.
* Used throughout the application for consistent layout and spacing.
*/
export type Theme = ReturnType<typeof getMetrics>;
/**
* React context for theme management.
* Provides the current theme object and a setter function to update it.
*/
export const ThemeContext = createContext<{
theme: Theme;
setTheme: (theme: Theme) => void;
}>({
theme: getMetrics(0, 0, 0),
setTheme: () => {},
});
// Theme provider
import { useState } from "react";
import { useUpdateTheme } from "../hooks/useUpdateTheme";
import { getMetrics, ThemeContext } from "./ThemeContext";
import { useToggleDebugMenu } from "../hooks/useToggleDebugMenu";
import { useTheme } from "./useTheme";
export function ThemeProvider(props: { children: React.ReactNode }) {
const [theme, setTheme] = useState(() => {
const { clientWidth, clientHeight } = document.body;
return getMetrics(clientWidth, clientHeight, clientWidth / clientHeight);
});
useUpdateTheme({ onChange: (w, h, r) => setTheme(getMetrics(w, h, r)) });
return (
<ThemeContext value={{ theme, setTheme }}>
{props.children}
</ThemeContext>
);
}
// useUpdateTheme
import { useCallback } from "react";
import { useResizeObserver } from "./useResizeObserver";
export type LayoutModes = "portrait" | "landscape";
export function useUpdateTheme({
onChange,
}: {
onChange: (width: number, height: number, ratio: number) => void;
}) {
const updateLayoutParams = useCallback(
(containerWidth: number, containerHeight: number) => {
const width = containerWidth;
const height = containerHeight;
const ratio = width / height;
onChange(width, height, ratio);
},
[onChange],
);
const onResize = useCallback(
(s: ResizeObserverEntry[]) => {
updateLayoutParams(s[0].contentRect.width, s[0].contentRect.height);
},
[updateLayoutParams],
);
useResizeObserver(document.getElementById("root"), onResize);
}
// useTheme.tsx
import { use, useCallback } from "react";
import { ThemeContext } from "./ThemeContext";
export function useTheme() {
const { theme } = use(ThemeContext);
const tiles = useCallback(
(count: number) => {
return count * theme.tileWidth;
},
[theme.tileWidth],
);
const yTileOffset = useCallback(
(count: number) => {
return count * theme.tileWidth + theme.ySafeZone;
},
[theme.tileWidth, theme.ySafeZone],
);
const yTileOffsetFromCenter = useCallback(
(count: number, heightInTiles: number) => {
const centerOffset = yTileOffset(Math.floor(theme.yTiles / 2));
const selfOffset = (heightInTiles * theme.tileWidth) / 2;
const finalOffset = centerOffset - selfOffset;
const offset = count * theme.tileWidth;
return finalOffset - offset;
},
[theme.yTiles, theme.tileWidth, yTileOffset],
);
const xTileOffset = useCallback(
(count: number) => {
return count * theme.tileWidth + theme.xSafeZone;
},
[theme.tileWidth, theme.xSafeZone],
);
// get width based on column count
const cols = useCallback(
(count: number) => {
const colWidth = count * theme.colWidth;
const gutterWidth = (count - 1) * theme.gutterWidth;
return colWidth + gutterWidth;
},
[theme],
);
// get left margin based on column count
const colOffset = useCallback(
(count: number, options?: { isNested?: boolean }) => {
const colWidth = count * theme.colWidth;
const gutterWidth = count * theme.gutterWidth;
return (options?.isNested ? 0 : theme.xSafeZone) + gutterWidth + colWidth;
},
[theme],
);
// get left margin, anchored to center, centering based on the width
// in center argument (attempt to center).
const colOffsetFromCenter = useCallback(
(count: number, colCount: number) => {
const centerCol = theme.cols / 2;
const centerOffset = colOffset(centerCol);
const finalOffset = centerOffset + (cols(count) + theme.gutterWidth);
const offsetCount = colCount / 2;
const offsetAmountFromWidth =
offsetCount * theme.colWidth + offsetCount * theme.gutterWidth;
return finalOffset - offsetAmountFromWidth;
},
[theme.cols, theme.colWidth, theme.gutterWidth, colOffset, cols],
);
return {
yTileOffsetFromCenter,
xTileOffset,
theme,
cols,
colOffset,
tiles,
yTileOffset,
colOffsetFromCenter,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment