Skip to content

feat(android): render branded block icons with contrast-aware tinting#468

Merged
jkmassel merged 4 commits intojkmassel/android-block-pickerfrom
jkmassel/android-svg-icons
Apr 29, 2026
Merged

feat(android): render branded block icons with contrast-aware tinting#468
jkmassel merged 4 commits intojkmassel/android-block-pickerfrom
jkmassel/android-svg-icons

Conversation

@jkmassel
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel commented Apr 23, 2026

Stacks on top of #461 (native block inserter). Renders block icons via AndroidSVG, with contrast-aware tinting so branded icons read correctly against either a light or dark dialog surface.

This PR is two related slices that land together — splitting them would just be churn:

  • Render SVG icons (commit a315e2f) — adopts AndroidSVG (com.caverock:androidsvg-aar:1.4) and mirrors BlockIconCache on iOS. Handles three @wordpress/icons quirks the browser papers over with CSS: missing viewBox (e.g. core/site-tagline), root fill="none" (e.g. core/icon), and multi-fill branded icons that would flatten under a PorterDuff tint.
  • Contrast-aware tinting (commit a6accad) — addresses the "known limitation" called out in the first commit. Pipes icon.foreground through the JS bridge so single-colour brand icons (Pocket Casts red) render correctly, and adds a 3:1 WCAG contrast check so low-contrast brands (X #000000, Dailymotion #333436, WordPress Embed #0073AA) fall back to the theme tint instead of disappearing into the surface.

Screenshots

Pixel 9 Pro XL. The embed section is the interesting one — it exercises every branch of the tinting decision (monochrome, multi-colour branded, single-colour branded with good/bad contrast).

Embeds

Dark Light
dark-embeds light-embeds

In dark mode, WordPress (#0073AA), X (#000000), and Dailymotion (#333436) fall back to the theme tint — all three fail the 3:1 contrast check against the chip surrogate. In light mode the same colours pass and render as-is. Pocket Casts / Reddit / Pinterest / Spotify / YouTube / Vimeo keep their brand colours in both modes (multi-colour or sufficient contrast).

Most used

Dark Light
dark-most-used light-most-used

Bridge

getBlockIcon at src/utils/blocks.js:44 previously dropped icon.foreground — the JS metadata that the web editor applies as CSS color, which paths inside the SVG pick up via currentColor. Adds getBlockIconForeground and includes it in the serialised inserter payload, with matching iconForeground: String? fields on the Android BlockType data class and the iOS BlockType struct (iOS gets the field for parity; no rendering change).

Chip background

Wraps each icon in a 44dp RoundedRectangle chip filled with ~12% of the theme's primary text colour, mirroring the iOS BlockIconView treatment (Color(.label).mask over a secondarySystemFill chip). Without this, low-contrast brand icons rendered as near-invisible smudges on the dark bottom sheet.

Tinting decision

SvgIconCache.shouldTint decides per-icon whether to apply the theme tint:

  1. No declared colours — pure monochrome; tint to text colour.
  2. Multiple declared colours (Pocket Casts red+white, Animoto, Bluesky) — render as-is. A PorterDuff SRC_IN tint would flatten internal contrast into a silhouette.
  3. Single declared colour — keep it if it has at least 3:1 contrast (WCAG 2.x SC 1.4.11 — minimum for UI graphics) against the surface the icon actually sits on; otherwise strip the brand colour and tint.

The contrast reference is the chip fill composited over the dialog surface, not the bare surface. Measuring against bare black makes marginal colours like WordPress blue (#0073AA) appear to pass at 3.16:1 while reading as dim against the actual ~#3B3B3D chip surrogate (2.1:1). resolveDialogSurface() looks up ?attr/colorSurface then ?android:attr/colorBackground so the surrogate matches what the user sees.

Test plan

  • ./gradlew :Gutenberg:test detekt :Gutenberg:lintDebug :app:compileDebugKotlin — BUILD SUCCESSFUL
  • Manually verified on Pixel 9 Pro XL with enableNativeBlockInserter on, dark theme: Pocket Casts renders red+white, Animoto/Bluesky render branded, X / Dailymotion / WordPress Embed render white (tint fallback), Site Tagline / Icon block still render correctly.
  • Manually verified on Pixel 9 Pro XL, light theme: WordPress / X / Dailymotion render in brand colour (pass contrast against the light chip), branded multi-colour icons unchanged.
  • Reviewer: verify iOS behaviour unchanged.

Related

@github-actions github-actions Bot added the [Type] Enhancement A suggestion for improvement. label Apr 23, 2026
@jkmassel jkmassel force-pushed the jkmassel/android-block-picker branch from 88d07a8 to 932e7ed Compare April 24, 2026 16:48
@jkmassel jkmassel force-pushed the jkmassel/android-svg-icons branch from ae080ac to f19065a Compare April 24, 2026 16:51
@jkmassel jkmassel force-pushed the jkmassel/android-block-picker branch from 932e7ed to 9eaf0d5 Compare April 27, 2026 20:12
@jkmassel jkmassel force-pushed the jkmassel/android-svg-icons branch from f19065a to 8fb72a1 Compare April 27, 2026 20:26
Adopts AndroidSVG (com.caverock:androidsvg-aar:1.4) — the same rendering
engine Coil-SVG wraps, used directly to avoid pulling in Coil — and wires
it into the block inserter dialog introduced in #461. Mirrors
`BlockIconCache` on iOS: parse each block's inline SVG once, keyed by
`BlockType.id`, and cache the rendered bitmap so RecyclerView rebinds
don't re-render on scroll.

Three @wordpress/icons patterns the browser handles via CSS that
AndroidSVG does not:

1. **Missing `viewBox`** (e.g. `core/site-tagline`) — intrinsic
   width/height are declared but paths render at native coordinate size
   inside our larger viewport, so the icon appears tiny. Synthesise a
   viewBox from the intrinsic dimensions and set document width/height
   to 100%.
2. **`fill="none"` at root** (e.g. `core/icon`) — paths without an
   explicit fill inherit `none` and render invisibly. The web editor's
   `@wordpress/components` CSS injects `fill: currentColor`; we do the
   same via `RenderOptions.css` at render time.
3. **Multi-fill branded icons** (e.g. Pocket Casts, Animoto) — these
   rely on colour contrast between inner/outer paths. A monochrome
   PorterDuff SRC_IN tint flattens them into a silhouette. Detect hex
   fills in the raw SVG string and skip the tint when present, letting
   branded icons render as-is.

Pocket Casts still renders black-and-white rather than brand red — its
foreground colour lives in JS metadata (`icon.foreground`) that
`getBlockIcon` at `src/utils/blocks.js:44` drops. Fixing this properly
requires piping `foreground` through the bridge and is deferred to a
follow-up.

- [x] `./gradlew :Gutenberg:test detekt :Gutenberg:lintDebug :app:compileDebugKotlin` — BUILD SUCCESSFUL
- [x] Manually verified on Pixel 9 Pro XL with `enableNativeBlockInserter` on: open inserter, scroll through all sections, confirmed Site Tagline (no viewBox), Icon block (root `fill="none"`), and Pocket Casts/Animoto/Bluesky (branded colours) all render correctly.
- [ ] Reviewer: verify iOS behaviour unchanged.
@jkmassel jkmassel force-pushed the jkmassel/android-svg-icons branch from 8fb72a1 to 92f7c65 Compare April 28, 2026 18:16
Follow-up to #461 / 444c640 that addresses the "known limitation" called
out in that commit: branded icons (Pocket Casts, Animoto, Bluesky) and
single-colour brand foregrounds (X, Dailymotion, WordPress Embed) now
render correctly against either light or dark dialog surfaces.

## Bridge

`getBlockIcon` at `src/utils/blocks.js:44` previously dropped
`icon.foreground` — the JS metadata that the web editor applies as CSS
`color`, which paths inside the SVG pick up via `currentColor`. Adds
`getBlockIconForeground` and includes it in the serialised inserter
payload, with matching `iconForeground: String?` fields on the Android
`BlockType` data class and the iOS `BlockType` struct (iOS gets the
field for parity; no rendering change).

## Chip background

Wraps each icon in a 44dp `RoundedRectangle` chip filled with ~12% of
the theme's primary text colour, mirroring the iOS `BlockIconView`
treatment (`Color(.label).mask` over a `secondarySystemFill` chip).
Without this, low-contrast brand icons (X's `#000000`, Dailymotion's
`#333436`) rendered as near-invisible smudges on the dark bottom sheet.

## Tinting decision

`SvgIconCache.shouldTint` decides per-icon whether to apply the theme
tint:

1. **No declared colours** — pure monochrome; tint to text colour.
2. **Multiple declared colours** (Pocket Casts red+white, Animoto,
   Bluesky) — render as-is. A PorterDuff SRC_IN tint would flatten
   internal contrast into a silhouette.
3. **Single declared colour** — keep it if it has at least 3:1 contrast
   (WCAG 2.x SC 1.4.11 — minimum for UI graphics) against the surface
   the icon actually sits on; otherwise strip the brand colour and tint.

The contrast reference is the chip fill **composited over the dialog
surface**, not the bare surface. Measuring against bare black makes
marginal colours like WordPress blue (#0073AA) appear to pass at 3.16:1
while reading as dim against the actual ~`#3B3B3D` chip surrogate
(2.1:1). `resolveDialogSurface()` looks up `?attr/colorSurface` then
`?android:attr/colorBackground` so the surrogate matches what the user
sees.

## Test plan

- [x] `./gradlew :Gutenberg:test detekt :Gutenberg:lintDebug :app:compileDebugKotlin` — BUILD SUCCESSFUL
- [x] Manually verified on Pixel 9 Pro XL with `enableNativeBlockInserter` on, dark theme: Pocket Casts renders red+white, Animoto/Bluesky render branded, X / Dailymotion / WordPress Embed render white (tint fallback), Site Tagline / Icon block still render correctly.
- [ ] Reviewer: verify in light theme; verify iOS behaviour unchanged.
@jkmassel jkmassel force-pushed the jkmassel/android-svg-icons branch from 92f7c65 to a6accad Compare April 28, 2026 18:21
@jkmassel jkmassel marked this pull request as ready for review April 28, 2026 18:44
@jkmassel jkmassel requested a review from adalpari April 28, 2026 18:45
jkmassel added a commit that referenced this pull request Apr 28, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
jkmassel added a commit that referenced this pull request Apr 28, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
jkmassel added a commit that referenced this pull request Apr 28, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
jkmassel added a commit that referenced this pull request Apr 28, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
@adalpari
Copy link
Copy Markdown

It looks good, but Claude found these three points I believe woult be gret to review:

Issues                    
        
- resolveTextColorPrimary has no fallback — can throw NullPointerException
                                                                                                                                                                                  
  BlockInserterDialog.kt:310–314                                                                                                                                                  
                                                                                                                                                                                  
  private fun Context.resolveTextColorPrimary(): Int {                                                                                                                            
      val typed = TypedValue()
      theme.resolveAttribute(android.R.attr.textColorPrimary, typed, true)
      return resources.getColorStateList(typed.resourceId, theme).defaultColor
  }                                                                                                                                                                               
   
  resolveAttribute can return false (attribute absent), leaving typed.resourceId == 0. getColorStateList(0, …) throws. resolveDialogSurface next to it handles this correctly with
   a for loop + fallback. This should do the same.
                                                                                                                                                                                  
  ---                       
- BlockViewHolder uses positional child access — fragile
                                                                                                                                                                                  
  BlockInserterDialog.kt:146–149
                                                                                                                                                                                  
  private val icon = (container.getChildAt(0) as FrameLayout).getChildAt(0) as ImageView
  private val textContainer = container.getChildAt(1) as LinearLayout                                                                                                             
  private val title = textContainer.getChildAt(0) as TextView
  private val description = textContainer.getChildAt(1) as TextView                                                                                                               
                            
  Any future change to the view hierarchy (e.g. inserting a badge or reordering children) silently breaks binding or crashes at runtime. The standard approach is to hold         
  references directly when building the view in buildBlockView/buildIconChip, then pass them into the ViewHolder constructor.
                                                                                                                                                                                  
  ---                       
- rightMargin used instead of marginEnd — breaks RTL layouts
                                                               
  BlockInserterDialog.kt:242
                                                                                                                                                                                  
  layoutParams = LinearLayout.LayoutParams(chipSizePx, chipSizePx).apply {
      rightMargin = rightMarginPx                                                                                                                                                 
  }                         
                                                                                                                                                                                  
  rightMargin is physical; marginEnd is layout-direction-aware. On RTL devices the icon chip should trail the text, not lead it. Use MarginLayoutParamsCompat.setMarginEnd() or   
  layoutParams.marginEnd = rightMarginPx.
                                                      

jkmassel added a commit that referenced this pull request Apr 29, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
Three fixes from #468 review:

- **resolveTextColorPrimary fallback** — `resolveAttribute` can return
  false (attribute absent) or leave `typed.resourceId == 0`, in which
  case `getColorStateList(0, …)` throws. Mirror the pattern already used
  by `resolveDialogSurface` and fall back to `Color.BLACK`.
- **BlockViewHolder positional access** — `(container.getChildAt(0) as
  FrameLayout).getChildAt(0) as ImageView` silently breaks if the view
  hierarchy is reordered. `buildBlockView` now returns a `BlockRowViews`
  struct holding direct references; the ViewHolder takes that struct
  rather than the root `View`.
- **rightMargin → marginEnd** — `rightMargin` is physical, `marginEnd`
  is layout-direction-aware so the chip leads correctly under RTL.

Verified manually on Pixel 9 Pro XL: most-used and embed sections still
render correctly with chip-backed icons.
Copy link
Copy Markdown

@adalpari adalpari left a comment

Choose a reason for hiding this comment

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

Thank you for the changes! LGTM!

…imary

The Color.BLACK fallback only triggered when theme.resolveAttribute
returned false — i.e. the host app's theme genuinely doesn't define
android.R.attr.textColorPrimary, which has been part of the framework
since API 1. Standard theme parents (AppCompat, Material*, even bare
@android:style/Theme) all define it. Bailing out a host that's already
broken in many other ways isn't our job.

Keep the typed.resourceId != 0 branch — that handles legitimate inline
literal themes (`<item name="...">#DD000000</item>`), which is real
real-world theme construction, not misconfiguration.
@jkmassel jkmassel merged commit dfaf054 into jkmassel/android-block-picker Apr 29, 2026
14 checks passed
@jkmassel jkmassel deleted the jkmassel/android-svg-icons branch April 29, 2026 17:36
jkmassel added a commit that referenced this pull request Apr 29, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
jkmassel added a commit that referenced this pull request Apr 29, 2026
…#468)

* feat(android): render SVG icons in block inserter

Adopts AndroidSVG (com.caverock:androidsvg-aar:1.4) — the same rendering
engine Coil-SVG wraps, used directly to avoid pulling in Coil — and wires
it into the block inserter dialog introduced in #461. Mirrors
`BlockIconCache` on iOS: parse each block's inline SVG once, keyed by
`BlockType.id`, and cache the rendered bitmap so RecyclerView rebinds
don't re-render on scroll.

Three @wordpress/icons patterns the browser handles via CSS that
AndroidSVG does not:

1. **Missing `viewBox`** (e.g. `core/site-tagline`) — intrinsic
   width/height are declared but paths render at native coordinate size
   inside our larger viewport, so the icon appears tiny. Synthesise a
   viewBox from the intrinsic dimensions and set document width/height
   to 100%.
2. **`fill="none"` at root** (e.g. `core/icon`) — paths without an
   explicit fill inherit `none` and render invisibly. The web editor's
   `@wordpress/components` CSS injects `fill: currentColor`; we do the
   same via `RenderOptions.css` at render time.
3. **Multi-fill branded icons** (e.g. Pocket Casts, Animoto) — these
   rely on colour contrast between inner/outer paths. A monochrome
   PorterDuff SRC_IN tint flattens them into a silhouette. Detect hex
   fills in the raw SVG string and skip the tint when present, letting
   branded icons render as-is.

Pocket Casts still renders black-and-white rather than brand red — its
foreground colour lives in JS metadata (`icon.foreground`) that
`getBlockIcon` at `src/utils/blocks.js:44` drops. Fixing this properly
requires piping `foreground` through the bridge and is deferred to a
follow-up.

- [x] `./gradlew :Gutenberg:test detekt :Gutenberg:lintDebug :app:compileDebugKotlin` — BUILD SUCCESSFUL
- [x] Manually verified on Pixel 9 Pro XL with `enableNativeBlockInserter` on: open inserter, scroll through all sections, confirmed Site Tagline (no viewBox), Icon block (root `fill="none"`), and Pocket Casts/Animoto/Bluesky (branded colours) all render correctly.
- [ ] Reviewer: verify iOS behaviour unchanged.

* feat(android): contrast-aware tinting for branded block icons

Follow-up to #461 / 444c640 that addresses the "known limitation" called
out in that commit: branded icons (Pocket Casts, Animoto, Bluesky) and
single-colour brand foregrounds (X, Dailymotion, WordPress Embed) now
render correctly against either light or dark dialog surfaces.

`getBlockIcon` at `src/utils/blocks.js:44` previously dropped
`icon.foreground` — the JS metadata that the web editor applies as CSS
`color`, which paths inside the SVG pick up via `currentColor`. Adds
`getBlockIconForeground` and includes it in the serialised inserter
payload, with matching `iconForeground: String?` fields on the Android
`BlockType` data class and the iOS `BlockType` struct (iOS gets the
field for parity; no rendering change).

Wraps each icon in a 44dp `RoundedRectangle` chip filled with ~12% of
the theme's primary text colour, mirroring the iOS `BlockIconView`
treatment (`Color(.label).mask` over a `secondarySystemFill` chip).
Without this, low-contrast brand icons (X's `#000000`, Dailymotion's
`#333436`) rendered as near-invisible smudges on the dark bottom sheet.

`SvgIconCache.shouldTint` decides per-icon whether to apply the theme
tint:

1. **No declared colours** — pure monochrome; tint to text colour.
2. **Multiple declared colours** (Pocket Casts red+white, Animoto,
   Bluesky) — render as-is. A PorterDuff SRC_IN tint would flatten
   internal contrast into a silhouette.
3. **Single declared colour** — keep it if it has at least 3:1 contrast
   (WCAG 2.x SC 1.4.11 — minimum for UI graphics) against the surface
   the icon actually sits on; otherwise strip the brand colour and tint.

The contrast reference is the chip fill **composited over the dialog
surface**, not the bare surface. Measuring against bare black makes
marginal colours like WordPress blue (#0073AA) appear to pass at 3.16:1
while reading as dim against the actual ~`#3B3B3D` chip surrogate
(2.1:1). `resolveDialogSurface()` looks up `?attr/colorSurface` then
`?android:attr/colorBackground` so the surrogate matches what the user
sees.

- [x] `./gradlew :Gutenberg:test detekt :Gutenberg:lintDebug :app:compileDebugKotlin` — BUILD SUCCESSFUL
- [x] Manually verified on Pixel 9 Pro XL with `enableNativeBlockInserter` on, dark theme: Pocket Casts renders red+white, Animoto/Bluesky render branded, X / Dailymotion / WordPress Embed render white (tint fallback), Site Tagline / Icon block still render correctly.
- [ ] Reviewer: verify in light theme; verify iOS behaviour unchanged.

* refactor(android): address review feedback in BlockInserterDialog

Three fixes from #468 review:

- **resolveTextColorPrimary fallback** — `resolveAttribute` can return
  false (attribute absent) or leave `typed.resourceId == 0`, in which
  case `getColorStateList(0, …)` throws. Mirror the pattern already used
  by `resolveDialogSurface` and fall back to `Color.BLACK`.
- **BlockViewHolder positional access** — `(container.getChildAt(0) as
  FrameLayout).getChildAt(0) as ImageView` silently breaks if the view
  hierarchy is reordered. `buildBlockView` now returns a `BlockRowViews`
  struct holding direct references; the ViewHolder takes that struct
  rather than the root `View`.
- **rightMargin → marginEnd** — `rightMargin` is physical, `marginEnd`
  is layout-direction-aware so the chip leads correctly under RTL.

Verified manually on Pixel 9 Pro XL: most-used and embed sections still
render correctly with chip-backed icons.

* refactor(android): drop over-defensive fallback in resolveTextColorPrimary

The Color.BLACK fallback only triggered when theme.resolveAttribute
returned false — i.e. the host app's theme genuinely doesn't define
android.R.attr.textColorPrimary, which has been part of the framework
since API 1. Standard theme parents (AppCompat, Material*, even bare
@android:style/Theme) all define it. Bailing out a host that's already
broken in many other ways isn't our job.

Keep the typed.resourceId != 0 branch — that handles legitimate inline
literal themes (`<item name="...">#DD000000</item>`), which is real
real-world theme construction, not misconfiguration.
jkmassel added a commit that referenced this pull request Apr 29, 2026
#468 added `iconForeground: getBlockIconForeground(item)` to the block
payload, but the schema (added in #467, in trunk) still has
`additionalProperties: false` on `BlockType` with no `iconForeground`
entry. Trunk doesn't emit the field so trunk CI is fine; this branch
carries both pieces and AJV rejects the payload.

Mirrors the existing `icon` entry: nullable string.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants