@mcp-native/react-native 0.5.0 → 0.7.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,40 @@
1
1
  import { createA2uiV1ActionEnvelope, validateA2uiV1SurfaceState } from "@mcp-native/a2ui";
2
- import { parseJsonObject, parseMcpNativeAction } from "@mcp-native/core";
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 } 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, createNativeCheckBoxAdapter, createNativeChoicePickerAdapter, createNativeDateTimeInputAdapter, createNativeDividerAdapter, createNativeIconAdapter, createNativeImageAdapter, createNativeModalAdapter, createNativeSliderAdapter, createNativeTabsAdapter, createNativeTextAdapter, createNativeTextInputAdapter, createNativeViewAdapter, } 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
+ "CheckBox",
18
+ "ChoicePicker",
19
+ "DateTimeInput",
20
+ "Divider",
21
+ "Icon",
22
+ "Image",
23
+ "Modal",
24
+ "Slider",
25
+ "Tabs",
26
+ ]);
27
+ /** Returns the exact A2UI subset backed by one locally installed and policy-ready host catalog. */
28
+ export function getA2uiV1NativeSupportedComponentNames(components, capabilities = {}) {
29
+ const installed = new Set(A2UI_V1_NATIVE_BASE_COMPONENT_NAMES);
30
+ for (const name of A2UI_V1_NATIVE_OPTIONAL_COMPONENT_SLOTS) {
31
+ if (components[name] !== undefined &&
32
+ (name !== "Image" || capabilities.imagePolicy !== undefined)) {
33
+ installed.add(name);
34
+ }
35
+ }
36
+ return Object.freeze(A2UI_V1_NATIVE_COMPONENT_NAMES.filter((name) => installed.has(name)));
37
+ }
6
38
  /** @deprecated Use `createA2uiV1NativeRenderPlan`. */
7
39
  export function createNativeRenderPlan(surface) {
8
40
  return renderNode(surface.root);
@@ -36,7 +68,7 @@ export function McpNativeSurface({ surface, components, onAction, onBindingChang
36
68
  * Mounts the supported A2UI v1 subset with renderer-local two-way bindings.
37
69
  * Agent events are resolved against the latest local model at press time.
38
70
  */
39
- export function A2uiV1NativeSurface({ surface, policy, components, onAction, openUrlPolicy, onOpenUrl, onDataModelChange, actionMetadata, locale, now = currentTimestamp, }) {
71
+ export function A2uiV1NativeSurface({ surface, policy, components, onAction, openUrlPolicy, onOpenUrl, imagePolicy, onDataModelChange, actionMetadata, locale, now = currentTimestamp, }) {
40
72
  const validatedSurface = useMemo(() => validateA2uiV1SurfaceState(surface, policy), [policy, surface]);
41
73
  const sourceDataModel = validatedSurface.dataModel;
42
74
  const sourceDataModelKey = createDataModelSourceKey(validatedSurface);
@@ -71,7 +103,8 @@ export function A2uiV1NativeSurface({ surface, policy, components, onAction, ope
71
103
  : createA2uiV1NativeRenderPlan)(validatedSurface, policy, {
72
104
  dataModel,
73
105
  ...(locale === undefined ? {} : { locale }),
74
- }), [dataModel, locale, policy, tolerateInvalidLocalOpenUrls, validatedSurface]);
106
+ ...(imagePolicy === undefined ? {} : { imagePolicy }),
107
+ }), [dataModel, imagePolicy, locale, policy, tolerateInvalidLocalOpenUrls, validatedSurface]);
75
108
  const handleBindingChange = useCallback((binding, value) => {
76
109
  const next = updateDataModelBinding(dataModelRef.current, binding, value);
77
110
  dataModelRef.current = next;
@@ -116,7 +149,7 @@ export function A2uiV1NativeSurface({ surface, policy, components, onAction, ope
116
149
  }, [locale, onOpenUrl, openUrlPolicy, policy, validatedSurface]);
117
150
  return renderElement(plan, components, {
118
151
  useComponentVariants: true,
119
- onBindingChange: handleBindingChange,
152
+ onV1BindingChange: handleBindingChange,
120
153
  onV1Event: handleEvent,
121
154
  ...(openUrlPolicy === undefined || onOpenUrl === undefined
122
155
  ? {}
@@ -244,15 +277,421 @@ function renderElement(element, components, handlers) {
244
277
  ...(invalid === undefined ? {} : { invalid }),
245
278
  ...(validationMessages === undefined ? {} : { validationMessages }),
246
279
  ...(value === undefined ? {} : { value }),
247
- ...(binding === undefined || handlers.onBindingChange === undefined
280
+ ...(binding === undefined ||
281
+ (handlers.onBindingChange === undefined && handlers.onV1BindingChange === undefined)
282
+ ? {}
283
+ : {
284
+ onChangeText: (nextValue) => {
285
+ if (handlers.onV1BindingChange !== undefined) {
286
+ handlers.onV1BindingChange(binding, nextValue);
287
+ }
288
+ else {
289
+ handlers.onBindingChange?.(binding, nextValue);
290
+ }
291
+ },
292
+ }),
293
+ });
294
+ }
295
+ case "Image": {
296
+ const uri = expectStringProp(element, "uri");
297
+ const fit = selectClosedStringProp(element, "fit", [
298
+ "contain",
299
+ "cover",
300
+ "fill",
301
+ "none",
302
+ "scaleDown",
303
+ ]);
304
+ const variant = selectClosedStringProp(element, "variant", [
305
+ "avatar",
306
+ "header",
307
+ "icon",
308
+ "largeFeature",
309
+ "mediumFeature",
310
+ "smallFeature",
311
+ ]);
312
+ const accessible = accessibilityProps.accessibilityLabel !== undefined &&
313
+ accessibilityProps.accessibilityElementsHidden !== true;
314
+ return createElement(selectImageComponent(element, components, handlers.useComponentVariants === true, variant), {
315
+ key: element.key,
316
+ uri,
317
+ fit,
318
+ resourcePolicy: expectImageResourcePolicy(element),
319
+ ...accessibilityProps,
320
+ accessible,
321
+ accessibilityRole: "image",
322
+ });
323
+ }
324
+ case "Icon": {
325
+ const name = expectStringProp(element, "name");
326
+ if (!A2UI_V1_NATIVE_ICON_NAMES.includes(name)) {
327
+ throw new TypeError(`Unsupported icon name at native element ${element.key}.name`);
328
+ }
329
+ const accessible = accessibilityProps.accessibilityLabel !== undefined &&
330
+ accessibilityProps.accessibilityElementsHidden !== true;
331
+ return createElement(requireHostComponent(components.Icon, "Icon", element.key), {
332
+ key: element.key,
333
+ name: name,
334
+ ...accessibilityProps,
335
+ accessible,
336
+ accessibilityRole: "image",
337
+ });
338
+ }
339
+ case "Divider":
340
+ return createElement(requireHostComponent(components.Divider, "Divider", element.key), {
341
+ key: element.key,
342
+ axis: selectClosedStringProp(element, "axis", ["horizontal", "vertical"]),
343
+ ...accessibilityProps,
344
+ accessible: false,
345
+ });
346
+ case "CheckBox": {
347
+ const label = expectStringProp(element, "label");
348
+ const value = expectBooleanProp(element, "value");
349
+ const binding = optionalStringProp(element, "binding");
350
+ const invalid = optionalBooleanProp(element, "invalid");
351
+ const validationMessages = optionalStringArrayProp(element, "validationMessages");
352
+ return createElement(requireHostComponent(components.CheckBox, "CheckBox", element.key), {
353
+ key: element.key,
354
+ label,
355
+ value,
356
+ ...accessibilityProps,
357
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
358
+ accessibilityLabel: accessibilityProps.accessibilityLabel ?? label,
359
+ accessibilityRole: "checkbox",
360
+ accessibilityState: { checked: value },
361
+ ...(invalid === undefined ? {} : { invalid }),
362
+ ...(validationMessages === undefined ? {} : { validationMessages }),
363
+ ...(binding === undefined || handlers.onV1BindingChange === undefined
364
+ ? {}
365
+ : {
366
+ onValueChange: (nextValue) => {
367
+ if (typeof nextValue !== "boolean") {
368
+ throw new TypeError(`Expected a boolean checkbox value at native element ${element.key}`);
369
+ }
370
+ handlers.onV1BindingChange?.(binding, nextValue);
371
+ },
372
+ }),
373
+ });
374
+ }
375
+ case "ChoicePicker": {
376
+ const options = expectChoicePickerOptions(element);
377
+ const value = expectStringArrayProp(element, "value");
378
+ const variant = selectClosedStringProp(element, "variant", [
379
+ "multipleSelection",
380
+ "mutuallyExclusive",
381
+ ]);
382
+ const displayStyle = selectClosedStringProp(element, "displayStyle", [
383
+ "checkbox",
384
+ "chips",
385
+ ]);
386
+ const filterable = expectBooleanProp(element, "filterable");
387
+ const label = optionalStringProp(element, "label");
388
+ const accessibilityLabel = expectStringProp(element, "accessibilityLabel");
389
+ const binding = optionalStringProp(element, "binding");
390
+ const invalid = optionalBooleanProp(element, "invalid");
391
+ const validationMessages = optionalStringArrayProp(element, "validationMessages");
392
+ return createElement(selectChoicePickerComponent(components, variant, element.key), {
393
+ key: element.key,
394
+ options,
395
+ value,
396
+ variant,
397
+ displayStyle,
398
+ filterable,
399
+ ...accessibilityProps,
400
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
401
+ accessibilityLabel,
402
+ ...(label === undefined ? {} : { label }),
403
+ ...(invalid === undefined ? {} : { invalid }),
404
+ ...(validationMessages === undefined ? {} : { validationMessages }),
405
+ ...(binding === undefined || handlers.onV1BindingChange === undefined
406
+ ? {}
407
+ : {
408
+ onValueChange: (nextValue) => {
409
+ const parsed = validateChoicePickerChange(nextValue, options, variant, element.key);
410
+ handlers.onV1BindingChange?.(binding, parsed);
411
+ },
412
+ }),
413
+ });
414
+ }
415
+ case "Slider": {
416
+ const value = expectFiniteNumberProp(element, "value");
417
+ const minimumValue = expectFiniteNumberProp(element, "minimum");
418
+ const maximumValue = expectFiniteNumberProp(element, "maximum");
419
+ const step = optionalFiniteNumberProp(element, "step");
420
+ const label = optionalStringProp(element, "label");
421
+ const accessibilityLabel = expectStringProp(element, "accessibilityLabel");
422
+ const binding = optionalStringProp(element, "binding");
423
+ const invalid = optionalBooleanProp(element, "invalid");
424
+ const validationMessages = optionalStringArrayProp(element, "validationMessages");
425
+ return createElement(requireHostComponent(components.Slider, "Slider", element.key), {
426
+ key: element.key,
427
+ value,
428
+ minimumValue,
429
+ maximumValue,
430
+ ...accessibilityProps,
431
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
432
+ accessibilityLabel,
433
+ accessibilityRole: "adjustable",
434
+ ...(label === undefined ? {} : { label }),
435
+ ...(step === undefined ? {} : { step }),
436
+ ...(invalid === undefined ? {} : { invalid }),
437
+ ...(validationMessages === undefined ? {} : { validationMessages }),
438
+ ...(binding === undefined || handlers.onV1BindingChange === undefined
439
+ ? {}
440
+ : {
441
+ onValueChange: (nextValue) => {
442
+ if (typeof nextValue !== "number" ||
443
+ !Number.isFinite(nextValue) ||
444
+ nextValue < minimumValue ||
445
+ nextValue > maximumValue ||
446
+ (step !== undefined && !isSliderStepValue(nextValue, minimumValue, step))) {
447
+ throw new TypeError(`Expected an in-range finite slider value at native element ${element.key}`);
448
+ }
449
+ handlers.onV1BindingChange?.(binding, nextValue);
450
+ },
451
+ }),
452
+ });
453
+ }
454
+ case "DateTimeInput": {
455
+ const value = expectStringProp(element, "value");
456
+ const enableDate = expectBooleanProp(element, "enableDate");
457
+ const enableTime = expectBooleanProp(element, "enableTime");
458
+ const minimum = optionalStringProp(element, "minimum");
459
+ const maximum = optionalStringProp(element, "maximum");
460
+ const label = optionalStringProp(element, "label");
461
+ const accessibilityLabel = expectStringProp(element, "accessibilityLabel");
462
+ const binding = optionalStringProp(element, "binding");
463
+ const invalid = optionalBooleanProp(element, "invalid");
464
+ const validationMessages = optionalStringArrayProp(element, "validationMessages");
465
+ return createElement(requireHostComponent(components.DateTimeInput, "DateTimeInput", element.key), {
466
+ key: element.key,
467
+ value,
468
+ enableDate,
469
+ enableTime,
470
+ ...accessibilityProps,
471
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
472
+ accessibilityLabel,
473
+ ...(label === undefined ? {} : { label }),
474
+ ...(minimum === undefined ? {} : { minimum }),
475
+ ...(maximum === undefined ? {} : { maximum }),
476
+ ...(invalid === undefined ? {} : { invalid }),
477
+ ...(validationMessages === undefined ? {} : { validationMessages }),
478
+ ...(binding === undefined || handlers.onV1BindingChange === undefined
248
479
  ? {}
249
480
  : {
250
- onChangeText: (nextValue) => handlers.onBindingChange?.(binding, nextValue),
481
+ onValueChange: (nextValue) => {
482
+ if (typeof nextValue !== "string") {
483
+ throw new TypeError(`Expected a string date/time value at native element ${element.key}`);
484
+ }
485
+ try {
486
+ validateA2uiV1NativeDateTimeInputChange(nextValue, enableDate, enableTime, minimum, maximum, `native element ${element.key}`);
487
+ }
488
+ catch (error) {
489
+ const message = error instanceof Error
490
+ ? error.message
491
+ : `Expected a valid date/time value at native element ${element.key}`;
492
+ throw new TypeError(message, { cause: error });
493
+ }
494
+ handlers.onV1BindingChange?.(binding, nextValue);
495
+ },
251
496
  }),
252
497
  });
253
498
  }
499
+ case "Tabs":
500
+ return createElement(NativeTabsRenderer, {
501
+ key: element.key,
502
+ element,
503
+ components,
504
+ handlers,
505
+ });
506
+ case "Modal":
507
+ return createElement(NativeModalRenderer, {
508
+ key: element.key,
509
+ element,
510
+ components,
511
+ handlers,
512
+ });
254
513
  }
255
514
  }
515
+ function isSliderStepValue(value, minimum, step) {
516
+ const offset = (value - minimum) / step;
517
+ return (Math.abs(offset - Math.round(offset)) <= Number.EPSILON * Math.max(1, Math.abs(offset)) * 8);
518
+ }
519
+ function NativeTabsRenderer({ element, components, handlers, }) {
520
+ const tabDefinitions = expectTabDefinitions(element);
521
+ const children = element.children ?? [];
522
+ if (tabDefinitions.length !== children.length || tabDefinitions.length === 0) {
523
+ throw new TypeError(`Expected matching non-empty tabs and children at native element ${element.key}`);
524
+ }
525
+ const [selectedIndex, setSelectedIndex] = useState(0);
526
+ const currentIndex = selectedIndex < tabDefinitions.length ? selectedIndex : 0;
527
+ useEffect(() => {
528
+ if (selectedIndex >= tabDefinitions.length) {
529
+ setSelectedIndex(0);
530
+ }
531
+ }, [selectedIndex, tabDefinitions.length]);
532
+ const accessibilityProps = selectAccessibilityProps(element);
533
+ return createElement(requireHostComponent(components.Tabs, "Tabs", element.key), {
534
+ tabs: tabDefinitions.map((tab, index) => ({
535
+ title: tab.title,
536
+ content: renderChildElement(children[index], components, handlers),
537
+ })),
538
+ selectedIndex: currentIndex,
539
+ onSelect: (index) => {
540
+ if (!Number.isInteger(index) || index < 0 || index >= tabDefinitions.length) {
541
+ throw new TypeError(`Expected an in-range tab index at native element ${element.key}`);
542
+ }
543
+ setSelectedIndex(index);
544
+ },
545
+ ...accessibilityProps,
546
+ accessible: accessibilityProps.accessibilityElementsHidden !== true,
547
+ });
548
+ }
549
+ function NativeModalRenderer({ element, components, handlers, }) {
550
+ const children = element.children ?? [];
551
+ if (children.length !== 2 || children[0]?.component !== "Button") {
552
+ throw new TypeError(`Expected a Button trigger and content at native element ${element.key}`);
553
+ }
554
+ const [open, setOpen] = useState(false);
555
+ const trigger = children[0];
556
+ const content = children[1];
557
+ const outerActivation = handlers.onElementActivate;
558
+ const triggerElement = renderElement(trigger, components, {
559
+ ...handlers,
560
+ onElementActivate: (key) => {
561
+ if (key === trigger.key) {
562
+ setOpen(true);
563
+ }
564
+ outerActivation?.(key);
565
+ },
566
+ });
567
+ const contentElement = renderElement(content, components, handlers);
568
+ return createElement(requireHostComponent(components.Modal, "Modal", element.key), {
569
+ trigger: triggerElement,
570
+ content: contentElement,
571
+ open,
572
+ onRequestClose: () => setOpen(false),
573
+ ...selectAccessibilityProps(element),
574
+ });
575
+ }
576
+ function requireHostComponent(component, name, key) {
577
+ if (component === undefined) {
578
+ throw new TypeError(`Missing host component ${JSON.stringify(name)} for native element ${key}`);
579
+ }
580
+ return component;
581
+ }
582
+ function selectImageComponent(element, components, useComponentVariants, variant) {
583
+ const base = requireHostComponent(components.Image, "Image", element.key);
584
+ if (!useComponentVariants) {
585
+ return base;
586
+ }
587
+ return components.variants?.Image?.[variant] ?? base;
588
+ }
589
+ function selectChoicePickerComponent(components, variant, key) {
590
+ return (components.variants?.ChoicePicker?.[variant] ??
591
+ requireHostComponent(components.ChoicePicker, "ChoicePicker", key));
592
+ }
593
+ function expectChoicePickerOptions(element) {
594
+ const value = element.props.options;
595
+ if (!Array.isArray(value)) {
596
+ throw new TypeError(`Expected choice options at native element ${element.key}.options`);
597
+ }
598
+ const seen = new Set();
599
+ return Object.freeze(value.map((entry, index) => {
600
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
601
+ throw new TypeError(`Expected a choice option at native element ${element.key}.options[${index}]`);
602
+ }
603
+ const option = entry;
604
+ const unknown = Object.keys(option).find((name) => name !== "label" && name !== "value");
605
+ if (unknown !== undefined ||
606
+ typeof option.label !== "string" ||
607
+ typeof option.value !== "string") {
608
+ throw new TypeError(`Expected a closed choice option at native element ${element.key}.options[${index}]`);
609
+ }
610
+ if (seen.has(option.value)) {
611
+ throw new TypeError(`Duplicate choice option at native element ${element.key}.options[${index}]`);
612
+ }
613
+ seen.add(option.value);
614
+ return Object.freeze({ label: option.label, value: option.value });
615
+ }));
616
+ }
617
+ function expectImageResourcePolicy(element) {
618
+ const path = `native element ${element.key}.resourcePolicy`;
619
+ const value = parseJsonObject(element.props.resourcePolicy, path);
620
+ rejectObjectKeys(value, [
621
+ "allowedRedirectOrigins",
622
+ "cacheMode",
623
+ "maximumBytes",
624
+ "maximumDecodedHeight",
625
+ "maximumDecodedPixels",
626
+ "maximumDecodedWidth",
627
+ "maximumRedirects",
628
+ ], path);
629
+ const origins = value.allowedRedirectOrigins;
630
+ if (!Array.isArray(origins) || origins.some((origin) => typeof origin !== "string")) {
631
+ throw new TypeError(`Expected redirect origins at ${path}.allowedRedirectOrigins`);
632
+ }
633
+ if (value.cacheMode !== "default" && value.cacheMode !== "no-store") {
634
+ throw new TypeError(`Expected a closed cache mode at ${path}.cacheMode`);
635
+ }
636
+ const maximumBytes = expectObjectPositiveInteger(value, "maximumBytes", path);
637
+ const maximumDecodedHeight = expectObjectPositiveInteger(value, "maximumDecodedHeight", path);
638
+ const maximumDecodedPixels = expectObjectPositiveInteger(value, "maximumDecodedPixels", path);
639
+ const maximumDecodedWidth = expectObjectPositiveInteger(value, "maximumDecodedWidth", path);
640
+ const maximumRedirects = value.maximumRedirects;
641
+ if (typeof maximumRedirects !== "number" ||
642
+ !Number.isInteger(maximumRedirects) ||
643
+ maximumRedirects < 0) {
644
+ throw new TypeError(`Expected a non-negative integer at ${path}.maximumRedirects`);
645
+ }
646
+ return Object.freeze({
647
+ allowedRedirectOrigins: Object.freeze([...origins]),
648
+ cacheMode: value.cacheMode,
649
+ maximumBytes,
650
+ maximumDecodedHeight,
651
+ maximumDecodedPixels,
652
+ maximumDecodedWidth,
653
+ maximumRedirects,
654
+ });
655
+ }
656
+ function expectObjectPositiveInteger(value, name, path) {
657
+ const field = value[name];
658
+ if (typeof field !== "number" || !Number.isInteger(field) || field < 1) {
659
+ throw new TypeError(`Expected a positive integer at ${path}.${name}`);
660
+ }
661
+ return field;
662
+ }
663
+ function validateChoicePickerChange(value, options, variant, key) {
664
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
665
+ throw new TypeError(`Expected an array of choice values at native element ${key}`);
666
+ }
667
+ if (new Set(value).size !== value.length) {
668
+ throw new TypeError(`Expected unique choice values at native element ${key}`);
669
+ }
670
+ if (variant === "mutuallyExclusive" && value.length > 1) {
671
+ throw new TypeError(`Expected at most one choice value at native element ${key}`);
672
+ }
673
+ const allowed = new Set(options.map((option) => option.value));
674
+ if (value.some((entry) => !allowed.has(entry))) {
675
+ throw new TypeError(`Expected known choice values at native element ${key}`);
676
+ }
677
+ return Object.freeze([...value]);
678
+ }
679
+ function expectTabDefinitions(element) {
680
+ const value = element.props.tabs;
681
+ if (!Array.isArray(value)) {
682
+ throw new TypeError(`Expected tabs at native element ${element.key}.tabs`);
683
+ }
684
+ return Object.freeze(value.map((entry, index) => {
685
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
686
+ throw new TypeError(`Expected a tab at native element ${element.key}.tabs[${index}]`);
687
+ }
688
+ const tab = entry;
689
+ if (Object.keys(tab).some((name) => name !== "title") || typeof tab.title !== "string") {
690
+ throw new TypeError(`Expected a closed tab at native element ${element.key}.tabs[${index}]`);
691
+ }
692
+ return Object.freeze({ title: tab.title });
693
+ }));
694
+ }
256
695
  function renderChildElement(element, components, handlers) {
257
696
  const rendered = renderElement(element, components, handlers);
258
697
  const weight = optionalFiniteNumberProp(element, "weight");
@@ -394,21 +833,36 @@ function createButtonPressHandler(element, handlers) {
394
833
  if (handlers.onAction === undefined) {
395
834
  throw new TypeError(`Missing native action handler for element ${element.key}`);
396
835
  }
397
- return disabled ? () => undefined : () => handlers.onAction?.(action);
836
+ return disabled
837
+ ? () => undefined
838
+ : () => {
839
+ handlers.onElementActivate?.(element.key);
840
+ handlers.onAction?.(action);
841
+ };
398
842
  }
399
843
  if (hasEvent) {
400
844
  const event = expectV1EventProp(element);
401
845
  if (handlers.onV1Event === undefined) {
402
846
  throw new TypeError(`Missing A2UI v1 event handler for element ${element.key}`);
403
847
  }
404
- return disabled ? () => undefined : () => handlers.onV1Event?.(event);
848
+ return disabled
849
+ ? () => undefined
850
+ : () => {
851
+ handlers.onElementActivate?.(element.key);
852
+ handlers.onV1Event?.(event);
853
+ };
405
854
  }
406
855
  if (hasOpenUrl) {
407
856
  const request = expectV1OpenUrlProp(element);
408
857
  if (handlers.onV1OpenUrl === undefined) {
409
858
  throw new TypeError(`Missing A2UI v1 openUrl policy or handler for element ${element.key}`);
410
859
  }
411
- return disabled ? () => undefined : () => handlers.onV1OpenUrl?.(request);
860
+ return disabled
861
+ ? () => undefined
862
+ : () => {
863
+ handlers.onElementActivate?.(element.key);
864
+ handlers.onV1OpenUrl?.(request);
865
+ };
412
866
  }
413
867
  expectV1InvalidLocalOpenUrlProp(element);
414
868
  if (!disabled) {
@@ -442,6 +896,13 @@ function expectStringProp(element, name) {
442
896
  }
443
897
  return value;
444
898
  }
899
+ function selectClosedStringProp(element, name, allowed) {
900
+ const value = expectStringProp(element, name);
901
+ if (!allowed.includes(value)) {
902
+ throw new TypeError(`Unsupported value ${JSON.stringify(value)} at native element ${element.key}.${name}`);
903
+ }
904
+ return value;
905
+ }
445
906
  function optionalStringProp(element, name) {
446
907
  const value = element.props[name];
447
908
  return value === undefined ? undefined : expectStringProp(element, name);
@@ -456,6 +917,20 @@ function optionalStringArrayProp(element, name) {
456
917
  }
457
918
  return value;
458
919
  }
920
+ function expectStringArrayProp(element, name) {
921
+ const value = optionalStringArrayProp(element, name);
922
+ if (value === undefined) {
923
+ throw new TypeError(`Expected an array of strings at native element ${element.key}.${name}`);
924
+ }
925
+ return value;
926
+ }
927
+ function expectBooleanProp(element, name) {
928
+ const value = optionalBooleanProp(element, name);
929
+ if (value === undefined) {
930
+ throw new TypeError(`Expected a boolean at native element ${element.key}.${name}`);
931
+ }
932
+ return value;
933
+ }
459
934
  function optionalBooleanProp(element, name) {
460
935
  const value = element.props[name];
461
936
  if (value !== undefined && typeof value !== "boolean") {
@@ -470,6 +945,13 @@ function optionalFiniteNumberProp(element, name) {
470
945
  }
471
946
  return value;
472
947
  }
948
+ function expectFiniteNumberProp(element, name) {
949
+ const value = optionalFiniteNumberProp(element, name);
950
+ if (value === undefined) {
951
+ throw new TypeError(`Expected a finite number at native element ${element.key}.${name}`);
952
+ }
953
+ return value;
954
+ }
473
955
  function optionalAccessibilityLiveProp(element) {
474
956
  const value = element.props.accessibilityLive;
475
957
  if (value === undefined || value === "assertive" || value === "off" || value === "polite") {
@@ -550,9 +1032,7 @@ function updateDataModelBinding(dataModel, binding, value) {
550
1032
  if (typeof binding !== "string" || !binding.startsWith("/") || binding.length === 1) {
551
1033
  throw new TypeError(`Expected a non-root absolute JSON Pointer binding, received ${JSON.stringify(binding)}`);
552
1034
  }
553
- if (typeof value !== "string") {
554
- throw new TypeError("Expected a string renderer binding value");
555
- }
1035
+ const parsedValue = parseJsonValue(value, "renderer binding value");
556
1036
  const next = parseJsonObject(dataModel, "renderer data model");
557
1037
  const tokens = binding
558
1038
  .slice(1)
@@ -570,10 +1050,7 @@ function updateDataModelBinding(dataModel, binding, value) {
570
1050
  throw new TypeError(`Renderer binding ${JSON.stringify(binding)} is missing`);
571
1051
  }
572
1052
  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;
1053
+ cursor[arrayIndex] = validateRendererBindingReplacement(cursor[arrayIndex], parsedValue, binding);
577
1054
  break;
578
1055
  }
579
1056
  cursor = cursor[arrayIndex];
@@ -583,16 +1060,27 @@ function updateDataModelBinding(dataModel, binding, value) {
583
1060
  throw new TypeError(`Renderer binding ${JSON.stringify(binding)} is missing`);
584
1061
  }
585
1062
  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);
1063
+ defineJsonProperty(cursor, token, validateRendererBindingReplacement(cursor[token], parsedValue, binding));
590
1064
  break;
591
1065
  }
592
1066
  cursor = cursor[token];
593
1067
  }
594
1068
  return parseJsonObject(next, "renderer data model");
595
1069
  }
1070
+ function validateRendererBindingReplacement(current, next, binding) {
1071
+ if ((typeof current === "string" && typeof next === "string") ||
1072
+ (typeof current === "boolean" && typeof next === "boolean") ||
1073
+ (typeof current === "number" && typeof next === "number")) {
1074
+ return next;
1075
+ }
1076
+ if (Array.isArray(current) &&
1077
+ current.every((value) => typeof value === "string") &&
1078
+ Array.isArray(next) &&
1079
+ next.every((value) => typeof value === "string")) {
1080
+ return Object.freeze([...next]);
1081
+ }
1082
+ throw new TypeError(`Renderer binding ${JSON.stringify(binding)} must preserve its string, boolean, number, or string-array value type`);
1083
+ }
596
1084
  function decodePointerToken(token, pointer) {
597
1085
  for (let index = 0; index < token.length; index += 1) {
598
1086
  if (token[index] !== "~") {
@@ -614,5 +1102,5 @@ function defineJsonProperty(object, key, value) {
614
1102
  writable: true,
615
1103
  });
616
1104
  }
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";
1105
+ 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_OPEN_URL_LENGTH, A2UI_V1_NATIVE_MAX_RENDER_NODES, A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES, A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS, createA2uiV1NativeRenderPlan, resolveA2uiV1NativeEvent, resolveA2uiV1NativeOpenUrl, } from "./v1.js";
618
1106
  //# sourceMappingURL=index.js.map