Skip to content

feat: resolvable dynamic values for widgets#130

Open
V3RON wants to merge 7 commits intomainfrom
feat/dynamic-values
Open

feat: resolvable dynamic values for widgets#130
V3RON wants to merge 7 commits intomainfrom
feat/dynamic-values

Conversation

@V3RON
Copy link
Copy Markdown
Contributor

@V3RON V3RON commented Apr 15, 2026

What is this?

Widgets and Live Activities render a payload that is computed once on the JS side and then handed off to the native layer. This works well for static content, but breaks down for properties that depend on the rendering environment — things like whether iOS is showing your widget in accented or full-color mode, or which Material color tokens Android is currently using for its dynamic theme.

Previously, the only way to handle these was to register multiple pre-rendered variants and hope the right one was picked, or to ignore the platform context entirely and hard-code colors. Neither is a real solution.

How does it work?

This PR introduces resolvable values — expressions that are embedded directly in a widget payload and evaluated by the native layer at render time, against the actual runtime environment. The JS side describes what the value should be, and the native side resolves what it actually is at the moment the widget is drawn.

The API is a small set of composable primitives exported from voltra:

import { env, when, match, eq, and, or, not, inList } from 'voltra'

// Read an environment value directly
color: env.renderingMode   // resolves to 'accented' | 'fullColor' | 'vibrant'

// Conditional expression
color: when(eq(env.renderingMode, 'accented'), '#F9FAFB', '#0F172A')

// Multi-branch match
color: match(env.renderingMode, {
  accented: '#F9FAFB',
  fullColor: '#0F172A',
  default: '#FDF2F8',
})

These can be used anywhere a style property or prop value is accepted. The expressions are serialized into the widget payload as compact opcode tuples and resolved natively — no JS re-render, no round trip.

Supported environment keys

iOS:

  • renderingMode'accented', 'fullColor', or 'vibrant'
  • showsWidgetContainerBackgroundtrue or false

Android:

  • All Material You dynamic color tokens (primary, onPrimary, surface, onSurface, etc.)

Why is this useful?

  • Correct accented mode colors — iOS can tint a widget with the user's accent color. Without resolvable values, text and borders are hard-coded and look wrong in accented or vibrant mode. With when(eq(env.renderingMode, 'accented'), ...) you adapt per-render.
  • Correct background-aware layoutsshowsWidgetContainerBackground tells you whether the system is drawing a widget background. You can conditionally add padding or a border only when there is no background.
  • No extra JS renders — Because resolution happens natively, the widget reacts to environment changes (e.g. switching between dark/accented mode in the system settings) without needing a new JS-side payload push.

Example: iOS playground widget

example/widgets/ios/IosResolvablePlaygroundWidget.tsx demonstrates the full API in a real widget. It renders a grid of indicators that highlight which rendering mode and background state are currently active — each indicator's border width and color is driven entirely by resolvable expressions:

const labelByMode = when(
  eq(env.renderingMode, 'accented'), '#CBD5E1',
  when(eq(env.renderingMode, 'fullColor'), '#475569', '#FBCFE8')
)

borderWidth: when(eq(env.renderingMode, 'accented'), 2, 1),
borderColor: when(eq(env.renderingMode, 'accented'), '#F9FAFB', labelByMode),

The widget is registered under the resolvable_playground widget ID and all three size variants (systemSmall, systemMedium, systemLarge) are exported as resolvablePlaygroundVariants.

🤖 Generated with Claude Code

V3RON and others added 7 commits April 14, 2026 10:15
Introduce the resolvable expression API, payload normalization and serialization
in @use-voltra/core, wire it through the renderer and stylesheet registry, and
implement evaluation on iOS (parser, evaluator, payload migration). Add Swift
and Node tests, an example iOS resolvable playground widget, and use a
realm-safe plain-object check so Expo prerender works with VM-evaluated styles.

Made-with: Cursor
Replace AndroidDynamicColors (~ string literals) with the same env/when/match
API as iOS. Extend RESOLVABLE_ENV_IDS with Material role keys; authors use
env.primary and siblings from voltra/android.

Android parses payloads through a Kotlin resolvable evaluator after
decompression; Material env ids resolve to existing ~ tokens for JSColorParser
and GlanceTheme-backed Dynamic colors. iOS accepts the new env ids but resolves
them to null outside Android.

Re-export resolvable helpers and types from @use-voltra/android and voltra;
AndroidColorValue is ResolvableValue<string>. Update examples and tests.

Made-with: Cursor
…g comment

Export isResolvableCondition from normalize.ts and import it in serialize.ts
instead of maintaining an identical (but weaker-typed) local copy. Also remove
a comment in VoltraElement.swift that narrated the code rather than explaining
a non-obvious constraint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tries

- Add Android Glance renderers (ControlFlowRenderers.kt) for ControlIf and
  ControlSwitch, dispatched from RenderElement/RenderElementWithModifier
- Expose ResolvableValueEvaluator.evaluateCondition() for renderer use;
  props are pre-resolved by ResolvablePayloadResolver so no env needed at render
- Add ControlIf/ControlSwitch to ios-server component registry (IDs 22/23)
- Fix android-server registry: AndroidControlIf=20, AndroidControlSwitch=21,
  AndroidChart=22 (was incorrectly mapped to 20, shadowing the control-flow IDs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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