@aglyn/shared-ui-theme 1.0.0-beta.230 → 1.0.0-beta.231
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/lib/console.theme.js +8 -2
- package/src/lib/console.theme.js.map +1 -1
- package/src/lib/constants.d.ts +2 -0
- package/src/lib/constants.js +1 -0
- package/src/lib/constants.js.map +1 -1
- package/src/lib/platform-brand.d.ts +25 -0
- package/src/lib/platform-brand.js +40 -0
- package/src/lib/platform-brand.js.map +1 -0
- package/src/lib/site-base-fonts.d.ts +54 -0
- package/src/lib/site-base-fonts.js +125 -0
- package/src/lib/site-base-fonts.js.map +1 -0
- package/src/lib/tenant.theme.d.ts +1 -2
- package/src/lib/tenant.theme.js +8 -30
- package/src/lib/tenant.theme.js.map +1 -1
- package/src/lib/util/font-stack.d.ts +44 -0
- package/src/lib/util/font-stack.js +79 -0
- package/src/lib/util/font-stack.js.map +1 -0
- package/src/lib/util/roboto-flex.d.ts +65 -0
- package/src/lib/util/roboto-flex.js +115 -0
- package/src/lib/util/roboto-flex.js.map +1 -0
- package/src/lib/util/self-hosted-fonts.d.ts +111 -1
- package/src/lib/util/self-hosted-fonts.js +349 -2
- package/src/lib/util/self-hosted-fonts.js.map +1 -1
- package/src/lib/util/theme-editor-fields.d.ts +53 -2
- package/src/lib/util/theme-editor-fields.js +171 -20
- package/src/lib/util/theme-editor-fields.js.map +1 -1
package/src/lib/tenant.theme.js
CHANGED
|
@@ -14,13 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
-
var // Dot notation, not brackets: Next substitutes `process.env.NAME`
|
|
18
|
-
// TEXTUALLY, and never the bracket form, so a bracket read is `undefined`
|
|
19
|
-
// in any browser or edge bundle and silently falls back to the default
|
|
20
|
-
// below — which on a self-host install would hand the operator Aglyn's
|
|
21
|
-
// hosts (AGL-2037).
|
|
22
|
-
_process_env_NEXT_PUBLIC_PLATFORM_BRAND_HOSTS;
|
|
23
17
|
import { consoleOptions, consoleOptionsDark, consoleThemeDark, consoleThemeLight } from "./console.theme.js";
|
|
18
|
+
import { TENANT_SYSTEM_FONT_STACK, wearsPlatformBrand } from "./platform-brand.js";
|
|
24
19
|
import createResponsiveTheme from "./util/create-responsive-theme.js";
|
|
25
20
|
/**
|
|
26
21
|
* The default palette a tenant site wears when it has authored none of its
|
|
@@ -199,12 +194,18 @@ const tenantColorScheme = {
|
|
|
199
194
|
}
|
|
200
195
|
}
|
|
201
196
|
};
|
|
197
|
+
// A site serves no Roboto Flex, so its default stack names none (AGL-3656).
|
|
198
|
+
const tenantTypography = (base)=>_extends({}, base.typography, {
|
|
199
|
+
fontFamily: TENANT_SYSTEM_FONT_STACK
|
|
200
|
+
});
|
|
202
201
|
export const tenantOptions = _extends({}, consoleOptions, {
|
|
202
|
+
typography: tenantTypography(consoleOptions),
|
|
203
203
|
palette: _extends({
|
|
204
204
|
mode: 'light'
|
|
205
205
|
}, tenantColorScheme.light)
|
|
206
206
|
});
|
|
207
207
|
export const tenantOptionsDark = _extends({}, consoleOptionsDark, {
|
|
208
|
+
typography: tenantTypography(consoleOptionsDark),
|
|
208
209
|
palette: _extends({
|
|
209
210
|
mode: 'dark'
|
|
210
211
|
}, tenantColorScheme.dark)
|
|
@@ -215,30 +216,7 @@ export const tenantThemeLight = createResponsiveTheme({
|
|
|
215
216
|
export const tenantThemeDark = createResponsiveTheme({
|
|
216
217
|
themeOptions: _extends({}, tenantOptionsDark)
|
|
217
218
|
});
|
|
218
|
-
|
|
219
|
-
* Hosts whose brand IS the operator's own brand, and which therefore keep
|
|
220
|
-
* `consoleOptions` rather than the tenant default.
|
|
221
|
-
*
|
|
222
|
-
* Comma-separated in `NEXT_PUBLIC_PLATFORM_BRAND_HOSTS`, so a self-host
|
|
223
|
-
* operator points it at their own marketing domain and their customers still
|
|
224
|
-
* resolve the neutral tenant palette. The literal is the `??` default and
|
|
225
|
-
* nothing else reads it: the platform's own deployment needs no variable to
|
|
226
|
-
* keep its brand. Setting the variable to an empty string puts every host,
|
|
227
|
-
* including the operator's own, on the tenant default.
|
|
228
|
-
*
|
|
229
|
-
* Matched on the registrable domain, after stripping the `cname--` prefix the
|
|
230
|
-
* tenant middleware puts on a CUSTOM DOMAIN before it becomes the `[host]`
|
|
231
|
-
* route segment: the param this is handed reads `cname--example.com`, never
|
|
232
|
-
* the bare apex. A platform subdomain resolves to a bare label instead
|
|
233
|
-
* (`acme` for `acme.aglyn.app`), which correctly matches nothing — a customer
|
|
234
|
-
* on a platform subdomain is still a tenant.
|
|
235
|
-
*/ const CNAME_PREFIX = 'cname--';
|
|
236
|
-
export const PLATFORM_BRAND_HOSTS = new Set(((_process_env_NEXT_PUBLIC_PLATFORM_BRAND_HOSTS = process.env.NEXT_PUBLIC_PLATFORM_BRAND_HOSTS) != null ? _process_env_NEXT_PUBLIC_PLATFORM_BRAND_HOSTS : 'aglyn.com,aglyn.io').split(',').map((host)=>host.trim().toLowerCase()).filter(Boolean));
|
|
237
|
-
export function wearsPlatformBrand(host) {
|
|
238
|
-
if (!host) return false;
|
|
239
|
-
const normalized = host.trim().toLowerCase();
|
|
240
|
-
return PLATFORM_BRAND_HOSTS.has(normalized.startsWith(CNAME_PREFIX) ? normalized.slice(CNAME_PREFIX.length) : normalized);
|
|
241
|
-
}
|
|
219
|
+
export { PLATFORM_BRAND_HOSTS, wearsPlatformBrand } from "./platform-brand.js";
|
|
242
220
|
/**
|
|
243
221
|
* The site key {@link wearsPlatformBrand} is asked about, read off a host
|
|
244
222
|
* document (AGL-3068).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/shared/ui/theme/src/lib/tenant.theme.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { PaletteOptions, Theme, ThemeOptions } from '../vendor/mui'\nimport {\n consoleOptions,\n consoleOptionsDark,\n consoleThemeDark,\n consoleThemeLight,\n} from './console.theme'\nimport createResponsiveTheme from './util/create-responsive-theme'\n\n/**\n * The default palette a tenant site wears when it has authored none of its\n * own.\n *\n * MUI's stock accents, extended with every slot this platform adds on top of\n * a MUI palette — `tertiary`, `surface`, `tint`, `inputOutline` and the three\n * `svg*` records — so a site that never opens the theme editor still resolves\n * every token a component can ask for.\n *\n * Stock rather than the Aglyn brand because the brand palette carries a\n * signed-off sub-AA pairing (white on `#00b0ff`, 2.43:1): a considered\n * decision about OUR brand, and not one to hand a customer who never made it.\n * Every accent here clears the 4.5:1 AA text bar as text and carries its own\n * `contrastText` at AA on the fill, in both schemes.\n *\n * Non-palette options come from `consoleOptions`, so component behaviour,\n * type ramp, spacing and shadows are identical platform-wide — the palette is\n * the only thing a tenant default changes.\n */\n/**\n * The one grey ramp, read from the brand palette rather than copied. A\n * neutral scale is not a brand decision, and a second copy is a second thing\n * to drift.\n */\nconst grey = (consoleOptions.palette as { grey: PaletteOptions['grey'] }).grey\n\nconst tenantColorScheme = {\n light: {\n primary: {\n main: '#1976d2',\n // The accent-as-text shade (`ACCENT_TEXT_SHADE`): 7.50:1 on the page,\n // 7.82:1 on paper. Authored for the same reason as the console's — the\n // slot every link resolves to belongs in the palette, not in a walk.\n dark: '#125393',\n contrastText: '#FFFFFF',\n },\n secondary: {\n main: '#9c27b0',\n dark: '#6d1b7b',\n contrastText: '#FFFFFF',\n },\n // A neutral slate for the third accent, matching the role `tertiary`\n // plays in the console palette.\n tertiary: {\n main: '#4a5568',\n dark: '#343b49',\n contrastText: '#FFFFFF',\n },\n surface: {\n main: '#f1f3f5',\n contrastText: '#000000DE',\n },\n tint: {\n primary: '#e8f1fb',\n secondary: '#f5e9f7',\n tertiary: '#eef0f3',\n },\n inputOutline: 'rgba(0, 0, 0, 0.23)',\n background: {\n default: '#fafafa',\n paper: '#ffffff',\n },\n info: {\n main: '#0288d1',\n dark: '#015f92',\n contrastText: '#000000DE',\n },\n error: {\n main: '#d32f2f',\n dark: '#942121',\n contrastText: '#FFFFFF',\n },\n success: {\n main: '#2e7d32',\n dark: '#205823',\n contrastText: '#FFFFFF',\n },\n warning: {\n main: '#ed6c02',\n dark: '#a64c01',\n contrastText: '#000000DE',\n },\n grey,\n svgBackground: {\n main: '#FAFAFA',\n hover: '#FAFAFA',\n active: '#FAFAFA',\n focus: '#FAFAFA',\n },\n svgFilled: {\n main: '#9E9E9E',\n hover: '#1976d2',\n active: '#1976d2',\n focus: '#1976d2',\n },\n svgStroke: {\n main: '#FFFFFF',\n hover: '#FFFFFF',\n active: '#FFFFFF',\n focus: '#FFFFFF',\n },\n },\n dark: {\n primary: {\n main: '#1976d2',\n // LIGHTER than `main`, as every accent-as-text shade must be on a dark\n // ground: 6.70:1 on the page, 5.96:1 on paper. MUI derives `dark` by\n // darkening in both schemes, which points the wrong way here.\n dark: '#5e9fe0',\n contrastText: '#FFFFFF',\n },\n secondary: {\n main: '#9c27b0',\n dark: '#ba68c8',\n contrastText: '#FFFFFF',\n },\n // Lifted from the light scheme's slate: `#4a5568` is 1.9:1 against this\n // page and would be a third accent nobody can read.\n tertiary: {\n main: '#94a3b8',\n dark: '#b4bfcd',\n contrastText: '#000000DE',\n },\n surface: {\n main: '#242c35',\n contrastText: '#FFFFFF',\n },\n // Tile fills, held around 1.5:1 from the page so a tinted tile has a\n // visible edge without a border.\n tint: {\n primary: '#1e3a5c',\n secondary: '#472653',\n tertiary: '#2e3742',\n },\n inputOutline: 'rgba(255, 255, 255, 0.23)',\n background: {\n default: '#121212',\n paper: '#1e1e1e',\n },\n info: {\n main: '#0288d1',\n dark: '#4eacdf',\n contrastText: '#000000DE',\n },\n error: {\n main: '#d32f2f',\n dark: '#e06d6d',\n contrastText: '#FFFFFF',\n },\n success: {\n main: '#2e7d32',\n dark: '#6da470',\n contrastText: '#FFFFFF',\n },\n warning: {\n main: '#ed6c02',\n dark: '#f2984e',\n contrastText: '#000000DE',\n },\n grey,\n svgBackground: {\n main: '#242c35',\n hover: '#242c35',\n active: '#242c35',\n focus: '#242c35',\n },\n svgFilled: {\n main: '#9E9E9E',\n hover: '#5e9fe0',\n active: '#5e9fe0',\n focus: '#5e9fe0',\n },\n svgStroke: {\n main: '#FFFFFF',\n hover: '#FFFFFF',\n active: '#FFFFFF',\n focus: '#FFFFFF',\n },\n },\n}\n\nexport const tenantOptions: ThemeOptions = {\n ...consoleOptions,\n palette: {\n mode: 'light',\n ...tenantColorScheme.light,\n },\n}\nexport const tenantOptionsDark: ThemeOptions = {\n ...consoleOptionsDark,\n palette: {\n mode: 'dark',\n ...tenantColorScheme.dark,\n },\n}\n\nexport const tenantThemeLight: Theme = createResponsiveTheme({\n themeOptions: { ...tenantOptions },\n})\nexport const tenantThemeDark: Theme = createResponsiveTheme({\n themeOptions: { ...tenantOptionsDark },\n})\n\n/**\n * Hosts whose brand IS the operator's own brand, and which therefore keep\n * `consoleOptions` rather than the tenant default.\n *\n * Comma-separated in `NEXT_PUBLIC_PLATFORM_BRAND_HOSTS`, so a self-host\n * operator points it at their own marketing domain and their customers still\n * resolve the neutral tenant palette. The literal is the `??` default and\n * nothing else reads it: the platform's own deployment needs no variable to\n * keep its brand. Setting the variable to an empty string puts every host,\n * including the operator's own, on the tenant default.\n *\n * Matched on the registrable domain, after stripping the `cname--` prefix the\n * tenant middleware puts on a CUSTOM DOMAIN before it becomes the `[host]`\n * route segment: the param this is handed reads `cname--example.com`, never\n * the bare apex. A platform subdomain resolves to a bare label instead\n * (`acme` for `acme.aglyn.app`), which correctly matches nothing — a customer\n * on a platform subdomain is still a tenant.\n */\nconst CNAME_PREFIX = 'cname--'\n\nexport const PLATFORM_BRAND_HOSTS: ReadonlySet<string> = new Set(\n // Dot notation, not brackets: Next substitutes `process.env.NAME`\n // TEXTUALLY, and never the bracket form, so a bracket read is `undefined`\n // in any browser or edge bundle and silently falls back to the default\n // below — which on a self-host install would hand the operator Aglyn's\n // hosts (AGL-2037).\n (process.env.NEXT_PUBLIC_PLATFORM_BRAND_HOSTS ?? 'aglyn.com,aglyn.io')\n .split(',')\n .map((host) => host.trim().toLowerCase())\n .filter(Boolean),\n)\n\nexport function wearsPlatformBrand(host: string | undefined): boolean {\n if (!host) return false\n const normalized = host.trim().toLowerCase()\n return PLATFORM_BRAND_HOSTS.has(\n normalized.startsWith(CNAME_PREFIX)\n ? normalized.slice(CNAME_PREFIX.length)\n : normalized,\n )\n}\n\n/**\n * The site key {@link wearsPlatformBrand} is asked about, read off a host\n * document (AGL-3068).\n *\n * The tenant is handed its `[host]` route param, which is the attached domain\n * under the middleware's `cname--` sentinel or a bare subdomain label. A\n * console surface has the document instead, where those are two fields: the\n * attached domain is what a platform-brand host is listed under, and the\n * subdomain is what every other site is reachable by. Reading them in that\n * order is what makes the editor ask the same question the published page\n * answers.\n */\nexport function hostBrandKey(\n host: { cname?: string; subdomain?: string } | null | undefined,\n): string | undefined {\n return host?.cname || host?.subdomain || undefined\n}\n\n/**\n * The options a site's own theme is layered onto: the platform's brand for\n * the operator's own hosts, the neutral tenant default for everyone else\n * (AGL-3068).\n *\n * One decision, because it used to be four. The tenant made it per host,\n * while the besigner canvas, the theme editor's preview and the \"Default ·\n * #…\" the editor prints beside every unset slot all built on the brand base\n * unconditionally — so a customer site was authored in Aglyn's cyan and\n * published in MUI's blue, and an author correcting contrast was correcting a\n * page that does not exist.\n *\n * An unknown host is NOT a platform host, exactly as `wearsPlatformBrand`\n * already reads it: the operator's hosts are the named exception, so a\n * surface that cannot name the site resolves what a customer site resolves.\n */\nexport function siteBaseOptions(\n host: string | undefined,\n scheme: 'light' | 'dark',\n): ThemeOptions {\n const brand = wearsPlatformBrand(host)\n if (scheme === 'dark') return brand ? consoleOptionsDark : tenantOptionsDark\n return brand ? consoleOptions : tenantOptions\n}\n\n/** The built theme a site with no customization of its own renders under. */\nexport function siteFallbackTheme(\n host: string | undefined,\n scheme: 'light' | 'dark',\n): Theme {\n const brand = wearsPlatformBrand(host)\n if (scheme === 'dark') return brand ? consoleThemeDark : tenantThemeDark\n return brand ? consoleThemeLight : tenantThemeLight\n}\n"],"names":["process","consoleOptions","consoleOptionsDark","consoleThemeDark","consoleThemeLight","createResponsiveTheme","grey","palette","tenantColorScheme","light","primary","main","dark","contrastText","secondary","tertiary","surface","tint","inputOutline","background","default","paper","info","error","success","warning","svgBackground","hover","active","focus","svgFilled","svgStroke","tenantOptions","mode","tenantOptionsDark","tenantThemeLight","themeOptions","tenantThemeDark","CNAME_PREFIX","PLATFORM_BRAND_HOSTS","Set","env","NEXT_PUBLIC_PLATFORM_BRAND_HOSTS","split","map","host","trim","toLowerCase","filter","Boolean","wearsPlatformBrand","normalized","has","startsWith","slice","length","hostBrandKey","cname","subdomain","undefined","siteBaseOptions","scheme","brand","siteFallbackTheme"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/ui/theme/src/lib/tenant.theme.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { PaletteOptions, Theme, ThemeOptions } from '../vendor/mui'\nimport {\n consoleOptions,\n consoleOptionsDark,\n consoleThemeDark,\n consoleThemeLight,\n} from './console.theme'\nimport { TENANT_SYSTEM_FONT_STACK, wearsPlatformBrand } from './platform-brand'\nimport createResponsiveTheme from './util/create-responsive-theme'\n\n/**\n * The default palette a tenant site wears when it has authored none of its\n * own.\n *\n * MUI's stock accents, extended with every slot this platform adds on top of\n * a MUI palette — `tertiary`, `surface`, `tint`, `inputOutline` and the three\n * `svg*` records — so a site that never opens the theme editor still resolves\n * every token a component can ask for.\n *\n * Stock rather than the Aglyn brand because the brand palette carries a\n * signed-off sub-AA pairing (white on `#00b0ff`, 2.43:1): a considered\n * decision about OUR brand, and not one to hand a customer who never made it.\n * Every accent here clears the 4.5:1 AA text bar as text and carries its own\n * `contrastText` at AA on the fill, in both schemes.\n *\n * Non-palette options come from `consoleOptions`, so component behaviour,\n * type ramp, spacing and shadows are identical platform-wide — the palette is\n * the only thing a tenant default changes.\n */\n/**\n * The one grey ramp, read from the brand palette rather than copied. A\n * neutral scale is not a brand decision, and a second copy is a second thing\n * to drift.\n */\nconst grey = (consoleOptions.palette as { grey: PaletteOptions['grey'] }).grey\n\nconst tenantColorScheme = {\n light: {\n primary: {\n main: '#1976d2',\n // The accent-as-text shade (`ACCENT_TEXT_SHADE`): 7.50:1 on the page,\n // 7.82:1 on paper. Authored for the same reason as the console's — the\n // slot every link resolves to belongs in the palette, not in a walk.\n dark: '#125393',\n contrastText: '#FFFFFF',\n },\n secondary: {\n main: '#9c27b0',\n dark: '#6d1b7b',\n contrastText: '#FFFFFF',\n },\n // A neutral slate for the third accent, matching the role `tertiary`\n // plays in the console palette.\n tertiary: {\n main: '#4a5568',\n dark: '#343b49',\n contrastText: '#FFFFFF',\n },\n surface: {\n main: '#f1f3f5',\n contrastText: '#000000DE',\n },\n tint: {\n primary: '#e8f1fb',\n secondary: '#f5e9f7',\n tertiary: '#eef0f3',\n },\n inputOutline: 'rgba(0, 0, 0, 0.23)',\n background: {\n default: '#fafafa',\n paper: '#ffffff',\n },\n info: {\n main: '#0288d1',\n dark: '#015f92',\n contrastText: '#000000DE',\n },\n error: {\n main: '#d32f2f',\n dark: '#942121',\n contrastText: '#FFFFFF',\n },\n success: {\n main: '#2e7d32',\n dark: '#205823',\n contrastText: '#FFFFFF',\n },\n warning: {\n main: '#ed6c02',\n dark: '#a64c01',\n contrastText: '#000000DE',\n },\n grey,\n svgBackground: {\n main: '#FAFAFA',\n hover: '#FAFAFA',\n active: '#FAFAFA',\n focus: '#FAFAFA',\n },\n svgFilled: {\n main: '#9E9E9E',\n hover: '#1976d2',\n active: '#1976d2',\n focus: '#1976d2',\n },\n svgStroke: {\n main: '#FFFFFF',\n hover: '#FFFFFF',\n active: '#FFFFFF',\n focus: '#FFFFFF',\n },\n },\n dark: {\n primary: {\n main: '#1976d2',\n // LIGHTER than `main`, as every accent-as-text shade must be on a dark\n // ground: 6.70:1 on the page, 5.96:1 on paper. MUI derives `dark` by\n // darkening in both schemes, which points the wrong way here.\n dark: '#5e9fe0',\n contrastText: '#FFFFFF',\n },\n secondary: {\n main: '#9c27b0',\n dark: '#ba68c8',\n contrastText: '#FFFFFF',\n },\n // Lifted from the light scheme's slate: `#4a5568` is 1.9:1 against this\n // page and would be a third accent nobody can read.\n tertiary: {\n main: '#94a3b8',\n dark: '#b4bfcd',\n contrastText: '#000000DE',\n },\n surface: {\n main: '#242c35',\n contrastText: '#FFFFFF',\n },\n // Tile fills, held around 1.5:1 from the page so a tinted tile has a\n // visible edge without a border.\n tint: {\n primary: '#1e3a5c',\n secondary: '#472653',\n tertiary: '#2e3742',\n },\n inputOutline: 'rgba(255, 255, 255, 0.23)',\n background: {\n default: '#121212',\n paper: '#1e1e1e',\n },\n info: {\n main: '#0288d1',\n dark: '#4eacdf',\n contrastText: '#000000DE',\n },\n error: {\n main: '#d32f2f',\n dark: '#e06d6d',\n contrastText: '#FFFFFF',\n },\n success: {\n main: '#2e7d32',\n dark: '#6da470',\n contrastText: '#FFFFFF',\n },\n warning: {\n main: '#ed6c02',\n dark: '#f2984e',\n contrastText: '#000000DE',\n },\n grey,\n svgBackground: {\n main: '#242c35',\n hover: '#242c35',\n active: '#242c35',\n focus: '#242c35',\n },\n svgFilled: {\n main: '#9E9E9E',\n hover: '#5e9fe0',\n active: '#5e9fe0',\n focus: '#5e9fe0',\n },\n svgStroke: {\n main: '#FFFFFF',\n hover: '#FFFFFF',\n active: '#FFFFFF',\n focus: '#FFFFFF',\n },\n },\n}\n\n// A site serves no Roboto Flex, so its default stack names none (AGL-3656).\nconst tenantTypography = (base: ThemeOptions): ThemeOptions['typography'] => ({\n ...(base.typography as object),\n fontFamily: TENANT_SYSTEM_FONT_STACK,\n})\n\nexport const tenantOptions: ThemeOptions = {\n ...consoleOptions,\n typography: tenantTypography(consoleOptions),\n palette: {\n mode: 'light',\n ...tenantColorScheme.light,\n },\n}\nexport const tenantOptionsDark: ThemeOptions = {\n ...consoleOptionsDark,\n typography: tenantTypography(consoleOptionsDark),\n palette: {\n mode: 'dark',\n ...tenantColorScheme.dark,\n },\n}\n\nexport const tenantThemeLight: Theme = createResponsiveTheme({\n themeOptions: { ...tenantOptions },\n})\nexport const tenantThemeDark: Theme = createResponsiveTheme({\n themeOptions: { ...tenantOptionsDark },\n})\n\nexport { PLATFORM_BRAND_HOSTS, wearsPlatformBrand } from './platform-brand'\n\n/**\n * The site key {@link wearsPlatformBrand} is asked about, read off a host\n * document (AGL-3068).\n *\n * The tenant is handed its `[host]` route param, which is the attached domain\n * under the middleware's `cname--` sentinel or a bare subdomain label. A\n * console surface has the document instead, where those are two fields: the\n * attached domain is what a platform-brand host is listed under, and the\n * subdomain is what every other site is reachable by. Reading them in that\n * order is what makes the editor ask the same question the published page\n * answers.\n */\nexport function hostBrandKey(\n host: { cname?: string; subdomain?: string } | null | undefined,\n): string | undefined {\n return host?.cname || host?.subdomain || undefined\n}\n\n/**\n * The options a site's own theme is layered onto: the platform's brand for\n * the operator's own hosts, the neutral tenant default for everyone else\n * (AGL-3068).\n *\n * One decision, because it used to be four. The tenant made it per host,\n * while the besigner canvas, the theme editor's preview and the \"Default ·\n * #…\" the editor prints beside every unset slot all built on the brand base\n * unconditionally — so a customer site was authored in Aglyn's cyan and\n * published in MUI's blue, and an author correcting contrast was correcting a\n * page that does not exist.\n *\n * An unknown host is NOT a platform host, exactly as `wearsPlatformBrand`\n * already reads it: the operator's hosts are the named exception, so a\n * surface that cannot name the site resolves what a customer site resolves.\n */\nexport function siteBaseOptions(\n host: string | undefined,\n scheme: 'light' | 'dark',\n): ThemeOptions {\n const brand = wearsPlatformBrand(host)\n if (scheme === 'dark') return brand ? consoleOptionsDark : tenantOptionsDark\n return brand ? consoleOptions : tenantOptions\n}\n\n/** The built theme a site with no customization of its own renders under. */\nexport function siteFallbackTheme(\n host: string | undefined,\n scheme: 'light' | 'dark',\n): Theme {\n const brand = wearsPlatformBrand(host)\n if (scheme === 'dark') return brand ? consoleThemeDark : tenantThemeDark\n return brand ? consoleThemeLight : tenantThemeLight\n}\n"],"names":["consoleOptions","consoleOptionsDark","consoleThemeDark","consoleThemeLight","TENANT_SYSTEM_FONT_STACK","wearsPlatformBrand","createResponsiveTheme","grey","palette","tenantColorScheme","light","primary","main","dark","contrastText","secondary","tertiary","surface","tint","inputOutline","background","default","paper","info","error","success","warning","svgBackground","hover","active","focus","svgFilled","svgStroke","tenantTypography","base","typography","fontFamily","tenantOptions","mode","tenantOptionsDark","tenantThemeLight","themeOptions","tenantThemeDark","PLATFORM_BRAND_HOSTS","hostBrandKey","host","cname","subdomain","undefined","siteBaseOptions","scheme","brand","siteFallbackTheme"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { HostThemeFont } from '@aglyn/shared-data-types';
|
|
18
|
+
/** The name a family's metric-matched fallback face is declared under. */
|
|
19
|
+
export declare function metricFallbackFamily(family: string): string;
|
|
20
|
+
/** The families in a CSS `font-family` stack, unquoted. */
|
|
21
|
+
export declare function stackFamilies(stack: string): string[];
|
|
22
|
+
/**
|
|
23
|
+
* A stack with each loaded family's fallback named right after it, once:
|
|
24
|
+
* `"Inter", system-ui` → `"Inter", "Inter Fallback", system-ui`. Where no
|
|
25
|
+
* page declares the fallback (the editor), the name matches nothing.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withMetricFallbacks(stack: string, families: readonly string[]): string;
|
|
28
|
+
/** The families a theme loads as web fonts: Google families and uploads. */
|
|
29
|
+
export declare function themeWebFontFamilies(fonts: readonly HostThemeFont[] | undefined): string[];
|
|
30
|
+
/**
|
|
31
|
+
* A theme with each web font's fallback named in its stacks, for the page
|
|
32
|
+
* that declares the fallbacks. `baseStack` and `baseFonts` stand in for a
|
|
33
|
+
* theme that names no face of its own but whose base loads one (the brand's
|
|
34
|
+
* on an operator host). Done on the server, so no browser bundle carries it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withThemeMetricFallbacks<T extends {
|
|
37
|
+
fonts?: readonly HostThemeFont[];
|
|
38
|
+
typography?: {
|
|
39
|
+
fontFamily?: string;
|
|
40
|
+
variants?: Record<string, {
|
|
41
|
+
fontFamily?: string;
|
|
42
|
+
} | undefined>;
|
|
43
|
+
};
|
|
44
|
+
}>(theme: T | undefined, baseStack?: string, baseFonts?: readonly HostThemeFont[]): T | undefined;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
// The browser half of the font loader (AGL-3656), kept apart from
|
|
18
|
+
// `self-hosted-fonts.ts` because every published page loads it.
|
|
19
|
+
/** The name a family's metric-matched fallback face is declared under. */ export function metricFallbackFamily(family) {
|
|
20
|
+
return `${family.replace(/['"\\\n\r]/g, '').trim()} Fallback`;
|
|
21
|
+
}
|
|
22
|
+
/** The families in a CSS `font-family` stack, unquoted. */ export function stackFamilies(stack) {
|
|
23
|
+
return stack.split(',').map((entry)=>entry.trim().replace(/^['"]|['"]$/g, '').trim()).filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A stack with each loaded family's fallback named right after it, once:
|
|
27
|
+
* `"Inter", system-ui` → `"Inter", "Inter Fallback", system-ui`. Where no
|
|
28
|
+
* page declares the fallback (the editor), the name matches nothing.
|
|
29
|
+
*/ export function withMetricFallbacks(stack, families) {
|
|
30
|
+
if (!families.length) return stack;
|
|
31
|
+
const loaded = new Set(families.map((family)=>family.toLowerCase()));
|
|
32
|
+
const present = new Set(stackFamilies(stack).map((family)=>family.toLowerCase()));
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const part of stack.split(',')){
|
|
35
|
+
out.push(part);
|
|
36
|
+
const name = part.trim().replace(/^['"]|['"]$/g, '').trim();
|
|
37
|
+
const fallback = metricFallbackFamily(name);
|
|
38
|
+
if (loaded.has(name.toLowerCase()) && !present.has(fallback.toLowerCase())) {
|
|
39
|
+
out.push(` "${fallback}"`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out.join(',');
|
|
43
|
+
}
|
|
44
|
+
/** The families a theme loads as web fonts: Google families and uploads. */ export function themeWebFontFamilies(fonts) {
|
|
45
|
+
return (fonts != null ? fonts : []).filter((font)=>{
|
|
46
|
+
var _font_source;
|
|
47
|
+
return font.family && ((_font_source = font.source) != null ? _font_source : 'google') !== 'system';
|
|
48
|
+
}).map((font)=>font.family.trim());
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A theme with each web font's fallback named in its stacks, for the page
|
|
52
|
+
* that declares the fallbacks. `baseStack` and `baseFonts` stand in for a
|
|
53
|
+
* theme that names no face of its own but whose base loads one (the brand's
|
|
54
|
+
* on an operator host). Done on the server, so no browser bundle carries it.
|
|
55
|
+
*/ export function withThemeMetricFallbacks(theme, baseStack, baseFonts = []) {
|
|
56
|
+
var _ref, _ref1;
|
|
57
|
+
const families = themeWebFontFamilies([
|
|
58
|
+
...(_ref = theme == null ? void 0 : theme.fonts) != null ? _ref : [],
|
|
59
|
+
...baseFonts
|
|
60
|
+
]);
|
|
61
|
+
if (!families.length) return theme;
|
|
62
|
+
const typography = theme == null ? void 0 : theme.typography;
|
|
63
|
+
const stack = (_ref1 = typography == null ? void 0 : typography.fontFamily) != null ? _ref1 : baseFonts.length ? baseStack : undefined;
|
|
64
|
+
const variants = (typography == null ? void 0 : typography.variants) ? Object.fromEntries(Object.entries(typography.variants).map(([key, variant])=>[
|
|
65
|
+
key,
|
|
66
|
+
(variant == null ? void 0 : variant.fontFamily) ? _extends({}, variant, {
|
|
67
|
+
fontFamily: withMetricFallbacks(variant.fontFamily, families)
|
|
68
|
+
}) : variant
|
|
69
|
+
])) : undefined;
|
|
70
|
+
return _extends({}, theme, {
|
|
71
|
+
typography: _extends({}, typography, stack ? {
|
|
72
|
+
fontFamily: withMetricFallbacks(stack, families)
|
|
73
|
+
} : {}, variants ? {
|
|
74
|
+
variants
|
|
75
|
+
} : {})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=font-stack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/theme/src/lib/util/font-stack.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostThemeFont } from '@aglyn/shared-data-types'\n\n// The browser half of the font loader (AGL-3656), kept apart from\n// `self-hosted-fonts.ts` because every published page loads it.\n\n/** The name a family's metric-matched fallback face is declared under. */\nexport function metricFallbackFamily(family: string): string {\n return `${family.replace(/['\"\\\\\\n\\r]/g, '').trim()} Fallback`\n}\n\n/** The families in a CSS `font-family` stack, unquoted. */\nexport function stackFamilies(stack: string): string[] {\n return stack\n .split(',')\n .map((entry) => entry.trim().replace(/^['\"]|['\"]$/g, '').trim())\n .filter(Boolean)\n}\n\n/**\n * A stack with each loaded family's fallback named right after it, once:\n * `\"Inter\", system-ui` → `\"Inter\", \"Inter Fallback\", system-ui`. Where no\n * page declares the fallback (the editor), the name matches nothing.\n */\nexport function withMetricFallbacks(stack: string, families: readonly string[]): string {\n if (!families.length) return stack\n const loaded = new Set(families.map((family) => family.toLowerCase()))\n const present = new Set(stackFamilies(stack).map((family) => family.toLowerCase()))\n const out: string[] = []\n for (const part of stack.split(',')) {\n out.push(part)\n const name = part.trim().replace(/^['\"]|['\"]$/g, '').trim()\n const fallback = metricFallbackFamily(name)\n if (loaded.has(name.toLowerCase()) && !present.has(fallback.toLowerCase())) {\n out.push(` \"${fallback}\"`)\n }\n }\n return out.join(',')\n}\n\n/** The families a theme loads as web fonts: Google families and uploads. */\nexport function themeWebFontFamilies(fonts: readonly HostThemeFont[] | undefined): string[] {\n return (fonts ?? [])\n .filter((font) => font.family && (font.source ?? 'google') !== 'system')\n .map((font) => font.family.trim())\n}\n\n/**\n * A theme with each web font's fallback named in its stacks, for the page\n * that declares the fallbacks. `baseStack` and `baseFonts` stand in for a\n * theme that names no face of its own but whose base loads one (the brand's\n * on an operator host). Done on the server, so no browser bundle carries it.\n */\nexport function withThemeMetricFallbacks<\n T extends {\n fonts?: readonly HostThemeFont[]\n typography?: { fontFamily?: string; variants?: Record<string, { fontFamily?: string } | undefined> }\n },\n>(theme: T | undefined, baseStack?: string, baseFonts: readonly HostThemeFont[] = []): T | undefined {\n const families = themeWebFontFamilies([...(theme?.fonts ?? []), ...baseFonts])\n if (!families.length) return theme\n const typography = theme?.typography\n const stack = typography?.fontFamily ?? (baseFonts.length ? baseStack : undefined)\n const variants = typography?.variants\n ? Object.fromEntries(\n Object.entries(typography.variants).map(([key, variant]) => [\n key,\n variant?.fontFamily\n ? { ...variant, fontFamily: withMetricFallbacks(variant.fontFamily, families) }\n : variant,\n ]),\n )\n : undefined\n return {\n ...(theme as T),\n typography: {\n ...typography,\n ...(stack ? { fontFamily: withMetricFallbacks(stack, families) } : {}),\n ...(variants ? { variants } : {}),\n },\n }\n}\n"],"names":["metricFallbackFamily","family","replace","trim","stackFamilies","stack","split","map","entry","filter","Boolean","withMetricFallbacks","families","length","loaded","Set","toLowerCase","present","out","part","push","name","fallback","has","join","themeWebFontFamilies","fonts","font","source","withThemeMetricFallbacks","theme","baseStack","baseFonts","typography","fontFamily","undefined","variants","Object","fromEntries","entries","key","variant"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAID,kEAAkE;AAClE,gEAAgE;AAEhE,wEAAwE,GACxE,OAAO,SAASA,qBAAqBC,MAAc;IACjD,OAAO,GAAGA,OAAOC,OAAO,CAAC,eAAe,IAAIC,IAAI,GAAG,SAAS,CAAC;AAC/D;AAEA,yDAAyD,GACzD,OAAO,SAASC,cAAcC,KAAa;IACzC,OAAOA,MACJC,KAAK,CAAC,KACNC,GAAG,CAAC,CAACC,QAAUA,MAAML,IAAI,GAAGD,OAAO,CAAC,gBAAgB,IAAIC,IAAI,IAC5DM,MAAM,CAACC;AACZ;AAEA;;;;CAIC,GACD,OAAO,SAASC,oBAAoBN,KAAa,EAAEO,QAA2B;IAC5E,IAAI,CAACA,SAASC,MAAM,EAAE,OAAOR;IAC7B,MAAMS,SAAS,IAAIC,IAAIH,SAASL,GAAG,CAAC,CAACN,SAAWA,OAAOe,WAAW;IAClE,MAAMC,UAAU,IAAIF,IAAIX,cAAcC,OAAOE,GAAG,CAAC,CAACN,SAAWA,OAAOe,WAAW;IAC/E,MAAME,MAAgB,EAAE;IACxB,KAAK,MAAMC,QAAQd,MAAMC,KAAK,CAAC,KAAM;QACnCY,IAAIE,IAAI,CAACD;QACT,MAAME,OAAOF,KAAKhB,IAAI,GAAGD,OAAO,CAAC,gBAAgB,IAAIC,IAAI;QACzD,MAAMmB,WAAWtB,qBAAqBqB;QACtC,IAAIP,OAAOS,GAAG,CAACF,KAAKL,WAAW,OAAO,CAACC,QAAQM,GAAG,CAACD,SAASN,WAAW,KAAK;YAC1EE,IAAIE,IAAI,CAAC,CAAC,EAAE,EAAEE,SAAS,CAAC,CAAC;QAC3B;IACF;IACA,OAAOJ,IAAIM,IAAI,CAAC;AAClB;AAEA,0EAA0E,GAC1E,OAAO,SAASC,qBAAqBC,KAA2C;IAC9E,OAAO,CAACA,gBAAAA,QAAS,EAAE,EAChBjB,MAAM,CAAC,CAACkB;YAAyBA;eAAhBA,KAAK1B,MAAM,IAAI,EAAC0B,eAAAA,KAAKC,MAAM,YAAXD,eAAe,cAAc;OAC9DpB,GAAG,CAAC,CAACoB,OAASA,KAAK1B,MAAM,CAACE,IAAI;AACnC;AAEA;;;;;CAKC,GACD,OAAO,SAAS0B,yBAKdC,KAAoB,EAAEC,SAAkB,EAAEC,YAAsC,EAAE;;IAClF,MAAMpB,WAAWa,qBAAqB;mBAAKK,yBAAAA,MAAOJ,KAAK,mBAAI,EAAE;WAAMM;KAAU;IAC7E,IAAI,CAACpB,SAASC,MAAM,EAAE,OAAOiB;IAC7B,MAAMG,aAAaH,yBAAAA,MAAOG,UAAU;IACpC,MAAM5B,iBAAQ4B,8BAAAA,WAAYC,UAAU,oBAAKF,UAAUnB,MAAM,GAAGkB,YAAYI;IACxE,MAAMC,WAAWH,CAAAA,8BAAAA,WAAYG,QAAQ,IACjCC,OAAOC,WAAW,CAChBD,OAAOE,OAAO,CAACN,WAAWG,QAAQ,EAAE7B,GAAG,CAAC,CAAC,CAACiC,KAAKC,QAAQ,GAAK;YAC1DD;YACAC,CAAAA,2BAAAA,QAASP,UAAU,IACf,aAAKO;gBAASP,YAAYvB,oBAAoB8B,QAAQP,UAAU,EAAEtB;iBAClE6B;SACL,KAEHN;IACJ,OAAO,aACDL;QACJG,YAAY,aACPA,YACC5B,QAAQ;YAAE6B,YAAYvB,oBAAoBN,OAAOO;QAAU,IAAI,CAAC,GAChEwB,WAAW;YAAEA;QAAS,IAAI,CAAC;;AAGrC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** The family the brand stack names. Quoted, as it appears in the stack. */
|
|
18
|
+
export declare const ROBOTO_FLEX_FAMILY: string;
|
|
19
|
+
/**
|
|
20
|
+
* A metric-matched local face that stands in while Roboto Flex loads.
|
|
21
|
+
*
|
|
22
|
+
* Arial, scaled and re-spaced so a line set in it occupies the space the
|
|
23
|
+
* same line takes in Roboto Flex: when the web font swaps in, nothing
|
|
24
|
+
* reflows. It must sit DIRECTLY after {@link ROBOTO_FLEX_FAMILY} in a stack
|
|
25
|
+
* to do that, ahead of the system faces.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ROBOTO_FLEX_FALLBACK_FAMILY = "\"Roboto Flex Fallback\"";
|
|
28
|
+
/** Where the files are served from on the console's origin. */
|
|
29
|
+
export declare const ROBOTO_FLEX_BASE_PATH = "/_static/fonts/roboto-flex";
|
|
30
|
+
export interface RobotoFlexFace {
|
|
31
|
+
subset: 'latin' | 'latin-ext';
|
|
32
|
+
file: string;
|
|
33
|
+
unicodeRange: string;
|
|
34
|
+
/** The one face a page preloads. */
|
|
35
|
+
preload: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** The cut files, one per `unicode-range` subset. */
|
|
38
|
+
export declare const ROBOTO_FLEX_FACES: readonly RobotoFlexFace[];
|
|
39
|
+
/**
|
|
40
|
+
* The fallback's overrides, measured rather than guessed.
|
|
41
|
+
*
|
|
42
|
+
* `size-adjust` is the ratio of the two faces' mean advance width over an
|
|
43
|
+
* English sample (Roboto Flex wght 400: 0.4384 em, Arial: 0.4418 em). The
|
|
44
|
+
* vertical overrides are Roboto Flex's own `hhea` metrics (ascender 1900,
|
|
45
|
+
* descender −500, line gap 0, on 2048 units) divided by that ratio, since
|
|
46
|
+
* `size-adjust` scales them too.
|
|
47
|
+
*/
|
|
48
|
+
export declare const ROBOTO_FLEX_FALLBACK_METRICS: {
|
|
49
|
+
readonly local: "Arial";
|
|
50
|
+
readonly sizeAdjust: "99.23%";
|
|
51
|
+
readonly ascentOverride: "93.49%";
|
|
52
|
+
readonly descentOverride: "24.60%";
|
|
53
|
+
readonly lineGapOverride: "0%";
|
|
54
|
+
};
|
|
55
|
+
/** The URL a face's file is served from below `basePath`. */
|
|
56
|
+
export declare function robotoFlexFontUrl(face: Pick<RobotoFlexFace, 'file'>, basePath?: string): string;
|
|
57
|
+
/** The URLs a page should preload: the Latin file and nothing else. */
|
|
58
|
+
export declare function robotoFlexPreloadUrls(basePath?: string): string[];
|
|
59
|
+
/**
|
|
60
|
+
* The `@font-face` rules: one per subset, plus the metric-matched fallback.
|
|
61
|
+
*
|
|
62
|
+
* `font-display: swap` so text paints at once in the fallback, which the
|
|
63
|
+
* overrides make the same size, and the brand face replaces it in place.
|
|
64
|
+
*/
|
|
65
|
+
export declare function robotoFlexFontFaceCss(basePath?: string): string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* Roboto Flex, the brand face, served from the app's own origin (AGL-3655).
|
|
18
|
+
*
|
|
19
|
+
* The brand stack names `"Roboto Flex"` first, and for a long time nothing
|
|
20
|
+
* declared it: only a machine with the family installed saw it. These are
|
|
21
|
+
* the `@font-face` rules that make the name resolve everywhere, built from
|
|
22
|
+
* one description so the rules, the preload and the files cannot disagree.
|
|
23
|
+
*
|
|
24
|
+
* The files are cut from the official OFL variable TTF (google/fonts
|
|
25
|
+
* `ofl/robotoflex`, v3.200), the same file the native apps bundle:
|
|
26
|
+
*
|
|
27
|
+
* - Only the `wght` axis is kept, at its full 100–1000 range, because the
|
|
28
|
+
* weight ramp is the only axis anything here asks for. `opsz`, `wdth`,
|
|
29
|
+
* `slnt` and `GRAD` are pinned at their defaults (14 / 100 / 0 / 0) and
|
|
30
|
+
* the parametric axes dropped. Keeping `opsz` as well cost 2.5× the
|
|
31
|
+
* bytes (84 KB against 33 KB for Latin) for automatic optical sizing on
|
|
32
|
+
* headings.
|
|
33
|
+
* - Split by `unicode-range` on Google's own subset boundaries, so a page
|
|
34
|
+
* in English downloads only the Latin file, and only the Latin file is
|
|
35
|
+
* preloaded.
|
|
36
|
+
*
|
|
37
|
+
* To recut them from the TTF (fontTools: `pip install fonttools brotli`):
|
|
38
|
+
*
|
|
39
|
+
* ```sh
|
|
40
|
+
* python3 -m fontTools.varLib.instancer RobotoFlex[…].ttf \
|
|
41
|
+
* opsz=14 GRAD=0 wdth=100 slnt=0 XOPQ=drop YOPQ=drop XTRA=drop \
|
|
42
|
+
* YTUC=drop YTLC=drop YTAS=drop YTDE=drop YTFI=drop -o wght.ttf
|
|
43
|
+
* python3 -m fontTools.subset wght.ttf --unicodes="<range>" \
|
|
44
|
+
* --flavor=woff2 --layout-features='*' --no-hinting --desubroutinize \
|
|
45
|
+
* --output-file=roboto-flex-v3200-<subset>.woff2
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* Pure: no React, no DOM. A published site that wants the face (AGL-3656)
|
|
49
|
+
* builds the same rules against its own base URL.
|
|
50
|
+
*
|
|
51
|
+
* Deep-import only (`@aglyn/shared-ui-theme/util/roboto-flex`), and kept out
|
|
52
|
+
* of the barrel: a published page has no use for these rules, and the
|
|
53
|
+
* barrel is in its module graph.
|
|
54
|
+
*/ import { FontFamily, ROBOTO_FLEX_FALLBACK } from "../constants.js";
|
|
55
|
+
/** The family the brand stack names. Quoted, as it appears in the stack. */ export const ROBOTO_FLEX_FAMILY = FontFamily.ROBOTO_FLEX;
|
|
56
|
+
/**
|
|
57
|
+
* A metric-matched local face that stands in while Roboto Flex loads.
|
|
58
|
+
*
|
|
59
|
+
* Arial, scaled and re-spaced so a line set in it occupies the space the
|
|
60
|
+
* same line takes in Roboto Flex: when the web font swaps in, nothing
|
|
61
|
+
* reflows. It must sit DIRECTLY after {@link ROBOTO_FLEX_FAMILY} in a stack
|
|
62
|
+
* to do that, ahead of the system faces.
|
|
63
|
+
*/ export const ROBOTO_FLEX_FALLBACK_FAMILY = ROBOTO_FLEX_FALLBACK;
|
|
64
|
+
/** Where the files are served from on the console's origin. */ export const ROBOTO_FLEX_BASE_PATH = '/_static/fonts/roboto-flex';
|
|
65
|
+
/** The cut files, one per `unicode-range` subset. */ export const ROBOTO_FLEX_FACES = [
|
|
66
|
+
{
|
|
67
|
+
subset: 'latin-ext',
|
|
68
|
+
file: 'roboto-flex-v3200-latin-ext.woff2',
|
|
69
|
+
unicodeRange: 'U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, ' + 'U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, ' + 'U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF',
|
|
70
|
+
preload: false
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
subset: 'latin',
|
|
74
|
+
file: 'roboto-flex-v3200-latin.woff2',
|
|
75
|
+
unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, ' + 'U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, ' + 'U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
|
|
76
|
+
preload: true
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* The fallback's overrides, measured rather than guessed.
|
|
81
|
+
*
|
|
82
|
+
* `size-adjust` is the ratio of the two faces' mean advance width over an
|
|
83
|
+
* English sample (Roboto Flex wght 400: 0.4384 em, Arial: 0.4418 em). The
|
|
84
|
+
* vertical overrides are Roboto Flex's own `hhea` metrics (ascender 1900,
|
|
85
|
+
* descender −500, line gap 0, on 2048 units) divided by that ratio, since
|
|
86
|
+
* `size-adjust` scales them too.
|
|
87
|
+
*/ export const ROBOTO_FLEX_FALLBACK_METRICS = {
|
|
88
|
+
local: 'Arial',
|
|
89
|
+
sizeAdjust: '99.23%',
|
|
90
|
+
ascentOverride: '93.49%',
|
|
91
|
+
descentOverride: '24.60%',
|
|
92
|
+
lineGapOverride: '0%'
|
|
93
|
+
};
|
|
94
|
+
/** The URL a face's file is served from below `basePath`. */ export function robotoFlexFontUrl(face, basePath = ROBOTO_FLEX_BASE_PATH) {
|
|
95
|
+
return `${basePath.replace(/\/+$/, '')}/${face.file}`;
|
|
96
|
+
}
|
|
97
|
+
/** The URLs a page should preload: the Latin file and nothing else. */ export function robotoFlexPreloadUrls(basePath = ROBOTO_FLEX_BASE_PATH) {
|
|
98
|
+
return ROBOTO_FLEX_FACES.filter((face)=>face.preload).map((face)=>robotoFlexFontUrl(face, basePath));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The `@font-face` rules: one per subset, plus the metric-matched fallback.
|
|
102
|
+
*
|
|
103
|
+
* `font-display: swap` so text paints at once in the fallback, which the
|
|
104
|
+
* overrides make the same size, and the brand face replaces it in place.
|
|
105
|
+
*/ export function robotoFlexFontFaceCss(basePath = ROBOTO_FLEX_BASE_PATH) {
|
|
106
|
+
const faces = ROBOTO_FLEX_FACES.map((face)=>`@font-face{font-family:${ROBOTO_FLEX_FAMILY};font-style:normal;` + `font-weight:100 1000;font-display:swap;` + `src:url(${robotoFlexFontUrl(face, basePath)}) format('woff2');` + `unicode-range:${face.unicodeRange}}`);
|
|
107
|
+
const m = ROBOTO_FLEX_FALLBACK_METRICS;
|
|
108
|
+
const fallback = `@font-face{font-family:${ROBOTO_FLEX_FALLBACK_FAMILY};` + `src:local('${m.local}');size-adjust:${m.sizeAdjust};` + `ascent-override:${m.ascentOverride};descent-override:${m.descentOverride};` + `line-gap-override:${m.lineGapOverride}}`;
|
|
109
|
+
return [
|
|
110
|
+
...faces,
|
|
111
|
+
fallback
|
|
112
|
+
].join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//# sourceMappingURL=roboto-flex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/theme/src/lib/util/roboto-flex.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Roboto Flex, the brand face, served from the app's own origin (AGL-3655).\n *\n * The brand stack names `\"Roboto Flex\"` first, and for a long time nothing\n * declared it: only a machine with the family installed saw it. These are\n * the `@font-face` rules that make the name resolve everywhere, built from\n * one description so the rules, the preload and the files cannot disagree.\n *\n * The files are cut from the official OFL variable TTF (google/fonts\n * `ofl/robotoflex`, v3.200), the same file the native apps bundle:\n *\n * - Only the `wght` axis is kept, at its full 100–1000 range, because the\n * weight ramp is the only axis anything here asks for. `opsz`, `wdth`,\n * `slnt` and `GRAD` are pinned at their defaults (14 / 100 / 0 / 0) and\n * the parametric axes dropped. Keeping `opsz` as well cost 2.5× the\n * bytes (84 KB against 33 KB for Latin) for automatic optical sizing on\n * headings.\n * - Split by `unicode-range` on Google's own subset boundaries, so a page\n * in English downloads only the Latin file, and only the Latin file is\n * preloaded.\n *\n * To recut them from the TTF (fontTools: `pip install fonttools brotli`):\n *\n * ```sh\n * python3 -m fontTools.varLib.instancer RobotoFlex[…].ttf \\\n * opsz=14 GRAD=0 wdth=100 slnt=0 XOPQ=drop YOPQ=drop XTRA=drop \\\n * YTUC=drop YTLC=drop YTAS=drop YTDE=drop YTFI=drop -o wght.ttf\n * python3 -m fontTools.subset wght.ttf --unicodes=\"<range>\" \\\n * --flavor=woff2 --layout-features='*' --no-hinting --desubroutinize \\\n * --output-file=roboto-flex-v3200-<subset>.woff2\n * ```\n *\n * Pure: no React, no DOM. A published site that wants the face (AGL-3656)\n * builds the same rules against its own base URL.\n *\n * Deep-import only (`@aglyn/shared-ui-theme/util/roboto-flex`), and kept out\n * of the barrel: a published page has no use for these rules, and the\n * barrel is in its module graph.\n */\n\nimport { FontFamily, ROBOTO_FLEX_FALLBACK } from '../constants'\n\n/** The family the brand stack names. Quoted, as it appears in the stack. */\nexport const ROBOTO_FLEX_FAMILY: string = FontFamily.ROBOTO_FLEX\n\n/**\n * A metric-matched local face that stands in while Roboto Flex loads.\n *\n * Arial, scaled and re-spaced so a line set in it occupies the space the\n * same line takes in Roboto Flex: when the web font swaps in, nothing\n * reflows. It must sit DIRECTLY after {@link ROBOTO_FLEX_FAMILY} in a stack\n * to do that, ahead of the system faces.\n */\nexport const ROBOTO_FLEX_FALLBACK_FAMILY = ROBOTO_FLEX_FALLBACK\n\n/** Where the files are served from on the console's origin. */\nexport const ROBOTO_FLEX_BASE_PATH = '/_static/fonts/roboto-flex'\n\nexport interface RobotoFlexFace {\n subset: 'latin' | 'latin-ext'\n file: string\n unicodeRange: string\n /** The one face a page preloads. */\n preload: boolean\n}\n\n/** The cut files, one per `unicode-range` subset. */\nexport const ROBOTO_FLEX_FACES: readonly RobotoFlexFace[] = [\n {\n subset: 'latin-ext',\n file: 'roboto-flex-v3200-latin-ext.woff2',\n unicodeRange:\n 'U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, ' +\n 'U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, ' +\n 'U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF',\n preload: false,\n },\n {\n subset: 'latin',\n file: 'roboto-flex-v3200-latin.woff2',\n unicodeRange:\n 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, ' +\n 'U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, ' +\n 'U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',\n preload: true,\n },\n]\n\n/**\n * The fallback's overrides, measured rather than guessed.\n *\n * `size-adjust` is the ratio of the two faces' mean advance width over an\n * English sample (Roboto Flex wght 400: 0.4384 em, Arial: 0.4418 em). The\n * vertical overrides are Roboto Flex's own `hhea` metrics (ascender 1900,\n * descender −500, line gap 0, on 2048 units) divided by that ratio, since\n * `size-adjust` scales them too.\n */\nexport const ROBOTO_FLEX_FALLBACK_METRICS = {\n local: 'Arial',\n sizeAdjust: '99.23%',\n ascentOverride: '93.49%',\n descentOverride: '24.60%',\n lineGapOverride: '0%',\n} as const\n\n/** The URL a face's file is served from below `basePath`. */\nexport function robotoFlexFontUrl(\n face: Pick<RobotoFlexFace, 'file'>,\n basePath: string = ROBOTO_FLEX_BASE_PATH,\n): string {\n return `${basePath.replace(/\\/+$/, '')}/${face.file}`\n}\n\n/** The URLs a page should preload: the Latin file and nothing else. */\nexport function robotoFlexPreloadUrls(\n basePath: string = ROBOTO_FLEX_BASE_PATH,\n): string[] {\n return ROBOTO_FLEX_FACES.filter((face) => face.preload).map((face) =>\n robotoFlexFontUrl(face, basePath),\n )\n}\n\n/**\n * The `@font-face` rules: one per subset, plus the metric-matched fallback.\n *\n * `font-display: swap` so text paints at once in the fallback, which the\n * overrides make the same size, and the brand face replaces it in place.\n */\nexport function robotoFlexFontFaceCss(\n basePath: string = ROBOTO_FLEX_BASE_PATH,\n): string {\n const faces = ROBOTO_FLEX_FACES.map(\n (face) =>\n `@font-face{font-family:${ROBOTO_FLEX_FAMILY};font-style:normal;` +\n `font-weight:100 1000;font-display:swap;` +\n `src:url(${robotoFlexFontUrl(face, basePath)}) format('woff2');` +\n `unicode-range:${face.unicodeRange}}`,\n )\n const m = ROBOTO_FLEX_FALLBACK_METRICS\n const fallback =\n `@font-face{font-family:${ROBOTO_FLEX_FALLBACK_FAMILY};` +\n `src:local('${m.local}');size-adjust:${m.sizeAdjust};` +\n `ascent-override:${m.ascentOverride};descent-override:${m.descentOverride};` +\n `line-gap-override:${m.lineGapOverride}}`\n return [...faces, fallback].join('\\n')\n}\n"],"names":["FontFamily","ROBOTO_FLEX_FALLBACK","ROBOTO_FLEX_FAMILY","ROBOTO_FLEX","ROBOTO_FLEX_FALLBACK_FAMILY","ROBOTO_FLEX_BASE_PATH","ROBOTO_FLEX_FACES","subset","file","unicodeRange","preload","ROBOTO_FLEX_FALLBACK_METRICS","local","sizeAdjust","ascentOverride","descentOverride","lineGapOverride","robotoFlexFontUrl","face","basePath","replace","robotoFlexPreloadUrls","filter","map","robotoFlexFontFaceCss","faces","m","fallback","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCC,GAED,SAASA,UAAU,EAAEC,oBAAoB,QAAQ,kBAAc;AAE/D,0EAA0E,GAC1E,OAAO,MAAMC,qBAA6BF,WAAWG,WAAW,CAAA;AAEhE;;;;;;;CAOC,GACD,OAAO,MAAMC,8BAA8BH,qBAAoB;AAE/D,6DAA6D,GAC7D,OAAO,MAAMI,wBAAwB,6BAA4B;AAUjE,mDAAmD,GACnD,OAAO,MAAMC,oBAA+C;IAC1D;QACEC,QAAQ;QACRC,MAAM;QACNC,cACE,sEACA,oEACA;QACFC,SAAS;IACX;IACA;QACEH,QAAQ;QACRC,MAAM;QACNC,cACE,oEACA,kEACA;QACFC,SAAS;IACX;CACD,CAAA;AAED;;;;;;;;CAQC,GACD,OAAO,MAAMC,+BAA+B;IAC1CC,OAAO;IACPC,YAAY;IACZC,gBAAgB;IAChBC,iBAAiB;IACjBC,iBAAiB;AACnB,EAAU;AAEV,2DAA2D,GAC3D,OAAO,SAASC,kBACdC,IAAkC,EAClCC,WAAmBd,qBAAqB;IAExC,OAAO,GAAGc,SAASC,OAAO,CAAC,QAAQ,IAAI,CAAC,EAAEF,KAAKV,IAAI,EAAE;AACvD;AAEA,qEAAqE,GACrE,OAAO,SAASa,sBACdF,WAAmBd,qBAAqB;IAExC,OAAOC,kBAAkBgB,MAAM,CAAC,CAACJ,OAASA,KAAKR,OAAO,EAAEa,GAAG,CAAC,CAACL,OAC3DD,kBAAkBC,MAAMC;AAE5B;AAEA;;;;;CAKC,GACD,OAAO,SAASK,sBACdL,WAAmBd,qBAAqB;IAExC,MAAMoB,QAAQnB,kBAAkBiB,GAAG,CACjC,CAACL,OACC,CAAC,uBAAuB,EAAEhB,mBAAmB,mBAAmB,CAAC,GACjE,CAAC,uCAAuC,CAAC,GACzC,CAAC,QAAQ,EAAEe,kBAAkBC,MAAMC,UAAU,kBAAkB,CAAC,GAChE,CAAC,cAAc,EAAED,KAAKT,YAAY,CAAC,CAAC,CAAC;IAEzC,MAAMiB,IAAIf;IACV,MAAMgB,WACJ,CAAC,uBAAuB,EAAEvB,4BAA4B,CAAC,CAAC,GACxD,CAAC,WAAW,EAAEsB,EAAEd,KAAK,CAAC,eAAe,EAAEc,EAAEb,UAAU,CAAC,CAAC,CAAC,GACtD,CAAC,gBAAgB,EAAEa,EAAEZ,cAAc,CAAC,kBAAkB,EAAEY,EAAEX,eAAe,CAAC,CAAC,CAAC,GAC5E,CAAC,kBAAkB,EAAEW,EAAEV,eAAe,CAAC,CAAC,CAAC;IAC3C,OAAO;WAAIS;QAAOE;KAAS,CAACC,IAAI,CAAC;AACnC"}
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type { HostTheme } from '@aglyn/shared-data-types';
|
|
17
|
+
import type { HostTheme, HostThemeFont, HostThemeFontCategory, HostThemeFontMetrics } from '@aglyn/shared-data-types';
|
|
18
|
+
export { metricFallbackFamily, themeWebFontFamilies, withMetricFallbacks } from './font-stack';
|
|
18
19
|
/**
|
|
19
20
|
* A theme's Google fonts, served from the site's own origin (AGL-3485).
|
|
20
21
|
*
|
|
@@ -94,3 +95,112 @@ export declare function selfHostedFontFaceCss(faces: readonly GoogleFontFace[]):
|
|
|
94
95
|
* from one file, which is then preloaded once.
|
|
95
96
|
*/
|
|
96
97
|
export declare function selfHostedFontPreloads(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined, faces: readonly GoogleFontFace[]): string[];
|
|
98
|
+
/**
|
|
99
|
+
* One face a page declares, wherever its file lives: a Google face rewritten
|
|
100
|
+
* onto {@link SELF_HOSTED_FONT_ROUTE}, or a site's own upload on its media
|
|
101
|
+
* route. Both are on the site's own origin.
|
|
102
|
+
*/
|
|
103
|
+
export interface SiteFontFace {
|
|
104
|
+
family: string;
|
|
105
|
+
style: 'normal' | 'italic';
|
|
106
|
+
/** A weight (`400`) or, for a variable file, a range (`300 900`). */
|
|
107
|
+
weight: string;
|
|
108
|
+
/** Site-relative URL of the WOFF2 file. */
|
|
109
|
+
url: string;
|
|
110
|
+
/** Google's subset name (`latin`, …); absent for a file with no split. */
|
|
111
|
+
subset?: string;
|
|
112
|
+
unicodeRange?: string;
|
|
113
|
+
}
|
|
114
|
+
/** A Google face as a {@link SiteFontFace} on the site's own route. */
|
|
115
|
+
export declare function siteFontFaceFromGoogle(face: GoogleFontFace): SiteFontFace;
|
|
116
|
+
/**
|
|
117
|
+
* The faces with every set of rules that share one file folded into one rule
|
|
118
|
+
* over the weights it covers.
|
|
119
|
+
*
|
|
120
|
+
* Google answers a request for several weights of a variable family with the
|
|
121
|
+
* SAME file once per weight, each rule naming one weight. A browser treats
|
|
122
|
+
* those as separate faces: it matches 500 to nothing between the 400 and 600
|
|
123
|
+
* rules a theme listed and renders it as 400, though the file holds every
|
|
124
|
+
* weight in between. One rule declaring the range (`400 700`) is what the
|
|
125
|
+
* file is.
|
|
126
|
+
*/
|
|
127
|
+
export declare function mergeSharedFiles<T extends {
|
|
128
|
+
url: string;
|
|
129
|
+
weight: string;
|
|
130
|
+
family: string;
|
|
131
|
+
style: string;
|
|
132
|
+
unicodeRange?: string;
|
|
133
|
+
}>(faces: readonly T[]): T[];
|
|
134
|
+
/** The rules for a page's faces, every one `font-display: swap`. */
|
|
135
|
+
export declare function siteFontFaceCss(faces: readonly SiteFontFace[]): string;
|
|
136
|
+
/**
|
|
137
|
+
* Whether a URL is one a rule may name: site-relative, on the font route or
|
|
138
|
+
* the media route, with nothing that could close the `url()` it is written
|
|
139
|
+
* into. A face whose URL fails is dropped rather than escaped.
|
|
140
|
+
*/
|
|
141
|
+
export declare function isSiteFontUrl(url: string): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* The `@font-face` rule for a family's metric-matched fallback: the local face
|
|
144
|
+
* of its category, scaled so its average character is as wide as the web
|
|
145
|
+
* font's (`size-adjust`) and given the web font's ascent, descent and line gap
|
|
146
|
+
* at that scale. Text set in it occupies the box the web font will, so the
|
|
147
|
+
* swap moves nothing — the CLS a `font-display: swap` page otherwise pays.
|
|
148
|
+
*
|
|
149
|
+
* The overrides are divided by the size adjustment because the browser
|
|
150
|
+
* applies them to the ADJUSTED size: an ascent of 0.97em on a face scaled to
|
|
151
|
+
* 1.07 has to be written as 0.97/1.07 to land at 0.97em.
|
|
152
|
+
*
|
|
153
|
+
* Empty for metrics that are missing or not numbers.
|
|
154
|
+
*/
|
|
155
|
+
export declare function metricFallbackFontFaceCss(family: string, metrics: HostThemeFontMetrics | undefined, category?: HostThemeFontCategory): string;
|
|
156
|
+
/** What one family of a theme needs loaded: the weights, and which as italics. */
|
|
157
|
+
export interface ThemeFontNeed {
|
|
158
|
+
family: string;
|
|
159
|
+
/** Upright weights the theme's text styles and named weights draw with. */
|
|
160
|
+
weights: number[];
|
|
161
|
+
/** The subset of `weights` text styles alone use, which a delivery choice is measured on. */
|
|
162
|
+
textWeights: number[];
|
|
163
|
+
italics: number[];
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The faces each of a theme's web font families is drawn with (AGL-3656).
|
|
167
|
+
*
|
|
168
|
+
* A theme lists weights for its font (`fonts[].weights`), but the page draws
|
|
169
|
+
* with whatever its text styles say: the platform's ramp sets `h1` in 900 and
|
|
170
|
+
* `h2` in 800, so a theme that loaded Inter at 400, 500 and 700 drew its
|
|
171
|
+
* headlines in a synthesized or substituted weight. The weights here are the
|
|
172
|
+
* union of the listed ones, every text style's weight whose family resolves to
|
|
173
|
+
* this one, and the theme's named weights (`fontWeightLight` … `Black`), which
|
|
174
|
+
* the weight picker offers content.
|
|
175
|
+
*
|
|
176
|
+
* `baseTypography` is the typography the site's theme is layered onto
|
|
177
|
+
* (`siteBaseOptions(...).typography`): a text style the theme leaves alone
|
|
178
|
+
* keeps the base's weight and family.
|
|
179
|
+
*
|
|
180
|
+
* One italic is loaded per family, at the body text's weight, so emphasis in
|
|
181
|
+
* running text is a real italic rather than a slanted upright; a theme can
|
|
182
|
+
* list more under `italics`.
|
|
183
|
+
*/
|
|
184
|
+
export declare function themeFontNeeds(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined, baseTypography: Record<string, unknown> | undefined, extraFonts?: readonly HostThemeFont[]): ThemeFontNeed[];
|
|
185
|
+
/**
|
|
186
|
+
* The weights to load from what a family offers: each wanted weight as the
|
|
187
|
+
* nearest one offered, without repeats. Unknown availability keeps the
|
|
188
|
+
* wanted weights as they are.
|
|
189
|
+
*/
|
|
190
|
+
export declare function offeredWeights(wanted: readonly number[], offered: readonly number[] | undefined): number[];
|
|
191
|
+
/**
|
|
192
|
+
* Google's CSS2 URL for some faces of one family: `ital,wght` tuples, sorted
|
|
193
|
+
* as the API requires. One weight asks for a static instance, which for a
|
|
194
|
+
* variable family is a file holding that weight alone — Inter's Latin 400 is
|
|
195
|
+
* 24 KB that way and 48 KB as the variable file.
|
|
196
|
+
*/
|
|
197
|
+
export declare function googleFontSheetUrl(family: string, faces: ReadonlyArray<{
|
|
198
|
+
weight: number;
|
|
199
|
+
style: 'normal' | 'italic';
|
|
200
|
+
}>): string | undefined;
|
|
201
|
+
/**
|
|
202
|
+
* The files the first screen paints with, to preload: the body text's face
|
|
203
|
+
* and the headline's, Latin and upright, from whatever faces the page
|
|
204
|
+
* declares. A file covering both is preloaded once.
|
|
205
|
+
*/
|
|
206
|
+
export declare function siteFontPreloads(theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined, faces: readonly SiteFontFace[], baseTypography?: Record<string, unknown>): string[];
|