From db0f6837ce4fd8d0b3b5d627b2b430c466bf4740 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 3 Sep 2026 16:10:26 +0200 Subject: [PATCH] Give the component what the app shell was providing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The component built and typechecked in the previous commit, but only because nothing had rendered it. Everything Element Call needs that `src/main.tsx` side-loads was missing from it. Its stylesheet: only main.tsx imported index.css, so the library build emitted CSS-module styles with every `--cpd-*` and `--font-size-*` unresolved. Split into base.css, which both the app and the component import, and the rules that are about owning a page, which only the app does. The split is a straight move — comment-stripped and sorted, the old file and the two new ones differ by exactly one line — and that line is the deliberate part: `.no-scroll-body` becomes `body.no-scroll-body`. Element Call adds that class to whatever it treats as its root, and since the root can now be a container, `position: fixed` would have taken that container out of the host's layout. Pinning the page is what it always meant. Its translations: `initializeElementCall` called `i18n.init` with neither resources nor a backend, so every key would have rendered as itself. English is bundled in. The app fetches locale files from URLs its own build emits, which a host serving the library from somewhere else could not resolve, so how a host picks a language is left open. And the types a host needs: `HostBridge` alone is not enough to implement `HostBridge` — `HostRequest`, `DeviceMuteState`, `DeviceMuteRequest` and `JoinCallData` all appear in its signatures, and `ConfigOptions` in `initializeElementCall`'s. --- component/ElementCall.module.css | 8 ++ component/index.tsx | 31 ++++- src/base.css | 203 +++++++++++++++++++++++++++++++ src/index.css | 191 ++--------------------------- 4 files changed, 250 insertions(+), 183 deletions(-) create mode 100644 src/base.css diff --git a/component/ElementCall.module.css b/component/ElementCall.module.css index 399178aea..461ad9b0c 100644 --- a/component/ElementCall.module.css +++ b/component/ElementCall.module.css @@ -22,3 +22,11 @@ using an iframe. */ -moz-osx-font-smoothing: grayscale; -webkit-tap-highlight-color: transparent; } + +/* Compound's overlay container, which holds tooltips and popovers, has to fill +the container for the elements inside it to be positioned against it. The +standalone page does the same for the container under `#root`. */ +.root > [data-overlay-container] { + position: relative; + block-size: 100%; +} diff --git a/component/index.tsx b/component/index.tsx index e904c8bed..1d3155d30 100644 --- a/component/index.tsx +++ b/component/index.tsx @@ -17,6 +17,18 @@ Please see LICENSE in the repository root for full details. * host instead, or is confined to the container it is mounted in. */ +// The design tokens, fonts and element defaults every Element Call stylesheet +// builds on. +// +// Where these land relative to the component stylesheets is the bundler's +// choice — the standalone app puts them first, this build puts them in the +// middle — so nothing in base.css may depend on winning or losing against a +// component's own rules at equal specificity. It currently does not: what it +// declares unlayered is custom properties on Element Call's root, which +// components inherit rather than compete with, and everything from Compound +// sits in a `@layer`, which loses to unlayered rules either way. +import "../src/base.css"; + import { type FC, type JSX, type ReactNode, useMemo, useState } from "react"; import { type MatrixClient } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/lib/logger"; @@ -26,6 +38,7 @@ import { TooltipProvider } from "@vector-im/compound-web"; import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill"; import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill.js"; +import EN from "../locales/en/app.json"; import { ElementCallView } from "../src/ElementCallView"; import { ClientProvider } from "../src/ClientContext"; import { @@ -50,7 +63,17 @@ import { useTheme } from "../src/useTheme"; import { useInitial } from "../src/useInitial"; import styles from "./ElementCall.module.css"; -export { type HostBridge } from "../src/HostBridge"; +// Everything needed to implement a HostBridge, not just the interface itself +export { + type DeviceMuteRequest, + type DeviceMuteState, + type HostBridge, + type HostRequest, +} from "../src/HostBridge"; +export { type JoinCallData } from "../src/widget"; +// The deployment-wide configuration, as distinct from ElementCallConfiguration +// above, which is per call +export { type ConfigOptions } from "../src/config/ConfigOptions"; /** * How Element Call should behave. Everything is optional; anything left out @@ -102,6 +125,12 @@ export async function initializeElementCall( contextSeparator: "|", lng: "en", interpolation: { escapeValue: false }, + // English only, bundled in. The standalone app fetches its locale files at + // runtime from URLs its own build emits, which a host serving the library + // from elsewhere could not resolve; bundling one language at least keeps + // the component self-contained. Letting a host supply the rest, or its own + // translations, is still to do. + resources: { en: { app: EN } }, }); } diff --git a/src/base.css b/src/base.css new file mode 100644 index 000000000..0d424e64a --- /dev/null +++ b/src/base.css @@ -0,0 +1,203 @@ +/* +Copyright 2021-2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +/* The styles Element Call needs wherever it is shown: the design tokens, fonts +and element defaults its own stylesheets build on top of. + +Split out from index.css so that Element Call embedded in a host application +can have these without also being given the standalone page's layout, which +would style the host's own document. What remains here does still reach outside +Element Call's container — normalize.css and the typography below use bare +element selectors, and the custom properties are declared on `:root` — so a +host gets those too. Narrowing them needs a real host to check against, so it +waits for the Element Web integration rather than being guessed at here. + +Nothing here should depend on where it lands relative to Element Call's +component stylesheets: the bundler decides that, and it decides differently for +the app and for the component build. */ + +@layer normalize, compound-legacy, compound; + +@import url("@fontsource/inter/400.css"); +@import url("@fontsource/inter/500.css"); +@import url("@fontsource/inter/600.css"); +@import url("@fontsource/inter/700.css"); +@import url("@fontsource/inconsolata/400.css"); +@import url("@fontsource/inconsolata/700.css"); + +@import url("normalize.css/normalize.css") layer(normalize); +@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound); +@import url("@vector-im/compound-web/dist/style.css") layer(compound.components); + +:root { + --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)); + + --cpd-color-border-accent: var(--cpd-color-green-800); + /* The distance to inset non-full-width content from the edge of the window + along the inline axis. This ramps up from 16px for typical mobile windows, to + 96px for typical desktop windows, and accounts for the safe area. */ + --content-inset-left: calc( + env(safe-area-inset-left) + + min( + var(--cpd-space-24x), + max(var(--cpd-space-4x), calc((100vw - 900px) / 3)) + ) + ); + --content-inset-right: calc( + env(safe-area-inset-right) + + min( + var(--cpd-space-24x), + max(var(--cpd-space-4x), calc((100vw - 900px) / 3)) + ) + ); + --small-drop-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15); + --big-drop-shadow: 0px 0px 24px 0px #1b1d221a; + --subtle-drop-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + + --call-view-overlay-layer: 1; + --call-view-header-footer-layer: 2; +} + +:root, +[class*="cpd-theme-"] { + --video-tile-background: var(--cpd-color-bg-subtle-secondary); +} + +.cpd-theme-dark { + --cpd-color-border-accent: var(--cpd-color-green-1100); + --stopgap-color-on-solid-accent: var(--cpd-color-text-primary); + --stopgap-background-85: rgba(16, 19, 23, 0.85); +} + +@media (min-height: 330px) { + [data-element-call-root][data-background="gradient"]::before { + content: ""; + position: fixed; + /* Chromium abruptly fades our images to fully transparent at the edge of + the element. If we just make the element a little bigger than the viewport, + this is no longer visible. */ + inset: -20px; + background-image: url("graphics/mobile-gradient.png"); + background-size: 1400px 305px; + background-position: bottom; + background-repeat: no-repeat; + } + + [data-element-call-root][data-background="gradient"][data-platform="desktop"]::before { + background-image: url("graphics/desktop-gradient.png"); + background-size: max(1440px, 100vw) max(1440px, 100vh); + background-position: center; + } +} + +/* We use this to not render the page at all until we know the theme.*/ +.no-theme { + opacity: 0; +} + +/* On Android and iOS, prefer native system fonts. The global.css file of +Compound Web is where these variables ultimately get consumed to set the page's +font-family. */ +[data-element-call-root][data-platform="android"] { + --cpd-font-family-sans: "Roboto", "Noto", "Inter", sans-serif; +} + +[data-element-call-root][data-platform="ios"] { + --cpd-font-family-sans: + -apple-system, BlinkMacSystemFont, "Inter", sans-serif; +} + +@layer compound-legacy { + h1, + h2, + h3, + h4, + h5, + h6, + p, + a { + margin-top: 0; + } + + /* Headline Semi Bold */ + h1 { + font-weight: 600; + font-size: var(--font-size-headline); + } + + /* Title */ + h2 { + font-weight: 600; + font-size: var(--font-size-title); + } + + /* Subtitle */ + h3 { + font-weight: 600; + font-size: var(--font-size-subtitle); + } + + /* Body Semi Bold */ + h4 { + font-weight: 600; + font-size: var(--font-size-body); + } + + h1, + h2, + h3 { + line-height: 1.2; + } + + /* Body */ + p { + font-size: var(--font-size-body); + line-height: var(--font-size-title); + } + + hr { + width: calc(100% - 24px); + border: none; + border-top: 1px solid var(--cpd-color-border-interactive-secondary); + color: var(--cpd-color-border-interactive-secondary); + overflow: visible; + text-align: center; + height: 5px; + font-weight: 600; + font-size: var(--font-size-body); + line-height: 24px; + margin: 0 12px; + } + + summary { + font-size: var(--font-size-body); + } + + details > :not(summary) { + margin-left: var(--font-size-body); + } + + details[open] > summary { + margin-bottom: var(--font-size-body); + } +} + +/* normalize.css sets the focus rings on buttons in Firefox to an unusual custom +outline, which is inconsistent with our other components and is not sufficiently +visible to be accessible. This resets it back to 'auto'. */ +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: auto; +} diff --git a/src/index.css b/src/index.css index 12f3951da..7188f5e79 100644 --- a/src/index.css +++ b/src/index.css @@ -5,64 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -@layer normalize, compound-legacy, compound; +/* Styles for Element Call as a page of its own. The parts that apply wherever +Element Call is shown live in base.css; these are about owning the document, +and are not loaded when a host embeds Element Call as a component. */ -@import url("@fontsource/inter/400.css"); -@import url("@fontsource/inter/500.css"); -@import url("@fontsource/inter/600.css"); -@import url("@fontsource/inter/700.css"); -@import url("@fontsource/inconsolata/400.css"); -@import url("@fontsource/inconsolata/700.css"); - -@import url("normalize.css/normalize.css") layer(normalize); -@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound); -@import url("@vector-im/compound-web/dist/style.css") layer(compound.components); - -:root { - --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)); - - --cpd-color-border-accent: var(--cpd-color-green-800); - /* The distance to inset non-full-width content from the edge of the window - along the inline axis. This ramps up from 16px for typical mobile windows, to - 96px for typical desktop windows, and accounts for the safe area. */ - --content-inset-left: calc( - env(safe-area-inset-left) + - min( - var(--cpd-space-24x), - max(var(--cpd-space-4x), calc((100vw - 900px) / 3)) - ) - ); - --content-inset-right: calc( - env(safe-area-inset-right) + - min( - var(--cpd-space-24x), - max(var(--cpd-space-4x), calc((100vw - 900px) / 3)) - ) - ); - --small-drop-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15); - --big-drop-shadow: 0px 0px 24px 0px #1b1d221a; - --subtle-drop-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); - - --call-view-overlay-layer: 1; - --call-view-header-footer-layer: 2; -} - -:root, -[class*="cpd-theme-"] { - --video-tile-background: var(--cpd-color-bg-subtle-secondary); -} - -.cpd-theme-dark { - --cpd-color-border-accent: var(--cpd-color-green-1100); - --stopgap-color-on-solid-accent: var(--cpd-color-text-primary); - --stopgap-background-85: rgba(16, 19, 23, 0.85); -} +@import url("./base.css"); body { background-color: var(--cpd-color-bg-canvas-default); @@ -74,39 +21,16 @@ body { -webkit-tap-highlight-color: transparent; } -@media (min-height: 330px) { - [data-element-call-root][data-background="gradient"]::before { - content: ""; - position: fixed; - /* Chromium abruptly fades our images to fully transparent at the edge of - the element. If we just make the element a little bigger than the viewport, - this is no longer visible. */ - inset: -20px; - background-image: url("graphics/mobile-gradient.png"); - background-size: 1400px 305px; - background-position: bottom; - background-repeat: no-repeat; - } - - [data-element-call-root][data-background="gradient"][data-platform="desktop"]::before { - background-image: url("graphics/desktop-gradient.png"); - background-size: max(1440px, 100vw) max(1440px, 100vh); - background-position: center; - } -} - /* This prohibits the view to scroll for pages smaller than 122px in width -we use this for mobile pip webviews */ -.no-scroll-body { +we use this for mobile pip webviews. Element Call adds this class to whatever +it treats as its root, but it is only ever the page that should be pinned like +this — done to a container inside a host application it would take that +container out of the host's layout — so the selector says so. */ +body.no-scroll-body { position: fixed; width: 100%; } -/* We use this to not render the page at all until we know the theme.*/ -.no-theme { - opacity: 0; -} - html, body, #root { @@ -123,104 +47,7 @@ body, isolation: isolate; } -/* On Android and iOS, prefer native system fonts. The global.css file of -Compound Web is where these variables ultimately get consumed to set the page's -font-family. */ -[data-element-call-root][data-platform="android"] { - --cpd-font-family-sans: "Roboto", "Noto", "Inter", sans-serif; -} - -[data-element-call-root][data-platform="ios"] { - --cpd-font-family-sans: - -apple-system, BlinkMacSystemFont, "Inter", sans-serif; -} - -@layer compound-legacy { - h1, - h2, - h3, - h4, - h5, - h6, - p, - a { - margin-top: 0; - } - - /* Headline Semi Bold */ - h1 { - font-weight: 600; - font-size: var(--font-size-headline); - } - - /* Title */ - h2 { - font-weight: 600; - font-size: var(--font-size-title); - } - - /* Subtitle */ - h3 { - font-weight: 600; - font-size: var(--font-size-subtitle); - } - - /* Body Semi Bold */ - h4 { - font-weight: 600; - font-size: var(--font-size-body); - } - - h1, - h2, - h3 { - line-height: 1.2; - } - - /* Body */ - p { - font-size: var(--font-size-body); - line-height: var(--font-size-title); - } - - hr { - width: calc(100% - 24px); - border: none; - border-top: 1px solid var(--cpd-color-border-interactive-secondary); - color: var(--cpd-color-border-interactive-secondary); - overflow: visible; - text-align: center; - height: 5px; - font-weight: 600; - font-size: var(--font-size-body); - line-height: 24px; - margin: 0 12px; - } - - summary { - font-size: var(--font-size-body); - } - - details > :not(summary) { - margin-left: var(--font-size-body); - } - - details[open] > summary { - margin-bottom: var(--font-size-body); - } -} - #root > [data-overlay-container] { position: relative; height: 100%; } - -/* normalize.css sets the focus rings on buttons in Firefox to an unusual custom -outline, which is inconsistent with our other components and is not sufficiently -visible to be accessible. This resets it back to 'auto'. */ -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: auto; -}