feat: optimize Material Symbols
This commit is contained in:
26
README.md
26
README.md
@@ -18,3 +18,29 @@ To get started, clone this repository and run `yarn install` in the root directo
|
||||
### Running `scandic-web` locally
|
||||
|
||||
To run the `scandic-web` app locally see its [README](./apps/scandic-web/README.md).
|
||||
|
||||
## Material Symbols
|
||||
|
||||
We download the font file from Google Fonts service and host it ourselves.
|
||||
|
||||
### Configuration
|
||||
|
||||
We use the following configuration:
|
||||
|
||||
- FILL axis: 0..1
|
||||
- wght axis: 400
|
||||
- GRAD axis: 0
|
||||
- opsz axis: 24
|
||||
|
||||
More info at https://developers.google.com/fonts/docs/material_symbols#optimize_the_icon_font
|
||||
|
||||
### Optimization
|
||||
|
||||
We optimize the font size by only including the icons we use in the repository.
|
||||
|
||||
Read more at: https://developers.google.com/fonts/docs/material_symbols#use_in_web
|
||||
|
||||
### Modifying icons
|
||||
|
||||
1. Update the list of icons to include in `scripts/material-symbols-update.mjs`.
|
||||
2. Run `yarn run icons:update` in monorepo root.
|
||||
|
||||
@@ -31,6 +31,10 @@ This will also spin up [Redis Insight ](https://redis.io/insight/) so that you c
|
||||
- Click **'Add Redis database'**
|
||||
- Provide Connection URL `redis://redis:6379`
|
||||
|
||||
### Icons / Material Symbols
|
||||
|
||||
Read the README.md in the monorepo root.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
@@ -2,7 +2,6 @@ import "@scandic-hotels/design-system/fonts.css"
|
||||
import "@/app/globals.css"
|
||||
import "@/public/_static/css/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/style.css"
|
||||
import "react-material-symbols/rounded"
|
||||
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||
import Script from "next/script"
|
||||
|
||||
@@ -2,7 +2,6 @@ import "@scandic-hotels/design-system/fonts.css"
|
||||
import "@/app/globals.css"
|
||||
import "@/public/_static/css/design-system-current-deprecated.css"
|
||||
import "@scandic-hotels/design-system/style.css"
|
||||
import "react-material-symbols/rounded"
|
||||
|
||||
import Script from "next/script"
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import "@scandic-hotels/design-system/fonts.css"
|
||||
import "@/app/globals.css"
|
||||
import "@/public/_static/css/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/style.css"
|
||||
import "react-material-symbols/rounded"
|
||||
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||
import Script from "next/script"
|
||||
|
||||
@@ -2,7 +2,6 @@ import "@scandic-hotels/design-system/fonts.css"
|
||||
import "@/app/globals.css"
|
||||
import "@/public/_static/css/design-system-new-deprecated.css"
|
||||
import "@scandic-hotels/design-system/style.css"
|
||||
import "react-material-symbols/rounded"
|
||||
|
||||
import Script from "next/script"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,8 @@
|
||||
"dev:web": "turbo run dev --filter=@scandic-hotels/scandic-web --output-logs new-only",
|
||||
"dev:ds": "turbo run dev --filter=@scandic-hotels/design-system --output-logs new-only",
|
||||
"test": "turbo run test",
|
||||
"postinstall": "husky"
|
||||
"postinstall": "husky",
|
||||
"icons:update": "node scripts/material-symbols-update.mjs"
|
||||
},
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
|
||||
@@ -148,6 +148,6 @@ Each component should have at least one Storybook story. A default story that sh
|
||||
|
||||
Stories that involve other non-related components are compositions. These should be placed in the `Compositions/` folder where the composition has the best chance of discoverability, typically inside the component folder of the outermost component of the composition. Exporting the same composition in multiple places can be good for discoverability.
|
||||
|
||||
### Icons
|
||||
### Icons / Material Symbols
|
||||
|
||||
Still a work in progress.
|
||||
Read the README.md in the monorepo root.
|
||||
|
||||
@@ -8,29 +8,17 @@ import { iconVariants } from '../variants'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
export interface MaterialIconProps
|
||||
extends Omit<MaterialSymbolProps, 'color' | 'fill'>,
|
||||
extends Pick<MaterialSymbolProps, 'size' | 'icon' | 'className' | 'style'>,
|
||||
VariantProps<typeof iconVariants> {
|
||||
isFilled?: boolean
|
||||
}
|
||||
|
||||
export type MaterialIconSetIconProps = Omit<MaterialIconProps, 'icon'>
|
||||
|
||||
export function MaterialIcon({
|
||||
icon,
|
||||
color,
|
||||
size = 24,
|
||||
isFilled = false,
|
||||
className,
|
||||
isFilled = false,
|
||||
...props
|
||||
}: MaterialIconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<MaterialSymbol
|
||||
icon={icon}
|
||||
size={size}
|
||||
className={classNames}
|
||||
fill={isFilled}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return <MaterialSymbol className={classNames} {...props} fill={isFilled} />
|
||||
}
|
||||
|
||||
@@ -263,3 +263,32 @@
|
||||
src: url(/_static/fonts/canela-deck/CanelaDeck-ThinItalic.otf)
|
||||
format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url(/_static/fonts/material-symbols/rounded-eaec15a9.woff2)
|
||||
format('woff2');
|
||||
}
|
||||
|
||||
.material-symbols {
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: inherit;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
user-select: none;
|
||||
font-feature-settings: 'liga';
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
Binary file not shown.
277
scripts/material-symbols-update.mjs
Normal file
277
scripts/material-symbols-update.mjs
Normal file
@@ -0,0 +1,277 @@
|
||||
// @ts-check
|
||||
import crypto from 'node:crypto';
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
|
||||
// Defines where the font lives
|
||||
const DESIGN_SYSTEM_FONT_DIR = `./packages/design-system/public/_static/fonts/material-symbols`;
|
||||
const WEB_FONT_DIR = `./apps/scandic-web/public/_static/fonts/material-symbols`;
|
||||
|
||||
// Defines the settings for the font
|
||||
const FONT_BASE_URL = `https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0..1,0`;
|
||||
|
||||
// Defines the subset of icons for the font
|
||||
const icons = [
|
||||
'accessibility',
|
||||
'accessible',
|
||||
'add',
|
||||
'add_circle',
|
||||
'air',
|
||||
'air_purifier_gen',
|
||||
'apparel',
|
||||
'arrow_back',
|
||||
'arrow_forward',
|
||||
'arrow_right',
|
||||
'arrow_upward',
|
||||
'asterisk',
|
||||
'bakery_dining',
|
||||
'beach_access',
|
||||
'bed',
|
||||
'business_center',
|
||||
'calendar_add_on',
|
||||
'calendar_month',
|
||||
'calendar_today',
|
||||
'call',
|
||||
'call_quality',
|
||||
'camera',
|
||||
'cancel',
|
||||
'chair',
|
||||
'check',
|
||||
'check_box',
|
||||
'check_circle',
|
||||
'checkroom',
|
||||
'chevron_left',
|
||||
'chevron_right',
|
||||
'close',
|
||||
'coffee',
|
||||
'compare_arrows',
|
||||
'concierge',
|
||||
'connected_tv',
|
||||
'content_copy',
|
||||
'contract',
|
||||
'countertops',
|
||||
'credit_card',
|
||||
'credit_card_heart',
|
||||
'deck',
|
||||
'delete',
|
||||
'desk',
|
||||
'device_thermostat',
|
||||
'diamond',
|
||||
'dining',
|
||||
'directions',
|
||||
'downhill_skiing',
|
||||
'download',
|
||||
'dresser',
|
||||
'edit',
|
||||
'edit_square',
|
||||
'electric_bike',
|
||||
'electric_car',
|
||||
'elevator',
|
||||
'emoji_transportation',
|
||||
'error',
|
||||
'error_circle_rounded',
|
||||
'exercise',
|
||||
'family_restroom',
|
||||
'fastfood',
|
||||
'favorite',
|
||||
'fax',
|
||||
'featured_seasonal_and_gifts',
|
||||
'filter',
|
||||
'filter_alt',
|
||||
'floor_lamp',
|
||||
'garage',
|
||||
'globe',
|
||||
'groups',
|
||||
'health_and_beauty',
|
||||
'heat',
|
||||
'hiking',
|
||||
'home',
|
||||
'hot_tub',
|
||||
'imagesmode',
|
||||
'info',
|
||||
'iron',
|
||||
'kayaking',
|
||||
'kettle',
|
||||
'keyboard_arrow_down',
|
||||
'keyboard_arrow_up',
|
||||
'laundry',
|
||||
'link',
|
||||
'liquor',
|
||||
'local_bar',
|
||||
'local_cafe',
|
||||
'local_convenience_store',
|
||||
'local_laundry_service',
|
||||
'local_parking',
|
||||
'location_city',
|
||||
'location_on',
|
||||
'lock',
|
||||
'luggage',
|
||||
'mail',
|
||||
'map',
|
||||
'meeting_room',
|
||||
'mode_fan',
|
||||
'museum',
|
||||
'nature',
|
||||
'nightlife',
|
||||
'open_in_new',
|
||||
'pan_zoom',
|
||||
'pedal_bike',
|
||||
'person',
|
||||
'pets',
|
||||
'phone',
|
||||
'pool',
|
||||
'refresh',
|
||||
'remove',
|
||||
'restaurant',
|
||||
'room_service',
|
||||
'sauna',
|
||||
'search',
|
||||
'sell',
|
||||
'shopping_bag',
|
||||
'skateboarding',
|
||||
'smoke_free',
|
||||
'smoking_rooms',
|
||||
'spa',
|
||||
'sports_esports',
|
||||
'sports_golf',
|
||||
'sports_tennis',
|
||||
'star',
|
||||
'straighten',
|
||||
'styler',
|
||||
'swipe',
|
||||
'sync_saved_locally',
|
||||
'theater_comedy',
|
||||
'train',
|
||||
'travel',
|
||||
'tv_remote',
|
||||
'upload',
|
||||
'visibility',
|
||||
'visibility_off',
|
||||
'warning',
|
||||
'water_full',
|
||||
'wifi',
|
||||
];
|
||||
|
||||
function createHash(value) {
|
||||
const stringified = stringify(value);
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(stringified);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
const hash = createHash(icons).substring(0, 8);
|
||||
|
||||
async function fetchIconUrl(url) {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept:
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Unable to fetch woff2 for ${url}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
const isWoff2 = /format\('woff2'\)/.test(text);
|
||||
if (!isWoff2) {
|
||||
console.error(`Unable to identify woff2 font in response`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const srcUrl = text.match(/src: url\(([^)]+)\)/);
|
||||
|
||||
if (srcUrl && srcUrl[1]) {
|
||||
return srcUrl[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function download(url, destFolder) {
|
||||
const dest = resolve(join(destFolder, `/rounded-${hash}.woff2`));
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Unable to fetch ${url}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
console.error(`Bad response from ${url}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fileStream = createWriteStream(dest);
|
||||
|
||||
// @ts-expect-error: type mismatch
|
||||
const readableNodeStream = Readable.fromWeb(response.body);
|
||||
|
||||
await pipeline(readableNodeStream, fileStream);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading file from ${url}:`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
async function cleanFontDirs(folderPath) {
|
||||
await rm(DESIGN_SYSTEM_FONT_DIR, { recursive: true, force: true });
|
||||
await mkdir(DESIGN_SYSTEM_FONT_DIR, { recursive: true });
|
||||
|
||||
await rm(WEB_FONT_DIR, { recursive: true, force: true });
|
||||
await mkdir(WEB_FONT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function updateFontCSS() {
|
||||
const file = './packages/design-system/lib/fonts.css';
|
||||
|
||||
const css = await readFile(file, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
await writeFile(
|
||||
file,
|
||||
css.replace(
|
||||
/url\(\/_static\/fonts\/material-symbols\/rounded[^)]+\)/,
|
||||
`url(/_static/fonts/material-symbols/rounded-${hash}.woff2)`
|
||||
),
|
||||
{
|
||||
encoding: 'utf-8',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const fontUrl = `${FONT_BASE_URL}&icon_names=${icons.join(',')}&display=block`;
|
||||
|
||||
const iconUrl = await fetchIconUrl(fontUrl);
|
||||
|
||||
if (iconUrl) {
|
||||
await cleanFontDirs();
|
||||
|
||||
await download(iconUrl, DESIGN_SYSTEM_FONT_DIR);
|
||||
await download(iconUrl, WEB_FONT_DIR);
|
||||
|
||||
await updateFontCSS();
|
||||
|
||||
console.log('Successfully updated icons!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(
|
||||
`Unable to find the icon font src URL in CSS response from Google Fonts at ${fontUrl}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user