@antglobal/copilot-cards-web 0.0.20260828032020-dev.1 → 0.0.20260828032020-dev.5

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/README.md CHANGED
@@ -113,10 +113,44 @@ Renders a complete schema and returns a `CardInstance`:
113
113
  ```ts
114
114
  interface CardInstance {
115
115
  updateVariables(variables: Record<string, unknown>): void;
116
+ onScroll(nodeId: string, listener: CardScrollListener): () => void;
116
117
  dispose(): void;
117
118
  }
118
119
  ```
119
120
 
121
+ `CardScrollEvent` and `CardScrollListener` are top-level package exports. The
122
+ event contains readonly `node`, `scrollTop`, `scrollLeft`, `scrollHeight`,
123
+ `scrollWidth`, `clientHeight`, and `clientWidth` values:
124
+
125
+ ```ts
126
+ import type {
127
+ CardScrollEvent,
128
+ CardScrollListener,
129
+ } from "@antglobal/copilot-cards-web";
130
+
131
+ const unsubscribe = card.onScroll("friendsList", event => {
132
+ const remaining =
133
+ event.scrollHeight - event.clientHeight - event.scrollTop;
134
+ });
135
+
136
+ unsubscribe(); // Idempotent; card.dispose() also removes the subscription.
137
+ ```
138
+
139
+ The subscription follows the logical node ID across physical replacement and
140
+ reports native-frequency values. It does not emit an initial, render, resize,
141
+ or `updateVariables` callback. Generated internal scroll tracks and scroll
142
+ ports inside Shadow DOM are not observed. Thresholds, throttling, requests,
143
+ locks, cursors, retries, and deduplication remain host responsibilities.
144
+
145
+ For pagination, continue passing the complete next array to `updateVariables`.
146
+ The SDK retains the Repeat owner and old prefix only when it can strictly prove
147
+ a structurally equal tail extension whose retained rendering is unchanged, on
148
+ a default-rendered `Container` with an unambiguous direct-child `default` or
149
+ `flex` layout. Every unproven case uses the existing replacement behavior; this
150
+ is not a general keyed diff or public append API. A trailing control object is
151
+ eligible only when it does not change retained rows. Put temporary loading UI
152
+ outside the Repeat owner when scroll identity matters.
153
+
120
154
  Common render options include:
121
155
 
122
156
  | Option | Purpose |
package/dist/index.d.ts CHANGED
@@ -55,6 +55,23 @@ interface CardFocusChangeEvent {
55
55
  previousNode: CardNodeHandle | null;
56
56
  }
57
57
  type CardFocusChangeListener = (event: CardFocusChangeEvent) => void;
58
+ interface CardScrollEvent {
59
+ /** Stable logical handle for the scrolling card node. */
60
+ readonly node: CardNodeHandle;
61
+ /** Raw native vertical scroll offset. */
62
+ readonly scrollTop: number;
63
+ /** Raw native horizontal scroll offset, including negative RTL values. */
64
+ readonly scrollLeft: number;
65
+ /** Raw native scrollable content height. */
66
+ readonly scrollHeight: number;
67
+ /** Raw native scrollable content width. */
68
+ readonly scrollWidth: number;
69
+ /** Raw native viewport height. */
70
+ readonly clientHeight: number;
71
+ /** Raw native viewport width. */
72
+ readonly clientWidth: number;
73
+ }
74
+ type CardScrollListener = (event: CardScrollEvent) => void;
58
75
 
59
76
  interface ResponsiveMobileOptions {
60
77
  readonly unit: 'rem';
@@ -108,6 +125,8 @@ interface CardInstance {
108
125
  getNode: (id: string) => CardNodeHandle;
109
126
  /** Observe logical focus changes within this card. */
110
127
  onFocusChange: (listener: CardFocusChangeListener) => () => void;
128
+ /** Observe native scroll metrics for one logical card node. */
129
+ onScroll: (nodeId: string, listener: CardScrollListener) => () => void;
111
130
  }
112
131
  /**
113
132
  * Render a card schema into a container element.
@@ -1841,4 +1860,4 @@ declare const componentRenderers: Record<string, ComponentRenderer>;
1841
1860
  declare function registerComponent(type: string, renderer: ComponentRenderer): void;
1842
1861
 
1843
1862
  export { BaseElement, BotSDK, CardButton, CardChoiceItem, CardChoiceList, CardCollapse, CardCounter, CardDivider, CardForm, CardHtml, CardIcon, CardImage, CardInput, CardLoading, CardPasscodeInput, CardProgress, CardRate, CardSelect, CardSteps, CardSwitch, CardTag, CardText, DEFAULT_MOBILE_RESPONSIVE, LocalActionConfigProvider, RemoteActionConfigProvider, buildStyleString, componentRenderers, connectSSE, connectStreaming, convertPixelTokens, createResponsiveContext, createWebActionContext, isMobileViewport, onViewportChange, pxToRem, pxToVw, registerComponent, renderCard, renderStreamingCard, resolveSize, sanitizeHtml, trimIncompleteTag };
1844
- export type { A2UIActionPayload, BotSDKOptions, ButtonProps, CardFocusChangeEvent, CardFocusChangeListener, CardInstance, CardNodeHandle, ChoiceIndicatorIcon, ChoiceItemProps, ChoiceListProps, ChoiceListValue, CollapseProps, ComponentRenderer, ConnectorConfig, CounterIcon, CounterProps, DividerProps, FormField, FormProps, FormRule, HtmlProps, IconProps, ImageProps, InputAffix, InputAffixConfig, InputProps, LoadingProps, PartialFinalizeResult, PasscodeInputProps, ProgressProps, ProgressSegment, RateProps, RenderCardOptions, ResponsiveContext, ResponsiveMobileOptions, ResponsiveOptions, SSEConnectOptions, SelectOption, SelectProps, SelectSize, SelectStyles, SelectVariant, StepIcon, StepItem, StepsProps, StreamingCardInstance, StreamingCardOptions, StreamingConnectOptions, StreamingConnection, SwitchProps, TagProps, TextProps, WebActionContextOptions };
1863
+ export type { A2UIActionPayload, BotSDKOptions, ButtonProps, CardFocusChangeEvent, CardFocusChangeListener, CardInstance, CardNodeHandle, CardScrollEvent, CardScrollListener, ChoiceIndicatorIcon, ChoiceItemProps, ChoiceListProps, ChoiceListValue, CollapseProps, ComponentRenderer, ConnectorConfig, CounterIcon, CounterProps, DividerProps, FormField, FormProps, FormRule, HtmlProps, IconProps, ImageProps, InputAffix, InputAffixConfig, InputProps, LoadingProps, PartialFinalizeResult, PasscodeInputProps, ProgressProps, ProgressSegment, RateProps, RenderCardOptions, ResponsiveContext, ResponsiveMobileOptions, ResponsiveOptions, SSEConnectOptions, SelectOption, SelectProps, SelectSize, SelectStyles, SelectVariant, StepIcon, StepItem, StepsProps, StreamingCardInstance, StreamingCardOptions, StreamingConnectOptions, StreamingConnection, SwitchProps, TagProps, TextProps, WebActionContextOptions };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getBuiltinIcon, cloneJsonData, createLifecycleManager, materializeCard, findAffectedRepeatOwners, createA2UIParameterResolver, createExpressionContext, replaceRootContents, resolveA2UIDeep, resolveDeep, resolveExpressionValue, resolveActionRef, isBoundRenderTreeNode, hasExpression, resolveExpression, runActionSteps, normalizeSchema, validateSchema, requiresBindingMaterialization, parseSchema, StreamingParser, StreamingEngine, bindingTopologyFingerprint, findTemplateRepeatOwners, extractPartialSchema, runActionStep, registry } from '@antglobal/copilot-cards-core';
1
+ import { getBuiltinIcon, cloneJsonData, createLifecycleManager, materializeCard, findAffectedRepeatOwners, createA2UIParameterResolver, createExpressionContext, replaceRootContents, getByJsonPointer, resolveA2UIDeep, resolveDeep, resolveExpressionValue, resolveActionRef, isBoundRenderTreeNode, hasExpression, resolveExpression, runActionSteps, normalizeSchema, validateSchema, requiresBindingMaterialization, parseSchema, StreamingParser, StreamingEngine, bindingTopologyFingerprint, findTemplateRepeatOwners, extractPartialSchema, runActionStep, registry } from '@antglobal/copilot-cards-core';
2
2
  export { ActionRegistry, a2uiComponentToElement, a2uiToCommand, convertLegacySchema, createLifecycleManager, hasExpression, isA2UIEnvelope, isLegacySchema, normalizeSchema, parseSchema, registerActionHandler, registry, resolveActionRef, resolveDeep, resolveExpression, resolveExpressionValue, runActionStep, runActionSteps, validateSchema } from '@antglobal/copilot-cards-core';
3
3
  import * as echarts from 'echarts/core';
4
4
  import { LineChart, BarChart, PieChart, ScatterChart, FunnelChart, HeatmapChart } from 'echarts/charts';
@@ -763,9 +763,33 @@ const CAROUSEL_HOST_PRESENTATION_OWNERSHIP = {
763
763
  attributes: [CAROUSEL_SCROLL_ID_ATTRIBUTE],
764
764
  styleProperties: Object.keys(carouselHostStyles(0)).map(toCSSPropertyName),
765
765
  };
766
+ function getNonDefaultSlotKeys(props) {
767
+ const slots = props.slots;
768
+ return slots
769
+ ? Object.keys(slots).filter(key => key !== SLOT_LAYOUT.DEFAULT)
770
+ : [];
771
+ }
766
772
  function findSpecialSlotKey(props) {
773
+ return getNonDefaultSlotKeys(props)[0];
774
+ }
775
+ /**
776
+ * Report whether a materialized Repeat slot maps directly onto its owner host.
777
+ * Ambiguous and wrapper/controller layouts deliberately fail closed.
778
+ */
779
+ function getDirectChildRepeatAppendLayout(props, repeatSlot) {
767
780
  const slots = props.slots;
768
- return slots && Object.keys(slots).find(key => key !== SLOT_LAYOUT.DEFAULT);
781
+ if (!slots || slots[repeatSlot] == null)
782
+ return undefined;
783
+ const nonDefaultKeys = getNonDefaultSlotKeys(props);
784
+ if (nonDefaultKeys.length > 1)
785
+ return undefined;
786
+ const effectiveSlot = findSpecialSlotKey(props) ?? SLOT_LAYOUT.DEFAULT;
787
+ if (effectiveSlot !== repeatSlot)
788
+ return undefined;
789
+ return effectiveSlot === SLOT_LAYOUT.DEFAULT
790
+ || effectiveSlot === SLOT_LAYOUT.FLEX
791
+ ? effectiveSlot
792
+ : undefined;
769
793
  }
770
794
  /** Host attributes/styles owned by the active slot renderer after shell render. */
771
795
  function getSlotHostPresentationOwnership(props) {
@@ -8917,12 +8941,14 @@ const FOCUS_TARGET_SELECTOR = [
8917
8941
  */
8918
8942
  function createCardNodeAccess(container) {
8919
8943
  const handles = new Map();
8920
- const listeners = new Set();
8944
+ const focusListeners = new Set();
8945
+ const scrollSubscriptions = new Map();
8921
8946
  let focusedId = null;
8922
8947
  let focusSyncScheduled = false;
8923
- let observing = false;
8948
+ let focusObserving = false;
8949
+ let scrollObserving = false;
8924
8950
  let observationEpoch = 0;
8925
- let observer = null;
8951
+ let focusObserver = null;
8926
8952
  let disposed = false;
8927
8953
  const resolveElement = (id) => {
8928
8954
  if (disposed)
@@ -8993,7 +9019,7 @@ function createCardNodeAccess(container) {
8993
9019
  return null;
8994
9020
  };
8995
9021
  const syncFocus = () => {
8996
- if (disposed || !observing)
9022
+ if (disposed || !focusObserving)
8997
9023
  return;
8998
9024
  const nextId = resolveFocusedId();
8999
9025
  if (nextId === focusedId)
@@ -9001,7 +9027,7 @@ function createCardNodeAccess(container) {
9001
9027
  const previousNode = focusedId ? getNode(focusedId) : null;
9002
9028
  const node = nextId ? getNode(nextId) : null;
9003
9029
  focusedId = nextId;
9004
- for (const listener of [...listeners]) {
9030
+ for (const listener of [...focusListeners]) {
9005
9031
  try {
9006
9032
  listener({ node, previousNode });
9007
9033
  }
@@ -9011,7 +9037,7 @@ function createCardNodeAccess(container) {
9011
9037
  }
9012
9038
  };
9013
9039
  const scheduleFocusSync = () => {
9014
- if (disposed || !observing || focusSyncScheduled)
9040
+ if (disposed || !focusObserving || focusSyncScheduled)
9015
9041
  return;
9016
9042
  focusSyncScheduled = true;
9017
9043
  const scheduledEpoch = observationEpoch;
@@ -9022,29 +9048,29 @@ function createCardNodeAccess(container) {
9022
9048
  syncFocus();
9023
9049
  });
9024
9050
  };
9025
- const startObserving = () => {
9026
- if (disposed || observing)
9051
+ const startFocusObserving = () => {
9052
+ if (disposed || focusObserving)
9027
9053
  return;
9028
- observing = true;
9054
+ focusObserving = true;
9029
9055
  observationEpoch += 1;
9030
9056
  focusedId = resolveFocusedId();
9031
9057
  container.addEventListener('focusin', scheduleFocusSync, true);
9032
9058
  container.addEventListener('focusout', scheduleFocusSync, true);
9033
9059
  if (typeof MutationObserver !== 'undefined') {
9034
- observer ?? (observer = new MutationObserver(scheduleFocusSync));
9035
- observer.observe(container, { childList: true, subtree: true });
9060
+ focusObserver ?? (focusObserver = new MutationObserver(scheduleFocusSync));
9061
+ focusObserver.observe(container, { childList: true, subtree: true });
9036
9062
  }
9037
9063
  };
9038
- const stopObserving = () => {
9039
- if (!observing)
9064
+ const stopFocusObserving = () => {
9065
+ if (!focusObserving)
9040
9066
  return;
9041
- observing = false;
9067
+ focusObserving = false;
9042
9068
  observationEpoch += 1;
9043
9069
  focusSyncScheduled = false;
9044
9070
  const cleanupSteps = [
9045
9071
  () => container.removeEventListener('focusin', scheduleFocusSync, true),
9046
9072
  () => container.removeEventListener('focusout', scheduleFocusSync, true),
9047
- () => observer?.disconnect(),
9073
+ () => focusObserver?.disconnect(),
9048
9074
  ];
9049
9075
  for (const cleanup of cleanupSteps) {
9050
9076
  try {
@@ -9056,30 +9082,111 @@ function createCardNodeAccess(container) {
9056
9082
  }
9057
9083
  focusedId = null;
9058
9084
  };
9085
+ const handleScroll = (event) => {
9086
+ if (disposed || !scrollObserving)
9087
+ return;
9088
+ const path = typeof event.composedPath === 'function'
9089
+ ? event.composedPath()
9090
+ : [];
9091
+ const target = path[0] ?? event.target;
9092
+ if (!(target instanceof HTMLElement)
9093
+ || !target.matches(CARD_NODE_SELECTOR)) {
9094
+ return;
9095
+ }
9096
+ const nodeId = target.getAttribute('data-card-id');
9097
+ if (!nodeId || resolveElement(nodeId) !== target)
9098
+ return;
9099
+ const subscriptions = scrollSubscriptions.get(nodeId);
9100
+ if (!subscriptions?.size)
9101
+ return;
9102
+ const snapshot = Object.freeze({
9103
+ node: getNode(nodeId),
9104
+ scrollTop: target.scrollTop,
9105
+ scrollLeft: target.scrollLeft,
9106
+ scrollHeight: target.scrollHeight,
9107
+ scrollWidth: target.scrollWidth,
9108
+ clientHeight: target.clientHeight,
9109
+ clientWidth: target.clientWidth,
9110
+ });
9111
+ for (const subscription of [...subscriptions]) {
9112
+ try {
9113
+ subscription.listener(snapshot);
9114
+ }
9115
+ catch (error) {
9116
+ console.error('[CardInstance] scroll listener failed', error);
9117
+ }
9118
+ }
9119
+ };
9120
+ const startScrollObserving = () => {
9121
+ if (disposed || scrollObserving)
9122
+ return;
9123
+ scrollObserving = true;
9124
+ container.addEventListener('scroll', handleScroll, {
9125
+ capture: true,
9126
+ passive: true,
9127
+ });
9128
+ };
9129
+ const stopScrollObserving = () => {
9130
+ if (!scrollObserving)
9131
+ return;
9132
+ scrollObserving = false;
9133
+ try {
9134
+ container.removeEventListener('scroll', handleScroll, true);
9135
+ }
9136
+ catch (error) {
9137
+ console.error('[CardInstance] Scroll observation cleanup failed', error);
9138
+ }
9139
+ };
9059
9140
  return {
9060
9141
  getNode,
9061
9142
  onFocusChange(listener) {
9062
9143
  if (disposed)
9063
9144
  return () => { };
9064
- listeners.add(listener);
9065
- if (listeners.size === 1)
9066
- startObserving();
9145
+ focusListeners.add(listener);
9146
+ if (focusListeners.size === 1)
9147
+ startFocusObserving();
9148
+ let subscribed = true;
9149
+ return () => {
9150
+ if (!subscribed)
9151
+ return;
9152
+ subscribed = false;
9153
+ focusListeners.delete(listener);
9154
+ if (focusListeners.size === 0)
9155
+ stopFocusObserving();
9156
+ };
9157
+ },
9158
+ onScroll(nodeId, listener) {
9159
+ if (disposed)
9160
+ return () => { };
9161
+ const subscription = { listener };
9162
+ let subscriptions = scrollSubscriptions.get(nodeId);
9163
+ if (!subscriptions) {
9164
+ subscriptions = new Set();
9165
+ scrollSubscriptions.set(nodeId, subscriptions);
9166
+ }
9167
+ subscriptions.add(subscription);
9168
+ startScrollObserving();
9067
9169
  let subscribed = true;
9068
9170
  return () => {
9069
9171
  if (!subscribed)
9070
9172
  return;
9071
9173
  subscribed = false;
9072
- listeners.delete(listener);
9073
- if (listeners.size === 0)
9074
- stopObserving();
9174
+ const active = scrollSubscriptions.get(nodeId);
9175
+ active?.delete(subscription);
9176
+ if (active?.size === 0)
9177
+ scrollSubscriptions.delete(nodeId);
9178
+ if (scrollSubscriptions.size === 0)
9179
+ stopScrollObserving();
9075
9180
  };
9076
9181
  },
9077
9182
  dispose() {
9078
9183
  if (disposed)
9079
9184
  return;
9080
- stopObserving();
9185
+ stopFocusObserving();
9186
+ stopScrollObserving();
9081
9187
  disposed = true;
9082
- listeners.clear();
9188
+ focusListeners.clear();
9189
+ scrollSubscriptions.clear();
9083
9190
  handles.clear();
9084
9191
  },
9085
9192
  };
@@ -9120,6 +9227,7 @@ function applyDisabledDescendants(element, disabled) {
9120
9227
  }
9121
9228
 
9122
9229
  const FORCE_REPLACE = Symbol('bound-force-replace');
9230
+ const REPEAT_APPEND = Symbol('bound-repeat-append');
9123
9231
  /**
9124
9232
  * Render the scoped/materialized branch of a card.
9125
9233
  *
@@ -9386,9 +9494,12 @@ function renderBoundCard(container, schema, options) {
9386
9494
  collectMountedOccurrences(child, occurrences);
9387
9495
  }
9388
9496
  }
9389
- function prepareBoundNode(node, occurrenceKey, renderVariables, forcedOwnerIds, current) {
9497
+ function prepareBoundNode(node, occurrenceKey, renderVariables, forcedOwnerIds, appendPlans, current) {
9390
9498
  const metadata = resolveBoundMetadata(node, occurrenceKey, renderVariables);
9391
9499
  const forceReplace = forcedOwnerIds.has(node.id);
9500
+ const appendPlan = current
9501
+ ? appendPlans.get(current.occurrenceKey)
9502
+ : undefined;
9392
9503
  if (!metadata.visible || forceReplace) {
9393
9504
  return {
9394
9505
  ...metadata,
@@ -9414,14 +9525,17 @@ function renderBoundCard(container, schema, options) {
9414
9525
  const ordinal = nextChildOrdinals.get(currentChild.nodeId) ?? 0;
9415
9526
  nextChildOrdinals.set(currentChild.nodeId, ordinal + 1);
9416
9527
  const child = matching[Math.min(ordinal, matching.length - 1)];
9417
- children.push(prepareBoundNode(child, currentChild.occurrenceKey, renderVariables, forcedOwnerIds, currentChild));
9528
+ children.push(prepareBoundNode(child, currentChild.occurrenceKey, renderVariables, forcedOwnerIds, appendPlans, currentChild));
9418
9529
  }
9419
9530
  return {
9420
9531
  ...metadata,
9532
+ ...(appendPlan ? { [REPEAT_APPEND]: true } : {}),
9421
9533
  children,
9422
9534
  };
9423
9535
  }
9424
9536
  function decideBoundUpdate(current, next) {
9537
+ if (next[REPEAT_APPEND])
9538
+ return 'retain';
9425
9539
  if (next[FORCE_REPLACE]
9426
9540
  || current.occurrenceKey !== next.occurrenceKey
9427
9541
  || current.nodeId !== next.nodeId
@@ -9483,11 +9597,308 @@ function renderBoundCard(container, schema, options) {
9483
9597
  }
9484
9598
  return changed;
9485
9599
  }
9486
- function selectForcedRepeatOwnerIds(before, draft) {
9487
- if (currentMaterialized.unresolvedRepeatOwners.size > 0) {
9488
- return new Set([currentMaterialized.root.id]);
9600
+ function pointerOverlaps(left, right) {
9601
+ return left === right
9602
+ || left === ''
9603
+ || right === ''
9604
+ || left.startsWith(`${right}/`)
9605
+ || right.startsWith(`${left}/`);
9606
+ }
9607
+ function changedPointersOutsideSuffix(before, draft, sourcePath) {
9608
+ const changed = collectChangedPointers(before, draft)
9609
+ .filter(pointer => pointer !== sourcePath);
9610
+ changed.push(`${sourcePath}/length`);
9611
+ return [...new Set(changed)];
9612
+ }
9613
+ function repeatOwnerIdentityEqual(current, next) {
9614
+ return current.key === next.key
9615
+ && current.parentKey === next.parentKey
9616
+ && current.runtimeOwnerId === next.runtimeOwnerId
9617
+ && current.sourceOwnerId === next.sourceOwnerId
9618
+ && current.slot === next.slot
9619
+ && current.templateId === next.templateId
9620
+ && current.emptyTemplateId === next.emptyTemplateId
9621
+ && current.sourcePath === next.sourcePath
9622
+ && current.dataPath === next.dataPath
9623
+ && current.instancePath === next.instancePath
9624
+ && current.depth === next.depth;
9625
+ }
9626
+ function hasStrictJsonPrefix(previous, next) {
9627
+ return next.length > previous.length
9628
+ && previous.every((value, index) => jsonDataEqual(value, next[index]));
9629
+ }
9630
+ function childNodeSequenceEqual(parent, expected) {
9631
+ const actual = Array.from(parent.childNodes);
9632
+ return actual.length === expected.length
9633
+ && actual.every((node, index) => node === expected[index]);
9634
+ }
9635
+ function occurrenceKeysForChildren(parentOccurrenceKey, children) {
9636
+ const ordinals = new Map();
9637
+ return children.map((child) => {
9638
+ const ordinal = ordinals.get(child.id) ?? 0;
9639
+ ordinals.set(child.id, ordinal + 1);
9640
+ return childOccurrenceKey(parentOccurrenceKey, child.id, ordinal);
9641
+ });
9642
+ }
9643
+ function findMountedByOccurrenceKey(root, occurrenceKey) {
9644
+ if (root.occurrenceKey === occurrenceKey)
9645
+ return root;
9646
+ for (const child of root.children) {
9647
+ const match = findMountedByOccurrenceKey(child, occurrenceKey);
9648
+ if (match)
9649
+ return match;
9650
+ }
9651
+ return null;
9652
+ }
9653
+ function replaceMountedByOccurrenceKey(root, occurrenceKey, replacement) {
9654
+ if (root.occurrenceKey === occurrenceKey)
9655
+ return replacement;
9656
+ for (let index = 0; index < root.children.length; index += 1) {
9657
+ const replacedChild = replaceMountedByOccurrenceKey(root.children[index], occurrenceKey, replacement);
9658
+ if (!replacedChild)
9659
+ continue;
9660
+ const children = root.children.slice();
9661
+ children[index] = replacedChild;
9662
+ return { ...root, children };
9663
+ }
9664
+ return null;
9665
+ }
9666
+ function mountedNodesById(root, nodeId) {
9667
+ const matches = [];
9668
+ const visit = (node) => {
9669
+ if (node.nodeId === nodeId)
9670
+ matches.push(node);
9671
+ for (const child of node.children)
9672
+ visit(child);
9673
+ };
9674
+ visit(root);
9675
+ return matches;
9676
+ }
9677
+ function mountedTreeOrderMatchesDom(root) {
9678
+ if (root.element.parentNode !== container
9679
+ || container.childNodes.length !== 1
9680
+ || container.firstChild !== root.element) {
9681
+ return false;
9682
+ }
9683
+ const visit = (node) => {
9684
+ let previous = null;
9685
+ for (const child of node.children) {
9686
+ if (!node.element.contains(child.element))
9687
+ return false;
9688
+ if (previous
9689
+ && previous.compareDocumentPosition(child.element)
9690
+ !== Node.DOCUMENT_POSITION_FOLLOWING) {
9691
+ return false;
9692
+ }
9693
+ previous = child.element;
9694
+ if (!visit(child))
9695
+ return false;
9696
+ }
9697
+ return true;
9698
+ };
9699
+ return visit(root);
9700
+ }
9701
+ function retainedSubtreeEquivalent(current, next) {
9702
+ return current.occurrenceKey === next.occurrenceKey
9703
+ && current.nodeId === next.nodeId
9704
+ && current.nodeType === next.nodeType
9705
+ && current.node.sourceId === next.node.sourceId
9706
+ && current.node.instancePath === next.node.instancePath
9707
+ && current.node.dataPath === next.node.dataPath
9708
+ && current.node.bindingDialect === next.node.bindingDialect
9709
+ && current.rendererToken === next.rendererToken
9710
+ && current.visible === next.visible
9711
+ && current.disabled === next.disabled
9712
+ && current.propsFingerprint === next.propsFingerprint
9713
+ && current.layoutFingerprint === next.layoutFingerprint
9714
+ && current.bindingFingerprint === next.bindingFingerprint
9715
+ && current.children.length === next.children.length
9716
+ && current.children.every((child, index) => (retainedSubtreeEquivalent(child, next.children[index])));
9717
+ }
9718
+ function prepareComparableRetainedNode(node, occurrenceKey, renderVariables, current) {
9719
+ const expectedKeys = occurrenceKeysForChildren(occurrenceKey, node.children);
9720
+ if (current.children.length !== node.children.length
9721
+ || current.children.some((child, index) => (child.occurrenceKey !== expectedKeys[index]))) {
9722
+ return null;
9723
+ }
9724
+ const children = [];
9725
+ for (let index = 0; index < node.children.length; index += 1) {
9726
+ const child = prepareComparableRetainedNode(node.children[index], expectedKeys[index], renderVariables, current.children[index]);
9727
+ if (!child)
9728
+ return null;
9729
+ children.push(child);
9730
+ }
9731
+ return {
9732
+ ...resolveBoundMetadata(node, occurrenceKey, renderVariables),
9733
+ children,
9734
+ };
9735
+ }
9736
+ function projectRepeatOwnerPrefix(node, owner, current) {
9737
+ const slots = node.props.slots;
9738
+ const slot = slots?.[owner.slot];
9739
+ const currentSlots = current.node.props.slots;
9740
+ const currentSlot = currentSlots?.[owner.slot];
9741
+ if (!slots
9742
+ || !slot
9743
+ || !Array.isArray(slot.children)
9744
+ || !currentSlot
9745
+ || !Array.isArray(currentSlot.children)
9746
+ || currentSlot.children.length >= slot.children.length
9747
+ || currentSlot.children.some((childId, index) => (childId !== slot.children[index]))) {
9748
+ return null;
9749
+ }
9750
+ const appendedChildIds = new Set(slot.children.slice(currentSlot.children.length));
9751
+ const children = node.children.filter(child => (!appendedChildIds.has(child.id)));
9752
+ if (children.length !== current.children.length
9753
+ || node.children.length - children.length !== appendedChildIds.size) {
9754
+ return null;
9755
+ }
9756
+ return {
9757
+ ...node,
9758
+ props: {
9759
+ ...node.props,
9760
+ slots: {
9761
+ ...slots,
9762
+ [owner.slot]: {
9763
+ ...slot,
9764
+ children: slot.children.slice(0, currentSlot.children.length),
9765
+ },
9766
+ },
9767
+ },
9768
+ children,
9769
+ };
9770
+ }
9771
+ function collectRetainedRepeatOwnerKeys(materialized, outerOwner) {
9772
+ const retained = new Set([outerOwner.key]);
9773
+ for (const candidate of materialized.repeatOwners.values()) {
9774
+ let parentKey = candidate.parentKey;
9775
+ while (parentKey) {
9776
+ if (parentKey === outerOwner.key) {
9777
+ retained.add(candidate.key);
9778
+ break;
9779
+ }
9780
+ parentKey = materialized.repeatOwners.get(parentKey)?.parentKey;
9781
+ }
9782
+ }
9783
+ return retained;
9784
+ }
9785
+ function retainedOwnersConsumeChangedPointer(materialized, retainedOwnerKeys, pointers) {
9786
+ for (const [key, dependencyOwners] of materialized.valueDependencies) {
9787
+ if (!key.startsWith('data:')
9788
+ || ![...dependencyOwners].some(ownerKey => (retainedOwnerKeys.has(ownerKey)))) {
9789
+ continue;
9790
+ }
9791
+ const dependency = key.slice('data:'.length);
9792
+ if (pointers.some(pointer => pointerOverlaps(pointer, dependency))) {
9793
+ return true;
9794
+ }
9795
+ }
9796
+ return false;
9797
+ }
9798
+ function createRepeatAppendPlan(currentOwner, nextMaterialized, before, draft) {
9799
+ if (!currentMountedTree || !mountedTreeOrderMatchesDom(currentMountedTree)) {
9800
+ return null;
9801
+ }
9802
+ const nextOwner = nextMaterialized.repeatOwners.get(currentOwner.key);
9803
+ if (!nextOwner || !repeatOwnerIdentityEqual(currentOwner, nextOwner)) {
9804
+ return null;
9805
+ }
9806
+ const currentOwnerCount = [...currentMaterialized.repeatOwners.values()]
9807
+ .filter(owner => owner.runtimeOwnerId === currentOwner.runtimeOwnerId)
9808
+ .length;
9809
+ const nextOwnerCount = [...nextMaterialized.repeatOwners.values()]
9810
+ .filter(owner => owner.runtimeOwnerId === nextOwner.runtimeOwnerId)
9811
+ .length;
9812
+ if (currentOwnerCount !== 1 || nextOwnerCount !== 1)
9813
+ return null;
9814
+ const mountedMatches = mountedNodesById(currentMountedTree, currentOwner.runtimeOwnerId);
9815
+ if (mountedMatches.length !== 1)
9816
+ return null;
9817
+ const current = mountedMatches[0];
9818
+ const nextNode = nextMaterialized.instances
9819
+ .get(nextOwner.runtimeOwnerId)?.node;
9820
+ if (!nextNode
9821
+ || current.nodeType !== 'Container'
9822
+ || nextNode.type !== 'Container'
9823
+ || current.rendererToken !== componentRenderers._default
9824
+ || !current.visible
9825
+ || !current.resolvedProps
9826
+ || !currentOwner.sourcePath
9827
+ || !nextOwner.sourcePath) {
9828
+ return null;
9829
+ }
9830
+ const nextMetadata = resolveBoundMetadata(nextNode, current.occurrenceKey, draft);
9831
+ const currentLayout = getDirectChildRepeatAppendLayout(current.resolvedProps, currentOwner.slot);
9832
+ const nextLayout = nextMetadata.resolvedProps
9833
+ ? getDirectChildRepeatAppendLayout(nextMetadata.resolvedProps, nextOwner.slot)
9834
+ : undefined;
9835
+ if (!currentLayout || currentLayout !== nextLayout)
9836
+ return null;
9837
+ let previousSource;
9838
+ let nextSource;
9839
+ try {
9840
+ previousSource = getByJsonPointer(lastPublishedVariables, currentOwner.sourcePath);
9841
+ nextSource = getByJsonPointer(draft, nextOwner.sourcePath);
9489
9842
  }
9490
- const selectedKeys = new Set();
9843
+ catch {
9844
+ return null;
9845
+ }
9846
+ if (!Array.isArray(previousSource)
9847
+ || !Array.isArray(nextSource)
9848
+ || !hasStrictJsonPrefix(previousSource, nextSource)) {
9849
+ return null;
9850
+ }
9851
+ const nextOccurrenceKeys = occurrenceKeysForChildren(current.occurrenceKey, nextNode.children);
9852
+ if (current.children.length >= nextNode.children.length
9853
+ || current.children.some((child, index) => (child.occurrenceKey !== nextOccurrenceKeys[index]))
9854
+ || !childNodeSequenceEqual(current.element, current.children.map(child => child.element))) {
9855
+ return null;
9856
+ }
9857
+ const projectedOwner = projectRepeatOwnerPrefix(nextNode, nextOwner, current);
9858
+ if (!projectedOwner)
9859
+ return null;
9860
+ const comparableOwner = prepareComparableRetainedNode(projectedOwner, current.occurrenceKey, draft, current);
9861
+ if (!comparableOwner || !retainedSubtreeEquivalent(current, comparableOwner)) {
9862
+ return null;
9863
+ }
9864
+ const retainedOwnerKeys = collectRetainedRepeatOwnerKeys(currentMaterialized, currentOwner);
9865
+ if (retainedOwnersConsumeChangedPointer(currentMaterialized, retainedOwnerKeys, changedPointersOutsideSuffix(before, draft, currentOwner.sourcePath))) {
9866
+ return null;
9867
+ }
9868
+ return { currentOwner, nextOwner, current, nextNode };
9869
+ }
9870
+ function classifyRepeatOwners(before, draft, nextMaterialized) {
9871
+ let isolatedUnresolvedAppendKey;
9872
+ if (currentMaterialized.repeatOwners.size === 1
9873
+ && nextMaterialized.repeatOwners.size === 1
9874
+ && currentMaterialized.unresolvedRepeatOwners.size === 1
9875
+ && nextMaterialized.unresolvedRepeatOwners.size === 1) {
9876
+ const [currentKey] = currentMaterialized.unresolvedRepeatOwners;
9877
+ const [nextKey] = nextMaterialized.unresolvedRepeatOwners;
9878
+ const currentOwner = currentMaterialized.repeatOwners.get(currentKey);
9879
+ const nextOwner = nextMaterialized.repeatOwners.get(nextKey);
9880
+ const changedPointers = collectChangedPointers(before, draft);
9881
+ if (currentKey === nextKey
9882
+ && currentOwner?.sourcePath
9883
+ && currentOwner.sourcePath === nextOwner?.sourcePath
9884
+ && changedPointers.length === 1
9885
+ && changedPointers[0] === currentOwner.sourcePath) {
9886
+ // Complex expressions inside one Repeat are conservatively unresolved.
9887
+ // A source-only update may still enter the existing append proof: the
9888
+ // strict JSON prefix and retained-subtree comparison below must both
9889
+ // succeed before any live DOM is adopted.
9890
+ isolatedUnresolvedAppendKey = currentKey;
9891
+ }
9892
+ }
9893
+ if ((currentMaterialized.unresolvedRepeatOwners.size > 0
9894
+ || nextMaterialized.unresolvedRepeatOwners.size > 0)
9895
+ && isolatedUnresolvedAppendKey === undefined) {
9896
+ return {
9897
+ forcedOwnerIds: new Set([currentMaterialized.root.id]),
9898
+ appendPlans: new Map(),
9899
+ };
9900
+ }
9901
+ const selectedKeys = new Set(isolatedUnresolvedAppendKey ? [isolatedUnresolvedAppendKey] : []);
9491
9902
  for (const pointer of collectChangedPointers(before, draft)) {
9492
9903
  for (const owner of findAffectedRepeatOwners(currentMaterialized, pointer)) {
9493
9904
  selectedKeys.add(owner.key);
@@ -9503,10 +9914,19 @@ function renderBoundCard(container, schema, options) {
9503
9914
  parentKey = currentMaterialized.repeatOwners.get(parentKey)?.parentKey;
9504
9915
  }
9505
9916
  }
9506
- return new Set([...selectedKeys].flatMap((key) => {
9507
- const id = currentMaterialized.repeatOwners.get(key)?.runtimeOwnerId;
9508
- return id ? [id] : [];
9509
- }));
9917
+ const forcedOwnerIds = new Set();
9918
+ const appendPlans = new Map();
9919
+ for (const key of selectedKeys) {
9920
+ const owner = currentMaterialized.repeatOwners.get(key);
9921
+ if (!owner)
9922
+ continue;
9923
+ const plan = createRepeatAppendPlan(owner, nextMaterialized, before, draft);
9924
+ if (plan)
9925
+ appendPlans.set(plan.current.occurrenceKey, plan);
9926
+ else
9927
+ forcedOwnerIds.add(owner.runtimeOwnerId);
9928
+ }
9929
+ return { forcedOwnerIds, appendPlans };
9510
9930
  }
9511
9931
  function collectMountedLifecycleIds(root) {
9512
9932
  const ids = new Set();
@@ -9538,15 +9958,77 @@ function renderBoundCard(container, schema, options) {
9538
9958
  prepareReplacementRoots(current.children[index], next.children[index], draft, replacements);
9539
9959
  }
9540
9960
  }
9961
+ function findPreparedByOccurrenceKey(root, occurrenceKey) {
9962
+ if (root.occurrenceKey === occurrenceKey)
9963
+ return root;
9964
+ for (const child of root.children) {
9965
+ const match = findPreparedByOccurrenceKey(child, occurrenceKey);
9966
+ if (match)
9967
+ return match;
9968
+ }
9969
+ return null;
9970
+ }
9971
+ function disposePreparedRepeatAppend(candidate) {
9972
+ if (candidate.state === 'disposed' || candidate.state === 'adopted')
9973
+ return;
9974
+ for (const suffix of candidate.suffix) {
9975
+ disposeBoundSubtreeWithoutThrow(suffix);
9976
+ }
9977
+ candidate.fragment.replaceChildren();
9978
+ candidate.state = 'disposed';
9979
+ }
9980
+ function prepareRepeatAppendCandidates(preparedRoot, appendPlans, draft) {
9981
+ const candidates = new Map();
9982
+ try {
9983
+ for (const [occurrenceKey, plan] of appendPlans) {
9984
+ const prepared = findPreparedByOccurrenceKey(preparedRoot, occurrenceKey);
9985
+ if (!prepared?.[REPEAT_APPEND]) {
9986
+ throw new Error(`[renderCard] Missing prepared repeat append owner "${occurrenceKey}"`);
9987
+ }
9988
+ const nextOccurrenceKeys = occurrenceKeysForChildren(plan.current.occurrenceKey, plan.nextNode.children);
9989
+ const suffix = [];
9990
+ const fragment = document.createDocumentFragment();
9991
+ const candidate = {
9992
+ occurrenceKey,
9993
+ current: plan.current,
9994
+ next: prepared,
9995
+ suffix,
9996
+ fragment,
9997
+ previousChildNodes: Array.from(plan.current.element.childNodes),
9998
+ expectedChildNodes: [],
9999
+ state: 'prepared',
10000
+ };
10001
+ candidates.set(occurrenceKey, candidate);
10002
+ for (let index = plan.current.children.length; index < plan.nextNode.children.length; index += 1) {
10003
+ const mounted = mountBoundSubtree(plan.nextNode.children[index], nextOccurrenceKeys[index], draft);
10004
+ suffix.push(mounted);
10005
+ fragment.appendChild(mounted.element);
10006
+ }
10007
+ candidate.expectedChildNodes = [
10008
+ ...candidate.previousChildNodes,
10009
+ ...suffix.map(node => node.element),
10010
+ ];
10011
+ }
10012
+ return candidates;
10013
+ }
10014
+ catch (error) {
10015
+ for (const candidate of candidates.values()) {
10016
+ disposePreparedRepeatAppend(candidate);
10017
+ }
10018
+ throw error;
10019
+ }
10020
+ }
9541
10021
  function prepareIncrementalCandidate(before, draft) {
9542
10022
  const materialized = materializeCard(schema, draft);
9543
- const forcedOwnerIds = currentMountedTree
9544
- ? materialized.unresolvedRepeatOwners.size > 0
9545
- ? new Set([materialized.root.id])
9546
- : selectForcedRepeatOwnerIds(before, draft)
9547
- : new Set();
9548
- const preparedRoot = prepareBoundNode(materialized.root, `${materialized.root.id}#0`, draft, forcedOwnerIds, currentMountedTree);
10023
+ const classification = currentMountedTree
10024
+ ? classifyRepeatOwners(before, draft, materialized)
10025
+ : {
10026
+ forcedOwnerIds: new Set(),
10027
+ appendPlans: new Map(),
10028
+ };
10029
+ const preparedRoot = prepareBoundNode(materialized.root, `${materialized.root.id}#0`, draft, classification.forcedOwnerIds, classification.appendPlans, currentMountedTree);
9549
10030
  const replacementRoots = new Map();
10031
+ const repeatAppends = prepareRepeatAppendCandidates(preparedRoot, classification.appendPlans, draft);
9550
10032
  try {
9551
10033
  if (currentMountedTree) {
9552
10034
  prepareReplacementRoots(currentMountedTree, preparedRoot, draft, replacementRoots);
@@ -9558,6 +10040,7 @@ function renderBoundCard(container, schema, options) {
9558
10040
  materialized,
9559
10041
  preparedRoot,
9560
10042
  replacementRoots,
10043
+ repeatAppends,
9561
10044
  autoFocusIds: currentMountedTree
9562
10045
  ? collectAutoFocusRevealIds(currentMaterialized, materialized, draft)
9563
10046
  : new Set(),
@@ -9567,6 +10050,9 @@ function renderBoundCard(container, schema, options) {
9567
10050
  for (const replacement of replacementRoots.values()) {
9568
10051
  disposeBoundSubtreeWithoutThrow(replacement);
9569
10052
  }
10053
+ for (const append of repeatAppends.values()) {
10054
+ disposePreparedRepeatAppend(append);
10055
+ }
9570
10056
  throw error;
9571
10057
  }
9572
10058
  }
@@ -9820,6 +10306,10 @@ function renderBoundCard(container, schema, options) {
9820
10306
  disposeBoundSubtreeWithoutThrow(replacement);
9821
10307
  }
9822
10308
  candidate.replacementRoots.clear();
10309
+ for (const append of candidate.repeatAppends.values()) {
10310
+ disposePreparedRepeatAppend(append);
10311
+ }
10312
+ candidate.repeatAppends.clear();
9823
10313
  }
9824
10314
  function restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates) {
9825
10315
  if (previous.element !== next.element) {
@@ -9868,6 +10358,111 @@ function renderBoundCard(container, schema, options) {
9868
10358
  throw adoptionError;
9869
10359
  throw new Error(failureMessage);
9870
10360
  }
10361
+ function rollbackRepeatAppend(candidate) {
10362
+ for (let index = candidate.suffix.length - 1; index >= 0; index -= 1) {
10363
+ const element = candidate.suffix[index].element;
10364
+ if (element.parentNode !== candidate.current.element)
10365
+ continue;
10366
+ try {
10367
+ candidate.current.element.removeChild(element);
10368
+ }
10369
+ catch {
10370
+ // The exact previous-sequence postcondition below decides success.
10371
+ }
10372
+ }
10373
+ const restored = childNodeSequenceEqual(candidate.current.element, candidate.previousChildNodes);
10374
+ candidate.state = restored ? 'rolled-back' : 'rollback-failed';
10375
+ return restored;
10376
+ }
10377
+ function tryAdoptRepeatAppend(candidate) {
10378
+ let adoptionError;
10379
+ try {
10380
+ candidate.current.element.appendChild(candidate.fragment);
10381
+ }
10382
+ catch (error) {
10383
+ adoptionError = error;
10384
+ }
10385
+ if (childNodeSequenceEqual(candidate.current.element, candidate.expectedChildNodes)) {
10386
+ candidate.state = 'adopted';
10387
+ return { status: 'adopted' };
10388
+ }
10389
+ const error = adoptionError ?? new Error('[renderCard] Bound repeat append postcondition failed');
10390
+ if (rollbackRepeatAppend(candidate)) {
10391
+ return { status: 'rolled-back', error };
10392
+ }
10393
+ return { status: 'rollback-failed', error };
10394
+ }
10395
+ function mountedSnapshotStillMatches(previous, working) {
10396
+ const appendOwner = Boolean(working[REPEAT_APPEND]);
10397
+ if (previous.element !== working.element
10398
+ || previous.occurrenceKey !== working.occurrenceKey
10399
+ || previous.nodeId !== working.nodeId
10400
+ || previous.nodeType !== working.nodeType
10401
+ || previous.rendererToken !== working.rendererToken
10402
+ || previous.visible !== working.visible
10403
+ || previous.disabled !== working.disabled
10404
+ || previous.bindingFingerprint !== working.bindingFingerprint
10405
+ || previous.children.length !== working.children.length
10406
+ || (!appendOwner && (previous.propsFingerprint !== working.propsFingerprint
10407
+ || previous.layoutFingerprint !== working.layoutFingerprint))) {
10408
+ return false;
10409
+ }
10410
+ if (appendOwner
10411
+ && !childNodeSequenceEqual(previous.element, previous.children.map(child => child.element))) {
10412
+ return false;
10413
+ }
10414
+ return previous.children.every((child, index) => (mountedSnapshotStillMatches(child, working.children[index])));
10415
+ }
10416
+ function adoptCompleteNextEmergencyRoot(candidate, renderVariables, previous, working, scrollPositions, mediaStates) {
10417
+ const emergency = mountBoundSubtree(candidate.materialized.root, candidate.preparedRoot.occurrenceKey, renderVariables);
10418
+ let adopted = false;
10419
+ try {
10420
+ syncMountedBindings(emergency);
10421
+ replaceContainerRoot(emergency.element, '[renderCard] Bound emergency root was not adopted');
10422
+ adopted = true;
10423
+ }
10424
+ finally {
10425
+ if (!adopted)
10426
+ disposeBoundSubtreeWithoutThrow(emergency);
10427
+ }
10428
+ disposeBoundSubtreeWithoutThrow(working);
10429
+ try {
10430
+ restoreChangedSubtreeState(previous, emergency, scrollPositions, mediaStates);
10431
+ }
10432
+ catch {
10433
+ // Presentation restoration cannot undo an adopted complete next root.
10434
+ }
10435
+ restoreDisabledDescendantPresentation(emergency);
10436
+ return emergency;
10437
+ }
10438
+ function recoverPreviousCommittedRoot(previous, working, scrollPositions, mediaStates) {
10439
+ const recovery = mountBoundSubtree(currentMaterialized.root, previous.occurrenceKey, lastPublishedVariables);
10440
+ let adopted = false;
10441
+ try {
10442
+ syncMountedBindings(recovery);
10443
+ replaceContainerRoot(recovery.element, '[renderCard] Previous bound root recovery was not adopted');
10444
+ adopted = true;
10445
+ }
10446
+ finally {
10447
+ if (!adopted)
10448
+ disposeBoundSubtreeWithoutThrow(recovery);
10449
+ }
10450
+ disposeBoundSubtreeWithoutThrow(working);
10451
+ try {
10452
+ restoreChangedSubtreeState(previous, recovery, scrollPositions, mediaStates);
10453
+ }
10454
+ catch {
10455
+ // Recovery validity is the adopted previous tree, not presentation state.
10456
+ }
10457
+ restoreDisabledDescendantPresentation(recovery);
10458
+ currentMountedTree = recovery;
10459
+ return recovery;
10460
+ }
10461
+ function repeatAppendUnreconciledError(failures) {
10462
+ const error = new Error('[renderCard] Bound repeat append unreconciled');
10463
+ error.failures = failures;
10464
+ return error;
10465
+ }
9871
10466
  function commitIncrementalCandidate(candidate, renderVariables) {
9872
10467
  const replacements = candidate.replacementRoots;
9873
10468
  try {
@@ -9889,7 +10484,10 @@ function renderBoundCard(container, schema, options) {
9889
10484
  const scrollPositions = captureScrollPositions$1(container);
9890
10485
  const mediaStates = captureMediaStates$1(container);
9891
10486
  let unreconciled = false;
9892
- const next = reconcileIncrementalNode(previous, candidate.preparedRoot, {
10487
+ let previousRecoveryRequired = false;
10488
+ let appendFailure;
10489
+ let repeatAppendTransaction = false;
10490
+ let next = reconcileIncrementalNode(previous, candidate.preparedRoot, {
9893
10491
  decide: (current, prepared) => decideBoundUpdate(current, prepared),
9894
10492
  update: (current, prepared) => updateBoundNode(current, prepared),
9895
10493
  mountReplacement: (prepared) => {
@@ -9923,6 +10521,60 @@ function renderBoundCard(container, schema, options) {
9923
10521
  unreconciled = true;
9924
10522
  },
9925
10523
  });
10524
+ if (!unreconciled) {
10525
+ for (const appendCandidate of candidate.repeatAppends.values()) {
10526
+ const reconciledOwner = findMountedByOccurrenceKey(next, appendCandidate.occurrenceKey);
10527
+ if (!reconciledOwner
10528
+ || reconciledOwner.element !== appendCandidate.current.element) {
10529
+ disposePreparedRepeatAppend(appendCandidate);
10530
+ candidate.repeatAppends.delete(appendCandidate.occurrenceKey);
10531
+ continue;
10532
+ }
10533
+ repeatAppendTransaction = true;
10534
+ const grafted = {
10535
+ ...appendCandidate.next,
10536
+ element: reconciledOwner.element,
10537
+ children: [
10538
+ ...reconciledOwner.children,
10539
+ ...appendCandidate.suffix,
10540
+ ],
10541
+ };
10542
+ const graftedRoot = replaceMountedByOccurrenceKey(next, appendCandidate.occurrenceKey, grafted);
10543
+ if (!graftedRoot) {
10544
+ disposePreparedRepeatAppend(appendCandidate);
10545
+ candidate.repeatAppends.delete(appendCandidate.occurrenceKey);
10546
+ unreconciled = true;
10547
+ appendFailure = new Error('[renderCard] Bound repeat append owner disappeared before adoption');
10548
+ break;
10549
+ }
10550
+ const result = tryAdoptRepeatAppend(appendCandidate);
10551
+ if (result.status === 'adopted') {
10552
+ next = graftedRoot;
10553
+ candidate.repeatAppends.delete(appendCandidate.occurrenceKey);
10554
+ continue;
10555
+ }
10556
+ appendFailure = result.error;
10557
+ if (result.status === 'rolled-back') {
10558
+ unreconciled = true;
10559
+ }
10560
+ else {
10561
+ previousRecoveryRequired = true;
10562
+ }
10563
+ break;
10564
+ }
10565
+ }
10566
+ if (previousRecoveryRequired) {
10567
+ try {
10568
+ recoverPreviousCommittedRoot(previous, next, scrollPositions, mediaStates);
10569
+ }
10570
+ catch (recoveryError) {
10571
+ throw repeatAppendUnreconciledError([
10572
+ appendFailure,
10573
+ recoveryError,
10574
+ ]);
10575
+ }
10576
+ throw appendFailure;
10577
+ }
9926
10578
  if (!unreconciled
9927
10579
  && (next.element.parentNode !== container
9928
10580
  || container.firstElementChild !== next.element)) {
@@ -9936,7 +10588,7 @@ function renderBoundCard(container, schema, options) {
9936
10588
  unreconciled = true;
9937
10589
  }
9938
10590
  }
9939
- if (unreconciled) {
10591
+ if (unreconciled && !repeatAppendTransaction) {
9940
10592
  let emergency;
9941
10593
  try {
9942
10594
  emergency = mountBoundSubtree(candidate.materialized.root, candidate.preparedRoot.occurrenceKey, renderVariables);
@@ -9967,6 +10619,29 @@ function renderBoundCard(container, schema, options) {
9967
10619
  restoreDisabledDescendantPresentation(emergency);
9968
10620
  return emergency;
9969
10621
  }
10622
+ if (unreconciled) {
10623
+ try {
10624
+ return adoptCompleteNextEmergencyRoot(candidate, renderVariables, previous, next, scrollPositions, mediaStates);
10625
+ }
10626
+ catch (emergencyError) {
10627
+ if (mountedTreeOrderMatchesDom(previous)
10628
+ && mountedSnapshotStillMatches(previous, next)) {
10629
+ syncMountedBindings(previous);
10630
+ throw appendFailure ?? emergencyError;
10631
+ }
10632
+ try {
10633
+ recoverPreviousCommittedRoot(previous, next, scrollPositions, mediaStates);
10634
+ }
10635
+ catch (recoveryError) {
10636
+ throw repeatAppendUnreconciledError([
10637
+ appendFailure,
10638
+ emergencyError,
10639
+ recoveryError,
10640
+ ]);
10641
+ }
10642
+ throw appendFailure ?? emergencyError;
10643
+ }
10644
+ }
9970
10645
  try {
9971
10646
  restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates);
9972
10647
  }
@@ -10221,6 +10896,7 @@ function renderBoundCard(container, schema, options) {
10221
10896
  return {
10222
10897
  getNode: nodeAccess.getNode,
10223
10898
  onFocusChange: nodeAccess.onFocusChange,
10899
+ onScroll: nodeAccess.onScroll,
10224
10900
  dispose() {
10225
10901
  if (disposed || disposeRequested)
10226
10902
  return;
@@ -11047,6 +11723,7 @@ function renderStaticCard(container, schema, options) {
11047
11723
  return {
11048
11724
  getNode: nodeAccess.getNode,
11049
11725
  onFocusChange: nodeAccess.onFocusChange,
11726
+ onScroll: nodeAccess.onScroll,
11050
11727
  dispose() {
11051
11728
  if (disposed || disposeRequested)
11052
11729
  return;
@@ -13074,6 +13751,7 @@ function renderStreamingCard(container, options = {}) {
13074
13751
  return {
13075
13752
  getNode: nodeAccess.getNode,
13076
13753
  onFocusChange: nodeAccess.onFocusChange,
13754
+ onScroll: nodeAccess.onScroll,
13077
13755
  applyCommand(command) {
13078
13756
  if (!lockMode('commands'))
13079
13757
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antglobal/copilot-cards-web",
3
- "version": "0.0.20260828032020-dev.1",
3
+ "version": "0.0.20260828032020-dev.5",
4
4
  "description": "Web Component renderer for copilot bot card SDK — PC + Mobile + WebView unified rendering via Custom Elements",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "@antglobal/copilot-cards-core": "0.0.20260828032020-dev.1",
31
+ "@antglobal/copilot-cards-core": "0.0.20260828032020-dev.5",
32
32
  "echarts": "^5.6.0",
33
33
  "marked": "^18.0.5",
34
34
  "tslib": "^2.8.1",