Skip to content

Commit 0a1c880

Browse files
cameronapakclaudejhampton
authored
feat(ui): swap the serif font from Source Serif 4 to Untitled Serif (YPE-1350, YPE-1910) (#304)
* feat(ui): load Untitled Serif from the gated Fonts API (YPE-1350, YPE-1910) Serif stack becomes 'Untitled Serif', 'Source Serif 4', serif in both declarations — core's --yv-font-serif and ui's @theme inline --font-serif. They are literal duplicates, not aliases, so font-tokens.test.ts now guards them against drift (the first test to assert a font token's value). Delivery is a new <YvFonts /> rendered from YouVersionProvider: a React 19 hoisted <link> to /v1/fonts/1/stylesheet, the gated endpoint ADR-0001 named as the required consumption pattern. This is the SDK's first runtime-value- dependent stylesheet — the URL needs the app key, which __YV_STYLES__ cannot know at build time. It renders only in the normal branch; no key, no font. Source Serif 4 stays loaded as the fallback, so nothing regresses when the request is blocked or the host has no key. Reader body text still renders Source Serif 4: Verse.Html sets --yv-reader-font-family inline from the reader's own state, which shadows the token. Phase 2 changes that constant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(ui): reader font picker offers "Untitled" and migrates stored preference (YPE-1350) The reader pins its own font: Verse.Html sets --yv-reader-font-family inline from component state, which shadows the CSS token. So the Phase 1 stack change could not reach passage body text until the constant behind that inline value changed. This is that change. - UNTITLED_SERIF_FONT is the new default; SOURCE_SERIF_FONT stays exported as @deprecated because the hydrate-time migration still needs to recognize it. - Returning readers who chose serif before this shipped stored the old stack. Without mapping it forward they would hydrate to a value matching neither picker button — no button active, and Source Serif 4 forever. The migration is deliberately narrow rather than full validation: FontFamily is an open type on purpose, so a host passing defaultFontFamily="Georgia" must still round-trip. A test covers that non-legacy values pass through untouched. - bible-card.tsx had the same inline pin hardcoded; swapped too. YPE-1910 names the Bible card explicitly. - Locale files trade sourceSerifFontName for untitledSerifFontName ("Untitled", per the ticket wording). They are SDK-private, so removal is safe. Verified in Chrome: body text renders Untitled Serif, picker shows "Untitled" active, a seeded legacy value migrates and re-persists, and Inter survives a reload unmigrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: record the Untitled Serif licensing decision in ADR-0003 (YPE-1350) ADR-0001 is Accepted and names two conditions for re-introducing brand fonts. Shipping the font without touching it would leave the repo's own documentation contradicting its code. ADR-0003 records both conditions as met, and is deliberately blunt about what the access control actually is: gated discovery plus revocable CDN URLs, not file-level authentication. ADR-0001's finding that the woff2 is publicly downloadable is unchanged and still true — what changed is the reading of the licence constraint, which is YouVersion's to interpret. Saying so keeps the two ADRs consistent; the alternative was a quiet contradiction. The sign-off is verbal, from Klim directly, relayed and confirmed by Cameron Pak on 2026-07-28. The ADR names it as verbal rather than implying a paper trail exists, and says to amend if written confirmation surfaces. Aktiv Grotesk stays reverted; ADR-0001 is superseded in part only. Also: the README's CSP section was stale — it still listed storage.googleapis.com for Aktiv Grotesk, which no longer ships. Replaced with the hosts actually used now. That section is the answer for strict-CSP consumers, since there is deliberately no opt-out prop. Changeset is minor across all three packages (core's CSS changes too) and calls out the new outbound request, the CSP entries, and the default font change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: keep licensing detail out of the public ADRs; scope the suspense claim This repo is public and both ADRs are published with it. Strip the licensing narrative from ADR-0001 and ADR-0003 down to what the code needs to know: which face is cleared, and that it must be served by the Fonts API rather than shipped in the repo. Licensing specifics are tracked internally. Also drop ADR-0003's CDN probe table, which published an unauthenticated font URL and a compliance argument that don't belong in a public repo. Code changes: - yv-fonts.tsx / yv-fonts.test.tsx: the comments asserted React "only suspends for stylesheets inserted during a transition". React's docs say the component rendering a `precedence` stylesheet suspends while it loads, without scoping that to transitions. The test proves the synchronous path only, so say that and flag the transition path as uncovered. - yv-fonts.test.tsx: the missing-key test goes through the provider's MissingAppKey guard, so YvFonts' own guard was never executed. Cover it directly for undefined / empty / whitespace keys, and restore the console spy it left installed. - README: drop the `font-display: swap` claim. That property comes from a stylesheet the Fonts API authors, not this SDK, so we can't promise it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: drop Aktiv Grotesk and the licensing framing from the public docs Review feedback on PR #304. Aktiv Grotesk was never published — it landed in a changeset and was reverted before release — so naming it in the ADRs and style comments documents a decision about a font we do not use and never shipped. Untitled Serif is cleared, so anything implying otherwise is wrong too. - ADR-0001: retitle without "pending licensing", drop the Aktiv Grotesk bullet and the licensing narrative. What remains is the decision the code actually depends on: a hardcoded @font-face is the wrong delivery pattern for an SDK that renders inside third-party apps, so brand fonts load from /v1/fonts/{font_id}/stylesheet. Reduced from two re-introduction conditions to that one. - ADR-0003: drop the Aktiv Grotesk mentions and the "licensing is cleared" paragraph; state the sans stack is simply unchanged. - theme.css / global.css / font-tokens.test.ts: same, in the comments. - bible-chapter-picker.test.tsx: two test names still said "Aktiv" while asserting only size and weight. Renamed to what they check. Filename kept as 0001-revert-brand-fonts-pending-licensing.md so the three published CHANGELOG links keep resolving; those changelogs are release history and are left as-is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: renumber Untitled Serif ADR 0003 -> 0004 ADR 0003 was taken by the locale-ownership-gate ADR merged in #308. Renames the file and heading, updates cross-references in ADR-0001, the changeset, theme.css, global.css, and yv-fonts.tsx. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(ui): drop locale edits; upstream untitledSerifFontName landed via #306 The rename now lives in platform-localization (merged to main in #306), so the feature PR no longer edits the upstream-owned locale files. The picker resolves t('untitledSerifFontName') from main's bundle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ui): contain font stylesheet suspension and correct serif docs Address PR review feedback: - Wrap <YvFonts /> in a local <Suspense fallback={null}> inside YouVersionProvider so the precedence-stylesheet suspension stays scoped to the provider instead of bubbling to the consumer's nearest boundary and holding their tree during the Fonts API fetch. - Correct the changeset and ADR-0004 wording: the picker button reads "Untitled Serif" (not "Untitled"), and sourceSerifFontName is retired in a follow-up sync rather than already gone from the private locales. Also deflake the dom-vapor and strictmode-vapor highlight tests: give their REMOVE -> deleteHighlight synchronization gates explicit 5s timeouts so a loaded CI runner can't starve them past the default 1s window. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Jeff Hampton <jhampton@gmail.com>
1 parent abc1877 commit 0a1c880

18 files changed

Lines changed: 573 additions & 91 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@youversion/platform-core': minor
3+
'@youversion/platform-react-hooks': minor
4+
'@youversion/platform-react-ui': minor
5+
---
6+
7+
Swap the SDK's serif face from Source Serif 4 to **Untitled Serif**, YouVersion's brand serif, delivered from the YouVersion Fonts API (YPE-1350, YPE-1910).
8+
9+
- The serif stack is now `'Untitled Serif', 'Source Serif 4', serif` in both token declarations (`--yv-font-serif` in core, `--font-serif` in the UI theme), so every serif surface follows: BibleReader body text, the Bible card, `BibleText`, the version-picker abbreviation tile, footnotes, chapter headings, and the `lg` Verse of the Day card. Untitled Serif is named first, so a host that loads its own copy takes priority regardless of who fetched it.
10+
- `YouVersionProvider` now loads the font for you. It renders a hoisted `<link rel="stylesheet">` to `https://api.youversion.com/v1/fonts/1/stylesheet`, using the app key you already supply — **a new outbound request** to `api.youversion.com`, plus woff2 fetches from `cdn.youversion.com`. No new prop and no setup; there is no opt-out. No font file ships in any package.
11+
- **Strict CSP consumers:** allowlist `https://api.youversion.com` in `style-src` and `https://cdn.youversion.com` in `font-src`. If they are blocked, serif text falls back to Source Serif 4 (still loaded from Google Fonts) with no layout break.
12+
- **The BibleReader's default font changes** from Source Serif 4 to Untitled Serif, and the font picker button now reads "Untitled Serif" instead of "Source Serif". Readers whose saved preference is the old Source Serif stack are migrated on load, so the picker still shows serif as active. Any other `fontFamily` value you pass or persist is left untouched.
13+
- The internal `SOURCE_SERIF_FONT` constant is deprecated (retained for that migration). The picker now reads a new `untitledSerifFontName` locale key; the old `sourceSerifFontName` key is retired in a follow-up sync. Neither is part of the public API; nothing is removed or retyped.
14+
15+
See `docs/adr/0004-adopt-untitled-serif-via-fonts-api.md` for the delivery rationale.
Lines changed: 26 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,48 @@
1-
# 1. Revert brand fonts to Inter / Source Serif 4 pending licensing
1+
# 1. Revert brand fonts to Inter / Source Serif 4
22

33
Date: 2026-06-24
44

55
## Status
66

7-
Accepted
7+
Accepted. **Superseded in part** by
8+
[ADR-0004](0004-adopt-untitled-serif-via-fonts-api.md) (2026-07-28), for **Untitled
9+
Serif only**: it now loads from the `/v1/fonts/1/stylesheet` endpoint, which is the
10+
condition below. The sans stack is unchanged and stays `'Inter', sans-serif`.
811

912
## Context
1013

11-
The SDK had begun shipping YouVersion brand fonts to consumer apps:
14+
The SDK had begun shipping brand fonts to consumer apps via hardcoded `@font-face`
15+
blocks pointing at CDN woff2 files — for the sans default (`--yv-font-sans`), the serif
16+
default (`--yv-font-serif`), and the Bible Version picker abbreviation tile. None of it
17+
reached a published release.
1218

13-
- **Aktiv Grotesk App** (Dalton Maag) as the sans default (`--yv-font-sans`),
14-
loaded via a hardcoded `@font-face` pointing at a public CDN woff2.
15-
- **Untitled Serif** (Klim Type Foundry) as the serif default (`--yv-font-serif`)
16-
and the Bible Version picker abbreviation tile, same hardcoded `@font-face` pattern.
19+
A hardcoded `@font-face` is the wrong delivery pattern for this SDK. Its purpose is to
20+
render inside **third-party developer apps**, so it puts the font file in front of third
21+
parties with nothing in the SDK able to gate it — access control is enforced server-side
22+
(API gateway + CDN), not in the SDK.
1723

18-
Both create the same exposure: the SDK's purpose is to render fonts inside
19-
**third-party developer apps**, so the font files are delivered to, and
20-
downloadable by, third parties.
21-
22-
- **Aktiv Grotesk (Dalton Maag):** the licence is breached the moment a
23-
third-party developer uses their app key and gains access to the actual font
24-
file (`.woff`/`.woff2`/`.otf`/`.ttf`). No licence tier we hold covers serving
25-
this font to arbitrary third parties. CORS / file-level protection is
26-
enforced server-side (YouVersion API gateway + CDN), not in the SDK — the SDK
27-
cannot make the file un-downloadable.
28-
- **Untitled Serif (Klim):** an Enterprise licence may permit third-party use
29-
if developers qualify as a "partner" (the licence enumerates affiliates,
30-
agencies, partners, vendors, contractors, freelancers). Whether a Platform
31-
developer is a "partner" is an **open legal question**.
32-
33-
A "browser-consumable stylesheet" endpoint exists
34-
(`GET /v1/fonts/{font_id}/stylesheet`, accepts `app_key`, gateway injects the
35-
app-id header). It is the correct future consumption pattern, but it does **not**
36-
by itself resolve licensing: the woff2 it references still sits at a public CDN
37-
URL, so switching to it does not make the font file un-downloadable.
24+
A browser-consumable stylesheet endpoint exists
25+
(`GET /v1/fonts/{font_id}/stylesheet`, accepts an app key; the gateway injects the
26+
app-id header). It is the correct consumption pattern for a font the SDK serves.
3827

3928
## Decision
4029

41-
Revert **both** brand fonts to the prior fallbacks for the shipping PR:
30+
Revert both font tokens to the prior fallbacks for the shipping PR:
4231

4332
- `--yv-font-sans``'Inter', sans-serif`
4433
- `--yv-font-serif``'Source Serif 4', serif`
4534

46-
Remove both brand `@font-face` blocks, the `--font-aktiv` / `--font-untitled-serif`
47-
aliases, the `yv:font-aktiv` / `yv:font-untitled-serif` usages, and the brand
48-
options in the Bible Reader font picker. The abbreviation-tile redesign and all
49-
other Figma layout/typography work, the `useOrganizations` hooks, and publisher
35+
Remove the brand `@font-face` blocks, their `--font-*` aliases and `yv:font-*` usages,
36+
and the brand options in the Bible Reader font picker. The abbreviation-tile redesign
37+
and all other Figma layout/typography work, the `useOrganizations` hooks, and publisher
5038
names are retained — only the font **family** is reverted.
5139

52-
The brand-font implementation is parked on branch `feat/youversion-brand-fonts`
53-
(snapshot at the pre-revert HEAD) for re-application once licensing clears.
54-
5540
## Consequences
5641

57-
- The SDK ships no licence-restricted font files to third parties. Defensible
58-
legal state.
59-
- The abbreviation tile and serif body text render in **Source Serif 4** (the
60-
serif fallback) rather than Untitled Serif — closest legal match to the Figma
42+
- The SDK ships no font files to third parties.
43+
- The abbreviation tile and serif body text render in **Source Serif 4** (the serif
44+
fallback) rather than the brand serif — the closest available substitute for the Figma
6145
serif intent; exact brand match is deferred.
62-
- Re-introducing brand fonts requires: (1) legal sign-off on Untitled Serif's
63-
"partner" classification and/or a resolved Aktiv licence path, and (2) loading
64-
via the gated `/v1/fonts/{font_id}/stylesheet` endpoint rather than hardcoded
65-
`@font-face`. Untitled Serif is `font_id` 1 / slug `untitled-serif`.
66-
- Re-application path: cherry-pick the font hunks from `feat/youversion-brand-fonts`
67-
onto then-current `main`.
46+
- Introducing a brand font requires loading it via the
47+
`/v1/fonts/{font_id}/stylesheet` endpoint rather than a hardcoded `@font-face`.
48+
Untitled Serif is `font_id` 1 / slug `untitled-serif`.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# 4. Adopt Untitled Serif via the Fonts API stylesheet endpoint
2+
3+
Date: 2026-07-28
4+
5+
## Status
6+
7+
Accepted. Supersedes [ADR-0001](0001-revert-brand-fonts-pending-licensing.md) **in
8+
part** — for Untitled Serif only.
9+
10+
## Context
11+
12+
ADR-0001 reverted the SDK's brand fonts and set one condition for bringing one back:
13+
load it through the `/v1/fonts/{font_id}/stylesheet` endpoint rather than a hardcoded
14+
`@font-face`, so the SDK never ships or hosts the file itself.
15+
16+
Untitled Serif meets that condition. `GET /v1/fonts/1/stylesheet` accepts the app key as
17+
either an `X-YVP-App-Key` header or an `?app_key=` query parameter, and returns `401`
18+
without one.
19+
20+
The sans stack is unchanged and stays `'Inter', sans-serif` — this is a serif-only
21+
change.
22+
23+
YPE-1350 (BibleReader renders Untitled Serif) and YPE-1910 (`--yv-font-serif` becomes
24+
`Untitled Serif → Source Serif 4 → serif`, covering `BibleText` and the Bible card, not
25+
just the reader) both depend on this.
26+
27+
## Decision
28+
29+
Adopt Untitled Serif as the SDK's serif face, loaded by the SDK itself from the
30+
stylesheet endpoint.
31+
32+
**1. Font stack (YPE-1910).** Both serif declarations become
33+
`'Untitled Serif', 'Source Serif 4', serif`:
34+
35+
- `packages/core/src/styles/theme.css``--yv-font-serif`
36+
- `packages/ui/src/styles/global.css``--font-serif`, inside `@theme inline`
37+
38+
These are literal duplicates in two packages, not aliases: core cannot import Tailwind,
39+
and `@theme inline` values are inlined into utilities rather than emitted as runtime
40+
custom properties. `packages/ui/src/styles/font-tokens.test.ts` reads both files off
41+
disk and fails if they drift apart. Source Serif 4 stays loaded from Google Fonts as the
42+
fallback, so nothing regresses when Untitled Serif is unavailable — and because the
43+
stack names Untitled Serif *first*, a host that loads its own copy wins regardless of
44+
who fetched it. That is YPE-1910's explicit requirement.
45+
46+
The change is SDK-wide, not reader-only: the version-picker abbreviation tile,
47+
footnotes, `Verse.Text` at `lg`, chapter headings, the Bible card, and the `lg` Verse of
48+
the Day card all follow the token.
49+
50+
**2. Delivery (YPE-1350).** A new `<YvFonts />` (`packages/ui/src/lib/yv-fonts.tsx`),
51+
sibling to `<YvStyles />`, rendered from `YouVersionProvider` in the normal branch only
52+
(no app key, no font):
53+
54+
```tsx
55+
<link
56+
rel="stylesheet"
57+
href={`https://${apiHost}/v1/fonts/1/stylesheet?app_key=${encodeURIComponent(appKey)}`}
58+
precedence="yv-sdk-fonts"
59+
/>
60+
```
61+
62+
React 19 hoists it to `<head>` and dedupes by `href`, so multiple providers still yield
63+
one link and SSR streaming works. `@font-face` is not subject to `@layer`, so cascade
64+
position is irrelevant; `precedence` is only there to opt into the hoist and dedupe.
65+
66+
This is the SDK's first runtime-value-dependent stylesheet, and it has to be. The build
67+
pipeline freezes `global.css` into the `__YV_STYLES__` string literal at `pnpm
68+
build:css`, with no access to a consumer's app key — a React-rendered `<link>` is the
69+
only seam that has one.
70+
71+
`font_id` is hardcoded to `1` (slug `untitled-serif`) rather than discovered via
72+
`GET /v1/fonts`. Discovery would add a request waterfall in front of first paint to
73+
guard against an id change that would itself be a breaking change on YouVersion's own
74+
service. The constant is named and comment-linked to this ADR so it is greppable if the
75+
API ever renumbers. `packages/core/src/schemas/font.ts` stays unwired; no `FontsClient`
76+
and no `useFonts` hook are built.
77+
78+
`apiHost` threads through the same way `ApiClient` does (`config.apiHost ??
79+
'api.youversion.com'`) so staging environments keep working.
80+
81+
**3. Reader picker (YPE-1350).** `UNTITLED_SERIF_FONT` becomes the reader's default font
82+
family and the right-hand picker button, labelled **"Untitled Serif"** per the ticket's
83+
explicit wording. `SOURCE_SERIF_FONT` stays exported as `@deprecated` solely so the
84+
hydration path can recognize it: a reader who chose serif before this shipped has the
85+
old stack in `localStorage`, and without mapping it forward they would hydrate to a
86+
value matching neither picker button. The mapping is deliberately narrow rather than
87+
full validation, because `FontFamily` is an open type on purpose and a host passing
88+
`defaultFontFamily="Georgia"` must keep round-tripping.
89+
90+
**4. No opt-out.** There is no `disableBrandFonts` prop, consistent with `<YvStyles />`,
91+
which has none. Strict-CSP consumers get documented CSP entries
92+
(`packages/ui/README.md`) rather than an escape hatch. Adding a prop later is
93+
non-breaking if the need turns out to be real.
94+
95+
## Consequences
96+
97+
- Every serif surface in the SDK renders the YouVersion brand serif for the first time.
98+
The abbreviation tile and reader body text are now an exact brand match rather than
99+
ADR-0001's closest available substitute.
100+
- **A new outbound request per consumer app**, to
101+
`https://api.youversion.com/v1/fonts/1/stylesheet`, and the woff2 fetches that follow
102+
it from `cdn.youversion.com`. Both are `cache-control: public` (86400s and 3600s
103+
respectively) and CORS-open. Consumers with a strict CSP must allowlist
104+
`api.youversion.com` in `style-src` and `cdn.youversion.com` in `font-src`; without
105+
them the SDK falls back to Source Serif 4 with no layout break.
106+
- **The app key appears in a URL query string.** It is already public browser-side (it
107+
ships in request headers on every API call), and the gateway accepts it on this route,
108+
but it will now also land in CDN/proxy access logs and `Referer` headers. Accepted
109+
knowingly.
110+
- **`<link rel="stylesheet" precedence>` can suspend the commit of the component that
111+
renders it while the sheet loads.** Verified that a plain synchronous mount commits its
112+
children immediately (`yv-fonts.test.tsx`). A mount that happens inside a transition —
113+
a Next.js App Router client navigation, for example — is not covered by that test and
114+
may hold the commit until the request settles. Failures settle too, so this is a
115+
latency risk rather than a hang. Revisit if a consumer reports a slow first navigation.
116+
- The default reader font changes from Source Serif 4 to Untitled Serif for new users,
117+
and returning serif readers are migrated on hydrate. No public API is removed or
118+
retyped; the change ships as a `minor` across all three packages.
119+
- The serif stack is declared twice and must stay in sync by hand.
120+
`font-tokens.test.ts` is the guard — it is the first test in the repo to assert a font
121+
token's literal value.
122+
- The revert, if it is ever needed, is small and local: drop `<YvFonts />` from
123+
`YouVersionProvider` and remove `'Untitled Serif'` from the two stacks. Everything
124+
else — the picker label, the migration, the CSP docs — degrades to Source Serif 4 on
125+
its own.

packages/core/src/styles/theme.css

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,14 @@
108108
--yv-sidebar-border: var(--yv-gray-15);
109109
--yv-sidebar-ring: var(--yv-blue-30);
110110

111-
/* Brand fonts (Aktiv Grotesk App / Untitled Serif) reverted to Inter / Source
112-
Serif 4 pending licensing — see docs/adr/0001-revert-brand-fonts-pending-licensing.md.
113-
Brand-font implementation parked on branch feat/youversion-brand-fonts. */
111+
/* Untitled Serif is the brand serif, loaded from the gated Fonts API stylesheet
112+
endpoint — see docs/adr/0004-adopt-untitled-serif-via-fonts-api.md. Source Serif 4
113+
(Google Fonts) stays in the stack as the fallback, so a host that never loads
114+
Untitled Serif sees no regression.
115+
NOTE: the serif stack is duplicated verbatim in packages/ui/src/styles/global.css
116+
(@theme inline, --font-serif). Edit both together — font-tokens.test.ts guards drift. */
114117
--yv-font-sans: 'Inter', sans-serif;
115-
--yv-font-serif: 'Source Serif 4', serif;
118+
--yv-font-serif: 'Untitled Serif', 'Source Serif 4', serif;
116119
--yv-reader-font-family: var(--yv-font-serif), var(--yv-font-sans);
117120

118121
&[data-yv-theme='dark'] {

packages/ui/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,15 @@ All component classes are prefixed with `yv:` to avoid collisions with your app'
5858
The SDK loads webfonts from external origins. If your app sets a strict `Content-Security-Policy`, allowlist these hosts so fonts aren't blocked (without them, components fall back to a system sans-serif):
5959

6060
```
61-
font-src https://fonts.gstatic.com https://storage.googleapis.com;
62-
style-src https://fonts.googleapis.com;
61+
font-src https://fonts.gstatic.com https://cdn.youversion.com;
62+
style-src https://fonts.googleapis.com https://api.youversion.com;
6363
```
6464

65-
- `fonts.gstatic.com` / `fonts.googleapis.com` — Inter and Source Serif (base typography)
66-
- `storage.googleapis.com` — Aktiv Grotesk App, the brand font used by `BibleChapterPicker`
65+
- `fonts.googleapis.com` / `fonts.gstatic.com` — Inter and Source Serif 4 (base typography), loaded from Google Fonts
66+
- `api.youversion.com` — the Fonts API stylesheet (`/v1/fonts/1/stylesheet`), which `YouVersionProvider` requests with your app key
67+
- `cdn.youversion.com` — the Untitled Serif woff2 files that stylesheet points at
68+
69+
Untitled Serif is YouVersion's brand serif and the SDK's default serif face. There is no prop to turn it off. If these hosts are blocked, serif text falls back to Source Serif 4 with no layout break — the stack is `'Untitled Serif', 'Source Serif 4', serif`. If you load Untitled Serif yourself, your copy is used; the stack names it first regardless of who fetched it.
6770

6871
## Theming
6972

packages/ui/src/components/YouVersionProvider.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React, { type ComponentProps, useEffect } from 'react';
1+
import React, { type ComponentProps, Suspense, useEffect } from 'react';
22
import { YouVersionPlatformConfiguration } from '@youversion/platform-core';
33
import { YouVersionProvider as BaseYouVersionProvider } from '@youversion/platform-react-hooks';
44
import { syncBrowserLanguageFromNavigator } from '@/i18n';
55
import { YvStyles } from '@/lib/yv-styles';
6+
import { YvFonts } from '@/lib/yv-fonts';
67
import { MissingAppKey } from '@/components/missing-app-key';
78

89
function resolveTheme(theme: 'light' | 'dark' | 'system' = 'light'): 'light' | 'dark' {
@@ -57,6 +58,16 @@ export function YouVersionProvider(
5758
return (
5859
<BaseYouVersionProvider {...props}>
5960
<YvStyles />
61+
{/* Only in this branch — the missing-app-key guard above has no key, and
62+
without a key the gated Fonts API request would 401.
63+
64+
React suspends the component that renders a `precedence` stylesheet
65+
while it loads. The local boundary keeps that suspension scoped to the
66+
font link so it can't bubble to the consumer's nearest boundary above
67+
the provider and hold their tree during the Fonts API fetch. */}
68+
<Suspense fallback={null}>
69+
<YvFonts appKey={props.appKey} apiHost={props.apiHost} />
70+
</Suspense>
6071
{props.children}
6172
</BaseYouVersionProvider>
6273
);

packages/ui/src/components/bible-card.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { BibleAppLogoLockup } from './bible-app-logo-lockup';
77
import { BibleVersionPicker, type BibleVersionPickerPressData } from './bible-version-picker';
88
import { Button } from './ui/button';
99
import { useControllableState } from '@radix-ui/react-use-controllable-state';
10-
import { SOURCE_SERIF_FONT } from '@/lib/verse-html-utils';
10+
import { UNTITLED_SERIF_FONT } from '@/lib/verse-html-utils';
1111
import { useDelayedLoading } from '@/lib/use-delayed-loading';
1212
import { LoaderIcon } from './icons/loader';
1313
import { AnimatedHeight } from './animated-height';
@@ -181,7 +181,7 @@ export function BibleCard({
181181
<BibleTextView
182182
theme={theme}
183183
fontSize={16}
184-
fontFamily={SOURCE_SERIF_FONT}
184+
fontFamily={UNTITLED_SERIF_FONT}
185185
reference={reference}
186186
versionId={versionNum}
187187
showVerseNumbers={false}

packages/ui/src/components/bible-chapter-picker.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,15 +250,15 @@ describe('BibleChapterPicker - typography (matches Figma sizing; sans inherited)
250250
expect(genesisTrigger).toHaveClass('yv:data-[state=open]:font-bold');
251251
});
252252

253-
it('chapter number buttons use Aktiv 16px bold', () => {
253+
it('chapter number buttons render at 16px bold', () => {
254254
renderContent();
255255

256256
const chapterButton = screen.getByText('2').closest('button');
257257
expect(chapterButton).not.toBeNull();
258258
expect(chapterButton).toHaveClass('yv:text-base', 'yv:font-bold');
259259
});
260260

261-
it('search input uses Aktiv 16px', () => {
261+
it('search input renders at 16px', () => {
262262
renderContent();
263263

264264
expect(screen.getByPlaceholderText('Search')).toHaveClass('yv:text-base');

0 commit comments

Comments
 (0)