@mcp-native/react-native 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,68 @@
1
- import { createA2uiV1ActionEnvelope, validateA2uiV1SurfaceState } from "@mcp-native/a2ui";
2
- import { parseJsonObject, parseMcpNativeAction } from "@mcp-native/core";
1
+ import { createA2uiV1ActionEnvelope, getA2uiV1HostExtensionManifestFingerprint, isA2uiV1HostExtensionRegistry, validateA2uiV1HostExtensionEvent, validateA2uiV1SurfaceState, } from "@mcp-native/a2ui";
2
+ import { parseJsonObject, parseJsonValue, parseMcpNativeAction } from "@mcp-native/core";
3
3
  import { createElement, useCallback, useEffect, useMemo, useRef, useState, } from "react";
4
- import { createA2uiV1NativeRenderPlan, createA2uiV1NativeRenderPlanForLocalEdits, parseA2uiV1NativeOpenUrlDescriptor, resolveA2uiV1NativeEvent, resolveA2uiV1NativeOpenUrl, } from "./v1.js";
5
- export { createNativeButtonAdapter, createNativeTextAdapter, createNativeTextInputAdapter, createNativeViewAdapter, } from "./component-adapters.js";
4
+ import { A2UI_V1_NATIVE_ICON_NAMES, isNativeHostExtensionRegistration, renderNativeHostExtensionRegistration, } from "./component-adapters.js";
5
+ import { A2UI_V1_NATIVE_COMPONENT_NAMES, createA2uiV1NativeRenderPlan, createA2uiV1NativeRenderPlanForLocalEdits, parseA2uiV1NativeOpenUrlDescriptor, resolveA2uiV1NativeEvent, resolveA2uiV1NativeOpenUrl, validateA2uiV1NativeDateTimeInputChange, } from "./v1.js";
6
+ export { A2UI_V1_NATIVE_ICON_NAMES, createNativeButtonAdapter, createNativeAudioPlayerAdapter, createNativeCheckBoxAdapter, createNativeChoicePickerAdapter, createNativeDateTimeInputAdapter, createNativeDividerAdapter, createNativeIconAdapter, createNativeImageAdapter, createNativeHostExtensionRegistration, createNativeModalAdapter, createNativeSliderAdapter, createNativeTabsAdapter, createNativeTextAdapter, createNativeTextInputAdapter, createNativeViewAdapter, createNativeVideoAdapter, } from "./component-adapters.js";
7
+ const A2UI_V1_NATIVE_BASE_COMPONENT_NAMES = Object.freeze([
8
+ "Button",
9
+ "Card",
10
+ "Column",
11
+ "List",
12
+ "Row",
13
+ "Text",
14
+ "TextField",
15
+ ]);
16
+ const A2UI_V1_NATIVE_OPTIONAL_COMPONENT_SLOTS = Object.freeze([
17
+ "AudioPlayer",
18
+ "CheckBox",
19
+ "ChoicePicker",
20
+ "DateTimeInput",
21
+ "Divider",
22
+ "Icon",
23
+ "Image",
24
+ "Modal",
25
+ "Slider",
26
+ "Tabs",
27
+ "Video",
28
+ ]);
29
+ /** Returns the exact A2UI subset backed by one locally installed and policy-ready host catalog. */
30
+ export function getA2uiV1NativeSupportedComponentNames(components, capabilities = {}) {
31
+ const installed = new Set(A2UI_V1_NATIVE_BASE_COMPONENT_NAMES);
32
+ for (const name of A2UI_V1_NATIVE_OPTIONAL_COMPONENT_SLOTS) {
33
+ if (components[name] !== undefined &&
34
+ (name !== "Image" || capabilities.imagePolicy !== undefined) &&
35
+ ((name !== "Video" && name !== "AudioPlayer") || capabilities.mediaPolicy !== undefined)) {
36
+ installed.add(name);
37
+ }
38
+ }
39
+ return Object.freeze(A2UI_V1_NATIVE_COMPONENT_NAMES.filter((name) => installed.has(name)));
40
+ }
41
+ /**
42
+ * Returns only extension catalogs backed by an exact helper-created local registration and a host
43
+ * capability policy. These IDs can be combined with the basic catalog in renderer capabilities.
44
+ */
45
+ export function getA2uiV1NativeSupportedHostExtensionCatalogIds(components, registry, hostExtensionPolicy) {
46
+ if (!isA2uiV1HostExtensionRegistry(registry)) {
47
+ throw new TypeError("Expected an opaque negotiated host-extension registry");
48
+ }
49
+ if (hostExtensionPolicy === undefined || typeof hostExtensionPolicy !== "function") {
50
+ return Object.freeze([]);
51
+ }
52
+ const catalogIds = new Set();
53
+ for (const manifest of registry.manifests) {
54
+ const registrations = components.hostExtensions?.filter((registration) => isNativeHostExtensionRegistration(registration) &&
55
+ registration.manifest.extensionId === manifest.extensionId &&
56
+ registration.manifest.catalogId === manifest.catalogId &&
57
+ registration.manifest.schemaVersion === manifest.schemaVersion &&
58
+ registration.manifest.componentName === manifest.componentName) ?? [];
59
+ if (registrations.length === 1 &&
60
+ registrations[0].manifestFingerprint === getA2uiV1HostExtensionManifestFingerprint(manifest)) {
61
+ catalogIds.add(manifest.catalogId);
62
+ }
63
+ }
64
+ return Object.freeze([...catalogIds]);
65
+ }
6
66
  /** @deprecated Use `createA2uiV1NativeRenderPlan`. */
7
67
  export function createNativeRenderPlan(surface) {
8
68
  return renderNode(surface.root);
@@ -36,7 +96,7 @@ export function McpNativeSurface({ surface, components, onAction, onBindingChang
36
96
  * Mounts the supported A2UI v1 subset with renderer-local two-way bindings.
37
97
  * Agent events are resolved against the latest local model at press time.
38
98
  */
39
- export function A2uiV1NativeSurface({ surface, policy, components, onAction, openUrlPolicy, onOpenUrl, onDataModelChange, actionMetadata, locale, now = currentTimestamp, }) {
99
+ export function A2uiV1NativeSurface({ surface, policy, components, onAction, openUrlPolicy, onOpenUrl, imagePolicy, mediaPolicy, hostExtensionPolicy, onHostExtensionEvent, onDataModelChange, actionMetadata, locale, now = currentTimestamp, }) {
40
100
  const validatedSurface = useMemo(() => validateA2uiV1SurfaceState(surface, policy), [policy, surface]);
41
101
  const sourceDataModel = validatedSurface.dataModel;
42
102
  const sourceDataModelKey = createDataModelSourceKey(validatedSurface);
@@ -71,7 +131,19 @@ export function A2uiV1NativeSurface({ surface, policy, components, onAction, ope
71
131
  : createA2uiV1NativeRenderPlan)(validatedSurface, policy, {
72
132
  dataModel,
73
133
  ...(locale === undefined ? {} : { locale }),
74
- }), [dataModel, locale, policy, tolerateInvalidLocalOpenUrls, validatedSurface]);
134
+ ...(imagePolicy === undefined ? {} : { imagePolicy }),
135
+ ...(mediaPolicy === undefined ? {} : { mediaPolicy }),
136
+ ...(hostExtensionPolicy === undefined ? {} : { hostExtensionPolicy }),
137
+ }), [
138
+ dataModel,
139
+ hostExtensionPolicy,
140
+ imagePolicy,
141
+ locale,
142
+ mediaPolicy,
143
+ policy,
144
+ tolerateInvalidLocalOpenUrls,
145
+ validatedSurface,
146
+ ]);
75
147
  const handleBindingChange = useCallback((binding, value) => {
76
148
  const next = updateDataModelBinding(dataModelRef.current, binding, value);
77
149
  dataModelRef.current = next;
@@ -116,11 +188,12 @@ export function A2uiV1NativeSurface({ surface, policy, components, onAction, ope
116
188
  }, [locale, onOpenUrl, openUrlPolicy, policy, validatedSurface]);
117
189
  return renderElement(plan, components, {
118
190
  useComponentVariants: true,
119
- onBindingChange: handleBindingChange,
191
+ onV1BindingChange: handleBindingChange,
120
192
  onV1Event: handleEvent,
121
193
  ...(openUrlPolicy === undefined || onOpenUrl === undefined
122
194
  ? {}
123
195
  : { onV1OpenUrl: handleOpenUrl }),
196
+ ...(onHostExtensionEvent === undefined ? {} : { onV1HostExtensionEvent: onHostExtensionEvent }),
124
197
  });
125
198
  }
126
199
  function createDataModelSourceKey(surface) {
@@ -244,14 +317,589 @@ function renderElement(element, components, handlers) {
244
317
  ...(invalid === undefined ? {} : { invalid }),
245
318
  ...(validationMessages === undefined ? {} : { validationMessages }),
246
319
  ...(value === undefined ? {} : { value }),
247
- ...(binding === undefined || handlers.onBindingChange === undefined
320
+ ...(binding === undefined ||
321
+ (handlers.onBindingChange === undefined && handlers.onV1BindingChange === undefined)
248
322
  ? {}
249
323
  : {
250
- onChangeText: (nextValue) => handlers.onBindingChange?.(binding, nextValue),
324
+ onChangeText: (nextValue) => {
325
+ if (handlers.onV1BindingChange !== undefined) {
326
+ handlers.onV1BindingChange(binding, nextValue);
327
+ }
328
+ else {
329
+ handlers.onBindingChange?.(binding, nextValue);
330
+ }
331
+ },
251
332
  }),
252
333
  });
253
334
  }
335
+ case "Image": {
336
+ const uri = expectStringProp(element, "uri");
337
+ const fit = selectClosedStringProp(element, "fit", [
338
+ "contain",
339
+ "cover",
340
+ "fill",
341
+ "none",
342
+ "scaleDown",
343
+ ]);
344
+ const variant = selectClosedStringProp(element, "variant", [
345
+ "avatar",
346
+ "header",
347
+ "icon",
348
+ "largeFeature",
349
+ "mediumFeature",
350
+ "smallFeature",
351
+ ]);
352
+ const accessible = accessibilityProps.accessibilityLabel !== undefined &&
353
+ accessibilityProps.accessibilityElementsHidden !== true;
354
+ return createElement(selectImageComponent(element, components, handlers.useComponentVariants === true, variant), {
355
+ key: element.key,
356
+ uri,
357
+ fit,
358
+ resourcePolicy: expectImageResourcePolicy(element),
359
+ ...accessibilityProps,
360
+ accessible,
361
+ accessibilityRole: "image",
362
+ });
363
+ }
364
+ case "Video": {
365
+ const uri = expectStringProp(element, "uri");
366
+ const posterUri = optionalStringProp(element, "posterUri");
367
+ const accessible = accessibilityProps.accessibilityElementsHidden !== true;
368
+ return createElement(requireHostComponent(components.Video, "Video", element.key), {
369
+ key: element.key,
370
+ uri,
371
+ ...(posterUri === undefined ? {} : { posterUri }),
372
+ resourcePolicy: expectMediaResourcePolicy(element),
373
+ ...(posterUri === undefined
374
+ ? {}
375
+ : { posterResourcePolicy: expectImageResourcePolicy(element, "posterResourcePolicy") }),
376
+ ...accessibilityProps,
377
+ accessible,
378
+ accessibilityLabel: accessibilityProps.accessibilityLabel ?? "Video",
379
+ accessibilityRole: "image",
380
+ });
381
+ }
382
+ case "AudioPlayer": {
383
+ const uri = expectStringProp(element, "uri");
384
+ const description = optionalStringProp(element, "description");
385
+ const accessible = accessibilityProps.accessibilityElementsHidden !== true;
386
+ return createElement(requireHostComponent(components.AudioPlayer, "AudioPlayer", element.key), {
387
+ key: element.key,
388
+ uri,
389
+ resourcePolicy: expectMediaResourcePolicy(element),
390
+ ...(description === undefined ? {} : { description }),
391
+ ...accessibilityProps,
392
+ accessible,
393
+ accessibilityLabel: accessibilityProps.accessibilityLabel ?? description ?? "Audio",
394
+ accessibilityRole: "button",
395
+ });
396
+ }
397
+ case "HostExtension": {
398
+ const extensionId = expectStringProp(element, "extensionId");
399
+ const catalogId = expectStringProp(element, "catalogId");
400
+ const schemaVersion = expectStringProp(element, "schemaVersion");
401
+ const componentName = expectStringProp(element, "componentName");
402
+ const manifestFingerprint = expectStringProp(element, "manifestFingerprint");
403
+ const sourceComponentId = expectStringProp(element, "sourceComponentId");
404
+ const surfaceId = expectStringProp(element, "surfaceId");
405
+ const semanticProps = parseJsonObject(element.props.semanticProps, `native element ${element.key}.semanticProps`);
406
+ const capabilityGrant = expectHostExtensionCapabilityGrant(element);
407
+ const registration = requireHostExtensionRegistration(components, { extensionId, catalogId, schemaVersion, componentName, manifestFingerprint }, element.key);
408
+ const trustedProps = {
409
+ semanticProps,
410
+ capabilityGrant,
411
+ ...accessibilityProps,
412
+ onEvent(name, payload, options) {
413
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
414
+ throw new TypeError("Expected closed host-extension event options");
415
+ }
416
+ const optionKeys = Object.keys(options);
417
+ if (optionKeys.length !== 1 || optionKeys[0] !== "userActivated") {
418
+ throw new TypeError("Expected only userActivated in host-extension event options");
419
+ }
420
+ const parsedPayload = validateA2uiV1HostExtensionEvent(registration.manifest, name, payload, options.userActivated);
421
+ handlers.onV1HostExtensionEvent?.({
422
+ extensionId,
423
+ catalogId,
424
+ schemaVersion,
425
+ componentName,
426
+ surfaceId,
427
+ sourceComponentId,
428
+ name,
429
+ payload: parsedPayload,
430
+ userActivated: options.userActivated,
431
+ });
432
+ },
433
+ };
434
+ return renderNativeHostExtensionRegistration(registration, trustedProps, element.key);
435
+ }
436
+ case "Icon": {
437
+ const name = expectStringProp(element, "name");
438
+ if (!A2UI_V1_NATIVE_ICON_NAMES.includes(name)) {
439
+ throw new TypeError(`Unsupported icon name at native element ${element.key}.name`);
440
+ }
441
+ const accessible = accessibilityProps.accessibilityLabel !== undefined &&
442
+ accessibilityProps.accessibilityElementsHidden !== true;
443
+ return createElement(requireHostComponent(components.Icon, "Icon", element.key), {
444
+ key: element.key,
445
+ name: name,
446
+ ...accessibilityProps,
447
+ accessible,
448
+ accessibilityRole: "image",
449
+ });
450
+ }
451
+ case "Divider":
452
+ return createElement(requireHostComponent(components.Divider, "Divider", element.key), {
453
+ key: element.key,
454
+ axis: selectClosedStringProp(element, "axis", ["horizontal", "vertical"]),
455
+ ...accessibilityProps,
456
+ accessible: false,
457
+ });
458
+ case "CheckBox": {
459
+ const label = expectStringProp(element, "label");
460
+ const value = expectBooleanProp(element, "value");
461
+ const binding = optionalStringProp(element, "binding");
462
+ const invalid = optionalBooleanProp(element, "invalid");
463
+ const validationMessages = optionalStringArrayProp(element, "validationMessages");
464
+ return createElement(requireHostComponent(components.CheckBox, "CheckBox", element.key), {
465
+ key: element.key,
466
+ label,
467
+ value,
468
+ ...accessibilityProps,
469
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
470
+ accessibilityLabel: accessibilityProps.accessibilityLabel ?? label,
471
+ accessibilityRole: "checkbox",
472
+ accessibilityState: { checked: value },
473
+ ...(invalid === undefined ? {} : { invalid }),
474
+ ...(validationMessages === undefined ? {} : { validationMessages }),
475
+ ...(binding === undefined || handlers.onV1BindingChange === undefined
476
+ ? {}
477
+ : {
478
+ onValueChange: (nextValue) => {
479
+ if (typeof nextValue !== "boolean") {
480
+ throw new TypeError(`Expected a boolean checkbox value at native element ${element.key}`);
481
+ }
482
+ handlers.onV1BindingChange?.(binding, nextValue);
483
+ },
484
+ }),
485
+ });
486
+ }
487
+ case "ChoicePicker": {
488
+ const options = expectChoicePickerOptions(element);
489
+ const value = expectStringArrayProp(element, "value");
490
+ const variant = selectClosedStringProp(element, "variant", [
491
+ "multipleSelection",
492
+ "mutuallyExclusive",
493
+ ]);
494
+ const displayStyle = selectClosedStringProp(element, "displayStyle", [
495
+ "checkbox",
496
+ "chips",
497
+ ]);
498
+ const filterable = expectBooleanProp(element, "filterable");
499
+ const label = optionalStringProp(element, "label");
500
+ const accessibilityLabel = expectStringProp(element, "accessibilityLabel");
501
+ const binding = optionalStringProp(element, "binding");
502
+ const invalid = optionalBooleanProp(element, "invalid");
503
+ const validationMessages = optionalStringArrayProp(element, "validationMessages");
504
+ return createElement(selectChoicePickerComponent(components, variant, element.key), {
505
+ key: element.key,
506
+ options,
507
+ value,
508
+ variant,
509
+ displayStyle,
510
+ filterable,
511
+ ...accessibilityProps,
512
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
513
+ accessibilityLabel,
514
+ ...(label === undefined ? {} : { label }),
515
+ ...(invalid === undefined ? {} : { invalid }),
516
+ ...(validationMessages === undefined ? {} : { validationMessages }),
517
+ ...(binding === undefined || handlers.onV1BindingChange === undefined
518
+ ? {}
519
+ : {
520
+ onValueChange: (nextValue) => {
521
+ const parsed = validateChoicePickerChange(nextValue, options, variant, element.key);
522
+ handlers.onV1BindingChange?.(binding, parsed);
523
+ },
524
+ }),
525
+ });
526
+ }
527
+ case "Slider": {
528
+ const value = expectFiniteNumberProp(element, "value");
529
+ const minimumValue = expectFiniteNumberProp(element, "minimum");
530
+ const maximumValue = expectFiniteNumberProp(element, "maximum");
531
+ const step = optionalFiniteNumberProp(element, "step");
532
+ const label = optionalStringProp(element, "label");
533
+ const accessibilityLabel = expectStringProp(element, "accessibilityLabel");
534
+ const binding = optionalStringProp(element, "binding");
535
+ const invalid = optionalBooleanProp(element, "invalid");
536
+ const validationMessages = optionalStringArrayProp(element, "validationMessages");
537
+ return createElement(requireHostComponent(components.Slider, "Slider", element.key), {
538
+ key: element.key,
539
+ value,
540
+ minimumValue,
541
+ maximumValue,
542
+ ...accessibilityProps,
543
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
544
+ accessibilityLabel,
545
+ accessibilityRole: "adjustable",
546
+ ...(label === undefined ? {} : { label }),
547
+ ...(step === undefined ? {} : { step }),
548
+ ...(invalid === undefined ? {} : { invalid }),
549
+ ...(validationMessages === undefined ? {} : { validationMessages }),
550
+ ...(binding === undefined || handlers.onV1BindingChange === undefined
551
+ ? {}
552
+ : {
553
+ onValueChange: (nextValue) => {
554
+ if (typeof nextValue !== "number" ||
555
+ !Number.isFinite(nextValue) ||
556
+ nextValue < minimumValue ||
557
+ nextValue > maximumValue ||
558
+ (step !== undefined && !isSliderStepValue(nextValue, minimumValue, step))) {
559
+ throw new TypeError(`Expected an in-range finite slider value at native element ${element.key}`);
560
+ }
561
+ handlers.onV1BindingChange?.(binding, nextValue);
562
+ },
563
+ }),
564
+ });
565
+ }
566
+ case "DateTimeInput": {
567
+ const value = expectStringProp(element, "value");
568
+ const enableDate = expectBooleanProp(element, "enableDate");
569
+ const enableTime = expectBooleanProp(element, "enableTime");
570
+ const minimum = optionalStringProp(element, "minimum");
571
+ const maximum = optionalStringProp(element, "maximum");
572
+ const label = optionalStringProp(element, "label");
573
+ const accessibilityLabel = expectStringProp(element, "accessibilityLabel");
574
+ const binding = optionalStringProp(element, "binding");
575
+ const invalid = optionalBooleanProp(element, "invalid");
576
+ const validationMessages = optionalStringArrayProp(element, "validationMessages");
577
+ return createElement(requireHostComponent(components.DateTimeInput, "DateTimeInput", element.key), {
578
+ key: element.key,
579
+ value,
580
+ enableDate,
581
+ enableTime,
582
+ ...accessibilityProps,
583
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
584
+ accessibilityLabel,
585
+ ...(label === undefined ? {} : { label }),
586
+ ...(minimum === undefined ? {} : { minimum }),
587
+ ...(maximum === undefined ? {} : { maximum }),
588
+ ...(invalid === undefined ? {} : { invalid }),
589
+ ...(validationMessages === undefined ? {} : { validationMessages }),
590
+ ...(binding === undefined || handlers.onV1BindingChange === undefined
591
+ ? {}
592
+ : {
593
+ onValueChange: (nextValue) => {
594
+ if (typeof nextValue !== "string") {
595
+ throw new TypeError(`Expected a string date/time value at native element ${element.key}`);
596
+ }
597
+ try {
598
+ validateA2uiV1NativeDateTimeInputChange(nextValue, enableDate, enableTime, minimum, maximum, `native element ${element.key}`);
599
+ }
600
+ catch (error) {
601
+ const message = error instanceof Error
602
+ ? error.message
603
+ : `Expected a valid date/time value at native element ${element.key}`;
604
+ throw new TypeError(message, { cause: error });
605
+ }
606
+ handlers.onV1BindingChange?.(binding, nextValue);
607
+ },
608
+ }),
609
+ });
610
+ }
611
+ case "Tabs":
612
+ return createElement(NativeTabsRenderer, {
613
+ key: element.key,
614
+ element,
615
+ components,
616
+ handlers,
617
+ });
618
+ case "Modal":
619
+ return createElement(NativeModalRenderer, {
620
+ key: element.key,
621
+ element,
622
+ components,
623
+ handlers,
624
+ });
625
+ }
626
+ }
627
+ function isSliderStepValue(value, minimum, step) {
628
+ const offset = (value - minimum) / step;
629
+ return (Math.abs(offset - Math.round(offset)) <= Number.EPSILON * Math.max(1, Math.abs(offset)) * 8);
630
+ }
631
+ function NativeTabsRenderer({ element, components, handlers, }) {
632
+ const tabDefinitions = expectTabDefinitions(element);
633
+ const children = element.children ?? [];
634
+ if (tabDefinitions.length !== children.length || tabDefinitions.length === 0) {
635
+ throw new TypeError(`Expected matching non-empty tabs and children at native element ${element.key}`);
636
+ }
637
+ const [selectedIndex, setSelectedIndex] = useState(0);
638
+ const currentIndex = selectedIndex < tabDefinitions.length ? selectedIndex : 0;
639
+ useEffect(() => {
640
+ if (selectedIndex >= tabDefinitions.length) {
641
+ setSelectedIndex(0);
642
+ }
643
+ }, [selectedIndex, tabDefinitions.length]);
644
+ const accessibilityProps = selectAccessibilityProps(element);
645
+ return createElement(requireHostComponent(components.Tabs, "Tabs", element.key), {
646
+ tabs: tabDefinitions.map((tab, index) => ({
647
+ title: tab.title,
648
+ content: renderChildElement(children[index], components, handlers),
649
+ })),
650
+ selectedIndex: currentIndex,
651
+ onSelect: (index) => {
652
+ if (!Number.isInteger(index) || index < 0 || index >= tabDefinitions.length) {
653
+ throw new TypeError(`Expected an in-range tab index at native element ${element.key}`);
654
+ }
655
+ setSelectedIndex(index);
656
+ },
657
+ ...accessibilityProps,
658
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
659
+ });
660
+ }
661
+ function NativeModalRenderer({ element, components, handlers, }) {
662
+ const children = element.children ?? [];
663
+ if (children.length !== 2 || children[0]?.component !== "Button") {
664
+ throw new TypeError(`Expected a Button trigger and content at native element ${element.key}`);
665
+ }
666
+ const [open, setOpen] = useState(false);
667
+ const trigger = children[0];
668
+ const content = children[1];
669
+ const outerActivation = handlers.onElementActivate;
670
+ const triggerElement = renderElement(trigger, components, {
671
+ ...handlers,
672
+ onElementActivate: (key) => {
673
+ if (key === trigger.key) {
674
+ setOpen(true);
675
+ }
676
+ outerActivation?.(key);
677
+ },
678
+ });
679
+ const contentElement = renderElement(content, components, handlers);
680
+ return createElement(requireHostComponent(components.Modal, "Modal", element.key), {
681
+ trigger: triggerElement,
682
+ content: contentElement,
683
+ open,
684
+ onRequestClose: () => setOpen(false),
685
+ ...selectAccessibilityProps(element),
686
+ });
687
+ }
688
+ function requireHostComponent(component, name, key) {
689
+ if (component === undefined) {
690
+ throw new TypeError(`Missing host component ${JSON.stringify(name)} for native element ${key}`);
691
+ }
692
+ return component;
693
+ }
694
+ function selectImageComponent(element, components, useComponentVariants, variant) {
695
+ const base = requireHostComponent(components.Image, "Image", element.key);
696
+ if (!useComponentVariants) {
697
+ return base;
698
+ }
699
+ return components.variants?.Image?.[variant] ?? base;
700
+ }
701
+ function selectChoicePickerComponent(components, variant, key) {
702
+ return (components.variants?.ChoicePicker?.[variant] ??
703
+ requireHostComponent(components.ChoicePicker, "ChoicePicker", key));
704
+ }
705
+ function expectChoicePickerOptions(element) {
706
+ const value = element.props.options;
707
+ if (!Array.isArray(value)) {
708
+ throw new TypeError(`Expected choice options at native element ${element.key}.options`);
709
+ }
710
+ const seen = new Set();
711
+ return Object.freeze(value.map((entry, index) => {
712
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
713
+ throw new TypeError(`Expected a choice option at native element ${element.key}.options[${index}]`);
714
+ }
715
+ const option = entry;
716
+ const unknown = Object.keys(option).find((name) => name !== "label" && name !== "value");
717
+ if (unknown !== undefined ||
718
+ typeof option.label !== "string" ||
719
+ typeof option.value !== "string") {
720
+ throw new TypeError(`Expected a closed choice option at native element ${element.key}.options[${index}]`);
721
+ }
722
+ if (seen.has(option.value)) {
723
+ throw new TypeError(`Duplicate choice option at native element ${element.key}.options[${index}]`);
724
+ }
725
+ seen.add(option.value);
726
+ return Object.freeze({ label: option.label, value: option.value });
727
+ }));
728
+ }
729
+ function expectImageResourcePolicy(element, property = "resourcePolicy") {
730
+ const path = `native element ${element.key}.${property}`;
731
+ const value = parseJsonObject(element.props[property], path);
732
+ rejectObjectKeys(value, [
733
+ "allowedRedirectOrigins",
734
+ "cacheMode",
735
+ "maximumBytes",
736
+ "maximumDecodedHeight",
737
+ "maximumDecodedPixels",
738
+ "maximumDecodedWidth",
739
+ "maximumRedirects",
740
+ ], path);
741
+ const origins = value.allowedRedirectOrigins;
742
+ if (!Array.isArray(origins) || origins.some((origin) => typeof origin !== "string")) {
743
+ throw new TypeError(`Expected redirect origins at ${path}.allowedRedirectOrigins`);
744
+ }
745
+ if (value.cacheMode !== "default" && value.cacheMode !== "no-store") {
746
+ throw new TypeError(`Expected a closed cache mode at ${path}.cacheMode`);
747
+ }
748
+ const maximumBytes = expectObjectPositiveInteger(value, "maximumBytes", path);
749
+ const maximumDecodedHeight = expectObjectPositiveInteger(value, "maximumDecodedHeight", path);
750
+ const maximumDecodedPixels = expectObjectPositiveInteger(value, "maximumDecodedPixels", path);
751
+ const maximumDecodedWidth = expectObjectPositiveInteger(value, "maximumDecodedWidth", path);
752
+ const maximumRedirects = value.maximumRedirects;
753
+ if (typeof maximumRedirects !== "number" ||
754
+ !Number.isInteger(maximumRedirects) ||
755
+ maximumRedirects < 0) {
756
+ throw new TypeError(`Expected a non-negative integer at ${path}.maximumRedirects`);
757
+ }
758
+ return Object.freeze({
759
+ allowedRedirectOrigins: Object.freeze([...origins]),
760
+ cacheMode: value.cacheMode,
761
+ maximumBytes,
762
+ maximumDecodedHeight,
763
+ maximumDecodedPixels,
764
+ maximumDecodedWidth,
765
+ maximumRedirects,
766
+ });
767
+ }
768
+ function expectMediaResourcePolicy(element) {
769
+ const path = `native element ${element.key}.resourcePolicy`;
770
+ const value = parseJsonObject(element.props.resourcePolicy, path);
771
+ rejectObjectKeys(value, [
772
+ "allowedMimeTypes",
773
+ "allowedRedirectOrigins",
774
+ "allowsAutoplay",
775
+ "allowsBackgroundPlayback",
776
+ "allowsExternalRoutes",
777
+ "maximumBytes",
778
+ "maximumRedirects",
779
+ "requiresUserActivation",
780
+ "sourceOrigin",
781
+ ], path);
782
+ const sourceOrigin = expectObjectString(value, "sourceOrigin", path);
783
+ const allowedRedirectOrigins = expectObjectStringArray(value, "allowedRedirectOrigins", path);
784
+ const allowedMimeTypes = expectObjectStringArray(value, "allowedMimeTypes", path);
785
+ if (allowedMimeTypes.length === 0) {
786
+ throw new TypeError(`Expected at least one MIME type at ${path}.allowedMimeTypes`);
787
+ }
788
+ const maximumBytes = expectObjectPositiveInteger(value, "maximumBytes", path);
789
+ const maximumRedirects = expectObjectNonNegativeInteger(value, "maximumRedirects", path);
790
+ const allowsAutoplay = expectObjectBoolean(value, "allowsAutoplay", path);
791
+ const allowsBackgroundPlayback = expectObjectBoolean(value, "allowsBackgroundPlayback", path);
792
+ const allowsExternalRoutes = expectObjectBoolean(value, "allowsExternalRoutes", path);
793
+ const requiresUserActivation = expectObjectBoolean(value, "requiresUserActivation", path);
794
+ if (allowsAutoplay && requiresUserActivation) {
795
+ throw new TypeError(`Conflicting autoplay and activation controls at ${path}`);
796
+ }
797
+ return Object.freeze({
798
+ sourceOrigin,
799
+ allowedRedirectOrigins,
800
+ allowedMimeTypes,
801
+ maximumBytes,
802
+ maximumRedirects,
803
+ allowsAutoplay,
804
+ allowsBackgroundPlayback,
805
+ allowsExternalRoutes,
806
+ requiresUserActivation,
807
+ });
808
+ }
809
+ function expectHostExtensionCapabilityGrant(element) {
810
+ const path = `native element ${element.key}.capabilityGrant`;
811
+ const value = parseJsonObject(element.props.capabilityGrant, path);
812
+ rejectObjectKeys(value, ["permissions", "resources"], path);
813
+ return Object.freeze({
814
+ permissions: expectObjectStringArray(value, "permissions", path),
815
+ resources: expectObjectStringArray(value, "resources", path),
816
+ });
817
+ }
818
+ function requireHostExtensionRegistration(components, identity, key) {
819
+ if (!Array.isArray(components.hostExtensions)) {
820
+ throw new TypeError(`Missing host-extension registrations for native element ${key}`);
821
+ }
822
+ const registrations = components.hostExtensions.filter((registration) => {
823
+ if (!isNativeHostExtensionRegistration(registration)) {
824
+ throw new TypeError("Host-extension catalogs accept only helper-created registrations");
825
+ }
826
+ const manifest = registration.manifest;
827
+ return (manifest.extensionId === identity.extensionId &&
828
+ manifest.catalogId === identity.catalogId &&
829
+ manifest.schemaVersion === identity.schemaVersion &&
830
+ manifest.componentName === identity.componentName);
831
+ });
832
+ if (registrations.length !== 1) {
833
+ throw new TypeError(`Expected exactly one local registration for ${JSON.stringify(identity.componentName)} at native element ${key}`);
834
+ }
835
+ const registration = registrations[0];
836
+ if (registration.manifestFingerprint !== identity.manifestFingerprint) {
837
+ throw new TypeError(`Local registration manifest mismatch for ${JSON.stringify(identity.componentName)} at native element ${key}`);
254
838
  }
839
+ return registration;
840
+ }
841
+ function expectObjectStringArray(value, name, path) {
842
+ const field = value[name];
843
+ if (!Array.isArray(field) || field.some((entry) => typeof entry !== "string")) {
844
+ throw new TypeError(`Expected a string array at ${path}.${name}`);
845
+ }
846
+ if (new Set(field).size !== field.length) {
847
+ throw new TypeError(`Expected unique strings at ${path}.${name}`);
848
+ }
849
+ return Object.freeze([...field]);
850
+ }
851
+ function expectObjectBoolean(value, name, path) {
852
+ const field = value[name];
853
+ if (typeof field !== "boolean") {
854
+ throw new TypeError(`Expected a boolean at ${path}.${name}`);
855
+ }
856
+ return field;
857
+ }
858
+ function expectObjectNonNegativeInteger(value, name, path) {
859
+ const field = value[name];
860
+ if (typeof field !== "number" || !Number.isInteger(field) || field < 0) {
861
+ throw new TypeError(`Expected a non-negative integer at ${path}.${name}`);
862
+ }
863
+ return field;
864
+ }
865
+ function expectObjectPositiveInteger(value, name, path) {
866
+ const field = value[name];
867
+ if (typeof field !== "number" || !Number.isInteger(field) || field < 1) {
868
+ throw new TypeError(`Expected a positive integer at ${path}.${name}`);
869
+ }
870
+ return field;
871
+ }
872
+ function validateChoicePickerChange(value, options, variant, key) {
873
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
874
+ throw new TypeError(`Expected an array of choice values at native element ${key}`);
875
+ }
876
+ if (new Set(value).size !== value.length) {
877
+ throw new TypeError(`Expected unique choice values at native element ${key}`);
878
+ }
879
+ if (variant === "mutuallyExclusive" && value.length > 1) {
880
+ throw new TypeError(`Expected at most one choice value at native element ${key}`);
881
+ }
882
+ const allowed = new Set(options.map((option) => option.value));
883
+ if (value.some((entry) => !allowed.has(entry))) {
884
+ throw new TypeError(`Expected known choice values at native element ${key}`);
885
+ }
886
+ return Object.freeze([...value]);
887
+ }
888
+ function expectTabDefinitions(element) {
889
+ const value = element.props.tabs;
890
+ if (!Array.isArray(value)) {
891
+ throw new TypeError(`Expected tabs at native element ${element.key}.tabs`);
892
+ }
893
+ return Object.freeze(value.map((entry, index) => {
894
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
895
+ throw new TypeError(`Expected a tab at native element ${element.key}.tabs[${index}]`);
896
+ }
897
+ const tab = entry;
898
+ if (Object.keys(tab).some((name) => name !== "title") || typeof tab.title !== "string") {
899
+ throw new TypeError(`Expected a closed tab at native element ${element.key}.tabs[${index}]`);
900
+ }
901
+ return Object.freeze({ title: tab.title });
902
+ }));
255
903
  }
256
904
  function renderChildElement(element, components, handlers) {
257
905
  const rendered = renderElement(element, components, handlers);
@@ -394,21 +1042,36 @@ function createButtonPressHandler(element, handlers) {
394
1042
  if (handlers.onAction === undefined) {
395
1043
  throw new TypeError(`Missing native action handler for element ${element.key}`);
396
1044
  }
397
- return disabled ? () => undefined : () => handlers.onAction?.(action);
1045
+ return disabled
1046
+ ? () => undefined
1047
+ : () => {
1048
+ handlers.onElementActivate?.(element.key);
1049
+ handlers.onAction?.(action);
1050
+ };
398
1051
  }
399
1052
  if (hasEvent) {
400
1053
  const event = expectV1EventProp(element);
401
1054
  if (handlers.onV1Event === undefined) {
402
1055
  throw new TypeError(`Missing A2UI v1 event handler for element ${element.key}`);
403
1056
  }
404
- return disabled ? () => undefined : () => handlers.onV1Event?.(event);
1057
+ return disabled
1058
+ ? () => undefined
1059
+ : () => {
1060
+ handlers.onElementActivate?.(element.key);
1061
+ handlers.onV1Event?.(event);
1062
+ };
405
1063
  }
406
1064
  if (hasOpenUrl) {
407
1065
  const request = expectV1OpenUrlProp(element);
408
1066
  if (handlers.onV1OpenUrl === undefined) {
409
1067
  throw new TypeError(`Missing A2UI v1 openUrl policy or handler for element ${element.key}`);
410
1068
  }
411
- return disabled ? () => undefined : () => handlers.onV1OpenUrl?.(request);
1069
+ return disabled
1070
+ ? () => undefined
1071
+ : () => {
1072
+ handlers.onElementActivate?.(element.key);
1073
+ handlers.onV1OpenUrl?.(request);
1074
+ };
412
1075
  }
413
1076
  expectV1InvalidLocalOpenUrlProp(element);
414
1077
  if (!disabled) {
@@ -442,6 +1105,13 @@ function expectStringProp(element, name) {
442
1105
  }
443
1106
  return value;
444
1107
  }
1108
+ function selectClosedStringProp(element, name, allowed) {
1109
+ const value = expectStringProp(element, name);
1110
+ if (!allowed.includes(value)) {
1111
+ throw new TypeError(`Unsupported value ${JSON.stringify(value)} at native element ${element.key}.${name}`);
1112
+ }
1113
+ return value;
1114
+ }
445
1115
  function optionalStringProp(element, name) {
446
1116
  const value = element.props[name];
447
1117
  return value === undefined ? undefined : expectStringProp(element, name);
@@ -456,6 +1126,20 @@ function optionalStringArrayProp(element, name) {
456
1126
  }
457
1127
  return value;
458
1128
  }
1129
+ function expectStringArrayProp(element, name) {
1130
+ const value = optionalStringArrayProp(element, name);
1131
+ if (value === undefined) {
1132
+ throw new TypeError(`Expected an array of strings at native element ${element.key}.${name}`);
1133
+ }
1134
+ return value;
1135
+ }
1136
+ function expectBooleanProp(element, name) {
1137
+ const value = optionalBooleanProp(element, name);
1138
+ if (value === undefined) {
1139
+ throw new TypeError(`Expected a boolean at native element ${element.key}.${name}`);
1140
+ }
1141
+ return value;
1142
+ }
459
1143
  function optionalBooleanProp(element, name) {
460
1144
  const value = element.props[name];
461
1145
  if (value !== undefined && typeof value !== "boolean") {
@@ -470,6 +1154,13 @@ function optionalFiniteNumberProp(element, name) {
470
1154
  }
471
1155
  return value;
472
1156
  }
1157
+ function expectFiniteNumberProp(element, name) {
1158
+ const value = optionalFiniteNumberProp(element, name);
1159
+ if (value === undefined) {
1160
+ throw new TypeError(`Expected a finite number at native element ${element.key}.${name}`);
1161
+ }
1162
+ return value;
1163
+ }
473
1164
  function optionalAccessibilityLiveProp(element) {
474
1165
  const value = element.props.accessibilityLive;
475
1166
  if (value === undefined || value === "assertive" || value === "off" || value === "polite") {
@@ -550,9 +1241,7 @@ function updateDataModelBinding(dataModel, binding, value) {
550
1241
  if (typeof binding !== "string" || !binding.startsWith("/") || binding.length === 1) {
551
1242
  throw new TypeError(`Expected a non-root absolute JSON Pointer binding, received ${JSON.stringify(binding)}`);
552
1243
  }
553
- if (typeof value !== "string") {
554
- throw new TypeError("Expected a string renderer binding value");
555
- }
1244
+ const parsedValue = parseJsonValue(value, "renderer binding value");
556
1245
  const next = parseJsonObject(dataModel, "renderer data model");
557
1246
  const tokens = binding
558
1247
  .slice(1)
@@ -570,10 +1259,7 @@ function updateDataModelBinding(dataModel, binding, value) {
570
1259
  throw new TypeError(`Renderer binding ${JSON.stringify(binding)} is missing`);
571
1260
  }
572
1261
  if (last) {
573
- if (typeof cursor[arrayIndex] !== "string") {
574
- throw new TypeError(`Renderer binding ${JSON.stringify(binding)} must reference an existing string value`);
575
- }
576
- cursor[arrayIndex] = value;
1262
+ cursor[arrayIndex] = validateRendererBindingReplacement(cursor[arrayIndex], parsedValue, binding);
577
1263
  break;
578
1264
  }
579
1265
  cursor = cursor[arrayIndex];
@@ -583,16 +1269,27 @@ function updateDataModelBinding(dataModel, binding, value) {
583
1269
  throw new TypeError(`Renderer binding ${JSON.stringify(binding)} is missing`);
584
1270
  }
585
1271
  if (last) {
586
- if (typeof cursor[token] !== "string") {
587
- throw new TypeError(`Renderer binding ${JSON.stringify(binding)} must reference an existing string value`);
588
- }
589
- defineJsonProperty(cursor, token, value);
1272
+ defineJsonProperty(cursor, token, validateRendererBindingReplacement(cursor[token], parsedValue, binding));
590
1273
  break;
591
1274
  }
592
1275
  cursor = cursor[token];
593
1276
  }
594
1277
  return parseJsonObject(next, "renderer data model");
595
1278
  }
1279
+ function validateRendererBindingReplacement(current, next, binding) {
1280
+ if ((typeof current === "string" && typeof next === "string") ||
1281
+ (typeof current === "boolean" && typeof next === "boolean") ||
1282
+ (typeof current === "number" && typeof next === "number")) {
1283
+ return next;
1284
+ }
1285
+ if (Array.isArray(current) &&
1286
+ current.every((value) => typeof value === "string") &&
1287
+ Array.isArray(next) &&
1288
+ next.every((value) => typeof value === "string")) {
1289
+ return Object.freeze([...next]);
1290
+ }
1291
+ throw new TypeError(`Renderer binding ${JSON.stringify(binding)} must preserve its string, boolean, number, or string-array value type`);
1292
+ }
596
1293
  function decodePointerToken(token, pointer) {
597
1294
  for (let index = 0; index < token.length; index += 1) {
598
1295
  if (token[index] !== "~") {
@@ -614,5 +1311,5 @@ function defineJsonProperty(object, key, value) {
614
1311
  writable: true,
615
1312
  });
616
1313
  }
617
- export { A2UI_V1_NATIVE_COMPONENT_NAMES, A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH, A2UI_V1_NATIVE_MAX_RENDER_NODES, createA2uiV1NativeRenderPlan, resolveA2uiV1NativeEvent, resolveA2uiV1NativeOpenUrl, } from "./v1.js";
1314
+ export { A2UI_V1_NATIVE_COMPONENT_NAMES, A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS, A2UI_V1_NATIVE_MAX_IMAGE_BYTES, A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION, A2UI_V1_NATIVE_MAX_IMAGE_PIXELS, A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS, A2UI_V1_NATIVE_MAX_IMAGE_REDIRECTS, A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH, A2UI_V1_NATIVE_MAX_IMAGES, A2UI_V1_NATIVE_MAX_MEDIA, A2UI_V1_NATIVE_MAX_MEDIA_BYTES, A2UI_V1_NATIVE_MAX_MEDIA_MIME_TYPES, A2UI_V1_NATIVE_MAX_MEDIA_REDIRECT_ORIGINS, A2UI_V1_NATIVE_MAX_MEDIA_REDIRECTS, A2UI_V1_NATIVE_MAX_MEDIA_URL_LENGTH, A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH, A2UI_V1_NATIVE_MAX_RENDER_NODES, A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES, A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS, A2UI_V1_NATIVE_MAX_TOTAL_MEDIA_BYTES, createA2uiV1NativeRenderPlan, resolveA2uiV1NativeEvent, resolveA2uiV1NativeOpenUrl, } from "./v1.js";
618
1315
  //# sourceMappingURL=index.js.map