diff --git a/README.md b/README.md index 29c162fad..40371e568 100644 --- a/README.md +++ b/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. diff --git a/apps/scandic-web/README.md b/apps/scandic-web/README.md index ae6b0cc67..8981fca49 100644 --- a/apps/scandic-web/README.md +++ b/apps/scandic-web/README.md @@ -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: diff --git a/apps/scandic-web/app/[lang]/(live)/layout.tsx b/apps/scandic-web/app/[lang]/(live)/layout.tsx index 1c19a1052..a9cb480fa 100644 --- a/apps/scandic-web/app/[lang]/(live)/layout.tsx +++ b/apps/scandic-web/app/[lang]/(live)/layout.tsx @@ -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" diff --git a/apps/scandic-web/app/[lang]/(live-current)/layout.tsx b/apps/scandic-web/app/[lang]/(live-current)/layout.tsx index b85a1bf7b..236b3611f 100644 --- a/apps/scandic-web/app/[lang]/(live-current)/layout.tsx +++ b/apps/scandic-web/app/[lang]/(live-current)/layout.tsx @@ -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" diff --git a/apps/scandic-web/app/[lang]/(partner)/layout.tsx b/apps/scandic-web/app/[lang]/(partner)/layout.tsx index 728b4b505..e06a3bdb2 100644 --- a/apps/scandic-web/app/[lang]/(partner)/layout.tsx +++ b/apps/scandic-web/app/[lang]/(partner)/layout.tsx @@ -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" diff --git a/apps/scandic-web/app/[lang]/webview/layout.tsx b/apps/scandic-web/app/[lang]/webview/layout.tsx index ccf5f938b..f65a6dc95 100644 --- a/apps/scandic-web/app/[lang]/webview/layout.tsx +++ b/apps/scandic-web/app/[lang]/webview/layout.tsx @@ -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" diff --git a/apps/scandic-web/public/_static/fonts/material-symbols/rounded-eaec15a9.woff2 b/apps/scandic-web/public/_static/fonts/material-symbols/rounded-eaec15a9.woff2 new file mode 100644 index 000000000..1abb94dce Binary files /dev/null and b/apps/scandic-web/public/_static/fonts/material-symbols/rounded-eaec15a9.woff2 differ diff --git a/package.json b/package.json index 365916d33..f7a8dcbd8 100644 --- a/package.json +++ b/package.json @@ -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/*", diff --git a/packages/design-system/README.md b/packages/design-system/README.md index 11cb36166..5b018c82d 100644 --- a/packages/design-system/README.md +++ b/packages/design-system/README.md @@ -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. diff --git a/packages/design-system/lib/components/Icons/MaterialIcon/MaterialIcon.tsx b/packages/design-system/lib/components/Icons/MaterialIcon/MaterialIcon.tsx index 72c937272..265d98c17 100644 --- a/packages/design-system/lib/components/Icons/MaterialIcon/MaterialIcon.tsx +++ b/packages/design-system/lib/components/Icons/MaterialIcon/MaterialIcon.tsx @@ -8,29 +8,17 @@ import { iconVariants } from '../variants' import type { VariantProps } from 'class-variance-authority' export interface MaterialIconProps - extends Omit, + extends Pick, VariantProps { isFilled?: boolean } - export type MaterialIconSetIconProps = Omit - export function MaterialIcon({ - icon, color, - size = 24, - isFilled = false, className, + isFilled = false, ...props }: MaterialIconProps) { const classNames = iconVariants({ className, color }) - return ( - - ) + return } diff --git a/packages/design-system/lib/fonts.css b/packages/design-system/lib/fonts.css index 7ccb54037..fb73439bd 100644 --- a/packages/design-system/lib/fonts.css +++ b/packages/design-system/lib/fonts.css @@ -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; +} diff --git a/packages/design-system/public/_static/fonts/material-symbols/rounded-eaec15a9.woff2 b/packages/design-system/public/_static/fonts/material-symbols/rounded-eaec15a9.woff2 new file mode 100644 index 000000000..1abb94dce Binary files /dev/null and b/packages/design-system/public/_static/fonts/material-symbols/rounded-eaec15a9.woff2 differ diff --git a/scripts/material-symbols-update.mjs b/scripts/material-symbols-update.mjs new file mode 100644 index 000000000..72ce3d0de --- /dev/null +++ b/scripts/material-symbols-update.mjs @@ -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();