@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 +11 -3
- package/fesm2022/praxisui-rich-content.mjs +210 -88
- package/package.json +2 -2
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,
|
|
113
|
-
|
|
114
|
-
|
|
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.' +
|
|
2577
|
-
<button type="button" (click)="confirmComposeItemRemoval(
|
|
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.' +
|
|
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(
|
|
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[' +
|
|
2605
|
-
setString: setComposeItemStringField.bind(this,
|
|
2606
|
-
setNumber: setComposeItemNumberField.bind(this,
|
|
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.' +
|
|
2641
|
-
<button type="button" (click)="confirmCardContentRemoval(
|
|
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.' +
|
|
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(
|
|
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[' +
|
|
2671
|
-
setString: setCardContentStringField.bind(this,
|
|
2672
|
-
setNumber: setCardContentNumberField.bind(this,
|
|
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.' +
|
|
2738
|
-
<button type="button" (click)="confirmTimelineItemRemoval(
|
|
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.' +
|
|
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(
|
|
2755
|
-
(ngModelChange)="setTimelineItemField(
|
|
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(
|
|
2762
|
-
(ngModelChange)="setTimelineItemField(
|
|
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(
|
|
2769
|
-
(ngModelChange)="setTimelineItemField(
|
|
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(
|
|
2776
|
-
(ngModelChange)="setTimelineItemField(
|
|
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.' +
|
|
3432
|
-
<button type="button" (click)="confirmComposeItemRemoval(
|
|
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.' +
|
|
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(
|
|
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[' +
|
|
3460
|
-
setString: setComposeItemStringField.bind(this,
|
|
3461
|
-
setNumber: setComposeItemNumberField.bind(this,
|
|
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.' +
|
|
3496
|
-
<button type="button" (click)="confirmCardContentRemoval(
|
|
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.' +
|
|
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(
|
|
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[' +
|
|
3526
|
-
setString: setCardContentStringField.bind(this,
|
|
3527
|
-
setNumber: setCardContentNumberField.bind(this,
|
|
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.' +
|
|
3593
|
-
<button type="button" (click)="confirmTimelineItemRemoval(
|
|
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.' +
|
|
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(
|
|
3610
|
-
(ngModelChange)="setTimelineItemField(
|
|
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(
|
|
3617
|
-
(ngModelChange)="setTimelineItemField(
|
|
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(
|
|
3624
|
-
(ngModelChange)="setTimelineItemField(
|
|
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(
|
|
3631
|
-
(ngModelChange)="setTimelineItemField(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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'
|
|
4214
|
-
submissionImpact:
|
|
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: ['
|
|
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'
|
|
4236
|
-
submissionImpact:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
7
|
+
"@praxisui/core": "^8.0.0-beta.13"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"tslib": "^2.3.0"
|