@praxisui/rich-content 7.0.0-beta.0 → 8.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,127 @@
1
1
  # PraxisRichContent
2
2
 
3
- This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.0.
3
+ `PraxisRichContent` renders metadata-driven rich nodes and blocks used by Praxis
4
+ components such as table renderers, list metrics and compact status surfaces.
5
+
6
+ ## Page Builder Integration
7
+
8
+ `@praxisui/rich-content` is a first-class widget for `@praxisui/page-builder`.
9
+ Register the metadata provider so the palette can discover the renderer:
10
+
11
+ ```ts
12
+ import { providePraxisRichContentMetadata } from '@praxisui/rich-content';
13
+
14
+ bootstrapApplication(AppComponent, {
15
+ providers: [
16
+ providePraxisRichContentMetadata(),
17
+ ],
18
+ });
19
+ ```
20
+
21
+ Use the component as a page widget by setting `definition.id` to
22
+ `praxis-rich-content` and passing the canonical document through
23
+ `definition.inputs.document`.
24
+
25
+ ```ts
26
+ const page = {
27
+ widgets: [
28
+ {
29
+ key: 'overview-rich-content',
30
+ definition: {
31
+ id: 'praxis-rich-content',
32
+ inputs: {
33
+ document: {
34
+ kind: 'praxis.rich-content',
35
+ version: '1.0.0',
36
+ nodes: [
37
+ {
38
+ type: 'card',
39
+ title: 'Resumo',
40
+ content: [
41
+ { type: 'text', text: 'Conteudo editorial declarativo' },
42
+ { type: 'badge', label: 'canonico' },
43
+ ],
44
+ },
45
+ ],
46
+ },
47
+ },
48
+ },
49
+ },
50
+ ],
51
+ };
52
+ ```
53
+
54
+ For AI-assisted page authoring, register `RICH_CONTENT_AI_CAPABILITIES` in the
55
+ page-builder widget catalog map for `praxis-rich-content`.
56
+
57
+ ## Visual Authoring
58
+
59
+ The component metadata exposes `PraxisRichContentConfigEditor` as the canonical
60
+ settings-panel editor for `praxis-rich-content`. Hosts such as
61
+ `@praxisui/page-builder` should open this editor from the component metadata
62
+ instead of creating local rich-content JSON editors.
63
+
64
+ The editor receives the widget input envelope and returns the same canonical
65
+ shape expected by the runtime:
66
+
67
+ ```ts
68
+ {
69
+ inputs: {
70
+ document: {
71
+ kind: 'praxis.rich-content',
72
+ version: '1.0.0',
73
+ nodes: []
74
+ },
75
+ layout: 'block',
76
+ rootClassName: 'employee-expansion-rich'
77
+ }
78
+ }
79
+ ```
80
+
81
+ This keeps the authoring round-trip aligned with the renderer:
82
+ `open -> edit -> apply/save -> persist -> reopen -> render`.
83
+
84
+ Authoring labels, helper text and validation messages use the rich-content i18n
85
+ dictionary. Apps can provide overrides with `providePraxisRichContentI18n()` or
86
+ reuse `resolvePraxisRichContentText()` when extending the canonical editor.
87
+
88
+ ## Layout Modes
89
+
90
+ The component defaults to `layout="block"` to preserve document-style rendering.
91
+ Use `layout="inline"` when rendering compact multi-node compositions such as
92
+ `icon + text`, badges, chips, links, buttons or value/caption pairs.
93
+
94
+ ```html
95
+ <praxis-rich-content
96
+ layout="inline"
97
+ rootClassName="status-badge__content"
98
+ [nodes]="[
99
+ { type: 'icon', icon: 'check_circle' },
100
+ { type: 'text', text: 'Ativo' }
101
+ ]"
102
+ ></praxis-rich-content>
103
+ ```
104
+
105
+ Inline mode adds `prx-rich-content-root--inline` to the root and
106
+ `prx-rich-node--inline` to each rendered node, allowing host components to align
107
+ siblings without overriding the default block contract.
108
+
109
+ Consumers can tune inline layout through inherited CSS custom properties on the
110
+ `praxis-rich-content` host or an ancestor:
111
+
112
+ ```css
113
+ .metric-value {
114
+ --prx-rich-content-inline-align-items: baseline;
115
+ --prx-rich-content-inline-flex-wrap: wrap;
116
+ --prx-rich-content-inline-gap: 6px;
117
+ }
118
+ ```
119
+
120
+ Supported inline variables:
121
+
122
+ - `--prx-rich-content-inline-align-items`, default `center`
123
+ - `--prx-rich-content-inline-flex-wrap`, default `nowrap`
124
+ - `--prx-rich-content-inline-gap`, default `6px`
4
125
 
5
126
  ## Code scaffolding
6
127
 
@@ -1,8 +1,11 @@
1
1
  import * as i1 from '@angular/common';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { InjectionToken, inject, Injectable, input, computed, Input, ChangeDetectionStrategy, Component } from '@angular/core';
5
- import { PraxisJsonLogicService } from '@praxisui/core';
4
+ import { InjectionToken, inject, Injectable, input, computed, Input, ChangeDetectionStrategy, Component, ChangeDetectorRef, ENVIRONMENT_INITIALIZER } from '@angular/core';
5
+ import { PraxisJsonLogicService, providePraxisI18n, PraxisI18nService, SETTINGS_PANEL_DATA, ComponentMetadataRegistry } from '@praxisui/core';
6
+ import * as i1$1 from '@angular/forms';
7
+ import { FormsModule } from '@angular/forms';
8
+ import { BehaviorSubject } from 'rxjs';
6
9
 
7
10
  const PRAXIS_RICH_BLOCK_PRESETS = new InjectionToken('PRAXIS_RICH_BLOCK_PRESETS');
8
11
  class RichContentPresetRegistryService {
@@ -31,6 +34,7 @@ class PraxisRichContent {
31
34
  nodes = input(null, ...(ngDevMode ? [{ debugName: "nodes" }] : []));
32
35
  context = input(null, ...(ngDevMode ? [{ debugName: "context" }] : []));
33
36
  hostCapabilities = input(null, ...(ngDevMode ? [{ debugName: "hostCapabilities" }] : []));
37
+ layout = input('block', ...(ngDevMode ? [{ debugName: "layout" }] : []));
34
38
  rootClassName = '';
35
39
  renderableNodes = computed(() => {
36
40
  const explicitNodes = this.nodes();
@@ -203,8 +207,12 @@ class PraxisRichContent {
203
207
  .join('');
204
208
  }
205
209
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisRichContent, deps: [], target: i0.ɵɵFactoryTarget.Component });
206
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisRichContent, isStandalone: true, selector: "praxis-rich-content", inputs: { document: { classPropertyName: "document", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, hostCapabilities: { classPropertyName: "hostCapabilities", publicName: "hostCapabilities", isSignal: true, isRequired: false, transformFunction: null }, rootClassName: { classPropertyName: "rootClassName", publicName: "rootClassName", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
207
- <div class="prx-rich-content-root" [ngClass]="rootClassName">
210
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisRichContent, isStandalone: true, selector: "praxis-rich-content", inputs: { document: { classPropertyName: "document", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, hostCapabilities: { classPropertyName: "hostCapabilities", publicName: "hostCapabilities", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, rootClassName: { classPropertyName: "rootClassName", publicName: "rootClassName", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
211
+ <div
212
+ class="prx-rich-content-root"
213
+ [class.prx-rich-content-root--inline]="layout() === 'inline'"
214
+ [ngClass]="rootClassName"
215
+ >
208
216
  <ng-container
209
217
  *ngTemplateOutlet="renderNodes; context: { $implicit: renderableNodes() }"
210
218
  ></ng-container>
@@ -215,6 +223,7 @@ class PraxisRichContent {
215
223
  @if (isNodeVisible(node)) {
216
224
  <div
217
225
  class="prx-rich-node"
226
+ [class.prx-rich-node--inline]="layout() === 'inline'"
218
227
  [attr.data-rich-node-type]="node.type"
219
228
  [attr.data-rich-node-id]="node.id || null"
220
229
  [attr.data-testid]="node.testId || null"
@@ -403,12 +412,16 @@ class PraxisRichContent {
403
412
  }
404
413
  }
405
414
  </ng-template>
406
- `, isInline: true, styles: [".prx-rich-content-root{display:block}.prx-rich-node{min-width:0}.prx-rich-compose{display:flex;align-items:center;gap:8px}.prx-rich-compose.direction-column{flex-direction:column;align-items:stretch}.prx-rich-compose.wrap{flex-wrap:wrap}.prx-rich-badge{display:inline-flex;align-items:center;padding:2px 10px;border-radius:999px;background:var(--md-sys-color-primary-container, #e8def8);color:var(--md-sys-color-on-primary-container, #21005d);font-size:12px;line-height:20px}.prx-rich-avatar{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:var(--md-sys-color-surface-container-high, #ece6f0);overflow:hidden}.prx-rich-avatar-image{width:100%;height:100%;object-fit:cover}.prx-rich-avatar-fallback{font-weight:600}.prx-rich-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, #cac4d0);border-radius:16px;background:var(--md-sys-color-surface, #fff)}.prx-rich-card-title{font-weight:600}.prx-rich-card-subtitle,.prx-rich-metric-caption{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-metric{display:flex;flex-direction:column;gap:4px}.prx-rich-metric-value{font-size:20px;font-weight:700}.prx-rich-progress{display:flex;flex-direction:column;gap:4px}.prx-rich-progress-bar{width:100%}.prx-rich-media-block{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;min-width:0}.prx-rich-media-block__avatar,.prx-rich-media-block__body,.prx-rich-media-block__trailing{min-width:0}.prx-rich-media-block__body{display:flex;flex-direction:column;gap:6px}.prx-rich-media-block__title{font-weight:700}.prx-rich-media-block__subtitle{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:13px}.prx-rich-media-block__meta{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-media-block__trailing{display:flex;align-items:center;justify-content:flex-end}.prx-rich-timeline{display:flex;flex-direction:column;gap:12px;min-width:0}.prx-rich-timeline__title{font-weight:700}.prx-rich-timeline__items{display:flex;flex-direction:column;gap:12px}.prx-rich-timeline__item{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:flex-start;min-width:0}.prx-rich-timeline__item-marker{display:inline-flex;align-items:center;justify-content:center;width:24px;min-height:24px}.prx-rich-timeline__item-icon{font-size:18px;line-height:18px;color:var(--md-sys-color-primary, #6750a4)}.prx-rich-timeline__item-dot{width:10px;height:10px;border-radius:50%;background:var(--md-sys-color-primary, #6750a4);display:inline-block;margin-top:4px}.prx-rich-timeline__item-body{display:flex;flex-direction:column;gap:4px;min-width:0}.prx-rich-timeline__item-title{font-weight:600}.prx-rich-timeline__item-subtitle,.prx-rich-timeline__item-meta,.prx-rich-timeline__empty{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-timeline__item-badge{display:inline-flex;align-items:flex-start;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
415
+ `, isInline: true, styles: [".prx-rich-content-root{display:block}.prx-rich-content-root--inline{display:inline-flex;align-items:var(--prx-rich-content-inline-align-items, center);flex-wrap:var(--prx-rich-content-inline-flex-wrap, nowrap);gap:var(--prx-rich-content-inline-gap, 6px);min-width:0;max-width:100%;vertical-align:middle}.prx-rich-node{min-width:0}.prx-rich-node--inline{display:inline-flex;align-items:center;min-width:0}.prx-rich-compose{display:flex;align-items:center;gap:8px}.prx-rich-compose.direction-column{flex-direction:column;align-items:stretch}.prx-rich-compose.wrap{flex-wrap:wrap}.prx-rich-badge{display:inline-flex;align-items:center;padding:2px 10px;border-radius:999px;background:var(--md-sys-color-primary-container, #e8def8);color:var(--md-sys-color-on-primary-container, #21005d);font-size:12px;line-height:20px}.prx-rich-avatar{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:var(--md-sys-color-surface-container-high, #ece6f0);overflow:hidden}.prx-rich-avatar-image{width:100%;height:100%;object-fit:cover}.prx-rich-avatar-fallback{font-weight:600}.prx-rich-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, #cac4d0);border-radius:16px;background:var(--md-sys-color-surface, #fff)}.prx-rich-card-title{font-weight:600}.prx-rich-card-subtitle,.prx-rich-metric-caption{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-metric{display:flex;flex-direction:column;gap:4px}.prx-rich-metric-value{font-size:20px;font-weight:700}.prx-rich-progress{display:flex;flex-direction:column;gap:4px}.prx-rich-progress-bar{width:100%}.prx-rich-media-block{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;min-width:0}.prx-rich-media-block__avatar,.prx-rich-media-block__body,.prx-rich-media-block__trailing{min-width:0}.prx-rich-media-block__body{display:flex;flex-direction:column;gap:6px}.prx-rich-media-block__title{font-weight:700}.prx-rich-media-block__subtitle{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:13px}.prx-rich-media-block__meta{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-media-block__trailing{display:flex;align-items:center;justify-content:flex-end}.prx-rich-timeline{display:flex;flex-direction:column;gap:12px;min-width:0}.prx-rich-timeline__title{font-weight:700}.prx-rich-timeline__items{display:flex;flex-direction:column;gap:12px}.prx-rich-timeline__item{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:flex-start;min-width:0}.prx-rich-timeline__item-marker{display:inline-flex;align-items:center;justify-content:center;width:24px;min-height:24px}.prx-rich-timeline__item-icon{font-size:18px;line-height:18px;color:var(--md-sys-color-primary, #6750a4)}.prx-rich-timeline__item-dot{width:10px;height:10px;border-radius:50%;background:var(--md-sys-color-primary, #6750a4);display:inline-block;margin-top:4px}.prx-rich-timeline__item-body{display:flex;flex-direction:column;gap:4px;min-width:0}.prx-rich-timeline__item-title{font-weight:600}.prx-rich-timeline__item-subtitle,.prx-rich-timeline__item-meta,.prx-rich-timeline__empty{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-timeline__item-badge{display:inline-flex;align-items:flex-start;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
407
416
  }
408
417
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisRichContent, decorators: [{
409
418
  type: Component,
410
419
  args: [{ selector: 'praxis-rich-content', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
411
- <div class="prx-rich-content-root" [ngClass]="rootClassName">
420
+ <div
421
+ class="prx-rich-content-root"
422
+ [class.prx-rich-content-root--inline]="layout() === 'inline'"
423
+ [ngClass]="rootClassName"
424
+ >
412
425
  <ng-container
413
426
  *ngTemplateOutlet="renderNodes; context: { $implicit: renderableNodes() }"
414
427
  ></ng-container>
@@ -419,6 +432,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
419
432
  @if (isNodeVisible(node)) {
420
433
  <div
421
434
  class="prx-rich-node"
435
+ [class.prx-rich-node--inline]="layout() === 'inline'"
422
436
  [attr.data-rich-node-type]="node.type"
423
437
  [attr.data-rich-node-id]="node.id || null"
424
438
  [attr.data-testid]="node.testId || null"
@@ -607,11 +621,598 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
607
621
  }
608
622
  }
609
623
  </ng-template>
610
- `, styles: [".prx-rich-content-root{display:block}.prx-rich-node{min-width:0}.prx-rich-compose{display:flex;align-items:center;gap:8px}.prx-rich-compose.direction-column{flex-direction:column;align-items:stretch}.prx-rich-compose.wrap{flex-wrap:wrap}.prx-rich-badge{display:inline-flex;align-items:center;padding:2px 10px;border-radius:999px;background:var(--md-sys-color-primary-container, #e8def8);color:var(--md-sys-color-on-primary-container, #21005d);font-size:12px;line-height:20px}.prx-rich-avatar{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:var(--md-sys-color-surface-container-high, #ece6f0);overflow:hidden}.prx-rich-avatar-image{width:100%;height:100%;object-fit:cover}.prx-rich-avatar-fallback{font-weight:600}.prx-rich-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, #cac4d0);border-radius:16px;background:var(--md-sys-color-surface, #fff)}.prx-rich-card-title{font-weight:600}.prx-rich-card-subtitle,.prx-rich-metric-caption{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-metric{display:flex;flex-direction:column;gap:4px}.prx-rich-metric-value{font-size:20px;font-weight:700}.prx-rich-progress{display:flex;flex-direction:column;gap:4px}.prx-rich-progress-bar{width:100%}.prx-rich-media-block{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;min-width:0}.prx-rich-media-block__avatar,.prx-rich-media-block__body,.prx-rich-media-block__trailing{min-width:0}.prx-rich-media-block__body{display:flex;flex-direction:column;gap:6px}.prx-rich-media-block__title{font-weight:700}.prx-rich-media-block__subtitle{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:13px}.prx-rich-media-block__meta{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-media-block__trailing{display:flex;align-items:center;justify-content:flex-end}.prx-rich-timeline{display:flex;flex-direction:column;gap:12px;min-width:0}.prx-rich-timeline__title{font-weight:700}.prx-rich-timeline__items{display:flex;flex-direction:column;gap:12px}.prx-rich-timeline__item{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:flex-start;min-width:0}.prx-rich-timeline__item-marker{display:inline-flex;align-items:center;justify-content:center;width:24px;min-height:24px}.prx-rich-timeline__item-icon{font-size:18px;line-height:18px;color:var(--md-sys-color-primary, #6750a4)}.prx-rich-timeline__item-dot{width:10px;height:10px;border-radius:50%;background:var(--md-sys-color-primary, #6750a4);display:inline-block;margin-top:4px}.prx-rich-timeline__item-body{display:flex;flex-direction:column;gap:4px;min-width:0}.prx-rich-timeline__item-title{font-weight:600}.prx-rich-timeline__item-subtitle,.prx-rich-timeline__item-meta,.prx-rich-timeline__empty{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-timeline__item-badge{display:inline-flex;align-items:flex-start;justify-content:flex-end}\n"] }]
611
- }], propDecorators: { document: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }], hostCapabilities: [{ type: i0.Input, args: [{ isSignal: true, alias: "hostCapabilities", required: false }] }], rootClassName: [{
624
+ `, styles: [".prx-rich-content-root{display:block}.prx-rich-content-root--inline{display:inline-flex;align-items:var(--prx-rich-content-inline-align-items, center);flex-wrap:var(--prx-rich-content-inline-flex-wrap, nowrap);gap:var(--prx-rich-content-inline-gap, 6px);min-width:0;max-width:100%;vertical-align:middle}.prx-rich-node{min-width:0}.prx-rich-node--inline{display:inline-flex;align-items:center;min-width:0}.prx-rich-compose{display:flex;align-items:center;gap:8px}.prx-rich-compose.direction-column{flex-direction:column;align-items:stretch}.prx-rich-compose.wrap{flex-wrap:wrap}.prx-rich-badge{display:inline-flex;align-items:center;padding:2px 10px;border-radius:999px;background:var(--md-sys-color-primary-container, #e8def8);color:var(--md-sys-color-on-primary-container, #21005d);font-size:12px;line-height:20px}.prx-rich-avatar{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:var(--md-sys-color-surface-container-high, #ece6f0);overflow:hidden}.prx-rich-avatar-image{width:100%;height:100%;object-fit:cover}.prx-rich-avatar-fallback{font-weight:600}.prx-rich-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, #cac4d0);border-radius:16px;background:var(--md-sys-color-surface, #fff)}.prx-rich-card-title{font-weight:600}.prx-rich-card-subtitle,.prx-rich-metric-caption{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-metric{display:flex;flex-direction:column;gap:4px}.prx-rich-metric-value{font-size:20px;font-weight:700}.prx-rich-progress{display:flex;flex-direction:column;gap:4px}.prx-rich-progress-bar{width:100%}.prx-rich-media-block{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;min-width:0}.prx-rich-media-block__avatar,.prx-rich-media-block__body,.prx-rich-media-block__trailing{min-width:0}.prx-rich-media-block__body{display:flex;flex-direction:column;gap:6px}.prx-rich-media-block__title{font-weight:700}.prx-rich-media-block__subtitle{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:13px}.prx-rich-media-block__meta{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-media-block__trailing{display:flex;align-items:center;justify-content:flex-end}.prx-rich-timeline{display:flex;flex-direction:column;gap:12px;min-width:0}.prx-rich-timeline__title{font-weight:700}.prx-rich-timeline__items{display:flex;flex-direction:column;gap:12px}.prx-rich-timeline__item{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:flex-start;min-width:0}.prx-rich-timeline__item-marker{display:inline-flex;align-items:center;justify-content:center;width:24px;min-height:24px}.prx-rich-timeline__item-icon{font-size:18px;line-height:18px;color:var(--md-sys-color-primary, #6750a4)}.prx-rich-timeline__item-dot{width:10px;height:10px;border-radius:50%;background:var(--md-sys-color-primary, #6750a4);display:inline-block;margin-top:4px}.prx-rich-timeline__item-body{display:flex;flex-direction:column;gap:4px;min-width:0}.prx-rich-timeline__item-title{font-weight:600}.prx-rich-timeline__item-subtitle,.prx-rich-timeline__item-meta,.prx-rich-timeline__empty{color:var(--md-sys-color-on-surface-variant, #49454f);font-size:12px}.prx-rich-timeline__item-badge{display:inline-flex;align-items:flex-start;justify-content:flex-end}\n"] }]
625
+ }], propDecorators: { document: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }], hostCapabilities: [{ type: i0.Input, args: [{ isSignal: true, alias: "hostCapabilities", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], rootClassName: [{
612
626
  type: Input
613
627
  }] } });
614
628
 
629
+ const PRAXIS_RICH_CONTENT_EN_US = {
630
+ 'praxis.richContent.editor.eyebrow': 'RichContentDocument',
631
+ 'praxis.richContent.editor.title': 'Configure rich content',
632
+ 'praxis.richContent.editor.subtitle': 'Edit the canonical document consumed by the renderer. The saved payload preserves the remaining widget inputs.',
633
+ 'praxis.richContent.editor.valid': 'Valid document',
634
+ 'praxis.richContent.editor.invalid': 'Invalid JSON',
635
+ 'praxis.richContent.editor.layout': 'Layout',
636
+ 'praxis.richContent.editor.layout.block': 'Block',
637
+ 'praxis.richContent.editor.layout.inline': 'Inline',
638
+ 'praxis.richContent.editor.rootClass': 'Root class',
639
+ 'praxis.richContent.editor.rootClassPlaceholder': 'Example: employee-expansion-rich',
640
+ 'praxis.richContent.editor.document': 'Document JSON',
641
+ 'praxis.richContent.editor.documentHelp': 'Paste or edit a RichContentDocument with kind "praxis.rich-content" and a nodes array.',
642
+ 'praxis.richContent.editor.restore': 'Restore',
643
+ 'praxis.richContent.editor.emptyDocument': 'Empty document',
644
+ 'praxis.richContent.editor.validation.documentShape': 'The document must have kind "praxis.rich-content" and nodes[].',
645
+ 'praxis.richContent.editor.validation.invalidJson': 'Invalid JSON document.',
646
+ };
647
+
648
+ const PRAXIS_RICH_CONTENT_PT_BR = {
649
+ 'praxis.richContent.editor.eyebrow': 'RichContentDocument',
650
+ 'praxis.richContent.editor.title': 'Configurar conteudo rico',
651
+ 'praxis.richContent.editor.subtitle': 'Edite o documento canonico consumido pelo renderer. O payload salvo preserva os demais inputs do widget.',
652
+ 'praxis.richContent.editor.valid': 'Documento valido',
653
+ 'praxis.richContent.editor.invalid': 'JSON invalido',
654
+ 'praxis.richContent.editor.layout': 'Layout',
655
+ 'praxis.richContent.editor.layout.block': 'Bloco',
656
+ 'praxis.richContent.editor.layout.inline': 'Inline',
657
+ 'praxis.richContent.editor.rootClass': 'Classe raiz',
658
+ 'praxis.richContent.editor.rootClassPlaceholder': 'Exemplo: employee-expansion-rich',
659
+ 'praxis.richContent.editor.document': 'JSON do documento',
660
+ 'praxis.richContent.editor.documentHelp': 'Cole ou edite um RichContentDocument com kind "praxis.rich-content" e um array nodes.',
661
+ 'praxis.richContent.editor.restore': 'Restaurar',
662
+ 'praxis.richContent.editor.emptyDocument': 'Documento vazio',
663
+ 'praxis.richContent.editor.validation.documentShape': 'O documento deve ter kind "praxis.rich-content" e nodes[].',
664
+ 'praxis.richContent.editor.validation.invalidJson': 'Documento JSON invalido.',
665
+ };
666
+
667
+ function createPraxisRichContentI18nConfig(options = {}) {
668
+ const dictionaries = {
669
+ 'pt-BR': {
670
+ ...PRAXIS_RICH_CONTENT_PT_BR,
671
+ ...(options.dictionaries?.['pt-BR'] ?? {}),
672
+ },
673
+ 'en-US': {
674
+ ...PRAXIS_RICH_CONTENT_EN_US,
675
+ ...(options.dictionaries?.['en-US'] ?? {}),
676
+ },
677
+ };
678
+ for (const [locale, dictionary] of Object.entries(options.dictionaries ?? {})) {
679
+ if (locale === 'pt-BR' || locale === 'en-US')
680
+ continue;
681
+ dictionaries[locale] = { ...(dictionaries[locale] ?? {}), ...dictionary };
682
+ }
683
+ return {
684
+ locale: options.locale,
685
+ fallbackLocale: options.fallbackLocale ?? 'en-US',
686
+ dictionaries,
687
+ };
688
+ }
689
+ function providePraxisRichContentI18n(options = {}) {
690
+ return providePraxisI18n(createPraxisRichContentI18nConfig(options));
691
+ }
692
+ function resolvePraxisRichContentText(i18n, key, fallback) {
693
+ const namespacedKey = key.startsWith('praxis.richContent.')
694
+ ? key
695
+ : `praxis.richContent.${key}`;
696
+ return i18n.t(namespacedKey, undefined, fallback) || fallback;
697
+ }
698
+
699
+ const EMPTY_DOCUMENT = {
700
+ kind: 'praxis.rich-content',
701
+ version: '1.0.0',
702
+ nodes: [],
703
+ };
704
+ class PraxisRichContentConfigEditor {
705
+ cdr = inject(ChangeDetectorRef);
706
+ i18n = inject(PraxisI18nService);
707
+ injectedData = inject(SETTINGS_PANEL_DATA, {
708
+ optional: true,
709
+ });
710
+ inputs = this.injectedData?.inputs ?? null;
711
+ isDirty$ = new BehaviorSubject(false);
712
+ isValid$ = new BehaviorSubject(true);
713
+ isBusy$ = new BehaviorSubject(false);
714
+ documentJson = '';
715
+ layout = 'block';
716
+ rootClassName = '';
717
+ errorMessage = '';
718
+ valid = true;
719
+ initialInputs = this.normalizeInputs(this.inputs);
720
+ ngOnChanges(changes) {
721
+ if (changes['inputs']) {
722
+ this.load(this.normalizeInputs(this.inputs), false);
723
+ }
724
+ }
725
+ ngOnInit() {
726
+ if (!this.documentJson) {
727
+ this.load(this.normalizeInputs(this.inputs), false);
728
+ }
729
+ }
730
+ getSettingsValue() {
731
+ const document = this.parseDocument();
732
+ if (!document)
733
+ return null;
734
+ return {
735
+ inputs: {
736
+ ...this.initialInputs,
737
+ document,
738
+ layout: this.layout,
739
+ rootClassName: this.rootClassName.trim() || null,
740
+ },
741
+ };
742
+ }
743
+ onSave() {
744
+ const value = this.getSettingsValue();
745
+ if (value) {
746
+ this.initialInputs = this.cloneInputs(value.inputs);
747
+ this.isDirty$.next(false);
748
+ }
749
+ return value;
750
+ }
751
+ reset() {
752
+ this.load(this.initialInputs, false);
753
+ this.cdr.detectChanges();
754
+ }
755
+ markDirty() {
756
+ this.isDirty$.next(!this.matchesInitialState());
757
+ }
758
+ onDocumentJsonChange() {
759
+ this.parseDocument();
760
+ this.markDirty();
761
+ }
762
+ tx(key, fallback) {
763
+ return resolvePraxisRichContentText(this.i18n, key, fallback);
764
+ }
765
+ useEmptyDocument() {
766
+ this.documentJson = this.stringify(EMPTY_DOCUMENT);
767
+ this.parseDocument();
768
+ this.markDirty();
769
+ }
770
+ load(inputs, dirty) {
771
+ const document = inputs.document ?? EMPTY_DOCUMENT;
772
+ const layout = inputs.layout === 'inline' ? 'inline' : 'block';
773
+ const rootClassName = typeof inputs.rootClassName === 'string' ? inputs.rootClassName : '';
774
+ this.initialInputs = {
775
+ ...this.cloneInputs(inputs),
776
+ document: this.cloneValue(document),
777
+ layout,
778
+ rootClassName: rootClassName.trim() || null,
779
+ };
780
+ this.layout = layout;
781
+ this.rootClassName = rootClassName;
782
+ this.documentJson = this.stringify(document);
783
+ this.parseDocument();
784
+ this.isDirty$.next(dirty);
785
+ }
786
+ normalizeInputs(inputs) {
787
+ return this.cloneInputs(inputs ?? {});
788
+ }
789
+ parseDocument() {
790
+ try {
791
+ const parsed = JSON.parse(this.documentJson);
792
+ if (!parsed ||
793
+ typeof parsed !== 'object' ||
794
+ parsed.kind !== 'praxis.rich-content' ||
795
+ !Array.isArray(parsed.nodes)) {
796
+ throw new Error(this.tx('editor.validation.documentShape', 'The document must have kind "praxis.rich-content" and nodes[].'));
797
+ }
798
+ this.valid = true;
799
+ this.errorMessage = '';
800
+ this.isValid$.next(true);
801
+ return parsed;
802
+ }
803
+ catch (error) {
804
+ this.valid = false;
805
+ this.errorMessage =
806
+ error instanceof Error
807
+ ? error.message
808
+ : this.tx('editor.validation.invalidJson', 'Invalid JSON document.');
809
+ this.isValid$.next(false);
810
+ return null;
811
+ }
812
+ }
813
+ matchesInitialState() {
814
+ const current = this.getSettingsValue();
815
+ if (!current)
816
+ return false;
817
+ return (JSON.stringify(current.inputs) === JSON.stringify(this.initialInputs));
818
+ }
819
+ cloneInputs(value) {
820
+ return this.cloneValue(value);
821
+ }
822
+ cloneValue(value) {
823
+ return JSON.parse(JSON.stringify(value));
824
+ }
825
+ stringify(value) {
826
+ return JSON.stringify(value, null, 2);
827
+ }
828
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisRichContentConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
829
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisRichContentConfigEditor, isStandalone: true, selector: "praxis-rich-content-config-editor", inputs: { inputs: "inputs" }, providers: [providePraxisRichContentI18n()], usesOnChanges: true, ngImport: i0, template: `
830
+ <section class="prx-rich-editor" data-testid="rich-content-config-editor">
831
+ <header class="prx-rich-editor__header">
832
+ <div>
833
+ <p class="prx-rich-editor__eyebrow">
834
+ {{ tx('editor.eyebrow', 'RichContentDocument') }}
835
+ </p>
836
+ <h2>{{ tx('editor.title', 'Configure rich content') }}</h2>
837
+ <p>
838
+ {{
839
+ tx(
840
+ 'editor.subtitle',
841
+ 'Edit the canonical document consumed by the renderer. The saved payload preserves the remaining widget inputs.'
842
+ )
843
+ }}
844
+ </p>
845
+ </div>
846
+ <span
847
+ class="prx-rich-editor__status"
848
+ [class.invalid]="!valid"
849
+ aria-live="polite"
850
+ >
851
+ {{
852
+ valid
853
+ ? tx('editor.valid', 'Valid document')
854
+ : tx('editor.invalid', 'Invalid JSON')
855
+ }}
856
+ </span>
857
+ </header>
858
+
859
+ <div class="prx-rich-editor__grid">
860
+ <label>
861
+ <span>{{ tx('editor.layout', 'Layout') }}</span>
862
+ <select
863
+ name="rich-content-layout"
864
+ [(ngModel)]="layout"
865
+ (ngModelChange)="markDirty()"
866
+ data-testid="rich-content-layout-input"
867
+ >
868
+ <option value="block">
869
+ {{ tx('editor.layout.block', 'Block') }}
870
+ </option>
871
+ <option value="inline">
872
+ {{ tx('editor.layout.inline', 'Inline') }}
873
+ </option>
874
+ </select>
875
+ </label>
876
+
877
+ <label>
878
+ <span>{{ tx('editor.rootClass', 'Root class') }}</span>
879
+ <input
880
+ name="rich-content-root-class"
881
+ [(ngModel)]="rootClassName"
882
+ (ngModelChange)="markDirty()"
883
+ [placeholder]="
884
+ tx('editor.rootClassPlaceholder', 'Example: employee-expansion-rich')
885
+ "
886
+ data-testid="rich-content-root-class-input"
887
+ />
888
+ </label>
889
+ </div>
890
+
891
+ <label class="prx-rich-editor__document">
892
+ <span>{{ tx('editor.document', 'Document JSON') }}</span>
893
+ <textarea
894
+ name="rich-content-document"
895
+ [(ngModel)]="documentJson"
896
+ (ngModelChange)="onDocumentJsonChange()"
897
+ [attr.aria-describedby]="
898
+ errorMessage
899
+ ? 'rich-content-config-error'
900
+ : 'rich-content-document-help'
901
+ "
902
+ [attr.aria-invalid]="!valid"
903
+ spellcheck="false"
904
+ data-testid="rich-content-document-input"
905
+ ></textarea>
906
+ </label>
907
+
908
+ @if (errorMessage) {
909
+ <p
910
+ id="rich-content-config-error"
911
+ class="prx-rich-editor__error"
912
+ data-testid="rich-content-config-error"
913
+ >
914
+ {{ errorMessage }}
915
+ </p>
916
+ } @else {
917
+ <p id="rich-content-document-help" class="prx-rich-editor__help">
918
+ {{
919
+ tx(
920
+ 'editor.documentHelp',
921
+ 'Paste or edit a RichContentDocument with kind "praxis.rich-content" and a nodes array.'
922
+ )
923
+ }}
924
+ </p>
925
+ }
926
+
927
+ <div class="prx-rich-editor__actions">
928
+ <button type="button" (click)="reset()">
929
+ {{ tx('editor.restore', 'Restore') }}
930
+ </button>
931
+ <button type="button" (click)="useEmptyDocument()">
932
+ {{ tx('editor.emptyDocument', 'Empty document') }}
933
+ </button>
934
+ </div>
935
+ </section>
936
+ `, isInline: true, styles: [":host{display:block;min-width:0}.prx-rich-editor{display:grid;gap:18px;color:var(--md-sys-color-on-surface, #1f2937)}.prx-rich-editor__header{display:flex;gap:16px;align-items:flex-start;justify-content:space-between}.prx-rich-editor__header h2{margin:0;font-size:1.25rem}.prx-rich-editor__header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673)}.prx-rich-editor__eyebrow{margin:0 0 4px;text-transform:uppercase;letter-spacing:.08em;font-size:.72rem;color:var(--md-sys-color-primary, #3154e7)}.prx-rich-editor__status{flex:0 0 auto;padding:6px 10px;border-radius:999px;background:color-mix(in srgb,#16a34a 12%,transparent);color:#166534;font-size:.78rem;font-weight:700}.prx-rich-editor__status.invalid{background:color-mix(in srgb,#dc2626 12%,transparent);color:#991b1b}.prx-rich-editor__grid{display:grid;gap:14px;grid-template-columns:minmax(0,180px) minmax(0,1fr)}label,.prx-rich-editor__document{display:grid;gap:7px;font-weight:700}input,select,textarea{box-sizing:border-box;width:100%;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:12px;padding:10px 12px;font:inherit;color:inherit;background:var(--md-sys-color-surface, #fff)}textarea{min-height:360px;resize:vertical;font-family:Cascadia Code,Fira Code,Consolas,monospace;font-size:.86rem;line-height:1.45}.prx-rich-editor__help{margin:-8px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.85rem}.prx-rich-editor__error{margin:0;padding:10px 12px;border-radius:12px;color:#991b1b;background:color-mix(in srgb,#dc2626 9%,transparent)}.prx-rich-editor__actions{display:flex;flex-wrap:wrap;gap:10px}button{border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:999px;padding:8px 14px;background:var(--md-sys-color-surface, #fff);color:var(--md-sys-color-primary, #3154e7);font-weight:700;cursor:pointer}@media(max-width:760px){.prx-rich-editor__header,.prx-rich-editor__grid{grid-template-columns:1fr;display:grid}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
937
+ }
938
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisRichContentConfigEditor, decorators: [{
939
+ type: Component,
940
+ args: [{ selector: 'praxis-rich-content-config-editor', standalone: true, imports: [CommonModule, FormsModule], providers: [providePraxisRichContentI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: `
941
+ <section class="prx-rich-editor" data-testid="rich-content-config-editor">
942
+ <header class="prx-rich-editor__header">
943
+ <div>
944
+ <p class="prx-rich-editor__eyebrow">
945
+ {{ tx('editor.eyebrow', 'RichContentDocument') }}
946
+ </p>
947
+ <h2>{{ tx('editor.title', 'Configure rich content') }}</h2>
948
+ <p>
949
+ {{
950
+ tx(
951
+ 'editor.subtitle',
952
+ 'Edit the canonical document consumed by the renderer. The saved payload preserves the remaining widget inputs.'
953
+ )
954
+ }}
955
+ </p>
956
+ </div>
957
+ <span
958
+ class="prx-rich-editor__status"
959
+ [class.invalid]="!valid"
960
+ aria-live="polite"
961
+ >
962
+ {{
963
+ valid
964
+ ? tx('editor.valid', 'Valid document')
965
+ : tx('editor.invalid', 'Invalid JSON')
966
+ }}
967
+ </span>
968
+ </header>
969
+
970
+ <div class="prx-rich-editor__grid">
971
+ <label>
972
+ <span>{{ tx('editor.layout', 'Layout') }}</span>
973
+ <select
974
+ name="rich-content-layout"
975
+ [(ngModel)]="layout"
976
+ (ngModelChange)="markDirty()"
977
+ data-testid="rich-content-layout-input"
978
+ >
979
+ <option value="block">
980
+ {{ tx('editor.layout.block', 'Block') }}
981
+ </option>
982
+ <option value="inline">
983
+ {{ tx('editor.layout.inline', 'Inline') }}
984
+ </option>
985
+ </select>
986
+ </label>
987
+
988
+ <label>
989
+ <span>{{ tx('editor.rootClass', 'Root class') }}</span>
990
+ <input
991
+ name="rich-content-root-class"
992
+ [(ngModel)]="rootClassName"
993
+ (ngModelChange)="markDirty()"
994
+ [placeholder]="
995
+ tx('editor.rootClassPlaceholder', 'Example: employee-expansion-rich')
996
+ "
997
+ data-testid="rich-content-root-class-input"
998
+ />
999
+ </label>
1000
+ </div>
1001
+
1002
+ <label class="prx-rich-editor__document">
1003
+ <span>{{ tx('editor.document', 'Document JSON') }}</span>
1004
+ <textarea
1005
+ name="rich-content-document"
1006
+ [(ngModel)]="documentJson"
1007
+ (ngModelChange)="onDocumentJsonChange()"
1008
+ [attr.aria-describedby]="
1009
+ errorMessage
1010
+ ? 'rich-content-config-error'
1011
+ : 'rich-content-document-help'
1012
+ "
1013
+ [attr.aria-invalid]="!valid"
1014
+ spellcheck="false"
1015
+ data-testid="rich-content-document-input"
1016
+ ></textarea>
1017
+ </label>
1018
+
1019
+ @if (errorMessage) {
1020
+ <p
1021
+ id="rich-content-config-error"
1022
+ class="prx-rich-editor__error"
1023
+ data-testid="rich-content-config-error"
1024
+ >
1025
+ {{ errorMessage }}
1026
+ </p>
1027
+ } @else {
1028
+ <p id="rich-content-document-help" class="prx-rich-editor__help">
1029
+ {{
1030
+ tx(
1031
+ 'editor.documentHelp',
1032
+ 'Paste or edit a RichContentDocument with kind "praxis.rich-content" and a nodes array.'
1033
+ )
1034
+ }}
1035
+ </p>
1036
+ }
1037
+
1038
+ <div class="prx-rich-editor__actions">
1039
+ <button type="button" (click)="reset()">
1040
+ {{ tx('editor.restore', 'Restore') }}
1041
+ </button>
1042
+ <button type="button" (click)="useEmptyDocument()">
1043
+ {{ tx('editor.emptyDocument', 'Empty document') }}
1044
+ </button>
1045
+ </div>
1046
+ </section>
1047
+ `, styles: [":host{display:block;min-width:0}.prx-rich-editor{display:grid;gap:18px;color:var(--md-sys-color-on-surface, #1f2937)}.prx-rich-editor__header{display:flex;gap:16px;align-items:flex-start;justify-content:space-between}.prx-rich-editor__header h2{margin:0;font-size:1.25rem}.prx-rich-editor__header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673)}.prx-rich-editor__eyebrow{margin:0 0 4px;text-transform:uppercase;letter-spacing:.08em;font-size:.72rem;color:var(--md-sys-color-primary, #3154e7)}.prx-rich-editor__status{flex:0 0 auto;padding:6px 10px;border-radius:999px;background:color-mix(in srgb,#16a34a 12%,transparent);color:#166534;font-size:.78rem;font-weight:700}.prx-rich-editor__status.invalid{background:color-mix(in srgb,#dc2626 12%,transparent);color:#991b1b}.prx-rich-editor__grid{display:grid;gap:14px;grid-template-columns:minmax(0,180px) minmax(0,1fr)}label,.prx-rich-editor__document{display:grid;gap:7px;font-weight:700}input,select,textarea{box-sizing:border-box;width:100%;border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:12px;padding:10px 12px;font:inherit;color:inherit;background:var(--md-sys-color-surface, #fff)}textarea{min-height:360px;resize:vertical;font-family:Cascadia Code,Fira Code,Consolas,monospace;font-size:.86rem;line-height:1.45}.prx-rich-editor__help{margin:-8px 0 0;color:var(--md-sys-color-on-surface-variant, #5f6673);font-size:.85rem}.prx-rich-editor__error{margin:0;padding:10px 12px;border-radius:12px;color:#991b1b;background:color-mix(in srgb,#dc2626 9%,transparent)}.prx-rich-editor__actions{display:flex;flex-wrap:wrap;gap:10px}button{border:1px solid var(--md-sys-color-outline-variant, #d7dce5);border-radius:999px;padding:8px 14px;background:var(--md-sys-color-surface, #fff);color:var(--md-sys-color-primary, #3154e7);font-weight:700;cursor:pointer}@media(max-width:760px){.prx-rich-editor__header,.prx-rich-editor__grid{grid-template-columns:1fr;display:grid}}\n"] }]
1048
+ }], propDecorators: { inputs: [{
1049
+ type: Input
1050
+ }] } });
1051
+
1052
+ const DEFAULT_RICH_CONTENT_DOCUMENT = {
1053
+ kind: 'praxis.rich-content',
1054
+ version: '1.0.0',
1055
+ nodes: [
1056
+ {
1057
+ type: 'card',
1058
+ title: 'Rich content',
1059
+ content: [
1060
+ {
1061
+ type: 'text',
1062
+ text: 'Configure este bloco com um RichContentDocument canonico.',
1063
+ },
1064
+ {
1065
+ type: 'badge',
1066
+ label: 'praxis.rich-content',
1067
+ },
1068
+ ],
1069
+ },
1070
+ ],
1071
+ };
1072
+ const PRAXIS_RICH_CONTENT_COMPONENT_METADATA = {
1073
+ id: 'praxis-rich-content',
1074
+ selector: 'praxis-rich-content',
1075
+ component: PraxisRichContent,
1076
+ friendlyName: 'Praxis Rich Content',
1077
+ description: 'Renderer canonico de RichContentDocument para blocos editoriais, cards, media blocks, timelines e primitives de apresentacao.',
1078
+ icon: 'dashboard_customize',
1079
+ inputs: [
1080
+ {
1081
+ name: 'document',
1082
+ type: 'RichContentDocument | null',
1083
+ label: 'Rich content document',
1084
+ description: 'Documento canonico praxis.rich-content renderizado pelo componente.',
1085
+ default: DEFAULT_RICH_CONTENT_DOCUMENT,
1086
+ },
1087
+ {
1088
+ name: 'nodes',
1089
+ type: 'RichBlockNode[] | null',
1090
+ label: 'Nodes',
1091
+ description: 'Lista direta de nodes quando o host ja controla o envelope do documento.',
1092
+ },
1093
+ {
1094
+ name: 'context',
1095
+ type: 'JsonLogicDataRecord | null',
1096
+ label: 'Context',
1097
+ description: 'Contexto usado para resolver Json Logic, expressoes e bindings dos nodes.',
1098
+ },
1099
+ {
1100
+ name: 'hostCapabilities',
1101
+ type: 'RichBlockHostCapabilities | null',
1102
+ label: 'Host capabilities',
1103
+ description: 'Capacidades mediadas pelo host para presets, dados, actions e embeds governados.',
1104
+ },
1105
+ {
1106
+ name: 'layout',
1107
+ type: "'block' | 'inline'",
1108
+ label: 'Layout',
1109
+ default: 'block',
1110
+ description: 'Modo de renderizacao do documento: bloco ou inline.',
1111
+ },
1112
+ {
1113
+ name: 'rootClassName',
1114
+ type: 'string',
1115
+ label: 'Root class',
1116
+ description: 'Classe CSS aplicada ao root do renderer para surfaces de host.',
1117
+ },
1118
+ ],
1119
+ outputs: [],
1120
+ tags: ['widget', 'rich-content', 'editorial', 'presentation', 'cards', 'timeline'],
1121
+ lib: '@praxisui/rich-content',
1122
+ configEditor: {
1123
+ component: PraxisRichContentConfigEditor,
1124
+ title: 'Configurar rich content',
1125
+ },
1126
+ layoutHints: {
1127
+ recommendedCols: 6,
1128
+ recommendedRows: 3,
1129
+ minCols: 2,
1130
+ minRows: 1,
1131
+ },
1132
+ };
1133
+ function providePraxisRichContentMetadata() {
1134
+ return {
1135
+ provide: ENVIRONMENT_INITIALIZER,
1136
+ multi: true,
1137
+ useFactory: (registry) => () => {
1138
+ registry.register(PRAXIS_RICH_CONTENT_COMPONENT_METADATA);
1139
+ },
1140
+ deps: [ComponentMetadataRegistry],
1141
+ };
1142
+ }
1143
+
1144
+ const RICH_CONTENT_AI_CAPABILITIES = {
1145
+ version: 'v1.0',
1146
+ enums: {
1147
+ richContentNodeType: [
1148
+ 'text',
1149
+ 'icon',
1150
+ 'image',
1151
+ 'badge',
1152
+ 'avatar',
1153
+ 'metric',
1154
+ 'progress',
1155
+ 'compose',
1156
+ 'card',
1157
+ 'mediaBlock',
1158
+ 'timeline',
1159
+ 'preset',
1160
+ ],
1161
+ richContentLayout: ['block', 'inline'],
1162
+ richContentGap: ['xs', 'sm', 'md', 'lg', 'xl'],
1163
+ richContentDirection: ['row', 'column'],
1164
+ },
1165
+ targets: [
1166
+ 'praxis-rich-content',
1167
+ 'rich-content',
1168
+ 'page-builder-rich-content-widget',
1169
+ 'dynamic-form-editorial',
1170
+ 'stepper-editorial',
1171
+ ],
1172
+ notes: [
1173
+ 'Use document como input principal quando o componente for hospedado pelo page-builder.',
1174
+ 'O documento deve usar kind "praxis.rich-content" e version "1.0.0".',
1175
+ 'Nodes de rich content representam apresentacao/documento rico; nao substituem field controls de dynamic-fields.',
1176
+ 'visibleWhen, classWhen e styleWhen usam Json Logic canonico, nao string DSL.',
1177
+ 'hostCapabilities e reservado para mediacao governada de preset/action/embed; nao serializar funcoes arbitrarias em contratos publicos.',
1178
+ ],
1179
+ capabilities: [
1180
+ { path: 'document', category: 'richContent', valueKind: 'object', description: 'RichContentDocument canonico renderizado pelo componente.' },
1181
+ { path: 'document.kind', category: 'richContent', valueKind: 'string', description: 'Identificador fixo do documento. Use "praxis.rich-content".' },
1182
+ { path: 'document.version', category: 'richContent', valueKind: 'string', description: 'Versao do contrato do documento. Use "1.0.0".' },
1183
+ { path: 'document.context', category: 'richContent', valueKind: 'object', description: 'Config de escopos e aliases de contexto para bindings.' },
1184
+ { path: 'document.nodes', category: 'richNode', valueKind: 'array', description: 'Lista declarativa de nodes ricos.' },
1185
+ { path: 'document.nodes[]', category: 'richNode', valueKind: 'object', description: 'Node rich-content; o shape depende de document.nodes[].type.' },
1186
+ { path: 'document.nodes[].type', category: 'richNode', valueKind: 'enum', allowedValues: ['text', 'icon', 'image', 'badge', 'avatar', 'metric', 'progress', 'compose', 'card', 'mediaBlock', 'timeline', 'preset'], description: 'Tipo canonico do node.' },
1187
+ { path: 'document.nodes[].id', category: 'richNode', valueKind: 'string', description: 'Identificador estavel opcional para diffs, testes e telemetria.' },
1188
+ { path: 'document.nodes[].testId', category: 'richNode', valueKind: 'string', description: 'Test id opcional para E2E.' },
1189
+ { path: 'document.nodes[].className', category: 'richNode', valueKind: 'string', description: 'Classe CSS declarativa aplicada ao node.' },
1190
+ { path: 'document.nodes[].style', category: 'richNode', valueKind: 'object', description: 'Estilos inline declarativos simples para o node.' },
1191
+ { path: 'document.nodes[].visibleWhen', category: 'richRules', valueKind: 'expression', description: 'Regra Json Logic que controla visibilidade do node.' },
1192
+ { path: 'document.nodes[].classWhen', category: 'richRules', valueKind: 'array', description: 'Lista de regras Json Logic que aplicam classes.' },
1193
+ { path: 'document.nodes[].styleWhen', category: 'richRules', valueKind: 'array', description: 'Lista de regras Json Logic que aplicam estilos.' },
1194
+ { path: 'document.nodes[].text', category: 'richNode', valueKind: 'string', description: 'Texto literal para node text.' },
1195
+ { path: 'document.nodes[].textExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para node text.' },
1196
+ { path: 'document.nodes[].icon', category: 'richNode', valueKind: 'string', description: 'Nome do icone Material Symbols para node icon ou badge.' },
1197
+ { path: 'document.nodes[].label', category: 'richNode', valueKind: 'string', description: 'Label literal para badge, metric ou progress.' },
1198
+ { path: 'document.nodes[].labelExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para label.' },
1199
+ { path: 'document.nodes[].src', category: 'richNode', valueKind: 'string', description: 'URL literal para image.' },
1200
+ { path: 'document.nodes[].srcExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para image src.' },
1201
+ { path: 'document.nodes[].alt', category: 'richNode', valueKind: 'string', description: 'Texto alternativo literal para image.' },
1202
+ { path: 'document.nodes[].name', category: 'richNode', valueKind: 'string', description: 'Nome literal para avatar.' },
1203
+ { path: 'document.nodes[].nameExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para avatar.' },
1204
+ { path: 'document.nodes[].valueExpr', category: 'richNode', valueKind: 'string', description: 'Path simples resolvido contra o contexto para metric/progress.' },
1205
+ { path: 'document.nodes[].items', category: 'richNode', valueKind: 'array', description: 'Itens internos de compose.' },
1206
+ { path: 'document.nodes[].content', category: 'richNode', valueKind: 'array', description: 'Conteudo interno de card.' },
1207
+ { path: 'document.nodes[].trailing', category: 'richNode', valueKind: 'array', description: 'Conteudo trailing de mediaBlock.' },
1208
+ { path: 'document.nodes[].ref', category: 'richNode', valueKind: 'object', description: 'Referencia registravel de preset rich-block.' },
1209
+ { path: 'context', category: 'richContent', valueKind: 'object', description: 'Contexto externo injetado pelo host ou page-builder.' },
1210
+ { path: 'layout', category: 'richContent', valueKind: 'enum', allowedValues: ['block', 'inline'], description: 'Modo de layout do renderer.' },
1211
+ { path: 'rootClassName', category: 'richContent', valueKind: 'string', description: 'Classe aplicada ao root do renderer.' },
1212
+ { path: 'hostCapabilities', category: 'richHost', valueKind: 'object', description: 'Capacidades mediadas pelo host. Nao usar funcoes serializadas em JSON publico.' },
1213
+ ],
1214
+ };
1215
+
615
1216
  /*
616
1217
  * Public API Surface of praxis-rich-content
617
1218
  */
@@ -620,4 +1221,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
620
1221
  * Generated bundle index. Do not edit.
621
1222
  */
622
1223
 
623
- export { PRAXIS_RICH_BLOCK_PRESETS, PraxisRichContent, RichContentPresetRegistryService };
1224
+ export { PRAXIS_RICH_BLOCK_PRESETS, PRAXIS_RICH_CONTENT_COMPONENT_METADATA, PraxisRichContent, PraxisRichContentConfigEditor, RICH_CONTENT_AI_CAPABILITIES, RichContentPresetRegistryService, createPraxisRichContentI18nConfig, providePraxisRichContentI18n, providePraxisRichContentMetadata, resolvePraxisRichContentText };
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as _angular_core from '@angular/core';
2
- import { InjectionToken } from '@angular/core';
3
- import { RichContentDocument, RichPrimitiveNode, RichPresetReferenceNode, JsonLogicRecord, RichBlockHostCapabilities, RichBlockNode, RichPresenterNode, RichComposeNode, RichCardNode, RichMediaBlockNode, RichTimelineNode, RichTimelineItem, CorePresetDescriptor, CorePresetRef } from '@praxisui/core';
2
+ import { OnChanges, OnInit, SimpleChanges, InjectionToken, Provider } from '@angular/core';
3
+ import { RichContentDocument, RichPrimitiveNode, RichPresetReferenceNode, JsonLogicDataRecord, RichBlockHostCapabilities, RichBlockNode, RichPresenterNode, RichComposeNode, RichCardNode, RichMediaBlockNode, RichTimelineNode, RichTimelineItem, SettingsValueProvider, CorePresetDescriptor, CorePresetRef, ComponentDocMeta, AiCapabilityCategory, AiValueKind, AiCapability, AiCapabilityCatalog, PraxisI18nDictionary, PraxisI18nConfig, PraxisI18nService } from '@praxisui/core';
4
+ import { BehaviorSubject } from 'rxjs';
4
5
 
5
6
  type RenderableNode = RichPrimitiveNode | RichPresetReferenceNode;
6
7
  declare class PraxisRichContent {
@@ -8,8 +9,9 @@ declare class PraxisRichContent {
8
9
  private readonly presetRegistry;
9
10
  readonly document: _angular_core.InputSignal<RichContentDocument>;
10
11
  readonly nodes: _angular_core.InputSignal<RenderableNode[]>;
11
- readonly context: _angular_core.InputSignal<JsonLogicRecord>;
12
+ readonly context: _angular_core.InputSignal<JsonLogicDataRecord>;
12
13
  readonly hostCapabilities: _angular_core.InputSignal<RichBlockHostCapabilities>;
14
+ readonly layout: _angular_core.InputSignal<"block" | "inline">;
13
15
  rootClassName: string;
14
16
  readonly renderableNodes: _angular_core.Signal<RenderableNode[]>;
15
17
  isNodeVisible(node: RichBlockNode): boolean;
@@ -69,7 +71,50 @@ declare class PraxisRichContent {
69
71
  private getByPath;
70
72
  private resolveInitials;
71
73
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<PraxisRichContent, never>;
72
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<PraxisRichContent, "praxis-rich-content", never, { "document": { "alias": "document"; "required": false; "isSignal": true; }; "nodes": { "alias": "nodes"; "required": false; "isSignal": true; }; "context": { "alias": "context"; "required": false; "isSignal": true; }; "hostCapabilities": { "alias": "hostCapabilities"; "required": false; "isSignal": true; }; "rootClassName": { "alias": "rootClassName"; "required": false; }; }, {}, never, never, true, never>;
74
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<PraxisRichContent, "praxis-rich-content", never, { "document": { "alias": "document"; "required": false; "isSignal": true; }; "nodes": { "alias": "nodes"; "required": false; "isSignal": true; }; "context": { "alias": "context"; "required": false; "isSignal": true; }; "hostCapabilities": { "alias": "hostCapabilities"; "required": false; "isSignal": true; }; "layout": { "alias": "layout"; "required": false; "isSignal": true; }; "rootClassName": { "alias": "rootClassName"; "required": false; }; }, {}, never, never, true, never>;
75
+ }
76
+
77
+ interface PraxisRichContentEditorInputs {
78
+ document?: RichContentDocument | null;
79
+ layout?: 'block' | 'inline';
80
+ rootClassName?: string | null;
81
+ [key: string]: unknown;
82
+ }
83
+ interface PraxisRichContentEditorValue {
84
+ inputs: PraxisRichContentEditorInputs;
85
+ }
86
+ declare class PraxisRichContentConfigEditor implements SettingsValueProvider, OnChanges, OnInit {
87
+ private readonly cdr;
88
+ private readonly i18n;
89
+ private readonly injectedData;
90
+ inputs: PraxisRichContentEditorInputs | null;
91
+ readonly isDirty$: BehaviorSubject<boolean>;
92
+ readonly isValid$: BehaviorSubject<boolean>;
93
+ readonly isBusy$: BehaviorSubject<boolean>;
94
+ documentJson: string;
95
+ layout: 'block' | 'inline';
96
+ rootClassName: string;
97
+ errorMessage: string;
98
+ valid: boolean;
99
+ private initialInputs;
100
+ ngOnChanges(changes: SimpleChanges): void;
101
+ ngOnInit(): void;
102
+ getSettingsValue(): PraxisRichContentEditorValue | null;
103
+ onSave(): PraxisRichContentEditorValue | null;
104
+ reset(): void;
105
+ markDirty(): void;
106
+ onDocumentJsonChange(): void;
107
+ tx(key: string, fallback: string): string;
108
+ useEmptyDocument(): void;
109
+ private load;
110
+ private normalizeInputs;
111
+ private parseDocument;
112
+ private matchesInitialState;
113
+ private cloneInputs;
114
+ private cloneValue;
115
+ private stringify;
116
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<PraxisRichContentConfigEditor, never>;
117
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<PraxisRichContentConfigEditor, "praxis-rich-content-config-editor", never, { "inputs": { "alias": "inputs"; "required": false; }; }, {}, never, never, true, never>;
73
118
  }
74
119
 
75
120
  interface RichBlockPresetDefinition extends CorePresetDescriptor {
@@ -87,5 +132,35 @@ declare class RichContentPresetRegistryService {
87
132
  static ɵprov: _angular_core.ɵɵInjectableDeclaration<RichContentPresetRegistryService>;
88
133
  }
89
134
 
90
- export { PRAXIS_RICH_BLOCK_PRESETS, PraxisRichContent, RichContentPresetRegistryService };
91
- export type { RichBlockPresetDefinition };
135
+ declare const PRAXIS_RICH_CONTENT_COMPONENT_METADATA: ComponentDocMeta;
136
+ declare function providePraxisRichContentMetadata(): Provider;
137
+
138
+ declare module '@praxisui/core' {
139
+ interface AiCapabilityCategoryMap {
140
+ richContent: true;
141
+ richNode: true;
142
+ richRules: true;
143
+ richHost: true;
144
+ }
145
+ }
146
+ type RichContentCapabilityCategory = AiCapabilityCategory;
147
+ type RichContentValueKind = AiValueKind;
148
+ interface RichContentCapability extends AiCapability {
149
+ category: RichContentCapabilityCategory;
150
+ }
151
+ interface RichContentCapabilityCatalog extends AiCapabilityCatalog {
152
+ capabilities: RichContentCapability[];
153
+ }
154
+ declare const RICH_CONTENT_AI_CAPABILITIES: RichContentCapabilityCatalog;
155
+
156
+ interface PraxisRichContentI18nOptions {
157
+ locale?: string;
158
+ fallbackLocale?: string;
159
+ dictionaries?: Record<string, PraxisI18nDictionary>;
160
+ }
161
+ declare function createPraxisRichContentI18nConfig(options?: PraxisRichContentI18nOptions): Partial<PraxisI18nConfig>;
162
+ declare function providePraxisRichContentI18n(options?: PraxisRichContentI18nOptions): _angular_core.Provider;
163
+ declare function resolvePraxisRichContentText(i18n: PraxisI18nService, key: string, fallback: string): string;
164
+
165
+ export { PRAXIS_RICH_BLOCK_PRESETS, PRAXIS_RICH_CONTENT_COMPONENT_METADATA, PraxisRichContent, PraxisRichContentConfigEditor, RICH_CONTENT_AI_CAPABILITIES, RichContentPresetRegistryService, createPraxisRichContentI18nConfig, providePraxisRichContentI18n, providePraxisRichContentMetadata, resolvePraxisRichContentText };
166
+ export type { PraxisRichContentEditorInputs, PraxisRichContentEditorValue, PraxisRichContentI18nOptions, RichBlockPresetDefinition, RichContentCapability, RichContentCapabilityCatalog, RichContentCapabilityCategory, RichContentValueKind };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@praxisui/rich-content",
3
- "version": "7.0.0-beta.0",
3
+ "version": "8.0.0-beta.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^20.3.0",
6
6
  "@angular/core": "^20.3.0",
7
- "@praxisui/core": "^7.0.0-beta.0"
7
+ "@praxisui/core": "^8.0.0-beta.0"
8
8
  },
9
9
  "dependencies": {
10
10
  "tslib": "^2.3.0"