@praxisui/page-builder 9.0.66 → 9.0.67
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 +53 -0
- package/ai/component-registry.json +13 -7
- package/fesm2022/praxisui-page-builder.mjs +240 -31
- package/package.json +4 -4
- package/types/praxisui-page-builder.d.ts +16 -1
package/README.md
CHANGED
|
@@ -6,6 +6,23 @@ Use this package when an application must let users compose governed operational
|
|
|
6
6
|
|
|
7
7
|
## Widget shell editor integrity
|
|
8
8
|
|
|
9
|
+
The shell editor exposes **Keep header visible while scrolling the page**
|
|
10
|
+
(`widget.shell.stickyHeader`). It retains the current title/subtitle rather than
|
|
11
|
+
copying record state into the host. Apply/save/reopen/reset use the same shell
|
|
12
|
+
document and the existing identity-authoring capability governs the option.
|
|
13
|
+
Pinning is visible in presentation mode, not while dragging/editing the canvas;
|
|
14
|
+
hidden headers, collapsed/overlay widgets and scroll recovery retain their own
|
|
15
|
+
behavior. Hosts with a fixed heading provide the Core CSS length
|
|
16
|
+
`--pdx-shell-sticky-header-offset`. The default remains normal flow.
|
|
17
|
+
|
|
18
|
+
The page editor keeps Material tab pagination/keyboard navigation with wrapped labels
|
|
19
|
+
for narrow panels. Its configuration summary is progressively disclosed; when hosted
|
|
20
|
+
in Settings Panel it does not repeat the shell's title, reset action or dirty status.
|
|
21
|
+
The selected widget/layout remains visible while scrolling the limits inspector.
|
|
22
|
+
Rendered-height diagnostics appear at the visible height field only; diagnostics for
|
|
23
|
+
another target or a hidden height field remain in the section summary. The validity
|
|
24
|
+
and apply/save protocol are unchanged.
|
|
25
|
+
|
|
9
26
|
Dynamic Page height validation follows viewport resize and Angular render completion
|
|
10
27
|
while the editor remains open. It reuses the runtime's transient validator and updates
|
|
11
28
|
the existing field diagnostics/Apply/Save gates only when the diagnostic changes.
|
|
@@ -51,6 +68,22 @@ content-aware minimums remain important, including in restored layouts; changing
|
|
|
51
68
|
recipe does not silently rewrite a saved user page. Auto-content rows still do not
|
|
52
69
|
provide pixel-continuous vertical shrinking of a one-row widget.
|
|
53
70
|
|
|
71
|
+
### Device composition
|
|
72
|
+
|
|
73
|
+
**Page settings → Responsiveness** opens on Mobile and shows one device at a time.
|
|
74
|
+
**Stack widgets** prepares a single-column variant in the base canvas's visual reading
|
|
75
|
+
order, with natural content height. It replaces positions and width bounds only for
|
|
76
|
+
that device, preserves hidden widgets and other overrides, and leaves the base and
|
|
77
|
+
other device layouts unchanged. Apply/Save materializes the existing
|
|
78
|
+
`deviceLayouts[device].canvas` contract; Reset restores the original draft.
|
|
79
|
+
Advanced JSON overrides remain available under **Advanced overrides**.
|
|
80
|
+
|
|
81
|
+
Reducing a device's column count also reflows inherited base positions in Core.
|
|
82
|
+
Explicit device positions remain authoritative. With no device variant the base
|
|
83
|
+
canvas is still inherited; the editor does not silently apply a mobile template.
|
|
84
|
+
Grouping settings are preserved for flow layouts; canvas geometry currently takes
|
|
85
|
+
precedence over grouping. The editor explains this distinction in the Grouping tab.
|
|
86
|
+
|
|
54
87
|
### Visual widget limits
|
|
55
88
|
|
|
56
89
|
Open **Page settings → Widget limits** in the default Dynamic Page editor to set
|
|
@@ -527,3 +560,23 @@ Main exports:
|
|
|
527
560
|
- Use component-owned config editors through metadata instead of duplicating widget-specific editors in Page Builder.
|
|
528
561
|
- Keep diagnostics and streaming opt-in for hosts that need auditability or richer authoring feedback.
|
|
529
562
|
- Use the official documentation for extended recipes, playground routes, and advanced AI authoring flows.
|
|
563
|
+
|
|
564
|
+
### Montagem manual de master detail a partir de uma página vazia
|
|
565
|
+
|
|
566
|
+
1. Insira Filter, Table e Dynamic Form e conecte-os ao recurso pelo catálogo.
|
|
567
|
+
2. No filtro, carregue o schema e selecione os campos sempre visíveis.
|
|
568
|
+
3. Na tabela, habilite seleção única por clique e escolha paginação no servidor para APIs paginadas; no formulário, escolha modo `view`.
|
|
569
|
+
4. Crie as conexões `Filter.change → Table.queryContext` e `Table.selectionChange → Dynamic Form.resourceId`. O editor usa os contratos públicos das portas para propor as transformações do filtro e do identificador.
|
|
570
|
+
5. Configure a grade, salve a página e reabra para conferir filtro, seleção e detalhe.
|
|
571
|
+
|
|
572
|
+
Ao converter uma página sem posições em grade, o editor acrescenta posições para todos os widgets e usa altura de conteúdo como padrão. Isso garante visibilidade; a distribuição lado a lado continua sendo uma decisão de layout do autor. O diagrama radial mantém os modos de edição e apresentação.
|
|
573
|
+
|
|
574
|
+
### Menu de ações e tamanho preciso
|
|
575
|
+
|
|
576
|
+
O editor de shell aceita `placement: 'menu'` para manter uma ação em Mais ações mesmo
|
|
577
|
+
com espaço livre no cabeçalho. A edição do canvas oferece `Mais ações → Tamanho…`,
|
|
578
|
+
com largura em colunas e altura em linhas ou pixels conforme `autoRows`. O ajuste usa
|
|
579
|
+
as restrições e o layout de dispositivo canônicos, com prévia e aplicação explícita.
|
|
580
|
+
Selecionar o widget continua expondo suas alças; o ícone dedicado e os painéis de setas
|
|
581
|
+
separados foram removidos. O editor oferece presets exatos de largura e recuperação do último resize da sessão,
|
|
582
|
+
incluindo vizinhos. Clique duplo e histórico geral continuam fora desta entrega.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-07T00:54:55.301Z",
|
|
4
4
|
"packageName": "@praxisui/page-builder",
|
|
5
|
-
"packageVersion": "9.0.
|
|
5
|
+
"packageVersion": "9.0.67",
|
|
6
6
|
"sourceRegistry": "praxis-component-registry-ingestion",
|
|
7
7
|
"sourceRegistryVersion": "1.0.0",
|
|
8
8
|
"componentCount": 1,
|
|
@@ -2059,7 +2059,13 @@
|
|
|
2059
2059
|
"type": "string"
|
|
2060
2060
|
},
|
|
2061
2061
|
"shell": {
|
|
2062
|
-
"type": "object"
|
|
2062
|
+
"type": "object",
|
|
2063
|
+
"properties": {
|
|
2064
|
+
"stickyHeader": {
|
|
2065
|
+
"type": "boolean",
|
|
2066
|
+
"description": "Retain the existing header within the widget during page scrolling; suspended for drag authoring, collapsed/overlay states and scroll recovery."
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2063
2069
|
}
|
|
2064
2070
|
}
|
|
2065
2071
|
},
|
|
@@ -3783,9 +3789,9 @@
|
|
|
3783
3789
|
{
|
|
3784
3790
|
"chunkIndex": 10,
|
|
3785
3791
|
"chunkKind": "authoring_manifest",
|
|
3786
|
-
"content": "{\"operationId\":\"widget.shell.configure\",\"schemaVersion\":\"1.0.0\",\"manifestVersion\":\"1.5.0\",\"componentId\":\"praxis-page-builder\",\"ownerPackage\":\"@praxisui/page-builder\",\"configSchemaId\":\"WidgetPageDefinition\",\"chunkSection\":\"operation_card\",\"semanticSummary\":\"Configure widget shell Effects: compile-domain-patch. Rename the employees widget card and enable the refresh shell action.\",\"scope\":\"templating\",\"target\":{\"kind\":\"widgetShell\",\"resolver\":\"widget-shell-by-widget-key\",\"required\":true},\"inputContract\":{\"required\":[\"widgetKey\"],\"properties\":[\"widgetKey\",\"shell\"],\"semanticTerms\":[\"widgetKey\",\"shell\"]},\"effects\":[\"compile-domain-patch\"],\"affectedPaths\":[\"widgets[].shell\"],\"validators\":[{\"validatorId\":\"widget-exists\",\"level\":\"error\",\"code\":\"PAGE_BUILDER_WIDGET_EXISTS\",\"description\":\"Target widget must exist.\"},{\"validatorId\":\"widget-shell-shape-valid\",\"level\":\"error\",\"code\":\"PAGE_BUILDER_SHELL_SHAPE\",\"description\":\"Widget shell config must match the shell editor contract.\"},{\"validatorId\":\"settings-panel-bridge-available\",\"level\":\"error\",\"code\":\"PAGE_BUILDER_SETTINGS_BRIDGE\",\"description\":\"Settings panel bridge must be available for shell/page editors.\"},{\"validatorId\":\"child-inputs-not-mutated\",\"level\":\"error\",\"code\":\"PAGE_BUILDER_CHILD_INPUTS_NOT_MUTATED\",\"description\":\"Shell edits cannot mutate child inputs.\"}],\"preconditions\":[\"page-loaded\",\"target-widget-exists\"],\"submissionImpact\":\"config-only\",\"presentationAffordances\":[],\"positiveExamples\":[{\"id\":\"configure-widget-shell\",\"request\":\"Rename the employees widget card and enable the refresh shell action.\",\"operationId\":\"widget.shell.configure\",\"target\":\"employees-table\",\"params\":{\"widgetKey\":\"employees-table\",\"shell\":{\"title\":\"Employees\",\"actions\":[{\"id\":\"refresh\"}]}},\"isPositive\":true}],\"negativeExamples\":[]}",
|
|
3792
|
+
"content": "{\"operationId\":\"widget.shell.configure\",\"schemaVersion\":\"1.0.0\",\"manifestVersion\":\"1.5.0\",\"componentId\":\"praxis-page-builder\",\"ownerPackage\":\"@praxisui/page-builder\",\"configSchemaId\":\"WidgetPageDefinition\",\"chunkSection\":\"operation_card\",\"semanticSummary\":\"Configure widget shell Effects: compile-domain-patch. Rename the employees widget card and enable the refresh shell action.\",\"scope\":\"templating\",\"target\":{\"kind\":\"widgetShell\",\"resolver\":\"widget-shell-by-widget-key\",\"required\":true},\"inputContract\":{\"required\":[\"widgetKey\"],\"properties\":[\"widgetKey\",\"shell\"],\"semanticTerms\":[\"widgetKey\",\"shell\",\"shell.stickyHeader\"]},\"effects\":[\"compile-domain-patch\"],\"affectedPaths\":[\"widgets[].shell\"],\"validators\":[{\"validatorId\":\"widget-exists\",\"level\":\"error\",\"code\":\"PAGE_BUILDER_WIDGET_EXISTS\",\"description\":\"Target widget must exist.\"},{\"validatorId\":\"widget-shell-shape-valid\",\"level\":\"error\",\"code\":\"PAGE_BUILDER_SHELL_SHAPE\",\"description\":\"Widget shell config must match the shell editor contract.\"},{\"validatorId\":\"settings-panel-bridge-available\",\"level\":\"error\",\"code\":\"PAGE_BUILDER_SETTINGS_BRIDGE\",\"description\":\"Settings panel bridge must be available for shell/page editors.\"},{\"validatorId\":\"child-inputs-not-mutated\",\"level\":\"error\",\"code\":\"PAGE_BUILDER_CHILD_INPUTS_NOT_MUTATED\",\"description\":\"Shell edits cannot mutate child inputs.\"}],\"preconditions\":[\"page-loaded\",\"target-widget-exists\"],\"submissionImpact\":\"config-only\",\"presentationAffordances\":[],\"positiveExamples\":[{\"id\":\"configure-widget-shell\",\"request\":\"Rename the employees widget card and enable the refresh shell action.\",\"operationId\":\"widget.shell.configure\",\"target\":\"employees-table\",\"params\":{\"widgetKey\":\"employees-table\",\"shell\":{\"title\":\"Employees\",\"actions\":[{\"id\":\"refresh\"}]}},\"isPositive\":true}],\"negativeExamples\":[]}",
|
|
3787
3793
|
"sourcePointer": "projects/praxis-page-builder/src/lib/ai/praxis-page-builder-authoring-manifest.ts",
|
|
3788
|
-
"contentHash": "
|
|
3794
|
+
"contentHash": "12355c4633b7f09a4c4e46d4edac2f23a2ff0e45b6d2a27251ec93b181e50b6e",
|
|
3789
3795
|
"sourceKind": "component_definition",
|
|
3790
3796
|
"sourceId": "praxis-page-builder",
|
|
3791
3797
|
"corpusVersion": "1.0.0"
|
|
@@ -3893,9 +3899,9 @@
|
|
|
3893
3899
|
{
|
|
3894
3900
|
"chunkIndex": 21,
|
|
3895
3901
|
"chunkKind": "authoring_manifest",
|
|
3896
|
-
"content": "{\"schemaVersion\":\"1.0.0\",\"manifestVersion\":\"1.5.0\",\"componentId\":\"praxis-page-builder\",\"ownerPackage\":\"@praxisui/page-builder\",\"configSchemaId\":\"WidgetPageDefinition\",\"chunkSection\":\"operations\",\"operations\":[{\"operationId\":\"page.configure\",\"title\":\"Configure page metadata\",\"scope\":\"global\",\"targetKind\":\"page\",\"target\":{\"kind\":\"page\",\"resolver\":\"widget-page-definition-root\",\"ambiguityPolicy\":\"fail\",\"required\":false},\"inputSchema\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"layoutPreset\":{\"type\":\"string\"},\"themePreset\":{\"type\":\"string\"},\"context\":{\"type\":\"object\"},\"metadata\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-page-configure\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition\",\"DynamicPageConfigEditorComponent\",\"SettingsPanelBridge\"],\"writes\":[\"page.title\",\"page.layoutPreset\",\"page.themePreset\",\"page.context\",\"page.metadata\"],\"identityKeys\":[\"pageIdentity.componentId\",\"pageIdentity.scope\"],\"inputSchema\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"layoutPreset\":{\"type\":\"string\"},\"themePreset\":{\"type\":\"string\"},\"context\":{\"type\":\"object\"},\"metadata\":{\"type\":\"object\"}}},\"failureModes\":[\"page-not-loaded\",\"context-not-json\",\"settings-panel-bridge-missing\"],\"description\":\"Updates root page metadata while preserving the renderable WidgetPageDefinition shape.\"}}],\"validators\":[\"widget-page-definition-valid\",\"no-legacy-grid-page-definition\",\"page-context-json-valid\",\"settings-panel-round-trip-valid\"],\"affectedPaths\":[\"title\",\"layoutPreset\",\"themePreset\",\"context\",\"metadata\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\"]},{\"operationId\":\"canvas.configure\",\"title\":\"Configure page canvas\",\"scope\":\"layout\",\"targetKind\":\"canvas\",\"target\":{\"kind\":\"canvas\",\"resolver\":\"widget-page-canvas\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"properties\":{\"columns\":{\"type\":\"number\"},\"rowUnit\":{\"type\":\"number\"},\"gap\":{\"type\":\"number\"},\"items\":{\"type\":\"array\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-canvas-configure\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.canvas\",\"DynamicPageBuilderComponent\"],\"writes\":[\"page.canvas.columns\",\"page.canvas.rowUnit\",\"page.canvas.gap\",\"page.canvas.items\"],\"identityKeys\":[\"canvas\"],\"inputSchema\":{\"type\":\"object\",\"properties\":{\"columns\":{\"type\":\"number\"},\"rowUnit\":{\"type\":\"number\"},\"gap\":{\"type\":\"number\"},\"items\":{\"type\":\"array\"}}},\"failureModes\":[\"canvas-missing\",\"canvas-item-references-missing-widget\",\"invalid-grid-geometry\"],\"description\":\"Configures the top-level canvas without introducing legacy GridPageDefinition semantics.\"}}],\"validators\":[\"canvas-columns-integer\",\"canvas-row-unit-valid\",\"canvas-gap-valid\",\"canvas-items-reference-existing-widgets\"],\"affectedPaths\":[\"canvas.columns\",\"canvas.rowUnit\",\"canvas.gap\",\"canvas.items\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\"]},{\"operationId\":\"widget.add\",\"title\":\"Add widget to page\",\"scope\":\"layout\",\"targetKind\":\"widget\",\"target\":{\"kind\":\"widget\",\"resolver\":\"widget-by-stable-key\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\",\"componentId\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"componentId\":{\"type\":\"string\"},\"title\":{\"type\":\"string\"},\"inputs\":{\"type\":\"object\"},\"canvasItem\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-widget-add\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.widgets\",\"WidgetPageDefinition.canvas\",\"ComponentDocRegistry\",\"PAGE_BUILDER_WIDGET_AI_CATALOGS\"],\"writes\":[\"page.widgets[]\",\"page.canvas.items[]\"],\"identityKeys\":[\"widgetKey\"],\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\",\"componentId\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"componentId\":{\"type\":\"string\"},\"title\":{\"type\":\"string\"},\"inputs\":{\"type\":\"object\"},\"canvasItem\":{\"type\":\"object\"}}},\"failureModes\":[\"widget-key-duplicate\",\"component-not-registered\",\"canvas-item-invalid\",\"child-input-contract-unknown\"],\"description\":\"Adds a registered component as a widget and places it on the canvas using a stable widgetKey.\"}}],\"validators\":[\"widget-key-unique\",\"component-registered\",\"canvas-item-valid\",\"child-inputs-delegated\",\"widget-key-not-array-index\"],\"affectedPaths\":[\"widgets[]\",\"canvas.items[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"component-catalog-loaded\"]},{\"operationId\":\"widget.remove\",\"title\":\"Remove widget from page\",\"scope\":\"layout\",\"targetKind\":\"widget\",\"target\":{\"kind\":\"widget\",\"resolver\":\"widget-by-stable-key\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"confirmed\":{\"type\":\"boolean\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-widget-remove\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.widgets\",\"WidgetPageDefinition.canvas.items\",\"WidgetPageDefinition.composition.links\"],\"writes\":[\"page.widgets[]\",\"page.canvas.items[]\",\"page.composition.links[]\"],\"identityKeys\":[\"widgetKey\"],\"failureModes\":[\"widget-not-found\",\"destructive-removal-not-confirmed\",\"dangling-composition-link\"],\"description\":\"Removes a widget and cleans canvas items and composition links that reference it.\"}}],\"destructive\":true,\"requiresConfirmation\":true,\"validators\":[\"widget-exists\",\"destructive-removal-confirmed\",\"composition-links-cleaned\",\"canvas-items-reference-existing-widgets\"],\"affectedPaths\":[\"widgets[]\",\"canvas.items[]\",\"composition.links[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"target-widget-exists\"]},{\"operationId\":\"widget.moveResize\",\"title\":\"Move or resize widget\",\"scope\":\"layout\",\"targetKind\":\"widget\",\"target\":{\"kind\":\"widget\",\"resolver\":\"widget-by-stable-key\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\",\"canvasItem\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"canvasItem\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-widget-move-resize\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.canvas.items\",\"WidgetPageDefinition.widgets\"],\"writes\":[\"page.canvas.items[]\"],\"identityKeys\":[\"widgetKey\"],\"failureModes\":[\"widget-not-found\",\"canvas-item-invalid\",\"canvas-overlap-invalid\"],\"description\":\"Updates canvas placement for an existing widget using widgetKey as the stable identity.\"}}],\"validators\":[\"widget-exists\",\"canvas-item-valid\",\"canvas-item-targets-existing-widget\",\"canvas-overlap-policy-valid\"],\"affectedPaths\":[\"canvas.items[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"target-widget-exists\"]},{\"operationId\":\"widget.shell.configure\",\"title\":\"Configure widget shell\",\"scope\":\"templating\",\"targetKind\":\"widgetShell\",\"target\":{\"kind\":\"widgetShell\",\"resolver\":\"widget-shell-by-widget-key\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"shell\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-widget-shell-configure\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.widgets\",\"WidgetShellEditorComponent\",\"SettingsPanelBridge\"],\"writes\":[\"page.widgets[].shell\"],\"identityKeys\":[\"widgetKey\"],\"failureModes\":[\"widget-not-found\",\"settings-panel-bridge-missing\",\"shell-config-invalid\"],\"description\":\"Updates the page-builder-owned widget shell without editing child component inputs.\"}}],\"validators\":[\"widget-exists\",\"widget-shell-shape-valid\",\"settings-panel-bridge-available\",\"child-inputs-not-mutated\"],\"affectedPaths\":[\"widgets[].shell\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"target-widget-exists\"]}]}",
|
|
3902
|
+
"content": "{\"schemaVersion\":\"1.0.0\",\"manifestVersion\":\"1.5.0\",\"componentId\":\"praxis-page-builder\",\"ownerPackage\":\"@praxisui/page-builder\",\"configSchemaId\":\"WidgetPageDefinition\",\"chunkSection\":\"operations\",\"operations\":[{\"operationId\":\"page.configure\",\"title\":\"Configure page metadata\",\"scope\":\"global\",\"targetKind\":\"page\",\"target\":{\"kind\":\"page\",\"resolver\":\"widget-page-definition-root\",\"ambiguityPolicy\":\"fail\",\"required\":false},\"inputSchema\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"layoutPreset\":{\"type\":\"string\"},\"themePreset\":{\"type\":\"string\"},\"context\":{\"type\":\"object\"},\"metadata\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-page-configure\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition\",\"DynamicPageConfigEditorComponent\",\"SettingsPanelBridge\"],\"writes\":[\"page.title\",\"page.layoutPreset\",\"page.themePreset\",\"page.context\",\"page.metadata\"],\"identityKeys\":[\"pageIdentity.componentId\",\"pageIdentity.scope\"],\"inputSchema\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"layoutPreset\":{\"type\":\"string\"},\"themePreset\":{\"type\":\"string\"},\"context\":{\"type\":\"object\"},\"metadata\":{\"type\":\"object\"}}},\"failureModes\":[\"page-not-loaded\",\"context-not-json\",\"settings-panel-bridge-missing\"],\"description\":\"Updates root page metadata while preserving the renderable WidgetPageDefinition shape.\"}}],\"validators\":[\"widget-page-definition-valid\",\"no-legacy-grid-page-definition\",\"page-context-json-valid\",\"settings-panel-round-trip-valid\"],\"affectedPaths\":[\"title\",\"layoutPreset\",\"themePreset\",\"context\",\"metadata\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\"]},{\"operationId\":\"canvas.configure\",\"title\":\"Configure page canvas\",\"scope\":\"layout\",\"targetKind\":\"canvas\",\"target\":{\"kind\":\"canvas\",\"resolver\":\"widget-page-canvas\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"properties\":{\"columns\":{\"type\":\"number\"},\"rowUnit\":{\"type\":\"number\"},\"gap\":{\"type\":\"number\"},\"items\":{\"type\":\"array\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-canvas-configure\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.canvas\",\"DynamicPageBuilderComponent\"],\"writes\":[\"page.canvas.columns\",\"page.canvas.rowUnit\",\"page.canvas.gap\",\"page.canvas.items\"],\"identityKeys\":[\"canvas\"],\"inputSchema\":{\"type\":\"object\",\"properties\":{\"columns\":{\"type\":\"number\"},\"rowUnit\":{\"type\":\"number\"},\"gap\":{\"type\":\"number\"},\"items\":{\"type\":\"array\"}}},\"failureModes\":[\"canvas-missing\",\"canvas-item-references-missing-widget\",\"invalid-grid-geometry\"],\"description\":\"Configures the top-level canvas without introducing legacy GridPageDefinition semantics.\"}}],\"validators\":[\"canvas-columns-integer\",\"canvas-row-unit-valid\",\"canvas-gap-valid\",\"canvas-items-reference-existing-widgets\"],\"affectedPaths\":[\"canvas.columns\",\"canvas.rowUnit\",\"canvas.gap\",\"canvas.items\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\"]},{\"operationId\":\"widget.add\",\"title\":\"Add widget to page\",\"scope\":\"layout\",\"targetKind\":\"widget\",\"target\":{\"kind\":\"widget\",\"resolver\":\"widget-by-stable-key\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\",\"componentId\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"componentId\":{\"type\":\"string\"},\"title\":{\"type\":\"string\"},\"inputs\":{\"type\":\"object\"},\"canvasItem\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-widget-add\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.widgets\",\"WidgetPageDefinition.canvas\",\"ComponentDocRegistry\",\"PAGE_BUILDER_WIDGET_AI_CATALOGS\"],\"writes\":[\"page.widgets[]\",\"page.canvas.items[]\"],\"identityKeys\":[\"widgetKey\"],\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\",\"componentId\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"componentId\":{\"type\":\"string\"},\"title\":{\"type\":\"string\"},\"inputs\":{\"type\":\"object\"},\"canvasItem\":{\"type\":\"object\"}}},\"failureModes\":[\"widget-key-duplicate\",\"component-not-registered\",\"canvas-item-invalid\",\"child-input-contract-unknown\"],\"description\":\"Adds a registered component as a widget and places it on the canvas using a stable widgetKey.\"}}],\"validators\":[\"widget-key-unique\",\"component-registered\",\"canvas-item-valid\",\"child-inputs-delegated\",\"widget-key-not-array-index\"],\"affectedPaths\":[\"widgets[]\",\"canvas.items[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"component-catalog-loaded\"]},{\"operationId\":\"widget.remove\",\"title\":\"Remove widget from page\",\"scope\":\"layout\",\"targetKind\":\"widget\",\"target\":{\"kind\":\"widget\",\"resolver\":\"widget-by-stable-key\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"confirmed\":{\"type\":\"boolean\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-widget-remove\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.widgets\",\"WidgetPageDefinition.canvas.items\",\"WidgetPageDefinition.composition.links\"],\"writes\":[\"page.widgets[]\",\"page.canvas.items[]\",\"page.composition.links[]\"],\"identityKeys\":[\"widgetKey\"],\"failureModes\":[\"widget-not-found\",\"destructive-removal-not-confirmed\",\"dangling-composition-link\"],\"description\":\"Removes a widget and cleans canvas items and composition links that reference it.\"}}],\"destructive\":true,\"requiresConfirmation\":true,\"validators\":[\"widget-exists\",\"destructive-removal-confirmed\",\"composition-links-cleaned\",\"canvas-items-reference-existing-widgets\"],\"affectedPaths\":[\"widgets[]\",\"canvas.items[]\",\"composition.links[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"target-widget-exists\"]},{\"operationId\":\"widget.moveResize\",\"title\":\"Move or resize widget\",\"scope\":\"layout\",\"targetKind\":\"widget\",\"target\":{\"kind\":\"widget\",\"resolver\":\"widget-by-stable-key\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\",\"canvasItem\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"canvasItem\":{\"type\":\"object\"}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-widget-move-resize\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.canvas.items\",\"WidgetPageDefinition.widgets\"],\"writes\":[\"page.canvas.items[]\"],\"identityKeys\":[\"widgetKey\"],\"failureModes\":[\"widget-not-found\",\"canvas-item-invalid\",\"canvas-overlap-invalid\"],\"description\":\"Updates canvas placement for an existing widget using widgetKey as the stable identity.\"}}],\"validators\":[\"widget-exists\",\"canvas-item-valid\",\"canvas-item-targets-existing-widget\",\"canvas-overlap-policy-valid\"],\"affectedPaths\":[\"canvas.items[]\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"target-widget-exists\"]},{\"operationId\":\"widget.shell.configure\",\"title\":\"Configure widget shell\",\"scope\":\"templating\",\"targetKind\":\"widgetShell\",\"target\":{\"kind\":\"widgetShell\",\"resolver\":\"widget-shell-by-widget-key\",\"ambiguityPolicy\":\"fail\",\"required\":true},\"inputSchema\":{\"type\":\"object\",\"required\":[\"widgetKey\"],\"properties\":{\"widgetKey\":{\"type\":\"string\"},\"shell\":{\"type\":\"object\",\"properties\":{\"stickyHeader\":{\"type\":\"boolean\",\"description\":\"Retain the existing header within the widget during page scrolling; suspended for drag authoring, collapsed/overlay states and scroll recovery.\"}}}}},\"effects\":[{\"kind\":\"compile-domain-patch\",\"handler\":\"page-builder-widget-shell-configure\",\"handlerContract\":{\"reads\":[\"WidgetPageDefinition.widgets\",\"WidgetShellEditorComponent\",\"SettingsPanelBridge\"],\"writes\":[\"page.widgets[].shell\"],\"identityKeys\":[\"widgetKey\"],\"failureModes\":[\"widget-not-found\",\"settings-panel-bridge-missing\",\"shell-config-invalid\"],\"description\":\"Updates the page-builder-owned widget shell without editing child component inputs.\"}}],\"validators\":[\"widget-exists\",\"widget-shell-shape-valid\",\"settings-panel-bridge-available\",\"child-inputs-not-mutated\"],\"affectedPaths\":[\"widgets[].shell\"],\"submissionImpact\":\"config-only\",\"preconditions\":[\"page-loaded\",\"target-widget-exists\"]}]}",
|
|
3897
3903
|
"sourcePointer": "projects/praxis-page-builder/src/lib/ai/praxis-page-builder-authoring-manifest.ts",
|
|
3898
|
-
"contentHash": "
|
|
3904
|
+
"contentHash": "9c3f606edc681ae101c35f0f28dd14c59a3978e73357a81b1c85080d9280c239",
|
|
3899
3905
|
"sourceKind": "component_definition",
|
|
3900
3906
|
"sourceId": "praxis-page-builder",
|
|
3901
3907
|
"corpusVersion": "1.0.0"
|
|
@@ -363,6 +363,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
363
363
|
}] } });
|
|
364
364
|
|
|
365
365
|
const PRAXIS_PAGE_BUILDER_EN_US = {
|
|
366
|
+
"praxis.pageBuilder.editor.devices.target": "Device to configure",
|
|
367
|
+
"praxis.pageBuilder.editor.devices.advanced": "Advanced overrides",
|
|
368
|
+
"praxis.pageBuilder.editor.grouping.canvasNotice": "Canvas positions control this page. Groups below are preserved for flow layouts and do not arrange widgets in the canvas. Use the widget move and resize controls to place them side by side.",
|
|
369
|
+
"praxis.pageBuilder.editor.devices.stack": "Stack widgets",
|
|
370
|
+
"praxis.pageBuilder.editor.devices.stackHelp": "Use full width and natural height, in reading order. This replaces positions and width limits only for this device; other layouts stay unchanged.",
|
|
371
|
+
"praxis.pageBuilder.editor.devices.stacked": "Single-column layout prepared. Save to apply; Reset undoes the changes.",
|
|
372
|
+
'praxis.pageBuilder.shell.menuPlacement': 'More actions menu',
|
|
366
373
|
'praxis.pageBuilder.editor.limits.tab': "Widget limits",
|
|
367
374
|
'praxis.pageBuilder.editor.limits.help': 'Grid limits use columns and rows. Content-row layouts also offer pixel height below. Leave limits blank for defaults or to inherit the base layout.',
|
|
368
375
|
'praxis.pageBuilder.editor.limits.target': "Widget and layout",
|
|
@@ -388,6 +395,7 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
|
|
|
388
395
|
'praxis.pageBuilder.editor.limits.errors.columns': "The minimum width exceeds the grid columns.",
|
|
389
396
|
'praxis.pageBuilder.editor.limits.errors.size': "The current size is outside these limits. Resize the widget first, or review the limits.",
|
|
390
397
|
'praxis.pageBuilder.shell.invalidCss': 'Invalid CSS value. Use a valid color, dimension or expression for this field.',
|
|
398
|
+
'praxis.pageBuilder.shell.stickyHeader': 'Keep header visible while scrolling the page',
|
|
391
399
|
'praxis.pageBuilder.authoringSource.alert.title': 'Semantic lineage requires attention',
|
|
392
400
|
'praxis.pageBuilder.authoringSource.alert.description': 'The page remains available, but this source will not be reused until new governed authoring restores its provenance.',
|
|
393
401
|
'praxis.pageBuilder.authoringSource.diagnostics.authoring-source-schema-invalid': 'The authoring envelope does not match the schema supported by Page Builder.',
|
|
@@ -1054,6 +1062,7 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
|
|
|
1054
1062
|
'praxis.pageBuilder.connections.draft.backToEdit': 'Back to editing',
|
|
1055
1063
|
'praxis.pageBuilder.connections.draft.present': 'Present diagram',
|
|
1056
1064
|
'praxis.pageBuilder.connections.draft.title': 'Connection draft',
|
|
1065
|
+
'praxis.pageBuilder.connections.draft.prerequisites': 'Enable the source event in its component settings. For row selection, enable single selection; configure the detail as read-only when needed.',
|
|
1057
1066
|
'praxis.pageBuilder.connections.draft.help': 'Choose the source and destination. Review the route and delivered value before applying.',
|
|
1058
1067
|
'praxis.pageBuilder.connections.draft.source': '1 · Source / event',
|
|
1059
1068
|
'praxis.pageBuilder.connections.draft.target': '2 · Destination / input',
|
|
@@ -1092,6 +1101,13 @@ const PRAXIS_PAGE_BUILDER_EN_US = {
|
|
|
1092
1101
|
};
|
|
1093
1102
|
|
|
1094
1103
|
const PRAXIS_PAGE_BUILDER_PT_BR = {
|
|
1104
|
+
"praxis.pageBuilder.editor.devices.target": "Dispositivo para configurar",
|
|
1105
|
+
"praxis.pageBuilder.editor.devices.advanced": "Ajustes avançados",
|
|
1106
|
+
"praxis.pageBuilder.editor.grouping.canvasNotice": "As posições no canvas controlam esta página. Os agrupamentos abaixo são preservados para layouts de fluxo e não organizam os widgets no canvas. Para colocá-los lado a lado, use os controles de mover e redimensionar de cada widget.",
|
|
1107
|
+
"praxis.pageBuilder.editor.devices.stack": "Empilhar widgets",
|
|
1108
|
+
"praxis.pageBuilder.editor.devices.stackHelp": "Usa a largura total e altura natural, na ordem de leitura. Substitui as posições e os limites de largura apenas neste dispositivo; os outros layouts são preservados.",
|
|
1109
|
+
"praxis.pageBuilder.editor.devices.stacked": "Layout de uma coluna preparado. Salve para aplicar; Redefinir desfaz os ajustes.",
|
|
1110
|
+
'praxis.pageBuilder.shell.menuPlacement': 'Menu Mais ações',
|
|
1095
1111
|
'praxis.pageBuilder.editor.limits.tab': "Limites dos widgets",
|
|
1096
1112
|
'praxis.pageBuilder.editor.limits.help': 'Os limites da grade usam colunas e linhas. Em linhas de conteúdo, a seção de altura também permite pixels. Deixe os limites em branco para usar os padrões ou herdar o layout base.',
|
|
1097
1113
|
'praxis.pageBuilder.editor.limits.target': "Widget e layout",
|
|
@@ -1117,6 +1133,7 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
|
|
|
1117
1133
|
'praxis.pageBuilder.editor.limits.errors.columns': "A largura mínima ultrapassa as colunas da grade.",
|
|
1118
1134
|
'praxis.pageBuilder.editor.limits.errors.size': "O tamanho atual está fora destes limites. Redimensione o widget antes ou revise os limites.",
|
|
1119
1135
|
'praxis.pageBuilder.shell.invalidCss': 'Valor CSS inválido. Use uma cor, medida ou expressão válida para este campo.',
|
|
1136
|
+
'praxis.pageBuilder.shell.stickyHeader': 'Manter cabeçalho visível ao rolar a página',
|
|
1120
1137
|
'praxis.pageBuilder.authoringSource.alert.title': 'Linhagem semântica requer atenção',
|
|
1121
1138
|
'praxis.pageBuilder.authoringSource.alert.description': 'A página continua disponível, mas esta fonte não será reutilizada até que uma nova autoria governada restabeleça sua proveniência.',
|
|
1122
1139
|
'praxis.pageBuilder.authoringSource.diagnostics.authoring-source-schema-invalid': 'O envelope de autoria não atende ao schema suportado pelo Page Builder.',
|
|
@@ -1783,6 +1800,7 @@ const PRAXIS_PAGE_BUILDER_PT_BR = {
|
|
|
1783
1800
|
'praxis.pageBuilder.connections.draft.backToEdit': 'Voltar à edição',
|
|
1784
1801
|
'praxis.pageBuilder.connections.draft.present': 'Apresentar diagrama',
|
|
1785
1802
|
'praxis.pageBuilder.connections.draft.title': 'Rascunho da conexão',
|
|
1803
|
+
'praxis.pageBuilder.connections.draft.prerequisites': 'Habilite o evento nas configurações do componente de origem. Para seleção de linhas, habilite a seleção única; configure o detalhe como somente leitura quando necessário.',
|
|
1786
1804
|
'praxis.pageBuilder.connections.draft.help': 'Escolha a origem e o destino. Revise o percurso e o valor entregue antes de aplicar.',
|
|
1787
1805
|
'praxis.pageBuilder.connections.draft.source': '1 · Origem / evento',
|
|
1788
1806
|
'praxis.pageBuilder.connections.draft.target': '2 · Destino / entrada',
|
|
@@ -2464,9 +2482,13 @@ const SET_INPUT_PRESETS = {
|
|
|
2464
2482
|
class WidgetShellEditorComponent {
|
|
2465
2483
|
registry;
|
|
2466
2484
|
i18n = inject(PraxisI18nService);
|
|
2485
|
+
menuPlacementLabel() { return this.i18n.t('praxis.pageBuilder.shell.menuPlacement'); }
|
|
2467
2486
|
appearanceError() {
|
|
2468
2487
|
return this.i18n.t('praxis.pageBuilder.shell.invalidCss');
|
|
2469
2488
|
}
|
|
2489
|
+
stickyHeaderLabel() {
|
|
2490
|
+
return this.i18n.t('praxis.pageBuilder.shell.stickyHeader');
|
|
2491
|
+
}
|
|
2470
2492
|
form = new FormGroup({
|
|
2471
2493
|
kind: new FormControl('dashboard-card', {
|
|
2472
2494
|
nonNullable: true,
|
|
@@ -2479,6 +2501,7 @@ class WidgetShellEditorComponent {
|
|
|
2479
2501
|
showHeader: new FormControl('auto', {
|
|
2480
2502
|
nonNullable: true,
|
|
2481
2503
|
}),
|
|
2504
|
+
stickyHeader: new FormControl(false, { nonNullable: true }),
|
|
2482
2505
|
collapsible: new FormControl(true, { nonNullable: true }),
|
|
2483
2506
|
cardBg: new FormControl('', { nonNullable: true }),
|
|
2484
2507
|
cardBorder: new FormControl('', { nonNullable: true }),
|
|
@@ -2636,6 +2659,7 @@ class WidgetShellEditorComponent {
|
|
|
2636
2659
|
subtitle: this.editableText(shell?.subtitle),
|
|
2637
2660
|
icon: shell?.icon || '',
|
|
2638
2661
|
showHeader,
|
|
2662
|
+
stickyHeader: shell?.stickyHeader === true,
|
|
2639
2663
|
collapsible: window.collapsible !== false,
|
|
2640
2664
|
cardBg: card.background || '',
|
|
2641
2665
|
cardBorder: card.borderColor || '',
|
|
@@ -2737,6 +2761,7 @@ class WidgetShellEditorComponent {
|
|
|
2737
2761
|
delete shell.subtitle;
|
|
2738
2762
|
delete shell.icon;
|
|
2739
2763
|
delete shell.showHeader;
|
|
2764
|
+
delete shell.stickyHeader;
|
|
2740
2765
|
if (raw.preset)
|
|
2741
2766
|
shell.preset = raw.preset;
|
|
2742
2767
|
const title = this.preserveTextDescriptor(this.initialTitle, raw.title);
|
|
@@ -2749,6 +2774,8 @@ class WidgetShellEditorComponent {
|
|
|
2749
2774
|
shell.icon = raw.icon.trim();
|
|
2750
2775
|
if (raw.showHeader !== 'auto')
|
|
2751
2776
|
shell.showHeader = raw.showHeader === 'true';
|
|
2777
|
+
if (raw.stickyHeader)
|
|
2778
|
+
shell.stickyHeader = true;
|
|
2752
2779
|
const windowActions = {
|
|
2753
2780
|
...(initial?.windowActions || {}),
|
|
2754
2781
|
};
|
|
@@ -2933,7 +2960,7 @@ class WidgetShellEditorComponent {
|
|
|
2933
2960
|
}
|
|
2934
2961
|
previewOverflowCount() {
|
|
2935
2962
|
const actions = this.previewActions().filter((a) => (a.placement || 'header') === 'header');
|
|
2936
|
-
return Math.max(0, actions.length - 3);
|
|
2963
|
+
return Math.max(0, actions.length - 3) + this.previewActions().filter(a => a.placement === 'menu').length;
|
|
2937
2964
|
}
|
|
2938
2965
|
previewWindowActions() {
|
|
2939
2966
|
return this.previewActions().filter((a) => (a.placement || 'header') === 'window');
|
|
@@ -3221,6 +3248,9 @@ class WidgetShellEditorComponent {
|
|
|
3221
3248
|
<mat-option value="false">Nunca</mat-option>
|
|
3222
3249
|
</mat-select>
|
|
3223
3250
|
</mat-form-field>
|
|
3251
|
+
<mat-checkbox [formControl]="form.controls.stickyHeader">
|
|
3252
|
+
{{ stickyHeaderLabel() }}
|
|
3253
|
+
</mat-checkbox>
|
|
3224
3254
|
</div>
|
|
3225
3255
|
|
|
3226
3256
|
<div class="shell-section">Controles de janela</div>
|
|
@@ -3684,6 +3714,7 @@ class WidgetShellEditorComponent {
|
|
|
3684
3714
|
<mat-select formControlName="placement">
|
|
3685
3715
|
<mat-option value="header">Cabeçalho</mat-option>
|
|
3686
3716
|
<mat-option value="window">Janela</mat-option>
|
|
3717
|
+
<mat-option value="menu">{{ menuPlacementLabel() }}</mat-option>
|
|
3687
3718
|
</mat-select>
|
|
3688
3719
|
</mat-form-field>
|
|
3689
3720
|
<mat-form-field appearance="outline">
|
|
@@ -3831,6 +3862,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
3831
3862
|
<mat-option value="false">Nunca</mat-option>
|
|
3832
3863
|
</mat-select>
|
|
3833
3864
|
</mat-form-field>
|
|
3865
|
+
<mat-checkbox [formControl]="form.controls.stickyHeader">
|
|
3866
|
+
{{ stickyHeaderLabel() }}
|
|
3867
|
+
</mat-checkbox>
|
|
3834
3868
|
</div>
|
|
3835
3869
|
|
|
3836
3870
|
<div class="shell-section">Controles de janela</div>
|
|
@@ -4294,6 +4328,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4294
4328
|
<mat-select formControlName="placement">
|
|
4295
4329
|
<mat-option value="header">Cabeçalho</mat-option>
|
|
4296
4330
|
<mat-option value="window">Janela</mat-option>
|
|
4331
|
+
<mat-option value="menu">{{ menuPlacementLabel() }}</mat-option>
|
|
4297
4332
|
</mat-select>
|
|
4298
4333
|
</mat-form-field>
|
|
4299
4334
|
<mat-form-field appearance="outline">
|
|
@@ -4487,13 +4522,24 @@ function deriveConnectionEditorGraph(page, options = {}) {
|
|
|
4487
4522
|
const angle = (index / count) * Math.PI * 2 - Math.PI / 2;
|
|
4488
4523
|
const widgetId = widget.key;
|
|
4489
4524
|
const componentType = widget.definition?.id ?? 'unknown';
|
|
4490
|
-
const
|
|
4525
|
+
const metadata = options.componentMetadata?.(componentType);
|
|
4526
|
+
const contracts = (metadata?.ports ?? []).filter(port => port.exposure?.public !== false && !port.exposure?.deprecated);
|
|
4527
|
+
const inputs = uniqueStrings([
|
|
4528
|
+
...contracts.filter(port => port.direction === 'input').map(port => port.id),
|
|
4529
|
+
...(contracts.length ? [] : metadata?.inputs ?? []).map(input => input.name),
|
|
4530
|
+
...Object.keys(widget.definition?.inputs ?? {}),
|
|
4531
|
+
]);
|
|
4491
4532
|
const outputs = uniqueStrings([
|
|
4533
|
+
...contracts.filter(port => port.direction === 'output').map(port => port.id),
|
|
4534
|
+
...(contracts.length ? [] : metadata?.outputs ?? []).map(output => output.name),
|
|
4492
4535
|
...Object.keys(widget.definition?.outputs ?? {}),
|
|
4493
4536
|
...configuredWidgetEventOutputs(widget),
|
|
4494
4537
|
]);
|
|
4495
|
-
const outputPorts = (outputs.length ? outputs : ['change']).map((port) => componentPort(widgetId, port, 'output', componentType));
|
|
4496
|
-
const inputPorts = prioritizedInputPorts(inputs).map((port) => componentPort(widgetId, port, 'input', componentType));
|
|
4538
|
+
const outputPorts = (outputs.length || metadata ? outputs : ['change']).map((port) => componentPort(widgetId, port, 'output', componentType));
|
|
4539
|
+
const inputPorts = (metadata ? inputs : prioritizedInputPorts(inputs)).map((port) => componentPort(widgetId, port, 'input', componentType));
|
|
4540
|
+
for (const port of [...inputPorts, ...outputPorts]) {
|
|
4541
|
+
port.contract = contracts.find(contract => contract.id === port.label && contract.direction === port.kind);
|
|
4542
|
+
}
|
|
4497
4543
|
for (const nested of collectNestedWidgets(widget)) {
|
|
4498
4544
|
const nestedComponentType = nested.definition.id ?? nested.path.find((segment) => segment.kind === 'widget')?.componentType;
|
|
4499
4545
|
const nestedPrefix = nested.definition.childWidgetKey
|
|
@@ -4787,6 +4833,17 @@ function buildCompositionLinkFromPorts(source, target, links) {
|
|
|
4787
4833
|
};
|
|
4788
4834
|
}
|
|
4789
4835
|
function inferCanonicalTransform(source, target) {
|
|
4836
|
+
if (source.kind === 'output' && target.kind === 'input'
|
|
4837
|
+
&& source.contract?.resourceQueryFieldSource && target.contract?.semanticKind === 'query-context') {
|
|
4838
|
+
return {
|
|
4839
|
+
id: `${source.nodeId}-${target.nodeId}-filter-query-projection`,
|
|
4840
|
+
version: '2.0', phase: 'link-propagation', mode: 'object-fragment',
|
|
4841
|
+
sourceBindings: [{ source: 'event' }],
|
|
4842
|
+
steps: [{ id: 'shape-filter-query', kind: 'object-template', phase: 'link-propagation',
|
|
4843
|
+
input: { source: 'event' }, config: { template: { filters: '${payload}' } } }],
|
|
4844
|
+
output: { semanticKind: 'query-context', cardinality: 'one', stableShape: true, materializationHint: 'replace' },
|
|
4845
|
+
};
|
|
4846
|
+
}
|
|
4790
4847
|
if (!isTableRowSelectionToFormResourceId(source, target)) {
|
|
4791
4848
|
return undefined;
|
|
4792
4849
|
}
|
|
@@ -5946,6 +6003,7 @@ class ConnectionEditorComponent {
|
|
|
5946
6003
|
}, ...(ngDevMode ? [{ debugName: "visibleEdges" }] : /* istanbul ignore next */ []));
|
|
5947
6004
|
selectedEdge = computed(() => this.model().edges.find((edge) => edge.id === this.selectedLinkId()) ?? null, ...(ngDevMode ? [{ debugName: "selectedEdge" }] : /* istanbul ignore next */ []));
|
|
5948
6005
|
editorGraph = computed(() => deriveConnectionEditorGraph(this.workingPage(), {
|
|
6006
|
+
componentMetadata: componentType => this.componentRegistry.get(componentType),
|
|
5949
6007
|
stateNodeTitle: this.tx('connections.editor.stateNode', 'Page state'),
|
|
5950
6008
|
globalActionIds: this.availableGlobalActionIds(),
|
|
5951
6009
|
widgetTitle: widget => {
|
|
@@ -5953,7 +6011,10 @@ class ConnectionEditorComponent {
|
|
|
5953
6011
|
if (title && !title.includes('${'))
|
|
5954
6012
|
return title;
|
|
5955
6013
|
const metadata = this.componentRegistry.get(widget.definition.id);
|
|
5956
|
-
|
|
6014
|
+
if (!metadata)
|
|
6015
|
+
return widget.key;
|
|
6016
|
+
const peers = (this.workingPage()?.widgets ?? []).filter(peer => peer.definition.id === widget.definition.id);
|
|
6017
|
+
return peers.length > 1 ? `${metadata.friendlyName} ${peers.findIndex(peer => peer.key === widget.key) + 1}` : metadata.friendlyName;
|
|
5957
6018
|
},
|
|
5958
6019
|
}), ...(ngDevMode ? [{ debugName: "editorGraph" }] : /* istanbul ignore next */ []));
|
|
5959
6020
|
connectedNodeIds = computed(() => {
|
|
@@ -9103,6 +9164,7 @@ class ConnectionEditorComponent {
|
|
|
9103
9164
|
@for (port of draftTargets(); track port.id) { <option [value]="port.id">{{ draftPortLabel(port) }}</option> }
|
|
9104
9165
|
</select>
|
|
9105
9166
|
</label>
|
|
9167
|
+
<p>{{ tx('connections.draft.prerequisites', 'Enable the source event in its component settings. For row selection, enable single selection; configure the detail as read-only when needed.') }}</p>
|
|
9106
9168
|
@if (selectedConnection(); as connection) {
|
|
9107
9169
|
<div class="connection-editor__draft-review">
|
|
9108
9170
|
<span>{{ tx('connections.draft.review', 'Review the route') }}</span>
|
|
@@ -10737,6 +10799,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
10737
10799
|
@for (port of draftTargets(); track port.id) { <option [value]="port.id">{{ draftPortLabel(port) }}</option> }
|
|
10738
10800
|
</select>
|
|
10739
10801
|
</label>
|
|
10802
|
+
<p>{{ tx('connections.draft.prerequisites', 'Enable the source event in its component settings. For row selection, enable single selection; configure the detail as read-only when needed.') }}</p>
|
|
10740
10803
|
@if (selectedConnection(); as connection) {
|
|
10741
10804
|
<div class="connection-editor__draft-review">
|
|
10742
10805
|
<span>{{ tx('connections.draft.review', 'Review the route') }}</span>
|
|
@@ -11789,9 +11852,12 @@ class DynamicPageConfigEditorComponent {
|
|
|
11789
11852
|
limitsError = signal(null, ...(ngDevMode ? [{ debugName: "limitsError" }] : /* istanbul ignore next */ []));
|
|
11790
11853
|
limitsErrorMatcher = { isErrorState: (control) => !!control?.invalid };
|
|
11791
11854
|
injectedData = inject(SETTINGS_PANEL_DATA$1, { optional: true });
|
|
11855
|
+
isHostedInSettingsPanel = this.injectedData != null;
|
|
11792
11856
|
i18n = inject(PraxisI18nService);
|
|
11793
11857
|
pageInput = null;
|
|
11794
11858
|
snapshot = '';
|
|
11859
|
+
stackedDevices = new Set();
|
|
11860
|
+
activeDevice = signal('mobile', ...(ngDevMode ? [{ debugName: "activeDevice" }] : /* istanbul ignore next */ []));
|
|
11795
11861
|
hydrating = false;
|
|
11796
11862
|
renderedCandidate = null;
|
|
11797
11863
|
renderedValidationFingerprint = '';
|
|
@@ -11808,6 +11874,19 @@ class DynamicPageConfigEditorComponent {
|
|
|
11808
11874
|
get validationStateLabel() { const issue = this.limitsError() || this.canvasError() || this.groupingError() || this.deviceError() || this.contextError() || this.stateError(); if (issue)
|
|
11809
11875
|
return this.tx('editor.status.validationIssue', 'Review: {{issue}}').replace('{{issue}}', issue); return this.isValid$.value ? this.tx('editor.status.valid', 'Valid configuration') : this.tx('editor.status.review', 'Review errors before saving'); }
|
|
11810
11876
|
get dirtyStateLabel() { return this.hasPendingChanges ? this.tx('editor.status.pending', 'Pending changes') : this.tx('editor.status.clean', 'No changes'); }
|
|
11877
|
+
get nonLimitError() {
|
|
11878
|
+
return this.canvasError() || this.groupingError() || this.deviceError() || this.contextError() || this.stateError();
|
|
11879
|
+
}
|
|
11880
|
+
get limitsSummaryError() {
|
|
11881
|
+
const message = this.limitsError();
|
|
11882
|
+
const form = this.canvasLimits.form.at(this.limitsTargetControl.value);
|
|
11883
|
+
// A rendered-height diagnostic is already attached to the visible height field.
|
|
11884
|
+
// Keep the summary for a different target or a field hidden by the current mode.
|
|
11885
|
+
return form?.controls.heightMode.value === 'fixed'
|
|
11886
|
+
&& this.limitsUseContentHeight()
|
|
11887
|
+
&& form.controls.heightPx.getError('canvasLimit') === 'rendered-height'
|
|
11888
|
+
? null : message;
|
|
11889
|
+
}
|
|
11811
11890
|
get selectedLayoutPresetLabel() { return this.tx('editor.summary.layout', 'Layout: {{label}}').replace('{{label}}', this.lookupPresetLabel(this.layoutPresetEntries, this.layoutPresetControl.value, this.tx('editor.fallback.manual', 'manual'))); }
|
|
11812
11891
|
get selectedThemePresetLabel() { return this.tx('editor.summary.theme', 'Theme: {{label}}').replace('{{label}}', this.lookupPresetLabel(this.themePresetEntries, this.themePresetControl.value, this.tx('editor.layout.layoutDefault', 'Layout default'))); }
|
|
11813
11892
|
get selectedLayoutPresetEntry() { return this.layoutPresetEntries.find((entry) => entry.id === this.layoutPresetControl.value) || null; }
|
|
@@ -11820,6 +11899,38 @@ class DynamicPageConfigEditorComponent {
|
|
|
11820
11899
|
reset() { this.hydrate(); }
|
|
11821
11900
|
pinRecommendedThemePreset() { const theme = this.recommendedThemePresetEntry; if (theme)
|
|
11822
11901
|
this.themePresetControl.setValue(theme.id); }
|
|
11902
|
+
get hasCanvasWidgets() { return !!this.currentPageInput()?.widgets.length; }
|
|
11903
|
+
stackDevice(device) {
|
|
11904
|
+
this.hydrating = true;
|
|
11905
|
+
this.stackedDevices.add(device);
|
|
11906
|
+
this.deviceForms[device].patchValue({ columns: 1, autoRows: 'content' }, { emitEvent: false });
|
|
11907
|
+
for (const [index, target] of this.canvasLimits.targets.entries()) {
|
|
11908
|
+
if (target.scope !== device)
|
|
11909
|
+
continue;
|
|
11910
|
+
this.canvasLimits.form.at(index).patchValue({
|
|
11911
|
+
minColSpan: 1, maxColSpan: 1,
|
|
11912
|
+
heightMode: 'auto', heightPx: null, offsetTopPx: null,
|
|
11913
|
+
}, { emitEvent: false });
|
|
11914
|
+
}
|
|
11915
|
+
this.hydrating = false;
|
|
11916
|
+
this.refreshPanelState();
|
|
11917
|
+
}
|
|
11918
|
+
stackedCanvas(device, previous) {
|
|
11919
|
+
const page = this.currentPageInput() || { widgets: [] };
|
|
11920
|
+
const source = this.completeCanvasItems(page);
|
|
11921
|
+
const widgetOverrides = this.parseWidgetOverrides(this.deviceForms[device].controls.widgetOverrides.value);
|
|
11922
|
+
const items = this.clone(previous.items || {});
|
|
11923
|
+
let row = 1;
|
|
11924
|
+
for (const [key] of Object.entries(source).sort(([aKey, a], [bKey, b]) => a.row - b.row || a.col - b.col || aKey.localeCompare(bKey))) {
|
|
11925
|
+
if (!page.widgets.some(widget => widget.key === key) || items[key]?.hidden || widgetOverrides?.[key]?.hidden)
|
|
11926
|
+
continue;
|
|
11927
|
+
const rowSpan = items[key]?.rowSpan ?? source[key].rowSpan;
|
|
11928
|
+
items[key] = { ...items[key], col: 1, row, colSpan: 1, rowSpan, contentSize: 'auto',
|
|
11929
|
+
constraints: { ...items[key]?.constraints, minColSpan: 1, maxColSpan: 1 } };
|
|
11930
|
+
row += rowSpan;
|
|
11931
|
+
}
|
|
11932
|
+
return { ...previous, items };
|
|
11933
|
+
}
|
|
11823
11934
|
addGroupingNode(seed) { this.groupingNodes.push(this.createGroupingNodeGroup(seed)); this.refreshPanelState(); }
|
|
11824
11935
|
removeGroupingNode(index) { this.groupingNodes.removeAt(index); this.refreshPanelState(); }
|
|
11825
11936
|
layoutPresetSummary(preset) { return this.tx('editor.preview.layoutSummary', '{{columns}} columns · {{gap}}').replace('{{columns}}', String(preset.defaultColumns)).replace('{{gap}}', preset.defaultGap || this.tx('editor.preview.defaultGap', 'default gap')); }
|
|
@@ -11858,7 +11969,7 @@ class DynamicPageConfigEditorComponent {
|
|
|
11858
11969
|
this.layoutPresetControl.value, this.themePresetControl.value, this.shellPresetControl.value,
|
|
11859
11970
|
this.contextControl.value, this.stateControl.value, this.groupingNodes.getRawValue(),
|
|
11860
11971
|
this.deviceKeys.map(key => this.deviceForms[key].getRawValue()),
|
|
11861
|
-
this.canvasLimits.form.getRawValue(),
|
|
11972
|
+
this.canvasLimits.form.getRawValue(), [...this.stackedDevices],
|
|
11862
11973
|
]);
|
|
11863
11974
|
}
|
|
11864
11975
|
limitsGeometryLabel() {
|
|
@@ -11933,7 +12044,7 @@ class DynamicPageConfigEditorComponent {
|
|
|
11933
12044
|
}
|
|
11934
12045
|
createDeviceVariantForm() { return new FormGroup({ columns: new FormControl(null, { validators: [Validators.min(1), integerColumnValidator] }), rowUnit: new FormControl('', { nonNullable: true }), gap: new FormControl('', { nonNullable: true }), autoRows: new FormControl('', { nonNullable: true }), groupingOverrides: new FormControl('', { nonNullable: true }), widgetOverrides: new FormControl('', { nonNullable: true }) }); }
|
|
11935
12046
|
createGroupingNodeGroup(seed) { const label = seed && 'label' in seed && typeof seed.label === 'string' ? seed.label : ''; return new FormGroup({ kind: new FormControl(seed?.kind || 'section', { nonNullable: true }), id: new FormControl(seed?.id || '', { nonNullable: true, validators: [Validators.required] }), label: new FormControl(label, { nonNullable: true }), widgetKeysCsv: new FormControl(this.stringifyGroupingWidgetKeys(seed), { nonNullable: true }), layout: new FormControl(seed?.kind === 'section' ? seed.layout || '' : '', { nonNullable: true }), emphasis: new FormControl(seed?.kind === 'hero' ? seed.emphasis || '' : '', { nonNullable: true }), side: new FormControl(seed?.kind === 'rail' ? seed.side || 'right' : 'right', { nonNullable: true }), tabsJson: new FormControl(seed?.kind === 'tabs' ? JSON.stringify(seed.tabs || [], null, 2) : '', { nonNullable: true }) }); }
|
|
11936
|
-
hydrate() { const page = this.currentPageInput() || { widgets: [] }; const canvas = page.canvas || this.defaultCanvas(); const context = this.clone(page.context || {}); this.hydrating = true; this.canvasLimits.hydrate(page); this.limitsTargetControl.setValue(0, { emitEvent: false }); this.limitsError.set(null); this.canvasColumnsControl.setValue(canvas.columns || 12, { emitEvent: false }); this.canvasRowUnitControl.setValue(canvas.rowUnit || '80px', { emitEvent: false }); this.canvasGapControl.setValue(canvas.gap || '16px', { emitEvent: false }); this.canvasAutoRowsControl.setValue(canvas.autoRows || 'fixed', { emitEvent: false }); this.layoutPresetControl.setValue(page.layoutPreset || '', { emitEvent: false }); this.themePresetControl.setValue(page.themePreset || '', { emitEvent: false }); this.shellPresetControl.setValue(this.extractShellPreset(context) || this.pagePreset || '', { emitEvent: false }); this.replaceGroupingNodes(page.grouping); this.contextControl.setValue(this.stringify(this.removeShellPresetFromContext(context)), { emitEvent: false }); this.stateControl.setValue(this.stringify(page.state || {}), { emitEvent: false }); for (const device of this.deviceKeys) {
|
|
12047
|
+
hydrate() { this.stackedDevices.clear(); const page = this.currentPageInput() || { widgets: [] }; const canvas = page.canvas || this.defaultCanvas(); const context = this.clone(page.context || {}); this.hydrating = true; this.canvasLimits.hydrate(page); this.limitsTargetControl.setValue(0, { emitEvent: false }); this.limitsError.set(null); this.canvasColumnsControl.setValue(canvas.columns || 12, { emitEvent: false }); this.canvasRowUnitControl.setValue(canvas.rowUnit || '80px', { emitEvent: false }); this.canvasGapControl.setValue(canvas.gap || '16px', { emitEvent: false }); this.canvasAutoRowsControl.setValue(canvas.autoRows || 'fixed', { emitEvent: false }); this.layoutPresetControl.setValue(page.layoutPreset || '', { emitEvent: false }); this.themePresetControl.setValue(page.themePreset || '', { emitEvent: false }); this.shellPresetControl.setValue(this.extractShellPreset(context) || this.pagePreset || '', { emitEvent: false }); this.replaceGroupingNodes(page.grouping); this.contextControl.setValue(this.stringify(this.removeShellPresetFromContext(context)), { emitEvent: false }); this.stateControl.setValue(this.stringify(page.state || {}), { emitEvent: false }); for (const device of this.deviceKeys) {
|
|
11937
12048
|
const variant = page.deviceLayouts?.[device];
|
|
11938
12049
|
this.deviceForms[device].setValue({ columns: variant?.canvas?.columns ?? null, rowUnit: variant?.canvas?.rowUnit || '', gap: variant?.canvas?.gap || '', autoRows: variant?.canvas?.autoRows || '', groupingOverrides: this.stringify(variant?.groupingOverrides || []), widgetOverrides: this.stringify(variant?.widgetOverrides || {}) }, { emitEvent: false });
|
|
11939
12050
|
} this.canvasError.set(null); this.groupingError.set(null); this.deviceError.set(null); this.contextError.set(null); this.stateError.set(null); this.hydrating = false; this.refreshPanelState(true); }
|
|
@@ -11944,7 +12055,7 @@ class DynamicPageConfigEditorComponent {
|
|
|
11944
12055
|
return null; this.groupingError.set(null); const context = this.parseRecordJson(this.contextControl.value, {}); if (context === null)
|
|
11945
12056
|
return this.invalidate('context', this.contextError, reportErrors, this.tx('editor.errors.contextInvalid', 'Invalid context')); this.contextError.set(null); const state = this.parseRecordJson(this.stateControl.value, {}); if (state === null)
|
|
11946
12057
|
return this.invalidate('state', this.stateError, reportErrors, this.tx('editor.errors.stateInvalid', 'Invalid state')); this.stateError.set(null); const deviceLayouts = this.buildDeviceLayouts(reportErrors, next.deviceLayouts); if (deviceLayouts === null)
|
|
11947
|
-
return null; next.canvas = { ...(next.canvas || this.defaultCanvas()), mode: 'grid', columns: this.canvasColumnsControl.value, rowUnit: this.canvasRowUnitControl.value.trim() || '80px', gap: this.canvasGapControl.value.trim() || '16px', autoRows: this.canvasAutoRowsControl.value, items: next
|
|
12058
|
+
return null; next.canvas = { ...(next.canvas || this.defaultCanvas()), mode: 'grid', columns: this.canvasColumnsControl.value, rowUnit: this.canvasRowUnitControl.value.trim() || '80px', gap: this.canvasGapControl.value.trim() || '16px', autoRows: this.canvasAutoRowsControl.value, items: this.completeCanvasItems(next) }; next.layoutPreset = this.layoutPresetControl.value.trim() || undefined; next.themePreset = this.themePresetControl.value.trim() || undefined; next.grouping = grouping.length ? grouping : undefined; next.deviceLayouts = Object.keys(deviceLayouts).length ? deviceLayouts : undefined; next.context = this.applyShellPresetToContext(context, this.shellPresetControl.value.trim()); next.state = Object.keys(state).length ? state : undefined; return this.applyCanvasLimits(next, reportErrors); }
|
|
11948
12059
|
buildGrouping(reportErrors) { const grouping = []; const seenIds = new Set(); for (const group of this.groupingNodes.controls) {
|
|
11949
12060
|
const kind = group.controls.kind.value;
|
|
11950
12061
|
const id = group.controls.id.value.trim();
|
|
@@ -11974,6 +12085,18 @@ class DynamicPageConfigEditorComponent {
|
|
|
11974
12085
|
}
|
|
11975
12086
|
grouping.push({ kind: 'rail', id, side: (group.controls.side.value || 'right'), widgetKeys });
|
|
11976
12087
|
} return grouping; }
|
|
12088
|
+
/** A grid must place every existing widget before replacing the flow layout. */
|
|
12089
|
+
completeCanvasItems(page) {
|
|
12090
|
+
const items = { ...(page.canvas?.items || {}) };
|
|
12091
|
+
let row = Math.max(1, ...Object.values(items).map(item => item.row + item.rowSpan));
|
|
12092
|
+
for (const widget of page.widgets) {
|
|
12093
|
+
if (items[widget.key])
|
|
12094
|
+
continue;
|
|
12095
|
+
items[widget.key] = { col: 1, row, colSpan: this.canvasColumnsControl.value, rowSpan: 1 };
|
|
12096
|
+
row += 1;
|
|
12097
|
+
}
|
|
12098
|
+
return items;
|
|
12099
|
+
}
|
|
11977
12100
|
buildDeviceLayouts(reportErrors, base) { const next = {}; for (const device of this.deviceKeys) {
|
|
11978
12101
|
const form = this.deviceForms[device];
|
|
11979
12102
|
if (form.controls.columns.invalid)
|
|
@@ -11987,7 +12110,7 @@ class DynamicPageConfigEditorComponent {
|
|
|
11987
12110
|
delete baseVariant.canvas;
|
|
11988
12111
|
delete baseVariant.groupingOverrides;
|
|
11989
12112
|
delete baseVariant.widgetOverrides;
|
|
11990
|
-
const canvas = { ...baseCanvas };
|
|
12113
|
+
const canvas = this.stackedDevices.has(device) ? this.stackedCanvas(device, baseCanvas) : { ...baseCanvas };
|
|
11991
12114
|
if (form.controls.columns.value != null)
|
|
11992
12115
|
canvas.columns = form.controls.columns.value;
|
|
11993
12116
|
else
|
|
@@ -12075,27 +12198,68 @@ class DynamicPageConfigEditorComponent {
|
|
|
12075
12198
|
next.ui.shell.preset = shellPreset;
|
|
12076
12199
|
} return Object.keys(next).length ? next : undefined; }
|
|
12077
12200
|
lookupPresetLabel(entries, value, fallback) { return entries.find((entry) => entry.id === value)?.label || fallback; }
|
|
12078
|
-
defaultCanvas() { return { mode: 'grid', columns: 12, rowUnit: '80px', gap: '16px', autoRows: '
|
|
12201
|
+
defaultCanvas() { return { mode: 'grid', columns: 12, rowUnit: '80px', gap: '16px', autoRows: 'content', items: {} }; }
|
|
12079
12202
|
clone(value) { if (value == null || typeof value !== 'object')
|
|
12080
12203
|
return value; return JSON.parse(JSON.stringify(value)); }
|
|
12081
12204
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicPageConfigEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12082
12205
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DynamicPageConfigEditorComponent, isStandalone: true, selector: "praxis-dynamic-page-config-editor", inputs: { page: "page", layout: "layout", presets: "presets", pagePreset: "pagePreset" }, outputs: { pageChange: "pageChange" }, host: { listeners: { "window:resize": "refreshRenderedValidation()" } }, providers: [providePraxisPageBuilderI18n()], ngImport: i0, template: `
|
|
12083
12206
|
<div class="editor">
|
|
12084
|
-
<div class="head">
|
|
12207
|
+
@if (!isHostedInSettingsPanel) { <div class="head">
|
|
12085
12208
|
<div>{{ tx('editor.title', 'Page settings') }}</div>
|
|
12086
12209
|
<button mat-stroked-button type="button" (click)="reset()">{{ tx('editor.reload', 'Reload') }}</button>
|
|
12087
|
-
</div>
|
|
12088
|
-
<
|
|
12089
|
-
|
|
12090
|
-
|
|
12210
|
+
</div> }
|
|
12211
|
+
<details class="summary-details">
|
|
12212
|
+
<summary>{{ tx('editor.summaryAria', 'Configuration summary') }}</summary>
|
|
12213
|
+
<div class="summary">
|
|
12214
|
+
@if (!isHostedInSettingsPanel) {
|
|
12215
|
+
<span>{{ validationStateLabel }}</span>
|
|
12216
|
+
<span>{{ dirtyStateLabel }}</span>
|
|
12217
|
+
}
|
|
12091
12218
|
<span>{{ selectedLayoutPresetLabel }}</span>
|
|
12092
12219
|
<span>{{ selectedThemePresetLabel }}</span>
|
|
12093
12220
|
<span>{{ canvasSummaryLabel }}</span>
|
|
12094
12221
|
</div>
|
|
12095
|
-
|
|
12222
|
+
</details>
|
|
12223
|
+
@if (nonLimitError) { <p class="limits-error" role="alert">{{ nonLimitError }}</p> }
|
|
12224
|
+
@if (activeTab() === 'limits' && canvasLimits.targets.length) {
|
|
12225
|
+
<div class="selection-identity" data-testid="canvas-limits-identity">
|
|
12226
|
+
<mat-icon aria-hidden="true">dashboard_customize</mat-icon>
|
|
12227
|
+
<strong>{{ limitsTargetLabel(limitsTargetControl.value) }}</strong>
|
|
12228
|
+
</div>
|
|
12229
|
+
}
|
|
12230
|
+
<mat-tab-group class="editor-tabs" [mat-stretch-tabs]="false" [selectedIndex]="selectedTabIndex" (selectedIndexChange)="onSelectedTabIndexChange($event)">
|
|
12096
12231
|
<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>
|
|
12097
|
-
<mat-tab [label]="tx('editor.tabs.grouping', 'Grouping')"><div class="panel"
|
|
12098
|
-
|
|
12232
|
+
<mat-tab [label]="tx('editor.tabs.grouping', 'Grouping')"><div class="panel">
|
|
12233
|
+
<p class="hint" data-testid="canvas-grouping-notice">{{ tx('editor.grouping.canvasNotice', 'Canvas positions control this page. Groups below are preserved for flow layouts and do not arrange widgets in the canvas. Use the widget move and resize controls to place them side by side.') }}</p><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>
|
|
12234
|
+
<mat-tab [label]="tx('editor.tabs.devices', 'Device Layouts')">
|
|
12235
|
+
<div class="panel">
|
|
12236
|
+
<mat-button-toggle-group [value]="activeDevice()" (change)="activeDevice.set($event.value)" [attr.aria-label]="tx('editor.devices.target', 'Device to configure')">
|
|
12237
|
+
@for (device of deviceKeys; track device) {
|
|
12238
|
+
<mat-button-toggle [value]="device">{{ deviceLabels[device] }}</mat-button-toggle>
|
|
12239
|
+
}
|
|
12240
|
+
</mat-button-toggle-group>
|
|
12241
|
+
@for (device of deviceKeys; track device) {
|
|
12242
|
+
@if (activeDevice() === device) {
|
|
12243
|
+
<div class="card">
|
|
12244
|
+
<div class="head"><strong>{{ deviceLabels[device] }}</strong><button mat-stroked-button type="button" [attr.data-testid]="'stack-device-' + device" [disabled]="!hasCanvasWidgets" (click)="stackDevice(device)"><mat-icon aria-hidden="true">view_agenda</mat-icon>{{ tx('editor.devices.stack', 'Stack widgets') }}</button></div>
|
|
12245
|
+
<p class="hint">{{ tx('editor.devices.stackHelp', 'Use full width and natural height, in reading order. This replaces positions and width limits only for this device; other layouts stay unchanged.') }}</p>
|
|
12246
|
+
@if (stackedDevices.has(device)) { <p class="hint" role="status">{{ tx('editor.devices.stacked', 'Single-column layout prepared. Save to apply; Reset undoes the changes.') }}</p> }
|
|
12247
|
+
<div class="grid">
|
|
12248
|
+
<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>
|
|
12249
|
+
<mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.gap', 'Gap') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.gap" /></mat-form-field>
|
|
12250
|
+
<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>
|
|
12251
|
+
</div>
|
|
12252
|
+
<details><summary>{{ tx('editor.devices.advanced', 'Advanced overrides') }}</summary><div class="grid">
|
|
12253
|
+
<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>
|
|
12254
|
+
<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>
|
|
12255
|
+
<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>
|
|
12256
|
+
</div></details>
|
|
12257
|
+
@if (!stackedDevices.has(device)) { <div class="hint">{{ tx('editor.preview.deviceCanvasHint', 'Existing canvas item overrides are preserved.') }}</div> }
|
|
12258
|
+
</div>
|
|
12259
|
+
}
|
|
12260
|
+
}
|
|
12261
|
+
</div>
|
|
12262
|
+
</mat-tab>
|
|
12099
12263
|
<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>
|
|
12100
12264
|
<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>
|
|
12101
12265
|
<mat-tab [label]="tx('editor.limits.tab', 'Widget limits')">
|
|
@@ -12169,7 +12333,7 @@ class DynamicPageConfigEditorComponent {
|
|
|
12169
12333
|
</div>
|
|
12170
12334
|
}
|
|
12171
12335
|
}
|
|
12172
|
-
@if (
|
|
12336
|
+
@if (limitsSummaryError) { <p class="limits-error" role="alert">{{ limitsSummaryError }}</p> }
|
|
12173
12337
|
} @else {
|
|
12174
12338
|
<p>{{ tx('editor.limits.empty', 'Add widgets to the canvas to configure their limits.') }}</p>
|
|
12175
12339
|
}
|
|
@@ -12177,27 +12341,68 @@ class DynamicPageConfigEditorComponent {
|
|
|
12177
12341
|
</mat-tab>
|
|
12178
12342
|
</mat-tab-group>
|
|
12179
12343
|
</div>
|
|
12180
|
-
`, isInline: true, styles: [":host{display:block;min-width:0;max-width:100%}.height-modes{width:fit-content;max-width:100%;flex-wrap:wrap;margin-bottom:16px}.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)}.limits-panel{display:grid;gap:8px;min-width:0}.limits-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:4px 16px}.limits-fields mat-form-field{min-width:0}.limits-notice{display:flex;gap:8px;align-items:flex-start;color:var(--md-sys-color-on-surface-variant, currentColor)}.limits-notice mat-icon{flex-shrink:0}.limits-error{color:var(--md-sys-color-error, #b3261e)}.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: MatButtonToggleModule }, { kind: "directive", type: i3$1.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i3$1.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { 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: i10.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: i10.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i8.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i8.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 });
|
|
12344
|
+
`, isInline: true, styles: [":host{display:block;min-width:0;max-width:100%}.height-modes{width:fit-content;max-width:100%;flex-wrap:wrap;margin-bottom:16px}.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)}.selection-identity{position:sticky;top:0;z-index:var(--praxis-layer-sticky-local, 100);display:flex;align-items:flex-start;gap:8px;padding-block:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface));overflow-wrap:anywhere}.selection-identity mat-icon{flex-shrink:0}:host ::ng-deep .editor-tabs>.mat-mdc-tab-header .mdc-tab{min-width:0;padding-inline:12px}:host ::ng-deep .editor-tabs>.mat-mdc-tab-header .mdc-tab__text-label{max-width:128px;white-space:normal;overflow-wrap:anywhere;text-align:center;letter-spacing:normal;line-height:1.25}.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)}.limits-panel{display:grid;gap:8px;min-width:0}.limits-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:4px 16px}.limits-fields mat-form-field{min-width:0}.limits-notice{display:flex;gap:8px;align-items:flex-start;color:var(--md-sys-color-on-surface-variant, currentColor)}.limits-notice mat-icon{flex-shrink:0}.limits-error{color:var(--md-sys-color-error, #b3261e)}.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: MatButtonToggleModule }, { kind: "directive", type: i3$1.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i3$1.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { 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: i10.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: i10.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i8.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i8.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 });
|
|
12181
12345
|
}
|
|
12182
12346
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicPageConfigEditorComponent, decorators: [{
|
|
12183
12347
|
type: Component,
|
|
12184
12348
|
args: [{ selector: 'praxis-dynamic-page-config-editor', standalone: true, imports: [ReactiveFormsModule, MatButtonModule, MatButtonToggleModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule, MatTabsModule, PraxisIconButtonComponent], providers: [providePraxisPageBuilderI18n()], template: `
|
|
12185
12349
|
<div class="editor">
|
|
12186
|
-
<div class="head">
|
|
12350
|
+
@if (!isHostedInSettingsPanel) { <div class="head">
|
|
12187
12351
|
<div>{{ tx('editor.title', 'Page settings') }}</div>
|
|
12188
12352
|
<button mat-stroked-button type="button" (click)="reset()">{{ tx('editor.reload', 'Reload') }}</button>
|
|
12189
|
-
</div>
|
|
12190
|
-
<
|
|
12191
|
-
|
|
12192
|
-
|
|
12353
|
+
</div> }
|
|
12354
|
+
<details class="summary-details">
|
|
12355
|
+
<summary>{{ tx('editor.summaryAria', 'Configuration summary') }}</summary>
|
|
12356
|
+
<div class="summary">
|
|
12357
|
+
@if (!isHostedInSettingsPanel) {
|
|
12358
|
+
<span>{{ validationStateLabel }}</span>
|
|
12359
|
+
<span>{{ dirtyStateLabel }}</span>
|
|
12360
|
+
}
|
|
12193
12361
|
<span>{{ selectedLayoutPresetLabel }}</span>
|
|
12194
12362
|
<span>{{ selectedThemePresetLabel }}</span>
|
|
12195
12363
|
<span>{{ canvasSummaryLabel }}</span>
|
|
12196
12364
|
</div>
|
|
12197
|
-
|
|
12365
|
+
</details>
|
|
12366
|
+
@if (nonLimitError) { <p class="limits-error" role="alert">{{ nonLimitError }}</p> }
|
|
12367
|
+
@if (activeTab() === 'limits' && canvasLimits.targets.length) {
|
|
12368
|
+
<div class="selection-identity" data-testid="canvas-limits-identity">
|
|
12369
|
+
<mat-icon aria-hidden="true">dashboard_customize</mat-icon>
|
|
12370
|
+
<strong>{{ limitsTargetLabel(limitsTargetControl.value) }}</strong>
|
|
12371
|
+
</div>
|
|
12372
|
+
}
|
|
12373
|
+
<mat-tab-group class="editor-tabs" [mat-stretch-tabs]="false" [selectedIndex]="selectedTabIndex" (selectedIndexChange)="onSelectedTabIndexChange($event)">
|
|
12198
12374
|
<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>
|
|
12199
|
-
<mat-tab [label]="tx('editor.tabs.grouping', 'Grouping')"><div class="panel"
|
|
12200
|
-
|
|
12375
|
+
<mat-tab [label]="tx('editor.tabs.grouping', 'Grouping')"><div class="panel">
|
|
12376
|
+
<p class="hint" data-testid="canvas-grouping-notice">{{ tx('editor.grouping.canvasNotice', 'Canvas positions control this page. Groups below are preserved for flow layouts and do not arrange widgets in the canvas. Use the widget move and resize controls to place them side by side.') }}</p><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>
|
|
12377
|
+
<mat-tab [label]="tx('editor.tabs.devices', 'Device Layouts')">
|
|
12378
|
+
<div class="panel">
|
|
12379
|
+
<mat-button-toggle-group [value]="activeDevice()" (change)="activeDevice.set($event.value)" [attr.aria-label]="tx('editor.devices.target', 'Device to configure')">
|
|
12380
|
+
@for (device of deviceKeys; track device) {
|
|
12381
|
+
<mat-button-toggle [value]="device">{{ deviceLabels[device] }}</mat-button-toggle>
|
|
12382
|
+
}
|
|
12383
|
+
</mat-button-toggle-group>
|
|
12384
|
+
@for (device of deviceKeys; track device) {
|
|
12385
|
+
@if (activeDevice() === device) {
|
|
12386
|
+
<div class="card">
|
|
12387
|
+
<div class="head"><strong>{{ deviceLabels[device] }}</strong><button mat-stroked-button type="button" [attr.data-testid]="'stack-device-' + device" [disabled]="!hasCanvasWidgets" (click)="stackDevice(device)"><mat-icon aria-hidden="true">view_agenda</mat-icon>{{ tx('editor.devices.stack', 'Stack widgets') }}</button></div>
|
|
12388
|
+
<p class="hint">{{ tx('editor.devices.stackHelp', 'Use full width and natural height, in reading order. This replaces positions and width limits only for this device; other layouts stay unchanged.') }}</p>
|
|
12389
|
+
@if (stackedDevices.has(device)) { <p class="hint" role="status">{{ tx('editor.devices.stacked', 'Single-column layout prepared. Save to apply; Reset undoes the changes.') }}</p> }
|
|
12390
|
+
<div class="grid">
|
|
12391
|
+
<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>
|
|
12392
|
+
<mat-form-field appearance="outline"><mat-label>{{ tx('editor.devices.gap', 'Gap') }}</mat-label><input matInput [formControl]="deviceForms[device].controls.gap" /></mat-form-field>
|
|
12393
|
+
<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>
|
|
12394
|
+
</div>
|
|
12395
|
+
<details><summary>{{ tx('editor.devices.advanced', 'Advanced overrides') }}</summary><div class="grid">
|
|
12396
|
+
<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>
|
|
12397
|
+
<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>
|
|
12398
|
+
<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>
|
|
12399
|
+
</div></details>
|
|
12400
|
+
@if (!stackedDevices.has(device)) { <div class="hint">{{ tx('editor.preview.deviceCanvasHint', 'Existing canvas item overrides are preserved.') }}</div> }
|
|
12401
|
+
</div>
|
|
12402
|
+
}
|
|
12403
|
+
}
|
|
12404
|
+
</div>
|
|
12405
|
+
</mat-tab>
|
|
12201
12406
|
<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>
|
|
12202
12407
|
<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>
|
|
12203
12408
|
<mat-tab [label]="tx('editor.limits.tab', 'Widget limits')">
|
|
@@ -12271,7 +12476,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
12271
12476
|
</div>
|
|
12272
12477
|
}
|
|
12273
12478
|
}
|
|
12274
|
-
@if (
|
|
12479
|
+
@if (limitsSummaryError) { <p class="limits-error" role="alert">{{ limitsSummaryError }}</p> }
|
|
12275
12480
|
} @else {
|
|
12276
12481
|
<p>{{ tx('editor.limits.empty', 'Add widgets to the canvas to configure their limits.') }}</p>
|
|
12277
12482
|
}
|
|
@@ -12279,7 +12484,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
12279
12484
|
</mat-tab>
|
|
12280
12485
|
</mat-tab-group>
|
|
12281
12486
|
</div>
|
|
12282
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, host: { '(window:resize)': 'refreshRenderedValidation()' }, styles: [":host{display:block;min-width:0;max-width:100%}.height-modes{width:fit-content;max-width:100%;flex-wrap:wrap;margin-bottom:16px}.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)}.limits-panel{display:grid;gap:8px;min-width:0}.limits-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:4px 16px}.limits-fields mat-form-field{min-width:0}.limits-notice{display:flex;gap:8px;align-items:flex-start;color:var(--md-sys-color-on-surface-variant, currentColor)}.limits-notice mat-icon{flex-shrink:0}.limits-error{color:var(--md-sys-color-error, #b3261e)}.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"] }]
|
|
12487
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, host: { '(window:resize)': 'refreshRenderedValidation()' }, styles: [":host{display:block;min-width:0;max-width:100%}.height-modes{width:fit-content;max-width:100%;flex-wrap:wrap;margin-bottom:16px}.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)}.selection-identity{position:sticky;top:0;z-index:var(--praxis-layer-sticky-local, 100);display:flex;align-items:flex-start;gap:8px;padding-block:8px;background:var(--md-sys-color-surface-container-low, var(--md-sys-color-surface));overflow-wrap:anywhere}.selection-identity mat-icon{flex-shrink:0}:host ::ng-deep .editor-tabs>.mat-mdc-tab-header .mdc-tab{min-width:0;padding-inline:12px}:host ::ng-deep .editor-tabs>.mat-mdc-tab-header .mdc-tab__text-label{max-width:128px;white-space:normal;overflow-wrap:anywhere;text-align:center;letter-spacing:normal;line-height:1.25}.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)}.limits-panel{display:grid;gap:8px;min-width:0}.limits-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:4px 16px}.limits-fields mat-form-field{min-width:0}.limits-notice{display:flex;gap:8px;align-items:flex-start;color:var(--md-sys-color-on-surface-variant, currentColor)}.limits-notice mat-icon{flex-shrink:0}.limits-error{color:var(--md-sys-color-error, #b3261e)}.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"] }]
|
|
12283
12488
|
}], ctorParameters: () => [], propDecorators: { page: [{
|
|
12284
12489
|
type: Input
|
|
12285
12490
|
}], layout: [{
|
|
@@ -15526,7 +15731,7 @@ const PRAXIS_PAGE_BUILDER_AUTHORING_MANIFEST = {
|
|
|
15526
15731
|
scope: 'templating',
|
|
15527
15732
|
targetKind: 'widgetShell',
|
|
15528
15733
|
target: { kind: 'widgetShell', resolver: 'widget-shell-by-widget-key', ambiguityPolicy: 'fail', required: true },
|
|
15529
|
-
inputSchema: { type: 'object', required: ['widgetKey'], properties: { widgetKey: { type: 'string' }, shell: { type: 'object' } } },
|
|
15734
|
+
inputSchema: { type: 'object', required: ['widgetKey'], properties: { widgetKey: { type: 'string' }, shell: { type: 'object', properties: { stickyHeader: { type: 'boolean', description: 'Retain the existing header within the widget during page scrolling; suspended for drag authoring, collapsed/overlay states and scroll recovery.' } } } } },
|
|
15530
15735
|
effects: [{ kind: 'compile-domain-patch', handler: 'page-builder-widget-shell-configure', handlerContract: {
|
|
15531
15736
|
reads: ['WidgetPageDefinition.widgets', 'WidgetShellEditorComponent', 'SettingsPanelBridge'],
|
|
15532
15737
|
writes: ['page.widgets[].shell'],
|
|
@@ -23899,6 +24104,10 @@ class DynamicPageBuilderComponent {
|
|
|
23899
24104
|
this.agenticAuthoringLastEtag.set(this.authoringDocument.etag?.trim() || null);
|
|
23900
24105
|
}
|
|
23901
24106
|
else if (authoringDocumentChange && !this.authoringDocument) {
|
|
24107
|
+
// Clearing the persisted document hands ownership back to the page input.
|
|
24108
|
+
const parsed = this.parsePage(this.page) || { widgets: [] };
|
|
24109
|
+
this.currentPage.set(parsed);
|
|
24110
|
+
this.lastRuntimePageChangeFingerprint = JSON.stringify(parsed);
|
|
23902
24111
|
this.persistedUiCompositionAuthoringSource.set(null);
|
|
23903
24112
|
this.agenticAuthoringLastEtag.set(null);
|
|
23904
24113
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisui/page-builder",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.67",
|
|
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.
|
|
12
|
-
"@praxisui/core": "^9.0.
|
|
13
|
-
"@praxisui/settings-panel": "^9.0.
|
|
11
|
+
"@praxisui/ai": "^9.0.67",
|
|
12
|
+
"@praxisui/core": "^9.0.67",
|
|
13
|
+
"@praxisui/settings-panel": "^9.0.67",
|
|
14
14
|
"rxjs": "~7.8.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
@@ -2,7 +2,7 @@ import { MatDialogRef, MatDialog } from '@angular/material/dialog';
|
|
|
2
2
|
import * as _angular_core from '@angular/core';
|
|
3
3
|
import { EventEmitter, OnInit, WritableSignal, InjectionToken, Provider, OnChanges, OnDestroy, Type, SimpleChanges } from '@angular/core';
|
|
4
4
|
import * as _praxisui_core from '@praxisui/core';
|
|
5
|
-
import { ComponentDocMeta, ComponentMetadataRegistry, PraxisI18nService, WidgetShellConfig, WidgetPageAuthoringCapabilities, WidgetShellAction, EndpointRef, ComponentPortPathSegment, LinkIntent, CompositionLink, WidgetPageDefinition, TransformKind, SurfaceOpenPayload, WidgetPageDeviceKind, SettingsValueProvider as SettingsValueProvider$1, WidgetPageGroupingDefinition, BUILTIN_PAGE_LAYOUT_PRESETS, BUILTIN_PAGE_THEME_PRESETS, PageIdentity, WidgetStateNode, WidgetDerivedStateNode, AiCapability, AiCapabilityCategory, AiCapabilityCatalog, AiValueKind, WidgetPageCanvasLayout, WidgetPageDeviceLayouts, WidgetPageSlotAssignments, WidgetPageStateInput, WidgetInstance, TransformOutputHint, TransformSemanticKind, ConfigAuthoringSource, DomainKnowledgeChangeSetRequest, DomainKnowledgeChangeSet, DomainKnowledgeChangeSetTimelineResponse, DomainKnowledgeChangeSetFilters, DomainKnowledgeValidationResponse, DomainKnowledgeStatusTransitionRequest, DomainRuleIntakeRequest, DomainRuleIntakeResponse, DomainRuleDefinitionRequest, DomainRuleDefinition, DomainRuleSimulationRequest, DomainRuleSimulationResponse, DomainRuleStatusTransitionRequest, DomainRulePublicationRequest, DomainRulePublicationResponse, DomainRuleMaterializationFilters, DomainRuleMaterialization, DomainRuleTimelineResponse, PraxisRuntimeComponentObservationEnvelope, ComponentContextPack, ComponentAuthoringManifest, DiagnosticRecord, NestedPortCatalogRegistry, CompositionValidatorService, ApiUrlEntry, DynamicWidgetPageComponent, ConfigDocument, WidgetEventEnvelope, ObservabilityAgenticTurnMetrics, SettingsPanelBridge, ObservabilityAgenticTurnMetricBucket, RichTimelineItem } from '@praxisui/core';
|
|
5
|
+
import { ComponentDocMeta, ComponentMetadataRegistry, PraxisI18nService, WidgetShellConfig, WidgetPageAuthoringCapabilities, WidgetShellAction, EndpointRef, PortContract, ComponentPortPathSegment, LinkIntent, CompositionLink, WidgetPageDefinition, TransformKind, SurfaceOpenPayload, WidgetPageDeviceKind, SettingsValueProvider as SettingsValueProvider$1, WidgetPageGroupingDefinition, BUILTIN_PAGE_LAYOUT_PRESETS, BUILTIN_PAGE_THEME_PRESETS, PageIdentity, WidgetStateNode, WidgetDerivedStateNode, AiCapability, AiCapabilityCategory, AiCapabilityCatalog, AiValueKind, WidgetPageCanvasLayout, WidgetPageDeviceLayouts, WidgetPageSlotAssignments, WidgetPageStateInput, WidgetInstance, TransformOutputHint, TransformSemanticKind, ConfigAuthoringSource, DomainKnowledgeChangeSetRequest, DomainKnowledgeChangeSet, DomainKnowledgeChangeSetTimelineResponse, DomainKnowledgeChangeSetFilters, DomainKnowledgeValidationResponse, DomainKnowledgeStatusTransitionRequest, DomainRuleIntakeRequest, DomainRuleIntakeResponse, DomainRuleDefinitionRequest, DomainRuleDefinition, DomainRuleSimulationRequest, DomainRuleSimulationResponse, DomainRuleStatusTransitionRequest, DomainRulePublicationRequest, DomainRulePublicationResponse, DomainRuleMaterializationFilters, DomainRuleMaterialization, DomainRuleTimelineResponse, PraxisRuntimeComponentObservationEnvelope, ComponentContextPack, ComponentAuthoringManifest, DiagnosticRecord, NestedPortCatalogRegistry, CompositionValidatorService, ApiUrlEntry, DynamicWidgetPageComponent, ConfigDocument, WidgetEventEnvelope, ObservabilityAgenticTurnMetrics, SettingsPanelBridge, ObservabilityAgenticTurnMetricBucket, RichTimelineItem } from '@praxisui/core';
|
|
6
6
|
import * as rxjs from 'rxjs';
|
|
7
7
|
import { BehaviorSubject, Observable } from 'rxjs';
|
|
8
8
|
import { FormGroup, FormControl, FormArray } from '@angular/forms';
|
|
@@ -164,7 +164,9 @@ type SetInputPreset = {
|
|
|
164
164
|
declare class WidgetShellEditorComponent implements SettingsValueProvider {
|
|
165
165
|
private registry?;
|
|
166
166
|
private readonly i18n;
|
|
167
|
+
menuPlacementLabel(): string;
|
|
167
168
|
appearanceError(): string;
|
|
169
|
+
stickyHeaderLabel(): string;
|
|
168
170
|
form: FormGroup<{
|
|
169
171
|
kind: FormControl<"none" | "dashboard-card">;
|
|
170
172
|
preset: FormControl<string>;
|
|
@@ -173,6 +175,7 @@ declare class WidgetShellEditorComponent implements SettingsValueProvider {
|
|
|
173
175
|
subtitle: FormControl<string>;
|
|
174
176
|
icon: FormControl<string>;
|
|
175
177
|
showHeader: FormControl<"auto" | "true" | "false">;
|
|
178
|
+
stickyHeader: FormControl<boolean>;
|
|
176
179
|
collapsible: FormControl<boolean>;
|
|
177
180
|
cardBg: FormControl<string>;
|
|
178
181
|
cardBorder: FormControl<string>;
|
|
@@ -280,6 +283,8 @@ interface ConnectionEditorPort {
|
|
|
280
283
|
label: string;
|
|
281
284
|
kind: ConnectionEditorPortKind;
|
|
282
285
|
endpoint: EndpointRef;
|
|
286
|
+
/** Transient registry evidence; never serialized into a composition endpoint. */
|
|
287
|
+
contract?: PortContract;
|
|
283
288
|
nestedPath?: ComponentPortPathSegment[];
|
|
284
289
|
}
|
|
285
290
|
interface ConnectionEditorNode {
|
|
@@ -1016,9 +1021,12 @@ declare class DynamicPageConfigEditorComponent implements OnInit, SettingsValueP
|
|
|
1016
1021
|
isErrorState: (control: FormControl | null) => boolean;
|
|
1017
1022
|
};
|
|
1018
1023
|
private readonly injectedData;
|
|
1024
|
+
readonly isHostedInSettingsPanel: boolean;
|
|
1019
1025
|
private readonly i18n;
|
|
1020
1026
|
private pageInput;
|
|
1021
1027
|
private snapshot;
|
|
1028
|
+
readonly stackedDevices: Set<DeviceKey>;
|
|
1029
|
+
readonly activeDevice: WritableSignal<DeviceKey>;
|
|
1022
1030
|
private hydrating;
|
|
1023
1031
|
private renderedCandidate;
|
|
1024
1032
|
private renderedValidationFingerprint;
|
|
@@ -1030,6 +1038,8 @@ declare class DynamicPageConfigEditorComponent implements OnInit, SettingsValueP
|
|
|
1030
1038
|
get hasPendingChanges(): boolean;
|
|
1031
1039
|
get validationStateLabel(): string;
|
|
1032
1040
|
get dirtyStateLabel(): string;
|
|
1041
|
+
get nonLimitError(): string | null;
|
|
1042
|
+
get limitsSummaryError(): string | null;
|
|
1033
1043
|
get selectedLayoutPresetLabel(): string;
|
|
1034
1044
|
get selectedThemePresetLabel(): string;
|
|
1035
1045
|
get selectedLayoutPresetEntry(): LayoutPresetEntry | null;
|
|
@@ -1041,6 +1051,9 @@ declare class DynamicPageConfigEditorComponent implements OnInit, SettingsValueP
|
|
|
1041
1051
|
onSave(): WidgetPageDefinition | null;
|
|
1042
1052
|
reset(): void;
|
|
1043
1053
|
pinRecommendedThemePreset(): void;
|
|
1054
|
+
get hasCanvasWidgets(): boolean;
|
|
1055
|
+
stackDevice(device: DeviceKey): void;
|
|
1056
|
+
private stackedCanvas;
|
|
1044
1057
|
addGroupingNode(seed?: WidgetPageGroupingDefinition): void;
|
|
1045
1058
|
removeGroupingNode(index: number): void;
|
|
1046
1059
|
layoutPresetSummary(preset: LayoutPresetEntry): string;
|
|
@@ -1064,6 +1077,8 @@ declare class DynamicPageConfigEditorComponent implements OnInit, SettingsValueP
|
|
|
1064
1077
|
private replaceGroupingNodes;
|
|
1065
1078
|
private buildCurrentPage;
|
|
1066
1079
|
private buildGrouping;
|
|
1080
|
+
/** A grid must place every existing widget before replacing the flow layout. */
|
|
1081
|
+
private completeCanvasItems;
|
|
1067
1082
|
private buildDeviceLayouts;
|
|
1068
1083
|
private invalidate;
|
|
1069
1084
|
private refreshPanelState;
|