@praxisui/rich-content 8.0.0-beta.11 → 8.0.0-beta.13

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
@@ -109,13 +109,21 @@ through `PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST`.
109
109
 
110
110
  The manifest treats rich content as structured `RichContentDocument` data, not
111
111
  HTML or markdown patches. It governs document replacement, block add/remove,
112
- block order, text updates, canonical link nodes, preset refs and sanitization
113
- policy. Security-sensitive policy edits and destructive removals require
114
- confirmation before a patch can be compiled.
112
+ block order, text updates, canonical link nodes, common node metadata,
113
+ `mediaBlock` fields, `timeline.items[]`, preset refs and sanitization policy.
114
+ Security-sensitive policy edits and destructive removals require confirmation
115
+ before a patch can be compiled.
115
116
 
116
117
  Link authoring uses a first-class `link` node with `label`, `href`, `target`
117
118
  and `rel`; unsafe protocols are rejected by `validateRichContentDocument()`.
118
119
 
120
+ Each operation declares its own editable target resolver, ambiguity policy,
121
+ preconditions, validators, affected paths, effects and typed submission impact.
122
+ Document, block, text, link, media, timeline, preset, metadata and sanitization
123
+ operations are `config-only` because they edit the structured widget input
124
+ contract. `display.mode.set` is `visual-only` because it changes `layout` and
125
+ `rootClassName` without mutating the `RichContentDocument`.
126
+
119
127
  ## Layout Modes
120
128
 
121
129
  The component defaults to `layout="block"` to preserve document-style rendering.
@@ -2333,7 +2333,7 @@ class PraxisRichContentConfigEditor {
2333
2333
 
2334
2334
  @if (parsedDocument?.nodes?.length) {
2335
2335
  <div class="prx-rich-editor__node-list">
2336
- @for (node of parsedDocument?.nodes ?? []; track node.id ?? $index) {
2336
+ @for (node of parsedDocument?.nodes ?? []; track node.id ?? $index; let nodeIndex = $index) {
2337
2337
  <article
2338
2338
  class="prx-rich-editor__node-card"
2339
2339
  [attr.data-rich-editor-node-type]="node.type"
@@ -2573,15 +2573,15 @@ class PraxisRichContentConfigEditor {
2573
2573
  <div class="prx-rich-editor__nested-node">
2574
2574
  <div class="prx-rich-editor__nested-actions">
2575
2575
  <strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
2576
- @if (isRemovalPending('nodes.' + $index + '.items', itemIndex)) {
2577
- <button type="button" (click)="confirmComposeItemRemoval($index, itemIndex)">
2576
+ @if (isRemovalPending('nodes.' + nodeIndex + '.items', itemIndex)) {
2577
+ <button type="button" (click)="confirmComposeItemRemoval(nodeIndex, itemIndex)">
2578
2578
  {{ tx('editor.confirmRemove', 'Confirm remove') }}
2579
2579
  </button>
2580
2580
  <button type="button" (click)="cancelRemoval()">
2581
2581
  {{ tx('editor.cancelRemove', 'Cancel') }}
2582
2582
  </button>
2583
2583
  } @else {
2584
- <button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.items', itemIndex)">
2584
+ <button type="button" (click)="requestNestedRemoval('nodes.' + nodeIndex + '.items', itemIndex)">
2585
2585
  {{ tx('editor.removeBlock', 'Remove') }}
2586
2586
  </button>
2587
2587
  }
@@ -2591,7 +2591,7 @@ class PraxisRichContentConfigEditor {
2591
2591
  <span>{{ tx('editor.blockType', 'Block type') }}</span>
2592
2592
  <select
2593
2593
  [ngModel]="item.type"
2594
- (ngModelChange)="changeComposeItemType($index, itemIndex, $event)"
2594
+ (ngModelChange)="changeComposeItemType(nodeIndex, itemIndex, $event)"
2595
2595
  >
2596
2596
  @for (type of presenterNodeTypes; track type) {
2597
2597
  <option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
@@ -2601,9 +2601,9 @@ class PraxisRichContentConfigEditor {
2601
2601
  <ng-container
2602
2602
  *ngTemplateOutlet="presenterFields; context: {
2603
2603
  node: item,
2604
- path: '$.nodes[' + $index + '].items[' + itemIndex + ']',
2605
- setString: setComposeItemStringField.bind(this, $index, itemIndex),
2606
- setNumber: setComposeItemNumberField.bind(this, $index, itemIndex)
2604
+ path: '$.nodes[' + nodeIndex + '].items[' + itemIndex + ']',
2605
+ setString: setComposeItemStringField.bind(this, nodeIndex, itemIndex),
2606
+ setNumber: setComposeItemNumberField.bind(this, nodeIndex, itemIndex)
2607
2607
  }"
2608
2608
  ></ng-container>
2609
2609
  </div>
@@ -2637,15 +2637,15 @@ class PraxisRichContentConfigEditor {
2637
2637
  <div class="prx-rich-editor__nested-node">
2638
2638
  <div class="prx-rich-editor__nested-actions">
2639
2639
  <strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
2640
- @if (isRemovalPending('nodes.' + $index + '.content', itemIndex)) {
2641
- <button type="button" (click)="confirmCardContentRemoval($index, itemIndex)">
2640
+ @if (isRemovalPending('nodes.' + nodeIndex + '.content', itemIndex)) {
2641
+ <button type="button" (click)="confirmCardContentRemoval(nodeIndex, itemIndex)">
2642
2642
  {{ tx('editor.confirmRemove', 'Confirm remove') }}
2643
2643
  </button>
2644
2644
  <button type="button" (click)="cancelRemoval()">
2645
2645
  {{ tx('editor.cancelRemove', 'Cancel') }}
2646
2646
  </button>
2647
2647
  } @else {
2648
- <button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.content', itemIndex)">
2648
+ <button type="button" (click)="requestNestedRemoval('nodes.' + nodeIndex + '.content', itemIndex)">
2649
2649
  {{ tx('editor.removeBlock', 'Remove') }}
2650
2650
  </button>
2651
2651
  }
@@ -2655,7 +2655,7 @@ class PraxisRichContentConfigEditor {
2655
2655
  <span>{{ tx('editor.blockType', 'Block type') }}</span>
2656
2656
  <select
2657
2657
  [ngModel]="isCardContentEditable(item.type) ? item.type : 'text'"
2658
- (ngModelChange)="changeCardContentType($index, itemIndex, $event)"
2658
+ (ngModelChange)="changeCardContentType(nodeIndex, itemIndex, $event)"
2659
2659
  [disabled]="!isCardContentEditable(item.type)"
2660
2660
  >
2661
2661
  @for (type of cardContentNodeTypes; track type) {
@@ -2667,9 +2667,9 @@ class PraxisRichContentConfigEditor {
2667
2667
  <ng-container
2668
2668
  *ngTemplateOutlet="presenterFields; context: {
2669
2669
  node: item,
2670
- path: '$.nodes[' + $index + '].content[' + itemIndex + ']',
2671
- setString: setCardContentStringField.bind(this, $index, itemIndex),
2672
- setNumber: setCardContentNumberField.bind(this, $index, itemIndex)
2670
+ path: '$.nodes[' + nodeIndex + '].content[' + itemIndex + ']',
2671
+ setString: setCardContentStringField.bind(this, nodeIndex, itemIndex),
2672
+ setNumber: setCardContentNumberField.bind(this, nodeIndex, itemIndex)
2673
2673
  }"
2674
2674
  ></ng-container>
2675
2675
  } @else {
@@ -2734,15 +2734,15 @@ class PraxisRichContentConfigEditor {
2734
2734
  <div class="prx-rich-editor__nested-node">
2735
2735
  <div class="prx-rich-editor__nested-actions">
2736
2736
  <strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
2737
- @if (isRemovalPending('nodes.' + $index + '.timelineItems', itemIndex)) {
2738
- <button type="button" (click)="confirmTimelineItemRemoval($index, itemIndex)">
2737
+ @if (isRemovalPending('nodes.' + nodeIndex + '.timelineItems', itemIndex)) {
2738
+ <button type="button" (click)="confirmTimelineItemRemoval(nodeIndex, itemIndex)">
2739
2739
  {{ tx('editor.confirmRemove', 'Confirm remove') }}
2740
2740
  </button>
2741
2741
  <button type="button" (click)="cancelRemoval()">
2742
2742
  {{ tx('editor.cancelRemove', 'Cancel') }}
2743
2743
  </button>
2744
2744
  } @else {
2745
- <button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.timelineItems', itemIndex)">
2745
+ <button type="button" (click)="requestNestedRemoval('nodes.' + nodeIndex + '.timelineItems', itemIndex)">
2746
2746
  {{ tx('editor.removeBlock', 'Remove') }}
2747
2747
  </button>
2748
2748
  }
@@ -2751,29 +2751,29 @@ class PraxisRichContentConfigEditor {
2751
2751
  <label>
2752
2752
  <span>{{ tx('editor.field.title', 'Title') }}</span>
2753
2753
  <input
2754
- [ngModel]="getTimelineItemField($index, itemIndex, 'title')"
2755
- (ngModelChange)="setTimelineItemField($index, itemIndex, 'title', $event)"
2754
+ [ngModel]="getTimelineItemField(nodeIndex, itemIndex, 'title')"
2755
+ (ngModelChange)="setTimelineItemField(nodeIndex, itemIndex, 'title', $event)"
2756
2756
  />
2757
2757
  </label>
2758
2758
  <label>
2759
2759
  <span>{{ tx('editor.field.subtitle', 'Subtitle') }}</span>
2760
2760
  <input
2761
- [ngModel]="getTimelineItemField($index, itemIndex, 'subtitle')"
2762
- (ngModelChange)="setTimelineItemField($index, itemIndex, 'subtitle', $event)"
2761
+ [ngModel]="getTimelineItemField(nodeIndex, itemIndex, 'subtitle')"
2762
+ (ngModelChange)="setTimelineItemField(nodeIndex, itemIndex, 'subtitle', $event)"
2763
2763
  />
2764
2764
  </label>
2765
2765
  <label>
2766
2766
  <span>{{ tx('editor.field.badge', 'Badge') }}</span>
2767
2767
  <input
2768
- [ngModel]="getTimelineItemField($index, itemIndex, 'badge')"
2769
- (ngModelChange)="setTimelineItemField($index, itemIndex, 'badge', $event)"
2768
+ [ngModel]="getTimelineItemField(nodeIndex, itemIndex, 'badge')"
2769
+ (ngModelChange)="setTimelineItemField(nodeIndex, itemIndex, 'badge', $event)"
2770
2770
  />
2771
2771
  </label>
2772
2772
  <label>
2773
2773
  <span>{{ tx('editor.field.icon', 'Icon') }}</span>
2774
2774
  <input
2775
- [ngModel]="getTimelineItemField($index, itemIndex, 'icon')"
2776
- (ngModelChange)="setTimelineItemField($index, itemIndex, 'icon', $event)"
2775
+ [ngModel]="getTimelineItemField(nodeIndex, itemIndex, 'icon')"
2776
+ (ngModelChange)="setTimelineItemField(nodeIndex, itemIndex, 'icon', $event)"
2777
2777
  [placeholder]="tx('editor.placeholder.icon', 'check_circle')"
2778
2778
  />
2779
2779
  </label>
@@ -3188,7 +3188,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3188
3188
 
3189
3189
  @if (parsedDocument?.nodes?.length) {
3190
3190
  <div class="prx-rich-editor__node-list">
3191
- @for (node of parsedDocument?.nodes ?? []; track node.id ?? $index) {
3191
+ @for (node of parsedDocument?.nodes ?? []; track node.id ?? $index; let nodeIndex = $index) {
3192
3192
  <article
3193
3193
  class="prx-rich-editor__node-card"
3194
3194
  [attr.data-rich-editor-node-type]="node.type"
@@ -3428,15 +3428,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3428
3428
  <div class="prx-rich-editor__nested-node">
3429
3429
  <div class="prx-rich-editor__nested-actions">
3430
3430
  <strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
3431
- @if (isRemovalPending('nodes.' + $index + '.items', itemIndex)) {
3432
- <button type="button" (click)="confirmComposeItemRemoval($index, itemIndex)">
3431
+ @if (isRemovalPending('nodes.' + nodeIndex + '.items', itemIndex)) {
3432
+ <button type="button" (click)="confirmComposeItemRemoval(nodeIndex, itemIndex)">
3433
3433
  {{ tx('editor.confirmRemove', 'Confirm remove') }}
3434
3434
  </button>
3435
3435
  <button type="button" (click)="cancelRemoval()">
3436
3436
  {{ tx('editor.cancelRemove', 'Cancel') }}
3437
3437
  </button>
3438
3438
  } @else {
3439
- <button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.items', itemIndex)">
3439
+ <button type="button" (click)="requestNestedRemoval('nodes.' + nodeIndex + '.items', itemIndex)">
3440
3440
  {{ tx('editor.removeBlock', 'Remove') }}
3441
3441
  </button>
3442
3442
  }
@@ -3446,7 +3446,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3446
3446
  <span>{{ tx('editor.blockType', 'Block type') }}</span>
3447
3447
  <select
3448
3448
  [ngModel]="item.type"
3449
- (ngModelChange)="changeComposeItemType($index, itemIndex, $event)"
3449
+ (ngModelChange)="changeComposeItemType(nodeIndex, itemIndex, $event)"
3450
3450
  >
3451
3451
  @for (type of presenterNodeTypes; track type) {
3452
3452
  <option [value]="type">{{ tx('editor.nodeType.' + type, type) }}</option>
@@ -3456,9 +3456,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3456
3456
  <ng-container
3457
3457
  *ngTemplateOutlet="presenterFields; context: {
3458
3458
  node: item,
3459
- path: '$.nodes[' + $index + '].items[' + itemIndex + ']',
3460
- setString: setComposeItemStringField.bind(this, $index, itemIndex),
3461
- setNumber: setComposeItemNumberField.bind(this, $index, itemIndex)
3459
+ path: '$.nodes[' + nodeIndex + '].items[' + itemIndex + ']',
3460
+ setString: setComposeItemStringField.bind(this, nodeIndex, itemIndex),
3461
+ setNumber: setComposeItemNumberField.bind(this, nodeIndex, itemIndex)
3462
3462
  }"
3463
3463
  ></ng-container>
3464
3464
  </div>
@@ -3492,15 +3492,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3492
3492
  <div class="prx-rich-editor__nested-node">
3493
3493
  <div class="prx-rich-editor__nested-actions">
3494
3494
  <strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
3495
- @if (isRemovalPending('nodes.' + $index + '.content', itemIndex)) {
3496
- <button type="button" (click)="confirmCardContentRemoval($index, itemIndex)">
3495
+ @if (isRemovalPending('nodes.' + nodeIndex + '.content', itemIndex)) {
3496
+ <button type="button" (click)="confirmCardContentRemoval(nodeIndex, itemIndex)">
3497
3497
  {{ tx('editor.confirmRemove', 'Confirm remove') }}
3498
3498
  </button>
3499
3499
  <button type="button" (click)="cancelRemoval()">
3500
3500
  {{ tx('editor.cancelRemove', 'Cancel') }}
3501
3501
  </button>
3502
3502
  } @else {
3503
- <button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.content', itemIndex)">
3503
+ <button type="button" (click)="requestNestedRemoval('nodes.' + nodeIndex + '.content', itemIndex)">
3504
3504
  {{ tx('editor.removeBlock', 'Remove') }}
3505
3505
  </button>
3506
3506
  }
@@ -3510,7 +3510,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3510
3510
  <span>{{ tx('editor.blockType', 'Block type') }}</span>
3511
3511
  <select
3512
3512
  [ngModel]="isCardContentEditable(item.type) ? item.type : 'text'"
3513
- (ngModelChange)="changeCardContentType($index, itemIndex, $event)"
3513
+ (ngModelChange)="changeCardContentType(nodeIndex, itemIndex, $event)"
3514
3514
  [disabled]="!isCardContentEditable(item.type)"
3515
3515
  >
3516
3516
  @for (type of cardContentNodeTypes; track type) {
@@ -3522,9 +3522,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3522
3522
  <ng-container
3523
3523
  *ngTemplateOutlet="presenterFields; context: {
3524
3524
  node: item,
3525
- path: '$.nodes[' + $index + '].content[' + itemIndex + ']',
3526
- setString: setCardContentStringField.bind(this, $index, itemIndex),
3527
- setNumber: setCardContentNumberField.bind(this, $index, itemIndex)
3525
+ path: '$.nodes[' + nodeIndex + '].content[' + itemIndex + ']',
3526
+ setString: setCardContentStringField.bind(this, nodeIndex, itemIndex),
3527
+ setNumber: setCardContentNumberField.bind(this, nodeIndex, itemIndex)
3528
3528
  }"
3529
3529
  ></ng-container>
3530
3530
  } @else {
@@ -3589,15 +3589,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3589
3589
  <div class="prx-rich-editor__nested-node">
3590
3590
  <div class="prx-rich-editor__nested-actions">
3591
3591
  <strong>{{ tx('editor.item', 'Item') }} {{ itemIndex + 1 }}</strong>
3592
- @if (isRemovalPending('nodes.' + $index + '.timelineItems', itemIndex)) {
3593
- <button type="button" (click)="confirmTimelineItemRemoval($index, itemIndex)">
3592
+ @if (isRemovalPending('nodes.' + nodeIndex + '.timelineItems', itemIndex)) {
3593
+ <button type="button" (click)="confirmTimelineItemRemoval(nodeIndex, itemIndex)">
3594
3594
  {{ tx('editor.confirmRemove', 'Confirm remove') }}
3595
3595
  </button>
3596
3596
  <button type="button" (click)="cancelRemoval()">
3597
3597
  {{ tx('editor.cancelRemove', 'Cancel') }}
3598
3598
  </button>
3599
3599
  } @else {
3600
- <button type="button" (click)="requestNestedRemoval('nodes.' + $index + '.timelineItems', itemIndex)">
3600
+ <button type="button" (click)="requestNestedRemoval('nodes.' + nodeIndex + '.timelineItems', itemIndex)">
3601
3601
  {{ tx('editor.removeBlock', 'Remove') }}
3602
3602
  </button>
3603
3603
  }
@@ -3606,29 +3606,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3606
3606
  <label>
3607
3607
  <span>{{ tx('editor.field.title', 'Title') }}</span>
3608
3608
  <input
3609
- [ngModel]="getTimelineItemField($index, itemIndex, 'title')"
3610
- (ngModelChange)="setTimelineItemField($index, itemIndex, 'title', $event)"
3609
+ [ngModel]="getTimelineItemField(nodeIndex, itemIndex, 'title')"
3610
+ (ngModelChange)="setTimelineItemField(nodeIndex, itemIndex, 'title', $event)"
3611
3611
  />
3612
3612
  </label>
3613
3613
  <label>
3614
3614
  <span>{{ tx('editor.field.subtitle', 'Subtitle') }}</span>
3615
3615
  <input
3616
- [ngModel]="getTimelineItemField($index, itemIndex, 'subtitle')"
3617
- (ngModelChange)="setTimelineItemField($index, itemIndex, 'subtitle', $event)"
3616
+ [ngModel]="getTimelineItemField(nodeIndex, itemIndex, 'subtitle')"
3617
+ (ngModelChange)="setTimelineItemField(nodeIndex, itemIndex, 'subtitle', $event)"
3618
3618
  />
3619
3619
  </label>
3620
3620
  <label>
3621
3621
  <span>{{ tx('editor.field.badge', 'Badge') }}</span>
3622
3622
  <input
3623
- [ngModel]="getTimelineItemField($index, itemIndex, 'badge')"
3624
- (ngModelChange)="setTimelineItemField($index, itemIndex, 'badge', $event)"
3623
+ [ngModel]="getTimelineItemField(nodeIndex, itemIndex, 'badge')"
3624
+ (ngModelChange)="setTimelineItemField(nodeIndex, itemIndex, 'badge', $event)"
3625
3625
  />
3626
3626
  </label>
3627
3627
  <label>
3628
3628
  <span>{{ tx('editor.field.icon', 'Icon') }}</span>
3629
3629
  <input
3630
- [ngModel]="getTimelineItemField($index, itemIndex, 'icon')"
3631
- (ngModelChange)="setTimelineItemField($index, itemIndex, 'icon', $event)"
3630
+ [ngModel]="getTimelineItemField(nodeIndex, itemIndex, 'icon')"
3631
+ (ngModelChange)="setTimelineItemField(nodeIndex, itemIndex, 'icon', $event)"
3632
3632
  [placeholder]="tx('editor.placeholder.icon', 'check_circle')"
3633
3633
  />
3634
3634
  </label>
@@ -3982,6 +3982,8 @@ const RICH_CONTENT_AI_CAPABILITIES = {
3982
3982
  { path: 'document.nodes[].icon', category: 'richNode', valueKind: 'string', description: 'Nome do icone Material Symbols para node icon ou badge.' },
3983
3983
  { path: 'document.nodes[].label', category: 'richNode', valueKind: 'string', description: 'Label literal para badge, metric ou progress.' },
3984
3984
  { path: 'document.nodes[].labelExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para label.' },
3985
+ { path: 'document.nodes[].title', category: 'richNode', valueKind: 'string', description: 'Titulo literal para card, mediaBlock ou timeline.' },
3986
+ { path: 'document.nodes[].subtitle', category: 'richNode', valueKind: 'string', description: 'Subtitulo literal para card ou mediaBlock.' },
3985
3987
  { path: 'document.nodes[].src', category: 'richNode', valueKind: 'string', description: 'URL literal para image.' },
3986
3988
  { path: 'document.nodes[].srcExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para image src.' },
3987
3989
  { path: 'document.nodes[].alt', category: 'richNode', valueKind: 'string', description: 'Texto alternativo literal para image.' },
@@ -3992,7 +3994,12 @@ const RICH_CONTENT_AI_CAPABILITIES = {
3992
3994
  { path: 'document.nodes[].nameExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para avatar.' },
3993
3995
  { path: 'document.nodes[].valueExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para metric/progress.' },
3994
3996
  { path: 'document.nodes[].items', category: 'richNode', valueKind: 'array', description: 'Itens internos de compose.' },
3997
+ { path: 'document.nodes[].items[].title', category: 'richNode', valueKind: 'string', description: 'Titulo de item de timeline quando document.nodes[].type for timeline.' },
3998
+ { path: 'document.nodes[].items[].subtitle', category: 'richNode', valueKind: 'string', description: 'Subtitulo de item de timeline quando document.nodes[].type for timeline.' },
3999
+ { path: 'document.nodes[].items[].badge', category: 'richNode', valueKind: 'string', description: 'Badge de item de timeline quando document.nodes[].type for timeline.' },
4000
+ { path: 'document.nodes[].items[].icon', category: 'richNode', valueKind: 'string', description: 'Icone de item de timeline quando document.nodes[].type for timeline.' },
3995
4001
  { path: 'document.nodes[].content', category: 'richNode', valueKind: 'array', description: 'Conteudo interno de card.' },
4002
+ { path: 'document.nodes[].avatar', category: 'richNode', valueKind: 'object', description: 'Avatar estruturado de mediaBlock.' },
3996
4003
  { path: 'document.nodes[].trailing', category: 'richNode', valueKind: 'array', description: 'Conteudo trailing de mediaBlock.' },
3997
4004
  { path: 'document.nodes[].ref', category: 'richNode', valueKind: 'object', description: 'Referencia registravel de preset rich-block.' },
3998
4005
  { path: 'context', category: 'richContent', valueKind: 'object', description: 'Contexto externo injetado pelo host ou page-builder.' },
@@ -4002,23 +4009,6 @@ const RICH_CONTENT_AI_CAPABILITIES = {
4002
4009
  ],
4003
4010
  };
4004
4011
 
4005
- const nodeTypeEnum = [
4006
- 'text',
4007
- 'icon',
4008
- 'image',
4009
- 'link',
4010
- 'badge',
4011
- 'avatar',
4012
- 'metric',
4013
- 'progress',
4014
- 'compose',
4015
- 'card',
4016
- 'mediaBlock',
4017
- 'timeline',
4018
- 'preset',
4019
- ];
4020
- const layoutEnum = ['block', 'inline'];
4021
- const linkTargetEnum = ['_self', '_blank'];
4022
4012
  const richContentDocumentSchema = {
4023
4013
  type: 'object',
4024
4014
  required: ['kind', 'version', 'nodes'],
@@ -4043,7 +4033,7 @@ const blockPatchSchema = {
4043
4033
  label: { type: 'string' },
4044
4034
  labelExpr: { type: 'string' },
4045
4035
  href: { type: 'string' },
4046
- target: { enum: linkTargetEnum },
4036
+ target: { enum: ['_self', '_blank'] },
4047
4037
  rel: { type: 'string' },
4048
4038
  src: { type: 'string' },
4049
4039
  alt: { type: 'string' },
@@ -4062,7 +4052,7 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4062
4052
  { name: 'nodes', type: 'RichBlockNode[] | null', description: 'Direct node list used only when the host controls the document envelope.' },
4063
4053
  { name: 'context', type: 'JsonLogicDataRecord | null', description: 'External context used by expression paths and Json Logic rules.' },
4064
4054
  { name: 'hostCapabilities', type: 'RichBlockHostCapabilities | null', description: 'Host-mediated preset, action, data and embed capabilities; functions are not serialized in public contracts.' },
4065
- { name: 'layout', type: "'block' | 'inline'", allowedValues: layoutEnum, description: 'Renderer layout mode.' },
4055
+ { name: 'layout', type: "'block' | 'inline'", allowedValues: ['block', 'inline'], description: 'Renderer layout mode.' },
4066
4056
  { name: 'rootClassName', type: 'string', description: 'CSS class applied to the renderer root.' },
4067
4057
  ],
4068
4058
  editableTargets: [
@@ -4071,6 +4061,8 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4071
4061
  { kind: 'text', resolver: 'rich-text-node-by-id-or-path', description: 'Text-bearing nodes, including text, badge, metric, progress, card and timeline labels.' },
4072
4062
  { kind: 'link', resolver: 'rich-link-node-by-id-or-path', description: 'Canonical link node with label, href, target and rel.' },
4073
4063
  { kind: 'media', resolver: 'rich-media-node-by-id-or-path', description: 'Image, avatar image and mediaBlock child nodes.' },
4064
+ { kind: 'timelineItem', resolver: 'rich-timeline-item-by-block-id-and-item-id', description: 'Timeline items nested inside timeline nodes; stable item id is preferred over index.' },
4065
+ { kind: 'metadata', resolver: 'rich-block-common-metadata', description: 'Common node metadata such as testId, className, visibleWhen and safe style.' },
4074
4066
  { kind: 'preset', resolver: 'rich-block-preset-ref', description: 'Preset nodes resolved through PRAXIS_RICH_BLOCK_PRESETS or hostCapabilities.resolvePreset.' },
4075
4067
  { kind: 'sanitizationPolicy', resolver: 'rich-content-validation-policy', description: 'Document validation policy enforced by validateRichContentDocument.' },
4076
4068
  { kind: 'display', resolver: 'rich-content-layout-and-root-class', description: 'Layout and rootClassName runtime inputs.' },
@@ -4084,9 +4076,11 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4084
4076
  target: { kind: 'document', resolver: 'rich-content-document-root', ambiguityPolicy: 'fail', required: false },
4085
4077
  inputSchema: richContentDocumentSchema,
4086
4078
  effects: [{ kind: 'set-value', path: 'document' }],
4079
+ destructive: false,
4080
+ requiresConfirmation: false,
4087
4081
  validators: ['document-shape-canonical', 'document-version-supported', 'node-types-supported', 'unsafe-url-rejected', 'unsafe-style-rejected', 'editor-runtime-round-trip'],
4088
4082
  affectedPaths: ['document'],
4089
- submissionImpact: false,
4083
+ submissionImpact: 'config-only',
4090
4084
  preconditions: ['config-initialized'],
4091
4085
  },
4092
4086
  {
@@ -4099,7 +4093,7 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4099
4093
  type: 'object',
4100
4094
  required: ['type'],
4101
4095
  properties: {
4102
- type: { enum: nodeTypeEnum },
4096
+ type: { enum: ['text', 'icon', 'image', 'link', 'badge', 'avatar', 'metric', 'progress', 'compose', 'card', 'mediaBlock', 'timeline', 'preset'] },
4103
4097
  node: { type: 'object' },
4104
4098
  afterBlockId: { type: 'string' },
4105
4099
  beforeBlockId: { type: 'string' },
@@ -4109,13 +4103,15 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4109
4103
  reads: ['document.nodes[]'],
4110
4104
  writes: ['document.nodes[]'],
4111
4105
  identityKeys: ['document.nodes[].id'],
4112
- inputSchema: { type: 'object', required: ['type'], properties: { type: { enum: nodeTypeEnum }, node: { type: 'object' }, afterBlockId: { type: 'string' }, beforeBlockId: { type: 'string' } } },
4106
+ inputSchema: { type: 'object', required: ['type'], properties: { type: { enum: ['text', 'icon', 'image', 'link', 'badge', 'avatar', 'metric', 'progress', 'compose', 'card', 'mediaBlock', 'timeline', 'preset'] }, node: { type: 'object' }, afterBlockId: { type: 'string' }, beforeBlockId: { type: 'string' } } },
4113
4107
  failureModes: ['unsupported-node-type', 'duplicate-node-id', 'invalid-node-shape', 'unsafe-url', 'unsafe-style'],
4114
4108
  description: 'Creates a supported RichBlockNode, validates it with validateRichContentDocument semantics and inserts it by stable neighbor id when provided.',
4115
4109
  } }],
4110
+ destructive: false,
4111
+ requiresConfirmation: false,
4116
4112
  validators: ['node-types-supported', 'block-id-unique', 'document-shape-canonical', 'unsafe-url-rejected', 'unsafe-style-rejected', 'editor-runtime-round-trip'],
4117
4113
  affectedPaths: ['document.nodes[]'],
4118
- submissionImpact: false,
4114
+ submissionImpact: 'config-only',
4119
4115
  preconditions: ['config-initialized'],
4120
4116
  },
4121
4117
  {
@@ -4130,7 +4126,7 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4130
4126
  requiresConfirmation: true,
4131
4127
  validators: ['block-exists', 'destructive-removal-confirmed', 'document-not-empty-when-required', 'editor-runtime-round-trip'],
4132
4128
  affectedPaths: ['document.nodes[]'],
4133
- submissionImpact: false,
4129
+ submissionImpact: 'config-only',
4134
4130
  preconditions: ['config-initialized', 'target-block-exists', 'confirmation-collected'],
4135
4131
  },
4136
4132
  {
@@ -4141,9 +4137,11 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4141
4137
  target: { kind: 'block', resolver: 'rich-block-by-id-or-index', ambiguityPolicy: 'fail', required: true },
4142
4138
  inputSchema: { type: 'object', required: ['blockId'], properties: { blockId: { type: 'string' }, beforeBlockId: { type: 'string' }, afterBlockId: { type: 'string' } } },
4143
4139
  effects: [{ kind: 'reorder-by-key', path: 'document.nodes[]', key: 'id' }],
4140
+ destructive: false,
4141
+ requiresConfirmation: false,
4144
4142
  validators: ['block-exists', 'block-order-deterministic', 'document-shape-canonical', 'editor-runtime-round-trip'],
4145
4143
  affectedPaths: ['document.nodes[]'],
4146
- submissionImpact: false,
4144
+ submissionImpact: 'config-only',
4147
4145
  preconditions: ['config-initialized', 'target-block-exists'],
4148
4146
  },
4149
4147
  {
@@ -4154,9 +4152,11 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4154
4152
  target: { kind: 'text', resolver: 'rich-text-node-by-id-or-path', ambiguityPolicy: 'fail', required: true },
4155
4153
  inputSchema: { type: 'object', minProperties: 1, properties: { text: { type: 'string' }, textExpr: { type: 'string' }, label: { type: 'string' }, labelExpr: { type: 'string' }, title: { type: 'string' }, subtitle: { type: 'string' } } },
4156
4154
  effects: [{ kind: 'merge-by-key', path: 'document.nodes[]', key: 'id' }],
4155
+ destructive: false,
4156
+ requiresConfirmation: false,
4157
4157
  validators: ['block-exists', 'text-target-supports-field', 'expression-path-safe', 'document-shape-canonical', 'editor-runtime-round-trip'],
4158
4158
  affectedPaths: ['document.nodes[].text', 'document.nodes[].textExpr', 'document.nodes[].label', 'document.nodes[].labelExpr', 'document.nodes[].title', 'document.nodes[].subtitle'],
4159
- submissionImpact: false,
4159
+ submissionImpact: 'config-only',
4160
4160
  preconditions: ['config-initialized', 'target-block-exists'],
4161
4161
  },
4162
4162
  {
@@ -4165,11 +4165,13 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4165
4165
  scope: 'templating',
4166
4166
  targetKind: 'link',
4167
4167
  target: { kind: 'link', resolver: 'document-nodes-array', ambiguityPolicy: 'fail', required: false },
4168
- inputSchema: { type: 'object', required: ['label', 'href'], properties: { id: { type: 'string' }, label: { type: 'string' }, labelExpr: { type: 'string' }, href: { type: 'string' }, target: { enum: linkTargetEnum }, rel: { type: 'string' } } },
4168
+ inputSchema: { type: 'object', required: ['label', 'href'], properties: { id: { type: 'string' }, label: { type: 'string' }, labelExpr: { type: 'string' }, href: { type: 'string' }, target: { enum: ['_self', '_blank'] }, rel: { type: 'string' } } },
4169
4169
  effects: [{ kind: 'append-unique', path: 'document.nodes[]', key: 'id' }],
4170
+ destructive: false,
4171
+ requiresConfirmation: false,
4170
4172
  validators: ['link-url-safe', 'link-policy-explicit', 'node-types-supported', 'block-id-unique', 'editor-runtime-round-trip'],
4171
4173
  affectedPaths: ['document.nodes[]', 'document.nodes[].href', 'document.nodes[].target', 'document.nodes[].rel'],
4172
- submissionImpact: false,
4174
+ submissionImpact: 'config-only',
4173
4175
  preconditions: ['config-initialized'],
4174
4176
  },
4175
4177
  {
@@ -4191,7 +4193,7 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4191
4193
  requiresConfirmation: true,
4192
4194
  validators: ['link-target-exists', 'destructive-removal-confirmed', 'document-shape-canonical', 'editor-runtime-round-trip'],
4193
4195
  affectedPaths: ['document.nodes[]'],
4194
- submissionImpact: false,
4196
+ submissionImpact: 'config-only',
4195
4197
  preconditions: ['config-initialized', 'target-link-exists', 'confirmation-collected'],
4196
4198
  },
4197
4199
  {
@@ -4209,11 +4211,116 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4209
4211
  failureModes: ['preset-not-found', 'preset-kind-invalid', 'preset-document-invalid', 'preset-cycle-detected'],
4210
4212
  description: 'Resolves a rich-block preset through the governed registry/host mediation and inserts or replaces a preset reference without serializing functions.',
4211
4213
  } }],
4214
+ destructive: false,
4215
+ requiresConfirmation: false,
4212
4216
  validators: ['preset-ref-valid', 'preset-exists-or-host-mediated', 'host-capabilities-serializable', 'document-shape-canonical', 'editor-runtime-round-trip'],
4213
- affectedPaths: ['document.nodes[]', 'document.nodes[].ref', 'document.nodes[].inputs', 'hostCapabilities'],
4214
- submissionImpact: false,
4217
+ affectedPaths: ['document.nodes[]', 'document.nodes[].ref', 'document.nodes[].inputs'],
4218
+ submissionImpact: 'config-only',
4215
4219
  preconditions: ['config-initialized'],
4216
4220
  },
4221
+ {
4222
+ operationId: 'metadata.update',
4223
+ title: 'Update rich block metadata',
4224
+ scope: 'templating',
4225
+ targetKind: 'metadata',
4226
+ target: { kind: 'metadata', resolver: 'rich-block-by-id-or-index', ambiguityPolicy: 'fail', required: true },
4227
+ inputSchema: { type: 'object', minProperties: 1, properties: { blockId: { type: 'string' }, testId: { type: 'string' }, className: { type: 'string' }, visibleWhen: { type: 'object' }, style: { type: 'object' } } },
4228
+ effects: [{ kind: 'merge-by-key', path: 'document.nodes[]', key: 'id' }],
4229
+ destructive: false,
4230
+ requiresConfirmation: false,
4231
+ validators: ['block-exists', 'class-name-safe', 'visible-when-json-logic', 'unsafe-style-rejected', 'document-shape-canonical', 'editor-runtime-round-trip'],
4232
+ affectedPaths: ['document.nodes[].testId', 'document.nodes[].className', 'document.nodes[].visibleWhen', 'document.nodes[].style'],
4233
+ submissionImpact: 'config-only',
4234
+ preconditions: ['config-initialized', 'target-block-exists'],
4235
+ },
4236
+ {
4237
+ operationId: 'mediaBlock.update',
4238
+ title: 'Update media block fields',
4239
+ scope: 'templating',
4240
+ targetKind: 'media',
4241
+ target: { kind: 'media', resolver: 'rich-media-node-by-id-or-path', ambiguityPolicy: 'fail', required: true },
4242
+ inputSchema: { type: 'object', minProperties: 1, properties: { blockId: { type: 'string' }, avatarName: { type: 'string' }, avatarImageSrc: { type: 'string' }, title: { type: 'string' }, titleExpr: { type: 'string' }, subtitle: { type: 'string' }, subtitleExpr: { type: 'string' } } },
4243
+ effects: [{ kind: 'compile-domain-patch', handler: 'rich-content-media-block-update', handlerContract: {
4244
+ reads: ['document.nodes[]', 'document.nodes[].id', 'document.nodes[].type'],
4245
+ writes: ['document.nodes[].avatar', 'document.nodes[].title', 'document.nodes[].subtitle'],
4246
+ identityKeys: ['document.nodes[].id'],
4247
+ inputSchema: { type: 'object', minProperties: 1, properties: { blockId: { type: 'string' }, avatarName: { type: 'string' }, avatarImageSrc: { type: 'string' }, title: { type: 'string' }, titleExpr: { type: 'string' }, subtitle: { type: 'string' }, subtitleExpr: { type: 'string' } } },
4248
+ failureModes: ['media-block-not-found', 'target-not-media-block', 'unsafe-url', 'invalid-text-node'],
4249
+ description: 'Updates the structured mediaBlock avatar/title/subtitle fields while preserving meta and trailing content for advanced JSON authoring.',
4250
+ } }],
4251
+ destructive: false,
4252
+ requiresConfirmation: false,
4253
+ validators: ['block-exists', 'media-block-target-valid', 'unsafe-url-rejected', 'expression-path-safe', 'document-shape-canonical', 'editor-runtime-round-trip'],
4254
+ affectedPaths: ['document.nodes[].avatar', 'document.nodes[].title', 'document.nodes[].subtitle'],
4255
+ submissionImpact: 'config-only',
4256
+ preconditions: ['config-initialized', 'target-block-exists'],
4257
+ },
4258
+ {
4259
+ operationId: 'timeline.item.add',
4260
+ title: 'Add timeline item',
4261
+ scope: 'templating',
4262
+ targetKind: 'timelineItem',
4263
+ target: { kind: 'timelineItem', resolver: 'rich-timeline-node-by-id-or-path', ambiguityPolicy: 'fail', required: true },
4264
+ inputSchema: { type: 'object', required: ['timelineBlockId', 'item'], properties: { timelineBlockId: { type: 'string' }, item: { type: 'object' }, afterItemId: { type: 'string' }, beforeItemId: { type: 'string' } } },
4265
+ effects: [{ kind: 'compile-domain-patch', handler: 'rich-content-timeline-item-add', handlerContract: {
4266
+ reads: ['document.nodes[]', 'document.nodes[].items[]'],
4267
+ writes: ['document.nodes[].items[]'],
4268
+ identityKeys: ['document.nodes[].id', 'document.nodes[].items[].id'],
4269
+ inputSchema: { type: 'object', required: ['timelineBlockId', 'item'], properties: { timelineBlockId: { type: 'string' }, item: { type: 'object' }, afterItemId: { type: 'string' }, beforeItemId: { type: 'string' } } },
4270
+ failureModes: ['timeline-not-found', 'duplicate-timeline-item-id', 'invalid-timeline-item'],
4271
+ description: 'Adds a timeline item to a timeline node using stable timeline and item ids when present; array index is a resolver fallback only.',
4272
+ } }],
4273
+ destructive: false,
4274
+ requiresConfirmation: false,
4275
+ validators: ['block-exists', 'timeline-target-valid', 'timeline-item-valid', 'timeline-item-id-unique', 'document-shape-canonical', 'editor-runtime-round-trip'],
4276
+ affectedPaths: ['document.nodes[].items[]'],
4277
+ submissionImpact: 'config-only',
4278
+ preconditions: ['config-initialized', 'target-block-exists'],
4279
+ },
4280
+ {
4281
+ operationId: 'timeline.item.update',
4282
+ title: 'Update timeline item',
4283
+ scope: 'templating',
4284
+ targetKind: 'timelineItem',
4285
+ target: { kind: 'timelineItem', resolver: 'rich-timeline-item-by-block-id-and-item-id', ambiguityPolicy: 'fail', required: true },
4286
+ inputSchema: { type: 'object', required: ['timelineBlockId'], minProperties: 2, properties: { timelineBlockId: { type: 'string' }, itemId: { type: 'string' }, itemIndex: { type: 'number' }, field: { enum: ['title', 'titleExpr', 'subtitle', 'subtitleExpr', 'meta', 'metaExpr', 'icon', 'iconExpr', 'badge', 'badgeExpr'] }, value: { type: 'string' }, patch: { type: 'object' } } },
4287
+ effects: [{ kind: 'compile-domain-patch', handler: 'rich-content-timeline-item-update', handlerContract: {
4288
+ reads: ['document.nodes[]', 'document.nodes[].items[]'],
4289
+ writes: ['document.nodes[].items[]'],
4290
+ identityKeys: ['document.nodes[].id', 'document.nodes[].items[].id'],
4291
+ inputSchema: { type: 'object', required: ['timelineBlockId'], properties: { timelineBlockId: { type: 'string' }, itemId: { type: 'string' }, itemIndex: { type: 'number' }, field: { enum: ['title', 'titleExpr', 'subtitle', 'subtitleExpr', 'meta', 'metaExpr', 'icon', 'iconExpr', 'badge', 'badgeExpr'] }, value: { type: 'string' }, patch: { type: 'object' } } },
4292
+ failureModes: ['timeline-not-found', 'timeline-item-not-found', 'unsupported-timeline-field', 'invalid-timeline-item'],
4293
+ description: 'Updates a timeline item by stable item id when available, preserving the item object shape accepted by RichTimelineItem.',
4294
+ } }],
4295
+ destructive: false,
4296
+ requiresConfirmation: false,
4297
+ validators: ['block-exists', 'timeline-target-valid', 'timeline-item-exists', 'timeline-item-field-supported', 'document-shape-canonical', 'editor-runtime-round-trip'],
4298
+ affectedPaths: ['document.nodes[].items[].title', 'document.nodes[].items[].subtitle', 'document.nodes[].items[].meta', 'document.nodes[].items[].icon', 'document.nodes[].items[].badge'],
4299
+ submissionImpact: 'config-only',
4300
+ preconditions: ['config-initialized', 'target-block-exists', 'target-timeline-item-exists'],
4301
+ },
4302
+ {
4303
+ operationId: 'timeline.item.remove',
4304
+ title: 'Remove timeline item',
4305
+ scope: 'templating',
4306
+ targetKind: 'timelineItem',
4307
+ target: { kind: 'timelineItem', resolver: 'rich-timeline-item-by-block-id-and-item-id', ambiguityPolicy: 'fail', required: true },
4308
+ inputSchema: { type: 'object', required: ['timelineBlockId'], properties: { timelineBlockId: { type: 'string' }, itemId: { type: 'string' }, itemIndex: { type: 'number' } } },
4309
+ effects: [{ kind: 'compile-domain-patch', handler: 'rich-content-timeline-item-remove', handlerContract: {
4310
+ reads: ['document.nodes[]', 'document.nodes[].items[]'],
4311
+ writes: ['document.nodes[].items[]'],
4312
+ identityKeys: ['document.nodes[].id', 'document.nodes[].items[].id'],
4313
+ inputSchema: { type: 'object', required: ['timelineBlockId'], properties: { timelineBlockId: { type: 'string' }, itemId: { type: 'string' }, itemIndex: { type: 'number' } } },
4314
+ failureModes: ['timeline-not-found', 'timeline-item-not-found', 'ambiguous-timeline-item'],
4315
+ description: 'Removes a timeline item from a timeline node after explicit confirmation; stable item id is preferred over index.',
4316
+ } }],
4317
+ destructive: true,
4318
+ requiresConfirmation: true,
4319
+ validators: ['timeline-target-valid', 'timeline-item-exists', 'destructive-removal-confirmed', 'document-shape-canonical', 'editor-runtime-round-trip'],
4320
+ affectedPaths: ['document.nodes[].items[]'],
4321
+ submissionImpact: 'config-only',
4322
+ preconditions: ['config-initialized', 'target-timeline-item-exists', 'confirmation-collected'],
4323
+ },
4217
4324
  {
4218
4325
  operationId: 'sanitizationPolicy.set',
4219
4326
  title: 'Set rich content sanitization policy',
@@ -4223,7 +4330,7 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4223
4330
  inputSchema: { type: 'object', properties: { allowHtml: { const: false }, allowedUrlProtocols: { type: 'array', items: { type: 'string' } }, allowImageDataUrls: { type: 'boolean' }, maxNodeDepth: { type: 'number' }, maxNodeCount: { type: 'number' } } },
4224
4331
  effects: [{ kind: 'compile-domain-patch', handler: 'rich-content-sanitization-policy', handlerContract: {
4225
4332
  reads: ['document', 'validateRichContentDocument'],
4226
- writes: ['diagnostics'],
4333
+ writes: ['document'],
4227
4334
  identityKeys: ['document.kind', 'document.version'],
4228
4335
  inputSchema: { type: 'object', properties: { allowHtml: { const: false }, allowedUrlProtocols: { type: 'array', items: { type: 'string' } }, allowImageDataUrls: { type: 'boolean' }, maxNodeDepth: { type: 'number' }, maxNodeCount: { type: 'number' } } },
4229
4336
  failureModes: ['html-not-supported', 'unsafe-protocol', 'max-depth-too-high', 'max-node-count-too-high'],
@@ -4232,8 +4339,8 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4232
4339
  destructive: true,
4233
4340
  requiresConfirmation: true,
4234
4341
  validators: ['sanitization-policy-explicit', 'unsafe-html-rejected', 'unsafe-url-rejected', 'security-change-confirmed', 'editor-runtime-round-trip'],
4235
- affectedPaths: ['document', 'diagnostics'],
4236
- submissionImpact: false,
4342
+ affectedPaths: ['document'],
4343
+ submissionImpact: 'config-only',
4237
4344
  preconditions: ['config-initialized', 'confirmation-collected'],
4238
4345
  },
4239
4346
  {
@@ -4242,11 +4349,13 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4242
4349
  scope: 'global',
4243
4350
  targetKind: 'display',
4244
4351
  target: { kind: 'display', resolver: 'rich-content-layout-and-root-class', ambiguityPolicy: 'fail', required: false },
4245
- inputSchema: { type: 'object', properties: { layout: { enum: layoutEnum }, rootClassName: { type: 'string' } } },
4352
+ inputSchema: { type: 'object', properties: { layout: { enum: ['block', 'inline'] }, rootClassName: { type: 'string' } } },
4246
4353
  effects: [{ kind: 'set-value', path: 'layout' }, { kind: 'set-value', path: 'rootClassName' }],
4354
+ destructive: false,
4355
+ requiresConfirmation: false,
4247
4356
  validators: ['layout-valid', 'root-class-safe', 'editor-runtime-round-trip'],
4248
4357
  affectedPaths: ['layout', 'rootClassName'],
4249
- submissionImpact: false,
4358
+ submissionImpact: 'visual-only',
4250
4359
  preconditions: ['config-initialized'],
4251
4360
  },
4252
4361
  ],
@@ -4275,12 +4384,21 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4275
4384
  { validatorId: 'layout-valid', level: 'error', code: 'PRC022', description: 'Layout must be block or inline.' },
4276
4385
  { validatorId: 'root-class-safe', level: 'warning', code: 'PRC023', description: 'rootClassName must contain safe CSS class tokens.' },
4277
4386
  { validatorId: 'editor-runtime-round-trip', level: 'error', code: 'PRC024', description: 'Config editor, Settings Panel payload and runtime renderer must preserve document structure, layout and rootClassName.' },
4387
+ { validatorId: 'class-name-safe', level: 'error', code: 'PRC025', description: 'Node className values must contain safe CSS class tokens.' },
4388
+ { validatorId: 'visible-when-json-logic', level: 'error', code: 'PRC026', description: 'visibleWhen must be a Json Logic expression object and not a string DSL.' },
4389
+ { validatorId: 'media-block-target-valid', level: 'error', code: 'PRC027', description: 'Media operations must target a mediaBlock node and preserve structured avatar/text child nodes.' },
4390
+ { validatorId: 'timeline-target-valid', level: 'error', code: 'PRC028', description: 'Timeline item operations must target a timeline node with an items array.' },
4391
+ { validatorId: 'timeline-item-valid', level: 'error', code: 'PRC029', description: 'Timeline items must be objects matching RichTimelineItem fields.' },
4392
+ { validatorId: 'timeline-item-id-unique', level: 'warning', code: 'PRC030', description: 'Timeline item ids should be unique inside a timeline node when present.' },
4393
+ { validatorId: 'timeline-item-exists', level: 'error', code: 'PRC031', description: 'Timeline item updates and removals must resolve an existing item by stable id or explicit fallback index.' },
4394
+ { validatorId: 'timeline-item-field-supported', level: 'error', code: 'PRC032', description: 'Timeline item updates may only target supported RichTimelineItem fields.' },
4278
4395
  ],
4279
4396
  roundTripRequirements: [
4280
4397
  'The editor must produce the same widget input envelope consumed by page-builder: inputs.document, inputs.layout and inputs.rootClassName.',
4281
4398
  'Rich content must remain structured RichContentDocument JSON; arbitrary HTML and script URL patches are rejected before persistence.',
4282
4399
  'Block identity should prefer document.nodes[].id; array index is a resolver fallback only and cannot be the canonical identity for generated patches.',
4283
4400
  'Link authoring uses canonical link nodes with safe href/target/rel instead of markdown or raw HTML embedded in text nodes.',
4401
+ 'MediaBlock, timeline item, metadata, visibleWhen and safe style authoring use the same structured paths exposed by the visual config editor.',
4284
4402
  'Preset authoring persists only rich-block refs and inputs; host capability functions remain runtime-mediated and are not serialized.',
4285
4403
  ],
4286
4404
  examples: [
@@ -4293,6 +4411,10 @@ const PRAXIS_RICH_CONTENT_AUTHORING_MANIFEST = {
4293
4411
  { id: 'reject-script-link', request: 'Add a link to javascript:alert(1).', operationId: 'link.add', params: { label: 'Bad link', href: 'javascript:alert(1)' }, isPositive: false },
4294
4412
  { id: 'remove-docs-link', request: 'Remove the documentation link but keep its label as text.', operationId: 'link.remove', target: 'docs-link', params: { linkId: 'docs-link', preserveLabelAsText: true }, isPositive: true },
4295
4413
  { id: 'apply-profile-preset', request: 'Apply the profile summary rich-block preset.', operationId: 'preset.apply', params: { ref: { kind: 'rich-block', namespace: 'praxis.rich-content', presetId: 'profile-summary', version: '1.0.0' } }, isPositive: true },
4414
+ { id: 'show-only-active-row', request: 'Show the status block only when row.active is true.', operationId: 'metadata.update', target: 'status', params: { blockId: 'status', visibleWhen: { '===': [{ var: 'row.active' }, true] } }, isPositive: true },
4415
+ { id: 'update-media-title', request: 'Update the profile media block title and avatar name.', operationId: 'mediaBlock.update', target: 'profile-media', params: { blockId: 'profile-media', avatarName: 'Ana Silva', title: 'Profile summary' }, isPositive: true },
4416
+ { id: 'add-created-timeline-item', request: 'Add a Created item to the activity timeline.', operationId: 'timeline.item.add', target: 'activity', params: { timelineBlockId: 'activity', item: { id: 'created', title: 'Created', badge: 'Done' } }, isPositive: true },
4417
+ { id: 'reject-unknown-timeline-field', request: 'Set a custom script field on a timeline item.', operationId: 'timeline.item.update', target: 'activity.created', params: { timelineBlockId: 'activity', itemId: 'created', field: 'script', value: 'alert(1)' }, isPositive: false },
4296
4418
  { id: 'reject-html-policy', request: 'Allow arbitrary HTML in this rich content document.', operationId: 'sanitizationPolicy.set', params: { allowHtml: true }, isPositive: false },
4297
4419
  { id: 'compact-inline-layout', request: 'Render this rich content inline with root class status-line.', operationId: 'display.mode.set', params: { layout: 'inline', rootClassName: 'status-line' }, isPositive: true },
4298
4420
  ],
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@praxisui/rich-content",
3
- "version": "8.0.0-beta.11",
3
+ "version": "8.0.0-beta.13",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^20.3.0",
6
6
  "@angular/core": "^20.3.0",
7
- "@praxisui/core": "^8.0.0-beta.11"
7
+ "@praxisui/core": "^8.0.0-beta.13"
8
8
  },
9
9
  "dependencies": {
10
10
  "tslib": "^2.3.0"