diff --git a/src/Header.module.css b/src/Header.module.css index 7f94581..857a73b 100644 --- a/src/Header.module.css +++ b/src/Header.module.css @@ -131,7 +131,7 @@ } .leftNav h3 { - font-size: 18px; + font-size: var(--font-size-subtitle); } .nav { diff --git a/src/ListBox.module.css b/src/ListBox.module.css index ab17f3c..2b9cc2d 100644 --- a/src/ListBox.module.css +++ b/src/ListBox.module.css @@ -19,7 +19,7 @@ padding: 8px 16px; outline: none; cursor: pointer; - font-size: 15px; + font-size: var(--font-size-body); min-height: 32px; } diff --git a/src/Menu.module.css b/src/Menu.module.css index 3ef29e6..110dd47 100644 --- a/src/Menu.module.css +++ b/src/Menu.module.css @@ -12,7 +12,10 @@ align-items: center; padding: 0 12px; color: var(--primary-content); - font-size: 14px; + font-size: var(--font-size-body); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } .menuItem > * { diff --git a/src/Modal.module.css b/src/Modal.module.css index 96bda6f..2def854 100644 --- a/src/Modal.module.css +++ b/src/Modal.module.css @@ -29,7 +29,7 @@ .modalHeader h3 { font-weight: 600; - font-size: 24px; + font-size: var(--font-size-title); margin: 0; } diff --git a/src/Tooltip.module.css b/src/Tooltip.module.css index fccb4cb..8d7a877 100644 --- a/src/Tooltip.module.css +++ b/src/Tooltip.module.css @@ -8,7 +8,7 @@ border-radius: 8px; max-width: 135px; width: max-content; - font-size: 12px; + font-size: var(--font-size-caption); font-weight: 500; text-align: center; } diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 01ea548..ce90427 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -21,30 +21,60 @@ export interface UrlParams { roomAlias: string | null; roomId: string | null; viaServers: string[]; - // Whether the app is running in embedded mode, and should keep the user - // confined to the current room + /** + * Whether the app is running in embedded mode, and should keep the user + * confined to the current room. + */ isEmbedded: boolean; - // Whether the app should pause before joining the call until it sees an - // io.element.join widget action, allowing it to be preloaded + /** + * Whether the app should pause before joining the call until it sees an + * io.element.join widget action, allowing it to be preloaded. + */ preload: boolean; - // Whether to hide the room header when in a call + /** + * Whether to hide the room header when in a call. + */ hideHeader: boolean; - // Whether to hide the screen-sharing button + /** + * Whether to hide the screen-sharing button. + */ hideScreensharing: boolean; - // Whether to start a walkie-talkie call instead of a video call + /** + * Whether to start a walkie-talkie call instead of a video call. + */ isPtt: boolean; - // Whether to use end-to-end encryption + /** + * Whether to use end-to-end encryption. + */ e2eEnabled: boolean; - // The user's ID (only used in matryoshka mode) + /** + * The user's ID (only used in matryoshka mode). + */ userId: string | null; - // The display name to use for auto-registration + /** + * The display name to use for auto-registration. + */ displayName: string | null; - // The device's ID (only used in matryoshka mode) + /** + * The device's ID (only used in matryoshka mode). + */ deviceId: string | null; - // The base URL of the homeserver to use for media lookups in matryoshka mode + /** + * The base URL of the homeserver to use for media lookups in matryoshka mode. + */ baseUrl: string | null; - // The BCP 47 code of the language the app should use + /** + * The BCP 47 code of the language the app should use. + */ lang: string | null; + /** + * The fonts which the interface should use, if not empty. + */ + fonts: string[]; + /** + * The factor by which to scale the interface's font size. + */ + fontScale: number | null; } /** @@ -81,6 +111,8 @@ export const getUrlParams = ( ? fragment : fragment.substring(0, fragmentQueryStart); + const fontScale = parseFloat(getParam("fontScale") ?? ""); + return { roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null, roomId: getParam("roomId"), @@ -96,6 +128,8 @@ export const getUrlParams = ( deviceId: getParam("deviceId"), baseUrl: getParam("baseUrl"), lang: getParam("lang"), + fonts: getAllParams("font"), + fontScale: Number.isNaN(fontScale) ? null : fontScale, }; }; diff --git a/src/UserMenu.module.css b/src/UserMenu.module.css index 2a0ed94..06e817c 100644 --- a/src/UserMenu.module.css +++ b/src/UserMenu.module.css @@ -10,13 +10,13 @@ .avatar { width: 24px; height: 24px; - font-size: 12px; + font-size: var(--font-size-caption); } @media (min-width: 800px) { .avatar { width: 32px; height: 32px; - font-size: 15px; + font-size: var(--font-size-body); } } diff --git a/src/auth/LoginPage.module.css b/src/auth/LoginPage.module.css index 0d135c3..4a798d7 100644 --- a/src/auth/LoginPage.module.css +++ b/src/auth/LoginPage.module.css @@ -36,7 +36,7 @@ .formContainer h4 { font-weight: normal; - font-size: 18px; + font-size: var(--font-size-subtitle); margin-bottom: 0; } @@ -48,7 +48,7 @@ .formContainer button { height: 48px; width: 100%; - font-size: 15px; + font-size: var(--font-size-body); font-weight: 600; } @@ -61,7 +61,7 @@ .authLinks { margin-bottom: 100px; - font-size: 15px; + font-size: var(--font-size-body); } .authLinks a { diff --git a/src/button/Button.module.css b/src/button/Button.module.css index 53e3b2d..eb8f0b1 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -41,7 +41,7 @@ limitations under the License. .copyButton { padding: 7px 15px; border-radius: 8px; - font-size: 14px; + font-size: var(--font-size-body); font-weight: 700; } @@ -142,7 +142,7 @@ limitations under the License. .copyButton span { font-weight: 600; - font-size: 15px; + font-size: var(--font-size-body); margin-right: 10px; overflow: hidden; white-space: nowrap; diff --git a/src/index.css b/src/index.css index 6e9a912..fbc9d1a 100644 --- a/src/index.css +++ b/src/index.css @@ -23,8 +23,20 @@ limitations under the License. @import "normalize.css/normalize.css"; :root { + --font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; --inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f, U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF; + + --font-scale: 1; + --font-size-micro: calc(10px * var(--font-scale)); + --font-size-caption: calc(12px * var(--font-scale)); + --font-size-body: calc(15px * var(--font-scale)); + --font-size-subtitle: calc(18px * var(--font-scale)); + --font-size-title: calc(24px * var(--font-scale)); + --font-size-headline: calc(32px * var(--font-scale)); + --accent: #0dbd8b; --accent-20: #0dbd8b33; --alert: #ff5b55; @@ -127,9 +139,7 @@ body { color: var(--primary-content); color-scheme: dark; margin: 0; - font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", - "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; + font-family: var(--font-family); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -159,28 +169,31 @@ a { /* Headline Semi Bold */ h1 { font-weight: 600; - font-size: 32px; - line-height: 39px; + font-size: var(--font-size-headline); } /* Title */ h2 { font-weight: 600; - font-size: 24px; - line-height: 29px; + font-size: var(--font-size-title); } /* Subtitle */ h3 { font-weight: 400; - font-size: 18px; - line-height: 22px; + font-size: var(--font-size-subtitle); +} + +h1, +h2, +h3 { + line-height: 1.2; } /* Body */ p { - font-size: 15px; - line-height: 24px; + font-size: var(--font-size-body); + line-height: var(--font-size-title); } a { @@ -202,21 +215,21 @@ hr { text-align: center; height: 5px; font-weight: 600; - font-size: 15px; + font-size: var(--font-size-body); line-height: 24px; margin: 0 12px; } summary { - font-size: 14px; + font-size: var(--font-size-body); } details > :not(summary) { - margin-left: 16px; + margin-left: var(--font-size-body); } details[open] > summary { - margin-bottom: 16px; + margin-bottom: var(--font-size-body); } #root > [data-overlay-container] { diff --git a/src/initializer.tsx b/src/initializer.tsx index 65183b0..6290764 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -133,6 +133,21 @@ export class Initializer { import.meta.env.VITE_THEME_BACKGROUND_85 as string ); } + + // Custom fonts + const { fonts, fontScale } = getUrlParams(); + if (fontScale !== null) { + document.documentElement.style.setProperty( + "--font-scale", + fontScale.toString() + ); + } + if (fonts.length > 0) { + document.documentElement.style.setProperty( + "--font-family", + fonts.map((f) => `"${f}"`).join(", ") + ); + } } public static init(): Promise | null { diff --git a/src/input/Input.module.css b/src/input/Input.module.css index c04a85a..fce0a93 100644 --- a/src/input/Input.module.css +++ b/src/input/Input.module.css @@ -32,7 +32,7 @@ .inputField input, .inputField textarea { font-weight: 400; - font-size: 15px; + font-size: var(--font-size-body); border: none; border-radius: 4px; padding: 12px 9px 10px 9px; @@ -73,7 +73,7 @@ top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s; color: var(--tertiary-content); background-color: transparent; - font-size: 15px; + font-size: var(--font-size-body); position: absolute; left: 0; top: 0; @@ -104,7 +104,7 @@ background-color: var(--system); transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s, top 0.25s ease-out 0s, background-color 0.25s ease-out 0s; - font-size: 10px; + font-size: var(--font-size-micro); top: -13px; padding: 0 2px; pointer-events: auto; @@ -125,7 +125,7 @@ display: flex; align-items: center; flex-grow: 1; - font-size: 15px; + font-size: var(--font-size-body); line-height: 24px; } @@ -174,7 +174,7 @@ .errorMessage { margin: 0; - font-size: 13px; + font-size: var(--font-size-caption); color: var(--alert); font-weight: 600; } diff --git a/src/input/SelectInput.module.css b/src/input/SelectInput.module.css index f1312c8..afa0ce4 100644 --- a/src/input/SelectInput.module.css +++ b/src/input/SelectInput.module.css @@ -7,7 +7,7 @@ .label { font-weight: 600; - font-size: 18px; + font-size: var(--font-size-subtitle); margin-top: 0; margin-bottom: 12px; } @@ -20,7 +20,7 @@ background-color: var(--background); border-radius: 8px; border: 1px solid var(--quinary-content); - font-size: 15px; + font-size: var(--font-size-body); color: var(--primary-content); height: 40px; max-width: 100%; diff --git a/src/room/GroupCallInspector.module.css b/src/room/GroupCallInspector.module.css index 289523f..74dc8e9 100644 --- a/src/room/GroupCallInspector.module.css +++ b/src/room/GroupCallInspector.module.css @@ -19,7 +19,7 @@ } .sequenceDiagramViewer :global(.messageText) { - font-size: 12px; + font-size: var(--font-size-caption); fill: var(--primary-content) !important; stroke: var(--primary-content) !important; } diff --git a/src/room/LobbyView.module.css b/src/room/LobbyView.module.css index 03efa7d..55b7b5b 100644 --- a/src/room/LobbyView.module.css +++ b/src/room/LobbyView.module.css @@ -54,7 +54,7 @@ limitations under the License. bottom: 86px; left: 50%; font-weight: 600; - font-size: 15px; + font-size: var(--font-size-body); transform: translateX(-50%); } diff --git a/src/room/PTTCallView.module.css b/src/room/PTTCallView.module.css index f053850..834840c 100644 --- a/src/room/PTTCallView.module.css +++ b/src/room/PTTCallView.module.css @@ -64,7 +64,7 @@ .actionTip { margin-top: 20px; margin-bottom: 20px; - font-size: 17px; + font-size: var(--font-size-subtitle); } .footer { diff --git a/src/typography/Typography.module.css b/src/typography/Typography.module.css index a22f22e..451f2d9 100644 --- a/src/typography/Typography.module.css +++ b/src/typography/Typography.module.css @@ -1,11 +1,11 @@ .caption { - font-size: 12px; - line-height: 15px; + font-size: var(--font-size-caption); + line-height: var(--font-size-body); } .micro { - font-size: 10px; - line-height: 12px; + font-size: var(--font-size-micro); + line-height: var(--font-size-caption); } .regular { diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css index ec496e6..94f53a7 100644 --- a/src/video-grid/VideoTile.module.css +++ b/src/video-grid/VideoTile.module.css @@ -119,9 +119,9 @@ } .memberName span { - font-size: 12px; + font-size: var(--font-size-caption); font-weight: 400; - line-height: 16px; + line-height: var(--font-size-body); text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -152,8 +152,8 @@ align-items: center; padding: 4px 8px; font-weight: normal; - font-size: 12px; - line-height: 15px; + font-size: var(--font-size-caption); + line-height: var(--font-size-body); } .screensharePIP { diff --git a/test/initializer-test.ts b/test/initializer-test.ts new file mode 100644 index 0000000..b31f6a8 --- /dev/null +++ b/test/initializer-test.ts @@ -0,0 +1,33 @@ +/* +Copyright 2022 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Initializer } from "../src/initializer"; + +test("initBeforeReact sets font family from URL param", () => { + window.location.hash = "#?font=DejaVu Sans"; + Initializer.initBeforeReact(); + expect( + getComputedStyle(document.documentElement).getPropertyValue("--font-family") + ).toBe('"DejaVu Sans"'); +}); + +test("initBeforeReact sets font scale from URL param", () => { + window.location.hash = "#?fontScale=1.2"; + Initializer.initBeforeReact(); + expect( + getComputedStyle(document.documentElement).getPropertyValue("--font-scale") + ).toBe("1.2"); +});