diff --git a/example/app.json b/example/app.json
index f0a0c578..9afa7709 100644
--- a/example/app.json
+++ b/example/app.json
@@ -128,6 +128,21 @@
"intervalMinutes": 15,
"refresh": true
}
+ },
+ {
+ "id": "material_colors",
+ "displayName": "Material Colors Widget",
+ "description": "Compare client-side and server-side rendering with Android dynamic colors",
+ "targetCellWidth": 2,
+ "targetCellHeight": 2,
+ "resizeMode": "horizontal|vertical",
+ "widgetCategory": "home_screen",
+ "initialStatePath": "./widgets/android/android-material-colors-initial.tsx",
+ "serverUpdate": {
+ "url": "http://10.0.2.2:3333",
+ "intervalMinutes": 15,
+ "refresh": true
+ }
}
]
},
diff --git a/example/app/android-widgets/material-colors.tsx b/example/app/android-widgets/material-colors.tsx
new file mode 100644
index 00000000..d5f2d285
--- /dev/null
+++ b/example/app/android-widgets/material-colors.tsx
@@ -0,0 +1,5 @@
+import AndroidMaterialColorsScreen from '~/screens/android/AndroidMaterialColorsScreen'
+
+export default function AndroidMaterialColorsRoute() {
+ return
+}
diff --git a/example/screens/android/AndroidMaterialColorsScreen.tsx b/example/screens/android/AndroidMaterialColorsScreen.tsx
new file mode 100644
index 00000000..dd254487
--- /dev/null
+++ b/example/screens/android/AndroidMaterialColorsScreen.tsx
@@ -0,0 +1,278 @@
+import { useRouter } from 'expo-router'
+import React, { useState } from 'react'
+import { Alert, Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
+import {
+ reloadAndroidWidgets,
+ requestPinAndroidWidget,
+ setWidgetServerCredentials,
+ updateAndroidWidget,
+ VoltraWidgetPreview,
+} from 'voltra/android/client'
+
+import { Button } from '~/components/Button'
+import { Card } from '~/components/Card'
+import {
+ AndroidMaterialColorsWidget,
+ type AndroidMaterialColorsRenderSource,
+} from '~/widgets/android/AndroidMaterialColorsWidget'
+
+const WIDGET_ID = 'material_colors'
+const DEMO_TOKEN = 'demo-token'
+
+const formatRenderTime = () =>
+ new Date().toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ })
+
+export default function AndroidMaterialColorsScreen() {
+ const router = useRouter()
+ const [isPinning, setIsPinning] = useState(false)
+ const [isRenderingClient, setIsRenderingClient] = useState(false)
+ const [isRenderingServer, setIsRenderingServer] = useState(false)
+ const [previewSource, setPreviewSource] = useState('initial')
+ const [previewTimestamp, setPreviewTimestamp] = useState('waiting for render')
+
+ const handlePinWidget = async () => {
+ if (Platform.OS !== 'android') {
+ Alert.alert('Not Available', 'This widget demo is only available on Android devices.')
+ return
+ }
+
+ setIsPinning(true)
+ try {
+ const success = await requestPinAndroidWidget(WIDGET_ID, {
+ previewWidth: 220,
+ previewHeight: 220,
+ })
+
+ if (success) {
+ Alert.alert('Pin requested', 'Add the widget on your home screen, then use the render buttons below.')
+ } else {
+ Alert.alert('Not supported', 'Widget pinning is not available on this device.')
+ }
+ } catch (error: any) {
+ const message = error?.message || String(error)
+ Alert.alert('Error', `Failed to pin widget: ${message}`)
+ } finally {
+ setIsPinning(false)
+ }
+ }
+
+ const handleRenderOnClient = async () => {
+ if (Platform.OS !== 'android') {
+ Alert.alert('Not Available', 'Client-side widget rendering is only available on Android devices.')
+ return
+ }
+
+ setIsRenderingClient(true)
+ try {
+ const renderedAt = formatRenderTime()
+
+ await updateAndroidWidget(WIDGET_ID, [
+ {
+ size: { width: 200, height: 200 },
+ content: ,
+ },
+ {
+ size: { width: 300, height: 200 },
+ content: ,
+ },
+ ])
+
+ setPreviewSource('client')
+ setPreviewTimestamp(renderedAt)
+ Alert.alert(
+ 'Client render complete',
+ 'The widget JSON was rendered inside the app and pushed straight to Android.'
+ )
+ } catch (error: any) {
+ const message = error?.message || String(error)
+ Alert.alert('Error', `Failed to render on client: ${message}`)
+ } finally {
+ setIsRenderingClient(false)
+ }
+ }
+
+ const handleRenderOnServer = async () => {
+ if (Platform.OS !== 'android') {
+ Alert.alert('Not Available', 'Server-side widget rendering is only available on Android devices.')
+ return
+ }
+
+ setIsRenderingServer(true)
+ try {
+ await setWidgetServerCredentials({
+ token: DEMO_TOKEN,
+ headers: {
+ 'X-Widget-Source': 'voltra-example',
+ },
+ })
+
+ await reloadAndroidWidgets([WIDGET_ID])
+
+ setPreviewSource('server')
+ setPreviewTimestamp('server timestamp')
+ Alert.alert(
+ 'Server render requested',
+ 'The widget will fetch fresh JSON from the example server. Make sure `npm run widget:server --workspace voltra-example` is running on your host machine.'
+ )
+ } catch (error: any) {
+ const message = error?.message || String(error)
+ Alert.alert('Error', `Failed to render on server: ${message}`)
+ } finally {
+ setIsRenderingServer(false)
+ }
+ }
+
+ return (
+
+
+ Material Colors Widget
+
+ Test the same Android widget through both render paths. It uses Android semantic color tokens, so both
+ client-side and server-side rendering resolve native Material You colors directly inside Glance.
+
+
+
+ 1. Pin the Widget
+
+ Add the widget to your home screen once, then switch between client-side and server-side renders.
+
+
+
+
+
+
+
+ 2. Choose the Render Path
+
+ Both buttons target the same {WIDGET_ID} widget. Use them to compare how
+ Material dynamic colors flow through the client and server pipelines.
+
+
+
+
+
+
+
+
+ Preview
+
+ This in-app preview mirrors the widget design. The home screen widget is the real test, but this makes it
+ easier to see which render path you triggered last.
+
+
+
+
+
+
+
+
+
+ Server Setup
+ Run the example widget server before using the server render button:
+
+ npm run widget:server --workspace voltra-example
+
+
+ Android emulators use 10.0.2.2 in the widget config, so the built-in
+ server-driven refresh hits your host machine automatically.
+
+
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ content: {
+ paddingHorizontal: 20,
+ paddingVertical: 24,
+ },
+ heading: {
+ fontSize: 24,
+ fontWeight: '700',
+ color: '#FFFFFF',
+ },
+ subheading: {
+ fontSize: 14,
+ lineHeight: 20,
+ color: '#CBD5F5',
+ marginBottom: 8,
+ },
+ buttonContainer: {
+ marginTop: 16,
+ },
+ actionsRow: {
+ flexDirection: 'row',
+ gap: 12,
+ marginTop: 16,
+ },
+ actionButton: {
+ flex: 1,
+ },
+ previewContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 24,
+ marginTop: 8,
+ backgroundColor: '#0F172A',
+ borderRadius: 12,
+ },
+ previewFrame: {
+ borderRadius: 28,
+ },
+ code: {
+ fontFamily: 'Courier',
+ fontSize: 12,
+ color: '#60A5FA',
+ backgroundColor: '#0F172A',
+ paddingHorizontal: 4,
+ paddingVertical: 2,
+ borderRadius: 4,
+ },
+ codeBlock: {
+ backgroundColor: '#0F172A',
+ borderRadius: 12,
+ padding: 16,
+ marginTop: 12,
+ },
+ codeText: {
+ color: '#E2E8F0',
+ fontFamily: 'Courier',
+ fontSize: 12,
+ },
+ footer: {
+ marginTop: 24,
+ alignItems: 'center',
+ },
+})
diff --git a/example/screens/android/AndroidScreen.tsx b/example/screens/android/AndroidScreen.tsx
index dd000f32..9a513ab8 100644
--- a/example/screens/android/AndroidScreen.tsx
+++ b/example/screens/android/AndroidScreen.tsx
@@ -48,6 +48,13 @@ const ANDROID_SECTIONS = [
'Serve dynamic widget content from a remote server using Voltra SSR. This example includes a sample widget server implementation.',
route: '/android-widgets/server-driven',
},
+ {
+ id: 'material-colors',
+ title: 'Material Colors',
+ description:
+ 'Test one Android widget through both client-side and server-side rendering, using Material dynamic colors from the current device theme.',
+ route: '/android-widgets/material-colors',
+ },
{
id: 'custom-fonts',
title: 'Custom Fonts',
diff --git a/example/server/widget-server.tsx b/example/server/widget-server.tsx
index fc53f150..e0460a61 100644
--- a/example/server/widget-server.tsx
+++ b/example/server/widget-server.tsx
@@ -11,6 +11,7 @@ import { createServer } from 'node:http'
import React from 'react'
import { createWidgetUpdateNodeHandler } from 'voltra/server'
import { IosPortfolioWidget } from '../widgets/ios/IosPortfolioWidget'
+import { AndroidMaterialColorsServerWidget } from '../widgets/android/AndroidMaterialColorsWidget'
import { AndroidPortfolioWidget } from '../widgets/android/AndroidPortfolioWidget'
const PORTFOLIO_TIMES = [
@@ -47,6 +48,10 @@ function generatePortfolioData() {
const handler = createWidgetUpdateNodeHandler({
renderIos: async (req: any) => {
+ if (req.widgetId !== 'portfolio') {
+ return null
+ }
+
const now = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })
const { chartData, change, balance } = generatePortfolioData()
const isPositive = change >= 0
@@ -65,6 +70,22 @@ const handler = createWidgetUpdateNodeHandler({
renderAndroid: async (req: any) => {
const now = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })
+
+ if (req.widgetId === 'material_colors') {
+ console.log(`[${now}] [Android] Rendering material colors widget`)
+
+ const content =
+
+ return [
+ { size: { width: 200, height: 200 }, content },
+ { size: { width: 300, height: 200 }, content },
+ ]
+ }
+
+ if (req.widgetId !== 'portfolio') {
+ return null
+ }
+
const { chartData, change, balance } = generatePortfolioData()
const isPositive = change >= 0
const changeText = `${isPositive ? '+' : ''}${change.toFixed(1)}%`
@@ -91,6 +112,8 @@ createServer(handler).listen(PORT, () => {
console.log(`\n Portfolio chart:`)
console.log(` iOS: GET http://localhost:${PORT}?widgetId=portfolio&platform=ios&family=systemSmall`)
console.log(` Android: GET http://10.0.2.2:${PORT}?widgetId=portfolio&platform=android`)
+ console.log(`\n Material colors:`)
+ console.log(` Android: GET http://10.0.2.2:${PORT}?widgetId=material_colors&platform=android`)
console.log(`\n (Android emulator uses 10.0.2.2 to reach the host machine)`)
console.log(`\nEach request generates randomized portfolio data.`)
console.log(`Press Ctrl+C to stop.\n`)
diff --git a/example/widgets/android/AndroidMaterialColorsWidget.tsx b/example/widgets/android/AndroidMaterialColorsWidget.tsx
new file mode 100644
index 00000000..172c6e71
--- /dev/null
+++ b/example/widgets/android/AndroidMaterialColorsWidget.tsx
@@ -0,0 +1,123 @@
+import React from 'react'
+import { AndroidDynamicColors, VoltraAndroid } from 'voltra/android'
+
+export type AndroidMaterialColorsRenderSource = 'client' | 'server' | 'initial'
+
+type AndroidMaterialColorsWidgetProps = {
+ source: AndroidMaterialColorsRenderSource
+ renderedAt: string
+}
+
+const SOURCE_LABELS: Record = {
+ client: 'Rendered in app',
+ server: 'Rendered on server',
+ initial: 'Initial placeholder',
+}
+
+const Swatch = ({
+ label,
+ backgroundColor,
+ textColor,
+}: {
+ label: string
+ backgroundColor: string
+ textColor: string
+}) => {
+ return (
+
+
+ {label}
+
+
+
+ {backgroundColor.slice(0, 7).toUpperCase()}
+
+
+ )
+}
+
+export const AndroidMaterialColorsWidget = ({ source, renderedAt }: AndroidMaterialColorsWidgetProps) => {
+ const colors = AndroidDynamicColors
+
+ return (
+
+
+
+
+
+ Material You
+
+
+
+ Dynamic Colors
+
+
+
+
+
+ {SOURCE_LABELS[source]}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The widget should match your wallpaper-driven palette.
+
+
+
+ Updated {renderedAt}
+
+
+
+
+
+ )
+}
+
+export const AndroidMaterialColorsServerWidget = ({ renderedAt }: { renderedAt: string }) => {
+ return
+}
diff --git a/example/widgets/android/android-material-colors-initial.tsx b/example/widgets/android/android-material-colors-initial.tsx
new file mode 100644
index 00000000..57cce5f9
--- /dev/null
+++ b/example/widgets/android/android-material-colors-initial.tsx
@@ -0,0 +1,14 @@
+import { AndroidMaterialColorsWidget } from './AndroidMaterialColorsWidget'
+
+const initialState = [
+ {
+ size: { width: 200, height: 200 },
+ content: ,
+ },
+ {
+ size: { width: 300, height: 200 },
+ content: ,
+ },
+]
+
+export default initialState
diff --git a/packages/android-server/package.json b/packages/android-server/package.json
index 50d559b7..f6864261 100644
--- a/packages/android-server/package.json
+++ b/packages/android-server/package.json
@@ -25,6 +25,7 @@
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
},
"dependencies": {
+ "@use-voltra/android": "1.3.1",
"@use-voltra/core": "1.3.1",
"@use-voltra/server": "1.3.1"
},
diff --git a/packages/android-server/src/index.ts b/packages/android-server/src/index.ts
index 3b33675a..588380c9 100644
--- a/packages/android-server/src/index.ts
+++ b/packages/android-server/src/index.ts
@@ -12,7 +12,7 @@ import {
createWidgetUpdateHandler,
createWidgetUpdateNodeHandler,
} from '@use-voltra/server'
-import type { ReactNode } from 'react'
+import { createElement, Fragment as ReactFragment, type ReactNode } from 'react'
export type {
WidgetRenderRequest,
@@ -20,6 +20,7 @@ export type {
WidgetUpdateHandler,
WidgetUpdateNodeHandler,
} from '@use-voltra/server'
+export type { AndroidColorValue, AndroidDynamicColorRole, AndroidDynamicColorToken } from '@use-voltra/android'
export type { WidgetPlatform, WidgetTheme } from '@use-voltra/server'
export type AndroidWidgetSize = {
@@ -53,6 +54,8 @@ export type AndroidLiveUpdateVariantsJson = {
export type AndroidLiveUpdateJson = AndroidLiveUpdateVariantsJson
+type AndroidWidgetRenderOptions = Record
+
const ANDROID_COMPONENT_NAME_TO_ID: Record = {
AndroidFilledButton: 0,
AndroidImage: 1,
@@ -122,12 +125,19 @@ export const renderAndroidLiveUpdateToString = (variants: AndroidLiveUpdateVaria
return JSON.stringify(renderAndroidLiveUpdateToJson(variants))
}
-export const renderAndroidWidgetToJson = (variants: AndroidWidgetVariants): Record => {
+export const renderAndroidWidgetToJson = (
+ variants: AndroidWidgetVariants,
+ _options?: AndroidWidgetRenderOptions
+): Record => {
const renderer = createVoltraRenderer(androidComponentRegistry)
for (const { size, content } of variants) {
+ if (content === null || content === undefined) {
+ continue
+ }
+
const key = `${size.width}x${size.height}`
- renderer.addRootNode(key, content)
+ renderer.addRootNode(key, createElement(ReactFragment, null, content))
}
const rendered = renderer.render()
@@ -146,8 +156,11 @@ export const renderAndroidWidgetToJson = (variants: AndroidWidgetVariants): Reco
return rendered
}
-export const renderAndroidWidgetToString = (variants: AndroidWidgetVariants): string => {
- return JSON.stringify(renderAndroidWidgetToJson(variants))
+export const renderAndroidWidgetToString = (
+ variants: AndroidWidgetVariants,
+ options?: AndroidWidgetRenderOptions
+): string => {
+ return JSON.stringify(renderAndroidWidgetToJson(variants, options))
}
export interface AndroidWidgetUpdateHandlerOptions {
@@ -155,7 +168,7 @@ export interface AndroidWidgetUpdateHandlerOptions {
validateToken?: (token: string) => Promise | boolean
}
-function toSharedOptions(options: AndroidWidgetUpdateHandlerOptions) {
+const toSharedOptions = (options: AndroidWidgetUpdateHandlerOptions) => {
return {
validateToken: options.validateToken,
renderAndroid: async (request: WidgetRenderRequest) => {
@@ -165,18 +178,18 @@ function toSharedOptions(options: AndroidWidgetUpdateHandlerOptions) {
}
}
-export function createAndroidWidgetUpdateHandler(options: AndroidWidgetUpdateHandlerOptions): WidgetUpdateHandler {
+export const createAndroidWidgetUpdateHandler = (options: AndroidWidgetUpdateHandlerOptions): WidgetUpdateHandler => {
return createWidgetUpdateHandler(toSharedOptions(options))
}
-export function createAndroidWidgetUpdateNodeHandler(
+export const createAndroidWidgetUpdateNodeHandler = (
options: AndroidWidgetUpdateHandlerOptions
-): WidgetUpdateNodeHandler {
+): WidgetUpdateNodeHandler => {
return createWidgetUpdateNodeHandler(toSharedOptions(options))
}
-export function createAndroidWidgetUpdateExpressHandler(
+export const createAndroidWidgetUpdateExpressHandler = (
options: AndroidWidgetUpdateHandlerOptions
-): WidgetUpdateExpressHandler {
+): WidgetUpdateExpressHandler => {
return createWidgetUpdateExpressHandler(toSharedOptions(options))
}
diff --git a/packages/android-server/tsconfig.base.json b/packages/android-server/tsconfig.base.json
index aae39635..e8edd987 100644
--- a/packages/android-server/tsconfig.base.json
+++ b/packages/android-server/tsconfig.base.json
@@ -4,6 +4,9 @@
"lib": ["ES2020", "DOM"],
"rootDir": "./src",
"moduleResolution": "node",
+ "paths": {
+ "@use-voltra/android/internal": ["../android/build/types/internal.d.ts"]
+ },
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
diff --git a/packages/android-server/tsconfig.typecheck.json b/packages/android-server/tsconfig.typecheck.json
index f1b7e2fb..d5d9364f 100644
--- a/packages/android-server/tsconfig.typecheck.json
+++ b/packages/android-server/tsconfig.typecheck.json
@@ -9,6 +9,7 @@
"voltra/*": ["packages/voltra/src/*"],
"@use-voltra/android": ["packages/android/src/index.ts"],
"@use-voltra/android/client": ["packages/android/src/client.ts"],
+ "@use-voltra/android/internal": ["packages/android/src/internal.ts"],
"@use-voltra/android/server": ["packages/android/src/server.ts"],
"@use-voltra/android-server": ["packages/android-server/src/index.ts"],
"@use-voltra/core": ["packages/core/src/index.ts"],
diff --git a/packages/android/package.json b/packages/android/package.json
index 645c5696..3d838fc2 100644
--- a/packages/android/package.json
+++ b/packages/android/package.json
@@ -24,6 +24,12 @@
"import": "./build/esm/server.js",
"default": "./build/esm/server.js"
},
+ "./internal": {
+ "types": "./build/types/internal.d.ts",
+ "require": "./build/cjs/internal.js",
+ "import": "./build/esm/internal.js",
+ "default": "./build/esm/internal.js"
+ },
"./package.json": "./package.json"
},
"files": [
diff --git a/packages/android/src/components/VoltraView.tsx b/packages/android/src/components/VoltraView.tsx
index 14da6ecb..f951b073 100644
--- a/packages/android/src/components/VoltraView.tsx
+++ b/packages/android/src/components/VoltraView.tsx
@@ -3,7 +3,7 @@ import React, { ReactNode, useEffect, useMemo } from 'react'
import { StyleProp, ViewStyle } from 'react-native'
import { addVoltraListener, VoltraInteractionEvent } from '../events.js'
-import { androidComponentRegistry, createVoltraRenderer } from '../renderer/renderer.js'
+import { renderAndroidViewToJson } from '../widgets/renderer.js'
const NativeVoltraView = requireNativeView('VoltraModule')
@@ -39,18 +39,7 @@ export function VoltraView({ id, children, style, onInteraction }: VoltraViewPro
// Generate a stable ID if not provided
const viewId = useMemo(() => id || generateViewId(), [id])
- const payload = useMemo(() => {
- const renderer = createVoltraRenderer(androidComponentRegistry)
- renderer.addRootNode('content', children)
- const rendered = renderer.render()
-
- // Move 'content' into 'variants' to match VoltraPayload structure
- const node = rendered.content
- delete rendered.content
- rendered.variants = { content: node }
-
- return JSON.stringify(rendered)
- }, [children])
+ const payload = useMemo(() => JSON.stringify(renderAndroidViewToJson(children)), [children])
// Subscribe to interaction events and filter by this view's ID
useEffect(() => {
diff --git a/packages/android/src/dynamic-colors.ts b/packages/android/src/dynamic-colors.ts
new file mode 100644
index 00000000..2a052834
--- /dev/null
+++ b/packages/android/src/dynamic-colors.ts
@@ -0,0 +1,33 @@
+export const AndroidDynamicColors = {
+ primary: '~p',
+ onPrimary: '~op',
+ primaryContainer: '~pc',
+ onPrimaryContainer: '~opc',
+ secondary: '~s',
+ onSecondary: '~os',
+ secondaryContainer: '~sc',
+ onSecondaryContainer: '~osc',
+ tertiary: '~t',
+ onTertiary: '~ot',
+ tertiaryContainer: '~tc',
+ onTertiaryContainer: '~otc',
+ error: '~e',
+ errorContainer: '~ec',
+ onError: '~oe',
+ onErrorContainer: '~oec',
+ background: '~b',
+ onBackground: '~ob',
+ surface: '~sf',
+ onSurface: '~osf',
+ surfaceVariant: '~sv',
+ onSurfaceVariant: '~osv',
+ outline: '~ol',
+ inverseOnSurface: '~ios',
+ inverseSurface: '~is',
+ inversePrimary: '~ip',
+ widgetBackground: '~wb',
+} as const
+
+export type AndroidDynamicColorRole = keyof typeof AndroidDynamicColors
+export type AndroidDynamicColorToken = (typeof AndroidDynamicColors)[AndroidDynamicColorRole]
+export type AndroidColorValue = string | AndroidDynamicColorToken
diff --git a/packages/android/src/index.ts b/packages/android/src/index.ts
index 0df79118..70dcfee0 100644
--- a/packages/android/src/index.ts
+++ b/packages/android/src/index.ts
@@ -1,5 +1,6 @@
// Android component namespace
export * as VoltraAndroid from './jsx/primitives.js'
+export { AndroidDynamicColors } from './dynamic-colors.js'
// Android types
export type { VoltraAndroidBaseProps } from './jsx/baseProps.js'
@@ -9,6 +10,7 @@ export type {
VoltraAndroidTextStyleProp,
VoltraAndroidViewStyle,
} from './styles/types.js'
+export type { AndroidColorValue, AndroidDynamicColorRole, AndroidDynamicColorToken } from './dynamic-colors.js'
// Component prop types
export type { BoxProps } from './jsx/Box.js'
diff --git a/packages/android/src/internal.ts b/packages/android/src/internal.ts
new file mode 100644
index 00000000..6b10d211
--- /dev/null
+++ b/packages/android/src/internal.ts
@@ -0,0 +1,2 @@
+export { renderAndroidViewToJson, renderAndroidWidgetToJson, renderAndroidWidgetToString } from './widgets/renderer.js'
+export type { AndroidWidgetRenderOptions } from './widgets/renderer.js'
diff --git a/packages/android/src/jsx/AreaMark.tsx b/packages/android/src/jsx/AreaMark.tsx
index 296d285c..41b6e71b 100644
--- a/packages/android/src/jsx/AreaMark.tsx
+++ b/packages/android/src/jsx/AreaMark.tsx
@@ -1,11 +1,12 @@
import { createElement } from 'react'
+import type { AndroidColorValue } from '../dynamic-colors.js'
import { VOLTRA_MARK_TAG } from './BarMark.js'
import type { ChartDataPoint } from './chart-types.js'
export type AreaMarkProps = {
data: ChartDataPoint[]
- color?: string
+ color?: AndroidColorValue
interpolation?: 'linear' | 'monotone' | 'stepStart' | 'stepEnd' | 'stepCenter' | 'cardinal' | 'catmullRom'
stacking?: 'standard' | 'normalized' | 'unstacked'
}
diff --git a/packages/android/src/jsx/BarMark.tsx b/packages/android/src/jsx/BarMark.tsx
index a35e4f31..c5b7ea42 100644
--- a/packages/android/src/jsx/BarMark.tsx
+++ b/packages/android/src/jsx/BarMark.tsx
@@ -1,12 +1,13 @@
import { createElement } from 'react'
+import type { AndroidColorValue } from '../dynamic-colors.js'
import type { ChartDataPoint } from './chart-types.js'
export const VOLTRA_MARK_TAG = Symbol.for('VOLTRA_MARK_TAG')
export type BarMarkProps = {
data: ChartDataPoint[]
- color?: string
+ color?: AndroidColorValue
stacking?: 'grouped'
cornerRadius?: number
width?: number
diff --git a/packages/android/src/jsx/LineMark.tsx b/packages/android/src/jsx/LineMark.tsx
index 417549f1..92aa5a24 100644
--- a/packages/android/src/jsx/LineMark.tsx
+++ b/packages/android/src/jsx/LineMark.tsx
@@ -1,11 +1,12 @@
import { createElement } from 'react'
+import type { AndroidColorValue } from '../dynamic-colors.js'
import { VOLTRA_MARK_TAG } from './BarMark.js'
import type { ChartDataPoint } from './chart-types.js'
export type LineMarkProps = {
data: ChartDataPoint[]
- color?: string
+ color?: AndroidColorValue
interpolation?: 'linear' | 'monotone' | 'stepStart' | 'stepEnd' | 'stepCenter' | 'cardinal' | 'catmullRom'
lineWidth?: number
symbol?: string
diff --git a/packages/android/src/jsx/PointMark.tsx b/packages/android/src/jsx/PointMark.tsx
index c3dfd937..5e600168 100644
--- a/packages/android/src/jsx/PointMark.tsx
+++ b/packages/android/src/jsx/PointMark.tsx
@@ -1,11 +1,12 @@
import { createElement } from 'react'
+import type { AndroidColorValue } from '../dynamic-colors.js'
import { VOLTRA_MARK_TAG } from './BarMark.js'
import type { ChartDataPoint } from './chart-types.js'
export type PointMarkProps = {
data: ChartDataPoint[]
- color?: string
+ color?: AndroidColorValue
symbol?: string
symbolSize?: number
}
diff --git a/packages/android/src/jsx/RuleMark.tsx b/packages/android/src/jsx/RuleMark.tsx
index 9d2ef2ac..e5ce11d2 100644
--- a/packages/android/src/jsx/RuleMark.tsx
+++ b/packages/android/src/jsx/RuleMark.tsx
@@ -1,11 +1,12 @@
import { createElement } from 'react'
+import type { AndroidColorValue } from '../dynamic-colors.js'
import { VOLTRA_MARK_TAG } from './BarMark.js'
export type RuleMarkProps = {
xValue?: string | number
yValue?: number
- color?: string
+ color?: AndroidColorValue
lineWidth?: number
}
diff --git a/packages/android/src/jsx/SectorMark.tsx b/packages/android/src/jsx/SectorMark.tsx
index bda3c480..c6a165a1 100644
--- a/packages/android/src/jsx/SectorMark.tsx
+++ b/packages/android/src/jsx/SectorMark.tsx
@@ -1,11 +1,12 @@
import { createElement } from 'react'
+import type { AndroidColorValue } from '../dynamic-colors.js'
import { VOLTRA_MARK_TAG } from './BarMark.js'
import type { SectorDataPoint } from './chart-types.js'
export type SectorMarkProps = {
data: SectorDataPoint[]
- color?: string
+ color?: AndroidColorValue
innerRadius?: number
outerRadius?: number
angularInset?: number
diff --git a/packages/android/src/jsx/props/CheckBox.ts b/packages/android/src/jsx/props/CheckBox.ts
index a7dae8dd..3684a057 100644
--- a/packages/android/src/jsx/props/CheckBox.ts
+++ b/packages/android/src/jsx/props/CheckBox.ts
@@ -1,3 +1,4 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidTextStyleProp } from '../../styles/types.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
@@ -9,9 +10,9 @@ export type CheckBoxProps = VoltraAndroidBaseProps & {
/** Style for the text */
style?: VoltraAndroidTextStyleProp
/** Color when checked */
- checkedColor?: string
+ checkedColor?: AndroidColorValue
/** Color when unchecked */
- uncheckedColor?: string
+ uncheckedColor?: AndroidColorValue
/** Maximum lines for text */
maxLines?: number
}
diff --git a/packages/android/src/jsx/props/CircleIconButton.ts b/packages/android/src/jsx/props/CircleIconButton.ts
index b72b3a98..bf215685 100644
--- a/packages/android/src/jsx/props/CircleIconButton.ts
+++ b/packages/android/src/jsx/props/CircleIconButton.ts
@@ -1,3 +1,4 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
import type { ImageSource } from '../Image.js'
@@ -9,7 +10,7 @@ export type CircleIconButtonProps = VoltraAndroidBaseProps & {
/** Content description for accessibility */
contentDescription?: string
/** Background color */
- backgroundColor?: string
+ backgroundColor?: AndroidColorValue
/** Icon color */
- contentColor?: string
+ contentColor?: AndroidColorValue
}
diff --git a/packages/android/src/jsx/props/CircularProgressIndicator.ts b/packages/android/src/jsx/props/CircularProgressIndicator.ts
index 04b1a4e6..acaa7ef9 100644
--- a/packages/android/src/jsx/props/CircularProgressIndicator.ts
+++ b/packages/android/src/jsx/props/CircularProgressIndicator.ts
@@ -1,3 +1,4 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
export type CircularProgressIndicatorProps = VoltraAndroidBaseProps & {
@@ -8,5 +9,5 @@ export type CircularProgressIndicatorProps = VoltraAndroidBaseProps & {
*/
progress?: number
/** Color for the progress indicator */
- color?: string
+ color?: AndroidColorValue
}
diff --git a/packages/android/src/jsx/props/FilledButton.ts b/packages/android/src/jsx/props/FilledButton.ts
index 6b2581b4..bc9adbe2 100644
--- a/packages/android/src/jsx/props/FilledButton.ts
+++ b/packages/android/src/jsx/props/FilledButton.ts
@@ -1,3 +1,4 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
import type { ImageSource } from '../Image.js'
@@ -9,9 +10,9 @@ export type FilledButtonProps = VoltraAndroidBaseProps & {
/** Icon to display */
icon?: ImageSource
/** Background color */
- backgroundColor?: string
+ backgroundColor?: AndroidColorValue
/** Content (text/icon) color */
- contentColor?: string
+ contentColor?: AndroidColorValue
/** Maximum lines for text */
maxLines?: number
}
diff --git a/packages/android/src/jsx/props/Image.ts b/packages/android/src/jsx/props/Image.ts
index cc088b54..da53231c 100644
--- a/packages/android/src/jsx/props/Image.ts
+++ b/packages/android/src/jsx/props/Image.ts
@@ -4,6 +4,7 @@
import type { ReactNode } from 'react'
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
export type ImageProps = VoltraAndroidBaseProps & {
@@ -18,9 +19,9 @@ export type ImageProps = VoltraAndroidBaseProps & {
/** Opacity (0.0 to 1.0) */
alpha?: number
/** Tint color */
- tintColor?: string
+ tintColor?: AndroidColorValue
/** Background color used when the image is missing */
- fallbackColor?: string
+ fallbackColor?: AndroidColorValue
/** Custom fallback content rendered when the image is missing */
fallback?: ReactNode
}
diff --git a/packages/android/src/jsx/props/LinearProgressIndicator.ts b/packages/android/src/jsx/props/LinearProgressIndicator.ts
index 6b06216e..e1af142d 100644
--- a/packages/android/src/jsx/props/LinearProgressIndicator.ts
+++ b/packages/android/src/jsx/props/LinearProgressIndicator.ts
@@ -1,10 +1,11 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
export type LinearProgressIndicatorProps = VoltraAndroidBaseProps & {
/** Current progress value (0.0 to 1.0) */
progress?: number
/** Color for the progress indicator */
- color?: string
+ color?: AndroidColorValue
/** Color for the background track */
- backgroundColor?: string
+ backgroundColor?: AndroidColorValue
}
diff --git a/packages/android/src/jsx/props/OutlineButton.ts b/packages/android/src/jsx/props/OutlineButton.ts
index 9ac5d15c..45cff252 100644
--- a/packages/android/src/jsx/props/OutlineButton.ts
+++ b/packages/android/src/jsx/props/OutlineButton.ts
@@ -1,3 +1,4 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
import type { ImageSource } from '../Image.js'
@@ -9,7 +10,7 @@ export type OutlineButtonProps = VoltraAndroidBaseProps & {
/** Icon to display */
icon?: ImageSource
/** Content (text/icon) color */
- contentColor?: string
+ contentColor?: AndroidColorValue
/** Maximum lines for text */
maxLines?: number
}
diff --git a/packages/android/src/jsx/props/RadioButton.ts b/packages/android/src/jsx/props/RadioButton.ts
index 80350aa6..81a4b443 100644
--- a/packages/android/src/jsx/props/RadioButton.ts
+++ b/packages/android/src/jsx/props/RadioButton.ts
@@ -1,3 +1,4 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidTextStyleProp } from '../../styles/types.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
@@ -9,9 +10,9 @@ export type RadioButtonProps = VoltraAndroidBaseProps & {
/** Style for the text */
style?: VoltraAndroidTextStyleProp
/** Color when checked */
- checkedColor?: string
+ checkedColor?: AndroidColorValue
/** Color when unchecked */
- uncheckedColor?: string
+ uncheckedColor?: AndroidColorValue
/** Maximum lines for text */
maxLines?: number
/** Whether the radio button is enabled */
diff --git a/packages/android/src/jsx/props/Scaffold.ts b/packages/android/src/jsx/props/Scaffold.ts
index 3107eb95..ecb73631 100644
--- a/packages/android/src/jsx/props/Scaffold.ts
+++ b/packages/android/src/jsx/props/Scaffold.ts
@@ -1,8 +1,9 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
export type ScaffoldProps = VoltraAndroidBaseProps & {
/** Background color for the scaffold - supports hex, rgb, hsl, and named colors */
- backgroundColor?: string
+ backgroundColor?: AndroidColorValue
/** Horizontal padding */
horizontalPadding?: number
}
diff --git a/packages/android/src/jsx/props/SquareIconButton.ts b/packages/android/src/jsx/props/SquareIconButton.ts
index deb56027..3ee3ac4e 100644
--- a/packages/android/src/jsx/props/SquareIconButton.ts
+++ b/packages/android/src/jsx/props/SquareIconButton.ts
@@ -1,3 +1,4 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
import type { ImageSource } from '../Image.js'
@@ -9,7 +10,7 @@ export type SquareIconButtonProps = VoltraAndroidBaseProps & {
/** Content description for accessibility */
contentDescription?: string
/** Background color */
- backgroundColor?: string
+ backgroundColor?: AndroidColorValue
/** Icon color */
- contentColor?: string
+ contentColor?: AndroidColorValue
}
diff --git a/packages/android/src/jsx/props/Switch.ts b/packages/android/src/jsx/props/Switch.ts
index d55b7457..807bb464 100644
--- a/packages/android/src/jsx/props/Switch.ts
+++ b/packages/android/src/jsx/props/Switch.ts
@@ -1,3 +1,4 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidTextStyleProp } from '../../styles/types.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
@@ -9,13 +10,13 @@ export type SwitchProps = VoltraAndroidBaseProps & {
/** Style for the text */
style?: VoltraAndroidTextStyleProp
/** Thumb color when checked */
- thumbCheckedColor?: string
+ thumbCheckedColor?: AndroidColorValue
/** Thumb color when unchecked */
- thumbUncheckedColor?: string
+ thumbUncheckedColor?: AndroidColorValue
/** Track color when checked */
- trackCheckedColor?: string
+ trackCheckedColor?: AndroidColorValue
/** Track color when unchecked */
- trackUncheckedColor?: string
+ trackUncheckedColor?: AndroidColorValue
/** Maximum lines for text */
maxLines?: number
}
diff --git a/packages/android/src/jsx/props/TitleBar.ts b/packages/android/src/jsx/props/TitleBar.ts
index 223b001e..42e0ced1 100644
--- a/packages/android/src/jsx/props/TitleBar.ts
+++ b/packages/android/src/jsx/props/TitleBar.ts
@@ -1,3 +1,4 @@
+import type { AndroidColorValue } from '../../dynamic-colors.js'
import type { VoltraAndroidBaseProps } from '../baseProps.js'
import type { ImageSource } from '../Image.js'
@@ -7,9 +8,9 @@ export type TitleBarProps = VoltraAndroidBaseProps & {
/** Start icon source */
startIcon: ImageSource
/** Text color - supports hex, rgb, hsl, and named colors */
- textColor?: string
+ textColor?: AndroidColorValue
/** Icon color - supports hex, rgb, hsl, and named colors */
- iconColor?: string
+ iconColor?: AndroidColorValue
/** Font family for the title */
fontFamily?: string
}
diff --git a/packages/android/src/payload/component-ids.ts b/packages/android/src/payload/component-ids.ts
index 5c7b6e97..8d47d5e3 100644
--- a/packages/android/src/payload/component-ids.ts
+++ b/packages/android/src/payload/component-ids.ts
@@ -1,3 +1,4 @@
+/* eslint-disable */
// 🤖 AUTO-GENERATED from data/components.json
// DO NOT EDIT MANUALLY - Changes will be overwritten
// Schema version: 1.0.0
diff --git a/packages/android/src/server.ts b/packages/android/src/server.ts
index f4d614cd..30fc036e 100644
--- a/packages/android/src/server.ts
+++ b/packages/android/src/server.ts
@@ -1,2 +1,3 @@
export { renderAndroidLiveUpdateToString } from './live-update/renderer.js'
export { renderAndroidWidgetToString } from './widgets/renderer.js'
+export type { AndroidColorValue, AndroidDynamicColorRole, AndroidDynamicColorToken } from './dynamic-colors.js'
diff --git a/packages/android/src/styles/types.ts b/packages/android/src/styles/types.ts
index ef068e02..0db9832f 100644
--- a/packages/android/src/styles/types.ts
+++ b/packages/android/src/styles/types.ts
@@ -3,6 +3,8 @@
* These types mirror the supported properties in the Android implementation (Glance).
*/
+import type { AndroidColorValue } from '../dynamic-colors.js'
+
export type StyleProp = T | T[] | null | undefined | false
export type VoltraAndroidViewStyle = {
@@ -61,13 +63,13 @@ export type VoltraAndroidViewStyle = {
zIndex?: number
/** Background color of the component */
- backgroundColor?: string
+ backgroundColor?: AndroidColorValue
/** Corner radius for the component (requires Android 12+) */
borderRadius?: number
/** Border width (Note: Not yet implemented in Glance) */
borderWidth?: number
/** Border color (Note: Not yet implemented in Glance) */
- borderColor?: string
+ borderColor?: AndroidColorValue
/** Opacity of the component (Note: Not supported in Glance - apply alpha to colors instead) */
opacity?: number
@@ -83,7 +85,7 @@ export type VoltraAndroidViewStyle = {
transform?: ({ rotate: string } | { rotateZ: string } | { scale: number } | { scaleX: number } | { scaleY: number })[]
/** Shadow properties (Note: Not supported in Glance) */
- shadowColor?: string
+ shadowColor?: AndroidColorValue
shadowOffset?: { width: number; height: number }
shadowOpacity?: number
shadowRadius?: number
@@ -94,7 +96,7 @@ export type VoltraAndroidViewStyle = {
export type VoltraAndroidTextStyle = VoltraAndroidViewStyle & {
/** Text color */
- color?: string
+ color?: AndroidColorValue
/** Font size in sp */
fontSize?: number
/** Font weight */
diff --git a/packages/android/src/widgets/renderer.ts b/packages/android/src/widgets/renderer.ts
index b7f2af2e..da9d0b68 100644
--- a/packages/android/src/widgets/renderer.ts
+++ b/packages/android/src/widgets/renderer.ts
@@ -1,3 +1,5 @@
+import { createElement, Fragment as ReactFragment, type ReactNode } from 'react'
+
import { getAndroidComponentId } from '../payload/component-ids.js'
import { ComponentRegistry, createVoltraRenderer } from '../renderer/renderer.js'
import type { AndroidWidgetVariants } from './types.js'
@@ -9,6 +11,8 @@ const androidComponentRegistry: ComponentRegistry = {
getComponentId: (name: string) => getAndroidComponentId(name),
}
+export type AndroidWidgetRenderOptions = Record
+
/**
* Renders Android widget variants to JSON with size breakpoints.
*
@@ -24,13 +28,16 @@ const androidComponentRegistry: ComponentRegistry = {
* "e": [...shared elements...]
* }
*/
-export const renderAndroidWidgetToJson = (variants: AndroidWidgetVariants): Record => {
+export const renderAndroidWidgetToJson = (
+ variants: AndroidWidgetVariants,
+ _options?: AndroidWidgetRenderOptions
+): Record => {
const renderer = createVoltraRenderer(androidComponentRegistry)
// Add each size variant with key format "WIDTHxHEIGHT"
for (const { size, content } of variants) {
const key = `${size.width}x${size.height}`
- renderer.addRootNode(key, content)
+ renderer.addRootNode(key, createElement(ReactFragment, null, content))
}
const rendered = renderer.render()
@@ -55,6 +62,29 @@ export const renderAndroidWidgetToJson = (variants: AndroidWidgetVariants): Reco
/**
* Renders Android widget variants to a JSON string.
*/
-export const renderAndroidWidgetToString = (variants: AndroidWidgetVariants): string => {
- return JSON.stringify(renderAndroidWidgetToJson(variants))
+export const renderAndroidWidgetToString = (
+ variants: AndroidWidgetVariants,
+ options?: AndroidWidgetRenderOptions
+): string => {
+ return JSON.stringify(renderAndroidWidgetToJson(variants, options))
+}
+
+/**
+ * Renders Android JSX to JSON for VoltraView component.
+ */
+export const renderAndroidViewToJson = (
+ children: ReactNode,
+ _options?: AndroidWidgetRenderOptions
+): Record => {
+ const renderer = createVoltraRenderer(androidComponentRegistry)
+
+ renderer.addRootNode('content', createElement(ReactFragment, null, children))
+
+ const rendered = renderer.render()
+ const node = rendered.content
+
+ delete rendered.content
+ rendered.variants = { content: node }
+
+ return rendered
}
diff --git a/packages/core/src/payload/short-names.ts b/packages/core/src/payload/short-names.ts
index 193ddf1a..7beec772 100644
--- a/packages/core/src/payload/short-names.ts
+++ b/packages/core/src/payload/short-names.ts
@@ -1,7 +1,12 @@
+/* eslint-disable */
// 🤖 AUTO-GENERATED from data/components.json
// DO NOT EDIT MANUALLY - Changes will be overwritten
// Schema version: 1.0.0
+/**
+ * Unified mapping from full names to short names
+ * Used for props and style properties
+ */
export const NAME_TO_SHORT: Record = {
absolutePosition: 'ap',
alignItems: 'ai',
@@ -19,6 +24,7 @@ export const NAME_TO_SHORT: Record = {
borderWidth: 'bw',
bottom: 'b',
buttonStyle: 'bs',
+ chartScrollableAxes: 'csa',
checked: 'chk',
clipped: 'clip',
color: 'c',
@@ -161,6 +167,9 @@ export const NAME_TO_SHORT: Record = {
zIndex: 'zi',
}
+/**
+ * Reverse mapping from short names to full names
+ */
export const SHORT_TO_NAME: Record = {
ap: 'absolutePosition',
ai: 'alignItems',
@@ -178,6 +187,7 @@ export const SHORT_TO_NAME: Record = {
bw: 'borderWidth',
b: 'bottom',
bs: 'buttonStyle',
+ csa: 'chartScrollableAxes',
chk: 'checked',
clip: 'clipped',
c: 'color',
@@ -320,10 +330,18 @@ export const SHORT_TO_NAME: Record = {
zi: 'zIndex',
}
+/**
+ * Shorten a name using the unified mapping
+ * Returns the original name if no short form exists
+ */
export function shorten(name: string): string {
return NAME_TO_SHORT[name] ?? name
}
+/**
+ * Expand a short name back to the full name
+ * Returns the original value if no expansion exists
+ */
export function expand(short: string): string {
return SHORT_TO_NAME[short] ?? short
}
diff --git a/packages/ios-server/src/index.ts b/packages/ios-server/src/index.ts
index d3a37cf7..796c1249 100644
--- a/packages/ios-server/src/index.ts
+++ b/packages/ios-server/src/index.ts
@@ -111,7 +111,7 @@ export const renderWidgetToJson = (variants: WidgetVariants): Record)
}
}
diff --git a/packages/ios/src/payload/component-ids.ts b/packages/ios/src/payload/component-ids.ts
index 588dfcda..e7e0fdf7 100644
--- a/packages/ios/src/payload/component-ids.ts
+++ b/packages/ios/src/payload/component-ids.ts
@@ -1,3 +1,4 @@
+/* eslint-disable */
// 🤖 AUTO-GENERATED from data/components.json
// DO NOT EDIT MANUALLY - Changes will be overwritten
// Schema version: 1.0.0
diff --git a/packages/ios/src/widgets/renderer.ts b/packages/ios/src/widgets/renderer.ts
index 32d096d7..7b782c9f 100644
--- a/packages/ios/src/widgets/renderer.ts
+++ b/packages/ios/src/widgets/renderer.ts
@@ -9,7 +9,7 @@ export const renderWidgetToJson = (variants: WidgetVariants): Record WidgetRenderResul
* Contains the widget ID, family, and any auth headers from the request.
*/
export interface WidgetRenderRequest {
+ /** Parsed request URL, including all query parameters. */
+ url: URL
/** The widget ID requesting an update */
widgetId: string
/** The platform the request is coming from */
@@ -180,6 +182,7 @@ export function createWidgetUpdateHandler(options: WidgetUpdateHandlerOptions):
}
const renderRequest: WidgetRenderRequest = {
+ url,
widgetId,
platform,
theme,
diff --git a/packages/voltra/android/src/main/java/voltra/generated/ShortNames.kt b/packages/voltra/android/src/main/java/voltra/generated/ShortNames.kt
index a0a2d72c..0d7ef2e7 100644
--- a/packages/voltra/android/src/main/java/voltra/generated/ShortNames.kt
+++ b/packages/voltra/android/src/main/java/voltra/generated/ShortNames.kt
@@ -31,6 +31,7 @@ object ShortNames {
"bw" to "borderWidth",
"b" to "bottom",
"bs" to "buttonStyle",
+ "csa" to "chartScrollableAxes",
"chk" to "checked",
"clip" to "clipped",
"c" to "color",
diff --git a/packages/voltra/android/src/main/java/voltra/glance/StyleUtils.kt b/packages/voltra/android/src/main/java/voltra/glance/StyleUtils.kt
index 2565fba7..96064236 100644
--- a/packages/voltra/android/src/main/java/voltra/glance/StyleUtils.kt
+++ b/packages/voltra/android/src/main/java/voltra/glance/StyleUtils.kt
@@ -15,6 +15,7 @@ data class ResolvedStyle(
val compositeStyle: CompositeStyle?,
)
+@Composable
fun resolveAndApplyStyle(
props: Map?,
sharedStyles: List