@yaoxiu/marketing-dsl 2.3.0 → 2.5.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.cjs CHANGED
@@ -541,6 +541,11 @@ function isLength(value) {
541
541
  if (/^calc\([^;{}]*\)$/.test(trimmed)) return true;
542
542
  return LENGTH_RE.test(trimmed);
543
543
  }
544
+ function clampZIndex(value) {
545
+ const num = typeof value === "number" ? value : parseFloat(String(value));
546
+ if (!isFinite(num)) return 0;
547
+ return Math.min(10, Math.max(0, Math.round(num)));
548
+ }
544
549
  var STYLE_MAP = {
545
550
  // 文字
546
551
  color: ["color", String],
@@ -593,6 +598,26 @@ var STYLE_MAP = {
593
598
  minHeight: ["minHeight", px],
594
599
  maxWidth: ["maxWidth", px],
595
600
  maxHeight: ["maxHeight", px],
601
+ // 动效相关。transform / transition 是做位移、缩放、hover 过渡的最小必需集;
602
+ // 不放开 position / display / grid —— rect 已经提供绝对定位,栅格用 flex wrap + 百分比宽复刻,
603
+ // 放开 position 等于把「全屏覆盖」这类越权手段又交回给配置。
604
+ transform: ["transform", String],
605
+ transformOrigin: ["transformOrigin", String],
606
+ transition: ["transition", String],
607
+ // 只为 hoverStyle 而放开:`animationPlayState: 'paused'` 才能做到「悬停时暂停呼吸动画」。
608
+ // 写进普通 style 也合法(等于一开始就暂停),但那基本没有意义。
609
+ animationPlayState: ["animationPlayState", String],
610
+ // 毛玻璃。Safari 至今只认带前缀的写法,所以两个键一起输出,见 EXTRA_CSS_KEYS
611
+ backdropFilter: ["backdropFilter", String],
612
+ textShadow: ["textShadow", String],
613
+ // 只允许 0~10:越权提层是弹窗类配置最容易做的坏事(盖住宿主自己的导航 / 收银台),
614
+ // 层内排布十层绰绰有余,真需要压过页面其它内容的是弹窗层,那由解释器自己给 1000
615
+ zIndex: ["zIndex", (v) => String(clampZIndex(v))],
616
+ // 装饰性图层(光斑、扫光)必须让点击穿透,否则会挡住下面的按钮
617
+ pointerEvents: ["pointerEvents", String],
618
+ // 横向排布里「这一项不许被压扁」,图标位和数字位常用
619
+ flexShrink: ["flexShrink", String],
620
+ fontStyle: ["fontStyle", String],
596
621
  // 图片
597
622
  objectFit: ["objectFit", String],
598
623
  // 宽度给百分比、高度按比例算。整图 banner 的热区必须靠它才能用百分比定位:
@@ -600,6 +625,9 @@ var STYLE_MAP = {
600
625
  aspectRatio: ["aspectRatio", String]
601
626
  };
602
627
  var ALLOWED_STYLE_KEYS = Object.keys(STYLE_MAP);
628
+ var EXTRA_CSS_KEYS = {
629
+ backdropFilter: ["WebkitBackdropFilter"]
630
+ };
603
631
  function toCssStyle(style) {
604
632
  const css = {};
605
633
  if (!style) return css;
@@ -608,7 +636,11 @@ function toCssStyle(style) {
608
636
  if (!rule) return;
609
637
  const value = style[key];
610
638
  if (value === void 0 || value === null || value === "") return;
611
- css[rule[0]] = rule[1](value);
639
+ const result = rule[1](value);
640
+ css[rule[0]] = result;
641
+ (EXTRA_CSS_KEYS[key] || []).forEach((extra) => {
642
+ css[extra] = result;
643
+ });
612
644
  });
613
645
  return css;
614
646
  }
@@ -621,27 +653,128 @@ function toRectStyle(rect) {
621
653
  return css;
622
654
  }
623
655
  function resolveNodeStyle(node, layout) {
624
- return Object.assign(
656
+ const css = Object.assign(
625
657
  {},
626
658
  layout === "absolute" ? toRectStyle(node.rect) : {},
627
659
  toCssStyle(node.style)
628
660
  );
661
+ if (css.zIndex !== void 0 && !css.position) css.position = "relative";
662
+ return css;
663
+ }
664
+
665
+ // src/css-value.ts
666
+ function sanitizeCssValue(value) {
667
+ if (value === void 0 || value === null) return null;
668
+ const text = String(value).trim();
669
+ if (!text) return null;
670
+ if (/[{}<>;@]/.test(text)) return null;
671
+ if (text.indexOf("/*") > -1 || text.indexOf("*/") > -1) return null;
672
+ if (text.indexOf("\\") > -1) return null;
673
+ return text;
674
+ }
675
+ function toCssProp(key) {
676
+ return key.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
677
+ }
678
+ function declarations(style) {
679
+ return Object.keys(style).map((key) => {
680
+ const value = sanitizeCssValue(style[key]);
681
+ return value === null ? "" : `${toCssProp(key)}:${value}`;
682
+ }).filter(Boolean).join(";");
683
+ }
684
+
685
+ // src/css.ts
686
+ var CSS_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,31}$/;
687
+ var ALLOWED_EASINGS = [
688
+ "linear",
689
+ "ease",
690
+ "ease-in",
691
+ "ease-out",
692
+ "ease-in-out",
693
+ "step-start",
694
+ "step-end"
695
+ ];
696
+ var CUBIC_BEZIER_RE = /^cubic-bezier\(\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*\)$/;
697
+ var OFFSET_RE = /^(from|to|\d+(\.\d+)?%)$/;
698
+ function isValidEasing(value) {
699
+ if (typeof value !== "string") return false;
700
+ const trimmed = value.trim();
701
+ return ALLOWED_EASINGS.indexOf(trimmed) > -1 || CUBIC_BEZIER_RE.test(trimmed);
702
+ }
703
+ function isKeyframeOffset(value) {
704
+ return typeof value === "string" && OFFSET_RE.test(value.trim());
705
+ }
706
+ var scopeSeed = 0;
707
+ function nextScopeUid() {
708
+ scopeSeed += 1;
709
+ return scopeSeed;
710
+ }
711
+ function createCssBuilder(uid, keyframes, hover) {
712
+ const rootClassName = `dsl-renderer dsl-r${uid}`;
713
+ const scope = `.dsl-r${uid}`;
714
+ const defined = keyframes && typeof keyframes === "object" ? keyframes : {};
715
+ const hoverRules = hover ? hover.rules : [];
716
+ function scopedKeyframeName(name) {
717
+ return `dsl-kf-${uid}-${name}`;
718
+ }
719
+ function animation(animate) {
720
+ if (!animate || typeof animate !== "object") return void 0;
721
+ const name = animate.name;
722
+ if (typeof name !== "string" || !CSS_NAME_PATTERN.test(name)) return void 0;
723
+ if (!Object.prototype.hasOwnProperty.call(defined, name)) return void 0;
724
+ const duration = toMs(animate.duration, 0);
725
+ if (duration <= 0) return void 0;
726
+ const delay = toMs(animate.delay, 0);
727
+ const easing = isValidEasing(animate.easing) ? String(animate.easing).trim() : "ease";
728
+ const iteration = animate.iteration === "infinite" ? "infinite" : String(Math.max(1, Math.round(Number(animate.iteration) || 1)));
729
+ const direction = pick(animate.direction, DIRECTIONS, "normal");
730
+ const fill = pick(animate.fill, FILLS, "both");
731
+ return `${scopedKeyframeName(name)} ${duration}ms ${easing} ${delay}ms ${iteration} ${direction} ${fill}`;
732
+ }
733
+ function hoverClass(owner) {
734
+ return owner && hover ? hover.classOf.get(owner) : void 0;
735
+ }
736
+ function build() {
737
+ const rules = keyframeRules().concat(hoverRules);
738
+ if (!rules.length) return "";
739
+ rules.push(
740
+ `@media (prefers-reduced-motion: reduce){${scope} *{animation:none!important;transition:none!important}}`
741
+ );
742
+ return rules.join("\n");
743
+ }
744
+ function keyframeRules() {
745
+ return Object.keys(defined).filter((name) => CSS_NAME_PATTERN.test(name)).map((name) => {
746
+ const frames = defined[name];
747
+ if (!frames || typeof frames !== "object") return "";
748
+ const body = Object.keys(frames).filter(isKeyframeOffset).map((offset) => {
749
+ const decl = declarations(toCssStyle(frames[offset]));
750
+ return decl ? `${offset.trim()}{${decl}}` : "";
751
+ }).filter(Boolean).join("");
752
+ return body ? `@keyframes ${scopedKeyframeName(name)}{${body}}` : "";
753
+ }).filter(Boolean);
754
+ }
755
+ return { uid, rootClassName, animation, hoverClass, build };
756
+ }
757
+ var DIRECTIONS = ["normal", "reverse", "alternate", "alternate-reverse"];
758
+ var FILLS = ["none", "forwards", "backwards", "both"];
759
+ function pick(value, allowed, fallback) {
760
+ return typeof value === "string" && allowed.indexOf(value) > -1 ? value : fallback;
761
+ }
762
+ function toMs(value, fallback) {
763
+ const num = Number(value);
764
+ if (!isFinite(num) || num < 0) return fallback;
765
+ return Math.round(num);
629
766
  }
630
767
 
631
768
  // src/resolve.ts
632
769
  function resolveTree(input) {
633
770
  const { normalized, viewStack, ready } = input;
771
+ const css = createCssBuilder(input.scopeUid, input.keyframes, input.hover);
772
+ const pass = Object.assign({}, input, { css });
634
773
  const layers = viewStack.map((name) => ({ name, view: normalized.views[name] })).filter((item) => !!item.view);
635
774
  const hasPopup = layers.some((item) => item.view.type === "popup");
636
775
  const countdowns = { endTimes: [], precision: "s" };
637
776
  const renderLayers = ready ? layers.map(
638
- (item, index) => resolveLayer(
639
- item.name,
640
- item.view,
641
- index === layers.length - 1,
642
- input,
643
- countdowns
644
- )
777
+ (item, index) => resolveLayer(item.name, item.view, index === layers.length - 1, pass, countdowns)
645
778
  ) : [];
646
779
  return {
647
780
  ready: ready && layers.length > 0,
@@ -652,7 +785,10 @@ function resolveTree(input) {
652
785
  rootStyle: { position: "relative" },
653
786
  layers: renderLayers,
654
787
  countdownEndTimes: countdowns.endTimes,
655
- countdownPrecision: countdowns.precision
788
+ countdownPrecision: countdowns.precision,
789
+ // build 必须在 layers 算完之后调:hover 规则是遍历节点时才登记进来的
790
+ css: css.build(),
791
+ rootClassName: css.rootClassName
656
792
  };
657
793
  }
658
794
  function resolveLayer(name, view, isTop, input, countdowns) {
@@ -661,7 +797,7 @@ function resolveLayer(name, view, isTop, input, countdowns) {
661
797
  const isPopup = view.type === "popup";
662
798
  const rootLayout = stage.layout === "flow" ? "flow" : "absolute";
663
799
  const maskClosable = !!stage.maskClosable;
664
- const onMaskClick = maskClosable ? () => closeTop("mask") : void 0;
800
+ const onMaskClick = maskClosable ? stage.maskAction ? () => input.dispatch(stage.maskAction, context) : () => closeTop("mask") : void 0;
665
801
  const nodes = (view.nodes || []).map(
666
802
  (node, index) => resolveNode(node, `${name}-${index}`, rootLayout, context, input, countdowns)
667
803
  ).filter((el) => !!el);
@@ -669,20 +805,26 @@ function resolveLayer(name, view, isTop, input, countdowns) {
669
805
  name,
670
806
  type: view.type,
671
807
  isTop,
672
- // 只有栈顶那层画遮罩,否则两层遮罩叠加会明显变黑
673
- mask: isPopup && stage.mask !== false && isTop,
674
- onMaskClick,
808
+ /**
809
+ * 弹窗层恒画遮罩元素,**非栈顶时只是变透明**(见下方 maskStyle)。
810
+ *
811
+ * 【为什么不是「只有栈顶才画」】那样写视觉上也对(两层实心遮罩叠加会明显变黑,
812
+ * 所以下层必须让位),但下层的遮罩 DOM 会被整个移除、退回栈顶时再新建一个 ——
813
+ * 新元素身上的 `maskAnimate` 于是又播一遍:关掉二次挽留层回到主弹窗时,
814
+ * 背景会重新淡入一次,看起来就是「闪了一下」。元素常驻就没这问题。
815
+ */
816
+ mask: isPopup && stage.mask !== false,
817
+ // 非栈顶层整层 pointerEvents: none,遮罩点不到,这里也就不必给它挂回调
818
+ onMaskClick: isTop ? onMaskClick : void 0,
675
819
  layerStyle: layerStyle(isPopup, isTop),
676
- maskStyle: {
677
- position: "absolute",
678
- top: "0",
679
- right: "0",
680
- bottom: "0",
681
- left: "0",
682
- background: "rgba(0, 0, 0, 0.55)"
683
- },
820
+ // 合并顺序固定:内置默认 → stage.maskStyle 覆盖 → maskAnimate 的 animation 收尾。
821
+ // 遮罩样式也走一遍插值,切 tab 时能跟着变(同 stage.style)
822
+ maskStyle: maskStyle(stage, isTop, context, input),
684
823
  scrollStyle: scrollStyle(isPopup),
685
- stageStyle: resolveStageStyle(stage, isPopup, context),
824
+ stageStyle: withAnimation(
825
+ resolveStageStyle(stage, isPopup, context),
826
+ input.css.animation(stage.animate)
827
+ ),
686
828
  clipStyle: {
687
829
  position: "relative",
688
830
  height: "100%",
@@ -690,10 +832,39 @@ function resolveLayer(name, view, isTop, input, countdowns) {
690
832
  overflow: "hidden",
691
833
  boxSizing: "border-box"
692
834
  },
693
- closeButton: resolveCloseButton(stage.closeButton, name, closeTop),
835
+ closeButton: resolveCloseButton(stage.closeButton, name, closeTop, input, context),
694
836
  nodes
695
837
  };
696
838
  }
839
+ function withAnimation(style, animation) {
840
+ if (animation) style.animation = animation;
841
+ return style;
842
+ }
843
+ function maskStyle(stage, isTop, context, input) {
844
+ const style = withAnimation(
845
+ Object.assign(
846
+ {
847
+ position: "absolute",
848
+ top: "0",
849
+ right: "0",
850
+ bottom: "0",
851
+ left: "0",
852
+ background: "rgba(0, 0, 0, 0.55)"
853
+ },
854
+ toCssStyle(interpolateDeep(stage.maskStyle, context))
855
+ ),
856
+ input.css.animation(stage.maskAnimate)
857
+ );
858
+ if (isTop) return style;
859
+ style.background = "transparent";
860
+ delete style.backdropFilter;
861
+ delete style.WebkitBackdropFilter;
862
+ return style;
863
+ }
864
+ function withHover(className, owner, css) {
865
+ const hoverClass = css.hoverClass(owner);
866
+ return hoverClass ? `${className} ${hoverClass}` : className;
867
+ }
697
868
  function layerStyle(isPopup, isTop) {
698
869
  const style = isPopup ? {
699
870
  position: "fixed",
@@ -751,7 +922,8 @@ function resolveStageStyle(stage, isPopup, context) {
751
922
  }
752
923
  return style;
753
924
  }
754
- function resolveCloseButton(config, layerName, closeTop) {
925
+ function resolveCloseButton(config, layerName, closeTop, input, context) {
926
+ const css = input.css;
755
927
  if (!config || config.show === false) return void 0;
756
928
  const offset = config.offset || [8, 8];
757
929
  const size = config.size === void 0 ? 26 : config.size;
@@ -789,12 +961,14 @@ function resolveCloseButton(config, layerName, closeTop) {
789
961
  style.right = toLength(offset[0]);
790
962
  }
791
963
  Object.assign(style, toCssStyle(config.style));
792
- const onClick = () => closeTop("close-button");
964
+ withAnimation(style, css.animation(config.animate));
965
+ const className = withHover("dsl-close", config, css);
966
+ const onClick = config.action ? () => input.dispatch(config.action, context) : () => closeTop("close-button");
793
967
  if (iconMode === "image") {
794
968
  return {
795
969
  key: `${layerName}-close`,
796
970
  tag: "div",
797
- className: "dsl-close",
971
+ className,
798
972
  style,
799
973
  onClick,
800
974
  children: [
@@ -817,7 +991,7 @@ function resolveCloseButton(config, layerName, closeTop) {
817
991
  return {
818
992
  key: `${layerName}-close`,
819
993
  tag: "div",
820
- className: "dsl-close",
994
+ className,
821
995
  style,
822
996
  onClick,
823
997
  text: config.icon
@@ -826,7 +1000,7 @@ function resolveCloseButton(config, layerName, closeTop) {
826
1000
  return {
827
1001
  key: `${layerName}-close`,
828
1002
  tag: "div",
829
- className: "dsl-close",
1003
+ className,
830
1004
  style,
831
1005
  onClick,
832
1006
  children: [closeIcon(`${layerName}-close-icon`, size)]
@@ -914,6 +1088,12 @@ function resolveNode(node, key, layout, context, input, countdowns) {
914
1088
  layout
915
1089
  )
916
1090
  );
1091
+ withAnimation(style, input.css.animation(node.animate));
1092
+ const hoverClass = input.css.hoverClass(node);
1093
+ const cls = (type, clickable) => {
1094
+ const base = nodeClass(type, clickable);
1095
+ return hoverClass ? `${base} ${hoverClass}` : base;
1096
+ };
917
1097
  const onClick = node.action ? () => input.dispatch(node.action, context) : void 0;
918
1098
  if (onClick && !style.cursor) style.cursor = "pointer";
919
1099
  switch (node.type) {
@@ -921,7 +1101,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
921
1101
  return {
922
1102
  key,
923
1103
  tag: "div",
924
- className: nodeClass("box", !!onClick),
1104
+ className: cls("box", !!onClick),
925
1105
  style: Object.assign({ position: "relative" }, style),
926
1106
  onClick,
927
1107
  children: resolveChildren(
@@ -937,7 +1117,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
937
1117
  return {
938
1118
  key,
939
1119
  tag: "div",
940
- className: nodeClass("flex", !!onClick),
1120
+ className: cls("flex", !!onClick),
941
1121
  style: Object.assign({ display: "flex" }, style),
942
1122
  onClick,
943
1123
  children: resolveChildren(node.children, key, "flow", context, input, countdowns)
@@ -946,7 +1126,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
946
1126
  return {
947
1127
  key,
948
1128
  tag: "div",
949
- className: nodeClass("repeat", !!onClick),
1129
+ className: cls("repeat", !!onClick),
950
1130
  style: Object.assign({ display: "flex" }, style),
951
1131
  onClick,
952
1132
  children: resolveRepeat(node, key, context, input, countdowns)
@@ -955,7 +1135,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
955
1135
  return {
956
1136
  key,
957
1137
  tag: "div",
958
- className: nodeClass("tabs", false),
1138
+ className: cls("tabs", false),
959
1139
  style: Object.assign({ display: "flex" }, style),
960
1140
  children: resolveTabs(node, key, context, input)
961
1141
  };
@@ -963,7 +1143,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
963
1143
  return {
964
1144
  key,
965
1145
  tag: "img",
966
- className: nodeClass("image", !!onClick),
1146
+ className: cls("image", !!onClick),
967
1147
  src: safeImageUrl(interpolate(node.src, context)),
968
1148
  style: Object.assign({ display: "block", objectFit: "cover" }, style),
969
1149
  onClick
@@ -972,7 +1152,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
972
1152
  return {
973
1153
  key,
974
1154
  tag: "div",
975
- className: nodeClass("text", !!onClick),
1155
+ className: cls("text", !!onClick),
976
1156
  style: Object.assign({ wordBreak: "break-word" }, style),
977
1157
  text: toText(interpolate(node.content, context)),
978
1158
  onClick
@@ -981,7 +1161,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
981
1161
  return {
982
1162
  key,
983
1163
  tag: "div",
984
- className: nodeClass("button", true),
1164
+ className: cls("button", true),
985
1165
  style: Object.assign(
986
1166
  {
987
1167
  display: "flex",
@@ -997,7 +1177,7 @@ function resolveNode(node, key, layout, context, input, countdowns) {
997
1177
  onClick
998
1178
  };
999
1179
  case "countdown":
1000
- return resolveCountdown(node, key, style, context, input, countdowns);
1180
+ return resolveCountdown(node, key, style, context, input, countdowns, hoverClass);
1001
1181
  // 配置比解释器新时,未知类型降级为不渲染,不阻断整个弹窗
1002
1182
  default:
1003
1183
  return void 0;
@@ -1059,10 +1239,23 @@ function resolveTabs(node, key, context, input) {
1059
1239
  };
1060
1240
  });
1061
1241
  }
1062
- function resolveCountdown(node, key, style, context, input, countdowns) {
1063
- var _a;
1242
+ function resolveCountdownEndTime(node, key, context, input) {
1064
1243
  const endTime = parseEndTime(interpolate(node.to, context));
1244
+ if (endTime > 0) return endTime;
1245
+ const duration = Number(interpolate(node.duration, context));
1246
+ if (!isFinite(duration) || duration <= 0) return 0;
1247
+ let start = input.countdownStarts.get(key);
1248
+ if (start === void 0) {
1249
+ start = Date.now();
1250
+ input.countdownStarts.set(key, start);
1251
+ }
1252
+ return start + duration;
1253
+ }
1254
+ function resolveCountdown(node, key, style, context, input, countdowns, hoverClass) {
1255
+ var _a;
1256
+ const endTime = resolveCountdownEndTime(node, key, context, input);
1065
1257
  const parts = computeParts(endTime);
1258
+ const cls = (base) => hoverClass ? `${base} ${hoverClass}` : base;
1066
1259
  countdowns.endTimes.push(endTime);
1067
1260
  if (node.precision === "cs") countdowns.precision = "cs";
1068
1261
  if (endTime && node.onEnd) {
@@ -1079,7 +1272,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
1079
1272
  return {
1080
1273
  key,
1081
1274
  tag: "div",
1082
- className: "dsl-node dsl-text dsl-countdown",
1275
+ className: cls("dsl-node dsl-text dsl-countdown"),
1083
1276
  style: Object.assign({ wordBreak: "break-word" }, style),
1084
1277
  text: node.endText || "\u5DF2\u7ED3\u675F"
1085
1278
  };
@@ -1088,7 +1281,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
1088
1281
  return {
1089
1282
  key,
1090
1283
  tag: "div",
1091
- className: "dsl-node dsl-text dsl-countdown",
1284
+ className: cls("dsl-node dsl-text dsl-countdown"),
1092
1285
  style: Object.assign({ wordBreak: "break-word" }, style),
1093
1286
  text: formatParts(parts, node.format)
1094
1287
  };
@@ -1097,7 +1290,7 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
1097
1290
  return {
1098
1291
  key,
1099
1292
  tag: "div",
1100
- className: "dsl-node dsl-flex dsl-countdown",
1293
+ className: cls("dsl-node dsl-flex dsl-countdown"),
1101
1294
  style: Object.assign({ display: "flex" }, style),
1102
1295
  children: resolveChildren(
1103
1296
  node.children,
@@ -1113,6 +1306,37 @@ function toText(value) {
1113
1306
  return value === void 0 || value === null ? "" : String(value);
1114
1307
  }
1115
1308
 
1309
+ // src/hover.ts
1310
+ function collectHoverStyles(views, uid, context) {
1311
+ const rules = [];
1312
+ const classOf = /* @__PURE__ */ new WeakMap();
1313
+ const scope = `.dsl-r${uid}`;
1314
+ let seed = 0;
1315
+ const register = (owner, style) => {
1316
+ if (!owner || !style || typeof style !== "object") return;
1317
+ const body = declarations(toCssStyle(interpolateDeep(style, context)));
1318
+ if (!body) return;
1319
+ seed += 1;
1320
+ const className = `dsl-hv-${uid}-${seed}`;
1321
+ classOf.set(owner, className);
1322
+ rules.push(`${scope} .${className}:hover{${body}}`);
1323
+ };
1324
+ const walkNode = (node) => {
1325
+ if (!node || typeof node !== "object") return;
1326
+ register(node, node.hoverStyle);
1327
+ (node.children || []).forEach(walkNode);
1328
+ walkNode(node.template);
1329
+ };
1330
+ Object.keys(views).forEach((name) => {
1331
+ const view = views[name];
1332
+ if (!view || typeof view !== "object") return;
1333
+ const closeButton = view.stage && view.stage.closeButton;
1334
+ register(closeButton, closeButton && closeButton.hoverStyle);
1335
+ (view.nodes || []).forEach(walkNode);
1336
+ });
1337
+ return { rules, classOf };
1338
+ }
1339
+
1116
1340
  // src/runtime.ts
1117
1341
  var noopEmit = () => {
1118
1342
  };
@@ -1124,6 +1348,7 @@ function createRuntime(dsl, options = {}) {
1124
1348
  editMode: !!options.editMode
1125
1349
  };
1126
1350
  const normalized = normalizeViews(dsl);
1351
+ const scopeUid = nextScopeUid();
1127
1352
  let state = Object.assign({}, dsl.state || {});
1128
1353
  let resolvedData = {};
1129
1354
  let viewStack = [normalized.entry];
@@ -1132,6 +1357,7 @@ function createRuntime(dsl, options = {}) {
1132
1357
  let destroyed = false;
1133
1358
  const listeners = /* @__PURE__ */ new Set();
1134
1359
  const countdownEnds = /* @__PURE__ */ new Map();
1360
+ const countdownStarts = /* @__PURE__ */ new Map();
1135
1361
  const notify = () => {
1136
1362
  if (destroyed) return;
1137
1363
  listeners.forEach((fn) => fn());
@@ -1265,7 +1491,9 @@ function createRuntime(dsl, options = {}) {
1265
1491
  });
1266
1492
  }
1267
1493
  loadData();
1494
+ let hover;
1268
1495
  function getTree() {
1496
+ if (!hover) hover = collectHoverStyles(normalized.views, scopeUid, buildContext());
1269
1497
  const tree = resolveTree({
1270
1498
  normalized,
1271
1499
  viewStack,
@@ -1275,7 +1503,11 @@ function createRuntime(dsl, options = {}) {
1275
1503
  dispatch,
1276
1504
  setState,
1277
1505
  closeTop,
1278
- countdownEnds
1506
+ countdownEnds,
1507
+ countdownStarts,
1508
+ scopeUid,
1509
+ keyframes: dsl.keyframes,
1510
+ hover
1279
1511
  });
1280
1512
  ticker.sync(tree.countdownEndTimes, tree.countdownPrecision);
1281
1513
  return tree;
@@ -1418,6 +1650,116 @@ function validateUserFields(dsl, allowed) {
1418
1650
  return issues;
1419
1651
  }
1420
1652
 
1653
+ // src/validate-animate.ts
1654
+ function validateKeyframes(keyframes, add, warn) {
1655
+ if (keyframes === void 0) return [];
1656
+ if (typeof keyframes !== "object" || keyframes === null || Array.isArray(keyframes)) {
1657
+ add(
1658
+ "keyframes",
1659
+ 'keyframes \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5F62\u5982 { "fadeIn": { "from": {...}, "to": {...} } }'
1660
+ );
1661
+ return [];
1662
+ }
1663
+ const names = Object.keys(keyframes);
1664
+ names.forEach((name) => {
1665
+ const at = `keyframes.${name}`;
1666
+ if (!CSS_NAME_PATTERN.test(name)) {
1667
+ add(
1668
+ at,
1669
+ `keyframes \u540D "${name}" \u4E0D\u5408\u89C4\uFF1A\u5B57\u6BCD\u5F00\u5934\uFF0C\u53EA\u80FD\u7528\u5B57\u6BCD / \u6570\u5B57 / \u4E0B\u5212\u7EBF / \u4E2D\u5212\u7EBF\uFF0C\u6700\u957F 32 \u4E2A\u5B57\u7B26`
1670
+ );
1671
+ }
1672
+ const frames = keyframes[name];
1673
+ if (!frames || typeof frames !== "object" || Array.isArray(frames)) {
1674
+ add(at, "\u6BCF\u7EC4 keyframes \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u952E\u662F\u504F\u79FB\u91CF\u3001\u503C\u662F\u6837\u5F0F");
1675
+ return;
1676
+ }
1677
+ const offsets = Object.keys(frames);
1678
+ if (!offsets.length) {
1679
+ add(at, 'keyframes \u81F3\u5C11\u8981\u6709\u4E00\u4E2A\u5173\u952E\u5E27\uFF08\u5982 "from" \u548C "to"\uFF09');
1680
+ return;
1681
+ }
1682
+ offsets.forEach((offset) => {
1683
+ if (!isKeyframeOffset(offset)) {
1684
+ add(
1685
+ `${at}.${offset}`,
1686
+ `\u5173\u952E\u5E27\u7684\u504F\u79FB\u91CF\u53EA\u80FD\u5199 "from" / "to" / "50%" \u8FD9\u4E09\u79CD\u5F62\u5F0F\uFF0C"${offset}" \u4E0D\u5408\u6CD5`
1687
+ );
1688
+ }
1689
+ const style = frames[offset];
1690
+ if (!style || typeof style !== "object" || Array.isArray(style)) {
1691
+ add(`${at}.${offset}`, "\u5173\u952E\u5E27\u7684\u503C\u5FC5\u987B\u662F\u6837\u5F0F\u5BF9\u8C61");
1692
+ return;
1693
+ }
1694
+ Object.keys(style).forEach((key) => {
1695
+ if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1696
+ warn(
1697
+ `${at}.${offset}.${key}`,
1698
+ `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
1699
+ );
1700
+ }
1701
+ });
1702
+ });
1703
+ });
1704
+ return names;
1705
+ }
1706
+ function validateAnimate(animate, path, add, keyframeNames, used) {
1707
+ if (animate === void 0) return;
1708
+ if (!animate || typeof animate !== "object" || Array.isArray(animate)) {
1709
+ add(path, "animate \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5F62\u5982 { name, duration, easing, iteration }");
1710
+ return;
1711
+ }
1712
+ const config = animate;
1713
+ if (typeof config.name !== "string" || !config.name) {
1714
+ add(`${path}.name`, "animate \u5FC5\u987B\u63D0\u4F9B name\uFF08keyframes \u540D\uFF09");
1715
+ } else {
1716
+ used.add(config.name);
1717
+ if (keyframeNames.indexOf(config.name) === -1) {
1718
+ add(
1719
+ `${path}.name`,
1720
+ `keyframes \u91CC\u6CA1\u6709\u5B9A\u4E49 "${config.name}"\uFF0C\u52A8\u753B\u4E0D\u4F1A\u64AD\u653E\u3002` + (keyframeNames.length ? `\u5DF2\u5B9A\u4E49\u7684\u6709\uFF1A${keyframeNames.join(" / ")}` : "\u8BF7\u5148\u5728\u9876\u5C42 keyframes \u91CC\u5B9A\u4E49\u8FD9\u7EC4\u5173\u952E\u5E27")
1721
+ );
1722
+ }
1723
+ }
1724
+ checkMs(config.duration, `${path}.duration`, add, true);
1725
+ checkMs(config.delay, `${path}.delay`, add, false);
1726
+ if (config.easing !== void 0 && !isValidEasing(config.easing)) {
1727
+ add(
1728
+ `${path}.easing`,
1729
+ `\u7F13\u52A8 "${String(config.easing)}" \u4E0D\u5728\u767D\u540D\u5355\u5185\u3002\u53EF\u7528\uFF1A${ALLOWED_EASINGS.join(" / ")} \u6216 cubic-bezier(0.34, 1.56, 0.64, 1)`
1730
+ );
1731
+ }
1732
+ if (config.iteration !== void 0 && config.iteration !== "infinite") {
1733
+ const times = config.iteration;
1734
+ if (typeof times !== "number" || !isFinite(times) || times <= 0 || times % 1 !== 0) {
1735
+ add(`${path}.iteration`, "iteration \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF0C\u6216\u5B57\u7B26\u4E32 'infinite'\uFF08\u65E0\u9650\u5FAA\u73AF\uFF09");
1736
+ }
1737
+ }
1738
+ checkEnum(config.direction, `${path}.direction`, DIRECTIONS2, add);
1739
+ checkEnum(config.fill, `${path}.fill`, FILLS2, add);
1740
+ }
1741
+ var DIRECTIONS2 = ["normal", "reverse", "alternate", "alternate-reverse"];
1742
+ var FILLS2 = ["none", "forwards", "backwards", "both"];
1743
+ function checkEnum(value, path, allowed, add) {
1744
+ if (value === void 0) return;
1745
+ if (typeof value !== "string" || allowed.indexOf(value) === -1) {
1746
+ add(path, `\u53EA\u80FD\u662F ${allowed.join(" / ")} \u4E4B\u4E00`);
1747
+ }
1748
+ }
1749
+ function checkMs(value, path, add, required) {
1750
+ if (value === void 0) {
1751
+ if (required) add(path, "animate \u5FC5\u987B\u63D0\u4F9B duration\uFF08\u6BEB\u79D2\uFF0C\u6B63\u6570\uFF09");
1752
+ return;
1753
+ }
1754
+ if (typeof value !== "number" || !isFinite(value) || value < 0) {
1755
+ add(path, `"${String(value)}" \u4E0D\u5408\u6CD5\uFF1A\u5FC5\u987B\u662F\u975E\u8D1F\u6570\u5B57\uFF0C\u5355\u4F4D\u6BEB\u79D2`);
1756
+ return;
1757
+ }
1758
+ if (required && value === 0) {
1759
+ add(path, "duration \u5FC5\u987B\u5927\u4E8E 0\uFF0C\u5426\u5219\u52A8\u753B\u4E0D\u4F1A\u64AD\u653E");
1760
+ }
1761
+ }
1762
+
1421
1763
  // src/validate.ts
1422
1764
  var LENGTH_STYLE_KEYS = [
1423
1765
  "width",
@@ -1491,14 +1833,26 @@ function validate(dsl, options) {
1491
1833
  const dataKeys = validateData(doc.data, add);
1492
1834
  const stateKeys = validateState(doc.state, add);
1493
1835
  validateDerived(doc.derived, dataKeys, stateKeys, add);
1836
+ const keyframeNames = validateKeyframes(doc.keyframes, add, warn);
1494
1837
  const isMulti = doc.views !== void 0;
1495
1838
  if (isMulti) validateMultiView(doc, add);
1496
1839
  const { views } = normalizeViews(doc);
1497
1840
  const viewNames = Object.keys(views);
1498
- const ctx = { add, warn, viewNames };
1841
+ const ctx = {
1842
+ add,
1843
+ warn,
1844
+ viewNames,
1845
+ keyframeNames,
1846
+ usedKeyframes: /* @__PURE__ */ new Set()
1847
+ };
1499
1848
  viewNames.forEach((name) => {
1500
1849
  validateView(views[name], isMulti ? `views.${name}` : "", ctx);
1501
1850
  });
1851
+ keyframeNames.forEach((name) => {
1852
+ if (!ctx.usedKeyframes.has(name)) {
1853
+ warn(`keyframes.${name}`, `keyframes "${name}" \u6CA1\u6709\u4EFB\u4F55 animate \u5F15\u7528\uFF0C\u4E0D\u4F1A\u751F\u6548`);
1854
+ }
1855
+ });
1502
1856
  if (options && options.userFields) {
1503
1857
  validateUserFields(doc, options.userFields).forEach(
1504
1858
  (issue) => add(issue.path, issue.message)
@@ -1597,8 +1951,45 @@ function validateStage(stage, add, warn, ctx, prefix) {
1597
1951
  if (stage.layout !== void 0 && ["absolute", "flow"].indexOf(stage.layout) === -1) {
1598
1952
  add("stage.layout", "stage.layout \u53EA\u80FD\u662F 'absolute'\uFF08\u9ED8\u8BA4\uFF09\u6216 'flow'");
1599
1953
  }
1600
- validateCloseButton(stage.closeButton, add, warn);
1601
1954
  const at = (key) => prefix ? `${prefix}.stage.${key}` : `stage.${key}`;
1955
+ if (stage.maskStyle !== void 0) {
1956
+ if (typeof stage.maskStyle !== "object" || stage.maskStyle === null || Array.isArray(stage.maskStyle)) {
1957
+ add("stage.maskStyle", "maskStyle \u5FC5\u987B\u662F\u6837\u5F0F\u5BF9\u8C61");
1958
+ } else {
1959
+ Object.keys(stage.maskStyle).forEach((key) => {
1960
+ if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1961
+ warn(
1962
+ `stage.maskStyle.${key}`,
1963
+ `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
1964
+ );
1965
+ }
1966
+ });
1967
+ }
1968
+ }
1969
+ if (stage.maskAction !== void 0) {
1970
+ validateAction(stage.maskAction, at("maskAction"), ctx);
1971
+ if (!stage.maskClosable) {
1972
+ warn(
1973
+ "stage.maskAction",
1974
+ "maskAction \u53EA\u6709\u5728 maskClosable \u4E3A true \u65F6\u624D\u4F1A\u6267\u884C\u2014\u2014\u906E\u7F69\u4E0D\u53EF\u70B9\uFF0C\u52A8\u4F5C\u5C31\u6CA1\u6709\u89E6\u53D1\u65F6\u673A"
1975
+ );
1976
+ }
1977
+ }
1978
+ validateCloseButton(stage.closeButton, add, warn, ctx, at("closeButton.action"));
1979
+ validateAnimate(
1980
+ stage.animate,
1981
+ "stage.animate",
1982
+ add,
1983
+ ctx.keyframeNames,
1984
+ ctx.usedKeyframes
1985
+ );
1986
+ validateAnimate(
1987
+ stage.maskAnimate,
1988
+ "stage.maskAnimate",
1989
+ add,
1990
+ ctx.keyframeNames,
1991
+ ctx.usedKeyframes
1992
+ );
1602
1993
  if (stage.onShow !== void 0) validateAction(stage.onShow, at("onShow"), ctx);
1603
1994
  if (stage.onClose !== void 0) validateAction(stage.onClose, at("onClose"), ctx);
1604
1995
  if (stage.height === "auto" && stage.layout !== "flow") {
@@ -1608,7 +1999,7 @@ function validateStage(stage, add, warn, ctx, prefix) {
1608
1999
  );
1609
2000
  }
1610
2001
  }
1611
- function validateCloseButton(config, add, warn) {
2002
+ function validateCloseButton(config, add, warn, ctx, actionPath) {
1612
2003
  if (config === void 0) return;
1613
2004
  if (typeof config !== "object" || config === null || Array.isArray(config)) {
1614
2005
  add("stage.closeButton", "closeButton \u5FC5\u987B\u662F\u5BF9\u8C61");
@@ -1640,16 +2031,28 @@ function validateCloseButton(config, add, warn) {
1640
2031
  if (config.image !== void 0 && typeof config.image !== "string") {
1641
2032
  add("stage.closeButton.image", "image \u5FC5\u987B\u662F\u56FE\u7247\u94FE\u63A5\u5B57\u7B26\u4E32");
1642
2033
  }
1643
- if (config.style) {
1644
- Object.keys(config.style).forEach((key) => {
2034
+ [
2035
+ ["style", config.style],
2036
+ ["hoverStyle", config.hoverStyle]
2037
+ ].forEach(([field, style]) => {
2038
+ if (!style) return;
2039
+ Object.keys(style).forEach((key) => {
1645
2040
  if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1646
2041
  warn(
1647
- `stage.closeButton.style.${key}`,
2042
+ `stage.closeButton.${field}.${key}`,
1648
2043
  `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
1649
2044
  );
1650
2045
  }
1651
2046
  });
1652
- }
2047
+ });
2048
+ if (config.action !== void 0) validateAction(config.action, actionPath, ctx);
2049
+ validateAnimate(
2050
+ config.animate,
2051
+ "stage.closeButton.animate",
2052
+ add,
2053
+ ctx.keyframeNames,
2054
+ ctx.usedKeyframes
2055
+ );
1653
2056
  }
1654
2057
  function validateData(data, add) {
1655
2058
  if (data === void 0) return [];
@@ -1698,8 +2101,41 @@ function validateDerived(derived, dataKeys, stateKeys, add) {
1698
2101
  }
1699
2102
  });
1700
2103
  }
1701
- function validateNode(node, path, layout, ctx) {
2104
+ function validateNodeStyle(style, path, ctx) {
1702
2105
  const { add, warn } = ctx;
2106
+ if (style) {
2107
+ Object.keys(style).forEach((key) => {
2108
+ if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
2109
+ warn(`${path}.${key}`, `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`);
2110
+ return;
2111
+ }
2112
+ if (LENGTH_STYLE_KEYS.indexOf(key) > -1 && !checkLength(style[key])) {
2113
+ add(
2114
+ `${path}.${key}`,
2115
+ `"${style[key]}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`
2116
+ );
2117
+ }
2118
+ if (key === "aspectRatio") {
2119
+ if (!checkAspectRatio(style[key])) {
2120
+ add(
2121
+ `${path}.aspectRatio`,
2122
+ "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
2123
+ );
2124
+ } else if (ctx.viewType === "banner") {
2125
+ const ratio = parseAspectRatio(style[key]);
2126
+ if (ratio !== null && ratio < BANNER_MIN_ASPECT_RATIO) {
2127
+ add(
2128
+ `${path}.aspectRatio`,
2129
+ `banner \u7684\u5BBD\u9AD8\u6BD4\u4E0D\u80FD\u5C0F\u4E8E ${BANNER_MIN_ASPECT_RATIO}:1\uFF08\u5F53\u524D\u7EA6 ${ratio.toFixed(1)}:1\uFF09\u3002banner \u5751\u4F4D\u5E38\u9A7B\u9875\u9762\u9876\u90E8\uFF0C\u6BD4\u4F8B\u8D8A\u65B9\u5728\u5BBD\u5C4F\u4E0A\u8D8A\u9AD8 \u2014\u2014 1800px \u5BBD\u7684\u5751\u4F4D\u91CC\uFF0C${ratio.toFixed(1)}:1 \u4F1A\u7B97\u51FA ${Math.round(1800 / ratio)}px \u9AD8\uFF0C\u5403\u6389\u9996\u5C4F\u4E00\u5927\u5757`
2130
+ );
2131
+ }
2132
+ }
2133
+ }
2134
+ });
2135
+ }
2136
+ }
2137
+ function validateNode(node, path, layout, ctx) {
2138
+ const { add } = ctx;
1703
2139
  if (!node || typeof node !== "object" || Array.isArray(node)) {
1704
2140
  add(path, "\u8282\u70B9\u5FC5\u987B\u662F\u5BF9\u8C61");
1705
2141
  return;
@@ -1725,36 +2161,15 @@ function validateNode(node, path, layout, ctx) {
1725
2161
  });
1726
2162
  }
1727
2163
  }
1728
- if (node.style) {
1729
- Object.keys(node.style).forEach((key) => {
1730
- if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1731
- warn(`${path}.style.${key}`, `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`);
1732
- return;
1733
- }
1734
- if (LENGTH_STYLE_KEYS.indexOf(key) > -1 && !checkLength(node.style[key])) {
1735
- add(
1736
- `${path}.style.${key}`,
1737
- `"${node.style[key]}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`
1738
- );
1739
- }
1740
- if (key === "aspectRatio") {
1741
- if (!checkAspectRatio(node.style[key])) {
1742
- add(
1743
- `${path}.style.aspectRatio`,
1744
- "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
1745
- );
1746
- } else if (ctx.viewType === "banner") {
1747
- const ratio = parseAspectRatio(node.style[key]);
1748
- if (ratio !== null && ratio < BANNER_MIN_ASPECT_RATIO) {
1749
- add(
1750
- `${path}.style.aspectRatio`,
1751
- `banner \u7684\u5BBD\u9AD8\u6BD4\u4E0D\u80FD\u5C0F\u4E8E ${BANNER_MIN_ASPECT_RATIO}:1\uFF08\u5F53\u524D\u7EA6 ${ratio.toFixed(1)}:1\uFF09\u3002banner \u5751\u4F4D\u5E38\u9A7B\u9875\u9762\u9876\u90E8\uFF0C\u6BD4\u4F8B\u8D8A\u65B9\u5728\u5BBD\u5C4F\u4E0A\u8D8A\u9AD8 \u2014\u2014 1800px \u5BBD\u7684\u5751\u4F4D\u91CC\uFF0C${ratio.toFixed(1)}:1 \u4F1A\u7B97\u51FA ${Math.round(1800 / ratio)}px \u9AD8\uFF0C\u5403\u6389\u9996\u5C4F\u4E00\u5927\u5757`
1752
- );
1753
- }
1754
- }
1755
- }
1756
- });
1757
- }
2164
+ validateNodeStyle(node.style, `${path}.style`, ctx);
2165
+ validateNodeStyle(node.hoverStyle, `${path}.hoverStyle`, ctx);
2166
+ validateAnimate(
2167
+ node.animate,
2168
+ `${path}.animate`,
2169
+ add,
2170
+ ctx.keyframeNames,
2171
+ ctx.usedKeyframes
2172
+ );
1758
2173
  if (node.visibleWhen !== void 0) {
1759
2174
  if (typeof node.visibleWhen !== "string") {
1760
2175
  add(`${path}.visibleWhen`, "visibleWhen \u5FC5\u987B\u662F\u8868\u8FBE\u5F0F\u5B57\u7B26\u4E32");
@@ -1767,7 +2182,7 @@ function validateNode(node, path, layout, ctx) {
1767
2182
  validateNodeByType(node, path, ctx);
1768
2183
  }
1769
2184
  function validateNodeByType(node, path, ctx) {
1770
- const { add } = ctx;
2185
+ const { add, warn } = ctx;
1771
2186
  if (node.type === "image" && !node.src) {
1772
2187
  add(`${path}.src`, "image \u5FC5\u987B\u63D0\u4F9B src");
1773
2188
  }
@@ -1788,9 +2203,25 @@ function validateNodeByType(node, path, ctx) {
1788
2203
  }
1789
2204
  }
1790
2205
  if (node.type === "countdown") {
1791
- if (!node.to) {
1792
- add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1793
- } else if (!isValidEndTime(node.to)) {
2206
+ if (!node.to && node.duration === void 0) {
2207
+ add(
2208
+ `${path}.to`,
2209
+ "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09\u6216 duration\uFF08\u76F8\u5BF9\u65F6\u957F\uFF0C\u6BEB\u79D2\uFF0C\u4ECE\u8BE5\u89C6\u56FE\u5C55\u793A\u90A3\u4E00\u523B\u8D77\u7B97\uFF09"
2210
+ );
2211
+ }
2212
+ if (node.duration !== void 0) {
2213
+ const duration = node.duration;
2214
+ if (typeof duration !== "number" || !isFinite(duration) || duration <= 0) {
2215
+ add(`${path}.duration`, "duration \u5FC5\u987B\u662F\u6B63\u6570\uFF0C\u5355\u4F4D\u6BEB\u79D2\uFF085 \u5206\u949F\u5C31\u5199 300000\uFF09");
2216
+ }
2217
+ }
2218
+ if (node.to && node.duration !== void 0) {
2219
+ warn(
2220
+ `${path}.duration`,
2221
+ "\u540C\u65F6\u5199\u4E86 to \u548C duration\uFF0C\u8FD0\u884C\u65F6\u4EE5 to \u4E3A\u51C6\uFF0Cduration \u4E0D\u4F1A\u751F\u6548"
2222
+ );
2223
+ }
2224
+ if (!node.to) ; else if (!isValidEndTime(node.to)) {
1794
2225
  add(
1795
2226
  `${path}.to`,
1796
2227
  `"${node.to}" \u4E0D\u662F\u80FD\u8BC6\u522B\u7684\u65F6\u95F4\u3002\u53EF\u4EE5\u5199 "2026-09-01 18:00:00" / "2026-09-01" / "2026\u5E749\u67081\u65E5 18\u70B9" / ISO \u683C\u5F0F / \u65F6\u95F4\u6233`
@@ -1883,10 +2314,10 @@ function formatIssues(issues) {
1883
2314
  }
1884
2315
 
1885
2316
  // src/host-capabilities.ts
1886
- var HOST_SOURCE_NAMES = ["shopAchievements", "listRenewTiers"];
2317
+ var HOST_SOURCE_NAMES = ["userStatistics", "listRenewTiers"];
1887
2318
  var HOST_HANDLER_NAMES = [];
1888
2319
  var HOST_SOURCE_LABELS = {
1889
- shopAchievements: "\u5E97\u94FA\u6210\u679C\u7EDF\u8BA1\uFF08\u5DF2\u4E0A\u67B6/\u8FD0\u884C\u4E2D/\u767D\u5E95\u56FE\u7B49\u683C\u5B50\u6570\u7EC4\uFF09",
2320
+ userStatistics: "\u7528\u6237\u6570\u636E\u7EDF\u8BA1\uFF08\u7D2F\u8BA1\u642C\u5BB6\u6570 / \u6B21\u6570\u4F59\u989D / AI \u4F18\u5316\u6B21\u6570\u7B49\uFF0C\u6241\u5E73\u5BF9\u8C61\uFF0C\u5B57\u6BB5\u540D\u4E3A\u540E\u7AEF snake_case\uFF09",
1890
2321
  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"
1891
2322
  };
1892
2323
  var HOST_HANDLER_LABELS = {};
@@ -1898,9 +2329,11 @@ function isRegisteredHostHandler(name) {
1898
2329
  }
1899
2330
 
1900
2331
  exports.ACTION_TYPES = ACTION_TYPES;
2332
+ exports.ALLOWED_EASINGS = ALLOWED_EASINGS;
1901
2333
  exports.ALLOWED_STYLE_KEYS = ALLOWED_STYLE_KEYS;
1902
2334
  exports.BANNER_MIN_ASPECT_RATIO = BANNER_MIN_ASPECT_RATIO;
1903
2335
  exports.CLOSE_POSITIONS = CLOSE_POSITIONS;
2336
+ exports.CSS_NAME_PATTERN = CSS_NAME_PATTERN;
1904
2337
  exports.DEFAULT_USER_FIELDS = DEFAULT_USER_FIELDS;
1905
2338
  exports.DSL_VERSION = DSL_VERSION;
1906
2339
  exports.HOST_HANDLER_LABELS = HOST_HANDLER_LABELS;
@@ -1919,14 +2352,17 @@ exports.formatIssues = formatIssues;
1919
2352
  exports.formatParts = formatParts;
1920
2353
  exports.interpolate = interpolate;
1921
2354
  exports.interpolateDeep = interpolateDeep;
2355
+ exports.isKeyframeOffset = isKeyframeOffset;
1922
2356
  exports.isLength = isLength;
1923
2357
  exports.isRegisteredHostHandler = isRegisteredHostHandler;
1924
2358
  exports.isRegisteredHostSource = isRegisteredHostSource;
2359
+ exports.isValidEasing = isValidEasing;
1925
2360
  exports.isValidEndTime = isValidEndTime;
1926
2361
  exports.normalizeViews = normalizeViews;
1927
2362
  exports.parseEndTime = parseEndTime;
1928
2363
  exports.safeImageUrl = safeImageUrl;
1929
2364
  exports.safeUrl = safeUrl;
2365
+ exports.sanitizeCssValue = sanitizeCssValue;
1930
2366
  exports.toCssStyle = toCssStyle;
1931
2367
  exports.toLength = toLength;
1932
2368
  exports.validate = validate;