@aglyn/shared-ui-theme 1.0.0-beta.230 → 1.0.0-beta.232

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.
Files changed (33) hide show
  1. package/package.json +4 -4
  2. package/src/lib/console-app.theme.d.ts +25 -0
  3. package/src/lib/console-app.theme.js +142 -0
  4. package/src/lib/console-app.theme.js.map +1 -0
  5. package/src/lib/console.theme.js +8 -2
  6. package/src/lib/console.theme.js.map +1 -1
  7. package/src/lib/constants.d.ts +2 -0
  8. package/src/lib/constants.js +1 -0
  9. package/src/lib/constants.js.map +1 -1
  10. package/src/lib/platform-brand.d.ts +25 -0
  11. package/src/lib/platform-brand.js +40 -0
  12. package/src/lib/platform-brand.js.map +1 -0
  13. package/src/lib/site-base-fonts.d.ts +54 -0
  14. package/src/lib/site-base-fonts.js +125 -0
  15. package/src/lib/site-base-fonts.js.map +1 -0
  16. package/src/lib/tenant.theme.d.ts +1 -2
  17. package/src/lib/tenant.theme.js +8 -30
  18. package/src/lib/tenant.theme.js.map +1 -1
  19. package/src/lib/util/font-stack.d.ts +44 -0
  20. package/src/lib/util/font-stack.js +79 -0
  21. package/src/lib/util/font-stack.js.map +1 -0
  22. package/src/lib/util/roboto-flex.d.ts +65 -0
  23. package/src/lib/util/roboto-flex.js +115 -0
  24. package/src/lib/util/roboto-flex.js.map +1 -0
  25. package/src/lib/util/self-hosted-fonts.d.ts +111 -1
  26. package/src/lib/util/self-hosted-fonts.js +349 -2
  27. package/src/lib/util/self-hosted-fonts.js.map +1 -1
  28. package/src/lib/util/theme-editor-edits.d.ts +105 -0
  29. package/src/lib/util/theme-editor-edits.js +168 -0
  30. package/src/lib/util/theme-editor-edits.js.map +1 -0
  31. package/src/lib/util/theme-editor-fields.d.ts +53 -2
  32. package/src/lib/util/theme-editor-fields.js +171 -20
  33. package/src/lib/util/theme-editor-fields.js.map +1 -1
@@ -0,0 +1,125 @@
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
+ import { buildFontFamilyList } from "./constants.js";
18
+ import { TENANT_SYSTEM_FONT_STACK, wearsPlatformBrand } from "./platform-brand.js";
19
+ /**
20
+ * What a site's base theme draws with, as far as its fonts go (AGL-3656): the
21
+ * face, and the weights of the platform's text styles. Read by the published
22
+ * page's server layout, so it holds no MUI; it is never in a browser bundle.
23
+ */ /** The platform brand's face. */ export const PLATFORM_BRAND_FONT_FAMILY = 'Roboto Flex';
24
+ /**
25
+ * The brand's face as a theme font: what an operator's own host loads when its
26
+ * theme names no font of its own. The brand stack has always named it first,
27
+ * and nothing loaded it, so aglyn.com drew in Roboto Flex on machines that
28
+ * have it installed and in the system font everywhere else.
29
+ */ export const PLATFORM_BRAND_FONT = {
30
+ family: PLATFORM_BRAND_FONT_FAMILY,
31
+ source: 'google',
32
+ category: 'sans-serif'
33
+ };
34
+ /**
35
+ * The weights the platform's text styles draw with — the brand's ramp over
36
+ * MUI's defaults — and its named weights, in the shape of MUI typography
37
+ * options. `platform-brand.spec.ts` holds it to the built console theme, so a
38
+ * change to the ramp there fails until it is made here too.
39
+ */ export const PLATFORM_TYPE_RAMP_WEIGHTS = {
40
+ fontWeightLight: 300,
41
+ fontWeightRegular: 400,
42
+ fontWeightMedium: 500,
43
+ fontWeightBold: 700,
44
+ fontWeightSemiBold: 600,
45
+ fontWeightExtraBold: 800,
46
+ fontWeightBlack: 900,
47
+ displayXl: {
48
+ fontWeight: 900
49
+ },
50
+ h1: {
51
+ fontWeight: 900
52
+ },
53
+ h2: {
54
+ fontWeight: 800
55
+ },
56
+ h3: {
57
+ fontWeight: 700
58
+ },
59
+ h4: {
60
+ fontWeight: 400
61
+ },
62
+ h5: {
63
+ fontWeight: 400
64
+ },
65
+ h6: {
66
+ fontWeight: 500
67
+ },
68
+ subtitle1: {
69
+ fontWeight: 400
70
+ },
71
+ subtitle2: {
72
+ fontWeight: 500
73
+ },
74
+ body1: {
75
+ fontWeight: 400
76
+ },
77
+ body2: {
78
+ fontWeight: 400
79
+ },
80
+ button: {
81
+ fontWeight: 500
82
+ },
83
+ caption: {
84
+ fontWeight: 400
85
+ },
86
+ overline: {
87
+ fontWeight: 400
88
+ },
89
+ lede: {
90
+ fontWeight: 400
91
+ },
92
+ bodyCompact: {
93
+ fontWeight: 400
94
+ },
95
+ micro: {
96
+ fontWeight: 400
97
+ }
98
+ };
99
+ /**
100
+ * The typography a site's theme is layered onto, as far as fonts go: the
101
+ * brand stack on an operator host, the system stack on a customer's, and the
102
+ * platform's weights either way.
103
+ */ export function siteBaseTypography(host) {
104
+ return _extends({}, PLATFORM_TYPE_RAMP_WEIGHTS, {
105
+ fontFamily: wearsPlatformBrand(host) ? buildFontFamilyList().join(',') : TENANT_SYSTEM_FONT_STACK
106
+ });
107
+ }
108
+ /**
109
+ * The fonts a site's base draws with that its theme does not list: the brand
110
+ * face on an operator host whose theme names no family of its own, else none.
111
+ */ export function siteBaseFonts(host, theme) {
112
+ var _ref;
113
+ var _theme_typography;
114
+ if (!wearsPlatformBrand(host)) return [];
115
+ if (theme == null ? void 0 : (_theme_typography = theme.typography) == null ? void 0 : _theme_typography.fontFamily) return [];
116
+ const listed = ((_ref = theme == null ? void 0 : theme.fonts) != null ? _ref : []).some((font)=>{
117
+ var _font_family;
118
+ return ((_font_family = font.family) == null ? void 0 : _font_family.trim().toLowerCase()) === PLATFORM_BRAND_FONT_FAMILY.toLowerCase();
119
+ });
120
+ return listed ? [] : [
121
+ PLATFORM_BRAND_FONT
122
+ ];
123
+ }
124
+
125
+ //# sourceMappingURL=site-base-fonts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/shared/ui/theme/src/lib/site-base-fonts.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'\nimport { buildFontFamilyList } from './constants'\nimport { TENANT_SYSTEM_FONT_STACK, wearsPlatformBrand } from './platform-brand'\n\n/**\n * What a site's base theme draws with, as far as its fonts go (AGL-3656): the\n * face, and the weights of the platform's text styles. Read by the published\n * page's server layout, so it holds no MUI; it is never in a browser bundle.\n */\n\n/** The platform brand's face. */\nexport const PLATFORM_BRAND_FONT_FAMILY = 'Roboto Flex'\n\n/**\n * The brand's face as a theme font: what an operator's own host loads when its\n * theme names no font of its own. The brand stack has always named it first,\n * and nothing loaded it, so aglyn.com drew in Roboto Flex on machines that\n * have it installed and in the system font everywhere else.\n */\nexport const PLATFORM_BRAND_FONT: HostThemeFont = {\n family: PLATFORM_BRAND_FONT_FAMILY,\n source: 'google',\n category: 'sans-serif',\n}\n\n/**\n * The weights the platform's text styles draw with — the brand's ramp over\n * MUI's defaults — and its named weights, in the shape of MUI typography\n * options. `platform-brand.spec.ts` holds it to the built console theme, so a\n * change to the ramp there fails until it is made here too.\n */\nexport const PLATFORM_TYPE_RAMP_WEIGHTS: Readonly<Record<string, unknown>> = {\n fontWeightLight: 300,\n fontWeightRegular: 400,\n fontWeightMedium: 500,\n fontWeightBold: 700,\n fontWeightSemiBold: 600,\n fontWeightExtraBold: 800,\n fontWeightBlack: 900,\n displayXl: { fontWeight: 900 },\n h1: { fontWeight: 900 },\n h2: { fontWeight: 800 },\n h3: { fontWeight: 700 },\n h4: { fontWeight: 400 },\n h5: { fontWeight: 400 },\n h6: { fontWeight: 500 },\n subtitle1: { fontWeight: 400 },\n subtitle2: { fontWeight: 500 },\n body1: { fontWeight: 400 },\n body2: { fontWeight: 400 },\n button: { fontWeight: 500 },\n caption: { fontWeight: 400 },\n overline: { fontWeight: 400 },\n lede: { fontWeight: 400 },\n bodyCompact: { fontWeight: 400 },\n micro: { fontWeight: 400 },\n}\n\n/**\n * The typography a site's theme is layered onto, as far as fonts go: the\n * brand stack on an operator host, the system stack on a customer's, and the\n * platform's weights either way.\n */\nexport function siteBaseTypography(host: string | undefined): Record<string, unknown> {\n return {\n ...PLATFORM_TYPE_RAMP_WEIGHTS,\n fontFamily: wearsPlatformBrand(host)\n ? buildFontFamilyList().join(',')\n : TENANT_SYSTEM_FONT_STACK,\n }\n}\n\n/**\n * The fonts a site's base draws with that its theme does not list: the brand\n * face on an operator host whose theme names no family of its own, else none.\n */\nexport function siteBaseFonts(\n host: string | undefined,\n theme: { fonts?: readonly HostThemeFont[]; typography?: { fontFamily?: string } } | undefined,\n): HostThemeFont[] {\n if (!wearsPlatformBrand(host)) return []\n if (theme?.typography?.fontFamily) return []\n const listed = (theme?.fonts ?? []).some(\n (font) => font.family?.trim().toLowerCase() === PLATFORM_BRAND_FONT_FAMILY.toLowerCase(),\n )\n return listed ? [] : [PLATFORM_BRAND_FONT]\n}\n"],"names":["buildFontFamilyList","TENANT_SYSTEM_FONT_STACK","wearsPlatformBrand","PLATFORM_BRAND_FONT_FAMILY","PLATFORM_BRAND_FONT","family","source","category","PLATFORM_TYPE_RAMP_WEIGHTS","fontWeightLight","fontWeightRegular","fontWeightMedium","fontWeightBold","fontWeightSemiBold","fontWeightExtraBold","fontWeightBlack","displayXl","fontWeight","h1","h2","h3","h4","h5","h6","subtitle1","subtitle2","body1","body2","button","caption","overline","lede","bodyCompact","micro","siteBaseTypography","host","fontFamily","join","siteBaseFonts","theme","typography","listed","fonts","some","font","trim","toLowerCase"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,mBAAmB,QAAQ,iBAAa;AACjD,SAASC,wBAAwB,EAAEC,kBAAkB,QAAQ,sBAAkB;AAE/E;;;;CAIC,GAED,+BAA+B,GAC/B,OAAO,MAAMC,6BAA6B,cAAa;AAEvD;;;;;CAKC,GACD,OAAO,MAAMC,sBAAqC;IAChDC,QAAQF;IACRG,QAAQ;IACRC,UAAU;AACZ,EAAC;AAED;;;;;CAKC,GACD,OAAO,MAAMC,6BAAgE;IAC3EC,iBAAiB;IACjBC,mBAAmB;IACnBC,kBAAkB;IAClBC,gBAAgB;IAChBC,oBAAoB;IACpBC,qBAAqB;IACrBC,iBAAiB;IACjBC,WAAW;QAAEC,YAAY;IAAI;IAC7BC,IAAI;QAAED,YAAY;IAAI;IACtBE,IAAI;QAAEF,YAAY;IAAI;IACtBG,IAAI;QAAEH,YAAY;IAAI;IACtBI,IAAI;QAAEJ,YAAY;IAAI;IACtBK,IAAI;QAAEL,YAAY;IAAI;IACtBM,IAAI;QAAEN,YAAY;IAAI;IACtBO,WAAW;QAAEP,YAAY;IAAI;IAC7BQ,WAAW;QAAER,YAAY;IAAI;IAC7BS,OAAO;QAAET,YAAY;IAAI;IACzBU,OAAO;QAAEV,YAAY;IAAI;IACzBW,QAAQ;QAAEX,YAAY;IAAI;IAC1BY,SAAS;QAAEZ,YAAY;IAAI;IAC3Ba,UAAU;QAAEb,YAAY;IAAI;IAC5Bc,MAAM;QAAEd,YAAY;IAAI;IACxBe,aAAa;QAAEf,YAAY;IAAI;IAC/BgB,OAAO;QAAEhB,YAAY;IAAI;AAC3B,EAAC;AAED;;;;CAIC,GACD,OAAO,SAASiB,mBAAmBC,IAAwB;IACzD,OAAO,aACF3B;QACH4B,YAAYlC,mBAAmBiC,QAC3BnC,sBAAsBqC,IAAI,CAAC,OAC3BpC;;AAER;AAEA;;;CAGC,GACD,OAAO,SAASqC,cACdH,IAAwB,EACxBI,KAA6F;;QAGzFA;IADJ,IAAI,CAACrC,mBAAmBiC,OAAO,OAAO,EAAE;IACxC,IAAII,0BAAAA,oBAAAA,MAAOC,UAAU,qBAAjBD,kBAAmBH,UAAU,EAAE,OAAO,EAAE;IAC5C,MAAMK,SAAS,SAACF,yBAAAA,MAAOG,KAAK,mBAAI,EAAE,EAAEC,IAAI,CACtC,CAACC;YAASA;eAAAA,EAAAA,eAAAA,KAAKvC,MAAM,qBAAXuC,aAAaC,IAAI,GAAGC,WAAW,QAAO3C,2BAA2B2C,WAAW;;IAExF,OAAOL,SAAS,EAAE,GAAG;QAACrC;KAAoB;AAC5C"}
@@ -19,8 +19,7 @@ export declare const tenantOptions: ThemeOptions;
19
19
  export declare const tenantOptionsDark: ThemeOptions;
20
20
  export declare const tenantThemeLight: Theme;
21
21
  export declare const tenantThemeDark: Theme;
22
- export declare const PLATFORM_BRAND_HOSTS: ReadonlySet<string>;
23
- export declare function wearsPlatformBrand(host: string | undefined): boolean;
22
+ export { PLATFORM_BRAND_HOSTS, wearsPlatformBrand } from './platform-brand';
24
23
  /**
25
24
  * The site key {@link wearsPlatformBrand} is asked about, read off a host
26
25
  * document (AGL-3068).
@@ -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"}