Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Responsive Images #124

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions library/foundations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,34 @@ This script is meant to be used locally to update the css variables in the theme

## yarn tokens:parse

This script parses the tokens and generates the token layers. It appends the package version as a suffix to the generated layers file if a RADIUS_LAYERS_SUFFIX environment variable is set.
This script parses the tokens and generates the token layers. It appends the package version as a suffix to the generated layers file if a `RADIUS_LAYERS_SUFFIX` environment variable is set.

## yarn tokens:validate

This script validates the token layers by comparing them with the original tokens file. It uses the generated layers file with the package version as a suffix if a RADIUS_LAYERS_SUFFIX environment variable is set.
This script validates the token layers by comparing them with the original tokens file. It uses the generated layers file with the package version as a suffix if a `RADIUS_LAYERS_SUFFIX` environment variable is set.

The validator identifies **four** types of contexts/layers:

- **Source layers (`core`)** — tokens there are not supposed to be used directly in the code, only as reference to others. they need no special treatment — the exporter detects them automatically and resolves their variables to values during the build
- **Semantic layers** are divided in two specialized categories:
- **Selectable layers** (`mode-light` or `mode-dark`) apply to some or all website features in a way developers can select using a className. To identify one of these, the exporter looks for a token named `section-name`. The value is the name of the scenario for each mode (ex.: `Light Mode` or `High Constrast`) that will be normalized automatically by the exporter to become a className (ex.: light-mode or high-contrast). The important thing to know about selectable layers is that they need to have the same tokens — or the exporter will throw an error.
- **Viewport size layers** (`breakpoint-large`, etc) apply automatically to semantic situations that vary based on the viewport size (mobile vs desktop, for example). In order for this to work, they should have special tokens `screen-min-size` and `screen-max-size` - to indicate their tokens are active when the viewport is between these values. Like selectable tokens, they also need to have the same tokens — or the exporter will throw an error.
- **Consolidation layers** (`components`) will only contain references to other layers. If these references come from more than one semantic layer (for example, if a token called `component.button.background` points to a token `semantic.surface.action` that exists in both `mode-light` and `mode-dark`) the exporter will take note and make sure the selection is properly controlled by the changes in the semantic layers (for example, in CSS, adding all the necessary classes to the `@radius-components` CSS Layer). Consolidation layers do not need special variables, like ‘screen-min-size’ or ’section-name). The exporter detects them automatically
- **Viewport size layers** (`breakpoint-large`, etc) apply automatically to semantic situations that vary based on the viewport size (mobile vs desktop, for example). In order for this to work, they should have special tokens `screen-min-size` and `screen-max-size` - to indicate their tokens are active when the viewport is between these values. Like selectable tokens, they also need to have the same tokens — or the exporter will throw an error.
- **Consolidation layers** (`components`) will only contain references to other layers. If these references come from more than one semantic layer (for example, if a token called `component.button.background` points to a token `semantic.surface.action` that exists in both `mode-light` and `mode-dark`) the exporter will take note and make sure the selection is properly controlled by the changes in the semantic layers (for example, in CSS, adding all the necessary classes to the `@radius-components` CSS Layer). Consolidation layers do not need special variables, like `screen-min-size` or `section-name`). The exporter detects them automatically

## yarn tokens:assets

Downloads all assets from snapshot file provided and resizes them to the bounds configured via `imageSizes` in `generator-config.ts`. Output assets are stored in `generated/brand/<BRAND>/assets/`

## yarn tokens:generate

This script generates the theme by using the generated token layers file with the package version as a suffix if a RADIUS_LAYERS_SUFFIX environment variable is set. It outputs the theme to the generated directory in the format specified by the second argument (in this case, css).
This script generates the theme by using the generated token layers file with the package version as a suffix if a `RADIUS_LAYERS_SUFFIX` environment variable is set. It outputs the theme to the generated directory in the format specified by the second argument (in this case, css).

It is important to note that the tokens:ci and tokens:update scripts use the RADIUS_LAYERS_SUFFIX environment variable to append the package version to the output file name. It is recommended to use these scripts in the specific scenarios described above.
It is important to note that the tokens:ci and tokens:update scripts use the `RADIUS_LAYERS_SUFFIX` environment variable to append the package version to the output file name. It is recommended to use these scripts in the specific scenarios described above.

## yarn tokens:types

This script generates type definition files by using the generated token layers file. It outputs them to the generated directory as a typescript file. It will run prettier afterwards to ensure the file obeys
linting rules.

These type definitions allow developers to use tokens in their component files without resorting to
javascript themes in runtime.
These type definitions allow developers to use tokens in their component files without resorting to
javascript themes in runtime.
5 changes: 5 additions & 0 deletions library/foundations/generator-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** Customizable Configuration for the Generator */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This config can be reused to configure the generator based on project needs, e.g. as an alternative to special tokens

export const config = {
/** Responsive image size bounds (max width/ max height in px) */
imageSizes: [400, 600, 800, 1000],
} as const;
3 changes: 2 additions & 1 deletion library/foundations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"files": [
"docs/",
"generated/*",
"src/"
"src/",
"./generator-config.ts"
Comment on lines +9 to +10
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publish config

],
"types": "generated/design-tokens.types.ts",
"repository": {
Expand Down
71 changes: 52 additions & 19 deletions library/foundations/scripts/save-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import axios from 'axios';
import { parseData } from './lib/token-parser';
import { TokenLayer, TokenLayers } from './lib/token-parser.types';
import { isVariableReference } from './lib/token-parser.utils';
import { config } from '../generator-config';

const MEDIA_FILES_PATH = './generated';

Expand Down Expand Up @@ -37,18 +38,37 @@ const findAssetSizeAndDimensions = async (url: string) => {
console.warn(`Error downloading ${url}`);
throw e;
})
.then((response) => {
.then(async (response) => {
console.info('downloaded. converting to webp');
return sharp(Buffer.from(response.data))
.webp()
.toBuffer({ resolveWithObject: true })
.catch((e) => {
console.warn(`Error parsing ${url}`);
throw e;
});
})
.then(({ data, info }) => {
return { data, info };
const image = sharp(Buffer.from(response.data));

return Promise.all(
[...config.imageSizes, 'full' as const].map((width) =>
image
.resize(
width === 'full' // full res version
? undefined
: {
// resize to max-size, this might sometimes create duplicates,
// if the source image is small, but this makes the
width: width,
height: width,
fit: 'inside',
withoutEnlargement: true,
}
)
Comment on lines +48 to +59
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resize based on config and combine with full-res version

.webp()
.toBuffer({ resolveWithObject: true })
.then((v) => ({
...v,
width,
}))
.catch((e) => {
console.warn(`Error parsing ${url}`);
throw e;
})
)
);
});
};

Expand All @@ -64,6 +84,12 @@ const filterUnique =
return acc;
};

// Promise to delay items in batches, e.g. to avoid upstream rate-limiting
const batchDelay = (requestIndex: number, batchSize = 20) =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), Math.floor(requestIndex / batchSize) * 20);
});

loadLayersFile(snapshotFileName)
.then(({ layers, order }) => {
return order
Expand All @@ -85,13 +111,17 @@ loadLayersFile(snapshotFileName)
.then((assets) =>
// for every asset, download it and convert it to webp
Promise.all(
assets.map(({ value, ...rest }) =>
findAssetSizeAndDimensions(value).then((metadata) => ({
...rest,
...metadata,
}))
assets.flatMap(({ value, ...rest }, i) =>
batchDelay(i)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

batch asset requests to avoid upstream (likely rate-limiting) timeouts.
Screenshot 2023-07-19 at 10 26 58 AM

.then(() => findAssetSizeAndDimensions(value))
.then((variations) =>
variations.map((variation) => ({
...rest,
...variation,
}))
)
)
)
).then((v) => v.flat())
)
.then((downloadedAssets) => {
// find all the directories we need to create
Expand Down Expand Up @@ -133,13 +163,16 @@ const saveFiles = <
data: Buffer;
name: string;
path: string;
width?: number | string;
}
>(
downloadedAssets: T[]
): Promise<void[]> =>
Promise.all(
downloadedAssets.map(({ name, data, path }) => {
const fileName = `${assetSavePath(path)}/${name}.webp`;
downloadedAssets.map(({ name, data, path, width }) => {
const fileName = `${assetSavePath(path)}/${name}${
width && width !== 'full' ? `_w${width}` : ''
}.webp`;
console.info(`SAVING ${fileName}`);
return promises.writeFile(fileName, data);
})
Expand Down
20 changes: 20 additions & 0 deletions library/foundations/src/helpers/src-to-src-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { config } from '../../generator-config';

/** expands `src` to `srcSet` using the configured image sized */
export const srcToSrcSet = (src: string) => {
const isLocalImage = src.startsWith('/') || src.startsWith('.');
if (!isLocalImage) {
return {
src,
};
}
const withoutExtension = src.replace('.webp', '');
const srcSet = config.imageSizes
.map((s) => `${withoutExtension}_w${s}.webp ${s}w`)
.join(', ');

return {
srcSet,
src,
};
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { forwardRef } from 'react';
import { cx } from '@emotion/css';
import { radiusTokens } from '@rangle/radius-foundations/generated/design-tokens.constants';
import { srcToSrcSet } from '@rangle/radius-foundations/src/helpers/src-to-src-set';
import {
RadiusAutoLayout,
Typography,
Expand Down Expand Up @@ -75,7 +76,7 @@ export const RadiusImageTextItem = forwardRef<
>
<RadiusAutoLayout
as="img"
src={src}
{...srcToSrcSet(src)}
Comment on lines -78 to +79
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use helper to auto-generate sourceset

alt={alt}
width="fill-parent"
height="fill-parent"
Expand Down