@vectoriox/iox-builder 1.4.3 → 1.4.5

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.
@@ -1294,177 +1294,541 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
1294
1294
  type: Injectable
1295
1295
  }], ctorParameters: () => [{ type: OverlayService }] });
1296
1296
 
1297
+ var StyleCategory;
1298
+ (function (StyleCategory) {
1299
+ StyleCategory["Layout"] = "Layout";
1300
+ StyleCategory["Size"] = "Size";
1301
+ StyleCategory["Typography"] = "Typography";
1302
+ StyleCategory["Spacing"] = "Spacing";
1303
+ StyleCategory["Border"] = "Border";
1304
+ StyleCategory["Position"] = "Position";
1305
+ StyleCategory["Background"] = "Background";
1306
+ StyleCategory["Effects"] = "Effects";
1307
+ // Legacy — kept for backward compat; prefer Size
1308
+ StyleCategory["Dimensions"] = "Dimensions";
1309
+ })(StyleCategory || (StyleCategory = {}));
1310
+ const TRIGGER_OPTIONS = [
1311
+ { value: 'pageLoad', label: 'Page load', icon: 'ph-thin ph-play' },
1312
+ { value: 'viewportEnter', label: 'Enter viewport', icon: 'ph-thin ph-eye' },
1313
+ { value: 'click', label: 'Click', icon: 'ph-thin ph-cursor-click' },
1314
+ { value: 'hover', label: 'Hover', icon: 'ph-thin ph-hand-pointing' },
1315
+ { value: 'scrollProgress', label: 'Scroll progress', icon: 'ph-thin ph-arrow-fat-lines-down' },
1316
+ ];
1317
+ const ACTION_TYPE_OPTIONS = [
1318
+ { value: 'fadeIn', label: 'Fade in' },
1319
+ { value: 'fadeOut', label: 'Fade out' },
1320
+ { value: 'moveUp', label: 'Move up' },
1321
+ { value: 'moveDown', label: 'Move down' },
1322
+ { value: 'moveLeft', label: 'Move left' },
1323
+ { value: 'moveRight', label: 'Move right' },
1324
+ { value: 'scaleIn', label: 'Scale in' },
1325
+ { value: 'scaleOut', label: 'Scale out' },
1326
+ { value: 'rotate', label: 'Rotate' },
1327
+ { value: 'show', label: 'Show' },
1328
+ { value: 'hide', label: 'Hide' },
1329
+ { value: 'toggleVisibility', label: 'Toggle visibility' },
1330
+ ];
1331
+ const EASING_OPTIONS = ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'];
1332
+ /** Interaction pseudo-classes — compiled to .iox-node-{id}:state */
1333
+ const INTERACTION_STATES = ['hover', 'active', 'focus'];
1334
+ /** Structural pseudo-classes — compiled to .iox-outer-{id}:state */
1335
+ const STRUCTURAL_STATES = ['first-child', 'last-child', 'nth-child(odd)', 'nth-child(even)'];
1336
+ const SUPPORTED_STATES = [...INTERACTION_STATES, ...STRUCTURAL_STATES];
1297
1337
  /**
1298
- * StyleRegistryService manages a single <style> tag for the builder canvas.
1299
- *
1300
- * Each builder component gets two CSS classes:
1301
- * .iox-node-{id} — applied to the inner element; handles layout/visual props
1302
- * (display, flex, background, border, padding, etc.)
1303
- * .iox-outer-{id} — applied to the host element by RenderDirective; handles
1304
- * properties that participate in the PARENT flow (margin,
1305
- * align-self, flex-grow/shrink/basis, order).
1306
- *
1307
- * This split is necessary because margin on an inner wrapper only adds internal
1308
- * space — it does not push sibling elements. Applying margin to the host element
1309
- * (which IS the flex/block child in the parent container) makes it work correctly.
1310
- *
1311
- * State styles (hover/active/focus) are stored under key `${nodeId}:${state}`
1312
- * and compiled to `.iox-node-{id}:hover { … }` selectors.
1313
- *
1314
- * Design token variables are stored under the reserved key `__tokens__` and
1315
- * compiled to a `:root { … }` block prepended to all other rules.
1316
- *
1317
- * Scoped to PageUiComponent.providers[] — one stylesheet per builder instance.
1338
+ * Build a full StyleTraitGroup[] schema populated with values from a flat
1339
+ * styleProps map. Used when editing a preset in the Style panel — the panel
1340
+ * receives the same `StyleTraitGroup[]` shape regardless of context.
1318
1341
  */
1319
- class StyleRegistryService {
1320
- constructor() {
1321
- this.changes$ = new Subject();
1322
- this.rules = new Map();
1323
- this.styleEl = null;
1324
- }
1325
- /** Properties that must live on the wrapper element to affect the parent layout. */
1326
- static { this.OUTER_PROPS = new Set([
1327
- 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
1328
- 'alignSelf', 'flexGrow', 'flexShrink', 'flexBasis', 'order',
1329
- 'width',
1330
- ]); }
1331
- init() {
1332
- this.styleEl = document.createElement('style');
1333
- this.styleEl.id = 'iox-runtime-styles';
1334
- document.head.appendChild(this.styleEl);
1342
+ function buildPresetStyleTraits(styleProps) {
1343
+ const allGroups = [
1344
+ new LayoutGroupStyleConfig(),
1345
+ new SizeGroupStyleConfig(),
1346
+ new BackgroundGroupStyleConfig(),
1347
+ new SpacingGroupStyleConfig(),
1348
+ new BorderGroupStyleConfig(),
1349
+ new TypographyGroupStyleConfig(),
1350
+ new EffectsGroupStyleConfig(),
1351
+ new PositionGroupStyleConfig(),
1352
+ ];
1353
+ return allGroups.map(group => ({
1354
+ ...group,
1355
+ traits: group.traits.map(trait => ({
1356
+ ...trait,
1357
+ default: styleProps[trait.name] !== undefined ? styleProps[trait.name] : trait.default,
1358
+ })),
1359
+ }));
1360
+ }
1361
+ var TraitInputType;
1362
+ (function (TraitInputType) {
1363
+ TraitInputType["Text"] = "text";
1364
+ TraitInputType["Number"] = "number";
1365
+ TraitInputType["Select"] = "select";
1366
+ TraitInputType["Checkbox"] = "checkbox";
1367
+ TraitInputType["Color"] = "colorPicker";
1368
+ TraitInputType["DirectionalSize"] = "directionalSize";
1369
+ TraitInputType["SelectButton"] = "selectButton";
1370
+ TraitInputType["Scrub"] = "scrub";
1371
+ TraitInputType["Icon"] = "icon";
1372
+ TraitInputType["Media"] = "media";
1373
+ TraitInputType["FontFamily"] = "fontFamily";
1374
+ TraitInputType["GridTemplate"] = "gridTemplate";
1375
+ })(TraitInputType || (TraitInputType = {}));
1376
+ function generateNodeId() {
1377
+ return Math.random().toString(36).substr(2, 9);
1378
+ }
1379
+ class ComponentConfig {
1380
+ constructor(type, selector, icon = 'ph-thin ph-cube', category = 'General') {
1381
+ this.id = generateNodeId();
1382
+ this.type = type;
1383
+ this.selector = selector;
1384
+ this.icon = icon;
1385
+ this.category = category;
1386
+ this.inputs = {};
1387
+ this.traits = [];
1388
+ this.styleTraits = [];
1335
1389
  }
1336
- /**
1337
- * Write or update the base styles for a node, or a pseudo-class state override.
1338
- *
1339
- * @param nodeId The node's CSS id (used in `.iox-node-{nodeId}`)
1340
- * @param styles Flat CSS property map (camelCase keys)
1341
- * @param state Optional pseudo-class state ('hover' | 'active' | 'focus').
1342
- * When provided, compiles to `.iox-node-{id}:{state} { … }`.
1343
- * State rules are NOT partitioned into inner/outer.
1344
- */
1345
- upsert(nodeId, styles, state) {
1346
- if (!nodeId)
1347
- return;
1348
- if (state) {
1349
- const css = this.compile(`iox-node-${nodeId}:${state}`, styles);
1350
- const key = `${nodeId}:${state}`;
1351
- if (css) {
1352
- this.rules.set(key, css);
1353
- }
1354
- else {
1355
- this.rules.delete(key);
1356
- }
1357
- this.flush();
1358
- return;
1359
- }
1360
- const inner = {};
1361
- const outer = {};
1362
- for (const [k, v] of Object.entries(styles)) {
1363
- if (StyleRegistryService.OUTER_PROPS.has(k)) {
1364
- outer[k] = v;
1365
- }
1366
- else {
1367
- inner[k] = v;
1390
+ /** Override specific trait defaults after styleTraits are set.
1391
+ * Call this at the end of each subclass constructor. */
1392
+ applyStyleDefaults(defaults) {
1393
+ for (const group of this.styleTraits) {
1394
+ for (const trait of group.traits) {
1395
+ if (defaults[trait.name] !== undefined) {
1396
+ trait.default = defaults[trait.name];
1397
+ }
1368
1398
  }
1369
1399
  }
1370
- const innerCss = this.compile(`iox-node-${nodeId}`, inner);
1371
- const outerCss = this.compile(`iox-outer-${nodeId}`, outer);
1372
- const combined = [innerCss, outerCss].filter(Boolean).join('\n');
1373
- if (combined) {
1374
- this.rules.set(nodeId, combined);
1375
- }
1376
- else {
1377
- this.rules.delete(nodeId);
1378
- }
1379
- this.flush();
1380
1400
  }
1381
- /**
1382
- * Write the org-level design token variables as a `:root { … }` block.
1383
- * Token names should be CSS custom property names without the `--` prefix
1384
- * (e.g., `{ 'iox-primary-color': '#cb9090' }`).
1385
- * Pass an empty object to clear all tokens.
1386
- */
1387
- upsertTokens(tokens) {
1388
- const entries = Object.entries(tokens)
1389
- .filter(([, v]) => v !== undefined && v !== null && v !== '')
1390
- .map(([k, v]) => ` --${k}: ${v};`);
1391
- if (entries.length) {
1392
- this.rules.set('__tokens__', `:root {\n${entries.join('\n')}\n}`);
1393
- }
1394
- else {
1395
- this.rules.delete('__tokens__');
1396
- }
1397
- this.flush();
1401
+ }
1402
+ class TraitConfig {
1403
+ constructor(name, label, type, options, defaultValue, inline, showWhen) {
1404
+ this.name = name;
1405
+ this.label = label;
1406
+ this.type = type;
1407
+ this.options = options;
1408
+ this.default = defaultValue;
1409
+ this.inline = inline;
1410
+ this.showWhen = showWhen;
1398
1411
  }
1399
- remove(nodeId) {
1400
- if (!nodeId)
1401
- return;
1402
- // Remove base rule and all state rules for this node
1403
- this.rules.delete(nodeId);
1404
- for (const key of this.rules.keys()) {
1405
- if (key.startsWith(`${nodeId}:`)) {
1406
- this.rules.delete(key);
1407
- }
1408
- }
1409
- this.flush();
1412
+ }
1413
+ class GroupStyleConfig {
1414
+ constructor(category, traits) {
1415
+ this.category = category;
1416
+ this.traits = traits;
1410
1417
  }
1411
- destroy() {
1412
- this.rules.clear();
1413
- if (this.styleEl) {
1414
- this.styleEl.remove();
1415
- this.styleEl = null;
1416
- }
1418
+ }
1419
+ // ─── Unit constants ───────────────────────────────────────────────────────────
1420
+ const UNITS_ALL = ['px', '%', 'rem', 'em', 'vw', 'vh'];
1421
+ const UNITS_NO_VW = ['px', '%', 'rem', 'em', 'vh'];
1422
+ const UNITS_FIXED = ['px', '%', 'rem', 'em'];
1423
+ const UNITS_DEG = ['deg'];
1424
+ // ─── Shared panel utilities ──────────────────────────────────────────────────
1425
+ function resolveTraitControllerType(type) {
1426
+ switch (type) {
1427
+ case TraitInputType.Number: return 'number';
1428
+ case TraitInputType.Select: return 'select';
1429
+ case TraitInputType.Checkbox: return 'switch';
1430
+ case TraitInputType.Color: return 'colorPicker';
1431
+ case TraitInputType.DirectionalSize: return 'directionalSize';
1432
+ case TraitInputType.SelectButton: return 'selectButton';
1433
+ case TraitInputType.Scrub: return 'scrub';
1434
+ case TraitInputType.Icon: return 'icon';
1435
+ case TraitInputType.Media: return 'media';
1436
+ case TraitInputType.FontFamily: return 'select';
1437
+ case TraitInputType.GridTemplate: return 'gridTemplate';
1438
+ default: return 'text';
1417
1439
  }
1418
- compile(className, styles) {
1419
- const entries = Object.entries(styles)
1420
- .filter(([, v]) => v !== undefined && v !== null && v !== '')
1421
- .map(([k, v]) => ` ${this.toKebabCase(k)}: ${v};`);
1422
- if (!entries.length)
1423
- return '';
1424
- return `.${className} {\n${entries.join('\n')}\n}`;
1440
+ }
1441
+ function resolveTraitOptions(trait) {
1442
+ if (trait.type === TraitInputType.Color ||
1443
+ trait.type === TraitInputType.DirectionalSize ||
1444
+ trait.type === TraitInputType.SelectButton ||
1445
+ trait.type === TraitInputType.Scrub ||
1446
+ trait.type === TraitInputType.Media) {
1447
+ return trait.options;
1425
1448
  }
1426
- toKebabCase(str) {
1427
- return str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
1449
+ if (trait.type === TraitInputType.FontFamily) {
1450
+ // FontFamily options are injected dynamically by the style panel via FontManagerService.
1451
+ // Return empty array here; the panel overrides this in resolveOptions().
1452
+ return [];
1428
1453
  }
1429
- flush() {
1430
- if (!this.styleEl)
1431
- return;
1432
- // __tokens__ always first so variables are available to all subsequent rules
1433
- const tokenBlock = this.rules.get('__tokens__');
1434
- const rest = Array.from(this.rules.entries())
1435
- .filter(([k]) => k !== '__tokens__')
1436
- .map(([, v]) => v);
1437
- this.styleEl.textContent = [tokenBlock, ...rest].filter(Boolean).join('\n');
1438
- this.changes$.next();
1454
+ if (trait.type !== TraitInputType.Select) {
1455
+ return undefined;
1439
1456
  }
1440
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1441
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService }); }
1457
+ const opts = trait.options ?? [];
1458
+ // Already formatted as {label, value}[]
1459
+ if (opts.length > 0 && typeof opts[0] === 'object' && 'label' in opts[0]) {
1460
+ return opts;
1461
+ }
1462
+ return opts.map((option) => ({ label: option, value: option }));
1463
+ }
1464
+ // ─── Style Groups ─────────────────────────────────────────────────────────────
1465
+ const NON_STATIC_POSITIONS = ['relative', 'absolute', 'fixed', 'sticky'];
1466
+ class PositionGroupStyleConfig extends GroupStyleConfig {
1467
+ constructor() {
1468
+ super(StyleCategory.Position, [
1469
+ new TraitConfig('position', 'Position', TraitInputType.Select, ['static', 'relative', 'absolute', 'fixed', 'sticky'], 'static'),
1470
+ new TraitConfig('top', 'T', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
1471
+ new TraitConfig('right', 'R', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
1472
+ new TraitConfig('bottom', 'B', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
1473
+ new TraitConfig('left', 'L', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
1474
+ new TraitConfig('zIndex', 'Z', TraitInputType.Scrub, { min: -99, max: 9999, step: 1, units: [''] }, '0', false, { trait: 'position', values: NON_STATIC_POSITIONS }),
1475
+ ]);
1476
+ }
1477
+ }
1478
+ // ─── Layout ───────────────────────────────────────────────────────────────────
1479
+ class LayoutGroupStyleConfig extends GroupStyleConfig {
1480
+ constructor() {
1481
+ super(StyleCategory.Layout, [
1482
+ new TraitConfig('display', 'Display', TraitInputType.Select, ['block', 'inline', 'inline-block', 'flex', 'grid'], 'block'),
1483
+ // ── Flex-only ─────────────────────────────────────────────────────
1484
+ new TraitConfig('flexDirection', 'Direction', TraitInputType.SelectButton, [
1485
+ { value: 'row', label: 'Horizontal', icon: 'ph-thin ph-arrow-right' },
1486
+ { value: 'row-reverse', label: 'Horizontal Reversed', icon: 'ph-thin ph-arrow-left' },
1487
+ { value: 'column', label: 'Vertical', icon: 'ph-thin ph-arrow-down' },
1488
+ { value: 'column-reverse', label: 'Vertical Reversed', icon: 'ph-thin ph-arrow-up' },
1489
+ ], 'row', false, { trait: 'display', values: 'flex' }),
1490
+ new TraitConfig('flexWrap', 'Flex Wrap', TraitInputType.SelectButton, [
1491
+ { value: 'nowrap', label: 'No Wrap', icon: 'ph-thin ph-minus' },
1492
+ { value: 'wrap', label: 'Wrap', icon: 'ph-thin ph-arrow-elbow-down-right' },
1493
+ { value: 'wrap-reverse', label: 'Wrap Reversed', icon: 'ph-thin ph-arrow-elbow-up-right' },
1494
+ ], 'nowrap', false, { trait: 'display', values: 'flex' }),
1495
+ // ── Shared flex + grid ────────────────────────────────────────────
1496
+ new TraitConfig('justifyContent', 'Justify', TraitInputType.SelectButton, [
1497
+ { value: 'flex-start', label: 'Start', icon: 'ph-thin ph-align-left' },
1498
+ { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-horizontal' },
1499
+ { value: 'flex-end', label: 'End', icon: 'ph-thin ph-align-right' },
1500
+ { value: 'space-between', label: 'Space Between', icon: 'ph-thin ph-distribute-horizontal' },
1501
+ { value: 'space-around', label: 'Space Around', icon: 'ph-thin ph-dots-three' },
1502
+ { value: 'space-evenly', label: 'Space Evenly', icon: 'ph-thin ph-dots-six' },
1503
+ ], 'flex-start', false, { trait: 'display', values: ['flex', 'grid'] }),
1504
+ new TraitConfig('alignItems', 'Align Items', TraitInputType.SelectButton, [
1505
+ { value: 'stretch', label: 'Stretch', icon: 'ph-thin ph-arrows-vertical' },
1506
+ { value: 'start', label: 'Start', icon: 'ph-thin ph-align-top' },
1507
+ { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-vertical' },
1508
+ { value: 'end', label: 'End', icon: 'ph-thin ph-align-bottom' },
1509
+ ], 'stretch', false, { trait: 'display', values: ['flex', 'grid'] }),
1510
+ new TraitConfig('gap', 'Gap', TraitInputType.Scrub, { min: 0, max: 200, step: 1, units: UNITS_FIXED }, '0px', false, { trait: 'display', values: ['flex', 'grid'] }),
1511
+ // ── Grid-only ─────────────────────────────────────────────────────
1512
+ new TraitConfig('gridTemplateColumns', 'Columns', TraitInputType.GridTemplate, undefined, '1fr', false, { trait: 'display', values: 'grid' }),
1513
+ new TraitConfig('gridTemplateRows', 'Rows', TraitInputType.GridTemplate, undefined, 'auto', false, { trait: 'display', values: 'grid' }),
1514
+ new TraitConfig('justifyItems', 'Justify Items', TraitInputType.SelectButton, [
1515
+ { value: 'stretch', label: 'Stretch', icon: 'ph-thin ph-arrows-horizontal' },
1516
+ { value: 'start', label: 'Start', icon: 'ph-thin ph-align-left' },
1517
+ { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-horizontal' },
1518
+ { value: 'end', label: 'End', icon: 'ph-thin ph-align-right' },
1519
+ ], 'stretch', false, { trait: 'display', values: 'grid' }),
1520
+ new TraitConfig('alignContent', 'Align Content', TraitInputType.SelectButton, [
1521
+ { value: 'start', label: 'Start', icon: 'ph-thin ph-align-top' },
1522
+ { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-vertical' },
1523
+ { value: 'end', label: 'End', icon: 'ph-thin ph-align-bottom' },
1524
+ { value: 'space-between', label: 'Space Between', icon: 'ph-thin ph-distribute-vertical' },
1525
+ { value: 'space-around', label: 'Space Around', icon: 'ph-thin ph-dots-three' },
1526
+ { value: 'stretch', label: 'Stretch', icon: 'ph-thin ph-arrows-vertical' },
1527
+ ], 'stretch', false, { trait: 'display', values: 'grid' }),
1528
+ // ── Direction (always visible) ────────────────────────────────────
1529
+ new TraitConfig('direction', 'Text Direction', TraitInputType.SelectButton, [
1530
+ { value: 'ltr', label: 'LTR' },
1531
+ { value: 'rtl', label: 'RTL' },
1532
+ ], 'ltr'),
1533
+ ]);
1534
+ }
1535
+ }
1536
+ // ─── Size (replaces Dimensions) ───────────────────────────────────────────────
1537
+ class SizeGroupStyleConfig extends GroupStyleConfig {
1538
+ constructor() {
1539
+ super(StyleCategory.Size, [
1540
+ new TraitConfig('width', 'W', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
1541
+ new TraitConfig('height', 'H', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
1542
+ new TraitConfig('minWidth', 'Min W', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
1543
+ new TraitConfig('minHeight', 'Min H', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
1544
+ new TraitConfig('maxWidth', 'Max W', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
1545
+ new TraitConfig('maxHeight', 'Max H', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
1546
+ new TraitConfig('aspectRatio', 'Aspect Ratio', TraitInputType.Text, undefined, 'auto'),
1547
+ ]);
1548
+ }
1549
+ }
1550
+ /** @deprecated Use SizeGroupStyleConfig instead. */
1551
+ class DimensionsGroupStyleConfig extends SizeGroupStyleConfig {
1552
+ }
1553
+ // ─── Background ───────────────────────────────────────────────────────────────
1554
+ class BackgroundGroupStyleConfig extends GroupStyleConfig {
1555
+ constructor() {
1556
+ super(StyleCategory.Background, [
1557
+ new TraitConfig('backgroundColor', 'Background', TraitInputType.Color, { allowGradient: true, allowTransparent: true, formats: ['hex', 'rgb', 'hsl', 'transparent'] }, 'transparent'),
1558
+ ]);
1559
+ }
1560
+ }
1561
+ class SpacingGroupStyleConfig extends GroupStyleConfig {
1562
+ constructor() {
1563
+ super(StyleCategory.Spacing, [
1564
+ new TraitConfig('margin', 'Margin', TraitInputType.DirectionalSize, {
1565
+ allowNegative: true,
1566
+ allowLinkedSlider: true,
1567
+ slider: { min: 0, max: 200, step: 1 }
1568
+ }, '0px 0px 0px 0px'),
1569
+ new TraitConfig('padding', 'Padding', TraitInputType.DirectionalSize, {
1570
+ allowNegative: false,
1571
+ allowLinkedSlider: true,
1572
+ slider: { min: 0, max: 200, step: 1 }
1573
+ }, '0px 0px 0px 0px')
1574
+ ]);
1575
+ }
1576
+ }
1577
+ class BorderGroupStyleConfig extends GroupStyleConfig {
1578
+ constructor() {
1579
+ super(StyleCategory.Border, [
1580
+ new TraitConfig('borderWidth', 'Border Width', TraitInputType.DirectionalSize, {
1581
+ units: UNITS_FIXED,
1582
+ allowNegative: false,
1583
+ allowLinkedSlider: true,
1584
+ slider: { min: 0, max: 32, step: 1 }
1585
+ }, '0px'),
1586
+ new TraitConfig('borderStyle', 'Border Style', TraitInputType.SelectButton, [
1587
+ { value: 'solid', icon: 'ph-thin ph-line-segment' },
1588
+ { value: 'dashed', icon: 'ph-thin ph-dots-three' },
1589
+ { value: 'dotted', icon: 'ph-thin ph-dots-six' },
1590
+ ], 'solid'),
1591
+ new TraitConfig('borderColor', 'Border Color', TraitInputType.Color, { allowGradient: false, allowTransparent: false, formats: ['hex', 'rgb', 'hsl'] }, '#CCCCCC'),
1592
+ new TraitConfig('borderRadius', 'Border Radius', TraitInputType.DirectionalSize, {
1593
+ units: UNITS_FIXED,
1594
+ allowNegative: false,
1595
+ allowLinkedSlider: true,
1596
+ linkByDefault: true,
1597
+ slider: { min: 0, max: 120, step: 1 },
1598
+ segments: [
1599
+ { key: 'top-left', icon: 'ph-arrow-bend-up-left', ariaLabel: 'top left radius' },
1600
+ { key: 'top-right', icon: 'ph-arrow-bend-up-right', ariaLabel: 'top right radius' },
1601
+ { key: 'bottom-right', icon: 'ph-arrow-bend-down-right', ariaLabel: 'bottom right radius' },
1602
+ { key: 'bottom-left', icon: 'ph-arrow-bend-down-left', ariaLabel: 'bottom left radius' }
1603
+ ]
1604
+ }, '0px')
1605
+ ]);
1606
+ }
1607
+ }
1608
+ class TypographyGroupStyleConfig extends GroupStyleConfig {
1609
+ constructor() {
1610
+ super(StyleCategory.Typography, [
1611
+ new TraitConfig('fontFamily', 'Font Family', TraitInputType.FontFamily, [], 'Poppins, sans-serif'),
1612
+ new TraitConfig('fontSize', 'Font Size', TraitInputType.Scrub, { min: 8, max: 200, step: 1, units: UNITS_FIXED }, '16px'),
1613
+ new TraitConfig('fontWeight', 'Font Weight', TraitInputType.Select, ['100', '200', '300', '400', '500', '600', '700', '800', '900'], '400'),
1614
+ new TraitConfig('color', 'Color', TraitInputType.Color, { allowGradient: false, allowTransparent: false, formats: ['hex', 'rgb', 'hsl'] }, '#000000'),
1615
+ new TraitConfig('lineHeight', 'Line Height', TraitInputType.Select, ['1', '1.2', '1.4', '1.5', '1.75', '2', '2.5'], '1.5'),
1616
+ new TraitConfig('letterSpacing', 'Letter Spacing', TraitInputType.Scrub, { min: -5, max: 50, step: 0.5, units: UNITS_FIXED }, '0px'),
1617
+ new TraitConfig('textAlign', 'Alignment', TraitInputType.SelectButton, [
1618
+ { value: 'left', icon: 'ph-thin ph-text-align-left' },
1619
+ { value: 'center', icon: 'ph-thin ph-text-align-center' },
1620
+ { value: 'right', icon: 'ph-thin ph-text-align-right' },
1621
+ { value: 'justify', icon: 'ph-thin ph-text-align-justify' },
1622
+ ], 'left'),
1623
+ new TraitConfig('fontStyle', 'Italic', TraitInputType.SelectButton, { items: [{ value: 'italic', icon: 'ph-thin ph-text-italic', label: 'Italic' }], allowEmpty: true, noneValue: 'normal' }, 'normal', true),
1624
+ new TraitConfig('textDecoration', 'Underline', TraitInputType.SelectButton, { items: [{ value: 'underline', icon: 'ph-thin ph-text-underline', label: 'Underline' }], allowEmpty: true, noneValue: 'none' }, 'none', true),
1625
+ ]);
1626
+ }
1627
+ }
1628
+ class EffectsGroupStyleConfig extends GroupStyleConfig {
1629
+ constructor() {
1630
+ super(StyleCategory.Effects, [
1631
+ new TraitConfig('opacity', 'Opacity', TraitInputType.Scrub, { min: 0, max: 1, step: 0.01, units: [''] }, '1'),
1632
+ new TraitConfig('mixBlendMode', 'Blend Mode', TraitInputType.Select, [
1633
+ 'normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten',
1634
+ 'color-dodge', 'color-burn', 'hard-light', 'soft-light',
1635
+ 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity',
1636
+ ], 'normal'),
1637
+ new TraitConfig('cursor', 'Cursor', TraitInputType.Select, [
1638
+ 'auto', 'default', 'pointer', 'text', 'move', 'not-allowed',
1639
+ 'grab', 'grabbing', 'crosshair', 'zoom-in', 'zoom-out', 'none',
1640
+ ], 'auto'),
1641
+ ]);
1642
+ }
1643
+ }
1644
+ function flattenSchemaFields(properties, prefix = '') {
1645
+ const result = [];
1646
+ for (const [key, field] of Object.entries(properties)) {
1647
+ const fullPath = prefix ? `${prefix}.${key}` : key;
1648
+ if (field.type === 'object' && field.properties) {
1649
+ result.push(...flattenSchemaFields(field.properties, fullPath));
1650
+ }
1651
+ else {
1652
+ result.push({ label: fullPath, value: fullPath });
1653
+ }
1654
+ }
1655
+ return result;
1442
1656
  }
1443
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, decorators: [{
1444
- type: Injectable
1445
- }] });
1446
1657
 
1447
- // Module-level counter — unique drop-list ID per Section instance across the whole app session.
1448
- let dropListCounter = 0;
1449
1658
  /**
1450
- * RenderDirectiveapplied to <ng-container> to dynamically instantiate a
1451
- * builder component with zero wrapper elements in the DOM.
1659
+ * StyleRegistryServicemanages a single <style> tag for the builder canvas.
1452
1660
  *
1453
- * A directive's ViewContainerRef inserts the created component's host element as
1454
- * a sibling of the ng-container comment node directly inside the parent element
1455
- * so the resulting DOM has no extra <app-renderer> wrapper.
1661
+ * Each builder component gets two CSS classes:
1662
+ * .iox-node-{id} applied to the inner element; handles layout/visual props
1663
+ * (display, flex, background, border, padding, etc.)
1664
+ * .iox-outer-{id} — applied to the host element by RenderDirective; handles
1665
+ * properties that participate in the PARENT flow (margin,
1666
+ * align-self, flex-grow/shrink/basis, order).
1456
1667
  *
1457
- * Usage:
1458
- * <ng-container [ioxRender]="node"
1459
- * (onClick)="handleClick($event)"
1460
- * (onMouseEnter)="handleMouseEnter($event)"
1461
- * (onMouseLeave)="handleMouseLeave()">
1462
- * </ng-container>
1668
+ * This split is necessary because margin on an inner wrapper only adds internal
1669
+ * space — it does not push sibling elements. Applying margin to the host element
1670
+ * (which IS the flex/block child in the parent container) makes it work correctly.
1671
+ *
1672
+ * State styles (hover/active/focus) are stored under key `${nodeId}:${state}`
1673
+ * and compiled to `.iox-node-{id}:hover { … }` selectors.
1674
+ *
1675
+ * Design token variables are stored under the reserved key `__tokens__` and
1676
+ * compiled to a `:root { … }` block prepended to all other rules.
1677
+ *
1678
+ * Scoped to PageUiComponent.providers[] — one stylesheet per builder instance.
1463
1679
  */
1464
- class RenderDirective {
1465
- constructor(vcr, injector, registryService, overlayService, styleRegistry, interactionEngine, cdr, panelEventService) {
1466
- this.vcr = vcr;
1467
- this.injector = injector;
1680
+ class StyleRegistryService {
1681
+ constructor() {
1682
+ this.changes$ = new Subject();
1683
+ this.rules = new Map();
1684
+ this.styleEl = null;
1685
+ }
1686
+ /** Properties that must live on the wrapper element to affect the parent layout. */
1687
+ static { this.OUTER_PROPS = new Set([
1688
+ 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
1689
+ 'alignSelf', 'flexGrow', 'flexShrink', 'flexBasis', 'order',
1690
+ 'width',
1691
+ ]); }
1692
+ init() {
1693
+ this.styleEl = document.createElement('style');
1694
+ this.styleEl.id = 'iox-runtime-styles';
1695
+ document.head.appendChild(this.styleEl);
1696
+ }
1697
+ /**
1698
+ * Write or update the base styles for a node, or a pseudo-class state override.
1699
+ *
1700
+ * @param nodeId The node's CSS id (used in `.iox-node-{nodeId}`)
1701
+ * @param styles Flat CSS property map (camelCase keys)
1702
+ * @param state Optional pseudo-class state ('hover' | 'active' | 'focus').
1703
+ * When provided, compiles to `.iox-node-{id}:{state} { … }`.
1704
+ * State rules are NOT partitioned into inner/outer.
1705
+ */
1706
+ upsert(nodeId, styles, state) {
1707
+ if (!nodeId)
1708
+ return;
1709
+ if (state) {
1710
+ // Structural pseudo-classes (first-child, last-child, nth-child) target the
1711
+ // outer wrapper element so they participate correctly in grid/flex layout.
1712
+ const prefix = STRUCTURAL_STATES.includes(state) ? 'iox-outer' : 'iox-node';
1713
+ const css = this.compile(`${prefix}-${nodeId}:${state}`, styles);
1714
+ const key = `${nodeId}:${state}`;
1715
+ if (css) {
1716
+ this.rules.set(key, css);
1717
+ }
1718
+ else {
1719
+ this.rules.delete(key);
1720
+ }
1721
+ this.flush();
1722
+ return;
1723
+ }
1724
+ const inner = {};
1725
+ const outer = {};
1726
+ for (const [k, v] of Object.entries(styles)) {
1727
+ if (StyleRegistryService.OUTER_PROPS.has(k)) {
1728
+ outer[k] = v;
1729
+ }
1730
+ else {
1731
+ inner[k] = v;
1732
+ }
1733
+ }
1734
+ const innerCss = this.compile(`iox-node-${nodeId}`, inner);
1735
+ const outerCss = this.compile(`iox-outer-${nodeId}`, outer);
1736
+ const combined = [innerCss, outerCss].filter(Boolean).join('\n');
1737
+ if (combined) {
1738
+ this.rules.set(nodeId, combined);
1739
+ }
1740
+ else {
1741
+ this.rules.delete(nodeId);
1742
+ }
1743
+ this.flush();
1744
+ }
1745
+ /**
1746
+ * Write the org-level design token variables as a `:root { … }` block.
1747
+ * Token names should be CSS custom property names without the `--` prefix
1748
+ * (e.g., `{ 'iox-primary-color': '#cb9090' }`).
1749
+ * Pass an empty object to clear all tokens.
1750
+ */
1751
+ upsertTokens(tokens) {
1752
+ const entries = Object.entries(tokens)
1753
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
1754
+ .map(([k, v]) => ` --${k}: ${v};`);
1755
+ if (entries.length) {
1756
+ this.rules.set('__tokens__', `:root {\n${entries.join('\n')}\n}`);
1757
+ }
1758
+ else {
1759
+ this.rules.delete('__tokens__');
1760
+ }
1761
+ this.flush();
1762
+ }
1763
+ remove(nodeId) {
1764
+ if (!nodeId)
1765
+ return;
1766
+ // Remove base rule and all state rules for this node
1767
+ this.rules.delete(nodeId);
1768
+ for (const key of this.rules.keys()) {
1769
+ if (key.startsWith(`${nodeId}:`)) {
1770
+ this.rules.delete(key);
1771
+ }
1772
+ }
1773
+ this.flush();
1774
+ }
1775
+ destroy() {
1776
+ this.rules.clear();
1777
+ if (this.styleEl) {
1778
+ this.styleEl.remove();
1779
+ this.styleEl = null;
1780
+ }
1781
+ }
1782
+ compile(className, styles) {
1783
+ const entries = Object.entries(styles)
1784
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
1785
+ .map(([k, v]) => ` ${this.toKebabCase(k)}: ${v};`);
1786
+ if (!entries.length)
1787
+ return '';
1788
+ return `.${className} {\n${entries.join('\n')}\n}`;
1789
+ }
1790
+ toKebabCase(str) {
1791
+ return str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
1792
+ }
1793
+ flush() {
1794
+ if (!this.styleEl)
1795
+ return;
1796
+ // __tokens__ always first so variables are available to all subsequent rules
1797
+ const tokenBlock = this.rules.get('__tokens__');
1798
+ const rest = Array.from(this.rules.entries())
1799
+ .filter(([k]) => k !== '__tokens__')
1800
+ .map(([, v]) => v);
1801
+ this.styleEl.textContent = [tokenBlock, ...rest].filter(Boolean).join('\n');
1802
+ this.changes$.next();
1803
+ }
1804
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1805
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService }); }
1806
+ }
1807
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StyleRegistryService, decorators: [{
1808
+ type: Injectable
1809
+ }] });
1810
+
1811
+ // Module-level counter — unique drop-list ID per Section instance across the whole app session.
1812
+ let dropListCounter = 0;
1813
+ /**
1814
+ * RenderDirective — applied to <ng-container> to dynamically instantiate a
1815
+ * builder component with zero wrapper elements in the DOM.
1816
+ *
1817
+ * A directive's ViewContainerRef inserts the created component's host element as
1818
+ * a sibling of the ng-container comment node — directly inside the parent element
1819
+ * — so the resulting DOM has no extra <app-renderer> wrapper.
1820
+ *
1821
+ * Usage:
1822
+ * <ng-container [ioxRender]="node"
1823
+ * (onClick)="handleClick($event)"
1824
+ * (onMouseEnter)="handleMouseEnter($event)"
1825
+ * (onMouseLeave)="handleMouseLeave()">
1826
+ * </ng-container>
1827
+ */
1828
+ class RenderDirective {
1829
+ constructor(vcr, injector, registryService, overlayService, styleRegistry, interactionEngine, cdr, panelEventService) {
1830
+ this.vcr = vcr;
1831
+ this.injector = injector;
1468
1832
  this.registryService = registryService;
1469
1833
  this.overlayService = overlayService;
1470
1834
  this.styleRegistry = styleRegistry;
@@ -3459,7 +3823,6 @@ class BuilderRepeaterComponent {
3459
3823
  }
3460
3824
  ngOnInit() {
3461
3825
  this.orgId = this.activatedRoute.snapshot.paramMap.get('orgId') || 'default';
3462
- // When a binding is added/removed on any node, regenerate the preview rows.
3463
3826
  this.bindingSub = this.panelEventService.subscribe(event => {
3464
3827
  if (event.type !== PanelEventTypes.BINDING_CHANGED)
3465
3828
  return;
@@ -3537,32 +3900,54 @@ class BuilderRepeaterComponent {
3537
3900
  }
3538
3901
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderRepeaterComponent, deps: [{ token: DragEngineService }, { token: OverlayService }, { token: IOX_CONTENT_SERVICE, optional: true }, { token: DataSourceRegistryService }, { token: PanelEventService }, { token: i5.ActivatedRoute }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
3539
3902
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: BuilderRepeaterComponent, isStandalone: false, selector: "app-builder-repeater", inputs: { children: "children", style: "style", nodeId: "nodeId", dropListId: "dropListId", source: "source", limit: "limit" }, outputs: { childClick: "childClick", childMouseEnter: "childMouseEnter", childMouseLeave: "childMouseLeave" }, ngImport: i0, template: `
3540
- <div class="repeater-root" [ngClass]="['iox-node-' + nodeId, children.length ? 'has-children' : '']">
3903
+ <ng-template #badgeContent>
3904
+ <i class="ph-thin ph-repeat"></i>
3905
+ <span *ngIf="source">{{ source }}</span>
3906
+ <span *ngIf="!source" class="repeater-badge-empty">No source</span>
3907
+ <span *ngIf="items.length" class="repeater-badge-count">&times; {{ items.length }}</span>
3908
+ <span *ngIf="isLoading" class="repeater-badge-loading"><i class="ph-thin ph-circle-notch"></i></span>
3909
+ </ng-template>
3541
3910
 
3542
- <!-- Badge: always visible when empty, absolute hover-only when has children -->
3543
- <div class="repeater-badge">
3544
- <i class="ph-thin ph-repeat"></i>
3545
- <span *ngIf="source">{{ source }}</span>
3546
- <span *ngIf="!source" class="repeater-badge-empty">No source</span>
3547
- <span *ngIf="items.length" class="repeater-badge-count">&times; {{ items.length }}</span>
3548
- <span *ngIf="isLoading" class="repeater-badge-loading"><i class="ph-thin ph-circle-notch"></i></span>
3911
+ <!--
3912
+ .repeater-wrapper is the layout root.
3913
+ In has-children state, .repeater-badge lives here as a direct child
3914
+ (position:absolute, hover-only) so it is NOT a child of .repeater-root
3915
+ and does NOT shift the :nth-child index of the item wrappers.
3916
+
3917
+ In empty state, .repeater-badge lives inside .repeater-root (no items
3918
+ to count, so :nth-child is irrelevant there).
3919
+ -->
3920
+ <div class="repeater-wrapper" [class.has-children]="children.length > 0">
3921
+
3922
+ <!-- HAS-CHILDREN: badge outside root, absolute over wrapper -->
3923
+ <div *ngIf="children.length > 0" class="repeater-badge">
3924
+ <ng-container *ngTemplateOutlet="badgeContent"></ng-container>
3549
3925
  </div>
3550
3926
 
3551
- <!-- Template drop zone — user designs one item here -->
3552
- <div class="repeater-template"
3927
+ <div class="repeater-root"
3928
+ [ngClass]="['iox-node-' + nodeId]"
3553
3929
  ioxDropzone
3554
3930
  [ioxDropzoneId]="dropListId"
3555
3931
  [ioxDropzoneData]="children"
3556
3932
  [ioxDropzonePostDrop]="refreshPreviewsBound"
3557
3933
  (ioxDrop)="onDrop($event)">
3558
3934
 
3559
- <div *ngIf="!children.length" class="repeater-placeholder">
3560
- <i class="ph-thin ph-rows"></i>
3561
- <span>Drop a component here as the item template</span>
3562
- </div>
3935
+ <!-- EMPTY STATE: badge + placeholder inside root -->
3936
+ <ng-container *ngIf="!children.length">
3937
+ <div class="repeater-badge">
3938
+ <ng-container *ngTemplateOutlet="badgeContent"></ng-container>
3939
+ </div>
3940
+ <div class="repeater-placeholder">
3941
+ <i class="ph-thin ph-rows"></i>
3942
+ <span>Drop a component here as the item template</span>
3943
+ </div>
3944
+ </ng-container>
3563
3945
 
3946
+ <!-- Template items — DIRECT children of repeater-root.
3947
+ :first-child / :nth-child(odd) etc. now correctly target these
3948
+ as siblings of the preview items below. -->
3564
3949
  <div *ngFor="let child of children"
3565
- [ngClass]="['repeater-template-child', 'iox-outer-' + (child.styleId ?? child.id)]"
3950
+ [ngClass]="['repeater-item-template', 'iox-outer-' + (child.styleId ?? child.id)]"
3566
3951
  ioxDraggable
3567
3952
  [ioxDragData]="child"
3568
3953
  [ioxDragSourceId]="dropListId">
@@ -3573,54 +3958,74 @@ class BuilderRepeaterComponent {
3573
3958
  (onMouseLeave)="childMouseLeave.emit()">
3574
3959
  </ng-container>
3575
3960
  </div>
3576
- </div>
3577
3961
 
3578
- <!-- Preview items for rows 2..N one iox-outer wrapper per child so
3579
- width/margin outer props apply, and items participate directly in
3580
- the repeater-root grid/flex (template uses display:contents). -->
3581
- <ng-container *ngFor="let row of previewRows">
3582
- <div *ngFor="let child of row"
3583
- [ngClass]="['repeater-preview-child', 'iox-outer-' + (child.styleId ?? child.id)]">
3584
- <ng-container
3585
- [ioxRender]="child"
3586
- (onClick)="$event"
3587
- (onMouseEnter)="$event"
3588
- (onMouseLeave)="undefined">
3589
- </ng-container>
3590
- </div>
3591
- </ng-container>
3962
+ <!-- Preview items also DIRECT children of repeater-root. -->
3963
+ <ng-container *ngFor="let row of previewRows">
3964
+ <div *ngFor="let child of row"
3965
+ [ngClass]="['repeater-preview-child', 'iox-outer-' + (child.styleId ?? child.id)]">
3966
+ <ng-container
3967
+ [ioxRender]="child"
3968
+ (onClick)="$event"
3969
+ (onMouseEnter)="$event"
3970
+ (onMouseLeave)="undefined">
3971
+ </ng-container>
3972
+ </div>
3973
+ </ng-container>
3974
+ </div>
3592
3975
  </div>
3593
- `, isInline: true, styles: [".repeater-root{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-root.has-children .repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-root.has-children:hover .repeater-badge{opacity:1}.repeater-root.has-children .repeater-template{border:none;border-radius:0;padding:0;min-height:unset;display:contents}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-template{border:1px dashed rgba(203,144,144,.4);border-radius:0 0 4px 4px;padding:4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-template-child{min-width:0}.repeater-preview-child{opacity:.55;pointer-events:none;min-width:0}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
3976
+ `, isInline: true, styles: [".repeater-wrapper{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-wrapper.has-children>.repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-wrapper.has-children:hover>.repeater-badge{opacity:1}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-root{box-sizing:border-box}.repeater-wrapper:not(.has-children) .repeater-root{border:1px dashed rgba(203,144,144,.4);border-top:none;border-radius:0 0 4px 4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-item-template{min-width:0}.repeater-preview-child{opacity:.55;pointer-events:none;min-width:0}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RenderDirective, selector: "[ioxRender]", inputs: ["ioxRender"], outputs: ["onClick", "onMouseEnter", "onMouseLeave"] }, { kind: "directive", type: IoxDraggableDirective, selector: "[ioxDraggable]", inputs: ["ioxDragData", "ioxDragSourceId"] }, { kind: "directive", type: IoxDropzoneDirective, selector: "[ioxDropzone]", inputs: ["ioxDropzoneId", "ioxDropzoneData", "ioxDropzonePostDrop"], outputs: ["ioxDrop"] }] }); }
3594
3977
  }
3595
3978
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BuilderRepeaterComponent, decorators: [{
3596
3979
  type: Component,
3597
3980
  args: [{ selector: 'app-builder-repeater', template: `
3598
- <div class="repeater-root" [ngClass]="['iox-node-' + nodeId, children.length ? 'has-children' : '']">
3981
+ <ng-template #badgeContent>
3982
+ <i class="ph-thin ph-repeat"></i>
3983
+ <span *ngIf="source">{{ source }}</span>
3984
+ <span *ngIf="!source" class="repeater-badge-empty">No source</span>
3985
+ <span *ngIf="items.length" class="repeater-badge-count">&times; {{ items.length }}</span>
3986
+ <span *ngIf="isLoading" class="repeater-badge-loading"><i class="ph-thin ph-circle-notch"></i></span>
3987
+ </ng-template>
3599
3988
 
3600
- <!-- Badge: always visible when empty, absolute hover-only when has children -->
3601
- <div class="repeater-badge">
3602
- <i class="ph-thin ph-repeat"></i>
3603
- <span *ngIf="source">{{ source }}</span>
3604
- <span *ngIf="!source" class="repeater-badge-empty">No source</span>
3605
- <span *ngIf="items.length" class="repeater-badge-count">&times; {{ items.length }}</span>
3606
- <span *ngIf="isLoading" class="repeater-badge-loading"><i class="ph-thin ph-circle-notch"></i></span>
3989
+ <!--
3990
+ .repeater-wrapper is the layout root.
3991
+ In has-children state, .repeater-badge lives here as a direct child
3992
+ (position:absolute, hover-only) so it is NOT a child of .repeater-root
3993
+ and does NOT shift the :nth-child index of the item wrappers.
3994
+
3995
+ In empty state, .repeater-badge lives inside .repeater-root (no items
3996
+ to count, so :nth-child is irrelevant there).
3997
+ -->
3998
+ <div class="repeater-wrapper" [class.has-children]="children.length > 0">
3999
+
4000
+ <!-- HAS-CHILDREN: badge outside root, absolute over wrapper -->
4001
+ <div *ngIf="children.length > 0" class="repeater-badge">
4002
+ <ng-container *ngTemplateOutlet="badgeContent"></ng-container>
3607
4003
  </div>
3608
4004
 
3609
- <!-- Template drop zone — user designs one item here -->
3610
- <div class="repeater-template"
4005
+ <div class="repeater-root"
4006
+ [ngClass]="['iox-node-' + nodeId]"
3611
4007
  ioxDropzone
3612
4008
  [ioxDropzoneId]="dropListId"
3613
4009
  [ioxDropzoneData]="children"
3614
4010
  [ioxDropzonePostDrop]="refreshPreviewsBound"
3615
4011
  (ioxDrop)="onDrop($event)">
3616
4012
 
3617
- <div *ngIf="!children.length" class="repeater-placeholder">
3618
- <i class="ph-thin ph-rows"></i>
3619
- <span>Drop a component here as the item template</span>
3620
- </div>
4013
+ <!-- EMPTY STATE: badge + placeholder inside root -->
4014
+ <ng-container *ngIf="!children.length">
4015
+ <div class="repeater-badge">
4016
+ <ng-container *ngTemplateOutlet="badgeContent"></ng-container>
4017
+ </div>
4018
+ <div class="repeater-placeholder">
4019
+ <i class="ph-thin ph-rows"></i>
4020
+ <span>Drop a component here as the item template</span>
4021
+ </div>
4022
+ </ng-container>
3621
4023
 
4024
+ <!-- Template items — DIRECT children of repeater-root.
4025
+ :first-child / :nth-child(odd) etc. now correctly target these
4026
+ as siblings of the preview items below. -->
3622
4027
  <div *ngFor="let child of children"
3623
- [ngClass]="['repeater-template-child', 'iox-outer-' + (child.styleId ?? child.id)]"
4028
+ [ngClass]="['repeater-item-template', 'iox-outer-' + (child.styleId ?? child.id)]"
3624
4029
  ioxDraggable
3625
4030
  [ioxDragData]="child"
3626
4031
  [ioxDragSourceId]="dropListId">
@@ -3631,24 +4036,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
3631
4036
  (onMouseLeave)="childMouseLeave.emit()">
3632
4037
  </ng-container>
3633
4038
  </div>
3634
- </div>
3635
4039
 
3636
- <!-- Preview items for rows 2..N one iox-outer wrapper per child so
3637
- width/margin outer props apply, and items participate directly in
3638
- the repeater-root grid/flex (template uses display:contents). -->
3639
- <ng-container *ngFor="let row of previewRows">
3640
- <div *ngFor="let child of row"
3641
- [ngClass]="['repeater-preview-child', 'iox-outer-' + (child.styleId ?? child.id)]">
3642
- <ng-container
3643
- [ioxRender]="child"
3644
- (onClick)="$event"
3645
- (onMouseEnter)="$event"
3646
- (onMouseLeave)="undefined">
3647
- </ng-container>
3648
- </div>
3649
- </ng-container>
4040
+ <!-- Preview items also DIRECT children of repeater-root. -->
4041
+ <ng-container *ngFor="let row of previewRows">
4042
+ <div *ngFor="let child of row"
4043
+ [ngClass]="['repeater-preview-child', 'iox-outer-' + (child.styleId ?? child.id)]">
4044
+ <ng-container
4045
+ [ioxRender]="child"
4046
+ (onClick)="$event"
4047
+ (onMouseEnter)="$event"
4048
+ (onMouseLeave)="undefined">
4049
+ </ng-container>
4050
+ </div>
4051
+ </ng-container>
4052
+ </div>
3650
4053
  </div>
3651
- `, standalone: false, styles: [".repeater-root{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-root.has-children .repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-root.has-children:hover .repeater-badge{opacity:1}.repeater-root.has-children .repeater-template{border:none;border-radius:0;padding:0;min-height:unset;display:contents}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-template{border:1px dashed rgba(203,144,144,.4);border-radius:0 0 4px 4px;padding:4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-template-child{min-width:0}.repeater-preview-child{opacity:.55;pointer-events:none;min-width:0}\n"] }]
4054
+ `, standalone: false, styles: [".repeater-wrapper{position:relative;box-sizing:border-box}.repeater-badge{display:flex;align-items:center;gap:.4rem;padding:4px 8px;font-size:11px;color:#cb9090;background:#cb909014;border:1px dashed rgba(203,144,144,.4);border-bottom:none;border-radius:4px 4px 0 0;-webkit-user-select:none;user-select:none}.repeater-wrapper.has-children>.repeater-badge{position:absolute;top:0;left:0;z-index:10;border-radius:0 0 4px;border:1px solid rgba(203,144,144,.5);opacity:0;cursor:pointer;transition:opacity .15s}.repeater-wrapper.has-children:hover>.repeater-badge{opacity:1}.repeater-badge i{font-size:13px}.repeater-badge-empty{opacity:.55}.repeater-badge-count{margin-left:auto;font-weight:600}.repeater-badge-loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.repeater-root{box-sizing:border-box}.repeater-wrapper:not(.has-children) .repeater-root{border:1px dashed rgba(203,144,144,.4);border-top:none;border-radius:0 0 4px 4px;min-height:40px}.repeater-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1.5rem 1rem;color:#64748b80;font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.repeater-placeholder i{font-size:20px}.repeater-item-template{min-width:0}.repeater-preview-child{opacity:.55;pointer-events:none;min-width:0}\n"] }]
3652
4055
  }], ctorParameters: () => [{ type: DragEngineService }, { type: OverlayService }, { type: undefined, decorators: [{
3653
4056
  type: Optional
3654
4057
  }, {
@@ -3825,363 +4228,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
3825
4228
  args: [PanelChildComponent]
3826
4229
  }] } });
3827
4230
 
3828
- var StyleCategory;
3829
- (function (StyleCategory) {
3830
- StyleCategory["Layout"] = "Layout";
3831
- StyleCategory["Size"] = "Size";
3832
- StyleCategory["Typography"] = "Typography";
3833
- StyleCategory["Spacing"] = "Spacing";
3834
- StyleCategory["Border"] = "Border";
3835
- StyleCategory["Position"] = "Position";
3836
- StyleCategory["Background"] = "Background";
3837
- StyleCategory["Effects"] = "Effects";
3838
- // Legacy — kept for backward compat; prefer Size
3839
- StyleCategory["Dimensions"] = "Dimensions";
3840
- })(StyleCategory || (StyleCategory = {}));
3841
- const TRIGGER_OPTIONS = [
3842
- { value: 'pageLoad', label: 'Page load', icon: 'ph-thin ph-play' },
3843
- { value: 'viewportEnter', label: 'Enter viewport', icon: 'ph-thin ph-eye' },
3844
- { value: 'click', label: 'Click', icon: 'ph-thin ph-cursor-click' },
3845
- { value: 'hover', label: 'Hover', icon: 'ph-thin ph-hand-pointing' },
3846
- { value: 'scrollProgress', label: 'Scroll progress', icon: 'ph-thin ph-arrow-fat-lines-down' },
3847
- ];
3848
- const ACTION_TYPE_OPTIONS = [
3849
- { value: 'fadeIn', label: 'Fade in' },
3850
- { value: 'fadeOut', label: 'Fade out' },
3851
- { value: 'moveUp', label: 'Move up' },
3852
- { value: 'moveDown', label: 'Move down' },
3853
- { value: 'moveLeft', label: 'Move left' },
3854
- { value: 'moveRight', label: 'Move right' },
3855
- { value: 'scaleIn', label: 'Scale in' },
3856
- { value: 'scaleOut', label: 'Scale out' },
3857
- { value: 'rotate', label: 'Rotate' },
3858
- { value: 'show', label: 'Show' },
3859
- { value: 'hide', label: 'Hide' },
3860
- { value: 'toggleVisibility', label: 'Toggle visibility' },
3861
- ];
3862
- const EASING_OPTIONS = ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'];
3863
- const SUPPORTED_STATES = ['hover', 'active', 'focus'];
3864
- /**
3865
- * Build a full StyleTraitGroup[] schema populated with values from a flat
3866
- * styleProps map. Used when editing a preset in the Style panel — the panel
3867
- * receives the same `StyleTraitGroup[]` shape regardless of context.
3868
- */
3869
- function buildPresetStyleTraits(styleProps) {
3870
- const allGroups = [
3871
- new LayoutGroupStyleConfig(),
3872
- new SizeGroupStyleConfig(),
3873
- new BackgroundGroupStyleConfig(),
3874
- new SpacingGroupStyleConfig(),
3875
- new BorderGroupStyleConfig(),
3876
- new TypographyGroupStyleConfig(),
3877
- new EffectsGroupStyleConfig(),
3878
- new PositionGroupStyleConfig(),
3879
- ];
3880
- return allGroups.map(group => ({
3881
- ...group,
3882
- traits: group.traits.map(trait => ({
3883
- ...trait,
3884
- default: styleProps[trait.name] !== undefined ? styleProps[trait.name] : trait.default,
3885
- })),
3886
- }));
3887
- }
3888
- var TraitInputType;
3889
- (function (TraitInputType) {
3890
- TraitInputType["Text"] = "text";
3891
- TraitInputType["Number"] = "number";
3892
- TraitInputType["Select"] = "select";
3893
- TraitInputType["Checkbox"] = "checkbox";
3894
- TraitInputType["Color"] = "colorPicker";
3895
- TraitInputType["DirectionalSize"] = "directionalSize";
3896
- TraitInputType["SelectButton"] = "selectButton";
3897
- TraitInputType["Scrub"] = "scrub";
3898
- TraitInputType["Icon"] = "icon";
3899
- TraitInputType["Media"] = "media";
3900
- TraitInputType["FontFamily"] = "fontFamily";
3901
- TraitInputType["GridTemplate"] = "gridTemplate";
3902
- })(TraitInputType || (TraitInputType = {}));
3903
- function generateNodeId() {
3904
- return Math.random().toString(36).substr(2, 9);
3905
- }
3906
- class ComponentConfig {
3907
- constructor(type, selector, icon = 'ph-thin ph-cube', category = 'General') {
3908
- this.id = generateNodeId();
3909
- this.type = type;
3910
- this.selector = selector;
3911
- this.icon = icon;
3912
- this.category = category;
3913
- this.inputs = {};
3914
- this.traits = [];
3915
- this.styleTraits = [];
3916
- }
3917
- /** Override specific trait defaults after styleTraits are set.
3918
- * Call this at the end of each subclass constructor. */
3919
- applyStyleDefaults(defaults) {
3920
- for (const group of this.styleTraits) {
3921
- for (const trait of group.traits) {
3922
- if (defaults[trait.name] !== undefined) {
3923
- trait.default = defaults[trait.name];
3924
- }
3925
- }
3926
- }
3927
- }
3928
- }
3929
- class TraitConfig {
3930
- constructor(name, label, type, options, defaultValue, inline, showWhen) {
3931
- this.name = name;
3932
- this.label = label;
3933
- this.type = type;
3934
- this.options = options;
3935
- this.default = defaultValue;
3936
- this.inline = inline;
3937
- this.showWhen = showWhen;
3938
- }
3939
- }
3940
- class GroupStyleConfig {
3941
- constructor(category, traits) {
3942
- this.category = category;
3943
- this.traits = traits;
3944
- }
3945
- }
3946
- // ─── Unit constants ───────────────────────────────────────────────────────────
3947
- const UNITS_ALL = ['px', '%', 'rem', 'em', 'vw', 'vh'];
3948
- const UNITS_NO_VW = ['px', '%', 'rem', 'em', 'vh'];
3949
- const UNITS_FIXED = ['px', '%', 'rem', 'em'];
3950
- const UNITS_DEG = ['deg'];
3951
- // ─── Shared panel utilities ──────────────────────────────────────────────────
3952
- function resolveTraitControllerType(type) {
3953
- switch (type) {
3954
- case TraitInputType.Number: return 'number';
3955
- case TraitInputType.Select: return 'select';
3956
- case TraitInputType.Checkbox: return 'switch';
3957
- case TraitInputType.Color: return 'colorPicker';
3958
- case TraitInputType.DirectionalSize: return 'directionalSize';
3959
- case TraitInputType.SelectButton: return 'selectButton';
3960
- case TraitInputType.Scrub: return 'scrub';
3961
- case TraitInputType.Icon: return 'icon';
3962
- case TraitInputType.Media: return 'media';
3963
- case TraitInputType.FontFamily: return 'select';
3964
- case TraitInputType.GridTemplate: return 'gridTemplate';
3965
- default: return 'text';
3966
- }
3967
- }
3968
- function resolveTraitOptions(trait) {
3969
- if (trait.type === TraitInputType.Color ||
3970
- trait.type === TraitInputType.DirectionalSize ||
3971
- trait.type === TraitInputType.SelectButton ||
3972
- trait.type === TraitInputType.Scrub ||
3973
- trait.type === TraitInputType.Media) {
3974
- return trait.options;
3975
- }
3976
- if (trait.type === TraitInputType.FontFamily) {
3977
- // FontFamily options are injected dynamically by the style panel via FontManagerService.
3978
- // Return empty array here; the panel overrides this in resolveOptions().
3979
- return [];
3980
- }
3981
- if (trait.type !== TraitInputType.Select) {
3982
- return undefined;
3983
- }
3984
- const opts = trait.options ?? [];
3985
- // Already formatted as {label, value}[]
3986
- if (opts.length > 0 && typeof opts[0] === 'object' && 'label' in opts[0]) {
3987
- return opts;
3988
- }
3989
- return opts.map((option) => ({ label: option, value: option }));
3990
- }
3991
- // ─── Style Groups ─────────────────────────────────────────────────────────────
3992
- const NON_STATIC_POSITIONS = ['relative', 'absolute', 'fixed', 'sticky'];
3993
- class PositionGroupStyleConfig extends GroupStyleConfig {
3994
- constructor() {
3995
- super(StyleCategory.Position, [
3996
- new TraitConfig('position', 'Position', TraitInputType.Select, ['static', 'relative', 'absolute', 'fixed', 'sticky'], 'static'),
3997
- new TraitConfig('top', 'T', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
3998
- new TraitConfig('right', 'R', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
3999
- new TraitConfig('bottom', 'B', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
4000
- new TraitConfig('left', 'L', TraitInputType.Scrub, { min: -9999, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true, { trait: 'position', values: NON_STATIC_POSITIONS }),
4001
- new TraitConfig('zIndex', 'Z', TraitInputType.Scrub, { min: -99, max: 9999, step: 1, units: [''] }, '0', false, { trait: 'position', values: NON_STATIC_POSITIONS }),
4002
- ]);
4003
- }
4004
- }
4005
- // ─── Layout ───────────────────────────────────────────────────────────────────
4006
- class LayoutGroupStyleConfig extends GroupStyleConfig {
4007
- constructor() {
4008
- super(StyleCategory.Layout, [
4009
- new TraitConfig('display', 'Display', TraitInputType.Select, ['block', 'inline', 'inline-block', 'flex', 'grid'], 'block'),
4010
- // ── Flex-only ─────────────────────────────────────────────────────
4011
- new TraitConfig('flexDirection', 'Direction', TraitInputType.SelectButton, [
4012
- { value: 'row', label: 'Horizontal', icon: 'ph-thin ph-arrow-right' },
4013
- { value: 'row-reverse', label: 'Horizontal Reversed', icon: 'ph-thin ph-arrow-left' },
4014
- { value: 'column', label: 'Vertical', icon: 'ph-thin ph-arrow-down' },
4015
- { value: 'column-reverse', label: 'Vertical Reversed', icon: 'ph-thin ph-arrow-up' },
4016
- ], 'row', false, { trait: 'display', values: 'flex' }),
4017
- new TraitConfig('flexWrap', 'Flex Wrap', TraitInputType.SelectButton, [
4018
- { value: 'nowrap', label: 'No Wrap', icon: 'ph-thin ph-minus' },
4019
- { value: 'wrap', label: 'Wrap', icon: 'ph-thin ph-arrow-elbow-down-right' },
4020
- { value: 'wrap-reverse', label: 'Wrap Reversed', icon: 'ph-thin ph-arrow-elbow-up-right' },
4021
- ], 'nowrap', false, { trait: 'display', values: 'flex' }),
4022
- // ── Shared flex + grid ────────────────────────────────────────────
4023
- new TraitConfig('justifyContent', 'Justify', TraitInputType.SelectButton, [
4024
- { value: 'flex-start', label: 'Start', icon: 'ph-thin ph-align-left' },
4025
- { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-horizontal' },
4026
- { value: 'flex-end', label: 'End', icon: 'ph-thin ph-align-right' },
4027
- { value: 'space-between', label: 'Space Between', icon: 'ph-thin ph-distribute-horizontal' },
4028
- { value: 'space-around', label: 'Space Around', icon: 'ph-thin ph-dots-three' },
4029
- { value: 'space-evenly', label: 'Space Evenly', icon: 'ph-thin ph-dots-six' },
4030
- ], 'flex-start', false, { trait: 'display', values: ['flex', 'grid'] }),
4031
- new TraitConfig('alignItems', 'Align Items', TraitInputType.SelectButton, [
4032
- { value: 'stretch', label: 'Stretch', icon: 'ph-thin ph-arrows-vertical' },
4033
- { value: 'start', label: 'Start', icon: 'ph-thin ph-align-top' },
4034
- { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-vertical' },
4035
- { value: 'end', label: 'End', icon: 'ph-thin ph-align-bottom' },
4036
- ], 'stretch', false, { trait: 'display', values: ['flex', 'grid'] }),
4037
- new TraitConfig('gap', 'Gap', TraitInputType.Scrub, { min: 0, max: 200, step: 1, units: UNITS_FIXED }, '0px', false, { trait: 'display', values: ['flex', 'grid'] }),
4038
- // ── Grid-only ─────────────────────────────────────────────────────
4039
- new TraitConfig('gridTemplateColumns', 'Columns', TraitInputType.GridTemplate, undefined, '1fr', false, { trait: 'display', values: 'grid' }),
4040
- new TraitConfig('gridTemplateRows', 'Rows', TraitInputType.GridTemplate, undefined, 'auto', false, { trait: 'display', values: 'grid' }),
4041
- new TraitConfig('justifyItems', 'Justify Items', TraitInputType.SelectButton, [
4042
- { value: 'stretch', label: 'Stretch', icon: 'ph-thin ph-arrows-horizontal' },
4043
- { value: 'start', label: 'Start', icon: 'ph-thin ph-align-left' },
4044
- { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-horizontal' },
4045
- { value: 'end', label: 'End', icon: 'ph-thin ph-align-right' },
4046
- ], 'stretch', false, { trait: 'display', values: 'grid' }),
4047
- new TraitConfig('alignContent', 'Align Content', TraitInputType.SelectButton, [
4048
- { value: 'start', label: 'Start', icon: 'ph-thin ph-align-top' },
4049
- { value: 'center', label: 'Center', icon: 'ph-thin ph-align-center-vertical' },
4050
- { value: 'end', label: 'End', icon: 'ph-thin ph-align-bottom' },
4051
- { value: 'space-between', label: 'Space Between', icon: 'ph-thin ph-distribute-vertical' },
4052
- { value: 'space-around', label: 'Space Around', icon: 'ph-thin ph-dots-three' },
4053
- { value: 'stretch', label: 'Stretch', icon: 'ph-thin ph-arrows-vertical' },
4054
- ], 'stretch', false, { trait: 'display', values: 'grid' }),
4055
- // ── Direction (always visible) ────────────────────────────────────
4056
- new TraitConfig('direction', 'Text Direction', TraitInputType.SelectButton, [
4057
- { value: 'ltr', label: 'LTR' },
4058
- { value: 'rtl', label: 'RTL' },
4059
- ], 'ltr'),
4060
- ]);
4061
- }
4062
- }
4063
- // ─── Size (replaces Dimensions) ───────────────────────────────────────────────
4064
- class SizeGroupStyleConfig extends GroupStyleConfig {
4065
- constructor() {
4066
- super(StyleCategory.Size, [
4067
- new TraitConfig('width', 'W', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
4068
- new TraitConfig('height', 'H', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
4069
- new TraitConfig('minWidth', 'Min W', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
4070
- new TraitConfig('minHeight', 'Min H', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
4071
- new TraitConfig('maxWidth', 'Max W', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
4072
- new TraitConfig('maxHeight', 'Max H', TraitInputType.Scrub, { min: 0, max: 9999, step: 1, units: UNITS_ALL, allowAuto: true }, 'auto', true),
4073
- new TraitConfig('aspectRatio', 'Aspect Ratio', TraitInputType.Text, undefined, 'auto'),
4074
- ]);
4075
- }
4076
- }
4077
- /** @deprecated Use SizeGroupStyleConfig instead. */
4078
- class DimensionsGroupStyleConfig extends SizeGroupStyleConfig {
4079
- }
4080
- // ─── Background ───────────────────────────────────────────────────────────────
4081
- class BackgroundGroupStyleConfig extends GroupStyleConfig {
4082
- constructor() {
4083
- super(StyleCategory.Background, [
4084
- new TraitConfig('backgroundColor', 'Background', TraitInputType.Color, { allowGradient: true, allowTransparent: true, formats: ['hex', 'rgb', 'hsl', 'transparent'] }, 'transparent'),
4085
- ]);
4086
- }
4087
- }
4088
- class SpacingGroupStyleConfig extends GroupStyleConfig {
4089
- constructor() {
4090
- super(StyleCategory.Spacing, [
4091
- new TraitConfig('margin', 'Margin', TraitInputType.DirectionalSize, {
4092
- allowNegative: true,
4093
- allowLinkedSlider: true,
4094
- slider: { min: 0, max: 200, step: 1 }
4095
- }, '0px 0px 0px 0px'),
4096
- new TraitConfig('padding', 'Padding', TraitInputType.DirectionalSize, {
4097
- allowNegative: false,
4098
- allowLinkedSlider: true,
4099
- slider: { min: 0, max: 200, step: 1 }
4100
- }, '0px 0px 0px 0px')
4101
- ]);
4102
- }
4103
- }
4104
- class BorderGroupStyleConfig extends GroupStyleConfig {
4105
- constructor() {
4106
- super(StyleCategory.Border, [
4107
- new TraitConfig('borderWidth', 'Border Width', TraitInputType.DirectionalSize, {
4108
- units: UNITS_FIXED,
4109
- allowNegative: false,
4110
- allowLinkedSlider: true,
4111
- slider: { min: 0, max: 32, step: 1 }
4112
- }, '0px'),
4113
- new TraitConfig('borderStyle', 'Border Style', TraitInputType.SelectButton, [
4114
- { value: 'solid', icon: 'ph-thin ph-line-segment' },
4115
- { value: 'dashed', icon: 'ph-thin ph-dots-three' },
4116
- { value: 'dotted', icon: 'ph-thin ph-dots-six' },
4117
- ], 'solid'),
4118
- new TraitConfig('borderColor', 'Border Color', TraitInputType.Color, { allowGradient: false, allowTransparent: false, formats: ['hex', 'rgb', 'hsl'] }, '#CCCCCC'),
4119
- new TraitConfig('borderRadius', 'Border Radius', TraitInputType.DirectionalSize, {
4120
- units: UNITS_FIXED,
4121
- allowNegative: false,
4122
- allowLinkedSlider: true,
4123
- linkByDefault: true,
4124
- slider: { min: 0, max: 120, step: 1 },
4125
- segments: [
4126
- { key: 'top-left', icon: 'ph-arrow-bend-up-left', ariaLabel: 'top left radius' },
4127
- { key: 'top-right', icon: 'ph-arrow-bend-up-right', ariaLabel: 'top right radius' },
4128
- { key: 'bottom-right', icon: 'ph-arrow-bend-down-right', ariaLabel: 'bottom right radius' },
4129
- { key: 'bottom-left', icon: 'ph-arrow-bend-down-left', ariaLabel: 'bottom left radius' }
4130
- ]
4131
- }, '0px')
4132
- ]);
4133
- }
4134
- }
4135
- class TypographyGroupStyleConfig extends GroupStyleConfig {
4136
- constructor() {
4137
- super(StyleCategory.Typography, [
4138
- new TraitConfig('fontFamily', 'Font Family', TraitInputType.FontFamily, [], 'Poppins, sans-serif'),
4139
- new TraitConfig('fontSize', 'Font Size', TraitInputType.Scrub, { min: 8, max: 200, step: 1, units: UNITS_FIXED }, '16px'),
4140
- new TraitConfig('fontWeight', 'Font Weight', TraitInputType.Select, ['100', '200', '300', '400', '500', '600', '700', '800', '900'], '400'),
4141
- new TraitConfig('color', 'Color', TraitInputType.Color, { allowGradient: false, allowTransparent: false, formats: ['hex', 'rgb', 'hsl'] }, '#000000'),
4142
- new TraitConfig('lineHeight', 'Line Height', TraitInputType.Select, ['1', '1.2', '1.4', '1.5', '1.75', '2', '2.5'], '1.5'),
4143
- new TraitConfig('letterSpacing', 'Letter Spacing', TraitInputType.Scrub, { min: -5, max: 50, step: 0.5, units: UNITS_FIXED }, '0px'),
4144
- new TraitConfig('textAlign', 'Alignment', TraitInputType.SelectButton, [
4145
- { value: 'left', icon: 'ph-thin ph-text-align-left' },
4146
- { value: 'center', icon: 'ph-thin ph-text-align-center' },
4147
- { value: 'right', icon: 'ph-thin ph-text-align-right' },
4148
- { value: 'justify', icon: 'ph-thin ph-text-align-justify' },
4149
- ], 'left'),
4150
- new TraitConfig('fontStyle', 'Italic', TraitInputType.SelectButton, { items: [{ value: 'italic', icon: 'ph-thin ph-text-italic', label: 'Italic' }], allowEmpty: true, noneValue: 'normal' }, 'normal', true),
4151
- new TraitConfig('textDecoration', 'Underline', TraitInputType.SelectButton, { items: [{ value: 'underline', icon: 'ph-thin ph-text-underline', label: 'Underline' }], allowEmpty: true, noneValue: 'none' }, 'none', true),
4152
- ]);
4153
- }
4154
- }
4155
- class EffectsGroupStyleConfig extends GroupStyleConfig {
4156
- constructor() {
4157
- super(StyleCategory.Effects, [
4158
- new TraitConfig('opacity', 'Opacity', TraitInputType.Scrub, { min: 0, max: 1, step: 0.01, units: [''] }, '1'),
4159
- new TraitConfig('mixBlendMode', 'Blend Mode', TraitInputType.Select, [
4160
- 'normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten',
4161
- 'color-dodge', 'color-burn', 'hard-light', 'soft-light',
4162
- 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity',
4163
- ], 'normal'),
4164
- new TraitConfig('cursor', 'Cursor', TraitInputType.Select, [
4165
- 'auto', 'default', 'pointer', 'text', 'move', 'not-allowed',
4166
- 'grab', 'grabbing', 'crosshair', 'zoom-in', 'zoom-out', 'none',
4167
- ], 'auto'),
4168
- ]);
4169
- }
4170
- }
4171
- function flattenSchemaFields(properties, prefix = '') {
4172
- const result = [];
4173
- for (const [key, field] of Object.entries(properties)) {
4174
- const fullPath = prefix ? `${prefix}.${key}` : key;
4175
- if (field.type === 'object' && field.properties) {
4176
- result.push(...flattenSchemaFields(field.properties, fullPath));
4177
- }
4178
- else {
4179
- result.push({ label: fullPath, value: fullPath });
4180
- }
4181
- }
4182
- return result;
4183
- }
4184
-
4185
4231
  class InteractionsPanelComponent {
4186
4232
  constructor(overlayService) {
4187
4233
  this.overlayService = overlayService;
@@ -4872,5 +4918,5 @@ class TextBlockComponentConfig extends ComponentConfig {
4872
4918
  * Generated bundle index. Do not edit.
4873
4919
  */
4874
4920
 
4875
- export { ACTION_TYPE_OPTIONS, BuilderButtonBlockComponent, BuilderButtonComponentConfig, BuilderComponent, BuilderContainerComponent, BuilderContainerComponentConfig, BuilderDividerComponentConfig, BuilderHeadingComponentConfig, BuilderIconComponentConfig, BuilderImageComponentConfig, BuilderLinkComponentConfig, BuilderLinkedContainerComponent, BuilderLinkedContainerConfig, BuilderMode, BuilderRepeaterComponent, BuilderSpacerComponentConfig, ButtonBlockComponentConfig, CardComponentConfig, ComponentConfig, ComponentRegistryService, DEVICE_OPTIONS, DataSourceRegistryService, DeviceMode, DragEngineService, EASING_OPTIONS$1 as EASING_OPTIONS, GroupStyleConfig, IOX_CONTENT_SERVICE, IOX_FONT_MANAGER, InteractionEngineService, InteractionsPanelComponent, IoxBuilderModule, IoxDraggableDirective, IoxDropzoneDirective, LayerTreeComponent, NodeAction, OverlayComponent, OverlayService, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, PresetRegistryService, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, SCREEN_WIDTH_OPTIONS, SUPPORTED_STATES, SectionComponent, SectionComponentConfig, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, ViewportService, ZOOM_OPTIONS, buildPresetStyleTraits, defaultPageSettings, generateNodeId, resolveTraitControllerType, resolveTraitOptions };
4921
+ export { ACTION_TYPE_OPTIONS, BuilderButtonBlockComponent, BuilderButtonComponentConfig, BuilderComponent, BuilderContainerComponent, BuilderContainerComponentConfig, BuilderDividerComponentConfig, BuilderHeadingComponentConfig, BuilderIconComponentConfig, BuilderImageComponentConfig, BuilderLinkComponentConfig, BuilderLinkedContainerComponent, BuilderLinkedContainerConfig, BuilderMode, BuilderRepeaterComponent, BuilderSpacerComponentConfig, ButtonBlockComponentConfig, CardComponentConfig, ComponentConfig, ComponentRegistryService, DEVICE_OPTIONS, DataSourceRegistryService, DeviceMode, DragEngineService, EASING_OPTIONS$1 as EASING_OPTIONS, GroupStyleConfig, INTERACTION_STATES, IOX_CONTENT_SERVICE, IOX_FONT_MANAGER, InteractionEngineService, InteractionsPanelComponent, IoxBuilderModule, IoxDraggableDirective, IoxDropzoneDirective, LayerTreeComponent, NodeAction, OverlayComponent, OverlayService, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, PresetRegistryService, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, SCREEN_WIDTH_OPTIONS, STRUCTURAL_STATES, SUPPORTED_STATES, SectionComponent, SectionComponentConfig, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, ViewportService, ZOOM_OPTIONS, buildPresetStyleTraits, defaultPageSettings, generateNodeId, resolveTraitControllerType, resolveTraitOptions };
4876
4922
  //# sourceMappingURL=vectoriox-iox-builder.mjs.map