Skip to content

fix: stop Dynamic Type from discarding the configured fontType - #79

Merged
NuPlay merged 2 commits into
mainfrom
fix/dynamic-type-preserves-font-type
Aug 29, 2026
Merged

fix: stop Dynamic Type from discarding the configured fontType#79
NuPlay merged 2 commits into
mainfrom
fix/dynamic-type-preserves-font-type

Conversation

@NuPlay

@NuPlay NuPlay commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Problem

Dynamic Type is expressed with the CSS font shorthand:

html { font: -apple-system-body; }
body { font: -apple-system-body; }
h1   { font: -apple-system-largeTitle; }

A shorthand resets every longhand it covers, which includes font-family and font-style. Those rules are appended to customCSS, and customCSS is emitted after the generated rules, so they always won the cascade.

The result: setting supportsDynamicType: true silently threw away fontType. .monospaced, .italic and .customName all rendered in the default family, with no warning and nothing in the generated CSS that looked wrong at a glance.

Changes

Add Configuration.resolvedCustomCSS, which appends a font override after the Dynamic Type rules:

html, body, h1, h2, h3, h4, h5, h6, p { font-family: <fontType.name>; <additionalCSSProperties> }

and use it everywhere customCSS was previously emitted (WebView.generateCSS() and Configuration.generateCompleteCSS()). customCSS itself is untouched, so anything reading that property keeps its current value and behaviour.

Note on #63

The originally reported symptom was fixed in v3.0.0 and this PR pins it down with a test. Root cause there: FontType.name used to return the raw PostScript name, e.g. .AppleSystemUIFontMonospaced-Regular. A CSS family starting with . is not a valid unquoted identifier, so WebKit discarded the whole font-family declaration and fell back to its default serif face, which is exactly what the reporter saw. v3.0.0 returns real CSS stacks and quotes custom names; the new test asserts that font names never start with ..

The Dynamic Type interaction above was the one part of #63 still broken on 3.x.

Fixes #63

Dynamic Type is expressed with the CSS `font` shorthand
(`html { font: -apple-system-body; }`), and a shorthand resets every longhand it
covers, including `font-family` and `font-style`. Those rules live in
`customCSS`, which is emitted after the generated rules, so they always won the
cascade. The result was that `supportsDynamicType: true` silently threw away
`fontType`, and `.monospaced`, `.italic` and `.customName` all rendered in the
default family.

Add `Configuration.resolvedCustomCSS`, which appends a font override after the
Dynamic Type rules, and use it everywhere `customCSS` was emitted. `customCSS`
itself is unchanged, so anything reading it keeps the current behaviour.

Also pins down the underlying #63 fix that shipped in v3.0.0: font names must be
valid CSS families. The pre-3.0 implementation returned raw PostScript names
such as `.AppleSystemUIFontMonospaced-Regular`, which WebKit could not parse as
an unquoted family, so it dropped the declaration and fell back to serif.

Refs #63
Copilot AI lite review requested due to automatic review settings August 29, 2026 03:54
@NuPlay NuPlay added the bug Something isn't working label Aug 29, 2026
@NuPlay NuPlay self-assigned this Aug 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The added override selector is not specific enough to beat Dynamic Type’s p.subheadline/etc rules and may also introduce an unintended cascade behavior change when fontType is the default system font.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes an interaction where enabling Dynamic Type (implemented via CSS font shorthand rules appended to customCSS) unintentionally reset font-family/font-style, causing the configured fontType (e.g., .monospaced, .italic, .customName) to be lost in the final cascade.

Changes:

  • Added Configuration.resolvedCustomCSS to append a framework-managed font override intended to win after Dynamic Type shorthand rules.
  • Switched CSS emission sites (WebView.generateCSS() and Configuration.generateCompleteCSS()) from customCSS to resolvedCustomCSS.
  • Added tests to validate CSS-safe font family strings and to assert ordering between Dynamic Type shorthands and font overrides.
File summaries
File Description
Tests/RichTextTests/RichTextSwiftTestingTests.swift Adds regression tests around CSS font name formatting and Dynamic Type + fontType behavior.
Sources/RichText/Views/Webview.swift Emits resolvedCustomCSS in generated HTML/CSS so overrides apply in the WebView.
Sources/RichText/Models/Configuration.swift Introduces resolvedCustomCSS and updates complete CSS generation to use it.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +113 to +123
public var resolvedCustomCSS: String {
guard supportsDynamicType else {
return customCSS
}

let fontOverrideCSS = """
html, body, h1, h2, h3, h4, h5, h6, p { font-family: \(fontType.name); \(fontType.additionalCSSProperties) }
"""

return customCSS + "\n" + fontOverrideCSS
}
Comment on lines +179 to +190
let shorthandRange = css.range(of: "font: -apple-system-body")
let familyRange = css.range(of: "font-family: \(fontType.name)", options: .backwards)

#expect(shorthandRange != nil)
#expect(familyRange != nil)

// The `font` shorthand resets `font-family`, so the family has to be re-applied
// after the Dynamic Type rules to survive the cascade.
if let shorthandRange, let familyRange {
#expect(familyRange.lowerBound > shorthandRange.lowerBound)
}
}
Addresses Copilot review feedback on #79.

Two problems with the first version of the override:

1. It used a bare `p` selector, specificity (0,0,1). `RichTextConstants.dynamicTypeCSS`
   styles `p.subheadline`, `p.footnote`, `p.caption1` and `p.caption2` at (0,1,1),
   so those four paragraph classes still won the cascade and kept resetting
   `font-family` no matter how late the override was emitted. Name the classes
   explicitly so specificity ties and source order decides.

2. It was emitted unconditionally. For the default system font the override is a
   no-op against the Dynamic Type shorthands, but it is appended after
   `customCSS`, so it would clobber a caller's own `font-family` rule for no
   benefit. Emit it only when the configured font actually differs, either in
   family or in the extra properties `.italic` and `.monospaced` add.

Tests now compare against the last Dynamic Type shorthand rather than the first,
assert the paragraph classes are covered, and assert that no override is emitted
for `.system`.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The fix is localized, aligns with the stated root cause (shorthand resetting longhands), and is backed by targeted regression tests for cascade order and selector specificity.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@NuPlay
NuPlay merged commit 90d7488 into main Aug 29, 2026
2 of 3 checks passed
@NuPlay
NuPlay deleted the fix/dynamic-type-preserves-font-type branch August 29, 2026 04:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

System font variants such as monospaced don't work

2 participants