@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 +21 -0
- package/ai/component-registry.json +15 -9
- package/fesm2022/praxisui-page-builder.mjs +362 -62
- package/package.json +4 -4
- package/types/praxisui-page-builder.d.ts +53 -1
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-
|
|
3
|
+
"generatedAt": "2026-08-11T17:36:05.628Z",
|
|
4
4
|
"packageName": "@praxisui/page-builder",
|
|
5
|
-
"packageVersion": "9.0.4
|
|
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": "
|
|
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": "
|
|
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\":{\"
|
|
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": "
|
|
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
|
-
|
|
1047
|
+
const group = new FormGroup({
|
|
1043
1048
|
id: new FormControl(seed?.id || '', { nonNullable: true, validators: [Validators.required] }),
|
|
1044
|
-
label: new FormControl(seed?.label
|
|
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
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
if (
|
|
1067
|
-
shell.
|
|
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 =
|
|
1078
|
-
.map((
|
|
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
|
-
|
|
1230
|
-
|
|
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 (
|
|
1236
|
-
action.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
|
-
<
|
|
1418
|
-
|
|
1419
|
-
|
|
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:
|
|
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
|
-
<
|
|
1832
|
-
|
|
1833
|
-
|
|
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:
|
|
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': '
|
|
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': '
|
|
2980
|
-
'praxis.pageBuilder.editor.fallback.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
|
|
2988
|
-
'praxis.pageBuilder.editor.tabs.grouping': 'Agrupamento
|
|
2989
|
-
'praxis.pageBuilder.editor.tabs.devices': '
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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': '
|
|
3023
|
-
'praxis.pageBuilder.editor.devices.title': '
|
|
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
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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
|
|
14062
|
+
widgets,
|
|
14032
14063
|
composition: {
|
|
14033
14064
|
version: '1.0.0',
|
|
14034
|
-
links:
|
|
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 =
|
|
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
|
|
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
|
|
12
|
-
"@praxisui/core": "^9.0.4
|
|
13
|
-
"@praxisui/settings-panel": "^9.0.4
|
|
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 };
|