Last active
March 6, 2025 03:57
-
-
Save samanpwbb/67f546b598c89b1e6dddba7de21ac396 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
// 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