npm package: @phntms/layout
Description: A lightweight library to implement fully responsive REM based layouts.
Author: Paulo Ferreira Jorge
Latest version: 0.0.1

# layout

[![NPM version][npm-image]][npm-url]
[![Actions Status][ci-image]][ci-url]
[![PR Welcome][npm-downloads-image]][npm-downloads-url]

A lightweight library to implement fully responsive REM based layouts in CSS-in-JS based libraries. Also SSR safe and won't break build if pre-rendered.

## Installation

Install this package with `npm`.

```bash
npm i @phntms/layout
```

## Usage

Initialize a new `Layout()` instance. On creation, you can also define custom configs or leave blank to inherit the default config.

To use, we recommend creating a global singleton file, this allows you to reference the layout anywhere in the project. Example singleton:

```ts
import Layout from "@phntms/layout";

export default new Layout();

// Or...

export default new Layout<"mobile" | "tablet" | "desktop">({
  mobile: {
    breakpoint: 768,
    targetViewportWidth: 375,

    // Number of columns to show below 767
    columns: 16,
  },

  tablet: {
    breakpoint: 1032,
    targetViewportWidth: 1032,

    // Number of columns to show between 768 and 1031
    columns: 20,
  },

  desktop: {
    breakpoint: 1440,
    targetViewportWidth: 1440,

    // Number of columns to show above 1032
    columns: 24,
  },
});
```

**Note**: If you want to have full control over your config and prefer not to use any defaults provided, you can set `applyDefaultConfig` to `false`.

```ts
import Layout from '@phntms/layout';

export default new Layout({...}, false);
```

Then to use, simply import the newly created `layout` singleton file. For example:

```ts
import layout from "~/layout";
```

## initialize()

To apply the config set in the constructor and once the app has loaded, this method is required to initialize the layout. Example usage:

```ts
import layout from "~/layout";

layout.initialize();
```

## destroy()

Optional method used to unsubscribe from event listeners at end of lifecycle. Example usage:

```ts
import layout from "~/layout";

layout.destroy();
```

## pxToRem()

Used to convert PX values to REM. Example usage:

```ts
import layout from "~/layout";

layout.pxToRem(10, false, "desktop");
```

In this example, we're converting `10px` to `rem` at the `targetScreenSize` of `desktop`.

### But, what does that mean?

Each breakpoint has a `targetScreenSize`. This is the size that we're basing our fluid `rem` values on. So in the example above, the default target is `1440`, which means at a screen size of `1440px`, the value will visually be `10px` (`0.167rem` in `rem`).

Other examples based on the default config:

- Mobile: `pxToRem(5, false, 'mobile')` = Visually `5px` at **375px**.
- Tablet: `pxToRem(5, false, 'tablet')` = Visually `5px` at **1024px**.
- Desktop: `pxToRem(5, false, 'desktop')` = Visually `5px` at **1440px**.

### What about if the screen size is higher?

The value will simply scale visually beyond `10px`, though the actual `rem` value won't change.

### What about if the screen size is lower?

Theres three different possible outcomes here.

In the example above as `pxAsMin` is set to `false`, the value would visually scale below `10px` until we reached a screen size of `0px` or until we reach a new breakpoint.

If `pxAsMin` was set to `true`, that value would stop visually scaling once the screen reaches `1440px` in width.

Alternatively, if `pxAsMin` is set to an integer value, such as `8px`, the value would visually scale till it reaches `8px` in size or a new breakpoint is reached.

In all outcomes the value will be visually unaffected if scaling above the base value.

**Note**: CSS `max` is used under the hood to clamp values, which means
`-${pxToRem(10, true, ...)}` wouldn't work if this is enabled as the result - `-max(10px, 0.167rem)` would be invalid. To use negative values, define them inside as the `px` value. Or set this to `false`, at which point `-${pxToRem(10, false, ...)}` is valid as it would result in `-0.167rem`.

## columns()

Used to create grid columns, irrespective of the current breakpoint.

```ts
import layout from "~/layout";

layout.columns(1);
```

This library works by dividing the screen into a number of columns, for example `24` by default on `desktop`. The nice part of this is that 1 gutter (on `mobile`, `tablet`, `desktop` or any breakpoint) = 1 `rem`.

**Note**: We recommend only using `columns` for horizontal spacings, where vertical spacings _should_ use `pxToRem` based values.

## toggleColumnOverlay()

Optional method used to toggle if column overlay is showing or not.

```ts
import layout from "~/layout";

layout.toggleColumnOverlay();
```

**Note**: To use grid overlay, you need to additionally import `overlay.css`. For example:

```ts
import "@phntms/layout/src/overlay.css";
```

## 🍪 Example Usage

The following examples are all in [styled-components](https://styled-components.com/), though they would work in any other CSS-in-JS based libraries.

### Media Queries

Due to how different browsers calculate media queries, when using `rem` based libraries such as this one, its recommended to use `em` values over `px` and `rem`. For further reading, see [this](https://zellwk.com/blog/media-query-units/) article exploring the topic. With this in mind, following is an example of how you might want to layout global queries:

```ts
const pxToEm = (px: number) => px / 16;

export const BREAKPOINT_MOBILE = DEFAULT_CONFIG.mobile.breakpoint;
const BREAKPOINT_MOBILE_AS_EM = `${pxToEm(BREAKPOINT_MOBILE)}em`;
/** 768px. */
export const QUERY_GREATER_THAN_MOBILE = `(min-width: ${BREAKPOINT_MOBILE_AS_EM})`;

export const BREAKPOINT_TABLET = DEFAULT_CONFIG.tablet.breakpoint;
const BREAKPOINT_TABLET_AS_EM = `${pxToEm(BREAKPOINT_TABLET)}em`;
/** 1032px. */
export const QUERY_GREATER_THAN_TABLET = `(min-width: ${BREAKPOINT_TABLET_AS_EM})`;

export const BREAKPOINT_DESKTOP = DEFAULT_CONFIG.desktop.breakpoint;
const BREAKPOINT_DESKTOP_AS_EM = `${pxToEm(BREAKPOINT_DESKTOP)}em`;
/** 1440px. */
export const QUERY_GREATER_THAN_DESKTOP = `(min-width: ${BREAKPOINT_DESKTOP_AS_EM})`;
```

### Scaling Typography

Combining the media queries above with type or any other value (such as paddings / margins etc), means you can then create values that scale fully based on the current breakpoint, for example:

```ts
import { css } from "styled-components";

import layout from "~/layout";

export const TYPE_SIZE_LARGE = css`
  font-size: ${layout.pxToRem(17, 14, "mobile")};

  @media ${QUERY_GREATER_THAN_MOBILE} {
    font-size: ${layout.pxToRem(17, 14, "tablet")};
  }

  @media ${QUERY_GREATER_THAN_TABLET} {
    font-size: ${layout.pxToRem(17, 14, "desktop")};
  }
`;
```

In this example, type will always maintain its base `17px` font size at the `targetViewportWidth` of each breakpoint. Though if type has a small base value, we recommend always adding a minimum font size for type legibility to ensure it never becomes too small. Here we set a minimum of `14px`.

### Grid

Here is an example of using the columns to create a fully fluid grid!

```ts
import styled from 'styled-components';

import layout from '~/layout';

const Cards = styled.div`
  display: grid;
  grid-column-gap: ${layout.columns(1)};
  grid-template-columns: repeat(3, ${layout.columns(6)});

  // Can even be used alongside `pxToRem`!
  margin-bottom: ${layout.pxToRem(60, true, 'desktop')}
`;
```

## 🍰 Contributing

Want to get involved, or found an issue? Please contribute using the GitHub Flow. Create a branch, add commits, and open a Pull Request or submit a new issue.

Please read `CONTRIBUTING` for details on our `CODE_OF_CONDUCT`, and the process for submitting pull requests to us!

[npm-image]: https://img.shields.io/npm/v/@phntms/layout.svg?style=flat-square&logo=react
[npm-url]: https://npmjs.org/package/@phntms/layout
[npm-downloads-image]: https://img.shields.io/npm/dm/@phntms/layout.svg
[npm-downloads-url]: https://npmcharts.com/compare/@phntms/layout?minimal=true
[ci-image]: https://github.com/phantomstudios/layout/workflows/test/badge.svg
[ci-url]: https://github.com/phantomstudios/layout/actions