@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/README.md CHANGED
@@ -141,11 +141,26 @@ unsubscribe();
141
141
  }],
142
142
  countdownEndTimes: [], // 有倒计时时运行时会自己起定时器
143
143
  countdownPrecision: 's',
144
+ css: '', // 要注入的 CSS 文本(keyframes + hover 规则),没有动画时是空串
145
+ rootClassName: 'dsl-renderer dsl-r3', // 根容器类名,后半截是这份物料的作用域前缀
144
146
  }
145
147
  ```
146
148
 
147
149
  框架壳照着 layer 的结构套四层 div 即可,**不需要自带任何 CSS**,基础样式已经写进行内样式了。
148
150
 
151
+ ### 动画与 hover:唯一需要一段真 CSS 的地方
152
+
153
+ `@keyframes` 和 `:hover` 行内表达不了,所以渲染树多带两个字段。框架壳的义务只有两条:
154
+ 根容器的 class 用 `tree.rootClassName`(不要写死 `dsl-renderer`),`tree.css` 非空时把它原样塞进
155
+ 一个 `<style>`、随变化更新、销毁时移除。**壳里不做任何 CSS 生成或判断**。
156
+
157
+ - `animate` 落的是**行内** `animation` 简写(节点、`stage`、`stage.maskAnimate`、`stage.closeButton` 都支持),
158
+ 只有 keyframes 定义与 hover 规则进 `tree.css`
159
+ - keyframes 名带作用域前缀(`dsl-kf-{uid}-{name}`),同一页面上多份物料不会互相覆盖
160
+ - `tree.css` 末尾恒带 `prefers-reduced-motion` 兜底:系统开了「减少动态效果」就全部静止
161
+ - CSS 文本是字符串拼接,所以每个值都过 `sanitizeCssValue`——含 `{ } < > ; @`、`/*` 或反斜杠的
162
+ **整条声明**直接丢弃(行内那条路径不需要,框架自己会转义)
163
+
149
164
  ### 框架壳必须遵守的一条契约
150
165
 
151
166
  节点有 `onClick` 时,挂事件要**阻止冒泡**。
@@ -440,6 +440,11 @@ function isLength(value) {
440
440
  if (/^calc\([^;{}]*\)$/.test(trimmed)) return true;
441
441
  return LENGTH_RE.test(trimmed);
442
442
  }
443
+ function clampZIndex(value) {
444
+ const num = typeof value === "number" ? value : parseFloat(String(value));
445
+ if (!isFinite(num)) return 0;
446
+ return Math.min(10, Math.max(0, Math.round(num)));
447
+ }
443
448
  var STYLE_MAP = {
444
449
  // 文字
445
450
  color: ["color", String],
@@ -492,6 +497,26 @@ var STYLE_MAP = {
492
497
  minHeight: ["minHeight", px],
493
498
  maxWidth: ["maxWidth", px],
494
499
  maxHeight: ["maxHeight", px],
500
+ // 动效相关。transform / transition 是做位移、缩放、hover 过渡的最小必需集;
501
+ // 不放开 position / display / grid —— rect 已经提供绝对定位,栅格用 flex wrap + 百分比宽复刻,
502
+ // 放开 position 等于把「全屏覆盖」这类越权手段又交回给配置。
503
+ transform: ["transform", String],
504
+ transformOrigin: ["transformOrigin", String],
505
+ transition: ["transition", String],
506
+ // 只为 hoverStyle 而放开:`animationPlayState: 'paused'` 才能做到「悬停时暂停呼吸动画」。
507
+ // 写进普通 style 也合法(等于一开始就暂停),但那基本没有意义。
508
+ animationPlayState: ["animationPlayState", String],
509
+ // 毛玻璃。Safari 至今只认带前缀的写法,所以两个键一起输出,见 EXTRA_CSS_KEYS
510
+ backdropFilter: ["backdropFilter", String],
511
+ textShadow: ["textShadow", String],
512
+ // 只允许 0~10:越权提层是弹窗类配置最容易做的坏事(盖住宿主自己的导航 / 收银台),
513
+ // 层内排布十层绰绰有余,真需要压过页面其它内容的是弹窗层,那由解释器自己给 1000
514
+ zIndex: ["zIndex", (v) => String(clampZIndex(v))],
515
+ // 装饰性图层(光斑、扫光)必须让点击穿透,否则会挡住下面的按钮
516
+ pointerEvents: ["pointerEvents", String],
517
+ // 横向排布里「这一项不许被压扁」,图标位和数字位常用
518
+ flexShrink: ["flexShrink", String],
519
+ fontStyle: ["fontStyle", String],
495
520
  // 图片
496
521
  objectFit: ["objectFit", String],
497
522
  // 宽度给百分比、高度按比例算。整图 banner 的热区必须靠它才能用百分比定位:
@@ -499,6 +524,9 @@ var STYLE_MAP = {
499
524
  aspectRatio: ["aspectRatio", String]
500
525
  };
501
526
  var ALLOWED_STYLE_KEYS = Object.keys(STYLE_MAP);
527
+ var EXTRA_CSS_KEYS = {
528
+ backdropFilter: ["WebkitBackdropFilter"]
529
+ };
502
530
  function toCssStyle(style) {
503
531
  const css = {};
504
532
  if (!style) return css;
@@ -507,7 +535,11 @@ function toCssStyle(style) {
507
535
  if (!rule) return;
508
536
  const value = style[key];
509
537
  if (value === void 0 || value === null || value === "") return;
510
- css[rule[0]] = rule[1](value);
538
+ const result = rule[1](value);
539
+ css[rule[0]] = result;
540
+ (EXTRA_CSS_KEYS[key] || []).forEach((extra) => {
541
+ css[extra] = result;
542
+ });
511
543
  });
512
544
  return css;
513
545
  }
@@ -520,11 +552,116 @@ function toRectStyle(rect) {
520
552
  return css;
521
553
  }
522
554
  function resolveNodeStyle(node, layout) {
523
- return Object.assign(
555
+ const css = Object.assign(
524
556
  {},
525
557
  layout === "absolute" ? toRectStyle(node.rect) : {},
526
558
  toCssStyle(node.style)
527
559
  );
560
+ if (css.zIndex !== void 0 && !css.position) css.position = "relative";
561
+ return css;
562
+ }
563
+
564
+ // src/css-value.ts
565
+ function sanitizeCssValue(value) {
566
+ if (value === void 0 || value === null) return null;
567
+ const text = String(value).trim();
568
+ if (!text) return null;
569
+ if (/[{}<>;@]/.test(text)) return null;
570
+ if (text.indexOf("/*") > -1 || text.indexOf("*/") > -1) return null;
571
+ if (text.indexOf("\\") > -1) return null;
572
+ return text;
573
+ }
574
+ function toCssProp(key) {
575
+ return key.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
576
+ }
577
+ function declarations(style) {
578
+ return Object.keys(style).map((key) => {
579
+ const value = sanitizeCssValue(style[key]);
580
+ return value === null ? "" : `${toCssProp(key)}:${value}`;
581
+ }).filter(Boolean).join(";");
582
+ }
583
+
584
+ // src/css.ts
585
+ var CSS_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,31}$/;
586
+ var ALLOWED_EASINGS = [
587
+ "linear",
588
+ "ease",
589
+ "ease-in",
590
+ "ease-out",
591
+ "ease-in-out",
592
+ "step-start",
593
+ "step-end"
594
+ ];
595
+ var CUBIC_BEZIER_RE = /^cubic-bezier\(\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*\)$/;
596
+ var OFFSET_RE = /^(from|to|\d+(\.\d+)?%)$/;
597
+ function isValidEasing(value) {
598
+ if (typeof value !== "string") return false;
599
+ const trimmed = value.trim();
600
+ return ALLOWED_EASINGS.indexOf(trimmed) > -1 || CUBIC_BEZIER_RE.test(trimmed);
601
+ }
602
+ function isKeyframeOffset(value) {
603
+ return typeof value === "string" && OFFSET_RE.test(value.trim());
604
+ }
605
+ var scopeSeed = 0;
606
+ function nextScopeUid() {
607
+ scopeSeed += 1;
608
+ return scopeSeed;
609
+ }
610
+ function createCssBuilder(uid, keyframes, hover) {
611
+ const rootClassName = `dsl-renderer dsl-r${uid}`;
612
+ const scope = `.dsl-r${uid}`;
613
+ const defined = keyframes && typeof keyframes === "object" ? keyframes : {};
614
+ const hoverRules = hover ? hover.rules : [];
615
+ function scopedKeyframeName(name) {
616
+ return `dsl-kf-${uid}-${name}`;
617
+ }
618
+ function animation(animate) {
619
+ if (!animate || typeof animate !== "object") return void 0;
620
+ const name = animate.name;
621
+ if (typeof name !== "string" || !CSS_NAME_PATTERN.test(name)) return void 0;
622
+ if (!Object.prototype.hasOwnProperty.call(defined, name)) return void 0;
623
+ const duration = toMs(animate.duration, 0);
624
+ if (duration <= 0) return void 0;
625
+ const delay = toMs(animate.delay, 0);
626
+ const easing = isValidEasing(animate.easing) ? String(animate.easing).trim() : "ease";
627
+ const iteration = animate.iteration === "infinite" ? "infinite" : String(Math.max(1, Math.round(Number(animate.iteration) || 1)));
628
+ const direction = pick(animate.direction, DIRECTIONS, "normal");
629
+ const fill = pick(animate.fill, FILLS, "both");
630
+ return `${scopedKeyframeName(name)} ${duration}ms ${easing} ${delay}ms ${iteration} ${direction} ${fill}`;
631
+ }
632
+ function hoverClass(owner) {
633
+ return owner && hover ? hover.classOf.get(owner) : void 0;
634
+ }
635
+ function build() {
636
+ const rules = keyframeRules().concat(hoverRules);
637
+ if (!rules.length) return "";
638
+ rules.push(
639
+ `@media (prefers-reduced-motion: reduce){${scope} *{animation:none!important;transition:none!important}}`
640
+ );
641
+ return rules.join("\n");
642
+ }
643
+ function keyframeRules() {
644
+ return Object.keys(defined).filter((name) => CSS_NAME_PATTERN.test(name)).map((name) => {
645
+ const frames = defined[name];
646
+ if (!frames || typeof frames !== "object") return "";
647
+ const body = Object.keys(frames).filter(isKeyframeOffset).map((offset) => {
648
+ const decl = declarations(toCssStyle(frames[offset]));
649
+ return decl ? `${offset.trim()}{${decl}}` : "";
650
+ }).filter(Boolean).join("");
651
+ return body ? `@keyframes ${scopedKeyframeName(name)}{${body}}` : "";
652
+ }).filter(Boolean);
653
+ }
654
+ return { uid, rootClassName, animation, hoverClass, build };
655
+ }
656
+ var DIRECTIONS = ["normal", "reverse", "alternate", "alternate-reverse"];
657
+ var FILLS = ["none", "forwards", "backwards", "both"];
658
+ function pick(value, allowed, fallback) {
659
+ return typeof value === "string" && allowed.indexOf(value) > -1 ? value : fallback;
660
+ }
661
+ function toMs(value, fallback) {
662
+ const num = Number(value);
663
+ if (!isFinite(num) || num < 0) return fallback;
664
+ return Math.round(num);
528
665
  }
529
666
 
530
667
  // src/user-fields.ts
@@ -643,6 +780,116 @@ function validateUserFields(dsl, allowed) {
643
780
  return issues;
644
781
  }
645
782
 
783
+ // src/validate-animate.ts
784
+ function validateKeyframes(keyframes, add, warn) {
785
+ if (keyframes === void 0) return [];
786
+ if (typeof keyframes !== "object" || keyframes === null || Array.isArray(keyframes)) {
787
+ add(
788
+ "keyframes",
789
+ 'keyframes \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5F62\u5982 { "fadeIn": { "from": {...}, "to": {...} } }'
790
+ );
791
+ return [];
792
+ }
793
+ const names = Object.keys(keyframes);
794
+ names.forEach((name) => {
795
+ const at = `keyframes.${name}`;
796
+ if (!CSS_NAME_PATTERN.test(name)) {
797
+ add(
798
+ at,
799
+ `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`
800
+ );
801
+ }
802
+ const frames = keyframes[name];
803
+ if (!frames || typeof frames !== "object" || Array.isArray(frames)) {
804
+ add(at, "\u6BCF\u7EC4 keyframes \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u952E\u662F\u504F\u79FB\u91CF\u3001\u503C\u662F\u6837\u5F0F");
805
+ return;
806
+ }
807
+ const offsets = Object.keys(frames);
808
+ if (!offsets.length) {
809
+ add(at, 'keyframes \u81F3\u5C11\u8981\u6709\u4E00\u4E2A\u5173\u952E\u5E27\uFF08\u5982 "from" \u548C "to"\uFF09');
810
+ return;
811
+ }
812
+ offsets.forEach((offset) => {
813
+ if (!isKeyframeOffset(offset)) {
814
+ add(
815
+ `${at}.${offset}`,
816
+ `\u5173\u952E\u5E27\u7684\u504F\u79FB\u91CF\u53EA\u80FD\u5199 "from" / "to" / "50%" \u8FD9\u4E09\u79CD\u5F62\u5F0F\uFF0C"${offset}" \u4E0D\u5408\u6CD5`
817
+ );
818
+ }
819
+ const style = frames[offset];
820
+ if (!style || typeof style !== "object" || Array.isArray(style)) {
821
+ add(`${at}.${offset}`, "\u5173\u952E\u5E27\u7684\u503C\u5FC5\u987B\u662F\u6837\u5F0F\u5BF9\u8C61");
822
+ return;
823
+ }
824
+ Object.keys(style).forEach((key) => {
825
+ if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
826
+ warn(
827
+ `${at}.${offset}.${key}`,
828
+ `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
829
+ );
830
+ }
831
+ });
832
+ });
833
+ });
834
+ return names;
835
+ }
836
+ function validateAnimate(animate, path, add, keyframeNames, used) {
837
+ if (animate === void 0) return;
838
+ if (!animate || typeof animate !== "object" || Array.isArray(animate)) {
839
+ add(path, "animate \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5F62\u5982 { name, duration, easing, iteration }");
840
+ return;
841
+ }
842
+ const config = animate;
843
+ if (typeof config.name !== "string" || !config.name) {
844
+ add(`${path}.name`, "animate \u5FC5\u987B\u63D0\u4F9B name\uFF08keyframes \u540D\uFF09");
845
+ } else {
846
+ used.add(config.name);
847
+ if (keyframeNames.indexOf(config.name) === -1) {
848
+ add(
849
+ `${path}.name`,
850
+ `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")
851
+ );
852
+ }
853
+ }
854
+ checkMs(config.duration, `${path}.duration`, add, true);
855
+ checkMs(config.delay, `${path}.delay`, add, false);
856
+ if (config.easing !== void 0 && !isValidEasing(config.easing)) {
857
+ add(
858
+ `${path}.easing`,
859
+ `\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)`
860
+ );
861
+ }
862
+ if (config.iteration !== void 0 && config.iteration !== "infinite") {
863
+ const times = config.iteration;
864
+ if (typeof times !== "number" || !isFinite(times) || times <= 0 || times % 1 !== 0) {
865
+ add(`${path}.iteration`, "iteration \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF0C\u6216\u5B57\u7B26\u4E32 'infinite'\uFF08\u65E0\u9650\u5FAA\u73AF\uFF09");
866
+ }
867
+ }
868
+ checkEnum(config.direction, `${path}.direction`, DIRECTIONS2, add);
869
+ checkEnum(config.fill, `${path}.fill`, FILLS2, add);
870
+ }
871
+ var DIRECTIONS2 = ["normal", "reverse", "alternate", "alternate-reverse"];
872
+ var FILLS2 = ["none", "forwards", "backwards", "both"];
873
+ function checkEnum(value, path, allowed, add) {
874
+ if (value === void 0) return;
875
+ if (typeof value !== "string" || allowed.indexOf(value) === -1) {
876
+ add(path, `\u53EA\u80FD\u662F ${allowed.join(" / ")} \u4E4B\u4E00`);
877
+ }
878
+ }
879
+ function checkMs(value, path, add, required) {
880
+ if (value === void 0) {
881
+ if (required) add(path, "animate \u5FC5\u987B\u63D0\u4F9B duration\uFF08\u6BEB\u79D2\uFF0C\u6B63\u6570\uFF09");
882
+ return;
883
+ }
884
+ if (typeof value !== "number" || !isFinite(value) || value < 0) {
885
+ add(path, `"${String(value)}" \u4E0D\u5408\u6CD5\uFF1A\u5FC5\u987B\u662F\u975E\u8D1F\u6570\u5B57\uFF0C\u5355\u4F4D\u6BEB\u79D2`);
886
+ return;
887
+ }
888
+ if (required && value === 0) {
889
+ add(path, "duration \u5FC5\u987B\u5927\u4E8E 0\uFF0C\u5426\u5219\u52A8\u753B\u4E0D\u4F1A\u64AD\u653E");
890
+ }
891
+ }
892
+
646
893
  // src/validate.ts
647
894
  var LENGTH_STYLE_KEYS = [
648
895
  "width",
@@ -716,14 +963,26 @@ function validate(dsl, options) {
716
963
  const dataKeys = validateData(doc.data, add);
717
964
  const stateKeys = validateState(doc.state, add);
718
965
  validateDerived(doc.derived, dataKeys, stateKeys, add);
966
+ const keyframeNames = validateKeyframes(doc.keyframes, add, warn);
719
967
  const isMulti = doc.views !== void 0;
720
968
  if (isMulti) validateMultiView(doc, add);
721
969
  const { views } = normalizeViews(doc);
722
970
  const viewNames = Object.keys(views);
723
- const ctx = { add, warn, viewNames };
971
+ const ctx = {
972
+ add,
973
+ warn,
974
+ viewNames,
975
+ keyframeNames,
976
+ usedKeyframes: /* @__PURE__ */ new Set()
977
+ };
724
978
  viewNames.forEach((name) => {
725
979
  validateView(views[name], isMulti ? `views.${name}` : "", ctx);
726
980
  });
981
+ keyframeNames.forEach((name) => {
982
+ if (!ctx.usedKeyframes.has(name)) {
983
+ warn(`keyframes.${name}`, `keyframes "${name}" \u6CA1\u6709\u4EFB\u4F55 animate \u5F15\u7528\uFF0C\u4E0D\u4F1A\u751F\u6548`);
984
+ }
985
+ });
727
986
  if (options && options.userFields) {
728
987
  validateUserFields(doc, options.userFields).forEach(
729
988
  (issue) => add(issue.path, issue.message)
@@ -822,8 +1081,45 @@ function validateStage(stage, add, warn, ctx, prefix) {
822
1081
  if (stage.layout !== void 0 && ["absolute", "flow"].indexOf(stage.layout) === -1) {
823
1082
  add("stage.layout", "stage.layout \u53EA\u80FD\u662F 'absolute'\uFF08\u9ED8\u8BA4\uFF09\u6216 'flow'");
824
1083
  }
825
- validateCloseButton(stage.closeButton, add, warn);
826
1084
  const at = (key) => prefix ? `${prefix}.stage.${key}` : `stage.${key}`;
1085
+ if (stage.maskStyle !== void 0) {
1086
+ if (typeof stage.maskStyle !== "object" || stage.maskStyle === null || Array.isArray(stage.maskStyle)) {
1087
+ add("stage.maskStyle", "maskStyle \u5FC5\u987B\u662F\u6837\u5F0F\u5BF9\u8C61");
1088
+ } else {
1089
+ Object.keys(stage.maskStyle).forEach((key) => {
1090
+ if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1091
+ warn(
1092
+ `stage.maskStyle.${key}`,
1093
+ `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
1094
+ );
1095
+ }
1096
+ });
1097
+ }
1098
+ }
1099
+ if (stage.maskAction !== void 0) {
1100
+ validateAction(stage.maskAction, at("maskAction"), ctx);
1101
+ if (!stage.maskClosable) {
1102
+ warn(
1103
+ "stage.maskAction",
1104
+ "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"
1105
+ );
1106
+ }
1107
+ }
1108
+ validateCloseButton(stage.closeButton, add, warn, ctx, at("closeButton.action"));
1109
+ validateAnimate(
1110
+ stage.animate,
1111
+ "stage.animate",
1112
+ add,
1113
+ ctx.keyframeNames,
1114
+ ctx.usedKeyframes
1115
+ );
1116
+ validateAnimate(
1117
+ stage.maskAnimate,
1118
+ "stage.maskAnimate",
1119
+ add,
1120
+ ctx.keyframeNames,
1121
+ ctx.usedKeyframes
1122
+ );
827
1123
  if (stage.onShow !== void 0) validateAction(stage.onShow, at("onShow"), ctx);
828
1124
  if (stage.onClose !== void 0) validateAction(stage.onClose, at("onClose"), ctx);
829
1125
  if (stage.height === "auto" && stage.layout !== "flow") {
@@ -833,7 +1129,7 @@ function validateStage(stage, add, warn, ctx, prefix) {
833
1129
  );
834
1130
  }
835
1131
  }
836
- function validateCloseButton(config, add, warn) {
1132
+ function validateCloseButton(config, add, warn, ctx, actionPath) {
837
1133
  if (config === void 0) return;
838
1134
  if (typeof config !== "object" || config === null || Array.isArray(config)) {
839
1135
  add("stage.closeButton", "closeButton \u5FC5\u987B\u662F\u5BF9\u8C61");
@@ -865,16 +1161,28 @@ function validateCloseButton(config, add, warn) {
865
1161
  if (config.image !== void 0 && typeof config.image !== "string") {
866
1162
  add("stage.closeButton.image", "image \u5FC5\u987B\u662F\u56FE\u7247\u94FE\u63A5\u5B57\u7B26\u4E32");
867
1163
  }
868
- if (config.style) {
869
- Object.keys(config.style).forEach((key) => {
1164
+ [
1165
+ ["style", config.style],
1166
+ ["hoverStyle", config.hoverStyle]
1167
+ ].forEach(([field, style]) => {
1168
+ if (!style) return;
1169
+ Object.keys(style).forEach((key) => {
870
1170
  if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
871
1171
  warn(
872
- `stage.closeButton.style.${key}`,
1172
+ `stage.closeButton.${field}.${key}`,
873
1173
  `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`
874
1174
  );
875
1175
  }
876
1176
  });
877
- }
1177
+ });
1178
+ if (config.action !== void 0) validateAction(config.action, actionPath, ctx);
1179
+ validateAnimate(
1180
+ config.animate,
1181
+ "stage.closeButton.animate",
1182
+ add,
1183
+ ctx.keyframeNames,
1184
+ ctx.usedKeyframes
1185
+ );
878
1186
  }
879
1187
  function validateData(data, add) {
880
1188
  if (data === void 0) return [];
@@ -923,8 +1231,41 @@ function validateDerived(derived, dataKeys, stateKeys, add) {
923
1231
  }
924
1232
  });
925
1233
  }
926
- function validateNode(node, path, layout, ctx) {
1234
+ function validateNodeStyle(style, path, ctx) {
927
1235
  const { add, warn } = ctx;
1236
+ if (style) {
1237
+ Object.keys(style).forEach((key) => {
1238
+ if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
1239
+ warn(`${path}.${key}`, `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`);
1240
+ return;
1241
+ }
1242
+ if (LENGTH_STYLE_KEYS.indexOf(key) > -1 && !checkLength(style[key])) {
1243
+ add(
1244
+ `${path}.${key}`,
1245
+ `"${style[key]}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`
1246
+ );
1247
+ }
1248
+ if (key === "aspectRatio") {
1249
+ if (!checkAspectRatio(style[key])) {
1250
+ add(
1251
+ `${path}.aspectRatio`,
1252
+ "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
1253
+ );
1254
+ } else if (ctx.viewType === "banner") {
1255
+ const ratio = parseAspectRatio(style[key]);
1256
+ if (ratio !== null && ratio < BANNER_MIN_ASPECT_RATIO) {
1257
+ add(
1258
+ `${path}.aspectRatio`,
1259
+ `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`
1260
+ );
1261
+ }
1262
+ }
1263
+ }
1264
+ });
1265
+ }
1266
+ }
1267
+ function validateNode(node, path, layout, ctx) {
1268
+ const { add } = ctx;
928
1269
  if (!node || typeof node !== "object" || Array.isArray(node)) {
929
1270
  add(path, "\u8282\u70B9\u5FC5\u987B\u662F\u5BF9\u8C61");
930
1271
  return;
@@ -950,36 +1291,15 @@ function validateNode(node, path, layout, ctx) {
950
1291
  });
951
1292
  }
952
1293
  }
953
- if (node.style) {
954
- Object.keys(node.style).forEach((key) => {
955
- if (ALLOWED_STYLE_KEYS.indexOf(key) === -1) {
956
- warn(`${path}.style.${key}`, `\u6837\u5F0F\u5C5E\u6027 "${key}" \u4E0D\u5728\u767D\u540D\u5355\u5185\uFF0C\u6E32\u67D3\u65F6\u4F1A\u88AB\u5FFD\u7565`);
957
- return;
958
- }
959
- if (LENGTH_STYLE_KEYS.indexOf(key) > -1 && !checkLength(node.style[key])) {
960
- add(
961
- `${path}.style.${key}`,
962
- `"${node.style[key]}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`
963
- );
964
- }
965
- if (key === "aspectRatio") {
966
- if (!checkAspectRatio(node.style[key])) {
967
- add(
968
- `${path}.style.aspectRatio`,
969
- "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
970
- );
971
- } else if (ctx.viewType === "banner") {
972
- const ratio = parseAspectRatio(node.style[key]);
973
- if (ratio !== null && ratio < BANNER_MIN_ASPECT_RATIO) {
974
- add(
975
- `${path}.style.aspectRatio`,
976
- `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`
977
- );
978
- }
979
- }
980
- }
981
- });
982
- }
1294
+ validateNodeStyle(node.style, `${path}.style`, ctx);
1295
+ validateNodeStyle(node.hoverStyle, `${path}.hoverStyle`, ctx);
1296
+ validateAnimate(
1297
+ node.animate,
1298
+ `${path}.animate`,
1299
+ add,
1300
+ ctx.keyframeNames,
1301
+ ctx.usedKeyframes
1302
+ );
983
1303
  if (node.visibleWhen !== void 0) {
984
1304
  if (typeof node.visibleWhen !== "string") {
985
1305
  add(`${path}.visibleWhen`, "visibleWhen \u5FC5\u987B\u662F\u8868\u8FBE\u5F0F\u5B57\u7B26\u4E32");
@@ -992,7 +1312,7 @@ function validateNode(node, path, layout, ctx) {
992
1312
  validateNodeByType(node, path, ctx);
993
1313
  }
994
1314
  function validateNodeByType(node, path, ctx) {
995
- const { add } = ctx;
1315
+ const { add, warn } = ctx;
996
1316
  if (node.type === "image" && !node.src) {
997
1317
  add(`${path}.src`, "image \u5FC5\u987B\u63D0\u4F9B src");
998
1318
  }
@@ -1013,9 +1333,25 @@ function validateNodeByType(node, path, ctx) {
1013
1333
  }
1014
1334
  }
1015
1335
  if (node.type === "countdown") {
1016
- if (!node.to) {
1017
- add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1018
- } else if (!isValidEndTime(node.to)) {
1336
+ if (!node.to && node.duration === void 0) {
1337
+ add(
1338
+ `${path}.to`,
1339
+ "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"
1340
+ );
1341
+ }
1342
+ if (node.duration !== void 0) {
1343
+ const duration = node.duration;
1344
+ if (typeof duration !== "number" || !isFinite(duration) || duration <= 0) {
1345
+ add(`${path}.duration`, "duration \u5FC5\u987B\u662F\u6B63\u6570\uFF0C\u5355\u4F4D\u6BEB\u79D2\uFF085 \u5206\u949F\u5C31\u5199 300000\uFF09");
1346
+ }
1347
+ }
1348
+ if (node.to && node.duration !== void 0) {
1349
+ warn(
1350
+ `${path}.duration`,
1351
+ "\u540C\u65F6\u5199\u4E86 to \u548C duration\uFF0C\u8FD0\u884C\u65F6\u4EE5 to \u4E3A\u51C6\uFF0Cduration \u4E0D\u4F1A\u751F\u6548"
1352
+ );
1353
+ }
1354
+ if (!node.to) ; else if (!isValidEndTime(node.to)) {
1019
1355
  add(
1020
1356
  `${path}.to`,
1021
1357
  `"${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`
@@ -1107,4 +1443,4 @@ function formatIssues(issues) {
1107
1443
  return issues.map((item) => item.path ? `${item.path}: ${item.message}` : item.message).join("\n");
1108
1444
  }
1109
1445
 
1110
- 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, createTicker, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, resolveNodeStyle, toCssStyle, toLength, validate, validateUserFields };
1446
+ 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, createCssBuilder, createTicker, declarations, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isKeyframeOffset, isLength, isValidEasing, isValidEndTime, nextScopeUid, normalizeViews, parseEndTime, resolveNodeStyle, sanitizeCssValue, toCssStyle, toLength, validate, validateUserFields };