@praxisui/settings-panel 9.0.4-rc.26 → 9.0.4-rc.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,12 +90,17 @@ Peer dependencies (Angular v21):
90
90
  - `title`, `titleIcon` e `subtitle` formam o cabecalho governado do drawer; o shell resolve espacamento, quebra responsiva e relaciona o subtitulo por `aria-describedby`, sem exigir CSS local do host.
91
91
  - O runtime drawer aceita `width`, `minWidth`, `maxWidth`, `resizable` e `persistSizeKey`; o handle visual so aparece quando `resizable=true`.
92
92
  - Quando `persistSizeKey` estiver presente, a largura volta no proximo open do mesmo painel.
93
+ - Uma sessao runtime pode manter uma unica surface primaria e navegar entre etapas com `surfaceRuntime.push(frame)`, `replace(frame)` e `back()`.
94
+ - O shell preserva a instancia Angular, o scroll e o alvo de foco dos frames anteriores. O botao Voltar so aparece quando existe historico e devolve o foco para a etapa restaurada.
95
+ - `surfaceRuntime.navigation$` publica `sessionId`, `activeFrameId`, profundidade e `canGoBack`; esse estado e efemero e nunca deve ser persistido em metadata, `x-ui` ou configuracao de componente.
96
+ - Formularios podem registrar `surfaceRuntime.setCanLeave(guard)` para impedir `back`, `replace`, Escape, backdrop ou fechamento enquanto o lifecycle da etapa exigir confirmacao. O guard e runtime-only e nao substitui regras de negocio nem autorizacao.
93
97
 
94
98
  ## Fronteira canonica
95
99
 
96
100
  - `SettingsPanelService` e `SETTINGS_PANEL_BRIDGE` pertencem a authoring/configuracao.
97
101
  - `SURFACE_DRAWER_BRIDGE` pertence ao runtime de `surface.open`.
98
102
  - Ambos compartilham a mesma infraestrutura base de side panel, mas com shells diferentes.
103
+ - Tipos de componente e callbacks de navegacao pertencem exclusivamente a sessao runtime; JSON governado continua descrevendo a intencao de abrir a surface, nao uma pilha imperativa serializada.
99
104
 
100
105
  ## Customizacao visual
101
106
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-08-04T22:30:51.687Z",
3
+ "generatedAt": "2026-08-04T23:26:34.977Z",
4
4
  "packageName": "@praxisui/settings-panel",
5
- "packageVersion": "9.0.4-rc.26",
5
+ "packageVersion": "9.0.4-rc.27",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 1,
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, ViewContainerRef, HostListener, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, InjectionToken, Injector, Injectable, ChangeDetectorRef, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { inject, DestroyRef, ViewContainerRef, HostListener, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, InjectionToken, Injector, Injectable, ElementRef, ChangeDetectorRef, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import * as i2 from '@angular/common';
4
4
  import { CommonModule, NgStyle, NgClass } from '@angular/common';
5
5
  import { CdkTrapFocus } from '@angular/cdk/a11y';
@@ -438,6 +438,7 @@ const PRAXIS_SETTINGS_PANEL_EN_US = {
438
438
  'Collapse panel': 'Collapse panel',
439
439
  'Expand panel': 'Expand panel',
440
440
  Close: 'Close',
441
+ Back: 'Back',
441
442
  Apply: 'Apply',
442
443
  'Save & Close': 'Save & Close',
443
444
  };
@@ -460,6 +461,7 @@ const PRAXIS_SETTINGS_PANEL_PT_BR = {
460
461
  'Collapse panel': 'Reduzir painel',
461
462
  'Expand panel': 'Expandir painel',
462
463
  Close: 'Fechar',
464
+ Back: 'Voltar',
463
465
  Apply: 'Aplicar',
464
466
  'Save & Close': 'Salvar & Fechar',
465
467
  };
@@ -1811,11 +1813,15 @@ class SurfaceDrawerComponent {
1811
1813
  useHostWidth = false;
1812
1814
  theme = null;
1813
1815
  onClose;
1816
+ onBack;
1814
1817
  static nextId = 0;
1815
1818
  titleId = `praxis-surface-drawer-title-${SurfaceDrawerComponent.nextId++}`;
1816
1819
  subtitleId = `${this.titleId}-description`;
1817
1820
  contentRef;
1818
1821
  contentHost;
1822
+ body;
1823
+ backButton;
1824
+ frames = [];
1819
1825
  constructor(cdr, i18n) {
1820
1826
  this.cdr = cdr;
1821
1827
  this.i18n = i18n;
@@ -1832,6 +1838,21 @@ class SurfaceDrawerComponent {
1832
1838
  : 'Fechar';
1833
1839
  return this.i18n.t('Close', undefined, fallback, PRAXIS_SETTINGS_PANEL_I18N_NAMESPACE);
1834
1840
  }
1841
+ get backLabel() {
1842
+ const fallback = this.i18n.getLocale().toLowerCase().startsWith('en')
1843
+ ? 'Back'
1844
+ : 'Voltar';
1845
+ return this.i18n.t('Back', undefined, fallback, PRAXIS_SETTINGS_PANEL_I18N_NAMESPACE);
1846
+ }
1847
+ get canGoBack() {
1848
+ return this.frames.length > 1;
1849
+ }
1850
+ get navigationDepth() {
1851
+ return this.frames.length;
1852
+ }
1853
+ get activeFrameId() {
1854
+ return this.frames[this.frames.length - 1]?.definition.id ?? '';
1855
+ }
1835
1856
  get hasHeader() {
1836
1857
  return !!(this.title || this.titleIcon || this.subtitle);
1837
1858
  }
@@ -1845,11 +1866,156 @@ class SurfaceDrawerComponent {
1845
1866
  this.title = title;
1846
1867
  this.cdr.markForCheck();
1847
1868
  }
1869
+ ngOnDestroy() {
1870
+ for (const frame of this.frames) {
1871
+ if (!frame.componentRef.hostView.destroyed) {
1872
+ frame.componentRef.destroy();
1873
+ }
1874
+ }
1875
+ this.frames = [];
1876
+ }
1848
1877
  attachContent(component, injector, inputs) {
1849
- this.contentRef = this.contentHost.createComponent(component, { injector });
1850
- this.applyInputs(this.contentRef, inputs);
1878
+ return this.initializeFrame({
1879
+ id: 'root',
1880
+ title: this.title,
1881
+ titleIcon: this.titleIcon,
1882
+ subtitle: this.subtitle,
1883
+ content: { component, inputs },
1884
+ }, injector, inputs);
1885
+ }
1886
+ initializeFrame(definition, injector, inputs) {
1887
+ this.destroyAllFrames();
1888
+ const frame = this.createFrame(definition, injector, inputs);
1889
+ this.frames = [frame];
1890
+ this.applyFrameHeader(frame);
1891
+ return frame.componentRef;
1892
+ }
1893
+ pushFrame(definition, injector, inputs) {
1894
+ this.captureActiveFrameState();
1895
+ this.detachActiveView();
1896
+ const frame = this.createFrame(definition, injector, inputs);
1897
+ this.frames.push(frame);
1898
+ this.applyFrameHeader(frame);
1899
+ this.focusNavigationControl();
1900
+ return frame.componentRef;
1901
+ }
1902
+ async replaceFrame(definition, injector, inputs) {
1903
+ if (!(await this.canLeaveActiveFrame())) {
1904
+ return null;
1905
+ }
1906
+ this.removeActiveFrame();
1907
+ const frame = this.createFrame(definition, injector, inputs);
1908
+ this.frames.push(frame);
1909
+ this.applyFrameHeader(frame);
1910
+ this.focusNavigationControl();
1911
+ return frame.componentRef;
1912
+ }
1913
+ async backFrame() {
1914
+ if (!this.canGoBack || !(await this.canLeaveActiveFrame())) {
1915
+ return false;
1916
+ }
1917
+ this.removeActiveFrame();
1918
+ const previous = this.frames[this.frames.length - 1];
1919
+ if (!previous) {
1920
+ return false;
1921
+ }
1922
+ this.contentHost.insert(previous.componentRef.hostView);
1923
+ this.contentRef = previous.componentRef;
1924
+ this.applyFrameHeader(previous);
1925
+ queueMicrotask(() => {
1926
+ if (this.body?.nativeElement) {
1927
+ this.body.nativeElement.scrollTop = previous.scrollTop;
1928
+ }
1929
+ const target = previous.restoreFocusTo;
1930
+ if (target?.isConnected) {
1931
+ target.focus();
1932
+ }
1933
+ else {
1934
+ this.backButton?.nativeElement.focus();
1935
+ }
1936
+ });
1937
+ return true;
1938
+ }
1939
+ setActiveCanLeave(guard) {
1940
+ const active = this.frames[this.frames.length - 1];
1941
+ if (active) {
1942
+ active.canLeave = guard;
1943
+ }
1944
+ }
1945
+ async canCloseSession() {
1946
+ for (let index = this.frames.length - 1; index >= 0; index -= 1) {
1947
+ const guard = this.frames[index].canLeave;
1948
+ if (guard && !(await Promise.resolve(guard()))) {
1949
+ return false;
1950
+ }
1951
+ }
1952
+ return true;
1953
+ }
1954
+ createFrame(definition, injector, inputs) {
1955
+ const componentRef = this.contentHost.createComponent(definition.content.component, { injector });
1956
+ this.applyInputs(componentRef, inputs);
1957
+ this.contentRef = componentRef;
1851
1958
  this.cdr.markForCheck();
1852
- return this.contentRef;
1959
+ return {
1960
+ definition,
1961
+ componentRef,
1962
+ scrollTop: 0,
1963
+ restoreFocusTo: null,
1964
+ canLeave: definition.canLeave ?? null,
1965
+ };
1966
+ }
1967
+ captureActiveFrameState() {
1968
+ const active = this.frames[this.frames.length - 1];
1969
+ if (!active)
1970
+ return;
1971
+ active.scrollTop = this.body?.nativeElement.scrollTop ?? 0;
1972
+ const focused = typeof document !== 'undefined'
1973
+ ? document.activeElement
1974
+ : null;
1975
+ active.restoreFocusTo = focused instanceof HTMLElement ? focused : null;
1976
+ }
1977
+ detachActiveView() {
1978
+ const active = this.frames[this.frames.length - 1];
1979
+ if (!active)
1980
+ return;
1981
+ const index = this.contentHost.indexOf(active.componentRef.hostView);
1982
+ if (index >= 0) {
1983
+ this.contentHost.detach(index);
1984
+ }
1985
+ }
1986
+ removeActiveFrame() {
1987
+ const active = this.frames.pop();
1988
+ if (!active)
1989
+ return;
1990
+ const index = this.contentHost.indexOf(active.componentRef.hostView);
1991
+ if (index >= 0) {
1992
+ this.contentHost.remove(index);
1993
+ }
1994
+ else if (!active.componentRef.hostView.destroyed) {
1995
+ active.componentRef.destroy();
1996
+ }
1997
+ }
1998
+ destroyAllFrames() {
1999
+ this.contentHost.clear();
2000
+ for (const frame of this.frames) {
2001
+ if (!frame.componentRef.hostView.destroyed) {
2002
+ frame.componentRef.destroy();
2003
+ }
2004
+ }
2005
+ this.frames = [];
2006
+ }
2007
+ async canLeaveActiveFrame() {
2008
+ const guard = this.frames[this.frames.length - 1]?.canLeave;
2009
+ return guard ? Promise.resolve(guard()) : true;
2010
+ }
2011
+ applyFrameHeader(frame) {
2012
+ this.title = frame.definition.title;
2013
+ this.titleIcon = frame.definition.titleIcon;
2014
+ this.subtitle = frame.definition.subtitle;
2015
+ this.cdr.markForCheck();
2016
+ }
2017
+ focusNavigationControl() {
2018
+ queueMicrotask(() => this.backButton?.nativeElement.focus());
1853
2019
  }
1854
2020
  applyInputs(componentRef, inputs) {
1855
2021
  if (!inputs) {
@@ -1882,7 +2048,7 @@ class SurfaceDrawerComponent {
1882
2048
  return new Set(Object.keys(declaredInputs));
1883
2049
  }
1884
2050
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SurfaceDrawerComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i1$2.PraxisI18nService }], target: i0.ɵɵFactoryTarget.Component });
1885
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: SurfaceDrawerComponent, isStandalone: true, selector: "praxis-surface-drawer", viewQueries: [{ propertyName: "contentHost", first: true, predicate: ["contentHost"], descendants: true, read: ViewContainerRef, static: true }], ngImport: i0, template: "<div\n class=\"pfx-surface-drawer\"\n [class.has-header]=\"hasHeader\"\n [class.width-narrow]=\"widthClass === 'width-narrow'\"\n [class.width-default]=\"widthClass === 'width-default'\"\n [class.width-wide]=\"widthClass === 'width-wide'\"\n [class.width-full]=\"widthClass === 'width-full'\"\n [class.use-host-width]=\"useHostWidth\"\n [ngClass]=\"hostClasses\"\n [ngStyle]=\"hostCssVars\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"hasHeader ? titleId : null\"\n [attr.aria-describedby]=\"subtitle ? subtitleId : null\"\n cdkTrapFocus\n cdkTrapFocusAutoCapture\n>\n @if (hasHeader) {\n <header class=\"pfx-surface-drawer__header\">\n <div class=\"pfx-surface-drawer__title-block\">\n @if (titleIcon) {\n <mat-icon class=\"pfx-surface-drawer__title-icon\" [praxisIcon]=\"titleIcon\"></mat-icon>\n }\n <div class=\"pfx-surface-drawer__copy\">\n @if (title) {\n <h2 class=\"pfx-surface-drawer__title\" [id]=\"titleId\">{{ title }}</h2>\n }\n @if (subtitle) {\n <p class=\"pfx-surface-drawer__subtitle\" [id]=\"subtitleId\">{{ subtitle }}</p>\n }\n </div>\n </div>\n <button\n praxisIconButton=\"close\"\n size=\"compact\"\n type=\"button\"\n class=\"pfx-surface-drawer__close\"\n [attr.aria-label]=\"closeLabel\"\n [matTooltip]=\"closeLabel\"\n (click)=\"onClose?.()\"\n ></button>\n </header>\n }\n\n <section class=\"pfx-surface-drawer__body\">\n <ng-template #contentHost></ng-template>\n </section>\n</div>\n", styles: [":host{display:block;height:100%;width:100%;min-width:0;flex:1 1 auto}.pfx-surface-drawer{display:grid;grid-template-rows:auto 1fr;height:100%;min-width:0;max-width:100%;overflow:hidden;background:var(--md-sys-color-surface-container, #fff);color:var(--md-sys-color-on-surface, #111);border-left:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .12));border-radius:var(--pfx-side-panel-border-radius, 0);box-shadow:var(--pfx-side-panel-elevation, var(--md-sys-elevation-level2, 0 8px 24px rgba(0, 0, 0, .16)));width:var(--pfx-side-panel-width, 720px);transition:width var(--pfx-side-panel-motion-duration-enter, .22s) var(--pfx-side-panel-motion-easing-enter, cubic-bezier(.2, 0, 0, 1)),box-shadow var(--pfx-side-panel-motion-duration-enter, .22s) var(--pfx-side-panel-motion-easing-enter, cubic-bezier(.2, 0, 0, 1))}.pfx-surface-drawer.use-host-width{width:100%;max-width:100%}.pfx-surface-drawer.width-narrow{width:var(--pfx-side-panel-width, 420px)}.pfx-surface-drawer.width-default{width:var(--pfx-side-panel-width, 720px)}.pfx-surface-drawer.width-wide{width:var(--pfx-side-panel-width, 960px)}.pfx-surface-drawer.width-full{width:min(100vw,var(--pfx-side-panel-max-width, 100vw))}.pfx-surface-drawer__header{display:flex;align-items:flex-start;gap:var(--pfx-side-panel-header-gap, 12px);min-height:var(--pfx-side-panel-header-height, 64px);padding:var(--pfx-side-panel-header-padding-y, 16px) var(--pfx-side-panel-header-padding-x, 20px);box-sizing:border-box;border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .12));background:var(--md-sys-color-surface-container-high, #f7f7f7)}.pfx-surface-drawer__title-block{display:flex;align-items:flex-start;gap:12px;min-width:0;flex:1 1 auto}.pfx-surface-drawer__title-icon{flex:0 0 auto;display:grid;place-items:center;width:36px;height:36px;margin-top:1px;border-radius:10px;background:var(--md-sys-color-primary-container, rgba(47, 78, 178, .12));color:var(--md-sys-color-primary, currentColor)}.pfx-surface-drawer__copy{min-width:0}.pfx-surface-drawer__title,.pfx-surface-drawer__subtitle{margin:0}.pfx-surface-drawer__title{font-size:1.125rem;font-weight:650;line-height:1.35}.pfx-surface-drawer__subtitle{margin-top:6px;color:var(--md-sys-color-on-surface-variant, rgba(0, 0, 0, .64));font-size:.875rem;line-height:1.45;text-wrap:pretty}.pfx-surface-drawer__close{flex:0 0 auto}.pfx-surface-drawer__body{min-height:0;overflow:auto;padding:var(--pfx-side-panel-body-padding, 16px);background:var(--md-sys-color-surface, #fff)}@media(prefers-reduced-motion:reduce){.pfx-surface-drawer{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2051
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: SurfaceDrawerComponent, isStandalone: true, selector: "praxis-surface-drawer", viewQueries: [{ propertyName: "contentHost", first: true, predicate: ["contentHost"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "body", first: true, predicate: ["body"], descendants: true, read: ElementRef }, { propertyName: "backButton", first: true, predicate: ["backButton"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div\n class=\"pfx-surface-drawer\"\n [class.has-header]=\"hasHeader\"\n [class.width-narrow]=\"widthClass === 'width-narrow'\"\n [class.width-default]=\"widthClass === 'width-default'\"\n [class.width-wide]=\"widthClass === 'width-wide'\"\n [class.width-full]=\"widthClass === 'width-full'\"\n [class.use-host-width]=\"useHostWidth\"\n [ngClass]=\"hostClasses\"\n [ngStyle]=\"hostCssVars\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"hasHeader ? titleId : null\"\n [attr.aria-describedby]=\"subtitle ? subtitleId : null\"\n cdkTrapFocus\n cdkTrapFocusAutoCapture\n>\n @if (hasHeader) {\n <header class=\"pfx-surface-drawer__header\">\n @if (canGoBack) {\n <button\n #backButton\n praxisIconButton=\"arrow_back\"\n size=\"compact\"\n type=\"button\"\n class=\"pfx-surface-drawer__back\"\n [attr.aria-label]=\"backLabel\"\n [matTooltip]=\"backLabel\"\n (click)=\"onBack?.()\"\n ></button>\n }\n <div class=\"pfx-surface-drawer__title-block\">\n @if (titleIcon) {\n <mat-icon class=\"pfx-surface-drawer__title-icon\" [praxisIcon]=\"titleIcon\"></mat-icon>\n }\n <div class=\"pfx-surface-drawer__copy\">\n @if (title) {\n <h2 class=\"pfx-surface-drawer__title\" [id]=\"titleId\">{{ title }}</h2>\n }\n @if (subtitle) {\n <p class=\"pfx-surface-drawer__subtitle\" [id]=\"subtitleId\">{{ subtitle }}</p>\n }\n </div>\n </div>\n <button\n praxisIconButton=\"close\"\n size=\"compact\"\n type=\"button\"\n class=\"pfx-surface-drawer__close\"\n [attr.aria-label]=\"closeLabel\"\n [matTooltip]=\"closeLabel\"\n (click)=\"onClose?.()\"\n ></button>\n </header>\n }\n\n <section #body class=\"pfx-surface-drawer__body\">\n <ng-template #contentHost></ng-template>\n </section>\n</div>\n", styles: [":host{display:block;height:100%;width:100%;min-width:0;flex:1 1 auto}.pfx-surface-drawer{display:grid;grid-template-rows:auto 1fr;height:100%;min-width:0;max-width:100%;overflow:hidden;background:var(--md-sys-color-surface-container, #fff);color:var(--md-sys-color-on-surface, #111);border-left:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .12));border-radius:var(--pfx-side-panel-border-radius, 0);box-shadow:var(--pfx-side-panel-elevation, var(--md-sys-elevation-level2, 0 8px 24px rgba(0, 0, 0, .16)));width:var(--pfx-side-panel-width, 720px);transition:width var(--pfx-side-panel-motion-duration-enter, .22s) var(--pfx-side-panel-motion-easing-enter, cubic-bezier(.2, 0, 0, 1)),box-shadow var(--pfx-side-panel-motion-duration-enter, .22s) var(--pfx-side-panel-motion-easing-enter, cubic-bezier(.2, 0, 0, 1))}.pfx-surface-drawer.use-host-width{width:100%;max-width:100%}.pfx-surface-drawer.width-narrow{width:var(--pfx-side-panel-width, 420px)}.pfx-surface-drawer.width-default{width:var(--pfx-side-panel-width, 720px)}.pfx-surface-drawer.width-wide{width:var(--pfx-side-panel-width, 960px)}.pfx-surface-drawer.width-full{width:min(100vw,var(--pfx-side-panel-max-width, 100vw))}.pfx-surface-drawer__header{display:flex;align-items:flex-start;gap:var(--pfx-side-panel-header-gap, 12px);min-height:var(--pfx-side-panel-header-height, 64px);padding:var(--pfx-side-panel-header-padding-y, 16px) var(--pfx-side-panel-header-padding-x, 20px);box-sizing:border-box;border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .12));background:var(--md-sys-color-surface-container-high, #f7f7f7)}.pfx-surface-drawer__title-block{display:flex;align-items:flex-start;gap:12px;min-width:0;flex:1 1 auto}.pfx-surface-drawer__title-icon{flex:0 0 auto;display:grid;place-items:center;width:36px;height:36px;margin-top:1px;border-radius:10px;background:var(--md-sys-color-primary-container, rgba(47, 78, 178, .12));color:var(--md-sys-color-primary, currentColor)}.pfx-surface-drawer__copy{min-width:0}.pfx-surface-drawer__title,.pfx-surface-drawer__subtitle{margin:0}.pfx-surface-drawer__title{font-size:1.125rem;font-weight:650;line-height:1.35}.pfx-surface-drawer__subtitle{margin-top:6px;color:var(--md-sys-color-on-surface-variant, rgba(0, 0, 0, .64));font-size:.875rem;line-height:1.45;text-wrap:pretty}.pfx-surface-drawer__close{flex:0 0 auto}.pfx-surface-drawer__back{flex:0 0 auto;margin-top:1px}.pfx-surface-drawer__body{min-height:0;overflow:auto;padding:var(--pfx-side-panel-body-padding, 16px);background:var(--md-sys-color-surface, #fff)}@media(prefers-reduced-motion:reduce){.pfx-surface-drawer{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1886
2052
  }
1887
2053
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SurfaceDrawerComponent, decorators: [{
1888
2054
  type: Component,
@@ -1895,10 +2061,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1895
2061
  CdkTrapFocus,
1896
2062
  PraxisIconButtonComponent,
1897
2063
  PraxisIconDirective,
1898
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"pfx-surface-drawer\"\n [class.has-header]=\"hasHeader\"\n [class.width-narrow]=\"widthClass === 'width-narrow'\"\n [class.width-default]=\"widthClass === 'width-default'\"\n [class.width-wide]=\"widthClass === 'width-wide'\"\n [class.width-full]=\"widthClass === 'width-full'\"\n [class.use-host-width]=\"useHostWidth\"\n [ngClass]=\"hostClasses\"\n [ngStyle]=\"hostCssVars\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"hasHeader ? titleId : null\"\n [attr.aria-describedby]=\"subtitle ? subtitleId : null\"\n cdkTrapFocus\n cdkTrapFocusAutoCapture\n>\n @if (hasHeader) {\n <header class=\"pfx-surface-drawer__header\">\n <div class=\"pfx-surface-drawer__title-block\">\n @if (titleIcon) {\n <mat-icon class=\"pfx-surface-drawer__title-icon\" [praxisIcon]=\"titleIcon\"></mat-icon>\n }\n <div class=\"pfx-surface-drawer__copy\">\n @if (title) {\n <h2 class=\"pfx-surface-drawer__title\" [id]=\"titleId\">{{ title }}</h2>\n }\n @if (subtitle) {\n <p class=\"pfx-surface-drawer__subtitle\" [id]=\"subtitleId\">{{ subtitle }}</p>\n }\n </div>\n </div>\n <button\n praxisIconButton=\"close\"\n size=\"compact\"\n type=\"button\"\n class=\"pfx-surface-drawer__close\"\n [attr.aria-label]=\"closeLabel\"\n [matTooltip]=\"closeLabel\"\n (click)=\"onClose?.()\"\n ></button>\n </header>\n }\n\n <section class=\"pfx-surface-drawer__body\">\n <ng-template #contentHost></ng-template>\n </section>\n</div>\n", styles: [":host{display:block;height:100%;width:100%;min-width:0;flex:1 1 auto}.pfx-surface-drawer{display:grid;grid-template-rows:auto 1fr;height:100%;min-width:0;max-width:100%;overflow:hidden;background:var(--md-sys-color-surface-container, #fff);color:var(--md-sys-color-on-surface, #111);border-left:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .12));border-radius:var(--pfx-side-panel-border-radius, 0);box-shadow:var(--pfx-side-panel-elevation, var(--md-sys-elevation-level2, 0 8px 24px rgba(0, 0, 0, .16)));width:var(--pfx-side-panel-width, 720px);transition:width var(--pfx-side-panel-motion-duration-enter, .22s) var(--pfx-side-panel-motion-easing-enter, cubic-bezier(.2, 0, 0, 1)),box-shadow var(--pfx-side-panel-motion-duration-enter, .22s) var(--pfx-side-panel-motion-easing-enter, cubic-bezier(.2, 0, 0, 1))}.pfx-surface-drawer.use-host-width{width:100%;max-width:100%}.pfx-surface-drawer.width-narrow{width:var(--pfx-side-panel-width, 420px)}.pfx-surface-drawer.width-default{width:var(--pfx-side-panel-width, 720px)}.pfx-surface-drawer.width-wide{width:var(--pfx-side-panel-width, 960px)}.pfx-surface-drawer.width-full{width:min(100vw,var(--pfx-side-panel-max-width, 100vw))}.pfx-surface-drawer__header{display:flex;align-items:flex-start;gap:var(--pfx-side-panel-header-gap, 12px);min-height:var(--pfx-side-panel-header-height, 64px);padding:var(--pfx-side-panel-header-padding-y, 16px) var(--pfx-side-panel-header-padding-x, 20px);box-sizing:border-box;border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .12));background:var(--md-sys-color-surface-container-high, #f7f7f7)}.pfx-surface-drawer__title-block{display:flex;align-items:flex-start;gap:12px;min-width:0;flex:1 1 auto}.pfx-surface-drawer__title-icon{flex:0 0 auto;display:grid;place-items:center;width:36px;height:36px;margin-top:1px;border-radius:10px;background:var(--md-sys-color-primary-container, rgba(47, 78, 178, .12));color:var(--md-sys-color-primary, currentColor)}.pfx-surface-drawer__copy{min-width:0}.pfx-surface-drawer__title,.pfx-surface-drawer__subtitle{margin:0}.pfx-surface-drawer__title{font-size:1.125rem;font-weight:650;line-height:1.35}.pfx-surface-drawer__subtitle{margin-top:6px;color:var(--md-sys-color-on-surface-variant, rgba(0, 0, 0, .64));font-size:.875rem;line-height:1.45;text-wrap:pretty}.pfx-surface-drawer__close{flex:0 0 auto}.pfx-surface-drawer__body{min-height:0;overflow:auto;padding:var(--pfx-side-panel-body-padding, 16px);background:var(--md-sys-color-surface, #fff)}@media(prefers-reduced-motion:reduce){.pfx-surface-drawer{transition:none}}\n"] }]
2064
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"pfx-surface-drawer\"\n [class.has-header]=\"hasHeader\"\n [class.width-narrow]=\"widthClass === 'width-narrow'\"\n [class.width-default]=\"widthClass === 'width-default'\"\n [class.width-wide]=\"widthClass === 'width-wide'\"\n [class.width-full]=\"widthClass === 'width-full'\"\n [class.use-host-width]=\"useHostWidth\"\n [ngClass]=\"hostClasses\"\n [ngStyle]=\"hostCssVars\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"hasHeader ? titleId : null\"\n [attr.aria-describedby]=\"subtitle ? subtitleId : null\"\n cdkTrapFocus\n cdkTrapFocusAutoCapture\n>\n @if (hasHeader) {\n <header class=\"pfx-surface-drawer__header\">\n @if (canGoBack) {\n <button\n #backButton\n praxisIconButton=\"arrow_back\"\n size=\"compact\"\n type=\"button\"\n class=\"pfx-surface-drawer__back\"\n [attr.aria-label]=\"backLabel\"\n [matTooltip]=\"backLabel\"\n (click)=\"onBack?.()\"\n ></button>\n }\n <div class=\"pfx-surface-drawer__title-block\">\n @if (titleIcon) {\n <mat-icon class=\"pfx-surface-drawer__title-icon\" [praxisIcon]=\"titleIcon\"></mat-icon>\n }\n <div class=\"pfx-surface-drawer__copy\">\n @if (title) {\n <h2 class=\"pfx-surface-drawer__title\" [id]=\"titleId\">{{ title }}</h2>\n }\n @if (subtitle) {\n <p class=\"pfx-surface-drawer__subtitle\" [id]=\"subtitleId\">{{ subtitle }}</p>\n }\n </div>\n </div>\n <button\n praxisIconButton=\"close\"\n size=\"compact\"\n type=\"button\"\n class=\"pfx-surface-drawer__close\"\n [attr.aria-label]=\"closeLabel\"\n [matTooltip]=\"closeLabel\"\n (click)=\"onClose?.()\"\n ></button>\n </header>\n }\n\n <section #body class=\"pfx-surface-drawer__body\">\n <ng-template #contentHost></ng-template>\n </section>\n</div>\n", styles: [":host{display:block;height:100%;width:100%;min-width:0;flex:1 1 auto}.pfx-surface-drawer{display:grid;grid-template-rows:auto 1fr;height:100%;min-width:0;max-width:100%;overflow:hidden;background:var(--md-sys-color-surface-container, #fff);color:var(--md-sys-color-on-surface, #111);border-left:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .12));border-radius:var(--pfx-side-panel-border-radius, 0);box-shadow:var(--pfx-side-panel-elevation, var(--md-sys-elevation-level2, 0 8px 24px rgba(0, 0, 0, .16)));width:var(--pfx-side-panel-width, 720px);transition:width var(--pfx-side-panel-motion-duration-enter, .22s) var(--pfx-side-panel-motion-easing-enter, cubic-bezier(.2, 0, 0, 1)),box-shadow var(--pfx-side-panel-motion-duration-enter, .22s) var(--pfx-side-panel-motion-easing-enter, cubic-bezier(.2, 0, 0, 1))}.pfx-surface-drawer.use-host-width{width:100%;max-width:100%}.pfx-surface-drawer.width-narrow{width:var(--pfx-side-panel-width, 420px)}.pfx-surface-drawer.width-default{width:var(--pfx-side-panel-width, 720px)}.pfx-surface-drawer.width-wide{width:var(--pfx-side-panel-width, 960px)}.pfx-surface-drawer.width-full{width:min(100vw,var(--pfx-side-panel-max-width, 100vw))}.pfx-surface-drawer__header{display:flex;align-items:flex-start;gap:var(--pfx-side-panel-header-gap, 12px);min-height:var(--pfx-side-panel-header-height, 64px);padding:var(--pfx-side-panel-header-padding-y, 16px) var(--pfx-side-panel-header-padding-x, 20px);box-sizing:border-box;border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .12));background:var(--md-sys-color-surface-container-high, #f7f7f7)}.pfx-surface-drawer__title-block{display:flex;align-items:flex-start;gap:12px;min-width:0;flex:1 1 auto}.pfx-surface-drawer__title-icon{flex:0 0 auto;display:grid;place-items:center;width:36px;height:36px;margin-top:1px;border-radius:10px;background:var(--md-sys-color-primary-container, rgba(47, 78, 178, .12));color:var(--md-sys-color-primary, currentColor)}.pfx-surface-drawer__copy{min-width:0}.pfx-surface-drawer__title,.pfx-surface-drawer__subtitle{margin:0}.pfx-surface-drawer__title{font-size:1.125rem;font-weight:650;line-height:1.35}.pfx-surface-drawer__subtitle{margin-top:6px;color:var(--md-sys-color-on-surface-variant, rgba(0, 0, 0, .64));font-size:.875rem;line-height:1.45;text-wrap:pretty}.pfx-surface-drawer__close{flex:0 0 auto}.pfx-surface-drawer__back{flex:0 0 auto;margin-top:1px}.pfx-surface-drawer__body{min-height:0;overflow:auto;padding:var(--pfx-side-panel-body-padding, 16px);background:var(--md-sys-color-surface, #fff)}@media(prefers-reduced-motion:reduce){.pfx-surface-drawer{transition:none}}\n"] }]
1899
2065
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i1$2.PraxisI18nService }], propDecorators: { contentHost: [{
1900
2066
  type: ViewChild,
1901
2067
  args: ['contentHost', { read: ViewContainerRef, static: true }]
2068
+ }], body: [{
2069
+ type: ViewChild,
2070
+ args: ['body', { read: ElementRef }]
2071
+ }], backButton: [{
2072
+ type: ViewChild,
2073
+ args: ['backButton', { read: ElementRef }]
1902
2074
  }] } });
1903
2075
 
1904
2076
  function buildSurfaceRuntime(ref) {
@@ -1910,6 +2082,12 @@ function buildSurfaceRuntime(ref) {
1910
2082
  complete: (outcome) => ref.complete?.(outcome),
1911
2083
  updateTitle: (title) => ref.updateTitle?.(title),
1912
2084
  updateSize: (preset) => ref.updateSize?.(preset),
2085
+ navigation$: ref.navigation$,
2086
+ push: (frame) => ref.push?.(frame),
2087
+ replace: (frame) => ref.replace?.(frame),
2088
+ back: () => ref.back?.(),
2089
+ canGoBack: () => ref.canGoBack?.() ?? false,
2090
+ setCanLeave: (guard) => ref.setCanLeave?.(guard),
1913
2091
  };
1914
2092
  }
1915
2093
  function enrichSurfaceContentInputs(inputs, ref) {
@@ -1955,7 +2133,157 @@ function providePraxisSurfaceDrawerBridge() {
1955
2133
  useFactory: (service, injector, layerScale) => ({
1956
2134
  open: (opts) => {
1957
2135
  layerScale.ensureInstalled();
1958
- return service.openHost({
2136
+ const sessionId = `${opts.id}:${Date.now().toString(36)}`;
2137
+ const navigationState = new BehaviorSubject({
2138
+ sessionId,
2139
+ activeFrameId: opts.id,
2140
+ depth: 1,
2141
+ canGoBack: false,
2142
+ });
2143
+ let panel = null;
2144
+ let activeRef = null;
2145
+ const frameRefs = [];
2146
+ const createContent = (frame, mode, runtimeRef) => {
2147
+ if (!panel)
2148
+ return null;
2149
+ const contentInputs = enrichSurfaceContentInputs(frame.content.inputs, runtimeRef);
2150
+ const contentInjector = Injector.create({
2151
+ providers: [
2152
+ { provide: BASE_SIDE_PANEL_DATA, useValue: contentInputs },
2153
+ { provide: BASE_SIDE_PANEL_REF, useValue: runtimeRef },
2154
+ ],
2155
+ parent: injector,
2156
+ });
2157
+ if (mode === 'initial') {
2158
+ return panel.initializeFrame(frame, contentInjector, contentInputs);
2159
+ }
2160
+ if (mode === 'push') {
2161
+ return panel.pushFrame(frame, contentInjector, contentInputs);
2162
+ }
2163
+ return panel.replaceFrame(frame, contentInjector, contentInputs);
2164
+ };
2165
+ const publishNavigationState = () => {
2166
+ if (!panel)
2167
+ return;
2168
+ navigationState.next({
2169
+ sessionId,
2170
+ activeFrameId: panel.activeFrameId,
2171
+ depth: panel.navigationDepth,
2172
+ canGoBack: panel.canGoBack,
2173
+ });
2174
+ };
2175
+ const closeFrameRef = (frameRef, result) => {
2176
+ const runtime = frameRef;
2177
+ runtime.__closeFrame?.(result);
2178
+ };
2179
+ const navigateBack = async (result) => {
2180
+ const leaving = frameRefs[frameRefs.length - 1];
2181
+ const moved = await panel?.backFrame() ?? false;
2182
+ if (!moved)
2183
+ return false;
2184
+ if (leaving) {
2185
+ frameRefs.pop();
2186
+ closeFrameRef(leaving, result ?? { type: 'back' });
2187
+ }
2188
+ publishNavigationState();
2189
+ return true;
2190
+ };
2191
+ const createFrameRef = (frameId) => {
2192
+ const closedSubject = new Subject();
2193
+ const resultSubject = new Subject();
2194
+ let closed = false;
2195
+ let cleanup = null;
2196
+ const frameRef = {
2197
+ frameId,
2198
+ closed$: closedSubject.asObservable(),
2199
+ result$: resultSubject.asObservable(),
2200
+ navigation$: navigationState.asObservable(),
2201
+ close: (result) => { void navigateBack(result); },
2202
+ emitResult: (result) => resultSubject.next(result),
2203
+ complete: (outcome) => {
2204
+ resultSubject.next(outcome);
2205
+ void navigateBack(outcome);
2206
+ },
2207
+ updateTitle: (title) => activeRef?.updateTitle?.(title),
2208
+ updateSize: (preset) => activeRef?.updateSize?.(preset),
2209
+ push: (frame) => activeRef?.push?.(frame) ?? Promise.resolve(null),
2210
+ replace: (frame) => activeRef?.replace?.(frame) ?? Promise.resolve(null),
2211
+ back: () => activeRef?.back?.() ?? Promise.resolve(false),
2212
+ canGoBack: () => activeRef?.canGoBack?.() ?? false,
2213
+ setCanLeave: (guard) => activeRef?.setCanLeave?.(guard),
2214
+ __closeFrame: (result) => {
2215
+ if (closed)
2216
+ return;
2217
+ closed = true;
2218
+ cleanup?.();
2219
+ cleanup = null;
2220
+ closedSubject.next(result);
2221
+ closedSubject.complete();
2222
+ resultSubject.complete();
2223
+ },
2224
+ __setCleanup: (nextCleanup) => {
2225
+ cleanup?.();
2226
+ cleanup = nextCleanup;
2227
+ },
2228
+ };
2229
+ return frameRef;
2230
+ };
2231
+ const configureRef = (ref) => {
2232
+ activeRef = ref;
2233
+ if (ref.navigation$) {
2234
+ return ref;
2235
+ }
2236
+ ref.navigation$ = navigationState.asObservable();
2237
+ ref.canGoBack = () => panel?.canGoBack ?? false;
2238
+ ref.setCanLeave = (guard) => panel?.setActiveCanLeave(guard);
2239
+ ref.push = async (frame) => {
2240
+ const frameRef = createFrameRef(frame.id);
2241
+ const contentRef = await createContent(frame, 'push', frameRef);
2242
+ if (!contentRef)
2243
+ return null;
2244
+ frameRefs.push(frameRef);
2245
+ frameRef.__setCleanup?.(bindSurfaceContentSelectionResults(contentRef.instance, frameRef));
2246
+ publishNavigationState();
2247
+ return frameRef;
2248
+ };
2249
+ ref.replace = async (frame) => {
2250
+ if ((panel?.navigationDepth ?? 1) === 1) {
2251
+ const rootRef = frameRefs[0]
2252
+ ?? ref;
2253
+ const contentRef = await createContent(frame, 'replace', rootRef);
2254
+ if (!contentRef)
2255
+ return null;
2256
+ Object.defineProperty(rootRef, 'frameId', {
2257
+ configurable: true,
2258
+ value: frame.id,
2259
+ });
2260
+ bindSurfaceContentSelectionResults(contentRef.instance, rootRef);
2261
+ publishNavigationState();
2262
+ return rootRef;
2263
+ }
2264
+ const frameRef = createFrameRef(frame.id);
2265
+ const contentRef = await createContent(frame, 'replace', frameRef);
2266
+ if (!contentRef)
2267
+ return null;
2268
+ const replaced = frameRefs.pop();
2269
+ if (replaced)
2270
+ closeFrameRef(replaced, { type: 'replace' });
2271
+ frameRefs.push(frameRef);
2272
+ frameRef.__setCleanup?.(bindSurfaceContentSelectionResults(contentRef.instance, frameRef));
2273
+ publishNavigationState();
2274
+ return frameRef;
2275
+ };
2276
+ ref.back = () => navigateBack();
2277
+ if (frameRefs.length === 0) {
2278
+ Object.defineProperty(ref, 'frameId', {
2279
+ configurable: true,
2280
+ value: opts.id,
2281
+ });
2282
+ frameRefs.push(ref);
2283
+ }
2284
+ return ref;
2285
+ };
2286
+ const result = service.openHost({
1959
2287
  component: SurfaceDrawerComponent,
1960
2288
  config: {
1961
2289
  id: opts.id,
@@ -1983,26 +2311,32 @@ function providePraxisSurfaceDrawerBridge() {
1983
2311
  minHeight: opts.minHeight,
1984
2312
  maxHeight: opts.maxHeight,
1985
2313
  },
1986
- createRef: (overlayRef) => new BaseSidePanelOverlayRef(overlayRef),
2314
+ createRef: (overlayRef) => {
2315
+ const ref = new BaseSidePanelOverlayRef(overlayRef);
2316
+ return configureRef(ref);
2317
+ },
1987
2318
  attach: (panelRef, ref) => {
1988
- const contentInputs = enrichSurfaceContentInputs(opts.content.inputs, ref);
1989
- panelRef.instance.title = opts.title;
1990
- panelRef.instance.titleIcon = opts.titleIcon;
1991
- panelRef.instance.subtitle = opts.subtitle;
2319
+ panel = panelRef.instance;
2320
+ configureRef(ref);
1992
2321
  panelRef.instance.widthPreset = opts.widthPreset ?? 'default';
1993
2322
  panelRef.instance.useHostWidth = !!(opts.width ||
1994
2323
  opts.minWidth ||
1995
2324
  opts.maxWidth);
1996
- panelRef.instance.onClose = () => ref.close({ type: 'close' });
2325
+ panelRef.instance.onClose = () => {
2326
+ void panelRef.instance.canCloseSession().then((allowed) => {
2327
+ if (allowed)
2328
+ ref.close({ type: 'close' });
2329
+ });
2330
+ };
2331
+ panelRef.instance.onBack = () => { void activeRef?.back?.(); };
1997
2332
  ref.bindTitleUpdater((title) => panelRef.instance.setTitle(title));
1998
- const contentInjector = Injector.create({
1999
- providers: [
2000
- { provide: BASE_SIDE_PANEL_DATA, useValue: contentInputs },
2001
- { provide: BASE_SIDE_PANEL_REF, useValue: ref },
2002
- ],
2003
- parent: injector,
2004
- });
2005
- const contentRef = panelRef.instance.attachContent(opts.content.component, contentInjector, contentInputs);
2333
+ const contentRef = createContent({
2334
+ id: opts.id,
2335
+ title: opts.title,
2336
+ titleIcon: opts.titleIcon,
2337
+ subtitle: opts.subtitle,
2338
+ content: opts.content,
2339
+ }, 'initial', frameRefs[0] ?? ref);
2006
2340
  const unbindSelectionResults = contentRef?.instance
2007
2341
  ? bindSurfaceContentSelectionResults(contentRef.instance, ref)
2008
2342
  : null;
@@ -2011,10 +2345,35 @@ function providePraxisSurfaceDrawerBridge() {
2011
2345
  }
2012
2346
  panelRef.changeDetectorRef.detectChanges();
2013
2347
  },
2014
- createCloser: (ref) => (reason = 'close') => ref.close({ type: reason }),
2015
- onBackdrop: (ref) => ref.close({ type: 'backdrop' }),
2016
- onEsc: (ref) => ref.close({ type: 'esc' }),
2348
+ createCloser: (ref) => (reason = 'close') => {
2349
+ void panel?.canCloseSession().then((allowed) => {
2350
+ if (allowed)
2351
+ ref.close({ type: reason });
2352
+ });
2353
+ },
2354
+ onBackdrop: (ref) => {
2355
+ void panel?.canCloseSession().then((allowed) => {
2356
+ if (allowed)
2357
+ ref.close({ type: 'backdrop' });
2358
+ });
2359
+ },
2360
+ onEsc: (ref) => {
2361
+ void panel?.canCloseSession().then((allowed) => {
2362
+ if (allowed)
2363
+ ref.close({ type: 'esc' });
2364
+ });
2365
+ },
2366
+ });
2367
+ result.closed$.subscribe(() => {
2368
+ while (frameRefs.length > 1) {
2369
+ closeFrameRef(frameRefs.pop(), { type: 'close' });
2370
+ }
2371
+ frameRefs.length = 0;
2372
+ navigationState.complete();
2373
+ panel = null;
2374
+ activeRef = null;
2017
2375
  });
2376
+ return result;
2018
2377
  },
2019
2378
  }),
2020
2379
  deps: [BaseSidePanelService, Injector, PraxisLayerScaleStyleService],
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@praxisui/settings-panel",
3
- "version": "9.0.4-rc.26",
3
+ "version": "9.0.4-rc.27",
4
4
  "description": "Settings panel for Praxis UI libraries: open editors for configuration at runtime and persist settings.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
7
7
  "@angular/core": "^21.0.0",
8
8
  "@angular/cdk": "^21.0.0",
9
9
  "@angular/material": "^21.0.0",
10
- "@praxisui/ai": "^9.0.4-rc.26",
11
- "@praxisui/core": "^9.0.4-rc.26",
12
- "@praxisui/dynamic-fields": "^9.0.4-rc.26",
10
+ "@praxisui/ai": "^9.0.4-rc.27",
11
+ "@praxisui/core": "^9.0.4-rc.27",
12
+ "@praxisui/dynamic-fields": "^9.0.4-rc.27",
13
13
  "rxjs": "~7.8.0"
14
14
  },
15
15
  "dependencies": {
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Type, Injector, ComponentRef, Provider, ChangeDetectorRef, ElementRef, InjectionToken, OnDestroy, OnInit, AfterViewInit, ViewContainerRef } from '@angular/core';
2
+ import { Type, Injector, ComponentRef, Provider, OnDestroy, ChangeDetectorRef, ElementRef, InjectionToken, OnInit, AfterViewInit, ViewContainerRef } from '@angular/core';
3
3
  import * as rxjs from 'rxjs';
4
4
  import { Observable, BehaviorSubject } from 'rxjs';
5
5
  import { OverlayRef, Overlay, OverlayContainer } from '@angular/cdk/overlay';
6
- import { SurfaceOutcome, PraxisI18nService, GlobalConfig, FormConfig, FormValueChangeEvent, AiCapability, AiCapabilityCategory, AiCapabilityCatalog, AiValueKind, ComponentAuthoringManifest, ComponentDocMeta } from '@praxisui/core';
6
+ import { SurfaceOutcome, PraxisI18nService, SurfaceDrawerNavigationFrame, SurfaceDrawerNavigationGuard, GlobalConfig, FormConfig, FormValueChangeEvent, AiCapability, AiCapabilityCategory, AiCapabilityCatalog, AiValueKind, ComponentAuthoringManifest, ComponentDocMeta } from '@praxisui/core';
7
7
  import { MatDialog } from '@angular/material/dialog';
8
8
  import { MatSnackBar } from '@angular/material/snack-bar';
9
9
  import { AiProviderCatalogItem } from '@praxisui/ai';
@@ -353,7 +353,7 @@ declare function providePraxisSettingsPanelBridge(): Provider[];
353
353
 
354
354
  declare function providePraxisSurfaceDrawerBridge(): Provider[];
355
355
 
356
- declare class SurfaceDrawerComponent {
356
+ declare class SurfaceDrawerComponent implements OnDestroy {
357
357
  private readonly cdr;
358
358
  private readonly i18n;
359
359
  title: string;
@@ -363,19 +363,42 @@ declare class SurfaceDrawerComponent {
363
363
  useHostWidth: boolean;
364
364
  theme: SidePanelThemeContract | null;
365
365
  onClose?: () => void;
366
+ onBack?: () => void;
366
367
  private static nextId;
367
368
  titleId: string;
368
369
  subtitleId: string;
369
370
  contentRef?: ComponentRef<any>;
370
371
  private contentHost;
372
+ private body?;
373
+ private backButton?;
374
+ private frames;
371
375
  constructor(cdr: ChangeDetectorRef, i18n: PraxisI18nService);
372
376
  get hostClasses(): string[];
373
377
  get hostCssVars(): Record<string, string>;
374
378
  get closeLabel(): string;
379
+ get backLabel(): string;
380
+ get canGoBack(): boolean;
381
+ get navigationDepth(): number;
382
+ get activeFrameId(): string;
375
383
  get hasHeader(): boolean;
376
384
  get widthClass(): string | null;
377
385
  setTitle(title: string): void;
386
+ ngOnDestroy(): void;
378
387
  attachContent(component: Type<any>, injector: Injector, inputs?: Record<string, unknown>): ComponentRef<any>;
388
+ initializeFrame(definition: SurfaceDrawerNavigationFrame, injector: Injector, inputs?: Record<string, unknown>): ComponentRef<any>;
389
+ pushFrame(definition: SurfaceDrawerNavigationFrame, injector: Injector, inputs?: Record<string, unknown>): ComponentRef<any>;
390
+ replaceFrame(definition: SurfaceDrawerNavigationFrame, injector: Injector, inputs?: Record<string, unknown>): Promise<ComponentRef<any> | null>;
391
+ backFrame(): Promise<boolean>;
392
+ setActiveCanLeave(guard: SurfaceDrawerNavigationGuard | null): void;
393
+ canCloseSession(): Promise<boolean>;
394
+ private createFrame;
395
+ private captureActiveFrameState;
396
+ private detachActiveView;
397
+ private removeActiveFrame;
398
+ private destroyAllFrames;
399
+ private canLeaveActiveFrame;
400
+ private applyFrameHeader;
401
+ private focusNavigationControl;
379
402
  private applyInputs;
380
403
  private resolveSupportedInputs;
381
404
  static ɵfac: i0.ɵɵFactoryDeclaration<SurfaceDrawerComponent, never>;