Input Agnostic Player and Native A2UI Support#866
Conversation
|
/docs |
Bundle ReportChanges will increase total bundle size by 670.02kB (12.96%) ⬆️
Affected Assets, Files, and Routes:view changes for bundle: plugins/check-path/coreAssets Changed:
view changes for bundle: tools/storybookAssets Changed:
view changes for bundle: plugins/beacon/coreAssets Changed:
view changes for bundle: core/playerAssets Changed:
view changes for bundle: react/playerAssets Changed:
view changes for bundle: plugins/external-state/coreAssets Changed:
view changes for bundle: plugins/markdown/coreAssets Changed:
view changes for bundle: plugins/metrics/coreAssets Changed:
view changes for bundle: plugins/async-node/coreAssets Changed:
view changes for bundle: plugins/reference-assets/coreAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #866 +/- ##
===========================
===========================
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
/canary |
…ion logic to plugin. Also migrate to Bazel Repo based node module ignore
|
/canary |
Benchmark ResultsComparison against baseline from
|
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.bar |
808.68K ops/s | 792.61K ops/s | +2.0% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets.1.name |
501.21K ops/s | 447.84K ops/s | +11.9% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets.01.name |
463.04K ops/s | 452.53K ops/s | +2.3% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets['01'].name |
466.11K ops/s | 461.73K ops/s | +1.0% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets[01].name |
475.51K ops/s | 464.71K ops/s | +2.3% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets[name = "frodo"].type |
298.41K ops/s | 214.51K ops/s | +39.1% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["name" = "sprinkles"].type |
231.57K ops/s | 189.71K ops/s | +22.1% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["isDog" = false].type |
298.58K ops/s | 271.18K ops/s | +10.1% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["isDog" = true].type |
308.96K ops/s | 228.34K ops/s | +35.3% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.bar |
607.63K ops/s | 484.95K ops/s | +25.3% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets.1.name |
400.50K ops/s | 259.12K ops/s | +54.6% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets.01.name |
381.75K ops/s | 279.20K ops/s | +36.7% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets['01'].name |
356.32K ops/s | 285.38K ops/s | +24.9% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets[01].name |
397.29K ops/s | 363.01K ops/s | +9.4% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets[name = "frodo"].type |
248.71K ops/s | 243.94K ops/s | +2.0% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["name" = "sprinkles"].type |
191.76K ops/s | 200.91K ops/s | -4.6% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["isDog" = false].type |
241.26K ops/s | 237.34K ops/s | +1.7% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["isDog" = true].type |
246.07K ops/s | 230.38K ops/s | +6.8% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = 1 + 3 (sync) |
363.84K ops/s | 431.98K ops/s | -15.8% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = 1 + 3 (async) |
275.02K ops/s | 256.47K ops/s | +7.2% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: conditional(true, true, false) (sync) |
466.41K ops/s | 446.50K ops/s | +4.5% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: conditional(true, true, false) (async) |
317.30K ops/s | 411.46K ops/s | -22.9% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional({{bar}} > 0, true, false) (sync) |
201.34K ops/s | 187.69K ops/s | +7.3% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional({{bar}} > 0, true, false) (async) |
168.65K ops/s | 176.75K ops/s | -4.6% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(conditional(true = false, false, true), conditional(false = false, true, false), conditional(true = true, false, true)) (sync) |
153.01K ops/s | 153.94K ops/s | -0.6% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(conditional(true = false, false, true), conditional(false = false, true, false), conditional(true = true, false, true)) (async) |
125.31K ops/s | 146.44K ops/s | -14.4% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = await(asyncTestFunction(1)) (sync) |
N/A | N/A | N/A |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = await(asyncTestFunction(1)) (async) |
219.16K ops/s | 274.66K ops/s | -20.2% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = asyncTestFunction(1) (sync) |
329.59K ops/s | 304.58K ops/s | +8.2% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = asyncTestFunction(1) (async) |
289.15K ops/s | 250.35K ops/s | +15.5% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: asyncTestFunction(1) (sync) |
865.46K ops/s | 802.33K ops/s | +7.9% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: asyncTestFunction(1) (async) |
678.36K ops/s | 571.23K ops/s | +18.8% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(!{{bar}} == false, await(asyncTestFunction(1)), false) (sync) |
183.32K ops/s | 155.77K ops/s | +17.7% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(!{{bar}} == false, await(asyncTestFunction(1)), false) (async) |
167.13K ops/s | 128.38K ops/s | +30.2% ✅ |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > initial resolve |
449.36 ops/s | 611.80 ops/s | -26.6% |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > Resolving from cache |
16.59K ops/s | 18.52K ops/s | -10.4% |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > data changes |
2.56K ops/s | 2.99K ops/s | -14.6% |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > data changes slow |
578.83 ops/s | 635.20 ops/s | -8.9% |
plugins/async-node/core ⚠️
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 1 times |
10.85K ops/s | 14.01K ops/s | -22.5% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 5 times |
11.17K ops/s | 12.49K ops/s | -10.6% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 10 times |
9.60K ops/s | 10.49K ops/s | -8.4% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 50 times |
3.16K ops/s | 3.25K ops/s | -2.9% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 100 times |
1.68K ops/s | 1.78K ops/s | -5.8% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 1 times |
5.93K ops/s | 5.91K ops/s | +0.3% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 5 times |
7.54K ops/s | 7.91K ops/s | -4.8% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 10 times |
6.24K ops/s | 6.92K ops/s | -9.8% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 50 times |
2.26K ops/s | 2.60K ops/s | -13.0% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 100 times |
1.47K ops/s | 1.50K ops/s | -1.8% |
react/player
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 1 ReactAssets |
590.10 ops/s | 532.19 ops/s | +10.9% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 1 ReactAssets |
731.35 ops/s | 742.64 ops/s | -1.5% |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 5 ReactAssets |
583.61 ops/s | 529.03 ops/s | +10.3% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 5 ReactAssets |
791.14 ops/s | 787.69 ops/s | +0.4% |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 10 ReactAssets |
548.97 ops/s | 530.01 ops/s | +3.6% |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 10 ReactAssets |
730.25 ops/s | 701.94 ops/s | +4.0% |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 50 ReactAssets |
431.28 ops/s | 426.81 ops/s | +1.0% |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 50 ReactAssets |
255.13 ops/s | 237.77 ops/s | +7.3% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 100 ReactAssets |
386.65 ops/s | 329.70 ops/s | +17.3% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 100 ReactAssets |
75.77 ops/s | 78.58 ops/s | -3.6% |
KVSRoyal
left a comment
There was a problem hiding this comment.
Just some small questions. But otherwise this looks good from an ios perspective.
| npm_package_target_name = "{dirname}", | ||
| npmrc = "//:.npmrc", | ||
| pnpm_lock = "//:pnpm-lock.yaml", | ||
| verify_node_modules_ignored = "//:.bazelignore", |
There was a problem hiding this comment.
What does removing verify_node_modules_ignored do?
There was a problem hiding this comment.
So before, we had to manually specify every node_module folder for Bazel to ignore to prevent loops in the dependency tree. This PR migrates away from that pattern to specify to just ignore all node_modules in the REPO.bazel flle. That way when we make a new JS based package we don't have to add it to that list.
| #if SWIFT_PACKAGE | ||
| import PlayerUI | ||
| import PlayerUISwiftUI | ||
| #endif |
There was a problem hiding this comment.
We can drop the conditional wrapper nowadays since we only release through Swift packages now. Can you make that change? Same for the other files like this.
// Just this now
import SwiftUI
import PlayerUI
import PlayerUISwiftUI| #if SWIFT_PACKAGE | ||
| ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module) | ||
| #else | ||
| ResourceUtilities.urlForFile( | ||
| name: fileName, | ||
| ext: "js", | ||
| bundle: Bundle(for: A2UIPlugin.self), | ||
| pathComponent: "PlayerUI_A2UI.bundle" | ||
| ) | ||
| #endif | ||
| } |
There was a problem hiding this comment.
Same as above. We can just keep the if part now:
override open func getUrlForFile(fileName: String) -> URL? {
ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module)
}|
/canary |
|
/canary |
| super({ | ||
| ...options, | ||
| plugins: [ | ||
| new A2UIPlugin() as unknown as ReactPlayerPlugin, |
There was a problem hiding this comment.
This type cast shouldn't be needed afaik. If there is an issue where it's required here I think we should try to tackle that. All PlayerPlugins should be acceptable here.
| (player as unknown as { logger: Parameters<typeof adaptA2UIToFlow>[1] }) | ||
| .logger, |
There was a problem hiding this comment.
Could we just use player.logger here?
| const snap = | ||
| (mod as { default?: A2UISnapshot }).default ?? (mod as A2UISnapshot); | ||
| return { default: snap as unknown as Record<string, unknown> }; |
There was a problem hiding this comment.
| const snap = | |
| (mod as { default?: A2UISnapshot }).default ?? (mod as A2UISnapshot); | |
| return { default: snap as unknown as Record<string, unknown> }; | |
| const snap = "default" in mod ? mod.default : mod; | |
| return { default: snap }; |
This change will introduce a type issue because snap as an A2UISnapshot doesn't match the Mock type, but I think if mocks can be anything at this point we may just want to make that type object, unknown, or Record<string, unknown>.
| @@ -0,0 +1,31 @@ | |||
| import React from "react"; | |||
| import type { A2UISnapshot } from "@player-ui/a2ui-plugin"; | |||
There was a problem hiding this comment.
This dependency has to be added to the package.json or BUILD for this package.
| ); | ||
|
|
||
| wp.start(jsonEditorValue.value) | ||
| wp.start(jsonEditorValue.value as never, format ? { format } : undefined) |
There was a problem hiding this comment.
| wp.start(jsonEditorValue.value as never, format ? { format } : undefined) | |
| wp.start(jsonEditorValue.value, format ? { format } : undefined) |
there are a few as never in adapter.ts as well that need to be removed
| format: options?.format ?? "player", | ||
| version: options?.version, | ||
| }; | ||
| const flow = this.hooks.transformContent.call(payload, meta) as Flow; |
There was a problem hiding this comment.
If I understand correctly, the reason for transforming the content at this level is to ensure we have all the properties that Flow has (namely things like id and navigation) but is this appropriate for all parts of the object?
One consideration I have here is that support for different formats for something like the views should happen at the parser level, since all that really matters for the view/assets is the AST that gets produced. This is also something we need to think about with how this will interact with the AsyncNodePlugin since the A2UI format supports dynamic content updates. We need to parse new content into an AST but if we have to follow the same step of A2UI > Flow > AST it feels like we'll just end up with extra processing when we could just go straight to the AST.
| transformContent: SyncWaterfallHook< | ||
| [unknown, ContentMeta], | ||
| Record<string, any> | ||
| >; |
There was a problem hiding this comment.
I think a Bail hook here is more appropriate than a Waterfall hook as it would let us require the output type to be a Flow. We could then tap in ourselves and for player format we can just return the object as it is. This would let us remove the type-casting from the Player start function which imo is error-prone.
| and needs no conversion. Any other `format` is recognized by a plugin tapping the | ||
| core `transformContent` hook (e.g. the A2UI plugin claims `"a2ui"`). | ||
| */ | ||
| public struct StartOptions { |
There was a problem hiding this comment.
Could we make StartOptions conform to Equatable and Sendable? Equatable allows us to write sth like ==, and Sendable for the thread safety
| (defaults to `nil`, i.e. Player `Flow`s). Pass e.g. `.a2ui` for an A2UI experience. | ||
| - onError: A handler for when the `SwiftUIPlayer` encounters an error | ||
| - loading: A closure providing a `View` to display while the `FlowManager` fetches flows | ||
| */ |
There was a problem hiding this comment.
May we use /// instead of /** */ for iOS doc purpose. had a discussion previously and made an agreement to follow this guide https://google.github.io/swift/
| - startOptions: Describes the content `format`/`version` for every flow in the session | ||
| - onError: A handler for when the `SwiftUIPlayer` encounters an error | ||
| - loading: A closure providing a `View` to display while the `FlowManager` fetches flows | ||
| */ |
There was a problem hiding this comment.
same here to replace /** */ with ///
| - onError: A handler for when the `SwiftUIPlayer` encounters an error | ||
| - loading: A closure providing a `View` to display while the `FlowManager` fetches flows | ||
| */ | ||
| public init( |
There was a problem hiding this comment.
this should not be public since ManagedPlayer14 is internal. It was made previously though.
| - plugins: The plugins to use for the `SwiftUIPlayer` | ||
| - handleScroll: Whether or not the `ManagedPlayer` should wrap content in a `ScrollView` | ||
| - startOptions: Describes the content `format`/`version` for every flow in the session | ||
| - onError: A handler for when the `SwiftUIPlayer` encounters an error |
There was a problem hiding this comment.
not from this pr, but seems there is no onError parameter in the init
What and Why
With the emergence of A2UI and other agent-driven standards, Player — in its mission to serve the broader SDUI space — should expand support for more formats beyond its own in order to broaden the scope of what Player solves for. Furthermore, this lets capabilities be built against Player itself, regardless of input format, allowing for the decoupling of implementation from the server driving it.
This PR adds the generic "unknown format" entrypoint to every platform (web, JVM, Android, iOS) and ships A2UI as the first non-Player format end-to-end.
Core Player Changes
transformContentwaterfall hook on Player's hooks. Fires at the top ofstart(), after plugins are registered and beforeresolveFlowContent. Plugins inspect aContentMeta{ format, version }and either convert the payload or pass it through.Player.start()signature widened from(payload: Flow)to(payload: unknown, options?: StartOptions). Default format is"player", which preserves existing behavior (a plain Flow flows through untouched).versionis free-form, so a single format plugin can dispatch across versions.Platform Entrypoints
The hook and signature widening live in the core JS bundle every platform loads; each platform entrypoint forwards the
format/versionthrough. Default"player"keeps all existing call sites untouched.ReactPlayer.start()mirrors the same signature (react/player/src/player.tsx) and forwards options to the underlyingPlayer.start().Player.start()gainsstart(flow: String, format: String, version: String? = null)(plus a matchingURLoverload).HeadlessPlayerimplements it and forwards{ format, version }to the JSplayer.start(payload, options)as a bridge-encodedMap.AndroidPlayer.start(flow, format, version)delegates to the wrappedHeadlessPlayer.StartOptions { format, version }value type;HeadlessPlayer.start(flow:options:completion:)appends the options object to the JSstartargs ([String: Any]→ JS object via JavaScriptCore). Threaded throughSwiftUIPlayer.init/Context.load/ManagedPlayervia a defaultedstartOptions:param (source-compatible). UseStartOptions.a2ui.Plugins
Core:
@player-ui/a2ui-pluginIncludes three sub-plugins:
A2UIContentPlugin: Only activates whenmeta.format === "a2ui". CallsadaptA2UIToFlow(snapshot)to transform content.A2UITransformPlugin: Per-asset transforms (Button/TextField/CheckBox/Slider/DateTimeInput/ChoicePicker/Text) that attachrun()/set()/valuehelpers consumed by the rendering layer.A2UIExpressionsPlugin: Registers the A2UI v0.9.1 standard library: validation (required,regex,length,numeric,email), formatters (formatString,formatNumber,formatCurrency,formatDate,pluralize),openUrl, and logic (and/or/not).This package emits a native JS bundle (
A2UIPlugin.native.js), which the JVM and iOS wrappers below load directly — so the adapter, transforms, and expression std-lib are shared across all platforms with zero reimplementation.Adapter Logic
Walks the flat
components[]list fromid: "root", inlines child references into a nested asset tree matching Player's{asset: ...}shape, and produces a Flow with a single VIEW state plus one END per unique event name encountered. Translation rules:{path: "/x/y"}→"x.y"(Player binding)formatString(...)→"Hello, {{x.y}}!"template{call, args}→@[fn(...)]@expressionchecks: [...]on inputs → lifted into a synthesizedFlow.schemaviasynthesizeSchemaso Player's existing SchemaController/ValidationController pick them up unchanged{path, componentId}blocks become indexed paths scoped to<scope>._index_Platform Renders
The A2UI v0.9.1 reference catalog — 16 assets — implemented per platform: Row, Column, List, Text, Image, Icon, Divider, Card, Modal, Tabs, Button, TextField, CheckBox, Slider, DateTimeInput, ChoicePicker. Asset
typestrings are PascalCase to match the adapter output; transformed assets consume the sharedrun()/set()/currentValuehelpers.@player-ui/a2ui-plugin-react— the full catalog as React components.plugins/a2ui/android—A2UIPlugin : AndroidPlayerPlugin, JSPluginWrapper by <jvm wrapper>registering the catalog as Jetpack Compose renderers. Function helpers decode as Kotlin function types.plugins/a2ui/swiftui—A2UIPlugin : JSBasePlugin, NativePluginthat loads the bundle and registers the catalog as SwiftUI renderers. Function helpers decode as SwiftWrappedFunctions.plugins/a2ui/jvm— generated KotlinJSPluginWrapper(A2UIPlugin) that loads the A2UI JS bundle (mirrorsreference-assets/jvm). Headless-capable, no UI layer.Packages
A new folder in the repo! The goal for this folder is to ship preconfigured Player entrypoints so consumers don't have to assemble plugins themselves. The
plugins/directory exports building blocks;packages/exports ready-to-use Players for specific content formats.Each preset comes with the A2UI plugin pre-added and appends any consumer-supplied plugins after it. Since
HeadlessPlayer/AndroidPlayerarefinal, the JVM/Android entries are idiomatic factory functions; iOS is a thinViewwrapper that also defaultsstartOptions: .a2ui.React:
@player-ui/a2uiJVM:
packages/a2ui/jvmAndroid:
packages/a2ui/androidiOS:
packages/a2ui/swiftuiMocks
The canonical catalog is
plugins/a2ui/mocks(21 snapshots). JS consumes it via@player-ui/a2ui-plugin-mocks; JVM/Android via//tools/mocks:jar. iOS mirrors the full set as faithful inline copies (the existing iOS demo/test convention, there is no runtime mechanism to read the canonical JSON on iOS today).Storybook
New A2UI stories with one story per asset, driven by a
createA2UIStoryhelper in the Player storybook extension that can render A2UI content.Demo Apps
.a2ui.PlayerViewModelregistersA2UIPluginalongside the reference assets (disjoint type namespaces).Notes
Image/Icon(no Coil /material-icons-coreonly) and iOSImage(deployment target iOS 14 predatesAsyncImage) render labelled placeholders; iOSIconuses SF Symbols.To Align On
Overall Approach
Change Type (required)
Indicate the type of change your pull request is:
patchminormajorN/ADoes your PR have any documentation updates?
Release Notes
TBD
📦 Published PR as canary version:
1.1.0--canary.866.38513Try this version out locally by upgrading relevant packages to 1.1.0--canary.866.38513