@yaoxiu/marketing-dsl 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { normalizeViews, createTicker, evaluate, resolveNodeStyle, interpolateDeep, interpolate, toLength, toCssStyle, parseEndTime, computeParts, formatParts } from './chunk-DTHKC5AL.js';
2
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, BANNER_MIN_ASPECT_RATIO, CLOSE_POSITIONS, DEFAULT_USER_FIELDS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, USER_FIELD_LABELS, check, computeParts, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, toCssStyle, toLength, validate, validateUserFields } from './chunk-DTHKC5AL.js';
1
+ import { normalizeViews, nextScopeUid, createTicker, createCssBuilder, declarations, toCssStyle, interpolateDeep, evaluate, resolveNodeStyle, interpolate, toLength, computeParts, formatParts, parseEndTime } from './chunk-DT3ZINDI.js';
2
+ export { ACTION_TYPES, ALLOWED_EASINGS, ALLOWED_STYLE_KEYS, BANNER_MIN_ASPECT_RATIO, CLOSE_POSITIONS, CSS_NAME_PATTERN, DEFAULT_USER_FIELDS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, USER_FIELD_LABELS, check, computeParts, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isKeyframeOffset, isLength, isValidEasing, isValidEndTime, normalizeViews, parseEndTime, sanitizeCssValue, toCssStyle, toLength, validate, validateUserFields } from './chunk-DT3ZINDI.js';
3
3
 
4
4
  // src/url.ts
5
5
  var SAFE_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"];
@@ -103,17 +103,13 @@ function createDispatcher(options) {
103
103
  // src/resolve.ts
104
104
  function resolveTree(input) {
105
105
  const { normalized, viewStack, ready } = input;
106
+ const css = createCssBuilder(input.scopeUid, input.keyframes, input.hover);
107
+ const pass = Object.assign({}, input, { css });
106
108
  const layers = viewStack.map((name) => ({ name, view: normalized.views[name] })).filter((item) => !!item.view);
107
109
  const hasPopup = layers.some((item) => item.view.type === "popup");
108
110
  const countdowns = { endTimes: [], precision: "s" };
109
111
  const renderLayers = ready ? layers.map(
110
- (item, index) => resolveLayer(
111
- item.name,
112
- item.view,
113
- index === layers.length - 1,
114
- input,
115
- countdowns
116
- )
112
+ (item, index) => resolveLayer(item.name, item.view, index === layers.length - 1, pass, countdowns)
117
113
  ) : [];
118
114
  return {
119
115
  ready: ready && layers.length > 0,
@@ -124,7 +120,10 @@ function resolveTree(input) {
124
120
  rootStyle: { position: "relative" },
125
121
  layers: renderLayers,
126
122
  countdownEndTimes: countdowns.endTimes,
127
- countdownPrecision: countdowns.precision
123
+ countdownPrecision: countdowns.precision,
124
+ // build 必须在 layers 算完之后调:hover 规则是遍历节点时才登记进来的
125
+ css: css.build(),
126
+ rootClassName: css.rootClassName
128
127
  };
129
128
  }
130
129
  function resolveLayer(name, view, isTop, input, countdowns) {
@@ -133,7 +132,7 @@ function resolveLayer(name, view, isTop, input, countdowns) {
133
132
  const isPopup = view.type === "popup";
134
133
  const rootLayout = stage.layout === "flow" ? "flow" : "absolute";
135
134
  const maskClosable = !!stage.maskClosable;
136
- const onMaskClick = maskClosable ? () => closeTop("mask") : void 0;
135
+ const onMaskClick = maskClosable ? stage.maskAction ? () => input.dispatch(stage.maskAction, context) : () => closeTop("mask") : void 0;
137
136
  const nodes = (view.nodes || []).map(
138
137
  (node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)
139
138
  ).filter((el) => !!el);
@@ -141,20 +140,26 @@ function resolveLayer(name, view, isTop, input, countdowns) {
141
140
  name,
142
141
  type: view.type,
143
142
  isTop,
144
- // 只有栈顶那层画遮罩,否则两层遮罩叠加会明显变黑
145
- mask: isPopup && stage.mask !== false && isTop,
146
- onMaskClick,
143
+ /**
144
+ * 弹窗层恒画遮罩元素,**非栈顶时只是变透明**(见下方 maskStyle)。
145
+ *
146
+ * 【为什么不是「只有栈顶才画」】那样写视觉上也对(两层实心遮罩叠加会明显变黑,
147
+ * 所以下层必须让位),但下层的遮罩 DOM 会被整个移除、退回栈顶时再新建一个 ——
148
+ * 新元素身上的 `maskAnimate` 于是又播一遍:关掉二次挽留层回到主弹窗时,
149
+ * 背景会重新淡入一次,看起来就是「闪了一下」。元素常驻就没这问题。
150
+ */
151
+ mask: isPopup && stage.mask !== false,
152
+ // 非栈顶层整层 pointerEvents: none,遮罩点不到,这里也就不必给它挂回调
153
+ onMaskClick: isTop ? onMaskClick : void 0,
147
154
  layerStyle: layerStyle(isPopup, isTop),
148
- maskStyle: {
149
- position: "absolute",
150
- top: "0",
151
- right: "0",
152
- bottom: "0",
153
- left: "0",
154
- background: "rgba(0, 0, 0, 0.55)"
155
- },
155
+ // 合并顺序固定:内置默认 → stage.maskStyle 覆盖 → maskAnimate 的 animation 收尾。
156
+ // 遮罩样式也走一遍插值,切 tab 时能跟着变(同 stage.style)
157
+ maskStyle: maskStyle(stage, isTop, context, input),
156
158
  scrollStyle: scrollStyle(isPopup),
157
- stageStyle: resolveStageStyle(stage, isPopup, context),
159
+ stageStyle: withAnimation(
160
+ resolveStageStyle(stage, isPopup, context),
161
+ input.css.animation(stage.animate)
162
+ ),
158
163
  clipStyle: {
159
164
  position: "relative",
160
165
  height: "100%",
@@ -162,10 +167,39 @@ function resolveLayer(name, view, isTop, input, countdowns) {
162
167
  overflow: "hidden",
163
168
  boxSizing: "border-box"
164
169
  },
165
- closeButton: resolveCloseButton(stage.closeButton, name, closeTop),
170
+ closeButton: resolveCloseButton(stage.closeButton, name, closeTop, input, context),
166
171
  nodes
167
172
  };
168
173
  }
174
+ function withAnimation(style, animation) {
175
+ if (animation) style.animation = animation;
176
+ return style;
177
+ }
178
+ function maskStyle(stage, isTop, context, input) {
179
+ const style = withAnimation(
180
+ Object.assign(
181
+ {
182
+ position: "absolute",
183
+ top: "0",
184
+ right: "0",
185
+ bottom: "0",
186
+ left: "0",
187
+ background: "rgba(0, 0, 0, 0.55)"
188
+ },
189
+ toCssStyle(interpolateDeep(stage.maskStyle, context))
190
+ ),
191
+ input.css.animation(stage.maskAnimate)
192
+ );
193
+ if (isTop) return style;
194
+ style.background = "transparent";
195
+ delete style.backdropFilter;
196
+ delete style.WebkitBackdropFilter;
197
+ return style;
198
+ }
199
+ function withHover(className, owner, css) {
200
+ const hoverClass = css.hoverClass(owner);
201
+ return hoverClass ? `${className} ${hoverClass}` : className;
202
+ }
169
203
  function layerStyle(isPopup, isTop) {
170
204
  const style = isPopup ? {
171
205
  position: "fixed",
@@ -223,7 +257,8 @@ function resolveStageStyle(stage, isPopup, context) {
223
257
  }
224
258
  return style;
225
259
  }
226
- function resolveCloseButton(config, layerName, closeTop) {
260
+ function resolveCloseButton(config, layerName, closeTop, input, context) {
261
+ const css = input.css;
227
262
  if (!config || config.show === false) return void 0;
228
263
  const offset = config.offset || [8, 8];
229
264
  const size = config.size === void 0 ? 26 : config.size;
@@ -261,12 +296,14 @@ function resolveCloseButton(config, layerName, closeTop) {
261
296
  style.right = toLength(offset[0]);
262
297
  }
263
298
  Object.assign(style, toCssStyle(config.style));
264
- const onClick = () => closeTop("close-button");
299
+ withAnimation(style, css.animation(config.animate));
300
+ const className = withHover("dsl-close", config, css);
301
+ const onClick = config.action ? () => input.dispatch(config.action, context) : () => closeTop("close-button");
265
302
  if (iconMode === "image") {
266
303
  return {
267
304
  key: `${layerName}-close`,
268
305
  tag: "div",
269
- className: "dsl-close",
306
+ className,
270
307
  style,
271
308
  onClick,
272
309
  children: [
@@ -289,7 +326,7 @@ function resolveCloseButton(config, layerName, closeTop) {
289
326
  return {
290
327
  key: `${layerName}-close`,
291
328
  tag: "div",
292
- className: "dsl-close",
329
+ className,
293
330
  style,
294
331
  onClick,
295
332
  text: config.icon
@@ -298,7 +335,7 @@ function resolveCloseButton(config, layerName, closeTop) {
298
335
  return {
299
336
  key: `${layerName}-close`,
300
337
  tag: "div",
301
- className: "dsl-close",
338
+ className,
302
339
  style,
303
340
  onClick,
304
341
  children: [closeIcon(`${layerName}-close-icon`, size)]
@@ -386,6 +423,12 @@ function resolveNode(node, key, layout, context, input, countdowns) {
386
423
  layout
387
424
  )
388
425
  );
426
+ withAnimation(style, input.css.animation(node.animate));
427
+ const hoverClass = input.css.hoverClass(node);
428
+ const cls = (type, clickable) => {
429
+ const base = nodeClass(type, clickable);
430
+ return hoverClass ? `${base} ${hoverClass}` : base;
431
+ };
389
432
  const onClick = node.action ? () => input.dispatch(node.action, context) : void 0;
390
433
  if (onClick && !style.cursor) style.cursor = "pointer";
391
434
  switch (node.type) {
@@ -393,7 +436,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
393
436
  return {
394
437
  key,
395
438
  tag: "div",
396
- className: nodeClass("box", !!onClick),
439
+ className: cls("box", !!onClick),
397
440
  style: Object.assign({ position: "relative" }, style),
398
441
  onClick,
399
442
  children: resolveChildren(
@@ -409,7 +452,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
409
452
  return {
410
453
  key,
411
454
  tag: "div",
412
- className: nodeClass("flex", !!onClick),
455
+ className: cls("flex", !!onClick),
413
456
  style: Object.assign({ display: "flex" }, style),
414
457
  onClick,
415
458
  children: resolveChildren(node.children, key, "flow", context, input, countdowns)
@@ -418,7 +461,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
418
461
  return {
419
462
  key,
420
463
  tag: "div",
421
- className: nodeClass("repeat", !!onClick),
464
+ className: cls("repeat", !!onClick),
422
465
  style: Object.assign({ display: "flex" }, style),
423
466
  onClick,
424
467
  children: resolveRepeat(node, key, context, input, countdowns)
@@ -427,7 +470,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
427
470
  return {
428
471
  key,
429
472
  tag: "div",
430
- className: nodeClass("tabs", false),
473
+ className: cls("tabs", false),
431
474
  style: Object.assign({ display: "flex" }, style),
432
475
  children: resolveTabs(node, key, context, input)
433
476
  };
@@ -435,7 +478,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
435
478
  return {
436
479
  key,
437
480
  tag: "img",
438
- className: nodeClass("image", !!onClick),
481
+ className: cls("image", !!onClick),
439
482
  src: safeImageUrl(interpolate(node.src, context)),
440
483
  style: Object.assign({ display: "block", objectFit: "cover" }, style),
441
484
  onClick
@@ -444,7 +487,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
444
487
  return {
445
488
  key,
446
489
  tag: "div",
447
- className: nodeClass("text", !!onClick),
490
+ className: cls("text", !!onClick),
448
491
  style: Object.assign({ wordBreak: "break-word" }, style),
449
492
  text: toText(interpolate(node.content, context)),
450
493
  onClick
@@ -453,7 +496,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
453
496
  return {
454
497
  key,
455
498
  tag: "div",
456
- className: nodeClass("button", true),
499
+ className: cls("button", true),
457
500
  style: Object.assign(
458
501
  {
459
502
  display: "flex",
@@ -469,7 +512,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
469
512
  onClick
470
513
  };
471
514
  case "countdown":
472
- return resolveCountdown(node, key, style, context, input, countdowns);
515
+ return resolveCountdown(node, key, style, context, input, countdowns, hoverClass);
473
516
  // 配置比解释器新时,未知类型降级为不渲染,不阻断整个弹窗
474
517
  default:
475
518
  return void 0;
@@ -531,10 +574,23 @@ function resolveTabs(node, key, context, input) {
531
574
  };
532
575
  });
533
576
  }
534
- function resolveCountdown(node, key, style, context, input, countdowns) {
535
- var _a;
577
+ function resolveCountdownEndTime(node, key, context, input) {
536
578
  const endTime = parseEndTime(interpolate(node.to, context));
579
+ if (endTime > 0) return endTime;
580
+ const duration = Number(interpolate(node.duration, context));
581
+ if (!isFinite(duration) || duration <= 0) return 0;
582
+ let start = input.countdownStarts.get(key);
583
+ if (start === void 0) {
584
+ start = Date.now();
585
+ input.countdownStarts.set(key, start);
586
+ }
587
+ return start + duration;
588
+ }
589
+ function resolveCountdown(node, key, style, context, input, countdowns, hoverClass) {
590
+ var _a;
591
+ const endTime = resolveCountdownEndTime(node, key, context, input);
537
592
  const parts = computeParts(endTime);
593
+ const cls = (base) => hoverClass ? `${base} ${hoverClass}` : base;
538
594
  countdowns.endTimes.push(endTime);
539
595
  if (node.precision === "cs") countdowns.precision = "cs";
540
596
  if (endTime && node.onEnd) {
@@ -551,7 +607,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
551
607
  return {
552
608
  key,
553
609
  tag: "div",
554
- className: "dsl-node dsl-text dsl-countdown",
610
+ className: cls("dsl-node dsl-text dsl-countdown"),
555
611
  style: Object.assign({ wordBreak: "break-word" }, style),
556
612
  text: node.endText || "\u5DF2\u7ED3\u675F"
557
613
  };
@@ -560,7 +616,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
560
616
  return {
561
617
  key,
562
618
  tag: "div",
563
- className: "dsl-node dsl-text dsl-countdown",
619
+ className: cls("dsl-node dsl-text dsl-countdown"),
564
620
  style: Object.assign({ wordBreak: "break-word" }, style),
565
621
  text: formatParts(parts, node.format)
566
622
  };
@@ -569,7 +625,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
569
625
  return {
570
626
  key,
571
627
  tag: "div",
572
- className: "dsl-node dsl-flex dsl-countdown",
628
+ className: cls("dsl-node dsl-flex dsl-countdown"),
573
629
  style: Object.assign({ display: "flex" }, style),
574
630
  children: resolveChildren(
575
631
  node.children,
@@ -585,6 +641,37 @@ function toText(value) {
585
641
  return value === void 0 || value === null ? "" : String(value);
586
642
  }
587
643
 
644
+ // src/hover.ts
645
+ function collectHoverStyles(views, uid, context) {
646
+ const rules = [];
647
+ const classOf = /* @__PURE__ */ new WeakMap();
648
+ const scope = `.dsl-r${uid}`;
649
+ let seed = 0;
650
+ const register = (owner, style) => {
651
+ if (!owner || !style || typeof style !== "object") return;
652
+ const body = declarations(toCssStyle(interpolateDeep(style, context)));
653
+ if (!body) return;
654
+ seed += 1;
655
+ const className = `dsl-hv-${uid}-${seed}`;
656
+ classOf.set(owner, className);
657
+ rules.push(`${scope} .${className}:hover{${body}}`);
658
+ };
659
+ const walkNode = (node) => {
660
+ if (!node || typeof node !== "object") return;
661
+ register(node, node.hoverStyle);
662
+ (node.children || []).forEach(walkNode);
663
+ walkNode(node.template);
664
+ };
665
+ Object.keys(views).forEach((name) => {
666
+ const view = views[name];
667
+ if (!view || typeof view !== "object") return;
668
+ const closeButton = view.stage && view.stage.closeButton;
669
+ register(closeButton, closeButton && closeButton.hoverStyle);
670
+ (view.nodes || []).forEach(walkNode);
671
+ });
672
+ return { rules, classOf };
673
+ }
674
+
588
675
  // src/runtime.ts
589
676
  var noopEmit = () => {
590
677
  };
@@ -596,6 +683,7 @@ function createRuntime(dsl, options = {}) {
596
683
  editMode: !!options.editMode
597
684
  };
598
685
  const normalized = normalizeViews(dsl);
686
+ const scopeUid = nextScopeUid();
599
687
  let state = Object.assign({}, dsl.state || {});
600
688
  let resolvedData = {};
601
689
  let viewStack = [normalized.entry];
@@ -604,6 +692,7 @@ function createRuntime(dsl, options = {}) {
604
692
  let destroyed = false;
605
693
  const listeners = /* @__PURE__ */ new Set();
606
694
  const countdownEnds = /* @__PURE__ */ new Map();
695
+ const countdownStarts = /* @__PURE__ */ new Map();
607
696
  const notify = () => {
608
697
  if (destroyed) return;
609
698
  listeners.forEach((fn) => fn());
@@ -737,7 +826,9 @@ function createRuntime(dsl, options = {}) {
737
826
  });
738
827
  }
739
828
  loadData();
829
+ let hover;
740
830
  function getTree() {
831
+ if (!hover) hover = collectHoverStyles(normalized.views, scopeUid, buildContext());
741
832
  const tree = resolveTree({
742
833
  normalized,
743
834
  viewStack,
@@ -747,7 +838,11 @@ function createRuntime(dsl, options = {}) {
747
838
  dispatch,
748
839
  setState,
749
840
  closeTop,
750
- countdownEnds
841
+ countdownEnds,
842
+ countdownStarts,
843
+ scopeUid,
844
+ keyframes: dsl.keyframes,
845
+ hover
751
846
  });
752
847
  ticker.sync(tree.countdownEndTimes, tree.countdownPrecision);
753
848
  return tree;
@@ -774,4 +869,19 @@ function createRuntime(dsl, options = {}) {
774
869
  };
775
870
  }
776
871
 
777
- export { createRuntime, safeImageUrl, safeUrl };
872
+ // src/host-capabilities.ts
873
+ var HOST_SOURCE_NAMES = ["shopAchievements", "listRenewTiers"];
874
+ var HOST_HANDLER_NAMES = [];
875
+ var HOST_SOURCE_LABELS = {
876
+ shopAchievements: "\u5E97\u94FA\u6210\u679C\u7EDF\u8BA1\uFF08\u5DF2\u4E0A\u67B6/\u8FD0\u884C\u4E2D/\u767D\u5E95\u56FE\u7B49\u683C\u5B50\u6570\u7EC4\uFF09",
877
+ listRenewTiers: "\u7EED\u8D39\u4EF7\u683C\u6863\u4F4D\u5217\u8868\uFF08\u6708\u4ED8/\u5B63\u4ED8/\u534A\u5E74/\u5E74\u4ED8\uFF0C\u542B\u4E0B\u5355\u5730\u5740\uFF09"
878
+ };
879
+ var HOST_HANDLER_LABELS = {};
880
+ function isRegisteredHostSource(name) {
881
+ return HOST_SOURCE_NAMES.indexOf(name) > -1;
882
+ }
883
+ function isRegisteredHostHandler(name) {
884
+ return HOST_HANDLER_NAMES.indexOf(name) > -1;
885
+ }
886
+
887
+ export { HOST_HANDLER_LABELS, HOST_HANDLER_NAMES, HOST_SOURCE_LABELS, HOST_SOURCE_NAMES, createRuntime, isRegisteredHostHandler, isRegisteredHostSource, safeImageUrl, safeUrl };
@@ -1,4 +1,4 @@
1
- import { G as RuntimeEventName, E as DslInteractionEvent } from '../types-Bj0xyWnx.cjs';
1
+ import { I as RuntimeEventName, G as DslInteractionEvent } from '../types-8FNxDXp0.cjs';
2
2
 
3
3
  /**
4
4
  * 营销物料上报载荷类型(上报契约的唯一数据源)。
@@ -1,4 +1,4 @@
1
- import { G as RuntimeEventName, E as DslInteractionEvent } from '../types-Bj0xyWnx.js';
1
+ import { I as RuntimeEventName, G as DslInteractionEvent } from '../types-8FNxDXp0.js';
2
2
 
3
3
  /**
4
4
  * 营销物料上报载荷类型(上报契约的唯一数据源)。
@@ -15,6 +15,40 @@ type DslLength = number | string;
15
15
  type DslRect = [DslLength, DslLength, DslLength, DslLength];
16
16
  /** 样式对象,键必须在白名单内,值可以带 {{ }} */
17
17
  type DslStyle = Record<string, unknown>;
18
+ /**
19
+ * 动画声明。
20
+ *
21
+ * 只描述「播哪一组 keyframes、怎么播」,关键帧本身写在 `Dsl.keyframes` 里。
22
+ * 解释器把它落成**行内** `animation` 简写,keyframes 定义才进 `RenderTree.css`——
23
+ * 这样同一份配置在页面上出现多次也不会互相污染(keyframes 名带作用域前缀)。
24
+ */
25
+ interface DslAnimate {
26
+ /** keyframes 名:必须在 `dsl.keyframes` 里定义过 */
27
+ name: string;
28
+ /** 时长,毫秒 */
29
+ duration: number;
30
+ /** 延迟,毫秒,默认 0。交错入场就靠它错开 */
31
+ delay?: number;
32
+ /**
33
+ * 缓动函数。白名单:
34
+ * linear / ease / ease-in / ease-out / ease-in-out / step-start / step-end /
35
+ * cubic-bezier(x, x, x, x)。默认 `ease`
36
+ */
37
+ easing?: string;
38
+ /** 播放次数,默认 1;`'infinite'` 表示无限循环(呼吸、扫光这类) */
39
+ iteration?: number | 'infinite';
40
+ /** 播放方向,默认 `normal` */
41
+ direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
42
+ /** 填充模式,默认 `both`(入场动画必须保留终态,否则播完会弹回初始值) */
43
+ fill?: 'none' | 'forwards' | 'backwards' | 'both';
44
+ }
45
+ /**
46
+ * 一组关键帧:偏移量 → 样式。
47
+ *
48
+ * 偏移量只接受 `'from'` / `'to'` / `'0%'` 这三种写法;
49
+ * 样式仍要过 style 白名单,写了白名单外的属性会被丢弃。
50
+ */
51
+ type DslKeyframe = Record<string, DslStyle>;
18
52
  interface DslActionBase {
19
53
  type: string;
20
54
  }
@@ -65,6 +99,15 @@ interface DslNode {
65
99
  /** 表达式,为假时整个节点不渲染 */
66
100
  visibleWhen?: string;
67
101
  action?: DslAction;
102
+ /** 入场 / 循环动画 */
103
+ animate?: DslAnimate;
104
+ /**
105
+ * 鼠标悬停时叠加的样式。
106
+ *
107
+ * **只会生成 CSS 规则,不进行内联**——行内样式没有 `:hover` 这个东西,
108
+ * 想在行内做只能靠 JS 监听鼠标事件,那既要框架壳配合、又天然丢触屏。
109
+ */
110
+ hoverStyle?: DslStyle;
68
111
  /** box / flex / countdown */
69
112
  children?: DslNode[];
70
113
  /** repeat / tabs:数组来源表达式 */
@@ -86,6 +129,14 @@ interface DslNode {
86
129
  text?: string;
87
130
  /** countdown */
88
131
  to?: string;
132
+ /**
133
+ * countdown 的相对时长(毫秒)。
134
+ *
135
+ * 从**该视图展示那一刻**起算,「每次打开重新倒 5 分钟」这类写法用它。
136
+ * 与 `to` 同时存在时 `to` 优先(绝对时间更明确)。
137
+ * 起算时间戳钉在运行时里,重算渲染树不会把剩余时间刷回去。
138
+ */
139
+ duration?: number;
89
140
  precision?: 's' | 'cs';
90
141
  as?: string;
91
142
  format?: string;
@@ -95,6 +146,15 @@ interface DslNode {
95
146
  type DslClosePosition = 'top-right' | 'top-left' | 'top-center' | 'bottom-right' | 'bottom-left' | 'bottom-center';
96
147
  interface DslCloseButton {
97
148
  show?: boolean;
149
+ /**
150
+ * 点它做什么。
151
+ *
152
+ * 不配就是默认行为「关掉当前这一层」(多视图里等于弹栈退回上一层,单视图即整体关闭)。
153
+ * 配了就**只**执行这个动作、不再自动关层 —— 「点 ✕ 先弹二次挽留」正是靠它:
154
+ * `{ "type": "open", "view": "confirm", "mode": "stack" }`。
155
+ * 想在动作里顺带关掉,自己写 `close` / `closeAll` 进去。
156
+ */
157
+ action?: DslAction;
98
158
  position?: DslClosePosition;
99
159
  /** [x, y],负值把按钮移到弹窗外面 */
100
160
  offset?: [DslLength, DslLength];
@@ -102,6 +162,10 @@ interface DslCloseButton {
102
162
  icon?: string;
103
163
  image?: string;
104
164
  style?: DslStyle;
165
+ /** 悬停样式,只生成 CSS 规则 */
166
+ hoverStyle?: DslStyle;
167
+ /** 关闭按钮自己的动画(如延迟淡入,先让用户看内容) */
168
+ animate?: DslAnimate;
105
169
  }
106
170
  interface DslStage {
107
171
  width?: DslLength;
@@ -110,8 +174,30 @@ interface DslStage {
110
174
  layout?: 'absolute' | 'flow';
111
175
  mask?: boolean;
112
176
  maskClosable?: boolean;
177
+ /**
178
+ * 点遮罩做什么。
179
+ *
180
+ * 不配就是默认行为「关掉当前这一层」。配了就执行这个动作而不关层
181
+ * (原页面点遮罩弹二次挽留就是这么来的)。
182
+ *
183
+ * 与 `maskClosable` 各管一件事、不冲突:`maskClosable: false` 时遮罩根本不响应点击,
184
+ * 这个动作也不会执行;`maskAction` 只决定「点了之后做什么」。
185
+ */
186
+ maskAction?: DslAction;
113
187
  closeButton?: DslCloseButton;
188
+ /**
189
+ * 遮罩自身样式,走 style 白名单。
190
+ *
191
+ * 内置默认是 `rgba(0, 0, 0, 0.55)` 纯色,这里配的会**覆盖**默认值
192
+ * (常用 `background` 改深浅、`backdropFilter: 'blur(4px)'` 加毛玻璃)。
193
+ * `maskAnimate` 生成的 `animation` 仍排在最后,不会被这里的配置盖掉。
194
+ */
195
+ maskStyle?: DslStyle;
114
196
  style?: DslStyle;
197
+ /** 舞台入场动画(弹窗主体的弹入 / 放大) */
198
+ animate?: DslAnimate;
199
+ /** 遮罩入场动画(一般是淡入,和舞台分开配才能各走各的时长) */
200
+ maskAnimate?: DslAnimate;
115
201
  /** 曝光埋点:数据就绪、视图真正展示时触发一次 */
116
202
  onShow?: DslAction;
117
203
  /** 关闭埋点:点 X、点遮罩、close 动作、被 replace 顶掉都会触发 */
@@ -145,6 +231,13 @@ interface Dsl extends Partial<DslView> {
145
231
  derived?: Record<string, DslDerived>;
146
232
  views?: Record<string, DslView>;
147
233
  entry?: string;
234
+ /**
235
+ * 关键帧库,`{ 名字: { 偏移量: 样式 } }`。
236
+ *
237
+ * 全配置共用一份(多视图之间也共用),`animate.name` 引用这里的名字。
238
+ * 名字只能是 `^[a-zA-Z][a-zA-Z0-9_-]{0,31}$`——它会被拼进 CSS 文本,必须可控。
239
+ */
240
+ keyframes?: Record<string, DslKeyframe>;
148
241
  }
149
242
  /** 已经算好的行内样式,键是 CSS 属性名(驼峰),值是最终字符串 */
150
243
  type CssStyle = Record<string, string>;
@@ -198,8 +291,15 @@ interface RenderLayer {
198
291
  name: string;
199
292
  type: DslViewType;
200
293
  isTop: boolean;
201
- /** 画不画遮罩。只有栈顶的弹窗层才为 true */
294
+ /**
295
+ * 画不画遮罩元素。弹窗层恒为 true(`stage.mask: false` 除外),**包括非栈顶的层**。
296
+ *
297
+ * 遮挡效果由 `maskStyle` 控制:非栈顶层的底色是 transparent,实际遮挡永远只有栈顶那层
298
+ * (两层实心遮罩叠加会明显变黑)。元素本身常驻是刻意的 —— 按 isTop 增删这个 DOM 节点,
299
+ * 会让下层的 `maskAnimate` 在退回栈顶时重播一遍,表现为「关掉二次挽留层,背景闪了一下」。
300
+ */
202
301
  mask: boolean;
302
+ /** 点遮罩的回调。只有栈顶那层才有:非栈顶层整层 pointerEvents: none,点不到 */
203
303
  onMaskClick?: () => void;
204
304
  layerStyle: CssStyle;
205
305
  maskStyle: CssStyle;
@@ -227,6 +327,20 @@ interface RenderTree {
227
327
  countdownEndTimes: number[];
228
328
  /** 只要有一个倒计时要厘秒,整体就按厘秒刷 */
229
329
  countdownPrecision: 's' | 'cs';
330
+ /**
331
+ * 这份配置要注入页面的全部 CSS 文本(keyframes 定义 + hover 规则 + reduced-motion 兜底)。
332
+ *
333
+ * 没有动画也没有 hover 时是**空串**,此时框架壳不该建 `<style>` 元素。
334
+ * 内容已经过安全过滤,壳直接塞进 `<style>` 即可,**不得在壳里做任何生成或判断**。
335
+ */
336
+ css: string;
337
+ /**
338
+ * 根容器类名,形如 `dsl-renderer dsl-r3`。
339
+ *
340
+ * 后半截是这个运行时实例的作用域前缀,`css` 里的每条规则都挂在它下面——
341
+ * 同一个页面上出现多份物料时,各自的 hover 规则不会串台。
342
+ */
343
+ rootClassName: string;
230
344
  }
231
345
  type DslSource = (params: Record<string, unknown>, user: Record<string, unknown>) => unknown | Promise<unknown>;
232
346
  type DslHandler = (params: Record<string, unknown>) => void;
@@ -342,4 +456,4 @@ interface RuntimeOptions {
342
456
  emit?: RuntimeEmit;
343
457
  }
344
458
 
345
- export type { DslHandler as A, DslInteractionTrigger as B, CssStyle as C, Dsl as D, DslInteractionEvent as E, RuntimeEvents as F, RuntimeEventName as G, RuntimeEmit as H, RuntimeOptions as R, RenderTree as a, DslView as b, DslStyle as c, DslViewType as d, DslNodeType as e, DslLength as f, DslRect as g, DslActionBase as h, DslNavigateAction as i, DslCloseAction as j, DslCloseAllAction as k, DslOpenAction as l, DslSetStateAction as m, DslTrackAction as n, DslCallAction as o, DslSequenceAction as p, DslAction as q, DslNode as r, DslClosePosition as s, DslCloseButton as t, DslStage as u, DslSourceRef as v, DslDerived as w, RenderElement as x, RenderLayer as y, DslSource as z };
459
+ export type { RenderLayer as A, DslSource as B, CssStyle as C, Dsl as D, DslHandler as E, DslInteractionTrigger as F, DslInteractionEvent as G, RuntimeEvents as H, RuntimeEventName as I, RuntimeEmit as J, RuntimeOptions as R, RenderTree as a, DslView as b, DslStyle as c, DslViewType as d, DslNodeType as e, DslLength as f, DslRect as g, DslAnimate as h, DslKeyframe as i, DslActionBase as j, DslNavigateAction as k, DslCloseAction as l, DslCloseAllAction as m, DslOpenAction as n, DslSetStateAction as o, DslTrackAction as p, DslCallAction as q, DslSequenceAction as r, DslAction as s, DslNode as t, DslClosePosition as u, DslCloseButton as v, DslStage as w, DslSourceRef as x, DslDerived as y, RenderElement as z };