Skip to content

Commit

Permalink
Add styleguide
Browse files Browse the repository at this point in the history
  • Loading branch information
esdete2 committed Jul 4, 2024
1 parent 42a4027 commit a96924c
Show file tree
Hide file tree
Showing 13 changed files with 918 additions and 69 deletions.
181 changes: 123 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ yarn add @networkteam/zebra-utils
// npm install @networkteam/zebra-utils
```

## Table of contents

- [Zebra](#zebra)
- [ImgProxy](#imgproxy)
- [Styleguide](#styleguide)
- [Utils](#utils)

## Zebra

### Revalidation

The `revalidate` function is used inside an API route to revalidate the document cache of Next.js. It compares the bearer token with a `REVALIDATE_TOKEN` environment variable.

#### Environment variable
#### Environment variables

```
REVALIDATE_TOKEN=
Expand Down Expand Up @@ -73,7 +80,7 @@ import NotFound from 'app/[[...slug]]/not-found';
export default localizedPage(NotFound, 'fr');
```

### Folder Structure
#### Folder Structure

For the above approach to work, the folder structure needs to look like this. Note: there must be **no** other layout or not-found file in the root of the app directory:

Expand All @@ -100,19 +107,11 @@ app

## ImgProxy

The next config provide a way to use a custom image loader for all (next)-images. It's possible to define custom imageSizes and deviceSizes as well, which are used to create the srcset of the responsive image. We restrict images with dimensions outside of these widths. By default the image loader middleware in this package uses the default image sizes concated with the default devices sizes.

### Environment variables

```
IMGPROXY_URL=
IMGPROXY_KEY=
IMGPROXY_SALT=
```
The next config provides a way to use a custom image loader for all (next)-images. It's possible to define custom imageSizes and deviceSizes as well, which are used to create the srcset of the responsive image. The image loader middleware restricts images with dimensions outside of these widths. It uses the default image sizes concatenated with the default device sizes used by Next.js.

### Loader

Unfortunately its not possible to pass a loader function directly to the next config, it has to be a file path. So create a `imageLoader.ts` inside of the Next.js root directory (or somewhere in the project), with the following content:
Unfortunately, it's not possible to pass a loader function directly to the next config; it has to be a file path. So, create an `imageLoade.ts` inside of the Next.js root directory (or somewhere else in the project), with the following content:

```ts
// imageLoader.ts
Expand All @@ -121,7 +120,7 @@ import { imgProxyLoader } from '@networkteam/zebra-utils';
export default imgProxyLoader('_img');
```

The imgProxyLoader creates a path with the provided width and quality (through the next image), as well as the base64 encoded src. The function takes a path segment (`_img` by default) which has to match the path segment in the following middleware.
The `imgProxyLoader` creates a path with the provided width and quality (through the next image), as well as the base64 encoded src. The function takes a path segment (`_img` by default) which has to match the path segment in the following middleware.

### Middleware

Expand All @@ -146,11 +145,11 @@ export const config = {
};
```

The middleware catches all paths starting with `/_img`, so all paths created by the imgproxy loader. If th Make sure the path segment matches the provided path segment of the imgproxy loader. Beside the request, imgProxyMiddleware takes `ImgProxyMiddlewareOptions`, where the allowedWiths could be overwritten.
The middleware catches all paths starting with `/_img`, so all paths created by the imgproxy loader. Make sure the path segment matches the provided path segment of the imgproxy loader. Besides the request, `imgProxyMiddleware` takes `ImgProxyMiddlewareOptions`, where the allowedWidths could be overwritten.

### Next config

The last part is to add the image loader to the next.config.js:
The last part is to add the image loader to the `next.config.js`:

```ts
// next.config.js
Expand All @@ -165,79 +164,145 @@ const config = {
};
```

Its possible to define custom imageSizes/devicesSizes. Make sure to pass all sizes to the imgProxyMiddleware as well.
It's possible to define custom imageSizes/deviceSizes. Make sure to pass all sizes to the imgProxyMiddleware as well.

## ImgProxy
## Styleguide

The next config provides a way to use a custom image loader for all (next)-images. It's possible to define custom imageSizes and deviceSizes as well, which are used to create the srcset of the responsive image. The image loader middleware restricts images with dimensions outside of these widths. It uses the default image sizes concatenated with the default device sizes used by Next.js.
This package contains a styleguide which is easy to set up for all components of the project. To enable the styleguide, set the following environment variable to any truthy value:

### Loader
### Environment variables

Unfortunately, it's not possible to pass a loader function directly to the next config; it has to be a file path. So, create an `imageLoade.ts` inside of the Next.js root directory (or somewhere else in the project), with the following content:
```
ENABLE_STYLEGUIDE="true"
```

```ts
// imageLoader.ts
import { imgProxyLoader } from '@networkteam/zebra-utils';
### Route

export default imgProxyLoader('_img');
```
First, create a folder structure for the styleguide in the app directory:

The `imgProxyLoader` creates a path with the provided width and quality (through the next image), as well as the base64 encoded src. The function takes a path segment (`_img` by default) which has to match the path segment in the following middleware.
```
app
└── styleguide
└── [[...slug]]
├── content.tsx
├── layout.tsx
└── page.tsx
### Middleware
```

Create a `middleware.ts` inside of the Next.js root directory with the following content:
Assuming all styles and fonts are imported in the project's RootLayout, we can simply import the RootLayout into the styleguide's layout:

```ts
// middleware.ts
import { imgProxyMiddleware } from '@networkteam/zebra-utils';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import RootLayout from 'app/[[...slug]]/layout';
import { localizedPage } from '@networkteam/zebra-utils';

export const middleware = async (request: NextRequest) => {
if (request.nextUrl.pathname.startsWith('/_img/')) {
return imgProxyMiddleware(request);
}
export default RootLayout;

return NextResponse.next();
};
// With multiple languages use the localizedPage helper:
// export default localizedPage(RootLayout);
```

export const config = {
matcher: '/_img/:path*',
};
In the page.tsx export the Styleguide route provided by this package:

```ts
import { Styleguide } from '@networkteam/zebra-utils';
import { content } from './content';

export default Styleguide(content);
```

The middleware catches all paths starting with `/_img`, so all paths created by the imgproxy loader. Make sure the path segment matches the provided path segment of the imgproxy loader. Besides the request, `imgProxyMiddleware` takes `ImgProxyMiddlewareOptions`, where the allowedWidths could be overwritten.
It's possible to use a different subpath for the styleguide, but make sure to pass the path to the `Styleguide` function as the second parameter, like `Styleguide(content, '/custom-path');`. It has to match the root folder name of the styleguide.

### Next config
### Content

The last part is to add the image loader to the `next.config.js`:
The content.tsx contains the structure and all components for the styleguide:

```ts
// next.config.js
const config = {
images: {
formats: ['image/avif', 'image/webp'],
loader: 'custom',
loaderFile: './imageLoader.ts',
// deviceSizes,
// imageSizes,
},
};
import { StyleguideColors, StyleguideContent } from '@networkteam/zebra-utils';

import Logo from 'lib/components/Logo';
import Signet from 'lib/components/Signet';
import TeaserCard from 'lib/components/TeaserCard';

import tailwindConfig from 'tailwind.config';

export const content: StyleguideContent = {
title: 'Styleguide',
description: 'My awesome styleguide',
pages: [
{
path: 'atoms',
title: 'Atoms',
description: 'Atoms are the smallest building blocks of a design system.',
pages: [
{
title: 'Logo',
variants: [
{
title: 'Default',
component: <Logo />,
},
{
title: 'Signet',
component: <Signet />,
},
],
},
{
title: 'Colors',
variants: [
{
component: <StyleguideColors tailwindConfig={tailwindConfig} groupShades />,
},
],
},
]
},
{
path: 'molecules',
title: 'Molecules',
description:
'Molecules are groups of atoms bonded together and are the smallest fundamental units of a compound.',
pages: [
{
title: 'Teaser Card',
variants: [
{
component: (
<TeaserCard
image="https://placebear.com/600/600"
headline="This is a Teaser"
text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr..."
buttonLabel="read more"
href="#"
/>
),
},
],
},
]
},
{
title: 'Organisms',
description:
'Organisms are relatively complex components composed of groups of molecules and/or atoms and/or other organisms.',
pages: []
}
]
```
It's possible to define custom imageSizes/deviceSizes. Make sure to pass all sizes to the imgProxyMiddleware as well.
This example follows the atomic design approach, but subpages could be named anything. The pages path is by default the page title (slugified), but could be explicitly set. Note that the Colors atom uses a custom function `StyleguideColors`, which extracts all colors of a passed `tailwindConfig` and displays them in a grid.
## Utils
These are some helper functions which are often used in Zebra projects.
### cn
This package provides a function named _cn_, based on the same-named function provided by _shadcn/ui_. It uses [clsx](https://github.com/lukeed/clsx) in combination with [tailwind-merge](https://github.com/gjtorikian/tailwind_merge). _tailwind-merge_ merges Tailwind CSS classes to prevent style conflicts. Use it the same way as _classNames_ or _clsx_:
This package provides a function named `cn`, based on the same-named function provided by `shadcn/ui`. It uses [clsx](https://github.com/lukeed/clsx) in combination with [tailwind-merge](https://github.com/gjtorikian/tailwind_merge). `tailwind-merge` merges Tailwind CSS classes to prevent style conflicts. Use it the same way as `classNames` or `clsx`:
```ts

<div
className={cn('p-4 rounded-lg',
{
Expand Down Expand Up @@ -287,7 +352,7 @@ const defaultSizes = new Map([
]);
```
For example, with the default sizes, when setting `marginMd` to `topLarge`, the resulting class would be `md:mt-28`. The sizes can be overwritten with the second argument of the `baseClasses` helper.
With the default sizes, when setting `marginMd` to `topLarge`, the resulting class would be `md:mt-28`. The sizes can be overwritten with the second argument of the `baseClasses` helper.
### slugify
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@
".": "./dist/index.js"
},
"scripts": {
"build": "yarn clean && yarn tsc --project tsconfig.json && yarn babel --config-file ./config/babel.config.js src --out-dir dist --extensions \".tsx,.ts,.js,.jsx\"",
"build": "yarn clean && yarn ts:compile && yarn babel:compile && yarn styleguide:build",
"ts:compile": "yarn tsc --project tsconfig.json",
"babel:compile": "yarn babel --config-file ./config/babel.config.js src --out-dir dist --extensions \".tsx,.ts,.js,.jsx\"",
"clean": "rm -rf dist",
"dev": "yarn clean && yarn watch",
"watch": "yarn tsc --project tsconfig.dev.json --watch",
"styleguide:watch": "npx tailwindcss -c tailwind.config.styleguide.js -o dist/styleguide/styles.css --watch",
"styleguide:build": "npx tailwindcss -c tailwind.config.styleguide.js -o dist/styleguide/styles.css",
"lint": "eslint src config"
},
"dependencies": {
Expand Down Expand Up @@ -50,6 +54,7 @@
"prettier": "^3.3.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^3.4.4",
"typescript": "^5.5.2"
},
"peerDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export { default as imgProxyMiddleware } from './imgproxy/middleware';
export { urlSafeBase64, hexDecode, sign } from './imgproxy/utils';
export { allowedWidths, defaultOptions } from './imgproxy/config';

// Styleguide
export { default as Styleguide } from './styleguide/Route';

Check failure on line 12 in src/index.ts

View workflow job for this annotation

GitHub Actions / release

Cannot find module './styleguide/Route' or its corresponding type declarations.
export { default as StyleguideColors } from './styleguide/Colors';

// Utils
export { baseClasses, marginClasses, paddingClasses } from './utils/baseClasses';
export { cn } from './utils/classnames';
Expand Down
80 changes: 80 additions & 0 deletions src/styleguide/Block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';

import { useEffect, useState } from 'react';
import { cn } from '../utils/classnames';

type StyleguideBlockProps = {
component: React.ReactNode;
title?: string;
description?: string;
align?: 'left' | 'center' | 'right';
className?: string;
};

const Block = ({ component, title, description, className, align = 'left' }: StyleguideBlockProps) => {
const [fullscreen, setFullscreen] = useState(false);

useEffect(() => {
if (fullscreen) {
document.documentElement.style.overflow = 'hidden';
document.addEventListener('keydown', handleEscape);
} else {
document.documentElement.style.overflow = '';
document.removeEventListener('keydown', handleEscape);
}
}, [fullscreen]);

const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setFullscreen(false);
}
};

if (!component) {
return null;
}

return (
<div className="sg-relative sg-mb-16 sg-border-b-2 sg-border-neutral-200 sg-pb-16 last-of-type:sg-border-none">
<div className="sg-relative sg-mb-2 sg-flex sg-justify-between">
<div className="sg-font-sans">
{title && <h2 className="sg-text-xl sg-font-bold">{title}</h2>}
{description && <p className="sg-text-base">{description}</p>}
</div>
<button
className={cn(
'sg-flex sg-h-6 sg-w-6 sg-items-center sg-justify-center sg-rounded sg-bg-neutral-200 hover:sg-bg-[#e0e0e0] md:sg-h-8 md:sg-w-8',
{
'sg-z-10': !fullscreen,
'sg-fixed sg-bottom-4 sg-right-4 sg-z-50': fullscreen,
}
)}
onClick={() => setFullscreen((v) => !v)}
>
<svg className="sg-h-4 sg-w-4 sg-text-neutral-500 md:sg-h-3.5 md:sg-w-3.5" viewBox="0 0 448 512">
<path
fill="currentColor"
d="M32 32C14.3 32 0 46.3 0 64v96c0 17.7 14.3 32 32 32s32-14.3 32-32V96h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H32zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7 14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H64V352zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32h64v64c0 17.7 14.3 32 32 32s32-14.3 32-32V64c0-17.7-14.3-32-32-32H320zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32V352z"
/>
</svg>
</button>
</div>
<div
className={cn(
{
'sg-fixed sg-inset-0 sg-z-40 sg-overflow-y-auto sg-overflow-x-hidden sg-bg-white': fullscreen,
'sg-z-0 sg-overflow-hidden sg-rounded-md sg-border sg-border-neutral-200 sg-p-4 md:sg-p-8': !fullscreen,
'sg-text-left': align === 'left' && !fullscreen,
'sg-text-center': align === 'center' && !fullscreen,
'sg-text-right': align === 'right' && !fullscreen,
},
className
)}
>
{component}
</div>
</div>
);
};

export default Block;
Loading

0 comments on commit a96924c

Please sign in to comment.