@safe-ugc-ui/validator 0.5.1 → 0.6.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.d.ts CHANGED
@@ -101,6 +101,7 @@ interface TraversableNode {
101
101
  type: string;
102
102
  children?: TraversableNode[] | ForLoopLike;
103
103
  style?: Record<string, unknown>;
104
+ responsive?: Record<string, unknown>;
104
105
  [key: string]: unknown;
105
106
  }
106
107
  interface ForLoopLike {
@@ -113,17 +114,18 @@ interface ForLoopLike {
113
114
  * Return `false` to skip traversing into this node's children.
114
115
  */
115
116
  type NodeVisitor = (node: TraversableNode, context: TraversalContext) => void | false;
117
+ type StyleResolver = (node: TraversableNode) => Record<string, unknown> | undefined;
116
118
  /**
117
119
  * Recursively traverse a single node and its descendants.
118
120
  */
119
- declare function traverseNode(node: TraversableNode, context: TraversalContext, visitor: NodeVisitor): void;
121
+ declare function traverseNode(node: TraversableNode, context: TraversalContext, visitor: NodeVisitor, styleResolver?: StyleResolver): void;
120
122
  /**
121
123
  * Traverse all nodes in every view of a card.
122
124
  *
123
125
  * @param views - The `views` object from a UGCCard (mapping view names to root nodes).
124
126
  * @param visitor - Called for every node in every view.
125
127
  */
126
- declare function traverseCard(views: Record<string, unknown>, visitor: NodeVisitor): void;
128
+ declare function traverseCard(views: Record<string, unknown>, visitor: NodeVisitor, styleResolver?: StyleResolver): void;
127
129
 
128
130
  /**
129
131
  * @safe-ugc-ui/validator — Schema (Structural) Validation
@@ -360,4 +362,4 @@ declare function validate(input: unknown): ValidationResult;
360
362
  */
361
363
  declare function validateRaw(rawJson: string): ValidationResult;
362
364
 
363
- export { type NodeVisitor, type TraversableNode, type TraversalContext, type ValidationError, type ValidationErrorCode, type ValidationResult, createError, invalidResult, merge, parseCard, toResult, traverseCard, traverseNode, validResult, validate, validateLimits, validateNodes, validateRaw, validateSchema, validateSecurity, validateStyles, validateValueTypes };
365
+ export { type NodeVisitor, type StyleResolver, type TraversableNode, type TraversalContext, type ValidationError, type ValidationErrorCode, type ValidationResult, createError, invalidResult, merge, parseCard, toResult, traverseCard, traverseNode, validResult, validate, validateLimits, validateNodes, validateRaw, validateSchema, validateSecurity, validateStyles, validateValueTypes };
package/dist/index.js CHANGED
@@ -110,7 +110,7 @@ function isForLoop(children) {
110
110
  function hasOverflowAuto(style) {
111
111
  return style?.overflow === "auto";
112
112
  }
113
- function traverseNode(node, context, visitor) {
113
+ function traverseNode(node, context, visitor, styleResolver) {
114
114
  const result = visitor(node, context);
115
115
  if (result === false) {
116
116
  return;
@@ -120,7 +120,7 @@ function traverseNode(node, context, visitor) {
120
120
  return;
121
121
  }
122
122
  const nextStackDepth = node.type === "Stack" ? context.stackDepth + 1 : context.stackDepth;
123
- const nextOverflowAuto = context.overflowAutoAncestor || hasOverflowAuto(node.style);
123
+ const nextOverflowAuto = context.overflowAutoAncestor || hasOverflowAuto(styleResolver ? styleResolver(node) : node.style);
124
124
  if (isForLoop(children)) {
125
125
  const childCtx = {
126
126
  path: `${context.path}.children.template`,
@@ -130,7 +130,7 @@ function traverseNode(node, context, visitor) {
130
130
  overflowAutoAncestor: nextOverflowAuto,
131
131
  stackDepth: nextStackDepth
132
132
  };
133
- traverseNode(children.template, childCtx, visitor);
133
+ traverseNode(children.template, childCtx, visitor, styleResolver);
134
134
  } else if (Array.isArray(children)) {
135
135
  for (let i = 0; i < children.length; i++) {
136
136
  const child = children[i];
@@ -143,12 +143,12 @@ function traverseNode(node, context, visitor) {
143
143
  overflowAutoAncestor: nextOverflowAuto,
144
144
  stackDepth: nextStackDepth
145
145
  };
146
- traverseNode(child, childCtx, visitor);
146
+ traverseNode(child, childCtx, visitor, styleResolver);
147
147
  }
148
148
  }
149
149
  }
150
150
  }
151
- function traverseCard(views, visitor) {
151
+ function traverseCard(views, visitor, styleResolver) {
152
152
  for (const [viewName, rootNode] of Object.entries(views)) {
153
153
  if (rootNode == null || typeof rootNode !== "object" || !("type" in rootNode)) {
154
154
  continue;
@@ -161,7 +161,7 @@ function traverseCard(views, visitor) {
161
161
  overflowAutoAncestor: false,
162
162
  stackDepth: 0
163
163
  };
164
- traverseNode(rootNode, context, visitor);
164
+ traverseNode(rootNode, context, visitor, styleResolver);
165
165
  }
166
166
  }
167
167
 
@@ -287,8 +287,7 @@ var STRUCTURED_OBJECT_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
287
287
  "boxShadow",
288
288
  "textShadow"
289
289
  ]);
290
- function validateNodeStyle(node, ctx, errors) {
291
- const style = node.style;
290
+ function validateNodeStyle(style, path, errors) {
292
291
  if (!style) {
293
292
  return;
294
293
  }
@@ -302,7 +301,7 @@ function validateNodeStyle(node, ctx, errors) {
302
301
  createError(
303
302
  "DYNAMIC_NOT_ALLOWED",
304
303
  `Style property "${prop}" must be a static literal; $ref is not allowed.`,
305
- `${ctx.path}.style.${prop}`
304
+ `${path}.${prop}`
306
305
  )
307
306
  );
308
307
  }
@@ -311,7 +310,7 @@ function validateNodeStyle(node, ctx, errors) {
311
310
  createError(
312
311
  "DYNAMIC_NOT_ALLOWED",
313
312
  `Style property "${prop}" must be an object literal; use $ref only inside its nested fields.`,
314
- `${ctx.path}.style.${prop}`
313
+ `${path}.${prop}`
315
314
  )
316
315
  );
317
316
  }
@@ -321,7 +320,18 @@ function validateNodeStyle(node, ctx, errors) {
321
320
  function validateValueTypes(views) {
322
321
  const errors = [];
323
322
  traverseCard(views, (node, ctx) => {
324
- validateNodeStyle(node, ctx, errors);
323
+ validateNodeStyle(node.style, `${ctx.path}.style`, errors);
324
+ const responsive = node.responsive;
325
+ if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
326
+ const compact = responsive.compact;
327
+ if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
328
+ validateNodeStyle(
329
+ compact,
330
+ `${ctx.path}.responsive.compact`,
331
+ errors
332
+ );
333
+ }
334
+ }
325
335
  });
326
336
  return errors;
327
337
  }
@@ -578,8 +588,15 @@ function collectNestedStyleRefErrors(value, path, errors) {
578
588
  collectNestedStyleRefErrors(child, childPath, errors);
579
589
  }
580
590
  }
581
- function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverStyleRefs = true) {
582
- const STRUCTURED_FIELDS = /* @__PURE__ */ new Set(["transition", "hoverStyle"]);
591
+ function validateSingleStyle(style, stylePath, errors, cardStyles, options = {}) {
592
+ const {
593
+ allowHoverStyle = true,
594
+ allowTransition = true,
595
+ allowHoverStyleRefs = true
596
+ } = options;
597
+ const STRUCTURED_FIELDS = /* @__PURE__ */ new Set();
598
+ if (allowTransition) STRUCTURED_FIELDS.add("transition");
599
+ if (allowHoverStyle) STRUCTURED_FIELDS.add("hoverStyle");
583
600
  for (const key of Object.keys(style)) {
584
601
  if (!STRUCTURED_FIELDS.has(key) && FORBIDDEN_STYLE_PROPERTIES.includes(key)) {
585
602
  errors.push(
@@ -591,6 +608,24 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
591
608
  );
592
609
  }
593
610
  }
611
+ if (!allowHoverStyle && "hoverStyle" in style) {
612
+ errors.push(
613
+ createError(
614
+ "INVALID_VALUE",
615
+ `hoverStyle is not allowed inside responsive overrides at "${stylePath}.hoverStyle"`,
616
+ `${stylePath}.hoverStyle`
617
+ )
618
+ );
619
+ }
620
+ if (!allowTransition && "transition" in style) {
621
+ errors.push(
622
+ createError(
623
+ "INVALID_VALUE",
624
+ `transition is not allowed inside responsive overrides at "${stylePath}.transition"`,
625
+ `${stylePath}.transition`
626
+ )
627
+ );
628
+ }
594
629
  if ("zIndex" in style && isLiteralNumber(style.zIndex)) {
595
630
  const v = style.zIndex;
596
631
  if (v < ZINDEX_MIN || v > ZINDEX_MAX) {
@@ -872,7 +907,7 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
872
907
  }
873
908
  }
874
909
  }
875
- if ("hoverStyle" in style && style.hoverStyle != null) {
910
+ if (allowHoverStyle && "hoverStyle" in style && style.hoverStyle != null) {
876
911
  const hoverStyle = style.hoverStyle;
877
912
  const hoverPath = `${stylePath}.hoverStyle`;
878
913
  if (typeof hoverStyle !== "object" || Array.isArray(hoverStyle)) {
@@ -901,12 +936,16 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
901
936
  hoverPath,
902
937
  errors,
903
938
  cardStyles,
904
- allowHoverStyleRefs
939
+ {
940
+ allowHoverStyle,
941
+ allowTransition,
942
+ allowHoverStyleRefs
943
+ }
905
944
  );
906
945
  }
907
946
  }
908
947
  }
909
- if ("transition" in style && style.transition != null) {
948
+ if (allowTransition && "transition" in style && style.transition != null) {
910
949
  const transition = style.transition;
911
950
  const transPath = `${stylePath}.transition`;
912
951
  if (typeof transition === "string") {
@@ -992,18 +1031,41 @@ function validateStyles(views, cardStyles) {
992
1031
  );
993
1032
  }
994
1033
  collectNestedStyleRefErrors(styleEntry, entryPath, errors);
995
- validateSingleStyle(styleEntry, entryPath, errors, void 0, false);
1034
+ validateSingleStyle(styleEntry, entryPath, errors, void 0, {
1035
+ allowHoverStyleRefs: false
1036
+ });
996
1037
  }
997
1038
  }
998
1039
  traverseCard(views, (node, ctx) => {
999
1040
  const style = node.style;
1000
- if (style == null || typeof style !== "object") {
1041
+ if (style != null && typeof style === "object") {
1042
+ const stylePath = `${ctx.path}.style`;
1043
+ const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
1044
+ if (mergedStyle) {
1045
+ validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
1046
+ }
1047
+ }
1048
+ const responsive = node.responsive;
1049
+ if (responsive == null || typeof responsive !== "object" || Array.isArray(responsive)) {
1050
+ return;
1051
+ }
1052
+ const compact = responsive.compact;
1053
+ if (compact == null || typeof compact !== "object" || Array.isArray(compact)) {
1001
1054
  return;
1002
1055
  }
1003
- const stylePath = `${ctx.path}.style`;
1004
- const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
1005
- if (mergedStyle) {
1006
- validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
1056
+ const compactPath = `${ctx.path}.responsive.compact`;
1057
+ const mergedCompact = resolveStyleRef(
1058
+ compact,
1059
+ compactPath,
1060
+ cardStyles,
1061
+ errors
1062
+ );
1063
+ if (mergedCompact) {
1064
+ validateSingleStyle(mergedCompact, compactPath, errors, cardStyles, {
1065
+ allowHoverStyle: false,
1066
+ allowTransition: false,
1067
+ allowHoverStyleRefs: false
1068
+ });
1007
1069
  }
1008
1070
  });
1009
1071
  return errors;
@@ -1014,6 +1076,72 @@ import {
1014
1076
  PROTOTYPE_POLLUTION_SEGMENTS,
1015
1077
  isRef as isRef2
1016
1078
  } from "@safe-ugc-ui/types";
1079
+
1080
+ // src/responsive-utils.ts
1081
+ var RESPONSIVE_MODES = [
1082
+ "default",
1083
+ "compact"
1084
+ ];
1085
+ function mergeNamedStyle(style, cardStyles) {
1086
+ if (!style) return void 0;
1087
+ const rawStyleName = style.$style;
1088
+ const styleName = typeof rawStyleName === "string" ? rawStyleName.trim() : rawStyleName;
1089
+ if (!styleName || typeof styleName !== "string" || !cardStyles) {
1090
+ if (style.$style !== void 0) {
1091
+ const { $style: _2, ...rest } = style;
1092
+ return rest;
1093
+ }
1094
+ return style;
1095
+ }
1096
+ const baseStyle = cardStyles[styleName];
1097
+ if (!baseStyle) {
1098
+ const { $style: _2, ...rest } = style;
1099
+ return rest;
1100
+ }
1101
+ const { $style: _, ...inlineWithoutStyleRef } = style;
1102
+ return { ...baseStyle, ...inlineWithoutStyleRef };
1103
+ }
1104
+ function getCompactResponsiveStyle(node) {
1105
+ const responsive = node.responsive;
1106
+ if (responsive == null || typeof responsive !== "object" || Array.isArray(responsive)) {
1107
+ return void 0;
1108
+ }
1109
+ const compact = responsive.compact;
1110
+ if (compact == null || typeof compact !== "object" || Array.isArray(compact)) {
1111
+ return void 0;
1112
+ }
1113
+ return compact;
1114
+ }
1115
+ function stripResponsiveOnlyUnsupportedFields(style) {
1116
+ if (!style) return void 0;
1117
+ const {
1118
+ hoverStyle: _hoverStyle,
1119
+ transition: _transition,
1120
+ ...rest
1121
+ } = style;
1122
+ return rest;
1123
+ }
1124
+ function getEffectiveStyleForMode(node, cardStyles, mode) {
1125
+ const baseStyle = mergeNamedStyle(node.style, cardStyles);
1126
+ if (mode === "default") {
1127
+ return baseStyle;
1128
+ }
1129
+ const compactStyle = stripResponsiveOnlyUnsupportedFields(
1130
+ mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles)
1131
+ );
1132
+ if (!compactStyle) {
1133
+ return baseStyle;
1134
+ }
1135
+ return {
1136
+ ...baseStyle ?? {},
1137
+ ...compactStyle
1138
+ };
1139
+ }
1140
+ function getMergedCompactResponsiveStyle(node, cardStyles) {
1141
+ return mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles);
1142
+ }
1143
+
1144
+ // src/security.ts
1017
1145
  var FORBIDDEN_URL_PREFIXES = [
1018
1146
  "http://",
1019
1147
  "https://",
@@ -1130,8 +1258,84 @@ function checkSrcValue(resolved, type, errorPath, errors) {
1130
1258
  }
1131
1259
  }
1132
1260
  }
1261
+ function pushUniqueError(errors, seen, error) {
1262
+ const key = `${error.code}|${error.path}|${error.message}`;
1263
+ if (seen.has(key)) {
1264
+ return;
1265
+ }
1266
+ seen.add(key);
1267
+ errors.push(error);
1268
+ }
1269
+ function scanStyleStringsForUrl(style, path, errors) {
1270
+ if (!style) {
1271
+ return;
1272
+ }
1273
+ for (const [prop, value] of Object.entries(style)) {
1274
+ if (typeof value === "string" && value.toLowerCase().includes("url(")) {
1275
+ errors.push(
1276
+ createError(
1277
+ "FORBIDDEN_CSS_FUNCTION",
1278
+ `CSS url() function is forbidden in style values. Found in "${prop}".`,
1279
+ `${path}.${prop}`
1280
+ )
1281
+ );
1282
+ }
1283
+ }
1284
+ }
1285
+ function validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen) {
1286
+ const styleResolver = (node) => getEffectiveStyleForMode(node, cardStyles, mode);
1287
+ traverseCard(views, (node, context) => {
1288
+ const effectiveStyle = styleResolver(node);
1289
+ if (effectiveStyle && typeof effectiveStyle.position === "string") {
1290
+ const position = effectiveStyle.position;
1291
+ if (position === "fixed") {
1292
+ pushUniqueError(
1293
+ errors,
1294
+ seen,
1295
+ createError(
1296
+ "POSITION_FIXED_FORBIDDEN",
1297
+ 'CSS position "fixed" is not allowed.',
1298
+ `${context.path}.style.position`
1299
+ )
1300
+ );
1301
+ } else if (position === "sticky") {
1302
+ pushUniqueError(
1303
+ errors,
1304
+ seen,
1305
+ createError(
1306
+ "POSITION_STICKY_FORBIDDEN",
1307
+ 'CSS position "sticky" is not allowed.',
1308
+ `${context.path}.style.position`
1309
+ )
1310
+ );
1311
+ } else if (position === "absolute" && context.parentType !== "Stack") {
1312
+ pushUniqueError(
1313
+ errors,
1314
+ seen,
1315
+ createError(
1316
+ "POSITION_ABSOLUTE_NOT_IN_STACK",
1317
+ 'CSS position "absolute" is only allowed inside a Stack component.',
1318
+ `${context.path}.style.position`
1319
+ )
1320
+ );
1321
+ }
1322
+ }
1323
+ if (effectiveStyle && effectiveStyle.overflow === "auto" && context.overflowAutoAncestor) {
1324
+ pushUniqueError(
1325
+ errors,
1326
+ seen,
1327
+ createError(
1328
+ "OVERFLOW_AUTO_NESTED",
1329
+ "Nested overflow:auto is not allowed. An ancestor already has overflow:auto.",
1330
+ `${context.path}.style.overflow`
1331
+ )
1332
+ );
1333
+ }
1334
+ }, styleResolver);
1335
+ }
1133
1336
  function validateSecurity(card) {
1134
1337
  const errors = [];
1338
+ const seen = /* @__PURE__ */ new Set();
1135
1339
  const { views, state, cardAssets, cardStyles } = card;
1136
1340
  if (cardAssets) {
1137
1341
  for (const [key, value] of Object.entries(cardAssets)) {
@@ -1178,74 +1382,29 @@ function validateSecurity(card) {
1178
1382
  }
1179
1383
  }
1180
1384
  }
1181
- if (style) {
1182
- for (const [prop, value] of Object.entries(style)) {
1183
- if (typeof value === "string" && value.toLowerCase().includes("url(")) {
1184
- errors.push(
1185
- createError(
1186
- "FORBIDDEN_CSS_FUNCTION",
1187
- `CSS url() function is forbidden in style values. Found in "${prop}".`,
1188
- `${path}.style.${prop}`
1189
- )
1190
- );
1191
- }
1192
- }
1193
- }
1194
- let effectiveStyle = style;
1195
- if (style && typeof style.$style === "string" && cardStyles && style.$style.trim() in cardStyles) {
1196
- const refName = style.$style.trim();
1197
- const merged = { ...cardStyles[refName] };
1198
- for (const [key, value] of Object.entries(style)) {
1199
- if (key !== "$style") {
1200
- merged[key] = value;
1201
- }
1202
- }
1203
- effectiveStyle = merged;
1204
- }
1205
- if (effectiveStyle && typeof effectiveStyle.position === "string") {
1206
- const position = effectiveStyle.position;
1207
- if (position === "fixed") {
1208
- errors.push(
1209
- createError(
1210
- "POSITION_FIXED_FORBIDDEN",
1211
- 'CSS position "fixed" is not allowed.',
1212
- `${path}.style.position`
1213
- )
1214
- );
1215
- } else if (position === "sticky") {
1216
- errors.push(
1217
- createError(
1218
- "POSITION_STICKY_FORBIDDEN",
1219
- 'CSS position "sticky" is not allowed.',
1220
- `${path}.style.position`
1221
- )
1385
+ scanStyleStringsForUrl(style, `${path}.style`, errors);
1386
+ const responsive = node.responsive;
1387
+ if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
1388
+ const compact = responsive.compact;
1389
+ if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
1390
+ scanStyleStringsForUrl(
1391
+ compact,
1392
+ `${path}.responsive.compact`,
1393
+ errors
1222
1394
  );
1223
- } else if (position === "absolute") {
1224
- if (context.parentType !== "Stack") {
1225
- errors.push(
1226
- createError(
1227
- "POSITION_ABSOLUTE_NOT_IN_STACK",
1228
- 'CSS position "absolute" is only allowed inside a Stack component.',
1229
- `${path}.style.position`
1230
- )
1231
- );
1232
- }
1233
1395
  }
1234
1396
  }
1235
- if (effectiveStyle && effectiveStyle.overflow === "auto" && context.overflowAutoAncestor) {
1236
- errors.push(
1237
- createError(
1238
- "OVERFLOW_AUTO_NESTED",
1239
- "Nested overflow:auto is not allowed. An ancestor already has overflow:auto.",
1240
- `${path}.style.overflow`
1241
- )
1242
- );
1243
- }
1244
1397
  scanForRefs(nodeFields, path, errors);
1245
1398
  if (style) {
1246
1399
  scanForRefs(style, `${path}.style`, errors);
1247
1400
  }
1401
+ if (node.responsive != null && typeof node.responsive === "object" && !Array.isArray(node.responsive)) {
1402
+ scanForRefs(node.responsive, `${path}.responsive`, errors);
1403
+ }
1248
1404
  });
1405
+ for (const mode of RESPONSIVE_MODES) {
1406
+ validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen);
1407
+ }
1249
1408
  return errors;
1250
1409
  }
1251
1410
 
@@ -1313,7 +1472,15 @@ function resolveRefFromState2(refPath, state) {
1313
1472
  return current;
1314
1473
  }
1315
1474
  function countTemplateMetrics(template, cardStyles) {
1316
- const result = { nodes: 0, textBytes: 0, styleBytes: 0, overflowAutoCount: 0 };
1475
+ const result = {
1476
+ nodes: 0,
1477
+ textBytes: 0,
1478
+ styleBytes: 0,
1479
+ overflowAutoCount: {
1480
+ default: 0,
1481
+ compact: 0
1482
+ }
1483
+ };
1317
1484
  if (template == null || typeof template !== "object") return result;
1318
1485
  const node = template;
1319
1486
  if (!node.type) return result;
@@ -1324,27 +1491,31 @@ function countTemplateMetrics(template, cardStyles) {
1324
1491
  result.textBytes = utf8ByteLength(content);
1325
1492
  }
1326
1493
  }
1327
- if (node.style != null && typeof node.style === "object") {
1328
- const style = node.style;
1329
- let styleForBytes = style;
1330
- if (typeof style.$style === "string" && cardStyles && style.$style.trim() in cardStyles) {
1331
- const refName = style.$style.trim();
1332
- const merged = { ...cardStyles[refName] };
1333
- for (const [key, value] of Object.entries(style)) {
1334
- if (key !== "$style") merged[key] = value;
1335
- }
1336
- styleForBytes = merged;
1337
- }
1338
- result.styleBytes = utf8ByteLength(JSON.stringify(styleForBytes));
1339
- let effectiveOverflow = style.overflow;
1340
- if (typeof style.$style === "string" && cardStyles && style.$style.trim() in cardStyles) {
1341
- const refName = style.$style.trim();
1342
- if (!("overflow" in style) || style.overflow === void 0) {
1343
- effectiveOverflow = cardStyles[refName].overflow;
1344
- }
1494
+ {
1495
+ const baseStyleForBytes = getEffectiveStyleForMode(
1496
+ node,
1497
+ cardStyles,
1498
+ "default"
1499
+ );
1500
+ if (baseStyleForBytes) {
1501
+ result.styleBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
1345
1502
  }
1346
- if (effectiveOverflow === "auto") {
1347
- result.overflowAutoCount = 1;
1503
+ const compactStyleForBytes = getMergedCompactResponsiveStyle(
1504
+ node,
1505
+ cardStyles
1506
+ );
1507
+ if (compactStyleForBytes) {
1508
+ result.styleBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
1509
+ }
1510
+ for (const mode of RESPONSIVE_MODES) {
1511
+ const effectiveStyle = getEffectiveStyleForMode(
1512
+ node,
1513
+ cardStyles,
1514
+ mode
1515
+ );
1516
+ if (effectiveStyle?.overflow === "auto") {
1517
+ result.overflowAutoCount[mode]++;
1518
+ }
1348
1519
  }
1349
1520
  }
1350
1521
  const children = node.children;
@@ -1354,7 +1525,9 @@ function countTemplateMetrics(template, cardStyles) {
1354
1525
  result.nodes += childMetrics.nodes;
1355
1526
  result.textBytes += childMetrics.textBytes;
1356
1527
  result.styleBytes += childMetrics.styleBytes;
1357
- result.overflowAutoCount += childMetrics.overflowAutoCount;
1528
+ for (const mode of RESPONSIVE_MODES) {
1529
+ result.overflowAutoCount[mode] += childMetrics.overflowAutoCount[mode];
1530
+ }
1358
1531
  }
1359
1532
  } else if (children != null && typeof children === "object" && !Array.isArray(children) && "template" in children) {
1360
1533
  const innerTemplate = children.template;
@@ -1362,7 +1535,9 @@ function countTemplateMetrics(template, cardStyles) {
1362
1535
  result.nodes += innerMetrics.nodes;
1363
1536
  result.textBytes += innerMetrics.textBytes;
1364
1537
  result.styleBytes += innerMetrics.styleBytes;
1365
- result.overflowAutoCount += innerMetrics.overflowAutoCount;
1538
+ for (const mode of RESPONSIVE_MODES) {
1539
+ result.overflowAutoCount[mode] += innerMetrics.overflowAutoCount[mode];
1540
+ }
1366
1541
  }
1367
1542
  return result;
1368
1543
  }
@@ -1371,7 +1546,10 @@ function validateLimits(card) {
1371
1546
  let nodeCount = 0;
1372
1547
  let textContentBytes = 0;
1373
1548
  let styleObjectsBytes = 0;
1374
- let overflowAutoCount = 0;
1549
+ const overflowAutoCount = {
1550
+ default: 0,
1551
+ compact: 0
1552
+ };
1375
1553
  traverseCard(card.views, (node, context) => {
1376
1554
  nodeCount++;
1377
1555
  if (node.type === "Text") {
@@ -1380,20 +1558,22 @@ function validateLimits(card) {
1380
1558
  textContentBytes += utf8ByteLength(content);
1381
1559
  }
1382
1560
  }
1383
- if (node.style != null && typeof node.style === "object") {
1384
- let styleForBytes = node.style;
1385
- if (typeof node.style.$style === "string" && card.cardStyles && node.style.$style.trim() in card.cardStyles) {
1386
- const refName = node.style.$style.trim();
1387
- const merged = { ...card.cardStyles[refName] };
1388
- for (const [key, value] of Object.entries(node.style)) {
1389
- if (key !== "$style") {
1390
- merged[key] = value;
1391
- }
1392
- }
1393
- styleForBytes = merged;
1561
+ {
1562
+ const baseStyleForBytes = getEffectiveStyleForMode(
1563
+ node,
1564
+ card.cardStyles,
1565
+ "default"
1566
+ );
1567
+ if (baseStyleForBytes) {
1568
+ styleObjectsBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
1569
+ }
1570
+ const compactStyleForBytes = getMergedCompactResponsiveStyle(
1571
+ node,
1572
+ card.cardStyles
1573
+ );
1574
+ if (compactStyleForBytes) {
1575
+ styleObjectsBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
1394
1576
  }
1395
- const serialized = JSON.stringify(styleForBytes);
1396
- styleObjectsBytes += utf8ByteLength(serialized);
1397
1577
  }
1398
1578
  const children = node.children;
1399
1579
  if (children != null && typeof children === "object" && !Array.isArray(children) && "for" in children && "in" in children && "template" in children) {
@@ -1436,7 +1616,9 @@ function validateLimits(card) {
1436
1616
  nodeCount += tplMetrics.nodes * multiplier;
1437
1617
  textContentBytes += tplMetrics.textBytes * multiplier;
1438
1618
  styleObjectsBytes += tplMetrics.styleBytes * multiplier;
1439
- overflowAutoCount += tplMetrics.overflowAutoCount * multiplier;
1619
+ for (const mode of RESPONSIVE_MODES) {
1620
+ overflowAutoCount[mode] += tplMetrics.overflowAutoCount[mode] * multiplier;
1621
+ }
1440
1622
  }
1441
1623
  }
1442
1624
  }
@@ -1450,16 +1632,10 @@ function validateLimits(card) {
1450
1632
  );
1451
1633
  }
1452
1634
  }
1453
- {
1454
- let effectiveOverflow = node.style?.overflow;
1455
- if (node.style && typeof node.style.$style === "string" && card.cardStyles && node.style.$style.trim() in card.cardStyles) {
1456
- const refName = node.style.$style.trim();
1457
- if (!("overflow" in node.style) || node.style.overflow === void 0) {
1458
- effectiveOverflow = card.cardStyles[refName].overflow;
1459
- }
1460
- }
1461
- if (effectiveOverflow === "auto") {
1462
- overflowAutoCount++;
1635
+ for (const mode of RESPONSIVE_MODES) {
1636
+ const effectiveStyle = getEffectiveStyleForMode(node, card.cardStyles, mode);
1637
+ if (effectiveStyle?.overflow === "auto") {
1638
+ overflowAutoCount[mode]++;
1463
1639
  }
1464
1640
  }
1465
1641
  if (node.type === "Stack" && context.stackDepth >= MAX_STACK_NESTING) {
@@ -1499,11 +1675,15 @@ function validateLimits(card) {
1499
1675
  )
1500
1676
  );
1501
1677
  }
1502
- if (overflowAutoCount > MAX_OVERFLOW_AUTO_COUNT) {
1678
+ const maxOverflowAutoCount = Math.max(
1679
+ overflowAutoCount.default,
1680
+ overflowAutoCount.compact
1681
+ );
1682
+ if (maxOverflowAutoCount > MAX_OVERFLOW_AUTO_COUNT) {
1503
1683
  errors.push(
1504
1684
  createError(
1505
1685
  "OVERFLOW_AUTO_COUNT_EXCEEDED",
1506
- `Card has ${overflowAutoCount} elements with overflow:auto, max is ${MAX_OVERFLOW_AUTO_COUNT}`,
1686
+ `Card has ${maxOverflowAutoCount} elements with overflow:auto in at least one responsive mode, max is ${MAX_OVERFLOW_AUTO_COUNT}`,
1507
1687
  "views"
1508
1688
  )
1509
1689
  );