Skip to content

feat(portable_text): per-subtree configuration via PortableTextTheme - #57

Open
Ortes wants to merge 2 commits into
vyuh-tech:mainfrom
Fullphysio:feat/per-subtree-portable-text-config
Open

feat(portable_text): per-subtree configuration via PortableTextTheme#57
Ortes wants to merge 2 commits into
vyuh-tech:mainfrom
Fullphysio:feat/per-subtree-portable-text-config

Conversation

@Ortes

@Ortes Ortes commented Jun 3, 2026

Copy link
Copy Markdown

Summary

Adds support for rendering different Portable Text subtrees with different configurations, instead of every PortableText widget sharing the single global PortableTextConfig.shared. Mirrors Flutter's Theme pattern and is fully backward-compatible.

Motivation

PortableTextConfig.shared is a process-wide singleton, so every Portable Text surface in an app must share one visual configuration. Apps that render Portable Text in multiple contexts (e.g. a full article body vs. compact card descriptions) can't vary styling per surface without mutating the global config.

What's added

  • PortableTextConfig.of(context) — resolves the nearest config from the widget tree, falling back to shared when none is present (like Theme.of / ThemeData.fallback).
  • PortableTextConfig.copyWith(...) — returns a derived config. Map fields (styles, blocks, blockContainers, markDefs) are merged (provided keys win, the rest are kept); scalar fields are replaced. This intentionally differs from ThemeData.copyWith (which replaces whole fields) because these maps are additive registries.
  • PortableTextTheme — an InheritedWidget that supplies a PortableTextConfig to its subtree.
  • PortableTextStyleOverride — an ergonomic helper: pass per-style TextStyle → TextStyle deltas (e.g. {'normal': (s) => s.copyWith(fontSize: s.fontSize! - 2)}); it wraps the inherited builders internally, so callers express only the delta and there's no recursion.
  • A public default constructor on PortableTextConfig so additional configs can be created.

The render paths (PortableTextBlock, defaultListBuilder, defaultBulletRenderer) now resolve their config via PortableTextConfig.of(context) instead of reading .shared directly.

Backward compatibility

Non-breaking. With no PortableTextTheme ancestor, of(context) returns shared, so existing apps render identically.

Note on mark deserialization

_markDefsFromJson runs at JSON-parse time with no BuildContext, so it intentionally keeps reading the context-free shared.markDefs registry to choose custom mark deserializers. Mark styling is still resolved per subtree at render time. This is documented inline.

Add PortableTextConfig.of(context) + copyWith and a PortableTextTheme InheritedWidget (plus PortableTextStyleOverride) so a subtree can override the rendering config without mutating the global shared instance. Render paths resolve the config from context, falling back to shared. Mark deserialization stays on the context-free shared registry.
Creating a fresh PortableTextConfig on every build made
PortableTextTheme.updateShouldNotify (identity comparison) always
return true, forcing all descendant Portable Text to rebuild on any
ancestor rebuild. Cache the derived config and recompute only when the
inherited config or the override map changes, keeping it identity-stable.

Also document that copyWith(markDefs:) affects rendering only, not
parse-time deserializer selection.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant