@praxisui/page-builder 9.0.4-rc.8 → 9.0.4

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
@@ -204,6 +204,27 @@ providers: [
204
204
 
205
205
  The package exports `PRAXIS_PAGE_BUILDER_AUTHORING_MANIFEST` for governed operation discovery. The persisted runtime page is still `WidgetPageDefinition`; intermediate AI plans such as `UiCompositionPlan` must compile before preview, apply, or save.
206
206
 
207
+ Repeated selection projections can be authored once through `UiCompositionPlan.selectionSyncs`. A sync declares structured component-output sources, a base state target, and a field-to-payload mapping. `compileUiCompositionPlan` expands every source/mapping pair into explicit `page.composition.links` with stable ids, canonical `pick-path` transforms, policy, condition, and `selection-sync` intent. The shorthand is never persisted in `WidgetPageDefinition`, so runtime inspection and audit continue to see the complete executable graph.
208
+
209
+ ```ts
210
+ selectionSyncs: [{
211
+ id: 'employee-selection',
212
+ intent: 'selection-sync',
213
+ sources: [
214
+ { kind: 'component-port', widget: 'employee-portfolio', port: 'rowClick', direction: 'output' },
215
+ { kind: 'component-port', widget: 'employee-portfolio', port: 'rowAction', direction: 'output' },
216
+ ],
217
+ target: { kind: 'state', path: 'selection', layer: 'values' },
218
+ mapping: {
219
+ employeeId: 'payload.row.id',
220
+ employeeIdentity: 'payload.resourceIdentity',
221
+ employee: 'payload.row',
222
+ },
223
+ }]
224
+ ```
225
+
226
+ Repeated parent/resource context can be declared through `UiCompositionPlan.contextScopes`. Each context value is explicitly either `constant` or `state`; each target uses a stable owner widget and optional canonical `nestedPath`. During compilation, constants are materialized into component inputs through core's `NestedWidgetConfigAccessor`, while state values become normal `state-read` links. Targets can inherit all keys or an explicit subset. The persisted page therefore contains only concrete widget inputs and `composition.links`, never ambient or implicit context inheritance.
227
+
207
228
  Streaming apply is fail-closed. A preview is persistable only when it is the unchanged payload of an applicable terminal `result` event and its diagnostics carry the matching `streamId`, `threadId`, `turnId`, and `resultEventId`. A locally regenerated or normalized preview remains available for review, but it cannot reuse an older terminal reference or call `page-apply`; the backend must issue a new terminal result for the new patch.
208
229
 
209
230
  ## Public API
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-08-02T14:11:23.065Z",
3
+ "generatedAt": "2026-08-11T17:36:05.628Z",
4
4
  "packageName": "@praxisui/page-builder",
5
- "packageVersion": "9.0.4-rc.8",
5
+ "packageVersion": "9.0.4",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 1,
@@ -2621,6 +2621,8 @@
2621
2621
  "handlerContract": {
2622
2622
  "reads": [
2623
2623
  "UiCompositionPlan",
2624
+ "UiCompositionPlan.selectionSyncs",
2625
+ "UiCompositionPlan.contextScopes",
2624
2626
  "compileUiCompositionPlan",
2625
2627
  "PAGE_BUILDER_WIDGET_AI_CATALOGS",
2626
2628
  "ComponentDocMeta.configEditor"
@@ -2631,7 +2633,9 @@
2631
2633
  "identityKeys": [
2632
2634
  "uiCompositionPlan.id",
2633
2635
  "widgetKey",
2634
- "uiCompositionPlan.bindings[].id"
2636
+ "uiCompositionPlan.bindings[].id",
2637
+ "uiCompositionPlan.selectionSyncs[].id",
2638
+ "uiCompositionPlan.contextScopes[].id"
2635
2639
  ],
2636
2640
  "failureModes": [
2637
2641
  "ui-composition-plan-invalid",
@@ -3261,6 +3265,8 @@
3261
3265
  "roundTripRequirements": [
3262
3266
  "WidgetPageDefinition is the canonical persisted runtime page shape for page-builder.",
3263
3267
  "UiCompositionPlan is an intermediate agentic plan and must compile before local preview/apply.",
3268
+ "UiCompositionPlan.selectionSyncs is an authoring shorthand that compiles into explicit canonical page.composition.links and is never persisted in WidgetPageDefinition.",
3269
+ "UiCompositionPlan.contextScopes compiles constant context into explicit widget inputs and state context into explicit page.composition.links; runtime inheritance is never ambient.",
3264
3270
  "Widget keys and composition link ids are stable identities; array indexes are never canonical identities.",
3265
3271
  "Persisted wiring uses page.composition.links and must not write legacy connections.",
3266
3272
  "Nested component ports use component-port refs with nestedPath ending in a stable widget key.",
@@ -3688,9 +3694,9 @@
3688
3694
  {
3689
3695
  "chunkIndex": 21,
3690
3696
  "chunkKind": "authoring_manifest",
3691
- "content": "{\"schemaVersion\":\"1.0.0\",\"manifestVersion\":\"1.0.0\",\"componentId\":\"praxis-page-builder\",\"ownerPackage\":\"@praxisui/page-builder\",\"configSchemaId\":\"WidgetPageDefinition\",\"chunkSection\":\"operations\",\"operations\":[{\"operationId\":\"composition.link.add\",\"title\":\"Add page composition link\",\"scope\":\"dataBinding\",\"targetKind\":\"compositionLink\",\"target\":{\"kind\":\"compositionLink\",\"resolver\":\"composition-link-by-id\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"id\",\"from\",\"to\",\"intent\"],\"additionalProperties\":false,\"properties\":{\"id\":{\"type\":\"string\"},\"from\":{\"oneOf\":[{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"component-port\"]},\"ref\":{\"type\":\"object\",\"required\":[\"widget\",\"port\",\"direction\"],\"additionalProperties\":false,\"properties\":{\"widget\":{\"type\":\"string\"},\"port\":{\"type\":\"string\"},\"direction\":{\"enum\":[\"input\",\"output\"]},\"bindingPath\":{\"type\":\"string\"},\"nestedPath\":{\"type\":\"array\"}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"state\"]},\"ref\":{\"type\":\"object\",\"required\":[\"path\"],\"additionalProperties\":false,\"properties\":{\"path\":{\"type\":\"string\"},\"layer\":{\"enum\":[\"values\",\"derived\",\"transient\"]}}}}}]},\"to\":{\"oneOf\":[{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"component-port\"]},\"ref\":{\"type\":\"object\",\"required\":[\"widget\",\"port\",\"direction\"],\"additionalProperties\":false,\"properties\":{\"widget\":{\"type\":\"string\"},\"port\":{\"type\":\"string\"},\"direction\":{\"enum\":[\"input\",\"output\"]},\"bindingPath\":{\"type\":\"string\"},\"nestedPath\":{\"type\":\"array\"}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"state\"]},\"ref\":{\"type\":\"object\",\"required\":[\"path\"],\"additionalProperties\":false,\"properties\":{\"path\":{\"type\":\"string\"},\"layer\":{\"enum\":[\"values\",\"derived\",\"transient\"]}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"global-action\"]},\"ref\":{\"type\":\"object\",\"required\":[\"actionId\"],\"additionalProperties\":false,\"properties\":{\"actionId\":{\"type\":\"string\"},\"payload\":{\"type\":\"object\"},\"payloadExpr\":{\"type\":\"string\"},\"meta\":{\"type\":\"object\"}}}}}]},\"intent\":{\"enum\":[\"event-propagation\",\"state-write\",\"state-read\",\"command-dispatch\",\"selection-sync\",\"data-projection\",\"status-propagation\"]},\"condition\":{\"type\":\"object\"},\"transform\":{\"type\":\"object\"},\"policy\":{\"type\":\"object\"},\"metadata\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-composition-link-add\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.composition.links\",\"WidgetPageDefinition.widgets\",\"PAGE_BUILDER_AI_CAPABILITIES\"],\"writes\":[\"page.composition.links[]\"],\"identityKeys\":[\"id\"],\"inputSchema\":{\"type\":\"object\",\"required\":[\"id\",\"from\",\"to\",\"intent\"],\"additionalProperties\":false,\"properties\":{\"id\":{\"type\":\"string\"},\"from\":{\"oneOf\":[{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"component-port\"]},\"ref\":{\"type\":\"object\",\"required\":[\"widget\",\"port\",\"direction\"],\"additionalProperties\":false,\"properties\":{\"widget\":{\"type\":\"string\"},\"port\":{\"type\":\"string\"},\"direction\":{\"enum\":[\"input\",\"output\"]},\"bindingPath\":{\"type\":\"string\"},\"nestedPath\":{\"type\":\"array\"}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"state\"]},\"ref\":{\"type\":\"object\",\"required\":[\"path\"],\"additionalProperties\":false,\"properties\":{\"path\":{\"type\":\"string\"},\"layer\":{\"enum\":[\"values\",\"derived\",\"transient\"]}}}}}]},\"to\":{\"oneOf\":[{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"component-port\"]},\"ref\":{\"type\":\"object\",\"required\":[\"widget\",\"port\",\"direction\"],\"additionalProperties\":false,\"properties\":{\"widget\":{\"type\":\"string\"},\"port\":{\"type\":\"string\"},\"direction\":{\"enum\":[\"input\",\"output\"]},\"bindingPath\":{\"type\":\"string\"},\"nestedPath\":{\"type\":\"array\"}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"state\"]},\"ref\":{\"type\":\"object\",\"required\":[\"path\"],\"additionalProperties\":false,\"properties\":{\"path\":{\"type\":\"string\"},\"layer\":{\"enum\":[\"values\",\"derived\",\"transient\"]}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"global-action\"]},\"ref\":{\"type\":\"object\",\"required\":[\"actionId\"],\"additionalProperties\":false,\"properties\":{\"actionId\":{\"type\":\"string\"},\"payload\":{\"type\":\"object\"},\"payloadExpr\":{\"type\":\"string\"},\"meta\":{\"type\":\"object\"}}}}}]},\"intent\":{\"enum\":[\"event-propagation\",\"state-write\",\"state-read\",\"command-dispatch\",\"selection-sync\",\"data-projection\",\"status-propagation\"]},\"condition\":{\"type\":\"object\"},\"transform\":{\"type\":\"object\"},\"policy\":{\"type\":\"object\"},\"metadata\":{\"type\":\"object\"}}},\"failureModes\":[\"link-id-duplicate\",\"endpoint-not-found\",\"nested-path-invalid\",\"json-logic-invalid\"],\"description\":\"Adds canonical declarative wiring in page.composition.links, including component-port nestedPath endpoints.\"}}],\"validators\":[\"composition-link-id-unique\",\"composition-endpoints-resolve\",\"nested-path-terminal-widget-key-required\",\"json-logic-condition-valid\",\"no-legacy-connections-write\"],\"affectedPaths\":[\"composition.links[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"widgets-resolvable\"]},{\"operationId\":\"composition.link.remove\",\"title\":\"Remove page composition link\",\"scope\":\"dataBinding\",\"targetKind\":\"compositionLink\",\"target\":{\"kind\":\"compositionLink\",\"resolver\":\"composition-link-by-id\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"id\"],\"properties\":{\"id\":{\"type\":\"string\"},\"confirmed\":{\"type\":\"boolean\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-composition-link-remove\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.composition.links\"],\"writes\":[\"page.composition.links[]\"],\"identityKeys\":[\"id\"],\"failureModes\":[\"composition-link-not-found\",\"destructive-removal-not-confirmed\"],\"description\":\"Removes a canonical composition link by stable id.\"}}],\"destructive\":true,\"requiresConfirmation\":true,\"validators\":[\"composition-link-exists\",\"destructive-removal-confirmed\",\"composition-links-still-valid\"],\"affectedPaths\":[\"composition.links[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"target-link-exists\"]},{\"operationId\":\"composition.plan.compile\",\"title\":\"Compile UI composition plan\",\"scope\":\"rules\",\"targetKind\":\"agenticPreview\",\"target\":{\"kind\":\"agenticPreview\",\"resolver\":\"agentic-preview-result\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"uiCompositionPlan\"],\"properties\":{\"uiCompositionPlan\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-ui-composition-plan-compile\",\"handlerContract\":{\"reads\":[\"UiCompositionPlan\",\"compileUiCompositionPlan\",\"PAGE_BUILDER_WIDGET_AI_CATALOGS\",\"ComponentDocMeta.configEditor\"],\"writes\":[\"compiledFormPatch.patch.page\"],\"identityKeys\":[\"uiCompositionPlan.id\",\"widgetKey\",\"uiCompositionPlan.bindings[].id\"],\"failureModes\":[\"ui-composition-plan-invalid\",\"widget-catalog-missing\",\"compiled-page-invalid\",\"child-config-editor-missing\"],\"description\":\"Compiles the agentic intermediate UiCompositionPlan into a renderable WidgetPageDefinition patch.page.\"}}],\"validators\":[\"ui-composition-plan-valid\",\"compiled-page-valid\",\"widget-keys-unique\",\"composition-endpoints-resolve\",\"canvas-items-reference-existing-widgets\"],\"affectedPaths\":[\"uiCompositionPlan\",\"compiledFormPatch.patch.page\"],\"submissionImpact\":\"none\",\"preconditions\":[\"agentic-preview-loaded\",\"component-catalog-loaded\"]}]}",
3697
+ "content": "{\"schemaVersion\":\"1.0.0\",\"manifestVersion\":\"1.0.0\",\"componentId\":\"praxis-page-builder\",\"ownerPackage\":\"@praxisui/page-builder\",\"configSchemaId\":\"WidgetPageDefinition\",\"chunkSection\":\"operations\",\"operations\":[{\"operationId\":\"composition.link.add\",\"title\":\"Add page composition link\",\"scope\":\"dataBinding\",\"targetKind\":\"compositionLink\",\"target\":{\"kind\":\"compositionLink\",\"resolver\":\"composition-link-by-id\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"id\",\"from\",\"to\",\"intent\"],\"additionalProperties\":false,\"properties\":{\"id\":{\"type\":\"string\"},\"from\":{\"oneOf\":[{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"component-port\"]},\"ref\":{\"type\":\"object\",\"required\":[\"widget\",\"port\",\"direction\"],\"additionalProperties\":false,\"properties\":{\"widget\":{\"type\":\"string\"},\"port\":{\"type\":\"string\"},\"direction\":{\"enum\":[\"input\",\"output\"]},\"bindingPath\":{\"type\":\"string\"},\"nestedPath\":{\"type\":\"array\"}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"state\"]},\"ref\":{\"type\":\"object\",\"required\":[\"path\"],\"additionalProperties\":false,\"properties\":{\"path\":{\"type\":\"string\"},\"layer\":{\"enum\":[\"values\",\"derived\",\"transient\"]}}}}}]},\"to\":{\"oneOf\":[{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"component-port\"]},\"ref\":{\"type\":\"object\",\"required\":[\"widget\",\"port\",\"direction\"],\"additionalProperties\":false,\"properties\":{\"widget\":{\"type\":\"string\"},\"port\":{\"type\":\"string\"},\"direction\":{\"enum\":[\"input\",\"output\"]},\"bindingPath\":{\"type\":\"string\"},\"nestedPath\":{\"type\":\"array\"}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"state\"]},\"ref\":{\"type\":\"object\",\"required\":[\"path\"],\"additionalProperties\":false,\"properties\":{\"path\":{\"type\":\"string\"},\"layer\":{\"enum\":[\"values\",\"derived\",\"transient\"]}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"global-action\"]},\"ref\":{\"type\":\"object\",\"required\":[\"actionId\"],\"additionalProperties\":false,\"properties\":{\"actionId\":{\"type\":\"string\"},\"payload\":{\"type\":\"object\"},\"payloadExpr\":{\"type\":\"string\"},\"meta\":{\"type\":\"object\"}}}}}]},\"intent\":{\"enum\":[\"event-propagation\",\"state-write\",\"state-read\",\"command-dispatch\",\"selection-sync\",\"data-projection\",\"status-propagation\"]},\"condition\":{\"type\":\"object\"},\"transform\":{\"type\":\"object\"},\"policy\":{\"type\":\"object\"},\"metadata\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-composition-link-add\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.composition.links\",\"WidgetPageDefinition.widgets\",\"PAGE_BUILDER_AI_CAPABILITIES\"],\"writes\":[\"page.composition.links[]\"],\"identityKeys\":[\"id\"],\"inputSchema\":{\"type\":\"object\",\"required\":[\"id\",\"from\",\"to\",\"intent\"],\"additionalProperties\":false,\"properties\":{\"id\":{\"type\":\"string\"},\"from\":{\"oneOf\":[{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"component-port\"]},\"ref\":{\"type\":\"object\",\"required\":[\"widget\",\"port\",\"direction\"],\"additionalProperties\":false,\"properties\":{\"widget\":{\"type\":\"string\"},\"port\":{\"type\":\"string\"},\"direction\":{\"enum\":[\"input\",\"output\"]},\"bindingPath\":{\"type\":\"string\"},\"nestedPath\":{\"type\":\"array\"}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"state\"]},\"ref\":{\"type\":\"object\",\"required\":[\"path\"],\"additionalProperties\":false,\"properties\":{\"path\":{\"type\":\"string\"},\"layer\":{\"enum\":[\"values\",\"derived\",\"transient\"]}}}}}]},\"to\":{\"oneOf\":[{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"component-port\"]},\"ref\":{\"type\":\"object\",\"required\":[\"widget\",\"port\",\"direction\"],\"additionalProperties\":false,\"properties\":{\"widget\":{\"type\":\"string\"},\"port\":{\"type\":\"string\"},\"direction\":{\"enum\":[\"input\",\"output\"]},\"bindingPath\":{\"type\":\"string\"},\"nestedPath\":{\"type\":\"array\"}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"state\"]},\"ref\":{\"type\":\"object\",\"required\":[\"path\"],\"additionalProperties\":false,\"properties\":{\"path\":{\"type\":\"string\"},\"layer\":{\"enum\":[\"values\",\"derived\",\"transient\"]}}}}},{\"type\":\"object\",\"required\":[\"kind\",\"ref\"],\"additionalProperties\":false,\"properties\":{\"kind\":{\"enum\":[\"global-action\"]},\"ref\":{\"type\":\"object\",\"required\":[\"actionId\"],\"additionalProperties\":false,\"properties\":{\"actionId\":{\"type\":\"string\"},\"payload\":{\"type\":\"object\"},\"payloadExpr\":{\"type\":\"string\"},\"meta\":{\"type\":\"object\"}}}}}]},\"intent\":{\"enum\":[\"event-propagation\",\"state-write\",\"state-read\",\"command-dispatch\",\"selection-sync\",\"data-projection\",\"status-propagation\"]},\"condition\":{\"type\":\"object\"},\"transform\":{\"type\":\"object\"},\"policy\":{\"type\":\"object\"},\"metadata\":{\"type\":\"object\"}}},\"failureModes\":[\"link-id-duplicate\",\"endpoint-not-found\",\"nested-path-invalid\",\"json-logic-invalid\"],\"description\":\"Adds canonical declarative wiring in page.composition.links, including component-port nestedPath endpoints.\"}}],\"validators\":[\"composition-link-id-unique\",\"composition-endpoints-resolve\",\"nested-path-terminal-widget-key-required\",\"json-logic-condition-valid\",\"no-legacy-connections-write\"],\"affectedPaths\":[\"composition.links[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"widgets-resolvable\"]},{\"operationId\":\"composition.link.remove\",\"title\":\"Remove page composition link\",\"scope\":\"dataBinding\",\"targetKind\":\"compositionLink\",\"target\":{\"kind\":\"compositionLink\",\"resolver\":\"composition-link-by-id\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"id\"],\"properties\":{\"id\":{\"type\":\"string\"},\"confirmed\":{\"type\":\"boolean\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-composition-link-remove\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.composition.links\"],\"writes\":[\"page.composition.links[]\"],\"identityKeys\":[\"id\"],\"failureModes\":[\"composition-link-not-found\",\"destructive-removal-not-confirmed\"],\"description\":\"Removes a canonical composition link by stable id.\"}}],\"destructive\":true,\"requiresConfirmation\":true,\"validators\":[\"composition-link-exists\",\"destructive-removal-confirmed\",\"composition-links-still-valid\"],\"affectedPaths\":[\"composition.links[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"target-link-exists\"]},{\"operationId\":\"composition.plan.compile\",\"title\":\"Compile UI composition plan\",\"scope\":\"rules\",\"targetKind\":\"agenticPreview\",\"target\":{\"kind\":\"agenticPreview\",\"resolver\":\"agentic-preview-result\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"uiCompositionPlan\"],\"properties\":{\"uiCompositionPlan\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-ui-composition-plan-compile\",\"handlerContract\":{\"reads\":[\"UiCompositionPlan\",\"UiCompositionPlan.selectionSyncs\",\"UiCompositionPlan.contextScopes\",\"compileUiCompositionPlan\",\"PAGE_BUILDER_WIDGET_AI_CATALOGS\",\"ComponentDocMeta.configEditor\"],\"writes\":[\"compiledFormPatch.patch.page\"],\"identityKeys\":[\"uiCompositionPlan.id\",\"widgetKey\",\"uiCompositionPlan.bindings[].id\",\"uiCompositionPlan.selectionSyncs[].id\",\"uiCompositionPlan.contextScopes[].id\"],\"failureModes\":[\"ui-composition-plan-invalid\",\"widget-catalog-missing\",\"compiled-page-invalid\",\"child-config-editor-missing\"],\"description\":\"Compiles the agentic intermediate UiCompositionPlan into a renderable WidgetPageDefinition patch.page.\"}}],\"validators\":[\"ui-composition-plan-valid\",\"compiled-page-valid\",\"widget-keys-unique\",\"composition-endpoints-resolve\",\"canvas-items-reference-existing-widgets\"],\"affectedPaths\":[\"uiCompositionPlan\",\"compiledFormPatch.patch.page\"],\"submissionImpact\":\"none\",\"preconditions\":[\"agentic-preview-loaded\",\"component-catalog-loaded\"]}]}",
3692
3698
  "sourcePointer": "projects/praxis-page-builder/src/lib/ai/praxis-page-builder-authoring-manifest.ts",
3693
- "contentHash": "a0e20e86811cdd1302049dbd6ec4f133d5fe9a60d962bc0cc60a9a772e2a4703",
3699
+ "contentHash": "f3de19943a6dfbd0452ecd46b1c1e5f4860cd71fd43eab9aa83958b6037604e9",
3694
3700
  "sourceKind": "component_definition",
3695
3701
  "sourceId": "praxis-page-builder",
3696
3702
  "corpusVersion": "1.0.0"
@@ -3718,9 +3724,9 @@
3718
3724
  {
3719
3725
  "chunkIndex": 24,
3720
3726
  "chunkKind": "authoring_manifest",
3721
- "content": "{\"schemaVersion\":\"1.0.0\",\"manifestVersion\":\"1.0.0\",\"componentId\":\"praxis-page-builder\",\"ownerPackage\":\"@praxisui/page-builder\",\"configSchemaId\":\"WidgetPageDefinition\",\"chunkSection\":\"roundTripRequirements\",\"roundTripRequirements\":[\"WidgetPageDefinition is the canonical persisted runtime page shape for page-builder.\",\"UiCompositionPlan is an intermediate agentic plan and must compile before local preview/apply.\",\"Widget keys and composition link ids are stable identities; array indexes are never canonical identities.\",\"Persisted wiring uses page.composition.links and must not write legacy connections.\",\"Nested component ports use component-port refs with nestedPath ending in a stable widget key.\",\"Child widget definition.inputs edits delegate to the child manifest or ComponentDocMeta.configEditor.\",\"Agentic preview applies only compiledFormPatch.patch.page to runtime; the full patch envelope is diagnostics/audit data.\",\"Save uses pageIdentity and ETag policy from praxis-config-starter; pageIdentity is not persisted inside WidgetPageDefinition.\",\"Settings panel apply/save/reset/reopen must preserve page settings and widget shell config without hidden normalization drift.\"]}",
3727
+ "content": "{\"schemaVersion\":\"1.0.0\",\"manifestVersion\":\"1.0.0\",\"componentId\":\"praxis-page-builder\",\"ownerPackage\":\"@praxisui/page-builder\",\"configSchemaId\":\"WidgetPageDefinition\",\"chunkSection\":\"roundTripRequirements\",\"roundTripRequirements\":[\"WidgetPageDefinition is the canonical persisted runtime page shape for page-builder.\",\"UiCompositionPlan is an intermediate agentic plan and must compile before local preview/apply.\",\"UiCompositionPlan.selectionSyncs is an authoring shorthand that compiles into explicit canonical page.composition.links and is never persisted in WidgetPageDefinition.\",\"UiCompositionPlan.contextScopes compiles constant context into explicit widget inputs and state context into explicit page.composition.links; runtime inheritance is never ambient.\",\"Widget keys and composition link ids are stable identities; array indexes are never canonical identities.\",\"Persisted wiring uses page.composition.links and must not write legacy connections.\",\"Nested component ports use component-port refs with nestedPath ending in a stable widget key.\",\"Child widget definition.inputs edits delegate to the child manifest or ComponentDocMeta.configEditor.\",\"Agentic preview applies only compiledFormPatch.patch.page to runtime; the full patch envelope is diagnostics/audit data.\",\"Save uses pageIdentity and ETag policy from praxis-config-starter; pageIdentity is not persisted inside WidgetPageDefinition.\",\"Settings panel apply/save/reset/reopen must preserve page settings and widget shell config without hidden normalization drift.\"]}",
3722
3728
  "sourcePointer": "projects/praxis-page-builder/src/lib/ai/praxis-page-builder-authoring-manifest.ts",
3723
- "contentHash": "42dad9779ac9ce50930e0b2954939f93d07bdaeb2dbe445835d1f980db78aaab",
3729
+ "contentHash": "01d817d6b28effa45c76146f3bf01e15e2c3488f97efa19eabbb62375a315f64",
3724
3730
  "sourceKind": "component_definition",
3725
3731
  "sourceId": "praxis-page-builder",
3726
3732
  "corpusVersion": "1.0.0"
@@ -3808,9 +3814,9 @@
3808
3814
  {
3809
3815
  "chunkIndex": 33,
3810
3816
  "chunkKind": "recipe",
3811
- "content": "{\"_comment\":\"Contrato operacional para orientar LLM/backend a emitir UiCompositionPlan antes do WidgetPageDefinition final.\",\"templateMeta\":{\"registryKey\":\"praxis-page-builder\",\"variantId\":\"ui-composition-plan\",\"status\":\"golden-fixture\",\"useWhen\":\"Agentic page creation or modification needs multiple Praxis widgets, state and bindings before compiling the persisted page JSON.\",\"examplePrompts\":[\"Crie uma tela master detail de departamentos com funcionarios, folha e resumo\",\"Monte um dashboard com lista mestre, tabela detalhe, grafico e texto dinamico\"]},\"outputEnvelope\":{\"uiCompositionPlan\":{\"version\":\"1.0\",\"kind\":\"praxis.ui-composition-plan\",\"layoutPreset\":\"{{layoutPreset}}\",\"state\":{\"values\":{\"{{stateKey}}\":null}},\"widgets\":[{\"key\":\"{{stableWidgetKey}}\",\"componentId\":\"{{registeredComponentId}}\",\"role\":\"{{semanticRole}}\",\"bindingOrder\":[\"{{inputName}}\"],\"inputs\":{\"{{inputName}}\":\"{{canonicalInputValue}}\"}}],\"bindings\":[{\"id\":\"{{stableBindingId}}\",\"from\":{\"kind\":\"component-port\",\"widget\":\"{{sourceWidgetKey}}\",\"port\":\"{{outputPort}}\",\"direction\":\"output\"},\"to\":{\"kind\":\"state\",\"path\":\"{{statePath}}\"},\"intent\":\"state-write\",\"transform\":{\"kind\":\"pick-path\",\"id\":\"{{transformId}}\",\"path\":\"{{payloadPath}}\"},\"metadata\":{\"source\":\"ui-composition-plan\"}}]},\"compiledFormPatch\":{\"version\":\"1.1\",\"patch\":{}}},\"validationRules\":[\"uiCompositionPlan.version deve ser 1.0.\",\"uiCompositionPlan.kind deve ser praxis.ui-composition-plan.\",\"widgets[].key deve ser estavel e unico.\",\"widgets[].componentId deve existir no catalogo de componentes registrado do Page Builder.\",\"bindings[].from/to com kind component-port devem referenciar widgets existentes no proprio plano.\",\"bindings[].from/to com kind state devem usar paths declarados ou justificaveis em state.values/state.derived.\",\"bindings[].to com kind global-action deve declarar actionId e compila para page.composition.links[].to.ref.[actionId].\",\"Use intent command-dispatch para bindings cujo destino e global-action.\",\"Use bindings como fonte declarativa. Nao emita connections legado nem page.composition.links concorrente no mesmo plano.\",\"Use pick-path para extrair payload/evento, query-context para filtros de recurso, template para gerar inputs estruturados e constant para valores fixos.\",\"Se API, metadata, surface, action, capability ou porta de componente nao estiver confirmada, retorne clarification/diagnostic em vez de alucinar o plano.\"],\"example\":{\"uiCompositionPlan\":{\"version\":\"1.0\",\"kind\":\"praxis.ui-composition-plan\",\"layoutPreset\":\"master-detail-dashboard\",\"state\":{\"values\":{\"selectedDepartmentId\":null,\"selectedDepartmentName\":null}},\"widgets\":[{\"key\":\"department-master\",\"componentId\":\"praxis-list\",\"role\":\"master\",\"inputs\":{\"config\":{\"dataSource\":{\"resourcePath\":\"/api/human-resources/departamentos\"}}}},{\"key\":\"department-employees\",\"componentId\":\"praxis-table\",\"role\":\"detail\",\"inputs\":{\"resourcePath\":\"/api/human-resources/funcionarios\",\"tableId\":\"department-employees\"}},{\"key\":\"department-payroll\",\"componentId\":\"praxis-chart\",\"role\":\"detail\",\"inputs\":{\"config\":{\"chartId\":\"department-payroll\",\"resourcePath\":\"/api/human-resources/vw-analytics-folha-pagamento\"}}},{\"key\":\"department-summary\",\"componentId\":\"praxis-rich-content\",\"role\":\"detail\",\"inputs\":{\"document\":{\"kind\":\"praxis.rich-content.document\",\"version\":\"1.0.0\",\"nodes\":[{\"type\":\"text\",\"text\":\"Selecione um departamento para revisar funcionarios, folha e observacoes.\"}]}}}],\"bindings\":[{\"id\":\"department-master.itemSelect->state.selectedDepartmentId\",\"from\":{\"kind\":\"component-port\",\"widget\":\"department-master\",\"port\":\"itemSelect\",\"direction\":\"output\"},\"to\":{\"kind\":\"state\",\"path\":\"selectedDepartmentId\"},\"intent\":\"state-write\",\"transform\":{\"kind\":\"pick-path\",\"id\":\"pick-department-id\",\"path\":\"payload.item.id\"},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"state\"]}},{\"id\":\"department-master.itemSelect->state.selectedDepartmentName\",\"from\":{\"kind\":\"component-port\",\"widget\":\"department-master\",\"port\":\"itemSelect\",\"direction\":\"output\"},\"to\":{\"kind\":\"state\",\"path\":\"selectedDepartmentName\"},\"intent\":\"state-write\",\"transform\":{\"kind\":\"pick-path\",\"id\":\"pick-department-name\",\"path\":\"payload.item.nome\"},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"state\"]}},{\"id\":\"department-master.itemSelect->global-action.department-selection-audit\",\"from\":{\"kind\":\"component-port\",\"widget\":\"department-master\",\"port\":\"itemSelect\",\"direction\":\"output\"},\"to\":{\"kind\":\"global-action\",\"actionId\":\"department.selection.audit\",\"payloadExpr\":\"payload.item\",\"meta\":{\"label\":\"Audit department selection\"}},\"intent\":\"command-dispatch\",\"policy\":{\"distinct\":true,\"distinctBy\":\"payload.item.id\",\"missingValuePolicy\":\"skip\"},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"global-action\"]}},{\"id\":\"state.selectedDepartmentId->department-employees.queryContext\",\"from\":{\"kind\":\"state\",\"path\":\"selectedDepartmentId\"},\"to\":{\"kind\":\"component-port\",\"widget\":\"department-employees\",\"port\":\"queryContext\",\"direction\":\"input\"},\"intent\":\"state-read\",\"condition\":{\"!=\":[{\"var\":\"state.selectedDepartmentId\"},null]},\"transform\":{\"kind\":\"query-context\",\"id\":\"department-filter\",\"field\":\"departamentoId\"},\"policy\":{\"distinct\":true,\"distinctBy\":\"value\",\"missingValuePolicy\":\"skip\"},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"table-filter\"]}},{\"id\":\"state.selectedDepartmentId->department-payroll.queryContext\",\"from\":{\"kind\":\"state\",\"path\":\"selectedDepartmentId\"},\"to\":{\"kind\":\"component-port\",\"widget\":\"department-payroll\",\"port\":\"queryContext\",\"direction\":\"input\"},\"intent\":\"state-read\",\"condition\":{\"!=\":[{\"var\":\"state.selectedDepartmentId\"},null]},\"transform\":{\"kind\":\"query-context\",\"id\":\"department-payroll-filter\",\"field\":\"departamentoId\"},\"policy\":{\"distinct\":true,\"missingValuePolicy\":\"skip\"},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"chart-filter\"]}},{\"id\":\"state.selectedDepartmentName->department-summary.document\",\"from\":{\"kind\":\"state\",\"path\":\"selectedDepartmentName\"},\"to\":{\"kind\":\"component-port\",\"widget\":\"department-summary\",\"port\":\"document\",\"direction\":\"input\"},\"intent\":\"state-read\",\"condition\":{\"!!\":[{\"var\":\"state.selectedDepartmentName\"}]},\"transform\":{\"kind\":\"template\",\"id\":\"department-summary-document\",\"template\":{\"kind\":\"praxis.rich-content.document\",\"version\":\"1.0.0\",\"nodes\":[{\"type\":\"text\",\"text\":{\"concat\":[\"Departamento selecionado: \",{\"var\":\"value\"}]}}]}},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"rich-content\"]}}]}},\"compiledOutput\":{\"summary\":\"O compilador gera WidgetPageDefinition com page.widgets[].definition.id, page.widgets[].definition.inputs e page.composition.links version 1.0.0. O JSON persistido nao deve conter connections legado.\",\"forbiddenLegacyPaths\":[\"page.options\",\"page.widgets[].layout\",\"page.connections\",\"connections\"]}}",
3817
+ "content": "{\"_comment\":\"Contrato operacional para orientar LLM/backend a emitir UiCompositionPlan antes do WidgetPageDefinition final.\",\"templateMeta\":{\"registryKey\":\"praxis-page-builder\",\"variantId\":\"ui-composition-plan\",\"status\":\"golden-fixture\",\"useWhen\":\"Agentic page creation or modification needs multiple Praxis widgets, state and bindings before compiling the persisted page JSON.\",\"examplePrompts\":[\"Crie uma tela master detail de departamentos com funcionarios, folha e resumo\",\"Monte um dashboard com lista mestre, tabela detalhe, grafico e texto dinamico\"]},\"outputEnvelope\":{\"uiCompositionPlan\":{\"version\":\"1.0\",\"kind\":\"praxis.ui-composition-plan\",\"layoutPreset\":\"{{layoutPreset}}\",\"state\":{\"values\":{\"{{stateKey}}\":null}},\"widgets\":[{\"key\":\"{{stableWidgetKey}}\",\"componentId\":\"{{registeredComponentId}}\",\"role\":\"{{semanticRole}}\",\"bindingOrder\":[\"{{inputName}}\"],\"inputs\":{\"{{inputName}}\":\"{{canonicalInputValue}}\"}}],\"selectionSyncs\":[{\"id\":\"{{stableSelectionSyncId}}\",\"intent\":\"selection-sync\",\"sources\":[{\"kind\":\"component-port\",\"widget\":\"{{sourceWidgetKey}}\",\"port\":\"{{selectionOutputPort}}\",\"direction\":\"output\"}],\"target\":{\"kind\":\"state\",\"path\":\"{{selectionStatePath}}\",\"layer\":\"values\"},\"mapping\":{\"{{selectionField}}\":\"{{selectionPayloadPath}}\"}}],\"contextScopes\":[{\"id\":\"{{stableContextScopeId}}\",\"context\":{\"parentResourcePath\":{\"kind\":\"constant\",\"value\":\"{{parentResourcePath}}\"},\"parentResourceId\":{\"kind\":\"state\",\"path\":\"{{parentResourceIdStatePath}}\",\"layer\":\"values\"}},\"targets\":[{\"widget\":\"{{contextOwnerWidgetKey}}\",\"nestedPath\":[{\"kind\":\"{{containerSegmentKind}}\",\"id\":\"{{containerSegmentId}}\"},{\"kind\":\"widget\",\"key\":\"{{nestedWidgetKey}}\"}]}]}],\"bindings\":[{\"id\":\"{{stableBindingId}}\",\"from\":{\"kind\":\"component-port\",\"widget\":\"{{sourceWidgetKey}}\",\"port\":\"{{outputPort}}\",\"direction\":\"output\"},\"to\":{\"kind\":\"state\",\"path\":\"{{statePath}}\"},\"intent\":\"state-write\",\"transform\":{\"kind\":\"pick-path\",\"id\":\"{{transformId}}\",\"path\":\"{{payloadPath}}\"},\"metadata\":{\"source\":\"ui-composition-plan\"}}]},\"compiledFormPatch\":{\"version\":\"1.1\",\"patch\":{}}},\"validationRules\":[\"uiCompositionPlan.version deve ser 1.0.\",\"uiCompositionPlan.kind deve ser praxis.ui-composition-plan.\",\"widgets[].key deve ser estavel e unico.\",\"widgets[].componentId deve existir no catalogo de componentes registrado do Page Builder.\",\"bindings[].from/to com kind component-port devem referenciar widgets existentes no proprio plano.\",\"bindings[].from/to com kind state devem usar paths declarados ou justificaveis em state.values/state.derived.\",\"bindings[].to com kind global-action deve declarar actionId e compila para page.composition.links[].to.ref.[actionId].\",\"Use intent command-dispatch para bindings cujo destino e global-action.\",\"selectionSyncs[] reduz autoria repetitiva e sempre compila cada source/mapping em page.composition.links explicitos com intent selection-sync.\",\"contextScopes[] materializa constantes em inputs e estado em links state-read explicitos; nao existe heranca de contexto ambiente no runtime persistido.\",\"Use bindings como fonte declarativa. Nao emita connections legado nem page.composition.links concorrente no mesmo plano.\",\"Use pick-path para extrair payload/evento, query-context para filtros de recurso, template para gerar inputs estruturados e constant para valores fixos.\",\"Se API, metadata, surface, action, capability ou porta de componente nao estiver confirmada, retorne clarification/diagnostic em vez de alucinar o plano.\"],\"example\":{\"uiCompositionPlan\":{\"version\":\"1.0\",\"kind\":\"praxis.ui-composition-plan\",\"layoutPreset\":\"master-detail-dashboard\",\"state\":{\"values\":{\"selection\":{\"departmentId\":null,\"departmentName\":null}}},\"widgets\":[{\"key\":\"department-master\",\"componentId\":\"praxis-list\",\"role\":\"master\",\"inputs\":{\"config\":{\"dataSource\":{\"resourcePath\":\"/api/human-resources/departamentos\"}}}},{\"key\":\"department-employees\",\"componentId\":\"praxis-table\",\"role\":\"detail\",\"inputs\":{\"resourcePath\":\"/api/human-resources/funcionarios\",\"tableId\":\"department-employees\"}},{\"key\":\"department-payroll\",\"componentId\":\"praxis-chart\",\"role\":\"detail\",\"inputs\":{\"config\":{\"chartId\":\"department-payroll\",\"resourcePath\":\"/api/human-resources/vw-analytics-folha-pagamento\"}}},{\"key\":\"department-summary\",\"componentId\":\"praxis-rich-content\",\"role\":\"detail\",\"inputs\":{\"document\":{\"kind\":\"praxis.rich-content.document\",\"version\":\"1.0.0\",\"nodes\":[{\"type\":\"text\",\"text\":\"Selecione um departamento para revisar funcionarios, folha e observacoes.\"}]}}}],\"selectionSyncs\":[{\"id\":\"department-selection\",\"intent\":\"selection-sync\",\"sources\":[{\"kind\":\"component-port\",\"widget\":\"department-master\",\"port\":\"itemSelect\",\"direction\":\"output\"}],\"target\":{\"kind\":\"state\",\"path\":\"selection\",\"layer\":\"values\"},\"mapping\":{\"departmentId\":\"payload.item.id\",\"departmentName\":\"payload.item.nome\"},\"policy\":{\"distinct\":true,\"missingValuePolicy\":\"skip\"},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"selection\"]}}],\"bindings\":[{\"id\":\"department-master.itemSelect->global-action.department-selection-audit\",\"from\":{\"kind\":\"component-port\",\"widget\":\"department-master\",\"port\":\"itemSelect\",\"direction\":\"output\"},\"to\":{\"kind\":\"global-action\",\"actionId\":\"department.selection.audit\",\"payloadExpr\":\"payload.item\",\"meta\":{\"label\":\"Audit department selection\"}},\"intent\":\"command-dispatch\",\"policy\":{\"distinct\":true,\"distinctBy\":\"payload.item.id\",\"missingValuePolicy\":\"skip\"},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"global-action\"]}},{\"id\":\"state.selection.departmentId->department-employees.queryContext\",\"from\":{\"kind\":\"state\",\"path\":\"selection.departmentId\"},\"to\":{\"kind\":\"component-port\",\"widget\":\"department-employees\",\"port\":\"queryContext\",\"direction\":\"input\"},\"intent\":\"state-read\",\"condition\":{\"!=\":[{\"var\":\"state.selection.departmentId\"},null]},\"transform\":{\"kind\":\"query-context\",\"id\":\"department-filter\",\"field\":\"departamentoId\"},\"policy\":{\"distinct\":true,\"distinctBy\":\"value\",\"missingValuePolicy\":\"skip\"},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"table-filter\"]}},{\"id\":\"state.selection.departmentId->department-payroll.queryContext\",\"from\":{\"kind\":\"state\",\"path\":\"selection.departmentId\"},\"to\":{\"kind\":\"component-port\",\"widget\":\"department-payroll\",\"port\":\"queryContext\",\"direction\":\"input\"},\"intent\":\"state-read\",\"condition\":{\"!=\":[{\"var\":\"state.selection.departmentId\"},null]},\"transform\":{\"kind\":\"query-context\",\"id\":\"department-payroll-filter\",\"field\":\"departamentoId\"},\"policy\":{\"distinct\":true,\"missingValuePolicy\":\"skip\"},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"chart-filter\"]}},{\"id\":\"state.selection.departmentName->department-summary.document\",\"from\":{\"kind\":\"state\",\"path\":\"selection.departmentName\"},\"to\":{\"kind\":\"component-port\",\"widget\":\"department-summary\",\"port\":\"document\",\"direction\":\"input\"},\"intent\":\"state-read\",\"condition\":{\"!!\":[{\"var\":\"state.selection.departmentName\"}]},\"transform\":{\"kind\":\"template\",\"id\":\"department-summary-document\",\"template\":{\"kind\":\"praxis.rich-content.document\",\"version\":\"1.0.0\",\"nodes\":[{\"type\":\"text\",\"text\":{\"concat\":[\"Departamento selecionado: \",{\"var\":\"value\"}]}}]}},\"metadata\":{\"source\":\"ui-composition-plan\",\"tags\":[\"master-detail\",\"rich-content\"]}}]}},\"compiledOutput\":{\"summary\":\"O compilador expande selectionSyncs e bindings em WidgetPageDefinition com page.widgets[].definition.id, page.widgets[].definition.inputs e page.composition.links version 1.0.0. O JSON persistido nao deve conter o plano intermediario nem connections legado.\",\"forbiddenLegacyPaths\":[\"page.options\",\"page.widgets[].layout\",\"page.connections\",\"connections\"]}}",
3812
3818
  "sourcePointer": "examples/ai-recipes/praxis-page-builder.ui-composition-plan.json",
3813
- "contentHash": "2daf850e63ae2f581e944afd15eded19c4978a1824f7beb9a37fa6f8af7beae3",
3819
+ "contentHash": "30f2d6b6b0b8fd552d4079e7ce6caf68d0d2c47151a9f604f5573e4ed0396252",
3814
3820
  "sourceKind": "component_definition",
3815
3821
  "sourceId": "praxis-page-builder",
3816
3822
  "corpusVersion": "1.0.0"
@@ -7,7 +7,7 @@ import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
7
7
  import * as i2$1 from '@angular/material/icon';
8
8
  import { MatIconModule } from '@angular/material/icon';
9
9
  import * as i2$2 from '@praxisui/core';
10
- import { PraxisIconDirective, BUILTIN_SHELL_PRESETS, PraxisIconButtonComponent, providePraxisI18n, PraxisI18nService, GLOBAL_ACTION_CATALOG, getGlobalActionCatalog, PRAXIS_GLOBAL_ACTION_CATALOG, SurfaceOpenActionEditorComponent, BUILTIN_PAGE_LAYOUT_PRESETS, BUILTIN_PAGE_THEME_PRESETS, SETTINGS_PANEL_DATA as SETTINGS_PANEL_DATA$1, deepMerge, generateId, ComponentMetadataRegistry, ObservabilityDashboardService, PraxisRuntimeComponentObservationRegistryService, domainKnowledgeTimelineToRichContentDocument, DomainRuleService, DomainKnowledgeService, SETTINGS_PANEL_BRIDGE, DynamicWidgetPageComponent, DYNAMIC_PAGE_SHELL_EDITOR } from '@praxisui/core';
10
+ import { PraxisIconDirective, BUILTIN_SHELL_PRESETS, PraxisIconButtonComponent, providePraxisI18n, PraxisI18nService, GLOBAL_ACTION_CATALOG, getGlobalActionCatalog, PRAXIS_GLOBAL_ACTION_CATALOG, SurfaceOpenActionEditorComponent, BUILTIN_PAGE_LAYOUT_PRESETS, BUILTIN_PAGE_THEME_PRESETS, SETTINGS_PANEL_DATA as SETTINGS_PANEL_DATA$1, NestedWidgetConfigAccessor, deepMerge, generateId, ComponentMetadataRegistry, ObservabilityDashboardService, PraxisRuntimeComponentObservationRegistryService, domainKnowledgeTimelineToRichContentDocument, DomainRuleService, DomainKnowledgeService, SETTINGS_PANEL_BRIDGE, DynamicWidgetPageComponent, DYNAMIC_PAGE_SHELL_EDITOR } from '@praxisui/core';
11
11
  import * as i3 from '@angular/material/tooltip';
12
12
  import { MatTooltipModule } from '@angular/material/tooltip';
13
13
  import * as i3$1 from '@angular/material/form-field';
@@ -900,6 +900,9 @@ class WidgetShellEditorComponent {
900
900
  isValid$ = this.validSubject.asObservable();
901
901
  isBusy$ = this.busySubject.asObservable();
902
902
  snapshot = '';
903
+ initialTitle;
904
+ initialSubtitle;
905
+ actionCopySources = new WeakMap();
903
906
  presetOptions = [...BUILTIN_PRESET_OPTIONS];
904
907
  presets = { ...BUILTIN_PRESET_MAP };
905
908
  availableActions = [];
@@ -971,6 +974,8 @@ class WidgetShellEditorComponent {
971
974
  this.applyShell(shell);
972
975
  }
973
976
  applyShell(shell) {
977
+ this.initialTitle = shell?.title;
978
+ this.initialSubtitle = shell?.subtitle;
974
979
  const showHeader = shell?.showHeader == null ? 'auto' : shell.showHeader ? 'true' : 'false';
975
980
  const window = shell?.windowActions || {};
976
981
  const appearance = shell?.appearance || {};
@@ -983,8 +988,8 @@ class WidgetShellEditorComponent {
983
988
  kind: shell?.kind === 'none' ? 'none' : 'dashboard-card',
984
989
  preset: shell?.preset || '',
985
990
  applyToAll: false,
986
- title: shell?.title || '',
987
- subtitle: shell?.subtitle || '',
991
+ title: this.editableText(shell?.title),
992
+ subtitle: this.editableText(shell?.subtitle),
988
993
  icon: shell?.icon || '',
989
994
  showHeader,
990
995
  collapsible: window.collapsible !== false,
@@ -1039,19 +1044,21 @@ class WidgetShellEditorComponent {
1039
1044
  this.availableActions = [...actions, ...commands];
1040
1045
  }
1041
1046
  createActionGroup(seed) {
1042
- return new FormGroup({
1047
+ const group = new FormGroup({
1043
1048
  id: new FormControl(seed?.id || '', { nonNullable: true, validators: [Validators.required] }),
1044
- label: new FormControl(seed?.label || '', { nonNullable: true }),
1049
+ label: new FormControl(this.editableText(seed?.label), { nonNullable: true }),
1045
1050
  icon: new FormControl(seed?.icon || '', { nonNullable: true }),
1046
1051
  variant: new FormControl(seed?.variant || 'text', { nonNullable: true }),
1047
1052
  placement: new FormControl(seed?.placement || 'header', { nonNullable: true }),
1048
1053
  emit: new FormControl(seed?.emit || '', { nonNullable: true }),
1049
- tooltip: new FormControl(seed?.tooltip || '', { nonNullable: true }),
1054
+ tooltip: new FormControl(this.editableText(seed?.tooltip), { nonNullable: true }),
1050
1055
  command: new FormControl(seed?.command || '', { nonNullable: true }),
1051
1056
  payload: new FormControl(seed?.payload != null ? JSON.stringify(seed.payload) : '', { nonNullable: true }),
1052
1057
  visible: new FormControl(seed?.visible !== false, { nonNullable: true }),
1053
1058
  disabled: new FormControl(!!seed?.disabled, { nonNullable: true }),
1054
1059
  });
1060
+ this.actionCopySources.set(group, { label: seed?.label, tooltip: seed?.tooltip });
1061
+ return group;
1055
1062
  }
1056
1063
  buildShell() {
1057
1064
  const raw = this.form.getRawValue();
@@ -1061,10 +1068,12 @@ class WidgetShellEditorComponent {
1061
1068
  const shell = {};
1062
1069
  if (raw.preset)
1063
1070
  shell.preset = raw.preset;
1064
- if (raw.title.trim())
1065
- shell.title = raw.title.trim();
1066
- if (raw.subtitle.trim())
1067
- shell.subtitle = raw.subtitle.trim();
1071
+ const title = this.preserveTextDescriptor(this.initialTitle, raw.title);
1072
+ const subtitle = this.preserveTextDescriptor(this.initialSubtitle, raw.subtitle);
1073
+ if (title)
1074
+ shell.title = title;
1075
+ if (subtitle)
1076
+ shell.subtitle = subtitle;
1068
1077
  if (raw.icon.trim())
1069
1078
  shell.icon = raw.icon.trim();
1070
1079
  if (raw.showHeader !== 'auto')
@@ -1074,8 +1083,8 @@ class WidgetShellEditorComponent {
1074
1083
  windowActions.collapsible = false;
1075
1084
  if (Object.keys(windowActions).length)
1076
1085
  shell.windowActions = windowActions;
1077
- const actions = (raw.actions || [])
1078
- .map((a) => this.normalizeAction(a))
1086
+ const actions = this.actions.controls
1087
+ .map((group) => this.normalizeAction(group.getRawValue(), this.actionCopySources.get(group)))
1079
1088
  .filter((a) => a.id);
1080
1089
  if (actions.length)
1081
1090
  shell.actions = actions;
@@ -1171,11 +1180,11 @@ class WidgetShellEditorComponent {
1171
1180
  return !!(raw.title || raw.subtitle || raw.icon);
1172
1181
  }
1173
1182
  previewTitle() {
1174
- const title = this.form.controls.title.value.trim();
1183
+ const title = this.editableText(this.form.controls.title.value).trim();
1175
1184
  return title || 'Titulo do card';
1176
1185
  }
1177
1186
  previewSubtitle() {
1178
- const subtitle = this.form.controls.subtitle.value.trim();
1187
+ const subtitle = this.editableText(this.form.controls.subtitle.value).trim();
1179
1188
  return subtitle || 'Subtitulo opcional';
1180
1189
  }
1181
1190
  buildPreviewAppearance() {
@@ -1219,21 +1228,23 @@ class WidgetShellEditorComponent {
1219
1228
  previewWindowActions() {
1220
1229
  return this.previewActions().filter((a) => (a.placement || 'header') === 'window');
1221
1230
  }
1222
- normalizeAction(raw) {
1231
+ normalizeAction(raw, source) {
1223
1232
  const action = { id: String(raw.id || '').trim() };
1224
1233
  const label = String(raw.label || '').trim();
1225
1234
  const icon = String(raw.icon || '').trim();
1226
1235
  const emit = String(raw.emit || '').trim();
1227
1236
  const tooltip = String(raw.tooltip || '').trim();
1228
1237
  const command = String(raw.command || '').trim();
1229
- if (label)
1230
- action.label = label;
1238
+ const normalizedLabel = this.preserveTextDescriptor(source?.label, label);
1239
+ const normalizedTooltip = this.preserveTextDescriptor(source?.tooltip, tooltip);
1240
+ if (normalizedLabel)
1241
+ action.label = normalizedLabel;
1231
1242
  if (icon)
1232
1243
  action.icon = icon;
1233
1244
  if (emit)
1234
1245
  action.emit = emit;
1235
- if (tooltip)
1236
- action.tooltip = tooltip;
1246
+ if (normalizedTooltip)
1247
+ action.tooltip = normalizedTooltip;
1237
1248
  if (command)
1238
1249
  action.command = command;
1239
1250
  if (raw.variant && raw.variant !== 'text')
@@ -1249,6 +1260,21 @@ class WidgetShellEditorComponent {
1249
1260
  action.payload = payload;
1250
1261
  return action;
1251
1262
  }
1263
+ editableText(value) {
1264
+ if (typeof value === 'string')
1265
+ return value;
1266
+ return value?.text || value?.key || '';
1267
+ }
1268
+ preserveTextDescriptor(source, editedValue) {
1269
+ const edited = String(editedValue ?? '').trim();
1270
+ if (!edited)
1271
+ return undefined;
1272
+ if (source && typeof source === 'object') {
1273
+ const authoredText = this.editableText(source).trim();
1274
+ return edited === authoredText ? { ...source } : edited;
1275
+ }
1276
+ return edited;
1277
+ }
1252
1278
  tryParsePayload(raw) {
1253
1279
  const value = String(raw || '').trim();
1254
1280
  if (!value)
@@ -1414,13 +1440,9 @@ class WidgetShellEditorComponent {
1414
1440
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: WidgetShellEditorComponent, deps: [{ token: SETTINGS_PANEL_DATA, optional: true }, { token: i2$2.ComponentMetadataRegistry, optional: true }], target: i0.ɵɵFactoryTarget.Component });
1415
1441
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: WidgetShellEditorComponent, isStandalone: true, selector: "praxis-widget-shell-editor", ngImport: i0, template: `
1416
1442
  <div class="shell-editor">
1417
- <div class="shell-head">
1418
- <div>
1419
- <div class="shell-title">Configurar Card</div>
1420
- <div class="shell-subtitle">Defina título, ícone e ações do cabeçalho.</div>
1421
- </div>
1422
- <button mat-stroked-button type="button" (click)="reset()">Recarregar</button>
1423
- </div>
1443
+ <p class="shell-intro">
1444
+ Defina a identidade, a aparência e as ações do card. Use a prévia para conferir o resultado antes de salvar.
1445
+ </p>
1424
1446
 
1425
1447
  <div class="shell-section">Geral</div>
1426
1448
  <div class="shell-grid">
@@ -1461,7 +1483,7 @@ class WidgetShellEditorComponent {
1461
1483
  <mat-label>Título</mat-label>
1462
1484
  <input matInput [formControl]="form.controls.title" />
1463
1485
  </mat-form-field>
1464
- <mat-form-field appearance="outline">
1486
+ <mat-form-field appearance="outline" class="shell-field--wide">
1465
1487
  <mat-label>Subtítulo</mat-label>
1466
1488
  <input matInput [formControl]="form.controls.subtitle" />
1467
1489
  </mat-form-field>
@@ -1809,7 +1831,7 @@ class WidgetShellEditorComponent {
1809
1831
  }
1810
1832
 
1811
1833
  </div>
1812
- `, isInline: true, styles: [".shell-editor{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.shell-head{display:flex;align-items:center;gap:12px}.shell-title{font-weight:600}.shell-subtitle{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-section{font-weight:600;margin-top:8px}.shell-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.shell-flags{display:flex;gap:16px;align-items:center}.shell-actions-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.shell-available{display:grid;gap:12px}.shell-available-toolbar{display:grid;gap:12px;grid-template-columns:minmax(200px,1fr)}.shell-available-filters{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.shell-available-list{display:grid;gap:10px}.shell-available-item{display:grid;gap:8px;grid-template-columns:1fr auto;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;background:var(--md-sys-color-surface-container-low)}.shell-available-title{display:flex;align-items:center;gap:10px}.shell-available-text{display:grid;gap:4px}.shell-available-label{font-weight:600}.shell-available-meta{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-available-payload{font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-warning{font-size:11px;color:var(--md-sys-color-error)}.shell-available-actions{display:flex;align-items:flex-start;justify-content:flex-end}.shell-badge{padding:2px 6px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);font-size:10px}.shell-code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.shell-actions{display:grid;gap:12px}.shell-action{border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;display:grid;gap:8px}.shell-action-row{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.shell-action-actions{display:flex;justify-content:flex-end}.shell-action-hint,.shell-empty{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-hint{align-self:center;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-quick-inputs{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));align-items:end}.shell-input-path{font-size:12px;color:var(--md-sys-color-on-surface-variant);align-self:center}.shell-editor .mat-mdc-form-field{width:100%}.shell-preview{border:1px dashed var(--md-sys-color-outline-variant);border-radius:12px;padding:12px}.shell-preview-card{background:var(--pdx-shell-card-bg, var(--md-sys-color-surface));border:1px solid var(--pdx-shell-card-border, var(--md-sys-color-outline-variant));border-radius:var(--pdx-shell-card-radius, 14px);box-shadow:var(--pdx-shell-card-shadow, var(--mat-elevation-level2));overflow:hidden}.shell-preview-card.no-shell{background:transparent;border:none;box-shadow:none}.shell-preview-card.expanded{box-shadow:var(--pdx-shell-card-shadow, 0 16px 34px rgba(0,0,0,.32));transform:scale(1.01)}.shell-preview-header{display:flex;align-items:center;gap:12px;padding:10px 12px 8px;border-bottom:1px solid var(--pdx-shell-header-border, var(--md-sys-color-outline-variant));background:var(--pdx-shell-header-bg, var(--md-sys-color-surface-container))}.shell-preview-title{display:flex;align-items:center;gap:10px;min-width:0;flex:1;color:var(--pdx-shell-title-color, inherit)}.shell-preview-title mat-icon{color:var(--pdx-shell-icon-color, currentColor)}.shell-preview-text{min-width:0}.shell-preview-title-text{font-weight:var(--pdx-shell-title-weight, 600);font-size:var(--pdx-shell-title-size, 14px);line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-subtitle{font-size:var(--pdx-shell-subtitle-size, 12px);opacity:.75;color:var(--pdx-shell-subtitle-color, currentColor);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-actions{display:flex;align-items:center;gap:6px}.pdx-preview-text{padding:0 8px}.pdx-preview-outlined{border:1px solid var(--md-sys-color-outline-variant);border-radius:999px;padding:0 10px}.pdx-preview-label{font-size:12px;font-weight:500}.shell-preview-body{padding:var(--pdx-shell-body-padding, 10px 12px 12px 12px);background:var(--pdx-shell-body-bg, transparent);color:var(--pdx-shell-body-color, inherit);font-size:12px}.shell-preview-body.hidden{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i5$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i6.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1834
+ `, isInline: true, styles: [".shell-editor{display:grid;gap:20px;padding:12px 4px 20px}.shell-intro{margin:0;max-width:68ch;color:var(--md-sys-color-on-surface-variant);font-size:14px;line-height:1.5}.shell-section{margin:4px 0 -8px;color:var(--md-sys-color-on-surface);font-size:14px;font-weight:650;line-height:1.4}.shell-grid{display:grid;gap:12px 16px;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}.shell-field--wide{grid-column:1 / -1}.shell-flags{display:flex;gap:16px;align-items:center}.shell-actions-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.shell-available{display:grid;gap:12px}.shell-available-toolbar{display:grid;gap:12px;grid-template-columns:minmax(200px,1fr)}.shell-available-filters{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.shell-available-list{display:grid;gap:10px}.shell-available-item{display:grid;gap:8px;grid-template-columns:1fr auto;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;background:var(--md-sys-color-surface-container-low)}.shell-available-title{display:flex;align-items:center;gap:10px}.shell-available-text{display:grid;gap:4px}.shell-available-label{font-weight:600}.shell-available-meta{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-available-payload{font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-warning{font-size:11px;color:var(--md-sys-color-error)}.shell-available-actions{display:flex;align-items:flex-start;justify-content:flex-end}.shell-badge{padding:2px 6px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);font-size:10px}.shell-code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.shell-actions{display:grid;gap:12px}.shell-action{border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;display:grid;gap:8px}.shell-action-row{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.shell-action-actions{display:flex;justify-content:flex-end}.shell-action-hint,.shell-empty{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-hint{align-self:center;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-quick-inputs{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));align-items:end}.shell-input-path{font-size:12px;color:var(--md-sys-color-on-surface-variant);align-self:center}.shell-editor .mat-mdc-form-field{width:100%}.shell-preview{border:1px dashed var(--md-sys-color-outline-variant);border-radius:12px;padding:12px}.shell-preview-card{background:var(--pdx-shell-card-bg, var(--md-sys-color-surface));border:1px solid var(--pdx-shell-card-border, var(--md-sys-color-outline-variant));border-radius:var(--pdx-shell-card-radius, 14px);box-shadow:var(--pdx-shell-card-shadow, var(--mat-elevation-level2));overflow:hidden}.shell-preview-card.no-shell{background:transparent;border:none;box-shadow:none}.shell-preview-card.expanded{box-shadow:var(--pdx-shell-card-shadow, 0 16px 34px rgba(0,0,0,.32));transform:scale(1.01)}.shell-preview-header{display:flex;align-items:center;gap:12px;padding:10px 12px 8px;border-bottom:1px solid var(--pdx-shell-header-border, var(--md-sys-color-outline-variant));background:var(--pdx-shell-header-bg, var(--md-sys-color-surface-container))}.shell-preview-title{display:flex;align-items:center;gap:10px;min-width:0;flex:1;color:var(--pdx-shell-title-color, inherit)}.shell-preview-title mat-icon{color:var(--pdx-shell-icon-color, currentColor)}.shell-preview-text{min-width:0}.shell-preview-title-text{font-weight:var(--pdx-shell-title-weight, 600);font-size:var(--pdx-shell-title-size, 14px);line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-subtitle{font-size:var(--pdx-shell-subtitle-size, 12px);opacity:.75;color:var(--pdx-shell-subtitle-color, currentColor);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-actions{display:flex;align-items:center;gap:6px}.pdx-preview-text{padding:0 8px}.pdx-preview-outlined{border:1px solid var(--md-sys-color-outline-variant);border-radius:999px;padding:0 10px}.pdx-preview-label{font-size:12px;font-weight:500}.shell-preview-body{padding:var(--pdx-shell-body-padding, 10px 12px 12px 12px);background:var(--pdx-shell-body-bg, transparent);color:var(--pdx-shell-body-color, inherit);font-size:12px}.shell-preview-body.hidden{display:none}@media(max-width:640px){.shell-editor{padding-inline:0}.shell-grid{grid-template-columns:minmax(0,1fr)}.shell-field--wide{grid-column:auto}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i5$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i6.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1813
1835
  }
1814
1836
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: WidgetShellEditorComponent, decorators: [{
1815
1837
  type: Component,
@@ -1828,13 +1850,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1828
1850
  PraxisIconDirective,
1829
1851
  ], template: `
1830
1852
  <div class="shell-editor">
1831
- <div class="shell-head">
1832
- <div>
1833
- <div class="shell-title">Configurar Card</div>
1834
- <div class="shell-subtitle">Defina título, ícone e ações do cabeçalho.</div>
1835
- </div>
1836
- <button mat-stroked-button type="button" (click)="reset()">Recarregar</button>
1837
- </div>
1853
+ <p class="shell-intro">
1854
+ Defina a identidade, a aparência e as ações do card. Use a prévia para conferir o resultado antes de salvar.
1855
+ </p>
1838
1856
 
1839
1857
  <div class="shell-section">Geral</div>
1840
1858
  <div class="shell-grid">
@@ -1875,7 +1893,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1875
1893
  <mat-label>Título</mat-label>
1876
1894
  <input matInput [formControl]="form.controls.title" />
1877
1895
  </mat-form-field>
1878
- <mat-form-field appearance="outline">
1896
+ <mat-form-field appearance="outline" class="shell-field--wide">
1879
1897
  <mat-label>Subtítulo</mat-label>
1880
1898
  <input matInput [formControl]="form.controls.subtitle" />
1881
1899
  </mat-form-field>
@@ -2223,7 +2241,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2223
2241
  }
2224
2242
 
2225
2243
  </div>
2226
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".shell-editor{display:grid;gap:12px;padding:16px;background:var(--md-sys-color-surface-container-lowest, var(--md-sys-color-surface));border:1px solid var(--md-sys-color-outline-variant);border-radius:12px}.shell-head{display:flex;align-items:center;gap:12px}.shell-title{font-weight:600}.shell-subtitle{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-section{font-weight:600;margin-top:8px}.shell-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.shell-flags{display:flex;gap:16px;align-items:center}.shell-actions-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.shell-available{display:grid;gap:12px}.shell-available-toolbar{display:grid;gap:12px;grid-template-columns:minmax(200px,1fr)}.shell-available-filters{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.shell-available-list{display:grid;gap:10px}.shell-available-item{display:grid;gap:8px;grid-template-columns:1fr auto;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;background:var(--md-sys-color-surface-container-low)}.shell-available-title{display:flex;align-items:center;gap:10px}.shell-available-text{display:grid;gap:4px}.shell-available-label{font-weight:600}.shell-available-meta{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-available-payload{font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-warning{font-size:11px;color:var(--md-sys-color-error)}.shell-available-actions{display:flex;align-items:flex-start;justify-content:flex-end}.shell-badge{padding:2px 6px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);font-size:10px}.shell-code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.shell-actions{display:grid;gap:12px}.shell-action{border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;display:grid;gap:8px}.shell-action-row{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.shell-action-actions{display:flex;justify-content:flex-end}.shell-action-hint,.shell-empty{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-hint{align-self:center;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-quick-inputs{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));align-items:end}.shell-input-path{font-size:12px;color:var(--md-sys-color-on-surface-variant);align-self:center}.shell-editor .mat-mdc-form-field{width:100%}.shell-preview{border:1px dashed var(--md-sys-color-outline-variant);border-radius:12px;padding:12px}.shell-preview-card{background:var(--pdx-shell-card-bg, var(--md-sys-color-surface));border:1px solid var(--pdx-shell-card-border, var(--md-sys-color-outline-variant));border-radius:var(--pdx-shell-card-radius, 14px);box-shadow:var(--pdx-shell-card-shadow, var(--mat-elevation-level2));overflow:hidden}.shell-preview-card.no-shell{background:transparent;border:none;box-shadow:none}.shell-preview-card.expanded{box-shadow:var(--pdx-shell-card-shadow, 0 16px 34px rgba(0,0,0,.32));transform:scale(1.01)}.shell-preview-header{display:flex;align-items:center;gap:12px;padding:10px 12px 8px;border-bottom:1px solid var(--pdx-shell-header-border, var(--md-sys-color-outline-variant));background:var(--pdx-shell-header-bg, var(--md-sys-color-surface-container))}.shell-preview-title{display:flex;align-items:center;gap:10px;min-width:0;flex:1;color:var(--pdx-shell-title-color, inherit)}.shell-preview-title mat-icon{color:var(--pdx-shell-icon-color, currentColor)}.shell-preview-text{min-width:0}.shell-preview-title-text{font-weight:var(--pdx-shell-title-weight, 600);font-size:var(--pdx-shell-title-size, 14px);line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-subtitle{font-size:var(--pdx-shell-subtitle-size, 12px);opacity:.75;color:var(--pdx-shell-subtitle-color, currentColor);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-actions{display:flex;align-items:center;gap:6px}.pdx-preview-text{padding:0 8px}.pdx-preview-outlined{border:1px solid var(--md-sys-color-outline-variant);border-radius:999px;padding:0 10px}.pdx-preview-label{font-size:12px;font-weight:500}.shell-preview-body{padding:var(--pdx-shell-body-padding, 10px 12px 12px 12px);background:var(--pdx-shell-body-bg, transparent);color:var(--pdx-shell-body-color, inherit);font-size:12px}.shell-preview-body.hidden{display:none}\n"] }]
2244
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".shell-editor{display:grid;gap:20px;padding:12px 4px 20px}.shell-intro{margin:0;max-width:68ch;color:var(--md-sys-color-on-surface-variant);font-size:14px;line-height:1.5}.shell-section{margin:4px 0 -8px;color:var(--md-sys-color-on-surface);font-size:14px;font-weight:650;line-height:1.4}.shell-grid{display:grid;gap:12px 16px;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}.shell-field--wide{grid-column:1 / -1}.shell-flags{display:flex;gap:16px;align-items:center}.shell-actions-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.shell-available{display:grid;gap:12px}.shell-available-toolbar{display:grid;gap:12px;grid-template-columns:minmax(200px,1fr)}.shell-available-filters{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.shell-available-list{display:grid;gap:10px}.shell-available-item{display:grid;gap:8px;grid-template-columns:1fr auto;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;background:var(--md-sys-color-surface-container-low)}.shell-available-title{display:flex;align-items:center;gap:10px}.shell-available-text{display:grid;gap:4px}.shell-available-label{font-weight:600}.shell-available-meta{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-available-payload{font-size:11px;color:var(--md-sys-color-on-surface-variant)}.shell-available-warning{font-size:11px;color:var(--md-sys-color-error)}.shell-available-actions{display:flex;align-items:flex-start;justify-content:flex-end}.shell-badge{padding:2px 6px;border-radius:999px;border:1px solid var(--md-sys-color-outline-variant);font-size:10px}.shell-code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.shell-actions{display:grid;gap:12px}.shell-action{border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;padding:12px;display:grid;gap:8px}.shell-action-row{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.shell-action-actions{display:flex;justify-content:flex-end}.shell-action-hint,.shell-empty{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-hint{align-self:center;font-size:12px;color:var(--md-sys-color-on-surface-variant)}.shell-quick-inputs{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));align-items:end}.shell-input-path{font-size:12px;color:var(--md-sys-color-on-surface-variant);align-self:center}.shell-editor .mat-mdc-form-field{width:100%}.shell-preview{border:1px dashed var(--md-sys-color-outline-variant);border-radius:12px;padding:12px}.shell-preview-card{background:var(--pdx-shell-card-bg, var(--md-sys-color-surface));border:1px solid var(--pdx-shell-card-border, var(--md-sys-color-outline-variant));border-radius:var(--pdx-shell-card-radius, 14px);box-shadow:var(--pdx-shell-card-shadow, var(--mat-elevation-level2));overflow:hidden}.shell-preview-card.no-shell{background:transparent;border:none;box-shadow:none}.shell-preview-card.expanded{box-shadow:var(--pdx-shell-card-shadow, 0 16px 34px rgba(0,0,0,.32));transform:scale(1.01)}.shell-preview-header{display:flex;align-items:center;gap:12px;padding:10px 12px 8px;border-bottom:1px solid var(--pdx-shell-header-border, var(--md-sys-color-outline-variant));background:var(--pdx-shell-header-bg, var(--md-sys-color-surface-container))}.shell-preview-title{display:flex;align-items:center;gap:10px;min-width:0;flex:1;color:var(--pdx-shell-title-color, inherit)}.shell-preview-title mat-icon{color:var(--pdx-shell-icon-color, currentColor)}.shell-preview-text{min-width:0}.shell-preview-title-text{font-weight:var(--pdx-shell-title-weight, 600);font-size:var(--pdx-shell-title-size, 14px);line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-subtitle{font-size:var(--pdx-shell-subtitle-size, 12px);opacity:.75;color:var(--pdx-shell-subtitle-color, currentColor);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.shell-preview-actions{display:flex;align-items:center;gap:6px}.pdx-preview-text{padding:0 8px}.pdx-preview-outlined{border:1px solid var(--md-sys-color-outline-variant);border-radius:999px;padding:0 10px}.pdx-preview-label{font-size:12px;font-weight:500}.shell-preview-body{padding:var(--pdx-shell-body-padding, 10px 12px 12px 12px);background:var(--pdx-shell-body-bg, transparent);color:var(--pdx-shell-body-color, inherit);font-size:12px}.shell-preview-body.hidden{display:none}@media(max-width:640px){.shell-editor{padding-inline:0}.shell-grid{grid-template-columns:minmax(0,1fr)}.shell-field--wide{grid-column:auto}}\n"] }]
2227
2245
  }], ctorParameters: () => [{ type: undefined, decorators: [{
2228
2246
  type: Optional
2229
2247
  }, {
@@ -2371,6 +2389,8 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
2371
2389
  'praxis.pageBuilder.editor.layout.gap': 'Gap (px)',
2372
2390
  'praxis.pageBuilder.editor.layout.rowUnit': 'Row unit',
2373
2391
  'praxis.pageBuilder.editor.layout.autoRows': 'Auto rows',
2392
+ 'praxis.pageBuilder.editor.layout.autoRowsFixed': 'Fixed height',
2393
+ 'praxis.pageBuilder.editor.layout.autoRowsContent': 'Fit content',
2374
2394
  'praxis.pageBuilder.editor.layout.presetSection': 'Layout Preset',
2375
2395
  'praxis.pageBuilder.editor.layout.presetHelp': 'Use presets to accelerate a consistent organization and refine only what is needed afterwards.',
2376
2396
  'praxis.pageBuilder.editor.layout.layoutPreset': 'Layout preset',
@@ -2973,37 +2993,39 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
2973
2993
  'praxis.pageBuilder.editor.status.pending': 'Alterações pendentes',
2974
2994
  'praxis.pageBuilder.editor.status.clean': 'Sem alterações',
2975
2995
  'praxis.pageBuilder.editor.status.validationIssue': 'Revisar: {{issue}}',
2976
- 'praxis.pageBuilder.editor.summary.layout': 'Layout: {{label}}',
2996
+ 'praxis.pageBuilder.editor.summary.layout': 'Organização: {{label}}',
2977
2997
  'praxis.pageBuilder.editor.summary.theme': 'Tema: {{label}}',
2978
2998
  'praxis.pageBuilder.editor.summary.shell': 'Shell: {{label}}',
2979
- 'praxis.pageBuilder.editor.summary.canvasColumns': 'Canvas: {{count}} colunas',
2980
- 'praxis.pageBuilder.editor.fallback.manual': 'manual',
2999
+ 'praxis.pageBuilder.editor.summary.canvasColumns': 'Grade: {{count}} colunas',
3000
+ 'praxis.pageBuilder.editor.fallback.manual': 'Personalizada',
2981
3001
  'praxis.pageBuilder.editor.fallback.default': 'padrão',
2982
3002
  'praxis.pageBuilder.editor.fallback.neutral': 'neutra',
2983
3003
  'praxis.pageBuilder.editor.identity.route': 'Rota',
2984
3004
  'praxis.pageBuilder.editor.identity.app': 'App',
2985
3005
  'praxis.pageBuilder.editor.identity.tenant': 'Tenant',
2986
3006
  'praxis.pageBuilder.editor.identity.user': 'Usuário',
2987
- 'praxis.pageBuilder.editor.tabs.layout': 'Grade (Grid)',
2988
- 'praxis.pageBuilder.editor.tabs.grouping': 'Agrupamento (Grouping)',
2989
- 'praxis.pageBuilder.editor.tabs.devices': 'Layouts por Dispositivo',
3007
+ 'praxis.pageBuilder.editor.tabs.layout': 'Grade',
3008
+ 'praxis.pageBuilder.editor.tabs.grouping': 'Agrupamento',
3009
+ 'praxis.pageBuilder.editor.tabs.devices': 'Responsividade',
2990
3010
  'praxis.pageBuilder.editor.tabs.context': 'Contexto',
2991
3011
  'praxis.pageBuilder.editor.tabs.state': 'Estado da Página',
2992
- 'praxis.pageBuilder.editor.layout.section': 'Grade (Grid)',
3012
+ 'praxis.pageBuilder.editor.layout.section': 'Grade',
2993
3013
  'praxis.pageBuilder.editor.layout.help': 'Ajuste a malha base antes de entrar em agrupamento e variações responsivas.',
2994
3014
  'praxis.pageBuilder.editor.layout.cols': 'Colunas',
2995
3015
  'praxis.pageBuilder.editor.layout.rowHeight': 'Altura da linha (px)',
2996
- 'praxis.pageBuilder.editor.layout.gap': 'Espaçamento (Gap - px)',
3016
+ 'praxis.pageBuilder.editor.layout.gap': 'Espaçamento entre blocos',
2997
3017
  'praxis.pageBuilder.editor.layout.rowUnit': 'Unidade da linha',
2998
3018
  'praxis.pageBuilder.editor.layout.autoRows': 'Linhas automáticas',
2999
- 'praxis.pageBuilder.editor.layout.presetSection': 'Predefinição de Layout (Preset)',
3019
+ 'praxis.pageBuilder.editor.layout.autoRowsFixed': 'Altura fixa',
3020
+ 'praxis.pageBuilder.editor.layout.autoRowsContent': 'Ajustar ao conteúdo',
3021
+ 'praxis.pageBuilder.editor.layout.presetSection': 'Modelos de layout',
3000
3022
  'praxis.pageBuilder.editor.layout.presetHelp': 'Use predefinições para acelerar uma organização consistente e depois refine apenas o necessário.',
3001
3023
  'praxis.pageBuilder.editor.layout.layoutPreset': 'Predefinição de layout',
3002
3024
  'praxis.pageBuilder.editor.layout.none': 'Nenhum',
3003
3025
  'praxis.pageBuilder.editor.layout.themePreset': 'Predefinição de tema',
3004
3026
  'praxis.pageBuilder.editor.layout.layoutDefault': 'Padrão do layout',
3005
3027
  'praxis.pageBuilder.editor.layout.shellPreset': 'Predefinição visual da shell',
3006
- 'praxis.pageBuilder.editor.grouping.section': 'Agrupamento (Grouping)',
3028
+ 'praxis.pageBuilder.editor.grouping.section': 'Agrupamento',
3007
3029
  'praxis.pageBuilder.editor.grouping.title': 'Agrupamento',
3008
3030
  'praxis.pageBuilder.editor.grouping.add': 'Adicionar grupo',
3009
3031
  'praxis.pageBuilder.editor.grouping.help': 'Organiza a página em seções, abas (tabs), hero e trilhos (rail) com intenção estrutural.',
@@ -3019,8 +3041,8 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
3019
3041
  'praxis.pageBuilder.editor.grouping.side': 'Lado (Side)',
3020
3042
  'praxis.pageBuilder.editor.grouping.tabsJson': 'Abas (Tabs - JSON)',
3021
3043
  'praxis.pageBuilder.editor.grouping.widgetKeysCsv': 'Chaves de widgets (vírgula)',
3022
- 'praxis.pageBuilder.editor.devices.section': 'Layouts por Dispositivo',
3023
- 'praxis.pageBuilder.editor.devices.title': 'Layouts por Dispositivo',
3044
+ 'praxis.pageBuilder.editor.devices.section': 'Responsividade',
3045
+ 'praxis.pageBuilder.editor.devices.title': 'Configurações por dispositivo',
3024
3046
  'praxis.pageBuilder.editor.devices.help': 'Sobrescritas (overrides) por dispositivo para layout, agrupamento e widgets com campos separados por intenção.',
3025
3047
  'praxis.pageBuilder.editor.devices.desktop': 'Desktop',
3026
3048
  'praxis.pageBuilder.editor.devices.tablet': 'Tablet',
@@ -3028,7 +3050,7 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
3028
3050
  'praxis.pageBuilder.editor.devices.orientation': 'Orientação',
3029
3051
  'praxis.pageBuilder.editor.devices.orientationDefault': 'Padrão',
3030
3052
  'praxis.pageBuilder.editor.devices.columns': 'Colunas',
3031
- 'praxis.pageBuilder.editor.devices.gap': 'Espaçamento (Gap)',
3053
+ 'praxis.pageBuilder.editor.devices.gap': 'Espaçamento entre blocos',
3032
3054
  'praxis.pageBuilder.editor.devices.breakpointsJson': 'Pontos de quebra (Breakpoints - JSON)',
3033
3055
  'praxis.pageBuilder.editor.devices.groupingOverridesJson': 'Sobrescritas de agrupamento (JSON)',
3034
3056
  'praxis.pageBuilder.editor.devices.widgetOverridesJson': 'Sobrescritas de widgets (JSON)',
@@ -3658,7 +3680,7 @@ function deriveConnectionEditorGraph(page, options = {}) {
3658
3680
  }
3659
3681
  return {
3660
3682
  id: widgetId,
3661
- title: widget.shell?.title || widgetId,
3683
+ title: editableText(widget.shell?.title) || widgetId,
3662
3684
  subtitle: componentType,
3663
3685
  icon: widget.shell?.icon ?? iconForComponentType(componentType),
3664
3686
  x: center.x + Math.cos(angle) * radiusX,
@@ -3755,6 +3777,11 @@ function deriveConnectionEditorGraph(page, options = {}) {
3755
3777
  diagnostics: connections.flatMap((connection) => connection.diagnostics),
3756
3778
  };
3757
3779
  }
3780
+ function editableText(value) {
3781
+ if (typeof value === 'string')
3782
+ return value;
3783
+ return value?.text ?? value?.key ?? '';
3784
+ }
3758
3785
  function feedbackDiagnosticsForLinks(links) {
3759
3786
  const edges = links.map((link) => ({
3760
3787
  link,
@@ -10439,14 +10466,14 @@ class DynamicPageConfigEditorComponent {
10439
10466
  <span>{{ canvasSummaryLabel }}</span>
10440
10467
  </div>
10441
10468
  <mat-tab-group [selectedIndex]="selectedTabIndex" (selectedIndexChange)="onSelectedTabIndexChange($event)">
10442
- <mat-tab [label]="tx('editor.tabs.layout', 'Layout')"><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.cols', 'Columns') }}</mat-label><input matInput type="number" min="1" [formControl]="canvasColumnsControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]="canvasRowUnitControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.gap', 'Gap') }}</mat-label><input matInput [formControl]="canvasGapControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]="canvasAutoRowsControl"><mat-option value="fixed">fixed</mat-option><mat-option value="content">content</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.layoutPreset', 'Layout preset') }}</mat-label><mat-select [formControl]="layoutPresetControl"><mat-option value="">{{ tx('editor.layout.none', 'None') }}</mat-option>@for (preset of layoutPresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.themePreset', 'Theme preset') }}</mat-label><mat-select [formControl]="themePresetControl"><mat-option value="">{{ tx('editor.layout.layoutDefault', 'Layout default') }}</mat-option>@for (preset of themePresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.shellPreset', 'Shell visual preset') }}</mat-label><mat-select [formControl]="shellPresetControl"><mat-option value="">{{ tx('editor.layout.none', 'None') }}</mat-option>@for (preset of shellPresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field></div>@if (selectedLayoutPresetEntry; as preset) {<div class="preset-preview"><div class="preset-preview__head"><div><span class="eyebrow">{{ tx('editor.preview.layoutEyebrow', 'Layout template') }} · {{ preset.category }}</span><strong>{{ preset.label }}</strong></div><span>{{ layoutPresetSummary(preset) }}</span></div><p>{{ preset.description || tx('editor.preview.layoutFallbackDescription', 'Page organization preset.') }}</p><div class="preset-preview__chips"><span>{{ slotsSummary(preset) }}</span><span>{{ requiredSlotsSummary(preset) }}</span><span>{{ devicePolicySummary(preset) }}</span></div>@if (recommendedThemePresetEntry; as theme) {<div class="preset-preview__recommendation"><div><span class="eyebrow">{{ tx('editor.preview.inheritedTheme', 'Inherited theme') }}</span><strong>{{ theme.label }}</strong><span>{{ inheritedThemeHelp(theme) }}</span></div><button mat-stroked-button type="button" [disabled]="themePresetControl.value === theme.id" (click)="pinRecommendedThemePreset()">{{ tx('editor.preview.pinTheme', 'Pin theme') }}</button></div>}</div>} @if (selectedThemePresetEntry; as theme) {<div class="preset-preview preset-preview--theme"><div class="preset-preview__head"><div><span class="eyebrow">{{ tx('editor.preview.explicitTheme', 'Explicit theme') }}</span><strong>{{ theme.label }}</strong></div><span>{{ theme.density }} · {{ theme.motion }}</span></div><p>{{ theme.description || tx('editor.preview.themeFallbackDescription', 'Page visual preset.') }}</p><div class="preset-preview__chips"><span>{{ tx('editor.preview.shellChip', 'Shell: {{value}}').replace('{{value}}', theme.shellPreset) }}</span><span>{{ tx('editor.preview.chartsChip', 'Charts: {{value}}').replace('{{value}}', theme.chartThemePreset) }}</span></div></div>}</mat-tab>
10469
+ <mat-tab [label]="tx('editor.tabs.layout', 'Layout')"><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.cols', 'Columns') }}</mat-label><input matInput type="number" min="1" [formControl]="canvasColumnsControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]="canvasRowUnitControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.gap', 'Gap') }}</mat-label><input matInput [formControl]="canvasGapControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]="canvasAutoRowsControl"><mat-option value="fixed">{{ tx('editor.layout.autoRowsFixed', 'Fixed height') }}</mat-option><mat-option value="content">{{ tx('editor.layout.autoRowsContent', 'Fit content') }}</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.layoutPreset', 'Layout preset') }}</mat-label><mat-select [formControl]="layoutPresetControl"><mat-option value="">{{ tx('editor.layout.none', 'None') }}</mat-option>@for (preset of layoutPresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.themePreset', 'Theme preset') }}</mat-label><mat-select [formControl]="themePresetControl"><mat-option value="">{{ tx('editor.layout.layoutDefault', 'Layout default') }}</mat-option>@for (preset of themePresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.shellPreset', 'Shell visual preset') }}</mat-label><mat-select [formControl]="shellPresetControl"><mat-option value="">{{ tx('editor.layout.none', 'None') }}</mat-option>@for (preset of shellPresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field></div>@if (selectedLayoutPresetEntry; as preset) {<div class="preset-preview"><div class="preset-preview__head"><div><span class="eyebrow">{{ tx('editor.preview.layoutEyebrow', 'Layout template') }} · {{ preset.category }}</span><strong>{{ preset.label }}</strong></div><span>{{ layoutPresetSummary(preset) }}</span></div><p>{{ preset.description || tx('editor.preview.layoutFallbackDescription', 'Page organization preset.') }}</p><div class="preset-preview__chips"><span>{{ slotsSummary(preset) }}</span><span>{{ requiredSlotsSummary(preset) }}</span><span>{{ devicePolicySummary(preset) }}</span></div>@if (recommendedThemePresetEntry; as theme) {<div class="preset-preview__recommendation"><div><span class="eyebrow">{{ tx('editor.preview.inheritedTheme', 'Inherited theme') }}</span><strong>{{ theme.label }}</strong><span>{{ inheritedThemeHelp(theme) }}</span></div><button mat-stroked-button type="button" [disabled]="themePresetControl.value === theme.id" (click)="pinRecommendedThemePreset()">{{ tx('editor.preview.pinTheme', 'Pin theme') }}</button></div>}</div>} @if (selectedThemePresetEntry; as theme) {<div class="preset-preview preset-preview--theme"><div class="preset-preview__head"><div><span class="eyebrow">{{ tx('editor.preview.explicitTheme', 'Explicit theme') }}</span><strong>{{ theme.label }}</strong></div><span>{{ theme.density }} · {{ theme.motion }}</span></div><p>{{ theme.description || tx('editor.preview.themeFallbackDescription', 'Page visual preset.') }}</p><div class="preset-preview__chips"><span>{{ tx('editor.preview.shellChip', 'Shell: {{value}}').replace('{{value}}', theme.shellPreset) }}</span><span>{{ tx('editor.preview.chartsChip', 'Charts: {{value}}').replace('{{value}}', theme.chartThemePreset) }}</span></div></div>}</mat-tab>
10443
10470
  <mat-tab [label]="tx('editor.tabs.grouping', 'Grouping')"><div class="panel"><div class="head"><span>{{ tx('editor.grouping.title', 'Grouping') }}</span><button mat-stroked-button type="button" (click)="addGroupingNode()"><mat-icon>add</mat-icon>{{ tx('editor.grouping.add', 'Add group') }}</button></div>@for (group of groupingNodes.controls; track $index) {<div class="card" [formGroup]="group"><div class="head"><span>{{ groupLabel($index) }}</span><button praxisIconButton="delete" size="compact" type="button" [attr.aria-label]="tx('editor.grouping.removeAria', 'Remove group')" (click)="removeGroupingNode($index)"></button></div><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.kind', 'Kind') }}</mat-label><mat-select formControlName="kind"><mat-option value="section">section</mat-option><mat-option value="tabs">tabs</mat-option><mat-option value="hero">hero</mat-option><mat-option value="rail">rail</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.id', 'Id') }}</mat-label><input matInput formControlName="id" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.label', 'Label') }}</mat-label><input matInput formControlName="label" /></mat-form-field>@if (group.controls.kind.value === 'section') {<mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.layout', 'Layout') }}</mat-label><mat-select formControlName="layout"><mat-option value="">{{ tx('editor.grouping.layoutDefault', 'Default') }}</mat-option><mat-option value="stack">stack</mat-option><mat-option value="grid">grid</mat-option><mat-option value="row">row</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'hero') {<mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.emphasis', 'Emphasis') }}</mat-label><mat-select formControlName="emphasis"><mat-option value="">{{ tx('editor.grouping.layoutDefault', 'Default') }}</mat-option><mat-option value="high">high</mat-option><mat-option value="medium">medium</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'rail') {<mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.side', 'Side') }}</mat-label><mat-select formControlName="side"><mat-option value="left">left</mat-option><mat-option value="right">right</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'tabs') {<mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.grouping.tabsJson', 'Tabs (JSON)') }}</mat-label><textarea matInput rows="4" formControlName="tabsJson"></textarea></mat-form-field>} @else {<mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.grouping.widgetKeysCsv', 'Widget keys (comma-separated)') }}</mat-label><input matInput formControlName="widgetKeysCsv" /></mat-form-field>}</div></div>}</div></mat-tab>
10444
- <mat-tab [label]="tx('editor.tabs.devices', 'Device Layouts')"><div class="panel">@for (device of deviceKeys; track device) {<div class="card"><div>{{ deviceLabels[device] }}</div><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.columns', 'Columns') }}</mat-label><input matInput type="number" min="1" [formControl]="deviceForms[device].controls.columns" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.rowUnit" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.gap', 'Gap') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.gap" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]="deviceForms[device].controls.autoRows"><mat-option value="">{{ tx('editor.devices.orientationDefault', 'Default') }}</mat-option><mat-option value="fixed">fixed</mat-option><mat-option value="content">content</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.devices.groupingOverridesJson', 'Grouping overrides (JSON)') }}</mat-label><textarea matInput rows="4" [formControl]="deviceForms[device].controls.groupingOverrides"></textarea></mat-form-field><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.devices.widgetOverridesJson', 'Widget overrides (JSON)') }}</mat-label><textarea matInput rows="4" [formControl]="deviceForms[device].controls.widgetOverrides"></textarea></mat-form-field></div><div class="hint">{{ tx('editor.preview.deviceCanvasHint', 'Existing canvas item overrides are preserved.') }}</div></div>}</div></mat-tab>
10471
+ <mat-tab [label]="tx('editor.tabs.devices', 'Device Layouts')"><div class="panel">@for (device of deviceKeys; track device) {<div class="card"><div>{{ deviceLabels[device] }}</div><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.columns', 'Columns') }}</mat-label><input matInput type="number" min="1" [formControl]="deviceForms[device].controls.columns" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.rowUnit" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.gap', 'Gap') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.gap" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]="deviceForms[device].controls.autoRows"><mat-option value="">{{ tx('editor.devices.orientationDefault', 'Default') }}</mat-option><mat-option value="fixed">{{ tx('editor.layout.autoRowsFixed', 'Fixed height') }}</mat-option><mat-option value="content">{{ tx('editor.layout.autoRowsContent', 'Fit content') }}</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.devices.groupingOverridesJson', 'Grouping overrides (JSON)') }}</mat-label><textarea matInput rows="4" [formControl]="deviceForms[device].controls.groupingOverrides"></textarea></mat-form-field><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.devices.widgetOverridesJson', 'Widget overrides (JSON)') }}</mat-label><textarea matInput rows="4" [formControl]="deviceForms[device].controls.widgetOverrides"></textarea></mat-form-field></div><div class="hint">{{ tx('editor.preview.deviceCanvasHint', 'Existing canvas item overrides are preserved.') }}</div></div>}</div></mat-tab>
10445
10472
  <mat-tab [label]="tx('editor.tabs.context', 'Context')"><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.context.json', 'Context (JSON)') }}</mat-label><textarea matInput rows="12" [formControl]="contextControl"></textarea></mat-form-field></mat-tab>
10446
10473
  <mat-tab [label]="tx('editor.tabs.state', 'Page State')"><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.state.valuesJson', 'Primary values (JSON)') }}</mat-label><textarea matInput rows="14" [formControl]="stateControl"></textarea></mat-form-field></mat-tab>
10447
10474
  </mat-tab-group>
10448
10475
  </div>
10449
- `, isInline: true, styles: [".editor,.panel{display:grid;gap:16px;color:var(--md-sys-color-on-surface, currentColor)}.head,.summary{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.summary{color:var(--md-sys-color-on-surface-variant, currentColor)}.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.card{display:grid;gap:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface-container, transparent));color:var(--md-sys-color-on-surface, currentColor)}.wide{grid-column:1 / -1}.hint{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px}.preset-preview{display:grid;gap:12px;margin-top:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low, transparent) 88%,var(--md-sys-color-primary-container, transparent) 12%)}.preset-preview--theme{background:var(--md-sys-color-surface-container-low, transparent)}.preset-preview__head,.preset-preview__recommendation{display:flex;justify-content:space-between;align-items:center;gap:16px}.preset-preview__head>div,.preset-preview__recommendation>div{display:grid;gap:4px;min-width:0}.preset-preview p{margin:0;color:var(--md-sys-color-on-surface-variant, currentColor);line-height:1.45}.preset-preview__chips{display:flex;flex-wrap:wrap;gap:8px}.preset-preview__chips span{padding:5px 9px;border-radius:999px;background:var(--md-sys-color-surface-container, color-mix(in srgb, currentColor 8%, transparent));color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px;line-height:1.3}.preset-preview__recommendation{padding-top:12px;border-top:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 14%, transparent))}.preset-preview__recommendation span:last-child{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:13px;line-height:1.35}.eyebrow{color:var(--md-sys-color-primary, currentColor);font-size:11px;font-weight:700;letter-spacing:0;text-transform:uppercase}:host ::ng-deep .mat-mdc-tab-body-content{padding-top:12px;box-sizing:border-box}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { 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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i7.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10476
+ `, isInline: true, styles: [":host{display:block;min-width:0;max-width:100%}.editor,.panel{display:grid;gap:16px;min-width:0;max-width:100%;color:var(--md-sys-color-on-surface, currentColor)}mat-tab-group,mat-form-field{min-width:0;max-width:100%}.head,.summary{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.summary{color:var(--md-sys-color-on-surface-variant, currentColor)}.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr))}@media(max-width:600px){.grid{grid-template-columns:minmax(0,1fr)}.head,.summary,.preset-preview__head,.preset-preview__recommendation{align-items:stretch}.head>button,.preset-preview__recommendation button{width:100%}}.card{display:grid;gap:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface-container, transparent));color:var(--md-sys-color-on-surface, currentColor)}.wide{grid-column:1 / -1}.hint{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px}.preset-preview{display:grid;gap:12px;margin-top:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low, transparent) 88%,var(--md-sys-color-primary-container, transparent) 12%)}.preset-preview--theme{background:var(--md-sys-color-surface-container-low, transparent)}.preset-preview__head,.preset-preview__recommendation{display:flex;justify-content:space-between;align-items:center;gap:16px}.preset-preview__head>div,.preset-preview__recommendation>div{display:grid;gap:4px;min-width:0}.preset-preview p{margin:0;color:var(--md-sys-color-on-surface-variant, currentColor);line-height:1.45}.preset-preview__chips{display:flex;flex-wrap:wrap;gap:8px}.preset-preview__chips span{padding:5px 9px;border-radius:999px;background:var(--md-sys-color-surface-container, color-mix(in srgb, currentColor 8%, transparent));color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px;line-height:1.3}.preset-preview__recommendation{padding-top:12px;border-top:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 14%, transparent))}.preset-preview__recommendation span:last-child{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:13px;line-height:1.35}.eyebrow{color:var(--md-sys-color-primary, currentColor);font-size:11px;font-weight:700;letter-spacing:0;text-transform:uppercase}:host ::ng-deep .mat-mdc-tab-body-content{padding-top:12px;box-sizing:border-box}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { 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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i7.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10450
10477
  }
10451
10478
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicPageConfigEditorComponent, decorators: [{
10452
10479
  type: Component,
@@ -10464,14 +10491,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
10464
10491
  <span>{{ canvasSummaryLabel }}</span>
10465
10492
  </div>
10466
10493
  <mat-tab-group [selectedIndex]="selectedTabIndex" (selectedIndexChange)="onSelectedTabIndexChange($event)">
10467
- <mat-tab [label]="tx('editor.tabs.layout', 'Layout')"><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.cols', 'Columns') }}</mat-label><input matInput type="number" min="1" [formControl]="canvasColumnsControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]="canvasRowUnitControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.gap', 'Gap') }}</mat-label><input matInput [formControl]="canvasGapControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]="canvasAutoRowsControl"><mat-option value="fixed">fixed</mat-option><mat-option value="content">content</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.layoutPreset', 'Layout preset') }}</mat-label><mat-select [formControl]="layoutPresetControl"><mat-option value="">{{ tx('editor.layout.none', 'None') }}</mat-option>@for (preset of layoutPresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.themePreset', 'Theme preset') }}</mat-label><mat-select [formControl]="themePresetControl"><mat-option value="">{{ tx('editor.layout.layoutDefault', 'Layout default') }}</mat-option>@for (preset of themePresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.shellPreset', 'Shell visual preset') }}</mat-label><mat-select [formControl]="shellPresetControl"><mat-option value="">{{ tx('editor.layout.none', 'None') }}</mat-option>@for (preset of shellPresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field></div>@if (selectedLayoutPresetEntry; as preset) {<div class="preset-preview"><div class="preset-preview__head"><div><span class="eyebrow">{{ tx('editor.preview.layoutEyebrow', 'Layout template') }} · {{ preset.category }}</span><strong>{{ preset.label }}</strong></div><span>{{ layoutPresetSummary(preset) }}</span></div><p>{{ preset.description || tx('editor.preview.layoutFallbackDescription', 'Page organization preset.') }}</p><div class="preset-preview__chips"><span>{{ slotsSummary(preset) }}</span><span>{{ requiredSlotsSummary(preset) }}</span><span>{{ devicePolicySummary(preset) }}</span></div>@if (recommendedThemePresetEntry; as theme) {<div class="preset-preview__recommendation"><div><span class="eyebrow">{{ tx('editor.preview.inheritedTheme', 'Inherited theme') }}</span><strong>{{ theme.label }}</strong><span>{{ inheritedThemeHelp(theme) }}</span></div><button mat-stroked-button type="button" [disabled]="themePresetControl.value === theme.id" (click)="pinRecommendedThemePreset()">{{ tx('editor.preview.pinTheme', 'Pin theme') }}</button></div>}</div>} @if (selectedThemePresetEntry; as theme) {<div class="preset-preview preset-preview--theme"><div class="preset-preview__head"><div><span class="eyebrow">{{ tx('editor.preview.explicitTheme', 'Explicit theme') }}</span><strong>{{ theme.label }}</strong></div><span>{{ theme.density }} · {{ theme.motion }}</span></div><p>{{ theme.description || tx('editor.preview.themeFallbackDescription', 'Page visual preset.') }}</p><div class="preset-preview__chips"><span>{{ tx('editor.preview.shellChip', 'Shell: {{value}}').replace('{{value}}', theme.shellPreset) }}</span><span>{{ tx('editor.preview.chartsChip', 'Charts: {{value}}').replace('{{value}}', theme.chartThemePreset) }}</span></div></div>}</mat-tab>
10494
+ <mat-tab [label]="tx('editor.tabs.layout', 'Layout')"><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.cols', 'Columns') }}</mat-label><input matInput type="number" min="1" [formControl]="canvasColumnsControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]="canvasRowUnitControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.gap', 'Gap') }}</mat-label><input matInput [formControl]="canvasGapControl" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]="canvasAutoRowsControl"><mat-option value="fixed">{{ tx('editor.layout.autoRowsFixed', 'Fixed height') }}</mat-option><mat-option value="content">{{ tx('editor.layout.autoRowsContent', 'Fit content') }}</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.layoutPreset', 'Layout preset') }}</mat-label><mat-select [formControl]="layoutPresetControl"><mat-option value="">{{ tx('editor.layout.none', 'None') }}</mat-option>@for (preset of layoutPresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.themePreset', 'Theme preset') }}</mat-label><mat-select [formControl]="themePresetControl"><mat-option value="">{{ tx('editor.layout.layoutDefault', 'Layout default') }}</mat-option>@for (preset of themePresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.shellPreset', 'Shell visual preset') }}</mat-label><mat-select [formControl]="shellPresetControl"><mat-option value="">{{ tx('editor.layout.none', 'None') }}</mat-option>@for (preset of shellPresetEntries; track preset.id) {<mat-option [value]="preset.id">{{ preset.label }}</mat-option>}</mat-select></mat-form-field></div>@if (selectedLayoutPresetEntry; as preset) {<div class="preset-preview"><div class="preset-preview__head"><div><span class="eyebrow">{{ tx('editor.preview.layoutEyebrow', 'Layout template') }} · {{ preset.category }}</span><strong>{{ preset.label }}</strong></div><span>{{ layoutPresetSummary(preset) }}</span></div><p>{{ preset.description || tx('editor.preview.layoutFallbackDescription', 'Page organization preset.') }}</p><div class="preset-preview__chips"><span>{{ slotsSummary(preset) }}</span><span>{{ requiredSlotsSummary(preset) }}</span><span>{{ devicePolicySummary(preset) }}</span></div>@if (recommendedThemePresetEntry; as theme) {<div class="preset-preview__recommendation"><div><span class="eyebrow">{{ tx('editor.preview.inheritedTheme', 'Inherited theme') }}</span><strong>{{ theme.label }}</strong><span>{{ inheritedThemeHelp(theme) }}</span></div><button mat-stroked-button type="button" [disabled]="themePresetControl.value === theme.id" (click)="pinRecommendedThemePreset()">{{ tx('editor.preview.pinTheme', 'Pin theme') }}</button></div>}</div>} @if (selectedThemePresetEntry; as theme) {<div class="preset-preview preset-preview--theme"><div class="preset-preview__head"><div><span class="eyebrow">{{ tx('editor.preview.explicitTheme', 'Explicit theme') }}</span><strong>{{ theme.label }}</strong></div><span>{{ theme.density }} · {{ theme.motion }}</span></div><p>{{ theme.description || tx('editor.preview.themeFallbackDescription', 'Page visual preset.') }}</p><div class="preset-preview__chips"><span>{{ tx('editor.preview.shellChip', 'Shell: {{value}}').replace('{{value}}', theme.shellPreset) }}</span><span>{{ tx('editor.preview.chartsChip', 'Charts: {{value}}').replace('{{value}}', theme.chartThemePreset) }}</span></div></div>}</mat-tab>
10468
10495
  <mat-tab [label]="tx('editor.tabs.grouping', 'Grouping')"><div class="panel"><div class="head"><span>{{ tx('editor.grouping.title', 'Grouping') }}</span><button mat-stroked-button type="button" (click)="addGroupingNode()"><mat-icon>add</mat-icon>{{ tx('editor.grouping.add', 'Add group') }}</button></div>@for (group of groupingNodes.controls; track $index) {<div class="card" [formGroup]="group"><div class="head"><span>{{ groupLabel($index) }}</span><button praxisIconButton="delete" size="compact" type="button" [attr.aria-label]="tx('editor.grouping.removeAria', 'Remove group')" (click)="removeGroupingNode($index)"></button></div><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.kind', 'Kind') }}</mat-label><mat-select formControlName="kind"><mat-option value="section">section</mat-option><mat-option value="tabs">tabs</mat-option><mat-option value="hero">hero</mat-option><mat-option value="rail">rail</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.id', 'Id') }}</mat-label><input matInput formControlName="id" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.label', 'Label') }}</mat-label><input matInput formControlName="label" /></mat-form-field>@if (group.controls.kind.value === 'section') {<mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.layout', 'Layout') }}</mat-label><mat-select formControlName="layout"><mat-option value="">{{ tx('editor.grouping.layoutDefault', 'Default') }}</mat-option><mat-option value="stack">stack</mat-option><mat-option value="grid">grid</mat-option><mat-option value="row">row</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'hero') {<mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.emphasis', 'Emphasis') }}</mat-label><mat-select formControlName="emphasis"><mat-option value="">{{ tx('editor.grouping.layoutDefault', 'Default') }}</mat-option><mat-option value="high">high</mat-option><mat-option value="medium">medium</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'rail') {<mat-form-field appearance="outline"><mat-label>{{ tx('editor.grouping.side', 'Side') }}</mat-label><mat-select formControlName="side"><mat-option value="left">left</mat-option><mat-option value="right">right</mat-option></mat-select></mat-form-field>}@if (group.controls.kind.value === 'tabs') {<mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.grouping.tabsJson', 'Tabs (JSON)') }}</mat-label><textarea matInput rows="4" formControlName="tabsJson"></textarea></mat-form-field>} @else {<mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.grouping.widgetKeysCsv', 'Widget keys (comma-separated)') }}</mat-label><input matInput formControlName="widgetKeysCsv" /></mat-form-field>}</div></div>}</div></mat-tab>
10469
- <mat-tab [label]="tx('editor.tabs.devices', 'Device Layouts')"><div class="panel">@for (device of deviceKeys; track device) {<div class="card"><div>{{ deviceLabels[device] }}</div><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.columns', 'Columns') }}</mat-label><input matInput type="number" min="1" [formControl]="deviceForms[device].controls.columns" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.rowUnit" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.gap', 'Gap') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.gap" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]="deviceForms[device].controls.autoRows"><mat-option value="">{{ tx('editor.devices.orientationDefault', 'Default') }}</mat-option><mat-option value="fixed">fixed</mat-option><mat-option value="content">content</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.devices.groupingOverridesJson', 'Grouping overrides (JSON)') }}</mat-label><textarea matInput rows="4" [formControl]="deviceForms[device].controls.groupingOverrides"></textarea></mat-form-field><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.devices.widgetOverridesJson', 'Widget overrides (JSON)') }}</mat-label><textarea matInput rows="4" [formControl]="deviceForms[device].controls.widgetOverrides"></textarea></mat-form-field></div><div class="hint">{{ tx('editor.preview.deviceCanvasHint', 'Existing canvas item overrides are preserved.') }}</div></div>}</div></mat-tab>
10496
+ <mat-tab [label]="tx('editor.tabs.devices', 'Device Layouts')"><div class="panel">@for (device of deviceKeys; track device) {<div class="card"><div>{{ deviceLabels[device] }}</div><div class="grid"><mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.columns', 'Columns') }}</mat-label><input matInput type="number" min="1" [formControl]="deviceForms[device].controls.columns" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.rowUnit', 'Row unit') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.rowUnit" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.gap', 'Gap') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.gap" /></mat-form-field><mat-form-field appearance="outline"><mat-label>{{ tx('editor.layout.autoRows', 'Auto rows') }}</mat-label><mat-select [formControl]="deviceForms[device].controls.autoRows"><mat-option value="">{{ tx('editor.devices.orientationDefault', 'Default') }}</mat-option><mat-option value="fixed">{{ tx('editor.layout.autoRowsFixed', 'Fixed height') }}</mat-option><mat-option value="content">{{ tx('editor.layout.autoRowsContent', 'Fit content') }}</mat-option></mat-select></mat-form-field><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.devices.groupingOverridesJson', 'Grouping overrides (JSON)') }}</mat-label><textarea matInput rows="4" [formControl]="deviceForms[device].controls.groupingOverrides"></textarea></mat-form-field><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.devices.widgetOverridesJson', 'Widget overrides (JSON)') }}</mat-label><textarea matInput rows="4" [formControl]="deviceForms[device].controls.widgetOverrides"></textarea></mat-form-field></div><div class="hint">{{ tx('editor.preview.deviceCanvasHint', 'Existing canvas item overrides are preserved.') }}</div></div>}</div></mat-tab>
10470
10497
  <mat-tab [label]="tx('editor.tabs.context', 'Context')"><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.context.json', 'Context (JSON)') }}</mat-label><textarea matInput rows="12" [formControl]="contextControl"></textarea></mat-form-field></mat-tab>
10471
10498
  <mat-tab [label]="tx('editor.tabs.state', 'Page State')"><mat-form-field appearance="outline" class="wide"><mat-label>{{ tx('editor.state.valuesJson', 'Primary values (JSON)') }}</mat-label><textarea matInput rows="14" [formControl]="stateControl"></textarea></mat-form-field></mat-tab>
10472
10499
  </mat-tab-group>
10473
10500
  </div>
10474
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".editor,.panel{display:grid;gap:16px;color:var(--md-sys-color-on-surface, currentColor)}.head,.summary{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.summary{color:var(--md-sys-color-on-surface-variant, currentColor)}.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.card{display:grid;gap:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface-container, transparent));color:var(--md-sys-color-on-surface, currentColor)}.wide{grid-column:1 / -1}.hint{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px}.preset-preview{display:grid;gap:12px;margin-top:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low, transparent) 88%,var(--md-sys-color-primary-container, transparent) 12%)}.preset-preview--theme{background:var(--md-sys-color-surface-container-low, transparent)}.preset-preview__head,.preset-preview__recommendation{display:flex;justify-content:space-between;align-items:center;gap:16px}.preset-preview__head>div,.preset-preview__recommendation>div{display:grid;gap:4px;min-width:0}.preset-preview p{margin:0;color:var(--md-sys-color-on-surface-variant, currentColor);line-height:1.45}.preset-preview__chips{display:flex;flex-wrap:wrap;gap:8px}.preset-preview__chips span{padding:5px 9px;border-radius:999px;background:var(--md-sys-color-surface-container, color-mix(in srgb, currentColor 8%, transparent));color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px;line-height:1.3}.preset-preview__recommendation{padding-top:12px;border-top:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 14%, transparent))}.preset-preview__recommendation span:last-child{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:13px;line-height:1.35}.eyebrow{color:var(--md-sys-color-primary, currentColor);font-size:11px;font-weight:700;letter-spacing:0;text-transform:uppercase}:host ::ng-deep .mat-mdc-tab-body-content{padding-top:12px;box-sizing:border-box}\n"] }]
10501
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;min-width:0;max-width:100%}.editor,.panel{display:grid;gap:16px;min-width:0;max-width:100%;color:var(--md-sys-color-on-surface, currentColor)}mat-tab-group,mat-form-field{min-width:0;max-width:100%}.head,.summary{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.summary{color:var(--md-sys-color-on-surface-variant, currentColor)}.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr))}@media(max-width:600px){.grid{grid-template-columns:minmax(0,1fr)}.head,.summary,.preset-preview__head,.preset-preview__recommendation{align-items:stretch}.head>button,.preset-preview__recommendation button{width:100%}}.card{display:grid;gap:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface-container, transparent));color:var(--md-sys-color-on-surface, currentColor)}.wide{grid-column:1 / -1}.hint{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px}.preset-preview{display:grid;gap:12px;margin-top:12px;padding:16px;border:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 18%, transparent));border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low, transparent) 88%,var(--md-sys-color-primary-container, transparent) 12%)}.preset-preview--theme{background:var(--md-sys-color-surface-container-low, transparent)}.preset-preview__head,.preset-preview__recommendation{display:flex;justify-content:space-between;align-items:center;gap:16px}.preset-preview__head>div,.preset-preview__recommendation>div{display:grid;gap:4px;min-width:0}.preset-preview p{margin:0;color:var(--md-sys-color-on-surface-variant, currentColor);line-height:1.45}.preset-preview__chips{display:flex;flex-wrap:wrap;gap:8px}.preset-preview__chips span{padding:5px 9px;border-radius:999px;background:var(--md-sys-color-surface-container, color-mix(in srgb, currentColor 8%, transparent));color:var(--md-sys-color-on-surface-variant, currentColor);font-size:12px;line-height:1.3}.preset-preview__recommendation{padding-top:12px;border-top:1px solid var(--md-sys-color-outline-variant, color-mix(in srgb, currentColor 14%, transparent))}.preset-preview__recommendation span:last-child{color:var(--md-sys-color-on-surface-variant, currentColor);font-size:13px;line-height:1.35}.eyebrow{color:var(--md-sys-color-primary, currentColor);font-size:11px;font-weight:700;letter-spacing:0;text-transform:uppercase}:host ::ng-deep .mat-mdc-tab-body-content{padding-top:12px;box-sizing:border-box}\n"] }]
10475
10502
  }], ctorParameters: () => [], propDecorators: { page: [{
10476
10503
  type: Input
10477
10504
  }], layout: [{
@@ -13738,9 +13765,9 @@ const PRAXIS_PAGE_BUILDER_AUTHORING_MANIFEST = {
13738
13765
  target: { kind: 'agenticPreview', resolver: 'agentic-preview-result', ambiguityPolicy: 'fail', required: true },
13739
13766
  inputSchema: { type: 'object', required: ['uiCompositionPlan'], properties: { uiCompositionPlan: { type: 'object' } } },
13740
13767
  effects: [{ kind: 'compile-domain-patch', handler: 'page-builder-ui-composition-plan-compile', handlerContract: {
13741
- reads: ['UiCompositionPlan', 'compileUiCompositionPlan', 'PAGE_BUILDER_WIDGET_AI_CATALOGS', 'ComponentDocMeta.configEditor'],
13768
+ reads: ['UiCompositionPlan', 'UiCompositionPlan.selectionSyncs', 'UiCompositionPlan.contextScopes', 'compileUiCompositionPlan', 'PAGE_BUILDER_WIDGET_AI_CATALOGS', 'ComponentDocMeta.configEditor'],
13742
13769
  writes: ['compiledFormPatch.patch.page'],
13743
- identityKeys: ['uiCompositionPlan.id', 'widgetKey', 'uiCompositionPlan.bindings[].id'],
13770
+ identityKeys: ['uiCompositionPlan.id', 'widgetKey', 'uiCompositionPlan.bindings[].id', 'uiCompositionPlan.selectionSyncs[].id', 'uiCompositionPlan.contextScopes[].id'],
13744
13771
  failureModes: ['ui-composition-plan-invalid', 'widget-catalog-missing', 'compiled-page-invalid', 'child-config-editor-missing'],
13745
13772
  description: 'Compiles the agentic intermediate UiCompositionPlan into a renderable WidgetPageDefinition patch.page.',
13746
13773
  } }],
@@ -13877,6 +13904,8 @@ const PRAXIS_PAGE_BUILDER_AUTHORING_MANIFEST = {
13877
13904
  roundTripRequirements: [
13878
13905
  'WidgetPageDefinition is the canonical persisted runtime page shape for page-builder.',
13879
13906
  'UiCompositionPlan is an intermediate agentic plan and must compile before local preview/apply.',
13907
+ 'UiCompositionPlan.selectionSyncs is an authoring shorthand that compiles into explicit canonical page.composition.links and is never persisted in WidgetPageDefinition.',
13908
+ 'UiCompositionPlan.contextScopes compiles constant context into explicit widget inputs and state context into explicit page.composition.links; runtime inheritance is never ambient.',
13880
13909
  'Widget keys and composition link ids are stable identities; array indexes are never canonical identities.',
13881
13910
  'Persisted wiring uses page.composition.links and must not write legacy connections.',
13882
13911
  'Nested component ports use component-port refs with nestedPath ending in a stable widget key.',
@@ -14017,6 +14046,8 @@ function compileUiCompositionPlan(plan) {
14017
14046
  diagnostics,
14018
14047
  };
14019
14048
  }
14049
+ const bindings = expandedBindings(plan);
14050
+ const widgets = materializeContextScopeConstants(plan, plan.widgets.map((widget) => toWidgetInstance(widget, bindings)));
14020
14051
  return {
14021
14052
  valid: true,
14022
14053
  page: {
@@ -14028,10 +14059,10 @@ function compileUiCompositionPlan(plan) {
14028
14059
  slotAssignments: clone(plan.slotAssignments),
14029
14060
  state: clone(plan.state),
14030
14061
  themePreset: plan.themePreset,
14031
- widgets: plan.widgets.map((widget) => toWidgetInstance(widget, plan.bindings ?? [])),
14062
+ widgets,
14032
14063
  composition: {
14033
14064
  version: '1.0.0',
14034
- links: (plan.bindings ?? []).map(toCompositionLink),
14065
+ links: bindings.map(toCompositionLink),
14035
14066
  },
14036
14067
  },
14037
14068
  diagnostics: [],
@@ -14172,12 +14203,176 @@ function validateUiCompositionPlan(plan) {
14172
14203
  });
14173
14204
  }
14174
14205
  }
14206
+ for (const [index, selectionSync] of (plan.selectionSyncs ?? []).entries()) {
14207
+ validateSelectionSync(selectionSync, widgetKeys, diagnostics, `selectionSyncs.${index}`);
14208
+ }
14209
+ for (const [index, contextScope] of (plan.contextScopes ?? []).entries()) {
14210
+ validateContextScope(contextScope, plan, widgetKeys, diagnostics, `contextScopes.${index}`);
14211
+ }
14175
14212
  validateCanvas(plan.canvas, widgetKeys, diagnostics);
14176
14213
  validateGrouping(plan.grouping, widgetKeys, diagnostics);
14177
14214
  validateDeviceLayouts(plan.deviceLayouts, widgetKeys, diagnostics);
14178
14215
  validateSlotAssignments(plan.slotAssignments, widgetKeys, diagnostics);
14179
14216
  return diagnostics;
14180
14217
  }
14218
+ function validateSelectionSync(selectionSync, widgetKeys, diagnostics, path) {
14219
+ if (!selectionSync || typeof selectionSync !== 'object') {
14220
+ diagnostics.push({
14221
+ code: 'selection-sync-required',
14222
+ message: 'Selection sync must be an object.',
14223
+ path,
14224
+ });
14225
+ return;
14226
+ }
14227
+ if (!selectionSync.id?.trim()) {
14228
+ diagnostics.push({
14229
+ code: 'selection-sync-id-required',
14230
+ message: 'Every selection sync must declare a stable id.',
14231
+ path: `${path}.id`,
14232
+ });
14233
+ }
14234
+ if (selectionSync.intent !== 'selection-sync') {
14235
+ diagnostics.push({
14236
+ code: 'selection-sync-intent-invalid',
14237
+ message: 'Selection sync intent must be "selection-sync".',
14238
+ path: `${path}.intent`,
14239
+ });
14240
+ }
14241
+ if (!selectionSync.sources?.length) {
14242
+ diagnostics.push({
14243
+ code: 'selection-sync-source-required',
14244
+ message: 'Selection sync must declare at least one component output source.',
14245
+ path: `${path}.sources`,
14246
+ });
14247
+ }
14248
+ for (const [sourceIndex, source] of (selectionSync.sources ?? []).entries()) {
14249
+ validateEndpoint(source, widgetKeys, diagnostics, `${path}.sources.${sourceIndex}`, false);
14250
+ if (source?.kind === 'component-port' && source.direction !== 'output') {
14251
+ diagnostics.push({
14252
+ code: 'selection-sync-source-direction-invalid',
14253
+ message: 'Selection sync sources must be component output ports.',
14254
+ path: `${path}.sources.${sourceIndex}.direction`,
14255
+ });
14256
+ }
14257
+ }
14258
+ if (selectionSync.target?.kind !== 'state') {
14259
+ diagnostics.push({
14260
+ code: 'selection-sync-target-state-required',
14261
+ message: 'Selection sync target must be a page state endpoint.',
14262
+ path: `${path}.target.kind`,
14263
+ });
14264
+ }
14265
+ else {
14266
+ validateEndpoint(selectionSync.target, widgetKeys, diagnostics, `${path}.target`, false);
14267
+ }
14268
+ const mappings = Object.entries(selectionSync.mapping ?? {});
14269
+ if (!mappings.length) {
14270
+ diagnostics.push({
14271
+ code: 'selection-sync-mapping-required',
14272
+ message: 'Selection sync must declare at least one state mapping.',
14273
+ path: `${path}.mapping`,
14274
+ });
14275
+ }
14276
+ for (const [targetKey, mapping] of mappings) {
14277
+ if (!targetKey.trim()) {
14278
+ diagnostics.push({
14279
+ code: 'selection-sync-target-key-required',
14280
+ message: 'Selection sync mapping keys must be non-empty.',
14281
+ path: `${path}.mapping`,
14282
+ });
14283
+ }
14284
+ const sourcePath = typeof mapping === 'string' ? mapping : mapping?.path;
14285
+ if (!sourcePath?.trim()) {
14286
+ diagnostics.push({
14287
+ code: 'selection-sync-source-path-required',
14288
+ message: `Selection sync mapping "${targetKey}" must declare a source path.`,
14289
+ path: `${path}.mapping.${targetKey}`,
14290
+ });
14291
+ }
14292
+ }
14293
+ }
14294
+ function validateContextScope(contextScope, plan, widgetKeys, diagnostics, path) {
14295
+ if (!contextScope || typeof contextScope !== 'object') {
14296
+ diagnostics.push({ code: 'context-scope-required', message: 'Context scope must be an object.', path });
14297
+ return;
14298
+ }
14299
+ if (!contextScope.id?.trim()) {
14300
+ diagnostics.push({
14301
+ code: 'context-scope-id-required',
14302
+ message: 'Every context scope must declare a stable id.',
14303
+ path: `${path}.id`,
14304
+ });
14305
+ }
14306
+ const contextEntries = Object.entries(contextScope.context ?? {});
14307
+ if (!contextEntries.length) {
14308
+ diagnostics.push({
14309
+ code: 'context-scope-context-required',
14310
+ message: 'Context scope must declare at least one context value.',
14311
+ path: `${path}.context`,
14312
+ });
14313
+ }
14314
+ for (const [contextKey, contextValue] of contextEntries) {
14315
+ if (!contextKey.trim()) {
14316
+ diagnostics.push({
14317
+ code: 'context-scope-key-required',
14318
+ message: 'Context scope keys must be non-empty.',
14319
+ path: `${path}.context`,
14320
+ });
14321
+ }
14322
+ if (contextValue?.kind === 'state' && !contextValue.path?.trim()) {
14323
+ diagnostics.push({
14324
+ code: 'context-scope-state-path-required',
14325
+ message: `Context value "${contextKey}" must declare a state path.`,
14326
+ path: `${path}.context.${contextKey}.path`,
14327
+ });
14328
+ }
14329
+ else if (contextValue?.kind !== 'state' && contextValue?.kind !== 'constant') {
14330
+ diagnostics.push({
14331
+ code: 'context-scope-value-kind-invalid',
14332
+ message: `Context value "${contextKey}" must be constant or state.`,
14333
+ path: `${path}.context.${contextKey}.kind`,
14334
+ });
14335
+ }
14336
+ }
14337
+ if (!contextScope.targets?.length) {
14338
+ diagnostics.push({
14339
+ code: 'context-scope-target-required',
14340
+ message: 'Context scope must declare at least one component target.',
14341
+ path: `${path}.targets`,
14342
+ });
14343
+ }
14344
+ const accessor = new NestedWidgetConfigAccessor();
14345
+ const owners = new Map((plan.widgets ?? []).map((widget) => [widget.key, toWidgetInstance(widget, [])]));
14346
+ for (const [targetIndex, target] of (contextScope.targets ?? []).entries()) {
14347
+ const targetPath = `${path}.targets.${targetIndex}`;
14348
+ if (!widgetKeys.has(target.widget)) {
14349
+ diagnostics.push({
14350
+ code: 'context-scope-target-widget-not-found',
14351
+ message: `Context scope target references unknown widget "${target.widget}".`,
14352
+ path: `${targetPath}.widget`,
14353
+ });
14354
+ continue;
14355
+ }
14356
+ validateNestedPath(target.nestedPath, diagnostics, `${targetPath}.nestedPath`);
14357
+ const owner = owners.get(target.widget);
14358
+ if (target.nestedPath?.length && owner && !accessor.resolveNestedWidget(owner, target.nestedPath)) {
14359
+ diagnostics.push({
14360
+ code: 'context-scope-nested-target-not-found',
14361
+ message: `Context scope target does not resolve inside widget "${target.widget}".`,
14362
+ path: `${targetPath}.nestedPath`,
14363
+ });
14364
+ }
14365
+ for (const inheritedKey of target.inherit ?? []) {
14366
+ if (!Object.prototype.hasOwnProperty.call(contextScope.context ?? {}, inheritedKey)) {
14367
+ diagnostics.push({
14368
+ code: 'context-scope-inherited-key-not-found',
14369
+ message: `Context scope target inherits unknown key "${inheritedKey}".`,
14370
+ path: `${targetPath}.inherit`,
14371
+ });
14372
+ }
14373
+ }
14374
+ }
14375
+ }
14181
14376
  function validateCanvas(canvas, widgetKeys, diagnostics) {
14182
14377
  if (!canvas)
14183
14378
  return;
@@ -14374,6 +14569,111 @@ function validateNestedPath(nestedPath, diagnostics, path) {
14374
14569
  });
14375
14570
  }
14376
14571
  }
14572
+ function expandedBindings(plan) {
14573
+ return [
14574
+ ...(plan.bindings ?? []).map((binding) => clone(binding)),
14575
+ ...(plan.selectionSyncs ?? []).flatMap(expandSelectionSync),
14576
+ ...(plan.contextScopes ?? []).flatMap(expandContextScopeBindings),
14577
+ ];
14578
+ }
14579
+ function expandSelectionSync(selectionSync) {
14580
+ const basePath = selectionSync.target.path.replace(/\.+$/, '');
14581
+ return selectionSync.sources.flatMap((source) => Object.entries(selectionSync.mapping).map(([targetKey, mapping]) => {
14582
+ const normalizedMapping = typeof mapping === 'string' ? { path: mapping } : mapping;
14583
+ const targetPath = basePath ? `${basePath}.${targetKey}` : targetKey;
14584
+ return {
14585
+ id: `${selectionSync.id}:${source.widget}.${source.port}->state.${targetPath}`,
14586
+ from: clone(source),
14587
+ to: {
14588
+ kind: 'state',
14589
+ path: targetPath,
14590
+ layer: selectionSync.target.layer,
14591
+ },
14592
+ intent: 'selection-sync',
14593
+ condition: clone(normalizedMapping.condition),
14594
+ transform: {
14595
+ kind: 'pick-path',
14596
+ id: `${selectionSync.id}:${source.widget}.${source.port}:pick-${targetKey}`,
14597
+ path: normalizedMapping.path,
14598
+ inputSource: normalizedMapping.inputSource,
14599
+ },
14600
+ policy: clone(selectionSync.policy),
14601
+ metadata: clone(selectionSync.metadata),
14602
+ };
14603
+ }));
14604
+ }
14605
+ function expandContextScopeBindings(contextScope) {
14606
+ return contextScope.targets.flatMap((target) => {
14607
+ const inheritedKeys = target.inherit?.length
14608
+ ? target.inherit
14609
+ : Object.keys(contextScope.context);
14610
+ return inheritedKeys.flatMap((contextKey) => {
14611
+ const contextValue = contextScope.context[contextKey];
14612
+ if (!contextValue || contextValue.kind !== 'state') {
14613
+ return [];
14614
+ }
14615
+ return [{
14616
+ id: `${contextScope.id}:state.${contextValue.path}->${target.widget}.${contextKey}${nestedTargetIdentity(target.nestedPath)}`,
14617
+ from: {
14618
+ kind: 'state',
14619
+ path: contextValue.path,
14620
+ layer: contextValue.layer,
14621
+ },
14622
+ to: {
14623
+ kind: 'component-port',
14624
+ widget: target.widget,
14625
+ port: contextKey,
14626
+ direction: 'input',
14627
+ ...(target.nestedPath?.length ? { nestedPath: clone(target.nestedPath) } : {}),
14628
+ },
14629
+ intent: 'state-read',
14630
+ condition: clone(contextValue.condition),
14631
+ policy: clone(contextScope.policy),
14632
+ metadata: clone(contextScope.metadata),
14633
+ }];
14634
+ });
14635
+ });
14636
+ }
14637
+ function materializeContextScopeConstants(plan, sourceWidgets) {
14638
+ const accessor = new NestedWidgetConfigAccessor();
14639
+ const widgets = sourceWidgets.map((widget) => clone(widget));
14640
+ for (const contextScope of plan.contextScopes ?? []) {
14641
+ for (const target of contextScope.targets) {
14642
+ const widgetIndex = widgets.findIndex((widget) => widget.key === target.widget);
14643
+ if (widgetIndex < 0)
14644
+ continue;
14645
+ const inheritedKeys = target.inherit?.length
14646
+ ? target.inherit
14647
+ : Object.keys(contextScope.context);
14648
+ for (const contextKey of inheritedKeys) {
14649
+ const contextValue = contextScope.context[contextKey];
14650
+ if (!contextValue || contextValue.kind !== 'constant')
14651
+ continue;
14652
+ if (target.nestedPath?.length) {
14653
+ const result = accessor.setNestedWidgetInput(widgets[widgetIndex], target.nestedPath, contextKey, contextValue.value);
14654
+ widgets[widgetIndex] = result.widget;
14655
+ }
14656
+ else {
14657
+ widgets[widgetIndex] = {
14658
+ ...widgets[widgetIndex],
14659
+ definition: {
14660
+ ...widgets[widgetIndex].definition,
14661
+ inputs: {
14662
+ ...(widgets[widgetIndex].definition.inputs ?? {}),
14663
+ [contextKey]: clone(contextValue.value),
14664
+ },
14665
+ },
14666
+ };
14667
+ }
14668
+ }
14669
+ }
14670
+ }
14671
+ return widgets;
14672
+ }
14673
+ function nestedTargetIdentity(nestedPath) {
14674
+ const child = nestedPath?.[nestedPath.length - 1];
14675
+ return child?.kind === 'widget' && child.key ? `#${child.key}` : '';
14676
+ }
14377
14677
  function toWidgetInstance(widget, bindings) {
14378
14678
  const outputs = linkedTopLevelOutputs(widget.key, widget.outputs, bindings);
14379
14679
  return {
@@ -23879,7 +24179,7 @@ class DynamicPageBuilderComponent {
23879
24179
  : label || fallbackKey;
23880
24180
  }
23881
24181
  resolveWidgetDisplayName(widget) {
23882
- const shellTitle = String(widget.shell?.title || '').trim();
24182
+ const shellTitle = this.i18n.resolve(widget.shell?.title, '').trim();
23883
24183
  if (shellTitle)
23884
24184
  return shellTitle;
23885
24185
  const componentId = widget.definition?.id || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/page-builder",
3
- "version": "9.0.4-rc.8",
3
+ "version": "9.0.4",
4
4
  "description": "Page and widget builder utilities for Praxis UI (grid, dynamic widgets, editors).",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
@@ -8,9 +8,9 @@
8
8
  "@angular/forms": "^21.0.0",
9
9
  "@angular/cdk": "^21.0.0",
10
10
  "@angular/material": "^21.0.0",
11
- "@praxisui/ai": "^9.0.4-rc.8",
12
- "@praxisui/core": "^9.0.4-rc.8",
13
- "@praxisui/settings-panel": "^9.0.4-rc.8",
11
+ "@praxisui/ai": "^9.0.4",
12
+ "@praxisui/core": "^9.0.4",
13
+ "@praxisui/settings-panel": "^9.0.4",
14
14
  "rxjs": "~7.8.0"
15
15
  },
16
16
  "dependencies": {
@@ -179,6 +179,9 @@ declare class WidgetShellEditorComponent implements SettingsValueProvider {
179
179
  isValid$: rxjs.Observable<boolean>;
180
180
  isBusy$: rxjs.Observable<boolean>;
181
181
  private snapshot;
182
+ private initialTitle;
183
+ private initialSubtitle;
184
+ private readonly actionCopySources;
182
185
  presetOptions: Array<{
183
186
  id: string;
184
187
  label: string;
@@ -222,6 +225,8 @@ declare class WidgetShellEditorComponent implements SettingsValueProvider {
222
225
  previewOverflowCount(): number;
223
226
  previewWindowActions(): WidgetShellAction[];
224
227
  private normalizeAction;
228
+ private editableText;
229
+ private preserveTextDescriptor;
225
230
  private tryParsePayload;
226
231
  private snapshotValue;
227
232
  filteredAvailableActions(): AvailableShellAction[];
@@ -1543,6 +1548,51 @@ interface UiCompositionPlanBinding {
1543
1548
  policy?: CompositionLink['policy'];
1544
1549
  metadata?: Record<string, unknown>;
1545
1550
  }
1551
+ type UiCompositionPlanSelectionSyncMapping = string | {
1552
+ path: string;
1553
+ inputSource?: UiCompositionPlanInputSource;
1554
+ condition?: unknown;
1555
+ };
1556
+ /**
1557
+ * Semantic authoring shorthand for projecting the same selection emitted by one
1558
+ * or more component outputs into page state. The compiler expands this shape
1559
+ * into explicit canonical composition links before preview or persistence.
1560
+ */
1561
+ interface UiCompositionPlanSelectionSync {
1562
+ id: string;
1563
+ intent: 'selection-sync';
1564
+ sources: UiCompositionPlanComponentPortEndpoint[];
1565
+ target: UiCompositionPlanStateEndpoint;
1566
+ mapping: Record<string, UiCompositionPlanSelectionSyncMapping>;
1567
+ policy?: CompositionLink['policy'];
1568
+ metadata?: Record<string, unknown>;
1569
+ }
1570
+ type UiCompositionPlanContextValue = {
1571
+ kind: 'constant';
1572
+ value: unknown;
1573
+ } | {
1574
+ kind: 'state';
1575
+ path: string;
1576
+ layer?: 'values' | 'derived' | 'transient';
1577
+ condition?: unknown;
1578
+ };
1579
+ interface UiCompositionPlanContextScopeTarget {
1580
+ widget: string;
1581
+ nestedPath?: ComponentPortPathSegment[];
1582
+ inherit?: string[];
1583
+ }
1584
+ /**
1585
+ * Groups reusable parent/resource context for one or more component targets.
1586
+ * Constants become explicit widget inputs and state values become explicit
1587
+ * canonical links during compilation; no hidden runtime inheritance remains.
1588
+ */
1589
+ interface UiCompositionPlanContextScope {
1590
+ id: string;
1591
+ context: Record<string, UiCompositionPlanContextValue>;
1592
+ targets: UiCompositionPlanContextScopeTarget[];
1593
+ policy?: CompositionLink['policy'];
1594
+ metadata?: Record<string, unknown>;
1595
+ }
1546
1596
  interface UiCompositionPlan {
1547
1597
  version: '1.0';
1548
1598
  kind: 'praxis.ui-composition-plan';
@@ -1555,6 +1605,8 @@ interface UiCompositionPlan {
1555
1605
  state?: WidgetPageStateInput;
1556
1606
  themePreset?: string;
1557
1607
  widgets: UiCompositionPlanWidget[];
1608
+ selectionSyncs?: UiCompositionPlanSelectionSync[];
1609
+ contextScopes?: UiCompositionPlanContextScope[];
1558
1610
  bindings?: UiCompositionPlanBinding[];
1559
1611
  }
1560
1612
  interface UiCompositionPlanDiagnostic {
@@ -2082,4 +2134,4 @@ declare const PRAXIS_PAGE_BUILDER_COMPONENT_METADATA: ComponentDocMeta;
2082
2134
  declare function providePraxisPageBuilderMetadata(): Provider;
2083
2135
 
2084
2136
  export { ComponentPaletteDialogComponent, ConfirmDialogComponent, ConnectionEditorComponent, DynamicPageBuilderComponent, DynamicPageConfigEditorComponent, FloatingToolbarComponent, PAGE_BUILDER_AGENTIC_AUTHORING_OPTIONS, PAGE_BUILDER_AI_CAPABILITIES, PAGE_BUILDER_COMPONENT_CONTEXT_PACK, PAGE_BUILDER_WIDGET_AI_CATALOGS, PLACEHOLDER, PRAXIS_PAGE_BUILDER_AUTHORING_MANIFEST, PRAXIS_PAGE_BUILDER_COMPONENT_METADATA, PageBuilderAgenticAuthoringService, PageConfigEditorComponent, TileToolbarComponent, WidgetShellEditorComponent, clearWidgetAiCatalogs, compileUiCompositionPlan, getPageAiCatalog, getWidgetAiCapabilities, providePageBuilderWidgetAiCatalogs, providePraxisPageBuilderMetadata, registerWidgetAiCatalog, registerWidgetAiCatalogs, validateUiCompositionPlan };
2085
- export type { Capability, CapabilityCatalog, CapabilityCategory, ComponentPaletteData, ComponentPaletteSelection, ConfirmDialogData, PageBuilderAgenticAuthoringAttachmentSummary, PageBuilderAgenticAuthoringCandidate, PageBuilderAgenticAuthoringComponentCapabilitiesResult, PageBuilderAgenticAuthoringComponentCapability, PageBuilderAgenticAuthoringComponentCapabilityCatalog, PageBuilderAgenticAuthoringComponentCapabilityExample, PageBuilderAgenticAuthoringComponentFieldAlias, PageBuilderAgenticAuthoringConversationMessage, PageBuilderAgenticAuthoringGateResult, PageBuilderAgenticAuthoringIntentResolutionRequest, PageBuilderAgenticAuthoringIntentResolutionResult, PageBuilderAgenticAuthoringOptions, PageBuilderAgenticAuthoringPendingClarification, PageBuilderAgenticAuthoringPromptRequest, PageBuilderAgenticAuthoringProvider, PageBuilderAgenticAuthoringQuickReply, PageBuilderAgenticAuthoringResourceCandidatesRequest, PageBuilderAgenticAuthoringResourceCandidatesResult, PageBuilderAgenticAuthoringSharedRuleHandoff, PageBuilderAgenticAuthoringTarget, PageBuilderAgenticAuthoringTurnStreamConnectionError, PageBuilderAgenticAuthoringTurnStreamEvent, PageBuilderAgenticAuthoringTurnStreamRequest, PageBuilderAgenticAuthoringTurnStreamStartResponse, PageBuilderApplyRequest, PageBuilderApplyResult, PageBuilderCompiledFormPatch, PageBuilderMinimalFormPlanResult, PageBuilderPreviewDiagnostics, PageBuilderPreviewResult, PageBuilderProjectKnowledgeAudit, UiCompositionPlan, UiCompositionPlanBinding, UiCompositionPlanCompileResult, UiCompositionPlanComponentPortEndpoint, UiCompositionPlanDiagnostic, UiCompositionPlanEndpoint, UiCompositionPlanGlobalActionEndpoint, UiCompositionPlanInputSource, UiCompositionPlanSourceEndpoint, UiCompositionPlanStateEndpoint, UiCompositionPlanTargetEndpoint, UiCompositionPlanTransform, UiCompositionPlanWidget, ValueKind };
2137
+ export type { Capability, CapabilityCatalog, CapabilityCategory, ComponentPaletteData, ComponentPaletteSelection, ConfirmDialogData, PageBuilderAgenticAuthoringAttachmentSummary, PageBuilderAgenticAuthoringCandidate, PageBuilderAgenticAuthoringComponentCapabilitiesResult, PageBuilderAgenticAuthoringComponentCapability, PageBuilderAgenticAuthoringComponentCapabilityCatalog, PageBuilderAgenticAuthoringComponentCapabilityExample, PageBuilderAgenticAuthoringComponentFieldAlias, PageBuilderAgenticAuthoringConversationMessage, PageBuilderAgenticAuthoringGateResult, PageBuilderAgenticAuthoringIntentResolutionRequest, PageBuilderAgenticAuthoringIntentResolutionResult, PageBuilderAgenticAuthoringOptions, PageBuilderAgenticAuthoringPendingClarification, PageBuilderAgenticAuthoringPromptRequest, PageBuilderAgenticAuthoringProvider, PageBuilderAgenticAuthoringQuickReply, PageBuilderAgenticAuthoringResourceCandidatesRequest, PageBuilderAgenticAuthoringResourceCandidatesResult, PageBuilderAgenticAuthoringSharedRuleHandoff, PageBuilderAgenticAuthoringTarget, PageBuilderAgenticAuthoringTurnStreamConnectionError, PageBuilderAgenticAuthoringTurnStreamEvent, PageBuilderAgenticAuthoringTurnStreamRequest, PageBuilderAgenticAuthoringTurnStreamStartResponse, PageBuilderApplyRequest, PageBuilderApplyResult, PageBuilderCompiledFormPatch, PageBuilderMinimalFormPlanResult, PageBuilderPreviewDiagnostics, PageBuilderPreviewResult, PageBuilderProjectKnowledgeAudit, UiCompositionPlan, UiCompositionPlanBinding, UiCompositionPlanCompileResult, UiCompositionPlanComponentPortEndpoint, UiCompositionPlanContextScope, UiCompositionPlanContextScopeTarget, UiCompositionPlanContextValue, UiCompositionPlanDiagnostic, UiCompositionPlanEndpoint, UiCompositionPlanGlobalActionEndpoint, UiCompositionPlanInputSource, UiCompositionPlanSelectionSync, UiCompositionPlanSelectionSyncMapping, UiCompositionPlanSourceEndpoint, UiCompositionPlanStateEndpoint, UiCompositionPlanTargetEndpoint, UiCompositionPlanTransform, UiCompositionPlanWidget, ValueKind };