@vectoriox/iox-builder 4.7.0 → 4.9.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.
@@ -16,7 +16,7 @@ import { SelectModule } from 'primeng/select';
16
16
  import { TooltipModule } from 'primeng/tooltip';
17
17
  import { DialogModule } from 'primeng/dialog';
18
18
  import * as i17 from '@vectoriox/iox-ui';
19
- import { DEFAULT_WIDTH_BY_TYPE, Breakpoint, BREAKPOINTS, countOverrides, setOverride, breakpointDef, breakpointForWidth, ContextStore, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, DeclarationContext, planResponsiveRules, resolveStyleProps, buildTargetedStateSelector, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, safeSetInput, composeVirtualTraits, stateNamesIn, collectContextSlotKeys, withContextSlots, nodeUsesContext, resolvePath, applyBindingPipes, IS_PREVIEW, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, IoxPageModule } from '@vectoriox/iox-ui';
19
+ import { DEFAULT_WIDTH_BY_TYPE, Breakpoint, BREAKPOINTS, countOverrides, setOverride, breakpointDef, breakpointForWidth, ContextStore, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, DeclarationContext, planResponsiveRules, resolveStyleProps, buildTargetedStateSelector, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, safeSetInput, composeVirtualTraits, stateNamesIn, resolveInputs, setInputOverride, collectContextSlotKeys, withContextSlots, nodeUsesContext, resolvePath, applyBindingPipes, IS_PREVIEW, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, IoxPageModule } from '@vectoriox/iox-ui';
20
20
  export { ComponentInstanceRegistryService, VIRTUAL_TRAIT_KEYS, composeVirtualTraits } from '@vectoriox/iox-ui';
21
21
  import Lenis from 'lenis';
22
22
  import { Subject, BehaviorSubject, of, forkJoin } from 'rxjs';
@@ -218,7 +218,7 @@ class ComponentConfig {
218
218
  }
219
219
  }
220
220
  class TraitConfig {
221
- constructor(name, label, type, options, defaultValue, inline, showWhen) {
221
+ constructor(name, label, type, options, defaultValue, inline, showWhen, responsive) {
222
222
  this.name = name;
223
223
  this.label = label;
224
224
  this.type = type;
@@ -226,6 +226,7 @@ class TraitConfig {
226
226
  this.default = defaultValue;
227
227
  this.inline = inline;
228
228
  this.showWhen = showWhen;
229
+ this.responsive = responsive;
229
230
  }
230
231
  }
231
232
  class GroupStyleConfig {
@@ -1201,6 +1202,10 @@ var AuditCode;
1201
1202
  AuditCode["TapTarget"] = "tap-target";
1202
1203
  AuditCode["TinyText"] = "tiny-text";
1203
1204
  AuditCode["UnboundedImage"] = "unbounded-image";
1205
+ /** A slider showing more slides than the width can carry — each one unreadably narrow. */
1206
+ AuditCode["SlidesOverflow"] = "slides-overflow";
1207
+ /** An out-of-flow node whose box lands outside the frame, so it is unreachable at this width. */
1208
+ AuditCode["OffscreenAbsolute"] = "offscreen-absolute";
1204
1209
  })(AuditCode || (AuditCode = {}));
1205
1210
  var AuditSeverity;
1206
1211
  (function (AuditSeverity) {
@@ -1214,6 +1219,10 @@ const MIN_TAP_PX = 44;
1214
1219
  const MIN_TEXT_PX = 12;
1215
1220
  /** Types a visitor taps. */
1216
1221
  const TAPPABLE = new Set(['Button', 'ButtonBlock', 'Link', 'LinkedContainer']);
1222
+ /** Narrower than this, a slide shows too little of its content to be worth showing at all. */
1223
+ const MIN_SLIDE_PX = 200;
1224
+ /** Out-of-flow positioning that can put a node outside the frame. */
1225
+ const OUT_OF_FLOW = new Set(['absolute', 'fixed']);
1217
1226
  const nameOf = (node) => node.label || node.type;
1218
1227
  const pxOf = (value) => {
1219
1228
  const match = /^(-?\d+(?:\.\d+)?)px$/i.exec(String(value ?? ''));
@@ -1276,6 +1285,37 @@ function auditNodes(nodes, viewportWidth) {
1276
1285
  fix: { height: 'auto', aspectRatio: '16 / 9' },
1277
1286
  });
1278
1287
  }
1288
+ // 6. A slider showing more slides than this width can carry. NOT a CSS problem: slide count
1289
+ // is a component trait, so the fix is a per-breakpoint TRAIT override (P3-G1), not a style.
1290
+ if (node.type === 'SliderContainer' && node.width > 0) {
1291
+ const perView = Number(node.inputs?.['slidesPerView']);
1292
+ if (Number.isFinite(perView) && perView > 1) {
1293
+ const slideWidth = node.width / perView;
1294
+ if (slideWidth < MIN_SLIDE_PX) {
1295
+ const fits = Math.max(1, Math.floor(node.width / MIN_SLIDE_PX));
1296
+ findings.push({
1297
+ nodeId: node.id, nodeName: name, code: AuditCode.SlidesOverflow, severity: AuditSeverity.Warning,
1298
+ message: `${name} shows ${perView} slides across ${Math.round(node.width)}px — ${Math.round(slideWidth)}px each. Show ${fits} at this width.`,
1299
+ inputFix: { slidesPerView: fits },
1300
+ });
1301
+ }
1302
+ }
1303
+ }
1304
+ // 7. An out-of-flow node that has left the frame. Measured, not inferred: a negative offset
1305
+ // is legitimate design until it actually puts the content beyond the edge.
1306
+ if (OUT_OF_FLOW.has(String(styles['position'] ?? '')) && node.left !== undefined && node.width > 0) {
1307
+ const right = node.left + node.width;
1308
+ const offLeft = right <= 0.5;
1309
+ const offRight = node.left >= viewportWidth - 0.5;
1310
+ if (offLeft || offRight) {
1311
+ findings.push({
1312
+ nodeId: node.id, nodeName: name, code: AuditCode.OffscreenAbsolute, severity: AuditSeverity.Error,
1313
+ message: `${name} is positioned ${offLeft ? 'left of' : 'beyond'} the ${viewportWidth}px viewport — it cannot be seen or tapped at this width.`,
1314
+ // No blind auto-fix: the author's intent (a decorative overflow vs a mistake)
1315
+ // is not knowable from geometry. Selecting the node is the useful action.
1316
+ });
1317
+ }
1318
+ }
1279
1319
  }
1280
1320
  const rank = { [AuditSeverity.Error]: 0, [AuditSeverity.Warning]: 1, [AuditSeverity.Info]: 2 };
1281
1321
  return findings
@@ -4273,6 +4313,12 @@ class RenderDirective {
4273
4313
  return;
4274
4314
  safeSetInput(componentRef, trait.name, trait.default);
4275
4315
  });
4316
+ // Per-breakpoint TRAIT overrides — the same hand-off the SSR engine makes. A component that
4317
+ // declares a `responsive` input (SliderContainer) converts it to its own width config, so the
4318
+ // canvas and the live site agree without the canvas needing a resize listener. Dropped by
4319
+ // safeSetInput for every component that does not declare it.
4320
+ if (node.responsive)
4321
+ safeSetInput(componentRef, 'responsive', node.responsive);
4276
4322
  // Build initial style map from styleTraits defaults.
4277
4323
  const initialStyle = {};
4278
4324
  if (node.styleTraits?.length) {
@@ -5600,6 +5646,9 @@ class BuilderComponent {
5600
5646
  this.auditRunning = true;
5601
5647
  const scale = this.viewportService.getScale() || 1;
5602
5648
  const width = this.viewportService.getState().width;
5649
+ // Positions are reported relative to the canvas FRAME, not the editor window, so an
5650
+ // "off-screen" finding means off the simulated viewport rather than off the user's monitor.
5651
+ const frame = this.previewCanvas?.nativeElement?.getBoundingClientRect() ?? { left: 0, top: 0 };
5603
5652
  const nodes = [];
5604
5653
  const visit = (list) => {
5605
5654
  for (const node of list) {
@@ -5611,8 +5660,13 @@ class BuilderComponent {
5611
5660
  type: node.type,
5612
5661
  label: node.label,
5613
5662
  styleProps: resolveStyleProps(this.baseStyleMapOf(node), node.responsive, this.activeBreakpoint),
5663
+ // Traits resolved at this width too — some failures (a 4-up slider on a
5664
+ // phone) are invisible to a style-only audit. See P3-G1/G4.
5665
+ inputs: resolveInputs(this.baseInputMapOf(node), node.responsive, this.activeBreakpoint),
5614
5666
  width: rect.width / scale,
5615
5667
  height: rect.height / scale,
5668
+ left: (rect.left - frame.left) / scale,
5669
+ top: (rect.top - frame.top) / scale,
5616
5670
  });
5617
5671
  }
5618
5672
  if (node.children?.length)
@@ -5625,10 +5679,38 @@ class BuilderComponent {
5625
5679
  this.auditRunning = false;
5626
5680
  this.cdr.detectChanges();
5627
5681
  }
5682
+ /** The node's OWN trait values — the Base layer the responsive `inputs` cascade sits on top of. */
5683
+ baseInputMapOf(node) {
5684
+ const out = {};
5685
+ for (const trait of (node.traits ?? [])) {
5686
+ if (trait?.name !== undefined && trait.default !== undefined)
5687
+ out[trait.name] = trait.default;
5688
+ }
5689
+ return out;
5690
+ }
5628
5691
  /** Apply one finding's fix into the current breakpoint's layer (or the base styles at Base). */
5629
5692
  applyAuditFix(finding) {
5630
5693
  const node = this.findNodeById(finding.nodeId, this.layout) ?? this.findNodeById(finding.nodeId, this.globalElements);
5631
- if (!node || !finding.fix)
5694
+ if (!node)
5695
+ return;
5696
+ // A TRAIT fix (slide count) is not CSS — it lands in the breakpoint's `inputs` layer, or on
5697
+ // the node itself at Base, mirroring exactly how a style fix is applied below.
5698
+ if (finding.inputFix) {
5699
+ for (const [name, value] of Object.entries(finding.inputFix)) {
5700
+ if (this.activeBreakpoint === Breakpoint.Base) {
5701
+ const trait = (node.traits ?? []).find((t) => t.name === name);
5702
+ if (trait)
5703
+ trait.default = value;
5704
+ }
5705
+ else {
5706
+ node.responsive = setInputOverride(node.responsive, this.activeBreakpoint, name, value);
5707
+ }
5708
+ }
5709
+ this.panelEventService.emit(PanelEventTypes.LAYOUT_CHANGED, this.layout);
5710
+ this.runAudit();
5711
+ return;
5712
+ }
5713
+ if (!finding.fix)
5632
5714
  return;
5633
5715
  if (this.activeBreakpoint === Breakpoint.Base) {
5634
5716
  for (const [name, value] of Object.entries(finding.fix)) {
@@ -5657,7 +5739,7 @@ class BuilderComponent {
5657
5739
  }
5658
5740
  /** Apply every fixable finding, then re-measure. */
5659
5741
  applyAllAuditFixes() {
5660
- for (const finding of this.auditFindings.filter(f => !!f.fix))
5742
+ for (const finding of this.auditFindings.filter(f => !!f.fix || !!f.inputFix))
5661
5743
  this.applyAuditFix(finding);
5662
5744
  }
5663
5745
  /** Select the node a finding is about. */
@@ -9462,6 +9544,14 @@ class RepeaterComponentConfig extends ComponentConfig {
9462
9544
  label: 'Limit',
9463
9545
  type: TraitInputType.Number,
9464
9546
  default: 10,
9547
+ // NOT marked `responsive` yet, deliberately. Row COUNT cannot be varied per width the
9548
+ // way the slider's can: Swiper reads its own breakpoints map at runtime, but a
9549
+ // repeater's count is decided when the markup is produced — and SSR has no viewport,
9550
+ // so rendering N on the server and M in the browser is a hydration mismatch. The fix
9551
+ // is to keep rendering every item and HIDE the surplus per width with a generated
9552
+ // `:nth-child(n+X) { display: none }` rule (CSS-expressible, hydration-safe), which is
9553
+ // a separate change. Marking it responsive now would ship a control that silently
9554
+ // does nothing on the live site — exactly the `slidesPerGroup` bug this pass fixed.
9465
9555
  },
9466
9556
  ];
9467
9557
  this.styleTraits = buildFullStyleTraits();
@@ -9562,15 +9652,15 @@ class SliderContainerConfig extends ComponentConfig {
9562
9652
  { label: '4', value: 4 },
9563
9653
  { label: '5', value: 5 },
9564
9654
  { label: '6', value: 6 },
9565
- ], 'auto'),
9566
- new TraitConfig('slidesPerGroup', 'Slides per group', TraitInputType.Scrub, undefined, 1),
9655
+ ], 'auto', undefined, undefined, /* responsive */ true),
9656
+ new TraitConfig('slidesPerGroup', 'Slides per group', TraitInputType.Scrub, undefined, 1, undefined, undefined, /* responsive */ true),
9567
9657
  new TraitConfig('loop', 'Loop', TraitInputType.SelectButton, [
9568
9658
  { label: 'Off', value: 'off' },
9569
9659
  { label: 'Loop', value: 'loop' },
9570
9660
  { label: 'Rewind', value: 'rewind' },
9571
9661
  ], 'off'),
9572
9662
  new TraitConfig('centeredSlides', 'Centered slides', TraitInputType.Checkbox, undefined, false),
9573
- new TraitConfig('spaceBetween', 'Space between (px)', TraitInputType.Scrub, undefined, 0),
9663
+ new TraitConfig('spaceBetween', 'Space between (px)', TraitInputType.Scrub, undefined, 0, undefined, undefined, /* responsive */ true),
9574
9664
  new TraitConfig('initialSlide', 'Initial slide', TraitInputType.Scrub, undefined, 0),
9575
9665
  new TraitConfig('autoHeight', 'Auto height', TraitInputType.Checkbox, undefined, false),
9576
9666
  new TraitConfig('grabCursor', 'Grab cursor', TraitInputType.Checkbox, undefined, true),