@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
|
|
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),
|