@yourself.create/ngx-form-designer 0.0.4 → 0.0.6

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.
@@ -1,6 +1,6 @@
1
1
  import { v4 } from 'uuid';
2
2
  import * as i0 from '@angular/core';
3
- import { Injectable, InjectionToken, NgModule, inject, signal, computed, EventEmitter, DestroyRef, Injector, afterNextRender, ViewContainerRef, Input, ViewChild, Output, Inject, ChangeDetectionStrategy, Component, effect, ElementRef, NgZone, input, output, HostListener, ContentChildren, untracked, ChangeDetectorRef, Pipe } from '@angular/core';
3
+ import { Injectable, InjectionToken, NgModule, inject, signal, computed, EventEmitter, DestroyRef, Injector, afterNextRender, ViewContainerRef, Input, ViewChild, Output, Inject, ChangeDetectionStrategy, Component, effect, ElementRef, NgZone, input, output, HostListener, untracked, ChangeDetectorRef, Pipe, ContentChildren } from '@angular/core';
4
4
  import { BehaviorSubject, Subject, merge, of, filter, map, debounceTime as debounceTime$1, skip, firstValueFrom } from 'rxjs';
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule, DOCUMENT } from '@angular/common';
@@ -491,18 +491,9 @@ class FormEngine {
491
491
  const field = this.getFieldById(fieldId);
492
492
  if (!field)
493
493
  return false;
494
- // 1. Legacy Conditional Visibility
495
- if (field.conditionalVisibility) {
496
- const cv = field.conditionalVisibility;
497
- const val = this.values[cv.fieldName];
498
- if (cv.operator === 'equals' && val !== cv.value)
499
- return false;
500
- if (cv.operator === 'notEquals' && val === cv.value)
501
- return false;
502
- }
503
- // 2. Dependencies (Legacy)
494
+ // 1. Dependencies (Legacy)
504
495
  let visible = this.evaluateDependencyRules(field, 'show', 'hide', true);
505
- // 3. Enterprise Rules
496
+ // 2. Enterprise Rules
506
497
  visible = this.evaluateEnterpriseRules(field, 'visible', 'hidden', visible);
507
498
  return visible;
508
499
  }
@@ -823,6 +814,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
823
814
  }]
824
815
  }] });
825
816
 
817
+ const TRANSFORM_CONTROL_KEYS = new Set([
818
+ 'transformX',
819
+ 'transformY',
820
+ 'transformZ',
821
+ 'rotate',
822
+ 'scale'
823
+ ]);
826
824
  const WRAPPER_SURFACE_STYLE_KEYS = new Set([
827
825
  'backgroundColor',
828
826
  'borderColor',
@@ -860,10 +858,13 @@ function normalizeStyle$1(style) {
860
858
  if (!style)
861
859
  return {};
862
860
  const result = {};
861
+ const transform = buildTransform(style);
863
862
  Object.keys(style).forEach(key => {
864
863
  const value = style[key];
865
864
  if (value === undefined || value === null || value === '')
866
865
  return;
866
+ if (TRANSFORM_CONTROL_KEYS.has(key))
867
+ return;
867
868
  // 1. Check for Spacing Tokens (padding*, margin*, gap)
868
869
  // Only map known tokens to avoid accidental string matching
869
870
  const isSpacing = /^(padding|margin|gap)/i.test(key);
@@ -892,6 +893,10 @@ function normalizeStyle$1(style) {
892
893
  }
893
894
  result[key] = value;
894
895
  });
896
+ if (transform) {
897
+ const existingTransform = typeof result['transform'] === 'string' ? result['transform'].trim() : '';
898
+ result['transform'] = existingTransform ? `${existingTransform} ${transform}` : transform;
899
+ }
895
900
  return result;
896
901
  }
897
902
  function mergeAndNormalize(base, override) {
@@ -930,6 +935,75 @@ function hasWrapperSurfaceStyles(style) {
930
935
  }
931
936
  return Object.keys(style).some(key => WRAPPER_SURFACE_STYLE_KEYS.has(key));
932
937
  }
938
+ function buildTransform(style) {
939
+ const transforms = [];
940
+ const translateX = normalizeLength(style['transformX']);
941
+ const translateY = normalizeLength(style['transformY']);
942
+ const translateZ = normalizeLength(style['transformZ']);
943
+ const rotate = normalizeAngle(style['rotate']);
944
+ const scale = normalizeScale(style['scale']);
945
+ if (translateX || translateY || translateZ) {
946
+ transforms.push(`translate3d(${translateX ?? '0px'}, ${translateY ?? '0px'}, ${translateZ ?? '0px'})`);
947
+ }
948
+ if (rotate) {
949
+ transforms.push(`rotate(${rotate})`);
950
+ }
951
+ if (scale) {
952
+ transforms.push(`scale(${scale})`);
953
+ }
954
+ return transforms.join(' ');
955
+ }
956
+ function normalizeLength(value) {
957
+ if (value === undefined || value === null || value === '') {
958
+ return null;
959
+ }
960
+ if (typeof value === 'number' && Number.isFinite(value)) {
961
+ return `${value}px`;
962
+ }
963
+ if (typeof value === 'string') {
964
+ const trimmed = value.trim();
965
+ if (!trimmed) {
966
+ return null;
967
+ }
968
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
969
+ return `${trimmed}px`;
970
+ }
971
+ return trimmed;
972
+ }
973
+ return null;
974
+ }
975
+ function normalizeAngle(value) {
976
+ if (value === undefined || value === null || value === '') {
977
+ return null;
978
+ }
979
+ if (typeof value === 'number' && Number.isFinite(value)) {
980
+ return `${value}deg`;
981
+ }
982
+ if (typeof value === 'string') {
983
+ const trimmed = value.trim();
984
+ if (!trimmed) {
985
+ return null;
986
+ }
987
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
988
+ return `${trimmed}deg`;
989
+ }
990
+ return trimmed;
991
+ }
992
+ return null;
993
+ }
994
+ function normalizeScale(value) {
995
+ if (value === undefined || value === null || value === '') {
996
+ return null;
997
+ }
998
+ if (typeof value === 'number' && Number.isFinite(value)) {
999
+ return String(value);
1000
+ }
1001
+ if (typeof value === 'string') {
1002
+ const trimmed = value.trim();
1003
+ return trimmed || null;
1004
+ }
1005
+ return null;
1006
+ }
933
1007
 
934
1008
  const WIDGET_EDITOR_CONTEXT = new InjectionToken('WIDGET_EDITOR_CONTEXT');
935
1009
 
@@ -1023,6 +1097,7 @@ class DesignerStateService {
1023
1097
  isReadOnly = signal(false);
1024
1098
  // Structure of clipboard data
1025
1099
  clipboard = signal(null);
1100
+ pendingFieldInsert = signal(null);
1026
1101
  history = [];
1027
1102
  historyIndex = signal(-1);
1028
1103
  historyLength = signal(0);
@@ -1043,6 +1118,21 @@ class DesignerStateService {
1043
1118
  const entry = this.layoutIndex()[id];
1044
1119
  return entry?.node ?? null;
1045
1120
  });
1121
+ selectedEntry = computed(() => {
1122
+ const id = this.selectedNodeId();
1123
+ if (!id)
1124
+ return null;
1125
+ return this.layoutIndex()[id] ?? null;
1126
+ });
1127
+ selectedColumnId = computed(() => this.findSelectionColumnEntry(this.selectedEntry())?.path.at(-1) ?? null);
1128
+ selectedRowId = computed(() => this.findSelectionRowEntry(this.selectedEntry())?.path.at(-1) ?? null);
1129
+ canInsertColumnBeforeSelection = computed(() => !!this.resolveColumnInsertTarget(this.selectedEntry()));
1130
+ canInsertColumnAfterSelection = computed(() => !!this.resolveColumnInsertTarget(this.selectedEntry()));
1131
+ canInsertRowInSelectedColumn = computed(() => !!this.findSelectionColumnEntry(this.selectedEntry()));
1132
+ canInsertRowBeforeSelection = computed(() => !!this.resolveRelativeRowInsertTarget(this.selectedEntry()));
1133
+ canInsertRowAfterSelection = computed(() => !!this.resolveRelativeRowInsertTarget(this.selectedEntry()));
1134
+ canArmFieldInsertBeforeSelection = computed(() => !!this.getSelectedFieldReference());
1135
+ canArmFieldInsertAfterSelection = computed(() => !!this.getSelectedFieldReference());
1046
1136
  selectedField = computed(() => {
1047
1137
  const selectedId = this.selectedNodeId();
1048
1138
  if (!selectedId)
@@ -1091,6 +1181,7 @@ class DesignerStateService {
1091
1181
  this.restoreSnapshot(nextIndex);
1092
1182
  }
1093
1183
  selectNode(id) {
1184
+ this.pendingFieldInsert.set(null);
1094
1185
  if (!id) {
1095
1186
  this.selectedNodeId.set(null);
1096
1187
  this.selectedNodeIds.set([]);
@@ -1102,6 +1193,7 @@ class DesignerStateService {
1102
1193
  this.closeContextMenu();
1103
1194
  }
1104
1195
  toggleNodeSelection(id) {
1196
+ this.pendingFieldInsert.set(null);
1105
1197
  const current = this.selectedNodeIds();
1106
1198
  const exists = current.includes(id);
1107
1199
  const next = exists ? current.filter(item => item !== id) : [...current, id];
@@ -1116,6 +1208,14 @@ class DesignerStateService {
1116
1208
  isNodeSelected(nodeId) {
1117
1209
  return this.selectedNodeIds().includes(nodeId);
1118
1210
  }
1211
+ isSelectionRowAncestor(nodeId) {
1212
+ const selectedId = this.selectedNodeId();
1213
+ return !!selectedId && selectedId !== nodeId && this.selectedRowId() === nodeId;
1214
+ }
1215
+ isSelectionColumnAncestor(nodeId) {
1216
+ const selectedId = this.selectedNodeId();
1217
+ return !!selectedId && selectedId !== nodeId && this.selectedColumnId() === nodeId;
1218
+ }
1119
1219
  composeScopedNodeId(scopePath, nodeId) {
1120
1220
  return composeScopedNodeId(scopePath, nodeId);
1121
1221
  }
@@ -1364,6 +1464,7 @@ class DesignerStateService {
1364
1464
  }
1365
1465
  };
1366
1466
  removeRecursive(scopeSchema.layout);
1467
+ this.pruneEmptyRows(scopeSchema.layout);
1367
1468
  scopeSchema.fields = scopeSchema.fields.filter(field => !fieldsToRemove.has(field.id));
1368
1469
  }
1369
1470
  this.setSchema(nextSchema);
@@ -1559,20 +1660,67 @@ class DesignerStateService {
1559
1660
  const selectedId = this.selectedNodeId();
1560
1661
  const selectedEntry = selectedId ? this.layoutIndex()[selectedId] : undefined;
1561
1662
  const insertionScopePath = this.resolveInsertionScopePath(selectedEntry);
1663
+ const pendingFieldInsert = this.pendingFieldInsert();
1664
+ const canInsertRelativeToSelectedWidget = !!selectedEntry
1665
+ && selectedEntry.node.type === 'widget'
1666
+ && this.sameScope(selectedEntry.scopePath, insertionScopePath)
1667
+ && !!selectedEntry.node.refId;
1562
1668
  const nextSchema = this.cloneValue(current);
1563
1669
  const targetSchema = this.resolveSchemaAtScope(nextSchema, insertionScopePath);
1564
1670
  if (!targetSchema)
1565
1671
  return;
1566
- const targetCol = this.resolveTargetColumnForFieldInsert(targetSchema, selectedEntry, insertionScopePath);
1567
- if (!targetCol)
1568
- return;
1569
1672
  const inserted = this.createInsertedWidgets(widgetDef, widgetDef.type);
1570
- targetSchema.fields.push(...inserted.fields);
1571
- targetCol.children.push(...inserted.nodes);
1673
+ const targetedInsert = pendingFieldInsert
1674
+ ? this.resolveProgrammaticFieldInsertTarget(nextSchema, {
1675
+ referenceFieldId: pendingFieldInsert.referenceFieldId,
1676
+ position: pendingFieldInsert.position
1677
+ })
1678
+ : canInsertRelativeToSelectedWidget
1679
+ ? this.resolveProgrammaticFieldInsertTarget(nextSchema, {
1680
+ referenceFieldId: selectedEntry.node.refId,
1681
+ position: 'after'
1682
+ })
1683
+ : null;
1684
+ if (targetedInsert) {
1685
+ targetedInsert.schema.fields.push(...inserted.fields);
1686
+ targetedInsert.column.children.splice(targetedInsert.index, 0, ...inserted.nodes);
1687
+ }
1688
+ else {
1689
+ const targetCol = this.resolveTargetColumnForFieldInsert(targetSchema, selectedEntry, insertionScopePath);
1690
+ if (!targetCol)
1691
+ return;
1692
+ targetSchema.fields.push(...inserted.fields);
1693
+ targetCol.children.push(...inserted.nodes);
1694
+ }
1695
+ this.pendingFieldInsert.set(null);
1572
1696
  this.setSchema(nextSchema);
1573
1697
  const primaryNode = inserted.primaryNode;
1574
1698
  this.selectNode(this.composeScopedNodeId(insertionScopePath, primaryNode.id));
1575
1699
  }
1700
+ insertColumnBeforeSelection() {
1701
+ this.insertColumnRelativeToSelection('before');
1702
+ }
1703
+ insertColumnAfterSelection() {
1704
+ this.insertColumnRelativeToSelection('after');
1705
+ }
1706
+ insertRowBeforeSelection() {
1707
+ this.insertRowRelativeToSelection('before');
1708
+ }
1709
+ insertRowAfterSelection() {
1710
+ this.insertRowRelativeToSelection('after');
1711
+ }
1712
+ insertRowInSelectedColumn() {
1713
+ const columnEntry = this.findSelectionColumnEntry(this.selectedEntry());
1714
+ if (!columnEntry)
1715
+ return;
1716
+ this.insertRowInColumn(columnEntry.path.at(-1) ?? columnEntry.rawNodeId, 1);
1717
+ }
1718
+ armFieldInsertBeforeSelection() {
1719
+ this.armFieldInsertForSelection('before');
1720
+ }
1721
+ armFieldInsertAfterSelection() {
1722
+ this.armFieldInsertForSelection('after');
1723
+ }
1576
1724
  insertField(options) {
1577
1725
  if (this.isReadOnly())
1578
1726
  return null;
@@ -1969,11 +2117,21 @@ class DesignerStateService {
1969
2117
  const scopeSchema = this.resolveSchemaAtScope(newSchema, entry.scopePath);
1970
2118
  if (!scopeSchema)
1971
2119
  return;
2120
+ const fieldsToRemove = new Set();
2121
+ const collectFieldIds = (node) => {
2122
+ if (node.type === 'widget' && node.refId) {
2123
+ fieldsToRemove.add(node.refId);
2124
+ }
2125
+ if (node.type === 'row' || node.type === 'col') {
2126
+ node.children.forEach(child => collectFieldIds(child));
2127
+ }
2128
+ };
1972
2129
  // Find parent row and remove column
1973
2130
  const removeFromRow = (node) => {
1974
2131
  if (node.type === 'row') {
1975
2132
  const idx = node.children.findIndex(c => c.id === entry.rawNodeId && c.type === 'col');
1976
2133
  if (idx !== -1) {
2134
+ collectFieldIds(node.children[idx]);
1977
2135
  node.children.splice(idx, 1);
1978
2136
  return true;
1979
2137
  }
@@ -1987,6 +2145,8 @@ class DesignerStateService {
1987
2145
  return false;
1988
2146
  };
1989
2147
  removeFromRow(scopeSchema.layout);
2148
+ this.pruneEmptyRows(scopeSchema.layout);
2149
+ scopeSchema.fields = scopeSchema.fields.filter(field => !fieldsToRemove.has(field.id));
1990
2150
  this.setSchema(newSchema);
1991
2151
  }
1992
2152
  /** Set preset column layout (e.g., 3,4,6) on a row */
@@ -2199,6 +2359,7 @@ class DesignerStateService {
2199
2359
  return false;
2200
2360
  };
2201
2361
  removeFromParent(scopeSchema.layout, entry.rawNodeId);
2362
+ this.pruneEmptyRows(scopeSchema.layout);
2202
2363
  scopeSchema.fields = scopeSchema.fields.filter(field => !fieldsToRemove.has(field.id));
2203
2364
  // Clear selection
2204
2365
  if (this.selectedNodeId() === nodeId || this.selectedNodeId() === this.composeScopedNodeId(entry.scopePath, entry.rawNodeId)) {
@@ -2418,6 +2579,22 @@ class DesignerStateService {
2418
2579
  return null;
2419
2580
  return selectedEntry;
2420
2581
  }
2582
+ findSelectionColumnEntry(entry) {
2583
+ return this.findAncestorEntryByType(entry, 'col');
2584
+ }
2585
+ findSelectionRowEntry(entry) {
2586
+ return this.findAncestorEntryByType(entry, 'row');
2587
+ }
2588
+ findAncestorEntryByType(entry, type) {
2589
+ let cursor = entry;
2590
+ while (cursor) {
2591
+ if (cursor.node.type === type) {
2592
+ return cursor;
2593
+ }
2594
+ cursor = cursor.parentId ? this.layoutIndex()[cursor.parentId] : null;
2595
+ }
2596
+ return null;
2597
+ }
2421
2598
  resolveSchemaAtScope(root, scopePath) {
2422
2599
  let cursor = root;
2423
2600
  for (const repeatableFieldId of scopePath) {
@@ -2464,6 +2641,102 @@ class DesignerStateService {
2464
2641
  }
2465
2642
  return this.findFirstColumn(targetSchema.layout);
2466
2643
  }
2644
+ resolveColumnInsertTarget(selectionEntry) {
2645
+ const columnEntry = this.findSelectionColumnEntry(selectionEntry);
2646
+ const rowEntry = this.findSelectionRowEntry(selectionEntry);
2647
+ if (!columnEntry || !rowEntry)
2648
+ return null;
2649
+ return { rowEntry, columnEntry };
2650
+ }
2651
+ insertColumnRelativeToSelection(position) {
2652
+ if (this.isReadOnly())
2653
+ return;
2654
+ const target = this.resolveColumnInsertTarget(this.selectedEntry());
2655
+ if (!target)
2656
+ return;
2657
+ const current = this.schema();
2658
+ const nextSchema = this.cloneValue(current);
2659
+ const scopeSchema = this.resolveSchemaAtScope(nextSchema, target.rowEntry.scopePath);
2660
+ if (!scopeSchema)
2661
+ return;
2662
+ const rowNode = this.findNode(scopeSchema.layout, target.rowEntry.rawNodeId);
2663
+ const selectedColumn = this.findNode(scopeSchema.layout, target.columnEntry.rawNodeId);
2664
+ if (rowNode?.type !== 'row' || selectedColumn?.type !== 'col')
2665
+ return;
2666
+ const insertIndex = position === 'before' ? target.columnEntry.index : target.columnEntry.index + 1;
2667
+ const nextColumn = {
2668
+ id: v4(),
2669
+ type: 'col',
2670
+ responsive: this.cloneValue(selectedColumn.responsive ?? { xs: 12 }),
2671
+ children: []
2672
+ };
2673
+ rowNode.children.splice(insertIndex, 0, nextColumn);
2674
+ this.setSchema(nextSchema);
2675
+ this.selectNode(this.composeScopedNodeId(target.rowEntry.scopePath, nextColumn.id));
2676
+ }
2677
+ resolveRelativeRowInsertTarget(selectionEntry) {
2678
+ if (!selectionEntry)
2679
+ return null;
2680
+ if (selectionEntry.node.type === 'widget') {
2681
+ const containerEntry = this.findSelectionColumnEntry(selectionEntry);
2682
+ if (!containerEntry)
2683
+ return null;
2684
+ return { containerEntry, referenceIndex: selectionEntry.index };
2685
+ }
2686
+ if (selectionEntry.node.type === 'row') {
2687
+ const containerEntry = selectionEntry.parentId
2688
+ ? this.findAncestorEntryByType(this.layoutIndex()[selectionEntry.parentId], 'col')
2689
+ : null;
2690
+ if (!containerEntry)
2691
+ return null;
2692
+ return { containerEntry, referenceIndex: selectionEntry.index };
2693
+ }
2694
+ return null;
2695
+ }
2696
+ insertRowRelativeToSelection(position) {
2697
+ if (this.isReadOnly())
2698
+ return;
2699
+ const target = this.resolveRelativeRowInsertTarget(this.selectedEntry());
2700
+ if (!target)
2701
+ return;
2702
+ const current = this.schema();
2703
+ const nextSchema = this.cloneValue(current);
2704
+ const scopeSchema = this.resolveSchemaAtScope(nextSchema, target.containerEntry.scopePath);
2705
+ if (!scopeSchema)
2706
+ return;
2707
+ const container = this.findNode(scopeSchema.layout, target.containerEntry.rawNodeId);
2708
+ if (container?.type !== 'col')
2709
+ return;
2710
+ const nextRow = {
2711
+ id: v4(),
2712
+ type: 'row',
2713
+ children: [
2714
+ {
2715
+ id: v4(),
2716
+ type: 'col',
2717
+ responsive: { xs: 12 },
2718
+ children: []
2719
+ }
2720
+ ]
2721
+ };
2722
+ const insertIndex = position === 'before' ? target.referenceIndex : target.referenceIndex + 1;
2723
+ container.children.splice(insertIndex, 0, nextRow);
2724
+ this.setSchema(nextSchema);
2725
+ this.selectNode(this.composeScopedNodeId(target.containerEntry.scopePath, nextRow.children[0].id));
2726
+ }
2727
+ armFieldInsertForSelection(position) {
2728
+ const referenceFieldId = this.getSelectedFieldReference();
2729
+ if (!referenceFieldId)
2730
+ return;
2731
+ this.pendingFieldInsert.set({ referenceFieldId, position });
2732
+ }
2733
+ getSelectedFieldReference() {
2734
+ const entry = this.selectedEntry();
2735
+ if (!entry || entry.node.type !== 'widget')
2736
+ return null;
2737
+ const refId = entry.node.refId;
2738
+ return typeof refId === 'string' && refId.trim().length > 0 ? refId : null;
2739
+ }
2467
2740
  resolveFieldWidgetDefinition(widgetId, type) {
2468
2741
  if (widgetId) {
2469
2742
  const byId = this.widgetDefs.find(widget => widget.id === widgetId);
@@ -2657,6 +2930,20 @@ class DesignerStateService {
2657
2930
  const [removed] = children.splice(result.index, 1);
2658
2931
  return removed ?? null;
2659
2932
  }
2933
+ pruneEmptyRows(node) {
2934
+ if (node.type === 'widget') {
2935
+ return true;
2936
+ }
2937
+ if (node.type === 'col') {
2938
+ node.children = node.children.filter(child => this.pruneEmptyRows(child));
2939
+ return true;
2940
+ }
2941
+ if (node.type === 'row') {
2942
+ node.children = node.children.filter(child => this.pruneEmptyRows(child));
2943
+ return node.children.length > 0;
2944
+ }
2945
+ return true;
2946
+ }
2660
2947
  findWidgetByRefId(node, refId) {
2661
2948
  if (node.type === 'widget' && node.refId === refId) {
2662
2949
  return node;
@@ -2863,6 +3150,12 @@ class LayoutNodeComponent {
2863
3150
  get isSelected() {
2864
3151
  return this.designerState.isNodeSelected(this.getScopedNodeId(this.node.id));
2865
3152
  }
3153
+ get isRowSelectionAncestor() {
3154
+ return this.node.type === 'row' && this.designerState.isSelectionRowAncestor(this.getScopedNodeId(this.node.id));
3155
+ }
3156
+ get isColumnSelectionAncestor() {
3157
+ return this.node.type === 'col' && this.designerState.isSelectionColumnAncestor(this.getScopedNodeId(this.node.id));
3158
+ }
2866
3159
  get isResizing() {
2867
3160
  return this.activeResizeNodeId === this.node.id;
2868
3161
  }
@@ -2961,6 +3254,24 @@ class LayoutNodeComponent {
2961
3254
  wrapWidgetInRow() {
2962
3255
  this.designerState.wrapWidgetInRow(this.getScopedNodeId(this.node.id));
2963
3256
  }
3257
+ decreaseSelectedColumnSpan() {
3258
+ this.adjustSelectedColumnSpan(-1);
3259
+ }
3260
+ increaseSelectedColumnSpan() {
3261
+ this.adjustSelectedColumnSpan(1);
3262
+ }
3263
+ canDecreaseSelectedColumnSpan() {
3264
+ const span = this.getSelectedColumnSpan();
3265
+ return span !== null && span > LayoutNodeComponent.MIN_COLUMN_SPAN;
3266
+ }
3267
+ canIncreaseSelectedColumnSpan() {
3268
+ const span = this.getSelectedColumnSpan();
3269
+ return span !== null && span < LayoutNodeComponent.MAX_COLUMN_SPAN;
3270
+ }
3271
+ getSelectedColumnSpanLabel() {
3272
+ const span = this.getSelectedColumnSpan();
3273
+ return span === null ? '--/12' : `${span}/12`;
3274
+ }
2964
3275
  getNodeTypeLabel() {
2965
3276
  if (this.node.type === 'widget') {
2966
3277
  const widgetNode = this.node;
@@ -3270,6 +3581,32 @@ class LayoutNodeComponent {
3270
3581
  node.responsive[this.breakpoint] = nextSpan;
3271
3582
  this.cdr.detectChanges();
3272
3583
  }
3584
+ adjustSelectedColumnSpan(delta) {
3585
+ const columnEntry = this.getSelectedColumnEntry();
3586
+ const column = columnEntry?.node;
3587
+ if (!column || column.type !== 'col')
3588
+ return;
3589
+ const responsive = { ...(column.responsive ?? { xs: LayoutNodeComponent.MAX_COLUMN_SPAN }) };
3590
+ const currentSpan = this.getEffectiveSpan(responsive);
3591
+ const nextSpan = Math.max(LayoutNodeComponent.MIN_COLUMN_SPAN, Math.min(LayoutNodeComponent.MAX_COLUMN_SPAN, currentSpan + delta));
3592
+ if (nextSpan === currentSpan)
3593
+ return;
3594
+ responsive[this.breakpoint] = nextSpan;
3595
+ this.designerState.updateNodeResponsive(columnEntry.path.at(-1) ?? columnEntry.rawNodeId, responsive);
3596
+ }
3597
+ getSelectedColumnSpan() {
3598
+ const columnEntry = this.getSelectedColumnEntry();
3599
+ const column = columnEntry?.node;
3600
+ if (!column || column.type !== 'col')
3601
+ return null;
3602
+ return this.getEffectiveSpan(column.responsive ?? { xs: LayoutNodeComponent.MAX_COLUMN_SPAN });
3603
+ }
3604
+ getSelectedColumnEntry() {
3605
+ const columnId = this.designerState.selectedColumnId();
3606
+ if (!columnId)
3607
+ return null;
3608
+ return this.designerState.layoutIndex()[columnId] ?? null;
3609
+ }
3273
3610
  getColClasses(node) {
3274
3611
  if (node.type !== 'col')
3275
3612
  return '';
@@ -3445,6 +3782,9 @@ class LayoutNodeComponent {
3445
3782
  [class.outline-dashed]="designMode && showLayoutGuides"
3446
3783
  [class.outline-1]="designMode && showLayoutGuides"
3447
3784
  [class.outline-blue-200]="designMode && showLayoutGuides && !isSelected"
3785
+ [class.ring-1]="designMode && isRowSelectionAncestor"
3786
+ [class.ring-emerald-200]="designMode && isRowSelectionAncestor"
3787
+ [class.bg-emerald-50]="designMode && isRowSelectionAncestor"
3448
3788
  [class.ring-2]="designMode && isSelected"
3449
3789
  [class.ring-blue-500]="designMode && isSelected"
3450
3790
  [class.bg-blue-50]="designMode && isSelected">
@@ -3520,6 +3860,9 @@ class LayoutNodeComponent {
3520
3860
  [class.outline-dashed]="designMode && showLayoutGuides"
3521
3861
  [class.outline-1]="designMode && showLayoutGuides"
3522
3862
  [class.outline-gray-300]="designMode && showLayoutGuides && !isSelected"
3863
+ [class.ring-1]="designMode && isColumnSelectionAncestor"
3864
+ [class.ring-amber-200]="designMode && isColumnSelectionAncestor"
3865
+ [class.bg-amber-50]="designMode && isColumnSelectionAncestor"
3523
3866
  [class.ring-2]="designMode && isSelected"
3524
3867
  [class.ring-blue-500]="designMode && isSelected"
3525
3868
  [class.bg-blue-50]="designMode && isSelected && asCol(node).children.length === 0">
@@ -3547,6 +3890,22 @@ class LayoutNodeComponent {
3547
3890
  <lucide-icon name="layout-list" class="w-3.5 h-3.5"></lucide-icon>
3548
3891
  </button>
3549
3892
  <div class="w-px h-4 bg-gray-700 mx-0.5"></div>
3893
+ <button type="button"
3894
+ (click)="decreaseSelectedColumnSpan()"
3895
+ class="px-2 py-1 hover:bg-gray-700 rounded transition-colors disabled:opacity-40 disabled:hover:bg-transparent"
3896
+ title="Decrease width"
3897
+ [disabled]="!canDecreaseSelectedColumnSpan()">
3898
+ -
3899
+ </button>
3900
+ <span class="min-w-10 text-center text-[10px] font-semibold text-gray-300">{{ getSelectedColumnSpanLabel() }}</span>
3901
+ <button type="button"
3902
+ (click)="increaseSelectedColumnSpan()"
3903
+ class="px-2 py-1 hover:bg-gray-700 rounded transition-colors disabled:opacity-40 disabled:hover:bg-transparent"
3904
+ title="Increase width"
3905
+ [disabled]="!canIncreaseSelectedColumnSpan()">
3906
+ +
3907
+ </button>
3908
+ <div class="w-px h-4 bg-gray-700 mx-0.5"></div>
3550
3909
  <span class="text-gray-400 uppercase font-semibold text-[10px] px-1.5">Column</span>
3551
3910
  </div>
3552
3911
 
@@ -3582,13 +3941,6 @@ class LayoutNodeComponent {
3582
3941
  <div *cdkDragPlaceholder class="min-h-[50px] bg-blue-50 border-2 border-blue-200 border-dashed mb-2 rounded"></div>
3583
3942
  </div>
3584
3943
 
3585
- <!-- Placeholder for empty col in designer -->
3586
- <div *ngIf="asCol(node).children.length === 0 && designMode"
3587
- class="h-full w-full min-h-[4rem] flex flex-col items-center justify-center p-4 border-2 border-dashed border-gray-200 rounded-lg bg-gray-50/50 hover:bg-gray-100 hover:border-blue-300 transition-all text-gray-400 gap-2">
3588
- <lucide-icon name="plus" class="w-5 h-5 opacity-50"></lucide-icon>
3589
- <span class="text-xs font-medium">Drop Widget Here</span>
3590
- </div>
3591
-
3592
3944
  <!-- Resize Handle - z-50 to stay above widget overlays -->
3593
3945
  <!-- Width Resize (Right) - Only if not last column in row -->
3594
3946
  <div *ngIf="designMode && isSelected"
@@ -3639,6 +3991,22 @@ class LayoutNodeComponent {
3639
3991
  <lucide-icon name="group" class="w-3.5 h-3.5"></lucide-icon>
3640
3992
  </button>
3641
3993
  <div class="w-px h-4 bg-gray-700 mx-0.5"></div>
3994
+ <button type="button"
3995
+ (click)="decreaseSelectedColumnSpan()"
3996
+ class="px-2 py-1 hover:bg-gray-700 rounded transition-colors disabled:opacity-40 disabled:hover:bg-transparent"
3997
+ title="Decrease width"
3998
+ [disabled]="!canDecreaseSelectedColumnSpan()">
3999
+ -
4000
+ </button>
4001
+ <span class="min-w-10 text-center text-[10px] font-semibold text-gray-300">{{ getSelectedColumnSpanLabel() }}</span>
4002
+ <button type="button"
4003
+ (click)="increaseSelectedColumnSpan()"
4004
+ class="px-2 py-1 hover:bg-gray-700 rounded transition-colors disabled:opacity-40 disabled:hover:bg-transparent"
4005
+ title="Increase width"
4006
+ [disabled]="!canIncreaseSelectedColumnSpan()">
4007
+ +
4008
+ </button>
4009
+ <div class="w-px h-4 bg-gray-700 mx-0.5"></div>
3642
4010
  <span class="text-gray-400 uppercase font-semibold text-[10px] px-1.5">{{ getNodeTypeLabel() }}</span>
3643
4011
  </div>
3644
4012
 
@@ -3699,6 +4067,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
3699
4067
  [class.outline-dashed]="designMode && showLayoutGuides"
3700
4068
  [class.outline-1]="designMode && showLayoutGuides"
3701
4069
  [class.outline-blue-200]="designMode && showLayoutGuides && !isSelected"
4070
+ [class.ring-1]="designMode && isRowSelectionAncestor"
4071
+ [class.ring-emerald-200]="designMode && isRowSelectionAncestor"
4072
+ [class.bg-emerald-50]="designMode && isRowSelectionAncestor"
3702
4073
  [class.ring-2]="designMode && isSelected"
3703
4074
  [class.ring-blue-500]="designMode && isSelected"
3704
4075
  [class.bg-blue-50]="designMode && isSelected">
@@ -3774,6 +4145,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
3774
4145
  [class.outline-dashed]="designMode && showLayoutGuides"
3775
4146
  [class.outline-1]="designMode && showLayoutGuides"
3776
4147
  [class.outline-gray-300]="designMode && showLayoutGuides && !isSelected"
4148
+ [class.ring-1]="designMode && isColumnSelectionAncestor"
4149
+ [class.ring-amber-200]="designMode && isColumnSelectionAncestor"
4150
+ [class.bg-amber-50]="designMode && isColumnSelectionAncestor"
3777
4151
  [class.ring-2]="designMode && isSelected"
3778
4152
  [class.ring-blue-500]="designMode && isSelected"
3779
4153
  [class.bg-blue-50]="designMode && isSelected && asCol(node).children.length === 0">
@@ -3801,6 +4175,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
3801
4175
  <lucide-icon name="layout-list" class="w-3.5 h-3.5"></lucide-icon>
3802
4176
  </button>
3803
4177
  <div class="w-px h-4 bg-gray-700 mx-0.5"></div>
4178
+ <button type="button"
4179
+ (click)="decreaseSelectedColumnSpan()"
4180
+ class="px-2 py-1 hover:bg-gray-700 rounded transition-colors disabled:opacity-40 disabled:hover:bg-transparent"
4181
+ title="Decrease width"
4182
+ [disabled]="!canDecreaseSelectedColumnSpan()">
4183
+ -
4184
+ </button>
4185
+ <span class="min-w-10 text-center text-[10px] font-semibold text-gray-300">{{ getSelectedColumnSpanLabel() }}</span>
4186
+ <button type="button"
4187
+ (click)="increaseSelectedColumnSpan()"
4188
+ class="px-2 py-1 hover:bg-gray-700 rounded transition-colors disabled:opacity-40 disabled:hover:bg-transparent"
4189
+ title="Increase width"
4190
+ [disabled]="!canIncreaseSelectedColumnSpan()">
4191
+ +
4192
+ </button>
4193
+ <div class="w-px h-4 bg-gray-700 mx-0.5"></div>
3804
4194
  <span class="text-gray-400 uppercase font-semibold text-[10px] px-1.5">Column</span>
3805
4195
  </div>
3806
4196
 
@@ -3836,13 +4226,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
3836
4226
  <div *cdkDragPlaceholder class="min-h-[50px] bg-blue-50 border-2 border-blue-200 border-dashed mb-2 rounded"></div>
3837
4227
  </div>
3838
4228
 
3839
- <!-- Placeholder for empty col in designer -->
3840
- <div *ngIf="asCol(node).children.length === 0 && designMode"
3841
- class="h-full w-full min-h-[4rem] flex flex-col items-center justify-center p-4 border-2 border-dashed border-gray-200 rounded-lg bg-gray-50/50 hover:bg-gray-100 hover:border-blue-300 transition-all text-gray-400 gap-2">
3842
- <lucide-icon name="plus" class="w-5 h-5 opacity-50"></lucide-icon>
3843
- <span class="text-xs font-medium">Drop Widget Here</span>
3844
- </div>
3845
-
3846
4229
  <!-- Resize Handle - z-50 to stay above widget overlays -->
3847
4230
  <!-- Width Resize (Right) - Only if not last column in row -->
3848
4231
  <div *ngIf="designMode && isSelected"
@@ -3893,6 +4276,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
3893
4276
  <lucide-icon name="group" class="w-3.5 h-3.5"></lucide-icon>
3894
4277
  </button>
3895
4278
  <div class="w-px h-4 bg-gray-700 mx-0.5"></div>
4279
+ <button type="button"
4280
+ (click)="decreaseSelectedColumnSpan()"
4281
+ class="px-2 py-1 hover:bg-gray-700 rounded transition-colors disabled:opacity-40 disabled:hover:bg-transparent"
4282
+ title="Decrease width"
4283
+ [disabled]="!canDecreaseSelectedColumnSpan()">
4284
+ -
4285
+ </button>
4286
+ <span class="min-w-10 text-center text-[10px] font-semibold text-gray-300">{{ getSelectedColumnSpanLabel() }}</span>
4287
+ <button type="button"
4288
+ (click)="increaseSelectedColumnSpan()"
4289
+ class="px-2 py-1 hover:bg-gray-700 rounded transition-colors disabled:opacity-40 disabled:hover:bg-transparent"
4290
+ title="Increase width"
4291
+ [disabled]="!canIncreaseSelectedColumnSpan()">
4292
+ +
4293
+ </button>
4294
+ <div class="w-px h-4 bg-gray-700 mx-0.5"></div>
3896
4295
  <span class="text-gray-400 uppercase font-semibold text-[10px] px-1.5">{{ getNodeTypeLabel() }}</span>
3897
4296
  </div>
3898
4297
 
@@ -4864,7 +5263,7 @@ class JsonFormRendererComponent {
4864
5263
  this.valueChange.emit(fieldValueMap);
4865
5264
  this.groupedValueChange.emit(groupedValues);
4866
5265
  this.combinedValueChange.emit(combinedValues);
4867
- this.validationChange.emit(this.getValidationResult());
5266
+ this.validationChange.emit(this.getValidationResult(true));
4868
5267
  }
4869
5268
  disposeRunner() {
4870
5269
  if (this.runner) {
@@ -5016,21 +5415,17 @@ class JsonFormRendererComponent {
5016
5415
  event.preventDefault();
5017
5416
  if (this.mode === 'design')
5018
5417
  return;
5019
- const errors = this.engine?.validate() ?? {};
5020
- const validation = {
5021
- errors: { ...errors },
5022
- isValid: Object.keys(errors).length === 0
5023
- };
5418
+ const validation = this.getValidationResult(true);
5024
5419
  this.validationChange.emit(validation);
5025
5420
  // Notify all widgets to show errors if any
5026
5421
  this.engine?.submit();
5027
5422
  const preUploadFieldValueMap = this.uploadOnSubmit
5028
- ? await this.buildFieldValueMap(this.engine?.getValues() ?? {})
5423
+ ? await this.buildFieldValueMap(this.engine?.getValues() ?? {}, { normalizeFileFieldsForSubmit: true })
5029
5424
  : undefined;
5030
5425
  const uploadedFiles = this.uploadOnSubmit ? await this.uploadPendingFiles() : {};
5031
5426
  this.uploadedFilesChange.emit(uploadedFiles);
5032
5427
  const values = this.engine?.getValues() ?? {};
5033
- const fieldValueMap = await this.buildFieldValueMap(values);
5428
+ const fieldValueMap = await this.buildFieldValueMap(values, { normalizeFileFieldsForSubmit: true });
5034
5429
  const submitValues = preUploadFieldValueMap
5035
5430
  ? this.mergeFileMetadata(fieldValueMap, preUploadFieldValueMap)
5036
5431
  : fieldValueMap;
@@ -5042,13 +5437,134 @@ class JsonFormRendererComponent {
5042
5437
  validation
5043
5438
  });
5044
5439
  }
5045
- getValidationResult() {
5046
- const errors = this.engine?.getErrors() ?? {};
5440
+ getValidationResult(revalidate = false) {
5441
+ const errors = revalidate
5442
+ ? (this.engine?.validate() ?? {})
5443
+ : (this.engine?.getErrors() ?? {});
5047
5444
  return {
5048
- errors,
5049
- isValid: Object.keys(errors).length === 0
5445
+ errors: { ...errors },
5446
+ isValid: Object.keys(errors).length === 0,
5447
+ fields: this.buildFieldValidationState(errors)
5050
5448
  };
5051
5449
  }
5450
+ buildFieldValidationState(errors) {
5451
+ const schema = this.engine?.getSchema() ?? this.schema;
5452
+ if (!schema)
5453
+ return {};
5454
+ const states = {};
5455
+ for (const field of schema.fields) {
5456
+ const visible = this.engine ? this.engine.isFieldVisible(field.id) : true;
5457
+ const required = visible && (this.engine
5458
+ ? this.engine.isFieldRequired(field.id)
5459
+ : !!field.html5?.required);
5460
+ const fieldErrors = errors[field.name] ? [...errors[field.name]] : [];
5461
+ states[field.name] = {
5462
+ fieldId: field.id,
5463
+ fieldName: field.name,
5464
+ ...(field.label ? { label: field.label } : {}),
5465
+ visible,
5466
+ required,
5467
+ valid: fieldErrors.length === 0,
5468
+ errors: fieldErrors,
5469
+ validators: this.describeFieldValidators(field, visible, required)
5470
+ };
5471
+ }
5472
+ return states;
5473
+ }
5474
+ describeFieldValidators(field, visible, required) {
5475
+ const validators = [];
5476
+ const value = this.engine?.getValue(field.name);
5477
+ const hasValue = !this.isValidationEmpty(value);
5478
+ if (this.hasRequiredValidation(field) || required) {
5479
+ validators.push({
5480
+ name: 'required',
5481
+ source: 'required',
5482
+ active: visible && required,
5483
+ message: 'This field is required.'
5484
+ });
5485
+ }
5486
+ if (field.html5?.minLength !== undefined) {
5487
+ validators.push({
5488
+ name: 'minLength',
5489
+ source: 'html5',
5490
+ active: visible && hasValue,
5491
+ value: field.html5.minLength
5492
+ });
5493
+ }
5494
+ if (field.html5?.maxLength !== undefined) {
5495
+ validators.push({
5496
+ name: 'maxLength',
5497
+ source: 'html5',
5498
+ active: visible && hasValue,
5499
+ value: field.html5.maxLength
5500
+ });
5501
+ }
5502
+ if (field.html5?.min !== undefined) {
5503
+ validators.push({
5504
+ name: 'min',
5505
+ source: 'html5',
5506
+ active: visible && hasValue,
5507
+ value: field.html5.min
5508
+ });
5509
+ }
5510
+ if (field.html5?.max !== undefined) {
5511
+ validators.push({
5512
+ name: 'max',
5513
+ source: 'html5',
5514
+ active: visible && hasValue,
5515
+ value: field.html5.max
5516
+ });
5517
+ }
5518
+ if (field.html5?.pattern) {
5519
+ validators.push({
5520
+ name: 'pattern',
5521
+ source: 'html5',
5522
+ active: visible && hasValue,
5523
+ value: field.html5.pattern
5524
+ });
5525
+ }
5526
+ for (const rule of field.validation ?? []) {
5527
+ validators.push({
5528
+ name: rule.type === 'builtin' ? (rule.name ?? 'builtin') : 'expression',
5529
+ source: 'custom',
5530
+ active: visible && hasValue && this.isValidationRuleActive(rule),
5531
+ value: rule.type === 'expression' ? rule.expression : rule.name,
5532
+ message: rule.message
5533
+ });
5534
+ }
5535
+ return validators;
5536
+ }
5537
+ hasRequiredValidation(field) {
5538
+ if (field.html5?.required)
5539
+ return true;
5540
+ if (field.dependencies?.some(rule => rule.effect === 'require' || rule.effect === 'optional')) {
5541
+ return true;
5542
+ }
5543
+ if (field.rules?.some(rule => rule.action === 'required'
5544
+ || rule.action === 'optional'
5545
+ || rule.elseAction === 'required'
5546
+ || rule.elseAction === 'optional')) {
5547
+ return true;
5548
+ }
5549
+ return false;
5550
+ }
5551
+ isValidationRuleActive(rule) {
5552
+ if (!rule.when)
5553
+ return true;
5554
+ try {
5555
+ const checkFn = new Function('form', `return ${rule.when}`);
5556
+ return checkFn(this.engine?.getValues() ?? {});
5557
+ }
5558
+ catch {
5559
+ return false;
5560
+ }
5561
+ }
5562
+ isValidationEmpty(value) {
5563
+ return value === null
5564
+ || value === undefined
5565
+ || value === ''
5566
+ || (Array.isArray(value) && value.length === 0);
5567
+ }
5052
5568
  async uploadPendingFiles() {
5053
5569
  if (!this.engine)
5054
5570
  return {};
@@ -5142,13 +5658,13 @@ class JsonFormRendererComponent {
5142
5658
  isObjectRecord(value) {
5143
5659
  return !!value && typeof value === 'object' && !Array.isArray(value);
5144
5660
  }
5145
- async buildFieldValueMap(values) {
5661
+ async buildFieldValueMap(values, options = {}) {
5146
5662
  const schema = this.engine?.getSchema();
5147
5663
  if (!schema)
5148
5664
  return {};
5149
- return this.buildFieldValueMapForSchema(schema, values);
5665
+ return this.buildFieldValueMapForSchema(schema, values, options);
5150
5666
  }
5151
- async buildFieldValueMapForSchema(schema, valuesScope) {
5667
+ async buildFieldValueMapForSchema(schema, valuesScope, options = {}) {
5152
5668
  const mapped = {};
5153
5669
  for (const field of schema.fields) {
5154
5670
  if (!field?.id || !field?.name)
@@ -5163,7 +5679,7 @@ class JsonFormRendererComponent {
5163
5679
  rows.push({});
5164
5680
  continue;
5165
5681
  }
5166
- rows.push(await this.buildFieldValueMapForSchema(itemSchema, row));
5682
+ rows.push(await this.buildFieldValueMapForSchema(itemSchema, row, options));
5167
5683
  }
5168
5684
  }
5169
5685
  mapped[field.id] = {
@@ -5175,7 +5691,10 @@ class JsonFormRendererComponent {
5175
5691
  if (field.type === 'file') {
5176
5692
  const fileValue = {
5177
5693
  fieldName: field.name,
5178
- fieldValue: rawValue,
5694
+ fieldValue: options.normalizeFileFieldsForSubmit
5695
+ ? this.normalizeFileFieldSubmitValue(rawValue)
5696
+ : rawValue,
5697
+ ...(options.normalizeFileFieldsForSubmit ? { fieldType: field.type } : {}),
5179
5698
  ...(await this.buildFileFieldMetadata(rawValue))
5180
5699
  };
5181
5700
  mapped[field.id] = fileValue;
@@ -5263,6 +5782,18 @@ class JsonFormRendererComponent {
5263
5782
  return undefined;
5264
5783
  return values.length === 1 ? values[0] : values;
5265
5784
  }
5785
+ normalizeFileFieldSubmitValue(value) {
5786
+ if (this.isFileList(value)) {
5787
+ return Array.from(value);
5788
+ }
5789
+ if (Array.isArray(value)) {
5790
+ return [...value];
5791
+ }
5792
+ if (value === null || value === undefined) {
5793
+ return [];
5794
+ }
5795
+ return [value];
5796
+ }
5266
5797
  getUploadedFileRefs(value) {
5267
5798
  if (this.isUploadedFileRef(value))
5268
5799
  return [value];
@@ -5678,11 +6209,7 @@ class FormJourneyViewerComponent {
5678
6209
  return { ok: false, reason: 'unknown-page' };
5679
6210
  }
5680
6211
  if (!this.viewOnly && this.renderer?.engine) {
5681
- const errors = this.renderer.engine.validate();
5682
- const validation = {
5683
- errors: { ...errors },
5684
- isValid: Object.keys(errors).length === 0
5685
- };
6212
+ const validation = this.renderer.getValidationResult(true);
5686
6213
  this.formValidationChange.emit(validation);
5687
6214
  if (!validation.isValid) {
5688
6215
  return { ok: false, reason: 'validation' };
@@ -7684,6 +8211,7 @@ class LayoutCanvasComponent {
7684
8211
  showLiveSchemaEditor = signal(false);
7685
8212
  liveSchemaEditorText = signal('');
7686
8213
  liveSchemaEditorError = signal('');
8214
+ openContextSubmenu = signal(null);
7687
8215
  liveSchemaEditorOptions = {
7688
8216
  fontSize: 12,
7689
8217
  lineNumbersMinChars: 3,
@@ -7801,6 +8329,7 @@ class LayoutCanvasComponent {
7801
8329
  this.closeContextMenu();
7802
8330
  }
7803
8331
  closeContextMenu() {
8332
+ this.openContextSubmenu.set(null);
7804
8333
  this.state.closeContextMenu();
7805
8334
  }
7806
8335
  groupSelected() {
@@ -7811,6 +8340,46 @@ class LayoutCanvasComponent {
7811
8340
  this.state.ungroupSelectedFields();
7812
8341
  this.closeContextMenu();
7813
8342
  }
8343
+ hasStructuralInsertActions() {
8344
+ return this.state.canInsertColumnBeforeSelection()
8345
+ || this.state.canInsertColumnAfterSelection()
8346
+ || this.state.canInsertRowInSelectedColumn()
8347
+ || this.state.canInsertRowBeforeSelection()
8348
+ || this.state.canInsertRowAfterSelection()
8349
+ || this.state.canArmFieldInsertBeforeSelection()
8350
+ || this.state.canArmFieldInsertAfterSelection();
8351
+ }
8352
+ toggleInsertSubmenu() {
8353
+ this.openContextSubmenu.update(current => current === 'insert' ? null : 'insert');
8354
+ }
8355
+ insertColumnBeforeSelection() {
8356
+ this.state.insertColumnBeforeSelection();
8357
+ this.closeContextMenu();
8358
+ }
8359
+ insertColumnAfterSelection() {
8360
+ this.state.insertColumnAfterSelection();
8361
+ this.closeContextMenu();
8362
+ }
8363
+ insertRowInSelectedColumn() {
8364
+ this.state.insertRowInSelectedColumn();
8365
+ this.closeContextMenu();
8366
+ }
8367
+ insertRowBeforeSelection() {
8368
+ this.state.insertRowBeforeSelection();
8369
+ this.closeContextMenu();
8370
+ }
8371
+ insertRowAfterSelection() {
8372
+ this.state.insertRowAfterSelection();
8373
+ this.closeContextMenu();
8374
+ }
8375
+ armFieldInsertBeforeSelection() {
8376
+ this.state.armFieldInsertBeforeSelection();
8377
+ this.closeContextMenu();
8378
+ }
8379
+ armFieldInsertAfterSelection() {
8380
+ this.state.armFieldInsertAfterSelection();
8381
+ this.closeContextMenu();
8382
+ }
7814
8383
  onCanvasContextMenu(event) {
7815
8384
  event.preventDefault();
7816
8385
  event.stopPropagation();
@@ -8249,6 +8818,56 @@ class LayoutCanvasComponent {
8249
8818
  <span>Delete</span>
8250
8819
  <span class="opacity-60 text-[10px]">Del</span>
8251
8820
  </button>
8821
+ <div *ngIf="hasStructuralInsertActions()" class="relative">
8822
+ <div class="h-px bg-border-default my-1"></div>
8823
+ <button type="button"
8824
+ aria-label="Open insert submenu"
8825
+ class="flex w-full items-center justify-between px-3 py-2 text-left text-text-primary hover:bg-slate-50"
8826
+ [attr.aria-expanded]="openContextSubmenu() === 'insert'"
8827
+ (click)="toggleInsertSubmenu(); $event.stopPropagation()">
8828
+ <span>Insert…</span>
8829
+ <span class="text-[10px] opacity-60">{{ openContextSubmenu() === 'insert' ? '‹' : '›' }}</span>
8830
+ </button>
8831
+
8832
+ <div *ngIf="openContextSubmenu() === 'insert'"
8833
+ class="absolute left-full top-0 ml-1 min-w-[210px] rounded-md border border-border-default bg-surface-default shadow-popover text-[12px]">
8834
+ <button *ngIf="!state.isReadOnly() && state.canInsertColumnBeforeSelection()" type="button"
8835
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8836
+ (click)="insertColumnBeforeSelection()">
8837
+ Add column left
8838
+ </button>
8839
+ <button *ngIf="!state.isReadOnly() && state.canInsertColumnAfterSelection()" type="button"
8840
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8841
+ (click)="insertColumnAfterSelection()">
8842
+ Add column right
8843
+ </button>
8844
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowInSelectedColumn()" type="button"
8845
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8846
+ (click)="insertRowInSelectedColumn()">
8847
+ Add row in column
8848
+ </button>
8849
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowBeforeSelection()" type="button"
8850
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8851
+ (click)="insertRowBeforeSelection()">
8852
+ Add row above
8853
+ </button>
8854
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowAfterSelection()" type="button"
8855
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8856
+ (click)="insertRowAfterSelection()">
8857
+ Add row below
8858
+ </button>
8859
+ <button *ngIf="!state.isReadOnly() && state.canArmFieldInsertBeforeSelection()" type="button"
8860
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8861
+ (click)="armFieldInsertBeforeSelection()">
8862
+ Next widget above
8863
+ </button>
8864
+ <button *ngIf="!state.isReadOnly() && state.canArmFieldInsertAfterSelection()" type="button"
8865
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8866
+ (click)="armFieldInsertAfterSelection()">
8867
+ Next widget below
8868
+ </button>
8869
+ </div>
8870
+ </div>
8252
8871
  <div class="h-px bg-border-default my-1"></div>
8253
8872
  <button *ngIf="!state.isReadOnly()" type="button"
8254
8873
  class="w-full px-3 py-2 text-left hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed text-text-primary"
@@ -8495,6 +9114,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
8495
9114
  <span>Delete</span>
8496
9115
  <span class="opacity-60 text-[10px]">Del</span>
8497
9116
  </button>
9117
+ <div *ngIf="hasStructuralInsertActions()" class="relative">
9118
+ <div class="h-px bg-border-default my-1"></div>
9119
+ <button type="button"
9120
+ aria-label="Open insert submenu"
9121
+ class="flex w-full items-center justify-between px-3 py-2 text-left text-text-primary hover:bg-slate-50"
9122
+ [attr.aria-expanded]="openContextSubmenu() === 'insert'"
9123
+ (click)="toggleInsertSubmenu(); $event.stopPropagation()">
9124
+ <span>Insert…</span>
9125
+ <span class="text-[10px] opacity-60">{{ openContextSubmenu() === 'insert' ? '‹' : '›' }}</span>
9126
+ </button>
9127
+
9128
+ <div *ngIf="openContextSubmenu() === 'insert'"
9129
+ class="absolute left-full top-0 ml-1 min-w-[210px] rounded-md border border-border-default bg-surface-default shadow-popover text-[12px]">
9130
+ <button *ngIf="!state.isReadOnly() && state.canInsertColumnBeforeSelection()" type="button"
9131
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9132
+ (click)="insertColumnBeforeSelection()">
9133
+ Add column left
9134
+ </button>
9135
+ <button *ngIf="!state.isReadOnly() && state.canInsertColumnAfterSelection()" type="button"
9136
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9137
+ (click)="insertColumnAfterSelection()">
9138
+ Add column right
9139
+ </button>
9140
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowInSelectedColumn()" type="button"
9141
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9142
+ (click)="insertRowInSelectedColumn()">
9143
+ Add row in column
9144
+ </button>
9145
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowBeforeSelection()" type="button"
9146
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9147
+ (click)="insertRowBeforeSelection()">
9148
+ Add row above
9149
+ </button>
9150
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowAfterSelection()" type="button"
9151
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9152
+ (click)="insertRowAfterSelection()">
9153
+ Add row below
9154
+ </button>
9155
+ <button *ngIf="!state.isReadOnly() && state.canArmFieldInsertBeforeSelection()" type="button"
9156
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9157
+ (click)="armFieldInsertBeforeSelection()">
9158
+ Next widget above
9159
+ </button>
9160
+ <button *ngIf="!state.isReadOnly() && state.canArmFieldInsertAfterSelection()" type="button"
9161
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9162
+ (click)="armFieldInsertAfterSelection()">
9163
+ Next widget below
9164
+ </button>
9165
+ </div>
9166
+ </div>
8498
9167
  <div class="h-px bg-border-default my-1"></div>
8499
9168
  <button *ngIf="!state.isReadOnly()" type="button"
8500
9169
  class="w-full px-3 py-2 text-left hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed text-text-primary"
@@ -9606,18 +10275,11 @@ class DynamicPropertiesComponent {
9606
10275
  onPropertyChange;
9607
10276
  designerCtx = inject(DesignerContext);
9608
10277
  validatorTypeOptions = [
9609
- { label: 'Required', value: 'required' },
9610
- { label: 'Email', value: 'email' },
9611
- { label: 'Min Value', value: 'min' },
9612
- { label: 'Max Value', value: 'max' },
9613
- { label: 'Min Length', value: 'minLength' },
9614
- { label: 'Max Length', value: 'maxLength' },
9615
- { label: 'Pattern', value: 'pattern' }
10278
+ { label: 'Built-in', value: 'builtin' },
10279
+ { label: 'Expression', value: 'expression' }
9616
10280
  ];
9617
- conditionalTypeOptions = [
9618
- { label: 'Always Visible', value: 'always' },
9619
- { label: 'Field Equals', value: 'equals' },
9620
- { label: 'Field Not Equals', value: 'notEquals' }
10281
+ builtinValidatorOptions = [
10282
+ { label: 'Email', value: 'email' }
9621
10283
  ];
9622
10284
  get properties() {
9623
10285
  if (!this.config)
@@ -9954,18 +10616,15 @@ class DynamicPropertiesComponent {
9954
10616
  addValidator(path) {
9955
10617
  if (this.readOnly)
9956
10618
  return;
9957
- const validators = this.getValue(path) || [];
9958
- validators.push({
9959
- name: 'required',
9960
- message: 'This field is required'
9961
- });
10619
+ const validators = [...(this.getValue(path) || [])];
10620
+ validators.push(this.createDefaultValidationRule());
9962
10621
  this.setValue(path, validators);
9963
10622
  this.handleFieldChange();
9964
10623
  }
9965
10624
  removeValidator(path, index) {
9966
10625
  if (this.readOnly)
9967
10626
  return;
9968
- const validators = this.getValue(path) || [];
10627
+ const validators = [...(this.getValue(path) || [])];
9969
10628
  validators.splice(index, 1);
9970
10629
  this.setValue(path, validators);
9971
10630
  this.handleFieldChange();
@@ -9973,57 +10632,47 @@ class DynamicPropertiesComponent {
9973
10632
  updateValidator(path, index, field, value) {
9974
10633
  if (this.readOnly)
9975
10634
  return;
9976
- const validators = this.getValue(path) || [];
9977
- if (validators[index]) {
9978
- validators[index][field] = value;
9979
- // Set default message if name changes
9980
- if (field === 'name') {
9981
- const messages = {
9982
- required: 'This field is required',
9983
- min: 'Value is too small',
9984
- max: 'Value is too large',
9985
- minLength: 'Too short',
9986
- maxLength: 'Too long',
9987
- pattern: 'Invalid format',
9988
- email: 'Invalid email'
9989
- };
9990
- validators[index].message = messages[value] || 'Invalid value';
9991
- }
9992
- this.setValue(path, validators);
10635
+ const validators = [...(this.getValue(path) || [])];
10636
+ const currentRule = validators[index];
10637
+ if (!currentRule) {
10638
+ return;
10639
+ }
10640
+ let nextRule = { ...currentRule };
10641
+ if (field === 'type') {
10642
+ nextRule = value === 'expression'
10643
+ ? {
10644
+ type: 'expression',
10645
+ expression: 'return true;',
10646
+ message: 'Validation failed.'
10647
+ }
10648
+ : this.createDefaultValidationRule();
10649
+ }
10650
+ else if (field === 'name') {
10651
+ nextRule.name = String(value);
10652
+ nextRule.message = this.defaultValidationMessage(nextRule);
10653
+ }
10654
+ else if (field === 'expression') {
10655
+ nextRule.expression = String(value);
9993
10656
  }
10657
+ else if (field === 'message') {
10658
+ nextRule.message = String(value);
10659
+ }
10660
+ validators[index] = nextRule;
10661
+ this.setValue(path, validators);
10662
+ this.handleFieldChange();
9994
10663
  }
9995
- getValidatorLabel(name) {
9996
- const labels = {
9997
- required: 'Required',
9998
- min: 'Minimum Value',
9999
- max: 'Maximum Value',
10000
- minLength: 'Min Length',
10001
- maxLength: 'Max Length',
10002
- pattern: 'Regex Pattern',
10003
- email: 'Email Address'
10664
+ createDefaultValidationRule() {
10665
+ return {
10666
+ type: 'builtin',
10667
+ name: 'email',
10668
+ message: 'Enter a valid email address.'
10004
10669
  };
10005
- return labels[name] || name;
10006
- }
10007
- // Conditional Logic Methods
10008
- enableConditional(path) {
10009
- if (this.readOnly)
10010
- return;
10011
- this.setValue(path, {
10012
- action: 'visible',
10013
- operator: 'eq'
10014
- });
10015
10670
  }
10016
- disableConditional(path) {
10017
- if (this.readOnly)
10018
- return;
10019
- this.setValue(path, null);
10020
- }
10021
- updateConditional(path, field, value) {
10022
- if (this.readOnly)
10023
- return;
10024
- const condition = this.getValue(path) || {};
10025
- condition[field] = value;
10026
- this.setValue(path, condition);
10671
+ defaultValidationMessage(rule) {
10672
+ if (rule.type === 'builtin' && rule.name === 'email') {
10673
+ return 'Enter a valid email address.';
10674
+ }
10675
+ return 'Validation failed.';
10027
10676
  }
10028
10677
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DynamicPropertiesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10029
10678
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: DynamicPropertiesComponent, isStandalone: true, selector: "app-dynamic-properties", inputs: { onPropertyChange: "onPropertyChange", config: "config", readOnly: "readOnly", includeSections: "includeSections", excludeSections: "excludeSections", allFields: "allFields" }, outputs: { configChange: "configChange" }, usesOnChanges: true, ngImport: i0, template: `
@@ -10180,16 +10829,27 @@ class DynamicPropertiesComponent {
10180
10829
  </div>
10181
10830
  <div class="space-y-2">
10182
10831
  <div *ngFor="let val of getValue(field.key) || []; let i = index" class="flex items-center gap-2 p-2 bg-white rounded border border-gray-200">
10183
- <select [(ngModel)]="val.type"
10184
- (ngModelChange)="handleFieldChange()"
10832
+ <select [ngModel]="val.type || 'builtin'"
10833
+ (ngModelChange)="updateValidator(field.key, i, 'type', $event)"
10185
10834
  class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10186
10835
  <option *ngFor="let option of validatorTypeOptions" [value]="option.value">{{ option.label }}</option>
10187
10836
  </select>
10188
- <input *ngIf="val.type !== 'required' && val.type !== 'email'"
10837
+ <select *ngIf="(val.type || 'builtin') === 'builtin'"
10838
+ [ngModel]="val.name || 'email'"
10839
+ (ngModelChange)="updateValidator(field.key, i, 'name', $event)"
10840
+ class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10841
+ <option *ngFor="let option of builtinValidatorOptions" [value]="option.value">{{ option.label }}</option>
10842
+ </select>
10843
+ <input *ngIf="val.type === 'expression'"
10189
10844
  type="text"
10190
- [(ngModel)]="val.value"
10191
- (blur)="handleFieldChange()"
10192
- placeholder="Value"
10845
+ [ngModel]="val.expression || ''"
10846
+ (ngModelChange)="updateValidator(field.key, i, 'expression', $event)"
10847
+ placeholder="return true;"
10848
+ class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10849
+ <input type="text"
10850
+ [ngModel]="val.message || ''"
10851
+ (ngModelChange)="updateValidator(field.key, i, 'message', $event)"
10852
+ placeholder="Validation message"
10193
10853
  class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10194
10854
  <button type="button"
10195
10855
  (click)="removeValidator(field.key, i)"
@@ -10201,36 +10861,6 @@ class DynamicPropertiesComponent {
10201
10861
  </div>
10202
10862
  </ui-field-wrapper>
10203
10863
 
10204
- <!-- Conditional Editor -->
10205
- <ui-field-wrapper *ngIf="field.type === 'conditional-editor'" [label]="field.label || ''" [helpText]="field.helpText || ''">
10206
- <div class="w-full border border-gray-200 rounded-lg p-3 bg-gray-50">
10207
- <div class="flex flex-col gap-2">
10208
- <select [ngModel]="getValue(field.key + '.type')"
10209
- (ngModelChange)="setValue(field.key + '.type', $event); handleFieldChange()"
10210
- class="h-8 w-full rounded border border-gray-300 bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10211
- <option *ngFor="let option of conditionalTypeOptions" [value]="option.value">{{ option.label }}</option>
10212
- </select>
10213
- <div *ngIf="getValue(field.key + '.type') !== 'always'" class="flex gap-2">
10214
- <input type="text"
10215
- [ngModel]="getValue(field.key + '.field')"
10216
- (ngModelChange)="setValue(field.key + '.field', $event)"
10217
- (blur)="handleFieldChange()"
10218
- placeholder="Field Name"
10219
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10220
- <input type="text"
10221
- [ngModel]="getValue(field.key + '.value')"
10222
- (ngModelChange)="setValue(field.key + '.value', $event)"
10223
- (blur)="handleFieldChange()"
10224
- placeholder="Value"
10225
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10226
- </div>
10227
- </div>
10228
- <div *ngIf="getValue(field.key + '.type') === 'always'" class="text-xs text-gray-400 mt-2">
10229
- Field always visible.
10230
- </div>
10231
- </div>
10232
- </ui-field-wrapper>
10233
-
10234
10864
  <!-- Field Reference -->
10235
10865
  <ui-field-wrapper *ngIf="field.type === 'field-reference'" [label]="field.label || ''" [helpText]="field.helpText || ''">
10236
10866
  <select [ngModel]="getValue(field.key) || ''"
@@ -10492,16 +11122,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
10492
11122
  </div>
10493
11123
  <div class="space-y-2">
10494
11124
  <div *ngFor="let val of getValue(field.key) || []; let i = index" class="flex items-center gap-2 p-2 bg-white rounded border border-gray-200">
10495
- <select [(ngModel)]="val.type"
10496
- (ngModelChange)="handleFieldChange()"
11125
+ <select [ngModel]="val.type || 'builtin'"
11126
+ (ngModelChange)="updateValidator(field.key, i, 'type', $event)"
10497
11127
  class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10498
11128
  <option *ngFor="let option of validatorTypeOptions" [value]="option.value">{{ option.label }}</option>
10499
11129
  </select>
10500
- <input *ngIf="val.type !== 'required' && val.type !== 'email'"
11130
+ <select *ngIf="(val.type || 'builtin') === 'builtin'"
11131
+ [ngModel]="val.name || 'email'"
11132
+ (ngModelChange)="updateValidator(field.key, i, 'name', $event)"
11133
+ class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
11134
+ <option *ngFor="let option of builtinValidatorOptions" [value]="option.value">{{ option.label }}</option>
11135
+ </select>
11136
+ <input *ngIf="val.type === 'expression'"
10501
11137
  type="text"
10502
- [(ngModel)]="val.value"
10503
- (blur)="handleFieldChange()"
10504
- placeholder="Value"
11138
+ [ngModel]="val.expression || ''"
11139
+ (ngModelChange)="updateValidator(field.key, i, 'expression', $event)"
11140
+ placeholder="return true;"
11141
+ class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
11142
+ <input type="text"
11143
+ [ngModel]="val.message || ''"
11144
+ (ngModelChange)="updateValidator(field.key, i, 'message', $event)"
11145
+ placeholder="Validation message"
10505
11146
  class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10506
11147
  <button type="button"
10507
11148
  (click)="removeValidator(field.key, i)"
@@ -10513,36 +11154,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
10513
11154
  </div>
10514
11155
  </ui-field-wrapper>
10515
11156
 
10516
- <!-- Conditional Editor -->
10517
- <ui-field-wrapper *ngIf="field.type === 'conditional-editor'" [label]="field.label || ''" [helpText]="field.helpText || ''">
10518
- <div class="w-full border border-gray-200 rounded-lg p-3 bg-gray-50">
10519
- <div class="flex flex-col gap-2">
10520
- <select [ngModel]="getValue(field.key + '.type')"
10521
- (ngModelChange)="setValue(field.key + '.type', $event); handleFieldChange()"
10522
- class="h-8 w-full rounded border border-gray-300 bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10523
- <option *ngFor="let option of conditionalTypeOptions" [value]="option.value">{{ option.label }}</option>
10524
- </select>
10525
- <div *ngIf="getValue(field.key + '.type') !== 'always'" class="flex gap-2">
10526
- <input type="text"
10527
- [ngModel]="getValue(field.key + '.field')"
10528
- (ngModelChange)="setValue(field.key + '.field', $event)"
10529
- (blur)="handleFieldChange()"
10530
- placeholder="Field Name"
10531
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10532
- <input type="text"
10533
- [ngModel]="getValue(field.key + '.value')"
10534
- (ngModelChange)="setValue(field.key + '.value', $event)"
10535
- (blur)="handleFieldChange()"
10536
- placeholder="Value"
10537
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10538
- </div>
10539
- </div>
10540
- <div *ngIf="getValue(field.key + '.type') === 'always'" class="text-xs text-gray-400 mt-2">
10541
- Field always visible.
10542
- </div>
10543
- </div>
10544
- </ui-field-wrapper>
10545
-
10546
11157
  <!-- Field Reference -->
10547
11158
  <ui-field-wrapper *ngIf="field.type === 'field-reference'" [label]="field.label || ''" [helpText]="field.helpText || ''">
10548
11159
  <select [ngModel]="getValue(field.key) || ''"
@@ -13616,6 +14227,161 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
13616
14227
  `, styles: [".inspector-input{height:1.75rem;padding-left:.5rem;padding-right:.5rem;font-size:.75rem;line-height:1rem;background-color:var(--color-input-dark);border:1px solid var(--color-border-dark);border-radius:var(--radius-md);color:var(--color-ink-700)}.inspector-input:focus{outline:none;border-color:var(--color-primary-blue)}.inspector-input-with-unit{display:flex;align-items:center;background-color:var(--color-input-dark);border:1px solid var(--color-border-dark);border-radius:var(--radius-md);overflow:hidden}.inspector-number-input{height:1.5rem;padding-left:.5rem;padding-right:.5rem;background-color:transparent;border:none;font-size:.75rem;line-height:1rem;color:var(--color-ink-700)}.inspector-number-input:focus{outline:none}.inspector-unit{font-size:10px;color:var(--color-ink-400);padding-left:.25rem;padding-right:.25rem;background-color:var(--color-input-dark);height:1.5rem;display:flex;align-items:center;border-left:1px solid var(--color-border-dark)}\n"] }]
13617
14228
  }] });
13618
14229
 
14230
+ class InspectorTransformSectionComponent {
14231
+ style = input({});
14232
+ styleChange = output();
14233
+ numberValue(key, fallback) {
14234
+ const value = this.style()?.[key];
14235
+ if (typeof value === 'number' && Number.isFinite(value)) {
14236
+ return value;
14237
+ }
14238
+ if (typeof value === 'string' && value.trim().length > 0) {
14239
+ const parsed = Number(value);
14240
+ if (Number.isFinite(parsed)) {
14241
+ return parsed;
14242
+ }
14243
+ }
14244
+ return fallback;
14245
+ }
14246
+ updateTransform(key, value) {
14247
+ const parsed = typeof value === 'number' ? value : Number(value);
14248
+ if (!Number.isFinite(parsed)) {
14249
+ return;
14250
+ }
14251
+ this.styleChange.emit({
14252
+ ...(this.style() ?? {}),
14253
+ [key]: parsed
14254
+ });
14255
+ }
14256
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: InspectorTransformSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14257
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.17", type: InspectorTransformSectionComponent, isStandalone: true, selector: "inspector-transform-section", inputs: { style: { classPropertyName: "style", publicName: "style", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { styleChange: "styleChange" }, ngImport: i0, template: `
14258
+ <div class="flex flex-col gap-3">
14259
+ <ui-range-number
14260
+ label="Translate X"
14261
+ prefix="X"
14262
+ hint="px"
14263
+ helpText="Move the widget horizontally."
14264
+ [min]="-200"
14265
+ [max]="200"
14266
+ [step]="1"
14267
+ [value]="numberValue('transformX', 0)"
14268
+ (valueChange)="updateTransform('transformX', $event)">
14269
+ </ui-range-number>
14270
+
14271
+ <ui-range-number
14272
+ label="Translate Y"
14273
+ prefix="Y"
14274
+ hint="px"
14275
+ helpText="Move the widget vertically."
14276
+ [min]="-200"
14277
+ [max]="200"
14278
+ [step]="1"
14279
+ [value]="numberValue('transformY', 0)"
14280
+ (valueChange)="updateTransform('transformY', $event)">
14281
+ </ui-range-number>
14282
+
14283
+ <ui-range-number
14284
+ label="Translate Z"
14285
+ prefix="Z"
14286
+ hint="px"
14287
+ helpText="Move the widget on the z-axis for 3D transforms."
14288
+ [min]="-200"
14289
+ [max]="200"
14290
+ [step]="1"
14291
+ [value]="numberValue('transformZ', 0)"
14292
+ (valueChange)="updateTransform('transformZ', $event)">
14293
+ </ui-range-number>
14294
+
14295
+ <ui-input
14296
+ label="Rotate"
14297
+ hint="deg"
14298
+ helpText="Rotate the widget in degrees."
14299
+ type="number"
14300
+ [step]="1"
14301
+ [model]="numberValue('rotate', 0)"
14302
+ (modelChange)="updateTransform('rotate', $event)">
14303
+ </ui-input>
14304
+
14305
+ <ui-input
14306
+ label="Scale"
14307
+ helpText="Scale the widget uniformly."
14308
+ type="number"
14309
+ [min]="0"
14310
+ [step]="0.1"
14311
+ [model]="numberValue('scale', 1)"
14312
+ (modelChange)="updateTransform('scale', $event)">
14313
+ </ui-input>
14314
+ </div>
14315
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UiInputComponent, selector: "ui-input", inputs: ["label", "hint", "helpText", "placeholder", "type", "min", "max", "step", "model"], outputs: ["modelChange", "onBlur"] }, { kind: "component", type: UiRangeNumberComponent, selector: "ui-range-number", inputs: ["label", "hint", "helpText", "prefix", "min", "max", "step", "value"], outputs: ["valueChange"] }] });
14316
+ }
14317
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: InspectorTransformSectionComponent, decorators: [{
14318
+ type: Component,
14319
+ args: [{
14320
+ selector: 'inspector-transform-section',
14321
+ standalone: true,
14322
+ imports: [CommonModule, UiInputComponent, UiRangeNumberComponent],
14323
+ template: `
14324
+ <div class="flex flex-col gap-3">
14325
+ <ui-range-number
14326
+ label="Translate X"
14327
+ prefix="X"
14328
+ hint="px"
14329
+ helpText="Move the widget horizontally."
14330
+ [min]="-200"
14331
+ [max]="200"
14332
+ [step]="1"
14333
+ [value]="numberValue('transformX', 0)"
14334
+ (valueChange)="updateTransform('transformX', $event)">
14335
+ </ui-range-number>
14336
+
14337
+ <ui-range-number
14338
+ label="Translate Y"
14339
+ prefix="Y"
14340
+ hint="px"
14341
+ helpText="Move the widget vertically."
14342
+ [min]="-200"
14343
+ [max]="200"
14344
+ [step]="1"
14345
+ [value]="numberValue('transformY', 0)"
14346
+ (valueChange)="updateTransform('transformY', $event)">
14347
+ </ui-range-number>
14348
+
14349
+ <ui-range-number
14350
+ label="Translate Z"
14351
+ prefix="Z"
14352
+ hint="px"
14353
+ helpText="Move the widget on the z-axis for 3D transforms."
14354
+ [min]="-200"
14355
+ [max]="200"
14356
+ [step]="1"
14357
+ [value]="numberValue('transformZ', 0)"
14358
+ (valueChange)="updateTransform('transformZ', $event)">
14359
+ </ui-range-number>
14360
+
14361
+ <ui-input
14362
+ label="Rotate"
14363
+ hint="deg"
14364
+ helpText="Rotate the widget in degrees."
14365
+ type="number"
14366
+ [step]="1"
14367
+ [model]="numberValue('rotate', 0)"
14368
+ (modelChange)="updateTransform('rotate', $event)">
14369
+ </ui-input>
14370
+
14371
+ <ui-input
14372
+ label="Scale"
14373
+ helpText="Scale the widget uniformly."
14374
+ type="number"
14375
+ [min]="0"
14376
+ [step]="0.1"
14377
+ [model]="numberValue('scale', 1)"
14378
+ (modelChange)="updateTransform('scale', $event)">
14379
+ </ui-input>
14380
+ </div>
14381
+ `
14382
+ }]
14383
+ }] });
14384
+
13619
14385
  const DEDICATED_STYLE_KEYS = new Set([
13620
14386
  'alignItems',
13621
14387
  'alignSelf',
@@ -13684,7 +14450,13 @@ const DEDICATED_STYLE_KEYS = new Set([
13684
14450
  'right',
13685
14451
  'textAlign',
13686
14452
  'textDecoration',
14453
+ 'transform',
14454
+ 'transformX',
14455
+ 'transformY',
14456
+ 'transformZ',
13687
14457
  'top',
14458
+ 'rotate',
14459
+ 'scale',
13688
14460
  'width',
13689
14461
  'zIndex'
13690
14462
  ]);
@@ -14668,6 +15440,8 @@ class DataPanelComponent {
14668
15440
  selectionFieldId;
14669
15441
  selectionMatchPath;
14670
15442
  childRowsPath;
15443
+ formatNumericOptionLabels = false;
15444
+ optionLabelPrefixPath;
14671
15445
  rootPathOptions = [];
14672
15446
  rowPathOptions = [];
14673
15447
  // Value/Image Config
@@ -14908,6 +15682,8 @@ class DataPanelComponent {
14908
15682
  this.selectionFieldId = undefined;
14909
15683
  this.selectionMatchPath = undefined;
14910
15684
  this.childRowsPath = undefined;
15685
+ this.formatNumericOptionLabels = false;
15686
+ this.optionLabelPrefixPath = undefined;
14911
15687
  this.rootPathOptions = [];
14912
15688
  this.rowPathOptions = [];
14913
15689
  }
@@ -14941,13 +15717,15 @@ class DataPanelComponent {
14941
15717
  this.staticOptions = (d.staticOptions || []).map(option => ({ ...option }));
14942
15718
  this.staticValue = d.staticValue !== undefined ? d.staticValue : this.readScalarTargetValue();
14943
15719
  this.selectedSourceId = d.datasourceId;
14944
- this.labelKey = d.labelPath ?? d.labelKey;
14945
- this.valueKey = d.valuePath ?? d.valueKey;
15720
+ this.labelKey = d.labelKey;
15721
+ this.valueKey = d.valueKey;
14946
15722
  this.rowsPath = d.rowsPath;
14947
15723
  this.rowSelectionMode = d.rowSelectionMode ?? 'first';
14948
15724
  this.selectionFieldId = d.selectionFieldId;
14949
15725
  this.selectionMatchPath = d.selectionMatchPath;
14950
15726
  this.childRowsPath = d.childRowsPath;
15727
+ this.formatNumericOptionLabels = d.formatNumericOptionLabels === true;
15728
+ this.optionLabelPrefixPath = d.optionLabelPrefixPath;
14951
15729
  // Search
14952
15730
  this.searchEnabled = !!d.searchEnabled;
14953
15731
  this.optionsLimit = d.optionsLimit;
@@ -14999,8 +15777,12 @@ class DataPanelComponent {
14999
15777
  labelKey: this.sourceType === 'source' ? this.labelKey : undefined,
15000
15778
  valueKey: this.sourceType === 'source' ? this.valueKey : undefined,
15001
15779
  rowsPath: this.sourceType === 'source' ? this.normalizedRowsPath() : undefined,
15002
- labelPath: this.sourceType === 'source' && this.shouldPersistStructuredLabelPath() ? this.labelKey : undefined,
15003
- valuePath: this.sourceType === 'source' && this.shouldPersistStructuredValuePath() ? this.valueKey : undefined,
15780
+ formatNumericOptionLabels: this.shouldPersistOptionLabelFormatting()
15781
+ ? this.formatNumericOptionLabels
15782
+ : undefined,
15783
+ optionLabelPrefixPath: this.shouldPersistOptionLabelFormatting()
15784
+ ? this.optionLabelPrefixPath
15785
+ : undefined,
15004
15786
  rowSelectionMode: this.sourceType === 'source' && this.bindingShape === 'scalar' && this.rowSelectionMode === 'selected'
15005
15787
  ? 'selected'
15006
15788
  : this.sourceType === 'source' && this.bindingShape === 'list' && this.rowSelectionMode === 'selected'
@@ -15109,6 +15891,9 @@ class DataPanelComponent {
15109
15891
  showOptionMappingControls() {
15110
15892
  return this.sourceType === 'source' && this.widgetType !== 'table' && this.usesOptionMapping();
15111
15893
  }
15894
+ showOptionLabelFormattingControls() {
15895
+ return this.widgetType === 'select' && this.usesOptionMapping();
15896
+ }
15112
15897
  showStaticOptionsEditor() {
15113
15898
  return this.sourceType === 'static' && this.widgetType !== 'table' && this.usesOptionMapping();
15114
15899
  }
@@ -15159,17 +15944,8 @@ class DataPanelComponent {
15159
15944
  const sample = this.extractPreviewRows(this.previewRows, this.effectiveRowsPath())[0];
15160
15945
  return sample ? collectArrayPaths(sample) : [];
15161
15946
  }
15162
- shouldPersistStructuredLabelPath() {
15163
- if (!this.usesOptionMapping() || !this.labelKey)
15164
- return false;
15165
- return !!this.normalizedRowsPath() || !!this.childRowsPath || hasPathSyntax$1(this.labelKey);
15166
- }
15167
- shouldPersistStructuredValuePath() {
15168
- if (!this.valueKey)
15169
- return false;
15170
- if (!this.usesOptionMapping())
15171
- return true;
15172
- return !!this.normalizedRowsPath() || !!this.childRowsPath || hasPathSyntax$1(this.valueKey);
15947
+ shouldPersistOptionLabelFormatting() {
15948
+ return this.widgetType === 'select' && this.usesOptionMapping();
15173
15949
  }
15174
15950
  usesOptionMapping() {
15175
15951
  return this.bindingShape === 'list' || this.widgetType === 'search';
@@ -15490,6 +16266,38 @@ class DataPanelComponent {
15490
16266
  </div>
15491
16267
  </div>
15492
16268
 
16269
+ <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showOptionLabelFormattingControls()">
16270
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Amount Display</div>
16271
+ <div class="mt-1 text-[11px] text-gray-500">Format the visible dropdown label without changing the stored option value.</div>
16272
+
16273
+ <label class="mt-3 flex items-center gap-2 text-sm text-gray-700">
16274
+ <input
16275
+ type="checkbox"
16276
+ [checked]="formatNumericOptionLabels"
16277
+ (change)="formatNumericOptionLabels = $any($event.target).checked; emitChange()"
16278
+ class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
16279
+ <span>Show thousand separators for numeric labels</span>
16280
+ </label>
16281
+
16282
+ <div class="mt-3 flex flex-col gap-1">
16283
+ <label class="text-xs font-medium text-gray-500">Prefix Key</label>
16284
+ <input
16285
+ [attr.list]="'prefix-cols-' + config.id"
16286
+ [(ngModel)]="optionLabelPrefixPath"
16287
+ (ngModelChange)="emitChange()"
16288
+ [placeholder]="effectiveRowsPath() ? 'e.g. currency or meta.currency' : 'e.g. currency'"
16289
+ class="h-8 w-full rounded-md border border-gray-300 px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
16290
+ <datalist [id]="'prefix-cols-' + config.id">
16291
+ <option *ngFor="let col of sourceColumns" [value]="col.name"></option>
16292
+ <option *ngFor="let path of availableRowPaths()" [value]="path"></option>
16293
+ <option *ngFor="let path of availableRootPaths()" [value]="path"></option>
16294
+ </datalist>
16295
+ <p class="text-[10px] text-gray-400">
16296
+ Reads a display-only prefix from the datasource or event payload. The select still stores only the mapped value.
16297
+ </p>
16298
+ </div>
16299
+ </div>
16300
+
15493
16301
  <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showScalarMappingControls()">
15494
16302
  <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Value Mapping</div>
15495
16303
  <div class="mt-1 text-[11px] text-gray-500">Choose the value path and how this field picks a row from the datasource.</div>
@@ -16077,6 +16885,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
16077
16885
  </div>
16078
16886
  </div>
16079
16887
 
16888
+ <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showOptionLabelFormattingControls()">
16889
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Amount Display</div>
16890
+ <div class="mt-1 text-[11px] text-gray-500">Format the visible dropdown label without changing the stored option value.</div>
16891
+
16892
+ <label class="mt-3 flex items-center gap-2 text-sm text-gray-700">
16893
+ <input
16894
+ type="checkbox"
16895
+ [checked]="formatNumericOptionLabels"
16896
+ (change)="formatNumericOptionLabels = $any($event.target).checked; emitChange()"
16897
+ class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
16898
+ <span>Show thousand separators for numeric labels</span>
16899
+ </label>
16900
+
16901
+ <div class="mt-3 flex flex-col gap-1">
16902
+ <label class="text-xs font-medium text-gray-500">Prefix Key</label>
16903
+ <input
16904
+ [attr.list]="'prefix-cols-' + config.id"
16905
+ [(ngModel)]="optionLabelPrefixPath"
16906
+ (ngModelChange)="emitChange()"
16907
+ [placeholder]="effectiveRowsPath() ? 'e.g. currency or meta.currency' : 'e.g. currency'"
16908
+ class="h-8 w-full rounded-md border border-gray-300 px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
16909
+ <datalist [id]="'prefix-cols-' + config.id">
16910
+ <option *ngFor="let col of sourceColumns" [value]="col.name"></option>
16911
+ <option *ngFor="let path of availableRowPaths()" [value]="path"></option>
16912
+ <option *ngFor="let path of availableRootPaths()" [value]="path"></option>
16913
+ </datalist>
16914
+ <p class="text-[10px] text-gray-400">
16915
+ Reads a display-only prefix from the datasource or event payload. The select still stores only the mapped value.
16916
+ </p>
16917
+ </div>
16918
+ </div>
16919
+
16080
16920
  <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showScalarMappingControls()">
16081
16921
  <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Value Mapping</div>
16082
16922
  <div class="mt-1 text-[11px] text-gray-500">Choose the value path and how this field picks a row from the datasource.</div>
@@ -17334,6 +18174,15 @@ class WidgetInspectorComponent {
17334
18174
  </div>
17335
18175
  </ui-accordion>
17336
18176
 
18177
+ <ui-accordion title="Transform" [expanded]="false">
18178
+ <div [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
18179
+ <inspector-transform-section
18180
+ [style]="currentStyle()"
18181
+ (styleChange)="onStyleChange($event)">
18182
+ </inspector-transform-section>
18183
+ </div>
18184
+ </ui-accordion>
18185
+
17337
18186
  <ui-accordion title="Position" [expanded]="false">
17338
18187
  <div [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
17339
18188
  <inspector-position-section
@@ -17366,7 +18215,7 @@ class WidgetInspectorComponent {
17366
18215
  [config]="inspectorField()"
17367
18216
  [allFields]="stateService.getSelectedScopeFields()"
17368
18217
  [readOnly]="readOnly()"
17369
- [excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position', 'Effects', 'Advanced']"
18218
+ [excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position', 'Effects', 'Transform', 'Advanced']"
17370
18219
  (configChange)="onFieldConfigChange($event)">
17371
18220
  </app-dynamic-properties>
17372
18221
  </div>
@@ -17406,7 +18255,7 @@ class WidgetInspectorComponent {
17406
18255
  </div>
17407
18256
  `, isInline: true, styles: [":host{display:block;height:100%}.custom-scrollbar::-webkit-scrollbar{width:8px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:4px}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background:#94a3b8}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: UiIconModule }, { kind: "component", type: i3$1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "component", type: UiAccordionComponent, selector: "ui-accordion", inputs: ["title", "subtitle", "expanded", "showAdd"] }, { kind: "component", type:
17408
18257
  // Style Sections
17409
- InspectorSpacingSectionComponent, selector: "inspector-spacing-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorSizeSectionComponent, selector: "inspector-size-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorTypographySectionComponent, selector: "inspector-typography-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBordersSectionComponent, selector: "inspector-borders-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorLayoutSectionComponent, selector: "inspector-layout-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBackgroundsSectionComponent, selector: "inspector-backgrounds-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorEffectsSectionComponent, selector: "inspector-effects-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorPositionSectionComponent, selector: "inspector-position-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorAdvancedSectionComponent, selector: "inspector-advanced-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type:
18258
+ InspectorSpacingSectionComponent, selector: "inspector-spacing-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorSizeSectionComponent, selector: "inspector-size-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorTypographySectionComponent, selector: "inspector-typography-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBordersSectionComponent, selector: "inspector-borders-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorLayoutSectionComponent, selector: "inspector-layout-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBackgroundsSectionComponent, selector: "inspector-backgrounds-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorEffectsSectionComponent, selector: "inspector-effects-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorPositionSectionComponent, selector: "inspector-position-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorTransformSectionComponent, selector: "inspector-transform-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorAdvancedSectionComponent, selector: "inspector-advanced-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type:
17410
18259
  // Functional Panels (All restored)
17411
18260
  DynamicPropertiesComponent, selector: "app-dynamic-properties", inputs: ["onPropertyChange", "config", "readOnly", "includeSections", "excludeSections", "allFields"], outputs: ["configChange"] }, { kind: "component", type: DataPanelComponent, selector: "app-data-panel", inputs: ["config", "readOnly", "dataConsumer", "bindingShape", "dataTargetPath", "widgetType", "allFields"], outputs: ["configChange"] }, { kind: "component", type: RulesPanelComponent, selector: "app-rules-panel", inputs: ["readOnly", "rules", "allFields"], outputs: ["rulesChange"] }] });
17412
18261
  }
@@ -17425,6 +18274,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17425
18274
  InspectorBackgroundsSectionComponent,
17426
18275
  InspectorEffectsSectionComponent,
17427
18276
  InspectorPositionSectionComponent,
18277
+ InspectorTransformSectionComponent,
17428
18278
  InspectorAdvancedSectionComponent,
17429
18279
  // Functional Panels (All restored)
17430
18280
  DynamicPropertiesComponent,
@@ -17541,6 +18391,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17541
18391
  </div>
17542
18392
  </ui-accordion>
17543
18393
 
18394
+ <ui-accordion title="Transform" [expanded]="false">
18395
+ <div [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
18396
+ <inspector-transform-section
18397
+ [style]="currentStyle()"
18398
+ (styleChange)="onStyleChange($event)">
18399
+ </inspector-transform-section>
18400
+ </div>
18401
+ </ui-accordion>
18402
+
17544
18403
  <ui-accordion title="Position" [expanded]="false">
17545
18404
  <div [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
17546
18405
  <inspector-position-section
@@ -17573,7 +18432,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17573
18432
  [config]="inspectorField()"
17574
18433
  [allFields]="stateService.getSelectedScopeFields()"
17575
18434
  [readOnly]="readOnly()"
17576
- [excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position', 'Effects', 'Advanced']"
18435
+ [excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position', 'Effects', 'Transform', 'Advanced']"
17577
18436
  (configChange)="onFieldConfigChange($event)">
17578
18437
  </app-dynamic-properties>
17579
18438
  </div>
@@ -17683,191 +18542,125 @@ class FormSettingsInspectorComponent {
17683
18542
  </div>
17684
18543
 
17685
18544
  <div class="h-px bg-border-default my-1"></div>
18545
+ </div>
18546
+ }
18547
+
18548
+ <!-- ===================== STYLE TAB ===================== -->
18549
+ @if (activeTab() === 'Style') {
18550
+ <div class="flex flex-col" [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
18551
+
18552
+ <div class="px-4 py-3 text-[12px] text-text-primary opacity-80 bg-slate-50 border-b border-border-default">
18553
+ Global styles applied to the form container.
18554
+ </div>
18555
+
18556
+ <ui-accordion title="Layout" [expanded]="true">
18557
+ <inspector-layout-section
18558
+ [style]="currentStyle()"
18559
+ (styleChange)="onStyleChange($event)">
18560
+ </inspector-layout-section>
18561
+ </ui-accordion>
17686
18562
 
18563
+ <ui-accordion title="Backgrounds" [expanded]="false">
18564
+ <inspector-backgrounds-section
18565
+ [style]="currentStyle()"
18566
+ (styleChange)="onStyleChange($event)">
18567
+ </inspector-backgrounds-section>
18568
+ </ui-accordion>
18569
+
18570
+ <ui-accordion title="Borders" [expanded]="false">
18571
+ <inspector-borders-section
18572
+ [style]="currentStyle()"
18573
+ (styleChange)="onStyleChange($event)">
18574
+ </inspector-borders-section>
18575
+ </ui-accordion>
18576
+
18577
+ <ui-accordion title="Effects" [expanded]="false">
18578
+ <inspector-effects-section
18579
+ [style]="currentStyle()"
18580
+ (styleChange)="onStyleChange($event)">
18581
+ </inspector-effects-section>
18582
+ </ui-accordion>
18583
+
18584
+ <ui-accordion title="Advanced" [expanded]="false">
18585
+ <inspector-advanced-section
18586
+ [style]="currentStyle()"
18587
+ (styleChange)="onStyleChange($event)">
18588
+ </inspector-advanced-section>
18589
+ </ui-accordion>
18590
+
18591
+ <!-- Bottom spacer -->
18592
+ <div class="h-10"></div>
18593
+ </div>
18594
+ }
18595
+
18596
+ </div>
18597
+ </div>
18598
+ `, isInline: true, styles: [":host{display:block;height:100%}.custom-scrollbar::-webkit-scrollbar{width:8px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:4px}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background:#94a3b8}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: UiIconModule }, { kind: "component", type: i3$1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "component", type: UiAccordionComponent, selector: "ui-accordion", inputs: ["title", "subtitle", "expanded", "showAdd"] }, { kind: "component", type: InspectorLayoutSectionComponent, selector: "inspector-layout-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBackgroundsSectionComponent, selector: "inspector-backgrounds-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBordersSectionComponent, selector: "inspector-borders-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorEffectsSectionComponent, selector: "inspector-effects-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorAdvancedSectionComponent, selector: "inspector-advanced-section", inputs: ["style"], outputs: ["styleChange"] }] });
18599
+ }
18600
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FormSettingsInspectorComponent, decorators: [{
18601
+ type: Component,
18602
+ args: [{ selector: 'form-settings-inspector', standalone: true, imports: [
18603
+ CommonModule,
18604
+ FormsModule,
18605
+ UiIconModule,
18606
+ UiAccordionComponent,
18607
+ InspectorLayoutSectionComponent,
18608
+ InspectorBackgroundsSectionComponent,
18609
+ InspectorBordersSectionComponent,
18610
+ InspectorEffectsSectionComponent,
18611
+ InspectorAdvancedSectionComponent,
18612
+ ], template: `
18613
+ <div class="h-full flex flex-col bg-surface-default font-sans text-[12px] text-text-primary">
18614
+
18615
+ <!-- Header -->
18616
+ <div class="flex items-center justify-between px-3 py-2 border-b border-border-default bg-slate-50">
18617
+ <div class="flex items-center gap-2 font-medium text-[12px] text-text-strong">
18618
+ <lucide-icon name="settings" class="w-4 h-4 text-text-primary opacity-60"></lucide-icon>
18619
+ <span>Form Settings</span>
18620
+ </div>
18621
+ </div>
18622
+
18623
+ <!-- Tabs -->
18624
+ <div class="flex items-center px-2 pt-2 border-b border-border-default bg-surface-default">
18625
+ @for (tab of tabs; track tab) {
18626
+ <button
18627
+ type="button"
18628
+ (click)="activeTab.set(tab)"
18629
+ [class]="activeTab() === tab
18630
+ ? 'px-2 pb-2 font-semibold text-primary-500 border-b-2 border-b-primary-500'
18631
+ : 'px-2 pb-2 text-text-primary opacity-60 hover:opacity-100 hover:text-text-primary border-b-2 border-transparent'">
18632
+ {{ tab }}
18633
+ </button>
18634
+ }
18635
+ </div>
18636
+
18637
+ <!-- Scrollable Content -->
18638
+ <div class="flex-1 overflow-y-auto custom-scrollbar bg-surface-default">
18639
+
18640
+ <!-- ===================== SETTINGS TAB ===================== -->
18641
+ @if (activeTab() === 'Settings') {
18642
+ <div class="p-4 flex flex-col gap-4">
18643
+
17687
18644
  <div class="flex flex-col gap-1">
17688
- <label class="text-[10px] text-text-primary opacity-70 uppercase font-semibold">Submit Button</label>
18645
+ <label class="text-[10px] text-text-primary opacity-70 uppercase font-semibold">Form Title</label>
17689
18646
  <input
17690
- [ngModel]="schema().submitButtonText"
18647
+ [ngModel]="schema().title"
17691
18648
  [disabled]="readOnly()"
17692
- (ngModelChange)="updateSettings('submitButtonText', $event)"
17693
- placeholder="Submit"
18649
+ (ngModelChange)="updateSettings('title', $event)"
17694
18650
  class="h-8 w-full rounded border border-border-default px-2 text-[12px] focus:border-focus-border focus:ring-1 focus:ring-focus-border outline-none bg-surface-default">
17695
18651
  </div>
17696
18652
 
17697
18653
  <div class="flex flex-col gap-1">
17698
- <label class="text-[10px] text-text-primary opacity-70 uppercase font-semibold">Reset Button</label>
17699
- <div class="flex items-center gap-2 mb-1">
17700
- <input
17701
- type="checkbox"
17702
- [ngModel]="schema().showResetButton !== false"
17703
- [disabled]="readOnly()"
17704
- (ngModelChange)="updateSettings('showResetButton', $event)"
17705
- id="showReset"
17706
- class="h-3.5 w-3.5 rounded border-border-default text-primary-500 focus:ring-primary-500">
17707
- <label for="showReset" class="text-[12px] text-text-primary">Show Reset Button</label>
17708
- </div>
17709
-
17710
- @if (schema().showResetButton !== false) {
17711
- <input
17712
- [ngModel]="schema().resetButtonText"
17713
- [disabled]="readOnly()"
17714
- (ngModelChange)="updateSettings('resetButtonText', $event)"
17715
- placeholder="Reset"
17716
- class="h-8 w-full rounded border border-border-default px-2 text-[12px] focus:border-focus-border focus:ring-1 focus:ring-focus-border outline-none bg-surface-default">
17717
- }
17718
- </div>
17719
- </div>
17720
- }
17721
-
17722
- <!-- ===================== STYLE TAB ===================== -->
17723
- @if (activeTab() === 'Style') {
17724
- <div class="flex flex-col" [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
17725
-
17726
- <div class="px-4 py-3 text-[12px] text-text-primary opacity-80 bg-slate-50 border-b border-border-default">
17727
- Global styles applied to the form container.
17728
- </div>
17729
-
17730
- <ui-accordion title="Layout" [expanded]="true">
17731
- <inspector-layout-section
17732
- [style]="currentStyle()"
17733
- (styleChange)="onStyleChange($event)">
17734
- </inspector-layout-section>
17735
- </ui-accordion>
17736
-
17737
- <ui-accordion title="Backgrounds" [expanded]="false">
17738
- <inspector-backgrounds-section
17739
- [style]="currentStyle()"
17740
- (styleChange)="onStyleChange($event)">
17741
- </inspector-backgrounds-section>
17742
- </ui-accordion>
17743
-
17744
- <ui-accordion title="Borders" [expanded]="false">
17745
- <inspector-borders-section
17746
- [style]="currentStyle()"
17747
- (styleChange)="onStyleChange($event)">
17748
- </inspector-borders-section>
17749
- </ui-accordion>
17750
-
17751
- <ui-accordion title="Effects" [expanded]="false">
17752
- <inspector-effects-section
17753
- [style]="currentStyle()"
17754
- (styleChange)="onStyleChange($event)">
17755
- </inspector-effects-section>
17756
- </ui-accordion>
17757
-
17758
- <ui-accordion title="Advanced" [expanded]="false">
17759
- <inspector-advanced-section
17760
- [style]="currentStyle()"
17761
- (styleChange)="onStyleChange($event)">
17762
- </inspector-advanced-section>
17763
- </ui-accordion>
17764
-
17765
- <!-- Bottom spacer -->
17766
- <div class="h-10"></div>
17767
- </div>
17768
- }
17769
-
17770
- </div>
17771
- </div>
17772
- `, isInline: true, styles: [":host{display:block;height:100%}.custom-scrollbar::-webkit-scrollbar{width:8px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:4px}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background:#94a3b8}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: UiIconModule }, { kind: "component", type: i3$1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "component", type: UiAccordionComponent, selector: "ui-accordion", inputs: ["title", "subtitle", "expanded", "showAdd"] }, { kind: "component", type: InspectorLayoutSectionComponent, selector: "inspector-layout-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBackgroundsSectionComponent, selector: "inspector-backgrounds-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBordersSectionComponent, selector: "inspector-borders-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorEffectsSectionComponent, selector: "inspector-effects-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorAdvancedSectionComponent, selector: "inspector-advanced-section", inputs: ["style"], outputs: ["styleChange"] }] });
17773
- }
17774
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FormSettingsInspectorComponent, decorators: [{
17775
- type: Component,
17776
- args: [{ selector: 'form-settings-inspector', standalone: true, imports: [
17777
- CommonModule,
17778
- FormsModule,
17779
- UiIconModule,
17780
- UiAccordionComponent,
17781
- InspectorLayoutSectionComponent,
17782
- InspectorBackgroundsSectionComponent,
17783
- InspectorBordersSectionComponent,
17784
- InspectorEffectsSectionComponent,
17785
- InspectorAdvancedSectionComponent,
17786
- ], template: `
17787
- <div class="h-full flex flex-col bg-surface-default font-sans text-[12px] text-text-primary">
17788
-
17789
- <!-- Header -->
17790
- <div class="flex items-center justify-between px-3 py-2 border-b border-border-default bg-slate-50">
17791
- <div class="flex items-center gap-2 font-medium text-[12px] text-text-strong">
17792
- <lucide-icon name="settings" class="w-4 h-4 text-text-primary opacity-60"></lucide-icon>
17793
- <span>Form Settings</span>
17794
- </div>
17795
- </div>
17796
-
17797
- <!-- Tabs -->
17798
- <div class="flex items-center px-2 pt-2 border-b border-border-default bg-surface-default">
17799
- @for (tab of tabs; track tab) {
17800
- <button
17801
- type="button"
17802
- (click)="activeTab.set(tab)"
17803
- [class]="activeTab() === tab
17804
- ? 'px-2 pb-2 font-semibold text-primary-500 border-b-2 border-b-primary-500'
17805
- : 'px-2 pb-2 text-text-primary opacity-60 hover:opacity-100 hover:text-text-primary border-b-2 border-transparent'">
17806
- {{ tab }}
17807
- </button>
17808
- }
17809
- </div>
17810
-
17811
- <!-- Scrollable Content -->
17812
- <div class="flex-1 overflow-y-auto custom-scrollbar bg-surface-default">
17813
-
17814
- <!-- ===================== SETTINGS TAB ===================== -->
17815
- @if (activeTab() === 'Settings') {
17816
- <div class="p-4 flex flex-col gap-4">
17817
-
17818
- <div class="flex flex-col gap-1">
17819
- <label class="text-[10px] text-text-primary opacity-70 uppercase font-semibold">Form Title</label>
17820
- <input
17821
- [ngModel]="schema().title"
17822
- [disabled]="readOnly()"
17823
- (ngModelChange)="updateSettings('title', $event)"
17824
- class="h-8 w-full rounded border border-border-default px-2 text-[12px] focus:border-focus-border focus:ring-1 focus:ring-focus-border outline-none bg-surface-default">
17825
- </div>
17826
-
17827
- <div class="flex flex-col gap-1">
17828
- <label class="text-[10px] text-text-primary opacity-70 uppercase font-semibold">Description</label>
17829
- <textarea
17830
- [ngModel]="schema().description"
17831
- [disabled]="readOnly()"
17832
- (ngModelChange)="updateSettings('description', $event)"
17833
- rows="3"
17834
- class="w-full rounded border border-border-default p-2 text-[12px] focus:border-focus-border focus:ring-1 focus:ring-focus-border outline-none bg-surface-default"></textarea>
17835
- </div>
17836
-
17837
- <div class="h-px bg-border-default my-1"></div>
17838
-
17839
- <div class="flex flex-col gap-1">
17840
- <label class="text-[10px] text-text-primary opacity-70 uppercase font-semibold">Submit Button</label>
17841
- <input
17842
- [ngModel]="schema().submitButtonText"
17843
- [disabled]="readOnly()"
17844
- (ngModelChange)="updateSettings('submitButtonText', $event)"
17845
- placeholder="Submit"
17846
- class="h-8 w-full rounded border border-border-default px-2 text-[12px] focus:border-focus-border focus:ring-1 focus:ring-focus-border outline-none bg-surface-default">
17847
- </div>
17848
-
17849
- <div class="flex flex-col gap-1">
17850
- <label class="text-[10px] text-text-primary opacity-70 uppercase font-semibold">Reset Button</label>
17851
- <div class="flex items-center gap-2 mb-1">
17852
- <input
17853
- type="checkbox"
17854
- [ngModel]="schema().showResetButton !== false"
17855
- [disabled]="readOnly()"
17856
- (ngModelChange)="updateSettings('showResetButton', $event)"
17857
- id="showReset"
17858
- class="h-3.5 w-3.5 rounded border-border-default text-primary-500 focus:ring-primary-500">
17859
- <label for="showReset" class="text-[12px] text-text-primary">Show Reset Button</label>
17860
- </div>
17861
-
17862
- @if (schema().showResetButton !== false) {
17863
- <input
17864
- [ngModel]="schema().resetButtonText"
17865
- [disabled]="readOnly()"
17866
- (ngModelChange)="updateSettings('resetButtonText', $event)"
17867
- placeholder="Reset"
17868
- class="h-8 w-full rounded border border-border-default px-2 text-[12px] focus:border-focus-border focus:ring-1 focus:ring-focus-border outline-none bg-surface-default">
17869
- }
18654
+ <label class="text-[10px] text-text-primary opacity-70 uppercase font-semibold">Description</label>
18655
+ <textarea
18656
+ [ngModel]="schema().description"
18657
+ [disabled]="readOnly()"
18658
+ (ngModelChange)="updateSettings('description', $event)"
18659
+ rows="3"
18660
+ class="w-full rounded border border-border-default p-2 text-[12px] focus:border-focus-border focus:ring-1 focus:ring-focus-border outline-none bg-surface-default"></textarea>
17870
18661
  </div>
18662
+
18663
+ <div class="h-px bg-border-default my-1"></div>
17871
18664
  </div>
17872
18665
  }
17873
18666
 
@@ -17924,165 +18717,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17924
18717
  `, styles: [":host{display:block;height:100%}.custom-scrollbar::-webkit-scrollbar{width:8px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:4px}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background:#94a3b8}\n"] }]
17925
18718
  }] });
17926
18719
 
17927
- class UiTabComponent {
17928
- label = '';
17929
- name = ''; // Unique key for controlled mode
17930
- disabled = false;
17931
- badge;
17932
- badgeTone = 'neutral';
17933
- template;
17934
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
17935
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: UiTabComponent, isStandalone: true, selector: "ui-tab", inputs: { label: "label", name: "name", disabled: "disabled", badge: "badge", badgeTone: "badgeTone" }, viewQueries: [{ propertyName: "template", first: true, predicate: ["tpl"], descendants: true, static: true }], ngImport: i0, template: `<ng-template #tpl><ng-content></ng-content></ng-template>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
17936
- }
17937
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabComponent, decorators: [{
17938
- type: Component,
17939
- args: [{
17940
- selector: 'ui-tab',
17941
- standalone: true,
17942
- imports: [CommonModule],
17943
- template: `<ng-template #tpl><ng-content></ng-content></ng-template>`
17944
- }]
17945
- }], propDecorators: { label: [{
17946
- type: Input
17947
- }], name: [{
17948
- type: Input
17949
- }], disabled: [{
17950
- type: Input
17951
- }], badge: [{
17952
- type: Input
17953
- }], badgeTone: [{
17954
- type: Input
17955
- }], template: [{
17956
- type: ViewChild,
17957
- args: ['tpl', { static: true }]
17958
- }] } });
17959
- class UiTabsComponent {
17960
- tabQuery;
17961
- activeTab;
17962
- activeTabChange = new EventEmitter();
17963
- // Internal state if uncontrolled
17964
- _internalIndex = 0;
17965
- tabs = [];
17966
- ngAfterContentInit() {
17967
- this.tabs = this.tabQuery.toArray();
17968
- }
17969
- isActive(tab) {
17970
- if (this.activeTab !== undefined) {
17971
- return this.activeTab === (tab.name || tab.label);
17972
- }
17973
- return this.tabs.indexOf(tab) === this._internalIndex;
17974
- }
17975
- activate(tab) {
17976
- if (tab.disabled)
17977
- return;
17978
- const key = tab.name || tab.label;
17979
- if (this.activeTab !== undefined) {
17980
- this.activeTabChange.emit(key);
17981
- }
17982
- else {
17983
- this._internalIndex = this.tabs.indexOf(tab);
17984
- }
17985
- }
17986
- get activeTemplate() {
17987
- if (this.tabs.length === 0)
17988
- return null;
17989
- let activeTab;
17990
- if (this.activeTab !== undefined) {
17991
- activeTab = this.tabs.find(t => (t.name || t.label) === this.activeTab);
17992
- }
17993
- else {
17994
- activeTab = this.tabs[this._internalIndex];
17995
- }
17996
- return activeTab?.template || null;
17997
- }
17998
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
17999
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: UiTabsComponent, isStandalone: true, selector: "ui-tabs", inputs: { activeTab: "activeTab" }, outputs: { activeTabChange: "activeTabChange" }, queries: [{ propertyName: "tabQuery", predicate: UiTabComponent }], ngImport: i0, template: `
18000
- <div class="flex flex-col min-h-0 h-full">
18001
- <div class="flex items-center gap-1 px-3 border-b border-slate-200 bg-white shrink-0">
18002
- <button
18003
- *ngFor="let tab of tabs; let i = index"
18004
- (click)="activate(tab)"
18005
- class="relative h-10 px-3 text-xs font-semibold transition-colors rounded-t-md border-b-2"
18006
- [class.text-accent-600]="isActive(tab)"
18007
- [class.border-accent-600]="isActive(tab)"
18008
- [class.text-ink-500]="!isActive(tab)"
18009
- [class.border-transparent]="!isActive(tab)"
18010
- [class.hover:text-ink-700]="!isActive(tab)"
18011
- [disabled]="tab.disabled"
18012
- >
18013
- <span class="flex items-center gap-2">
18014
- {{ tab.label }}
18015
- <span *ngIf="tab.badge" class="text-[10px] px-1.5 py-0.5 rounded-full border"
18016
- [class.bg-accent-50]="tab.badgeTone === 'accent'"
18017
- [class.border-accent-200]="tab.badgeTone === 'accent'"
18018
- [class.text-accent-700]="tab.badgeTone === 'accent'"
18019
- [class.bg-slate-100]="tab.badgeTone === 'neutral'"
18020
- [class.border-slate-200]="tab.badgeTone === 'neutral'"
18021
- [class.text-slate-600]="tab.badgeTone === 'neutral'">
18022
- {{ tab.badge }}
18023
- </span>
18024
- </span>
18025
- </button>
18026
- </div>
18027
-
18028
- <div class="flex-1 min-h-0 overflow-hidden bg-slate-50/30">
18029
- <ng-container *ngIf="activeTemplate">
18030
- <ng-container *ngTemplateOutlet="activeTemplate"></ng-container>
18031
- </ng-container>
18032
- </div>
18033
- </div>
18034
- `, isInline: true, styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
18035
- }
18036
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabsComponent, decorators: [{
18037
- type: Component,
18038
- args: [{ selector: 'ui-tabs', standalone: true, imports: [CommonModule], template: `
18039
- <div class="flex flex-col min-h-0 h-full">
18040
- <div class="flex items-center gap-1 px-3 border-b border-slate-200 bg-white shrink-0">
18041
- <button
18042
- *ngFor="let tab of tabs; let i = index"
18043
- (click)="activate(tab)"
18044
- class="relative h-10 px-3 text-xs font-semibold transition-colors rounded-t-md border-b-2"
18045
- [class.text-accent-600]="isActive(tab)"
18046
- [class.border-accent-600]="isActive(tab)"
18047
- [class.text-ink-500]="!isActive(tab)"
18048
- [class.border-transparent]="!isActive(tab)"
18049
- [class.hover:text-ink-700]="!isActive(tab)"
18050
- [disabled]="tab.disabled"
18051
- >
18052
- <span class="flex items-center gap-2">
18053
- {{ tab.label }}
18054
- <span *ngIf="tab.badge" class="text-[10px] px-1.5 py-0.5 rounded-full border"
18055
- [class.bg-accent-50]="tab.badgeTone === 'accent'"
18056
- [class.border-accent-200]="tab.badgeTone === 'accent'"
18057
- [class.text-accent-700]="tab.badgeTone === 'accent'"
18058
- [class.bg-slate-100]="tab.badgeTone === 'neutral'"
18059
- [class.border-slate-200]="tab.badgeTone === 'neutral'"
18060
- [class.text-slate-600]="tab.badgeTone === 'neutral'">
18061
- {{ tab.badge }}
18062
- </span>
18063
- </span>
18064
- </button>
18065
- </div>
18066
-
18067
- <div class="flex-1 min-h-0 overflow-hidden bg-slate-50/30">
18068
- <ng-container *ngIf="activeTemplate">
18069
- <ng-container *ngTemplateOutlet="activeTemplate"></ng-container>
18070
- </ng-container>
18071
- </div>
18072
- </div>
18073
- `, styles: [":host{display:block;height:100%}\n"] }]
18074
- }], propDecorators: { tabQuery: [{
18075
- type: ContentChildren,
18076
- args: [UiTabComponent]
18077
- }], activeTab: [{
18078
- type: Input
18079
- }], activeTabChange: [{
18080
- type: Output
18081
- }] } });
18082
-
18083
18720
  class PropertiesPanelComponent {
18084
18721
  state;
18085
18722
  injector;
18723
+ layoutInspectorTabs = ['Style', 'Settings'];
18724
+ activeLayoutInspectorTab = signal('Style');
18086
18725
  spacingOptions = [
18087
18726
  { label: 'None', value: 'none' },
18088
18727
  { label: 'XS', value: 'xs' },
@@ -18105,9 +18744,11 @@ class PropertiesPanelComponent {
18105
18744
  // No need to initialize form settings manually anymore, component inputs handle it
18106
18745
  // Track the previous widget to avoid unnecessary re-renders
18107
18746
  let previousWidgetId = null;
18747
+ let previousLayoutNodeKey = null;
18108
18748
  effect(() => {
18109
18749
  const node = this.state.selectedNode();
18110
18750
  const currentWidgetId = (node && node.type === 'widget') ? node.id : null;
18751
+ const currentLayoutNodeKey = (node && node.type !== 'widget') ? `${node.type}:${node.id}` : null;
18111
18752
  // Only re-render if the widget actually changed
18112
18753
  if (currentWidgetId !== previousWidgetId) {
18113
18754
  previousWidgetId = currentWidgetId;
@@ -18120,6 +18761,12 @@ class PropertiesPanelComponent {
18120
18761
  this.inspectorContainer.clear();
18121
18762
  }
18122
18763
  }
18764
+ if (currentLayoutNodeKey !== previousLayoutNodeKey) {
18765
+ previousLayoutNodeKey = currentLayoutNodeKey;
18766
+ if (currentLayoutNodeKey) {
18767
+ this.activeLayoutInspectorTab.set('Style');
18768
+ }
18769
+ }
18123
18770
  });
18124
18771
  }
18125
18772
  responsiveWidthOptions(includeInherit) {
@@ -18359,7 +19006,7 @@ class PropertiesPanelComponent {
18359
19006
  this.state.selectNode(null);
18360
19007
  }
18361
19008
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PropertiesPanelComponent, deps: [{ token: DesignerStateService }, { token: WIDGET_DEFINITIONS }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component });
18362
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: PropertiesPanelComponent, isStandalone: true, selector: "app-properties-panel", viewQueries: [{ propertyName: "inspectorContainer", first: true, predicate: ["inspectorContainer"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: `
19009
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: PropertiesPanelComponent, isStandalone: true, selector: "app-properties-panel", viewQueries: [{ propertyName: "inspectorContainer", first: true, predicate: ["inspectorContainer"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: `
18363
19010
  <div class="properties-shell h-full border-l border-border-default bg-surface-default flex flex-col font-sans text-[12px]">
18364
19011
 
18365
19012
  <div *ngIf="state.selectedNode() as node; else noSelection" class="flex flex-col h-full">
@@ -18386,239 +19033,182 @@ class PropertiesPanelComponent {
18386
19033
  </div>
18387
19034
  </div>
18388
19035
 
18389
- <!-- Tabs -->
18390
- <ui-tabs class="flex-1 flex flex-col min-h-0 bg-surface-default">
18391
- <ui-tab label="Style">
18392
- <div class="panel-body p-4 pt-3 flex flex-col gap-4 overflow-y-auto h-full custom-scrollbar">
18393
-
18394
- <!-- ROW Specific -->
18395
- <!-- Binds to LayoutNode.style (Row Container Styles) -->
18396
- <ng-container *ngIf="node.type === 'row'">
18397
- <div class="inspector-stack space-y-4">
18398
- <div *ngIf="!state.isReadOnly()" class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm">
18399
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Column Structure</div>
18400
-
18401
- <!-- Presets -->
18402
- <div class="grid grid-cols-3 gap-2 mb-3">
18403
- <button (click)="applyPreset(node.id, 1)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="1 Column">
18404
- <div class="flex gap-1 w-full h-4 justify-center px-1">
18405
- <div class="w-full bg-border-default rounded-sm"></div>
18406
- </div>
18407
- </button>
18408
- <button (click)="applyPreset(node.id, 2)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="2 Columns">
18409
- <div class="flex gap-0.5 w-full h-4 px-1">
18410
- <div class="w-1/2 bg-border-default rounded-sm"></div>
18411
- <div class="w-1/2 bg-border-default rounded-sm"></div>
18412
- </div>
18413
- </button>
18414
- <button (click)="applyPreset(node.id, 3)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="3 Columns">
18415
- <div class="flex gap-0.5 w-full h-4 px-1">
18416
- <div class="w-1/3 bg-border-default rounded-sm"></div>
18417
- <div class="w-1/3 bg-border-default rounded-sm"></div>
18418
- <div class="w-1/3 bg-border-default rounded-sm"></div>
18419
- </div>
18420
- </button>
18421
- <button (click)="applyPreset(node.id, 4)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="4 Columns">
18422
- <div class="flex gap-0.5 w-full h-4 px-1">
18423
- <div class="w-1/4 bg-border-default rounded-sm"></div>
18424
- <div class="w-1/4 bg-border-default rounded-sm"></div>
18425
- <div class="w-1/4 bg-border-default rounded-sm"></div>
18426
- <div class="w-1/4 bg-border-default rounded-sm"></div>
18427
- </div>
18428
- </button>
18429
- <button (click)="applyPreset(node.id, 6)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="6 Columns">
18430
- <span class="text-[12px] text-text-primary font-medium">6 Col</span>
18431
- </button>
18432
- <button (click)="applyPreset(node.id, 12)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="12 Columns">
18433
- <span class="text-[12px] text-text-primary font-medium">12 Col</span>
18434
- </button>
18435
- </div>
18436
-
18437
- <!-- Manual Actions -->
18438
- <div class="flex flex-col gap-2">
18439
- <button (click)="addColumn(node.id)" class="w-full h-9 bg-primary-500 text-white rounded-md hover:opacity-90 flex items-center justify-center gap-2 text-[12px] font-medium transition-colors">
18440
- <lucide-icon name="plus" class="w-4 h-4"></lucide-icon> Add Column
18441
- </button>
18442
- </div>
18443
- </div>
18444
-
18445
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18446
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Layout</div>
18447
- <inspector-layout-section
18448
- [style]="nodeStyle(node)"
18449
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18450
- </inspector-layout-section>
18451
- </div>
18452
-
18453
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18454
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Spacing</div>
18455
- <inspector-spacing-section
18456
- [style]="nodeStyle(node)"
18457
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18458
- </inspector-spacing-section>
18459
- </div>
18460
-
18461
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18462
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Size</div>
18463
- <inspector-size-section
18464
- [style]="nodeStyle(node)"
18465
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18466
- </inspector-size-section>
18467
- </div>
18468
-
18469
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18470
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Backgrounds</div>
18471
- <inspector-backgrounds-section
18472
- [style]="nodeStyle(node)"
18473
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18474
- </inspector-backgrounds-section>
18475
- </div>
18476
-
18477
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18478
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Borders</div>
18479
- <inspector-borders-section
18480
- [style]="nodeStyle(node)"
18481
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18482
- </inspector-borders-section>
18483
- </div>
18484
-
18485
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18486
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Effects</div>
18487
- <inspector-effects-section
18488
- [style]="nodeStyle(node)"
18489
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18490
- </inspector-effects-section>
18491
- </div>
18492
-
18493
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18494
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Advanced</div>
18495
- <inspector-advanced-section
18496
- [style]="nodeStyle(node)"
18497
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18498
- </inspector-advanced-section>
18499
- </div>
18500
- </div>
18501
- </ng-container>
18502
- <ng-container *ngIf="node.type === 'col'">
18503
- <div class="inspector-stack space-y-4">
18504
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18505
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Responsive Width (1-12)</div>
18506
-
18507
- <!-- XS (Mobile) -->
18508
- <div class="flex items-center gap-2 mb-2">
18509
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XS</span>
18510
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.xs" (ngModelChange)="onResponsiveChange(node.id, 'xs', $event)">
18511
- <option *ngFor="let option of responsiveWidthOptions(false)" [ngValue]="option.value">{{ option.label }}</option>
18512
- </select>
18513
- </div>
18514
-
18515
- <!-- SM (Large Phones) -->
18516
- <div class="flex items-center gap-2 mb-2">
18517
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">SM</span>
18518
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.sm" (ngModelChange)="onResponsiveChange(node.id, 'sm', $event)">
18519
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18520
- </select>
18521
- </div>
18522
-
18523
- <!-- MD (Tablet) -->
18524
- <div class="flex items-center gap-2 mb-2">
18525
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">MD</span>
18526
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.md" (ngModelChange)="onResponsiveChange(node.id, 'md', $event)">
18527
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18528
- </select>
18529
- </div>
18530
-
18531
- <!-- LG (Desktop) -->
18532
- <div class="flex items-center gap-2 mb-2">
18533
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">LG</span>
18534
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.lg" (ngModelChange)="onResponsiveChange(node.id, 'lg', $event)">
18535
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18536
- </select>
18537
- </div>
18538
-
18539
- <!-- XL (Large Desktop) -->
18540
- <div class="flex items-center gap-2 mb-2">
18541
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XL</span>
18542
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.xl" (ngModelChange)="onResponsiveChange(node.id, 'xl', $event)">
18543
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18544
- </select>
18545
- </div>
18546
-
18547
- <!-- 2XL (Extra Large) -->
18548
- <div class="flex items-center gap-2 mb-2">
18549
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">2XL</span>
18550
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive ? node.responsive['2xl'] : undefined" (ngModelChange)="onResponsiveChange(node.id, '2xl', $event)">
18551
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18552
- </select>
18553
- </div>
18554
- </div>
18555
-
18556
- <div *ngIf="!state.isReadOnly()" class="inspector-card bg-red-50 border border-red-100 rounded-lg p-3">
18557
- <button (click)="removeColumn(node.id)" class="w-full h-8 text-red-600 hover:text-red-700 flex items-center justify-center gap-2 text-[12px] font-medium transition-colors">
18558
- <lucide-icon name="trash-2" class="w-4 h-4"></lucide-icon> Remove Column
18559
- </button>
18560
- </div>
19036
+ <div class="flex items-center px-2 pt-2 border-b border-border-default bg-surface-default">
19037
+ @for (tab of layoutInspectorTabs; track tab) {
19038
+ <button
19039
+ type="button"
19040
+ (click)="activeLayoutInspectorTab.set(tab)"
19041
+ [class]="activeLayoutInspectorTab() === tab
19042
+ ? 'px-2 pb-2 font-semibold text-primary-500 border-b-2 border-b-primary-500'
19043
+ : 'px-2 pb-2 text-text-primary opacity-60 hover:opacity-100 hover:text-text-primary border-b-2 border-transparent'">
19044
+ {{ tab }}
19045
+ </button>
19046
+ }
19047
+ </div>
18561
19048
 
18562
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18563
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Layout</div>
18564
- <inspector-layout-section
18565
- [style]="nodeStyle(node)"
18566
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18567
- </inspector-layout-section>
18568
- </div>
19049
+ <div class="flex-1 overflow-y-auto custom-scrollbar bg-surface-default">
19050
+ @if (activeLayoutInspectorTab() === 'Style') {
19051
+ <div class="flex flex-col" [class.pointer-events-none]="state.isReadOnly()" [class.opacity-60]="state.isReadOnly()">
19052
+ <ui-accordion title="Layout" [expanded]="true">
19053
+ <inspector-layout-section
19054
+ [style]="nodeStyle(node)"
19055
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19056
+ </inspector-layout-section>
19057
+ </ui-accordion>
19058
+
19059
+ <ui-accordion title="Spacing" [expanded]="false">
19060
+ <inspector-spacing-section
19061
+ [style]="nodeStyle(node)"
19062
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19063
+ </inspector-spacing-section>
19064
+ </ui-accordion>
19065
+
19066
+ <ui-accordion title="Size" [expanded]="false">
19067
+ <inspector-size-section
19068
+ [style]="nodeStyle(node)"
19069
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19070
+ </inspector-size-section>
19071
+ </ui-accordion>
19072
+
19073
+ <ui-accordion title="Backgrounds" [expanded]="false">
19074
+ <inspector-backgrounds-section
19075
+ [style]="nodeStyle(node)"
19076
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19077
+ </inspector-backgrounds-section>
19078
+ </ui-accordion>
19079
+
19080
+ <ui-accordion title="Borders" [expanded]="false">
19081
+ <inspector-borders-section
19082
+ [style]="nodeStyle(node)"
19083
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19084
+ </inspector-borders-section>
19085
+ </ui-accordion>
19086
+
19087
+ <ui-accordion title="Effects" [expanded]="false">
19088
+ <inspector-effects-section
19089
+ [style]="nodeStyle(node)"
19090
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19091
+ </inspector-effects-section>
19092
+ </ui-accordion>
19093
+
19094
+ <ui-accordion title="Advanced" [expanded]="false">
19095
+ <inspector-advanced-section
19096
+ [style]="nodeStyle(node)"
19097
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19098
+ </inspector-advanced-section>
19099
+ </ui-accordion>
19100
+
19101
+ <div class="h-10"></div>
19102
+ </div>
19103
+ }
19104
+
19105
+ @if (activeLayoutInspectorTab() === 'Settings') {
19106
+ <div class="p-4 flex flex-col gap-4">
19107
+ @if (node.type === 'row') {
19108
+ <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm"
19109
+ [class.opacity-60]="state.isReadOnly()"
19110
+ [class.pointer-events-none]="state.isReadOnly()">
19111
+ <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Column Structure</div>
19112
+ <div class="grid grid-cols-3 gap-2 mb-3">
19113
+ <button (click)="applyPreset(node.id, 1)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="1 Column">
19114
+ <div class="flex gap-1 w-full h-4 justify-center px-1">
19115
+ <div class="w-full bg-border-default rounded-sm"></div>
19116
+ </div>
19117
+ </button>
19118
+ <button (click)="applyPreset(node.id, 2)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="2 Columns">
19119
+ <div class="flex gap-0.5 w-full h-4 px-1">
19120
+ <div class="w-1/2 bg-border-default rounded-sm"></div>
19121
+ <div class="w-1/2 bg-border-default rounded-sm"></div>
19122
+ </div>
19123
+ </button>
19124
+ <button (click)="applyPreset(node.id, 3)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="3 Columns">
19125
+ <div class="flex gap-0.5 w-full h-4 px-1">
19126
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19127
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19128
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19129
+ </div>
19130
+ </button>
19131
+ <button (click)="applyPreset(node.id, 4)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="4 Columns">
19132
+ <div class="flex gap-0.5 w-full h-4 px-1">
19133
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19134
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19135
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19136
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19137
+ </div>
19138
+ </button>
19139
+ <button (click)="applyPreset(node.id, 6)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="6 Columns">
19140
+ <span class="text-[12px] text-text-primary font-medium">6 Col</span>
19141
+ </button>
19142
+ <button (click)="applyPreset(node.id, 12)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="12 Columns">
19143
+ <span class="text-[12px] text-text-primary font-medium">12 Col</span>
19144
+ </button>
19145
+ </div>
19146
+ <button (click)="addColumn(node.id)" class="w-full h-9 bg-primary-500 text-white rounded-md hover:opacity-90 flex items-center justify-center gap-2 text-[12px] font-medium transition-colors">
19147
+ <lucide-icon name="plus" class="w-4 h-4"></lucide-icon> Add Column
19148
+ </button>
19149
+ </div>
19150
+ }
19151
+
19152
+ @if (node.type === 'col') {
19153
+ <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm"
19154
+ [class.opacity-60]="state.isReadOnly()"
19155
+ [class.pointer-events-none]="state.isReadOnly()">
19156
+ <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Responsive Width (1-12)</div>
19157
+
19158
+ <div class="flex items-center gap-2 mb-2">
19159
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XS</span>
19160
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.xs" (ngModelChange)="onResponsiveChange(node.id, 'xs', $event)">
19161
+ <option *ngFor="let option of responsiveWidthOptions(false)" [ngValue]="option.value">{{ option.label }}</option>
19162
+ </select>
19163
+ </div>
18569
19164
 
18570
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18571
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Spacing</div>
18572
- <inspector-spacing-section
18573
- [style]="nodeStyle(node)"
18574
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18575
- </inspector-spacing-section>
18576
- </div>
19165
+ <div class="flex items-center gap-2 mb-2">
19166
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">SM</span>
19167
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.sm" (ngModelChange)="onResponsiveChange(node.id, 'sm', $event)">
19168
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19169
+ </select>
19170
+ </div>
18577
19171
 
18578
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18579
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Size</div>
18580
- <inspector-size-section
18581
- [style]="nodeStyle(node)"
18582
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18583
- </inspector-size-section>
18584
- </div>
19172
+ <div class="flex items-center gap-2 mb-2">
19173
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">MD</span>
19174
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.md" (ngModelChange)="onResponsiveChange(node.id, 'md', $event)">
19175
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19176
+ </select>
19177
+ </div>
18585
19178
 
18586
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18587
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Backgrounds</div>
18588
- <inspector-backgrounds-section
18589
- [style]="nodeStyle(node)"
18590
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18591
- </inspector-backgrounds-section>
18592
- </div>
19179
+ <div class="flex items-center gap-2 mb-2">
19180
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">LG</span>
19181
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.lg" (ngModelChange)="onResponsiveChange(node.id, 'lg', $event)">
19182
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19183
+ </select>
19184
+ </div>
18593
19185
 
18594
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18595
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Borders</div>
18596
- <inspector-borders-section
18597
- [style]="nodeStyle(node)"
18598
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18599
- </inspector-borders-section>
18600
- </div>
19186
+ <div class="flex items-center gap-2 mb-2">
19187
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XL</span>
19188
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.xl" (ngModelChange)="onResponsiveChange(node.id, 'xl', $event)">
19189
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19190
+ </select>
19191
+ </div>
18601
19192
 
18602
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18603
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Effects</div>
18604
- <inspector-effects-section
18605
- [style]="nodeStyle(node)"
18606
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18607
- </inspector-effects-section>
18608
- </div>
19193
+ <div class="flex items-center gap-2">
19194
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">2XL</span>
19195
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive ? node.responsive['2xl'] : undefined" (ngModelChange)="onResponsiveChange(node.id, '2xl', $event)">
19196
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19197
+ </select>
19198
+ </div>
19199
+ </div>
18609
19200
 
18610
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18611
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Advanced</div>
18612
- <inspector-advanced-section
18613
- [style]="nodeStyle(node)"
18614
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18615
- </inspector-advanced-section>
18616
- </div>
18617
- </div>
18618
- </ng-container>
18619
- </div>
18620
- </ui-tab>
18621
- </ui-tabs>
19201
+ @if (!state.isReadOnly()) {
19202
+ <div class="inspector-card bg-red-50 border border-red-100 rounded-lg p-3">
19203
+ <button (click)="removeColumn(node.id)" class="w-full h-8 text-red-600 hover:text-red-700 flex items-center justify-center gap-2 text-[12px] font-medium transition-colors">
19204
+ <lucide-icon name="trash-2" class="w-4 h-4"></lucide-icon> Remove Column
19205
+ </button>
19206
+ </div>
19207
+ }
19208
+ }
19209
+ </div>
19210
+ }
19211
+ </div>
18622
19212
  </ng-container>
18623
19213
  </div>
18624
19214
 
@@ -18631,7 +19221,7 @@ class PropertiesPanelComponent {
18631
19221
  </form-settings-inspector>
18632
19222
  </ng-template>
18633
19223
  </div>
18634
- `, isInline: true, styles: [".custom-scrollbar::-webkit-scrollbar{width:6px}.custom-scrollbar::-webkit-scrollbar-thumb{background-color:#d1d5db;border-radius:99px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: UiIconModule }, { kind: "component", type: i3$1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "component", type: UiTabsComponent, selector: "ui-tabs", inputs: ["activeTab"], outputs: ["activeTabChange"] }, { kind: "component", type: UiTabComponent, selector: "ui-tab", inputs: ["label", "name", "disabled", "badge", "badgeTone"] }, { kind: "component", type: WidgetInspectorComponent, selector: "widget-inspector", inputs: ["node", "field", "readOnly"], outputs: ["fieldChange", "styleChange", "rulesChange", "duplicate", "delete"] }, { kind: "component", type: FormSettingsInspectorComponent, selector: "form-settings-inspector", inputs: ["schema", "readOnly"], outputs: ["settingsChange", "styleChange"] }, { kind: "component", type: InspectorLayoutSectionComponent, selector: "inspector-layout-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorSpacingSectionComponent, selector: "inspector-spacing-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorSizeSectionComponent, selector: "inspector-size-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBackgroundsSectionComponent, selector: "inspector-backgrounds-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBordersSectionComponent, selector: "inspector-borders-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorEffectsSectionComponent, selector: "inspector-effects-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorAdvancedSectionComponent, selector: "inspector-advanced-section", inputs: ["style"], outputs: ["styleChange"] }] });
19224
+ `, isInline: true, styles: [".custom-scrollbar::-webkit-scrollbar{width:6px}.custom-scrollbar::-webkit-scrollbar-thumb{background-color:#d1d5db;border-radius:99px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: UiIconModule }, { kind: "component", type: i3$1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "component", type: UiAccordionComponent, selector: "ui-accordion", inputs: ["title", "subtitle", "expanded", "showAdd"] }, { kind: "component", type: WidgetInspectorComponent, selector: "widget-inspector", inputs: ["node", "field", "readOnly"], outputs: ["fieldChange", "styleChange", "rulesChange", "duplicate", "delete"] }, { kind: "component", type: FormSettingsInspectorComponent, selector: "form-settings-inspector", inputs: ["schema", "readOnly"], outputs: ["settingsChange", "styleChange"] }, { kind: "component", type: InspectorLayoutSectionComponent, selector: "inspector-layout-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorSpacingSectionComponent, selector: "inspector-spacing-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorSizeSectionComponent, selector: "inspector-size-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBackgroundsSectionComponent, selector: "inspector-backgrounds-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorBordersSectionComponent, selector: "inspector-borders-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorEffectsSectionComponent, selector: "inspector-effects-section", inputs: ["style"], outputs: ["styleChange"] }, { kind: "component", type: InspectorAdvancedSectionComponent, selector: "inspector-advanced-section", inputs: ["style"], outputs: ["styleChange"] }] });
18635
19225
  }
18636
19226
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PropertiesPanelComponent, decorators: [{
18637
19227
  type: Component,
@@ -18639,8 +19229,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
18639
19229
  CommonModule,
18640
19230
  FormsModule,
18641
19231
  UiIconModule,
18642
- UiTabsComponent,
18643
- UiTabComponent,
19232
+ UiAccordionComponent,
18644
19233
  WidgetInspectorComponent,
18645
19234
  FormSettingsInspectorComponent,
18646
19235
  InspectorLayoutSectionComponent,
@@ -18677,239 +19266,182 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
18677
19266
  </div>
18678
19267
  </div>
18679
19268
 
18680
- <!-- Tabs -->
18681
- <ui-tabs class="flex-1 flex flex-col min-h-0 bg-surface-default">
18682
- <ui-tab label="Style">
18683
- <div class="panel-body p-4 pt-3 flex flex-col gap-4 overflow-y-auto h-full custom-scrollbar">
18684
-
18685
- <!-- ROW Specific -->
18686
- <!-- Binds to LayoutNode.style (Row Container Styles) -->
18687
- <ng-container *ngIf="node.type === 'row'">
18688
- <div class="inspector-stack space-y-4">
18689
- <div *ngIf="!state.isReadOnly()" class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm">
18690
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Column Structure</div>
18691
-
18692
- <!-- Presets -->
18693
- <div class="grid grid-cols-3 gap-2 mb-3">
18694
- <button (click)="applyPreset(node.id, 1)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="1 Column">
18695
- <div class="flex gap-1 w-full h-4 justify-center px-1">
18696
- <div class="w-full bg-border-default rounded-sm"></div>
18697
- </div>
18698
- </button>
18699
- <button (click)="applyPreset(node.id, 2)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="2 Columns">
18700
- <div class="flex gap-0.5 w-full h-4 px-1">
18701
- <div class="w-1/2 bg-border-default rounded-sm"></div>
18702
- <div class="w-1/2 bg-border-default rounded-sm"></div>
18703
- </div>
18704
- </button>
18705
- <button (click)="applyPreset(node.id, 3)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="3 Columns">
18706
- <div class="flex gap-0.5 w-full h-4 px-1">
18707
- <div class="w-1/3 bg-border-default rounded-sm"></div>
18708
- <div class="w-1/3 bg-border-default rounded-sm"></div>
18709
- <div class="w-1/3 bg-border-default rounded-sm"></div>
18710
- </div>
18711
- </button>
18712
- <button (click)="applyPreset(node.id, 4)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="4 Columns">
18713
- <div class="flex gap-0.5 w-full h-4 px-1">
18714
- <div class="w-1/4 bg-border-default rounded-sm"></div>
18715
- <div class="w-1/4 bg-border-default rounded-sm"></div>
18716
- <div class="w-1/4 bg-border-default rounded-sm"></div>
18717
- <div class="w-1/4 bg-border-default rounded-sm"></div>
18718
- </div>
18719
- </button>
18720
- <button (click)="applyPreset(node.id, 6)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="6 Columns">
18721
- <span class="text-[12px] text-text-primary font-medium">6 Col</span>
18722
- </button>
18723
- <button (click)="applyPreset(node.id, 12)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="12 Columns">
18724
- <span class="text-[12px] text-text-primary font-medium">12 Col</span>
18725
- </button>
18726
- </div>
18727
-
18728
- <!-- Manual Actions -->
18729
- <div class="flex flex-col gap-2">
18730
- <button (click)="addColumn(node.id)" class="w-full h-9 bg-primary-500 text-white rounded-md hover:opacity-90 flex items-center justify-center gap-2 text-[12px] font-medium transition-colors">
18731
- <lucide-icon name="plus" class="w-4 h-4"></lucide-icon> Add Column
18732
- </button>
18733
- </div>
18734
- </div>
18735
-
18736
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18737
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Layout</div>
18738
- <inspector-layout-section
18739
- [style]="nodeStyle(node)"
18740
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18741
- </inspector-layout-section>
18742
- </div>
18743
-
18744
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18745
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Spacing</div>
18746
- <inspector-spacing-section
18747
- [style]="nodeStyle(node)"
18748
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18749
- </inspector-spacing-section>
18750
- </div>
18751
-
18752
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18753
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Size</div>
18754
- <inspector-size-section
18755
- [style]="nodeStyle(node)"
18756
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18757
- </inspector-size-section>
18758
- </div>
18759
-
18760
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18761
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Backgrounds</div>
18762
- <inspector-backgrounds-section
18763
- [style]="nodeStyle(node)"
18764
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18765
- </inspector-backgrounds-section>
18766
- </div>
18767
-
18768
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18769
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Borders</div>
18770
- <inspector-borders-section
18771
- [style]="nodeStyle(node)"
18772
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18773
- </inspector-borders-section>
18774
- </div>
18775
-
18776
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18777
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Effects</div>
18778
- <inspector-effects-section
18779
- [style]="nodeStyle(node)"
18780
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18781
- </inspector-effects-section>
18782
- </div>
18783
-
18784
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18785
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Advanced</div>
18786
- <inspector-advanced-section
18787
- [style]="nodeStyle(node)"
18788
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18789
- </inspector-advanced-section>
18790
- </div>
18791
- </div>
18792
- </ng-container>
18793
- <ng-container *ngIf="node.type === 'col'">
18794
- <div class="inspector-stack space-y-4">
18795
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18796
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Responsive Width (1-12)</div>
18797
-
18798
- <!-- XS (Mobile) -->
18799
- <div class="flex items-center gap-2 mb-2">
18800
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XS</span>
18801
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.xs" (ngModelChange)="onResponsiveChange(node.id, 'xs', $event)">
18802
- <option *ngFor="let option of responsiveWidthOptions(false)" [ngValue]="option.value">{{ option.label }}</option>
18803
- </select>
18804
- </div>
18805
-
18806
- <!-- SM (Large Phones) -->
18807
- <div class="flex items-center gap-2 mb-2">
18808
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">SM</span>
18809
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.sm" (ngModelChange)="onResponsiveChange(node.id, 'sm', $event)">
18810
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18811
- </select>
18812
- </div>
18813
-
18814
- <!-- MD (Tablet) -->
18815
- <div class="flex items-center gap-2 mb-2">
18816
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">MD</span>
18817
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.md" (ngModelChange)="onResponsiveChange(node.id, 'md', $event)">
18818
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18819
- </select>
18820
- </div>
18821
-
18822
- <!-- LG (Desktop) -->
18823
- <div class="flex items-center gap-2 mb-2">
18824
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">LG</span>
18825
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.lg" (ngModelChange)="onResponsiveChange(node.id, 'lg', $event)">
18826
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18827
- </select>
18828
- </div>
18829
-
18830
- <!-- XL (Large Desktop) -->
18831
- <div class="flex items-center gap-2 mb-2">
18832
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XL</span>
18833
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.xl" (ngModelChange)="onResponsiveChange(node.id, 'xl', $event)">
18834
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18835
- </select>
18836
- </div>
18837
-
18838
- <!-- 2XL (Extra Large) -->
18839
- <div class="flex items-center gap-2 mb-2">
18840
- <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">2XL</span>
18841
- <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive ? node.responsive['2xl'] : undefined" (ngModelChange)="onResponsiveChange(node.id, '2xl', $event)">
18842
- <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
18843
- </select>
18844
- </div>
18845
- </div>
18846
-
18847
- <div *ngIf="!state.isReadOnly()" class="inspector-card bg-red-50 border border-red-100 rounded-lg p-3">
18848
- <button (click)="removeColumn(node.id)" class="w-full h-8 text-red-600 hover:text-red-700 flex items-center justify-center gap-2 text-[12px] font-medium transition-colors">
18849
- <lucide-icon name="trash-2" class="w-4 h-4"></lucide-icon> Remove Column
18850
- </button>
18851
- </div>
19269
+ <div class="flex items-center px-2 pt-2 border-b border-border-default bg-surface-default">
19270
+ @for (tab of layoutInspectorTabs; track tab) {
19271
+ <button
19272
+ type="button"
19273
+ (click)="activeLayoutInspectorTab.set(tab)"
19274
+ [class]="activeLayoutInspectorTab() === tab
19275
+ ? 'px-2 pb-2 font-semibold text-primary-500 border-b-2 border-b-primary-500'
19276
+ : 'px-2 pb-2 text-text-primary opacity-60 hover:opacity-100 hover:text-text-primary border-b-2 border-transparent'">
19277
+ {{ tab }}
19278
+ </button>
19279
+ }
19280
+ </div>
18852
19281
 
18853
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18854
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Layout</div>
18855
- <inspector-layout-section
18856
- [style]="nodeStyle(node)"
18857
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18858
- </inspector-layout-section>
18859
- </div>
19282
+ <div class="flex-1 overflow-y-auto custom-scrollbar bg-surface-default">
19283
+ @if (activeLayoutInspectorTab() === 'Style') {
19284
+ <div class="flex flex-col" [class.pointer-events-none]="state.isReadOnly()" [class.opacity-60]="state.isReadOnly()">
19285
+ <ui-accordion title="Layout" [expanded]="true">
19286
+ <inspector-layout-section
19287
+ [style]="nodeStyle(node)"
19288
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19289
+ </inspector-layout-section>
19290
+ </ui-accordion>
19291
+
19292
+ <ui-accordion title="Spacing" [expanded]="false">
19293
+ <inspector-spacing-section
19294
+ [style]="nodeStyle(node)"
19295
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19296
+ </inspector-spacing-section>
19297
+ </ui-accordion>
19298
+
19299
+ <ui-accordion title="Size" [expanded]="false">
19300
+ <inspector-size-section
19301
+ [style]="nodeStyle(node)"
19302
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19303
+ </inspector-size-section>
19304
+ </ui-accordion>
19305
+
19306
+ <ui-accordion title="Backgrounds" [expanded]="false">
19307
+ <inspector-backgrounds-section
19308
+ [style]="nodeStyle(node)"
19309
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19310
+ </inspector-backgrounds-section>
19311
+ </ui-accordion>
19312
+
19313
+ <ui-accordion title="Borders" [expanded]="false">
19314
+ <inspector-borders-section
19315
+ [style]="nodeStyle(node)"
19316
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19317
+ </inspector-borders-section>
19318
+ </ui-accordion>
19319
+
19320
+ <ui-accordion title="Effects" [expanded]="false">
19321
+ <inspector-effects-section
19322
+ [style]="nodeStyle(node)"
19323
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19324
+ </inspector-effects-section>
19325
+ </ui-accordion>
19326
+
19327
+ <ui-accordion title="Advanced" [expanded]="false">
19328
+ <inspector-advanced-section
19329
+ [style]="nodeStyle(node)"
19330
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19331
+ </inspector-advanced-section>
19332
+ </ui-accordion>
19333
+
19334
+ <div class="h-10"></div>
19335
+ </div>
19336
+ }
19337
+
19338
+ @if (activeLayoutInspectorTab() === 'Settings') {
19339
+ <div class="p-4 flex flex-col gap-4">
19340
+ @if (node.type === 'row') {
19341
+ <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm"
19342
+ [class.opacity-60]="state.isReadOnly()"
19343
+ [class.pointer-events-none]="state.isReadOnly()">
19344
+ <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Column Structure</div>
19345
+ <div class="grid grid-cols-3 gap-2 mb-3">
19346
+ <button (click)="applyPreset(node.id, 1)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="1 Column">
19347
+ <div class="flex gap-1 w-full h-4 justify-center px-1">
19348
+ <div class="w-full bg-border-default rounded-sm"></div>
19349
+ </div>
19350
+ </button>
19351
+ <button (click)="applyPreset(node.id, 2)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="2 Columns">
19352
+ <div class="flex gap-0.5 w-full h-4 px-1">
19353
+ <div class="w-1/2 bg-border-default rounded-sm"></div>
19354
+ <div class="w-1/2 bg-border-default rounded-sm"></div>
19355
+ </div>
19356
+ </button>
19357
+ <button (click)="applyPreset(node.id, 3)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="3 Columns">
19358
+ <div class="flex gap-0.5 w-full h-4 px-1">
19359
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19360
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19361
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19362
+ </div>
19363
+ </button>
19364
+ <button (click)="applyPreset(node.id, 4)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="4 Columns">
19365
+ <div class="flex gap-0.5 w-full h-4 px-1">
19366
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19367
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19368
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19369
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19370
+ </div>
19371
+ </button>
19372
+ <button (click)="applyPreset(node.id, 6)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="6 Columns">
19373
+ <span class="text-[12px] text-text-primary font-medium">6 Col</span>
19374
+ </button>
19375
+ <button (click)="applyPreset(node.id, 12)" class="h-9 border border-border-default rounded hover:bg-slate-50 flex items-center justify-center p-1" title="12 Columns">
19376
+ <span class="text-[12px] text-text-primary font-medium">12 Col</span>
19377
+ </button>
19378
+ </div>
19379
+ <button (click)="addColumn(node.id)" class="w-full h-9 bg-primary-500 text-white rounded-md hover:opacity-90 flex items-center justify-center gap-2 text-[12px] font-medium transition-colors">
19380
+ <lucide-icon name="plus" class="w-4 h-4"></lucide-icon> Add Column
19381
+ </button>
19382
+ </div>
19383
+ }
19384
+
19385
+ @if (node.type === 'col') {
19386
+ <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm"
19387
+ [class.opacity-60]="state.isReadOnly()"
19388
+ [class.pointer-events-none]="state.isReadOnly()">
19389
+ <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Responsive Width (1-12)</div>
19390
+
19391
+ <div class="flex items-center gap-2 mb-2">
19392
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XS</span>
19393
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.xs" (ngModelChange)="onResponsiveChange(node.id, 'xs', $event)">
19394
+ <option *ngFor="let option of responsiveWidthOptions(false)" [ngValue]="option.value">{{ option.label }}</option>
19395
+ </select>
19396
+ </div>
18860
19397
 
18861
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18862
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Spacing</div>
18863
- <inspector-spacing-section
18864
- [style]="nodeStyle(node)"
18865
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18866
- </inspector-spacing-section>
18867
- </div>
19398
+ <div class="flex items-center gap-2 mb-2">
19399
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">SM</span>
19400
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.sm" (ngModelChange)="onResponsiveChange(node.id, 'sm', $event)">
19401
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19402
+ </select>
19403
+ </div>
18868
19404
 
18869
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18870
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Size</div>
18871
- <inspector-size-section
18872
- [style]="nodeStyle(node)"
18873
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18874
- </inspector-size-section>
18875
- </div>
19405
+ <div class="flex items-center gap-2 mb-2">
19406
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">MD</span>
19407
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.md" (ngModelChange)="onResponsiveChange(node.id, 'md', $event)">
19408
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19409
+ </select>
19410
+ </div>
18876
19411
 
18877
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18878
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Backgrounds</div>
18879
- <inspector-backgrounds-section
18880
- [style]="nodeStyle(node)"
18881
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18882
- </inspector-backgrounds-section>
18883
- </div>
19412
+ <div class="flex items-center gap-2 mb-2">
19413
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">LG</span>
19414
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.lg" (ngModelChange)="onResponsiveChange(node.id, 'lg', $event)">
19415
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19416
+ </select>
19417
+ </div>
18884
19418
 
18885
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18886
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Borders</div>
18887
- <inspector-borders-section
18888
- [style]="nodeStyle(node)"
18889
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18890
- </inspector-borders-section>
18891
- </div>
19419
+ <div class="flex items-center gap-2 mb-2">
19420
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XL</span>
19421
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive.xl" (ngModelChange)="onResponsiveChange(node.id, 'xl', $event)">
19422
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19423
+ </select>
19424
+ </div>
18892
19425
 
18893
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18894
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Effects</div>
18895
- <inspector-effects-section
18896
- [style]="nodeStyle(node)"
18897
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18898
- </inspector-effects-section>
18899
- </div>
19426
+ <div class="flex items-center gap-2">
19427
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">2XL</span>
19428
+ <select class="h-8 flex-1 rounded-md border border-border-default bg-white px-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" [ngModel]="node.responsive ? node.responsive['2xl'] : undefined" (ngModelChange)="onResponsiveChange(node.id, '2xl', $event)">
19429
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19430
+ </select>
19431
+ </div>
19432
+ </div>
18900
19433
 
18901
- <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm" [class.opacity-50]="state.isReadOnly()" [class.pointer-events-none]="state.isReadOnly()">
18902
- <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Advanced</div>
18903
- <inspector-advanced-section
18904
- [style]="nodeStyle(node)"
18905
- (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18906
- </inspector-advanced-section>
18907
- </div>
18908
- </div>
18909
- </ng-container>
18910
- </div>
18911
- </ui-tab>
18912
- </ui-tabs>
19434
+ @if (!state.isReadOnly()) {
19435
+ <div class="inspector-card bg-red-50 border border-red-100 rounded-lg p-3">
19436
+ <button (click)="removeColumn(node.id)" class="w-full h-8 text-red-600 hover:text-red-700 flex items-center justify-center gap-2 text-[12px] font-medium transition-colors">
19437
+ <lucide-icon name="trash-2" class="w-4 h-4"></lucide-icon> Remove Column
19438
+ </button>
19439
+ </div>
19440
+ }
19441
+ }
19442
+ </div>
19443
+ }
19444
+ </div>
18913
19445
  </ng-container>
18914
19446
  </div>
18915
19447
 
@@ -20856,9 +21388,9 @@ const BASIC_CONTACT_TEMPLATE = {
20856
21388
  },
20857
21389
  {
20858
21390
  "id": "btn_submit",
20859
- "widgetId": "core.form:submit-button",
21391
+ "widgetId": "core.form:form-button",
20860
21392
  "name": "submit",
20861
- "type": "submit-button",
21393
+ "type": "form-button",
20862
21394
  "label": "Send message",
20863
21395
  "variant": "primary",
20864
21396
  "buttonType": "submit",
@@ -20875,9 +21407,9 @@ const BASIC_CONTACT_TEMPLATE = {
20875
21407
  },
20876
21408
  {
20877
21409
  "id": "btn_reset",
20878
- "widgetId": "core.form:reset-button",
21410
+ "widgetId": "core.form:form-button",
20879
21411
  "name": "reset",
20880
- "type": "reset-button",
21412
+ "type": "form-button",
20881
21413
  "label": "Reset",
20882
21414
  "variant": "secondary",
20883
21415
  "buttonType": "reset"
@@ -21269,9 +21801,9 @@ const RULES_SHIPPING_TEMPLATE = {
21269
21801
  },
21270
21802
  {
21271
21803
  "id": "btn_submit",
21272
- "widgetId": "core.form:submit-button",
21804
+ "widgetId": "core.form:form-button",
21273
21805
  "name": "submit",
21274
- "type": "submit-button",
21806
+ "type": "form-button",
21275
21807
  "label": "Place order",
21276
21808
  "variant": "primary",
21277
21809
  "buttonType": "submit",
@@ -21286,9 +21818,9 @@ const RULES_SHIPPING_TEMPLATE = {
21286
21818
  },
21287
21819
  {
21288
21820
  "id": "btn_reset",
21289
- "widgetId": "core.form:reset-button",
21821
+ "widgetId": "core.form:form-button",
21290
21822
  "name": "reset",
21291
- "type": "reset-button",
21823
+ "type": "form-button",
21292
21824
  "label": "Reset",
21293
21825
  "variant": "secondary",
21294
21826
  "buttonType": "reset"
@@ -21578,9 +22110,9 @@ const DATA_SOURCES_TEMPLATE = {
21578
22110
  },
21579
22111
  {
21580
22112
  "id": "btn_submit",
21581
- "widgetId": "core.form:submit-button",
22113
+ "widgetId": "core.form:form-button",
21582
22114
  "name": "submit",
21583
- "type": "submit-button",
22115
+ "type": "form-button",
21584
22116
  "label": "Create profile",
21585
22117
  "variant": "primary",
21586
22118
  "buttonType": "submit"
@@ -31299,18 +31831,15 @@ class DefaultDataProvider extends DataProvider {
31299
31831
  return runtimeOptions;
31300
31832
  }
31301
31833
  const cfg = getEffectiveDataConfig(field);
31302
- // 1. Resolve Rows
31303
- let rows = await this.getRawRows(cfg, field, engine);
31304
- rows = this.resolveCollectionRows(rows, cfg, engine);
31305
- // 2. Apply Filters
31306
- rows = this.applyRowFilters(rows, cfg.filters, engine);
31834
+ const rowContexts = await this.getOptionRowContexts(cfg, field, engine);
31835
+ const filteredContexts = this.applyOptionRowFilters(rowContexts, cfg.filters, engine);
31307
31836
  // 3. Map to Options
31308
31837
  if (cfg.type === 'static') {
31309
- return rows.map(row => this.mapRowToOption(row, 'label', 'value'));
31838
+ return filteredContexts.map(context => this.mapContextToOption(context, 'label', 'value', cfg));
31310
31839
  }
31311
- const labelKey = cfg.labelPath || cfg.labelKey || 'label';
31312
- const valueKey = cfg.valuePath || cfg.valueKey || 'value';
31313
- return rows.map(row => this.mapRowToOption(row, labelKey, valueKey));
31840
+ const labelKey = cfg.labelKey || 'label';
31841
+ const valueKey = cfg.valueKey || 'value';
31842
+ return filteredContexts.map(context => this.mapContextToOption(context, labelKey, valueKey, cfg));
31314
31843
  }
31315
31844
  async queryOptions(field, query, engine) {
31316
31845
  const runtimeOptions = await this.getRuntimeOptions(field, engine);
@@ -31428,8 +31957,13 @@ class DefaultDataProvider extends DataProvider {
31428
31957
  if (!rows || rows.length === 0) {
31429
31958
  return field.defaultValue;
31430
31959
  }
31431
- const row = this.selectScalarRow(rows, cfg, engine) ?? rows[0];
31432
- const resolvedPath = cfg.valuePath || cfg.valueKey;
31960
+ const selectedRow = this.selectScalarRow(rows, cfg, engine);
31961
+ if (cfg.rowSelectionMode === 'selected' && !selectedRow) {
31962
+ const currentValue = field.name && engine ? engine.getValue(field.name) : undefined;
31963
+ return currentValue !== undefined ? currentValue : field.defaultValue;
31964
+ }
31965
+ const row = selectedRow ?? rows[0];
31966
+ const resolvedPath = cfg.valueKey;
31433
31967
  const resolvedValue = resolvePathValue(row, resolvedPath);
31434
31968
  if (resolvedPath && resolvedValue !== undefined) {
31435
31969
  return resolvedValue;
@@ -31479,6 +32013,21 @@ class DefaultDataProvider extends DataProvider {
31479
32013
  const result = await this.client.query(cfg.datasourceId);
31480
32014
  return result.rows;
31481
32015
  }
32016
+ async getOptionRowContexts(cfg, field, engine) {
32017
+ const rows = await this.getRawRows(cfg, field, engine);
32018
+ return this.resolveCollectionRowContexts(rows, cfg, engine);
32019
+ }
32020
+ resolveCollectionRowContexts(rows, cfg, engine) {
32021
+ const parentContexts = this.extractRowContexts(rows, cfg.rowsPath);
32022
+ if (cfg.rowSelectionMode === 'selected' && cfg.childRowsPath) {
32023
+ const selectedParentContext = this.selectOptionContext(parentContexts, cfg, engine);
32024
+ if (!selectedParentContext) {
32025
+ return [];
32026
+ }
32027
+ return this.extractRowContexts([selectedParentContext.row], cfg.childRowsPath, selectedParentContext);
32028
+ }
32029
+ return parentContexts;
32030
+ }
31482
32031
  resolveCollectionRows(rows, cfg, engine) {
31483
32032
  const parentRows = this.extractRows(rows, cfg.rowsPath);
31484
32033
  if (cfg.rowSelectionMode === 'selected' && cfg.childRowsPath) {
@@ -31493,6 +32042,38 @@ class DefaultDataProvider extends DataProvider {
31493
32042
  }
31494
32043
  return parentRows;
31495
32044
  }
32045
+ extractRowContexts(rows, path, sourceContext) {
32046
+ const normalizedPath = path?.trim();
32047
+ if (!normalizedPath) {
32048
+ return rows.map(row => ({
32049
+ row,
32050
+ parentRow: sourceContext?.row,
32051
+ sourceRow: sourceContext?.sourceRow ?? row
32052
+ }));
32053
+ }
32054
+ const flattened = [];
32055
+ for (const row of rows) {
32056
+ const resolved = resolvePathValue(row, normalizedPath);
32057
+ if (Array.isArray(resolved)) {
32058
+ for (const entry of resolved) {
32059
+ flattened.push({
32060
+ row: this.toRowRecord(entry),
32061
+ parentRow: sourceContext?.row,
32062
+ sourceRow: sourceContext?.sourceRow ?? row
32063
+ });
32064
+ }
32065
+ continue;
32066
+ }
32067
+ if (resolved !== undefined && resolved !== null) {
32068
+ flattened.push({
32069
+ row: this.toRowRecord(resolved),
32070
+ parentRow: sourceContext?.row,
32071
+ sourceRow: sourceContext?.sourceRow ?? row
32072
+ });
32073
+ }
32074
+ }
32075
+ return flattened;
32076
+ }
31496
32077
  extractRows(rows, path) {
31497
32078
  const normalizedPath = path?.trim();
31498
32079
  if (!normalizedPath) {
@@ -31530,36 +32111,57 @@ class DefaultDataProvider extends DataProvider {
31530
32111
  }
31531
32112
  return rows.find(row => valuesMatch(resolvePathValue(row, cfg.selectionMatchPath), selectorValue));
31532
32113
  }
32114
+ selectOptionContext(contexts, cfg, engine) {
32115
+ if (contexts.length === 0)
32116
+ return undefined;
32117
+ if (cfg.rowSelectionMode !== 'selected' || !cfg.selectionFieldId || !cfg.selectionMatchPath || !engine) {
32118
+ return contexts[0];
32119
+ }
32120
+ const schema = engine.getSchema();
32121
+ const selectorField = schema.fields.find(candidate => candidate.id === cfg.selectionFieldId);
32122
+ if (!selectorField) {
32123
+ return undefined;
32124
+ }
32125
+ const selectorValue = engine.getValue(selectorField.name);
32126
+ if (selectorValue === undefined || selectorValue === null || selectorValue === '') {
32127
+ return undefined;
32128
+ }
32129
+ return contexts.find(context => valuesMatch(resolvePathValue(context.row, cfg.selectionMatchPath), selectorValue));
32130
+ }
31533
32131
  applyRowFilters(rows, filters, engine) {
31534
32132
  if (!filters || filters.length === 0)
31535
32133
  return rows;
31536
- return rows.filter(row => {
31537
- return filters.every(filter => {
31538
- const expected = this.resolveFilterValue(filter, engine);
31539
- if (expected === undefined)
31540
- return true; // Ignore if dependency missing
31541
- const actual = resolvePathValue(row, filter.column);
31542
- const val = expected;
31543
- switch (filter.op) {
31544
- case 'eq': return actual === val;
31545
- case 'neq': return actual !== val;
31546
- case 'in': return Array.isArray(val) ? val.includes(actual) : actual === val;
31547
- // String Ops
31548
- case 'contains':
31549
- case 'like':
31550
- return String(actual ?? '').toLowerCase().includes(String(val ?? '').toLowerCase());
31551
- case 'startsWith':
31552
- return String(actual ?? '').toLowerCase().startsWith(String(val ?? '').toLowerCase());
31553
- case 'endsWith':
31554
- return String(actual ?? '').toLowerCase().endsWith(String(val ?? '').toLowerCase());
31555
- // Comparison Ops
31556
- case 'gt': return (actual ?? 0) > (val ?? 0);
31557
- case 'gte': return (actual ?? 0) >= (val ?? 0);
31558
- case 'lt': return (actual ?? 0) < (val ?? 0);
31559
- case 'lte': return (actual ?? 0) <= (val ?? 0);
31560
- default: return true;
31561
- }
31562
- });
32134
+ return rows.filter(row => this.matchesRowFilters(row, filters, engine));
32135
+ }
32136
+ applyOptionRowFilters(contexts, filters, engine) {
32137
+ if (!filters || filters.length === 0)
32138
+ return contexts;
32139
+ return contexts.filter(context => this.matchesRowFilters(context.row, filters, engine));
32140
+ }
32141
+ matchesRowFilters(row, filters, engine) {
32142
+ return filters.every(filter => {
32143
+ const expected = this.resolveFilterValue(filter, engine);
32144
+ if (expected === undefined)
32145
+ return true;
32146
+ const actual = resolvePathValue(row, filter.column);
32147
+ const val = expected;
32148
+ switch (filter.op) {
32149
+ case 'eq': return actual === val;
32150
+ case 'neq': return actual !== val;
32151
+ case 'in': return Array.isArray(val) ? val.includes(actual) : actual === val;
32152
+ case 'contains':
32153
+ case 'like':
32154
+ return String(actual ?? '').toLowerCase().includes(String(val ?? '').toLowerCase());
32155
+ case 'startsWith':
32156
+ return String(actual ?? '').toLowerCase().startsWith(String(val ?? '').toLowerCase());
32157
+ case 'endsWith':
32158
+ return String(actual ?? '').toLowerCase().endsWith(String(val ?? '').toLowerCase());
32159
+ case 'gt': return (actual ?? 0) > (val ?? 0);
32160
+ case 'gte': return (actual ?? 0) >= (val ?? 0);
32161
+ case 'lt': return (actual ?? 0) < (val ?? 0);
32162
+ case 'lte': return (actual ?? 0) <= (val ?? 0);
32163
+ default: return true;
32164
+ }
31563
32165
  });
31564
32166
  }
31565
32167
  resolveFilters(cfg, engine) {
@@ -31617,6 +32219,56 @@ class DefaultDataProvider extends DataProvider {
31617
32219
  value: this.toOptionValue(resolvePathValue(row, valueKey))
31618
32220
  };
31619
32221
  }
32222
+ mapContextToOption(context, labelKey, valueKey, cfg) {
32223
+ const rawLabel = String(resolvePathValue(context.row, labelKey) ?? '');
32224
+ const prefix = this.resolveOptionLabelPrefix(context, cfg);
32225
+ const label = this.formatOptionLabel(rawLabel, cfg);
32226
+ return {
32227
+ label: prefix ? `${prefix} ${label}` : label,
32228
+ value: this.toOptionValue(resolvePathValue(context.row, valueKey))
32229
+ };
32230
+ }
32231
+ resolveOptionLabelPrefix(context, cfg) {
32232
+ const prefixPath = cfg.optionLabelPrefixPath?.trim();
32233
+ if (!prefixPath) {
32234
+ return '';
32235
+ }
32236
+ for (const candidate of [context.row, context.parentRow, context.sourceRow]) {
32237
+ if (!candidate)
32238
+ continue;
32239
+ const resolved = resolvePathValue(candidate, prefixPath);
32240
+ if (resolved === undefined || resolved === null)
32241
+ continue;
32242
+ const value = String(resolved).trim();
32243
+ if (value.length > 0) {
32244
+ return value;
32245
+ }
32246
+ }
32247
+ return '';
32248
+ }
32249
+ formatOptionLabel(label, cfg) {
32250
+ if (!cfg.formatNumericOptionLabels) {
32251
+ return label;
32252
+ }
32253
+ const trimmed = label.trim();
32254
+ if (!trimmed) {
32255
+ return label;
32256
+ }
32257
+ const normalized = trimmed.replace(/,/g, '');
32258
+ if (!/^-?\d+(\.\d+)?$/.test(normalized)) {
32259
+ return label;
32260
+ }
32261
+ const numericValue = Number(normalized);
32262
+ if (!Number.isFinite(numericValue)) {
32263
+ return label;
32264
+ }
32265
+ const fractionPart = normalized.split('.')[1];
32266
+ return new Intl.NumberFormat(undefined, {
32267
+ useGrouping: true,
32268
+ minimumFractionDigits: fractionPart?.length ?? 0,
32269
+ maximumFractionDigits: fractionPart?.length ?? 0
32270
+ }).format(numericValue);
32271
+ }
31620
32272
  async getRuntimeOptions(field, engine) {
31621
32273
  if (!engine)
31622
32274
  return undefined;
@@ -31631,7 +32283,7 @@ class DefaultDataProvider extends DataProvider {
31631
32283
  return cfg.type === 'source' || cfg.type === 'global' || cfg.type === 'api';
31632
32284
  }
31633
32285
  shouldUseLocalResolution(cfg) {
31634
- if (cfg.rowsPath || cfg.labelPath || cfg.valuePath || cfg.rowSelectionMode || cfg.selectionFieldId || cfg.selectionMatchPath || cfg.childRowsPath) {
32286
+ if (cfg.rowsPath || cfg.optionLabelPrefixPath || cfg.rowSelectionMode || cfg.selectionFieldId || cfg.selectionMatchPath || cfg.childRowsPath) {
31635
32287
  return true;
31636
32288
  }
31637
32289
  if (hasPathSyntax(cfg.labelKey) || hasPathSyntax(cfg.valueKey)) {
@@ -32022,7 +32674,6 @@ class TextFieldWidgetComponent {
32022
32674
  dataConfig.type ?? '',
32023
32675
  sourceKey,
32024
32676
  String(dataConfig.valueKey ?? ''),
32025
- String(dataConfig.valuePath ?? ''),
32026
32677
  String(dataConfig.rowsPath ?? ''),
32027
32678
  String(dataConfig.rowSelectionMode ?? ''),
32028
32679
  String(dataConfig.selectionFieldId ?? ''),
@@ -32817,12 +33468,14 @@ class SelectWidgetComponent {
32817
33468
  loading = false;
32818
33469
  loadError = null;
32819
33470
  options = [];
33471
+ rawOptions = [];
32820
33472
  searchTerms$ = new Subject();
32821
33473
  requestId = 0;
32822
33474
  currentSearchTerm = '';
32823
33475
  runtimeManagedField = false;
32824
33476
  runtimeOptionsLoaded = false;
32825
33477
  dependencyValueSnapshot = new Map();
33478
+ displayDependencyValueSnapshot = new Map();
32826
33479
  cachedStyleSource;
32827
33480
  cachedWrapperStyles = { width: '100%' };
32828
33481
  cachedControlCssVars = this.toCssVarMap({});
@@ -32864,7 +33517,7 @@ class SelectWidgetComponent {
32864
33517
  return;
32865
33518
  this.runtimeOptionsLoaded = false;
32866
33519
  this.currentSearchTerm = '';
32867
- this.options = [];
33520
+ this.setOptionsFromRaw([]);
32868
33521
  void this.loadOptions(this.currentSearchTerm);
32869
33522
  });
32870
33523
  }
@@ -32886,7 +33539,7 @@ class SelectWidgetComponent {
32886
33539
  this.runtimeManagedField = this.runtimeFieldDataAccessRegistry.hasFieldAccess(this.config, this.engine);
32887
33540
  if (!this.runtimeManagedField) {
32888
33541
  if (this.areApiCallsSuppressed() && this.hasSelectedValue()) {
32889
- this.options = this.withSelectedValueFallbackOptions([]);
33542
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions([]));
32890
33543
  }
32891
33544
  else {
32892
33545
  void this.loadOptions(this.currentSearchTerm);
@@ -32895,7 +33548,7 @@ class SelectWidgetComponent {
32895
33548
  }
32896
33549
  else if (this.hasSelectedValue()) {
32897
33550
  if (this.areApiCallsSuppressed()) {
32898
- this.options = this.withSelectedValueFallbackOptions([]);
33551
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions([]));
32899
33552
  this.runtimeOptionsLoaded = true;
32900
33553
  }
32901
33554
  else {
@@ -32904,32 +33557,38 @@ class SelectWidgetComponent {
32904
33557
  });
32905
33558
  }
32906
33559
  }
32907
- const dependencyIds = this.getDependencyFieldIds();
32908
33560
  if (this.engine) {
32909
33561
  const initialValues = this.engine.getValues();
32910
- this.seedDependencySnapshotFromValues(dependencyIds, initialValues);
33562
+ this.seedValueSnapshotFromValues(this.dependencyValueSnapshot, this.getDependencyFieldIds(), initialValues);
33563
+ this.seedValueSnapshotFromValues(this.displayDependencyValueSnapshot, this.getDisplayDependencyFieldIds(), initialValues);
32911
33564
  this.engine.valueChanges$
32912
33565
  .pipe(takeUntilDestroyed(this.destroyRef))
32913
33566
  .subscribe(values => {
32914
33567
  this.syncEnabledState();
32915
33568
  if (this.areApiCallsSuppressed()) {
32916
- this.options = this.withSelectedValueFallbackOptions(this.options);
33569
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(this.rawOptions));
32917
33570
  this.cdr.markForCheck();
32918
33571
  }
32919
- if (dependencyIds.length === 0)
32920
- return;
32921
33572
  const schema = this.engine.getSchema();
32922
- const depNames = schema.fields
32923
- .filter(f => dependencyIds.includes(f.id))
32924
- .map(f => f.name);
32925
- if (!this.haveDependencyValuesChanged(depNames, values))
33573
+ const dependencyIds = this.getDependencyFieldIds();
33574
+ const displayDependencyIds = this.getDisplayDependencyFieldIds();
33575
+ const dependencyNames = this.resolveFieldNames(schema.fields, dependencyIds);
33576
+ const displayDependencyNames = this.resolveFieldNames(schema.fields, displayDependencyIds);
33577
+ if (displayDependencyNames.length > 0 && this.haveFieldValuesChanged(this.displayDependencyValueSnapshot, displayDependencyNames, values)) {
33578
+ this.applyDisplayFormatting();
33579
+ this.syncStoredFieldLabel(this.control.value);
33580
+ this.cdr.markForCheck();
33581
+ }
33582
+ if (dependencyNames.length === 0)
33583
+ return;
33584
+ if (!this.haveFieldValuesChanged(this.dependencyValueSnapshot, dependencyNames, values))
32926
33585
  return;
32927
33586
  this.runtimeFieldDataAccessRegistry.invalidateFieldAndDescendants(this.engine, this.config.id);
32928
33587
  if (this.hasSelectedValue()) {
32929
33588
  this.control.setValue(null);
32930
33589
  }
32931
33590
  this.currentSearchTerm = '';
32932
- this.options = [];
33591
+ this.setOptionsFromRaw([]);
32933
33592
  this.runtimeOptionsLoaded = false;
32934
33593
  if (!this.runtimeManagedField) {
32935
33594
  void this.loadOptions(this.currentSearchTerm);
@@ -33072,6 +33731,10 @@ class SelectWidgetComponent {
33072
33731
  return [];
33073
33732
  return cfg.dependsOn.map(d => d.fieldId).filter((id) => !!id);
33074
33733
  }
33734
+ getDisplayDependencyFieldIds() {
33735
+ const prefixFieldId = this.config.dataConfig?.optionLabelPrefixFieldId;
33736
+ return prefixFieldId ? [prefixFieldId] : [];
33737
+ }
33075
33738
  toCssVarMap(controlStyles) {
33076
33739
  const vars = {
33077
33740
  '--fd-select-border-color': OUTLINED_FIELD_IDLE_BORDER_COLOR,
@@ -33140,7 +33803,7 @@ class SelectWidgetComponent {
33140
33803
  opts = await this.dataProvider.getOptions(this.config, this.engine);
33141
33804
  }
33142
33805
  if (this.requestId === reqId) {
33143
- this.options = this.withSelectedValueFallbackOptions(opts);
33806
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(opts));
33144
33807
  this.syncStoredFieldLabel(this.control.value);
33145
33808
  this.loading = false;
33146
33809
  this.loadError = null;
@@ -33152,7 +33815,7 @@ class SelectWidgetComponent {
33152
33815
  this.loading = false;
33153
33816
  this.loadError = 'Failed to load options.';
33154
33817
  if (!isSearch) {
33155
- this.options = this.withSelectedValueFallbackOptions(this.config.staticOptions || []);
33818
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(this.config.staticOptions || []));
33156
33819
  }
33157
33820
  this.cdr.markForCheck();
33158
33821
  }
@@ -33173,7 +33836,7 @@ class SelectWidgetComponent {
33173
33836
  if (this.runtimeOptionsLoaded && this.options.length > 0)
33174
33837
  return;
33175
33838
  if (this.areApiCallsSuppressed()) {
33176
- this.options = this.withSelectedValueFallbackOptions(this.options);
33839
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(this.rawOptions));
33177
33840
  this.runtimeOptionsLoaded = true;
33178
33841
  return;
33179
33842
  }
@@ -33182,25 +33845,29 @@ class SelectWidgetComponent {
33182
33845
  this.runtimeOptionsLoaded = true;
33183
33846
  }
33184
33847
  }
33185
- seedDependencySnapshotFromValues(dependencyIds, values) {
33186
- if (!this.engine || dependencyIds.length === 0)
33848
+ seedValueSnapshotFromValues(snapshot, fieldIds, values) {
33849
+ if (!this.engine || fieldIds.length === 0)
33187
33850
  return;
33188
- const schema = this.engine.getSchema();
33189
- const depNames = schema.fields
33190
- .filter(field => dependencyIds.includes(field.id))
33191
- .map(field => field.name);
33192
- for (const depName of depNames) {
33193
- this.dependencyValueSnapshot.set(depName, values[depName]);
33851
+ for (const fieldName of this.resolveFieldNames(this.engine.getSchema().fields, fieldIds)) {
33852
+ snapshot.set(fieldName, values[fieldName]);
33194
33853
  }
33195
33854
  }
33196
- haveDependencyValuesChanged(dependencyNames, values) {
33855
+ resolveFieldNames(fields, fieldIds) {
33856
+ if (fieldIds.length === 0) {
33857
+ return [];
33858
+ }
33859
+ return fields
33860
+ .filter(field => fieldIds.includes(field.id))
33861
+ .map(field => field.name);
33862
+ }
33863
+ haveFieldValuesChanged(snapshot, fieldNames, values) {
33197
33864
  let changed = false;
33198
- for (const depName of dependencyNames) {
33199
- const previousValue = this.dependencyValueSnapshot.get(depName);
33200
- const nextValue = values[depName];
33865
+ for (const fieldName of fieldNames) {
33866
+ const previousValue = snapshot.get(fieldName);
33867
+ const nextValue = values[fieldName];
33201
33868
  if (!Object.is(previousValue, nextValue)) {
33202
33869
  changed = true;
33203
- this.dependencyValueSnapshot.set(depName, nextValue);
33870
+ snapshot.set(fieldName, nextValue);
33204
33871
  }
33205
33872
  }
33206
33873
  return changed;
@@ -33212,12 +33879,19 @@ class SelectWidgetComponent {
33212
33879
  this.syncEnabledState();
33213
33880
  this.runtimeManagedField = this.runtimeFieldDataAccessRegistry.hasFieldAccess(this.config, this.engine);
33214
33881
  this.runtimeOptionsLoaded = false;
33215
- this.options = [];
33882
+ this.dependencyValueSnapshot.clear();
33883
+ this.displayDependencyValueSnapshot.clear();
33884
+ this.setOptionsFromRaw([]);
33216
33885
  this.loadError = null;
33217
33886
  this.currentSearchTerm = '';
33887
+ if (this.engine) {
33888
+ const currentValues = this.engine.getValues();
33889
+ this.seedValueSnapshotFromValues(this.dependencyValueSnapshot, this.getDependencyFieldIds(), currentValues);
33890
+ this.seedValueSnapshotFromValues(this.displayDependencyValueSnapshot, this.getDisplayDependencyFieldIds(), currentValues);
33891
+ }
33218
33892
  if (!this.runtimeManagedField) {
33219
33893
  if (this.areApiCallsSuppressed() && this.hasSelectedValue()) {
33220
- this.options = this.withSelectedValueFallbackOptions([]);
33894
+ this.setOptionsFromRaw(this.withSelectedValueFallbackOptions([]));
33221
33895
  return;
33222
33896
  }
33223
33897
  void this.loadOptions(this.currentSearchTerm);
@@ -33253,6 +33927,8 @@ class SelectWidgetComponent {
33253
33927
  String(dataConfig.datasourceId ?? ''),
33254
33928
  String(dataConfig.labelKey ?? ''),
33255
33929
  String(dataConfig.valueKey ?? ''),
33930
+ String(dataConfig.formatNumericOptionLabels ?? ''),
33931
+ String(dataConfig.optionLabelPrefixFieldId ?? ''),
33256
33932
  String(dataConfig.searchEnabled ?? ''),
33257
33933
  String(dataConfig.optionsLimit ?? ''),
33258
33934
  dependencySignature,
@@ -33297,6 +33973,62 @@ class SelectWidgetComponent {
33297
33973
  this.cachedInputAttributes = { 'aria-label': accessibleLabel };
33298
33974
  }
33299
33975
  }
33976
+ setOptionsFromRaw(options) {
33977
+ this.rawOptions = [...options];
33978
+ this.applyDisplayFormatting();
33979
+ }
33980
+ applyDisplayFormatting() {
33981
+ this.options = this.rawOptions.map(option => ({
33982
+ ...option,
33983
+ label: this.formatOptionLabel(option.label)
33984
+ }));
33985
+ }
33986
+ formatOptionLabel(label) {
33987
+ const formattedLabel = this.config.dataConfig?.formatNumericOptionLabels
33988
+ ? this.formatNumericLabel(label)
33989
+ : label;
33990
+ const prefix = this.resolveOptionLabelPrefix();
33991
+ return prefix ? `${prefix} ${formattedLabel}` : formattedLabel;
33992
+ }
33993
+ resolveOptionLabelPrefix() {
33994
+ const prefixFieldId = this.config.dataConfig?.optionLabelPrefixFieldId;
33995
+ if (!prefixFieldId
33996
+ || !this.engine
33997
+ || typeof this.engine.getValue !== 'function') {
33998
+ return '';
33999
+ }
34000
+ const prefixField = this.engine.getSchema().fields.find(field => field.id === prefixFieldId);
34001
+ if (!prefixField) {
34002
+ return '';
34003
+ }
34004
+ const value = this.engine.getValue(prefixField.name);
34005
+ if (value === undefined || value === null) {
34006
+ return '';
34007
+ }
34008
+ const trimmed = String(value).trim();
34009
+ return trimmed;
34010
+ }
34011
+ formatNumericLabel(label) {
34012
+ const trimmed = label.trim();
34013
+ if (!trimmed) {
34014
+ return label;
34015
+ }
34016
+ const normalized = trimmed.replace(/,/g, '');
34017
+ if (!/^-?\d+(\.\d+)?$/.test(normalized)) {
34018
+ return label;
34019
+ }
34020
+ const numericValue = Number(normalized);
34021
+ if (!Number.isFinite(numericValue)) {
34022
+ return label;
34023
+ }
34024
+ const fractionPart = normalized.split('.')[1];
34025
+ const formatter = new Intl.NumberFormat(undefined, {
34026
+ useGrouping: true,
34027
+ minimumFractionDigits: fractionPart?.length ?? 0,
34028
+ maximumFractionDigits: fractionPart?.length ?? 0
34029
+ });
34030
+ return formatter.format(numericValue);
34031
+ }
33300
34032
  toSafeNonNegativeInt(value, fallback) {
33301
34033
  if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
33302
34034
  return Math.floor(value);
@@ -33330,7 +34062,11 @@ class SelectWidgetComponent {
33330
34062
  if (!this.hasMeaningfulValue(rawValue)) {
33331
34063
  return [];
33332
34064
  }
33333
- return buildFallbackOptions(rawValue, this.getStoredFieldLabel());
34065
+ const storedLabel = this.getStoredFieldLabel();
34066
+ if (this.shouldUseStoredFallbackLabel(storedLabel)) {
34067
+ return buildFallbackOptions(rawValue, storedLabel);
34068
+ }
34069
+ return buildFallbackOptions(rawValue);
33334
34070
  }
33335
34071
  hasOptionValue(options, value) {
33336
34072
  return options.some(option => Object.is(option.value, value) || String(option.value) === String(value));
@@ -33369,6 +34105,18 @@ class SelectWidgetComponent {
33369
34105
  }
33370
34106
  return this.engine.getFieldLabel(this.config.name);
33371
34107
  }
34108
+ shouldUseStoredFallbackLabel(label) {
34109
+ if (label === undefined) {
34110
+ return false;
34111
+ }
34112
+ if (this.config.dataConfig?.optionLabelPrefixPath) {
34113
+ return true;
34114
+ }
34115
+ if (this.config.dataConfig?.optionLabelPrefixFieldId) {
34116
+ return this.resolveOptionLabelPrefix().length === 0;
34117
+ }
34118
+ return this.config.dataConfig?.formatNumericOptionLabels !== true;
34119
+ }
33372
34120
  setStoredFieldLabel(label) {
33373
34121
  if (!this.engine || typeof this.engine.setFieldLabel !== 'function') {
33374
34122
  return;
@@ -36926,10 +37674,6 @@ const FILE_BASIC_FIELDS = [
36926
37674
  { key: 'tooltip', type: 'text', label: 'Tooltip' },
36927
37675
  ...COMMON_GROUP_FIELDS
36928
37676
  ];
36929
- const COMMON_ICON_FIELDS = [
36930
- { key: 'prefixIcon', type: 'text', label: 'Prefix Icon (Material Name)' },
36931
- { key: 'suffixIcon', type: 'text', label: 'Suffix Icon (Material Name)' }
36932
- ];
36933
37677
  const COMMON_APPEARANCE_FIELDS = [
36934
37678
  {
36935
37679
  key: 'appearance',
@@ -36964,18 +37708,6 @@ const COMMON_STYLE_SECTIONS = [
36964
37708
  STYLE_TRANSFORM_SECTION,
36965
37709
  STYLE_EFFECTS_SECTION
36966
37710
  ];
36967
- const COMMON_VALIDATION_SECTION = {
36968
- label: 'Validation',
36969
- fields: [
36970
- { key: 'validators', type: 'validators-editor', label: 'Validation Rules' }
36971
- ]
36972
- };
36973
- const COMMON_CONDITIONAL_SECTION = {
36974
- label: 'Conditional',
36975
- fields: [
36976
- { key: 'conditional', type: 'conditional-editor', label: 'Conditional Logic' }
36977
- ]
36978
- };
36979
37711
  // Start with standard sections for Text Field
36980
37712
  const BASE_REQUIRED_FIELD = {
36981
37713
  key: 'html5.required',
@@ -36983,67 +37715,25 @@ const BASE_REQUIRED_FIELD = {
36983
37715
  label: 'Required',
36984
37716
  helpText: 'Base required state; Rules can override this dynamically.'
36985
37717
  };
36986
- const TEXT_VALIDATION_SECTION = {
36987
- label: 'Validation',
36988
- fields: [
36989
- BASE_REQUIRED_FIELD,
36990
- { key: 'html5.minLength', type: 'number', label: 'Min Length' },
36991
- { key: 'html5.maxLength', type: 'number', label: 'Max Length' },
36992
- { key: 'html5.pattern', type: 'text', label: 'Pattern (regex)' },
36993
- { key: 'validators', type: 'validators-editor', label: 'Advanced Rules' }
36994
- ]
36995
- };
36996
- const NUMBER_VALIDATION_SECTION = {
36997
- label: 'Validation',
36998
- fields: [
36999
- BASE_REQUIRED_FIELD,
37000
- { key: 'html5.min', type: 'number', label: 'Minimum Value' },
37001
- { key: 'html5.max', type: 'number', label: 'Maximum Value' },
37002
- { key: 'html5.step', type: 'number', label: 'Step (Increment)' },
37003
- { key: 'validators', type: 'validators-editor', label: 'Advanced Rules' }
37004
- ]
37005
- };
37006
- const DATE_VALIDATION_SECTION = {
37007
- label: 'Validation',
37008
- fields: [
37009
- BASE_REQUIRED_FIELD,
37010
- { key: 'html5.min', type: 'text', label: 'Earliest Date (YYYY-MM-DD)' },
37011
- { key: 'html5.max', type: 'text', label: 'Latest Date (YYYY-MM-DD)' },
37012
- { key: 'validators', type: 'validators-editor', label: 'Advanced Rules' }
37013
- ]
37014
- };
37015
- const TIME_VALIDATION_SECTION = {
37016
- label: 'Validation',
37017
- fields: [
37018
- BASE_REQUIRED_FIELD,
37019
- { key: 'html5.min', type: 'text', label: 'Earliest Time (HH:MM)' },
37020
- { key: 'html5.max', type: 'text', label: 'Latest Time (HH:MM)' },
37021
- { key: 'validators', type: 'validators-editor', label: 'Advanced Rules' }
37022
- ]
37023
- };
37024
- const DATETIME_VALIDATION_SECTION = {
37025
- label: 'Validation',
37026
- fields: [
37027
- BASE_REQUIRED_FIELD,
37028
- { key: 'html5.min', type: 'text', label: 'Earliest (YYYY-MM-DDTHH:MM)' },
37029
- { key: 'html5.max', type: 'text', label: 'Latest (YYYY-MM-DDTHH:MM)' },
37030
- { key: 'validators', type: 'validators-editor', label: 'Advanced Rules' }
37031
- ]
37032
- };
37033
- const SELECT_VALIDATION_SECTION = {
37034
- label: 'Validation',
37035
- fields: [
37036
- BASE_REQUIRED_FIELD,
37037
- { key: 'validators', type: 'validators-editor', label: 'Advanced Rules' }
37038
- ]
37039
- };
37040
- const FILE_VALIDATION_SECTION = {
37041
- label: 'Validation',
37042
- fields: [
37043
- BASE_REQUIRED_FIELD,
37044
- { key: 'validators', type: 'validators-editor', label: 'Advanced Rules' }
37045
- ]
37718
+ const CUSTOM_VALIDATION_RULES_FIELD = {
37719
+ key: 'validation',
37720
+ type: 'validators-editor',
37721
+ label: 'Custom Validation Rules',
37722
+ helpText: 'Use these for field-level validation checks. Use the Rules tab for conditional behaviour.'
37046
37723
  };
37724
+ function createValidationSection(...fields) {
37725
+ return {
37726
+ label: 'Validation',
37727
+ fields: [...fields, CUSTOM_VALIDATION_RULES_FIELD]
37728
+ };
37729
+ }
37730
+ const TEXT_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD, { key: 'html5.minLength', type: 'number', label: 'Min Length' }, { key: 'html5.maxLength', type: 'number', label: 'Max Length' }, { key: 'html5.pattern', type: 'text', label: 'Pattern (regex)' });
37731
+ const NUMBER_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD, { key: 'html5.min', type: 'number', label: 'Minimum Value' }, { key: 'html5.max', type: 'number', label: 'Maximum Value' }, { key: 'html5.step', type: 'number', label: 'Step (Increment)' });
37732
+ const DATE_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD, { key: 'html5.min', type: 'text', label: 'Earliest Date (YYYY-MM-DD)' }, { key: 'html5.max', type: 'text', label: 'Latest Date (YYYY-MM-DD)' });
37733
+ const TIME_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD, { key: 'html5.min', type: 'text', label: 'Earliest Time (HH:MM)' }, { key: 'html5.max', type: 'text', label: 'Latest Time (HH:MM)' });
37734
+ const DATETIME_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD, { key: 'html5.min', type: 'text', label: 'Earliest (YYYY-MM-DDTHH:MM)' }, { key: 'html5.max', type: 'text', label: 'Latest (YYYY-MM-DDTHH:MM)' });
37735
+ const SELECT_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD);
37736
+ const FILE_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD);
37047
37737
  const BUTTON_VARIANT_OPTIONS = [
37048
37738
  { label: 'Primary (Blue)', value: 'primary' },
37049
37739
  { label: 'Secondary (Outline)', value: 'secondary' }
@@ -37084,7 +37774,6 @@ const IMAGE_BUTTON_PROPERTIES = [
37084
37774
  ];
37085
37775
  const TEXT_WIDGET_PROPERTIES = [
37086
37776
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37087
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37088
37777
  TEXT_VALIDATION_SECTION,
37089
37778
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37090
37779
  ...COMMON_STYLE_SECTIONS
@@ -37115,7 +37804,6 @@ const FILE_WIDGET_PROPERTIES = [
37115
37804
  ]
37116
37805
  },
37117
37806
  FILE_VALIDATION_SECTION,
37118
- COMMON_CONDITIONAL_SECTION,
37119
37807
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37120
37808
  ...COMMON_STYLE_SECTIONS
37121
37809
  ];
@@ -37250,9 +37938,7 @@ const FIELD_WIDGETS = [
37250
37938
  ...COMMON_BASIC_FIELDS
37251
37939
  ]
37252
37940
  },
37253
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37254
37941
  NUMBER_VALIDATION_SECTION,
37255
- COMMON_CONDITIONAL_SECTION,
37256
37942
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37257
37943
  ...COMMON_STYLE_SECTIONS
37258
37944
  ]
@@ -37280,7 +37966,6 @@ const FIELD_WIDGETS = [
37280
37966
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37281
37967
  properties: [
37282
37968
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37283
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37284
37969
  DATE_VALIDATION_SECTION,
37285
37970
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37286
37971
  ...COMMON_STYLE_SECTIONS
@@ -37309,7 +37994,6 @@ const FIELD_WIDGETS = [
37309
37994
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37310
37995
  properties: [
37311
37996
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37312
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37313
37997
  TIME_VALIDATION_SECTION,
37314
37998
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37315
37999
  ...COMMON_STYLE_SECTIONS
@@ -37336,7 +38020,6 @@ const FIELD_WIDGETS = [
37336
38020
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37337
38021
  properties: [
37338
38022
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37339
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37340
38023
  DATETIME_VALIDATION_SECTION,
37341
38024
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37342
38025
  ...COMMON_STYLE_SECTIONS
@@ -37366,7 +38049,6 @@ const FIELD_WIDGETS = [
37366
38049
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37367
38050
  properties: [
37368
38051
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37369
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37370
38052
  DATE_VALIDATION_SECTION,
37371
38053
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37372
38054
  ...COMMON_STYLE_SECTIONS
@@ -37396,7 +38078,6 @@ const FIELD_WIDGETS = [
37396
38078
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37397
38079
  properties: [
37398
38080
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37399
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37400
38081
  DATE_VALIDATION_SECTION,
37401
38082
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37402
38083
  ...COMMON_STYLE_SECTIONS
@@ -37583,7 +38264,6 @@ const FIELD_WIDGETS = [
37583
38264
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37584
38265
  properties: [
37585
38266
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37586
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37587
38267
  NUMBER_VALIDATION_SECTION,
37588
38268
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37589
38269
  ...COMMON_STYLE_SECTIONS
@@ -37663,44 +38343,6 @@ const FIELD_WIDGETS = [
37663
38343
  dataConsumer: 'none',
37664
38344
  properties: BUTTON_PROPERTIES
37665
38345
  }),
37666
- defineWidget(pluginId$2, {
37667
- kind: 'button',
37668
- flavor: 'form',
37669
- type: 'submit-button',
37670
- icon: 'send',
37671
- label: 'Submit Button',
37672
- createConfig: () => ({
37673
- id: generateId$2(),
37674
- name: 'submit_' + Date.now(),
37675
- type: 'submit-button',
37676
- label: 'Submit',
37677
- variant: 'primary',
37678
- buttonType: 'submit',
37679
- style: { width: 'auto' }
37680
- }),
37681
- renderer: ButtonWidgetComponent,
37682
- dataConsumer: 'none',
37683
- properties: BUTTON_PROPERTIES
37684
- }),
37685
- defineWidget(pluginId$2, {
37686
- kind: 'button',
37687
- flavor: 'form',
37688
- type: 'reset-button',
37689
- icon: 'rotate-ccw',
37690
- label: 'Reset Button',
37691
- createConfig: () => ({
37692
- id: generateId$2(),
37693
- name: 'reset_' + Date.now(),
37694
- type: 'reset-button',
37695
- label: 'Reset',
37696
- variant: 'secondary',
37697
- buttonType: 'reset',
37698
- style: { width: 'auto' }
37699
- }),
37700
- renderer: ButtonWidgetComponent,
37701
- dataConsumer: 'none',
37702
- properties: BUTTON_PROPERTIES
37703
- }),
37704
38346
  defineWidget(pluginId$2, {
37705
38347
  kind: 'button',
37706
38348
  flavor: 'form',
@@ -37758,7 +38400,6 @@ const FIELD_WIDGETS = [
37758
38400
  },
37759
38401
  // Options are now handled in Data Tab
37760
38402
  SELECT_VALIDATION_SECTION,
37761
- COMMON_CONDITIONAL_SECTION,
37762
38403
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37763
38404
  ...COMMON_STYLE_SECTIONS
37764
38405
  ]
@@ -37788,7 +38429,6 @@ const FIELD_WIDGETS = [
37788
38429
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37789
38430
  // Options handled in Data Tab
37790
38431
  SELECT_VALIDATION_SECTION,
37791
- COMMON_CONDITIONAL_SECTION,
37792
38432
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37793
38433
  ...COMMON_STYLE_SECTIONS
37794
38434
  ]
@@ -37818,7 +38458,6 @@ const FIELD_WIDGETS = [
37818
38458
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37819
38459
  // Options handled in Data Tab
37820
38460
  SELECT_VALIDATION_SECTION,
37821
- COMMON_CONDITIONAL_SECTION,
37822
38461
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37823
38462
  ...COMMON_STYLE_SECTIONS
37824
38463
  ]
@@ -37843,14 +38482,7 @@ const FIELD_WIDGETS = [
37843
38482
  properties: [
37844
38483
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37845
38484
  // Boolean specific validation or standard
37846
- {
37847
- label: 'Validation',
37848
- fields: [
37849
- BASE_REQUIRED_FIELD,
37850
- { key: 'validators', type: 'validators-editor', label: 'Advanced Rules' }
37851
- ]
37852
- },
37853
- COMMON_CONDITIONAL_SECTION,
38485
+ createValidationSection(BASE_REQUIRED_FIELD),
37854
38486
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37855
38487
  ...COMMON_STYLE_SECTIONS
37856
38488
  ]
@@ -37881,7 +38513,6 @@ const FIELD_WIDGETS = [
37881
38513
  properties: [
37882
38514
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37883
38515
  SELECT_VALIDATION_SECTION,
37884
- COMMON_CONDITIONAL_SECTION,
37885
38516
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37886
38517
  ...COMMON_STYLE_SECTIONS
37887
38518
  ]
@@ -37945,7 +38576,6 @@ const FIELD_WIDGETS = [
37945
38576
  ]
37946
38577
  },
37947
38578
  ...COMMON_STYLE_SECTIONS,
37948
- COMMON_CONDITIONAL_SECTION
37949
38579
  ]
37950
38580
  })
37951
38581
  ];
@@ -40147,6 +40777,162 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
40147
40777
  args: [DOCUMENT]
40148
40778
  }] }] });
40149
40779
 
40780
+ class UiTabComponent {
40781
+ label = '';
40782
+ name = ''; // Unique key for controlled mode
40783
+ disabled = false;
40784
+ badge;
40785
+ badgeTone = 'neutral';
40786
+ template;
40787
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
40788
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: UiTabComponent, isStandalone: true, selector: "ui-tab", inputs: { label: "label", name: "name", disabled: "disabled", badge: "badge", badgeTone: "badgeTone" }, viewQueries: [{ propertyName: "template", first: true, predicate: ["tpl"], descendants: true, static: true }], ngImport: i0, template: `<ng-template #tpl><ng-content></ng-content></ng-template>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
40789
+ }
40790
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabComponent, decorators: [{
40791
+ type: Component,
40792
+ args: [{
40793
+ selector: 'ui-tab',
40794
+ standalone: true,
40795
+ imports: [CommonModule],
40796
+ template: `<ng-template #tpl><ng-content></ng-content></ng-template>`
40797
+ }]
40798
+ }], propDecorators: { label: [{
40799
+ type: Input
40800
+ }], name: [{
40801
+ type: Input
40802
+ }], disabled: [{
40803
+ type: Input
40804
+ }], badge: [{
40805
+ type: Input
40806
+ }], badgeTone: [{
40807
+ type: Input
40808
+ }], template: [{
40809
+ type: ViewChild,
40810
+ args: ['tpl', { static: true }]
40811
+ }] } });
40812
+ class UiTabsComponent {
40813
+ tabQuery;
40814
+ activeTab;
40815
+ activeTabChange = new EventEmitter();
40816
+ // Internal state if uncontrolled
40817
+ _internalIndex = 0;
40818
+ tabs = [];
40819
+ ngAfterContentInit() {
40820
+ this.tabs = this.tabQuery.toArray();
40821
+ }
40822
+ isActive(tab) {
40823
+ if (this.activeTab !== undefined) {
40824
+ return this.activeTab === (tab.name || tab.label);
40825
+ }
40826
+ return this.tabs.indexOf(tab) === this._internalIndex;
40827
+ }
40828
+ activate(tab) {
40829
+ if (tab.disabled)
40830
+ return;
40831
+ const key = tab.name || tab.label;
40832
+ if (this.activeTab !== undefined) {
40833
+ this.activeTabChange.emit(key);
40834
+ }
40835
+ else {
40836
+ this._internalIndex = this.tabs.indexOf(tab);
40837
+ }
40838
+ }
40839
+ get activeTemplate() {
40840
+ if (this.tabs.length === 0)
40841
+ return null;
40842
+ let activeTab;
40843
+ if (this.activeTab !== undefined) {
40844
+ activeTab = this.tabs.find(t => (t.name || t.label) === this.activeTab);
40845
+ }
40846
+ else {
40847
+ activeTab = this.tabs[this._internalIndex];
40848
+ }
40849
+ return activeTab?.template || null;
40850
+ }
40851
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
40852
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: UiTabsComponent, isStandalone: true, selector: "ui-tabs", inputs: { activeTab: "activeTab" }, outputs: { activeTabChange: "activeTabChange" }, queries: [{ propertyName: "tabQuery", predicate: UiTabComponent }], ngImport: i0, template: `
40853
+ <div class="flex flex-col min-h-0 h-full">
40854
+ <div class="flex items-center gap-1 px-3 border-b border-slate-200 bg-white shrink-0">
40855
+ <button
40856
+ *ngFor="let tab of tabs; let i = index"
40857
+ (click)="activate(tab)"
40858
+ class="relative h-10 px-3 text-xs font-semibold transition-colors rounded-t-md border-b-2"
40859
+ [class.text-accent-600]="isActive(tab)"
40860
+ [class.border-accent-600]="isActive(tab)"
40861
+ [class.text-ink-500]="!isActive(tab)"
40862
+ [class.border-transparent]="!isActive(tab)"
40863
+ [class.hover:text-ink-700]="!isActive(tab)"
40864
+ [disabled]="tab.disabled"
40865
+ >
40866
+ <span class="flex items-center gap-2">
40867
+ {{ tab.label }}
40868
+ <span *ngIf="tab.badge" class="text-[10px] px-1.5 py-0.5 rounded-full border"
40869
+ [class.bg-accent-50]="tab.badgeTone === 'accent'"
40870
+ [class.border-accent-200]="tab.badgeTone === 'accent'"
40871
+ [class.text-accent-700]="tab.badgeTone === 'accent'"
40872
+ [class.bg-slate-100]="tab.badgeTone === 'neutral'"
40873
+ [class.border-slate-200]="tab.badgeTone === 'neutral'"
40874
+ [class.text-slate-600]="tab.badgeTone === 'neutral'">
40875
+ {{ tab.badge }}
40876
+ </span>
40877
+ </span>
40878
+ </button>
40879
+ </div>
40880
+
40881
+ <div class="flex-1 min-h-0 overflow-hidden bg-slate-50/30">
40882
+ <ng-container *ngIf="activeTemplate">
40883
+ <ng-container *ngTemplateOutlet="activeTemplate"></ng-container>
40884
+ </ng-container>
40885
+ </div>
40886
+ </div>
40887
+ `, isInline: true, styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
40888
+ }
40889
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabsComponent, decorators: [{
40890
+ type: Component,
40891
+ args: [{ selector: 'ui-tabs', standalone: true, imports: [CommonModule], template: `
40892
+ <div class="flex flex-col min-h-0 h-full">
40893
+ <div class="flex items-center gap-1 px-3 border-b border-slate-200 bg-white shrink-0">
40894
+ <button
40895
+ *ngFor="let tab of tabs; let i = index"
40896
+ (click)="activate(tab)"
40897
+ class="relative h-10 px-3 text-xs font-semibold transition-colors rounded-t-md border-b-2"
40898
+ [class.text-accent-600]="isActive(tab)"
40899
+ [class.border-accent-600]="isActive(tab)"
40900
+ [class.text-ink-500]="!isActive(tab)"
40901
+ [class.border-transparent]="!isActive(tab)"
40902
+ [class.hover:text-ink-700]="!isActive(tab)"
40903
+ [disabled]="tab.disabled"
40904
+ >
40905
+ <span class="flex items-center gap-2">
40906
+ {{ tab.label }}
40907
+ <span *ngIf="tab.badge" class="text-[10px] px-1.5 py-0.5 rounded-full border"
40908
+ [class.bg-accent-50]="tab.badgeTone === 'accent'"
40909
+ [class.border-accent-200]="tab.badgeTone === 'accent'"
40910
+ [class.text-accent-700]="tab.badgeTone === 'accent'"
40911
+ [class.bg-slate-100]="tab.badgeTone === 'neutral'"
40912
+ [class.border-slate-200]="tab.badgeTone === 'neutral'"
40913
+ [class.text-slate-600]="tab.badgeTone === 'neutral'">
40914
+ {{ tab.badge }}
40915
+ </span>
40916
+ </span>
40917
+ </button>
40918
+ </div>
40919
+
40920
+ <div class="flex-1 min-h-0 overflow-hidden bg-slate-50/30">
40921
+ <ng-container *ngIf="activeTemplate">
40922
+ <ng-container *ngTemplateOutlet="activeTemplate"></ng-container>
40923
+ </ng-container>
40924
+ </div>
40925
+ </div>
40926
+ `, styles: [":host{display:block;height:100%}\n"] }]
40927
+ }], propDecorators: { tabQuery: [{
40928
+ type: ContentChildren,
40929
+ args: [UiTabComponent]
40930
+ }], activeTab: [{
40931
+ type: Input
40932
+ }], activeTabChange: [{
40933
+ type: Output
40934
+ }] } });
40935
+
40150
40936
  class AiToolRegistryService {
40151
40937
  state;
40152
40938
  widgetDefs;