@safe-ugc-ui/validator 0.5.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -100,74 +100,252 @@ function parseCard(input) {
100
100
  return result.success ? result.data : null;
101
101
  }
102
102
 
103
- // src/node-validator.ts
104
- import { ALL_COMPONENT_TYPES } from "@safe-ugc-ui/types";
105
-
106
103
  // src/traverse.ts
107
104
  function isForLoop(children) {
108
105
  return typeof children === "object" && children !== null && "for" in children && "in" in children && "template" in children;
109
106
  }
107
+ function isTraversableNode(value) {
108
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
109
+ }
110
+ function isFragmentUseLike(value) {
111
+ return typeof value === "object" && value !== null && "$use" in value && typeof value.$use === "string";
112
+ }
113
+ function getEmbeddedRenderables(node) {
114
+ const entries = [];
115
+ const interactiveField = node.type === "Accordion" ? "items" : node.type === "Tabs" ? "tabs" : null;
116
+ if (interactiveField) {
117
+ const items = node[interactiveField];
118
+ if (!Array.isArray(items)) {
119
+ return entries;
120
+ }
121
+ for (let i = 0; i < items.length; i++) {
122
+ const item = items[i];
123
+ if (item == null || typeof item !== "object" || Array.isArray(item)) {
124
+ continue;
125
+ }
126
+ const content = item.content;
127
+ if (isTraversableNode(content) || isFragmentUseLike(content)) {
128
+ entries.push({
129
+ pathSuffix: `${interactiveField}[${i}].content`,
130
+ renderable: content
131
+ });
132
+ }
133
+ }
134
+ }
135
+ return entries;
136
+ }
137
+ function resolveRenderableNode(node, fragments, fragmentStack = []) {
138
+ if (isTraversableNode(node)) {
139
+ return node;
140
+ }
141
+ if (!isFragmentUseLike(node) || !fragments) {
142
+ return null;
143
+ }
144
+ if (fragmentStack.length > 0 || fragmentStack.includes(node.$use)) {
145
+ return null;
146
+ }
147
+ const target = fragments[node.$use];
148
+ return isTraversableNode(target) ? target : null;
149
+ }
110
150
  function hasOverflowAuto(style) {
111
151
  return style?.overflow === "auto";
112
152
  }
113
- function traverseNode(node, context, visitor) {
114
- const result = visitor(node, context);
115
- if (result === false) {
153
+ function traverseNode(node, context, visitor, styleResolver, fragments, fragmentStack = []) {
154
+ const resolvedNode = resolveRenderableNode(node, fragments, fragmentStack);
155
+ if (!resolvedNode) {
116
156
  return;
117
157
  }
118
- const children = node.children;
119
- if (children == null) {
158
+ const nextFragmentStack = isFragmentUseLike(node) ? [...fragmentStack, node.$use] : fragmentStack;
159
+ const result = visitor(resolvedNode, context);
160
+ if (result === false) {
120
161
  return;
121
162
  }
122
- const nextStackDepth = node.type === "Stack" ? context.stackDepth + 1 : context.stackDepth;
123
- const nextOverflowAuto = context.overflowAutoAncestor || hasOverflowAuto(node.style);
124
- if (isForLoop(children)) {
125
- const childCtx = {
126
- path: `${context.path}.children.template`,
163
+ const nextStackDepth = resolvedNode.type === "Stack" ? context.stackDepth + 1 : context.stackDepth;
164
+ const nextOverflowAuto = context.overflowAutoAncestor || hasOverflowAuto(styleResolver ? styleResolver(resolvedNode) : resolvedNode.style);
165
+ const children = resolvedNode.children;
166
+ if (children != null) {
167
+ if (isForLoop(children)) {
168
+ const childCtx = {
169
+ path: `${context.path}.children.template`,
170
+ depth: context.depth + 1,
171
+ parentType: resolvedNode.type,
172
+ loopDepth: context.loopDepth + 1,
173
+ overflowAutoAncestor: nextOverflowAuto,
174
+ stackDepth: nextStackDepth
175
+ };
176
+ traverseNode(children.template, childCtx, visitor, styleResolver, fragments, nextFragmentStack);
177
+ } else if (Array.isArray(children)) {
178
+ for (let i = 0; i < children.length; i++) {
179
+ const child = children[i];
180
+ if (isTraversableNode(child) || isFragmentUseLike(child)) {
181
+ const childCtx = {
182
+ path: `${context.path}.children[${i}]`,
183
+ depth: context.depth + 1,
184
+ parentType: resolvedNode.type,
185
+ loopDepth: context.loopDepth,
186
+ overflowAutoAncestor: nextOverflowAuto,
187
+ stackDepth: nextStackDepth
188
+ };
189
+ traverseNode(child, childCtx, visitor, styleResolver, fragments, nextFragmentStack);
190
+ }
191
+ }
192
+ }
193
+ }
194
+ for (const entry of getEmbeddedRenderables(resolvedNode)) {
195
+ const embeddedCtx = {
196
+ path: `${context.path}.${entry.pathSuffix}`,
127
197
  depth: context.depth + 1,
128
- parentType: node.type,
129
- loopDepth: context.loopDepth + 1,
198
+ parentType: resolvedNode.type,
199
+ loopDepth: context.loopDepth,
130
200
  overflowAutoAncestor: nextOverflowAuto,
131
201
  stackDepth: nextStackDepth
132
202
  };
133
- traverseNode(children.template, childCtx, visitor);
134
- } else if (Array.isArray(children)) {
135
- for (let i = 0; i < children.length; i++) {
136
- const child = children[i];
137
- if (child && typeof child === "object" && "type" in child) {
138
- const childCtx = {
139
- path: `${context.path}.children[${i}]`,
140
- depth: context.depth + 1,
141
- parentType: node.type,
142
- loopDepth: context.loopDepth,
143
- overflowAutoAncestor: nextOverflowAuto,
144
- stackDepth: nextStackDepth
145
- };
146
- traverseNode(child, childCtx, visitor);
147
- }
148
- }
203
+ traverseNode(
204
+ entry.renderable,
205
+ embeddedCtx,
206
+ visitor,
207
+ styleResolver,
208
+ fragments,
209
+ nextFragmentStack
210
+ );
149
211
  }
150
212
  }
151
- function traverseCard(views, visitor) {
213
+ function traverseCard(views, visitor, styleResolver, fragments, pathPrefix = "views") {
152
214
  for (const [viewName, rootNode] of Object.entries(views)) {
153
- if (rootNode == null || typeof rootNode !== "object" || !("type" in rootNode)) {
215
+ if (!isTraversableNode(rootNode) && !isFragmentUseLike(rootNode)) {
154
216
  continue;
155
217
  }
156
218
  const context = {
157
- path: `views.${viewName}`,
219
+ path: `${pathPrefix}.${viewName}`,
158
220
  depth: 0,
159
221
  parentType: null,
160
222
  loopDepth: 0,
161
223
  overflowAutoAncestor: false,
162
224
  stackDepth: 0
163
225
  };
164
- traverseNode(rootNode, context, visitor);
226
+ traverseNode(rootNode, context, visitor, styleResolver, fragments);
165
227
  }
166
228
  }
167
229
 
230
+ // src/renderable-walk.ts
231
+ function isForLoopLike(value) {
232
+ return typeof value === "object" && value !== null && "for" in value && "in" in value && "template" in value;
233
+ }
234
+ function walkRenderableNode(node, path, inFragments, visitor) {
235
+ visitor(node, { path, inFragments });
236
+ if (!isTraversableNode(node)) {
237
+ return;
238
+ }
239
+ const children = node.children;
240
+ if (Array.isArray(children)) {
241
+ for (let i = 0; i < children.length; i++) {
242
+ const child = children[i];
243
+ if (!isTraversableNode(child) && !isFragmentUseLike(child)) {
244
+ continue;
245
+ }
246
+ walkRenderableNode(
247
+ child,
248
+ `${path}.children[${i}]`,
249
+ inFragments,
250
+ visitor
251
+ );
252
+ }
253
+ }
254
+ if (isForLoopLike(children)) {
255
+ const template = children.template;
256
+ if (isTraversableNode(template) || isFragmentUseLike(template)) {
257
+ walkRenderableNode(
258
+ template,
259
+ `${path}.children.template`,
260
+ inFragments,
261
+ visitor
262
+ );
263
+ }
264
+ }
265
+ for (const entry of getEmbeddedRenderables(node)) {
266
+ walkRenderableNode(
267
+ entry.renderable,
268
+ `${path}.${entry.pathSuffix}`,
269
+ inFragments,
270
+ visitor
271
+ );
272
+ }
273
+ }
274
+ function walkRenderableCard(views, fragments, visitor) {
275
+ for (const [viewName, rootNode] of Object.entries(views)) {
276
+ if (!isTraversableNode(rootNode) && !isFragmentUseLike(rootNode)) {
277
+ continue;
278
+ }
279
+ walkRenderableNode(
280
+ rootNode,
281
+ `views.${viewName}`,
282
+ false,
283
+ visitor
284
+ );
285
+ }
286
+ if (!fragments) {
287
+ return;
288
+ }
289
+ for (const [fragmentName, fragmentRoot] of Object.entries(fragments)) {
290
+ if (!isTraversableNode(fragmentRoot) && !isFragmentUseLike(fragmentRoot)) {
291
+ continue;
292
+ }
293
+ walkRenderableNode(
294
+ fragmentRoot,
295
+ `fragments.${fragmentName}`,
296
+ true,
297
+ visitor
298
+ );
299
+ }
300
+ }
301
+
302
+ // src/fragment-validator.ts
303
+ var FRAGMENT_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
304
+ function validateFragments(views, fragments) {
305
+ const errors = [];
306
+ if (fragments) {
307
+ for (const fragmentName of Object.keys(fragments)) {
308
+ if (!FRAGMENT_NAME_PATTERN.test(fragmentName)) {
309
+ errors.push(
310
+ createError(
311
+ "INVALID_FRAGMENT_NAME",
312
+ `Fragment name "${fragmentName}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/.`,
313
+ `fragments.${fragmentName}`
314
+ )
315
+ );
316
+ }
317
+ }
318
+ }
319
+ walkRenderableCard(views, fragments, (node, context) => {
320
+ if (!isFragmentUseLike(node)) {
321
+ return;
322
+ }
323
+ if (context.inFragments) {
324
+ errors.push(
325
+ createError(
326
+ "FRAGMENT_NESTED_USE",
327
+ `Fragments may not contain "$use" references. Found at "${context.path}".`,
328
+ context.path
329
+ )
330
+ );
331
+ return;
332
+ }
333
+ if (!fragments || !(node.$use in fragments)) {
334
+ errors.push(
335
+ createError(
336
+ "FRAGMENT_REF_NOT_FOUND",
337
+ `Fragment "${node.$use}" was not found.`,
338
+ context.path
339
+ )
340
+ );
341
+ }
342
+ });
343
+ return errors;
344
+ }
345
+
168
346
  // src/node-validator.ts
347
+ import { ALL_COMPONENT_TYPES, MAX_INTERACTIVE_ITEMS } from "@safe-ugc-ui/types";
169
348
  var REQUIRED_FIELDS = {
170
- Text: ["content"],
171
349
  Image: ["src"],
172
350
  ProgressBar: ["value", "max"],
173
351
  Avatar: ["src"],
@@ -175,7 +353,9 @@ var REQUIRED_FIELDS = {
175
353
  Badge: ["label"],
176
354
  Chip: ["label"],
177
355
  Button: ["label", "action"],
178
- Toggle: ["value", "onToggle"]
356
+ Toggle: ["value", "onToggle"],
357
+ Accordion: ["items"],
358
+ Tabs: ["tabs"]
179
359
  };
180
360
  var KNOWN_TYPES = new Set(ALL_COMPONENT_TYPES);
181
361
  function looksLikeForLoop(value) {
@@ -203,17 +383,42 @@ function validateForLoop(children, path) {
203
383
  );
204
384
  }
205
385
  const template = children["template"];
206
- if (typeof template !== "object" || template === null || !("type" in template)) {
386
+ if (typeof template !== "object" || template === null || !("type" in template) && !isFragmentUseLike(template)) {
207
387
  errors.push(
208
388
  createError(
209
389
  "INVALID_VALUE",
210
- 'ForLoop "template" must be an object with a "type" property.',
390
+ 'ForLoop "template" must be an object with a "type" property or "$use" reference.',
211
391
  `${path}.children.template`
212
392
  )
213
393
  );
214
394
  }
215
395
  return errors;
216
396
  }
397
+ function collectUniqueInteractiveItemIds(items, nodeType, path, errors) {
398
+ const itemIds = /* @__PURE__ */ new Set();
399
+ for (let i = 0; i < items.length; i++) {
400
+ const item = items[i];
401
+ if (item == null || typeof item !== "object" || Array.isArray(item)) {
402
+ continue;
403
+ }
404
+ const itemId = item.id;
405
+ if (typeof itemId !== "string" || itemId.length === 0) {
406
+ continue;
407
+ }
408
+ if (itemIds.has(itemId)) {
409
+ errors.push(
410
+ createError(
411
+ "INVALID_VALUE",
412
+ `"${nodeType}" item ids must be unique. Duplicate id "${itemId}".`,
413
+ `${path}[${i}].id`
414
+ )
415
+ );
416
+ continue;
417
+ }
418
+ itemIds.add(itemId);
419
+ }
420
+ return itemIds;
421
+ }
217
422
  function validateNode(node, context) {
218
423
  const errors = [];
219
424
  const { path } = context;
@@ -227,6 +432,28 @@ function validateNode(node, context) {
227
432
  );
228
433
  return errors;
229
434
  }
435
+ if (node.type === "Text") {
436
+ const hasContent = "content" in node && node.content !== void 0;
437
+ const hasSpans = "spans" in node && node.spans !== void 0;
438
+ if (!hasContent && !hasSpans) {
439
+ errors.push(
440
+ createError(
441
+ "MISSING_FIELD",
442
+ '"Text" node must define either "content" or "spans".',
443
+ `${path}.content`
444
+ )
445
+ );
446
+ }
447
+ if (hasContent && hasSpans) {
448
+ errors.push(
449
+ createError(
450
+ "INVALID_VALUE",
451
+ '"Text" node cannot define both "content" and "spans".',
452
+ path
453
+ )
454
+ );
455
+ }
456
+ }
230
457
  const requiredFields = REQUIRED_FIELDS[node.type];
231
458
  if (requiredFields && requiredFields.length > 0) {
232
459
  for (const field of requiredFields) {
@@ -249,12 +476,148 @@ function validateNode(node, context) {
249
476
  )
250
477
  );
251
478
  }
479
+ if (node.type === "Accordion") {
480
+ const items = Array.isArray(node.items) ? node.items : void 0;
481
+ if (items) {
482
+ if (items.length > MAX_INTERACTIVE_ITEMS) {
483
+ errors.push(
484
+ createError(
485
+ "INVALID_VALUE",
486
+ `"Accordion" node may define at most ${MAX_INTERACTIVE_ITEMS} items.`,
487
+ `${path}.items`
488
+ )
489
+ );
490
+ }
491
+ const itemIds = collectUniqueInteractiveItemIds(
492
+ items,
493
+ "Accordion",
494
+ `${path}.items`,
495
+ errors
496
+ );
497
+ if (Array.isArray(node.defaultExpanded)) {
498
+ if (node.allowMultiple !== true && node.defaultExpanded.length > 1) {
499
+ errors.push(
500
+ createError(
501
+ "INVALID_VALUE",
502
+ '"Accordion" cannot define multiple defaultExpanded ids unless "allowMultiple" is true.',
503
+ `${path}.defaultExpanded`
504
+ )
505
+ );
506
+ }
507
+ for (let i = 0; i < node.defaultExpanded.length; i++) {
508
+ const itemId = node.defaultExpanded[i];
509
+ if (typeof itemId !== "string" || !itemIds.has(itemId)) {
510
+ errors.push(
511
+ createError(
512
+ "INVALID_VALUE",
513
+ `"Accordion" defaultExpanded id "${String(itemId)}" was not found in items.`,
514
+ `${path}.defaultExpanded[${i}]`
515
+ )
516
+ );
517
+ }
518
+ }
519
+ }
520
+ }
521
+ }
522
+ if (node.type === "Tabs") {
523
+ const tabs = Array.isArray(node.tabs) ? node.tabs : void 0;
524
+ if (tabs) {
525
+ if (tabs.length > MAX_INTERACTIVE_ITEMS) {
526
+ errors.push(
527
+ createError(
528
+ "INVALID_VALUE",
529
+ `"Tabs" node may define at most ${MAX_INTERACTIVE_ITEMS} tabs.`,
530
+ `${path}.tabs`
531
+ )
532
+ );
533
+ }
534
+ const tabIds = collectUniqueInteractiveItemIds(
535
+ tabs,
536
+ "Tabs",
537
+ `${path}.tabs`,
538
+ errors
539
+ );
540
+ if ("defaultTab" in node) {
541
+ const defaultTab = node.defaultTab;
542
+ if (typeof defaultTab !== "string" || !tabIds.has(defaultTab)) {
543
+ errors.push(
544
+ createError(
545
+ "INVALID_VALUE",
546
+ `"Tabs" defaultTab "${String(defaultTab)}" was not found in tabs.`,
547
+ `${path}.defaultTab`
548
+ )
549
+ );
550
+ }
551
+ }
552
+ }
553
+ }
252
554
  return errors;
253
555
  }
254
- function validateNodes(views) {
556
+ function validateNodes(views, fragments) {
255
557
  const errors = [];
256
- traverseCard(views, (node, context) => {
257
- errors.push(...validateNode(node, context));
558
+ walkRenderableCard(views, fragments, (node, context) => {
559
+ if (!("type" in node) || typeof node.type !== "string") {
560
+ return;
561
+ }
562
+ errors.push(
563
+ ...validateNode(
564
+ node,
565
+ {
566
+ path: context.path,
567
+ depth: 0,
568
+ parentType: null,
569
+ loopDepth: 0,
570
+ overflowAutoAncestor: false,
571
+ stackDepth: 0
572
+ }
573
+ )
574
+ );
575
+ });
576
+ return errors;
577
+ }
578
+
579
+ // src/condition-validator.ts
580
+ import { MAX_CONDITION_DEPTH, isRef } from "@safe-ugc-ui/types";
581
+ function validateConditionDepth(condition, path, errors, depth = 1) {
582
+ if (depth > MAX_CONDITION_DEPTH) {
583
+ errors.push(
584
+ createError(
585
+ "CONDITION_DEPTH_EXCEEDED",
586
+ `$if condition exceeds maximum depth of ${MAX_CONDITION_DEPTH} at "${path}"`,
587
+ path
588
+ )
589
+ );
590
+ return;
591
+ }
592
+ if (typeof condition === "boolean" || isRef(condition)) {
593
+ return;
594
+ }
595
+ if (typeof condition !== "object" || condition === null || Array.isArray(condition)) {
596
+ return;
597
+ }
598
+ const conditionObj = condition;
599
+ const op = conditionObj.op;
600
+ if (op === "not") {
601
+ validateConditionDepth(conditionObj.value, `${path}.value`, errors, depth + 1);
602
+ return;
603
+ }
604
+ if ((op === "and" || op === "or") && Array.isArray(conditionObj.values)) {
605
+ for (let i = 0; i < conditionObj.values.length; i++) {
606
+ validateConditionDepth(
607
+ conditionObj.values[i],
608
+ `${path}.values[${i}]`,
609
+ errors,
610
+ depth + 1
611
+ );
612
+ }
613
+ }
614
+ }
615
+ function validateConditions(views, fragments) {
616
+ const errors = [];
617
+ walkRenderableCard(views, fragments, (node, ctx) => {
618
+ if ("$if" in node) {
619
+ validateConditionDepth(node.$if, `${ctx.path}.$if`, errors);
620
+ }
258
621
  });
259
622
  return errors;
260
623
  }
@@ -287,8 +650,7 @@ var STRUCTURED_OBJECT_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
287
650
  "boxShadow",
288
651
  "textShadow"
289
652
  ]);
290
- function validateNodeStyle(node, ctx, errors) {
291
- const style = node.style;
653
+ function validateNodeStyle(style, path, errors) {
292
654
  if (!style) {
293
655
  return;
294
656
  }
@@ -302,7 +664,7 @@ function validateNodeStyle(node, ctx, errors) {
302
664
  createError(
303
665
  "DYNAMIC_NOT_ALLOWED",
304
666
  `Style property "${prop}" must be a static literal; $ref is not allowed.`,
305
- `${ctx.path}.style.${prop}`
667
+ `${path}.${prop}`
306
668
  )
307
669
  );
308
670
  }
@@ -311,23 +673,39 @@ function validateNodeStyle(node, ctx, errors) {
311
673
  createError(
312
674
  "DYNAMIC_NOT_ALLOWED",
313
675
  `Style property "${prop}" must be an object literal; use $ref only inside its nested fields.`,
314
- `${ctx.path}.style.${prop}`
676
+ `${path}.${prop}`
315
677
  )
316
678
  );
317
679
  }
318
680
  }
319
681
  }
320
682
  }
321
- function validateValueTypes(views) {
683
+ function validateValueTypes(views, fragments) {
322
684
  const errors = [];
323
- traverseCard(views, (node, ctx) => {
324
- validateNodeStyle(node, ctx, errors);
685
+ walkRenderableCard(views, fragments, (node, ctx) => {
686
+ if (!("type" in node) || typeof node.type !== "string") {
687
+ return;
688
+ }
689
+ const style = node.style != null && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : void 0;
690
+ validateNodeStyle(style, `${ctx.path}.style`, errors);
691
+ const responsive = node.responsive;
692
+ if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
693
+ const compact = responsive.compact;
694
+ if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
695
+ validateNodeStyle(
696
+ compact,
697
+ `${ctx.path}.responsive.compact`,
698
+ errors
699
+ );
700
+ }
701
+ }
325
702
  });
326
703
  return errors;
327
704
  }
328
705
 
329
706
  // src/style-validator.ts
330
707
  import {
708
+ ASPECT_RATIO_PATTERN,
331
709
  FORBIDDEN_STYLE_PROPERTIES,
332
710
  DANGEROUS_CSS_FUNCTIONS,
333
711
  ZINDEX_MIN,
@@ -353,7 +731,7 @@ import {
353
731
  TRANSITION_DELAY_MAX,
354
732
  TRANSITION_MAX_COUNT,
355
733
  ALLOWED_TRANSITION_PROPERTIES,
356
- isRef
734
+ isRef as isRef2
357
735
  } from "@safe-ugc-ui/types";
358
736
  var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
359
737
  var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
@@ -409,7 +787,7 @@ function isLiteralString(value) {
409
787
  return typeof value === "string";
410
788
  }
411
789
  function isDynamic(value) {
412
- return isRef(value);
790
+ return isRef2(value);
413
791
  }
414
792
  function isValidColor(value) {
415
793
  const lower = value.toLowerCase();
@@ -437,6 +815,22 @@ function parseLengthValue(value) {
437
815
  }
438
816
  return Number(match[1]);
439
817
  }
818
+ function isValidAspectRatioLiteral(value) {
819
+ if (typeof value === "number") {
820
+ return Number.isFinite(value) && value > 0;
821
+ }
822
+ const match = value.match(ASPECT_RATIO_PATTERN);
823
+ if (!match) {
824
+ return false;
825
+ }
826
+ const parts = value.split("/");
827
+ if (parts.length !== 2) {
828
+ return false;
829
+ }
830
+ const width = Number(parts[0].trim());
831
+ const height = Number(parts[1].trim());
832
+ return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0;
833
+ }
440
834
  function collectDangerousCssErrors(value, path, errors) {
441
835
  if (typeof value === "string") {
442
836
  const lower = value.toLowerCase();
@@ -454,7 +848,7 @@ function collectDangerousCssErrors(value, path, errors) {
454
848
  }
455
849
  return;
456
850
  }
457
- if (isRef(value)) {
851
+ if (isRef2(value)) {
458
852
  return;
459
853
  }
460
854
  if (Array.isArray(value)) {
@@ -578,8 +972,15 @@ function collectNestedStyleRefErrors(value, path, errors) {
578
972
  collectNestedStyleRefErrors(child, childPath, errors);
579
973
  }
580
974
  }
581
- function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverStyleRefs = true) {
582
- const STRUCTURED_FIELDS = /* @__PURE__ */ new Set(["transition", "hoverStyle"]);
975
+ function validateSingleStyle(style, stylePath, errors, cardStyles, options = {}) {
976
+ const {
977
+ allowHoverStyle = true,
978
+ allowTransition = true,
979
+ allowHoverStyleRefs = true
980
+ } = options;
981
+ const STRUCTURED_FIELDS = /* @__PURE__ */ new Set();
982
+ if (allowTransition) STRUCTURED_FIELDS.add("transition");
983
+ if (allowHoverStyle) STRUCTURED_FIELDS.add("hoverStyle");
583
984
  for (const key of Object.keys(style)) {
584
985
  if (!STRUCTURED_FIELDS.has(key) && FORBIDDEN_STYLE_PROPERTIES.includes(key)) {
585
986
  errors.push(
@@ -591,6 +992,24 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
591
992
  );
592
993
  }
593
994
  }
995
+ if (!allowHoverStyle && "hoverStyle" in style) {
996
+ errors.push(
997
+ createError(
998
+ "INVALID_VALUE",
999
+ `hoverStyle is not allowed inside responsive overrides at "${stylePath}.hoverStyle"`,
1000
+ `${stylePath}.hoverStyle`
1001
+ )
1002
+ );
1003
+ }
1004
+ if (!allowTransition && "transition" in style) {
1005
+ errors.push(
1006
+ createError(
1007
+ "INVALID_VALUE",
1008
+ `transition is not allowed inside responsive overrides at "${stylePath}.transition"`,
1009
+ `${stylePath}.transition`
1010
+ )
1011
+ );
1012
+ }
594
1013
  if ("zIndex" in style && isLiteralNumber(style.zIndex)) {
595
1014
  const v = style.zIndex;
596
1015
  if (v < ZINDEX_MIN || v > ZINDEX_MAX) {
@@ -819,6 +1238,18 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
819
1238
  }
820
1239
  }
821
1240
  }
1241
+ if ("aspectRatio" in style && style.aspectRatio != null && !isDynamic(style.aspectRatio)) {
1242
+ const aspectRatio = style.aspectRatio;
1243
+ if (typeof aspectRatio !== "number" && typeof aspectRatio !== "string" || !isValidAspectRatioLiteral(aspectRatio)) {
1244
+ errors.push(
1245
+ createError(
1246
+ "INVALID_VALUE",
1247
+ `Invalid aspectRatio "${String(aspectRatio)}" at "${stylePath}.aspectRatio"`,
1248
+ `${stylePath}.aspectRatio`
1249
+ )
1250
+ );
1251
+ }
1252
+ }
822
1253
  for (const [prop, range] of Object.entries(RANGE_LENGTH_PROPERTIES)) {
823
1254
  if (prop in style && isLiteralString(style[prop]) && !isDynamic(style[prop])) {
824
1255
  const numericValue = parseLengthValue(style[prop]);
@@ -872,7 +1303,7 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
872
1303
  }
873
1304
  }
874
1305
  }
875
- if ("hoverStyle" in style && style.hoverStyle != null) {
1306
+ if (allowHoverStyle && "hoverStyle" in style && style.hoverStyle != null) {
876
1307
  const hoverStyle = style.hoverStyle;
877
1308
  const hoverPath = `${stylePath}.hoverStyle`;
878
1309
  if (typeof hoverStyle !== "object" || Array.isArray(hoverStyle)) {
@@ -901,12 +1332,16 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
901
1332
  hoverPath,
902
1333
  errors,
903
1334
  cardStyles,
904
- allowHoverStyleRefs
1335
+ {
1336
+ allowHoverStyle,
1337
+ allowTransition,
1338
+ allowHoverStyleRefs
1339
+ }
905
1340
  );
906
1341
  }
907
1342
  }
908
1343
  }
909
- if ("transition" in style && style.transition != null) {
1344
+ if (allowTransition && "transition" in style && style.transition != null) {
910
1345
  const transition = style.transition;
911
1346
  const transPath = `${stylePath}.transition`;
912
1347
  if (typeof transition === "string") {
@@ -977,7 +1412,7 @@ function mergeStyleWithRef(cardStyleEntry, inlineStyle) {
977
1412
  }
978
1413
  return merged;
979
1414
  }
980
- function validateStyles(views, cardStyles) {
1415
+ function validateStyles(views, cardStyles, fragments) {
981
1416
  const errors = [];
982
1417
  if (cardStyles) {
983
1418
  for (const [styleName, styleEntry] of Object.entries(cardStyles)) {
@@ -992,18 +1427,66 @@ function validateStyles(views, cardStyles) {
992
1427
  );
993
1428
  }
994
1429
  collectNestedStyleRefErrors(styleEntry, entryPath, errors);
995
- validateSingleStyle(styleEntry, entryPath, errors, void 0, false);
1430
+ validateSingleStyle(styleEntry, entryPath, errors, void 0, {
1431
+ allowHoverStyleRefs: false
1432
+ });
996
1433
  }
997
1434
  }
998
- traverseCard(views, (node, ctx) => {
999
- const style = node.style;
1000
- if (style == null || typeof style !== "object") {
1435
+ walkRenderableCard(views, fragments, (node, ctx) => {
1436
+ if (!("type" in node) || typeof node.type !== "string") {
1001
1437
  return;
1002
1438
  }
1003
- const stylePath = `${ctx.path}.style`;
1004
- const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
1005
- if (mergedStyle) {
1006
- validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
1439
+ const style = node.style != null && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : void 0;
1440
+ if (style != null && typeof style === "object") {
1441
+ const stylePath = `${ctx.path}.style`;
1442
+ const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
1443
+ if (mergedStyle) {
1444
+ validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
1445
+ }
1446
+ }
1447
+ const responsive = node.responsive;
1448
+ if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
1449
+ const compact = responsive.compact;
1450
+ if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
1451
+ const compactPath = `${ctx.path}.responsive.compact`;
1452
+ const mergedCompact = resolveStyleRef(
1453
+ compact,
1454
+ compactPath,
1455
+ cardStyles,
1456
+ errors
1457
+ );
1458
+ if (mergedCompact) {
1459
+ validateSingleStyle(mergedCompact, compactPath, errors, cardStyles, {
1460
+ allowHoverStyle: false,
1461
+ allowTransition: false,
1462
+ allowHoverStyleRefs: false
1463
+ });
1464
+ }
1465
+ }
1466
+ }
1467
+ const spans = Array.isArray(node.spans) ? node.spans : void 0;
1468
+ if (node.type === "Text" && spans) {
1469
+ for (let i = 0; i < spans.length; i++) {
1470
+ const span = spans[i];
1471
+ if (span == null || typeof span !== "object" || Array.isArray(span)) {
1472
+ continue;
1473
+ }
1474
+ const spanStyle = span.style;
1475
+ if (spanStyle == null || typeof spanStyle !== "object" || Array.isArray(spanStyle)) {
1476
+ continue;
1477
+ }
1478
+ validateSingleStyle(
1479
+ spanStyle,
1480
+ `${ctx.path}.spans[${i}].style`,
1481
+ errors,
1482
+ void 0,
1483
+ {
1484
+ allowHoverStyle: false,
1485
+ allowTransition: false,
1486
+ allowHoverStyleRefs: false
1487
+ }
1488
+ );
1489
+ }
1007
1490
  }
1008
1491
  });
1009
1492
  return errors;
@@ -1012,8 +1495,74 @@ function validateStyles(views, cardStyles) {
1012
1495
  // src/security.ts
1013
1496
  import {
1014
1497
  PROTOTYPE_POLLUTION_SEGMENTS,
1015
- isRef as isRef2
1498
+ isRef as isRef3
1016
1499
  } from "@safe-ugc-ui/types";
1500
+
1501
+ // src/responsive-utils.ts
1502
+ var RESPONSIVE_MODES = [
1503
+ "default",
1504
+ "compact"
1505
+ ];
1506
+ function mergeNamedStyle(style, cardStyles) {
1507
+ if (!style) return void 0;
1508
+ const rawStyleName = style.$style;
1509
+ const styleName = typeof rawStyleName === "string" ? rawStyleName.trim() : rawStyleName;
1510
+ if (!styleName || typeof styleName !== "string" || !cardStyles) {
1511
+ if (style.$style !== void 0) {
1512
+ const { $style: _2, ...rest } = style;
1513
+ return rest;
1514
+ }
1515
+ return style;
1516
+ }
1517
+ const baseStyle = cardStyles[styleName];
1518
+ if (!baseStyle) {
1519
+ const { $style: _2, ...rest } = style;
1520
+ return rest;
1521
+ }
1522
+ const { $style: _, ...inlineWithoutStyleRef } = style;
1523
+ return { ...baseStyle, ...inlineWithoutStyleRef };
1524
+ }
1525
+ function getCompactResponsiveStyle(node) {
1526
+ const responsive = node.responsive;
1527
+ if (responsive == null || typeof responsive !== "object" || Array.isArray(responsive)) {
1528
+ return void 0;
1529
+ }
1530
+ const compact = responsive.compact;
1531
+ if (compact == null || typeof compact !== "object" || Array.isArray(compact)) {
1532
+ return void 0;
1533
+ }
1534
+ return compact;
1535
+ }
1536
+ function stripResponsiveOnlyUnsupportedFields(style) {
1537
+ if (!style) return void 0;
1538
+ const {
1539
+ hoverStyle: _hoverStyle,
1540
+ transition: _transition,
1541
+ ...rest
1542
+ } = style;
1543
+ return rest;
1544
+ }
1545
+ function getEffectiveStyleForMode(node, cardStyles, mode) {
1546
+ const baseStyle = mergeNamedStyle(node.style, cardStyles);
1547
+ if (mode === "default") {
1548
+ return baseStyle;
1549
+ }
1550
+ const compactStyle = stripResponsiveOnlyUnsupportedFields(
1551
+ mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles)
1552
+ );
1553
+ if (!compactStyle) {
1554
+ return baseStyle;
1555
+ }
1556
+ return {
1557
+ ...baseStyle ?? {},
1558
+ ...compactStyle
1559
+ };
1560
+ }
1561
+ function getMergedCompactResponsiveStyle(node, cardStyles) {
1562
+ return mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles);
1563
+ }
1564
+
1565
+ // src/security.ts
1017
1566
  var FORBIDDEN_URL_PREFIXES = [
1018
1567
  "http://",
1019
1568
  "https://",
@@ -1025,7 +1574,7 @@ function scanForRefs(obj, path, errors) {
1025
1574
  if (obj === null || obj === void 0 || typeof obj !== "object") {
1026
1575
  return;
1027
1576
  }
1028
- if (isRef2(obj)) {
1577
+ if (isRef3(obj)) {
1029
1578
  const refStr = obj.$ref;
1030
1579
  const segments = refStr.split(/[.\[]/);
1031
1580
  for (const segment of segments) {
@@ -1130,9 +1679,102 @@ function checkSrcValue(resolved, type, errorPath, errors) {
1130
1679
  }
1131
1680
  }
1132
1681
  }
1682
+ function pushUniqueError(errors, seen, error) {
1683
+ const key = `${error.code}|${error.path}|${error.message}`;
1684
+ if (seen.has(key)) {
1685
+ return;
1686
+ }
1687
+ seen.add(key);
1688
+ errors.push(error);
1689
+ }
1690
+ function getScannableNodeFields(node) {
1691
+ const nodeFields = { ...node };
1692
+ delete nodeFields.type;
1693
+ delete nodeFields.style;
1694
+ delete nodeFields.children;
1695
+ const interactiveField = node.type === "Accordion" ? "items" : node.type === "Tabs" ? "tabs" : null;
1696
+ if (interactiveField && Array.isArray(node[interactiveField])) {
1697
+ nodeFields[interactiveField] = node[interactiveField].map((item) => {
1698
+ if (item == null || typeof item !== "object" || Array.isArray(item)) {
1699
+ return item;
1700
+ }
1701
+ const { content: _content, ...rest } = item;
1702
+ return rest;
1703
+ });
1704
+ }
1705
+ return nodeFields;
1706
+ }
1707
+ function scanStyleStringsForUrl(style, path, errors) {
1708
+ if (!style) {
1709
+ return;
1710
+ }
1711
+ for (const [prop, value] of Object.entries(style)) {
1712
+ if (typeof value === "string" && value.toLowerCase().includes("url(")) {
1713
+ errors.push(
1714
+ createError(
1715
+ "FORBIDDEN_CSS_FUNCTION",
1716
+ `CSS url() function is forbidden in style values. Found in "${prop}".`,
1717
+ `${path}.${prop}`
1718
+ )
1719
+ );
1720
+ }
1721
+ }
1722
+ }
1723
+ function validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen) {
1724
+ const styleResolver = (node) => getEffectiveStyleForMode(node, cardStyles, mode);
1725
+ traverseCard(views, (node, context) => {
1726
+ const effectiveStyle = styleResolver(node);
1727
+ if (effectiveStyle && typeof effectiveStyle.position === "string") {
1728
+ const position = effectiveStyle.position;
1729
+ if (position === "fixed") {
1730
+ pushUniqueError(
1731
+ errors,
1732
+ seen,
1733
+ createError(
1734
+ "POSITION_FIXED_FORBIDDEN",
1735
+ 'CSS position "fixed" is not allowed.',
1736
+ `${context.path}.style.position`
1737
+ )
1738
+ );
1739
+ } else if (position === "sticky") {
1740
+ pushUniqueError(
1741
+ errors,
1742
+ seen,
1743
+ createError(
1744
+ "POSITION_STICKY_FORBIDDEN",
1745
+ 'CSS position "sticky" is not allowed.',
1746
+ `${context.path}.style.position`
1747
+ )
1748
+ );
1749
+ } else if (position === "absolute" && context.parentType !== "Stack") {
1750
+ pushUniqueError(
1751
+ errors,
1752
+ seen,
1753
+ createError(
1754
+ "POSITION_ABSOLUTE_NOT_IN_STACK",
1755
+ 'CSS position "absolute" is only allowed inside a Stack component.',
1756
+ `${context.path}.style.position`
1757
+ )
1758
+ );
1759
+ }
1760
+ }
1761
+ if (effectiveStyle && effectiveStyle.overflow === "auto" && context.overflowAutoAncestor) {
1762
+ pushUniqueError(
1763
+ errors,
1764
+ seen,
1765
+ createError(
1766
+ "OVERFLOW_AUTO_NESTED",
1767
+ "Nested overflow:auto is not allowed. An ancestor already has overflow:auto.",
1768
+ `${context.path}.style.overflow`
1769
+ )
1770
+ );
1771
+ }
1772
+ }, styleResolver, fragments);
1773
+ }
1133
1774
  function validateSecurity(card) {
1134
1775
  const errors = [];
1135
- const { views, state, cardAssets, cardStyles } = card;
1776
+ const seen = /* @__PURE__ */ new Set();
1777
+ const { views, state, cardAssets, cardStyles, fragments } = card;
1136
1778
  if (cardAssets) {
1137
1779
  for (const [key, value] of Object.entries(cardAssets)) {
1138
1780
  const assetError = validateAssetPath(value);
@@ -1155,18 +1797,20 @@ function validateSecurity(card) {
1155
1797
  }
1156
1798
  }
1157
1799
  }
1158
- traverseCard(views, (node, context) => {
1800
+ walkRenderableCard(views, fragments, (node, context) => {
1801
+ if (!("type" in node) || typeof node.type !== "string") {
1802
+ return;
1803
+ }
1804
+ const traversableNode = node;
1159
1805
  const { path } = context;
1160
- const { style, type } = node;
1161
- const nodeFields = { ...node };
1162
- delete nodeFields.type;
1163
- delete nodeFields.style;
1164
- delete nodeFields.children;
1806
+ const style = traversableNode.style != null && typeof traversableNode.style === "object" && !Array.isArray(traversableNode.style) ? traversableNode.style : void 0;
1807
+ const type = traversableNode.type;
1808
+ const nodeFields = getScannableNodeFields(traversableNode);
1165
1809
  if (type === "Image" || type === "Avatar") {
1166
1810
  const src = node.src;
1167
1811
  if (typeof src === "string") {
1168
1812
  checkSrcValue(src, type, `${path}.src`, errors);
1169
- } else if (isRef2(src)) {
1813
+ } else if (isRef3(src)) {
1170
1814
  if (state) {
1171
1815
  const resolved = resolveRefFromState(
1172
1816
  src.$ref,
@@ -1178,74 +1822,46 @@ function validateSecurity(card) {
1178
1822
  }
1179
1823
  }
1180
1824
  }
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
- }
1825
+ scanStyleStringsForUrl(style, `${path}.style`, errors);
1826
+ const responsive = traversableNode.responsive;
1827
+ if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
1828
+ const compact = responsive.compact;
1829
+ if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
1830
+ scanStyleStringsForUrl(
1831
+ compact,
1832
+ `${path}.responsive.compact`,
1833
+ errors
1834
+ );
1192
1835
  }
1193
1836
  }
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;
1837
+ if (type === "Text" && Array.isArray(traversableNode.spans)) {
1838
+ const spans = traversableNode.spans;
1839
+ for (let i = 0; i < spans.length; i++) {
1840
+ const span = spans[i];
1841
+ if (span == null || typeof span !== "object" || Array.isArray(span)) {
1842
+ continue;
1201
1843
  }
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
- )
1222
- );
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
- )
1844
+ const spanStyle = span.style;
1845
+ if (spanStyle != null && typeof spanStyle === "object" && !Array.isArray(spanStyle)) {
1846
+ scanStyleStringsForUrl(
1847
+ spanStyle,
1848
+ `${path}.spans[${i}].style`,
1849
+ errors
1231
1850
  );
1232
1851
  }
1233
1852
  }
1234
1853
  }
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
1854
  scanForRefs(nodeFields, path, errors);
1245
1855
  if (style) {
1246
1856
  scanForRefs(style, `${path}.style`, errors);
1247
1857
  }
1858
+ if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
1859
+ scanForRefs(responsive, `${path}.responsive`, errors);
1860
+ }
1248
1861
  });
1862
+ for (const mode of RESPONSIVE_MODES) {
1863
+ validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen);
1864
+ }
1249
1865
  return errors;
1250
1866
  }
1251
1867
 
@@ -1258,8 +1874,7 @@ import {
1258
1874
  MAX_NESTED_LOOPS,
1259
1875
  MAX_OVERFLOW_AUTO_COUNT,
1260
1876
  MAX_STACK_NESTING,
1261
- PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2,
1262
- isRef as isRef3
1877
+ PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2
1263
1878
  } from "@safe-ugc-ui/types";
1264
1879
  function utf8ByteLength(str) {
1265
1880
  let bytes = 0;
@@ -1278,6 +1893,54 @@ function utf8ByteLength(str) {
1278
1893
  }
1279
1894
  return bytes;
1280
1895
  }
1896
+ function isTemplateObject(value) {
1897
+ return typeof value === "object" && value !== null && "$template" in value && Array.isArray(value.$template);
1898
+ }
1899
+ function countLiteralTemplatedStringBytes(value) {
1900
+ if (typeof value === "string") {
1901
+ return utf8ByteLength(value);
1902
+ }
1903
+ if (!isTemplateObject(value)) {
1904
+ return 0;
1905
+ }
1906
+ return value.$template.reduce((total, part) => {
1907
+ if (typeof part === "string") {
1908
+ return total + utf8ByteLength(part);
1909
+ }
1910
+ if (typeof part === "number" || typeof part === "boolean" || part === null) {
1911
+ return total + utf8ByteLength(String(part));
1912
+ }
1913
+ return total;
1914
+ }, 0);
1915
+ }
1916
+ function countTextNodeLiteralBytes(node) {
1917
+ if (Array.isArray(node.spans)) {
1918
+ return node.spans.reduce((total, span) => {
1919
+ if (span == null || typeof span !== "object" || Array.isArray(span)) {
1920
+ return total;
1921
+ }
1922
+ return total + countLiteralTemplatedStringBytes(
1923
+ span.text
1924
+ );
1925
+ }, 0);
1926
+ }
1927
+ return countLiteralTemplatedStringBytes(node.content);
1928
+ }
1929
+ function countTextSpanStyleBytes(node) {
1930
+ if (!Array.isArray(node.spans)) {
1931
+ return 0;
1932
+ }
1933
+ return node.spans.reduce((total, span) => {
1934
+ if (span == null || typeof span !== "object" || Array.isArray(span)) {
1935
+ return total;
1936
+ }
1937
+ const style = span.style;
1938
+ if (style == null || typeof style !== "object" || Array.isArray(style)) {
1939
+ return total;
1940
+ }
1941
+ return total + utf8ByteLength(JSON.stringify(style));
1942
+ }, 0);
1943
+ }
1281
1944
  function resolveRefFromState2(refPath, state) {
1282
1945
  const path = refPath.startsWith("$") ? refPath.slice(1) : refPath;
1283
1946
  const dotSegments = path.split(".");
@@ -1312,58 +1975,49 @@ function resolveRefFromState2(refPath, state) {
1312
1975
  }
1313
1976
  return current;
1314
1977
  }
1315
- function countTemplateMetrics(template, cardStyles) {
1316
- const result = { nodes: 0, textBytes: 0, styleBytes: 0, overflowAutoCount: 0 };
1317
- if (template == null || typeof template !== "object") return result;
1318
- const node = template;
1319
- if (!node.type) return result;
1320
- result.nodes = 1;
1321
- if (node.type === "Text") {
1322
- const content = node.content;
1323
- if (typeof content === "string" && !isRef3(content)) {
1324
- result.textBytes = utf8ByteLength(content);
1325
- }
1326
- }
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;
1978
+ function countTemplateMetrics(template, cardStyles, fragments) {
1979
+ const result = {
1980
+ nodes: 0,
1981
+ textBytes: 0,
1982
+ styleBytes: 0,
1983
+ overflowAutoCount: {
1984
+ default: 0,
1985
+ compact: 0
1986
+ }
1987
+ };
1988
+ traverseCard(
1989
+ { __template: template },
1990
+ (node) => {
1991
+ result.nodes += 1;
1992
+ if (node.type === "Text") {
1993
+ result.textBytes += countTextNodeLiteralBytes(node);
1994
+ result.styleBytes += countTextSpanStyleBytes(node);
1335
1995
  }
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;
1996
+ const baseStyleForBytes = getEffectiveStyleForMode(
1997
+ node,
1998
+ cardStyles,
1999
+ "default"
2000
+ );
2001
+ if (baseStyleForBytes) {
2002
+ result.styleBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
1344
2003
  }
1345
- }
1346
- if (effectiveOverflow === "auto") {
1347
- result.overflowAutoCount = 1;
1348
- }
1349
- }
1350
- const children = node.children;
1351
- if (Array.isArray(children)) {
1352
- for (const child of children) {
1353
- const childMetrics = countTemplateMetrics(child, cardStyles);
1354
- result.nodes += childMetrics.nodes;
1355
- result.textBytes += childMetrics.textBytes;
1356
- result.styleBytes += childMetrics.styleBytes;
1357
- result.overflowAutoCount += childMetrics.overflowAutoCount;
1358
- }
1359
- } else if (children != null && typeof children === "object" && !Array.isArray(children) && "template" in children) {
1360
- const innerTemplate = children.template;
1361
- const innerMetrics = countTemplateMetrics(innerTemplate, cardStyles);
1362
- result.nodes += innerMetrics.nodes;
1363
- result.textBytes += innerMetrics.textBytes;
1364
- result.styleBytes += innerMetrics.styleBytes;
1365
- result.overflowAutoCount += innerMetrics.overflowAutoCount;
1366
- }
2004
+ const compactStyleForBytes = getMergedCompactResponsiveStyle(
2005
+ node,
2006
+ cardStyles
2007
+ );
2008
+ if (compactStyleForBytes) {
2009
+ result.styleBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
2010
+ }
2011
+ for (const mode of RESPONSIVE_MODES) {
2012
+ const effectiveStyle = getEffectiveStyleForMode(node, cardStyles, mode);
2013
+ if (effectiveStyle?.overflow === "auto") {
2014
+ result.overflowAutoCount[mode]++;
2015
+ }
2016
+ }
2017
+ },
2018
+ void 0,
2019
+ fragments
2020
+ );
1367
2021
  return result;
1368
2022
  }
1369
2023
  function validateLimits(card) {
@@ -1371,29 +2025,32 @@ function validateLimits(card) {
1371
2025
  let nodeCount = 0;
1372
2026
  let textContentBytes = 0;
1373
2027
  let styleObjectsBytes = 0;
1374
- let overflowAutoCount = 0;
2028
+ const overflowAutoCount = {
2029
+ default: 0,
2030
+ compact: 0
2031
+ };
1375
2032
  traverseCard(card.views, (node, context) => {
1376
2033
  nodeCount++;
1377
2034
  if (node.type === "Text") {
1378
- const content = node.content;
1379
- if (typeof content === "string" && !isRef3(content)) {
1380
- textContentBytes += utf8ByteLength(content);
1381
- }
2035
+ textContentBytes += countTextNodeLiteralBytes(node);
2036
+ styleObjectsBytes += countTextSpanStyleBytes(node);
1382
2037
  }
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;
2038
+ {
2039
+ const baseStyleForBytes = getEffectiveStyleForMode(
2040
+ node,
2041
+ card.cardStyles,
2042
+ "default"
2043
+ );
2044
+ if (baseStyleForBytes) {
2045
+ styleObjectsBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
2046
+ }
2047
+ const compactStyleForBytes = getMergedCompactResponsiveStyle(
2048
+ node,
2049
+ card.cardStyles
2050
+ );
2051
+ if (compactStyleForBytes) {
2052
+ styleObjectsBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
1394
2053
  }
1395
- const serialized = JSON.stringify(styleForBytes);
1396
- styleObjectsBytes += utf8ByteLength(serialized);
1397
2054
  }
1398
2055
  const children = node.children;
1399
2056
  if (children != null && typeof children === "object" && !Array.isArray(children) && "for" in children && "in" in children && "template" in children) {
@@ -1431,12 +2088,14 @@ function validateLimits(card) {
1431
2088
  )
1432
2089
  );
1433
2090
  } else if (source.length > 1) {
1434
- const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles);
2091
+ const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles, card.fragments);
1435
2092
  const multiplier = source.length - 1;
1436
2093
  nodeCount += tplMetrics.nodes * multiplier;
1437
2094
  textContentBytes += tplMetrics.textBytes * multiplier;
1438
2095
  styleObjectsBytes += tplMetrics.styleBytes * multiplier;
1439
- overflowAutoCount += tplMetrics.overflowAutoCount * multiplier;
2096
+ for (const mode of RESPONSIVE_MODES) {
2097
+ overflowAutoCount[mode] += tplMetrics.overflowAutoCount[mode] * multiplier;
2098
+ }
1440
2099
  }
1441
2100
  }
1442
2101
  }
@@ -1450,16 +2109,10 @@ function validateLimits(card) {
1450
2109
  );
1451
2110
  }
1452
2111
  }
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++;
2112
+ for (const mode of RESPONSIVE_MODES) {
2113
+ const effectiveStyle = getEffectiveStyleForMode(node, card.cardStyles, mode);
2114
+ if (effectiveStyle?.overflow === "auto") {
2115
+ overflowAutoCount[mode]++;
1463
2116
  }
1464
2117
  }
1465
2118
  if (node.type === "Stack" && context.stackDepth >= MAX_STACK_NESTING) {
@@ -1471,7 +2124,7 @@ function validateLimits(card) {
1471
2124
  )
1472
2125
  );
1473
2126
  }
1474
- });
2127
+ }, void 0, card.fragments);
1475
2128
  if (nodeCount > MAX_NODE_COUNT) {
1476
2129
  errors.push(
1477
2130
  createError(
@@ -1499,11 +2152,15 @@ function validateLimits(card) {
1499
2152
  )
1500
2153
  );
1501
2154
  }
1502
- if (overflowAutoCount > MAX_OVERFLOW_AUTO_COUNT) {
2155
+ const maxOverflowAutoCount = Math.max(
2156
+ overflowAutoCount.default,
2157
+ overflowAutoCount.compact
2158
+ );
2159
+ if (maxOverflowAutoCount > MAX_OVERFLOW_AUTO_COUNT) {
1503
2160
  errors.push(
1504
2161
  createError(
1505
2162
  "OVERFLOW_AUTO_COUNT_EXCEEDED",
1506
- `Card has ${overflowAutoCount} elements with overflow:auto, max is ${MAX_OVERFLOW_AUTO_COUNT}`,
2163
+ `Card has ${maxOverflowAutoCount} elements with overflow:auto in at least one responsive mode, max is ${MAX_OVERFLOW_AUTO_COUNT}`,
1507
2164
  "views"
1508
2165
  )
1509
2166
  );
@@ -1533,17 +2190,26 @@ function runAllChecks(input) {
1533
2190
  const obj = input;
1534
2191
  const views = obj.views;
1535
2192
  const cardStyles = obj.styles;
2193
+ const fragments = obj.fragments;
1536
2194
  const errors = [];
1537
- errors.push(...validateNodes(views));
1538
- errors.push(...validateValueTypes(views));
1539
- errors.push(...validateStyles(views, cardStyles));
2195
+ errors.push(...validateFragments(views, fragments));
2196
+ errors.push(...validateNodes(views, fragments));
2197
+ errors.push(...validateConditions(views, fragments));
2198
+ errors.push(...validateValueTypes(views, fragments));
2199
+ errors.push(...validateStyles(views, cardStyles, fragments));
1540
2200
  errors.push(...validateSecurity({
1541
2201
  views,
1542
2202
  state: obj.state,
1543
2203
  cardAssets: obj.assets,
1544
- cardStyles
2204
+ cardStyles,
2205
+ fragments
2206
+ }));
2207
+ errors.push(...validateLimits({
2208
+ state: obj.state,
2209
+ views,
2210
+ cardStyles,
2211
+ fragments
1545
2212
  }));
1546
- errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
1547
2213
  return errors;
1548
2214
  }
1549
2215
  function validate(input) {
@@ -1586,6 +2252,8 @@ export {
1586
2252
  traverseNode,
1587
2253
  validResult,
1588
2254
  validate,
2255
+ validateConditions,
2256
+ validateFragments,
1589
2257
  validateLimits,
1590
2258
  validateNodes,
1591
2259
  validateRaw,