@praxisui/page-builder 9.0.68-rc.2 → 10.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,7 +81,7 @@ through its host. Preset selection does not introduce collective repacking or ne
81
81
 
82
82
  ### Device composition
83
83
 
84
- **Page settings → Responsiveness** opens on Mobile and shows one device at a time.
84
+ **Page settings → Adjustments → Responsiveness** opens on Mobile and shows one device at a time.
85
85
  **Stack widgets** prepares a single-column variant in the base canvas's visual reading
86
86
  order, with natural content height. It replaces positions and width bounds only for
87
87
  that device, preserves hidden widgets and other overrides, and leaves the base and
@@ -97,7 +97,7 @@ precedence over grouping. The editor explains this distinction in the Grouping t
97
97
 
98
98
  ### Visual widget limits
99
99
 
100
- Open **Page settings → Widget limits** in the default Dynamic Page editor to set
100
+ Open **Page settings → Adjustments → Widget limits** in the default Dynamic Page editor to set
101
101
  minimum/maximum width (columns) and height (rows). Choose the base canvas or a
102
102
  desktop, tablet or mobile override, without closing the editor. Blank base fields use Core defaults (minimum one,
103
103
  maximum grid width/unbounded row span); blank device fields inherit base limits.
@@ -978,7 +978,9 @@ In **Page settings → Arrange**, choose a visual distribution. This is an
978
978
  explicit arrangement of a complete existing base canvas, independent of layout preset
979
979
  selection. The current geometric reading order assigns columns; no titles or business
980
980
  roles are inferred. Heights, content, shell appearance, widget keys and composition links
981
- are preserved. The last column receives any remainder when the grid is indivisible.
981
+ are preserved. Indivisible columns are distributed by fractional remainder; ties favor
982
+ the larger weight, then the later lane. For example, four equal lanes over seven
983
+ columns produce `1 + 2 + 2 + 2`, not `1 + 1 + 1 + 4`.
982
984
 
983
985
  Review the numbered before/after maps and widget names, then choose **Use arrangement
984
986
  in draft** and the Settings Panel's **Apply**. Preparation does not dirty or emit the page.
@@ -1016,3 +1018,44 @@ Undo restores only `col`, `row`, `colSpan` and `rowSpan`. Additional gap, theme,
1016
1018
  or limit edits remain in the draft. The owner-confirmed Apply receipt retains the undo
1017
1019
  when the reviewed geometry matches, including when unrelated fields were edited before
1018
1020
  Apply. Reset or replacement of the page still invalidates the transaction.
1021
+
1022
+
1023
+ ### Corporate adoption and validation boundaries
1024
+
1025
+ The template studio is a presentation and geometry editor. Its visual templates are
1026
+ Core theme presets; its layout templates describe composition roles. Neither gallery
1027
+ creates a governed business template, grants resource permissions, generates data
1028
+ connections, or implements a master-detail relationship by selecting a card.
1029
+ Governed template publication remains owned by Config Starter and its canonical
1030
+ references; host authoring eligibility, persistence identity and strong ETag handling
1031
+ remain required at their existing boundaries. Geometry locks are editing constraints,
1032
+ not an authorization mechanism.
1033
+
1034
+ Use the following acceptance scope when adopting the editor in a corporate host:
1035
+
1036
+ | Concern | Required evidence |
1037
+ | --- | --- |
1038
+ | Document integrity | Apply, Save and fresh reopen preserve widget inputs, keys, composition links, context, state, slot assignments and untouched device overrides. |
1039
+ | Concurrency and governance | The real host rejects stale ETags and unauthorized writes; local Apply or a passing geometry test does not prove either. |
1040
+ | Responsive authoring | Validate each intended device with its own locks, bounds, hidden widgets and child-content height. Base-arrangement admission does not certify every device editing operation. |
1041
+ | User experience | Check keyboard, desktop/narrow layout, locale and dark theme with representative business data. Automated accessibility checks do not replace task-based usability testing. |
1042
+ | Scale | Measure proposal generation and review rendering with the host's expected widget counts; small fixtures do not establish a performance envelope. |
1043
+ | Publication | Validate the actual npm train in the consumer. A local RC build is not a hosted-production smoke or approval to promote the stable channel. |
1044
+
1045
+ The public Landing Studio deliberately hides full page settings under its authoring
1046
+ policy. Its JSON/Visual compatibility tests do not demonstrate the template gallery;
1047
+ use the canonical Dynamic Page laboratory or a host that enables page settings for that
1048
+ journey. Enabling that surface is a host policy decision, not a package upgrade effect.
1049
+
1050
+ The source corrections following the 9.0.68-rc.2 review reject device stacking
1051
+ before changing the draft when it would alter effective position/size locks, including
1052
+ content height for size-locked widgets. Hidden untouched items remain unchanged. An
1053
+ explicit device override of a lock continues to follow the existing inheritance contract;
1054
+ this does not grant permission to change locks.
1055
+
1056
+ Context paths `ui` and `ui.shell`, when present, must be objects; `ui.shell.preset`
1057
+ must be text when present. Incompatible imported or edited values remain in the draft
1058
+ and block value production with a localized diagnostic, allowing correction without
1059
+ losing unrelated context. Arrangement choices track their draft baseline even when the
1060
+ selected distribution initially produces no changes, so later edits clear stale status.
1061
+ These fixes are not part of the already published 9.0.68-rc.2 package.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-10T01:25:13.157Z",
3
+ "generatedAt": "2026-09-10T12:25:16.581Z",
4
4
  "packageName": "@praxisui/page-builder",
5
- "packageVersion": "9.0.68-rc.2",
5
+ "packageVersion": "10.0.0-rc.0",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 1,
@@ -816,6 +816,8 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
816
816
  'praxis.pageBuilder.editor.apply': 'Apply',
817
817
  'praxis.pageBuilder.editor.errors.invalidJson': 'Invalid JSON',
818
818
  'praxis.pageBuilder.editor.errors.canvasInvalid': 'Invalid canvas',
819
+ 'praxis.pageBuilder.editor.devices.locked': 'Stacking would change a locked widget. Review its device locks first.',
820
+ 'praxis.pageBuilder.editor.errors.contextStructure': 'Context ui and ui.shell must be objects; shell.preset must be text. Correct these fields without removing other context data.',
819
821
  'praxis.pageBuilder.editor.errors.contextInvalid': 'Invalid context',
820
822
  'praxis.pageBuilder.editor.errors.stateInvalid': 'Invalid state',
821
823
  'praxis.pageBuilder.editor.errors.duplicateGroupIds': 'Duplicate group IDs',
@@ -1879,6 +1881,8 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
1879
1881
  'praxis.pageBuilder.editor.apply': 'Aplicar',
1880
1882
  'praxis.pageBuilder.editor.errors.invalidJson': 'JSON inválido',
1881
1883
  'praxis.pageBuilder.editor.errors.canvasInvalid': 'Canvas inválido',
1884
+ 'praxis.pageBuilder.editor.devices.locked': 'O empilhamento alteraria um widget bloqueado. Revise os bloqueios desse dispositivo primeiro.',
1885
+ 'praxis.pageBuilder.editor.errors.contextStructure': 'No contexto, ui e ui.shell devem ser objetos; shell.preset deve ser texto. Corrija esses campos preservando os demais dados.',
1882
1886
  'praxis.pageBuilder.editor.errors.contextInvalid': 'Contexto inválido',
1883
1887
  'praxis.pageBuilder.editor.errors.stateInvalid': 'Estado inválido',
1884
1888
  'praxis.pageBuilder.editor.errors.duplicateGroupIds': 'IDs de grupo duplicados',
@@ -13554,6 +13558,7 @@ class DynamicPageConfigEditorComponent {
13554
13558
  focusArrangementFeedback = false;
13555
13559
  workspaceTab = signal(0, ...(ngDevMode ? [{ debugName: "workspaceTab" }] : /* istanbul ignore next */ []));
13556
13560
  arrangementSelection = signal('', ...(ngDevMode ? [{ debugName: "arrangementSelection" }] : /* istanbul ignore next */ []));
13561
+ arrangementSelectionSource = '';
13557
13562
  // Transient authoring choices, expressed through the existing Core width contract.
13558
13563
  arrangementChoices = [
13559
13564
  { id: 'stack', weights: [1] }, { id: 'compare', weights: [1, 1] },
@@ -13603,6 +13608,8 @@ class DynamicPageConfigEditorComponent {
13603
13608
  previousArrangementItems;
13604
13609
  prepareArrangement(id) {
13605
13610
  this.arrangementPreview.set(null);
13611
+ this.arrangementSelection.set('');
13612
+ this.arrangementSelectionSource = '';
13606
13613
  const page = this.buildCurrentPage(true);
13607
13614
  if (!page)
13608
13615
  return;
@@ -13616,6 +13623,7 @@ class DynamicPageConfigEditorComponent {
13616
13623
  return;
13617
13624
  }
13618
13625
  this.arrangementSelection.set(id);
13626
+ this.arrangementSelectionSource = this.draftFingerprint();
13619
13627
  this.arrangementMessage.set(proposal.changedKeys.length ? '' : this.tx('editor.arrange.unchanged', 'This arrangement already matches the page.'));
13620
13628
  if (proposal.changedKeys.length)
13621
13629
  this.arrangementPreview.set({ source: this.draftFingerprint(), before: page.canvas, canvas: proposal.canvas, keys: proposal.changedKeys });
@@ -13638,6 +13646,7 @@ class DynamicPageConfigEditorComponent {
13638
13646
  this.arrangementSourcePage = this.clone(current);
13639
13647
  this.previousArrangementItems = this.clone(current.canvas.items);
13640
13648
  this.arrangedItems = preview.canvas.items;
13649
+ this.arrangementSelectionSource = this.draftFingerprint();
13641
13650
  this.arrangementGeometry = this.geometryFingerprint(preview.canvas);
13642
13651
  this.arrangementUndoAvailable.set(true);
13643
13652
  this.arrangementPreview.set(null);
@@ -13762,6 +13771,13 @@ class DynamicPageConfigEditorComponent {
13762
13771
  this.themePresetControl.setValue(theme.id); }
13763
13772
  get hasCanvasWidgets() { return !!this.currentPageInput()?.widgets.length; }
13764
13773
  stackDevice(device) {
13774
+ const previous = this.currentPageInput()?.deviceLayouts?.[device]?.canvas || {};
13775
+ if (this.stackViolatesLocks(device, this.stackedCanvas(device, previous))) {
13776
+ this.deviceError.set(this.tx('editor.devices.locked', 'Stacking would change a locked widget. Review its device locks first.'));
13777
+ this.focusEditorTab('devices');
13778
+ return;
13779
+ }
13780
+ this.deviceError.set(null);
13765
13781
  this.hydrating = true;
13766
13782
  this.stackedDevices.add(device);
13767
13783
  this.deviceForms[device].patchValue({ columns: 1, autoRows: 'content' }, { emitEvent: false });
@@ -13776,6 +13792,23 @@ class DynamicPageConfigEditorComponent {
13776
13792
  this.hydrating = false;
13777
13793
  this.refreshPanelState();
13778
13794
  }
13795
+ stackViolatesLocks(device, canvas) {
13796
+ const page = this.currentPageInput();
13797
+ if (!page?.canvas)
13798
+ return false;
13799
+ const overrides = page.deviceLayouts?.[device]?.canvas?.items || {};
13800
+ return Object.entries(canvas.items || {}).some(([key, after]) => {
13801
+ const base = page.canvas.items[key];
13802
+ if (!base)
13803
+ return false;
13804
+ const before = { ...base, ...overrides[key] };
13805
+ after = { ...base, ...after };
13806
+ const locks = { ...base.constraints, ...overrides[key]?.constraints };
13807
+ return !!((locks.lockPosition && (before.col !== after.col || before.row !== after.row))
13808
+ || (locks.lockSize && (before.colSpan !== after.colSpan || before.rowSpan !== after.rowSpan
13809
+ || JSON.stringify(before.contentSize) !== JSON.stringify(after.contentSize))));
13810
+ });
13811
+ }
13779
13812
  stackedCanvas(device, previous) {
13780
13813
  const page = this.currentPageInput() || { widgets: [] };
13781
13814
  const source = this.completeCanvasItems(page);
@@ -13939,7 +13972,8 @@ class DynamicPageConfigEditorComponent {
13939
13972
  buildCurrentPage(reportErrors) { const next = this.clone(this.currentPageInput() || { widgets: [] }); if (this.canvasColumnsControl.invalid)
13940
13973
  return this.invalidate('canvas', this.canvasError, reportErrors, this.tx('editor.errors.canvasInvalid', 'Invalid canvas')); this.canvasError.set(null); const grouping = this.buildGrouping(reportErrors); if (grouping === null)
13941
13974
  return null; this.groupingError.set(null); const context = this.parseRecordJson(this.contextControl.value, {}); if (context === null)
13942
- return this.invalidate('context', this.contextError, reportErrors, this.tx('editor.errors.contextInvalid', 'Invalid context')); this.contextError.set(null); const state = this.parseRecordJson(this.stateControl.value, {}); if (state === null)
13975
+ return this.invalidate('context', this.contextError, reportErrors, this.tx('editor.errors.contextInvalid', 'Invalid context')); if (!this.validShellContext(context))
13976
+ return this.invalidate('context', this.contextError, reportErrors, this.tx('editor.errors.contextStructure', 'Context ui and ui.shell must be objects; shell.preset must be text. Correct these fields without removing other context data.')); this.contextError.set(null); const state = this.parseRecordJson(this.stateControl.value, {}); if (state === null)
13943
13977
  return this.invalidate('state', this.stateError, reportErrors, this.tx('editor.errors.stateInvalid', 'Invalid state')); this.stateError.set(null); const deviceLayouts = this.buildDeviceLayouts(reportErrors, next.deviceLayouts); if (deviceLayouts === null)
13944
13978
  return null; next.canvas = this.buildCanvas(next); if (next.canvas && this.arrangedItems)
13945
13979
  next.canvas = { ...next.canvas, items: this.clone(this.arrangedItems) }; next.layout = withPagePaddingInline(next.layout, this.paddingInlineControl.value); next.layoutPreset = this.layoutPresetControl.value.trim() || undefined; next.themePreset = this.themePresetControl.value.trim() || undefined; next.grouping = grouping.length ? grouping : undefined; next.deviceLayouts = Object.keys(deviceLayouts).length ? deviceLayouts : undefined; next.context = this.buildPageContext(next.context, context); next.state = Object.keys(state).length ? state : undefined; return this.applyCanvasLimits(next, reportErrors); }
@@ -14032,6 +14066,8 @@ class DynamicPageConfigEditorComponent {
14032
14066
  delete baseVariant.groupingOverrides;
14033
14067
  delete baseVariant.widgetOverrides;
14034
14068
  const canvas = this.stackedDevices.has(device) ? this.stackedCanvas(device, baseCanvas) : { ...baseCanvas };
14069
+ if (this.stackedDevices.has(device) && this.stackViolatesLocks(device, canvas))
14070
+ return this.invalidate('devices', this.deviceError, reportErrors, this.tx('editor.devices.locked', 'Stacking would change a locked widget. Review its device locks first.'));
14035
14071
  if (form.controls.columns.value != null)
14036
14072
  canvas.columns = form.controls.columns.value;
14037
14073
  else
@@ -14062,7 +14098,7 @@ class DynamicPageConfigEditorComponent {
14062
14098
  const current = this.buildCurrentPage(false);
14063
14099
  this.isValid$.next(!!current);
14064
14100
  const fingerprint = this.draftFingerprint();
14065
- if (this.arrangementPreview() && this.arrangementPreview().source !== fingerprint) {
14101
+ if (this.arrangementSelection() && this.arrangementSelectionSource !== fingerprint) {
14066
14102
  this.arrangementPreview.set(null);
14067
14103
  this.arrangementSelection.set('');
14068
14104
  this.focusArrangementFeedback = false;
@@ -14113,13 +14149,22 @@ class DynamicPageConfigEditorComponent {
14113
14149
  return ''; return JSON.stringify(value, null, 2); }
14114
14150
  stringifyGroupingWidgetKeys(seed) { if (!seed || seed.kind === 'tabs')
14115
14151
  return ''; return seed.widgetKeys.join(', '); }
14116
- extractShellPreset(context) { return context?.ui?.shell?.preset || ''; }
14117
- removeShellPresetFromContext(context) { const next = this.clone(context || {}); if (next.ui?.shell) {
14152
+ extractShellPreset(context) { const preset = context?.ui?.shell?.preset; return typeof preset === 'string' ? preset : ''; }
14153
+ removeShellPresetFromContext(context) { const next = this.clone(context || {}); if (this.isPlainObject(next.ui) && this.isPlainObject(next.ui.shell) && typeof next.ui.shell.preset === 'string') {
14118
14154
  delete next.ui.shell.preset;
14119
14155
  if (!Object.keys(next.ui.shell).length)
14120
14156
  delete next.ui.shell;
14121
- } if (next.ui && !Object.keys(next.ui).length)
14157
+ } if (this.isPlainObject(next.ui) && !Object.keys(next.ui).length)
14122
14158
  delete next.ui; return next; }
14159
+ validShellContext(context) {
14160
+ if (context['ui'] === undefined)
14161
+ return true;
14162
+ if (!this.isPlainObject(context['ui']))
14163
+ return false;
14164
+ const shell = context['ui']['shell'];
14165
+ return shell === undefined || (this.isPlainObject(shell)
14166
+ && (shell['preset'] === undefined || typeof shell['preset'] === 'string'));
14167
+ }
14123
14168
  applyShellPresetToContext(context, shellPreset) { const next = this.clone(context || {}); if (shellPreset) {
14124
14169
  next.ui = next.ui || {};
14125
14170
  next.ui.shell = next.ui.shell || {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/page-builder",
3
- "version": "9.0.68-rc.2",
3
+ "version": "10.0.0-rc.0",
4
4
  "description": "Page and widget builder utilities for Praxis UI (grid, dynamic widgets, editors).",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
@@ -8,9 +8,9 @@
8
8
  "@angular/forms": "^21.0.0",
9
9
  "@angular/cdk": "^21.0.0",
10
10
  "@angular/material": "^21.0.0",
11
- "@praxisui/ai": "^9.0.68-rc.2",
12
- "@praxisui/core": "^9.0.68-rc.2",
13
- "@praxisui/settings-panel": "^9.0.68-rc.2",
11
+ "@praxisui/ai": "^10.0.0-rc.0",
12
+ "@praxisui/core": "^10.0.0-rc.0",
13
+ "@praxisui/settings-panel": "^10.0.0-rc.0",
14
14
  "rxjs": "~7.8.0"
15
15
  },
16
16
  "dependencies": {
@@ -1128,6 +1128,7 @@ declare class DynamicPageConfigEditorComponent implements OnInit, SettingsValueP
1128
1128
  private focusArrangementFeedback;
1129
1129
  readonly workspaceTab: WritableSignal<number>;
1130
1130
  readonly arrangementSelection: WritableSignal<string>;
1131
+ private arrangementSelectionSource;
1131
1132
  readonly arrangementChoices: readonly [{
1132
1133
  readonly id: "stack";
1133
1134
  readonly weights: readonly [1];
@@ -1261,6 +1262,7 @@ declare class DynamicPageConfigEditorComponent implements OnInit, SettingsValueP
1261
1262
  pinRecommendedThemePreset(): void;
1262
1263
  get hasCanvasWidgets(): boolean;
1263
1264
  stackDevice(device: DeviceKey): void;
1265
+ private stackViolatesLocks;
1264
1266
  private stackedCanvas;
1265
1267
  addGroupingNode(seed?: WidgetPageGroupingDefinition): void;
1266
1268
  removeGroupingNode(index: number): void;
@@ -1312,6 +1314,7 @@ declare class DynamicPageConfigEditorComponent implements OnInit, SettingsValueP
1312
1314
  private stringifyGroupingWidgetKeys;
1313
1315
  private extractShellPreset;
1314
1316
  private removeShellPresetFromContext;
1317
+ private validShellContext;
1315
1318
  private applyShellPresetToContext;
1316
1319
  private lookupPresetLabel;
1317
1320
  private defaultCanvas;