@antglobal/copilot-cards-web 1.0.6 → 1.0.7

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +58 -6
  2. package/dist/index.js +3075 -479
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getBuiltinIcon, cloneJsonData, createLifecycleManager, materializeCard, createA2UIParameterResolver, createExpressionContext, resolveActionRef, replaceRootContents, resolveA2UIDeep, hasExpression, resolveExpression, resolveDeep, resolveExpressionValue, isBoundRenderTreeNode, runActionSteps, normalizeSchema, validateSchema, requiresBindingMaterialization, parseSchema, StreamingParser, StreamingEngine, findAffectedRepeatOwners, bindingTopologyFingerprint, findTemplateRepeatOwners, extractPartialSchema, runActionStep, registry } from '@antglobal/copilot-cards-core';
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';
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';
@@ -332,11 +332,21 @@ function disconnectChartResize(chartRoot) {
332
332
  const registration = chartResizeRegistrations.get(chartRoot);
333
333
  if (!registration)
334
334
  return;
335
- registration.observer.disconnect();
335
+ chartResizeRegistrations.delete(chartRoot);
336
+ try {
337
+ registration.observer.disconnect();
338
+ }
339
+ catch (error) {
340
+ console.error('[renderCard] Chart observer cleanup failed', error);
341
+ }
336
342
  if (registration.frameID !== null) {
337
- cancelAnimationFrame(registration.frameID);
343
+ try {
344
+ cancelAnimationFrame(registration.frameID);
345
+ }
346
+ catch (error) {
347
+ console.error('[renderCard] Chart frame cleanup failed', error);
348
+ }
338
349
  }
339
- chartResizeRegistrations.delete(chartRoot);
340
350
  }
341
351
  function observeChartResize(chartRoot, chart) {
342
352
  disconnectChartResize(chartRoot);
@@ -376,12 +386,20 @@ function observeChartResize(chartRoot, chart) {
376
386
  * host node, so skipping this leaks one instance per chart per re-render.
377
387
  */
378
388
  function disposeChartsIn(container) {
379
- const roots = container.querySelectorAll(`[${CHART_ROOT_ATTR}]`);
389
+ const roots = [];
390
+ if (container.hasAttribute?.(CHART_ROOT_ATTR))
391
+ roots.push(container);
392
+ roots.push(...container.querySelectorAll(`[${CHART_ROOT_ATTR}]`));
380
393
  roots.forEach((node) => {
381
394
  disconnectChartResize(node);
382
- const inst = echarts.getInstanceByDom(node);
383
- if (inst)
384
- inst.dispose();
395
+ try {
396
+ const inst = echarts.getInstanceByDom(node);
397
+ if (inst)
398
+ inst.dispose();
399
+ }
400
+ catch (error) {
401
+ console.error('[renderCard] Chart instance cleanup failed', error);
402
+ }
385
403
  });
386
404
  }
387
405
  function renderChartSlot(container, slotContent, actionContext) {
@@ -706,6 +724,7 @@ function buildHeatmapOption(config) {
706
724
  */
707
725
  // ─── Slot Layout Constants ──────────────────────────────────────
708
726
  const SLOT_LAYOUT = {
727
+ DEFAULT: 'default',
709
728
  FLEX: 'flex',
710
729
  COLUMNS: 'columns',
711
730
  GRID: 'grid',
@@ -717,6 +736,377 @@ const SLOT_LAYOUT = {
717
736
  TABLE: 'table',
718
737
  CHART: 'chart',
719
738
  };
739
+ const EMPTY_HOST_PRESENTATION_OWNERSHIP = {
740
+ attributes: [],
741
+ styleProperties: [],
742
+ };
743
+ const CAROUSEL_SCROLL_ID_ATTRIBUTE = 'data-scroll-id';
744
+ function carouselHostStyles(gap) {
745
+ return {
746
+ display: 'flex',
747
+ flexDirection: 'row',
748
+ overflowX: 'auto',
749
+ scrollSnapType: 'x mandatory',
750
+ WebkitOverflowScrolling: 'touch',
751
+ gap: `${gap}px`,
752
+ paddingTop: '16px',
753
+ paddingBottom: '16px',
754
+ scrollbarWidth: 'none',
755
+ cursor: 'grab',
756
+ position: 'relative',
757
+ };
758
+ }
759
+ function toCSSPropertyName(property) {
760
+ return property.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
761
+ }
762
+ const CAROUSEL_HOST_PRESENTATION_OWNERSHIP = {
763
+ attributes: [CAROUSEL_SCROLL_ID_ATTRIBUTE],
764
+ styleProperties: Object.keys(carouselHostStyles(0)).map(toCSSPropertyName),
765
+ };
766
+ function findSpecialSlotKey(props) {
767
+ const slots = props.slots;
768
+ return slots && Object.keys(slots).find(key => key !== SLOT_LAYOUT.DEFAULT);
769
+ }
770
+ /** Host attributes/styles owned by the active slot renderer after shell render. */
771
+ function getSlotHostPresentationOwnership(props) {
772
+ return findSpecialSlotKey(props) === SLOT_LAYOUT.CAROUSEL
773
+ ? CAROUSEL_HOST_PRESENTATION_OWNERSHIP
774
+ : EMPTY_HOST_PRESENTATION_OWNERSHIP;
775
+ }
776
+ /** Resolve the exact carousel values consumed by the renderer. */
777
+ function resolveCarouselConfig(props, childCount) {
778
+ const slotContent = props.slots?.[SLOT_LAYOUT.CAROUSEL];
779
+ const config = slotContent?.config ?? {};
780
+ const itemWidthPx = config.itemWidthPx;
781
+ return {
782
+ scale: config.scale ?? props.carouselScale ?? 0.85,
783
+ opacity: config.opacity ?? props.carouselOpacity ?? 0.4,
784
+ overlayColor: config.overlayColor
785
+ ?? props.carouselOverlayColor
786
+ ?? 'rgba(255,255,255,0.5)',
787
+ gap: config.gap ?? props.carouselGap ?? 16,
788
+ itemWidthPercent: config.itemWidthPercent
789
+ ?? props.carouselItemWidth
790
+ ?? 70,
791
+ itemWidthPx: itemWidthPx ? itemWidthPx : null,
792
+ initialIndex: Math.max(0, Math.min(childCount - 1, config.initialIndex ?? props.carouselInitialIndex ?? 0)),
793
+ autoplay: config.autoplay ?? props.carouselAutoplay ?? false,
794
+ autoplayInterval: config.autoplayInterval
795
+ ?? props.carouselAutoplayInterval
796
+ ?? 3000,
797
+ };
798
+ }
799
+ function carouselNumericFingerprint(value) {
800
+ const numeric = Number(value);
801
+ return Number.isFinite(numeric) ? numeric : String(numeric);
802
+ }
803
+ function carouselLayoutFingerprint(config) {
804
+ const autoplay = Boolean(config.autoplay);
805
+ return {
806
+ scale: carouselNumericFingerprint(config.scale),
807
+ opacity: carouselNumericFingerprint(config.opacity),
808
+ overlayColor: String(config.overlayColor),
809
+ gap: `${config.gap}px`,
810
+ itemBasis: config.itemWidthPx
811
+ ? `${config.itemWidthPx}px`
812
+ : `${config.itemWidthPercent}%`,
813
+ initialIndex: carouselNumericFingerprint(config.initialIndex),
814
+ autoplay,
815
+ autoplayInterval: autoplay
816
+ ? carouselNumericFingerprint(config.autoplayInterval)
817
+ : null,
818
+ };
819
+ }
820
+ /** JSON-like layout dependencies used by the static incremental boundary. */
821
+ function getSlotLayoutFingerprintInput(props, children, resolveChildProps) {
822
+ const slotKey = findSpecialSlotKey(props);
823
+ if (slotKey === SLOT_LAYOUT.CAROUSEL) {
824
+ return {
825
+ slotKey,
826
+ childOccurrences: children.map((child, index) => ({
827
+ index,
828
+ id: child.id,
829
+ })),
830
+ carousel: carouselLayoutFingerprint(resolveCarouselConfig(props, children.length)),
831
+ };
832
+ }
833
+ const input = {
834
+ childIds: children.map(child => child.id),
835
+ slots: props.slots ?? null,
836
+ };
837
+ if (slotKey === SLOT_LAYOUT.GRID) {
838
+ input.gridPlacements = children.map((child, index) => {
839
+ const placement = gridPlacementStyles(resolveChildProps?.(child) ?? child.props);
840
+ return {
841
+ index,
842
+ id: child.id,
843
+ gridColumn: placement['grid-column'] ?? null,
844
+ gridRow: placement['grid-row'] ?? null,
845
+ gridArea: placement['grid-area'] ?? null,
846
+ };
847
+ });
848
+ }
849
+ return input;
850
+ }
851
+ const slotItemBindings = new WeakMap();
852
+ const slotOwnerBindings = new WeakMap();
853
+ function clearSlotOwnerBehavior(element) {
854
+ slotOwnerBindings.get(element)?.cleanup();
855
+ }
856
+ function createSlotOwnerCleanup(element) {
857
+ clearSlotOwnerBehavior(element);
858
+ const cleanups = [];
859
+ let disposed = false;
860
+ const binding = {
861
+ cleanup() {
862
+ if (disposed)
863
+ return;
864
+ disposed = true;
865
+ if (slotOwnerBindings.get(element) === binding) {
866
+ slotOwnerBindings.delete(element);
867
+ }
868
+ for (const cleanup of cleanups.splice(0).reverse()) {
869
+ try {
870
+ cleanup();
871
+ }
872
+ catch {
873
+ // Teardown is best-effort; one browser resource cannot block others.
874
+ }
875
+ }
876
+ },
877
+ };
878
+ slotOwnerBindings.set(element, binding);
879
+ return {
880
+ add(cleanup) {
881
+ if (disposed)
882
+ cleanup();
883
+ else
884
+ cleanups.push(cleanup);
885
+ },
886
+ dispose: binding.cleanup,
887
+ };
888
+ }
889
+ function addSlotOwnerListener(controller, target, type, listener, options) {
890
+ target.addEventListener(type, listener, options);
891
+ controller.add(() => target.removeEventListener(type, listener, options));
892
+ }
893
+ function clearSlotItemPresentation(element) {
894
+ const binding = slotItemBindings.get(element);
895
+ if (!binding)
896
+ return;
897
+ try {
898
+ binding.cleanup?.();
899
+ }
900
+ finally {
901
+ slotItemBindings.delete(element);
902
+ }
903
+ }
904
+ function bindSlotItemPresentation(element, descriptor) {
905
+ clearSlotItemPresentation(element);
906
+ const behavior = descriptor.apply(element) ?? {};
907
+ slotItemBindings.set(element, { descriptor, ...behavior });
908
+ }
909
+ function stylePropertyNames(style) {
910
+ const names = new Set();
911
+ for (let index = 0; index < style.length; index += 1) {
912
+ const name = style.item(index);
913
+ if (name)
914
+ names.add(name);
915
+ }
916
+ return names;
917
+ }
918
+ function captureSlotItemPresentation(element, ownership) {
919
+ const styleNames = stylePropertyNames(element.style);
920
+ return {
921
+ attributes: ownership.attributes.map(name => ({
922
+ name,
923
+ present: element.hasAttribute(name),
924
+ value: element.getAttribute(name) ?? '',
925
+ })),
926
+ styles: ownership.styleProperties.map(name => ({
927
+ name,
928
+ present: styleNames.has(name),
929
+ value: element.style.getPropertyValue(name),
930
+ priority: element.style.getPropertyPriority(name),
931
+ })),
932
+ };
933
+ }
934
+ function restoreSlotItemPresentation(element, snapshot) {
935
+ for (const attribute of snapshot.attributes) {
936
+ if (attribute.present)
937
+ element.setAttribute(attribute.name, attribute.value);
938
+ else
939
+ element.removeAttribute(attribute.name);
940
+ }
941
+ for (const style of snapshot.styles) {
942
+ if (style.present) {
943
+ element.style.setProperty(style.name, style.value, style.priority);
944
+ }
945
+ else {
946
+ element.style.removeProperty(style.name);
947
+ }
948
+ }
949
+ }
950
+ function slotItemOwnership(styles) {
951
+ return {
952
+ attributes: [],
953
+ styleProperties: Object.keys(styles),
954
+ };
955
+ }
956
+ function applySlotItemStyles(element, styles) {
957
+ for (const [name, value] of Object.entries(styles)) {
958
+ element.style.setProperty(name, value);
959
+ }
960
+ }
961
+ /** Copy an immediate parent's slot-item decoration onto a detached replacement. */
962
+ function transferSlotItemPresentation(current, replacement) {
963
+ const binding = slotItemBindings.get(current);
964
+ if (!binding)
965
+ return;
966
+ const resume = binding.suspend?.();
967
+ try {
968
+ const snapshot = captureSlotItemPresentation(current, binding.descriptor.ownership);
969
+ bindSlotItemPresentation(replacement, binding.descriptor);
970
+ restoreSlotItemPresentation(replacement, snapshot);
971
+ binding.descriptor.transfer?.(current, replacement);
972
+ }
973
+ finally {
974
+ resume?.();
975
+ }
976
+ }
977
+ /** Item styles owned by the live parent slot, used by shallow shell updates. */
978
+ function getSlotItemPresentationOwnership(element) {
979
+ return slotItemBindings.get(element)?.descriptor.ownership
980
+ ?? EMPTY_HOST_PRESENTATION_OWNERSHIP;
981
+ }
982
+ /** Preserve parent-owned presentation and transient behavior around an item update. */
983
+ function updateSlotItemShellPresentation(element, update, additionalOwnership = EMPTY_HOST_PRESENTATION_OWNERSHIP) {
984
+ const binding = slotItemBindings.get(element);
985
+ const ownership = mergePresentationOwnership(binding?.descriptor.ownership ?? EMPTY_HOST_PRESENTATION_OWNERSHIP, additionalOwnership);
986
+ if (!binding
987
+ && ownership.attributes.length === 0
988
+ && ownership.styleProperties.length === 0) {
989
+ update();
990
+ return;
991
+ }
992
+ const resume = binding?.suspend?.();
993
+ try {
994
+ const snapshot = captureSlotItemPresentation(element, ownership);
995
+ try {
996
+ update();
997
+ }
998
+ finally {
999
+ restoreSlotItemPresentation(element, snapshot);
1000
+ }
1001
+ }
1002
+ finally {
1003
+ resume?.();
1004
+ }
1005
+ }
1006
+ function mergePresentationOwnership(...ownership) {
1007
+ return {
1008
+ attributes: Array.from(new Set(ownership.flatMap(value => value.attributes))),
1009
+ styleProperties: Array.from(new Set(ownership.flatMap(value => value.styleProperties))),
1010
+ };
1011
+ }
1012
+ /** Patch renderer-owned shell presentation without overwriting layout state. */
1013
+ function patchElementShellPresentation(current, previous, desired, ...preservedOwnership) {
1014
+ const ownership = mergePresentationOwnership(...preservedOwnership);
1015
+ const layoutAttributes = new Set(ownership.attributes);
1016
+ const layoutStyleProperties = new Set(ownership.styleProperties);
1017
+ const attributeNames = new Set([
1018
+ ...Array.from(previous.attributes, attribute => attribute.name),
1019
+ ...Array.from(desired.attributes, attribute => attribute.name),
1020
+ ]);
1021
+ attributeNames.delete('style');
1022
+ for (const name of attributeNames) {
1023
+ if (layoutAttributes.has(name))
1024
+ continue;
1025
+ const previousHas = previous.hasAttribute(name);
1026
+ const desiredHas = desired.hasAttribute(name);
1027
+ const previousValue = previous.getAttribute(name);
1028
+ const desiredValue = desired.getAttribute(name);
1029
+ if (previousHas === desiredHas
1030
+ && previousValue === desiredValue) {
1031
+ continue;
1032
+ }
1033
+ if (previousHas) {
1034
+ if (!current.hasAttribute(name)
1035
+ || current.getAttribute(name) !== previousValue) {
1036
+ continue;
1037
+ }
1038
+ }
1039
+ else if (current.hasAttribute(name)) {
1040
+ continue;
1041
+ }
1042
+ if (desiredHas)
1043
+ current.setAttribute(name, desiredValue);
1044
+ else
1045
+ current.removeAttribute(name);
1046
+ }
1047
+ const previousPropertyNames = stylePropertyNames(previous.style);
1048
+ const desiredPropertyNames = stylePropertyNames(desired.style);
1049
+ const propertyNames = new Set([
1050
+ ...previousPropertyNames,
1051
+ ...desiredPropertyNames,
1052
+ ]);
1053
+ const currentPropertyNames = stylePropertyNames(current.style);
1054
+ for (const name of propertyNames) {
1055
+ if (layoutStyleProperties.has(name))
1056
+ continue;
1057
+ const previousHas = previousPropertyNames.has(name);
1058
+ const desiredHas = desiredPropertyNames.has(name);
1059
+ const previousValue = previous.style.getPropertyValue(name);
1060
+ const desiredValue = desired.style.getPropertyValue(name);
1061
+ const previousPriority = previous.style.getPropertyPriority(name);
1062
+ const desiredPriority = desired.style.getPropertyPriority(name);
1063
+ if (previousHas === desiredHas
1064
+ && previousValue === desiredValue
1065
+ && previousPriority === desiredPriority) {
1066
+ continue;
1067
+ }
1068
+ if (previousHas) {
1069
+ if (!currentPropertyNames.has(name)
1070
+ || current.style.getPropertyValue(name) !== previousValue
1071
+ || current.style.getPropertyPriority(name) !== previousPriority) {
1072
+ continue;
1073
+ }
1074
+ }
1075
+ else if (currentPropertyNames.has(name)) {
1076
+ continue;
1077
+ }
1078
+ if (desiredHas) {
1079
+ current.style.setProperty(name, desiredValue, desiredPriority);
1080
+ }
1081
+ else {
1082
+ current.style.removeProperty(name);
1083
+ }
1084
+ }
1085
+ }
1086
+ /** Release slot-owned item listeners within a disposed subtree. */
1087
+ function clearSlotItemPresentationsIn(root) {
1088
+ let hasError = false;
1089
+ let firstError;
1090
+ const clear = (element) => {
1091
+ try {
1092
+ clearSlotItemPresentation(element);
1093
+ }
1094
+ catch (error) {
1095
+ if (!hasError)
1096
+ firstError = error;
1097
+ hasError = true;
1098
+ }
1099
+ };
1100
+ clear(root);
1101
+ root.querySelectorAll('*').forEach(clear);
1102
+ if (hasError)
1103
+ throw firstError;
1104
+ }
1105
+ /** Release slot-owner listeners, timers, frames, and observers in a subtree. */
1106
+ function clearSlotOwnerBehaviorsIn(root) {
1107
+ clearSlotOwnerBehavior(root);
1108
+ root.querySelectorAll('*').forEach(clearSlotOwnerBehavior);
1109
+ }
720
1110
  // ─── Dispatcher ─────────────────────────────────────────────────
721
1111
  /**
722
1112
  * Apply host-level layout styles based on the slot key.
@@ -746,11 +1136,11 @@ function resolveFlexGap(value) {
746
1136
  * Returns true if a special layout was applied (children already appended).
747
1137
  * Returns false for `default` — caller uses original append loop.
748
1138
  */
749
- function renderSlotLayout(container, children, props, renderChild, childrenMap, actionContext) {
1139
+ function renderSlotLayout(container, children, props, renderChild, childrenMap, actionContext, resolveChildProps) {
750
1140
  const slots = props.slots;
751
1141
  if (!slots)
752
1142
  return false;
753
- const slotKey = Object.keys(slots).find((k) => k !== 'default');
1143
+ const slotKey = findSpecialSlotKey(props);
754
1144
  if (!slotKey)
755
1145
  return false;
756
1146
  const slotContent = slots[slotKey];
@@ -759,7 +1149,7 @@ function renderSlotLayout(container, children, props, renderChild, childrenMap,
759
1149
  renderColumnsSlot(container, slotContent, childrenMap, renderChild);
760
1150
  return true;
761
1151
  case SLOT_LAYOUT.GRID:
762
- renderGridSlot(container, children, slotContent, renderChild);
1152
+ renderGridSlot(container, children, slotContent, renderChild, resolveChildProps);
763
1153
  return true;
764
1154
  case SLOT_LAYOUT.HORIZONTAL_SCROLL:
765
1155
  renderHorizontalScrollSlot(container, children, slotContent, renderChild);
@@ -833,7 +1223,7 @@ function renderColumnsSlot(container, slotContent, childrenMap, renderChild) {
833
1223
  *
834
1224
  * Schema: `slots: { grid: { children: [...], config: { columns: 2, gap: '8px', rows?: 3, rowHeight?: '48px' } } }`
835
1225
  */
836
- function renderGridSlot(container, children, slotContent, renderChild) {
1226
+ function renderGridSlot(container, children, slotContent, renderChild, resolveChildProps) {
837
1227
  const columns = slotContent?.config?.columns ?? 2;
838
1228
  const gap = slotContent?.config?.gap ?? '8px';
839
1229
  const rows = slotContent?.config?.rows;
@@ -851,7 +1241,7 @@ function renderGridSlot(container, children, slotContent, renderChild) {
851
1241
  // regardless of whether the child component applies props.style to its host
852
1242
  // or to an inner shadow-DOM node. No-op for children without placement, so
853
1243
  // auto-flow cards are unaffected.
854
- applyGridPlacement(el, child);
1244
+ bindSlotItemPresentation(el, gridItemDescriptor(resolveChildProps?.(child) ?? child.props));
855
1245
  grid.appendChild(el);
856
1246
  }
857
1247
  container.appendChild(grid);
@@ -864,16 +1254,31 @@ function renderGridSlot(container, children, slotContent, renderChild) {
864
1254
  * unresolved expression object) is ignored. Children that declare no placement
865
1255
  * are left untouched — preserving auto-flow behaviour for legacy cards.
866
1256
  */
867
- function applyGridPlacement(el, child) {
868
- const style = child?.props?.style;
869
- if (!style || typeof style !== 'object')
870
- return;
871
- if (isCssPlacement(style.gridColumn))
872
- el.style.gridColumn = String(style.gridColumn);
873
- if (isCssPlacement(style.gridRow))
874
- el.style.gridRow = String(style.gridRow);
875
- if (isCssPlacement(style.gridArea))
876
- el.style.gridArea = String(style.gridArea);
1257
+ function gridPlacementStyles(props) {
1258
+ const style = props?.style;
1259
+ const placement = {};
1260
+ if (style && typeof style === 'object') {
1261
+ if (isCssPlacement(style.gridColumn)) {
1262
+ placement['grid-column'] = String(style.gridColumn);
1263
+ }
1264
+ if (isCssPlacement(style.gridRow)) {
1265
+ placement['grid-row'] = String(style.gridRow);
1266
+ }
1267
+ if (isCssPlacement(style.gridArea)) {
1268
+ placement['grid-area'] = String(style.gridArea);
1269
+ }
1270
+ }
1271
+ return placement;
1272
+ }
1273
+ function gridItemDescriptor(props) {
1274
+ const placement = gridPlacementStyles(props);
1275
+ return {
1276
+ kind: 'grid',
1277
+ ownership: slotItemOwnership(placement),
1278
+ apply(element) {
1279
+ applySlotItemStyles(element, placement);
1280
+ },
1281
+ };
877
1282
  }
878
1283
  function isCssPlacement(value) {
879
1284
  return typeof value === 'string' || typeof value === 'number';
@@ -897,7 +1302,14 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
897
1302
  const autoScrollLoop = config.autoScrollLoop !== false;
898
1303
  const scrollbar = resolveScrollbarMode(config.scrollbar);
899
1304
  const hidesScrollbar = scrollbar == null || scrollbar === 'hidden';
1305
+ const itemDescriptor = horizontalScrollItemDescriptor(itemWidth, snap, itemHoverStyle);
900
1306
  const track = document.createElement('div');
1307
+ const ownerCleanup = createSlotOwnerCleanup(track);
1308
+ let ownerDisposed = false;
1309
+ let wheelTimer = 0;
1310
+ let autoTimer = 0;
1311
+ let overlayFrame = 0;
1312
+ let resizeObserver = null;
901
1313
  track.style.cssText = [
902
1314
  'display:flex',
903
1315
  'overflow-x:auto',
@@ -917,20 +1329,17 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
917
1329
  track.dataset.scrollbar = scrollbar;
918
1330
  for (const child of children) {
919
1331
  const item = renderChild(child);
920
- item.style.flexShrink = '0';
921
- if (itemWidth)
922
- item.style.width = itemWidth;
923
- if (snap)
924
- item.style.scrollSnapAlign = 'start';
925
- if (itemHoverStyle)
926
- applyHoverStyle(item, itemHoverStyle);
1332
+ bindSlotItemPresentation(item, itemDescriptor);
927
1333
  track.appendChild(item);
928
1334
  }
929
1335
  // Mouse drag (desktop)
930
1336
  let isDragging = false;
931
1337
  let startX = 0;
932
1338
  let scrollLeft = 0;
933
- track.addEventListener('mousedown', (e) => {
1339
+ const handleMouseDown = (event) => {
1340
+ if (ownerDisposed)
1341
+ return;
1342
+ const e = event;
934
1343
  if (e.button !== 0)
935
1344
  return; // left button only — keep middle/right clicks out of drag state
936
1345
  // Nested scrollers: consume the gesture so an enclosing horizontalScroll doesn't
@@ -944,8 +1353,11 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
944
1353
  track.style.userSelect = 'none';
945
1354
  if (snap)
946
1355
  track.style.scrollSnapType = 'none';
947
- });
948
- track.addEventListener('mousemove', (e) => {
1356
+ };
1357
+ const handleMouseMove = (event) => {
1358
+ if (ownerDisposed)
1359
+ return;
1360
+ const e = event;
949
1361
  if (!isDragging)
950
1362
  return;
951
1363
  // Only swallow the move while THIS track owns the drag — otherwise an in-progress
@@ -954,7 +1366,7 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
954
1366
  e.stopPropagation();
955
1367
  e.preventDefault();
956
1368
  track.scrollLeft = scrollLeft - (e.pageX - startX);
957
- });
1369
+ };
958
1370
  const stopDrag = () => {
959
1371
  if (!isDragging)
960
1372
  return;
@@ -964,15 +1376,19 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
964
1376
  if (snap)
965
1377
  track.style.scrollSnapType = 'x mandatory';
966
1378
  };
967
- track.addEventListener('mouseup', stopDrag);
968
- track.addEventListener('mouseleave', stopDrag);
1379
+ addSlotOwnerListener(ownerCleanup, track, 'mousedown', handleMouseDown);
1380
+ addSlotOwnerListener(ownerCleanup, track, 'mousemove', handleMouseMove);
1381
+ addSlotOwnerListener(ownerCleanup, track, 'mouseup', stopDrag);
1382
+ addSlotOwnerListener(ownerCleanup, track, 'mouseleave', stopDrag);
969
1383
  // Wheel / trackpad → horizontal scroll.
970
1384
  // Mandatory snap would swallow small wheel deltas (each increment snaps right
971
1385
  // back to the same item, so the wheel appears dead) — suspend snap while
972
1386
  // wheeling and restore it shortly after the last tick, which also re-aligns
973
1387
  // the track to the nearest snap point.
974
- let wheelTimer = 0;
975
- track.addEventListener('wheel', (e) => {
1388
+ const handleWheel = (event) => {
1389
+ if (ownerDisposed)
1390
+ return;
1391
+ const e = event;
976
1392
  e.preventDefault();
977
1393
  // Consume the wheel so a nested horizontalScroll scrolls only itself instead of
978
1394
  // also driving an enclosing scroller (simplest nested model — no edge hand-off
@@ -985,12 +1401,16 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
985
1401
  const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
986
1402
  track.scrollLeft += delta;
987
1403
  if (snap) {
988
- clearTimeout(wheelTimer);
1404
+ window.clearTimeout(wheelTimer);
989
1405
  wheelTimer = window.setTimeout(() => {
1406
+ wheelTimer = 0;
1407
+ if (ownerDisposed)
1408
+ return;
990
1409
  track.style.scrollSnapType = 'x mandatory';
991
1410
  }, 150);
992
1411
  }
993
- }, { passive: false });
1412
+ };
1413
+ addSlotOwnerListener(ownerCleanup, track, 'wheel', handleWheel, { passive: false });
994
1414
  // Hover autoplay (desktop): while the pointer is over the track, glide to the next
995
1415
  // item every `autoScrollInterval` ms, looping back to the first at the end when
996
1416
  // `autoScrollLoop`. Opt-in via `autoScroll`. Manual drag pauses it (resumes on the
@@ -999,9 +1419,10 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
999
1419
  // chrome. Targets are item starts (= snap points), so mandatory snap won't fight the
1000
1420
  // glide — same reason the arrow buttons can scrollBy smoothly without suspending snap.
1001
1421
  if (autoScroll) {
1002
- let autoTimer = 0;
1003
1422
  const glideTo = (left) => track.scrollTo({ left, behavior: 'smooth' });
1004
1423
  const advance = () => {
1424
+ if (ownerDisposed)
1425
+ return stopAuto();
1005
1426
  // A variable-driven re-render rebuilds the track; bail and clear so the interval
1006
1427
  // doesn't keep driving a detached node (mirrors the ResizeObserver self-teardown).
1007
1428
  if (!track.isConnected)
@@ -1022,6 +1443,8 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
1022
1443
  glideTo(track.scrollLeft + (next.getBoundingClientRect().left - trackLeft));
1023
1444
  };
1024
1445
  const startAuto = () => {
1446
+ if (ownerDisposed)
1447
+ return;
1025
1448
  if (autoTimer)
1026
1449
  return;
1027
1450
  if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
@@ -1031,13 +1454,33 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
1031
1454
  function stopAuto() {
1032
1455
  if (!autoTimer)
1033
1456
  return;
1034
- clearInterval(autoTimer);
1457
+ window.clearInterval(autoTimer);
1035
1458
  autoTimer = 0;
1036
1459
  }
1037
- track.addEventListener('mouseenter', startAuto);
1038
- track.addEventListener('mouseleave', stopAuto);
1039
- track.addEventListener('mousedown', stopAuto);
1460
+ addSlotOwnerListener(ownerCleanup, track, 'mouseenter', startAuto);
1461
+ addSlotOwnerListener(ownerCleanup, track, 'mouseleave', stopAuto);
1462
+ addSlotOwnerListener(ownerCleanup, track, 'mousedown', stopAuto);
1040
1463
  }
1464
+ ownerCleanup.add(() => {
1465
+ ownerDisposed = true;
1466
+ stopDrag();
1467
+ if (wheelTimer) {
1468
+ window.clearTimeout(wheelTimer);
1469
+ wheelTimer = 0;
1470
+ }
1471
+ if (autoTimer) {
1472
+ window.clearInterval(autoTimer);
1473
+ autoTimer = 0;
1474
+ }
1475
+ if (resizeObserver) {
1476
+ resizeObserver.disconnect();
1477
+ resizeObserver = null;
1478
+ }
1479
+ if (overlayFrame) {
1480
+ cancelAnimationFrame(overlayFrame);
1481
+ overlayFrame = 0;
1482
+ }
1483
+ });
1041
1484
  if (hidesScrollbar) {
1042
1485
  const trackClass = `card-scroll-${cardId}`;
1043
1486
  track.classList.add(trackClass);
@@ -1082,13 +1525,25 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
1082
1525
  : typeof config.arrowOffset === 'number' ? `${config.arrowOffset}px` : String(config.arrowOffset);
1083
1526
  leftArrow = createArrowButton('left', arrowStyle, arrowOffset, config.arrowIconLeft ?? config.arrowIcon, config.arrowIconLeft == null);
1084
1527
  rightArrow = createArrowButton('right', arrowStyle, arrowOffset, config.arrowIconRight ?? config.arrowIcon, false);
1085
- leftArrow.addEventListener('click', () => track.scrollBy({ left: -step(), behavior: 'smooth' }));
1086
- rightArrow.addEventListener('click', () => track.scrollBy({ left: step(), behavior: 'smooth' }));
1528
+ const scrollLeft = () => {
1529
+ if (!ownerDisposed) {
1530
+ track.scrollBy({ left: -step(), behavior: 'smooth' });
1531
+ }
1532
+ };
1533
+ const scrollRight = () => {
1534
+ if (!ownerDisposed) {
1535
+ track.scrollBy({ left: step(), behavior: 'smooth' });
1536
+ }
1537
+ };
1538
+ addSlotOwnerListener(ownerCleanup, leftArrow, 'click', scrollLeft);
1539
+ addSlotOwnerListener(ownerCleanup, rightArrow, 'click', scrollRight);
1087
1540
  wrapper.appendChild(leftArrow);
1088
1541
  wrapper.appendChild(rightArrow);
1089
1542
  }
1090
1543
  // Show each arrow/mask only while the track can scroll in that direction.
1091
1544
  const updateOverlays = () => {
1545
+ if (ownerDisposed)
1546
+ return;
1092
1547
  const canLeft = track.scrollLeft > 1;
1093
1548
  const canRight = track.scrollLeft + track.clientWidth < track.scrollWidth - 1;
1094
1549
  if (leftArrow)
@@ -1100,27 +1555,54 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
1100
1555
  if (rightMask)
1101
1556
  rightMask.style.opacity = canRight ? maskOpacity : '0';
1102
1557
  };
1103
- track.addEventListener('scroll', updateOverlays, { passive: true });
1558
+ addSlotOwnerListener(ownerCleanup, track, 'scroll', updateOverlays, { passive: true });
1104
1559
  // ResizeObserver fires once on observe (covers initial layout) and again on
1105
1560
  // any size change; it self-disconnects once the track leaves the DOM.
1106
1561
  if (typeof ResizeObserver !== 'undefined') {
1107
- const ro = new ResizeObserver(() => {
1562
+ resizeObserver = new ResizeObserver(() => {
1563
+ if (ownerDisposed)
1564
+ return;
1108
1565
  if (!track.isConnected) {
1109
- ro.disconnect();
1566
+ resizeObserver?.disconnect();
1567
+ resizeObserver = null;
1110
1568
  return;
1111
1569
  }
1112
1570
  updateOverlays();
1113
1571
  });
1114
- ro.observe(track);
1572
+ resizeObserver.observe(track);
1115
1573
  }
1116
1574
  else {
1117
- requestAnimationFrame(updateOverlays);
1575
+ overlayFrame = requestAnimationFrame(() => {
1576
+ overlayFrame = 0;
1577
+ updateOverlays();
1578
+ });
1118
1579
  }
1119
1580
  }
1581
+ function horizontalScrollItemDescriptor(itemWidth, snap, hoverStyle) {
1582
+ const styles = {
1583
+ 'flex-shrink': '0',
1584
+ };
1585
+ if (itemWidth)
1586
+ styles.width = itemWidth;
1587
+ if (snap)
1588
+ styles['scroll-snap-align'] = 'start';
1589
+ return {
1590
+ kind: 'horizontalScroll',
1591
+ ownership: slotItemOwnership(styles),
1592
+ apply(element) {
1593
+ applySlotItemStyles(element, styles);
1594
+ return hoverStyle ? applyHoverStyle(element, hoverStyle) : undefined;
1595
+ },
1596
+ };
1597
+ }
1120
1598
  /** Apply `hoverStyle` on mouseenter and restore the previous inline values on leave. */
1121
1599
  function applyHoverStyle(item, hoverStyle) {
1122
1600
  const previous = {};
1123
- item.addEventListener('mouseenter', () => {
1601
+ let hovered = false;
1602
+ const enter = () => {
1603
+ if (hovered)
1604
+ return;
1605
+ hovered = true;
1124
1606
  for (const [key, value] of Object.entries(hoverStyle)) {
1125
1607
  if (key.startsWith('--')) {
1126
1608
  const previousValue = item.style.getPropertyValue(key);
@@ -1143,8 +1625,11 @@ function applyHoverStyle(item, hoverStyle) {
1143
1625
  item.style[key] = value;
1144
1626
  }
1145
1627
  }
1146
- });
1147
- item.addEventListener('mouseleave', () => {
1628
+ };
1629
+ const leave = () => {
1630
+ if (!hovered)
1631
+ return;
1632
+ hovered = false;
1148
1633
  for (const [key, state] of Object.entries(previous)) {
1149
1634
  if (state.isCustomProperty) {
1150
1635
  if (state.wasPresent) {
@@ -1157,8 +1642,38 @@ function applyHoverStyle(item, hoverStyle) {
1157
1642
  else {
1158
1643
  item.style[key] = state.value;
1159
1644
  }
1645
+ delete previous[key];
1160
1646
  }
1161
- });
1647
+ };
1648
+ item.addEventListener('mouseenter', enter);
1649
+ item.addEventListener('mouseleave', leave);
1650
+ return {
1651
+ cleanup() {
1652
+ let hasError = false;
1653
+ let firstError;
1654
+ const cleanup = (operation) => {
1655
+ try {
1656
+ operation();
1657
+ }
1658
+ catch (error) {
1659
+ if (!hasError)
1660
+ firstError = error;
1661
+ hasError = true;
1662
+ }
1663
+ };
1664
+ cleanup(leave);
1665
+ cleanup(() => item.removeEventListener('mouseenter', enter));
1666
+ cleanup(() => item.removeEventListener('mouseleave', leave));
1667
+ if (hasError)
1668
+ throw firstError;
1669
+ },
1670
+ suspend() {
1671
+ if (!hovered)
1672
+ return;
1673
+ leave();
1674
+ return enter;
1675
+ },
1676
+ };
1162
1677
  }
1163
1678
  /** Translucent edge-fade mask hinting that more content is available in that direction. */
1164
1679
  function createEdgeMask(side, width, color) {
@@ -1238,6 +1753,66 @@ function createArrowButton(side, arrowStyle, offset, icon, mirror = false) {
1238
1753
  return btn;
1239
1754
  }
1240
1755
  // ─── Carousel ───────────────────────────────────────────────────
1756
+ function carouselOverlay(item) {
1757
+ for (let index = item.children.length - 1; index >= 0; index -= 1) {
1758
+ const child = item.children.item(index);
1759
+ if (child instanceof HTMLElement
1760
+ && child.classList.contains('carousel-overlay')) {
1761
+ return child;
1762
+ }
1763
+ }
1764
+ return undefined;
1765
+ }
1766
+ function carouselItems(container) {
1767
+ return Array.from(container.children).filter((child) => (child instanceof HTMLElement
1768
+ && slotItemBindings.get(child)?.descriptor.kind === 'carousel'));
1769
+ }
1770
+ function carouselItemDescriptor(index, itemCount, itemBasis, overlayColor) {
1771
+ const baseStyles = {
1772
+ 'flex-grow': '0',
1773
+ 'flex-shrink': '0',
1774
+ 'flex-basis': itemBasis,
1775
+ 'scroll-snap-align': 'center',
1776
+ 'transform-origin': 'center center',
1777
+ position: 'relative',
1778
+ overflow: 'hidden',
1779
+ };
1780
+ const ownedStyles = {
1781
+ ...baseStyles,
1782
+ transform: '',
1783
+ opacity: '',
1784
+ };
1785
+ if (index === 0)
1786
+ ownedStyles['margin-left'] = '';
1787
+ if (index === itemCount - 1)
1788
+ ownedStyles['margin-right'] = '';
1789
+ return {
1790
+ kind: 'carousel',
1791
+ ownership: slotItemOwnership(ownedStyles),
1792
+ apply(element) {
1793
+ applySlotItemStyles(element, baseStyles);
1794
+ const overlay = document.createElement('div');
1795
+ overlay.className = 'carousel-overlay';
1796
+ Object.assign(overlay.style, {
1797
+ position: 'absolute',
1798
+ inset: '0',
1799
+ background: overlayColor,
1800
+ opacity: '0',
1801
+ pointerEvents: 'none',
1802
+ borderRadius: 'inherit',
1803
+ zIndex: '1',
1804
+ });
1805
+ element.appendChild(overlay);
1806
+ },
1807
+ transfer(current, replacement) {
1808
+ const currentOverlay = carouselOverlay(current);
1809
+ const replacementOverlay = carouselOverlay(replacement);
1810
+ if (currentOverlay && replacementOverlay) {
1811
+ replacementOverlay.style.opacity = currentOverlay.style.opacity;
1812
+ }
1813
+ },
1814
+ };
1815
+ }
1241
1816
  /**
1242
1817
  * Center-focused carousel with scale/opacity/overlay effects and snap.
1243
1818
  *
@@ -1248,30 +1823,40 @@ function createArrowButton(side, arrowStyle, offset, icon, mirror = false) {
1248
1823
  * to the first after the last (default false)
1249
1824
  * - `autoplayInterval` — ms between auto-advances (default 3000)
1250
1825
  */
1251
- function renderCarouselSlot(container, children, slotContent, props, renderChild) {
1252
- const config = slotContent?.config ?? {};
1253
- const inactiveScale = config.scale ?? props.carouselScale ?? 0.85;
1254
- const inactiveOpacity = config.opacity ?? props.carouselOpacity ?? 0.4;
1255
- const overlayColor = config.overlayColor ?? props.carouselOverlayColor ?? 'rgba(255,255,255,0.5)';
1256
- const gap = config.gap ?? props.carouselGap ?? 16;
1257
- const itemWidth = config.itemWidthPercent ?? props.carouselItemWidth ?? 70;
1258
- const itemWidthPx = config.itemWidthPx;
1259
- const initialIndex = Math.max(0, Math.min(children.length - 1, config.initialIndex ?? props.carouselInitialIndex ?? 0));
1260
- const autoplay = config.autoplay ?? props.carouselAutoplay ?? false;
1261
- const autoplayInterval = config.autoplayInterval ?? props.carouselAutoplayInterval ?? 3000;
1262
- Object.assign(container.style, {
1263
- display: 'flex',
1264
- flexDirection: 'row',
1265
- overflowX: 'auto',
1266
- scrollSnapType: 'x mandatory',
1267
- WebkitOverflowScrolling: 'touch',
1268
- gap: `${gap}px`,
1269
- paddingTop: '16px',
1270
- paddingBottom: '16px',
1271
- scrollbarWidth: 'none',
1272
- cursor: 'grab',
1273
- position: 'relative',
1274
- });
1826
+ function renderCarouselSlot(container, children, _slotContent, props, renderChild) {
1827
+ const effectiveConfig = resolveCarouselConfig(props, children.length);
1828
+ const inactiveScale = effectiveConfig.scale;
1829
+ const inactiveOpacity = effectiveConfig.opacity;
1830
+ const overlayColor = effectiveConfig.overlayColor;
1831
+ const gap = effectiveConfig.gap;
1832
+ const itemWidth = effectiveConfig.itemWidthPercent;
1833
+ const itemWidthPx = effectiveConfig.itemWidthPx;
1834
+ const initialIndex = effectiveConfig.initialIndex;
1835
+ const autoplay = effectiveConfig.autoplay;
1836
+ const autoplayInterval = effectiveConfig.autoplayInterval;
1837
+ const ownerCleanup = createSlotOwnerCleanup(container);
1838
+ let ownerDisposed = false;
1839
+ const pendingFrames = new Set();
1840
+ const requestOwnerFrame = (callback) => {
1841
+ let frameId = 0;
1842
+ let completedSynchronously = false;
1843
+ frameId = requestAnimationFrame((timestamp) => {
1844
+ completedSynchronously = true;
1845
+ pendingFrames.delete(frameId);
1846
+ if (!ownerDisposed)
1847
+ callback(timestamp);
1848
+ });
1849
+ if (!completedSynchronously)
1850
+ pendingFrames.add(frameId);
1851
+ return completedSynchronously ? 0 : frameId;
1852
+ };
1853
+ const cancelOwnerFrame = (frameId) => {
1854
+ if (!frameId)
1855
+ return;
1856
+ cancelAnimationFrame(frameId);
1857
+ pendingFrames.delete(frameId);
1858
+ };
1859
+ Object.assign(container.style, carouselHostStyles(gap));
1275
1860
  // Hide scrollbar
1276
1861
  const styleEl = document.createElement('style');
1277
1862
  const id = container.getAttribute('data-card-id') ?? '';
@@ -1279,34 +1864,16 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
1279
1864
  container.appendChild(styleEl);
1280
1865
  // The carousel element scrolls itself — tag it so renderCard can snapshot/restore
1281
1866
  // its scrollLeft across variable-driven re-renders (see captureScrollPositions/restoreScrollPositions).
1282
- container.setAttribute('data-scroll-id', id || 'x');
1867
+ container.setAttribute(CAROUSEL_SCROLL_ID_ATTRIBUTE, id || 'x');
1283
1868
  // Render children
1284
- const items = [];
1285
1869
  for (let i = 0; i < children.length; i++) {
1286
1870
  const child = children[i];
1287
1871
  const item = renderChild(child);
1288
- item.style.flex = itemWidthPx ? `0 0 ${itemWidthPx}px` : `0 0 ${itemWidth}%`;
1289
- item.style.scrollSnapAlign = 'center';
1290
- item.style.transformOrigin = 'center center';
1291
- item.style.position = 'relative';
1292
- item.style.overflow = 'hidden';
1293
- const overlay = document.createElement('div');
1294
- overlay.className = 'carousel-overlay';
1295
- Object.assign(overlay.style, {
1296
- position: 'absolute',
1297
- inset: '0',
1298
- background: overlayColor,
1299
- opacity: '0',
1300
- pointerEvents: 'none',
1301
- borderRadius: 'inherit',
1302
- zIndex: '1',
1303
- });
1304
- item.appendChild(overlay);
1872
+ bindSlotItemPresentation(item, carouselItemDescriptor(i, children.length, itemWidthPx ? `${itemWidthPx}px` : `${itemWidth}%`, overlayColor));
1305
1873
  container.appendChild(item);
1306
- items.push(item);
1307
1874
  }
1308
1875
  // Index of the item whose center is closest to the viewport center
1309
- const nearestIndex = () => {
1876
+ const nearestIndex = (items) => {
1310
1877
  const centerX = container.scrollLeft + container.offsetWidth / 2;
1311
1878
  let idx = 0;
1312
1879
  let minDist = Infinity;
@@ -1320,9 +1887,29 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
1320
1887
  });
1321
1888
  return idx;
1322
1889
  };
1890
+ let pendingSnapScrollEnd = null;
1891
+ let pendingSnapTimer = 0;
1892
+ const clearPendingSnap = () => {
1893
+ if (pendingSnapScrollEnd) {
1894
+ container.removeEventListener('scrollend', pendingSnapScrollEnd);
1895
+ pendingSnapScrollEnd = null;
1896
+ }
1897
+ if (pendingSnapTimer) {
1898
+ window.clearTimeout(pendingSnapTimer);
1899
+ pendingSnapTimer = 0;
1900
+ }
1901
+ };
1902
+ const restoreSnap = () => {
1903
+ clearPendingSnap();
1904
+ if (!ownerDisposed)
1905
+ container.style.scrollSnapType = 'x mandatory';
1906
+ };
1323
1907
  // Center a given item. 'smooth' animates and restores CSS snap afterwards;
1324
1908
  // 'auto' jumps instantly (used for initial positioning).
1325
1909
  const centerItem = (item, behavior = 'smooth') => {
1910
+ if (ownerDisposed)
1911
+ return;
1912
+ clearPendingSnap();
1326
1913
  const target = item.offsetLeft - (container.offsetWidth - item.offsetWidth) / 2;
1327
1914
  if (behavior === 'auto') {
1328
1915
  // Disable mandatory snap for the instant jump — a mandatory-snap container
@@ -1330,7 +1917,7 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
1330
1917
  // Restore snap next frame once the scroll offset is committed.
1331
1918
  container.style.scrollSnapType = 'none';
1332
1919
  container.scrollLeft = target;
1333
- requestAnimationFrame(() => {
1920
+ requestOwnerFrame(() => {
1334
1921
  container.style.scrollSnapType = 'x mandatory';
1335
1922
  });
1336
1923
  return;
@@ -1338,12 +1925,12 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
1338
1925
  // Disable CSS snap during the smooth animation, restore after for pixel-perfect alignment
1339
1926
  container.style.scrollSnapType = 'none';
1340
1927
  container.scrollTo({ left: target, behavior: 'smooth' });
1341
- container.addEventListener('scrollend', () => {
1342
- container.style.scrollSnapType = 'x mandatory';
1343
- }, { once: true });
1928
+ pendingSnapScrollEnd = restoreSnap;
1929
+ container.addEventListener('scrollend', pendingSnapScrollEnd, { once: true });
1344
1930
  // Fallback for browsers without scrollend event
1345
- setTimeout(() => {
1346
- container.style.scrollSnapType = 'x mandatory';
1931
+ pendingSnapTimer = window.setTimeout(() => {
1932
+ pendingSnapTimer = 0;
1933
+ restoreSnap();
1347
1934
  }, 400);
1348
1935
  };
1349
1936
  // Snap helper
@@ -1352,7 +1939,10 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
1352
1939
  let isDragging = false;
1353
1940
  let startX = 0;
1354
1941
  let scrollStart = 0;
1355
- container.addEventListener('mousedown', (e) => {
1942
+ const handleMouseDown = (event) => {
1943
+ if (ownerDisposed)
1944
+ return;
1945
+ const e = event;
1356
1946
  if (e.button !== 0)
1357
1947
  return; // left button only — keep middle/right clicks out of drag state
1358
1948
  isDragging = true;
@@ -1361,35 +1951,52 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
1361
1951
  container.style.cursor = 'grabbing';
1362
1952
  container.style.scrollSnapType = 'none';
1363
1953
  e.preventDefault();
1364
- });
1365
- document.addEventListener('mousemove', (e) => {
1366
- if (!isDragging)
1954
+ };
1955
+ const handleMouseMove = (event) => {
1956
+ if (ownerDisposed || !isDragging)
1367
1957
  return;
1958
+ const e = event;
1368
1959
  container.scrollLeft = scrollStart - (e.pageX - startX);
1369
- });
1370
- document.addEventListener('mouseup', () => {
1371
- if (!isDragging)
1960
+ };
1961
+ const handleMouseUp = () => {
1962
+ if (ownerDisposed || !isDragging)
1372
1963
  return;
1373
1964
  isDragging = false;
1374
1965
  container.style.cursor = 'grab';
1375
1966
  snapToNearest();
1376
- });
1377
- container.addEventListener('touchend', () => {
1378
- snapToNearest();
1379
- }, { passive: true });
1380
- // Wheel support
1967
+ };
1968
+ const handleTouchEnd = () => {
1969
+ if (!ownerDisposed)
1970
+ snapToNearest();
1971
+ };
1972
+ addSlotOwnerListener(ownerCleanup, container, 'mousedown', handleMouseDown);
1973
+ addSlotOwnerListener(ownerCleanup, document, 'mousemove', handleMouseMove);
1974
+ addSlotOwnerListener(ownerCleanup, document, 'mouseup', handleMouseUp);
1975
+ addSlotOwnerListener(ownerCleanup, container, 'touchend', handleTouchEnd, { passive: true });
1976
+ // Wheel support
1381
1977
  let wheelTimer = 0;
1382
- container.addEventListener('wheel', (e) => {
1978
+ const handleWheel = (event) => {
1979
+ if (ownerDisposed)
1980
+ return;
1981
+ const e = event;
1383
1982
  if (Math.abs(e.deltaX) < Math.abs(e.deltaY)) {
1384
1983
  e.preventDefault();
1385
1984
  container.style.scrollSnapType = 'none';
1386
1985
  container.scrollLeft += e.deltaY;
1387
- clearTimeout(wheelTimer);
1388
- wheelTimer = window.setTimeout(() => snapToNearest(), 150);
1986
+ window.clearTimeout(wheelTimer);
1987
+ wheelTimer = window.setTimeout(() => {
1988
+ wheelTimer = 0;
1989
+ if (!ownerDisposed)
1990
+ snapToNearest();
1991
+ }, 150);
1389
1992
  }
1390
- }, { passive: false });
1993
+ };
1994
+ addSlotOwnerListener(ownerCleanup, container, 'wheel', handleWheel, { passive: false });
1391
1995
  // Scale/opacity effects
1392
1996
  const updateEffects = () => {
1997
+ if (ownerDisposed)
1998
+ return;
1999
+ const items = carouselItems(container);
1393
2000
  const containerWidth = container.offsetWidth;
1394
2001
  const scrollCenter = container.scrollLeft + containerWidth / 2;
1395
2002
  items.forEach((item) => {
@@ -1402,20 +2009,27 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
1402
2009
  const opacity = 1 - ratio * (1 - inactiveOpacity);
1403
2010
  item.style.transform = `scale3d(${scale},${scale},1)`;
1404
2011
  item.style.opacity = `${opacity}`;
1405
- const overlayEl = item.querySelector('.carousel-overlay');
2012
+ const overlayEl = carouselOverlay(item);
1406
2013
  if (overlayEl)
1407
2014
  overlayEl.style.opacity = `${ratio}`;
1408
2015
  });
1409
2016
  };
1410
2017
  let rafId = 0;
1411
- container.addEventListener('scroll', () => {
1412
- cancelAnimationFrame(rafId);
1413
- rafId = requestAnimationFrame(updateEffects);
1414
- }, { passive: true });
2018
+ const handleScroll = () => {
2019
+ if (ownerDisposed)
2020
+ return;
2021
+ cancelOwnerFrame(rafId);
2022
+ rafId = requestOwnerFrame(() => {
2023
+ rafId = 0;
2024
+ updateEffects();
2025
+ });
2026
+ };
2027
+ addSlotOwnerListener(ownerCleanup, container, 'scroll', handleScroll, { passive: true });
1415
2028
  // Smooth snap to nearest item center + restore scrollSnapType for pixel-perfect final position
1416
2029
  snapToNearest = () => {
2030
+ const items = carouselItems(container);
1417
2031
  if (items.length > 0)
1418
- centerItem(items[nearestIndex()]);
2032
+ centerItem(items[nearestIndex(items)]);
1419
2033
  };
1420
2034
  // Autoplay — advance to the next item on an interval, looping back to the
1421
2035
  // first after the last. Pauses while the user hovers or interacts.
@@ -1423,33 +2037,64 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
1423
2037
  let autoplayTimer = 0;
1424
2038
  const stopAutoplay = () => {
1425
2039
  if (autoplayTimer) {
1426
- clearInterval(autoplayTimer);
2040
+ window.clearInterval(autoplayTimer);
1427
2041
  autoplayTimer = 0;
1428
2042
  }
1429
2043
  };
1430
2044
  const startAutoplay = () => {
1431
- if (!autoplay || items.length < 2)
2045
+ const items = carouselItems(container);
2046
+ if (ownerDisposed
2047
+ || !container.isConnected
2048
+ || !autoplay
2049
+ || items.length < 2)
1432
2050
  return;
1433
2051
  stopAutoplay();
1434
2052
  autoplayTimer = window.setInterval(() => {
1435
- // Self-clean once the carousel leaves the DOM (no teardown hook is available here)
1436
- if (!container.isConnected) {
2053
+ if (ownerDisposed || !container.isConnected) {
1437
2054
  stopAutoplay();
1438
2055
  return;
1439
2056
  }
1440
2057
  if (paused || isDragging)
1441
2058
  return;
1442
- centerItem(items[(nearestIndex() + 1) % items.length]);
2059
+ const items = carouselItems(container);
2060
+ if (items.length < 2)
2061
+ return;
2062
+ centerItem(items[(nearestIndex(items) + 1) % items.length]);
1443
2063
  }, autoplayInterval);
1444
2064
  };
1445
2065
  if (autoplay) {
1446
- container.addEventListener('mouseenter', () => { paused = true; });
1447
- container.addEventListener('mouseleave', () => { paused = false; });
1448
- container.addEventListener('touchstart', () => { paused = true; }, { passive: true });
1449
- container.addEventListener('touchend', () => { paused = false; }, { passive: true });
1450
- }
2066
+ const pauseAutoplay = () => {
2067
+ if (!ownerDisposed)
2068
+ paused = true;
2069
+ };
2070
+ const resumeAutoplay = () => {
2071
+ if (!ownerDisposed)
2072
+ paused = false;
2073
+ };
2074
+ addSlotOwnerListener(ownerCleanup, container, 'mouseenter', pauseAutoplay);
2075
+ addSlotOwnerListener(ownerCleanup, container, 'mouseleave', resumeAutoplay);
2076
+ addSlotOwnerListener(ownerCleanup, container, 'touchstart', pauseAutoplay, { passive: true });
2077
+ addSlotOwnerListener(ownerCleanup, container, 'touchend', resumeAutoplay, { passive: true });
2078
+ }
2079
+ ownerCleanup.add(() => {
2080
+ ownerDisposed = true;
2081
+ paused = true;
2082
+ isDragging = false;
2083
+ container.style.cursor = 'grab';
2084
+ if (wheelTimer) {
2085
+ window.clearTimeout(wheelTimer);
2086
+ wheelTimer = 0;
2087
+ }
2088
+ clearPendingSnap();
2089
+ stopAutoplay();
2090
+ for (const frameId of pendingFrames)
2091
+ cancelAnimationFrame(frameId);
2092
+ pendingFrames.clear();
2093
+ rafId = 0;
2094
+ });
1451
2095
  // Initial position & effects — deferred until element is in DOM and laid out
1452
- requestAnimationFrame(() => {
2096
+ requestOwnerFrame(() => {
2097
+ const items = carouselItems(container);
1453
2098
  if (items.length > 0) {
1454
2099
  // Use ACTUAL rendered width (includes padding in content-box mode) for perfect centering
1455
2100
  const containerWidth = container.offsetWidth;
@@ -1464,13 +2109,13 @@ function renderCarouselSlot(container, children, slotContent, props, renderChild
1464
2109
  delete container.dataset.restoreScrollLeft;
1465
2110
  container.style.scrollSnapType = 'none';
1466
2111
  container.scrollLeft = parseFloat(saved);
1467
- requestAnimationFrame(() => {
2112
+ requestOwnerFrame(() => {
1468
2113
  container.style.scrollSnapType = 'x mandatory';
1469
2114
  });
1470
2115
  }
1471
2116
  else {
1472
2117
  // Jump to the configured initial item (defaults to the first) — CSS snap maintains alignment
1473
- centerItem(items[initialIndex], 'auto');
2118
+ centerItem(items[Math.min(initialIndex, items.length - 1)], 'auto');
1474
2119
  }
1475
2120
  }
1476
2121
  updateEffects();
@@ -1757,6 +2402,97 @@ class BaseElement extends HTMLElement {
1757
2402
  this.shadowRoot.innerHTML = html;
1758
2403
  applyResponsiveStyles(this.shadowRoot, this._responsive);
1759
2404
  }
2405
+ /**
2406
+ * Patch freshly rendered markup around an existing interactive element.
2407
+ *
2408
+ * Replacing a focused native control through `shadowRoot.innerHTML` resets
2409
+ * its selection and, on mobile WebViews, can also recreate the soft
2410
+ * keyboard with a different layout. This helper keeps the selected element
2411
+ * connected while reconciling its surrounding markup.
2412
+ */
2413
+ patchShadowHTMLPreservingElement(html, preserved, selector) {
2414
+ if (!this.shadowRoot || !this.shadowRoot.contains(preserved))
2415
+ return false;
2416
+ const template = document.createElement('template');
2417
+ template.innerHTML = html;
2418
+ const desiredPreserved = template.content.querySelector(selector);
2419
+ if (!desiredPreserved
2420
+ || desiredPreserved.tagName !== preserved.tagName
2421
+ || (preserved instanceof HTMLInputElement
2422
+ && desiredPreserved instanceof HTMLInputElement
2423
+ && desiredPreserved.type !== preserved.type)) {
2424
+ return false;
2425
+ }
2426
+ this.reconcilePreservedTree(this.shadowRoot, template.content, preserved);
2427
+ applyResponsiveStyles(this.shadowRoot, this._responsive);
2428
+ return this.shadowRoot.contains(preserved);
2429
+ }
2430
+ reconcilePreservedTree(currentParent, desiredParent, preserved) {
2431
+ const desiredChildren = Array.from(desiredParent.children);
2432
+ const currentChildren = Array.from(currentParent.children);
2433
+ const matches = new Map();
2434
+ const claimed = new Set();
2435
+ for (const desired of desiredChildren) {
2436
+ const desiredKey = this.reconcileKey(desired);
2437
+ const current = currentChildren.find(candidate => (!claimed.has(candidate)
2438
+ && this.reconcileKey(candidate) === desiredKey));
2439
+ if (current) {
2440
+ claimed.add(current);
2441
+ matches.set(desired, current);
2442
+ }
2443
+ }
2444
+ for (const current of currentChildren) {
2445
+ if (!claimed.has(current))
2446
+ current.remove();
2447
+ }
2448
+ desiredChildren.forEach((desired, index) => {
2449
+ let current = matches.get(desired);
2450
+ if (!current) {
2451
+ current = desired.cloneNode(true);
2452
+ }
2453
+ const childAtIndex = currentParent.children.item(index);
2454
+ if (childAtIndex !== current) {
2455
+ currentParent.insertBefore(current, childAtIndex);
2456
+ }
2457
+ this.patchPreservedElement(current, desired, preserved);
2458
+ });
2459
+ }
2460
+ patchPreservedElement(current, desired, preserved) {
2461
+ for (const attribute of Array.from(current.attributes)) {
2462
+ if (!desired.hasAttribute(attribute.name)) {
2463
+ current.removeAttribute(attribute.name);
2464
+ }
2465
+ }
2466
+ for (const attribute of Array.from(desired.attributes)) {
2467
+ current.setAttribute(attribute.name, attribute.value);
2468
+ }
2469
+ if (current === preserved)
2470
+ return;
2471
+ if (current.contains(preserved)) {
2472
+ this.reconcilePreservedTree(current, desired, preserved);
2473
+ return;
2474
+ }
2475
+ current.innerHTML = desired.innerHTML;
2476
+ }
2477
+ reconcileKey(element) {
2478
+ if (element.classList.contains('card-input'))
2479
+ return 'card-input';
2480
+ if (element.classList.contains('card-input-wrapper')) {
2481
+ return 'card-input-wrapper';
2482
+ }
2483
+ if (element.classList.contains('card-input-label')) {
2484
+ return 'card-input-label';
2485
+ }
2486
+ if (element.classList.contains('input-control'))
2487
+ return 'input-control';
2488
+ if (element.classList.contains('input-prefix'))
2489
+ return 'input-prefix';
2490
+ if (element.classList.contains('input-suffix'))
2491
+ return 'input-suffix';
2492
+ if (element.classList.contains('number-stepper'))
2493
+ return 'number-stepper';
2494
+ return element.tagName;
2495
+ }
1760
2496
  /**
1761
2497
  * Escape a value before interpolating it into a double-quoted HTML
1762
2498
  * attribute. Browsers decode the entities before parsing inline CSS, so
@@ -2745,10 +3481,18 @@ function normalizeNumberAttribute(value, options = {}) {
2745
3481
  return numeric;
2746
3482
  }
2747
3483
  class CardInput extends BaseElement {
3484
+ constructor() {
3485
+ super(...arguments);
3486
+ this.wiredInputs = new WeakSet();
3487
+ this.wiredNumberInputs = new WeakSet();
3488
+ }
2748
3489
  render() {
2749
3490
  if (!this.shadowRoot || !this._node)
2750
3491
  return;
2751
- const { placeholder = '', autoFocus = false, inputType = 'text', label, defaultValue = '', disabled = false, readonly: readOnly = false, maxLength, rows, min, max, step = 1, controls = true, prefix, suffix, style, inputStyle, isExpressionResultStyle, } = this._props;
3492
+ const activeControl = this.shadowRoot.activeElement?.matches('.card-input')
3493
+ ? this.shadowRoot.activeElement
3494
+ : null;
3495
+ const { placeholder = '', inputType = 'text', inputMode, label, defaultValue = '', disabled = false, readonly: readOnly = false, maxLength, rows, min, max, step = 1, controls = true, prefix, suffix, style, inputStyle, isExpressionResultStyle, } = this._props;
2752
3496
  const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
2753
3497
  const legacyInputStyle = style && typeof style === 'object' && style.resize != null
2754
3498
  ? { resize: style.resize }
@@ -2818,6 +3562,7 @@ class CardInput extends BaseElement {
2818
3562
  id="${inputId}"
2819
3563
  class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
2820
3564
  placeholder="${this.escapeAttr(String(placeholder))}"
3565
+ ${inputMode ? `inputmode="${this.escapeAttr(String(inputMode))}"` : ''}
2821
3566
  ${describedBy}
2822
3567
  ${disabled ? 'disabled' : ''}
2823
3568
  ${readOnly ? 'readonly' : ''}
@@ -2829,6 +3574,7 @@ class CardInput extends BaseElement {
2829
3574
  id="${inputId}"
2830
3575
  class="card-input ${isMobile ? 'card-mobile' : 'card-desktop'}"
2831
3576
  type="${this.escapeAttr(String(inputType))}"
3577
+ ${inputMode ? `inputmode="${this.escapeAttr(String(inputMode))}"` : ''}
2832
3578
  placeholder="${this.escapeAttr(String(placeholder))}"
2833
3579
  value="${this.escapeAttr(String(defaultValue))}"
2834
3580
  ${describedBy}
@@ -2838,7 +3584,7 @@ class CardInput extends BaseElement {
2838
3584
  ${numberAttributes}
2839
3585
  style="${nativeInlineStyle}"
2840
3586
  />`;
2841
- this.setShadowHTML(`
3587
+ const markup = `
2842
3588
  <style>
2843
3589
  :host {
2844
3590
  display: block;
@@ -3012,7 +3758,11 @@ class CardInput extends BaseElement {
3012
3758
  ${numberStepperHtml}
3013
3759
  </div>
3014
3760
  </div>
3015
- `);
3761
+ `;
3762
+ if (!activeControl
3763
+ || !this.patchShadowHTMLPreservingElement(markup, activeControl, '.card-input')) {
3764
+ this.setShadowHTML(markup);
3765
+ }
3016
3766
  // Wire up native input/change events that bubble out of Shadow DOM.
3017
3767
  // renderCard listens for these standard event names (mapped from onInput / onChange).
3018
3768
  // The `detail.value` carries the current input value so that:
@@ -3020,41 +3770,41 @@ class CardInput extends BaseElement {
3020
3770
  // 2. action handlers can access it if needed
3021
3771
  const inputEl = this.shadowRoot.querySelector('.card-input');
3022
3772
  if (inputEl) {
3023
- inputEl.addEventListener('input', () => {
3024
- this.dispatchEvent(new CustomEvent('input', {
3025
- bubbles: true,
3026
- composed: true,
3027
- detail: { value: inputEl.value },
3028
- }));
3029
- });
3030
- inputEl.addEventListener('change', () => {
3031
- this.dispatchEvent(new CustomEvent('change', {
3032
- bubbles: true,
3033
- composed: true,
3034
- detail: { value: inputEl.value },
3035
- }));
3036
- });
3037
- if (autoFocus && !disabled) {
3038
- queueMicrotask(() => {
3039
- const control = this.getAutoFocusControl();
3040
- if (control && this.isFirstAvailableAutoFocusInput()) {
3041
- control.focus();
3042
- }
3773
+ if (!this.wiredInputs.has(inputEl)) {
3774
+ this.wiredInputs.add(inputEl);
3775
+ inputEl.addEventListener('input', () => {
3776
+ this.dispatchEvent(new CustomEvent('input', {
3777
+ bubbles: true,
3778
+ composed: true,
3779
+ detail: { value: inputEl.value },
3780
+ }));
3781
+ });
3782
+ inputEl.addEventListener('change', () => {
3783
+ this.dispatchEvent(new CustomEvent('change', {
3784
+ bubbles: true,
3785
+ composed: true,
3786
+ detail: { value: inputEl.value },
3787
+ }));
3043
3788
  });
3044
3789
  }
3045
3790
  if (isNumber &&
3046
3791
  showNumberStepper &&
3047
3792
  inputEl instanceof HTMLInputElement) {
3048
- const increaseButton = this.shadowRoot.querySelector('.number-step-button.increase');
3049
- const decreaseButton = this.shadowRoot.querySelector('.number-step-button.decrease');
3050
3793
  const refreshStepperState = () => {
3051
3794
  const state = getNumberStepperDisabledState(inputEl);
3795
+ const increaseButton = this.shadowRoot?.querySelector('.number-step-button.increase');
3796
+ const decreaseButton = this.shadowRoot?.querySelector('.number-step-button.decrease');
3052
3797
  if (increaseButton)
3053
3798
  increaseButton.disabled = state.increase;
3054
3799
  if (decreaseButton)
3055
3800
  decreaseButton.disabled = state.decrease;
3056
3801
  };
3057
- inputEl.addEventListener('input', refreshStepperState);
3802
+ if (!this.wiredNumberInputs.has(inputEl)) {
3803
+ this.wiredNumberInputs.add(inputEl);
3804
+ inputEl.addEventListener('input', refreshStepperState);
3805
+ }
3806
+ const increaseButton = this.shadowRoot.querySelector('.number-step-button.increase');
3807
+ const decreaseButton = this.shadowRoot.querySelector('.number-step-button.decrease');
3058
3808
  increaseButton?.addEventListener('click', () => {
3059
3809
  stepNumberInput(inputEl, 'increase');
3060
3810
  inputEl.focus();
@@ -3070,21 +3820,25 @@ class CardInput extends BaseElement {
3070
3820
  }
3071
3821
  }
3072
3822
  // ─── Helpers ──────────────────────────────────────────────────
3073
- getAutoFocusControl() {
3823
+ /**
3824
+ * Consume an interaction-driven focus request from the card renderer.
3825
+ * Mounts and ordinary updates never call this method.
3826
+ */
3827
+ requestAutoFocus() {
3074
3828
  if (!this.isConnected
3075
3829
  || !this._props.autoFocus
3076
3830
  || this.hasAttribute('data-disabled')) {
3077
- return null;
3831
+ return false;
3078
3832
  }
3079
3833
  const control = this.shadowRoot?.querySelector('.card-input');
3080
- return control && !control.disabled ? control : null;
3081
- }
3082
- isFirstAvailableAutoFocusInput() {
3083
- const root = this.getRootNode();
3084
- if (!('querySelectorAll' in root))
3834
+ if (!control
3835
+ || control.disabled
3836
+ || control.readOnly
3837
+ || control.value !== '') {
3085
3838
  return false;
3086
- const firstAvailable = Array.from(root.querySelectorAll(CardInput.is)).find(input => input.getAutoFocusControl() !== null);
3087
- return firstAvailable === this;
3839
+ }
3840
+ control.focus({ preventScroll: true });
3841
+ return this.shadowRoot?.activeElement === control;
3088
3842
  }
3089
3843
  /** Escape HTML entities for safe insertion. */
3090
3844
  escapeHtml(str) {
@@ -3146,15 +3900,37 @@ class CardImage extends BaseElement {
3146
3900
  if (!this.shadowRoot || !this._node)
3147
3901
  return;
3148
3902
  const { src, alt = '', width, height, objectFit = 'cover', preview = true, style, isExpressionResultStyle, } = this._props;
3149
- const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
3150
3903
  const imgSrc = this.resolveContent(src);
3151
3904
  const imgAlt = this.resolveContent(alt);
3905
+ // Keep the custom-element box independent from the image's intrinsic
3906
+ // dimensions. A failed image has different intrinsic sizing rules, so
3907
+ // sizing only the inner <img> makes flex/grid layouts jump on error.
3908
+ const hostWidth = this.applyHostDimension('width', style?.width, width);
3909
+ const hostHeight = this.applyHostDimension('height', style?.height, height);
3910
+ const ownsWidth = hostWidth != null;
3911
+ const ownsHeight = hostHeight != null;
3912
+ const reservesWidth = ownsWidth && hostWidth !== 'auto';
3913
+ const reservesHeight = ownsHeight && hostHeight !== 'auto';
3914
+ // Width and height belong to the host layout box. Remove them from the
3915
+ // inner style so relative lengths (for example 50%) are not applied twice.
3916
+ const imageStyle = style && typeof style === 'object' ? { ...style } : style;
3917
+ if (imageStyle && typeof imageStyle === 'object') {
3918
+ if (ownsWidth)
3919
+ delete imageStyle.width;
3920
+ if (ownsHeight)
3921
+ delete imageStyle.height;
3922
+ }
3923
+ const inlineStyle = this.buildInlineStyle(imageStyle, isExpressionResultStyle);
3152
3924
  // Build image inline styles
3153
3925
  const imgStyles = [];
3154
- if (width)
3155
- imgStyles.push(`width:${width}`);
3156
- if (height)
3157
- imgStyles.push(`height:${height}`);
3926
+ if (ownsWidth)
3927
+ imgStyles.push(`width:${reservesWidth ? '100%' : 'auto'}`);
3928
+ if (ownsHeight)
3929
+ imgStyles.push(`height:${reservesHeight ? '100%' : 'auto'}`);
3930
+ if (reservesWidth)
3931
+ imgStyles.push('min-width:0');
3932
+ if (reservesHeight)
3933
+ imgStyles.push('min-height:0');
3158
3934
  if (objectFit)
3159
3935
  imgStyles.push(`object-fit:${objectFit}`);
3160
3936
  if (inlineStyle)
@@ -3170,9 +3946,12 @@ class CardImage extends BaseElement {
3170
3946
  position: relative;
3171
3947
  display: inline-block;
3172
3948
  max-width: 100%;
3949
+ ${reservesWidth ? 'width: 100%;' : ''}
3950
+ ${reservesHeight ? 'height: 100%;' : ''}
3173
3951
  }
3174
3952
  .card-image {
3175
3953
  display: block;
3954
+ box-sizing: border-box;
3176
3955
  max-width: 100%;
3177
3956
  height: auto;
3178
3957
  transition: transform 0.2s ease, opacity 0.2s ease;
@@ -3193,8 +3972,8 @@ class CardImage extends BaseElement {
3193
3972
  100% { background-position: -200% 0; }
3194
3973
  }
3195
3974
  .card-image.error {
3196
- min-width: 100px;
3197
- min-height: 60px;
3975
+ min-width: ${reservesWidth ? '0' : '100px'};
3976
+ min-height: ${reservesHeight ? '0' : '60px'};
3198
3977
  background: #f5f5f5;
3199
3978
  display: flex;
3200
3979
  align-items: center;
@@ -3271,6 +4050,26 @@ class CardImage extends BaseElement {
3271
4050
  `);
3272
4051
  this.bindEvents(preview);
3273
4052
  }
4053
+ applyHostDimension(property, styleValue, propValue) {
4054
+ this.style.removeProperty(property);
4055
+ for (const value of [styleValue, propValue]) {
4056
+ const resolved = this.resolveDeclaredDimension(value);
4057
+ if (resolved == null)
4058
+ continue;
4059
+ this.style.setProperty(property, resolved);
4060
+ const applied = this.style.getPropertyValue(property);
4061
+ if (applied)
4062
+ return applied;
4063
+ }
4064
+ return undefined;
4065
+ }
4066
+ resolveDeclaredDimension(value) {
4067
+ if (typeof value === 'number' && Number.isFinite(value))
4068
+ return this.toCSS(value);
4069
+ if (typeof value === 'string' && value.trim() !== '')
4070
+ return this.toCSS(value.trim());
4071
+ return undefined;
4072
+ }
3274
4073
  bindEvents(preview) {
3275
4074
  if (!this.shadowRoot)
3276
4075
  return;
@@ -3385,9 +4184,7 @@ class CardDivider extends BaseElement {
3385
4184
  <div class="divider-line"></div>
3386
4185
  ${displayText ? `<span class="divider-text">${displayText}</span><div class="divider-line"></div>` : ''}
3387
4186
  `);
3388
- if (inlineStyle) {
3389
- this.style.cssText += ';' + inlineStyle;
3390
- }
4187
+ this.style.cssText = inlineStyle;
3391
4188
  }
3392
4189
  }
3393
4190
  CardDivider.is = 'ai-card-divider';
@@ -5316,9 +6113,7 @@ class CardLoading extends BaseElement {
5316
6113
  <div class="spinner"></div>
5317
6114
  ${displayText ? `<span class="loading-text">${displayText}</span>` : ''}
5318
6115
  `);
5319
- if (inlineStyle) {
5320
- this.style.cssText += ';' + inlineStyle;
5321
- }
6116
+ this.style.cssText = inlineStyle;
5322
6117
  }
5323
6118
  }
5324
6119
  CardLoading.is = 'ai-card-loading';
@@ -7787,6 +8582,468 @@ function resolveSizeInStyle(style) {
7787
8582
  return resolved;
7788
8583
  }
7789
8584
 
8585
+ const states = new WeakMap();
8586
+ function dispatchCurrent(element, state, domEvent, event) {
8587
+ const config = state.current;
8588
+ if (!config.isActive())
8589
+ return;
8590
+ if (domEvent === 'input'
8591
+ && config.variableKey
8592
+ && config.writeVariable
8593
+ && event.target === element) {
8594
+ const value = event instanceof CustomEvent
8595
+ && event.detail?.value !== undefined
8596
+ ? event.detail.value
8597
+ : event.target?.value;
8598
+ if (value !== undefined) {
8599
+ config.writeVariable(config.variableKey, value);
8600
+ }
8601
+ }
8602
+ for (const binding of config.events) {
8603
+ if (binding.domEvent !== domEvent)
8604
+ continue;
8605
+ if (binding.ownsValueEvent && event.target !== element)
8606
+ continue;
8607
+ binding.dispatch(event);
8608
+ }
8609
+ }
8610
+ function syncElementBindings(element, config) {
8611
+ let state = states.get(element);
8612
+ if (!state) {
8613
+ state = {
8614
+ current: config,
8615
+ listeners: new Map(),
8616
+ };
8617
+ states.set(element, state);
8618
+ }
8619
+ state.current = config;
8620
+ const desiredEvents = new Set(config.events.map(event => event.domEvent));
8621
+ if (config.variableKey)
8622
+ desiredEvents.add('input');
8623
+ for (const [name, listener] of state.listeners) {
8624
+ if (desiredEvents.has(name))
8625
+ continue;
8626
+ element.removeEventListener(name, listener);
8627
+ state.listeners.delete(name);
8628
+ }
8629
+ for (const name of desiredEvents) {
8630
+ if (state.listeners.has(name))
8631
+ continue;
8632
+ const listener = event => {
8633
+ const latest = states.get(element);
8634
+ if (latest) {
8635
+ dispatchCurrent(element, latest, name, event);
8636
+ }
8637
+ };
8638
+ element.addEventListener(name, listener);
8639
+ state.listeners.set(name, listener);
8640
+ }
8641
+ }
8642
+ function clearElementBindings(element) {
8643
+ const state = states.get(element);
8644
+ if (!state)
8645
+ return;
8646
+ let hasError = false;
8647
+ let firstError;
8648
+ for (const [name, listener] of state.listeners) {
8649
+ try {
8650
+ element.removeEventListener(name, listener);
8651
+ }
8652
+ catch (error) {
8653
+ if (!hasError)
8654
+ firstError = error;
8655
+ hasError = true;
8656
+ }
8657
+ }
8658
+ state.listeners.clear();
8659
+ states.delete(element);
8660
+ if (hasError)
8661
+ throw firstError;
8662
+ }
8663
+ function clearElementBindingsIn(root) {
8664
+ let hasError = false;
8665
+ let firstError;
8666
+ const clear = (element) => {
8667
+ try {
8668
+ clearElementBindings(element);
8669
+ }
8670
+ catch (error) {
8671
+ if (!hasError)
8672
+ firstError = error;
8673
+ hasError = true;
8674
+ }
8675
+ };
8676
+ clear(root);
8677
+ root.querySelectorAll('*').forEach(clear);
8678
+ if (hasError)
8679
+ throw firstError;
8680
+ }
8681
+
8682
+ let unserializableFingerprint = 0;
8683
+ function nextUnserializableFingerprint() {
8684
+ unserializableFingerprint += 1;
8685
+ return `__unserializable__${unserializableFingerprint}`;
8686
+ }
8687
+ function serializeJsonLike(value, ancestors) {
8688
+ if (value === null)
8689
+ return 'null';
8690
+ switch (typeof value) {
8691
+ case 'string':
8692
+ return JSON.stringify(value);
8693
+ case 'boolean':
8694
+ return value ? 'true' : 'false';
8695
+ case 'number':
8696
+ if (!Number.isFinite(value))
8697
+ throw new TypeError('Non-finite number');
8698
+ return JSON.stringify(value);
8699
+ case 'object':
8700
+ break;
8701
+ default:
8702
+ throw new TypeError('Unsupported JSON value');
8703
+ }
8704
+ if (ancestors.has(value))
8705
+ throw new TypeError('Circular JSON value');
8706
+ ancestors.add(value);
8707
+ try {
8708
+ if (Array.isArray(value)) {
8709
+ if (Object.getPrototypeOf(value) !== Array.prototype) {
8710
+ throw new TypeError('Unsupported array prototype');
8711
+ }
8712
+ const ownKeys = Reflect.ownKeys(value);
8713
+ const expectedKeys = new Set(['length']);
8714
+ for (let index = 0; index < value.length; index += 1) {
8715
+ expectedKeys.add(String(index));
8716
+ }
8717
+ if (ownKeys.some(key => typeof key !== 'string' || !expectedKeys.has(key))
8718
+ || ownKeys.length !== expectedKeys.size) {
8719
+ throw new TypeError('Unsupported array shape');
8720
+ }
8721
+ const values = [];
8722
+ for (let index = 0; index < value.length; index += 1) {
8723
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
8724
+ if (!descriptor?.enumerable || !('value' in descriptor)) {
8725
+ throw new TypeError('Unsupported array item');
8726
+ }
8727
+ values.push(serializeJsonLike(descriptor.value, ancestors));
8728
+ }
8729
+ return `[${values.join(',')}]`;
8730
+ }
8731
+ const prototype = Object.getPrototypeOf(value);
8732
+ if (prototype !== Object.prototype && prototype !== null) {
8733
+ throw new TypeError('Unsupported object prototype');
8734
+ }
8735
+ const ownKeys = Reflect.ownKeys(value);
8736
+ if (ownKeys.some(key => typeof key !== 'string')) {
8737
+ throw new TypeError('Unsupported symbol key');
8738
+ }
8739
+ const entries = ownKeys.sort().map(key => {
8740
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
8741
+ if (!descriptor?.enumerable || !('value' in descriptor)) {
8742
+ throw new TypeError('Unsupported object property');
8743
+ }
8744
+ return `${JSON.stringify(key)}:${serializeJsonLike(descriptor.value, ancestors)}`;
8745
+ });
8746
+ return `{${entries.join(',')}}`;
8747
+ }
8748
+ finally {
8749
+ ancestors.delete(value);
8750
+ }
8751
+ }
8752
+ function stableFingerprint(value) {
8753
+ try {
8754
+ return serializeJsonLike(value, new Set());
8755
+ }
8756
+ catch {
8757
+ return nextUnserializableFingerprint();
8758
+ }
8759
+ }
8760
+ function disposeWithoutThrow(mounted, hooks) {
8761
+ try {
8762
+ hooks.dispose(mounted);
8763
+ }
8764
+ catch {
8765
+ // DOM ownership has already been decided; cleanup errors cannot change it.
8766
+ }
8767
+ }
8768
+ function reportUnreconciled(hooks, error) {
8769
+ try {
8770
+ hooks.onUnreconciled?.(error);
8771
+ }
8772
+ catch {
8773
+ // Reporting cannot change which DOM tree is live.
8774
+ }
8775
+ }
8776
+ function replaceIncrementalNode(current, next, hooks) {
8777
+ const parent = current.element.parentNode;
8778
+ if (!parent) {
8779
+ reportUnreconciled(hooks);
8780
+ return current;
8781
+ }
8782
+ let replacement;
8783
+ try {
8784
+ replacement = hooks.mountReplacement(next);
8785
+ }
8786
+ catch (error) {
8787
+ reportUnreconciled(hooks, error);
8788
+ return current;
8789
+ }
8790
+ if (replacement.element === current.element
8791
+ || replacement.element.parentNode !== null) {
8792
+ reportUnreconciled(hooks);
8793
+ return current;
8794
+ }
8795
+ let adopted = false;
8796
+ let adoptionError;
8797
+ try {
8798
+ parent.replaceChild(replacement.element, current.element);
8799
+ adopted = replacement.element.parentNode === parent
8800
+ && current.element.parentNode !== parent;
8801
+ }
8802
+ catch (error) {
8803
+ adoptionError = error;
8804
+ adopted = replacement.element.parentNode === parent
8805
+ && current.element.parentNode !== parent;
8806
+ }
8807
+ if (!adopted) {
8808
+ if (replacement.element.parentNode === null) {
8809
+ disposeWithoutThrow(replacement, hooks);
8810
+ }
8811
+ reportUnreconciled(hooks, adoptionError);
8812
+ return current;
8813
+ }
8814
+ disposeWithoutThrow(current, hooks);
8815
+ return replacement;
8816
+ }
8817
+ function reconcileIncrementalNode(current, next, hooks) {
8818
+ const parent = current.element.parentNode;
8819
+ const sameChildren = current.children.length === next.children.length
8820
+ && current.children.every((child, index) => (child.occurrenceKey === next.children[index].occurrenceKey));
8821
+ let decision = 'replace';
8822
+ if (sameChildren) {
8823
+ try {
8824
+ decision = hooks.decide(current, next);
8825
+ }
8826
+ catch {
8827
+ return replaceIncrementalNode(current, next, hooks);
8828
+ }
8829
+ if (current.element.parentNode !== parent) {
8830
+ reportUnreconciled(hooks);
8831
+ return current;
8832
+ }
8833
+ }
8834
+ if (decision === 'replace') {
8835
+ return replaceIncrementalNode(current, next, hooks);
8836
+ }
8837
+ if (decision === 'update') {
8838
+ try {
8839
+ hooks.update(current, next);
8840
+ }
8841
+ catch {
8842
+ return replaceIncrementalNode(current, next, hooks);
8843
+ }
8844
+ if (current.element.parentNode !== parent) {
8845
+ reportUnreconciled(hooks);
8846
+ return current;
8847
+ }
8848
+ }
8849
+ try {
8850
+ hooks.syncBindings(current, next);
8851
+ }
8852
+ catch {
8853
+ return replaceIncrementalNode(current, next, hooks);
8854
+ }
8855
+ if (current.element.parentNode !== parent) {
8856
+ reportUnreconciled(hooks);
8857
+ return current;
8858
+ }
8859
+ const children = current.children.map((child, index) => (reconcileIncrementalNode(child, next.children[index], hooks)));
8860
+ return { ...next, element: current.element, children };
8861
+ }
8862
+
8863
+ const CARD_NODE_SELECTOR = '[data-card-id][data-card-type]';
8864
+ const FOCUS_TARGET_SELECTOR = [
8865
+ 'input:not([disabled])',
8866
+ 'textarea:not([disabled])',
8867
+ 'select:not([disabled])',
8868
+ 'button:not([disabled])',
8869
+ '[contenteditable="true"]',
8870
+ '[tabindex]:not([tabindex="-1"])',
8871
+ ].join(',');
8872
+ /**
8873
+ * Add logical node access to one rendered card without depending on whether
8874
+ * its renderer retains or replaces DOM elements between updates.
8875
+ */
8876
+ function createCardNodeAccess(container) {
8877
+ const handles = new Map();
8878
+ const listeners = new Set();
8879
+ let focusedId = null;
8880
+ let focusSyncScheduled = false;
8881
+ let observing = false;
8882
+ let observationEpoch = 0;
8883
+ let observer = null;
8884
+ let disposed = false;
8885
+ const resolveElement = (id) => {
8886
+ if (disposed)
8887
+ return null;
8888
+ return Array.from(container.querySelectorAll(CARD_NODE_SELECTOR)).find(element => element.getAttribute('data-card-id') === id) ?? null;
8889
+ };
8890
+ const getNode = (id) => {
8891
+ const existing = handles.get(id);
8892
+ if (existing)
8893
+ return existing;
8894
+ const handle = {
8895
+ id,
8896
+ get type() {
8897
+ return resolveElement(id)?.getAttribute('data-card-type') ?? null;
8898
+ },
8899
+ get current() {
8900
+ return resolveElement(id);
8901
+ },
8902
+ getRect() {
8903
+ return resolveElement(id)?.getBoundingClientRect() ?? null;
8904
+ },
8905
+ focus(options) {
8906
+ const element = resolveElement(id);
8907
+ if (!element || element.hasAttribute('data-disabled'))
8908
+ return false;
8909
+ const target = element.shadowRoot
8910
+ ?.querySelector(FOCUS_TARGET_SELECTOR) ?? element;
8911
+ target.focus(options);
8912
+ return document.activeElement === element
8913
+ || element.shadowRoot?.activeElement != null;
8914
+ },
8915
+ blur() {
8916
+ const element = resolveElement(id);
8917
+ if (!element)
8918
+ return false;
8919
+ const active = element.shadowRoot?.activeElement;
8920
+ if (active instanceof HTMLElement) {
8921
+ active.blur();
8922
+ }
8923
+ else {
8924
+ element.blur();
8925
+ }
8926
+ return true;
8927
+ },
8928
+ scrollIntoView(options) {
8929
+ const element = resolveElement(id);
8930
+ if (!element || typeof element.scrollIntoView !== 'function') {
8931
+ return false;
8932
+ }
8933
+ element.scrollIntoView(options);
8934
+ return true;
8935
+ },
8936
+ };
8937
+ handles.set(id, handle);
8938
+ return handle;
8939
+ };
8940
+ const resolveFocusedId = () => {
8941
+ let active = document.activeElement;
8942
+ if (!active || !container.contains(active))
8943
+ return null;
8944
+ while (active) {
8945
+ if (active instanceof HTMLElement
8946
+ && active.matches(CARD_NODE_SELECTOR)) {
8947
+ return active.getAttribute('data-card-id');
8948
+ }
8949
+ active = active.shadowRoot?.activeElement ?? null;
8950
+ }
8951
+ return null;
8952
+ };
8953
+ const syncFocus = () => {
8954
+ if (disposed || !observing)
8955
+ return;
8956
+ const nextId = resolveFocusedId();
8957
+ if (nextId === focusedId)
8958
+ return;
8959
+ const previousNode = focusedId ? getNode(focusedId) : null;
8960
+ const node = nextId ? getNode(nextId) : null;
8961
+ focusedId = nextId;
8962
+ for (const listener of [...listeners]) {
8963
+ try {
8964
+ listener({ node, previousNode });
8965
+ }
8966
+ catch (error) {
8967
+ console.error('[CardInstance] focus-change listener failed', error);
8968
+ }
8969
+ }
8970
+ };
8971
+ const scheduleFocusSync = () => {
8972
+ if (disposed || !observing || focusSyncScheduled)
8973
+ return;
8974
+ focusSyncScheduled = true;
8975
+ const scheduledEpoch = observationEpoch;
8976
+ queueMicrotask(() => {
8977
+ if (scheduledEpoch !== observationEpoch)
8978
+ return;
8979
+ focusSyncScheduled = false;
8980
+ syncFocus();
8981
+ });
8982
+ };
8983
+ const startObserving = () => {
8984
+ if (disposed || observing)
8985
+ return;
8986
+ observing = true;
8987
+ observationEpoch += 1;
8988
+ focusedId = resolveFocusedId();
8989
+ container.addEventListener('focusin', scheduleFocusSync, true);
8990
+ container.addEventListener('focusout', scheduleFocusSync, true);
8991
+ if (typeof MutationObserver !== 'undefined') {
8992
+ observer ?? (observer = new MutationObserver(scheduleFocusSync));
8993
+ observer.observe(container, { childList: true, subtree: true });
8994
+ }
8995
+ };
8996
+ const stopObserving = () => {
8997
+ if (!observing)
8998
+ return;
8999
+ observing = false;
9000
+ observationEpoch += 1;
9001
+ focusSyncScheduled = false;
9002
+ const cleanupSteps = [
9003
+ () => container.removeEventListener('focusin', scheduleFocusSync, true),
9004
+ () => container.removeEventListener('focusout', scheduleFocusSync, true),
9005
+ () => observer?.disconnect(),
9006
+ ];
9007
+ for (const cleanup of cleanupSteps) {
9008
+ try {
9009
+ cleanup();
9010
+ }
9011
+ catch (error) {
9012
+ console.error('[CardInstance] Focus observation cleanup failed', error);
9013
+ }
9014
+ }
9015
+ focusedId = null;
9016
+ };
9017
+ return {
9018
+ getNode,
9019
+ onFocusChange(listener) {
9020
+ if (disposed)
9021
+ return () => { };
9022
+ listeners.add(listener);
9023
+ if (listeners.size === 1)
9024
+ startObserving();
9025
+ let subscribed = true;
9026
+ return () => {
9027
+ if (!subscribed)
9028
+ return;
9029
+ subscribed = false;
9030
+ listeners.delete(listener);
9031
+ if (listeners.size === 0)
9032
+ stopObserving();
9033
+ };
9034
+ },
9035
+ dispose() {
9036
+ if (disposed)
9037
+ return;
9038
+ stopObserving();
9039
+ disposed = true;
9040
+ listeners.clear();
9041
+ handles.clear();
9042
+ },
9043
+ };
9044
+ }
9045
+
9046
+ const FORCE_REPLACE = Symbol('bound-force-replace');
7790
9047
  /**
7791
9048
  * Render the scoped/materialized branch of a card.
7792
9049
  *
@@ -7795,6 +9052,7 @@ function resolveSizeInStyle(style) {
7795
9052
  * only after materialization and detached DOM construction both succeed.
7796
9053
  */
7797
9054
  function renderBoundCard(container, schema, options) {
9055
+ const nodeAccess = createCardNodeAccess(container);
7798
9056
  const variables = cloneJsonData({
7799
9057
  ...schema.variables,
7800
9058
  ...options.variables,
@@ -7805,11 +9063,17 @@ function renderBoundCard(container, schema, options) {
7805
9063
  const inflightRequests = new Map();
7806
9064
  const activeLifecycleNodes = new Map();
7807
9065
  let currentMaterialized;
9066
+ let currentMountedTree = null;
9067
+ let lastPublishedVariables = cloneJsonData(variables);
7808
9068
  let revision = 0;
7809
9069
  let disposed = false;
9070
+ let disposeRequested = false;
9071
+ let publishing = false;
7810
9072
  const isMobile = options.isMobile === true;
7811
9073
  let actionQueue = Promise.resolve();
9074
+ let queuedActionCount = 0;
7812
9075
  let lifecycleQueue = Promise.resolve();
9076
+ const repeatIdentityCountCache = new WeakMap();
7813
9077
  function expressionContextFor(node) {
7814
9078
  return isBoundRenderTreeNode(node)
7815
9079
  ? createExpressionContext(node.scope)
@@ -7846,99 +9110,408 @@ function renderBoundCard(container, schema, options) {
7846
9110
  inflightRequests,
7847
9111
  };
7848
9112
  }
7849
- function renderNode(node, renderVariables, lifecycleIds) {
7850
- if (node.directives?.visible) {
7851
- const resolved = resolveNodeValue(node.directives.visible, node, renderVariables);
7852
- if (resolved === false
7853
- || resolved === 'false'
7854
- || resolved === ''
7855
- || resolved === 0) {
7856
- const placeholder = document.createElement('div');
7857
- placeholder.style.display = 'none';
7858
- placeholder.setAttribute('data-card-id', node.id);
7859
- return placeholder;
7860
- }
9113
+ function childOccurrenceKey(parentKey, id, sameIdOrdinal) {
9114
+ return `${parentKey}/${encodeURIComponent(id)}#${sameIdOrdinal}`;
9115
+ }
9116
+ function isNodeDisabled(node, renderVariables) {
9117
+ if (!node.directives?.disabled)
9118
+ return false;
9119
+ const resolved = resolveNodeValue(node.directives.disabled, node, renderVariables);
9120
+ return resolved === true || resolved === 'true' || resolved === 1;
9121
+ }
9122
+ function nodeBindingFingerprint(node, resolvedProps, disabled) {
9123
+ const binding = { disabled };
9124
+ if (resolvedProps?.variableKey !== undefined) {
9125
+ binding.variableKey = resolvedProps.variableKey;
9126
+ }
9127
+ if (!disabled && node.events !== undefined)
9128
+ binding.events = node.events;
9129
+ return stableFingerprint(binding);
9130
+ }
9131
+ function resolveBoundMetadata(node, occurrenceKey, renderVariables) {
9132
+ const visible = isVisible(node, renderVariables);
9133
+ const rendererToken = componentRenderers[node.type]
9134
+ ?? componentRenderers._default;
9135
+ if (!visible) {
9136
+ return {
9137
+ occurrenceKey,
9138
+ node,
9139
+ nodeId: node.id,
9140
+ nodeType: node.type,
9141
+ rendererToken,
9142
+ visible: false,
9143
+ disabled: false,
9144
+ resolvedProps: null,
9145
+ propsFingerprint: stableFingerprint(null),
9146
+ layoutFingerprint: stableFingerprint(null),
9147
+ bindingFingerprint: stableFingerprint(null),
9148
+ };
7861
9149
  }
7862
9150
  const resolvedProps = resolveNodeProps(node, renderVariables);
7863
- let isDisabled = false;
7864
- if (node.directives?.disabled) {
7865
- const resolved = resolveNodeValue(node.directives.disabled, node, renderVariables);
7866
- isDisabled = (resolved === true
7867
- || resolved === 'true'
7868
- || resolved === 1);
7869
- }
7870
- const renderer = (componentRenderers[node.type] ?? componentRenderers._default);
7871
- const el = renderer(node, resolvedProps, isMobile, options.responsive);
7872
- if (isDisabled) {
7873
- el.setAttribute('data-disabled', 'true');
7874
- el.style.background = '#F5F5F5';
7875
- el.style.color = '#C0C0C0';
7876
- el.style.setProperty('--card-disabled-color', '#C0C0C0');
7877
- el.style.pointerEvents = 'none';
7878
- el.style.cursor = 'default';
9151
+ const disabled = isNodeDisabled(node, renderVariables);
9152
+ return {
9153
+ occurrenceKey,
9154
+ node,
9155
+ nodeId: node.id,
9156
+ nodeType: node.type,
9157
+ rendererToken,
9158
+ visible: true,
9159
+ disabled,
9160
+ resolvedProps,
9161
+ propsFingerprint: stableFingerprint(resolvedProps),
9162
+ layoutFingerprint: stableFingerprint(getSlotLayoutFingerprintInput(resolvedProps, node.children, child => resolveNodeProps(child, renderVariables))),
9163
+ bindingFingerprint: nodeBindingFingerprint(node, resolvedProps, disabled),
9164
+ };
9165
+ }
9166
+ function applyDisabledState(element, disabled) {
9167
+ if (!disabled)
9168
+ return;
9169
+ element.setAttribute('data-disabled', 'true');
9170
+ element.style.background = '#F5F5F5';
9171
+ element.style.color = '#C0C0C0';
9172
+ element.style.setProperty('--card-disabled-color', '#C0C0C0');
9173
+ element.style.pointerEvents = 'none';
9174
+ element.style.cursor = 'default';
9175
+ }
9176
+ function applyDisabledDescendants(element, disabled) {
9177
+ if (!disabled)
9178
+ return;
9179
+ element.querySelectorAll('*').forEach((child) => {
9180
+ const htmlChild = child;
9181
+ htmlChild.setAttribute('data-disabled', 'true');
9182
+ htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
9183
+ });
9184
+ }
9185
+ function disposeBoundElement(element) {
9186
+ const cleanupSteps = [
9187
+ () => clearSlotOwnerBehaviorsIn(element),
9188
+ () => clearSlotItemPresentationsIn(element),
9189
+ () => clearElementBindingsIn(element),
9190
+ () => disposeChartsIn(element),
9191
+ ];
9192
+ for (const cleanup of cleanupSteps) {
9193
+ try {
9194
+ cleanup();
9195
+ }
9196
+ catch (error) {
9197
+ console.error('[renderCard] Bound element cleanup failed', error);
9198
+ }
7879
9199
  }
7880
- const variableKey = resolvedProps.variableKey;
7881
- if (variableKey) {
7882
- el.addEventListener('input', ((event) => {
7883
- if (event.target !== el || disposed)
7884
- return;
7885
- const value = event.detail?.value
7886
- ?? event.target?.value;
7887
- if (value !== undefined) {
7888
- // Keep the existing focus-preserving, no-rerender behavior. Bumping
7889
- // the revision prevents an older async action from overwriting it.
7890
- variables[variableKey] = value;
7891
- revision += 1;
9200
+ }
9201
+ function disposeUncoveredBoundBranches(current, coverageRoot) {
9202
+ for (const child of current.children) {
9203
+ if (coverageRoot.contains(child.element)) {
9204
+ disposeUncoveredBoundBranches(child, coverageRoot);
9205
+ }
9206
+ else {
9207
+ disposeBoundSubtree(child);
9208
+ }
9209
+ }
9210
+ }
9211
+ function disposeBoundSubtree(current) {
9212
+ disposeBoundElement(current.element);
9213
+ disposeUncoveredBoundBranches(current, current.element);
9214
+ }
9215
+ function disposeBoundBranchesOutsideContainer(current, disposedCoverage = null) {
9216
+ if (container.contains(current.element)) {
9217
+ for (const child of current.children) {
9218
+ disposeBoundBranchesOutsideContainer(child);
9219
+ }
9220
+ return;
9221
+ }
9222
+ let nextCoverage = disposedCoverage;
9223
+ if (!disposedCoverage?.contains(current.element)) {
9224
+ try {
9225
+ disposeBoundElement(current.element);
9226
+ nextCoverage = current.element;
9227
+ }
9228
+ catch {
9229
+ nextCoverage = null;
9230
+ }
9231
+ }
9232
+ for (const child of current.children) {
9233
+ disposeBoundBranchesOutsideContainer(child, nextCoverage);
9234
+ }
9235
+ }
9236
+ function disposeBoundSubtreeWithoutThrow(current) {
9237
+ try {
9238
+ disposeBoundSubtree(current);
9239
+ }
9240
+ catch {
9241
+ // Cleanup cannot change which candidate owns the live DOM.
9242
+ }
9243
+ }
9244
+ function mountBoundSubtree(node, occurrenceKey, renderVariables) {
9245
+ const metadata = resolveBoundMetadata(node, occurrenceKey, renderVariables);
9246
+ if (!metadata.visible) {
9247
+ const placeholder = document.createElement('div');
9248
+ placeholder.style.display = 'none';
9249
+ placeholder.setAttribute('data-card-id', node.id);
9250
+ return { ...metadata, element: placeholder, children: [] };
9251
+ }
9252
+ const renderer = metadata.rendererToken;
9253
+ const element = renderer(node, metadata.resolvedProps, isMobile, options.responsive);
9254
+ const children = [];
9255
+ try {
9256
+ applyDisabledState(element, metadata.disabled);
9257
+ const sameIdOrdinals = new Map();
9258
+ const renderChild = (child) => {
9259
+ const sameIdOrdinal = sameIdOrdinals.get(child.id) ?? 0;
9260
+ sameIdOrdinals.set(child.id, sameIdOrdinal + 1);
9261
+ const mounted = mountBoundSubtree(child, childOccurrenceKey(occurrenceKey, child.id, sameIdOrdinal), renderVariables);
9262
+ children.push(mounted);
9263
+ return mounted.element;
9264
+ };
9265
+ const childrenMap = {};
9266
+ for (const child of node.children)
9267
+ childrenMap[child.id] = child;
9268
+ const layoutApplied = renderSlotLayout(element, node.children, metadata.resolvedProps, child => renderChild(child), childrenMap, createPassiveActionContext(renderVariables), child => resolveNodeProps(child, renderVariables));
9269
+ if (!layoutApplied) {
9270
+ for (const child of node.children) {
9271
+ element.appendChild(renderChild(child));
7892
9272
  }
7893
- }));
9273
+ }
9274
+ applyDisabledDescendants(element, metadata.disabled);
9275
+ applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
9276
+ return { ...metadata, element, children };
7894
9277
  }
7895
- if (node.events && !isDisabled) {
7896
- for (const [eventName, eventValue] of Object.entries(node.events)) {
9278
+ catch (error) {
9279
+ disposeBoundSubtreeWithoutThrow({ ...metadata, element, children });
9280
+ throw error;
9281
+ }
9282
+ }
9283
+ function syncBoundBindings(element, node) {
9284
+ const events = [];
9285
+ if (node.visible && !node.disabled && node.node.events) {
9286
+ const repeatTargetFingerprint = getRepeatTargetFingerprint(node.node);
9287
+ for (const [schemaEvent, eventValue] of Object.entries(node.node.events)) {
7897
9288
  if (!eventValue || !resolveActionRef(eventValue, schemaActions)) {
7898
9289
  continue;
7899
9290
  }
7900
- const domEvent = EVENT_MAP[eventName] ?? eventName;
7901
- el.addEventListener(domEvent, ((event) => {
7902
- const ownsValueEvent = ((domEvent === 'input' || domEvent === 'change')
7903
- && VALUE_CONTROL_TYPES$1.has(node.type));
7904
- if (ownsValueEvent && event.target !== el)
7905
- return;
7906
- const eventDetail = (event instanceof CustomEvent && event.detail != null)
7907
- ? event.detail
7908
- : undefined;
7909
- enqueueBoundEvent(node.id, eventName, eventDetail);
7910
- }));
9291
+ const domEvent = EVENT_MAP[schemaEvent] ?? schemaEvent;
9292
+ events.push({
9293
+ schemaEvent,
9294
+ domEvent,
9295
+ ownsValueEvent: ((domEvent === 'input' || domEvent === 'change')
9296
+ && VALUE_CONTROL_TYPES$1.has(node.nodeType)),
9297
+ dispatch: (event) => {
9298
+ const detail = event instanceof CustomEvent && event.detail != null
9299
+ ? event.detail
9300
+ : undefined;
9301
+ enqueueBoundEvent(node.nodeId, schemaEvent, detail, repeatTargetFingerprint);
9302
+ },
9303
+ });
7911
9304
  }
7912
9305
  }
7913
- if (node.lifecycle) {
7914
- lifecycleIds.add(node.id);
9306
+ const variableKey = node.resolvedProps?.variableKey;
9307
+ syncElementBindings(element, {
9308
+ variableKey: typeof variableKey === 'string' ? variableKey : undefined,
9309
+ writeVariable: (key, value) => {
9310
+ if (disposed || publishing || !container.contains(element))
9311
+ return;
9312
+ variables[key] = value;
9313
+ revision += 1;
9314
+ },
9315
+ events,
9316
+ isActive: () => (!disposed
9317
+ && !publishing
9318
+ && container.contains(element)),
9319
+ });
9320
+ }
9321
+ function syncMountedBindings(current) {
9322
+ syncBoundBindings(current.element, current);
9323
+ for (const child of current.children)
9324
+ syncMountedBindings(child);
9325
+ }
9326
+ function collectMountedOccurrences(current, occurrences) {
9327
+ occurrences.set(current.occurrenceKey, current);
9328
+ for (const child of current.children) {
9329
+ collectMountedOccurrences(child, occurrences);
7915
9330
  }
7916
- const renderChild = (child) => renderNode(child, renderVariables, lifecycleIds);
7917
- const childrenMap = {};
9331
+ }
9332
+ function prepareBoundNode(node, occurrenceKey, renderVariables, forcedOwnerIds, current) {
9333
+ const metadata = resolveBoundMetadata(node, occurrenceKey, renderVariables);
9334
+ const forceReplace = forcedOwnerIds.has(node.id);
9335
+ if (!metadata.visible || forceReplace) {
9336
+ return {
9337
+ ...metadata,
9338
+ ...(forceReplace ? { [FORCE_REPLACE]: true } : {}),
9339
+ children: [],
9340
+ };
9341
+ }
9342
+ if (!current)
9343
+ return { ...metadata, children: [] };
9344
+ const nextChildrenById = new Map();
7918
9345
  for (const child of node.children) {
7919
- childrenMap[child.id] = child;
9346
+ const matching = nextChildrenById.get(child.id) ?? [];
9347
+ matching.push(child);
9348
+ nextChildrenById.set(child.id, matching);
9349
+ }
9350
+ const nextChildOrdinals = new Map();
9351
+ const children = [];
9352
+ for (const currentChild of current.children) {
9353
+ const matching = nextChildrenById.get(currentChild.nodeId);
9354
+ if (!matching?.length) {
9355
+ return { ...metadata, [FORCE_REPLACE]: true, children: [] };
9356
+ }
9357
+ const ordinal = nextChildOrdinals.get(currentChild.nodeId) ?? 0;
9358
+ nextChildOrdinals.set(currentChild.nodeId, ordinal + 1);
9359
+ const child = matching[Math.min(ordinal, matching.length - 1)];
9360
+ children.push(prepareBoundNode(child, currentChild.occurrenceKey, renderVariables, forcedOwnerIds, currentChild));
7920
9361
  }
7921
- const layoutApplied = renderSlotLayout(el, node.children, resolvedProps, child => renderChild(child), childrenMap, createPassiveActionContext(renderVariables));
7922
- if (!layoutApplied) {
7923
- for (const child of node.children) {
7924
- el.appendChild(renderChild(child));
9362
+ return {
9363
+ ...metadata,
9364
+ children,
9365
+ };
9366
+ }
9367
+ function decideBoundUpdate(current, next) {
9368
+ if (next[FORCE_REPLACE]
9369
+ || current.occurrenceKey !== next.occurrenceKey
9370
+ || current.nodeId !== next.nodeId
9371
+ || current.nodeType !== next.nodeType
9372
+ || current.rendererToken !== next.rendererToken
9373
+ || current.visible !== next.visible
9374
+ || current.disabled !== next.disabled
9375
+ || current.layoutFingerprint !== next.layoutFingerprint) {
9376
+ return 'replace';
9377
+ }
9378
+ if (current.propsFingerprint === next.propsFingerprint)
9379
+ return 'retain';
9380
+ return (current.element instanceof BaseElement
9381
+ || current.rendererToken === componentRenderers._default) ? 'update' : 'replace';
9382
+ }
9383
+ function updateBoundNode(current, next) {
9384
+ const element = current.element;
9385
+ if (element instanceof BaseElement) {
9386
+ updateSlotItemShellPresentation(element, () => {
9387
+ element.updateProps(next.resolvedProps, isMobile, options.responsive);
9388
+ applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
9389
+ }, getSlotHostPresentationOwnership(next.resolvedProps));
9390
+ return;
9391
+ }
9392
+ if (current.rendererToken !== componentRenderers._default) {
9393
+ throw new Error('[renderCard] Bound node cannot update in place');
9394
+ }
9395
+ const previous = componentRenderers._default(current.node, current.resolvedProps, isMobile, options.responsive);
9396
+ const desired = componentRenderers._default(next.node, next.resolvedProps, isMobile, options.responsive);
9397
+ applyDisabledState(previous, current.disabled);
9398
+ applyDisabledState(desired, next.disabled);
9399
+ const hostOwnership = getSlotHostPresentationOwnership(next.resolvedProps);
9400
+ updateSlotItemShellPresentation(element, () => {
9401
+ patchElementShellPresentation(element, previous, desired, hostOwnership, getSlotItemPresentationOwnership(element));
9402
+ }, hostOwnership);
9403
+ }
9404
+ function collectChangedPointers(before, after, path = '') {
9405
+ if (Object.is(before, after))
9406
+ return [];
9407
+ if (before === null
9408
+ || after === null
9409
+ || typeof before !== 'object'
9410
+ || typeof after !== 'object') {
9411
+ return [path];
9412
+ }
9413
+ if (Array.isArray(before) || Array.isArray(after)) {
9414
+ if (!Array.isArray(before) || !Array.isArray(after))
9415
+ return [path];
9416
+ if (before.length !== after.length)
9417
+ return [path];
9418
+ }
9419
+ const left = before;
9420
+ const right = after;
9421
+ const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
9422
+ const changed = [];
9423
+ for (const key of keys) {
9424
+ const token = key.replace(/~/g, '~0').replace(/\//g, '~1');
9425
+ changed.push(...collectChangedPointers(left[key], right[key], `${path}/${token}`));
9426
+ }
9427
+ return changed;
9428
+ }
9429
+ function selectForcedRepeatOwnerIds(before, draft) {
9430
+ if (currentMaterialized.unresolvedRepeatOwners.size > 0) {
9431
+ return new Set([currentMaterialized.root.id]);
9432
+ }
9433
+ const selectedKeys = new Set();
9434
+ for (const pointer of collectChangedPointers(before, draft)) {
9435
+ for (const owner of findAffectedRepeatOwners(currentMaterialized, pointer)) {
9436
+ selectedKeys.add(owner.key);
7925
9437
  }
7926
9438
  }
7927
- if (isDisabled) {
7928
- el.querySelectorAll('*').forEach((child) => {
7929
- const htmlChild = child;
7930
- htmlChild.setAttribute('data-disabled', 'true');
7931
- htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
7932
- });
9439
+ for (const key of [...selectedKeys]) {
9440
+ let parentKey = currentMaterialized.repeatOwners.get(key)?.parentKey;
9441
+ while (parentKey) {
9442
+ if (selectedKeys.has(parentKey)) {
9443
+ selectedKeys.delete(key);
9444
+ break;
9445
+ }
9446
+ parentKey = currentMaterialized.repeatOwners.get(parentKey)?.parentKey;
9447
+ }
9448
+ }
9449
+ return new Set([...selectedKeys].flatMap((key) => {
9450
+ const id = currentMaterialized.repeatOwners.get(key)?.runtimeOwnerId;
9451
+ return id ? [id] : [];
9452
+ }));
9453
+ }
9454
+ function collectMountedLifecycleIds(root) {
9455
+ const ids = new Set();
9456
+ const visit = (node) => {
9457
+ if (!node.visible)
9458
+ return;
9459
+ if (node.node.lifecycle)
9460
+ ids.add(node.nodeId);
9461
+ for (const child of node.children)
9462
+ visit(child);
9463
+ };
9464
+ if (root)
9465
+ visit(root);
9466
+ return ids;
9467
+ }
9468
+ function prepareReplacementRoots(current, next, draft, replacements) {
9469
+ const sameChildren = current.children.length === next.children.length
9470
+ && current.children.every((child, index) => (child.occurrenceKey === next.children[index].occurrenceKey));
9471
+ const decision = sameChildren
9472
+ ? decideBoundUpdate(current, next)
9473
+ : 'replace';
9474
+ if (decision === 'replace'
9475
+ || (decision === 'update' && next.children.length === 0)) {
9476
+ replacements.set(next.occurrenceKey, mountBoundSubtree(next.node, next.occurrenceKey, draft));
9477
+ }
9478
+ if (decision === 'replace')
9479
+ return;
9480
+ for (let index = 0; index < next.children.length; index += 1) {
9481
+ prepareReplacementRoots(current.children[index], next.children[index], draft, replacements);
7933
9482
  }
7934
- applyResponsiveStyles(el, createResponsiveContext(isMobile, options.responsive));
7935
- return el;
7936
9483
  }
7937
- function prepareCandidate(draft) {
9484
+ function prepareIncrementalCandidate(before, draft) {
7938
9485
  const materialized = materializeCard(schema, draft);
7939
- const lifecycleIds = new Set();
7940
- const dom = renderNode(materialized.root, draft, lifecycleIds);
7941
- return { materialized, dom, lifecycleIds };
9486
+ const forcedOwnerIds = currentMountedTree
9487
+ ? materialized.unresolvedRepeatOwners.size > 0
9488
+ ? new Set([materialized.root.id])
9489
+ : selectForcedRepeatOwnerIds(before, draft)
9490
+ : new Set();
9491
+ const preparedRoot = prepareBoundNode(materialized.root, `${materialized.root.id}#0`, draft, forcedOwnerIds, currentMountedTree);
9492
+ const replacementRoots = new Map();
9493
+ try {
9494
+ if (currentMountedTree) {
9495
+ prepareReplacementRoots(currentMountedTree, preparedRoot, draft, replacementRoots);
9496
+ }
9497
+ else {
9498
+ replacementRoots.set(preparedRoot.occurrenceKey, mountBoundSubtree(materialized.root, preparedRoot.occurrenceKey, draft));
9499
+ }
9500
+ return {
9501
+ materialized,
9502
+ preparedRoot,
9503
+ replacementRoots,
9504
+ autoFocusIds: currentMountedTree
9505
+ ? collectAutoFocusRevealIds(currentMaterialized, materialized, draft)
9506
+ : new Set(),
9507
+ };
9508
+ }
9509
+ catch (error) {
9510
+ for (const replacement of replacementRoots.values()) {
9511
+ disposeBoundSubtreeWithoutThrow(replacement);
9512
+ }
9513
+ throw error;
9514
+ }
7942
9515
  }
7943
9516
  function indexNodes(root) {
7944
9517
  const nodes = new Map();
@@ -7949,6 +9522,93 @@ function renderBoundCard(container, schema, options) {
7949
9522
  visit(root);
7950
9523
  return nodes;
7951
9524
  }
9525
+ function getRepeatTargetFingerprint(node) {
9526
+ const identityToken = (key, value) => {
9527
+ if (value === null)
9528
+ return `${key}:null`;
9529
+ if (typeof value === 'string')
9530
+ return `${key}:string:${value}`;
9531
+ if (typeof value === 'boolean')
9532
+ return `${key}:boolean:${value}`;
9533
+ return `${key}:number:${Object.is(value, -0) ? '-0' : String(value)}`;
9534
+ };
9535
+ const identityCountsFor = (root) => {
9536
+ const cached = repeatIdentityCountCache.get(root);
9537
+ if (cached)
9538
+ return cached;
9539
+ const counts = new Map();
9540
+ const visited = new Set();
9541
+ const visit = (candidate) => {
9542
+ if (candidate === null || typeof candidate !== 'object')
9543
+ return;
9544
+ if (visited.has(candidate))
9545
+ return;
9546
+ visited.add(candidate);
9547
+ if (!Array.isArray(candidate)) {
9548
+ for (const key of ['id', 'key']) {
9549
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
9550
+ if (!descriptor || !('value' in descriptor))
9551
+ continue;
9552
+ const identity = descriptor.value;
9553
+ if (identity === null
9554
+ || (typeof identity !== 'string'
9555
+ && typeof identity !== 'number'
9556
+ && typeof identity !== 'boolean')) {
9557
+ continue;
9558
+ }
9559
+ const token = identityToken(key, identity);
9560
+ counts.set(token, (counts.get(token) ?? 0) + 1);
9561
+ }
9562
+ }
9563
+ for (const nested of Object.values(candidate))
9564
+ visit(nested);
9565
+ };
9566
+ visit(root);
9567
+ repeatIdentityCountCache.set(root, counts);
9568
+ return counts;
9569
+ };
9570
+ const identityFor = (value, root) => {
9571
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
9572
+ return value;
9573
+ }
9574
+ const record = value;
9575
+ for (const key of ['id', 'key']) {
9576
+ const descriptor = Object.getOwnPropertyDescriptor(record, key);
9577
+ if (!descriptor || !('value' in descriptor))
9578
+ continue;
9579
+ const identity = descriptor.value;
9580
+ if (identity === null
9581
+ || (typeof identity !== 'string'
9582
+ && typeof identity !== 'number'
9583
+ && typeof identity !== 'boolean')) {
9584
+ continue;
9585
+ }
9586
+ const matches = identityCountsFor(root).get(identityToken(key, identity)) ?? 0;
9587
+ if (matches === 1)
9588
+ return { [key]: identity };
9589
+ }
9590
+ return value;
9591
+ };
9592
+ const targets = [];
9593
+ let scope = node.scope;
9594
+ while (scope) {
9595
+ const scopeRoot = scope.root;
9596
+ const localEntries = Object.entries(scope.locals);
9597
+ if (localEntries.length > 0) {
9598
+ targets.push(Object.fromEntries(localEntries.map(([key, value]) => [
9599
+ key,
9600
+ identityFor(value, scopeRoot),
9601
+ ])));
9602
+ }
9603
+ else if (scope.bindingDialect === 'a2ui' && scope.dataPath !== '') {
9604
+ targets.push(identityFor(resolveA2UIDeep({ path: '' }, scopeRoot, scope.dataPath), scopeRoot));
9605
+ }
9606
+ scope = scope.parent;
9607
+ }
9608
+ if (targets.length === 0)
9609
+ return undefined;
9610
+ return stableFingerprint({ sourceId: node.sourceId, targets });
9611
+ }
7952
9612
  function selectLifecycleNodes(materialized, ids) {
7953
9613
  const indexed = indexNodes(materialized.root);
7954
9614
  const selected = new Map();
@@ -7959,26 +9619,80 @@ function renderBoundCard(container, schema, options) {
7959
9619
  }
7960
9620
  return selected;
7961
9621
  }
7962
- function createLifecycleActionContext(node) {
9622
+ function isVisible(node, renderVariables) {
9623
+ if (!node.directives?.visible)
9624
+ return true;
9625
+ const resolved = resolveNodeValue(node.directives.visible, node, renderVariables);
9626
+ return !(resolved === false
9627
+ || resolved === 'false'
9628
+ || resolved === ''
9629
+ || resolved === 0);
9630
+ }
9631
+ function collectAutoFocusRevealIds(previous, next, draft) {
9632
+ const previousNodes = indexNodes(previous.root);
9633
+ const requested = new Set();
9634
+ const visit = (nextNode, previousParentVisible, nextParentVisible) => {
9635
+ const previousNode = previousNodes.get(nextNode.id);
9636
+ const wasVisible = Boolean(previousNode
9637
+ && previousParentVisible
9638
+ && isVisible(previousNode, variables));
9639
+ const nowVisible = nextParentVisible && isVisible(nextNode, draft);
9640
+ if (nextNode.type === 'Input'
9641
+ && !wasVisible
9642
+ && nowVisible
9643
+ && resolveNodeProps(nextNode, draft).autoFocus === true) {
9644
+ requested.add(nextNode.id);
9645
+ }
9646
+ nextNode.children.forEach(child => visit(child, wasVisible, nowVisible));
9647
+ };
9648
+ visit(next.root, true, true);
9649
+ return requested;
9650
+ }
9651
+ function applyAutoFocus(candidate) {
9652
+ if (candidate.autoFocusIds.size === 0)
9653
+ return;
9654
+ const inputs = container.querySelectorAll(CardInput.is);
9655
+ for (const input of inputs) {
9656
+ const id = input.getAttribute('data-card-id');
9657
+ if (id && candidate.autoFocusIds.has(id) && input.requestAutoFocus()) {
9658
+ break;
9659
+ }
9660
+ }
9661
+ }
9662
+ function createLifecycleActionSession(node) {
9663
+ const lifecycleVariables = cloneJsonData(variables);
9664
+ const rebaseScope = (scope) => ({
9665
+ ...scope,
9666
+ root: lifecycleVariables,
9667
+ parent: scope.parent ? rebaseScope(scope.parent) : undefined,
9668
+ });
9669
+ const lifecycleScope = rebaseScope(node.scope);
9670
+ let authorityRevision = revision;
7963
9671
  const writeLiveVariable = (key, value, silent = false) => {
7964
- if (disposed)
9672
+ const nextValue = cloneJsonData(value);
9673
+ writeDraftVariable(lifecycleVariables, key, nextValue);
9674
+ repeatIdentityCountCache.delete(lifecycleVariables);
9675
+ if (disposed || authorityRevision !== revision)
7965
9676
  return;
7966
9677
  if (silent) {
7967
- writeDraftVariable(variables, key, cloneJsonData(value));
9678
+ writeDraftVariable(variables, key, nextValue);
7968
9679
  return;
7969
9680
  }
7970
- updateVariables({ [key]: value });
9681
+ // Publish the complete lifecycle draft so direct additions, changes,
9682
+ // and deletions join this explicit write in one authoritative render.
9683
+ commitDraft(cloneJsonData(lastPublishedVariables), lifecycleVariables, authorityRevision);
9684
+ authorityRevision = revision;
7971
9685
  };
7972
- return {
9686
+ const context = {
7973
9687
  ...createWebActionContext({
7974
9688
  ...options,
7975
9689
  setVariable: writeLiveVariable,
7976
9690
  abortSignal: abortController.signal,
7977
9691
  }),
7978
- variables,
7979
- expressionContext: createExpressionContext(node.scope),
9692
+ variables: lifecycleVariables,
9693
+ expressionContext: createExpressionContext(lifecycleScope),
7980
9694
  parameterResolver: node.bindingDialect === 'a2ui'
7981
- ? createA2UIParameterResolver(variables, node.dataPath)
9695
+ ? createA2UIParameterResolver(lifecycleVariables, node.dataPath)
7982
9696
  : undefined,
7983
9697
  variableWriter: (key, value, options) => {
7984
9698
  writeLiveVariable(key, value, options.silent);
@@ -7986,6 +9700,20 @@ function renderBoundCard(container, schema, options) {
7986
9700
  botId: options.botId,
7987
9701
  inflightRequests,
7988
9702
  };
9703
+ return {
9704
+ context,
9705
+ publishDirectMutations() {
9706
+ if (disposed
9707
+ || authorityRevision !== revision
9708
+ || !hasVariableChanges(variables, lifecycleVariables)) {
9709
+ return;
9710
+ }
9711
+ // Direct custom-handler mutations historically updated the live
9712
+ // variables without rendering. Preserve that behavior while keeping
9713
+ // a stale async lifecycle detached after a newer revision wins.
9714
+ replaceRootContents(variables, lifecycleVariables);
9715
+ },
9716
+ };
7989
9717
  }
7990
9718
  function reconcileLifecycles(nextNodes) {
7991
9719
  const removed = [...activeLifecycleNodes.entries()]
@@ -7998,45 +9726,263 @@ function renderBoundCard(container, schema, options) {
7998
9726
  }
7999
9727
  lifecycleQueue = lifecycleQueue.then(async () => {
8000
9728
  for (const [id, node] of removed) {
8001
- await lifecycleManager.destroy(id, createLifecycleActionContext(node));
8002
- lifecycleManager.unregister(id);
9729
+ if (disposed)
9730
+ return;
9731
+ const session = createLifecycleActionSession(node);
9732
+ try {
9733
+ await lifecycleManager.destroy(id, session.context);
9734
+ session.publishDirectMutations();
9735
+ }
9736
+ catch (error) {
9737
+ console.error('[renderCard] Bound lifecycle action failed', error);
9738
+ }
9739
+ finally {
9740
+ lifecycleManager.unregister(id);
9741
+ }
8003
9742
  }
8004
- if (disposed)
8005
- return;
8006
9743
  for (const [id, node] of added) {
9744
+ if (disposed)
9745
+ return;
8007
9746
  lifecycleManager.register(id, node.lifecycle);
8008
- await lifecycleManager.mount(id, createLifecycleActionContext(node));
9747
+ const session = createLifecycleActionSession(node);
9748
+ try {
9749
+ await lifecycleManager.mount(id, session.context);
9750
+ session.publishDirectMutations();
9751
+ }
9752
+ catch (error) {
9753
+ lifecycleManager.unregister(id);
9754
+ console.error('[renderCard] Bound lifecycle action failed', error);
9755
+ }
8009
9756
  }
8010
9757
  }).catch((error) => {
8011
9758
  console.error('[renderCard] Bound lifecycle action failed', error);
8012
9759
  });
8013
9760
  }
8014
- function publishDOM(candidate, incrementRevision) {
8015
- const scrollPositions = captureScrollPositions$1(container);
8016
- const mediaStates = captureMediaStates$1(container);
8017
- disposeChartsIn(container);
8018
- container.replaceChildren(candidate.dom);
8019
- currentMaterialized = candidate.materialized;
8020
- if (incrementRevision)
8021
- revision += 1;
8022
- reconcileLifecycles(selectLifecycleNodes(currentMaterialized, candidate.lifecycleIds));
8023
- restoreScrollPositions$1(container, scrollPositions);
8024
- restoreMediaStates$1(container, mediaStates);
9761
+ function disposeCandidate(candidate) {
9762
+ for (const replacement of candidate.replacementRoots.values()) {
9763
+ disposeBoundSubtreeWithoutThrow(replacement);
9764
+ }
9765
+ candidate.replacementRoots.clear();
9766
+ }
9767
+ function restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates) {
9768
+ if (previous.element !== next.element) {
9769
+ restoreScrollPositions$1(next.element, scrollPositions);
9770
+ restoreMediaStates$1(next.element, mediaStates);
9771
+ return;
9772
+ }
9773
+ if (previous.propsFingerprint !== next.propsFingerprint
9774
+ && (next.nodeType === 'Audio' || next.nodeType === 'Video')) {
9775
+ restoreMediaStates$1(next.element, mediaStates);
9776
+ }
9777
+ for (let index = 0; index < next.children.length; index += 1) {
9778
+ const previousChild = previous.children[index];
9779
+ const nextChild = next.children[index];
9780
+ if (previousChild
9781
+ && previousChild.occurrenceKey === nextChild.occurrenceKey) {
9782
+ restoreChangedSubtreeState(previousChild, nextChild, scrollPositions, mediaStates);
9783
+ }
9784
+ }
9785
+ }
9786
+ function restoreDisabledDescendantPresentation(current) {
9787
+ if (current.disabled) {
9788
+ applyDisabledState(current.element, true);
9789
+ applyDisabledDescendants(current.element, true);
9790
+ }
9791
+ for (const child of current.children) {
9792
+ restoreDisabledDescendantPresentation(child);
9793
+ }
9794
+ }
9795
+ function replaceContainerRoot(element, failureMessage) {
9796
+ let adoptionThrew = false;
9797
+ let adoptionError;
9798
+ try {
9799
+ container.replaceChildren(element);
9800
+ }
9801
+ catch (error) {
9802
+ adoptionThrew = true;
9803
+ adoptionError = error;
9804
+ }
9805
+ const adopted = (element.parentNode === container
9806
+ && container.firstChild === element
9807
+ && container.childNodes.length === 1);
9808
+ if (adopted)
9809
+ return;
9810
+ if (adoptionThrew)
9811
+ throw adoptionError;
9812
+ throw new Error(failureMessage);
9813
+ }
9814
+ function commitIncrementalCandidate(candidate, renderVariables) {
9815
+ const replacements = candidate.replacementRoots;
9816
+ try {
9817
+ if (!currentMountedTree) {
9818
+ const initial = replacements.get(candidate.preparedRoot.occurrenceKey);
9819
+ if (!initial) {
9820
+ throw new Error('[renderCard] Missing bound root candidate');
9821
+ }
9822
+ syncMountedBindings(initial);
9823
+ disposeChartsIn(container);
9824
+ replaceContainerRoot(initial.element, '[renderCard] Bound root candidate was not adopted');
9825
+ replacements.delete(candidate.preparedRoot.occurrenceKey);
9826
+ restoreDisabledDescendantPresentation(initial);
9827
+ return initial;
9828
+ }
9829
+ const previous = currentMountedTree;
9830
+ const replacementSources = new Map();
9831
+ collectMountedOccurrences(previous, replacementSources);
9832
+ const scrollPositions = captureScrollPositions$1(container);
9833
+ const mediaStates = captureMediaStates$1(container);
9834
+ let unreconciled = false;
9835
+ const next = reconcileIncrementalNode(previous, candidate.preparedRoot, {
9836
+ decide: (current, prepared) => decideBoundUpdate(current, prepared),
9837
+ update: (current, prepared) => updateBoundNode(current, prepared),
9838
+ mountReplacement: (prepared) => {
9839
+ const candidateReplacement = replacements.get(prepared.occurrenceKey);
9840
+ const replacement = candidateReplacement ?? mountBoundSubtree(prepared.node, prepared.occurrenceKey, renderVariables);
9841
+ const current = replacementSources.get(prepared.occurrenceKey);
9842
+ let staged = false;
9843
+ try {
9844
+ if (replacement.element.parentNode !== null) {
9845
+ throw new Error(`[renderCard] Bound replacement "${prepared.occurrenceKey}" is not detached`);
9846
+ }
9847
+ syncMountedBindings(replacement);
9848
+ if (current && replacement.element !== current.element) {
9849
+ transferSlotItemPresentation(current.element, replacement.element);
9850
+ }
9851
+ staged = true;
9852
+ }
9853
+ finally {
9854
+ if (!staged && !candidateReplacement) {
9855
+ disposeBoundSubtreeWithoutThrow(replacement);
9856
+ }
9857
+ }
9858
+ if (candidateReplacement) {
9859
+ replacements.delete(prepared.occurrenceKey);
9860
+ }
9861
+ return replacement;
9862
+ },
9863
+ dispose: disposeBoundSubtree,
9864
+ syncBindings: () => { },
9865
+ onUnreconciled: () => {
9866
+ unreconciled = true;
9867
+ },
9868
+ });
9869
+ if (!unreconciled
9870
+ && (next.element.parentNode !== container
9871
+ || container.firstElementChild !== next.element)) {
9872
+ unreconciled = true;
9873
+ }
9874
+ if (!unreconciled) {
9875
+ try {
9876
+ syncMountedBindings(next);
9877
+ }
9878
+ catch {
9879
+ unreconciled = true;
9880
+ }
9881
+ }
9882
+ if (unreconciled) {
9883
+ let emergency;
9884
+ try {
9885
+ emergency = mountBoundSubtree(candidate.materialized.root, candidate.preparedRoot.occurrenceKey, renderVariables);
9886
+ }
9887
+ catch (error) {
9888
+ disposeBoundBranchesOutsideContainer(next);
9889
+ throw error;
9890
+ }
9891
+ let adopted = false;
9892
+ try {
9893
+ syncMountedBindings(emergency);
9894
+ replaceContainerRoot(emergency.element, '[renderCard] Bound emergency root was not adopted');
9895
+ adopted = true;
9896
+ }
9897
+ finally {
9898
+ if (!adopted) {
9899
+ disposeBoundSubtreeWithoutThrow(emergency);
9900
+ disposeBoundBranchesOutsideContainer(next);
9901
+ }
9902
+ }
9903
+ disposeBoundSubtreeWithoutThrow(next);
9904
+ try {
9905
+ restoreChangedSubtreeState(previous, emergency, scrollPositions, mediaStates);
9906
+ }
9907
+ catch {
9908
+ // Presentation restoration cannot roll back an adopted next root.
9909
+ }
9910
+ restoreDisabledDescendantPresentation(emergency);
9911
+ return emergency;
9912
+ }
9913
+ try {
9914
+ restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates);
9915
+ }
9916
+ catch {
9917
+ // Presentation restoration does not affect published data ownership.
9918
+ }
9919
+ restoreDisabledDescendantPresentation(next);
9920
+ return next;
9921
+ }
9922
+ finally {
9923
+ disposeCandidate(candidate);
9924
+ }
8025
9925
  }
8026
9926
  function assertCurrentRevision(baseRevision) {
8027
- if (disposed || revision !== baseRevision) {
9927
+ if (disposed || publishing || revision !== baseRevision) {
8028
9928
  const error = new Error('[renderCard] BOUND_TRANSACTION_CONFLICT');
8029
9929
  error.code = 'BOUND_TRANSACTION_CONFLICT';
8030
9930
  throw error;
8031
9931
  }
8032
9932
  }
8033
- function commitDraft(draft, candidate, baseRevision) {
9933
+ function commitDraft(before, draft, baseRevision) {
8034
9934
  assertCurrentRevision(baseRevision);
8035
- replaceRootContents(variables, draft);
8036
- // Re-materialize against the stable public variables root. The detached
8037
- // DOM already represents the exact same validated JSON data.
8038
- candidate.materialized = materializeCard(schema, variables);
8039
- publishDOM(candidate, true);
9935
+ const validationRoot = cloneJsonData(variables);
9936
+ replaceRootContents(validationRoot, draft);
9937
+ const candidate = prepareIncrementalCandidate(before, draft);
9938
+ try {
9939
+ assertCurrentRevision(baseRevision);
9940
+ }
9941
+ catch (error) {
9942
+ disposeCandidate(candidate);
9943
+ throw error;
9944
+ }
9945
+ publishing = true;
9946
+ let publishingFailed = false;
9947
+ let publishingError;
9948
+ let deferredDisposeFailed = false;
9949
+ let deferredDisposeError;
9950
+ try {
9951
+ const nextMounted = commitIncrementalCandidate(candidate, draft);
9952
+ replaceRootContents(variables, draft);
9953
+ currentMaterialized = materializeCard(schema, variables);
9954
+ currentMountedTree = nextMounted;
9955
+ lastPublishedVariables = cloneJsonData(variables);
9956
+ revision += 1;
9957
+ }
9958
+ catch (error) {
9959
+ publishingFailed = true;
9960
+ publishingError = error;
9961
+ }
9962
+ finally {
9963
+ publishing = false;
9964
+ if (disposeRequested) {
9965
+ try {
9966
+ disposeNow();
9967
+ }
9968
+ catch (error) {
9969
+ deferredDisposeFailed = true;
9970
+ deferredDisposeError = error;
9971
+ }
9972
+ }
9973
+ }
9974
+ if (publishingFailed) {
9975
+ if (deferredDisposeFailed) {
9976
+ console.error('[renderCard] Bound deferred dispose failed', deferredDisposeError);
9977
+ }
9978
+ throw publishingError;
9979
+ }
9980
+ if (deferredDisposeFailed)
9981
+ throw deferredDisposeError;
9982
+ if (disposed)
9983
+ return;
9984
+ reconcileLifecycles(selectLifecycleNodes(currentMaterialized, collectMountedLifecycleIds(currentMountedTree)));
9985
+ applyAutoFocus(candidate);
8040
9986
  }
8041
9987
  function writeDraftVariable(draft, key, value) {
8042
9988
  Object.defineProperty(draft, String(key), {
@@ -8093,11 +10039,12 @@ function renderBoundCard(container, schema, options) {
8093
10039
  inflightRequests,
8094
10040
  };
8095
10041
  }
8096
- async function runBoundEvent(runtimeId, eventName, eventDetail) {
10042
+ async function runBoundEvent(runtimeId, eventName, eventDetail, baseRevision, repeatTargetFingerprint) {
8097
10043
  if (disposed)
8098
10044
  return;
8099
- const baseRevision = revision;
8100
- const draft = cloneJsonData(variables);
10045
+ assertCurrentRevision(baseRevision);
10046
+ const transactionBefore = cloneJsonData(variables);
10047
+ const draft = cloneJsonData(transactionBefore);
8101
10048
  if (eventDetail !== undefined) {
8102
10049
  writeDraftVariable(draft, '_event', cloneJsonData(eventDetail));
8103
10050
  }
@@ -8105,8 +10052,14 @@ function renderBoundCard(container, schema, options) {
8105
10052
  const freshMaterialized = materializeCard(schema, draft);
8106
10053
  const freshNode = indexNodes(freshMaterialized.root).get(runtimeId);
8107
10054
  if (!freshNode) {
10055
+ if (repeatTargetFingerprint !== undefined)
10056
+ return;
8108
10057
  throw new Error(`[renderCard] Bound runtime node "${runtimeId}" no longer exists`);
8109
10058
  }
10059
+ if (repeatTargetFingerprint !== undefined
10060
+ && getRepeatTargetFingerprint(freshNode) !== repeatTargetFingerprint) {
10061
+ return;
10062
+ }
8110
10063
  const eventValue = freshNode.events?.[eventName];
8111
10064
  const steps = eventValue
8112
10065
  ? resolveActionRef(eventValue, schemaActions)
@@ -8117,14 +10070,19 @@ function renderBoundCard(container, schema, options) {
8117
10070
  assertCurrentRevision(baseRevision);
8118
10071
  if (!hasVariableChanges(actionBaseline, draft))
8119
10072
  return;
8120
- const candidate = prepareCandidate(draft);
8121
- commitDraft(draft, candidate, baseRevision);
10073
+ // The target check above may have cached identity counts before action
10074
+ // steps changed the draft. Recount once when binding the published tree.
10075
+ repeatIdentityCountCache.delete(draft);
10076
+ commitDraft(cloneJsonData(lastPublishedVariables), draft, baseRevision);
8122
10077
  }
8123
- function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
10078
+ function enqueueBoundEvent(runtimeId, eventName, eventDetail, repeatTargetFingerprint) {
8124
10079
  if (disposed)
8125
10080
  return;
10081
+ const enqueueRevision = revision;
10082
+ const queuedBehindAnotherAction = queuedActionCount > 0;
10083
+ queuedActionCount += 1;
8126
10084
  actionQueue = actionQueue
8127
- .then(() => runBoundEvent(runtimeId, eventName, eventDetail))
10085
+ .then(() => runBoundEvent(runtimeId, eventName, eventDetail, queuedBehindAnotherAction ? revision : enqueueRevision, repeatTargetFingerprint))
8128
10086
  .catch((error) => {
8129
10087
  if (error
8130
10088
  && typeof error === 'object'
@@ -8133,44 +10091,87 @@ function renderBoundCard(container, schema, options) {
8133
10091
  return;
8134
10092
  }
8135
10093
  console.error('[renderCard] Bound action failed', error);
10094
+ })
10095
+ .finally(() => {
10096
+ queuedActionCount -= 1;
8136
10097
  });
8137
10098
  }
8138
10099
  function updateVariables(newVariables) {
8139
10100
  if (disposed)
8140
10101
  return;
8141
10102
  const baseRevision = revision;
10103
+ const before = cloneJsonData(lastPublishedVariables);
8142
10104
  const draft = cloneJsonData(variables);
8143
10105
  const patch = cloneJsonData(newVariables);
8144
10106
  for (const key of Object.keys(patch)) {
8145
10107
  writeDraftVariable(draft, key, patch[key]);
8146
10108
  }
8147
- const candidate = prepareCandidate(draft);
8148
- commitDraft(draft, candidate, baseRevision);
10109
+ commitDraft(before, draft, baseRevision);
8149
10110
  }
8150
- const initialCandidate = prepareCandidate(variables);
8151
- currentMaterialized = initialCandidate.materialized;
8152
- publishDOM(initialCandidate, false);
8153
- return {
8154
- dispose() {
8155
- if (disposed)
8156
- return;
8157
- disposed = true;
8158
- abortController.abort();
10111
+ function disposeNow() {
10112
+ if (disposed)
10113
+ return;
10114
+ disposed = true;
10115
+ disposeRequested = false;
10116
+ nodeAccess.dispose();
10117
+ abortController.abort();
10118
+ if (currentMountedTree) {
10119
+ disposeBoundSubtree(currentMountedTree);
10120
+ currentMountedTree = null;
10121
+ }
10122
+ else {
8159
10123
  disposeChartsIn(container);
8160
- container.replaceChildren();
8161
- const lifecycleNodes = [...activeLifecycleNodes.entries()];
8162
- activeLifecycleNodes.clear();
8163
- lifecycleQueue = lifecycleQueue
8164
- .then(async () => {
8165
- for (const [id, node] of lifecycleNodes) {
8166
- await lifecycleManager.destroy(id, createLifecycleActionContext(node));
10124
+ }
10125
+ container.replaceChildren();
10126
+ const lifecycleNodes = [...activeLifecycleNodes.entries()];
10127
+ activeLifecycleNodes.clear();
10128
+ lifecycleQueue = lifecycleQueue
10129
+ .then(async () => {
10130
+ for (const [id, node] of lifecycleNodes) {
10131
+ const session = createLifecycleActionSession(node);
10132
+ try {
10133
+ await lifecycleManager.destroy(id, session.context);
10134
+ session.publishDirectMutations();
10135
+ }
10136
+ catch (error) {
10137
+ console.error('[renderCard] Bound lifecycle dispose failed', error);
10138
+ }
10139
+ finally {
8167
10140
  lifecycleManager.unregister(id);
8168
10141
  }
10142
+ }
10143
+ try {
8169
10144
  await lifecycleManager.dispose(createPassiveActionContext(variables));
8170
- })
8171
- .catch((error) => {
10145
+ }
10146
+ catch (error) {
8172
10147
  console.error('[renderCard] Bound lifecycle dispose failed', error);
8173
- });
10148
+ }
10149
+ })
10150
+ .catch((error) => {
10151
+ console.error('[renderCard] Bound lifecycle dispose failed', error);
10152
+ });
10153
+ }
10154
+ const initialCandidate = prepareIncrementalCandidate(variables, variables);
10155
+ publishing = true;
10156
+ try {
10157
+ currentMountedTree = commitIncrementalCandidate(initialCandidate, variables);
10158
+ currentMaterialized = materializeCard(schema, variables);
10159
+ }
10160
+ finally {
10161
+ publishing = false;
10162
+ }
10163
+ reconcileLifecycles(selectLifecycleNodes(currentMaterialized, collectMountedLifecycleIds(currentMountedTree)));
10164
+ return {
10165
+ getNode: nodeAccess.getNode,
10166
+ onFocusChange: nodeAccess.onFocusChange,
10167
+ dispose() {
10168
+ if (disposed || disposeRequested)
10169
+ return;
10170
+ if (publishing) {
10171
+ disposeRequested = true;
10172
+ return;
10173
+ }
10174
+ disposeNow();
8174
10175
  },
8175
10176
  updateVariables,
8176
10177
  };
@@ -8188,7 +10189,11 @@ function captureScrollPositions$1(root) {
8188
10189
  function restoreScrollPositions$1(root, positions) {
8189
10190
  if (positions.size === 0)
8190
10191
  return;
8191
- root.querySelectorAll('[data-scroll-id]').forEach((element) => {
10192
+ const scrollers = [];
10193
+ if (root.matches('[data-scroll-id]'))
10194
+ scrollers.push(root);
10195
+ scrollers.push(...root.querySelectorAll('[data-scroll-id]'));
10196
+ scrollers.forEach((element) => {
8192
10197
  const id = element.getAttribute('data-scroll-id');
8193
10198
  const saved = id ? positions.get(id) : undefined;
8194
10199
  if (saved == null)
@@ -8217,7 +10222,11 @@ function captureMediaStates$1(root) {
8217
10222
  function restoreMediaStates$1(root, states) {
8218
10223
  if (states.size === 0)
8219
10224
  return;
8220
- root.querySelectorAll('ai-card-audio, ai-card-video').forEach((element) => {
10225
+ const mediaHosts = [];
10226
+ if (root.matches('ai-card-audio, ai-card-video'))
10227
+ mediaHosts.push(root);
10228
+ mediaHosts.push(...root.querySelectorAll('ai-card-audio, ai-card-video'));
10229
+ mediaHosts.forEach((element) => {
8221
10230
  const id = element.getAttribute('data-card-id');
8222
10231
  const snapshot = id ? states.get(id) : undefined;
8223
10232
  if (!snapshot)
@@ -8279,22 +10288,72 @@ function renderCard(container, schemaInput, options = {}) {
8279
10288
  return renderBoundCard(container, schema, options);
8280
10289
  }
8281
10290
  function renderStaticCard(container, schema, options) {
10291
+ const nodeAccess = createCardNodeAccess(container);
8282
10292
  // 2. Parse into render tree
8283
10293
  const tree = parseSchema(schema);
8284
10294
  // 3. Reactive variables store (mutable copy, merged with external variables)
8285
10295
  let variables = { ...schema.variables, ...options.variables };
8286
10296
  let disposed = false;
10297
+ let publishing = false;
10298
+ let disposeRequested = false;
8287
10299
  let hostAuthorityEpoch = 0;
10300
+ let nextStaticEventSequence = 0;
10301
+ const latestStaticEventWriterByKey = new Map();
8288
10302
  const lifecycleRecords = new Map();
8289
10303
  const abortController = new AbortController();
8290
10304
  // Per-instance request dedup map (isolated from other cards on the page).
8291
10305
  const inflightRequests = new Map();
8292
- function buildActionContext() {
10306
+ function isVisible(node, renderVariables, parentVisible) {
10307
+ if (!parentVisible)
10308
+ return false;
10309
+ if (!node.directives?.visible)
10310
+ return true;
10311
+ const visible = node.directives.visible;
10312
+ const resolved = hasExpression(visible)
10313
+ ? resolveExpression(visible, renderVariables)
10314
+ : visible;
10315
+ return !(resolved === false
10316
+ || resolved === 'false'
10317
+ || resolved === ''
10318
+ || resolved === 0);
10319
+ }
10320
+ function collectAutoFocusRevealIds(previousVariables, nextVariables) {
10321
+ const requested = new Set();
10322
+ const visit = (node, previousParentVisible, nextParentVisible) => {
10323
+ const wasVisible = isVisible(node, previousVariables, previousParentVisible);
10324
+ const nowVisible = isVisible(node, nextVariables, nextParentVisible);
10325
+ if (node.type === 'Input'
10326
+ && !wasVisible
10327
+ && nowVisible
10328
+ && resolveDeep(node.props, nextVariables).autoFocus === true) {
10329
+ requested.add(node.id);
10330
+ }
10331
+ node.children.forEach(child => visit(child, wasVisible, nowVisible));
10332
+ };
10333
+ visit(tree, true, true);
10334
+ return requested;
10335
+ }
10336
+ function applyAutoFocus(requested) {
10337
+ if (requested.size === 0)
10338
+ return;
10339
+ for (const input of container.querySelectorAll(CardInput.is)) {
10340
+ const id = input.getAttribute('data-card-id');
10341
+ if (id && requested.has(id) && input.requestAutoFocus())
10342
+ break;
10343
+ }
10344
+ }
10345
+ function buildActionContext(pendingAutoFocusIds, initialVariables = variables, eventSequence) {
8293
10346
  const baseAuthorityEpoch = hostAuthorityEpoch;
8294
10347
  let context;
8295
10348
  const writeVariable = (key, value, { silent }) => {
8296
10349
  let actionVariables = context.variables;
8297
- if (disposed || baseAuthorityEpoch !== hostAuthorityEpoch) {
10350
+ const latestWriter = latestStaticEventWriterByKey.get(key);
10351
+ if (disposed
10352
+ || publishing
10353
+ || baseAuthorityEpoch !== hostAuthorityEpoch
10354
+ || (eventSequence !== undefined
10355
+ && latestWriter !== undefined
10356
+ && latestWriter > eventSequence)) {
8298
10357
  if (actionVariables === variables) {
8299
10358
  actionVariables = { ...actionVariables };
8300
10359
  context.variables = actionVariables;
@@ -8302,9 +10361,50 @@ function renderStaticCard(container, schema, options) {
8302
10361
  actionVariables[key] = value;
8303
10362
  return;
8304
10363
  }
10364
+ const previousVariables = !silent && pendingAutoFocusIds
10365
+ ? { ...actionVariables }
10366
+ : null;
10367
+ const liveVariables = variables;
10368
+ const hadLiveValue = Object.prototype.hasOwnProperty.call(liveVariables, key);
10369
+ const previousLiveValue = liveVariables[key];
8305
10370
  actionVariables[key] = value;
8306
- if (!silent)
10371
+ if (silent)
10372
+ return;
10373
+ // Event actions use a private snapshot so a later event cannot replace
10374
+ // their `_event` detail while they await. Publish only the key written by
10375
+ // this step, preserving concurrent actions that update different keys.
10376
+ if (actionVariables !== variables) {
10377
+ variables[key] = value;
10378
+ }
10379
+ if (previousVariables && pendingAutoFocusIds) {
10380
+ for (const id of collectAutoFocusRevealIds(previousVariables, actionVariables)) {
10381
+ pendingAutoFocusIds.add(id);
10382
+ }
10383
+ }
10384
+ try {
8307
10385
  rerender();
10386
+ }
10387
+ catch (error) {
10388
+ if (variables === liveVariables) {
10389
+ if (hadLiveValue)
10390
+ liveVariables[key] = previousLiveValue;
10391
+ else
10392
+ Reflect.deleteProperty(liveVariables, key);
10393
+ if (eventSequence !== undefined
10394
+ && latestStaticEventWriterByKey.get(key) === eventSequence) {
10395
+ if (latestWriter === undefined) {
10396
+ latestStaticEventWriterByKey.delete(key);
10397
+ }
10398
+ else {
10399
+ latestStaticEventWriterByKey.set(key, latestWriter);
10400
+ }
10401
+ }
10402
+ pendingAutoFocusIds?.clear();
10403
+ if (!disposed)
10404
+ actionContext = buildActionContext();
10405
+ }
10406
+ throw error;
10407
+ }
8308
10408
  };
8309
10409
  context = {
8310
10410
  ...createWebActionContext({
@@ -8314,7 +10414,7 @@ function renderStaticCard(container, schema, options) {
8314
10414
  },
8315
10415
  abortSignal: abortController.signal,
8316
10416
  }),
8317
- variables,
10417
+ variables: initialVariables,
8318
10418
  variableWriter: writeVariable,
8319
10419
  botId: options.botId,
8320
10420
  inflightRequests,
@@ -8359,8 +10459,431 @@ function renderStaticCard(container, schema, options) {
8359
10459
  }
8360
10460
  });
8361
10461
  }
8362
- // 7. Render function
10462
+ let mountedTree = null;
8363
10463
  let renderGeneration = 0;
10464
+ function childOccurrenceKey(parentKey, id, sameIdOrdinal) {
10465
+ return `${parentKey}/${encodeURIComponent(id)}#${sameIdOrdinal}`;
10466
+ }
10467
+ function resolveNodeProps(node) {
10468
+ const resolvedProps = resolveDeep(node.props, variables);
10469
+ if (resolvedProps.content
10470
+ && typeof resolvedProps.content === 'object'
10471
+ && 'type' in resolvedProps.content) {
10472
+ resolvedProps.content = resolveExpressionValue(resolvedProps.content, variables);
10473
+ }
10474
+ return resolvedProps;
10475
+ }
10476
+ function isNodeDisabled(node) {
10477
+ if (!node.directives?.disabled)
10478
+ return false;
10479
+ const disabled = node.directives.disabled;
10480
+ const resolved = hasExpression(disabled)
10481
+ ? resolveExpression(disabled, variables)
10482
+ : disabled;
10483
+ return resolved === true || resolved === 'true' || resolved === 1;
10484
+ }
10485
+ function nodeBindingFingerprint(node, resolvedProps, disabled) {
10486
+ const binding = { disabled };
10487
+ if (resolvedProps && resolvedProps.variableKey !== undefined) {
10488
+ binding.variableKey = resolvedProps.variableKey;
10489
+ }
10490
+ if (!disabled && node.events !== undefined) {
10491
+ binding.events = node.events;
10492
+ }
10493
+ return stableFingerprint(binding);
10494
+ }
10495
+ function resolveStaticMetadata(node, occurrenceKey) {
10496
+ const visible = isVisible(node, variables, true);
10497
+ const rendererToken = componentRenderers[node.type]
10498
+ ?? componentRenderers._default;
10499
+ if (!visible) {
10500
+ return {
10501
+ occurrenceKey,
10502
+ node,
10503
+ nodeId: node.id,
10504
+ nodeType: node.type,
10505
+ rendererToken,
10506
+ visible: false,
10507
+ disabled: false,
10508
+ resolvedProps: null,
10509
+ propsFingerprint: stableFingerprint(null),
10510
+ layoutFingerprint: stableFingerprint(null),
10511
+ bindingFingerprint: stableFingerprint(null),
10512
+ };
10513
+ }
10514
+ const resolvedProps = resolveNodeProps(node);
10515
+ const disabled = isNodeDisabled(node);
10516
+ return {
10517
+ occurrenceKey,
10518
+ node,
10519
+ nodeId: node.id,
10520
+ nodeType: node.type,
10521
+ rendererToken,
10522
+ visible: true,
10523
+ disabled,
10524
+ resolvedProps,
10525
+ propsFingerprint: stableFingerprint(resolvedProps),
10526
+ layoutFingerprint: stableFingerprint(getSlotLayoutFingerprintInput(resolvedProps, node.children, resolveNodeProps)),
10527
+ bindingFingerprint: nodeBindingFingerprint(node, resolvedProps, disabled),
10528
+ };
10529
+ }
10530
+ function applyDisabledState(element, disabled) {
10531
+ if (!disabled)
10532
+ return;
10533
+ element.setAttribute('data-disabled', 'true');
10534
+ element.style.background = '#F5F5F5';
10535
+ element.style.color = '#C0C0C0';
10536
+ element.style.setProperty('--card-disabled-color', '#C0C0C0');
10537
+ element.style.pointerEvents = 'none';
10538
+ element.style.cursor = 'default';
10539
+ }
10540
+ function applyDisabledDescendants(element, disabled) {
10541
+ if (!disabled)
10542
+ return;
10543
+ element.querySelectorAll('*').forEach((child) => {
10544
+ const htmlChild = child;
10545
+ htmlChild.setAttribute('data-disabled', 'true');
10546
+ htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
10547
+ });
10548
+ }
10549
+ function runStaticEventActions(steps, detail) {
10550
+ if (disposed)
10551
+ return;
10552
+ if (detail != null)
10553
+ variables._event = detail;
10554
+ const sourceVariables = variables;
10555
+ const actionAuthorityEpoch = hostAuthorityEpoch;
10556
+ const eventSequence = ++nextStaticEventSequence;
10557
+ const localVariables = Object.create(null);
10558
+ if (Object.prototype.hasOwnProperty.call(sourceVariables, '_event')) {
10559
+ localVariables._event = sourceVariables._event;
10560
+ }
10561
+ const deletedProperties = new Set();
10562
+ let frozenVariables = null;
10563
+ const eventVariables = new Proxy(localVariables, {
10564
+ get(target, property) {
10565
+ if (frozenVariables)
10566
+ return Reflect.get(frozenVariables, property);
10567
+ if (deletedProperties.has(property))
10568
+ return undefined;
10569
+ return Object.prototype.hasOwnProperty.call(target, property)
10570
+ ? Reflect.get(target, property)
10571
+ : Reflect.get(sourceVariables, property);
10572
+ },
10573
+ set(target, property, value) {
10574
+ if (frozenVariables) {
10575
+ Reflect.set(frozenVariables, property, value);
10576
+ return true;
10577
+ }
10578
+ deletedProperties.delete(property);
10579
+ Reflect.set(target, property, value);
10580
+ if (property !== '_event'
10581
+ && !disposed
10582
+ && !publishing
10583
+ && variables === sourceVariables
10584
+ && actionAuthorityEpoch === hostAuthorityEpoch
10585
+ && (latestStaticEventWriterByKey.get(property) === undefined
10586
+ || latestStaticEventWriterByKey.get(property) <= eventSequence)) {
10587
+ latestStaticEventWriterByKey.set(property, eventSequence);
10588
+ Reflect.set(sourceVariables, property, value);
10589
+ }
10590
+ return true;
10591
+ },
10592
+ deleteProperty(target, property) {
10593
+ if (frozenVariables) {
10594
+ return Reflect.deleteProperty(frozenVariables, property);
10595
+ }
10596
+ deletedProperties.add(property);
10597
+ Reflect.deleteProperty(target, property);
10598
+ if (property !== '_event'
10599
+ && !disposed
10600
+ && !publishing
10601
+ && variables === sourceVariables
10602
+ && actionAuthorityEpoch === hostAuthorityEpoch
10603
+ && (latestStaticEventWriterByKey.get(property) === undefined
10604
+ || latestStaticEventWriterByKey.get(property) <= eventSequence)) {
10605
+ latestStaticEventWriterByKey.set(property, eventSequence);
10606
+ Reflect.deleteProperty(sourceVariables, property);
10607
+ }
10608
+ return true;
10609
+ },
10610
+ has(target, property) {
10611
+ if (!frozenVariables && deletedProperties.has(property))
10612
+ return false;
10613
+ return frozenVariables
10614
+ ? property in frozenVariables
10615
+ : property in target || property in sourceVariables;
10616
+ },
10617
+ ownKeys(target) {
10618
+ return Reflect.ownKeys(frozenVariables ?? { ...sourceVariables, ...target }).filter(property => !deletedProperties.has(property));
10619
+ },
10620
+ getOwnPropertyDescriptor(target, property) {
10621
+ if (!frozenVariables && deletedProperties.has(property)) {
10622
+ return undefined;
10623
+ }
10624
+ const owner = frozenVariables
10625
+ ?? (Object.prototype.hasOwnProperty.call(target, property)
10626
+ ? target
10627
+ : sourceVariables);
10628
+ const descriptor = Object.getOwnPropertyDescriptor(owner, property);
10629
+ return descriptor ? { ...descriptor, configurable: true } : undefined;
10630
+ },
10631
+ });
10632
+ const pendingAutoFocusIds = new Set();
10633
+ void runActionSteps(steps, buildActionContext(pendingAutoFocusIds, eventVariables, eventSequence))
10634
+ .catch((error) => {
10635
+ console.error('[renderCard] Static action failed', error);
10636
+ })
10637
+ .finally(() => {
10638
+ const snapshot = {
10639
+ ...sourceVariables,
10640
+ ...localVariables,
10641
+ };
10642
+ for (const property of deletedProperties) {
10643
+ Reflect.deleteProperty(snapshot, property);
10644
+ }
10645
+ frozenVariables = snapshot;
10646
+ if (disposed || actionAuthorityEpoch !== hostAuthorityEpoch)
10647
+ return;
10648
+ applyAutoFocus(pendingAutoFocusIds);
10649
+ });
10650
+ }
10651
+ function syncStaticBindings(element, node) {
10652
+ const resolvedEventBindings = [];
10653
+ if (node.visible && !node.disabled && node.node.events) {
10654
+ for (const [schemaEvent, eventValue] of Object.entries(node.node.events)) {
10655
+ if (!eventValue)
10656
+ continue;
10657
+ const steps = resolveActionRef(eventValue, schemaActions);
10658
+ if (!steps)
10659
+ continue;
10660
+ const domEvent = eventMap[schemaEvent] ?? schemaEvent;
10661
+ resolvedEventBindings.push({
10662
+ schemaEvent,
10663
+ domEvent,
10664
+ ownsValueEvent: ((domEvent === 'input' || domEvent === 'change')
10665
+ && VALUE_CONTROL_TYPES.has(node.nodeType)),
10666
+ dispatch: (event) => {
10667
+ const detail = event instanceof CustomEvent
10668
+ ? event.detail
10669
+ : undefined;
10670
+ runStaticEventActions(steps, detail);
10671
+ },
10672
+ });
10673
+ }
10674
+ }
10675
+ const variableKey = node.resolvedProps?.variableKey;
10676
+ syncElementBindings(element, {
10677
+ variableKey: typeof variableKey === 'string' ? variableKey : undefined,
10678
+ writeVariable: (key, value) => {
10679
+ if (disposed
10680
+ || publishing
10681
+ || !container.contains(element)) {
10682
+ return;
10683
+ }
10684
+ variables[key] = value;
10685
+ hostAuthorityEpoch += 1;
10686
+ },
10687
+ events: resolvedEventBindings,
10688
+ isActive: () => (!disposed
10689
+ && !publishing
10690
+ && container.contains(element)),
10691
+ });
10692
+ }
10693
+ function mountStaticSubtree(node, occurrenceKey) {
10694
+ const metadata = resolveStaticMetadata(node, occurrenceKey);
10695
+ if (!metadata.visible) {
10696
+ const placeholder = document.createElement('div');
10697
+ placeholder.style.display = 'none';
10698
+ placeholder.setAttribute('data-card-id', node.id);
10699
+ syncStaticBindings(placeholder, { ...metadata});
10700
+ return { ...metadata, element: placeholder, children: [] };
10701
+ }
10702
+ const renderer = metadata.rendererToken;
10703
+ const element = renderer(node, metadata.resolvedProps, isMobile, options.responsive);
10704
+ try {
10705
+ applyDisabledState(element, metadata.disabled);
10706
+ syncStaticBindings(element, { ...metadata, children: [] });
10707
+ const children = [];
10708
+ const sameIdOrdinals = new Map();
10709
+ const renderChild = (child) => {
10710
+ const sameIdOrdinal = sameIdOrdinals.get(child.id) ?? 0;
10711
+ sameIdOrdinals.set(child.id, sameIdOrdinal + 1);
10712
+ const mounted = mountStaticSubtree(child, childOccurrenceKey(occurrenceKey, child.id, sameIdOrdinal));
10713
+ children.push(mounted);
10714
+ return mounted.element;
10715
+ };
10716
+ const childrenMap = {};
10717
+ for (const child of node.children)
10718
+ childrenMap[child.id] = child;
10719
+ const layoutApplied = renderSlotLayout(element, node.children, metadata.resolvedProps, renderChild, childrenMap, actionContext, resolveNodeProps);
10720
+ if (!layoutApplied) {
10721
+ for (const child of node.children) {
10722
+ element.appendChild(renderChild(child));
10723
+ }
10724
+ }
10725
+ applyDisabledDescendants(element, metadata.disabled);
10726
+ applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
10727
+ return { ...metadata, element, children };
10728
+ }
10729
+ catch (error) {
10730
+ disposeStaticElement(element);
10731
+ throw error;
10732
+ }
10733
+ }
10734
+ function prepareStaticNode(current) {
10735
+ const metadata = resolveStaticMetadata(current.node, current.occurrenceKey);
10736
+ if (current.visible !== metadata.visible
10737
+ || current.layoutFingerprint !== metadata.layoutFingerprint) {
10738
+ return { ...metadata, children: [] };
10739
+ }
10740
+ return {
10741
+ ...metadata,
10742
+ children: current.children.map(prepareStaticNode),
10743
+ };
10744
+ }
10745
+ function mountStaticReplacement(current, next) {
10746
+ const replacement = mountStaticSubtree(next.node, next.occurrenceKey);
10747
+ try {
10748
+ if (replacement.element !== current.element
10749
+ && replacement.element.parentNode === null) {
10750
+ transferSlotItemPresentation(current.element, replacement.element);
10751
+ }
10752
+ return replacement;
10753
+ }
10754
+ catch (error) {
10755
+ disposeStaticSubtree(replacement);
10756
+ throw error;
10757
+ }
10758
+ }
10759
+ /**
10760
+ * Build every renderer-owned replacement before touching the mounted DOM.
10761
+ * This preserves the old committed card when a later business renderer
10762
+ * rejects the same host update. Retained BaseElements are intentionally not
10763
+ * instantiated here: their native controls must stay untouched unless the
10764
+ * live update actually needs its existing replacement fallback.
10765
+ */
10766
+ function stageStaticReplacements(current, next, staged) {
10767
+ const sameChildren = current.children.length === next.children.length
10768
+ && current.children.every((child, index) => (child.occurrenceKey === next.children[index].occurrenceKey));
10769
+ const decision = sameChildren
10770
+ ? decideStaticUpdate(current, next)
10771
+ : 'replace';
10772
+ if (decision === 'replace') {
10773
+ staged.set(next.occurrenceKey, mountStaticReplacement(current, next));
10774
+ }
10775
+ if (decision === 'replace')
10776
+ return;
10777
+ current.children.forEach((child, index) => {
10778
+ stageStaticReplacements(child, next.children[index], staged);
10779
+ });
10780
+ }
10781
+ function decideStaticUpdate(current, next) {
10782
+ if (current.occurrenceKey !== next.occurrenceKey
10783
+ || current.nodeId !== next.nodeId
10784
+ || current.nodeType !== next.nodeType
10785
+ || current.rendererToken !== next.rendererToken
10786
+ || current.visible !== next.visible
10787
+ || current.disabled !== next.disabled
10788
+ || current.layoutFingerprint !== next.layoutFingerprint) {
10789
+ return 'replace';
10790
+ }
10791
+ if (current.propsFingerprint === next.propsFingerprint
10792
+ && current.bindingFingerprint === next.bindingFingerprint) {
10793
+ return 'retain';
10794
+ }
10795
+ if (current.element instanceof BaseElement
10796
+ || current.rendererToken === componentRenderers._default) {
10797
+ return 'update';
10798
+ }
10799
+ return 'replace';
10800
+ }
10801
+ function updateStaticNode(current, next) {
10802
+ const element = current.element;
10803
+ if (element instanceof BaseElement) {
10804
+ updateSlotItemShellPresentation(element, () => {
10805
+ element.updateProps(next.resolvedProps, isMobile, options.responsive);
10806
+ applyResponsiveStyles(element, createResponsiveContext(isMobile, options.responsive));
10807
+ }, getSlotHostPresentationOwnership(next.resolvedProps));
10808
+ return;
10809
+ }
10810
+ const previous = componentRenderers._default(current.node, current.resolvedProps, isMobile, options.responsive);
10811
+ const desired = componentRenderers._default(next.node, next.resolvedProps, isMobile, options.responsive);
10812
+ applyDisabledState(previous, current.disabled);
10813
+ applyDisabledState(desired, next.disabled);
10814
+ const hostOwnership = getSlotHostPresentationOwnership(next.resolvedProps);
10815
+ updateSlotItemShellPresentation(current.element, () => {
10816
+ patchElementShellPresentation(current.element, previous, desired, hostOwnership, getSlotItemPresentationOwnership(current.element));
10817
+ }, hostOwnership);
10818
+ }
10819
+ function disposeStaticElement(element) {
10820
+ const cleanupSteps = [
10821
+ () => clearSlotOwnerBehaviorsIn(element),
10822
+ () => clearSlotItemPresentationsIn(element),
10823
+ () => clearElementBindingsIn(element),
10824
+ () => disposeChartsIn(element),
10825
+ ];
10826
+ for (const cleanup of cleanupSteps) {
10827
+ try {
10828
+ cleanup();
10829
+ }
10830
+ catch (error) {
10831
+ console.error('[renderCard] Static element cleanup failed', error);
10832
+ }
10833
+ }
10834
+ }
10835
+ function disposeStaticSubtree(current) {
10836
+ disposeStaticElement(current.element);
10837
+ }
10838
+ function collectMountedOccurrences(current, occurrences) {
10839
+ occurrences.set(current.occurrenceKey, current);
10840
+ for (const child of current.children) {
10841
+ collectMountedOccurrences(child, occurrences);
10842
+ }
10843
+ }
10844
+ function collectMountedLifecycles(current, collection) {
10845
+ if (!current.visible
10846
+ || !container.contains(current.element)) {
10847
+ return;
10848
+ }
10849
+ if (current.node.lifecycle) {
10850
+ collection.push({
10851
+ id: current.nodeId,
10852
+ lifecycle: current.node.lifecycle,
10853
+ });
10854
+ }
10855
+ for (const child of current.children) {
10856
+ collectMountedLifecycles(child, collection);
10857
+ }
10858
+ }
10859
+ function restoreDisabledDescendantPresentation(current) {
10860
+ if (current.disabled) {
10861
+ applyDisabledState(current.element, true);
10862
+ applyDisabledDescendants(current.element, true);
10863
+ }
10864
+ for (const child of current.children) {
10865
+ restoreDisabledDescendantPresentation(child);
10866
+ }
10867
+ }
10868
+ function restoreChangedSubtreeState(previous, next, scrollPositions, mediaStates) {
10869
+ if (previous.element !== next.element) {
10870
+ restoreScrollPositions(next.element, scrollPositions);
10871
+ restoreMediaStates(next.element, mediaStates);
10872
+ return;
10873
+ }
10874
+ if (previous.propsFingerprint !== next.propsFingerprint
10875
+ && (next.nodeType === 'Audio' || next.nodeType === 'Video')) {
10876
+ restoreMediaStates(next.element, mediaStates);
10877
+ }
10878
+ for (let index = 0; index < next.children.length; index += 1) {
10879
+ const previousChild = previous.children[index];
10880
+ const nextChild = next.children[index];
10881
+ if (previousChild
10882
+ && previousChild.occurrenceKey === nextChild.occurrenceKey) {
10883
+ restoreChangedSubtreeState(previousChild, nextChild, scrollPositions, mediaStates);
10884
+ }
10885
+ }
10886
+ }
8364
10887
  function render() {
8365
10888
  if (disposed)
8366
10889
  return;
@@ -8368,28 +10891,85 @@ function renderStaticCard(container, schema, options) {
8368
10891
  const collectedLifecycles = [];
8369
10892
  const scrollPositions = captureScrollPositions(container);
8370
10893
  const mediaStates = captureMediaStates(container);
8371
- disposeChartsIn(container); // tear down old chart instances before clearing
8372
- const dom = renderNode(tree, variables, actionContext, isMobile, options.responsive, schemaActions, (key, value, sourceVariables) => {
8373
- if (disposed || sourceVariables !== variables)
10894
+ let previousTree = null;
10895
+ if (!mountedTree) {
10896
+ const initialTree = mountStaticSubtree(tree, `${tree.id}#0`);
10897
+ if (disposed || generation !== renderGeneration) {
10898
+ disposeStaticSubtree(initialTree);
8374
10899
  return;
8375
- variables[key] = value;
8376
- hostAuthorityEpoch += 1;
8377
- }, (steps, detail) => {
10900
+ }
10901
+ disposeChartsIn(container);
10902
+ container.replaceChildren(initialTree.element);
10903
+ mountedTree = initialTree;
10904
+ }
10905
+ else {
10906
+ previousTree = mountedTree;
10907
+ let unreconciled = false;
10908
+ let unreconciledError;
10909
+ const replacementSources = new Map();
10910
+ collectMountedOccurrences(mountedTree, replacementSources);
10911
+ const preparedTree = prepareStaticNode(mountedTree);
10912
+ if (disposed || generation !== renderGeneration)
10913
+ return;
10914
+ const stagedReplacements = new Map();
10915
+ publishing = true;
10916
+ try {
10917
+ stageStaticReplacements(mountedTree, preparedTree, stagedReplacements);
10918
+ if (disposeRequested)
10919
+ return;
10920
+ mountedTree = reconcileIncrementalNode(mountedTree, preparedTree, {
10921
+ decide: decideStaticUpdate,
10922
+ update: updateStaticNode,
10923
+ mountReplacement: next => {
10924
+ const staged = stagedReplacements.get(next.occurrenceKey);
10925
+ if (staged) {
10926
+ stagedReplacements.delete(next.occurrenceKey);
10927
+ return staged;
10928
+ }
10929
+ const replacement = mountStaticSubtree(next.node, next.occurrenceKey);
10930
+ const current = replacementSources.get(next.occurrenceKey);
10931
+ if (current
10932
+ && replacement.element !== current.element
10933
+ && replacement.element.parentNode === null) {
10934
+ transferSlotItemPresentation(current.element, replacement.element);
10935
+ }
10936
+ return replacement;
10937
+ },
10938
+ dispose: disposeStaticSubtree,
10939
+ syncBindings: (current, next) => {
10940
+ syncStaticBindings(current.element, next);
10941
+ },
10942
+ onUnreconciled: (error) => {
10943
+ if (unreconciled)
10944
+ return;
10945
+ unreconciled = true;
10946
+ unreconciledError = error ?? new Error('[renderCard] Static incremental update could not be reconciled');
10947
+ },
10948
+ });
10949
+ }
10950
+ finally {
10951
+ for (const replacement of stagedReplacements.values()) {
10952
+ disposeStaticSubtree(replacement);
10953
+ }
10954
+ stagedReplacements.clear();
10955
+ publishing = false;
10956
+ if (disposeRequested)
10957
+ disposeNow();
10958
+ }
10959
+ if (unreconciled)
10960
+ throw unreconciledError;
8378
10961
  if (disposed)
8379
10962
  return;
8380
- if (detail != null)
8381
- variables._event = detail;
8382
- void runActionSteps(steps, buildActionContext()).catch((error) => {
8383
- console.error('[renderCard] Static action failed', error);
8384
- });
8385
- }, (id, lifecycle) => {
8386
- collectedLifecycles.push({ id, lifecycle });
8387
- });
8388
- if (disposed || generation !== renderGeneration)
8389
- return;
8390
- container.replaceChildren(dom);
8391
- restoreScrollPositions(container, scrollPositions);
8392
- restoreMediaStates(container, mediaStates);
10963
+ }
10964
+ restoreDisabledDescendantPresentation(mountedTree);
10965
+ collectMountedLifecycles(mountedTree, collectedLifecycles);
10966
+ if (previousTree) {
10967
+ restoreChangedSubtreeState(previousTree, mountedTree, scrollPositions, mediaStates);
10968
+ }
10969
+ else {
10970
+ restoreScrollPositions(container, scrollPositions);
10971
+ restoreMediaStates(container, mediaStates);
10972
+ }
8393
10973
  if (disposed || generation !== renderGeneration)
8394
10974
  return;
8395
10975
  for (const { id, lifecycle } of collectedLifecycles) {
@@ -8404,27 +10984,63 @@ function renderStaticCard(container, schema, options) {
8404
10984
  actionContext = buildActionContext();
8405
10985
  render();
8406
10986
  }
10987
+ function disposeNow() {
10988
+ if (disposed)
10989
+ return;
10990
+ disposed = true;
10991
+ disposeRequested = false;
10992
+ hostAuthorityEpoch += 1;
10993
+ nodeAccess.dispose();
10994
+ abortController.abort();
10995
+ if (mountedTree) {
10996
+ disposeStaticSubtree(mountedTree);
10997
+ mountedTree = null;
10998
+ }
10999
+ else {
11000
+ disposeChartsIn(container);
11001
+ }
11002
+ container.replaceChildren();
11003
+ for (const record of lifecycleRecords.values())
11004
+ destroyLifecycle(record);
11005
+ }
8407
11006
  // 7. Initial render
8408
11007
  render();
8409
11008
  // 8. Return instance handle
8410
11009
  return {
11010
+ getNode: nodeAccess.getNode,
11011
+ onFocusChange: nodeAccess.onFocusChange,
8411
11012
  dispose() {
8412
- if (disposed)
11013
+ if (disposed || disposeRequested)
8413
11014
  return;
8414
- disposed = true;
8415
- hostAuthorityEpoch += 1;
8416
- abortController.abort();
8417
- disposeChartsIn(container);
8418
- container.replaceChildren();
8419
- for (const record of lifecycleRecords.values())
8420
- destroyLifecycle(record);
11015
+ if (publishing) {
11016
+ disposeRequested = true;
11017
+ return;
11018
+ }
11019
+ disposeNow();
8421
11020
  },
8422
11021
  updateVariables(newVars) {
8423
11022
  if (disposed)
8424
11023
  return;
11024
+ if (publishing) {
11025
+ const error = new Error('[renderCard] STATIC_TRANSACTION_CONFLICT');
11026
+ error.code = 'STATIC_TRANSACTION_CONFLICT';
11027
+ throw error;
11028
+ }
11029
+ const previousVariables = variables;
11030
+ const previousAuthorityEpoch = hostAuthorityEpoch;
8425
11031
  hostAuthorityEpoch += 1;
8426
11032
  variables = { ...variables, ...newVars };
8427
- rerender();
11033
+ try {
11034
+ rerender();
11035
+ }
11036
+ catch (error) {
11037
+ if (!disposed) {
11038
+ variables = previousVariables;
11039
+ hostAuthorityEpoch = previousAuthorityEpoch;
11040
+ actionContext = buildActionContext();
11041
+ }
11042
+ throw error;
11043
+ }
8428
11044
  },
8429
11045
  };
8430
11046
  }
@@ -8450,7 +11066,11 @@ function captureScrollPositions(root) {
8450
11066
  function restoreScrollPositions(root, positions) {
8451
11067
  if (positions.size === 0)
8452
11068
  return;
8453
- root.querySelectorAll('[data-scroll-id]').forEach((el) => {
11069
+ const scrollers = [];
11070
+ if (root.matches('[data-scroll-id]'))
11071
+ scrollers.push(root);
11072
+ scrollers.push(...root.querySelectorAll('[data-scroll-id]'));
11073
+ scrollers.forEach((el) => {
8454
11074
  const id = el.getAttribute('data-scroll-id');
8455
11075
  const saved = id ? positions.get(id) : undefined;
8456
11076
  if (saved == null)
@@ -8490,7 +11110,11 @@ function captureMediaStates(root) {
8490
11110
  function restoreMediaStates(root, states) {
8491
11111
  if (states.size === 0)
8492
11112
  return;
8493
- root.querySelectorAll('ai-card-audio, ai-card-video').forEach((el) => {
11113
+ const mediaHosts = [];
11114
+ if (root.matches('ai-card-audio, ai-card-video'))
11115
+ mediaHosts.push(root);
11116
+ mediaHosts.push(...root.querySelectorAll('ai-card-audio, ai-card-video'));
11117
+ mediaHosts.forEach((el) => {
8494
11118
  const id = el.getAttribute('data-card-id');
8495
11119
  const snap = id ? states.get(id) : undefined;
8496
11120
  if (!snap)
@@ -8499,121 +11123,6 @@ function restoreMediaStates(root, states) {
8499
11123
  restoreMedia(media, snap);
8500
11124
  });
8501
11125
  }
8502
- // ─── Recursive Node Renderer ─────────────────────────────────────
8503
- function renderNode(node, variables, actionContext, isMobile, responsive, schemaActions, writeInputVariable, runEventActions, collectLifecycle) {
8504
- // Check directives.visible
8505
- if (node.directives?.visible) {
8506
- const visibleExpr = node.directives.visible;
8507
- const resolved = hasExpression(visibleExpr)
8508
- ? resolveExpression(visibleExpr, variables)
8509
- : visibleExpr;
8510
- if (resolved === false || resolved === 'false' || resolved === '' || resolved === 0) {
8511
- // Hidden element — return empty placeholder
8512
- const placeholder = document.createElement('div');
8513
- placeholder.style.display = 'none';
8514
- placeholder.setAttribute('data-card-id', node.id);
8515
- return placeholder;
8516
- }
8517
- }
8518
- // Resolve props with variable expressions
8519
- const resolvedProps = resolveDeep(node.props, variables);
8520
- // Resolve content if it's an ExpressionValue
8521
- if (resolvedProps.content && typeof resolvedProps.content === 'object' && 'type' in resolvedProps.content) {
8522
- resolvedProps.content = resolveExpressionValue(resolvedProps.content, variables);
8523
- }
8524
- // Check directives.disabled
8525
- let isDisabled = false;
8526
- if (node.directives?.disabled) {
8527
- const disabledExpr = node.directives.disabled;
8528
- const resolved = hasExpression(disabledExpr)
8529
- ? resolveExpression(disabledExpr, variables)
8530
- : disabledExpr;
8531
- isDisabled = resolved === true || resolved === 'true' || resolved === 1;
8532
- }
8533
- // Lookup component renderer
8534
- const renderer = componentRenderers[node.type] ?? componentRenderers['_default'];
8535
- const el = renderer(node, resolvedProps, isMobile, responsive);
8536
- // Apply disabled styling & attribute
8537
- if (isDisabled) {
8538
- el.setAttribute('data-disabled', 'true');
8539
- el.style.background = '#F5F5F5';
8540
- el.style.color = '#C0C0C0';
8541
- el.style.setProperty('--card-disabled-color', '#C0C0C0');
8542
- el.style.pointerEvents = 'none';
8543
- el.style.cursor = 'default';
8544
- }
8545
- // ── variableKey auto-sync for Input components ──────────────
8546
- // When an Input element declares `variableKey`, its value is
8547
- // automatically written into the reactive variables store on
8548
- // every `input` event (without triggering a full re-render).
8549
- // The value is then available as `${variableKey}` in action params.
8550
- const variableKey = resolvedProps.variableKey;
8551
- if (variableKey) {
8552
- el.addEventListener('input', ((e) => {
8553
- // Nested value controls own their variableKey. Only a value event
8554
- // dispatched by this component may update this component's variable.
8555
- if (e.target !== el)
8556
- return;
8557
- const value = e.detail?.value
8558
- ?? e.target?.value;
8559
- if (value !== undefined) {
8560
- // Write silently — do NOT trigger rerender (avoids losing focus)
8561
- writeInputVariable(variableKey, value, variables);
8562
- }
8563
- }));
8564
- }
8565
- // ── Bind events (skip if disabled) ──
8566
- if (node.events && !isDisabled) {
8567
- for (const [event, eventValue] of Object.entries(node.events)) {
8568
- if (!eventValue)
8569
- continue;
8570
- const resolvedSteps = resolveActionRef(eventValue, schemaActions);
8571
- if (!resolvedSteps)
8572
- continue;
8573
- const domEvent = eventMap[event] ?? event;
8574
- el.addEventListener(domEvent, ((e) => {
8575
- // Value controls own their input/change handlers; a nested control's
8576
- // same-named event must not trigger the parent's schema action.
8577
- // Other component/event pairs keep the renderer's existing bubbling
8578
- // behavior (for example a Container-level click handler).
8579
- const ownsValueEvent = ((domEvent === 'input' || domEvent === 'change')
8580
- && VALUE_CONTROL_TYPES.has(node.type));
8581
- if (ownsValueEvent && e.target !== el)
8582
- return;
8583
- const detail = e instanceof CustomEvent ? e.detail : undefined;
8584
- runEventActions(resolvedSteps, detail);
8585
- }));
8586
- }
8587
- }
8588
- // Register lifecycle
8589
- if (node.lifecycle) {
8590
- collectLifecycle(node.id, node.lifecycle);
8591
- }
8592
- // Render children — use slot layout if applicable, otherwise flat append
8593
- const renderChild = (child) => renderNode(child, variables, actionContext, isMobile, responsive, schemaActions, writeInputVariable, runEventActions, collectLifecycle);
8594
- // Build children-by-id map for layouts that reference IDs (columns groups, float overlays)
8595
- const childrenMap = {};
8596
- for (const child of node.children) {
8597
- childrenMap[child.id] = child;
8598
- }
8599
- const layoutApplied = renderSlotLayout(el, node.children, resolvedProps, renderChild, childrenMap, actionContext);
8600
- if (!layoutApplied) {
8601
- for (const child of node.children) {
8602
- el.appendChild(renderChild(child));
8603
- }
8604
- }
8605
- // Apply disabled grey text to all descendants (after children are appended)
8606
- // Propagate disabled state to child elements (including Shadow DOM custom elements)
8607
- if (isDisabled) {
8608
- el.querySelectorAll('*').forEach((child) => {
8609
- const htmlChild = child;
8610
- htmlChild.setAttribute('data-disabled', 'true');
8611
- htmlChild.style.setProperty('--card-disabled-color', '#C0C0C0');
8612
- });
8613
- }
8614
- applyResponsiveStyles(el, createResponsiveContext(isMobile, responsive));
8615
- return el;
8616
- }
8617
11126
  /** Map schema event names → DOM event names */
8618
11127
  const eventMap = {
8619
11128
  onClick: 'click',
@@ -8667,6 +11176,7 @@ const VALUE_CONTROL_TYPES = new Set([
8667
11176
  * ```
8668
11177
  */
8669
11178
  function renderStreamingCard(container, options = {}) {
11179
+ const nodeAccess = createCardNodeAccess(container);
8670
11180
  // ─── State ──────────────────────────────────────────────────────
8671
11181
  const parser = new StreamingParser(options.parserOptions);
8672
11182
  const elementMap = new Map(); // elementId → DOM element
@@ -8694,19 +11204,32 @@ function renderStreamingCard(container, options = {}) {
8694
11204
  let boundActionQueue = Promise.resolve();
8695
11205
  let boundLifecycleEpoch = 0;
8696
11206
  let disposed = false;
11207
+ let userActionDepth = 0;
8697
11208
  const sourceOccurrences = new Map();
8698
11209
  const activeBoundLifecycles = new Map();
8699
11210
  const mountedBoundLifecycles = new Map();
8700
11211
  const boundLifecycleGenerations = new Map();
11212
+ function applyAutoFocusIds(ids) {
11213
+ if (ids.size === 0)
11214
+ return;
11215
+ for (const id of ids) {
11216
+ const input = elementMap.get(id);
11217
+ if (input instanceof CardInput && input.requestAutoFocus())
11218
+ break;
11219
+ }
11220
+ }
8701
11221
  // ─── Action Context ─────────────────────────────────────────────
8702
11222
  function buildActionContext() {
8703
11223
  return {
8704
11224
  ...createWebActionContext({
8705
11225
  ...options,
8706
11226
  setVariable: (key, value) => {
11227
+ const previousVariables = userActionDepth > 0
11228
+ ? { ...variables }
11229
+ : undefined;
8707
11230
  variables[key] = value;
8708
11231
  // On variable change, patch only affected elements (keyed diff)
8709
- diffAllElements();
11232
+ diffAllElements(previousVariables);
8710
11233
  },
8711
11234
  abortSignal: abortController.signal,
8712
11235
  }),
@@ -8789,6 +11312,27 @@ function renderStreamingCard(container, options = {}) {
8789
11312
  || resolved === ''
8790
11313
  || resolved === 0);
8791
11314
  }
11315
+ function collectBoundAutoFocusRevealIds(previous, next, previousVariables, nextVariables) {
11316
+ const previousNodes = indexBoundNodes(previous.root);
11317
+ const requested = new Set();
11318
+ const visit = (nextNode, previousParentVisible, nextParentVisible) => {
11319
+ const previousNode = previousNodes.get(nextNode.id);
11320
+ const wasVisible = Boolean(previousNode
11321
+ && previousParentVisible
11322
+ && computeBoundVisible(previousNode, previousVariables));
11323
+ const nowVisible = nextParentVisible
11324
+ && computeBoundVisible(nextNode, nextVariables);
11325
+ if (nextNode.type === 'Input'
11326
+ && !wasVisible
11327
+ && nowVisible
11328
+ && resolveBoundNodeProps(nextNode, nextVariables).autoFocus === true) {
11329
+ requested.add(nextNode.id);
11330
+ }
11331
+ nextNode.children.forEach(child => visit(child, wasVisible, nowVisible));
11332
+ };
11333
+ visit(next.root, true, true);
11334
+ return requested;
11335
+ }
8792
11336
  function computeBoundDisabled(node, renderVariables) {
8793
11337
  const disabled = node.directives?.disabled;
8794
11338
  if (!disabled)
@@ -8964,7 +11508,10 @@ function renderStreamingCard(container, options = {}) {
8964
11508
  if (e instanceof CustomEvent && e.detail != null) {
8965
11509
  variables._event = e.detail;
8966
11510
  }
8967
- runSteps(resolvedSteps, node.id);
11511
+ userActionDepth += 1;
11512
+ void runSteps(resolvedSteps, node.id).finally(() => {
11513
+ userActionDepth -= 1;
11514
+ });
8968
11515
  }));
8969
11516
  }
8970
11517
  }
@@ -9019,6 +11566,43 @@ function renderStreamingCard(container, options = {}) {
9019
11566
  : visibleExpr;
9020
11567
  return !(resolved === false || resolved === 'false' || resolved === '' || resolved === 0);
9021
11568
  }
11569
+ function collectLegacyAutoFocusRevealIds(schema, previousVariables) {
11570
+ let tree;
11571
+ try {
11572
+ tree = parseSchema(schema);
11573
+ }
11574
+ catch {
11575
+ return new Set();
11576
+ }
11577
+ const requested = new Set();
11578
+ const ownVisibility = (node, renderVariables) => {
11579
+ const expression = node.directives?.visible;
11580
+ if (!expression)
11581
+ return true;
11582
+ const resolved = hasExpression(expression)
11583
+ ? resolveExpression(expression, renderVariables)
11584
+ : expression;
11585
+ return !(resolved === false
11586
+ || resolved === 'false'
11587
+ || resolved === ''
11588
+ || resolved === 0);
11589
+ };
11590
+ const visit = (node, previousParentVisible, nextParentVisible) => {
11591
+ const wasVisible = previousParentVisible
11592
+ && ownVisibility(node, previousVariables);
11593
+ const nowVisible = nextParentVisible && ownVisibility(node, variables);
11594
+ if (node.type === 'Input'
11595
+ && !wasVisible
11596
+ && nowVisible
11597
+ && resolveDeep(node.props, variables)
11598
+ .autoFocus === true) {
11599
+ requested.add(node.id);
11600
+ }
11601
+ node.children.forEach(child => visit(child, wasVisible, nowVisible));
11602
+ };
11603
+ visit(tree, true, true);
11604
+ return requested;
11605
+ }
9022
11606
  /** Entrance transition for blocks streamed in incrementally. */
9023
11607
  function animateEnter(el) {
9024
11608
  if (options.appearTransition === false)
@@ -9092,11 +11676,14 @@ function renderStreamingCard(container, options = {}) {
9092
11676
  * (visibility flips / container shape changes) re-render only their subtree.
9093
11677
  * Falls back to a full render solely when a subtree can't be rebuilt.
9094
11678
  */
9095
- function diffAllElements() {
11679
+ function diffAllElements(previousVariables) {
9096
11680
  actionContext = buildActionContext();
9097
11681
  const schema = (currentSurfaceId ? engine.getSchema(currentSurfaceId) : undefined) ?? currentSchema;
9098
11682
  if (!schema)
9099
11683
  return;
11684
+ const autoFocusIds = previousVariables
11685
+ ? collectLegacyAutoFocusRevealIds(schema, previousVariables)
11686
+ : new Set();
9100
11687
  for (const [id, element] of Object.entries(schema.elements)) {
9101
11688
  const el = elementMap.get(id);
9102
11689
  if (!el)
@@ -9107,6 +11694,7 @@ function renderStreamingCard(container, options = {}) {
9107
11694
  // Visibility flipped (either direction) → rebuild this subtree in place
9108
11695
  if (!replaceSubtree(schema, id, el)) {
9109
11696
  safeRenderFull();
11697
+ applyAutoFocusIds(autoFocusIds);
9110
11698
  return;
9111
11699
  }
9112
11700
  continue;
@@ -9123,9 +11711,11 @@ function renderStreamingCard(container, options = {}) {
9123
11711
  }
9124
11712
  else if (!replaceSubtree(schema, id, el)) {
9125
11713
  safeRenderFull();
11714
+ applyAutoFocusIds(autoFocusIds);
9126
11715
  return;
9127
11716
  }
9128
11717
  }
11718
+ applyAutoFocusIds(autoFocusIds);
9129
11719
  }
9130
11720
  function captureBoundScrollPositions(root) {
9131
11721
  const positions = new Map();
@@ -9848,7 +12438,10 @@ function renderStreamingCard(container, options = {}) {
9848
12438
  if (!steps)
9849
12439
  return;
9850
12440
  await runBoundSteps(steps, sourceNode, createBoundActionContext(sourceNode, draft));
12441
+ const nextMaterialized = materializeStreamingCard(currentSchema, draft);
12442
+ const autoFocusIds = collectBoundAutoFocusRevealIds(currentMaterialized, nextMaterialized, before, draft);
9851
12443
  commitBoundDraftTransaction(before, draft, baseRevision);
12444
+ applyAutoFocusIds(autoFocusIds);
9852
12445
  }
9853
12446
  function enqueueBoundEvent(runtimeId, eventName, eventDetail) {
9854
12447
  boundActionQueue = boundActionQueue
@@ -10430,6 +13023,8 @@ function renderStreamingCard(container, options = {}) {
10430
13023
  });
10431
13024
  // ─── Public API ─────────────────────────────────────────────────
10432
13025
  return {
13026
+ getNode: nodeAccess.getNode,
13027
+ onFocusChange: nodeAccess.onFocusChange,
10433
13028
  applyCommand(command) {
10434
13029
  if (!lockMode('commands'))
10435
13030
  return;
@@ -10506,6 +13101,7 @@ function renderStreamingCard(container, options = {}) {
10506
13101
  if (disposed)
10507
13102
  return;
10508
13103
  disposed = true;
13104
+ nodeAccess.dispose();
10509
13105
  boundRevision += 1;
10510
13106
  abortController.abort();
10511
13107
  teardownBoundLifecycles();