@antglobal/copilot-cards-web 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,14 +78,19 @@ Common render options include:
78
78
  | Option | Purpose |
79
79
  | --- | --- |
80
80
  | `variables` | Overrides initial schema variables |
81
- | `botId` | Selects bot-scoped custom action handlers |
82
- | `isMobile` | Overrides automatic viewport detection |
81
+ | `botId` | Optionally selects bot-scoped custom action handlers |
82
+ | `isMobile` | Explicitly enables mobile component layout; defaults to `false` |
83
+ | `responsive` | Explicitly enables mobile px-to-rem conversion when configured |
83
84
  | `fetch` | Supplies a custom request implementation |
84
85
  | `showToast` | Connects toast actions to the host UI |
85
86
  | `navigate` | Connects URL actions to host navigation |
86
87
  | `emit` | Receives events emitted by a card |
87
88
  | `copyText` | Connects copy actions to the host clipboard |
88
89
 
90
+ The SDK does not infer mobile mode or CSS units from viewport width. Pass only
91
+ `isMobile: true` for mobile layout with px output. To opt into REM conversion,
92
+ also pass `responsive: { mobile: { unit: "rem", rootValue: 100 } }`.
93
+
89
94
  ### Streaming
90
95
 
91
96
  Use `renderStreamingCard` when the card arrives incrementally from an AI model or server:
@@ -121,6 +126,8 @@ const bot = new BotSDK({
121
126
  await bot.renderCard(container, schema);
122
127
  ```
123
128
 
129
+ `botId` is optional. Omit it when the application uses only one default action scope; provide it when isolating custom handlers or loading per-bot action configuration.
130
+
124
131
  ## Built-in components
125
132
 
126
133
  The renderer includes:
package/dist/index.d.ts CHANGED
@@ -42,6 +42,7 @@ interface ResponsiveContext {
42
42
  resolveLength(value: string | number): string;
43
43
  convertCSS(value: string): string;
44
44
  }
45
+ /** Reusable REM preset; applied only when passed explicitly as `responsive.mobile`. */
45
46
  declare const DEFAULT_MOBILE_RESPONSIVE: ResponsiveMobileOptions;
46
47
  /** Convert CSS px lengths while preserving strings, comments, and URLs. */
47
48
  declare function convertPixelTokens(value: string, rootValue: number): string;
@@ -62,9 +63,9 @@ declare function createResponsiveContext(isMobile: boolean, responsive?: Respons
62
63
  */
63
64
 
64
65
  interface RenderCardOptions extends WebActionContextOptions {
65
- /** Force mobile mode; auto-detected from viewport if omitted */
66
+ /** Enable mobile component layout explicitly (default false). */
66
67
  isMobile?: boolean;
67
- /** Mobile sizing conversion. `isMobile: true` defaults to rem/rootValue 100. */
68
+ /** Optional mobile sizing conversion; omitted values preserve px output. */
68
69
  responsive?: ResponsiveOptions;
69
70
  /** External variables to merge into schema.variables (overrides schema defaults) */
70
71
  variables?: Record<string, any>;
@@ -282,8 +283,8 @@ declare function connectSSE(instance: StreamingCardInstance, options: SSEConnect
282
283
  */
283
284
 
284
285
  interface BotSDKOptions {
285
- /** Bot ID identifies which bot this instance is for */
286
- botId: string;
286
+ /** Optional bot ID used to scope custom actions and action configuration */
287
+ botId?: string;
287
288
  /** Base URL for business API requests (e.g. 'https://api.example.com') */
288
289
  baseUrl?: string;
289
290
  /**
@@ -433,7 +434,7 @@ declare function buildStyleString(styles: Record<string, string | number | undef
433
434
  * common style helpers. Designed to be used by `renderCard` which
434
435
  * passes resolved props via `setData()`.
435
436
  *
436
- * Note: lifecycle management, event binding, and viewport detection
437
+ * Note: lifecycle management, event binding, and mobile-mode selection
437
438
  * are handled externally by `renderCard` / the render pipeline.
438
439
  * BaseElement keeps itself lightweight and focused on DOM rendering.
439
440
  */
@@ -620,6 +621,8 @@ declare class CardButton extends BaseElement {
620
621
  declare class CardInput extends BaseElement {
621
622
  static readonly is = "ai-card-input";
622
623
  protected render(): void;
624
+ private getAutoFocusControl;
625
+ private isFirstAvailableAutoFocusInput;
623
626
  /** Escape HTML entities for safe insertion. */
624
627
  private escapeHtml;
625
628
  /** Escape attribute values for safe insertion. */
@@ -1017,6 +1020,8 @@ type InputAffix = string | InputAffixConfig;
1017
1020
  interface InputProps {
1018
1021
  /** Placeholder text */
1019
1022
  placeholder?: string;
1023
+ /** Focus the native input after the component is mounted (default false) */
1024
+ autoFocus?: boolean;
1020
1025
  /** HTML input type: text / textarea / password / number etc. */
1021
1026
  inputType?: 'text' | 'textarea' | 'password' | (string & {});
1022
1027
  /** Label text displayed above the input */
@@ -1290,7 +1295,7 @@ interface ChoiceListProps {
1290
1295
  indicatorStyle?: Record<string, any>;
1291
1296
  /** Style merged into selected indicators */
1292
1297
  selectedIndicatorStyle?: Record<string, any>;
1293
- /** Checked mark used by multiple mode; defaults to built-in `check_bold` */
1298
+ /** Selected mark override; single mode keeps its radio dot when omitted, while multiple mode defaults to `check_bold` */
1294
1299
  checkedIcon?: ChoiceIndicatorIcon;
1295
1300
  }
1296
1301
  interface ChoiceIndicatorIcon {
@@ -1318,7 +1323,7 @@ interface ChoiceItemProps {
1318
1323
  indicatorStyle?: Record<string, any>;
1319
1324
  /** Per-item selected indicator style */
1320
1325
  selectedIndicatorStyle?: Record<string, any>;
1321
- /** Per-item checked mark override */
1326
+ /** Per-item selected mark override; also replaces the single-select radio dot */
1322
1327
  checkedIcon?: ChoiceIndicatorIcon;
1323
1328
  }
1324
1329
  interface PasscodeInputProps {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getBuiltinIcon, cloneJsonData, createLifecycleManager, materializeCard, resolveActionRef, createA2UIParameterResolver, createExpressionContext, resolveA2UIDeep, hasExpression, resolveExpression, resolveDeep, resolveExpressionValue, replaceRootContents, 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, 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';
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';
@@ -96,6 +96,7 @@ function buildStyleString(styles) {
96
96
  .join(';');
97
97
  }
98
98
 
99
+ /** Reusable REM preset; applied only when passed explicitly as `responsive.mobile`. */
99
100
  const DEFAULT_MOBILE_RESPONSIVE = Object.freeze({
100
101
  unit: 'rem',
101
102
  rootValue: 100,
@@ -236,10 +237,10 @@ function convertPixelTokens(value, rootValue) {
236
237
  return result;
237
238
  }
238
239
  function createResponsiveContext(isMobile, responsive) {
239
- const mobile = responsive?.mobile ?? DEFAULT_MOBILE_RESPONSIVE;
240
- const unit = mobile.unit;
241
- const rootValue = mobile.rootValue;
242
- if (!Number.isFinite(rootValue) || rootValue <= 0) {
240
+ const mobile = responsive?.mobile;
241
+ const unit = mobile?.unit;
242
+ const rootValue = mobile?.rootValue ?? DEFAULT_MOBILE_RESPONSIVE.rootValue;
243
+ if (mobile && (!Number.isFinite(rootValue) || rootValue <= 0)) {
243
244
  throw new Error('[renderCard] responsive.mobile.rootValue must be a positive number');
244
245
  }
245
246
  const active = isMobile && unit === 'rem';
@@ -705,6 +706,7 @@ function buildHeatmapOption(config) {
705
706
  */
706
707
  // ─── Slot Layout Constants ──────────────────────────────────────
707
708
  const SLOT_LAYOUT = {
709
+ FLEX: 'flex',
708
710
  COLUMNS: 'columns',
709
711
  GRID: 'grid',
710
712
  HORIZONTAL_SCROLL: 'horizontalScroll',
@@ -715,6 +717,30 @@ const SLOT_LAYOUT = {
715
717
  TABLE: 'table',
716
718
  CHART: 'chart',
717
719
  };
720
+ // ─── Dispatcher ─────────────────────────────────────────────────
721
+ /**
722
+ * Apply host-level layout styles based on the slot key.
723
+ * Called from renderDefault BEFORE props.style so explicit styles can override.
724
+ * Layouts that require wrapper DOM continue to render in renderSlotLayout.
725
+ */
726
+ function applySlotBaseStyle(container, props) {
727
+ const flex = props.slots?.[SLOT_LAYOUT.FLEX];
728
+ if (!flex || typeof flex !== 'object')
729
+ return;
730
+ const config = flex.config && typeof flex.config === 'object' ? flex.config : {};
731
+ container.style.display = 'flex';
732
+ container.style.flexDirection = config.direction ?? 'column';
733
+ container.style.alignItems = config.align ?? 'flex-start';
734
+ container.style.justifyContent = config.justify ?? 'flex-start';
735
+ container.style.flexWrap = config.wrap ?? 'nowrap';
736
+ container.style.gap = resolveFlexGap(config.gap);
737
+ }
738
+ function resolveFlexGap(value) {
739
+ if (typeof value === 'number' && Number.isFinite(value)) {
740
+ return `${value}px`;
741
+ }
742
+ return typeof value === 'string' && value.length > 0 ? value : '0px';
743
+ }
718
744
  /**
719
745
  * Render children into a container using the slot layout strategy.
720
746
  * Returns true if a special layout was applied (children already appended).
@@ -852,40 +878,12 @@ function applyGridPlacement(el, child) {
852
878
  function isCssPlacement(value) {
853
879
  return typeof value === 'string' || typeof value === 'number';
854
880
  }
855
- // ─── Horizontal Scroll ──────────────────────────────────────────
856
- /**
857
- * Horizontal scrollable row with mouse drag support.
858
- *
859
- * Schema: `slots: { horizontalScroll: { children: [...], config: { gap, itemWidth, snap, arrows, arrowStyle, scrollStep, mask, maskWidth, maskColor, itemHoverStyle, autoScroll, autoScrollInterval, autoScrollLoop } } }`
860
- *
861
- * Optional config:
862
- * - `arrows` — show prev/next arrow buttons; each is only visible while
863
- * the track can actually scroll in that direction (default false)
864
- * - `arrowStyle` — free-form CSS object merged onto both arrow buttons; every
865
- * default (circle, background, shadow…) can be overridden
866
- * - `arrowIcon` — replaces the built-in chevron on both buttons: an image URL
867
- * (http/https/data:image) or a text glyph/emoji. The left
868
- * button gets a mirrored copy.
869
- * - `arrowIconLeft` / `arrowIconRight` — per-side icon, same formats, never
870
- * mirrored; takes precedence over `arrowIcon`
871
- * - `arrowOffset` — distance of each arrow from its own edge (default '8px');
872
- * number = px, negative values push the arrows outside /
873
- * straddling the container edge
874
- * - `scrollStep` — px scrolled per arrow click (default 80% of the visible width)
875
- * - `mask` — show translucent edge-fade masks hinting at more content;
876
- * follows the same can-scroll state as the arrows (default false)
877
- * - `maskWidth` — width of each edge mask (default '48px')
878
- * - `maskColor` — base color the mask fades out from; any CSS color incl.
879
- * rgba for translucency (default '#fff')
880
- * - `maskOpacity` — opacity of a mask while visible (default 1)
881
- * - `itemHoverStyle` — CSS object applied to an item on hover and reverted on
882
- * leave; pure inline-style swap, never triggers a re-render
883
- * - `autoScroll` — while the pointer hovers the track, auto-advance one item
884
- * every `autoScrollInterval` ms (default false)
885
- * - `autoScrollInterval` — ms each item stays before advancing (default 2000)
886
- * - `autoScrollLoop` — loop back to the first item after the last (default true);
887
- * when false it stops on the last item until re-hovered
888
- */
881
+ function resolveScrollbarMode(value) {
882
+ if (value === 'auto' || value === 'hidden' || value === 'visible') {
883
+ return value;
884
+ }
885
+ return undefined;
886
+ }
889
887
  function renderHorizontalScrollSlot(container, children, slotContent, renderChild) {
890
888
  const config = slotContent?.config ?? {};
891
889
  const gap = config.gap ?? '8px';
@@ -897,13 +895,16 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
897
895
  const autoScroll = config.autoScroll === true;
898
896
  const autoScrollInterval = typeof config.autoScrollInterval === 'number' ? config.autoScrollInterval : 2000;
899
897
  const autoScrollLoop = config.autoScrollLoop !== false;
898
+ const scrollbar = resolveScrollbarMode(config.scrollbar);
899
+ const hidesScrollbar = scrollbar == null || scrollbar === 'hidden';
900
900
  const track = document.createElement('div');
901
901
  track.style.cssText = [
902
902
  'display:flex',
903
903
  'overflow-x:auto',
904
904
  'overflow-y:hidden',
905
905
  '-webkit-overflow-scrolling:touch',
906
- 'scrollbar-width:none',
906
+ hidesScrollbar ? 'scrollbar-width:none' : '',
907
+ scrollbar === 'visible' ? 'scrollbar-width:auto' : '',
907
908
  `gap:${gap}`,
908
909
  'cursor:grab',
909
910
  snap ? 'scroll-snap-type:x mandatory' : '',
@@ -912,11 +913,8 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
912
913
  // across variable-driven re-renders (see captureScrollPositions/restoreScrollPositions).
913
914
  const cardId = container.getAttribute('data-card-id') ?? 'x';
914
915
  track.setAttribute('data-scroll-id', cardId);
915
- // Hide scrollbar for WebKit
916
- const styleEl = document.createElement('style');
917
- const trackClass = `card-scroll-${cardId}`;
918
- styleEl.textContent = `.${trackClass}::-webkit-scrollbar{display:none}`;
919
- track.classList.add(trackClass);
916
+ if (scrollbar)
917
+ track.dataset.scrollbar = scrollbar;
920
918
  for (const child of children) {
921
919
  const item = renderChild(child);
922
920
  item.style.flexShrink = '0';
@@ -1040,7 +1038,18 @@ function renderHorizontalScrollSlot(container, children, slotContent, renderChil
1040
1038
  track.addEventListener('mouseleave', stopAuto);
1041
1039
  track.addEventListener('mousedown', stopAuto);
1042
1040
  }
1043
- container.appendChild(styleEl);
1041
+ if (hidesScrollbar) {
1042
+ const trackClass = `card-scroll-${cardId}`;
1043
+ track.classList.add(trackClass);
1044
+ const styleEl = document.createElement('style');
1045
+ styleEl.textContent = `.${trackClass}::-webkit-scrollbar{display:none}`;
1046
+ container.appendChild(styleEl);
1047
+ }
1048
+ else if (scrollbar === 'visible') {
1049
+ const styleEl = document.createElement('style');
1050
+ styleEl.textContent = '[data-scroll-id][data-scrollbar="visible"]::-webkit-scrollbar{display:block}';
1051
+ container.appendChild(styleEl);
1052
+ }
1044
1053
  // Plain track — no overlay chrome requested
1045
1054
  if (!arrows && !mask) {
1046
1055
  container.appendChild(track);
@@ -1113,13 +1122,41 @@ function applyHoverStyle(item, hoverStyle) {
1113
1122
  const previous = {};
1114
1123
  item.addEventListener('mouseenter', () => {
1115
1124
  for (const [key, value] of Object.entries(hoverStyle)) {
1116
- previous[key] = item.style[key];
1117
- item.style[key] = value;
1125
+ if (key.startsWith('--')) {
1126
+ const previousValue = item.style.getPropertyValue(key);
1127
+ const previousPriority = item.style.getPropertyPriority(key);
1128
+ previous[key] = {
1129
+ value: previousValue,
1130
+ priority: previousPriority,
1131
+ wasPresent: previousValue !== '' || previousPriority !== '',
1132
+ isCustomProperty: true,
1133
+ };
1134
+ item.style.setProperty(key, String(value));
1135
+ }
1136
+ else {
1137
+ previous[key] = {
1138
+ value: item.style[key],
1139
+ priority: '',
1140
+ wasPresent: true,
1141
+ isCustomProperty: false,
1142
+ };
1143
+ item.style[key] = value;
1144
+ }
1118
1145
  }
1119
1146
  });
1120
1147
  item.addEventListener('mouseleave', () => {
1121
- for (const [key, value] of Object.entries(previous)) {
1122
- item.style[key] = value;
1148
+ for (const [key, state] of Object.entries(previous)) {
1149
+ if (state.isCustomProperty) {
1150
+ if (state.wasPresent) {
1151
+ item.style.setProperty(key, state.value, state.priority);
1152
+ }
1153
+ else {
1154
+ item.style.removeProperty(key);
1155
+ }
1156
+ }
1157
+ else {
1158
+ item.style[key] = state.value;
1159
+ }
1123
1160
  }
1124
1161
  });
1125
1162
  }
@@ -1647,7 +1684,7 @@ function renderTableSlot(container, slotContent) {
1647
1684
  * common style helpers. Designed to be used by `renderCard` which
1648
1685
  * passes resolved props via `setData()`.
1649
1686
  *
1650
- * Note: lifecycle management, event binding, and viewport detection
1687
+ * Note: lifecycle management, event binding, and mobile-mode selection
1651
1688
  * are handled externally by `renderCard` / the render pipeline.
1652
1689
  * BaseElement keeps itself lightweight and focused on DOM rendering.
1653
1690
  */
@@ -2335,6 +2372,30 @@ function sanitizeIconHtml(icon) {
2335
2372
  walk(tpl.content);
2336
2373
  return tpl.innerHTML;
2337
2374
  }
2375
+ function escapeIconAttribute(value) {
2376
+ return value
2377
+ .replace(/&/g, '&amp;')
2378
+ .replace(/"/g, '&quot;')
2379
+ .replace(/</g, '&lt;')
2380
+ .replace(/>/g, '&gt;');
2381
+ }
2382
+ function isDirectImageUrl(icon) {
2383
+ const normalized = icon.replace(/[\s\x00-\x1f]/g, '').toLowerCase();
2384
+ return /^(?:https?:\/\/|\/\/|data:image\/)/.test(normalized)
2385
+ || /^(?:\.{0,2}\/).+\.(?:avif|gif|jpe?g|png|svg|webp)(?:[?#].*)?$/i.test(icon);
2386
+ }
2387
+ /**
2388
+ * Button.icon historically accepts sanitized HTML or text. A direct image URL
2389
+ * is a common shorthand, so convert only recognizable image URLs to markup and
2390
+ * leave inline SVG, <img> HTML, emoji, and other text on the existing path.
2391
+ */
2392
+ function renderButtonIcon(icon) {
2393
+ const value = String(icon ?? '').trim();
2394
+ if (isDirectImageUrl(value)) {
2395
+ return `<img class="card-btn-url-icon" src="${escapeIconAttribute(value)}" alt="" aria-hidden="true" />`;
2396
+ }
2397
+ return sanitizeIconHtml(value);
2398
+ }
2338
2399
  class CardButton extends BaseElement {
2339
2400
  render() {
2340
2401
  if (!this.shadowRoot || !this._node)
@@ -2489,12 +2550,18 @@ class CardButton extends BaseElement {
2489
2550
  width: 1em;
2490
2551
  height: 1em;
2491
2552
  }
2553
+ .card-btn-url-icon {
2554
+ display: block;
2555
+ width: 100%;
2556
+ height: 100%;
2557
+ object-fit: contain;
2558
+ }
2492
2559
  </style>
2493
2560
  <button
2494
2561
  class="card-btn ${variant} ${isMobile ? 'card-mobile' : 'card-desktop'}"
2495
2562
  ${disabled ? 'disabled' : ''}
2496
2563
  style="${inlineStyle}"
2497
- ><span class="card-btn-content">${icon ? `<span class="card-btn-icon">${sanitizeIconHtml(String(icon))}</span>` : ''}${displayText}</span></button>
2564
+ ><span class="card-btn-content">${icon ? `<span class="card-btn-icon">${renderButtonIcon(icon)}</span>` : ''}${displayText}</span></button>
2498
2565
  `);
2499
2566
  }
2500
2567
  }
@@ -2681,7 +2748,7 @@ class CardInput extends BaseElement {
2681
2748
  render() {
2682
2749
  if (!this.shadowRoot || !this._node)
2683
2750
  return;
2684
- const { placeholder = '', inputType = 'text', label, defaultValue = '', disabled = false, readonly: readOnly = false, maxLength, rows, min, max, step = 1, controls = true, prefix, suffix, style, inputStyle, isExpressionResultStyle, } = this._props;
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;
2685
2752
  const inlineStyle = this.buildInlineStyle(style, isExpressionResultStyle);
2686
2753
  const legacyInputStyle = style && typeof style === 'object' && style.resize != null
2687
2754
  ? { resize: style.resize }
@@ -2967,6 +3034,14 @@ class CardInput extends BaseElement {
2967
3034
  detail: { value: inputEl.value },
2968
3035
  }));
2969
3036
  });
3037
+ if (autoFocus && !disabled) {
3038
+ queueMicrotask(() => {
3039
+ const control = this.getAutoFocusControl();
3040
+ if (control && this.isFirstAvailableAutoFocusInput()) {
3041
+ control.focus();
3042
+ }
3043
+ });
3044
+ }
2970
3045
  if (isNumber &&
2971
3046
  showNumberStepper &&
2972
3047
  inputEl instanceof HTMLInputElement) {
@@ -2995,6 +3070,22 @@ class CardInput extends BaseElement {
2995
3070
  }
2996
3071
  }
2997
3072
  // ─── Helpers ──────────────────────────────────────────────────
3073
+ getAutoFocusControl() {
3074
+ if (!this.isConnected
3075
+ || !this._props.autoFocus
3076
+ || this.hasAttribute('data-disabled')) {
3077
+ return null;
3078
+ }
3079
+ 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))
3085
+ return false;
3086
+ const firstAvailable = Array.from(root.querySelectorAll(CardInput.is)).find(input => input.getAutoFocusControl() !== null);
3087
+ return firstAvailable === this;
3088
+ }
2998
3089
  /** Escape HTML entities for safe insertion. */
2999
3090
  escapeHtml(str) {
3000
3091
  return str
@@ -4716,7 +4807,7 @@ class CardForm extends BaseElement {
4716
4807
  this._bindEvents(fields, disabled);
4717
4808
  }
4718
4809
  _renderField(field, index) {
4719
- const { name, label, type = 'text', placeholder = '', required = false, rules = [], prefix, suffix, options = [], length = 6, } = field;
4810
+ const { name, label, type = 'text', placeholder = '', required = false, rules = [], prefix, suffix, length = 6, } = field;
4720
4811
  const val = this._values[name] ?? '';
4721
4812
  const error = this._errors[name] || '';
4722
4813
  const errorClass = error ? 'error' : '';
@@ -6988,11 +7079,12 @@ class CardChoiceItem extends BaseElement {
6988
7079
  ...(selected ? selectedIndicatorStyle || {} : {}),
6989
7080
  }, isExpressionResultStyle);
6990
7081
  const indicatorRole = selectionMode === 'single' ? 'radio' : 'checkbox';
6991
- const resolvedCheckedIcon = checkedIcon ?? listCheckedIcon ?? {
7082
+ const configuredCheckedIcon = checkedIcon ?? listCheckedIcon;
7083
+ const resolvedCheckedIcon = configuredCheckedIcon ?? {
6992
7084
  name: 'check_bold',
6993
7085
  size: 12,
6994
7086
  };
6995
- const checkedMark = selectionMode === 'multiple'
7087
+ const checkedMark = selectionMode === 'multiple' || configuredCheckedIcon
6996
7088
  ? `<span class="choice-icon" aria-hidden="true">${renderIconContent(resolvedCheckedIcon)}</span>`
6997
7089
  : '<span class="radio-dot" aria-hidden="true"></span>';
6998
7090
  this.setAttribute('data-selected', String(selected));
@@ -7631,6 +7723,8 @@ function renderDefault(node, props, isMobile, responsive) {
7631
7723
  div.className = `card-element card-${node.type.toLowerCase()} ${isMobile ? 'card-mobile' : 'card-desktop'}`;
7632
7724
  div.setAttribute('data-card-id', node.id);
7633
7725
  div.setAttribute('data-card-type', node.type);
7726
+ // Apply slot base styles FIRST (so props.style can override)
7727
+ applySlotBaseStyle(div, props);
7634
7728
  if (props.style && typeof props.style === 'object') {
7635
7729
  const resolvedStyle = buildStyleString(props.isExpressionResultStyle
7636
7730
  ? props.style
@@ -7713,18 +7807,9 @@ function renderBoundCard(container, schema, options) {
7713
7807
  let currentMaterialized;
7714
7808
  let revision = 0;
7715
7809
  let disposed = false;
7716
- let isMobile = options.isMobile ?? isMobileViewport();
7810
+ const isMobile = options.isMobile === true;
7717
7811
  let actionQueue = Promise.resolve();
7718
7812
  let lifecycleQueue = Promise.resolve();
7719
- const removeViewportListener = options.isMobile == null
7720
- ? onViewportChange((mobile) => {
7721
- if (disposed)
7722
- return;
7723
- isMobile = mobile;
7724
- const candidate = prepareCandidate(variables);
7725
- publishDOM(candidate, false);
7726
- })
7727
- : () => { };
7728
7813
  function expressionContextFor(node) {
7729
7814
  return isBoundRenderTreeNode(node)
7730
7815
  ? createExpressionContext(node.scope)
@@ -7961,6 +8046,33 @@ function renderBoundCard(container, schema, options) {
7961
8046
  writable: true,
7962
8047
  });
7963
8048
  }
8049
+ function jsonDataEqual(before, after) {
8050
+ if (Object.is(before, after))
8051
+ return true;
8052
+ if (Array.isArray(before) || Array.isArray(after)) {
8053
+ if (!Array.isArray(before) || !Array.isArray(after))
8054
+ return false;
8055
+ return before.length === after.length
8056
+ && before.every((value, index) => jsonDataEqual(value, after[index]));
8057
+ }
8058
+ if (before === null
8059
+ || after === null
8060
+ || typeof before !== 'object'
8061
+ || typeof after !== 'object') {
8062
+ return false;
8063
+ }
8064
+ const beforeRecord = before;
8065
+ const afterRecord = after;
8066
+ const beforeKeys = Object.keys(beforeRecord);
8067
+ const afterKeys = Object.keys(afterRecord);
8068
+ return beforeKeys.length === afterKeys.length
8069
+ && beforeKeys.every(key => Object.prototype.hasOwnProperty.call(afterRecord, key)
8070
+ && jsonDataEqual(beforeRecord[key], afterRecord[key]));
8071
+ }
8072
+ function hasVariableChanges(before, after) {
8073
+ const validatedAfter = cloneJsonData(after);
8074
+ return !jsonDataEqual(before, validatedAfter);
8075
+ }
7964
8076
  function createDraftActionContext(node, draft) {
7965
8077
  const write = (key, value) => {
7966
8078
  writeDraftVariable(draft, key, value);
@@ -7989,6 +8101,7 @@ function renderBoundCard(container, schema, options) {
7989
8101
  if (eventDetail !== undefined) {
7990
8102
  writeDraftVariable(draft, '_event', cloneJsonData(eventDetail));
7991
8103
  }
8104
+ const actionBaseline = cloneJsonData(draft);
7992
8105
  const freshMaterialized = materializeCard(schema, draft);
7993
8106
  const freshNode = indexNodes(freshMaterialized.root).get(runtimeId);
7994
8107
  if (!freshNode) {
@@ -8002,6 +8115,8 @@ function renderBoundCard(container, schema, options) {
8002
8115
  return;
8003
8116
  await runActionSteps(steps, createDraftActionContext(freshNode, draft));
8004
8117
  assertCurrentRevision(baseRevision);
8118
+ if (!hasVariableChanges(actionBaseline, draft))
8119
+ return;
8005
8120
  const candidate = prepareCandidate(draft);
8006
8121
  commitDraft(draft, candidate, baseRevision);
8007
8122
  }
@@ -8041,7 +8156,6 @@ function renderBoundCard(container, schema, options) {
8041
8156
  return;
8042
8157
  disposed = true;
8043
8158
  abortController.abort();
8044
- removeViewportListener();
8045
8159
  disposeChartsIn(container);
8046
8160
  container.replaceChildren();
8047
8161
  const lifecycleNodes = [...activeLifecycleNodes.entries()];
@@ -8169,50 +8283,124 @@ function renderStaticCard(container, schema, options) {
8169
8283
  const tree = parseSchema(schema);
8170
8284
  // 3. Reactive variables store (mutable copy, merged with external variables)
8171
8285
  let variables = { ...schema.variables, ...options.variables };
8172
- // 4. Create action context with setVariable wired into re-render
8173
- const lifecycleManager = createLifecycleManager();
8286
+ let disposed = false;
8287
+ let hostAuthorityEpoch = 0;
8288
+ const lifecycleRecords = new Map();
8174
8289
  const abortController = new AbortController();
8175
8290
  // Per-instance request dedup map (isolated from other cards on the page).
8176
8291
  const inflightRequests = new Map();
8177
8292
  function buildActionContext() {
8178
- return {
8293
+ const baseAuthorityEpoch = hostAuthorityEpoch;
8294
+ let context;
8295
+ const writeVariable = (key, value, { silent }) => {
8296
+ let actionVariables = context.variables;
8297
+ if (disposed || baseAuthorityEpoch !== hostAuthorityEpoch) {
8298
+ if (actionVariables === variables) {
8299
+ actionVariables = { ...actionVariables };
8300
+ context.variables = actionVariables;
8301
+ }
8302
+ actionVariables[key] = value;
8303
+ return;
8304
+ }
8305
+ actionVariables[key] = value;
8306
+ if (!silent)
8307
+ rerender();
8308
+ };
8309
+ context = {
8179
8310
  ...createWebActionContext({
8180
8311
  ...options,
8181
8312
  setVariable: (key, value) => {
8182
- variables[key] = value;
8183
- // Trigger re-render after variable change
8184
- rerender();
8313
+ writeVariable(key, value, { silent: false});
8185
8314
  },
8186
8315
  abortSignal: abortController.signal,
8187
8316
  }),
8188
8317
  variables,
8318
+ variableWriter: writeVariable,
8189
8319
  botId: options.botId,
8190
8320
  inflightRequests,
8191
8321
  };
8322
+ return context;
8192
8323
  }
8193
8324
  let actionContext = buildActionContext();
8194
8325
  // 5. Responsive
8195
- let isMobile = options.isMobile ?? isMobileViewport();
8196
- const removeViewportListener = options.isMobile == null
8197
- ? onViewportChange((mobile) => {
8198
- isMobile = mobile;
8199
- rerender();
8200
- })
8201
- : () => { };
8326
+ const isMobile = options.isMobile === true;
8202
8327
  // 6. Schema-level action definitions (for string references in events)
8203
8328
  const schemaActions = schema.actions ?? {};
8329
+ function destroyLifecycle(record) {
8330
+ if (record.state !== 'mounted')
8331
+ return;
8332
+ record.state = 'destroying';
8333
+ void runActionSteps(record.lifecycle.onDestroy ?? [], buildActionContext())
8334
+ .catch((error) => {
8335
+ console.error('[renderCard] Static lifecycle destroy failed', error);
8336
+ })
8337
+ .finally(() => {
8338
+ record.state = 'destroyed';
8339
+ });
8340
+ }
8341
+ function startLifecycleMount(id, lifecycle) {
8342
+ if (disposed || lifecycleRecords.has(id))
8343
+ return;
8344
+ const record = { lifecycle, state: 'mounting' };
8345
+ lifecycleRecords.set(id, record);
8346
+ void runActionSteps(lifecycle.onMount ?? [], buildActionContext())
8347
+ .then(() => {
8348
+ record.state = 'mounted';
8349
+ if (disposed)
8350
+ destroyLifecycle(record);
8351
+ })
8352
+ .catch((error) => {
8353
+ console.error('[renderCard] Static lifecycle mount failed', error);
8354
+ if (disposed) {
8355
+ record.state = 'destroyed';
8356
+ }
8357
+ else if (lifecycleRecords.get(id) === record) {
8358
+ lifecycleRecords.delete(id);
8359
+ }
8360
+ });
8361
+ }
8204
8362
  // 7. Render function
8363
+ let renderGeneration = 0;
8205
8364
  function render() {
8365
+ if (disposed)
8366
+ return;
8367
+ const generation = ++renderGeneration;
8368
+ const collectedLifecycles = [];
8206
8369
  const scrollPositions = captureScrollPositions(container);
8207
8370
  const mediaStates = captureMediaStates(container);
8208
8371
  disposeChartsIn(container); // tear down old chart instances before clearing
8209
- container.innerHTML = '';
8210
- const dom = renderNode(tree, variables, actionContext, isMobile, options.responsive, lifecycleManager, schemaActions);
8211
- container.appendChild(dom);
8372
+ const dom = renderNode(tree, variables, actionContext, isMobile, options.responsive, schemaActions, (key, value, sourceVariables) => {
8373
+ if (disposed || sourceVariables !== variables)
8374
+ return;
8375
+ variables[key] = value;
8376
+ hostAuthorityEpoch += 1;
8377
+ }, (steps, detail) => {
8378
+ if (disposed)
8379
+ 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);
8212
8391
  restoreScrollPositions(container, scrollPositions);
8213
8392
  restoreMediaStates(container, mediaStates);
8393
+ if (disposed || generation !== renderGeneration)
8394
+ return;
8395
+ for (const { id, lifecycle } of collectedLifecycles) {
8396
+ if (disposed || generation !== renderGeneration)
8397
+ break;
8398
+ startLifecycleMount(id, lifecycle);
8399
+ }
8214
8400
  }
8215
8401
  function rerender() {
8402
+ if (disposed)
8403
+ return;
8216
8404
  actionContext = buildActionContext();
8217
8405
  render();
8218
8406
  }
@@ -8221,13 +8409,20 @@ function renderStaticCard(container, schema, options) {
8221
8409
  // 8. Return instance handle
8222
8410
  return {
8223
8411
  dispose() {
8412
+ if (disposed)
8413
+ return;
8414
+ disposed = true;
8415
+ hostAuthorityEpoch += 1;
8224
8416
  abortController.abort();
8225
- removeViewportListener();
8226
- lifecycleManager.dispose(actionContext);
8227
8417
  disposeChartsIn(container);
8228
- container.innerHTML = '';
8418
+ container.replaceChildren();
8419
+ for (const record of lifecycleRecords.values())
8420
+ destroyLifecycle(record);
8229
8421
  },
8230
8422
  updateVariables(newVars) {
8423
+ if (disposed)
8424
+ return;
8425
+ hostAuthorityEpoch += 1;
8231
8426
  variables = { ...variables, ...newVars };
8232
8427
  rerender();
8233
8428
  },
@@ -8305,7 +8500,7 @@ function restoreMediaStates(root, states) {
8305
8500
  });
8306
8501
  }
8307
8502
  // ─── Recursive Node Renderer ─────────────────────────────────────
8308
- function renderNode(node, variables, actionContext, isMobile, responsive, lifecycleManager, schemaActions) {
8503
+ function renderNode(node, variables, actionContext, isMobile, responsive, schemaActions, writeInputVariable, runEventActions, collectLifecycle) {
8309
8504
  // Check directives.visible
8310
8505
  if (node.directives?.visible) {
8311
8506
  const visibleExpr = node.directives.visible;
@@ -8363,7 +8558,7 @@ function renderNode(node, variables, actionContext, isMobile, responsive, lifecy
8363
8558
  ?? e.target?.value;
8364
8559
  if (value !== undefined) {
8365
8560
  // Write silently — do NOT trigger rerender (avoids losing focus)
8366
- variables[variableKey] = value;
8561
+ writeInputVariable(variableKey, value, variables);
8367
8562
  }
8368
8563
  }));
8369
8564
  }
@@ -8385,21 +8580,17 @@ function renderNode(node, variables, actionContext, isMobile, responsive, lifecy
8385
8580
  && VALUE_CONTROL_TYPES.has(node.type));
8386
8581
  if (ownsValueEvent && e.target !== el)
8387
8582
  return;
8388
- // Write event.detail to _event variable so action params can use ${_event.xxx}
8389
- if (e instanceof CustomEvent && e.detail != null) {
8390
- variables._event = e.detail;
8391
- }
8392
- runActionSteps(resolvedSteps, actionContext);
8583
+ const detail = e instanceof CustomEvent ? e.detail : undefined;
8584
+ runEventActions(resolvedSteps, detail);
8393
8585
  }));
8394
8586
  }
8395
8587
  }
8396
8588
  // Register lifecycle
8397
8589
  if (node.lifecycle) {
8398
- lifecycleManager.register(node.id, node.lifecycle);
8399
- lifecycleManager.mount(node.id, actionContext);
8590
+ collectLifecycle(node.id, node.lifecycle);
8400
8591
  }
8401
8592
  // Render children — use slot layout if applicable, otherwise flat append
8402
- const renderChild = (child) => renderNode(child, variables, actionContext, isMobile, responsive, lifecycleManager, schemaActions);
8593
+ const renderChild = (child) => renderNode(child, variables, actionContext, isMobile, responsive, schemaActions, writeInputVariable, runEventActions, collectLifecycle);
8403
8594
  // Build children-by-id map for layouts that reference IDs (columns groups, float overlays)
8404
8595
  const childrenMap = {};
8405
8596
  for (const child of node.children) {
@@ -8495,7 +8686,7 @@ function renderStreamingCard(container, options = {}) {
8495
8686
  let partialVariablesSent = false;
8496
8687
  let partialFinalized = false;
8497
8688
  let variables = { ...options.variables };
8498
- let isMobile = options.isMobile ?? isMobileViewport();
8689
+ const isMobile = options.isMobile === true;
8499
8690
  let currentSchema = null;
8500
8691
  let currentSurfaceId = null;
8501
8692
  let currentMaterialized = null;
@@ -8507,16 +8698,6 @@ function renderStreamingCard(container, options = {}) {
8507
8698
  const activeBoundLifecycles = new Map();
8508
8699
  const mountedBoundLifecycles = new Map();
8509
8700
  const boundLifecycleGenerations = new Map();
8510
- // Responsive viewport detection
8511
- const removeViewportListener = options.isMobile == null
8512
- ? onViewportChange((mobile) => {
8513
- isMobile = mobile;
8514
- // Re-render all elements with new mobile state if schema exists
8515
- if (currentSchema) {
8516
- rerenderAll();
8517
- }
8518
- })
8519
- : () => { };
8520
8701
  // ─── Action Context ─────────────────────────────────────────────
8521
8702
  function buildActionContext() {
8522
8703
  return {
@@ -9888,14 +10069,6 @@ function renderStreamingCard(container, options = {}) {
9888
10069
  console.warn('[renderStreamingCard] Deferred render (incomplete schema):', error);
9889
10070
  }
9890
10071
  }
9891
- /**
9892
- * Re-render all elements (viewport changes — isMobile affects every renderer's
9893
- * output, so the incremental diff cache cannot be reused here).
9894
- */
9895
- function rerenderAll() {
9896
- actionContext = buildActionContext();
9897
- safeRenderFull();
9898
- }
9899
10072
  // ─── Engine Event Handlers ──────────────────────────────────────
9900
10073
  const engine = new StreamingEngine({
9901
10074
  onSurfaceCreated(surfaceId, schemaInput) {
@@ -10240,7 +10413,7 @@ function renderStreamingCard(container, options = {}) {
10240
10413
  }
10241
10414
  }
10242
10415
  },
10243
- onSurfaceDeleted(surfaceId) {
10416
+ onSurfaceDeleted(_surfaceId) {
10244
10417
  disposeChartsIn(container); // release old ECharts instances before clearing
10245
10418
  container.innerHTML = '';
10246
10419
  elementMap.clear();
@@ -10335,7 +10508,6 @@ function renderStreamingCard(container, options = {}) {
10335
10508
  disposed = true;
10336
10509
  boundRevision += 1;
10337
10510
  abortController.abort();
10338
- removeViewportListener();
10339
10511
  teardownBoundLifecycles();
10340
10512
  lifecycleManager.dispose(actionContext);
10341
10513
  disposeChartsIn(container); // release ECharts instances before clearing
@@ -10504,7 +10676,7 @@ class BotSDK {
10504
10676
  this._instances = [];
10505
10677
  /** Declarative action chains loaded from actionProvider */
10506
10678
  this._actionChains = new Map();
10507
- this.botId = options.botId;
10679
+ this.botId = options.botId ?? '';
10508
10680
  this.baseUrl = options.baseUrl ?? '';
10509
10681
  // Source B: Batch-register action handler functions (sync, immediate)
10510
10682
  if (options.onAction) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antglobal/copilot-cards-web",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Web Component renderer for copilot bot card SDK — PC + Mobile + WebView unified rendering via Custom Elements",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "@antglobal/copilot-cards-core": "^1.0.4",
31
+ "@antglobal/copilot-cards-core": "^1.0.6",
32
32
  "echarts": "^5.6.0",
33
33
  "marked": "^18.0.5",
34
34
  "tslib": "^2.8.1",