Skip to content

feat: add native modifiers for SwiftUI and Jetpack Glance - #284

Merged
V3RON merged 36 commits into
mainfrom
claude/native-modifiers-ios-android-gtimpe
Sep 28, 2026
Merged

V3RON merged 36 commits into
mainfrom
claude/native-modifiers-ios-android-gtimpe

Conversation

@V3RON

@V3RON V3RON commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

What is this?

Every Voltra component now accepts a modifiers prop that applies platform-native modifiers on top of its style. Voltra.modifiers provides 31 SwiftUI modifiers, such as widgetURL, containerBackground, privacySensitive, contentTransition and clipShape. VoltraAndroid.modifiers provides 16 Jetpack Glance modifiers, such as semantics, appWidgetBackground, background (a static color, a Material dynamic color, or a day/night pair), the size modifiers and visibility. Until now there was no way to express these from JSX, because style only covers a portable React Native subset.

const { containerBackground, widgetURL, privacySensitive } = Voltra.modifiers

<Voltra.VStack modifiers={[containerBackground('#101828'), widgetURL('myapp://portfolio')]}>
  <Voltra.Text modifiers={[privacySensitive()]}>{balance}</Voltra.Text>
</Voltra.VStack>

Passing a modifier from the other platform, or a plain object, is a TypeScript error. The design is in docs/adr/0005-native-modifiers.md. Closes #275.

How does it work?

Modifiers are small descriptors, { $type, ...params }, created by factory functions. The renderer sends the list as one JSON-encoded string prop, so none of the existing parsing layers on either platform change.

  • iOS: each element is wrapped in its modifiers in list order, around the finished styled component. Every link in the chain has the same type, so value changes between timeline entries still animate. Modifiers newer than the pod's iOS 16.4 minimum check availability and leave the component unchanged on older systems.
  • Android: modifiers are appended after style. Glance ignores their order, except that padding adds up. Dynamic color tokens resolve against the Glance theme. Glance fails the whole widget when two views carry appWidgetBackground, so the render root picks one owner from the whole tree before rendering: the first element rendered once, and none when a Scaffold already marks its own root.
  • Encapsulation: the catalogs, registries and decoding live in one folder per layer: packages/{ios,android}/src/modifiers, ios-client/ios/ui/Modifiers and android-client/.../voltra/modifiers. Host code makes one call per layer. Widget and Live Activity roots set their deep link and container background through VoltraRootDefaults.swift, instead of naming modifiers themselves. A configured deepLinkUrl or start/update tint takes precedence: the host applies it once per presentation and the tree's widgetURL or activityBackgroundTint steps aside.
  • Robustness: unknown types, unexpected parameter names, and values that do not decode are logged and skipped, so a bad descriptor never breaks a widget. An older native client ignores the new prop and renders the component without modifiers.
  • Parity: the TypeScript tests write one fixture per platform. The Swift and Kotlin tests decode every entry and require the registry and the fixture to list the same modifiers, so a modifier or parameter added on one side only fails CI.

Native modifiers are meant for Dynamic Widgets and Dynamic Live Activities. Payload widgets and pushed Live Activities accept them, but they count against the payload size limit; the new website pages say so. The modifiers prop name is now reserved on every component, like style.

Why is this useful?

Widgets can use the SwiftUI and Glance features that matter most on the Home Screen and Lock Screen: tap targets through widgetURL, removable container backgrounds for StandBy and tinted modes, privacy redaction, numeric content transitions, accessibility descriptions, and launcher background animations. None of this needs a new Voltra component or a hand-edited generated file, and adding a modifier later means one TypeScript factory, one native implementation and a fixture entry.

claude and others added 13 commits September 14, 2026 11:39
Adds ADR 0005, a proposed design for a per-platform `modifiers` prop that
carries SwiftUI and Jetpack Glance modifiers to the device for Dynamic
Widgets and Dynamic Live Activities. Covers the typed JSX API, the wire
format, Dynamic-only enforcement, the manifest-driven generator outputs,
the single insertion point on each platform, user-defined modifiers, and
a phased implementation plan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sL2XXxyKtSRDnm4EDyM9X
…rning

Drops the Dynamic-only rejection from ADR 0005. Every renderer accepts
the `modifiers` prop and emits the same wire format; the documentation
warns that native modifiers are unsafe on payload widgets and pushed Live
Activities because they count against the payload size limit, and the
existing budget check and size snapshots remain the guard rails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sL2XXxyKtSRDnm4EDyM9X
Removes the proposed modifiers.json manifest and generator target from
ADR 0005. Modifiers are written once in TypeScript and once natively, with
a fixture-based parity test between them. Factories live on the existing
Voltra and VoltraAndroid namespaces instead of a new package entry point,
and the first version drops per-component scope from the brand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sL2XXxyKtSRDnm4EDyM9X
Links ADR 0005 to #275. Moves user-defined modifiers to future work,
drops the Glance background image overload from the first version,
explains why view and text modifiers need no type-level split, spells out
the iOS ordering rule with its consequences, and rewrites the runCallback
question so it stands on its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sL2XXxyKtSRDnm4EDyM9X
Click handling from JSX is the subject of #276. The Android catalog keeps
layout, background, corner radius, visibility, semantics and
appWidgetBackground; deepLinkUrl remains the only source of a click, so
applyClickableIfNeeded stays untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sL2XXxyKtSRDnm4EDyM9X
Adds the modifiers prop, the JSON-string wire format, the Swift and Kotlin
registries with their single insertion points, three modifiers per platform,
and fixture-based parity tests (ADR 0005, #275).
Reject unexpected modifier parameters so renamed parameters fail parity,
stop root widgetURL(nil) from shadowing the widgetURL modifier, treat falsy
modifiers as none, keep $type authoritative, widen the Kotlin failure net,
add a payload-size test, and record the deviations in ADR 0005.
The home widget always set a synthetic voltraui widgetURL at the root, which
won over a widgetURL native modifier. Skip that fallback when the rendered
tree carries one, and correct the minimum iOS version in ADR 0005.
Adds 31 SwiftUI modifiers (availability-gated) and 16 Jetpack Glance
modifiers, resolves Glance theme color tokens at the composable insertion
point, documents both catalogs on the website, and adds a version plan
(ADR 0005, #275).
Let a containerBackground modifier replace the home widget's default clear
background, keep only the first appWidgetBackground per Glance widget,
document flex and Live Activity tint precedence, correct modifier docs,
link Dynamic Live Activities to the new page, and show the catalog in the
example Dynamic Widgets.
Move modifier encoding to its own core module, let widget roots ask
VoltraRootDefaults instead of naming modifiers, and move the Glance render
state and theme lookup into voltra/modifiers so VoltraRenderContext is
unchanged and resolveAndApplyStyle makes a single call.
@V3RON
V3RON force-pushed the claude/native-modifiers-ios-android-gtimpe branch from eda5e8b to 71f0f3a Compare September 14, 2026 11:55
A containerBackground modifier with an unparsable color made the Home Screen widget drop its default clear container background, and the modifier itself was then skipped, leaving an iOS 17 widget with no container background at all. The host now steps aside only for a widgetURL or containerBackground descriptor that actually applies.

@V3RON V3RON left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated high-effort review: 10 inline findings, most severe first. The Android example widget references an undefined unreadCount, which is what fails the Kotlin Test and Android build CI jobs.


Generated by Claude Code

modifiers={[
VoltraAndroid.modifiers.appWidgetBackground(),
VoltraAndroid.modifiers.background(bg),
VoltraAndroid.modifiers.semantics({ contentDescription: `Dynamic Widget demo, ${unreadCount} unread` }),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

unreadCount is never declared in this component. props.unreadCount exists, but no local const unreadCount = ... is defined like the one in the iOS demo. Rendering the widget throws ReferenceError: unreadCount is not defined. This is why prebuild fails in the [Kotlin] Test and [Android] Tarball + all-ABI native build CI jobs ("Failed to prerender Dynamic Widget "AndroidClientDemoWidget"").

Suggested change
VoltraAndroid.modifiers.semantics({ contentDescription: `Dynamic Widget demo, ${unreadCount} unread` }),
VoltraAndroid.modifiers.semantics({ contentDescription: `Dynamic Widget demo, ${props.unreadCount ?? 0} unread` }),

Generated by Claude Code

* element to ask, and again for that same element on recomposition.
*/
fun claimAppWidgetBackground(owner: Any): Boolean =
appWidgetBackgroundOwner.compareAndSet(null, owner) || appWidgetBackgroundOwner.get() === owner

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The owner of appWidgetBackground is matched by object identity (=== on the element's props map), but GlanceFactory keeps this state with remember(context, node). remember compares its keys with equals, and VoltraRenderContext, VoltraNode and VoltraElement are data classes.

Failure case: a Dynamic Widget recomposes, and renderNode(...) re-parses a tree that is structurally equal to the previous one (for example after a configuration or input flow emits the same output). remember returns the old state, which still holds the old props map. The new props map is a different instance, so compareAndSet fails and get() === owner is false. The root's appWidgetBackground is then dropped with a warning, and the launcher's open animation is lost.

The reverse also fails. A shared element (VoltraNode.Ref) that appears twice in a tree resolves to the same p instance in both places. Both views claim successfully, and Glance fails the whole widget.

Key the claim on something positional, or reset the state per composition, instead of relying on map identity.


Generated by Claude Code

@@ -0,0 +1,25 @@
const isSkippedModifier = (value: unknown) => value === undefined || value === null || value === false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The comment below says [condition && modifier] means "no modifier", but only false, null and undefined are skipped. With a numeric or string condition, which is common with counts, [unreadCount && privacySensitive()] with unreadCount = 0 yields [0]. Likewise modifiers={label && [...]} with label = '' yields ''. Both reach the throw, and the whole widget or Live Activity render fails instead of rendering without the modifier. Treat every falsy value (0, '', NaN) as skipped, the way React treats falsy children.


Generated by Claude Code

/// `widgetURL` modifier.
static func widgetURL(configured: URL?, fallback: () -> URL?, root: VoltraNode?) -> URL? {
if let configured {
return configured

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When a deepLinkUrl is configured, the host still applies .widgetURL(configured) outside the tree, and the tree's own widgetURL modifier still applies inside it. The widget then has two widgetURL modifiers. Apple documents that case as undefined behavior ("If the widget has multiple widgetURL modifiers, the behavior is undefined"), so the documented rule that "a deepLinkUrl configured for the widget takes precedence" is not guaranteed. The Live Activity hosts in VoltraWidget.swift and VoltraDynamicLiveActivityRenderer.swift have the same problem.

To make the precedence real, drop the tree's widgetURL when a configured URL exists, or skip the host's widgetURL when the tree provides one.


Generated by Claude Code

alignment="leading"
spacing={3}
style={{ flex: 1, padding: 12 }}
modifiers={[Voltra.modifiers.containerBackground('#000000'), Voltra.modifiers.widgetURL('voltra://ios/widgets')]}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

backgroundColor: '#000000' was removed from style and replaced by containerBackground, which only exists on iOS 17 and later. The pod supports iOS 16.4. On iOS 16 this widget now draws no black background, so the white title text sits on the default light widget background and is unreadable. The new docs give the opposite advice ("It needs iOS 17; on iOS 16 use style.backgroundColor instead"). Keep the style background as a fallback.


Generated by Claude Code

tag?.let { testTag = it }
}
},
"appWidgetBackground" to

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The one-per-widget dedup only knows about claims made through this modifier. Glance's Scaffold (rendered by RenderScaffold) applies appWidgetBackground() to its own root Box. If a widget uses VoltraAndroid.Scaffold and also puts appWidgetBackground() on another component (for example a child Column, as the docs recommend for "the outermost component"), two views carry the marker. The render-state claim still succeeds because nothing claimed first, and Glance fails the whole widget, which is exactly the case this guard is meant to prevent. Reserve the claim when rendering a Scaffold, or reject the modifier inside one.


Generated by Claude Code

if element.nativeModifiers.contains(where: { $0.type == type && VoltraModifierRegistry.canApply($0) }) {
return true
}
return element.children?.containsNativeModifier(type) ?? false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

containsNativeModifier walks only children. It skips nodes stored in component props, which are rendered through componentProp(...): labels of Gauge, Button, Toggle and similar components, and minimumValueLabel. A widgetURL or containerBackground modifier inside such a prop node is applied at render time, but the host doesn't see it. The host then also applies its synthetic default widgetURL (two widgetURLs, undefined behavior) or containerBackground(.clear) on top of the tree's background.


Generated by Claude Code

extension VoltraElement {
/// Descriptors decoded from the JSON-encoded `modifiers` prop.
var nativeModifiers: [VoltraModifierDescriptor] {
VoltraModifierRegistry.parseDescriptors(props?["modifiers"]?.stringValue)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a computed property. Every access rebuilds the expanded props dictionary (VoltraElement.props expands every short key each time) and runs JSONSerialization on the modifiers string. It runs in every VoltraElementView.body evaluation. VoltraHomeWidgetView.body also calls containsNativeModifier twice, which walks the whole tree, parsing each element's JSON and building each modifier through canApply. In memory- and time-limited widget extensions, that is repeated work for data that never changes after parsing. Decode the descriptors once in VoltraElement.init, as _style already is, and store them.


Generated by Claude Code

}
return ResolvedStyle(modifier, compositeStyle)
// Native modifiers go after style, so Glance keeps their value where both set the same thing.
return ResolvedStyle(styledModifier.applyNativeModifiers(props), compositeStyle)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

resolveAndApplyStyle runs several times per element. For each child of a Row or Column it runs in extractWeightFromChild (only for the weight), again in RenderChildWithWeight, and a third time in the child renderer: RenderColumn, RenderRow, RenderBox, RenderSpacer, RenderTitleBar and RenderScaffold call it unconditionally even when modifier is passed in. Each call now JSON-parses the modifiers string with kotlinx, builds every Glance modifier, and logs any invalid descriptor again (three warnings per bad entry). The weight lookup also performs an appWidgetBackground claim as a side effect. Parse the descriptors once per element, for example cached on the model, and keep native modifiers out of the weight-only path.


Generated by Claude Code

Comment thread packages/android/src/modifiers/index.ts Outdated
*
* @since Android 7.0
*/
export const background = (color: AndroidColorValue | DayNightColors) =>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The TypeScript contract accepts inputs that the native side always rejects, so they are silently dropped at runtime. DayNightColors types day and night as string, so background({ day: AndroidDynamicColors.primary, night: AndroidDynamicColors.inversePrimary }) type-checks, but Kotlin throws "Invalid day" because only static colors are allowed. Likewise semantics({}) type-checks, but the native side throws "semantics needs contentDescription or testTag". Narrow the types (exclude AndroidDynamicColorToken from DayNightColors, and require at least one semantics key) so these become compile-time errors.


Generated by Claude Code

The Android demo widget read an undeclared unreadCount, which threw during prerender and failed the Android CI builds. The iOS demo relied on containerBackground alone, which needs iOS 17, so its white text was unreadable on iOS 16.
A condition such as count && privacySensitive() with count = 0, or label && [...] with an empty label, reached the descriptor check and failed the whole render. Falsy values now mean no modifier, the way React skips falsy children.
The owner was claimed at render time by props-map identity, so a structurally equal tree kept the old owner and dropped the marker, a shared element rendered twice claimed it twice, and a Scaffold's own marker was not counted. The render root now picks the first element rendered once, none next to a Scaffold, and matches it by value.

Native modifiers are decoded once per element and built once per rendered element: renderers resolve style and modifiers only when no modifier is passed in, and the weight lookup reads style alone. The TypeScript types reject dynamic color tokens in background day/night pairs and an empty semantics call, which the device always skipped.
A configured deep link now suppresses the tree's widgetURL modifiers through an environment value, so a widget or Live Activity never carries two widgetURLs, which Apple leaves undefined. The host checks for widgetURL and containerBackground also look through nodes stored in component props, such as Gauge labels. Descriptors are decoded once in VoltraElement.init instead of on every body evaluation.
The renderer skips every falsy value, and the ADR shows [count && modifier], but the prop was typed as a plain modifier list, so typed code could not use the pattern. NativeModifiersProp from core now allows falsy entries and a falsy whole value.
VoltraText and the text style always set multilineTextAlignment, defaulting to leading, and SwiftUI uses the setting closest to the view, so the native modifier never had an effect. Alignment is now set only when textAlign or the multilineTextAlignment prop is given.
A configured deep link is now set once on the Dynamic Island, the default for every region, and once on each Lock Screen presentation, including the iOS 18 small family, instead of on every region. A tint passed when starting or updating a Live Activity now suppresses the tree's activityBackgroundTint the way a configured deep link suppresses widgetURL. The widgetURL modifier resolves paths like deepLinkUrl, prop nodes are parsed once with their element, and the parity tests compare parameter names.
Also documents that testTag is invisible outside Glance's test APIs and how appWidgetBackground picks its owner.
…ng renderers

Chart, ArcProgressIndicator and bitmap Text appended their own width and height after the native modifiers, and Glance keeps the last value, so size modifiers had no effect on them. They now size from the modifiers first and style second. Renderers that hand a description to a Glance Image or icon button pass the semantics modifier's contentDescription, which Glance would otherwise replace.

An Image fallback now counts toward the appWidgetBackground owner only when the Image has no source, since it is not drawn otherwise, and the owner is computed once per tree.
…allback

Round 3 skipped the fallback of an Image with a source, so a component used both as a sibling and as that fallback counted once and became the owner; when the image failed to load, both copies were marked and Glance failed the widget. Every fallback now counts toward uniqueness, and one that may not be drawn is never picked.

Only the last semantics modifier counts, as in Glance. The icon buttons go back to passing their own description, since Glance keeps the modifier's on the tappable view, and the arc's inner image gets none when the modifier describes the arc, so it is not read twice. A weight decides a Chart's height, so its bitmap ignores a native height then.
It is drawn only if the source fails to load, so a containerBackground or widgetURL inside it no longer makes the widget drop a default it then lacks.
VoltraViewRoot replaced its hosting controller on every payload, so SwiftUI never diffed the old and new trees: animation, transition and contentTransition modifiers had nothing to animate in the app, and view state such as a timer's default start time reset on every update. It now keeps one hosting controller and replaces its rootView.
An insertion or removal is animated by the parent, so animation on the component that appears has nothing to animate, and children without an id are matched by position.
A tap on the compact or minimal Dynamic Island opens only DynamicIsland.widgetURL,
which the hosts set from the configured deep link. E2E testing on iOS 18 showed
that a widgetURL inside the compact regions is ignored, so log a warning and
document deepLinkUrl as the way to set where an island tap opens.
@V3RON

V3RON commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

E2E results

An agent ran four E2E rounds on 2026-09-27 and 2026-09-28, using agent-device on:

  • the iOS 18.0 simulator (iPhone 16 Pro);
  • Android emulators on API 35, API 36 and API 24.

It tested in-app previews in a playground screen, plus Home Screen widgets, launcher widgets and Live Activities. The playground and the variant controls live on a local branch that is not part of this PR.

Round 1: broad coverage (e9be7a5e)

Group Pass Fail Blocked or skipped
Invalid input is skipped and logged, never crashes (iOS and Android) 4 0 0
iOS in-app modifiers 4 1 0
iOS Home Screen widget 5 0 2
Live Activities 3 0 0
Android in-app modifiers 8 0 2
Android launcher widget 2 0 2
  • Failure: animation, contentTransition('numericText') and transition did not animate in an in-app VoltraView, because every update replaced the hosting controller. Fixed in fc684f0: the controller is reused and its rootView is updated in place.
  • Blocked or skipped:
    • the iOS 16 and 17 repeats (no runtimes installed);
    • the API 24 repeats (the emulator had no disk space);
    • the Chart height under flex;
    • a launcher widget tap (the demo widget has no tap target).

Round 2: follow-up (fc684f04)

  • Passed:
    • appWidgetBackground ownership on the launcher (H2a–d): the widget renders, and logcat has no "At most one view can be set as AppWidgetBackground". This includes the case where a shared element is both a child and an Image fallback.
    • A Chart with flex: 1 ignores a height modifier.
    • The VoltraView regression checks: previews update from state, a timer progress bar keeps advancing across unrelated re-renders, and sizes are right after navigation and rotation.
    • The in-app numericText roll now animates.
  • Failed: the in-app transition('slide') case. The cause was the test layout: animation sat on the inserted child, and the siblings had no id. SwiftUI animates an insertion from a parent that persists, so the docs and JSDoc now say so (b8cd922).
  • Blocked: the iOS widget tests. WidgetKit stopped reloading timelines (CHSErrorDomain 1050), which round 3 traced to a test build without entitlements. The Live Activity and API 24 tests did not run.

Round 3: remaining tests (b8cd922d plus the harness)

Area Result
widgetURL with a path (/ios/widgets) resolves against the app's scheme Pass
containerBackground: none in the tree, only inside a Gauge label, only inside an Image fallback that is not drawn, and with an invalid color Pass: no "Please adopt containerBackground API" placeholder in any case. The label counts, the undrawn fallback does not, and the invalid color is logged
numericText across widget updates The number updates. Whether it rolls or jumps is WidgetKit's choice and was not judged
activityBackgroundTint: the modifier alone, and the modifier plus a start option Pass: the modifier applies alone; the start option wins over it
Lock Screen tap: tree widgetURL, and tree widgetURL plus deepLinkUrl Pass: the tree URL applies alone; the configured link wins
In-app slide transition, rebuilt with animation on the parent and ids on the children Pass, with the old layout as a control (no animation, as documented)
API 24: cornerRadius in-app, and the launcher widget's semantics Pass: square corners with no crash; content-desc "Dynamic Widget demo, 0 unread"
Dynamic Island tap Inconclusive; see round 4

Round 4: Dynamic Island taps

Each tap was on the compact island, with the device unlocked and the app in the background. SpringBoard was in the log stream.

URL source Leading tap Trailing tap
deepLinkUrl, set on the DynamicIsland Opens the URL Opens the URL
widgetURL on both compact regions Opens the app, no URL Opens the app, no URL

WidgetKit reads the regions' widgetURL, but a tap on the island only uses DynamicIsland.widgetURL. Using the tree's URL for the island would mean running a dynamic definition outside any view environment. So d255cf1 logs a warning when a compact or minimal region sets widgetURL with no deep link configured, and the docs point to deepLinkUrl.

Changes that came out of E2E

  • fc684f0: in-app VoltraView updates in place, so animations and transitions work.
  • b8cd922: transition docs say where animation goes.
  • d255cf1: warning and docs for widgetURL in the compact or minimal Dynamic Island.

Not covered

  • iOS 16 and 17 (no simulator runtimes installed).
  • Physical devices. If a real iPhone honors a compact region's widgetURL, the new warning is too strict.
  • The minimal Dynamic Island presentation. The warning covers it because Apple groups it with the compact presentation.
  • activitySystemActionForegroundColor: the demo Live Activity has no system action button.

@V3RON
V3RON merged commit a268ec2 into main Sep 28, 2026
15 checks passed
@V3RON
V3RON deleted the claude/native-modifiers-ios-android-gtimpe branch September 28, 2026 13:07
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.

Native modifiers: expose SwiftUI and Jetpack Glance modifiers from JSX

2 participants