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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,11 +5415,7 @@ 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();
@@ -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 {};
@@ -5678,11 +6194,7 @@ class FormJourneyViewerComponent {
5678
6194
  return { ok: false, reason: 'unknown-page' };
5679
6195
  }
5680
6196
  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
- };
6197
+ const validation = this.renderer.getValidationResult(true);
5686
6198
  this.formValidationChange.emit(validation);
5687
6199
  if (!validation.isValid) {
5688
6200
  return { ok: false, reason: 'validation' };
@@ -7684,6 +8196,7 @@ class LayoutCanvasComponent {
7684
8196
  showLiveSchemaEditor = signal(false);
7685
8197
  liveSchemaEditorText = signal('');
7686
8198
  liveSchemaEditorError = signal('');
8199
+ openContextSubmenu = signal(null);
7687
8200
  liveSchemaEditorOptions = {
7688
8201
  fontSize: 12,
7689
8202
  lineNumbersMinChars: 3,
@@ -7801,6 +8314,7 @@ class LayoutCanvasComponent {
7801
8314
  this.closeContextMenu();
7802
8315
  }
7803
8316
  closeContextMenu() {
8317
+ this.openContextSubmenu.set(null);
7804
8318
  this.state.closeContextMenu();
7805
8319
  }
7806
8320
  groupSelected() {
@@ -7811,6 +8325,46 @@ class LayoutCanvasComponent {
7811
8325
  this.state.ungroupSelectedFields();
7812
8326
  this.closeContextMenu();
7813
8327
  }
8328
+ hasStructuralInsertActions() {
8329
+ return this.state.canInsertColumnBeforeSelection()
8330
+ || this.state.canInsertColumnAfterSelection()
8331
+ || this.state.canInsertRowInSelectedColumn()
8332
+ || this.state.canInsertRowBeforeSelection()
8333
+ || this.state.canInsertRowAfterSelection()
8334
+ || this.state.canArmFieldInsertBeforeSelection()
8335
+ || this.state.canArmFieldInsertAfterSelection();
8336
+ }
8337
+ toggleInsertSubmenu() {
8338
+ this.openContextSubmenu.update(current => current === 'insert' ? null : 'insert');
8339
+ }
8340
+ insertColumnBeforeSelection() {
8341
+ this.state.insertColumnBeforeSelection();
8342
+ this.closeContextMenu();
8343
+ }
8344
+ insertColumnAfterSelection() {
8345
+ this.state.insertColumnAfterSelection();
8346
+ this.closeContextMenu();
8347
+ }
8348
+ insertRowInSelectedColumn() {
8349
+ this.state.insertRowInSelectedColumn();
8350
+ this.closeContextMenu();
8351
+ }
8352
+ insertRowBeforeSelection() {
8353
+ this.state.insertRowBeforeSelection();
8354
+ this.closeContextMenu();
8355
+ }
8356
+ insertRowAfterSelection() {
8357
+ this.state.insertRowAfterSelection();
8358
+ this.closeContextMenu();
8359
+ }
8360
+ armFieldInsertBeforeSelection() {
8361
+ this.state.armFieldInsertBeforeSelection();
8362
+ this.closeContextMenu();
8363
+ }
8364
+ armFieldInsertAfterSelection() {
8365
+ this.state.armFieldInsertAfterSelection();
8366
+ this.closeContextMenu();
8367
+ }
7814
8368
  onCanvasContextMenu(event) {
7815
8369
  event.preventDefault();
7816
8370
  event.stopPropagation();
@@ -8249,6 +8803,56 @@ class LayoutCanvasComponent {
8249
8803
  <span>Delete</span>
8250
8804
  <span class="opacity-60 text-[10px]">Del</span>
8251
8805
  </button>
8806
+ <div *ngIf="hasStructuralInsertActions()" class="relative">
8807
+ <div class="h-px bg-border-default my-1"></div>
8808
+ <button type="button"
8809
+ aria-label="Open insert submenu"
8810
+ class="flex w-full items-center justify-between px-3 py-2 text-left text-text-primary hover:bg-slate-50"
8811
+ [attr.aria-expanded]="openContextSubmenu() === 'insert'"
8812
+ (click)="toggleInsertSubmenu(); $event.stopPropagation()">
8813
+ <span>Insert…</span>
8814
+ <span class="text-[10px] opacity-60">{{ openContextSubmenu() === 'insert' ? '‹' : '›' }}</span>
8815
+ </button>
8816
+
8817
+ <div *ngIf="openContextSubmenu() === 'insert'"
8818
+ class="absolute left-full top-0 ml-1 min-w-[210px] rounded-md border border-border-default bg-surface-default shadow-popover text-[12px]">
8819
+ <button *ngIf="!state.isReadOnly() && state.canInsertColumnBeforeSelection()" type="button"
8820
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8821
+ (click)="insertColumnBeforeSelection()">
8822
+ Add column left
8823
+ </button>
8824
+ <button *ngIf="!state.isReadOnly() && state.canInsertColumnAfterSelection()" type="button"
8825
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8826
+ (click)="insertColumnAfterSelection()">
8827
+ Add column right
8828
+ </button>
8829
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowInSelectedColumn()" type="button"
8830
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8831
+ (click)="insertRowInSelectedColumn()">
8832
+ Add row in column
8833
+ </button>
8834
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowBeforeSelection()" type="button"
8835
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8836
+ (click)="insertRowBeforeSelection()">
8837
+ Add row above
8838
+ </button>
8839
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowAfterSelection()" type="button"
8840
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8841
+ (click)="insertRowAfterSelection()">
8842
+ Add row below
8843
+ </button>
8844
+ <button *ngIf="!state.isReadOnly() && state.canArmFieldInsertBeforeSelection()" type="button"
8845
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8846
+ (click)="armFieldInsertBeforeSelection()">
8847
+ Next widget above
8848
+ </button>
8849
+ <button *ngIf="!state.isReadOnly() && state.canArmFieldInsertAfterSelection()" type="button"
8850
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
8851
+ (click)="armFieldInsertAfterSelection()">
8852
+ Next widget below
8853
+ </button>
8854
+ </div>
8855
+ </div>
8252
8856
  <div class="h-px bg-border-default my-1"></div>
8253
8857
  <button *ngIf="!state.isReadOnly()" type="button"
8254
8858
  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 +9099,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
8495
9099
  <span>Delete</span>
8496
9100
  <span class="opacity-60 text-[10px]">Del</span>
8497
9101
  </button>
9102
+ <div *ngIf="hasStructuralInsertActions()" class="relative">
9103
+ <div class="h-px bg-border-default my-1"></div>
9104
+ <button type="button"
9105
+ aria-label="Open insert submenu"
9106
+ class="flex w-full items-center justify-between px-3 py-2 text-left text-text-primary hover:bg-slate-50"
9107
+ [attr.aria-expanded]="openContextSubmenu() === 'insert'"
9108
+ (click)="toggleInsertSubmenu(); $event.stopPropagation()">
9109
+ <span>Insert…</span>
9110
+ <span class="text-[10px] opacity-60">{{ openContextSubmenu() === 'insert' ? '‹' : '›' }}</span>
9111
+ </button>
9112
+
9113
+ <div *ngIf="openContextSubmenu() === 'insert'"
9114
+ class="absolute left-full top-0 ml-1 min-w-[210px] rounded-md border border-border-default bg-surface-default shadow-popover text-[12px]">
9115
+ <button *ngIf="!state.isReadOnly() && state.canInsertColumnBeforeSelection()" type="button"
9116
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9117
+ (click)="insertColumnBeforeSelection()">
9118
+ Add column left
9119
+ </button>
9120
+ <button *ngIf="!state.isReadOnly() && state.canInsertColumnAfterSelection()" type="button"
9121
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9122
+ (click)="insertColumnAfterSelection()">
9123
+ Add column right
9124
+ </button>
9125
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowInSelectedColumn()" type="button"
9126
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9127
+ (click)="insertRowInSelectedColumn()">
9128
+ Add row in column
9129
+ </button>
9130
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowBeforeSelection()" type="button"
9131
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9132
+ (click)="insertRowBeforeSelection()">
9133
+ Add row above
9134
+ </button>
9135
+ <button *ngIf="!state.isReadOnly() && state.canInsertRowAfterSelection()" type="button"
9136
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9137
+ (click)="insertRowAfterSelection()">
9138
+ Add row below
9139
+ </button>
9140
+ <button *ngIf="!state.isReadOnly() && state.canArmFieldInsertBeforeSelection()" type="button"
9141
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9142
+ (click)="armFieldInsertBeforeSelection()">
9143
+ Next widget above
9144
+ </button>
9145
+ <button *ngIf="!state.isReadOnly() && state.canArmFieldInsertAfterSelection()" type="button"
9146
+ class="w-full px-3 py-2 text-left hover:bg-slate-50 text-text-primary"
9147
+ (click)="armFieldInsertAfterSelection()">
9148
+ Next widget below
9149
+ </button>
9150
+ </div>
9151
+ </div>
8498
9152
  <div class="h-px bg-border-default my-1"></div>
8499
9153
  <button *ngIf="!state.isReadOnly()" type="button"
8500
9154
  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 +10260,11 @@ class DynamicPropertiesComponent {
9606
10260
  onPropertyChange;
9607
10261
  designerCtx = inject(DesignerContext);
9608
10262
  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' }
10263
+ { label: 'Built-in', value: 'builtin' },
10264
+ { label: 'Expression', value: 'expression' }
9616
10265
  ];
9617
- conditionalTypeOptions = [
9618
- { label: 'Always Visible', value: 'always' },
9619
- { label: 'Field Equals', value: 'equals' },
9620
- { label: 'Field Not Equals', value: 'notEquals' }
10266
+ builtinValidatorOptions = [
10267
+ { label: 'Email', value: 'email' }
9621
10268
  ];
9622
10269
  get properties() {
9623
10270
  if (!this.config)
@@ -9954,18 +10601,15 @@ class DynamicPropertiesComponent {
9954
10601
  addValidator(path) {
9955
10602
  if (this.readOnly)
9956
10603
  return;
9957
- const validators = this.getValue(path) || [];
9958
- validators.push({
9959
- name: 'required',
9960
- message: 'This field is required'
9961
- });
10604
+ const validators = [...(this.getValue(path) || [])];
10605
+ validators.push(this.createDefaultValidationRule());
9962
10606
  this.setValue(path, validators);
9963
10607
  this.handleFieldChange();
9964
10608
  }
9965
10609
  removeValidator(path, index) {
9966
10610
  if (this.readOnly)
9967
10611
  return;
9968
- const validators = this.getValue(path) || [];
10612
+ const validators = [...(this.getValue(path) || [])];
9969
10613
  validators.splice(index, 1);
9970
10614
  this.setValue(path, validators);
9971
10615
  this.handleFieldChange();
@@ -9973,57 +10617,47 @@ class DynamicPropertiesComponent {
9973
10617
  updateValidator(path, index, field, value) {
9974
10618
  if (this.readOnly)
9975
10619
  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);
10620
+ const validators = [...(this.getValue(path) || [])];
10621
+ const currentRule = validators[index];
10622
+ if (!currentRule) {
10623
+ return;
9993
10624
  }
10625
+ let nextRule = { ...currentRule };
10626
+ if (field === 'type') {
10627
+ nextRule = value === 'expression'
10628
+ ? {
10629
+ type: 'expression',
10630
+ expression: 'return true;',
10631
+ message: 'Validation failed.'
10632
+ }
10633
+ : this.createDefaultValidationRule();
10634
+ }
10635
+ else if (field === 'name') {
10636
+ nextRule.name = String(value);
10637
+ nextRule.message = this.defaultValidationMessage(nextRule);
10638
+ }
10639
+ else if (field === 'expression') {
10640
+ nextRule.expression = String(value);
10641
+ }
10642
+ else if (field === 'message') {
10643
+ nextRule.message = String(value);
10644
+ }
10645
+ validators[index] = nextRule;
10646
+ this.setValue(path, validators);
10647
+ this.handleFieldChange();
9994
10648
  }
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'
10649
+ createDefaultValidationRule() {
10650
+ return {
10651
+ type: 'builtin',
10652
+ name: 'email',
10653
+ message: 'Enter a valid email address.'
10004
10654
  };
10005
- return labels[name] || name;
10006
10655
  }
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
- }
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);
10656
+ defaultValidationMessage(rule) {
10657
+ if (rule.type === 'builtin' && rule.name === 'email') {
10658
+ return 'Enter a valid email address.';
10659
+ }
10660
+ return 'Validation failed.';
10027
10661
  }
10028
10662
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DynamicPropertiesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10029
10663
  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 +10814,27 @@ class DynamicPropertiesComponent {
10180
10814
  </div>
10181
10815
  <div class="space-y-2">
10182
10816
  <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()"
10817
+ <select [ngModel]="val.type || 'builtin'"
10818
+ (ngModelChange)="updateValidator(field.key, i, 'type', $event)"
10185
10819
  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
10820
  <option *ngFor="let option of validatorTypeOptions" [value]="option.value">{{ option.label }}</option>
10187
10821
  </select>
10188
- <input *ngIf="val.type !== 'required' && val.type !== 'email'"
10822
+ <select *ngIf="(val.type || 'builtin') === 'builtin'"
10823
+ [ngModel]="val.name || 'email'"
10824
+ (ngModelChange)="updateValidator(field.key, i, 'name', $event)"
10825
+ 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">
10826
+ <option *ngFor="let option of builtinValidatorOptions" [value]="option.value">{{ option.label }}</option>
10827
+ </select>
10828
+ <input *ngIf="val.type === 'expression'"
10189
10829
  type="text"
10190
- [(ngModel)]="val.value"
10191
- (blur)="handleFieldChange()"
10192
- placeholder="Value"
10830
+ [ngModel]="val.expression || ''"
10831
+ (ngModelChange)="updateValidator(field.key, i, 'expression', $event)"
10832
+ placeholder="return true;"
10833
+ 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">
10834
+ <input type="text"
10835
+ [ngModel]="val.message || ''"
10836
+ (ngModelChange)="updateValidator(field.key, i, 'message', $event)"
10837
+ placeholder="Validation message"
10193
10838
  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
10839
  <button type="button"
10195
10840
  (click)="removeValidator(field.key, i)"
@@ -10201,36 +10846,6 @@ class DynamicPropertiesComponent {
10201
10846
  </div>
10202
10847
  </ui-field-wrapper>
10203
10848
 
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
10849
  <!-- Field Reference -->
10235
10850
  <ui-field-wrapper *ngIf="field.type === 'field-reference'" [label]="field.label || ''" [helpText]="field.helpText || ''">
10236
10851
  <select [ngModel]="getValue(field.key) || ''"
@@ -10492,16 +11107,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
10492
11107
  </div>
10493
11108
  <div class="space-y-2">
10494
11109
  <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()"
11110
+ <select [ngModel]="val.type || 'builtin'"
11111
+ (ngModelChange)="updateValidator(field.key, i, 'type', $event)"
10497
11112
  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
11113
  <option *ngFor="let option of validatorTypeOptions" [value]="option.value">{{ option.label }}</option>
10499
11114
  </select>
10500
- <input *ngIf="val.type !== 'required' && val.type !== 'email'"
11115
+ <select *ngIf="(val.type || 'builtin') === 'builtin'"
11116
+ [ngModel]="val.name || 'email'"
11117
+ (ngModelChange)="updateValidator(field.key, i, 'name', $event)"
11118
+ 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">
11119
+ <option *ngFor="let option of builtinValidatorOptions" [value]="option.value">{{ option.label }}</option>
11120
+ </select>
11121
+ <input *ngIf="val.type === 'expression'"
10501
11122
  type="text"
10502
- [(ngModel)]="val.value"
10503
- (blur)="handleFieldChange()"
10504
- placeholder="Value"
11123
+ [ngModel]="val.expression || ''"
11124
+ (ngModelChange)="updateValidator(field.key, i, 'expression', $event)"
11125
+ placeholder="return true;"
11126
+ 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">
11127
+ <input type="text"
11128
+ [ngModel]="val.message || ''"
11129
+ (ngModelChange)="updateValidator(field.key, i, 'message', $event)"
11130
+ placeholder="Validation message"
10505
11131
  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
11132
  <button type="button"
10507
11133
  (click)="removeValidator(field.key, i)"
@@ -10513,36 +11139,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
10513
11139
  </div>
10514
11140
  </ui-field-wrapper>
10515
11141
 
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
11142
  <!-- Field Reference -->
10547
11143
  <ui-field-wrapper *ngIf="field.type === 'field-reference'" [label]="field.label || ''" [helpText]="field.helpText || ''">
10548
11144
  <select [ngModel]="getValue(field.key) || ''"
@@ -13616,6 +14212,161 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
13616
14212
  `, 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
14213
  }] });
13618
14214
 
14215
+ class InspectorTransformSectionComponent {
14216
+ style = input({});
14217
+ styleChange = output();
14218
+ numberValue(key, fallback) {
14219
+ const value = this.style()?.[key];
14220
+ if (typeof value === 'number' && Number.isFinite(value)) {
14221
+ return value;
14222
+ }
14223
+ if (typeof value === 'string' && value.trim().length > 0) {
14224
+ const parsed = Number(value);
14225
+ if (Number.isFinite(parsed)) {
14226
+ return parsed;
14227
+ }
14228
+ }
14229
+ return fallback;
14230
+ }
14231
+ updateTransform(key, value) {
14232
+ const parsed = typeof value === 'number' ? value : Number(value);
14233
+ if (!Number.isFinite(parsed)) {
14234
+ return;
14235
+ }
14236
+ this.styleChange.emit({
14237
+ ...(this.style() ?? {}),
14238
+ [key]: parsed
14239
+ });
14240
+ }
14241
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: InspectorTransformSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14242
+ 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: `
14243
+ <div class="flex flex-col gap-3">
14244
+ <ui-range-number
14245
+ label="Translate X"
14246
+ prefix="X"
14247
+ hint="px"
14248
+ helpText="Move the widget horizontally."
14249
+ [min]="-200"
14250
+ [max]="200"
14251
+ [step]="1"
14252
+ [value]="numberValue('transformX', 0)"
14253
+ (valueChange)="updateTransform('transformX', $event)">
14254
+ </ui-range-number>
14255
+
14256
+ <ui-range-number
14257
+ label="Translate Y"
14258
+ prefix="Y"
14259
+ hint="px"
14260
+ helpText="Move the widget vertically."
14261
+ [min]="-200"
14262
+ [max]="200"
14263
+ [step]="1"
14264
+ [value]="numberValue('transformY', 0)"
14265
+ (valueChange)="updateTransform('transformY', $event)">
14266
+ </ui-range-number>
14267
+
14268
+ <ui-range-number
14269
+ label="Translate Z"
14270
+ prefix="Z"
14271
+ hint="px"
14272
+ helpText="Move the widget on the z-axis for 3D transforms."
14273
+ [min]="-200"
14274
+ [max]="200"
14275
+ [step]="1"
14276
+ [value]="numberValue('transformZ', 0)"
14277
+ (valueChange)="updateTransform('transformZ', $event)">
14278
+ </ui-range-number>
14279
+
14280
+ <ui-input
14281
+ label="Rotate"
14282
+ hint="deg"
14283
+ helpText="Rotate the widget in degrees."
14284
+ type="number"
14285
+ [step]="1"
14286
+ [model]="numberValue('rotate', 0)"
14287
+ (modelChange)="updateTransform('rotate', $event)">
14288
+ </ui-input>
14289
+
14290
+ <ui-input
14291
+ label="Scale"
14292
+ helpText="Scale the widget uniformly."
14293
+ type="number"
14294
+ [min]="0"
14295
+ [step]="0.1"
14296
+ [model]="numberValue('scale', 1)"
14297
+ (modelChange)="updateTransform('scale', $event)">
14298
+ </ui-input>
14299
+ </div>
14300
+ `, 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"] }] });
14301
+ }
14302
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: InspectorTransformSectionComponent, decorators: [{
14303
+ type: Component,
14304
+ args: [{
14305
+ selector: 'inspector-transform-section',
14306
+ standalone: true,
14307
+ imports: [CommonModule, UiInputComponent, UiRangeNumberComponent],
14308
+ template: `
14309
+ <div class="flex flex-col gap-3">
14310
+ <ui-range-number
14311
+ label="Translate X"
14312
+ prefix="X"
14313
+ hint="px"
14314
+ helpText="Move the widget horizontally."
14315
+ [min]="-200"
14316
+ [max]="200"
14317
+ [step]="1"
14318
+ [value]="numberValue('transformX', 0)"
14319
+ (valueChange)="updateTransform('transformX', $event)">
14320
+ </ui-range-number>
14321
+
14322
+ <ui-range-number
14323
+ label="Translate Y"
14324
+ prefix="Y"
14325
+ hint="px"
14326
+ helpText="Move the widget vertically."
14327
+ [min]="-200"
14328
+ [max]="200"
14329
+ [step]="1"
14330
+ [value]="numberValue('transformY', 0)"
14331
+ (valueChange)="updateTransform('transformY', $event)">
14332
+ </ui-range-number>
14333
+
14334
+ <ui-range-number
14335
+ label="Translate Z"
14336
+ prefix="Z"
14337
+ hint="px"
14338
+ helpText="Move the widget on the z-axis for 3D transforms."
14339
+ [min]="-200"
14340
+ [max]="200"
14341
+ [step]="1"
14342
+ [value]="numberValue('transformZ', 0)"
14343
+ (valueChange)="updateTransform('transformZ', $event)">
14344
+ </ui-range-number>
14345
+
14346
+ <ui-input
14347
+ label="Rotate"
14348
+ hint="deg"
14349
+ helpText="Rotate the widget in degrees."
14350
+ type="number"
14351
+ [step]="1"
14352
+ [model]="numberValue('rotate', 0)"
14353
+ (modelChange)="updateTransform('rotate', $event)">
14354
+ </ui-input>
14355
+
14356
+ <ui-input
14357
+ label="Scale"
14358
+ helpText="Scale the widget uniformly."
14359
+ type="number"
14360
+ [min]="0"
14361
+ [step]="0.1"
14362
+ [model]="numberValue('scale', 1)"
14363
+ (modelChange)="updateTransform('scale', $event)">
14364
+ </ui-input>
14365
+ </div>
14366
+ `
14367
+ }]
14368
+ }] });
14369
+
13619
14370
  const DEDICATED_STYLE_KEYS = new Set([
13620
14371
  'alignItems',
13621
14372
  'alignSelf',
@@ -13684,7 +14435,13 @@ const DEDICATED_STYLE_KEYS = new Set([
13684
14435
  'right',
13685
14436
  'textAlign',
13686
14437
  'textDecoration',
14438
+ 'transform',
14439
+ 'transformX',
14440
+ 'transformY',
14441
+ 'transformZ',
13687
14442
  'top',
14443
+ 'rotate',
14444
+ 'scale',
13688
14445
  'width',
13689
14446
  'zIndex'
13690
14447
  ]);
@@ -17334,6 +18091,15 @@ class WidgetInspectorComponent {
17334
18091
  </div>
17335
18092
  </ui-accordion>
17336
18093
 
18094
+ <ui-accordion title="Transform" [expanded]="false">
18095
+ <div [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
18096
+ <inspector-transform-section
18097
+ [style]="currentStyle()"
18098
+ (styleChange)="onStyleChange($event)">
18099
+ </inspector-transform-section>
18100
+ </div>
18101
+ </ui-accordion>
18102
+
17337
18103
  <ui-accordion title="Position" [expanded]="false">
17338
18104
  <div [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
17339
18105
  <inspector-position-section
@@ -17366,7 +18132,7 @@ class WidgetInspectorComponent {
17366
18132
  [config]="inspectorField()"
17367
18133
  [allFields]="stateService.getSelectedScopeFields()"
17368
18134
  [readOnly]="readOnly()"
17369
- [excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position', 'Effects', 'Advanced']"
18135
+ [excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position', 'Effects', 'Transform', 'Advanced']"
17370
18136
  (configChange)="onFieldConfigChange($event)">
17371
18137
  </app-dynamic-properties>
17372
18138
  </div>
@@ -17406,7 +18172,7 @@ class WidgetInspectorComponent {
17406
18172
  </div>
17407
18173
  `, 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
18174
  // 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:
18175
+ 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
18176
  // Functional Panels (All restored)
17411
18177
  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
18178
  }
@@ -17425,6 +18191,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17425
18191
  InspectorBackgroundsSectionComponent,
17426
18192
  InspectorEffectsSectionComponent,
17427
18193
  InspectorPositionSectionComponent,
18194
+ InspectorTransformSectionComponent,
17428
18195
  InspectorAdvancedSectionComponent,
17429
18196
  // Functional Panels (All restored)
17430
18197
  DynamicPropertiesComponent,
@@ -17541,6 +18308,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17541
18308
  </div>
17542
18309
  </ui-accordion>
17543
18310
 
18311
+ <ui-accordion title="Transform" [expanded]="false">
18312
+ <div [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
18313
+ <inspector-transform-section
18314
+ [style]="currentStyle()"
18315
+ (styleChange)="onStyleChange($event)">
18316
+ </inspector-transform-section>
18317
+ </div>
18318
+ </ui-accordion>
18319
+
17544
18320
  <ui-accordion title="Position" [expanded]="false">
17545
18321
  <div [class.pointer-events-none]="readOnly()" [class.opacity-60]="readOnly()">
17546
18322
  <inspector-position-section
@@ -17573,7 +18349,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17573
18349
  [config]="inspectorField()"
17574
18350
  [allFields]="stateService.getSelectedScopeFields()"
17575
18351
  [readOnly]="readOnly()"
17576
- [excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position', 'Effects', 'Advanced']"
18352
+ [excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position', 'Effects', 'Transform', 'Advanced']"
17577
18353
  (configChange)="onFieldConfigChange($event)">
17578
18354
  </app-dynamic-properties>
17579
18355
  </div>
@@ -17683,39 +18459,6 @@ class FormSettingsInspectorComponent {
17683
18459
  </div>
17684
18460
 
17685
18461
  <div class="h-px bg-border-default my-1"></div>
17686
-
17687
- <div class="flex flex-col gap-1">
17688
- <label class="text-[10px] text-text-primary opacity-70 uppercase font-semibold">Submit Button</label>
17689
- <input
17690
- [ngModel]="schema().submitButtonText"
17691
- [disabled]="readOnly()"
17692
- (ngModelChange)="updateSettings('submitButtonText', $event)"
17693
- placeholder="Submit"
17694
- 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
- </div>
17696
-
17697
- <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
18462
  </div>
17720
18463
  }
17721
18464
 
@@ -17769,7 +18512,7 @@ class FormSettingsInspectorComponent {
17769
18512
 
17770
18513
  </div>
17771
18514
  </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"] }] });
18515
+ `, 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"] }] });
17773
18516
  }
17774
18517
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: FormSettingsInspectorComponent, decorators: [{
17775
18518
  type: Component,
@@ -17835,39 +18578,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17835
18578
  </div>
17836
18579
 
17837
18580
  <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
- }
17870
- </div>
17871
18581
  </div>
17872
18582
  }
17873
18583
 
@@ -17924,165 +18634,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17924
18634
  `, 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
18635
  }] });
17926
18636
 
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
18637
  class PropertiesPanelComponent {
18084
18638
  state;
18085
18639
  injector;
18640
+ layoutInspectorTabs = ['Style', 'Settings'];
18641
+ activeLayoutInspectorTab = signal('Style');
18086
18642
  spacingOptions = [
18087
18643
  { label: 'None', value: 'none' },
18088
18644
  { label: 'XS', value: 'xs' },
@@ -18105,9 +18661,11 @@ class PropertiesPanelComponent {
18105
18661
  // No need to initialize form settings manually anymore, component inputs handle it
18106
18662
  // Track the previous widget to avoid unnecessary re-renders
18107
18663
  let previousWidgetId = null;
18664
+ let previousLayoutNodeKey = null;
18108
18665
  effect(() => {
18109
18666
  const node = this.state.selectedNode();
18110
18667
  const currentWidgetId = (node && node.type === 'widget') ? node.id : null;
18668
+ const currentLayoutNodeKey = (node && node.type !== 'widget') ? `${node.type}:${node.id}` : null;
18111
18669
  // Only re-render if the widget actually changed
18112
18670
  if (currentWidgetId !== previousWidgetId) {
18113
18671
  previousWidgetId = currentWidgetId;
@@ -18120,6 +18678,12 @@ class PropertiesPanelComponent {
18120
18678
  this.inspectorContainer.clear();
18121
18679
  }
18122
18680
  }
18681
+ if (currentLayoutNodeKey !== previousLayoutNodeKey) {
18682
+ previousLayoutNodeKey = currentLayoutNodeKey;
18683
+ if (currentLayoutNodeKey) {
18684
+ this.activeLayoutInspectorTab.set('Style');
18685
+ }
18686
+ }
18123
18687
  });
18124
18688
  }
18125
18689
  responsiveWidthOptions(includeInherit) {
@@ -18359,7 +18923,7 @@ class PropertiesPanelComponent {
18359
18923
  this.state.selectNode(null);
18360
18924
  }
18361
18925
  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: `
18926
+ 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
18927
  <div class="properties-shell h-full border-l border-border-default bg-surface-default flex flex-col font-sans text-[12px]">
18364
18928
 
18365
18929
  <div *ngIf="state.selectedNode() as node; else noSelection" class="flex flex-col h-full">
@@ -18386,239 +18950,182 @@ class PropertiesPanelComponent {
18386
18950
  </div>
18387
18951
  </div>
18388
18952
 
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>
18953
+ <div class="flex items-center px-2 pt-2 border-b border-border-default bg-surface-default">
18954
+ @for (tab of layoutInspectorTabs; track tab) {
18955
+ <button
18956
+ type="button"
18957
+ (click)="activeLayoutInspectorTab.set(tab)"
18958
+ [class]="activeLayoutInspectorTab() === tab
18959
+ ? 'px-2 pb-2 font-semibold text-primary-500 border-b-2 border-b-primary-500'
18960
+ : 'px-2 pb-2 text-text-primary opacity-60 hover:opacity-100 hover:text-text-primary border-b-2 border-transparent'">
18961
+ {{ tab }}
18962
+ </button>
18963
+ }
18964
+ </div>
18561
18965
 
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>
18966
+ <div class="flex-1 overflow-y-auto custom-scrollbar bg-surface-default">
18967
+ @if (activeLayoutInspectorTab() === 'Style') {
18968
+ <div class="flex flex-col" [class.pointer-events-none]="state.isReadOnly()" [class.opacity-60]="state.isReadOnly()">
18969
+ <ui-accordion title="Layout" [expanded]="true">
18970
+ <inspector-layout-section
18971
+ [style]="nodeStyle(node)"
18972
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18973
+ </inspector-layout-section>
18974
+ </ui-accordion>
18975
+
18976
+ <ui-accordion title="Spacing" [expanded]="false">
18977
+ <inspector-spacing-section
18978
+ [style]="nodeStyle(node)"
18979
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18980
+ </inspector-spacing-section>
18981
+ </ui-accordion>
18982
+
18983
+ <ui-accordion title="Size" [expanded]="false">
18984
+ <inspector-size-section
18985
+ [style]="nodeStyle(node)"
18986
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18987
+ </inspector-size-section>
18988
+ </ui-accordion>
18989
+
18990
+ <ui-accordion title="Backgrounds" [expanded]="false">
18991
+ <inspector-backgrounds-section
18992
+ [style]="nodeStyle(node)"
18993
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
18994
+ </inspector-backgrounds-section>
18995
+ </ui-accordion>
18996
+
18997
+ <ui-accordion title="Borders" [expanded]="false">
18998
+ <inspector-borders-section
18999
+ [style]="nodeStyle(node)"
19000
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19001
+ </inspector-borders-section>
19002
+ </ui-accordion>
19003
+
19004
+ <ui-accordion title="Effects" [expanded]="false">
19005
+ <inspector-effects-section
19006
+ [style]="nodeStyle(node)"
19007
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19008
+ </inspector-effects-section>
19009
+ </ui-accordion>
19010
+
19011
+ <ui-accordion title="Advanced" [expanded]="false">
19012
+ <inspector-advanced-section
19013
+ [style]="nodeStyle(node)"
19014
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19015
+ </inspector-advanced-section>
19016
+ </ui-accordion>
19017
+
19018
+ <div class="h-10"></div>
19019
+ </div>
19020
+ }
19021
+
19022
+ @if (activeLayoutInspectorTab() === 'Settings') {
19023
+ <div class="p-4 flex flex-col gap-4">
19024
+ @if (node.type === 'row') {
19025
+ <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm"
19026
+ [class.opacity-60]="state.isReadOnly()"
19027
+ [class.pointer-events-none]="state.isReadOnly()">
19028
+ <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Column Structure</div>
19029
+ <div class="grid grid-cols-3 gap-2 mb-3">
19030
+ <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">
19031
+ <div class="flex gap-1 w-full h-4 justify-center px-1">
19032
+ <div class="w-full bg-border-default rounded-sm"></div>
19033
+ </div>
19034
+ </button>
19035
+ <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">
19036
+ <div class="flex gap-0.5 w-full h-4 px-1">
19037
+ <div class="w-1/2 bg-border-default rounded-sm"></div>
19038
+ <div class="w-1/2 bg-border-default rounded-sm"></div>
19039
+ </div>
19040
+ </button>
19041
+ <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">
19042
+ <div class="flex gap-0.5 w-full h-4 px-1">
19043
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19044
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19045
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19046
+ </div>
19047
+ </button>
19048
+ <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">
19049
+ <div class="flex gap-0.5 w-full h-4 px-1">
19050
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19051
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19052
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19053
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19054
+ </div>
19055
+ </button>
19056
+ <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">
19057
+ <span class="text-[12px] text-text-primary font-medium">6 Col</span>
19058
+ </button>
19059
+ <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">
19060
+ <span class="text-[12px] text-text-primary font-medium">12 Col</span>
19061
+ </button>
19062
+ </div>
19063
+ <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">
19064
+ <lucide-icon name="plus" class="w-4 h-4"></lucide-icon> Add Column
19065
+ </button>
19066
+ </div>
19067
+ }
19068
+
19069
+ @if (node.type === 'col') {
19070
+ <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm"
19071
+ [class.opacity-60]="state.isReadOnly()"
19072
+ [class.pointer-events-none]="state.isReadOnly()">
19073
+ <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Responsive Width (1-12)</div>
19074
+
19075
+ <div class="flex items-center gap-2 mb-2">
19076
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XS</span>
19077
+ <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)">
19078
+ <option *ngFor="let option of responsiveWidthOptions(false)" [ngValue]="option.value">{{ option.label }}</option>
19079
+ </select>
19080
+ </div>
18569
19081
 
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>
19082
+ <div class="flex items-center gap-2 mb-2">
19083
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">SM</span>
19084
+ <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)">
19085
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19086
+ </select>
19087
+ </div>
18577
19088
 
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>
19089
+ <div class="flex items-center gap-2 mb-2">
19090
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">MD</span>
19091
+ <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)">
19092
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19093
+ </select>
19094
+ </div>
18585
19095
 
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>
19096
+ <div class="flex items-center gap-2 mb-2">
19097
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">LG</span>
19098
+ <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)">
19099
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19100
+ </select>
19101
+ </div>
18593
19102
 
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>
19103
+ <div class="flex items-center gap-2 mb-2">
19104
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XL</span>
19105
+ <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)">
19106
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19107
+ </select>
19108
+ </div>
18601
19109
 
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>
19110
+ <div class="flex items-center gap-2">
19111
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">2XL</span>
19112
+ <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)">
19113
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19114
+ </select>
19115
+ </div>
19116
+ </div>
18609
19117
 
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>
19118
+ @if (!state.isReadOnly()) {
19119
+ <div class="inspector-card bg-red-50 border border-red-100 rounded-lg p-3">
19120
+ <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">
19121
+ <lucide-icon name="trash-2" class="w-4 h-4"></lucide-icon> Remove Column
19122
+ </button>
19123
+ </div>
19124
+ }
19125
+ }
19126
+ </div>
19127
+ }
19128
+ </div>
18622
19129
  </ng-container>
18623
19130
  </div>
18624
19131
 
@@ -18631,7 +19138,7 @@ class PropertiesPanelComponent {
18631
19138
  </form-settings-inspector>
18632
19139
  </ng-template>
18633
19140
  </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"] }] });
19141
+ `, 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
19142
  }
18636
19143
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PropertiesPanelComponent, decorators: [{
18637
19144
  type: Component,
@@ -18639,8 +19146,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
18639
19146
  CommonModule,
18640
19147
  FormsModule,
18641
19148
  UiIconModule,
18642
- UiTabsComponent,
18643
- UiTabComponent,
19149
+ UiAccordionComponent,
18644
19150
  WidgetInspectorComponent,
18645
19151
  FormSettingsInspectorComponent,
18646
19152
  InspectorLayoutSectionComponent,
@@ -18677,239 +19183,182 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
18677
19183
  </div>
18678
19184
  </div>
18679
19185
 
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>
19186
+ <div class="flex items-center px-2 pt-2 border-b border-border-default bg-surface-default">
19187
+ @for (tab of layoutInspectorTabs; track tab) {
19188
+ <button
19189
+ type="button"
19190
+ (click)="activeLayoutInspectorTab.set(tab)"
19191
+ [class]="activeLayoutInspectorTab() === tab
19192
+ ? 'px-2 pb-2 font-semibold text-primary-500 border-b-2 border-b-primary-500'
19193
+ : 'px-2 pb-2 text-text-primary opacity-60 hover:opacity-100 hover:text-text-primary border-b-2 border-transparent'">
19194
+ {{ tab }}
19195
+ </button>
19196
+ }
19197
+ </div>
18852
19198
 
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>
19199
+ <div class="flex-1 overflow-y-auto custom-scrollbar bg-surface-default">
19200
+ @if (activeLayoutInspectorTab() === 'Style') {
19201
+ <div class="flex flex-col" [class.pointer-events-none]="state.isReadOnly()" [class.opacity-60]="state.isReadOnly()">
19202
+ <ui-accordion title="Layout" [expanded]="true">
19203
+ <inspector-layout-section
19204
+ [style]="nodeStyle(node)"
19205
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19206
+ </inspector-layout-section>
19207
+ </ui-accordion>
19208
+
19209
+ <ui-accordion title="Spacing" [expanded]="false">
19210
+ <inspector-spacing-section
19211
+ [style]="nodeStyle(node)"
19212
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19213
+ </inspector-spacing-section>
19214
+ </ui-accordion>
19215
+
19216
+ <ui-accordion title="Size" [expanded]="false">
19217
+ <inspector-size-section
19218
+ [style]="nodeStyle(node)"
19219
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19220
+ </inspector-size-section>
19221
+ </ui-accordion>
19222
+
19223
+ <ui-accordion title="Backgrounds" [expanded]="false">
19224
+ <inspector-backgrounds-section
19225
+ [style]="nodeStyle(node)"
19226
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19227
+ </inspector-backgrounds-section>
19228
+ </ui-accordion>
19229
+
19230
+ <ui-accordion title="Borders" [expanded]="false">
19231
+ <inspector-borders-section
19232
+ [style]="nodeStyle(node)"
19233
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19234
+ </inspector-borders-section>
19235
+ </ui-accordion>
19236
+
19237
+ <ui-accordion title="Effects" [expanded]="false">
19238
+ <inspector-effects-section
19239
+ [style]="nodeStyle(node)"
19240
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19241
+ </inspector-effects-section>
19242
+ </ui-accordion>
19243
+
19244
+ <ui-accordion title="Advanced" [expanded]="false">
19245
+ <inspector-advanced-section
19246
+ [style]="nodeStyle(node)"
19247
+ (styleChange)="onNodeInspectorStyleChange(node.id, $event)">
19248
+ </inspector-advanced-section>
19249
+ </ui-accordion>
19250
+
19251
+ <div class="h-10"></div>
19252
+ </div>
19253
+ }
19254
+
19255
+ @if (activeLayoutInspectorTab() === 'Settings') {
19256
+ <div class="p-4 flex flex-col gap-4">
19257
+ @if (node.type === 'row') {
19258
+ <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm"
19259
+ [class.opacity-60]="state.isReadOnly()"
19260
+ [class.pointer-events-none]="state.isReadOnly()">
19261
+ <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Column Structure</div>
19262
+ <div class="grid grid-cols-3 gap-2 mb-3">
19263
+ <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">
19264
+ <div class="flex gap-1 w-full h-4 justify-center px-1">
19265
+ <div class="w-full bg-border-default rounded-sm"></div>
19266
+ </div>
19267
+ </button>
19268
+ <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">
19269
+ <div class="flex gap-0.5 w-full h-4 px-1">
19270
+ <div class="w-1/2 bg-border-default rounded-sm"></div>
19271
+ <div class="w-1/2 bg-border-default rounded-sm"></div>
19272
+ </div>
19273
+ </button>
19274
+ <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">
19275
+ <div class="flex gap-0.5 w-full h-4 px-1">
19276
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19277
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19278
+ <div class="w-1/3 bg-border-default rounded-sm"></div>
19279
+ </div>
19280
+ </button>
19281
+ <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">
19282
+ <div class="flex gap-0.5 w-full h-4 px-1">
19283
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19284
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19285
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19286
+ <div class="w-1/4 bg-border-default rounded-sm"></div>
19287
+ </div>
19288
+ </button>
19289
+ <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">
19290
+ <span class="text-[12px] text-text-primary font-medium">6 Col</span>
19291
+ </button>
19292
+ <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">
19293
+ <span class="text-[12px] text-text-primary font-medium">12 Col</span>
19294
+ </button>
19295
+ </div>
19296
+ <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">
19297
+ <lucide-icon name="plus" class="w-4 h-4"></lucide-icon> Add Column
19298
+ </button>
19299
+ </div>
19300
+ }
19301
+
19302
+ @if (node.type === 'col') {
19303
+ <div class="inspector-card bg-surface-default border border-border-default rounded-lg p-4 shadow-sm"
19304
+ [class.opacity-60]="state.isReadOnly()"
19305
+ [class.pointer-events-none]="state.isReadOnly()">
19306
+ <div class="section-title text-[11px] font-semibold text-text-primary opacity-70 uppercase tracking-wide mb-3">Responsive Width (1-12)</div>
19307
+
19308
+ <div class="flex items-center gap-2 mb-2">
19309
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XS</span>
19310
+ <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)">
19311
+ <option *ngFor="let option of responsiveWidthOptions(false)" [ngValue]="option.value">{{ option.label }}</option>
19312
+ </select>
19313
+ </div>
18860
19314
 
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>
19315
+ <div class="flex items-center gap-2 mb-2">
19316
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">SM</span>
19317
+ <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)">
19318
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19319
+ </select>
19320
+ </div>
18868
19321
 
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>
19322
+ <div class="flex items-center gap-2 mb-2">
19323
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">MD</span>
19324
+ <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)">
19325
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19326
+ </select>
19327
+ </div>
18876
19328
 
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>
19329
+ <div class="flex items-center gap-2 mb-2">
19330
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">LG</span>
19331
+ <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)">
19332
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19333
+ </select>
19334
+ </div>
18884
19335
 
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>
19336
+ <div class="flex items-center gap-2 mb-2">
19337
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">XL</span>
19338
+ <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)">
19339
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19340
+ </select>
19341
+ </div>
18892
19342
 
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>
19343
+ <div class="flex items-center gap-2">
19344
+ <span class="text-[12px] w-8 text-text-primary opacity-70 font-medium">2XL</span>
19345
+ <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)">
19346
+ <option *ngFor="let option of responsiveWidthOptions(true)" [ngValue]="option.value">{{ option.label }}</option>
19347
+ </select>
19348
+ </div>
19349
+ </div>
18900
19350
 
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>
19351
+ @if (!state.isReadOnly()) {
19352
+ <div class="inspector-card bg-red-50 border border-red-100 rounded-lg p-3">
19353
+ <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">
19354
+ <lucide-icon name="trash-2" class="w-4 h-4"></lucide-icon> Remove Column
19355
+ </button>
19356
+ </div>
19357
+ }
19358
+ }
19359
+ </div>
19360
+ }
19361
+ </div>
18913
19362
  </ng-container>
18914
19363
  </div>
18915
19364
 
@@ -20856,9 +21305,9 @@ const BASIC_CONTACT_TEMPLATE = {
20856
21305
  },
20857
21306
  {
20858
21307
  "id": "btn_submit",
20859
- "widgetId": "core.form:submit-button",
21308
+ "widgetId": "core.form:form-button",
20860
21309
  "name": "submit",
20861
- "type": "submit-button",
21310
+ "type": "form-button",
20862
21311
  "label": "Send message",
20863
21312
  "variant": "primary",
20864
21313
  "buttonType": "submit",
@@ -20875,9 +21324,9 @@ const BASIC_CONTACT_TEMPLATE = {
20875
21324
  },
20876
21325
  {
20877
21326
  "id": "btn_reset",
20878
- "widgetId": "core.form:reset-button",
21327
+ "widgetId": "core.form:form-button",
20879
21328
  "name": "reset",
20880
- "type": "reset-button",
21329
+ "type": "form-button",
20881
21330
  "label": "Reset",
20882
21331
  "variant": "secondary",
20883
21332
  "buttonType": "reset"
@@ -21269,9 +21718,9 @@ const RULES_SHIPPING_TEMPLATE = {
21269
21718
  },
21270
21719
  {
21271
21720
  "id": "btn_submit",
21272
- "widgetId": "core.form:submit-button",
21721
+ "widgetId": "core.form:form-button",
21273
21722
  "name": "submit",
21274
- "type": "submit-button",
21723
+ "type": "form-button",
21275
21724
  "label": "Place order",
21276
21725
  "variant": "primary",
21277
21726
  "buttonType": "submit",
@@ -21286,9 +21735,9 @@ const RULES_SHIPPING_TEMPLATE = {
21286
21735
  },
21287
21736
  {
21288
21737
  "id": "btn_reset",
21289
- "widgetId": "core.form:reset-button",
21738
+ "widgetId": "core.form:form-button",
21290
21739
  "name": "reset",
21291
- "type": "reset-button",
21740
+ "type": "form-button",
21292
21741
  "label": "Reset",
21293
21742
  "variant": "secondary",
21294
21743
  "buttonType": "reset"
@@ -21578,9 +22027,9 @@ const DATA_SOURCES_TEMPLATE = {
21578
22027
  },
21579
22028
  {
21580
22029
  "id": "btn_submit",
21581
- "widgetId": "core.form:submit-button",
22030
+ "widgetId": "core.form:form-button",
21582
22031
  "name": "submit",
21583
- "type": "submit-button",
22032
+ "type": "form-button",
21584
22033
  "label": "Create profile",
21585
22034
  "variant": "primary",
21586
22035
  "buttonType": "submit"
@@ -36926,10 +37375,6 @@ const FILE_BASIC_FIELDS = [
36926
37375
  { key: 'tooltip', type: 'text', label: 'Tooltip' },
36927
37376
  ...COMMON_GROUP_FIELDS
36928
37377
  ];
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
37378
  const COMMON_APPEARANCE_FIELDS = [
36934
37379
  {
36935
37380
  key: 'appearance',
@@ -36964,18 +37409,6 @@ const COMMON_STYLE_SECTIONS = [
36964
37409
  STYLE_TRANSFORM_SECTION,
36965
37410
  STYLE_EFFECTS_SECTION
36966
37411
  ];
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
37412
  // Start with standard sections for Text Field
36980
37413
  const BASE_REQUIRED_FIELD = {
36981
37414
  key: 'html5.required',
@@ -36983,67 +37416,25 @@ const BASE_REQUIRED_FIELD = {
36983
37416
  label: 'Required',
36984
37417
  helpText: 'Base required state; Rules can override this dynamically.'
36985
37418
  };
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
- ]
37419
+ const CUSTOM_VALIDATION_RULES_FIELD = {
37420
+ key: 'validation',
37421
+ type: 'validators-editor',
37422
+ label: 'Custom Validation Rules',
37423
+ helpText: 'Use these for field-level validation checks. Use the Rules tab for conditional behaviour.'
37046
37424
  };
37425
+ function createValidationSection(...fields) {
37426
+ return {
37427
+ label: 'Validation',
37428
+ fields: [...fields, CUSTOM_VALIDATION_RULES_FIELD]
37429
+ };
37430
+ }
37431
+ 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)' });
37432
+ 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)' });
37433
+ 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)' });
37434
+ 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)' });
37435
+ 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)' });
37436
+ const SELECT_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD);
37437
+ const FILE_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD);
37047
37438
  const BUTTON_VARIANT_OPTIONS = [
37048
37439
  { label: 'Primary (Blue)', value: 'primary' },
37049
37440
  { label: 'Secondary (Outline)', value: 'secondary' }
@@ -37084,7 +37475,6 @@ const IMAGE_BUTTON_PROPERTIES = [
37084
37475
  ];
37085
37476
  const TEXT_WIDGET_PROPERTIES = [
37086
37477
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37087
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37088
37478
  TEXT_VALIDATION_SECTION,
37089
37479
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37090
37480
  ...COMMON_STYLE_SECTIONS
@@ -37115,7 +37505,6 @@ const FILE_WIDGET_PROPERTIES = [
37115
37505
  ]
37116
37506
  },
37117
37507
  FILE_VALIDATION_SECTION,
37118
- COMMON_CONDITIONAL_SECTION,
37119
37508
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37120
37509
  ...COMMON_STYLE_SECTIONS
37121
37510
  ];
@@ -37250,9 +37639,7 @@ const FIELD_WIDGETS = [
37250
37639
  ...COMMON_BASIC_FIELDS
37251
37640
  ]
37252
37641
  },
37253
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37254
37642
  NUMBER_VALIDATION_SECTION,
37255
- COMMON_CONDITIONAL_SECTION,
37256
37643
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37257
37644
  ...COMMON_STYLE_SECTIONS
37258
37645
  ]
@@ -37280,7 +37667,6 @@ const FIELD_WIDGETS = [
37280
37667
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37281
37668
  properties: [
37282
37669
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37283
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37284
37670
  DATE_VALIDATION_SECTION,
37285
37671
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37286
37672
  ...COMMON_STYLE_SECTIONS
@@ -37309,7 +37695,6 @@ const FIELD_WIDGETS = [
37309
37695
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37310
37696
  properties: [
37311
37697
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37312
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37313
37698
  TIME_VALIDATION_SECTION,
37314
37699
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37315
37700
  ...COMMON_STYLE_SECTIONS
@@ -37336,7 +37721,6 @@ const FIELD_WIDGETS = [
37336
37721
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37337
37722
  properties: [
37338
37723
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37339
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37340
37724
  DATETIME_VALIDATION_SECTION,
37341
37725
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37342
37726
  ...COMMON_STYLE_SECTIONS
@@ -37366,7 +37750,6 @@ const FIELD_WIDGETS = [
37366
37750
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37367
37751
  properties: [
37368
37752
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37369
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37370
37753
  DATE_VALIDATION_SECTION,
37371
37754
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37372
37755
  ...COMMON_STYLE_SECTIONS
@@ -37396,7 +37779,6 @@ const FIELD_WIDGETS = [
37396
37779
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37397
37780
  properties: [
37398
37781
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37399
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37400
37782
  DATE_VALIDATION_SECTION,
37401
37783
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37402
37784
  ...COMMON_STYLE_SECTIONS
@@ -37583,7 +37965,6 @@ const FIELD_WIDGETS = [
37583
37965
  dataBinding: { shape: 'scalar', targetPath: 'defaultValue' },
37584
37966
  properties: [
37585
37967
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37586
- { label: 'Icons', fields: COMMON_ICON_FIELDS },
37587
37968
  NUMBER_VALIDATION_SECTION,
37588
37969
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37589
37970
  ...COMMON_STYLE_SECTIONS
@@ -37663,44 +38044,6 @@ const FIELD_WIDGETS = [
37663
38044
  dataConsumer: 'none',
37664
38045
  properties: BUTTON_PROPERTIES
37665
38046
  }),
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
38047
  defineWidget(pluginId$2, {
37705
38048
  kind: 'button',
37706
38049
  flavor: 'form',
@@ -37758,7 +38101,6 @@ const FIELD_WIDGETS = [
37758
38101
  },
37759
38102
  // Options are now handled in Data Tab
37760
38103
  SELECT_VALIDATION_SECTION,
37761
- COMMON_CONDITIONAL_SECTION,
37762
38104
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37763
38105
  ...COMMON_STYLE_SECTIONS
37764
38106
  ]
@@ -37788,7 +38130,6 @@ const FIELD_WIDGETS = [
37788
38130
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37789
38131
  // Options handled in Data Tab
37790
38132
  SELECT_VALIDATION_SECTION,
37791
- COMMON_CONDITIONAL_SECTION,
37792
38133
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37793
38134
  ...COMMON_STYLE_SECTIONS
37794
38135
  ]
@@ -37818,7 +38159,6 @@ const FIELD_WIDGETS = [
37818
38159
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37819
38160
  // Options handled in Data Tab
37820
38161
  SELECT_VALIDATION_SECTION,
37821
- COMMON_CONDITIONAL_SECTION,
37822
38162
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37823
38163
  ...COMMON_STYLE_SECTIONS
37824
38164
  ]
@@ -37843,14 +38183,7 @@ const FIELD_WIDGETS = [
37843
38183
  properties: [
37844
38184
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37845
38185
  // 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,
38186
+ createValidationSection(BASE_REQUIRED_FIELD),
37854
38187
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37855
38188
  ...COMMON_STYLE_SECTIONS
37856
38189
  ]
@@ -37881,7 +38214,6 @@ const FIELD_WIDGETS = [
37881
38214
  properties: [
37882
38215
  { label: 'Basic', fields: COMMON_BASIC_FIELDS },
37883
38216
  SELECT_VALIDATION_SECTION,
37884
- COMMON_CONDITIONAL_SECTION,
37885
38217
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
37886
38218
  ...COMMON_STYLE_SECTIONS
37887
38219
  ]
@@ -37945,7 +38277,6 @@ const FIELD_WIDGETS = [
37945
38277
  ]
37946
38278
  },
37947
38279
  ...COMMON_STYLE_SECTIONS,
37948
- COMMON_CONDITIONAL_SECTION
37949
38280
  ]
37950
38281
  })
37951
38282
  ];
@@ -40147,6 +40478,162 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
40147
40478
  args: [DOCUMENT]
40148
40479
  }] }] });
40149
40480
 
40481
+ class UiTabComponent {
40482
+ label = '';
40483
+ name = ''; // Unique key for controlled mode
40484
+ disabled = false;
40485
+ badge;
40486
+ badgeTone = 'neutral';
40487
+ template;
40488
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
40489
+ 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 }] });
40490
+ }
40491
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabComponent, decorators: [{
40492
+ type: Component,
40493
+ args: [{
40494
+ selector: 'ui-tab',
40495
+ standalone: true,
40496
+ imports: [CommonModule],
40497
+ template: `<ng-template #tpl><ng-content></ng-content></ng-template>`
40498
+ }]
40499
+ }], propDecorators: { label: [{
40500
+ type: Input
40501
+ }], name: [{
40502
+ type: Input
40503
+ }], disabled: [{
40504
+ type: Input
40505
+ }], badge: [{
40506
+ type: Input
40507
+ }], badgeTone: [{
40508
+ type: Input
40509
+ }], template: [{
40510
+ type: ViewChild,
40511
+ args: ['tpl', { static: true }]
40512
+ }] } });
40513
+ class UiTabsComponent {
40514
+ tabQuery;
40515
+ activeTab;
40516
+ activeTabChange = new EventEmitter();
40517
+ // Internal state if uncontrolled
40518
+ _internalIndex = 0;
40519
+ tabs = [];
40520
+ ngAfterContentInit() {
40521
+ this.tabs = this.tabQuery.toArray();
40522
+ }
40523
+ isActive(tab) {
40524
+ if (this.activeTab !== undefined) {
40525
+ return this.activeTab === (tab.name || tab.label);
40526
+ }
40527
+ return this.tabs.indexOf(tab) === this._internalIndex;
40528
+ }
40529
+ activate(tab) {
40530
+ if (tab.disabled)
40531
+ return;
40532
+ const key = tab.name || tab.label;
40533
+ if (this.activeTab !== undefined) {
40534
+ this.activeTabChange.emit(key);
40535
+ }
40536
+ else {
40537
+ this._internalIndex = this.tabs.indexOf(tab);
40538
+ }
40539
+ }
40540
+ get activeTemplate() {
40541
+ if (this.tabs.length === 0)
40542
+ return null;
40543
+ let activeTab;
40544
+ if (this.activeTab !== undefined) {
40545
+ activeTab = this.tabs.find(t => (t.name || t.label) === this.activeTab);
40546
+ }
40547
+ else {
40548
+ activeTab = this.tabs[this._internalIndex];
40549
+ }
40550
+ return activeTab?.template || null;
40551
+ }
40552
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
40553
+ 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: `
40554
+ <div class="flex flex-col min-h-0 h-full">
40555
+ <div class="flex items-center gap-1 px-3 border-b border-slate-200 bg-white shrink-0">
40556
+ <button
40557
+ *ngFor="let tab of tabs; let i = index"
40558
+ (click)="activate(tab)"
40559
+ class="relative h-10 px-3 text-xs font-semibold transition-colors rounded-t-md border-b-2"
40560
+ [class.text-accent-600]="isActive(tab)"
40561
+ [class.border-accent-600]="isActive(tab)"
40562
+ [class.text-ink-500]="!isActive(tab)"
40563
+ [class.border-transparent]="!isActive(tab)"
40564
+ [class.hover:text-ink-700]="!isActive(tab)"
40565
+ [disabled]="tab.disabled"
40566
+ >
40567
+ <span class="flex items-center gap-2">
40568
+ {{ tab.label }}
40569
+ <span *ngIf="tab.badge" class="text-[10px] px-1.5 py-0.5 rounded-full border"
40570
+ [class.bg-accent-50]="tab.badgeTone === 'accent'"
40571
+ [class.border-accent-200]="tab.badgeTone === 'accent'"
40572
+ [class.text-accent-700]="tab.badgeTone === 'accent'"
40573
+ [class.bg-slate-100]="tab.badgeTone === 'neutral'"
40574
+ [class.border-slate-200]="tab.badgeTone === 'neutral'"
40575
+ [class.text-slate-600]="tab.badgeTone === 'neutral'">
40576
+ {{ tab.badge }}
40577
+ </span>
40578
+ </span>
40579
+ </button>
40580
+ </div>
40581
+
40582
+ <div class="flex-1 min-h-0 overflow-hidden bg-slate-50/30">
40583
+ <ng-container *ngIf="activeTemplate">
40584
+ <ng-container *ngTemplateOutlet="activeTemplate"></ng-container>
40585
+ </ng-container>
40586
+ </div>
40587
+ </div>
40588
+ `, 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"] }] });
40589
+ }
40590
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: UiTabsComponent, decorators: [{
40591
+ type: Component,
40592
+ args: [{ selector: 'ui-tabs', standalone: true, imports: [CommonModule], template: `
40593
+ <div class="flex flex-col min-h-0 h-full">
40594
+ <div class="flex items-center gap-1 px-3 border-b border-slate-200 bg-white shrink-0">
40595
+ <button
40596
+ *ngFor="let tab of tabs; let i = index"
40597
+ (click)="activate(tab)"
40598
+ class="relative h-10 px-3 text-xs font-semibold transition-colors rounded-t-md border-b-2"
40599
+ [class.text-accent-600]="isActive(tab)"
40600
+ [class.border-accent-600]="isActive(tab)"
40601
+ [class.text-ink-500]="!isActive(tab)"
40602
+ [class.border-transparent]="!isActive(tab)"
40603
+ [class.hover:text-ink-700]="!isActive(tab)"
40604
+ [disabled]="tab.disabled"
40605
+ >
40606
+ <span class="flex items-center gap-2">
40607
+ {{ tab.label }}
40608
+ <span *ngIf="tab.badge" class="text-[10px] px-1.5 py-0.5 rounded-full border"
40609
+ [class.bg-accent-50]="tab.badgeTone === 'accent'"
40610
+ [class.border-accent-200]="tab.badgeTone === 'accent'"
40611
+ [class.text-accent-700]="tab.badgeTone === 'accent'"
40612
+ [class.bg-slate-100]="tab.badgeTone === 'neutral'"
40613
+ [class.border-slate-200]="tab.badgeTone === 'neutral'"
40614
+ [class.text-slate-600]="tab.badgeTone === 'neutral'">
40615
+ {{ tab.badge }}
40616
+ </span>
40617
+ </span>
40618
+ </button>
40619
+ </div>
40620
+
40621
+ <div class="flex-1 min-h-0 overflow-hidden bg-slate-50/30">
40622
+ <ng-container *ngIf="activeTemplate">
40623
+ <ng-container *ngTemplateOutlet="activeTemplate"></ng-container>
40624
+ </ng-container>
40625
+ </div>
40626
+ </div>
40627
+ `, styles: [":host{display:block;height:100%}\n"] }]
40628
+ }], propDecorators: { tabQuery: [{
40629
+ type: ContentChildren,
40630
+ args: [UiTabComponent]
40631
+ }], activeTab: [{
40632
+ type: Input
40633
+ }], activeTabChange: [{
40634
+ type: Output
40635
+ }] } });
40636
+
40150
40637
  class AiToolRegistryService {
40151
40638
  state;
40152
40639
  widgetDefs;